"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(" ")}
>
);
})}
);
}
export function TextAreaField({
label,
value,
placeholder,
rows = 4,
onChange,
}: {
label?: string;
value: string;
placeholder?: string;
rows?: number;
onChange: (value: string) => void;
}) {
return (
);
}
// Radix Select 不允许空字符串 value,用哨兵表示"未选/无"
const NONE_VALUE = "__none__";
export function ResourceSelectField({
label,
value,
options,
noneLabel,
onChange,
}: {
label?: string;
value: string;
options: { value: string; label: string }[];
/** 提供则在顶部加一个"无/默认"选项,选中映射为空串 */
noneLabel?: string;
onChange: (value: string) => void;
}) {
return (
{label && (
{label}
)}
);
}
export function KnowledgeRetrievalConfigDialog({
disabled,
value,
onChange,
}: {
disabled: boolean;
value: KnowledgeRetrievalConfig;
onChange: (config: KnowledgeRetrievalConfig) => void;
}) {
const [open, setOpen] = useState(false);
const [draft, setDraft] = useState(value);
const [error, setError] = useState(null);
function openDialog() {
setDraft(value);
setError(null);
setOpen(true);
}
function saveDraft() {
if (draft.topN === 0 || draft.topN < -1 || !Number.isInteger(draft.topN)) {
setError("Top N 必须为 -1 或大于 0 的整数");
return;
}
if (draft.scoreThreshold < 0 || draft.scoreThreshold > 1) {
setError("最低相关度必须在 0 到 1 之间");
return;
}
onChange(draft);
setOpen(false);
}
return (
<>
>
);
}
export function ToggleRow({
icon,
title,
description,
hint,
checked,
onChange,
}: {
icon?: React.ReactNode;
title: string;
description?: string;
hint?: string;
checked: boolean;
onChange: (checked: boolean) => void;
}) {
const hasIcon = Boolean(icon);
return (
{icon && (
{icon}
)}
{title}
{hint && }
{description && (
{description}
)}
);
}