feat: add Makelore Robot hardware module

This commit is contained in:
2026-08-13 23:17:22 +08:00
parent 22add3f01f
commit aba5cae286
25 changed files with 3169 additions and 72 deletions

View File

@@ -0,0 +1,391 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { Bot, Link2, Loader2, Plus, RefreshCw, Settings2 } from 'lucide-react';
import { FeedbackState } from '@/components/common/FeedbackState';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import {
Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle,
} from '@/components/ui/dialog';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea';
import {
AiHardwareApiError,
bindAiHardwareDevice,
createAiHardwareAgent,
getAiHardwareAgentConfiguration,
getAiHardwareAssignment,
getAiHardwareOverview,
recoverAiHardwareCredential,
updateAiHardwareAgentConfiguration,
updateAiHardwareAssignment,
type AiHardwareAgent,
type AiHardwareAgentConfiguration,
type AiHardwareAgentConfigurationUpdate,
type AiHardwareClearableField,
type AiHardwareDevice,
type AiHardwareOverview,
} from '@/lib/ai-hardware';
type PageState = 'loading' | 'ready' | 'disabled' | 'auth' | 'error';
type BindRetryIntent = { agentId: string; fingerprint: ArrayBuffer; operationId: string };
type ConfigDraft = {
agent_name: string; system_prompt: string; language: string; lang_code: string;
asr_model_id: string; vad_model_id: string; llm_model_id: string; slm_model_id: string;
vllm_model_id: string; tts_model_id: string;
tts_voice_id: string; tts_volume: string; tts_rate: string; tts_pitch: string;
tts_language: string; mem_model_id: string; intent_model_id: string;
chat_history_conf: string;
};
const nullableTextFields = [
'system_prompt', 'language', 'lang_code', 'asr_model_id', 'vad_model_id', 'llm_model_id',
'slm_model_id', 'vllm_model_id', 'tts_model_id', 'tts_voice_id', 'tts_language',
'mem_model_id', 'intent_model_id',
] as const;
const nullableNumberFields = ['tts_volume', 'tts_rate', 'tts_pitch'] as const;
const shortId = (value: string) => value.length > 12 ? `${value.slice(0, 6)}${value.slice(-4)}` : value;
const isRevisionConflict = (error: unknown) => error instanceof AiHardwareApiError
&& ['ai_hardware_revision_conflict', 'REVISION_MISMATCH', 'AI_HARDWARE_REVISION_CONFLICT'].includes(error.code);
function safeMessage(error: unknown): string {
if (!(error instanceof AiHardwareApiError)) return '操作没有完成,请稍后重试。';
if (isRevisionConflict(error)) return '内容已在其他位置更新,请重新核对后再保存。';
const messages: Record<string, string> = {
AI_HARDWARE_AUTH_REQUIRED: '请先登录 Works Square然后重试。',
AI_HARDWARE_FORBIDDEN: '当前账号无权执行此操作。',
AI_HARDWARE_DISABLED: 'AI 机器模块尚未启用,请联系管理员。',
ai_hardware_unconfigured: 'AI 机器服务尚未配置,请联系管理员。',
ai_hardware_credential_recovery_required: '需要管理员恢复设备凭据后才能继续。',
ai_hardware_credential_unavailable: '设备凭据暂不可用,请联系管理员恢复。',
ai_hardware_credential_recovery_unavailable: '当前状态无需或不能恢复凭据,请刷新查看最新状态。',
ai_hardware_activation_code_invalid: '激活码无效或已过期,请从设备上获取新激活码。',
ai_hardware_device_already_bound: '设备已被绑定,无法重复绑定。',
ai_hardware_idempotency_conflict: '本次输入与待重试操作不一致,请再次提交以启动新操作。',
AI_HARDWARE_RATE_LIMITED: '请求过于频繁,请稍后重试。',
};
if (error.code === 'ai_hardware_operation_in_progress') {
return error.retryAfterSeconds === null
? '操作仍在处理中,请稍后重试。'
: `操作仍在处理中,请在 ${error.retryAfterSeconds} 秒后重试。`;
}
return messages[error.code] ?? (error.retryable ? '服务暂时不可用,请稍后重试。' : '操作没有完成,请稍后重试。');
}
const retryOperationId = (error: unknown): string | null => error instanceof AiHardwareApiError
&& (error.retryable || error.code === 'ai_hardware_operation_in_progress') ? error.operationId : null;
function draftFrom(config: AiHardwareAgentConfiguration): ConfigDraft {
return {
agent_name: config.name,
system_prompt: config.system_prompt ?? '', language: config.language ?? '', lang_code: config.lang_code ?? '',
asr_model_id: config.asr_model_id ?? '', vad_model_id: config.vad_model_id ?? '',
llm_model_id: config.llm_model_id ?? '', slm_model_id: config.slm_model_id ?? '',
vllm_model_id: config.vllm_model_id ?? '', tts_model_id: config.tts_model_id ?? '',
tts_voice_id: config.tts_voice_id ?? '', tts_volume: config.tts_volume?.toString() ?? '',
tts_rate: config.tts_rate?.toString() ?? '', tts_pitch: config.tts_pitch?.toString() ?? '',
tts_language: config.tts_language ?? '', mem_model_id: config.mem_model_id ?? '',
intent_model_id: config.intent_model_id ?? '',
chat_history_conf: config.chat_history_conf?.toString() ?? '',
};
}
function configurationUpdate(config: AiHardwareAgentConfiguration, draft: ConfigDraft): AiHardwareAgentConfigurationUpdate {
const update: AiHardwareAgentConfigurationUpdate = {};
const clearFields: AiHardwareClearableField[] = [];
if (draft.agent_name.trim() !== config.name) update.agent_name = draft.agent_name.trim();
for (const field of nullableTextFields) {
const value = draft[field].trim();
if (!value && config[field] !== null) clearFields.push(field);
else if (value && value !== config[field]) update[field] = value;
}
for (const field of nullableNumberFields) {
const raw = draft[field].trim();
if (!raw && config[field] !== null) clearFields.push(field);
else if (raw && Number(raw) !== config[field]) update[field] = Number(raw);
}
if (draft.chat_history_conf !== '') {
const history = Number(draft.chat_history_conf);
if (history !== config.chat_history_conf) update.chat_history_conf = history;
}
if (clearFields.length) update.clear_fields = clearFields;
return update;
}
function replayConfigurationUpdate(
config: AiHardwareAgentConfiguration,
update: AiHardwareAgentConfigurationUpdate,
): ConfigDraft {
const next = draftFrom(config);
if (update.agent_name !== undefined) next.agent_name = update.agent_name;
for (const field of nullableTextFields) {
if (update[field] !== undefined) next[field] = update[field];
if (update.clear_fields?.includes(field)) next[field] = '';
}
for (const field of nullableNumberFields) {
if (update[field] !== undefined) next[field] = update[field].toString();
if (update.clear_fields?.includes(field)) next[field] = '';
}
if (update.chat_history_conf !== undefined) next.chat_history_conf = update.chat_history_conf.toString();
return next;
}
async function createBindFingerprintKey(): Promise<CryptoKey> {
return crypto.subtle.generateKey({ name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
}
async function bindFingerprint(key: CryptoKey, activationCode: string): Promise<ArrayBuffer> {
return crypto.subtle.sign('HMAC', key, new TextEncoder().encode(activationCode));
}
function equalFingerprint(left: ArrayBuffer, right: ArrayBuffer): boolean {
const a = new Uint8Array(left);
const b = new Uint8Array(right);
if (a.length !== b.length) return false;
let difference = 0;
for (let index = 0; index < a.length; index += 1) difference |= a[index] ^ b[index];
return difference === 0;
}
export function AiHardware() {
const [pageState, setPageState] = useState<PageState>('loading');
const [overview, setOverview] = useState<AiHardwareOverview | null>(null);
const [selectedAgentId, setSelectedAgentId] = useState<string | null>(null);
const [config, setConfig] = useState<AiHardwareAgentConfiguration | null>(null);
const [configRevision, setConfigRevision] = useState<number | null>(null);
const [configLoading, setConfigLoading] = useState(false);
const [notice, setNotice] = useState<string | null>(null);
const [createOpen, setCreateOpen] = useState(false);
const [bindOpen, setBindOpen] = useState(false);
const [configOpen, setConfigOpen] = useState(false);
const [assignmentDevice, setAssignmentDevice] = useState<AiHardwareDevice | null>(null);
const [agentName, setAgentName] = useState('');
const [dialogAgentId, setDialogAgentId] = useState('');
const [draft, setDraft] = useState<ConfigDraft | null>(null);
const [dialogError, setDialogError] = useState<string | null>(null);
const [busy, setBusy] = useState(false);
const [createOperationId, setCreateOperationId] = useState<string | null>(null);
const [configOperationId, setConfigOperationId] = useState<string | null>(null);
const [assignmentOperationId, setAssignmentOperationId] = useState<string | null>(null);
const [createOperationBody, setCreateOperationBody] = useState<string | null>(null);
const [configOperationBody, setConfigOperationBody] = useState<string | null>(null);
const [assignmentOperationBody, setAssignmentOperationBody] = useState<string | null>(null);
const [recoveryOperationId, setRecoveryOperationId] = useState<string | null>(null);
const [recoveryError, setRecoveryError] = useState<string | null>(null);
const [recoveryBusy, setRecoveryBusy] = useState(false);
const activationCodeInputRef = useRef<HTMLInputElement | null>(null);
const bindFingerprintKeyRef = useRef<CryptoKey | null>(null);
const bindRetryIntentRef = useRef<BindRetryIntent | null>(null);
const clearCreateOperation = () => { setCreateOperationId(null); setCreateOperationBody(null); };
const clearBindOperation = () => {
bindRetryIntentRef.current = null;
bindFingerprintKeyRef.current = null;
if (activationCodeInputRef.current) activationCodeInputRef.current.value = '';
};
const clearConfigOperation = () => { setConfigOperationId(null); setConfigOperationBody(null); };
const clearAssignmentOperation = () => { setAssignmentOperationId(null); setAssignmentOperationBody(null); };
const recoverCredential = async () => {
if (recoveryBusy) return;
setRecoveryBusy(true); setRecoveryError(null);
try {
const value = recoveryOperationId
? await recoverAiHardwareCredential({ operationId: recoveryOperationId })
: await recoverAiHardwareCredential();
setRecoveryOperationId(null); setOverview(value); setPageState('ready');
setSelectedAgentId(value.agents[0]?.id ?? null);
} catch (error) {
if (error instanceof AiHardwareApiError && error.code === 'ai_hardware_credential_recovery_unavailable') {
setRecoveryOperationId(null);
await loadOverview();
return;
}
setRecoveryOperationId(retryOperationId(error));
setRecoveryError(safeMessage(error));
} finally { setRecoveryBusy(false); }
};
const loadOverview = useCallback(async () => {
setPageState('loading'); setNotice(null);
try {
const value = await getAiHardwareOverview();
setOverview(value); setPageState('ready');
setSelectedAgentId((current) => value.agents.some((item) => item.id === current) ? current : value.agents[0]?.id ?? null);
} catch (error) {
setOverview(null);
if (error instanceof AiHardwareApiError && error.code === 'AI_HARDWARE_AUTH_REQUIRED') setPageState('auth');
else if (error instanceof AiHardwareApiError && ['AI_HARDWARE_DISABLED', 'ai_hardware_unconfigured'].includes(error.code)) setPageState('disabled');
else setPageState('error');
}
}, []);
useEffect(() => { void loadOverview(); }, [loadOverview]);
useEffect(() => {
if (!selectedAgentId) { setConfig(null); setConfigRevision(null); return; }
let active = true;
setConfig(null);
setConfigRevision(null);
setNotice(null);
setConfigLoading(true);
void getAiHardwareAgentConfiguration(selectedAgentId).then((result) => {
if (!active) return;
setConfig(result.data); setConfigRevision(result.revision);
}).catch(() => { if (active) setNotice('无法读取智能体配置,请刷新后重试。'); })
.finally(() => { if (active) setConfigLoading(false); });
return () => { active = false; };
}, [selectedAgentId]);
const selectedAgent = overview?.agents.find((item) => item.id === selectedAgentId) ?? null;
const devices = useMemo(() => overview?.devices ?? [], [overview]);
const resetDialog = () => {
setDialogError(null); setBusy(false);
clearCreateOperation(); clearBindOperation(); clearConfigOperation(); clearAssignmentOperation();
};
const createAgent = async () => {
const name = agentName.trim();
if (!name || name.length > 64) { setDialogError('名称需要包含 164 个字符。'); return; }
setBusy(true); setDialogError(null);
try {
const canRetry = createOperationId && createOperationBody === name;
const agent = canRetry ? await createAiHardwareAgent(name, { operationId: createOperationId }) : await createAiHardwareAgent(name);
setCreateOperationId(null);
setCreateOperationBody(null);
setCreateOpen(false); setAgentName(''); await loadOverview(); setSelectedAgentId(agent.id);
} catch (error) { setCreateOperationId(retryOperationId(error)); setCreateOperationBody(name); setDialogError(safeMessage(error)); } finally { setBusy(false); }
};
const bindDevice = async () => {
const activationCode = activationCodeInputRef.current?.value ?? '';
if (!/^[0-9]{6}$/.test(activationCode)) { setDialogError('请输入 6 位数字激活码。'); return; }
if (!dialogAgentId) { setDialogError('请选择要绑定的智能体。'); return; }
setBusy(true); setDialogError(null);
try {
const key = bindFingerprintKeyRef.current ?? await createBindFingerprintKey();
bindFingerprintKeyRef.current = key;
const fingerprint = await bindFingerprint(key, activationCode);
const retry = bindRetryIntentRef.current;
if (retry?.agentId === dialogAgentId && equalFingerprint(retry.fingerprint, fingerprint)) {
await bindAiHardwareDevice(activationCode, dialogAgentId, { operationId: retry.operationId });
}
else await bindAiHardwareDevice(activationCode, dialogAgentId);
clearBindOperation();
setBindOpen(false); await loadOverview();
} catch (error) {
const operationId = retryOperationId(error);
if (operationId && bindFingerprintKeyRef.current) {
bindRetryIntentRef.current = {
agentId: dialogAgentId,
fingerprint: await bindFingerprint(bindFingerprintKeyRef.current, activationCode),
operationId,
};
} else bindRetryIntentRef.current = null;
setDialogError(safeMessage(error));
if (activationCodeInputRef.current) activationCodeInputRef.current.value = '';
} finally { setBusy(false); }
};
const saveConfiguration = async () => {
if (!config || !draft || configRevision === null) return;
if (!draft.agent_name.trim() || draft.agent_name.trim().length > 64) { setDialogError('名称需要包含 164 个字符。'); return; }
for (const field of nullableNumberFields) {
if (draft[field] && (!Number.isInteger(Number(draft[field])) || Number(draft[field]) < -100 || Number(draft[field]) > 100)) {
setDialogError('音量、语速和音调必须是 -100 到 100 的整数。'); return;
}
}
const update = configurationUpdate(config, draft);
if (!Object.keys(update).length) { setConfigOpen(false); return; }
setBusy(true); setDialogError(null);
try {
const body = JSON.stringify(update);
const result = configOperationId && configOperationBody === body
? await updateAiHardwareAgentConfiguration(config.id, configRevision, update, { operationId: configOperationId })
: await updateAiHardwareAgentConfiguration(config.id, configRevision, update);
setConfigOperationId(null);
setConfigOperationBody(null);
setConfig(result.data); setConfigRevision(result.revision); setConfigOpen(false); await loadOverview();
} catch (error) {
if (isRevisionConflict(error)) {
setConfigOperationId(null);
const fresh = await getAiHardwareAgentConfiguration(config.id).catch(() => null);
if (fresh) {
setConfig(fresh.data);
setConfigRevision(fresh.revision);
setDraft(replayConfigurationUpdate(fresh.data, update));
}
}
else { setConfigOperationId(retryOperationId(error)); setConfigOperationBody(JSON.stringify(update)); }
setDialogError(safeMessage(error));
} finally { setBusy(false); }
};
const openAssignment = async (device: AiHardwareDevice) => {
setDialogError(null); setBusy(true); setDialogAgentId(device.agent_id); setAssignmentOperationId(null);
try { const result = await getAiHardwareAssignment(device.id); setAssignmentDevice({ ...result.data, assignment_revision: result.revision }); }
catch { setNotice('无法读取设备指派,请刷新后重试。'); }
finally { setBusy(false); }
};
const saveAssignment = async () => {
if (!assignmentDevice || !dialogAgentId) return;
setBusy(true); setDialogError(null);
try {
const body = dialogAgentId;
if (assignmentOperationId && assignmentOperationBody === body) await updateAiHardwareAssignment(assignmentDevice.id, assignmentDevice.assignment_revision, dialogAgentId, { operationId: assignmentOperationId });
else await updateAiHardwareAssignment(assignmentDevice.id, assignmentDevice.assignment_revision, dialogAgentId);
setAssignmentOperationId(null);
setAssignmentOperationBody(null);
setAssignmentDevice(null); await loadOverview();
} catch (error) {
if (isRevisionConflict(error)) {
setAssignmentOperationId(null);
const fresh = await getAiHardwareAssignment(assignmentDevice.id).catch(() => null);
if (fresh) setAssignmentDevice({ ...fresh.data, assignment_revision: fresh.revision });
}
else { setAssignmentOperationId(retryOperationId(error)); setAssignmentOperationBody(dialogAgentId); }
setDialogError(safeMessage(error));
} finally { setBusy(false); }
};
if (pageState === 'loading') return <main data-testid="ai-hardware-page" role="status" aria-live="polite" className="flex min-h-full items-center justify-center"><FeedbackState state="loading" title="正在读取 AI 机器" description="正在同步智能体与机器人设备状态。" /></main>;
if (pageState === 'auth') return <main data-testid="ai-hardware-page" role="alert" className="flex min-h-full items-center justify-center"><FeedbackState state="error" title="请先登录 Works Square" description="登录后即可管理机器人智能体和设备。" action={<Button variant="outline" onClick={() => void loadOverview()}></Button>} /></main>;
if (pageState === 'disabled') return <main data-testid="ai-hardware-page" className="flex min-h-full items-center justify-center"><Card className="max-w-md"><CardHeader><CardTitle>AI </CardTitle><CardDescription></CardDescription></CardHeader><CardContent><Button variant="outline" onClick={() => void loadOverview()}><RefreshCw className="mr-2 h-4 w-4" /></Button></CardContent></Card></main>;
if (pageState === 'error' || !overview) return <main data-testid="ai-hardware-page" role="alert" className="flex min-h-full items-center justify-center"><FeedbackState state="error" title="暂时无法读取 AI 机器" description="连接没有成功,请稍后重试。" action={<Button variant="outline" onClick={() => void loadOverview()}></Button>} /></main>;
if (overview.status === 'credential_recovery_required' || overview.status === 'invalid') return <main data-testid="ai-hardware-page" role="alert" className="flex min-h-full items-center justify-center"><div className="space-y-3 text-center"><FeedbackState state="error" title="需要恢复设备凭据" description="可在此安全地重新签发设备凭据;凭据内容不会显示在客户端。" action={<Button variant="outline" aria-busy={recoveryBusy} disabled={recoveryBusy} onClick={() => void recoverCredential()}>{recoveryBusy && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}</Button>} />{recoveryError ? <p className="text-sm text-destructive">{recoveryError}</p> : null}</div></main>;
if (overview.status === 'provisioning') return <main data-testid="ai-hardware-page" className="flex min-h-full items-center justify-center"><FeedbackState state="empty" title="正在准备机器人工作台" description="完成开通后即可创建智能体并绑定机器人设备。" action={<Button variant="outline" onClick={() => void loadOverview()}></Button>} /></main>;
return (
<main data-testid="ai-hardware-page" className="-m-5 min-h-full bg-background p-5 text-foreground sm:-m-6 sm:p-6">
<header className="mb-6 flex flex-wrap items-start justify-between gap-4">
<div><h1 className="text-balance text-2xl font-semibold"></h1><p className="mt-1 text-pretty text-sm text-muted-foreground"></p></div>
<Button aria-label="刷新 AI 机器" variant="outline" onClick={() => void loadOverview()}><RefreshCw className="mr-2 h-4 w-4" /></Button>
</header>
{notice ? <p role="alert" className="mb-4 rounded-xl bg-amber-500/10 px-4 py-3 text-sm font-medium text-amber-800">{notice}</p> : null}
{overview.agents.length === 0 ? (
<Card className="mx-auto max-w-md text-center"><CardHeader><CardTitle></CardTitle><CardDescription>{overview.status === 'unprovisioned' ? '创建智能体将同时开通你的机器人工作台。' : '智能体创建后,才能把机器人设备绑定给它。'}</CardDescription></CardHeader><CardContent><Button onClick={() => { resetDialog(); setCreateOpen(true); }}><Plus className="mr-2 h-4 w-4" /></Button></CardContent></Card>
) : (
<div className="grid gap-5 lg:grid-cols-[minmax(240px,0.7fr)_minmax(0,1.3fr)]">
<Card><CardHeader className="flex-row items-center justify-between"><div><CardTitle></CardTitle><CardDescription>{overview.agents.length} </CardDescription></div><Button size="icon" aria-label="创建智能体" onClick={() => { resetDialog(); setCreateOpen(true); }}><Plus className="h-4 w-4" /></Button></CardHeader><CardContent className="space-y-2">{overview.agents.map((agent) => <button key={agent.id} type="button" aria-pressed={agent.id === selectedAgentId} onClick={() => setSelectedAgentId(agent.id)} className={`motion-press flex min-h-12 w-full items-center gap-3 rounded-xl px-3 py-2 text-left ${agent.id === selectedAgentId ? 'bg-brand-soft' : 'bg-surface-subtle hover:bg-surface-tertiary'}`}><Bot className="h-4 w-4 shrink-0" /><span className="min-w-0 flex-1"><span className="block truncate text-sm font-semibold">{agent.name}</span><span title={agent.id} className="block text-xs tabular-nums text-muted-foreground">{shortId(agent.id)} · r{agent.config_revision}</span></span></button>)}</CardContent></Card>
<div className="space-y-5">
<Card><CardHeader className="flex-row items-center justify-between"><div><CardTitle>{selectedAgent?.name ?? '智能体配置'}</CardTitle><CardDescription></CardDescription></div><Button variant="outline" disabled={configLoading || !config} onClick={() => { if (!config) return; resetDialog(); setDraft(draftFrom(config)); setConfigOpen(true); }}>{configLoading ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : <Settings2 className="mr-2 h-4 w-4" />}</Button></CardHeader><CardContent>{config ? <dl className="grid gap-3 text-sm sm:grid-cols-2"><div><dt className="text-muted-foreground"></dt><dd>{config.language || config.lang_code || '未设置'}</dd></div><div><dt className="text-muted-foreground"></dt><dd>{config.tts_voice_id || '未设置'}</dd></div><div className="sm:col-span-2"><dt className="text-muted-foreground"></dt><dd className="mt-1 whitespace-pre-wrap">{config.system_prompt || '未设置'}</dd></div></dl> : <FeedbackState state="loading" title="正在读取配置" />}</CardContent></Card>
<Card><CardHeader className="flex-row items-center justify-between"><div><CardTitle></CardTitle><CardDescription>{devices.length} </CardDescription></div><Button onClick={() => { resetDialog(); setDialogAgentId(selectedAgentId ?? overview.agents[0].id); setBindOpen(true); }}><Link2 className="mr-2 h-4 w-4" /></Button></CardHeader><CardContent>{devices.length ? <div className="space-y-2">{devices.map((device) => <div key={device.id} className="flex min-h-12 items-center justify-between gap-3 rounded-xl bg-surface-subtle px-3 py-2"><span className="min-w-0"><span className="block text-sm font-semibold"> {shortId(device.id)}</span><span title={device.id} className="text-xs tabular-nums text-muted-foreground"> r{device.assignment_revision}</span></span><Button variant="outline" size="sm" onClick={() => void openAssignment(device)}></Button></div>)}</div> : <FeedbackState state="empty" title="还没有绑定设备" description="使用设备上的 6 位激活码完成绑定。" />}</CardContent></Card>
</div>
</div>
)}
<Dialog open={createOpen} onOpenChange={(open) => { if (!busy) { setCreateOpen(open); if (!open) { setAgentName(''); resetDialog(); } } }}><DialogContent><DialogHeader><DialogTitle></DialogTitle><DialogDescription></DialogDescription></DialogHeader><div><Label htmlFor="hardware-agent-name"></Label><Input id="hardware-agent-name" autoFocus maxLength={64} value={agentName} onChange={(e) => { setAgentName(e.target.value); clearCreateOperation(); }} /></div>{dialogError ? <p role="alert" className="text-sm text-destructive">{dialogError}</p> : null}<DialogFooter><Button variant="outline" disabled={busy} onClick={() => { setCreateOpen(false); resetDialog(); }}></Button><Button aria-busy={busy} disabled={busy} onClick={() => void createAgent()}>{busy && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}</Button></DialogFooter></DialogContent></Dialog>
<Dialog open={bindOpen} onOpenChange={(open) => { if (!busy) { setBindOpen(open); if (!open) resetDialog(); } }}><DialogContent><DialogHeader><DialogTitle></DialogTitle><DialogDescription></DialogDescription></DialogHeader><div><Label htmlFor="hardware-activation-code">6 </Label><Input ref={activationCodeInputRef} id="hardware-activation-code" autoFocus type="text" inputMode="numeric" autoComplete="off" maxLength={6} onInput={(e) => { e.currentTarget.value = e.currentTarget.value.replace(/\D/g, '').slice(0, 6); }} /></div><AgentSelect agents={overview.agents} value={dialogAgentId} onChange={(value) => { setDialogAgentId(value); clearBindOperation(); }} />{dialogError ? <p role="alert" className="text-sm text-destructive">{dialogError}</p> : null}<DialogFooter><Button variant="outline" disabled={busy} onClick={() => { setBindOpen(false); resetDialog(); }}></Button><Button aria-busy={busy} disabled={busy} onClick={() => void bindDevice()}>{busy && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}</Button></DialogFooter></DialogContent></Dialog>
<Dialog open={configOpen} onOpenChange={(open) => { if (!busy) { setConfigOpen(open); if (!open) resetDialog(); } }}><DialogContent className="max-h-[90vh] overflow-y-auto"><DialogHeader><DialogTitle></DialogTitle><DialogDescription></DialogDescription></DialogHeader>{draft ? <ConfigFields draft={draft} setDraft={(next) => { setDraft(next); clearConfigOperation(); }} /> : null}{dialogError ? <p role="alert" className="text-sm text-destructive">{dialogError}</p> : null}<DialogFooter><Button variant="outline" disabled={busy} onClick={() => { setConfigOpen(false); resetDialog(); }}></Button><Button aria-busy={busy} disabled={busy} onClick={() => void saveConfiguration()}>{busy && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}</Button></DialogFooter></DialogContent></Dialog>
<Dialog open={Boolean(assignmentDevice)} onOpenChange={(open) => { if (!open && !busy) { setAssignmentDevice(null); resetDialog(); } }}><DialogContent><DialogHeader><DialogTitle></DialogTitle><DialogDescription></DialogDescription></DialogHeader><AgentSelect agents={overview.agents} value={dialogAgentId} onChange={(value) => { setDialogAgentId(value); clearAssignmentOperation(); }} />{dialogError ? <p role="alert" className="text-sm text-destructive">{dialogError}</p> : null}<DialogFooter><Button variant="outline" disabled={busy} onClick={() => { setAssignmentDevice(null); resetDialog(); }}></Button><Button aria-busy={busy} disabled={busy} onClick={() => void saveAssignment()}>{busy && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}</Button></DialogFooter></DialogContent></Dialog>
</main>
);
}
function AgentSelect({ agents, value, onChange }: { agents: AiHardwareAgent[]; value: string; onChange: (value: string) => void }) {
return <div><Label htmlFor="hardware-agent-select"></Label><select id="hardware-agent-select" className="mt-1 h-10 w-full rounded-xl border border-border/80 bg-surface-input px-3 text-sm" value={value} onChange={(e) => onChange(e.target.value)}>{agents.map((agent) => <option key={agent.id} value={agent.id}>{agent.name}</option>)}</select></div>;
}
function ConfigFields({ draft, setDraft }: { draft: ConfigDraft; setDraft: (draft: ConfigDraft) => void }) {
const field = (key: keyof ConfigDraft, label: string, type = 'text') => <div><Label htmlFor={`hardware-${key}`}>{label}</Label><Input id={`hardware-${key}`} type={type} value={draft[key]} onChange={(e) => setDraft({ ...draft, [key]: e.target.value })} /></div>;
return <div className="space-y-4"><div className="grid gap-4 sm:grid-cols-2">{field('agent_name', '名称')}<div className="sm:col-span-2"><Label htmlFor="hardware-system_prompt"></Label><Textarea id="hardware-system_prompt" value={draft.system_prompt} onChange={(e) => setDraft({ ...draft, system_prompt: e.target.value })} /></div>{field('language', '语言')}{field('lang_code', '语言代码')}{field('tts_voice_id', 'TTS 语音 ID')}{field('tts_volume', '音量 (-100100)', 'number')}{field('tts_rate', '语速 (-100100)', 'number')}{field('tts_pitch', '音调 (-100100)', 'number')}<div><Label htmlFor="hardware-chat_history_conf"></Label><select id="hardware-chat_history_conf" className="mt-1 h-10 w-full rounded-xl border border-border/80 bg-surface-input px-3 text-sm" value={draft.chat_history_conf} onChange={(e) => setDraft({ ...draft, chat_history_conf: e.target.value })}><option value=""></option><option value="0"></option><option value="1"></option><option value="2"></option></select></div></div><details className="rounded-xl border border-border/80 bg-surface-subtle px-4 py-3"><summary className="cursor-pointer text-sm font-semibold"></summary><p className="mt-2 text-xs text-muted-foreground"> ID </p><div className="mt-4 grid gap-4 sm:grid-cols-2">{field('asr_model_id', 'ASR 模型 ID')}{field('vad_model_id', 'VAD 模型 ID')}{field('llm_model_id', 'LLM 模型 ID')}{field('slm_model_id', 'SLM 模型 ID')}{field('vllm_model_id', 'VLLM 模型 ID')}{field('tts_model_id', 'TTS 模型 ID')}{field('tts_language', 'TTS 语言')}{field('mem_model_id', '记忆模型 ID')}{field('intent_model_id', '意图模型 ID')}</div></details></div>;
}
export default AiHardware;