diff --git a/web/package.json b/web/package.json index 7d348ed..2fc76b6 100644 --- a/web/package.json +++ b/web/package.json @@ -25,7 +25,7 @@ "@astrojs/vue": "^5.1.3", "@kevisual/cache": "^0.0.5", "@kevisual/context": "^0.0.4", - "@kevisual/query": "^0.0.33", + "@kevisual/query": "^0.0.34", "@kevisual/query-login": "^0.0.7", "@kevisual/registry": "^0.0.1", "@kevisual/router": "^0.0.52", @@ -34,6 +34,7 @@ "@radix-ui/react-label": "^2.1.8", "@radix-ui/react-slot": "^1.2.4", "@tailwindcss/vite": "^4.1.18", + "@tanstack/react-table": "^8.21.3", "@uiw/react-md-editor": "^4.0.11", "antd": "^6.1.3", "astro": "^5.16.6", @@ -53,7 +54,7 @@ "react": "^19.2.3", "react-dom": "^19.2.3", "react-hook-form": "^7.69.0", - "react-resizable-panels": "^4.1.0", + "react-resizable-panels": "^4.2.0", "react-toastify": "^11.0.5", "tailwind-merge": "^3.4.0", "vue": "^3.5.26", @@ -63,10 +64,11 @@ "access": "public" }, "devDependencies": { - "@kevisual/api": "^0.0.14", + "@kevisual/api": "^0.0.16", "@kevisual/types": "^0.0.10", "@types/react": "^19.2.7", "@types/react-dom": "^19.2.3", + "baseline-browser-mapping": "^2.9.11", "dotenv": "^17.2.3", "tailwindcss": "^4.1.18", "tw-animate-css": "^1.4.0" diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index dffb1ba..a7cc38c 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -27,11 +27,11 @@ importers: specifier: ^0.0.4 version: 0.0.4 '@kevisual/query': - specifier: ^0.0.33 - version: 0.0.33 + specifier: ^0.0.34 + version: 0.0.34 '@kevisual/query-login': specifier: ^0.0.7 - version: 0.0.7(@kevisual/query@0.0.33) + version: 0.0.7(@kevisual/query@0.0.34) '@kevisual/registry': specifier: ^0.0.1 version: 0.0.1(typescript@5.9.3) @@ -53,6 +53,9 @@ importers: '@tailwindcss/vite': specifier: ^4.1.18 version: 4.1.18(vite@6.4.1(@types/node@24.7.2)(jiti@2.6.1)(lightningcss@1.30.2)) + '@tanstack/react-table': + specifier: ^8.21.3 + version: 8.21.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@uiw/react-md-editor': specifier: ^4.0.11 version: 4.0.11(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -111,8 +114,8 @@ importers: specifier: ^7.69.0 version: 7.69.0(react@19.2.3) react-resizable-panels: - specifier: ^4.1.0 - version: 4.1.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + specifier: ^4.2.0 + version: 4.2.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) react-toastify: specifier: ^11.0.5 version: 11.0.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -127,8 +130,8 @@ importers: version: 5.0.9(@types/react@19.2.7)(react@19.2.3) devDependencies: '@kevisual/api': - specifier: ^0.0.14 - version: 0.0.14 + specifier: ^0.0.16 + version: 0.0.16 '@kevisual/types': specifier: ^0.0.10 version: 0.0.10 @@ -138,6 +141,9 @@ importers: '@types/react-dom': specifier: ^19.2.3 version: 19.2.3(@types/react@19.2.7) + baseline-browser-mapping: + specifier: ^2.9.11 + version: 2.9.11 dotenv: specifier: ^17.2.3 version: 17.2.3 @@ -723,8 +729,8 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} - '@kevisual/api@0.0.14': - resolution: {integrity: sha512-GOs61Jvjxs+7PB8+iSPko9/RGeWENxltHueV75M6W0psRsnx/J+06I48/cO413FwCoqSOqpOoivdRgSENdHM9g==} + '@kevisual/api@0.0.16': + resolution: {integrity: sha512-JInnqWHjUxos1oWHe8dmwxWOMCRgv5nI/7HbSrzvHDQxHE6Egc3xA5iALUcRDdkNOnPz98ErZnLmSgHHJDOwYQ==} '@kevisual/cache@0.0.3': resolution: {integrity: sha512-BWEck69KYL96/ywjYVkML974RHjDJTj2ITQND1zFPR+hlBV1H1p55QZgSYRJCObg3EAV1S9Zic/fR2T4pfe8yg==} @@ -746,8 +752,8 @@ packages: peerDependencies: '@kevisual/query': ^0 - '@kevisual/query@0.0.33': - resolution: {integrity: sha512-3w74bcLpwV3z483eg8n0DgkftfjWC6iLONXBvfyjW6IZf6jMOuouFaM4Rk+uEsTgElU6XGMKseNTp6dlQdWYkg==} + '@kevisual/query@0.0.34': + resolution: {integrity: sha512-UHA0qEJYzU76pffUx0OhcOL5zKuxR/Kg269OHjrFm7+7RO85Qzv4ON1vUJDFp61hRuRVwiwOEKucQLHLE6UpMg==} '@kevisual/registry@0.0.1': resolution: {integrity: sha512-//OHu9m4JDrMjgP8o8dcjZd3D3IAUkRVlkTSviouZEH7r5m7mccA3Hvzw0XJ/lelx6exC6LWsyv6c4uV0Dp+gw==} @@ -1667,6 +1673,17 @@ packages: peerDependencies: vite: ^5.2.0 || ^6 || ^7 + '@tanstack/react-table@8.21.3': + resolution: {integrity: sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww==} + engines: {node: '>=12'} + peerDependencies: + react: '>=16.8' + react-dom: '>=16.8' + + '@tanstack/table-core@8.21.3': + resolution: {integrity: sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==} + engines: {node: '>=12'} + '@types/babel__core@7.20.5': resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} @@ -1925,8 +1942,8 @@ packages: base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} - baseline-browser-mapping@2.8.16: - resolution: {integrity: sha512-OMu3BGQ4E7P1ErFsIPpbJh0qvDudM/UuJeHgkAvfWe+0HFJCXh+t/l8L6fVLR55RI/UbKrVLnAXZSVwd9ysWYw==} + baseline-browser-mapping@2.9.11: + resolution: {integrity: sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ==} hasBin: true bcp-47-match@2.0.3: @@ -3101,8 +3118,8 @@ packages: '@types/react': optional: true - react-resizable-panels@4.1.0: - resolution: {integrity: sha512-8ZpOwdKQz6bCs2LGnfS6HuBITxkOLelSMzBX4DrWsgHaU3ukTPxmBNAeK8Bsp3LAEdtXeG6ll6UPN7OJNua4sw==} + react-resizable-panels@4.2.0: + resolution: {integrity: sha512-X/WbnyT/bgx09KEGvtJvaTr3axRrcBGcJdELIoGXZipCxc2hPwFsH/pfpVgwNVq5LpQxF/E5pPXGTQdjBnidPw==} peerDependencies: react: ^18.0.0 || ^19.0.0 react-dom: ^18.0.0 || ^19.0.0 @@ -4404,7 +4421,7 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 - '@kevisual/api@0.0.14': + '@kevisual/api@0.0.16': dependencies: '@kevisual/js-filter': 0.0.3 '@kevisual/load': 0.0.6 @@ -4430,13 +4447,15 @@ snapshots: dependencies: eventemitter3: 5.0.1 - '@kevisual/query-login@0.0.7(@kevisual/query@0.0.33)': + '@kevisual/query-login@0.0.7(@kevisual/query@0.0.34)': dependencies: '@kevisual/cache': 0.0.3 - '@kevisual/query': 0.0.33 + '@kevisual/query': 0.0.34 dotenv: 17.2.3 - '@kevisual/query@0.0.33': {} + '@kevisual/query@0.0.34': + dependencies: + tslib: 2.8.1 '@kevisual/registry@0.0.1(typescript@5.9.3)': dependencies: @@ -5434,6 +5453,14 @@ snapshots: tailwindcss: 4.1.18 vite: 6.4.1(@types/node@24.7.2)(jiti@2.6.1)(lightningcss@1.30.2) + '@tanstack/react-table@8.21.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@tanstack/table-core': 8.21.3 + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + + '@tanstack/table-core@8.21.3': {} + '@types/babel__core@7.20.5': dependencies: '@babel/parser': 7.28.5 @@ -5930,7 +5957,7 @@ snapshots: base64-js@1.5.1: {} - baseline-browser-mapping@2.8.16: {} + baseline-browser-mapping@2.9.11: {} bcp-47-match@2.0.3: {} @@ -5957,7 +5984,7 @@ snapshots: browserslist@4.26.3: dependencies: - baseline-browser-mapping: 2.8.16 + baseline-browser-mapping: 2.9.11 caniuse-lite: 1.0.30001750 electron-to-chromium: 1.5.235 node-releases: 2.0.23 @@ -7424,7 +7451,7 @@ snapshots: optionalDependencies: '@types/react': 19.2.7 - react-resizable-panels@4.1.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + react-resizable-panels@4.2.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3): dependencies: react: 19.2.3 react-dom: 19.2.3(react@19.2.3) diff --git a/web/src/apps/chat/index.tsx b/web/src/apps/chat/index.tsx new file mode 100644 index 0000000..c2fc3a9 --- /dev/null +++ b/web/src/apps/chat/index.tsx @@ -0,0 +1,103 @@ +import { app } from '@/index.ts' +import { useStudioStore } from '../studio/store'; +import { useShallow } from 'zustand/shallow'; +import { useState } from 'react'; +import { query } from '@/modules/query.ts' +export const Chat = () => { + const studioStore = useStudioStore(useShallow((state) => ({ + routes: state.routes, + }))); + const [text, setText] = useState(''); + const onSend = async () => { + const { routes } = studioStore; + let callPrompts = ''; + const toolsList = routes.map((r, index) => + `${index + 1}. 工具名称: ${r.id}\n 描述: ${r.description}` + ).join('\n\n'); + + callPrompts = `你是一个 AI 助手,你可以使用以下工具来帮助用户完成任务: + +${toolsList} + +## 回复规则 +1. 如果用户的请求可以使用上述工具完成,请返回 JSON 格式数据 +2. 如果没有合适的工具,请直接分析并回答用户问题 + +## JSON 数据格式 +\`\`\`json +{ + "id": "工具的id", + "payload": { + // 工具所需的参数(如果需要) + // 例如: "id": "xxx", "name": "xxx" + } +} +\`\`\` + +注意: +- payload 中包含工具执行所需的所有参数 +- 如果工具不需要参数,payload 可以为空对象 {} +- 确保返回的 id 与上述工具列表中的工具名称完全匹配` + + const res = await query.post({ + path: 'ai', + payload: { + messages: [ + { + role: 'system', + content: callPrompts + }, + { + role: 'user', + content: text + } + ], + isJson: true + } + }) + console.log('发送消息', text, res); + if (res.code === 200) { + // 处理返回结果 + const payload = res.data?.action; + if (payload) { + const route = routes.find(r => r.id === payload.id); + console.log('找到工具', route); + const { path, key } = route || {}; + const { id, ...otherParams } = payload.payload || {}; + if (route) { + const r = await app.run({ path, key, ...otherParams }); + console.log('工具调用结果', r); + } else { + console.error('未找到对应工具', payload.id); + } + } + } + } + return
+
+
聊天
+
+
+
欢迎使用聊天功能!
+
+
+ {/* 聊天内容区域 */} +
+
+ setText(e.target.value)} + onKeyDown={(e) => e.key === 'Enter' && onSend()} + className="flex-1 px-3 py-2 border border-gray-300 rounded-md bg-white text-black placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-gray-800 transition-all" + /> + +
+
+} \ No newline at end of file diff --git a/web/src/apps/chat/modules/messages.tsx b/web/src/apps/chat/modules/messages.tsx new file mode 100644 index 0000000..8de234d --- /dev/null +++ b/web/src/apps/chat/modules/messages.tsx @@ -0,0 +1,46 @@ +type MessageData = { + id: string; + type: 'md' | 'api'; + api?: { + url?: string; + /** + * 默认不存在,动态生成 + */ + query?: any; + }; + question?: string; + action?: any; + /** + * 默认不存在,动态生成 + */ + response?: any; +} + +type MessageProps = { + data: MessageData; +} +export const Message = (props: MessageProps) => { + const { data } = props; + if (data.type === 'md') { + return
+ {/* Markdown 渲染组件 */} + Markdown 内容 +
+ } + if (data.type === 'api') { + return
+ {/* 查询结果渲染组件 */} + 查询结果内容 +
+ } + return
未知消息类型
+} + +export const Messages = (props: { items: MessageData[] }) => { + const items = props.items || []; + return
+ {items.map((item) => ( + + ))} +
+} \ No newline at end of file diff --git a/web/src/apps/chat/store.ts b/web/src/apps/chat/store.ts new file mode 100644 index 0000000..e69de29 diff --git a/web/src/apps/query-view/index.tsx b/web/src/apps/query-view/index.tsx new file mode 100644 index 0000000..771f1c3 --- /dev/null +++ b/web/src/apps/query-view/index.tsx @@ -0,0 +1,101 @@ +import { QueryProxy } from '@kevisual/api/proxy' +import { app } from '@/index.ts' +import { useEffect, useState } from 'react' +import { flexRender, useReactTable, getCoreRowModel } from '@tanstack/react-table'; +type Props = { + data: any + type: 'component' | 'page' +} +export const QueryView = (props: Props) => { + + return
API 视图
+} + + +const queryProxy = new QueryProxy({ + router: app as any +}); +export const App = () => { + const [data, setData] = useState([]) + const [columns, setColumns] = useState([]) + const table = useReactTable({ + data, + columns: columns, + getCoreRowModel: getCoreRowModel(), + }) + const main = async () => { + const res = await queryProxy.runByRouteView({ + id: 'getData', + description: '获取数据', + title: '获取数据', + type: 'api', + api: { + url: "/api/router", + }, + action: { + path: 'router', + key: 'list' + } + }) + const response = res.response; + setData(response.data.list) + console.log('res', res); + const [firstItem] = response.data.list || [] + if (firstItem) { + const cols = Object.keys(firstItem).map(key => ({ + accessorKey: key, + header: key.toUpperCase(), + })) + setColumns(cols) + } + } + useEffect(() => { + main() + }, []) + return
+ + + {table.getHeaderGroups().map(headerGroup => ( + + {headerGroup.headers.map(header => ( + + ))} + + ))} + + + {table.getRowModel().rows.map((row, idx) => ( + + {row.getVisibleCells().map(cell => ( + + ))} + + ))} + +
+ {flexRender( + header.column.columnDef.header, + header.getContext() + )} +
+ {flexRender(cell.column.columnDef.cell, cell.getContext())} +
+
+} + +export const AppProvider = (props: { children?: React.ReactNode }) => { + return
+ +
+} \ No newline at end of file diff --git a/web/src/apps/studio/index.tsx b/web/src/apps/studio/index.tsx index ad0a93c..51a11da 100644 --- a/web/src/apps/studio/index.tsx +++ b/web/src/apps/studio/index.tsx @@ -5,7 +5,7 @@ import { MonitorPlay, Play, PanelLeft, PanelLeftClose } from 'lucide-react'; import { Panel, Group } from 'react-resizable-panels' import { ViewList } from '../view/list.tsx'; import { useShallow } from 'zustand/shallow'; - +import { Chat } from '../chat/index.tsx'; export const AppProvider = () => { const { showLeftPanel } = useStudioStore(useShallow((state) => ({ showLeftPanel: state.showLeftPanel, @@ -17,9 +17,17 @@ export const AppProvider = () => { } - + + + + + + + + + { - const { routes, getRouteList, run, loading } = useStudioStore(); + const { routes, queryRouteList, run, loading } = useStudioStore(); const [expandedIds, setExpandedIds] = useState>(new Set()); const [visibleIds, setVisibleIds] = useState>(new Set()); useEffect(() => { - getRouteList(); + queryRouteList(); }, []); const toggleDescription = (id: string) => { @@ -143,8 +151,7 @@ export const App = () => { className="cursor-pointer group" >
{isExpanded ? (

diff --git a/web/src/apps/studio/store.ts b/web/src/apps/studio/store.ts index 3a5f92d..e646378 100644 --- a/web/src/apps/studio/store.ts +++ b/web/src/apps/studio/store.ts @@ -5,7 +5,7 @@ import { toast } from 'react-toastify'; import { use } from '@kevisual/context' import { MyCache } from '@kevisual/cache' import { persist } from 'zustand/middleware'; - +import { app } from '@/index.ts' const historyReplace = (url: string) => { if (window.history.replaceState) { window.history.replaceState(null, '', url); @@ -21,16 +21,17 @@ type RouteItem = { type RouteViewList = Array; + interface StudioState { loading: boolean; setLoading: (loading: boolean) => void; routes: Array; - getRouteList: (viewId?: string) => Promise; run: (route: RouteItem) => Promise; queryProxy?: QueryProxy; init: (force?: boolean) => Promise<{ queryProxy: QueryProxy }>; routeViewList: RouteViewList; getViewList: () => Promise; + queryRouteList: () => Promise; getCurrentView: () => Promise; updateRouteView: (view: RouterViewData) => Promise; deleteRouteView: (id: string) => Promise; @@ -38,6 +39,8 @@ interface StudioState { setCurrentView: (view?: RouterViewData) => Promise; showLeftPanel: boolean; setShowLeftPanel: (show: boolean) => void; + showRightPanel: boolean; + setShowRightPanel: (show: boolean) => void; } const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); @@ -47,7 +50,7 @@ export const useStudioStore = create()( loading: false, setLoading: (loading: boolean) => set({ loading }), routes: [], - getRouteList: async () => { + queryRouteList: async () => { await get().getCurrentView(); const state = await get().init(); let currentView: RouterViewData | undefined = get().currentView; @@ -67,8 +70,9 @@ export const useStudioStore = create()( url.searchParams.delete('viewId'); } historyReplace(url.toString()); + console.log('视图切换', beforeView, view); await get().init(beforeView?.id !== view?.id); - await get().getRouteList(); + await get().queryRouteList(); }, getViewList: async () => { const res = await query.post({ path: 'views', key: 'list' }); @@ -154,26 +158,30 @@ export const useStudioStore = create()( }, viewId: '', } + console.log('初始化 QueryProxy', routerViewData); queryProxy = new QueryProxy({ - routerViewData + routerViewData, + router: app as any, }); set({ loading: true }); + await sleep(1000); // 保证 loading 状态更新 await queryProxy.init(); - await sleep(500); set({ loading: false }); set({ queryProxy }); return { queryProxy } }, showLeftPanel: false, setShowLeftPanel: (show: boolean) => set({ showLeftPanel: show }), + showRightPanel: false, + setShowRightPanel: (show: boolean) => set({ showRightPanel: show }), }), { name: 'studio-storage', - partialize: (state) => ({ showLeftPanel: state.showLeftPanel }), + partialize: (state) => ({ showLeftPanel: state.showLeftPanel, showRightPanel: state.showRightPanel }), } ) ); use('studioStore', () => { - return useStudioStore.getState(); + return useStudioStore; }); diff --git a/web/src/apps/view/components/ViewEditor.tsx b/web/src/apps/view/components/ViewEditor.tsx index 5a25175..c0bd9ec 100644 --- a/web/src/apps/view/components/ViewEditor.tsx +++ b/web/src/apps/view/components/ViewEditor.tsx @@ -63,11 +63,26 @@ export const ViewEditor = ({ open, onOpenChange, data, onSave }: ViewEditorProps } const handleSave = () => { + const pickData = dataItems.map(item => { + if (item.type === 'api') { + delete item.api.query + } + if (item.type === 'worker') { + delete item.worker.worker + } + if (item.type === 'context') { + delete item.context.router + } + if (item.type === 'page') { + + } + return item + }) const viewData = { id: data?.id, title, data: { - items: dataItems + items: pickData }, views } diff --git a/web/src/index.ts b/web/src/index.ts new file mode 100644 index 0000000..740d02e --- /dev/null +++ b/web/src/index.ts @@ -0,0 +1,5 @@ +import { app } from '@/app.ts'; + +import './routes/left-panel.ts'; + +export { app }; \ No newline at end of file diff --git a/web/src/pages/query-view.astro b/web/src/pages/query-view.astro new file mode 100644 index 0000000..c025afd --- /dev/null +++ b/web/src/pages/query-view.astro @@ -0,0 +1,8 @@ +--- +import Html from '@/components/html.astro'; +import { AppProvider } from '@/apps/query-view/index.tsx'; +--- + + + + diff --git a/web/src/routes/left-panel.ts b/web/src/routes/left-panel.ts new file mode 100644 index 0000000..30dd698 --- /dev/null +++ b/web/src/routes/left-panel.ts @@ -0,0 +1,22 @@ +import { app } from '@/app.ts'; +import { use } from '@kevisual/context' + +app.route({ + path: 'web', + key: 'togglePanel', + description: '当前的网页页面功能,切换左侧面板显示与隐藏', + metadata: { + tags: ['web', 'studio', 'page'], + } +}).define(async (ctx) => { + const store = use('studioStore'); + try { + + const state = store.getState(); + state.setShowLeftPanel(!state.showLeftPanel); + ctx.body = { success: true }; + } catch (error) { + ctx.body = { success: false, message: (error as Error).message }; + } + +}).addTo(app) \ No newline at end of file