UI: whisper config reorg

This commit is contained in:
qixinbo
2026-03-28 20:25:13 +08:00
parent c983829392
commit b0a8a69373
3 changed files with 103 additions and 58 deletions
+7 -57
View File
@@ -14,10 +14,7 @@ import { useTranslation } from "react-i18next";
import { InlineVisualizationCard } from "./InlineVisualizationCard"; import { InlineVisualizationCard } from "./InlineVisualizationCard";
import { useProjectStore } from "@/store/projectStore"; import { useProjectStore } from "@/store/projectStore";
import { SlashCommandMenu } from "./SlashCommandMenu"; import { SlashCommandMenu } from "./SlashCommandMenu";
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog"; import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
interface Message { interface Message {
id: string; id: string;
@@ -315,16 +312,6 @@ export function ChatInterface() {
const audioContextRef = useRef<AudioContext | null>(null); const audioContextRef = useRef<AudioContext | null>(null);
const audioAnimationRef = useRef<number | 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 = () => { const stopAudioMeter = () => {
if (audioAnimationRef.current) { if (audioAnimationRef.current) {
cancelAnimationFrame(audioAnimationRef.current); cancelAnimationFrame(audioAnimationRef.current);
@@ -364,6 +351,11 @@ export function ChatInterface() {
const startRecording = async () => { const startRecording = async () => {
try { try {
const configuredWhisperUrl = (localStorage.getItem("whisper_url") || "").trim();
if (!configuredWhisperUrl) {
alert(t('voiceServerNotConfigured', '请先配置语音识别服务地址:点击左下角用户名 -> 语音输入配置'));
return;
}
const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
const mediaRecorder = new MediaRecorder(stream); const mediaRecorder = new MediaRecorder(stream);
mediaRecorderRef.current = mediaRecorder; mediaRecorderRef.current = mediaRecorder;
@@ -389,7 +381,7 @@ export function ChatInterface() {
const formData = new FormData(); const formData = new FormData();
formData.append("file", audioBlob, "audio.webm"); formData.append("file", audioBlob, "audio.webm");
const baseUrl = whisperUrl || "http://localhost:8001"; const baseUrl = configuredWhisperUrl;
const response = await fetch(`${baseUrl.replace(/\/$/, '')}/transcribe`, { const response = await fetch(`${baseUrl.replace(/\/$/, '')}/transcribe`, {
method: "POST", method: "POST",
body: formData, body: formData,
@@ -1282,13 +1274,6 @@ export function ChatInterface() {
<Mic className="h-5 w-5" /> <Mic className="h-5 w-5" />
)} )}
</button> </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 <button
onClick={handleSend} onClick={handleSend}
disabled={isLoading || !input.trim()} disabled={isLoading || !input.trim()}
@@ -1724,13 +1709,6 @@ export function ChatInterface() {
<Mic className="h-5 w-5" /> <Mic className="h-5 w-5" />
)} )}
</button> </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 <button
onClick={isLoading ? handleForceStop : handleSend} onClick={isLoading ? handleForceStop : handleSend}
disabled={isLoading ? false : !input.trim()} disabled={isLoading ? false : !input.trim()}
@@ -1796,34 +1774,6 @@ export function ChatInterface() {
</div> </div>
</DialogContent> </DialogContent>
</Dialog> </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> </div>
); );
} }
+92 -1
View File
@@ -1,7 +1,7 @@
import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet"; import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { ScrollArea } from "@/components/ui/scroll-area"; 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 { useState, useRef, useEffect } from "react";
import { Link, useNavigate, useLocation } from "react-router-dom"; import { Link, useNavigate, useLocation } from "react-router-dom";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@@ -376,6 +376,11 @@ function SidebarBody() {
const { t, i18n } = useTranslation(); const { t, i18n } = useTranslation();
const [showUserMenu, setShowUserMenu] = useState(false); const [showUserMenu, setShowUserMenu] = useState(false);
const menuRef = useRef<HTMLDivElement>(null); 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 // Session management state
const [sessions, setSessions] = useState<SessionInfo[]>([]); const [sessions, setSessions] = useState<SessionInfo[]>([]);
@@ -445,6 +450,50 @@ function SidebarBody() {
navigate("/login"); 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) => { const handleSelectSession = (key: string) => {
navigate(`/?session=${encodeURIComponent(key)}`); navigate(`/?session=${encodeURIComponent(key)}`);
}; };
@@ -771,6 +820,37 @@ function SidebarBody() {
</DialogContent> </DialogContent>
</Dialog> </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="p-4 border-t border-border mt-auto relative" ref={menuRef}>
<div className="flex items-center justify-between text-muted-foreground"> <div className="flex items-center justify-between text-muted-foreground">
<button <button
@@ -850,6 +930,17 @@ function SidebarBody() {
{t('personalSettings')} {t('personalSettings')}
</button> </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 && ( {user?.is_admin && (
<> <>
<button <button
+4
View File
@@ -41,6 +41,10 @@ print("Loading Whisper model (small)... This may take a moment.")
model = whisper.load_model("small") model = whisper.load_model("small")
print("Model loaded successfully.") print("Model loaded successfully.")
@app.get("/health")
async def health():
return {"status": "ok"}
@app.post("/transcribe") @app.post("/transcribe")
async def transcribe_audio(file: UploadFile = File(...)): async def transcribe_audio(file: UploadFile = File(...)):
# Save the uploaded file to a temporary file # Save the uploaded file to a temporary file