Refine WebUI commands and data skills
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user