UI: whisper config reorg
This commit is contained in:
@@ -14,10 +14,7 @@ import { useTranslation } from "react-i18next";
|
||||
import { InlineVisualizationCard } from "./InlineVisualizationCard";
|
||||
import { useProjectStore } from "@/store/projectStore";
|
||||
import { SlashCommandMenu } from "./SlashCommandMenu";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||
|
||||
interface Message {
|
||||
id: string;
|
||||
@@ -315,16 +312,6 @@ export function ChatInterface() {
|
||||
const audioContextRef = useRef<AudioContext | null>(null);
|
||||
const audioAnimationRef = useRef<number | null>(null);
|
||||
|
||||
// Local storage for whisper URL
|
||||
const [whisperUrl, setWhisperUrl] = useState(() => localStorage.getItem("whisper_url") || "http://localhost:8001");
|
||||
const [isVoiceSettingsOpen, setIsVoiceSettingsOpen] = useState(false);
|
||||
|
||||
const handleSaveWhisperUrl = (url: string) => {
|
||||
setWhisperUrl(url);
|
||||
localStorage.setItem("whisper_url", url);
|
||||
setIsVoiceSettingsOpen(false);
|
||||
};
|
||||
|
||||
const stopAudioMeter = () => {
|
||||
if (audioAnimationRef.current) {
|
||||
cancelAnimationFrame(audioAnimationRef.current);
|
||||
@@ -364,6 +351,11 @@ export function ChatInterface() {
|
||||
|
||||
const startRecording = async () => {
|
||||
try {
|
||||
const configuredWhisperUrl = (localStorage.getItem("whisper_url") || "").trim();
|
||||
if (!configuredWhisperUrl) {
|
||||
alert(t('voiceServerNotConfigured', '请先配置语音识别服务地址:点击左下角用户名 -> 语音输入配置'));
|
||||
return;
|
||||
}
|
||||
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||
const mediaRecorder = new MediaRecorder(stream);
|
||||
mediaRecorderRef.current = mediaRecorder;
|
||||
@@ -389,7 +381,7 @@ export function ChatInterface() {
|
||||
const formData = new FormData();
|
||||
formData.append("file", audioBlob, "audio.webm");
|
||||
|
||||
const baseUrl = whisperUrl || "http://localhost:8001";
|
||||
const baseUrl = configuredWhisperUrl;
|
||||
const response = await fetch(`${baseUrl.replace(/\/$/, '')}/transcribe`, {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
@@ -1282,13 +1274,6 @@ export function ChatInterface() {
|
||||
<Mic className="h-5 w-5" />
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setIsVoiceSettingsOpen(true)}
|
||||
className="flex items-center justify-center h-10 w-10 rounded-full bg-transparent text-muted-foreground hover:bg-muted transition-colors"
|
||||
title={t('voiceSettings', '语音输入配置')}
|
||||
>
|
||||
<Settings className="h-4 w-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={handleSend}
|
||||
disabled={isLoading || !input.trim()}
|
||||
@@ -1724,13 +1709,6 @@ export function ChatInterface() {
|
||||
<Mic className="h-5 w-5" />
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setIsVoiceSettingsOpen(true)}
|
||||
className="flex items-center justify-center h-10 w-10 rounded-full bg-transparent text-muted-foreground hover:bg-muted transition-colors"
|
||||
title={t('voiceSettings', '语音输入配置')}
|
||||
>
|
||||
<Settings className="h-4 w-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={isLoading ? handleForceStop : handleSend}
|
||||
disabled={isLoading ? false : !input.trim()}
|
||||
@@ -1796,34 +1774,6 @@ export function ChatInterface() {
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<Dialog open={isVoiceSettingsOpen} onOpenChange={setIsVoiceSettingsOpen}>
|
||||
<DialogContent className="sm:max-w-[425px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t('voiceSettings', '语音输入配置')}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="grid gap-4 py-4">
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="whisperUrl" className="text-right">
|
||||
{t('serviceUrl', '服务地址')}
|
||||
</Label>
|
||||
<Input
|
||||
id="whisperUrl"
|
||||
value={whisperUrl}
|
||||
onChange={(e) => setWhisperUrl(e.target.value)}
|
||||
className="col-span-3"
|
||||
placeholder="http://localhost:8001"
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground px-1">
|
||||
请在此配置独立的 Whisper 语音识别服务地址。例如:http://localhost:8001
|
||||
</p>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setIsVoiceSettingsOpen(false)}>{t('cancel', '取消')}</Button>
|
||||
<Button onClick={() => handleSaveWhisperUrl(whisperUrl)}>{t('save', '保存')}</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { Menu, LayoutDashboard, Plus, MoreVertical, User, Search, Settings, Brain, Trash2, Pencil, Pin, Archive, Database, CheckSquare, Square, ListChecks, RotateCcw, Wand2, Folder, Globe, Bot } from "lucide-react";
|
||||
import { Menu, LayoutDashboard, Plus, MoreVertical, User, Search, Settings, Brain, Trash2, Pencil, Pin, Archive, Database, CheckSquare, Square, ListChecks, RotateCcw, Wand2, Folder, Globe, Bot, Mic, Loader2, CheckCircle2, XCircle } from "lucide-react";
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
import { Link, useNavigate, useLocation } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -376,6 +376,11 @@ function SidebarBody() {
|
||||
const { t, i18n } = useTranslation();
|
||||
const [showUserMenu, setShowUserMenu] = useState(false);
|
||||
const menuRef = useRef<HTMLDivElement>(null);
|
||||
const [voiceSettingsOpen, setVoiceSettingsOpen] = useState(false);
|
||||
const [whisperUrlDraft, setWhisperUrlDraft] = useState("");
|
||||
const [isTestingVoice, setIsTestingVoice] = useState(false);
|
||||
const [voiceTestStatus, setVoiceTestStatus] = useState<"success" | "error" | null>(null);
|
||||
const [voiceTestMessage, setVoiceTestMessage] = useState("");
|
||||
|
||||
// Session management state
|
||||
const [sessions, setSessions] = useState<SessionInfo[]>([]);
|
||||
@@ -445,6 +450,50 @@ function SidebarBody() {
|
||||
navigate("/login");
|
||||
};
|
||||
|
||||
const openVoiceSettings = () => {
|
||||
const saved = (localStorage.getItem("whisper_url") || "").trim();
|
||||
setWhisperUrlDraft(saved);
|
||||
setVoiceTestStatus(null);
|
||||
setVoiceTestMessage("");
|
||||
setVoiceSettingsOpen(true);
|
||||
};
|
||||
|
||||
const handleSaveVoiceSettings = () => {
|
||||
const normalized = whisperUrlDraft.trim();
|
||||
if (!normalized) {
|
||||
alert(t('voiceServerRequired', '请填写语音识别服务地址'));
|
||||
return;
|
||||
}
|
||||
localStorage.setItem("whisper_url", normalized);
|
||||
setVoiceSettingsOpen(false);
|
||||
};
|
||||
|
||||
const handleTestVoiceConnection = async () => {
|
||||
const normalized = whisperUrlDraft.trim();
|
||||
if (!normalized) {
|
||||
alert(t('voiceServerRequired', '请填写语音识别服务地址'));
|
||||
return;
|
||||
}
|
||||
setIsTestingVoice(true);
|
||||
setVoiceTestStatus(null);
|
||||
setVoiceTestMessage("");
|
||||
try {
|
||||
const response = await fetch(`${normalized.replace(/\/$/, "")}/health`, {
|
||||
method: "GET",
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}`);
|
||||
}
|
||||
setVoiceTestStatus("success");
|
||||
setVoiceTestMessage(t('voiceConnectionSuccess', '连接成功'));
|
||||
} catch (error: any) {
|
||||
setVoiceTestStatus("error");
|
||||
setVoiceTestMessage(`${t('voiceConnectionFailed', '连接失败')}: ${error?.message || t('unknownError', '未知错误')}`);
|
||||
} finally {
|
||||
setIsTestingVoice(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelectSession = (key: string) => {
|
||||
navigate(`/?session=${encodeURIComponent(key)}`);
|
||||
};
|
||||
@@ -771,6 +820,37 @@ function SidebarBody() {
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Dialog open={voiceSettingsOpen} onOpenChange={setVoiceSettingsOpen}>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t('voiceSettings', '语音输入配置')}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="py-4 space-y-3">
|
||||
<Input
|
||||
value={whisperUrlDraft}
|
||||
onChange={(e) => setWhisperUrlDraft(e.target.value)}
|
||||
placeholder="http://localhost:8001"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t('voiceSettingsHint', '请输入语音识别服务地址,例如:http://localhost:8001')}
|
||||
</p>
|
||||
{voiceTestStatus && (
|
||||
<div className={`flex items-center gap-2 text-xs ${voiceTestStatus === "success" ? "text-emerald-600" : "text-red-600"}`}>
|
||||
{voiceTestStatus === "success" ? <CheckCircle2 className="h-3.5 w-3.5" /> : <XCircle className="h-3.5 w-3.5" />}
|
||||
<span>{voiceTestMessage}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setVoiceSettingsOpen(false)}>{t('cancel')}</Button>
|
||||
<Button variant="outline" onClick={handleTestVoiceConnection} disabled={isTestingVoice}>
|
||||
{isTestingVoice ? <Loader2 className="h-4 w-4 animate-spin" /> : t('testConnection', '测试连接')}
|
||||
</Button>
|
||||
<Button className="bg-indigo-600 hover:bg-indigo-700 text-primary-foreground" onClick={handleSaveVoiceSettings}>{t('save')}</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<div className="p-4 border-t border-border mt-auto relative" ref={menuRef}>
|
||||
<div className="flex items-center justify-between text-muted-foreground">
|
||||
<button
|
||||
@@ -850,6 +930,17 @@ function SidebarBody() {
|
||||
{t('personalSettings')}
|
||||
</button>
|
||||
|
||||
<button
|
||||
className="w-full flex items-center gap-2 px-3 py-2 text-sm text-foreground/80 hover:bg-muted transition-colors"
|
||||
onClick={() => {
|
||||
openVoiceSettings();
|
||||
setShowUserMenu(false);
|
||||
}}
|
||||
>
|
||||
<Mic className="h-4 w-4 text-muted-foreground" />
|
||||
{t('voiceSettings', '语音输入配置')}
|
||||
</button>
|
||||
|
||||
{user?.is_admin && (
|
||||
<>
|
||||
<button
|
||||
|
||||
@@ -41,6 +41,10 @@ print("Loading Whisper model (small)... This may take a moment.")
|
||||
model = whisper.load_model("small")
|
||||
print("Model loaded successfully.")
|
||||
|
||||
@app.get("/health")
|
||||
async def health():
|
||||
return {"status": "ok"}
|
||||
|
||||
@app.post("/transcribe")
|
||||
async def transcribe_audio(file: UploadFile = File(...)):
|
||||
# Save the uploaded file to a temporary file
|
||||
|
||||
Reference in New Issue
Block a user