From b0a8a69373141964dca142dd58131f70c9c5ad2b Mon Sep 17 00:00:00 2001 From: qixinbo Date: Sat, 28 Mar 2026 20:25:13 +0800 Subject: [PATCH] UI: whisper config reorg --- frontend/src/components/ChatInterface.tsx | 64 ++-------------- frontend/src/components/Sidebar.tsx | 93 ++++++++++++++++++++++- whisper/main.py | 4 + 3 files changed, 103 insertions(+), 58 deletions(-) diff --git a/frontend/src/components/ChatInterface.tsx b/frontend/src/components/ChatInterface.tsx index 951e60a..91a451f 100644 --- a/frontend/src/components/ChatInterface.tsx +++ b/frontend/src/components/ChatInterface.tsx @@ -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(null); const audioAnimationRef = useRef(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() { )} - - - - - - ); } diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx index 01fe11f..f8a8c2d 100644 --- a/frontend/src/components/Sidebar.tsx +++ b/frontend/src/components/Sidebar.tsx @@ -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(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([]); @@ -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() { + + + + {t('voiceSettings', '语音输入配置')} + +
+ setWhisperUrlDraft(e.target.value)} + placeholder="http://localhost:8001" + /> +

+ {t('voiceSettingsHint', '请输入语音识别服务地址,例如:http://localhost:8001')} +

+ {voiceTestStatus && ( +
+ {voiceTestStatus === "success" ? : } + {voiceTestMessage} +
+ )} +
+ + + + + +
+
+
+ {user?.is_admin && ( <>