From 5135188c70503004e7f68a62907477fe4ad17af2 Mon Sep 17 00:00:00 2001 From: qixinbo Date: Sun, 15 Mar 2026 21:57:38 +0800 Subject: [PATCH] reorg chat --- backend/main.py | 9 - frontend/src/components/ChatInterface.tsx | 306 +++++++++------------- 2 files changed, 121 insertions(+), 194 deletions(-) diff --git a/backend/main.py b/backend/main.py index cf6b16f..3afe38b 100644 --- a/backend/main.py +++ b/backend/main.py @@ -299,12 +299,3 @@ def update_session_context_file(session_id: str, payload: SessionFileContextUpda session.updated_at = datetime.now() nanobot_service.agent.sessions.save(session) return {"status": "success", "metadata": session.metadata} - -@app.post("/api/v1/agent/nl2sql", response_model=NL2SQLResponse) -async def run_nl2sql(request: NL2SQLRequest): - result = await process_nl2sql(request) - if request.session_id: - text = _build_sql_chart_text(result) - viz_payload = _build_sql_chart_viz(result) - _persist_session_turn(request.session_id, request.query, text, {"viz": viz_payload}) - return result diff --git a/frontend/src/components/ChatInterface.tsx b/frontend/src/components/ChatInterface.tsx index f4cab43..5845ca7 100644 --- a/frontend/src/components/ChatInterface.tsx +++ b/frontend/src/components/ChatInterface.tsx @@ -2,7 +2,7 @@ import { useState, useRef, useEffect } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { ScrollArea } from "@/components/ui/scroll-area"; -import { User, Loader2, Sparkles, Search, ArrowUp, ChevronDown, Table, Paperclip, Check, X, File as FileIcon, Square } from "lucide-react"; +import { User, Loader2, Sparkles, ArrowUp, ChevronDown, Paperclip, Check, X, File as FileIcon, Square } from "lucide-react"; import { api } from "@/lib/api"; import { type ChartSpec } from "@/store/visualizationStore"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; @@ -62,7 +62,6 @@ interface SessionData { export function ChatInterface() { const [messages, setMessages] = useState([]); const [input, setInput] = useState(""); - const [selectedCapability, setSelectedCapability] = useState("智能问答"); const [selectedDataSource, setSelectedDataSource] = useState("postgres-main"); const [isLoading, setIsLoading] = useState(false); const scrollRef = useRef(null); @@ -172,11 +171,6 @@ export function ChatInterface() { const currentModel = models.find(m => m.id === selectedModelId); - const capabilities = [ - { icon: Sparkles, label: "智能问答", color: "text-purple-500", bg: "bg-purple-50" }, - { icon: Table, label: "表格问答", color: "text-orange-500", bg: "bg-orange-50" }, - { icon: Search, label: "深度问数", color: "text-blue-500", bg: "bg-blue-50" }, - ]; const chartIntentPattern = /(图表|可视化|画图|作图|柱状图|折线图|饼图|趋势|分布|chart|plot|visuali[sz]e)/i; const buildMessageViz = (payload: { @@ -281,176 +275,132 @@ export function ChatInterface() { setIsLoading(true); try { - if (selectedCapability === "智能问答") { - const assistantId = (Date.now() + 1).toString(); - setMessages(prev => [...prev, { - id: assistantId, - role: "assistant", - content: "", - awaitingFirstToken: true - }]); + const assistantId = (Date.now() + 1).toString(); + setMessages(prev => [...prev, { + id: assistantId, + role: "assistant", + content: "", + awaitingFirstToken: true + }]); - const token = localStorage.getItem("token"); - const effectiveModelId = selectedModelId || currentModel?.id || ""; - const selectedSource = selectedDataSource.split('-')[0]; - const useUploadSource = Boolean( - currentAttachedFile?.url?.startsWith("local://") || - (selectedSource === "upload" && activeDataFile?.url?.startsWith("local://")) - ); - const source = useUploadSource ? "upload" : selectedSource; - const fileUrl = useUploadSource ? (currentAttachedFile?.url || activeDataFile?.url) : undefined; - const preferSqlChart = chartIntentPattern.test(messagePayload); - const response = await fetch("/nanobot/chat/stream", { - method: "POST", - headers: { - "Content-Type": "application/json", - ...(token ? { Authorization: `Bearer ${token}` } : {}), - }, - body: JSON.stringify({ - message: messagePayload, - session_id: activeSessionKey, - model_id: effectiveModelId, - source, - prefer_sql_chart: preferSqlChart, - file_url: fileUrl, - }), - signal: controller.signal, - }); - - if (!response.ok || !response.body) { - const err = await response.json().catch(() => ({})); - throw new Error(err.detail || "流式响应失败"); - } - - const reader = response.body.getReader(); - const decoder = new TextDecoder("utf-8"); - let buffer = ""; - let streamedText = ""; - let streamedViz: MessageViz | null = null; - - while (true) { - const { done, value } = await reader.read(); - if (done) break; - buffer += decoder.decode(value, { stream: true }); - const events = buffer.split("\n\n"); - buffer = events.pop() || ""; - - for (const eventBlock of events) { - const line = eventBlock - .split("\n") - .find((item) => item.startsWith("data:")); - if (!line) continue; - const payloadText = line.slice(5).trim(); - if (!payloadText) continue; - const payload = JSON.parse(payloadText) as { - type: string; - content?: string; - sql?: string; - result?: unknown; - error?: string; - chart?: { chart_spec?: ChartSpec | null; reasoning?: string; can_visualize?: boolean; chart_type?: string } | null; - }; - - if (payload.type === "delta" && payload.content) { - streamedText = `${streamedText}${payload.content}`; - setMessages((prev) => - prev.map((msg) => - msg.id === assistantId ? { ...msg, content: streamedText, awaitingFirstToken: false } : msg - ) - ); - } - - if (payload.type === "final" && payload.content) { - streamedText = payload.content; - setMessages((prev) => - prev.map((msg) => - msg.id === assistantId ? { ...msg, content: payload.content || "", awaitingFirstToken: false, viz: streamedViz ?? msg.viz } : msg - ) - ); - } - - if (payload.type === "error") { - throw new Error(payload.content || "流式响应错误"); - } - - if (payload.type === "viz") { - streamedViz = buildMessageViz(payload); - setMessages((prev) => - prev.map((msg) => - msg.id === assistantId ? { ...msg, viz: streamedViz || undefined } : msg - ) - ); - } - } - } - - if (!streamedText) { - const fallback = await api.post<{ - response: string; - viz?: { - sql?: string; - result?: unknown; - error?: string | null; - chart?: { chart_spec?: ChartSpec | null; reasoning?: string; can_visualize?: boolean; chart_type?: string } | null; - }; - }>("/nanobot/chat", { + const token = localStorage.getItem("token"); + const effectiveModelId = selectedModelId || currentModel?.id || ""; + const selectedSource = selectedDataSource.split('-')[0]; + const useUploadSource = Boolean( + currentAttachedFile?.url?.startsWith("local://") || + (selectedSource === "upload" && activeDataFile?.url?.startsWith("local://")) + ); + const source = useUploadSource ? "upload" : selectedSource; + const fileUrl = useUploadSource ? (currentAttachedFile?.url || activeDataFile?.url) : undefined; + const preferSqlChart = chartIntentPattern.test(messagePayload); + const response = await fetch("/nanobot/chat/stream", { + method: "POST", + headers: { + "Content-Type": "application/json", + ...(token ? { Authorization: `Bearer ${token}` } : {}), + }, + body: JSON.stringify({ message: messagePayload, session_id: activeSessionKey, model_id: effectiveModelId, - source, - prefer_sql_chart: preferSqlChart, - file_url: fileUrl, - }, { signal: controller.signal }); - const fallbackViz = fallback.viz ? buildMessageViz(fallback.viz) : undefined; - setMessages((prev) => - prev.map((msg) => - msg.id === assistantId ? { ...msg, content: fallback.response || "暂无回复", awaitingFirstToken: false, viz: fallbackViz } : msg - ) - ); - } - } else { - // Fallback to existing NL2SQL or other skills (e.g. for "表格问答" or "深度问数") - const selectedSource = selectedDataSource.split('-')[0]; - const useUploadSource = Boolean( - currentAttachedFile?.url?.startsWith("local://") || - (selectedSource === "upload" && activeDataFile?.url?.startsWith("local://")) - ); - const source = useUploadSource ? "upload" : selectedSource; - const fileUrl = useUploadSource ? (currentAttachedFile?.url || activeDataFile?.url) : undefined; - const response = await api.post<{ - sql?: string, - result?: unknown, - error?: string, - chart?: { chart_spec?: ChartSpec | null, reasoning?: string, can_visualize?: boolean, chart_type?: string } - }>('/api/v1/agent/nl2sql', { - query: messagePayload, - source: source, - file_url: fileUrl, - session_id: activeSessionKey, - model_id: selectedModelId - }, { signal: controller.signal }); + source, + prefer_sql_chart: preferSqlChart, + file_url: fileUrl, + }), + signal: controller.signal, + }); - if (response.error) { - setMessages(prev => [...prev, { - id: (Date.now() + 1).toString(), - role: 'assistant', - content: `Error: ${response.error}` - }]); - } else { - const canVisualize = Boolean(response.chart?.can_visualize); - const viz = buildMessageViz({ - sql: response.sql, - result: response.result, - chart: response.chart, - }); - setMessages(prev => [...prev, { - id: (Date.now() + 1).toString(), - role: 'assistant', - content: `已为你生成 SQL 并查询到 ${viz.rows.length} 行数据。${canVisualize ? '图表已附在回答下方。' : '本次结果不适合图表展示。'}${response.chart?.reasoning ? `\n\n可视化说明:${response.chart.reasoning}` : ''}`, - viz, - }]); + if (!response.ok || !response.body) { + const err = await response.json().catch(() => ({})); + throw new Error(err.detail || "流式响应失败"); + } + + const reader = response.body.getReader(); + const decoder = new TextDecoder("utf-8"); + let buffer = ""; + let streamedText = ""; + let streamedViz: MessageViz | null = null; + + while (true) { + const { done, value } = await reader.read(); + if (done) break; + buffer += decoder.decode(value, { stream: true }); + const events = buffer.split("\n\n"); + buffer = events.pop() || ""; + + for (const eventBlock of events) { + const line = eventBlock + .split("\n") + .find((item) => item.startsWith("data:")); + if (!line) continue; + const payloadText = line.slice(5).trim(); + if (!payloadText) continue; + const payload = JSON.parse(payloadText) as { + type: string; + content?: string; + sql?: string; + result?: unknown; + error?: string; + chart?: { chart_spec?: ChartSpec | null; reasoning?: string; can_visualize?: boolean; chart_type?: string } | null; + }; + + if (payload.type === "delta" && payload.content) { + streamedText = `${streamedText}${payload.content}`; + setMessages((prev) => + prev.map((msg) => + msg.id === assistantId ? { ...msg, content: streamedText, awaitingFirstToken: false } : msg + ) + ); + } + + if (payload.type === "final" && payload.content) { + streamedText = payload.content; + setMessages((prev) => + prev.map((msg) => + msg.id === assistantId ? { ...msg, content: payload.content || "", awaitingFirstToken: false, viz: streamedViz ?? msg.viz } : msg + ) + ); + } + + if (payload.type === "error") { + throw new Error(payload.content || "流式响应错误"); + } + + if (payload.type === "viz") { + streamedViz = buildMessageViz(payload); + setMessages((prev) => + prev.map((msg) => + msg.id === assistantId ? { ...msg, viz: streamedViz || undefined } : msg + ) + ); + } } - } + } + + if (!streamedText) { + const fallback = await api.post<{ + response: string; + viz?: { + sql?: string; + result?: unknown; + error?: string | null; + chart?: { chart_spec?: ChartSpec | null; reasoning?: string; can_visualize?: boolean; chart_type?: string } | null; + }; + }>("/nanobot/chat", { + message: messagePayload, + session_id: activeSessionKey, + model_id: effectiveModelId, + source, + prefer_sql_chart: preferSqlChart, + file_url: fileUrl, + }, { signal: controller.signal }); + const fallbackViz = fallback.viz ? buildMessageViz(fallback.viz) : undefined; + setMessages((prev) => + prev.map((msg) => + msg.id === assistantId ? { ...msg, content: fallback.response || "暂无回复", awaitingFirstToken: false, viz: fallbackViz } : msg + ) + ); + } } catch (error: any) { if (error?.name === "AbortError" || String(error?.message || "").toLowerCase().includes("aborted")) { setMessages((prev) => @@ -598,20 +548,6 @@ export function ChatInterface() {
- {capabilities.map((cap) => ( - - ))}