feat(desktop): 首屏工作台商业化重做(定调)
把"工程师内部控制台"改成"给客户用的产品门面"(精致深色专业版方向): - 问候式头部(下午好,X · 工作区上下文)+ 主 CTA「新建编排」抬到显眼位 - 指标去黑话:评测均分/忠实 → 「回答质量 89%」;Token → 「今日用量」 - 能力卡改结果语言(不再暴露 Eino/向量·全文·图谱/Word 等架构黑话) - 删「服务·网关/总线/PG/Redis/Milvus/Neo4j」健康灯带(客户不看数据库名) - 空态改引导(还没运行记录 → 从新建编排开始 CTA) - 图标去彩虹化,单色 + hover 才亮 brand,右上箭头暗示可点 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -303,7 +303,7 @@ export default function App() {
|
|||||||
<LeftNav active={view} onSelect={setView} />
|
<LeftNav active={view} onSelect={setView} />
|
||||||
<main className="min-w-0 flex-1 overflow-hidden">
|
<main className="min-w-0 flex-1 overflow-hidden">
|
||||||
{view === "home" ? (
|
{view === "home" ? (
|
||||||
<Home onSelect={setView} />
|
<Home onSelect={setView} userName={user.name || user.email} spaceName={space?.space?.name} />
|
||||||
) : view === "studio" ? (
|
) : view === "studio" ? (
|
||||||
<StudioView onRun={onRun} phase={run.phase} identity={identity} readOnly={tenant?.role === "viewer"} spaceId={space?.space?.id ?? ""} spaceReadOnly={space?.role === "viewer"} />
|
<StudioView onRun={onRun} phase={run.phase} identity={identity} readOnly={tenant?.role === "viewer"} spaceId={space?.space?.id ?? ""} spaceReadOnly={space?.role === "viewer"} />
|
||||||
) : view === "kb" ? (
|
) : view === "kb" ? (
|
||||||
|
|||||||
@@ -1,62 +1,56 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Workflow, Database, Bookmark, FileText, Plus, Upload, Activity, Coins, Gauge, ArrowRight, type LucideIcon } from "lucide-react";
|
import { Workflow, Database, Bookmark, FileText, Plus, Upload, Activity, Coins, Gauge, ArrowRight, ArrowUpRight, type LucideIcon } from "lucide-react";
|
||||||
import { statsOverview, type Overview, type RecentRun } from "../lib/api";
|
import { statsOverview, type Overview, type RecentRun } from "../lib/api";
|
||||||
import type { ViewKey } from "../shell/LeftNav";
|
import type { ViewKey } from "../shell/LeftNav";
|
||||||
import { Button, cn } from "../ui";
|
import { Button, cn } from "../ui";
|
||||||
|
|
||||||
// 迷你火花线:把一串数值画成 SVG 折线(仪表盘趋势)。
|
// 迷你火花线:把一串数值画成 SVG 折线(指标趋势)。
|
||||||
function Spark({ data, stroke }: { data: number[]; stroke: string }) {
|
function Spark({ data, stroke }: { data: number[]; stroke: string }) {
|
||||||
if (data.length < 2) return null;
|
if (data.length < 2) return null;
|
||||||
const max = Math.max(...data, 1);
|
const max = Math.max(...data, 1);
|
||||||
const min = Math.min(...data, 0);
|
const min = Math.min(...data, 0);
|
||||||
const span = max - min || 1;
|
const span = max - min || 1;
|
||||||
const w = 60;
|
const w = 64;
|
||||||
const h = 18;
|
const h = 20;
|
||||||
const pts = data
|
const pts = data.map((v, i) => `${(i / (data.length - 1)) * w},${h - ((v - min) / span) * (h - 2) - 1}`).join(" ");
|
||||||
.map((v, i) => `${(i / (data.length - 1)) * w},${h - ((v - min) / span) * (h - 2) - 1}`)
|
|
||||||
.join(" ");
|
|
||||||
return (
|
return (
|
||||||
<svg width={w} height={h} viewBox={`0 0 ${w} ${h}`} aria-hidden>
|
<svg width={w} height={h} viewBox={`0 0 ${w} ${h}`} aria-hidden className="opacity-90">
|
||||||
<polyline points={pts} fill="none" stroke={stroke} strokeWidth={1.5} strokeLinejoin="round" />
|
<polyline points={pts} fill="none" stroke={stroke} strokeWidth={1.5} strokeLinecap="round" strokeLinejoin="round" />
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function Metric({ icon: Icon, label, value, sub, spark, sparkColor, accent }: {
|
function Metric({ icon: Icon, label, value, sub, spark }: {
|
||||||
icon: LucideIcon; label: string; value: string; sub: string;
|
icon: LucideIcon; label: string; value: string; sub: string; spark?: number[];
|
||||||
spark?: number[]; sparkColor?: string; accent?: string;
|
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="rounded-lg border border-line bg-ink-850 p-4">
|
<div className="rounded-xl border border-line bg-ink-850 p-4 transition-colors hover:border-ink-600">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center gap-2 text-slate-500">
|
||||||
<span className="text-xs text-slate-500">{label}</span>
|
<Icon className="h-4 w-4" strokeWidth={1.8} />
|
||||||
<Icon className="h-4 w-4 text-slate-600" strokeWidth={1.8} />
|
<span className="text-xs">{label}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className={cn("mt-1 text-2xl font-semibold tabular-nums", accent ?? "text-slate-100")}>{value}</div>
|
<div className="mt-2 text-[28px] font-semibold leading-none tabular-nums text-slate-100">{value}</div>
|
||||||
<div className="mt-1.5 flex items-center justify-between gap-2">
|
<div className="mt-2.5 flex items-end justify-between gap-2">
|
||||||
<span className="text-[11px] text-slate-500">{sub}</span>
|
<span className="text-[11px] text-slate-500">{sub}</span>
|
||||||
{spark && spark.some((v) => v > 0) ? <Spark data={spark} stroke={sparkColor ?? "#a78bfa"} /> : null}
|
{spark && spark.some((v) => v > 0) ? <Spark data={spark} stroke="rgb(var(--slate-500))" /> : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const CAPS: { icon: LucideIcon; title: string; desc: string; to: ViewKey; color: string }[] = [
|
// 能力入口:用「结果语言」描述,不暴露架构黑话。
|
||||||
{ icon: Workflow, title: "Agent 编排", desc: "画布 → Eino 动态图 → 流式执行", to: "studio", color: "text-brand-400" },
|
const CAPS: { icon: LucideIcon; title: string; desc: string; to: ViewKey }[] = [
|
||||||
{ icon: Database, title: "RAG 知识库", desc: "向量 / 全文 / 图谱 三路混合检索", to: "kb", color: "text-accent-400" },
|
{ icon: Workflow, title: "智能体编排", desc: "把多步任务串成可复用的自动化工作流", to: "studio" },
|
||||||
{ icon: FileText, title: "报告生成", desc: "规划 → 分章并行 → 渲染 Word", to: "report", color: "text-indigo-300" },
|
{ icon: Database, title: "知识库问答", desc: "上传资料,让 AI 基于你的文档精准作答", to: "kb" },
|
||||||
{ icon: Bookmark, title: "偏好记忆", desc: "画像 + 多轮历史", to: "memory", color: "text-success" },
|
{ icon: FileText, title: "报告生成", desc: "从大纲到成稿,一键产出结构化文档", to: "report" },
|
||||||
|
{ icon: Bookmark, title: "偏好记忆", desc: "记住你的习惯与历史,越用越懂你", to: "memory" },
|
||||||
];
|
];
|
||||||
|
|
||||||
const STATUS_DOT: Record<string, string> = {
|
const STATUS_LABEL: Record<string, { text: string; cls: string }> = {
|
||||||
done: "bg-success",
|
done: { text: "完成", cls: "text-success" },
|
||||||
failed: "bg-danger", timeout: "bg-danger", rejected: "bg-danger",
|
failed: { text: "失败", cls: "text-danger" }, timeout: { text: "超时", cls: "text-danger" }, rejected: { text: "已拒", cls: "text-danger" },
|
||||||
waiting: "bg-warn",
|
waiting: { text: "待审", cls: "text-warn" },
|
||||||
running: "bg-brand", submitted: "bg-brand",
|
running: { text: "运行中", cls: "text-brand-400" }, submitted: { text: "已提交", cls: "text-brand-400" },
|
||||||
};
|
|
||||||
|
|
||||||
const SERVICE_LABEL: Record<string, string> = {
|
|
||||||
gateway: "网关", nats: "总线", db: "PG", redis: "Redis", milvus: "Milvus", neo4j: "Neo4j",
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function relTime(iso: string): string {
|
function relTime(iso: string): string {
|
||||||
@@ -68,18 +62,24 @@ function relTime(iso: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function RunRow({ r }: { r: RecentRun }) {
|
function RunRow({ r }: { r: RecentRun }) {
|
||||||
|
const s = STATUS_LABEL[r.status] ?? { text: r.status, cls: "text-slate-500" };
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-3 border-b border-line/60 py-2 last:border-0">
|
<div className="flex items-center gap-3 rounded-lg px-2 py-2 transition-colors hover:bg-ink-850">
|
||||||
<span className={cn("h-1.5 w-1.5 flex-none rounded-full", STATUS_DOT[r.status] ?? "bg-ink-600")} />
|
<span className={cn("h-1.5 w-1.5 flex-none rounded-full", s.cls.replace("text-", "bg-"))} />
|
||||||
<span className="flex-1 truncate font-mono text-xs text-slate-300">{r.task_id}</span>
|
<span className="flex-1 truncate font-mono text-xs text-slate-400">{r.task_id}</span>
|
||||||
<span className="text-[11px] text-slate-500">{r.status}</span>
|
<span className={cn("text-[11px]", s.cls)}>{s.text}</span>
|
||||||
<span className="w-16 text-right text-[11px] text-slate-600">{relTime(r.at)}</span>
|
<span className="w-16 text-right text-[11px] text-slate-600">{relTime(r.at)}</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 工作台:实时驾驶舱(指标 + 趋势 + 近期运行 + 服务健康 + 快捷动作)。
|
function greet(): string {
|
||||||
export function Home({ onSelect }: { onSelect: (v: ViewKey) => void }) {
|
const h = new Date().getHours();
|
||||||
|
return h < 6 ? "凌晨好" : h < 12 ? "早上好" : h < 14 ? "中午好" : h < 18 ? "下午好" : "晚上好";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 工作台:商业级门面 —— 问候 + 主行动 + 关键指标 + 最近任务 + 快速开始(去基建、去黑话)。
|
||||||
|
export function Home({ onSelect, userName, spaceName }: { onSelect: (v: ViewKey) => void; userName?: string; spaceName?: string }) {
|
||||||
const [ov, setOv] = useState<Overview | null>(null);
|
const [ov, setOv] = useState<Overview | null>(null);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let alive = true;
|
let alive = true;
|
||||||
@@ -93,92 +93,91 @@ export function Home({ onSelect }: { onSelect: (v: ViewKey) => void }) {
|
|||||||
const tokenTrend = ov?.token_trend.map((d) => d.count) ?? [];
|
const tokenTrend = ov?.token_trend.map((d) => d.count) ?? [];
|
||||||
const maxTask = Math.max(...taskTrend, 1);
|
const maxTask = Math.max(...taskTrend, 1);
|
||||||
const fmtTokens = (n: number) => (n >= 1e6 ? `${(n / 1e6).toFixed(1)}M` : n >= 1e3 ? `${(n / 1e3).toFixed(1)}K` : `${n}`);
|
const fmtTokens = (n: number) => (n >= 1e6 ? `${(n / 1e6).toFixed(1)}M` : n >= 1e3 ? `${(n / 1e3).toFixed(1)}K` : `${n}`);
|
||||||
|
const hasRuns = !!ov && ov.recent_runs.length > 0;
|
||||||
|
const quality = ov?.eval_count ? `${Math.round((ov.eval_avg ?? 0) * 100)}%` : "—";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full overflow-auto p-8">
|
<div className="h-full overflow-auto p-8">
|
||||||
<div className="mx-auto max-w-6xl">
|
<div className="mx-auto max-w-6xl">
|
||||||
{/* 头部 */}
|
{/* 问候 + 主行动 */}
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex flex-wrap items-end justify-between gap-4">
|
||||||
<div className="flex h-11 w-11 items-center justify-center rounded-xl bg-gradient-to-br from-brand to-accent text-lg font-bold text-white shadow-glow">S</div>
|
<div>
|
||||||
<div className="flex-1">
|
<h1 className="text-2xl font-semibold tracking-tight text-slate-100">
|
||||||
<h1 className="brand-gradient text-2xl font-bold leading-tight">工作台</h1>
|
{greet()}{userName ? `,${userName}` : ""}
|
||||||
<p className="text-sm text-slate-500">实时概览 · 编排 / 知识库 / 报告 / 记忆</p>
|
</h1>
|
||||||
|
<p className="mt-1 text-sm text-slate-500">
|
||||||
|
{spaceName ? <>工作区 · <span className="text-slate-400">{spaceName}</span> · </> : null}今天想做点什么?
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<span className="flex items-center gap-1.5 rounded-full border border-line bg-ink-850 px-3 py-1 text-[11px] text-slate-500">
|
<Button variant="primary" size="md" icon={Plus} onClick={() => onSelect("studio")}>新建编排</Button>
|
||||||
<span className={cn("h-1.5 w-1.5 rounded-full", ov?.services.gateway ? "bg-success" : "bg-danger")} />
|
|
||||||
{ov?.services.gateway ? "实时" : "连接中"}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 指标卡 */}
|
{/* 关键指标 */}
|
||||||
<div className="mt-6 grid grid-cols-2 gap-3 lg:grid-cols-4">
|
<div className="mt-7 grid grid-cols-2 gap-3 lg:grid-cols-4">
|
||||||
<Metric icon={Activity} label="今日任务" value={`${ov?.tasks_today ?? 0}`} sub={`累计 ${ov?.tasks_total ?? 0}`}
|
<Metric icon={Activity} label="今日任务" value={`${ov?.tasks_today ?? 0}`} sub={`累计 ${ov?.tasks_total ?? 0} 次`} spark={taskTrend} />
|
||||||
spark={taskTrend} sparkColor="#34d399" accent="text-brand-400" />
|
<Metric icon={Coins} label="今日用量" value={fmtTokens(ov?.tokens_today ?? 0)} sub={ov?.daily_budget ? `额度 ${fmtTokens(ov.daily_budget)}` : "未限额"} spark={tokenTrend} />
|
||||||
<Metric icon={Coins} label="今日 Token" value={fmtTokens(ov?.tokens_today ?? 0)}
|
<Metric icon={Database} label="知识库" value={`${ov?.kb_docs ?? 0}`} sub={`${ov?.kb_count ?? 0} 个库 · ${ov?.kb_docs ?? 0} 篇文档`} />
|
||||||
sub={ov?.daily_budget ? `预算 ${fmtTokens(ov.daily_budget)}` : "未限额"} spark={tokenTrend} sparkColor="#a78bfa" accent="text-accent-400" />
|
<Metric icon={Gauge} label="回答质量" value={quality} sub={ov?.eval_count ? `近 ${ov.eval_count} 次评估` : "暂无评估"} />
|
||||||
<Metric icon={Gauge} label="评测均分" value={(ov?.eval_avg ?? 0).toFixed(2)}
|
|
||||||
sub={`忠实 ${(ov?.faithful_avg ?? 0).toFixed(2)} · ${ov?.eval_count ?? 0} 次`} accent="text-success" />
|
|
||||||
<Metric icon={Database} label="知识库" value={`${ov?.kb_docs ?? 0}`} sub={`${ov?.kb_count ?? 0} 库 · 三路检索`} accent="text-indigo-300" />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 近期运行 + 7 日趋势 */}
|
{/* 最近任务 + 7 日趋势 */}
|
||||||
<div className="mt-4 grid grid-cols-1 gap-3 lg:grid-cols-[1.6fr_1fr]">
|
<div className="mt-4 grid grid-cols-1 gap-3 lg:grid-cols-[1.7fr_1fr]">
|
||||||
<div className="rounded-lg border border-line bg-ink-900 p-4">
|
<div className="rounded-xl border border-line bg-ink-900 p-4">
|
||||||
<div className="mb-2 flex items-center justify-between">
|
<div className="mb-1 flex items-center justify-between px-2">
|
||||||
<span className="text-sm font-medium text-slate-300">近期运行</span>
|
<span className="text-sm font-medium text-slate-200">最近任务</span>
|
||||||
<button onClick={() => onSelect("runs")} className="flex items-center gap-1 text-xs text-brand-400 transition hover:opacity-80">
|
{hasRuns && (
|
||||||
|
<button onClick={() => onSelect("runs")} className="flex items-center gap-1 text-xs text-slate-400 transition hover:text-slate-200">
|
||||||
全部 <ArrowRight className="h-3 w-3" />
|
全部 <ArrowRight className="h-3 w-3" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
|
||||||
{ov && ov.recent_runs.length > 0 ? (
|
|
||||||
ov.recent_runs.map((r) => <RunRow key={r.task_id} r={r} />)
|
|
||||||
) : (
|
|
||||||
<div className="py-8 text-center text-xs text-slate-600">暂无运行记录</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-lg border border-line bg-ink-900 p-4">
|
{hasRuns ? (
|
||||||
<div className="mb-3 text-sm font-medium text-slate-300">7 日任务量</div>
|
<div className="mt-1">{ov!.recent_runs.map((r) => <RunRow key={r.task_id} r={r} />)}</div>
|
||||||
<div className="flex h-24 items-end gap-2">
|
) : (
|
||||||
|
<div className="flex flex-col items-center gap-3 py-10 text-center">
|
||||||
|
<div className="text-sm text-slate-400">还没有运行记录</div>
|
||||||
|
<div className="max-w-xs text-xs leading-relaxed text-slate-600">搭一张编排、或上传资料建个知识库,任务跑起来后会在这里出现。</div>
|
||||||
|
<Button size="sm" icon={Plus} onClick={() => onSelect("studio")}>从新建编排开始</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="rounded-xl border border-line bg-ink-900 p-4">
|
||||||
|
<div className="mb-3 text-sm font-medium text-slate-200">近 7 日任务量</div>
|
||||||
|
<div className="flex h-28 items-end gap-2">
|
||||||
{(ov?.task_trend ?? []).map((d, i) => (
|
{(ov?.task_trend ?? []).map((d, i) => (
|
||||||
<div key={i} className="flex flex-1 flex-col items-center gap-1.5">
|
<div key={i} className="flex flex-1 flex-col items-center gap-1.5">
|
||||||
<div className="w-full rounded-t bg-brand/70 transition-all" style={{ height: `${Math.max((d.count / maxTask) * 100, 4)}%` }} title={`${d.count}`} />
|
<div className="w-full rounded-t bg-brand/60 transition-all hover:bg-brand" style={{ height: `${Math.max((d.count / maxTask) * 100, 3)}%` }} title={`${d.key}:${d.count} 次`} />
|
||||||
<span className="text-[10px] text-slate-600">{d.key.slice(-2)}</span>
|
<span className="text-[10px] tabular-nums text-slate-600">{d.key.slice(-2)}</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
{!ov && <div className="flex-1 text-center text-xs text-slate-600">加载中…</div>}
|
{!ov && <div className="flex-1 self-center text-center text-xs text-slate-600">加载中…</div>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 服务健康灯带 */}
|
{/* 快速开始 */}
|
||||||
<div className="mt-4 flex flex-wrap items-center gap-x-5 gap-y-2 rounded-lg border border-line bg-ink-850 px-4 py-2.5">
|
<div className="mt-8">
|
||||||
<span className="text-xs text-slate-500">服务</span>
|
<div className="mb-3 text-xs font-medium uppercase tracking-widest text-slate-600">快速开始</div>
|
||||||
{Object.entries(SERVICE_LABEL).map(([k, label]) => (
|
<div className="grid grid-cols-1 gap-3 md:grid-cols-2 lg:grid-cols-4">
|
||||||
<span key={k} className="flex items-center gap-1.5 text-xs text-slate-400">
|
|
||||||
<span className={cn("h-1.5 w-1.5 rounded-full", ov?.services[k] ? "bg-success" : "bg-ink-600")} />
|
|
||||||
{label}
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 能力入口 */}
|
|
||||||
<div className="mt-6 grid grid-cols-1 gap-3 md:grid-cols-2 lg:grid-cols-4">
|
|
||||||
{CAPS.map((c) => {
|
{CAPS.map((c) => {
|
||||||
const Icon = c.icon;
|
const Icon = c.icon;
|
||||||
return (
|
return (
|
||||||
<button key={c.to} onClick={() => onSelect(c.to)}
|
<button key={c.to} onClick={() => onSelect(c.to)}
|
||||||
className="group rounded-lg border border-line bg-ink-850 p-4 text-left transition hover:border-brand/50 hover:bg-ink-800">
|
className="group flex flex-col rounded-xl border border-line bg-ink-850 p-4 text-left transition-colors hover:border-brand/40 hover:bg-ink-800">
|
||||||
<Icon className={cn("h-5 w-5", c.color)} strokeWidth={1.8} />
|
<div className="flex items-center justify-between">
|
||||||
<div className="mt-2.5 text-sm font-medium text-slate-100">{c.title}</div>
|
<Icon className="h-5 w-5 text-slate-300 transition-colors group-hover:text-brand-400" strokeWidth={1.8} />
|
||||||
|
<ArrowUpRight className="h-4 w-4 text-slate-700 transition-colors group-hover:text-slate-400" />
|
||||||
|
</div>
|
||||||
|
<div className="mt-3 text-sm font-medium text-slate-100">{c.title}</div>
|
||||||
<div className="mt-1 text-xs leading-relaxed text-slate-500">{c.desc}</div>
|
<div className="mt-1 text-xs leading-relaxed text-slate-500">{c.desc}</div>
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* 快捷动作 */}
|
{/* 次要动作 */}
|
||||||
<div className="mt-4 flex flex-wrap gap-2">
|
<div className="mt-4 flex flex-wrap gap-2">
|
||||||
<Button variant="primary" size="sm" icon={Plus} onClick={() => onSelect("studio")}>新建编排</Button>
|
|
||||||
<Button size="sm" icon={Upload} onClick={() => onSelect("kb")}>入库知识</Button>
|
<Button size="sm" icon={Upload} onClick={() => onSelect("kb")}>入库知识</Button>
|
||||||
<Button size="sm" icon={FileText} onClick={() => onSelect("report")}>生成报告</Button>
|
<Button size="sm" icon={FileText} onClick={() => onSelect("report")}>生成报告</Button>
|
||||||
<Button size="sm" icon={Bookmark} onClick={() => onSelect("memory")}>管理记忆</Button>
|
<Button size="sm" icon={Bookmark} onClick={() => onSelect("memory")}>管理记忆</Button>
|
||||||
|
|||||||
Reference in New Issue
Block a user