UI: whisper config reorg
This commit is contained in:
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user