## Backend
- Add workspace_card SSE event protocol: {id, name, props, merge}
- Add _extract_llm_text / _maybe_emit_workspace_card helpers in chat.py
- Refactor all tools to dual-output format: {llm_text, ui: {name, props}}
- kb_search → KnowledgeResultCard
- ticket_list/detail → TicketSummaryCard / TicketDetailCard
- web_search → SearchResultCard
- generate_document → DocumentResultCard
- sandbox_run → SandboxResultCard
- Update SYSTEM_PROMPT: instruct LLM not to repeat tool data (UI shows it)
## Frontend
- Three-column layout: sidebar + chat + Agent Workspace (360px right panel)
- WorkspaceSession state model with ActivityNode + WorkspaceCard
- New components/workspace/: AgentWorkspace, ActivityTimeline, WorkspaceCardRenderer
- 6 card components: Knowledge/Ticket/Search/Document/Sandbox/ErrorCard
- GeminiChat: workspace state management, SSE routing for workspace_card events
- GeminiMessage: replace TracePanel with lightweight activity summary line
- lib/api.ts: add WorkspaceSession/ActivityNode/WorkspaceCard types
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
44 lines
1.7 KiB
TypeScript
44 lines
1.7 KiB
TypeScript
"use client";
|
|
import { Terminal, CheckCircle, XCircle } from "lucide-react";
|
|
|
|
interface SandboxResultCardProps {
|
|
language: string;
|
|
exit_code: number;
|
|
stdout: string;
|
|
has_more: boolean;
|
|
duration_ms?: number;
|
|
}
|
|
|
|
export function SandboxResultCard({ language, exit_code, stdout, has_more, duration_ms }: SandboxResultCardProps) {
|
|
const success = exit_code === 0;
|
|
return (
|
|
<div className="rounded-xl border border-[var(--gem-border)] bg-[var(--gem-surface)] p-4">
|
|
<div className="flex items-center gap-2 mb-3">
|
|
<Terminal size={15} className="text-[var(--gem-blue,#4285f4)]" />
|
|
<span className="text-sm font-medium text-[var(--gem-text)]">代码执行</span>
|
|
<span className="ml-auto flex items-center gap-1 text-xs">
|
|
{success
|
|
? <CheckCircle size={13} className="text-green-400" />
|
|
: <XCircle size={13} className="text-red-400" />
|
|
}
|
|
<span className={success ? "text-green-400" : "text-red-400"}>
|
|
{success ? "成功" : `退出码 ${exit_code}`}
|
|
</span>
|
|
</span>
|
|
</div>
|
|
<div className="flex items-center gap-2 mb-2">
|
|
<span className="text-[10px] text-[var(--gem-text-muted)] bg-[var(--gem-surface-2)] px-2 py-0.5 rounded">{language}</span>
|
|
{duration_ms != null && (
|
|
<span className="text-[10px] text-[var(--gem-text-muted)]">{duration_ms}ms</span>
|
|
)}
|
|
</div>
|
|
{stdout && (
|
|
<pre className="text-xs text-[var(--gem-text-secondary)] bg-[var(--gem-surface-2)] rounded-lg p-3 overflow-x-auto whitespace-pre-wrap max-h-48 overflow-y-auto font-mono">
|
|
{stdout}
|
|
{has_more && "\n… (输出已截断)"}
|
|
</pre>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|