"use client"; import type React from "react"; import { useEffect, useRef, useState } from "react"; import { AudioLines, Check, ChevronLeft, Copy, Pencil, Settings2, Waypoints, } from "lucide-react"; import { HelpHint } from "@/components/editor/section-card"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Switch } from "@/components/ui/switch"; import { Textarea } from "@/components/ui/textarea"; import type { KnowledgeRetrievalConfig } from "@/lib/api"; import type { RuntimeMode } from "./types"; export function EditorBackButton({ onClick, ariaLabel = "返回助手列表", }: { onClick: () => void; ariaLabel?: string; }) { return ( ); } export function AssistantIdentity({ assistantId, entityLabel = "助手", }: { assistantId: string | null; entityLabel?: string; }) { const [copied, setCopied] = useState(false); async function copyId() { if (!assistantId) return; await navigator.clipboard.writeText(assistantId); setCopied(true); window.setTimeout(() => setCopied(false), 1600); } return (
{assistantId ? `ID · ${assistantId}` : "ID · 保存后生成"} {assistantId && ( )} {!assistantId && }
); } export function EditableTitle({ value, onChange, placeholder = "未命名助手", editLabel = "助手名称", variant = "page", allowEmpty = false, maxLength, }: { value: string; onChange: (value: string) => void; placeholder?: string; editLabel?: string; variant?: "page" | "panel"; allowEmpty?: boolean; maxLength?: number; }) { const [editing, setEditing] = useState(false); const [draft, setDraft] = useState(value); const inputRef = useRef(null); useEffect(() => { if (editing) { inputRef.current?.focus(); inputRef.current?.select(); } }, [editing]); function startEdit() { setDraft(value); setEditing(true); } function commit() { const next = draft.trim(); if (next || allowEmpty) { onChange(next); } setEditing(false); } if (editing) { return ( setDraft(event.target.value)} onBlur={commit} onKeyDown={(event) => { if (event.key === "Enter") { event.preventDefault(); commit(); } else if (event.key === "Escape") { event.preventDefault(); setEditing(false); } }} aria-label={`修改${editLabel}`} className={ variant === "panel" ? "min-w-0 flex-1 border-b border-primary bg-transparent px-3 py-1.5 text-sm font-medium text-foreground outline-none" : "font-display w-[min(60vw,420px)] border-b border-primary bg-transparent text-lg text-ink outline-none" } /> ); } return ( ); } export function RuntimeModeSelector({ value, onChange, }: { value: RuntimeMode; onChange: (mode: RuntimeMode) => void; }) { const options = [ { value: "pipeline" as const, label: "Pipeline 模式", hint: "通过 ASR、LLM 和 TTS 级联组成语音管线,灵活选配各模块。", icon: Waypoints, }, { value: "realtime" as const, label: "Realtime 模式", hint: "使用原生实时语音模型,模型直接处理音频输入并生成语音回复。", icon: AudioLines, }, ]; return (
{options.map((option) => { const Icon = option.icon; const selected = value === option.value; const select = () => onChange(option.value); return (
{ if (event.key === "Enter" || event.key === " ") { event.preventDefault(); select(); } }} className={[ "cursor-pointer rounded-xl border p-3.5 text-left transition-colors", selected ? "border-primary bg-primary/5 ring-1 ring-primary" : "border-hairline bg-canvas-soft hover:border-hairline-strong", ].join(" ")} >
{option.label}
{selected && ( )}
); })}
); } export function TextAreaField({ label, value, placeholder, rows = 4, onChange, }: { label?: string; value: string; placeholder?: string; rows?: number; onChange: (value: string) => void; }) { return (