feat: Introduce DashScope agent configuration, a WAV client for duplex testing, and new UI components for assistants.
This commit is contained in:
@@ -206,10 +206,11 @@ interface DrawerProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
title: string;
|
||||
className?: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const Drawer: React.FC<DrawerProps> = ({ isOpen, onClose, title, children }) => {
|
||||
export const Drawer: React.FC<DrawerProps> = ({ isOpen, onClose, title, className, children }) => {
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
@@ -218,7 +219,7 @@ export const Drawer: React.FC<DrawerProps> = ({ isOpen, onClose, title, children
|
||||
<div className="fixed inset-0 bg-black/60 backdrop-blur-sm transition-opacity" onClick={onClose} />
|
||||
|
||||
{/* Drawer Content */}
|
||||
<div className="relative ml-auto flex h-full w-full max-w-md flex-col overflow-y-auto bg-background/95 border-l border-white/10 p-6 shadow-2xl animate-in slide-in-from-right sm:max-w-lg">
|
||||
<div className={`relative ml-auto flex h-full w-full flex-col overflow-y-auto bg-background/95 border-l border-white/10 p-6 shadow-2xl animate-in slide-in-from-right ${className || 'max-w-md sm:max-w-lg'}`}>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-lg font-semibold text-foreground">{title}</h2>
|
||||
<Button variant="ghost" size="icon" onClick={onClose}>
|
||||
|
||||
@@ -1132,12 +1132,12 @@ export const AssistantsPage: React.FC = () => {
|
||||
onChange={(e) => updateManualOpenerToolCall(idx, { toolName: e.target.value })}
|
||||
>
|
||||
<option value="">选择已启用工具</option>
|
||||
{normalizeToolId(call.toolName || '') && !openerToolOptions.some((tool) => tool.id === normalizeToolId(call.toolName || '')) && (
|
||||
{normalizeToolId(call.toolName || '') && !openerToolOptions.some((tool: any) => tool.id === normalizeToolId(call.toolName || '')) && (
|
||||
<option value={normalizeToolId(call.toolName || '')}>
|
||||
{`${normalizeToolId(call.toolName || '')} (未启用/不存在)`}
|
||||
</option>
|
||||
)}
|
||||
{openerToolOptions.map((tool) => (
|
||||
{openerToolOptions.map((tool: any) => (
|
||||
<option key={tool.id} value={tool.id}>
|
||||
{tool.label}
|
||||
</option>
|
||||
@@ -2489,7 +2489,7 @@ export const DebugDrawer: React.FC<{
|
||||
const selectedToolSchemas = useMemo(() => {
|
||||
const ids = Array.from(new Set((assistant.tools || []).map((id) => normalizeToolId(id))));
|
||||
const byId = new Map(tools.map((t) => [normalizeToolId(t.id), { ...t, id: normalizeToolId(t.id) }]));
|
||||
return ids.map((id) => {
|
||||
return ids.map((id: any) => {
|
||||
const item = byId.get(id);
|
||||
const toolId = item?.id || id;
|
||||
const debugClientTool = DEBUG_CLIENT_TOOLS.find((tool) => tool.id === toolId);
|
||||
@@ -4371,8 +4371,11 @@ export const DebugDrawer: React.FC<{
|
||||
|
||||
return (
|
||||
<>
|
||||
<Drawer isOpen={isOpen} onClose={() => { handleHangup(); onClose(); }} title={`调试: ${assistant.name}`}>
|
||||
<div className="relative flex flex-col h-full min-h-0 overflow-hidden">
|
||||
<Drawer isOpen={isOpen} onClose={() => { handleHangup(); onClose(); }} title={`调试: ${assistant.name}`} className="w-[90vw] sm:w-[85vw] max-w-none">
|
||||
<div className="relative flex h-full min-h-0 overflow-hidden gap-6">
|
||||
|
||||
{/* Left Column: Call Interface */}
|
||||
<div className="flex-1 flex flex-col min-h-0 overflow-hidden">
|
||||
<div className="flex items-center gap-2 mb-4 shrink-0">
|
||||
<div className="flex-1 flex justify-center bg-white/5 p-1 rounded-lg">
|
||||
{(['text', 'voice', 'video'] as const).map(m => (
|
||||
@@ -4384,15 +4387,31 @@ export const DebugDrawer: React.FC<{
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="h-[36px] w-[36px] shrink-0 bg-white/5 border-white/10 hover:bg-white/10 hover:text-primary transition-colors"
|
||||
onClick={() => setSettingsDrawerOpen(true)}
|
||||
title="调试设置"
|
||||
>
|
||||
<Wrench className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-h-0 overflow-hidden flex flex-col">
|
||||
<div className={`flex-1 overflow-hidden flex flex-col min-h-0 ${mode === 'text' && textSessionStarted ? 'mb-0' : 'mb-4'}`}>
|
||||
<div className={`flex-1 overflow-hidden flex flex-col min-h-0 mb-4`}>
|
||||
{mode === 'text' ? (
|
||||
textSessionStarted ? (
|
||||
<div className="flex-1 min-h-0 overflow-hidden animate-in fade-in flex flex-col">
|
||||
<div className="h-[68vh] min-h-[420px] max-h-[68vh] w-full flex flex-col min-h-0 overflow-hidden">
|
||||
<TranscriptionLog scrollRef={scrollRef} messages={messages} isLoading={isLoading} className="flex-1 min-h-0 h-full" />
|
||||
<div className="flex-1 flex flex-col items-center justify-center space-y-6 border border-white/5 rounded-xl bg-black/20 animate-in fade-in zoom-in-95 px-6">
|
||||
<div className="relative">
|
||||
<div className="absolute inset-0 bg-primary/20 rounded-full blur-2xl animate-pulse"></div>
|
||||
<div className="relative h-24 w-24 rounded-full bg-white/5 border border-white/10 flex items-center justify-center">
|
||||
<MessageSquare className="h-10 w-10 text-primary" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<h3 className="text-lg font-bold text-white mb-1">通话中</h3>
|
||||
<p className="text-xs text-muted-foreground">文本交互测试进行中</p>
|
||||
</div>
|
||||
</div>
|
||||
) : wsStatus === 'connecting' ? (
|
||||
@@ -4468,41 +4487,57 @@ export const DebugDrawer: React.FC<{
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<div className="flex-1 flex flex-col min-h-0 space-y-2">
|
||||
<div className="flex-1 flex flex-col min-h-0 space-y-4">
|
||||
{mode === 'voice' ? (
|
||||
<div className="h-[68vh] min-h-[420px] max-h-[68vh] w-full flex flex-col min-h-0 overflow-hidden animate-in fade-in">
|
||||
<TranscriptionLog scrollRef={scrollRef} messages={messages} isLoading={isLoading} className="flex-1 min-h-0 h-full" />
|
||||
<div className="flex-1 flex flex-col items-center justify-center space-y-6 border border-white/5 rounded-xl bg-black/20 animate-in fade-in px-6 relative overflow-hidden">
|
||||
<div className="absolute inset-0 bg-primary/5 rounded-full blur-3xl animate-pulse"></div>
|
||||
<div className="relative z-10">
|
||||
<div className="relative">
|
||||
<div className="absolute inset-0 bg-primary/20 rounded-full blur-2xl animate-pulse"></div>
|
||||
<div className="relative h-32 w-32 rounded-full bg-card border border-white/10 flex items-center justify-center shadow-2xl">
|
||||
<Mic className="h-12 w-12 text-primary" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-center z-10">
|
||||
<h3 className="text-xl font-bold text-white mb-2 tracking-tight">通话进行中</h3>
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<span className="relative flex h-2.5 w-2.5">
|
||||
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75"></span>
|
||||
<span className="relative inline-flex rounded-full h-2.5 w-2.5 bg-green-500"></span>
|
||||
</span>
|
||||
<p className="text-sm font-medium text-green-400">已连接</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col h-full min-h-0 space-y-2 animate-in fade-in">
|
||||
<div className="h-3/5 shrink-0 flex flex-col gap-2">
|
||||
<div className="flex flex-col h-full min-h-0 space-y-3 animate-in fade-in">
|
||||
<div className="flex gap-2 shrink-0">
|
||||
<select className="flex-1 text-xs rounded-md border border-white/10 bg-white/5 px-2 py-1 text-foreground appearance-none cursor-pointer focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary/50 [&>option]:bg-card [&>option]:text-foreground" value={selectedCamera} onChange={e => setSelectedCamera(e.target.value)}>
|
||||
<select className="flex-1 text-xs rounded-md border border-white/10 bg-white/5 px-3 py-2 text-foreground appearance-none cursor-pointer focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary/50 transition-colors hover:bg-white/10 [&>option]:bg-card [&>option]:text-foreground" value={selectedCamera} onChange={e => setSelectedCamera(e.target.value)}>
|
||||
{devices.filter(d => d.kind === 'videoinput').map(d => <option key={d.deviceId} value={d.deviceId}>{d.label || 'Camera'}</option>)}
|
||||
</select>
|
||||
<select className="flex-1 text-xs rounded-md border border-white/10 bg-white/5 px-2 py-1 text-foreground appearance-none cursor-pointer focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary/50 [&>option]:bg-card [&>option]:text-foreground" value={selectedMic} onChange={e => setSelectedMic(e.target.value)}>
|
||||
<select className="flex-1 text-xs rounded-md border border-white/10 bg-white/5 px-3 py-2 text-foreground appearance-none cursor-pointer focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary/50 transition-colors hover:bg-white/10 [&>option]:bg-card [&>option]:text-foreground" value={selectedMic} onChange={e => setSelectedMic(e.target.value)}>
|
||||
{devices.filter(d => d.kind === 'audioinput').map(d => <option key={d.deviceId} value={d.deviceId}>{d.label || 'Mic'}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
<div className="flex-1 relative rounded-lg overflow-hidden border border-white/10 bg-black min-h-0">
|
||||
<div className="absolute inset-0">{isSwapped ? renderLocalVideo(false) : renderRemoteVideo(false)}</div>
|
||||
<div className="absolute bottom-2 right-2 w-24 h-36 z-10">{isSwapped ? renderRemoteVideo(true) : renderLocalVideo(true)}</div>
|
||||
<button className="absolute top-2 right-2 z-20 h-8 w-8 rounded-full bg-black/50 backdrop-blur flex items-center justify-center text-white border border-white/10 hover:bg-primary/80" onClick={() => setIsSwapped(!isSwapped)}><ArrowLeftRight className="h-3.5 w-3.5" /></button>
|
||||
<div className="flex-1 flex items-center justify-center relative rounded-xl overflow-hidden border border-white/10 bg-black min-h-0 shadow-inner">
|
||||
<div className="absolute inset-0 object-cover">{isSwapped ? renderLocalVideo(false) : renderRemoteVideo(false)}</div>
|
||||
<div className="absolute bottom-4 right-4 w-32 h-44 z-10 rounded-lg overflow-hidden border border-white/20 shadow-2xl transition-transform hover:scale-[1.02]">{isSwapped ? renderRemoteVideo(true) : renderLocalVideo(true)}</div>
|
||||
<button className="absolute top-4 right-4 z-20 h-10 w-10 rounded-full bg-black/50 backdrop-blur-md flex items-center justify-center text-white border border-white/10 hover:bg-primary/80 transition-all hover:scale-110 shadow-lg" onClick={() => setIsSwapped(!isSwapped)} title="切换视图"><ArrowLeftRight className="h-4 w-4" /></button>
|
||||
</div>
|
||||
</div>
|
||||
<TranscriptionLog scrollRef={scrollRef} messages={messages} isLoading={isLoading} className="flex-1 min-h-0 overflow-y-auto" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={mode === 'text' && textSessionStarted ? 'shrink-0 mt-3 px-1 mb-3' : mode === 'voice' ? 'shrink-0 space-y-3 mt-3 px-1 mb-3' : 'shrink-0 space-y-2 mt-2 px-1 mb-3'}>
|
||||
{mode === 'voice' && (
|
||||
<div className="w-full flex items-center gap-2 pb-1">
|
||||
<span className="text-xs text-muted-foreground shrink-0">麦克风</span>
|
||||
{/* Hangup / Mic Select area (Left Column Bottom) */}
|
||||
<div className={'shrink-0 space-y-3 mt-auto px-1 mb-1'}>
|
||||
{mode === 'voice' && callStatus === 'active' && (
|
||||
<div className="w-full flex items-center gap-2 pb-2">
|
||||
<span className="text-xs font-medium text-muted-foreground shrink-0 uppercase tracking-wider">麦克风</span>
|
||||
<select
|
||||
className="flex-1 text-xs rounded-md border border-white/10 bg-white/5 px-2 py-1 text-foreground appearance-none cursor-pointer focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary/50 [&>option]:bg-card [&>option]:text-foreground"
|
||||
className="flex-1 text-xs rounded-md border border-white/10 bg-white/5 px-3 py-1.5 text-foreground appearance-none cursor-pointer focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary/50 transition-colors hover:bg-white/10 [&>option]:bg-card [&>option]:text-foreground"
|
||||
value={selectedMic}
|
||||
onChange={(e) => setSelectedMic(e.target.value)}
|
||||
>
|
||||
@@ -4512,44 +4547,71 @@ export const DebugDrawer: React.FC<{
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
<div className="w-full flex items-center gap-2 min-w-0">
|
||||
<div className="w-full flex justify-center items-center">
|
||||
{mode === 'text' && textSessionStarted && (
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
className="h-9 px-3 font-bold shrink-0 whitespace-nowrap"
|
||||
size="lg"
|
||||
className="w-full font-bold shadow-lg shadow-destructive/20 hover:shadow-destructive/40 transition-all"
|
||||
onClick={closeWs}
|
||||
>
|
||||
<PhoneOff className="h-4 w-4" />
|
||||
<span className="ml-1.5">结束测试</span>
|
||||
<PhoneOff className="h-5 w-5 mr-2" />
|
||||
结束测试
|
||||
</Button>
|
||||
)}
|
||||
{mode === 'voice' && callStatus === 'active' && (
|
||||
{mode !== 'text' && callStatus === 'active' && (
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
className="h-9 px-3 font-bold shrink-0 whitespace-nowrap"
|
||||
size="lg"
|
||||
className="w-full font-bold shadow-lg shadow-destructive/20 hover:shadow-destructive/40 transition-all"
|
||||
onClick={handleHangup}
|
||||
>
|
||||
<PhoneOff className="h-4 w-4" />
|
||||
<span className="ml-1.5">结束通话</span>
|
||||
<PhoneOff className="h-5 w-5 mr-2" />
|
||||
结束通话
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Column: Transcript */}
|
||||
<div className="w-[380px] lg:w-[450px] shrink-0 border-l border-white/10 pl-6 flex flex-col min-h-0 h-full">
|
||||
<div className="flex items-center gap-2 mb-4 shrink-0 px-1">
|
||||
<MessageSquare className="h-4 w-4 text-primary" />
|
||||
<h3 className="font-semibold text-foreground tracking-tight">Transcript</h3>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-h-0 overflow-hidden flex flex-col bg-card/30 rounded-xl border border-white/5 p-2 mb-4">
|
||||
<div className="flex-1 min-h-0 overflow-y-auto pr-1 custom-scrollbar">
|
||||
{(messages.length === 0 && !isLoading) ? (
|
||||
<div className="h-full flex flex-col items-center justify-center text-muted-foreground/60 space-y-3">
|
||||
<MessageSquare className="h-8 w-8 opacity-20" />
|
||||
<p className="text-xs">暂无对话记录</p>
|
||||
</div>
|
||||
) : (
|
||||
<TranscriptionLog scrollRef={scrollRef} messages={messages} isLoading={isLoading} className="pb-4" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Input bar */}
|
||||
<div className="shrink-0 flex items-center gap-2 mt-auto p-1 bg-card/50 rounded-lg border border-white/10 shadow-sm focus-within:border-primary/40 focus-within:ring-1 focus-within:ring-primary/20 transition-all">
|
||||
<Input
|
||||
value={inputText}
|
||||
onChange={e => setInputText(e.target.value)}
|
||||
placeholder={mode === 'text' && !textSessionStarted ? "请先发起呼叫后输入消息..." : (mode === 'text' ? "输入消息..." : "输入文本模拟交互...")}
|
||||
onKeyDown={e => e.key === 'Enter' && handleSend()}
|
||||
disabled={mode === 'text' ? !textSessionStarted : (isLoading || callStatus !== 'active')}
|
||||
className="flex-1 min-w-0"
|
||||
className="flex-1 min-w-0 border-0 bg-transparent focus-visible:ring-0 shadow-none px-3"
|
||||
/>
|
||||
<Button
|
||||
size="icon"
|
||||
className="h-9 w-9 shrink-0"
|
||||
className="h-8 w-8 shrink-0 rounded-md bg-primary/90 hover:bg-primary shadow-sm mr-1"
|
||||
onClick={handleSend}
|
||||
disabled={mode === 'text' ? !textSessionStarted : (isLoading || callStatus !== 'active')}
|
||||
>
|
||||
<Send className="h-4 w-4" />
|
||||
<Send className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -4604,7 +4666,7 @@ export const DebugDrawer: React.FC<{
|
||||
)}
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{choicePromptDialog.options.map((option) => (
|
||||
{choicePromptDialog.options.map((option: any) => (
|
||||
<Button
|
||||
key={option.id}
|
||||
variant="outline"
|
||||
@@ -4625,27 +4687,18 @@ export const DebugDrawer: React.FC<{
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Drawer>
|
||||
{isOpen && (
|
||||
<div className="fixed inset-y-0 z-[51] right-[min(100vw,32rem)]">
|
||||
<button
|
||||
className={`absolute inset-y-0 w-10 border-r border-white/10 bg-background/90 backdrop-blur-md text-muted-foreground hover:text-foreground hover:bg-background/95 transition-[right,color,background-color] duration-300 flex flex-col items-center justify-center gap-2 ${settingsDrawerOpen ? 'right-[min(78vw,28rem)]' : 'right-0'}`}
|
||||
onClick={() => setSettingsDrawerOpen((v) => !v)}
|
||||
title={settingsDrawerOpen ? '收起调试设置' : '展开调试设置'}
|
||||
<Dialog
|
||||
isOpen={settingsDrawerOpen}
|
||||
onClose={() => setSettingsDrawerOpen(false)}
|
||||
title="调试设置"
|
||||
contentClassName="max-w-[90vw] md:max-w-[70vw] lg:max-w-2xl h-[85vh] flex flex-col"
|
||||
>
|
||||
<span className={`text-[10px] font-mono tracking-tight ${settingsDrawerOpen ? 'text-primary animate-pulse' : 'opacity-90 animate-pulse'}`}>
|
||||
{settingsDrawerOpen ? '>>' : '<<'}
|
||||
</span>
|
||||
<Wrench className="h-4 w-4" />
|
||||
<span className="text-[10px] tracking-widest [writing-mode:vertical-rl]">调试设置</span>
|
||||
</button>
|
||||
<div className="absolute inset-y-0 right-0 w-[78vw] max-w-md overflow-hidden pointer-events-none">
|
||||
<div className={`h-full transition-transform duration-300 ease-out will-change-transform ${settingsDrawerOpen ? 'translate-x-0 pointer-events-auto' : 'translate-x-full pointer-events-none'}`}>
|
||||
<div className="flex-1 min-h-0 overflow-y-auto">
|
||||
{settingsPanel}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -28,7 +28,7 @@ const normalizeToolIdList = (value: unknown): string[] => {
|
||||
return result;
|
||||
};
|
||||
|
||||
const normalizeManualOpenerToolCalls = (value: unknown): AnyRecord[] => {
|
||||
const normalizeManualOpenerToolCalls = (value: unknown): any[] => {
|
||||
if (!Array.isArray(value)) return [];
|
||||
return value
|
||||
.filter((item) => item && typeof item === 'object')
|
||||
@@ -41,7 +41,7 @@ const normalizeManualOpenerToolCalls = (value: unknown): AnyRecord[] => {
|
||||
toolName,
|
||||
};
|
||||
})
|
||||
.filter(Boolean) as AnyRecord[];
|
||||
.filter(Boolean) as any[];
|
||||
};
|
||||
|
||||
const withLimit = (path: string, limit: number = DEFAULT_LIST_LIMIT): string =>
|
||||
|
||||
Reference in New Issue
Block a user