diff --git a/langgraph/src/agent-uis/enterprise/knowledge-result/index.tsx b/langgraph/src/agent-uis/enterprise/knowledge-result/index.tsx index 65b8506..2b1d597 100644 --- a/langgraph/src/agent-uis/enterprise/knowledge-result/index.tsx +++ b/langgraph/src/agent-uis/enterprise/knowledge-result/index.tsx @@ -1,15 +1,21 @@ import { BookOpen } from "lucide-react"; +import { ActionBar } from "@/components/ActionBar"; +import { SourceBadge } from "@/components/SourceBadge"; interface KnowledgeResultProps { query: string; total: number; results: Array<{ title: string; category: string; snippet: string }>; + sourceType?: string; + confidence?: "high" | "medium" | "low"; } export default function KnowledgeResult({ query, total, results, + sourceType, + confidence, }: KnowledgeResultProps) { return (
@@ -17,6 +23,7 @@ export default function KnowledgeResult({
知识库检索 + {total} 条结果 @@ -33,7 +40,7 @@ export default function KnowledgeResult({
    {results.length === 0 ? (
  • - 未找到相关结果 + 知识库中暂无相关内容
  • ) : ( results.map((item, idx) => ( @@ -53,6 +60,13 @@ export default function KnowledgeResult({ )) )}
+
+ +
); } diff --git a/langgraph/src/agent-uis/enterprise/sandbox-result/index.tsx b/langgraph/src/agent-uis/enterprise/sandbox-result/index.tsx index 6d90a96..d4e38fb 100644 --- a/langgraph/src/agent-uis/enterprise/sandbox-result/index.tsx +++ b/langgraph/src/agent-uis/enterprise/sandbox-result/index.tsx @@ -1,5 +1,7 @@ import { Terminal, CheckCircle, XCircle } from "lucide-react"; import { cn } from "@/lib/utils"; +import { ActionBar } from "@/components/ActionBar"; +import { SourceBadge } from "@/components/SourceBadge"; interface SandboxResultProps { language: string; @@ -7,6 +9,8 @@ interface SandboxResultProps { stdout: string; has_more: boolean; duration_ms?: number; + sourceType?: string; + confidence?: "high" | "medium" | "low"; } const LANGUAGE_LABEL: Record = { @@ -21,6 +25,8 @@ export default function SandboxResult({ stdout, has_more, duration_ms, + sourceType, + confidence, }: SandboxResultProps) { const success = exit_code === 0; @@ -30,6 +36,7 @@ export default function SandboxResult({
代码执行 + {/* Language badge */} @@ -82,6 +89,13 @@ export default function SandboxResult({ 输出已截断,显示前 2000 字符
)} +
+ +
); } diff --git a/langgraph/src/agent-uis/enterprise/search-result/index.tsx b/langgraph/src/agent-uis/enterprise/search-result/index.tsx index 5049de6..c13eb5c 100644 --- a/langgraph/src/agent-uis/enterprise/search-result/index.tsx +++ b/langgraph/src/agent-uis/enterprise/search-result/index.tsx @@ -1,9 +1,13 @@ import { Globe } from "lucide-react"; +import { ActionBar } from "@/components/ActionBar"; +import { SourceBadge } from "@/components/SourceBadge"; interface SearchResultProps { query: string; total: number; results: Array<{ title: string; url: string; snippet: string }>; + sourceType?: string; + confidence?: "high" | "medium" | "low"; } function getDomain(url: string): string { @@ -18,6 +22,8 @@ export default function SearchResult({ query, total, results, + sourceType, + confidence, }: SearchResultProps) { return (
@@ -25,6 +31,7 @@ export default function SearchResult({
网络搜索 + {total} 条结果 @@ -41,7 +48,7 @@ export default function SearchResult({
    {results.length === 0 ? (
  • - 未找到相关结果 + 未找到相关结果,试试换个关键词?
  • ) : ( results.map((item, idx) => ( @@ -72,6 +79,13 @@ export default function SearchResult({ )) )}
+
+ +
); } diff --git a/langgraph/src/agent-uis/enterprise/ticket-summary/index.tsx b/langgraph/src/agent-uis/enterprise/ticket-summary/index.tsx index 8440e5f..5e2193d 100644 --- a/langgraph/src/agent-uis/enterprise/ticket-summary/index.tsx +++ b/langgraph/src/agent-uis/enterprise/ticket-summary/index.tsx @@ -1,5 +1,7 @@ import { TicketIcon } from "lucide-react"; import { cn } from "@/lib/utils"; +import { ActionBar } from "@/components/ActionBar"; +import { SourceBadge } from "@/components/SourceBadge"; interface TicketSummaryProps { total: number; @@ -12,6 +14,8 @@ interface TicketSummaryProps { created: string; }>; stats: Record; + sourceType?: string; + confidence?: "high" | "medium" | "low"; } function priorityClass(priority: string): string { @@ -48,6 +52,8 @@ export default function TicketSummary({ total, tickets, stats, + sourceType, + confidence, }: TicketSummaryProps) { return (
@@ -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 ( +
+ + {suggestedActions.map((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}…

+ +
+ )} + {(!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