Refine WebUI commands and data skills

This commit is contained in:
武阳
2026-05-06 19:35:48 +08:00
parent 93a0eb74d7
commit a0da3c2423
13 changed files with 731 additions and 219 deletions
+113 -33
View File
@@ -27,7 +27,6 @@ import {
MoreHorizontalIcon,
PencilIcon,
RefreshCwIcon,
SlashIcon,
SquareIcon,
WrenchIcon,
} from "lucide-react";
@@ -69,6 +68,13 @@ type SkillSummary = {
when_to_use?: string;
};
type SlashCommandSummary = {
names: string[];
primary: string;
description?: string;
webui_supported?: boolean;
};
type ClawState = {
model?: string;
active_session_id?: string | null;
@@ -699,46 +705,19 @@ function modelProvider(model: string) {
const ComposerAssistButtons: FC = () => {
return (
<div className="flex items-center gap-1">
<PromptInsertDialog
title="快捷入口"
description="选择一个数据开发入口,会把可编辑提示词插入输入框。"
triggerIcon={<SlashIcon className="size-4" />}
triggerLabel="/"
items={QUICK_PROMPTS}
/>
<SlashCommandDialog />
<SkillInsertDialog />
<PromptInsertDialog
title="常用工具"
description="这里用于调试和明确意图,最终是否调用仍由 Agent 决策。"
triggerIcon={<WrenchIcon className="size-4" />}
triggerLabel="Tools"
triggerLabel=""
items={TOOL_PROMPTS}
/>
</div>
);
};
const QUICK_PROMPTS = [
{
title: "产品定义生成数据",
description: "从产品定义、例子 query 或手写边界整理数据目标。",
prompt:
"请根据我提供的产品定义或手写规则,先总结 query 语义边界、目标标签和排除范围,给我 review 数据生成目标,确认后再生成 canonical records。",
},
{
title: "线上数据挖掘",
description: "构造关键词/正则/domain 条件,采样候选并转换线上样本。",
prompt:
"请进入线上数据挖掘流程:先理解我的挖掘需求,设计筛选策略并采样候选,待我 review 后,把选中的线上数据直接转换为 canonical records,不要走生成数据分支。",
},
{
title: "校验 records",
description: "检查 canonical records 的字段、标签和多轮上下文。",
prompt:
"请校验我提供的 canonical records,重点检查 query、target、prev_session、timestamp 和标签格式,并输出需要修复的问题。",
},
];
const TOOL_PROMPTS = [
{
title: "data_agent_profile_router_sessions",
@@ -792,7 +771,9 @@ function PromptInsertDialog({
aria-label={title}
>
{triggerIcon}
<span className="hidden text-xs md:inline">{triggerLabel}</span>
{triggerLabel ? (
<span className="hidden text-xs md:inline">{triggerLabel}</span>
) : null}
</Button>
</DialogTrigger>
<DialogContent>
@@ -823,6 +804,100 @@ function PromptInsertDialog({
);
}
function SlashCommandDialog() {
const [open, setOpen] = useState(false);
const [commands, setCommands] = useState<SlashCommandSummary[]>([]);
const [status, setStatus] = useState(
"点击后读取当前后端 slash command 列表。",
);
async function loadCommands() {
setStatus("正在读取 slash command 列表...");
try {
const response = await fetch("/api/claw/slash-commands", {
cache: "no-store",
});
const payload = (await response.json()) as
| SlashCommandSummary[]
| { error?: string };
if (!response.ok || !Array.isArray(payload)) {
throw new Error(
Array.isArray(payload) ? "读取失败" : (payload.error ?? "读取失败"),
);
}
setCommands(payload);
setStatus(payload.length ? "" : "当前没有可用 slash command。");
} catch (err) {
setStatus(err instanceof Error ? err.message : "读取 slash command 失败");
}
}
return (
<Dialog
open={open}
onOpenChange={(open) => {
setOpen(open);
if (open) loadCommands();
}}
>
<DialogTrigger asChild>
<Button
type="button"
variant="ghost"
size="sm"
className="h-8 rounded-full px-2 font-mono text-muted-foreground text-sm"
aria-label="Slash Commands"
>
/
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription>
`/` WebUI
</DialogDescription>
</DialogHeader>
{status ? (
<p className="text-muted-foreground text-sm">{status}</p>
) : null}
<div className="grid max-h-96 gap-2 overflow-y-auto">
{commands.map((command) => {
const commandText = `/${command.primary}`;
const aliases = command.names
.filter((name) => name !== command.primary)
.map((name) => `/${name}`)
.join(" ");
return (
<button
key={command.primary}
type="button"
className="rounded-lg border px-3 py-2 text-left transition-colors hover:bg-muted"
onClick={() => {
insertComposerText(`${commandText}\n`);
setOpen(false);
}}
>
<div className="font-medium font-mono text-sm">
{commandText}
</div>
<div className="mt-1 line-clamp-2 text-muted-foreground text-xs">
{command.description || "暂无说明"}
</div>
{aliases ? (
<div className="mt-1 text-muted-foreground/80 text-[11px]">
{aliases}
</div>
) : null}
</button>
);
})}
</div>
</DialogContent>
</Dialog>
);
}
function SkillInsertDialog() {
const [open, setOpen] = useState(false);
const [skills, setSkills] = useState<SkillSummary[]>([]);
@@ -864,7 +939,6 @@ function SkillInsertDialog() {
aria-label="Skills"
>
<BookOpenIcon className="size-4" />
<span className="hidden text-xs md:inline">Skills</span>
</Button>
</DialogTrigger>
<DialogContent>
@@ -1032,10 +1106,16 @@ function summarizeActivityChainLabel(
return runningToolCount > 0 ? "调用工具中" : "思考中";
}
const label = lastReasoningLine || "思考完成";
const label = compactActivityLabel(lastReasoningLine || "思考完成");
return toolCount > 0 ? `${label} · 调用了 ${toolCount} 个工具` : label;
}
function compactActivityLabel(label: string) {
const normalized = label.replace(/\s+/g, " ").trim();
if (normalized.length <= 36) return normalized;
return `${normalized.slice(0, 35)}`;
}
const AssistantActionBar: FC = () => {
return (
<ActionBarPrimitive.Root