From 266631d2fdfd5f9509eb18f35d1d63568944f815 Mon Sep 17 00:00:00 2001 From: ThearchyHelios Date: Thu, 17 Sep 2026 14:26:28 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AF=B9=E8=AF=9D=E5=BC=8F=E5=85=A5=E5=8F=A3?= =?UTF-8?q?=EF=BC=9A=E8=AF=B4=E9=9C=80=E6=B1=82=EF=BC=8C=E4=B8=8D=E7=94=A8?= =?UTF-8?q?=E7=A2=B0=E7=94=BB=E5=B8=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 阶段③。画布是给编排的人用的——15 种节点、每种十几个配置项,那是把控制权 交到手里的代价。但多数时候人要的只是一个答案:"MES 不合格数据里记录最多的 工厂是哪个",不关心它是 input→agent→output 还是别的什么。 这一页把图藏起来:你问,它自己接数据源、建流程、跑完、给结论。图仍然真实 存在(可以展开看"它是怎么做的",也能一键放到画布继续改),只是不再是必经之路。 默认路由也改到这里,画布退到第二项。 ## 状态分开放 新建 store/chat.ts 而不是复用 studio store:画布那套状态(选中节点、脏标记、 校验问题)在这里一个都用不上,混进去只会让两边互相牵制——对话页跑一次图就把 人家画布上没保存的东西冲掉,那是灾难。 ## 事件翻译成人话 Copilot 的操作流和运行事件都不直接暴露。add_node → "正在搭建流程…(3 步)", tool.start(db_query) → "查询数据",tool.end 里的 row_count 追加成"· 246 行"。 SQL 原文折叠在步骤里,想看能点开——不给看和硬塞给人看都不对。 等待期间显示模型的思考(复用上一轮加的 thinking 事件),这是 40 秒建图过程里 唯一能看的东西。 ## 人工介入就地处理 停在审批时直接在对话流里渲染 ApprovalCard(上一轮导出的),处理完自动重新 接上事件流看后续,不用切去别的页面再切回来。 ## 起始建议用真实表名 空状态不写死示例,而是读真实数据源的表名造建议——用户一眼看到的是自己的数据。 没有数据源时直接指路"设置 → 数据源",而不是给一个点了没反应的输入框。 ## 实测 按 store 的同样调用顺序跑完整链路:建图 44 秒(其中 39 秒是模型在想, 期间有心跳和 plan 摘要顶着)→ 3 步图 → agent 先查表结构再聚合 → "记录最多的工厂是上海头枕(工厂编码 1051),共 190470 条记录。" --- frontend/src/App.tsx | 8 +- frontend/src/api/client.ts | 13 ++ frontend/src/pages/ChatPage.tsx | 335 ++++++++++++++++++++++++++++++++ frontend/src/store/chat.ts | 283 +++++++++++++++++++++++++++ 4 files changed, 637 insertions(+), 2 deletions(-) create mode 100644 frontend/src/pages/ChatPage.tsx create mode 100644 frontend/src/store/chat.ts diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 52962535..7e4ec260 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,9 +1,10 @@ import { useEffect } from 'react' import { NavLink, Navigate, Route, Routes } from 'react-router-dom' -import { BookOpen, FlaskConical, History, Settings, Wrench } from 'lucide-react' +import { BookOpen, FlaskConical, History, MessageSquare, Settings, Wrench } from 'lucide-react' import clsx from 'clsx' import { api } from './api/client' import { useCatalog } from './store/catalog' +import { ChatPage } from './pages/ChatPage' import { StudioPage } from './pages/StudioPage' import { RunsPage } from './pages/RunsPage' import { ToolsPage } from './pages/ToolsPage' @@ -12,6 +13,7 @@ import { SettingsPage, applyTheme } from './pages/SettingsPage' import { Spinner } from './components/ui' const NAV = [ + { to: '/chat', label: '问数据', icon: MessageSquare }, { to: '/studio', label: '编排', icon: FlaskConical }, { to: '/runs', label: '运行', icon: History }, { to: '/tools', label: '工具', icon: Wrench }, @@ -73,7 +75,9 @@ export default function App() {
- } /> + {/* 默认落到对话页:多数人要的是答案,画布留给要自己编排的人 */} + } /> + } /> } /> } /> } /> diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index f64a5117..e5b74929 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -77,6 +77,19 @@ export const api = { }, // ---- 运行 ---- + datasources: { + list: () => get('/datasources'), + kinds: () => get('/datasources/kinds'), + create: (body: any) => post('/datasources', body), + update: (id: string, body: any) => patch(`/datasources/${id}`, body), + remove: (id: string) => del(`/datasources/${id}`), + test: (id: string) => post(`/datasources/${id}/test`, {}), + introspect: (id: string, schema?: string) => + post(`/datasources/${id}/introspect${schema ? `?schema=${encodeURIComponent(schema)}` : ''}`, {}), + schema: (id: string, table?: string) => + get(`/datasources/${id}/schema${table ? `?table=${encodeURIComponent(table)}` : ''}`), + }, + runs: { list: (params?: { workflow_id?: string; status?: string; limit?: number }) => { const q = new URLSearchParams( diff --git a/frontend/src/pages/ChatPage.tsx b/frontend/src/pages/ChatPage.tsx new file mode 100644 index 00000000..342cfc14 --- /dev/null +++ b/frontend/src/pages/ChatPage.tsx @@ -0,0 +1,335 @@ +import { useEffect, useMemo, useRef, useState } from 'react' +import { + ChevronRight, Database, MessageSquare, Send, Square, Table2, Trash2, Wrench, XCircle, +} from 'lucide-react' +import clsx from 'clsx' +import { api } from '../api/client' +import { ApprovalCard } from '../run/RunPanel' +import { useCatalog } from '../store/catalog' +import { useChat, type ChatTurn } from '../store/chat' +import { useStudio, toFlow } from '../store/studio' +import { Empty, Spinner, useToast } from '../components/ui' + +/** + * 对话式入口。 + * + * 画布是给编排的人用的:15 种节点、每种十几个配置项,那是把控制权交到手里的 + * 代价。但大多数时候人要的只是一个答案——"上季度华东销量怎么样",不关心它 + * 是 input→agent→output 还是别的什么。 + * + * 所以这一页把图藏起来:你问,它自己接数据源、建流程、跑完、给结论。图仍然 + * 真实存在(可以展开看,也能拿到画布里继续改),只是不再是必经之路。 + */ +export function ChatPage() { + const { turns, busy, ask, stop, clear } = useChat() + const [draft, setDraft] = useState('') + const bottomRef = useRef(null) + const sources = useDataSources() + + useEffect(() => { + bottomRef.current?.scrollIntoView({ behavior: 'smooth' }) + }, [turns.length, turns[turns.length - 1]?.phase]) + + const send = () => { + const q = draft.trim() + if (!q || busy) return + ask(q) + setDraft('') + } + + return ( +
+
+ + 问数据 + + 说需求,它自己接数据源、建流程、跑完给结论 + + + {!!turns.length && ( + + )} +
+ +
+ {!turns.length ? ( + { setDraft(q); }} /> + ) : ( +
+ {turns.map((turn) => )} +
+ )} +
+
+ +
+
+