@@ -55,6 +61,7 @@ export default function TicketSummary({
工单列表
+
共 {total} 条
@@ -127,6 +134,13 @@ export default function TicketSummary({
))
)}
+
);
}
diff --git a/langgraph/src/components/ActionBar.tsx b/langgraph/src/components/ActionBar.tsx
new file mode 100644
index 0000000..72776f5
--- /dev/null
+++ b/langgraph/src/components/ActionBar.tsx
@@ -0,0 +1,33 @@
+import React from "react";
+
+interface ActionBarProps {
+ sourceType?: string;
+ context?: string;
+ suggestedActions?: string[];
+}
+
+export function ActionBar({ context = "", suggestedActions = [] }: ActionBarProps) {
+ const dispatch = (text: string) => {
+ window.dispatchEvent(new CustomEvent("soc:prefill-input", { detail: { text } }));
+ };
+
+ return (
+
+ dispatch(`继续追问:${context}`)}
+ className="inline-flex items-center px-2.5 py-1 text-xs rounded-full border border-border hover:bg-primary/10 hover:border-primary/40 transition-colors text-muted-foreground hover:text-primary"
+ >
+ 继续追问
+
+ {suggestedActions.map((action) => (
+ dispatch(`${action},基于以下内容:${context}`)}
+ className="inline-flex items-center px-2.5 py-1 text-xs rounded-full border border-border hover:bg-primary/10 hover:border-primary/40 transition-colors text-muted-foreground hover:text-primary"
+ >
+ {action}
+
+ ))}
+
+ );
+}
diff --git a/langgraph/src/components/MessageBubble.tsx b/langgraph/src/components/MessageBubble.tsx
index 829edc8..210b7ec 100644
--- a/langgraph/src/components/MessageBubble.tsx
+++ b/langgraph/src/components/MessageBubble.tsx
@@ -75,8 +75,31 @@ function CodeBlock({
);
}
+function extractSummary(text: string): string {
+ const sentences = text.split(/(?<=[.。!!??])\s+/);
+ return sentences.slice(0, 2).join(" ").trim();
+}
+
export default function MessageBubble({ content }: MessageBubbleProps) {
+ const [summaryExpanded, setSummaryExpanded] = useState(false);
+ const isLong = content.length > 800;
+ const summary = isLong ? extractSummary(content) : null;
+
return (
+ <>
+ {isLong && summary && !summaryExpanded && (
+
+
{summary}…
+
setSummaryExpanded(true)}
+ className="mt-1 text-xs text-primary hover:underline"
+ >
+ 展开全文
+
+
+ )}
+ {(!isLong || summaryExpanded) && (
{content}
+ )}
+ >
);
}
diff --git a/langgraph/src/components/SourceBadge.tsx b/langgraph/src/components/SourceBadge.tsx
new file mode 100644
index 0000000..93e3cfd
--- /dev/null
+++ b/langgraph/src/components/SourceBadge.tsx
@@ -0,0 +1,27 @@
+import React from "react";
+
+const SOURCE_CONFIG: Record
= {
+ internal_kb: { label: "内部知识库", className: "bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400" },
+ ticket_system: { label: "工单系统", className: "bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-400" },
+ external_web: { label: "外部搜索", className: "bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400" },
+ code_execution: { label: "代码执行", className: "bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-400" },
+ generated_doc: { label: "生成文档", className: "bg-cyan-100 text-cyan-700 dark:bg-cyan-900/30 dark:text-cyan-400" },
+ inferred: { label: "模型推断", className: "bg-gray-100 text-gray-600 dark:bg-gray-800 dark:text-gray-400" },
+};
+
+const CONFIDENCE_ICON: Record = { high: "●", medium: "○", low: "!" };
+
+interface SourceBadgeProps {
+ sourceType?: string;
+ confidence?: "high" | "medium" | "low";
+}
+
+export function SourceBadge({ sourceType = "inferred", confidence = "high" }: SourceBadgeProps) {
+ const config = SOURCE_CONFIG[sourceType] ?? SOURCE_CONFIG.inferred;
+ return (
+
+ {CONFIDENCE_ICON[confidence]}
+ {config.label}
+
+ );
+}
diff --git a/langgraph/src/components/ToolCallStatus.tsx b/langgraph/src/components/ToolCallStatus.tsx
index 433a0f1..cb5c14a 100644
--- a/langgraph/src/components/ToolCallStatus.tsx
+++ b/langgraph/src/components/ToolCallStatus.tsx
@@ -3,19 +3,36 @@ import { useState } from "react";
import { LoadExternalComponent } from "@langchain/langgraph-sdk/react-ui";
const TOOL_NAME_MAP: Record = {
- kb_search: "搜索知识库",
- ticket_list: "查询工单列表",
- ticket_detail: "查询工单详情",
- web_search: "搜索网络",
- google_search: "搜索网络",
+ kb_search: "知识库检索",
+ ticket_list: "工单查询",
+ ticket_detail: "工单详情",
+ web_search: "网络搜索",
+ google_search: "网络搜索",
web_search_deep: "深度搜索",
- web_read: "阅读网页",
- code_execute: "执行代码",
- code_install: "安装依赖",
- doc_create: "创建文档",
- doc_edit: "编辑文档",
- doc_translate: "翻译文档",
- sandbox_run: "运行沙盒",
+ web_read: "网页阅读",
+ code_execute: "代码执行",
+ code_install: "依赖安装",
+ doc_create: "文档创建",
+ doc_edit: "文档编辑",
+ doc_translate: "文档翻译",
+ sandbox_run: "代码沙盒",
+};
+
+// 按工具类型显示 loading 文案
+const TOOL_LOADING_MAP: Record = {
+ kb_search: "正在检索知识库...",
+ ticket_list: "正在查询工单...",
+ ticket_detail: "正在获取工单详情...",
+ web_search: "正在搜索网络...",
+ google_search: "正在搜索网络...",
+ web_search_deep: "正在深度搜索...",
+ web_read: "正在阅读网页...",
+ code_execute: "正在执行代码...",
+ code_install: "正在安装依赖...",
+ sandbox_run: "正在运行沙盒...",
+ doc_create: "正在创建文档...",
+ doc_edit: "正在编辑文档...",
+ doc_translate: "正在翻译文档...",
};
interface ToolCall {
@@ -80,7 +97,7 @@ function ToolCallRow({
)}
{label}
- {isDone ? (canExpand ? (expanded ? "收起" : "查看结果") : "已完成") : "执行中..."}
+ {isDone ? (canExpand ? (expanded ? "收起" : "查看结果") : `已完成 · ${label}`) : (tc.name ? (TOOL_LOADING_MAP[tc.name] ?? `正在${label}...`) : "思考中...")}
{expanded && uiItem && stream && components && (
@@ -105,23 +122,31 @@ export default function ToolCallStatus({
}: ToolCallStatusProps) {
if (!toolCalls.length) return null;
+ const UI_NAME_MAP: Record
= {
+ kb_search: "knowledge-result",
+ ticket_list: "ticket-summary",
+ ticket_detail: "ticket-detail",
+ web_search: "search-result",
+ google_search: "search-result",
+ sandbox_run: "sandbox-result",
+ };
+
+ // Track how many times each ui component name has been matched, so multiple
+ // calls of the same tool type pick different UI cards in order.
+ const matchCounters: Record = {};
+
return (
{toolCalls.map((tc, i) => {
const isDone = !isLoading || (tc.id && completedToolIds?.has(tc.id));
- // Match UI item by tool name
- const uiItem = uiItems.find((ui) => {
- if (!tc.name) return false;
- const nameMap: Record = {
- kb_search: "knowledge-result",
- ticket_list: "ticket-summary",
- ticket_detail: "ticket-detail",
- web_search: "search-result",
- google_search: "search-result",
- sandbox_run: "sandbox-result",
- };
- return ui.name === nameMap[tc.name];
- });
+ let uiItem: UIMsgLocal | undefined;
+ if (tc.name && UI_NAME_MAP[tc.name]) {
+ const uiName = UI_NAME_MAP[tc.name];
+ const matchIdx = matchCounters[uiName] ?? 0;
+ matchCounters[uiName] = matchIdx + 1;
+ const candidates = uiItems.filter((ui) => ui.name === uiName);
+ uiItem = candidates[matchIdx];
+ }
return (
window.removeEventListener("open-canvas", handler);
}, []);
+ // Listen for prefill-input events from ActionBar
+ useEffect(() => {
+ const handler = (e: Event) => {
+ const ce = e as CustomEvent<{ text: string }>;
+ setInput(ce.detail.text);
+ setTimeout(() => textareaRef.current?.focus(), 50);
+ };
+ window.addEventListener("soc:prefill-input", handler);
+ return () => window.removeEventListener("soc:prefill-input", handler);
+ }, []);
+
// Load threads on mount
useEffect(() => {
client.threads