feat: enhance code graph with AI assistant and file management improvements
This commit is contained in:
221
src/modules/opencode-api.ts
Normal file
221
src/modules/opencode-api.ts
Normal file
@@ -0,0 +1,221 @@
|
|||||||
|
import { createQueryApi } from '@kevisual/query/api';
|
||||||
|
import { query } from '@/modules/query.ts';
|
||||||
|
const api = {
|
||||||
|
"opencode": {
|
||||||
|
/**
|
||||||
|
* 创建 OpenCode 客户端,如果存在则复用
|
||||||
|
*
|
||||||
|
* @param data - Request parameters
|
||||||
|
* @param data.port - {number} OpenCode 服务端口,默认为 4096
|
||||||
|
*/
|
||||||
|
"create": {
|
||||||
|
"path": "opencode",
|
||||||
|
"key": "create",
|
||||||
|
"description": "创建 OpenCode 客户端",
|
||||||
|
"metadata": {
|
||||||
|
"tags": [
|
||||||
|
"opencode"
|
||||||
|
],
|
||||||
|
"args": {
|
||||||
|
"port": {
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"description": "OpenCode 服务端口,默认为 4096",
|
||||||
|
"type": "number",
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"skill": "create-opencode-client",
|
||||||
|
"title": "创建 OpenCode 客户端",
|
||||||
|
"summary": "创建 OpenCode 客户端,如果存在则复用",
|
||||||
|
"url": "/root/v1/cnb-dev",
|
||||||
|
"source": "query-proxy-api"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 关闭 OpenCode 客户端, 未提供端口则关闭默认端口
|
||||||
|
*
|
||||||
|
* @param data - Request parameters
|
||||||
|
* @param data.port - {number} OpenCode 服务端口,默认为 4096
|
||||||
|
*/
|
||||||
|
"close": {
|
||||||
|
"path": "opencode",
|
||||||
|
"key": "close",
|
||||||
|
"description": "关闭 OpenCode 客户端",
|
||||||
|
"metadata": {
|
||||||
|
"tags": [
|
||||||
|
"opencode"
|
||||||
|
],
|
||||||
|
"args": {
|
||||||
|
"port": {
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"description": "OpenCode 服务端口,默认为 4096",
|
||||||
|
"type": "number",
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"skill": "close-opencode-client",
|
||||||
|
"title": "关闭 OpenCode 客户端",
|
||||||
|
"summary": "关闭 OpenCode 客户端, 未提供端口则关闭默认端口",
|
||||||
|
"url": "/root/v1/cnb-dev",
|
||||||
|
"source": "query-proxy-api"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 重启 OpenCode 客户端
|
||||||
|
*
|
||||||
|
* @param data - Request parameters
|
||||||
|
* @param data.port - {number} OpenCode 服务端口,默认为 4096
|
||||||
|
*/
|
||||||
|
"restart": {
|
||||||
|
"path": "opencode",
|
||||||
|
"key": "restart",
|
||||||
|
"description": "重启 OpenCode 客户端",
|
||||||
|
"metadata": {
|
||||||
|
"tags": [
|
||||||
|
"opencode"
|
||||||
|
],
|
||||||
|
"args": {
|
||||||
|
"port": {
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"description": "OpenCode 服务端口,默认为 4096",
|
||||||
|
"type": "number",
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"skill": "restart-opencode-client",
|
||||||
|
"title": "重启 OpenCode 客户端",
|
||||||
|
"summary": "重启 OpenCode 客户端",
|
||||||
|
"url": "/root/v1/cnb-dev",
|
||||||
|
"source": "query-proxy-api"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 获取当前 OpenCode 服务的 URL 地址
|
||||||
|
*
|
||||||
|
* @param data - Request parameters
|
||||||
|
* @param data.port - {number} OpenCode 服务端口,默认为 4096
|
||||||
|
*/
|
||||||
|
"getUrl": {
|
||||||
|
"path": "opencode",
|
||||||
|
"key": "getUrl",
|
||||||
|
"description": "获取 OpenCode 服务 URL",
|
||||||
|
"metadata": {
|
||||||
|
"tags": [
|
||||||
|
"opencode"
|
||||||
|
],
|
||||||
|
"args": {
|
||||||
|
"port": {
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"description": "OpenCode 服务端口,默认为 4096",
|
||||||
|
"type": "number",
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"skill": "get-opencode-url",
|
||||||
|
"title": "获取 OpenCode 服务 URL",
|
||||||
|
"summary": "获取当前 OpenCode 服务的 URL 地址",
|
||||||
|
"url": "/root/v1/cnb-dev",
|
||||||
|
"source": "query-proxy-api"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ls-projects": {
|
||||||
|
"path": "opencode",
|
||||||
|
"key": "ls-projects",
|
||||||
|
"metadata": {
|
||||||
|
"url": "/root/v1/cnb-dev",
|
||||||
|
"source": "query-proxy-api"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 运行一个已有的 OpenCode 项目
|
||||||
|
*
|
||||||
|
* @param data - Request parameters
|
||||||
|
* @param data.projectPath - {string} OpenCode 项目的路径, 默认为 /workspace
|
||||||
|
*/
|
||||||
|
"runProject": {
|
||||||
|
"path": "opencode",
|
||||||
|
"key": "runProject",
|
||||||
|
"metadata": {
|
||||||
|
"tags": [
|
||||||
|
"opencode"
|
||||||
|
],
|
||||||
|
"args": {
|
||||||
|
"projectPath": {
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"description": "OpenCode 项目的路径, 默认为 /workspace",
|
||||||
|
"type": "string",
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"skill": "run-opencode-project",
|
||||||
|
"title": "运行 OpenCode 项目",
|
||||||
|
"summary": "运行一个已有的 OpenCode 项目",
|
||||||
|
"url": "/root/v1/cnb-dev",
|
||||||
|
"source": "query-proxy-api"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"opencode-cnb": {
|
||||||
|
/**
|
||||||
|
* 创建 OpenCode 客户端
|
||||||
|
*
|
||||||
|
* @param data - Request parameters
|
||||||
|
* @param data.question - {string} 问题
|
||||||
|
* @param data.baseUrl - {string} OpenCode 服务地址,默认为 http://localhost:4096
|
||||||
|
* @param data.directory - {string} 运行目录,默认为根目录
|
||||||
|
* @param data.messageID - {string} 消息 ID,选填
|
||||||
|
* @param data.sessionId - {string} 会话 ID,选填
|
||||||
|
* @param data.parts - {array} 消息内容的分块,优先于 question 参数
|
||||||
|
*/
|
||||||
|
"question": {
|
||||||
|
"path": "opencode-cnb",
|
||||||
|
"key": "question",
|
||||||
|
"description": "创建 OpenCode 客户端",
|
||||||
|
"metadata": {
|
||||||
|
"args": {
|
||||||
|
"question": {
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"type": "string",
|
||||||
|
"description": "问题"
|
||||||
|
},
|
||||||
|
"baseUrl": {
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"description": "OpenCode 服务地址,默认为 http://localhost:4096",
|
||||||
|
"type": "string",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"directory": {
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"description": "运行目录,默认为根目录",
|
||||||
|
"type": "string",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"messageID": {
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"description": "消息 ID,选填",
|
||||||
|
"type": "string",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"sessionId": {
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"description": "会话 ID,选填",
|
||||||
|
"type": "string",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"parts": {
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"description": "消息内容的分块,优先于 question 参数",
|
||||||
|
"type": "array",
|
||||||
|
"items": {},
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"url": "/root/v1/cnb-dev",
|
||||||
|
"source": "query-proxy-api"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} as const;
|
||||||
|
const queryApi = createQueryApi({ api, query });
|
||||||
|
|
||||||
|
export { queryApi };
|
||||||
22
src/pages/code-graph/assets/openclaw.svg
Normal file
22
src/pages/code-graph/assets/openclaw.svg
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<svg viewBox="0 0 120 120" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="lobster-gradient" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" stop-color="#ff4d4d"/>
|
||||||
|
<stop offset="100%" stop-color="#991b1b"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<!-- Body -->
|
||||||
|
<path d="M60 10 C30 10 15 35 15 55 C15 75 30 95 45 100 L45 110 L55 110 L55 100 C55 100 60 102 65 100 L65 110 L75 110 L75 100 C90 95 105 75 105 55 C105 35 90 10 60 10Z" fill="url(#lobster-gradient)"/>
|
||||||
|
<!-- Left Claw -->
|
||||||
|
<path d="M20 45 C5 40 0 50 5 60 C10 70 20 65 25 55 C28 48 25 45 20 45Z" fill="url(#lobster-gradient)"/>
|
||||||
|
<!-- Right Claw -->
|
||||||
|
<path d="M100 45 C115 40 120 50 115 60 C110 70 100 65 95 55 C92 48 95 45 100 45Z" fill="url(#lobster-gradient)"/>
|
||||||
|
<!-- Antenna -->
|
||||||
|
<path d="M45 15 Q35 5 30 8" stroke="#ff4d4d" stroke-width="3" stroke-linecap="round"/>
|
||||||
|
<path d="M75 15 Q85 5 90 8" stroke="#ff4d4d" stroke-width="3" stroke-linecap="round"/>
|
||||||
|
<!-- Eyes -->
|
||||||
|
<circle cx="45" cy="35" r="6" fill="#050810"/>
|
||||||
|
<circle cx="75" cy="35" r="6" fill="#050810"/>
|
||||||
|
<circle cx="46" cy="34" r="2.5" fill="#00e5cc"/>
|
||||||
|
<circle cx="76" cy="34" r="2.5" fill="#00e5cc"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.2 KiB |
BIN
src/pages/code-graph/assets/opencode.png
Normal file
BIN
src/pages/code-graph/assets/opencode.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 245 B |
120
src/pages/code-graph/components/BotHelperModal.tsx
Normal file
120
src/pages/code-graph/components/BotHelperModal.tsx
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
import { BotIcon, XIcon, FileIcon, FolderIcon, DatabaseIcon } from 'lucide-react';
|
||||||
|
import { useBotHelperStore, BOT_KEYS, BotKey } from '../store/bot-helper';
|
||||||
|
import { useShallow } from 'zustand/react/shallow';
|
||||||
|
import { useCodeGraphStore, NodeInfoData } from '../store';
|
||||||
|
import openclawSvg from '../assets/openclaw.svg';
|
||||||
|
import opencodePng from '../assets/opencode.png';
|
||||||
|
|
||||||
|
const BOT_ICONS: Record<BotKey, string> = {
|
||||||
|
openclaw: openclawSvg,
|
||||||
|
opencode: opencodePng,
|
||||||
|
};
|
||||||
|
|
||||||
|
function NodeIcon({ kind, color }: { kind: NodeInfoData['kind']; color: string }) {
|
||||||
|
const cls = 'size-4 shrink-0';
|
||||||
|
if (kind === 'root') return <DatabaseIcon className={cls} style={{ color }} />;
|
||||||
|
if (kind === 'dir') return <FolderIcon className={cls} style={{ color }} />;
|
||||||
|
return <FileIcon className={cls} style={{ color }} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function BotHelperModal() {
|
||||||
|
const { open, input, setInput, closeModal, activeKey, setActiveKey } = useBotHelperStore(
|
||||||
|
useShallow((s) => ({
|
||||||
|
open: s.open,
|
||||||
|
input: s.input,
|
||||||
|
setInput: s.setInput,
|
||||||
|
closeModal: s.closeModal,
|
||||||
|
activeKey: s.activeKey,
|
||||||
|
setActiveKey: s.setActiveKey,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
const { nodeInfoData, createQuestion } = useCodeGraphStore(useShallow((s) => ({
|
||||||
|
nodeInfoData: s.nodeInfoData,
|
||||||
|
createQuestion: s.createQuestion,
|
||||||
|
})));
|
||||||
|
|
||||||
|
const relativePath = nodeInfoData
|
||||||
|
? nodeInfoData.fullPath.replace((nodeInfoData.projectPath || '') + '/', '') || '/'
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const handleConfirm = async () => {
|
||||||
|
if (nodeInfoData) {
|
||||||
|
const res = await createQuestion({
|
||||||
|
question: input,
|
||||||
|
projectPath: nodeInfoData.projectPath,
|
||||||
|
engine: activeKey,
|
||||||
|
});
|
||||||
|
console.log(res);
|
||||||
|
}
|
||||||
|
closeModal();
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!open) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='fixed inset-0 z-[200] flex items-center justify-center'>
|
||||||
|
{/* Mask:点击不关闭 */}
|
||||||
|
<div className='absolute inset-0 bg-black/50 backdrop-blur-sm' />
|
||||||
|
<div className='relative z-10 w-96 rounded-xl border border-white/10 bg-slate-900 shadow-2xl flex flex-col'>
|
||||||
|
{/* 标题栏 */}
|
||||||
|
<div className='flex items-center justify-between px-4 py-3 border-b border-white/10'>
|
||||||
|
<div className='flex items-center gap-2'>
|
||||||
|
<BotIcon className='size-4 text-emerald-400' />
|
||||||
|
<span className='text-sm font-medium text-slate-100'>AI 助手</span>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={closeModal}
|
||||||
|
className='text-slate-500 hover:text-slate-200 transition-colors'>
|
||||||
|
<XIcon className='size-4' />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/* 内容区 */}
|
||||||
|
<div className='px-4 py-4 flex flex-col gap-4'>
|
||||||
|
{/* 节点信息 + 按钮组 */}
|
||||||
|
<div className='flex items-center gap-2'>
|
||||||
|
{nodeInfoData && relativePath && (
|
||||||
|
<div className='flex-1 flex items-center gap-2 px-3 py-2 rounded-lg bg-slate-800/60 border border-white/5 min-w-0'>
|
||||||
|
<NodeIcon kind={nodeInfoData.kind} color={nodeInfoData.color} />
|
||||||
|
<span
|
||||||
|
className='text-xs text-slate-300 font-mono truncate'
|
||||||
|
title={relativePath}>
|
||||||
|
{relativePath}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{/* Bot 切换按钮组 */}
|
||||||
|
<div className='flex items-center gap-1 shrink-0 ml-auto'>
|
||||||
|
{BOT_KEYS.map((key) => (
|
||||||
|
<button
|
||||||
|
key={key}
|
||||||
|
title={key}
|
||||||
|
onClick={() => setActiveKey(key)}
|
||||||
|
className={`p-1.5 rounded-lg border transition-colors ${activeKey === key
|
||||||
|
? 'border-emerald-500/60 bg-emerald-500/10'
|
||||||
|
: 'border-white/5 bg-slate-800/60 opacity-40 hover:opacity-70'
|
||||||
|
}`}>
|
||||||
|
<img src={BOT_ICONS[key]} alt={key} className='size-5 object-contain' />
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<textarea
|
||||||
|
className='w-full rounded-lg border border-white/10 bg-slate-800 px-3 py-2 text-sm text-slate-200 placeholder:text-slate-500 focus:outline-none focus:ring-1 focus:ring-emerald-500 resize-none'
|
||||||
|
rows={5}
|
||||||
|
placeholder='请输入内容...'
|
||||||
|
value={input}
|
||||||
|
onChange={(e) => setInput(e.target.value)}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={handleConfirm}
|
||||||
|
className='w-full rounded-lg bg-emerald-600 hover:bg-emerald-500 active:bg-emerald-700 text-white text-sm font-medium py-2 transition-colors'>
|
||||||
|
确定
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default BotHelperModal;
|
||||||
@@ -100,7 +100,7 @@ function buildGraph3DData(files: FileProjectData[]): Graph3DData {
|
|||||||
label: name,
|
label: name,
|
||||||
kind: 'root',
|
kind: 'root',
|
||||||
color: '#f59e0b',
|
color: '#f59e0b',
|
||||||
nodeSize: 10,
|
nodeSize: 15,
|
||||||
fullPath: projectPath,
|
fullPath: projectPath,
|
||||||
projectPath,
|
projectPath,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -10,8 +10,9 @@ import { markdown } from '@codemirror/lang-markdown';
|
|||||||
import { GraphNode } from '../modules/graph';
|
import { GraphNode } from '../modules/graph';
|
||||||
import { queryApi as projectApi } from '@/modules/project-api';
|
import { queryApi as projectApi } from '@/modules/project-api';
|
||||||
import { FileProjectData } from '../modules/tree';
|
import { FileProjectData } from '../modules/tree';
|
||||||
import { getFilesApi } from '../modules/api/get-files';
|
|
||||||
import './CodePod.css';
|
import './CodePod.css';
|
||||||
|
import { useCodeGraphStore } from '../store';
|
||||||
|
import { useShallow } from 'zustand/shallow';
|
||||||
// ─── 目录树类型 ────────────────────────────────────────────────────────────────
|
// ─── 目录树类型 ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
type TreeNode = {
|
type TreeNode = {
|
||||||
@@ -186,7 +187,9 @@ export function CodePod({ open, onClose, nodeAttrs }: CodePodProps) {
|
|||||||
const isDir = nodeAttrs?.kind === 'dir' || nodeAttrs?.kind === 'root';
|
const isDir = nodeAttrs?.kind === 'dir' || nodeAttrs?.kind === 'root';
|
||||||
const [sidebarOpen, setSidebarOpen] = useState(true);
|
const [sidebarOpen, setSidebarOpen] = useState(true);
|
||||||
const rootPath = nodeAttrs?.fullPath ?? '';
|
const rootPath = nodeAttrs?.fullPath ?? '';
|
||||||
|
const codeGraphStore = useCodeGraphStore(useShallow((s) => ({
|
||||||
|
getFiles: s.getFiles,
|
||||||
|
})));
|
||||||
|
|
||||||
// 打开时重置
|
// 打开时重置
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -200,9 +203,9 @@ export function CodePod({ open, onClose, nodeAttrs }: CodePodProps) {
|
|||||||
}, [open, nodeAttrs]);
|
}, [open, nodeAttrs]);
|
||||||
const init = async (nodeAttrs: GraphNode) => {
|
const init = async (nodeAttrs: GraphNode) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const res = await getFilesApi({
|
const res = await codeGraphStore.getFiles({
|
||||||
filepath: nodeAttrs.fullPath,
|
filepath: nodeAttrs.fullPath,
|
||||||
// projectPath: nodeAttrs.projectPath
|
projectPath: nodeAttrs.projectPath,
|
||||||
getContent: true,
|
getContent: true,
|
||||||
});
|
});
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import { FileIcon, FolderIcon, DatabaseIcon, XIcon, MoveIcon, SquarePenIcon } from 'lucide-react';
|
import { FileIcon, FolderIcon, DatabaseIcon, XIcon, MoveIcon, SquarePenIcon, BotIcon } from 'lucide-react';
|
||||||
import { useCodeGraphStore, NodeInfoData } from '../store';
|
import { useCodeGraphStore, NodeInfoData } from '../store';
|
||||||
|
import { useBotHelperStore } from '../store/bot-helper';
|
||||||
import { useShallow } from 'zustand/react/shallow';
|
import { useShallow } from 'zustand/react/shallow';
|
||||||
|
|
||||||
function KindIcon({ kind, color }: { kind: NodeInfoData['kind']; color: string }) {
|
function KindIcon({ kind, color }: { kind: NodeInfoData['kind']; color: string }) {
|
||||||
@@ -47,6 +48,8 @@ export function NodeInfo() {
|
|||||||
setOffset({ x: 0, y: 0 });
|
setOffset({ x: 0, y: 0 });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const openBotModal = useBotHelperStore((s) => s.openModal);
|
||||||
|
|
||||||
// 拖拽偏移
|
// 拖拽偏移
|
||||||
const [offset, setOffset] = useState({ x: 0, y: 0 });
|
const [offset, setOffset] = useState({ x: 0, y: 0 });
|
||||||
const [pinLeft, setPinLeft] = useState(false); // 编辑后固定到右下角
|
const [pinLeft, setPinLeft] = useState(false); // 编辑后固定到右下角
|
||||||
@@ -115,6 +118,12 @@ export function NodeInfo() {
|
|||||||
className='ml-1 text-slate-500 hover:text-indigo-400 transition-colors'>
|
className='ml-1 text-slate-500 hover:text-indigo-400 transition-colors'>
|
||||||
<SquarePenIcon className='size-3.5' />
|
<SquarePenIcon className='size-3.5' />
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={openBotModal}
|
||||||
|
title='AI 助手'
|
||||||
|
className='ml-1 text-slate-500 hover:text-emerald-400 transition-colors'>
|
||||||
|
<BotIcon className='size-3.5' />
|
||||||
|
</button>
|
||||||
|
|
||||||
<span className='text-[10px] text-slate-500 bg-slate-800 px-1.5 py-0.5 rounded'>
|
<span className='text-[10px] text-slate-500 bg-slate-800 px-1.5 py-0.5 rounded'>
|
||||||
{KIND_LABEL[nodeInfoData.kind]}
|
{KIND_LABEL[nodeInfoData.kind]}
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
import { queryApi as projectApi } from "@/modules/project-api";
|
|
||||||
import { FileProjectData } from "../tree";
|
|
||||||
import { Result } from "@kevisual/query";
|
|
||||||
/** 从后端 API 获取文件列表 */
|
|
||||||
export const getFilesApi = async (opts?: {
|
|
||||||
filepath?: string; // 可选的目录路径,默认为根目录
|
|
||||||
q?: string; // 可选的搜索关键词
|
|
||||||
projectPath?: string; // 项目路径,必填
|
|
||||||
getContent?: boolean; // 是否获取文件内容,默认为 false
|
|
||||||
}, dataOpts?: {
|
|
||||||
url?: string; // 可选的 API 基础 URL,默认为 "/root/v1/dev-cnb"
|
|
||||||
}): Promise<Result<{ list: FileProjectData[] }>> => {
|
|
||||||
const url = dataOpts?.url ?? "/root/v1/cnb-dev";
|
|
||||||
const res = await projectApi["project-search"].files({
|
|
||||||
...opts,
|
|
||||||
q: opts?.q ?? "",
|
|
||||||
}, {
|
|
||||||
url
|
|
||||||
});
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { FileProjectData } from './modules/tree';
|
import { FileProjectData } from './modules/tree';
|
||||||
import { getFilesApi } from './modules/api/get-files';
|
|
||||||
import { toast } from 'sonner';
|
|
||||||
import { useShallow } from 'zustand/react/shallow';
|
import { useShallow } from 'zustand/react/shallow';
|
||||||
import { DatabaseIcon } from 'lucide-react';
|
import { DatabaseIcon } from 'lucide-react';
|
||||||
import { CodePod } from './components/CodePod';
|
import { CodePod } from './components/CodePod';
|
||||||
@@ -10,35 +8,32 @@ import CodeGraphView from './components/CodeGraph';
|
|||||||
import { Code3DGraph } from './components/Code3DGraph';
|
import { Code3DGraph } from './components/Code3DGraph';
|
||||||
import { NodeInfo } from './components/NodeInfo';
|
import { NodeInfo } from './components/NodeInfo';
|
||||||
import { ProjectDialog } from './components/ProjectDialog';
|
import { ProjectDialog } from './components/ProjectDialog';
|
||||||
|
import { BotHelperModal } from './components/BotHelperModal';
|
||||||
|
import { useLayoutStore } from '../auth/store';
|
||||||
type ViewMode = '2d' | '3d';
|
type ViewMode = '2d' | '3d';
|
||||||
|
|
||||||
export default function CodeGraphPage() {
|
export default function CodeGraphPage() {
|
||||||
const [files, setFiles] = useState<FileProjectData[]>([]);
|
|
||||||
const [viewMode, setViewMode] = useState<ViewMode>('3d');
|
const [viewMode, setViewMode] = useState<ViewMode>('3d');
|
||||||
const { codePodOpen, setCodePodOpen, codePodAttrs, setProjectDialogOpen, loadProjects } = useCodeGraphStore(
|
const layoutStore = useLayoutStore(useShallow((s) => ({
|
||||||
|
me: s.me,
|
||||||
|
})));
|
||||||
|
const { codePodOpen, setCodePodOpen, codePodAttrs, setProjectDialogOpen, init, files } = useCodeGraphStore(
|
||||||
useShallow((s) => ({
|
useShallow((s) => ({
|
||||||
|
files: s.files,
|
||||||
|
setFiles: s.setFiles,
|
||||||
codePodOpen: s.codePodOpen,
|
codePodOpen: s.codePodOpen,
|
||||||
setCodePodOpen: s.setCodePodOpen,
|
setCodePodOpen: s.setCodePodOpen,
|
||||||
codePodAttrs: s.codePodAttrs,
|
codePodAttrs: s.codePodAttrs,
|
||||||
setProjectDialogOpen: s.setProjectDialogOpen,
|
setProjectDialogOpen: s.setProjectDialogOpen,
|
||||||
loadProjects: s.loadProjects
|
init: s.init,
|
||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadData();
|
if (!layoutStore.me?.username) return;
|
||||||
loadProjects();
|
init(layoutStore.me);
|
||||||
}, []);
|
}, [layoutStore.me]);
|
||||||
// 页面加载时从 API 获取文件列表
|
|
||||||
const loadData = async () => {
|
|
||||||
const res = await getFilesApi();
|
|
||||||
if (res.code === 200) {
|
|
||||||
setFiles(res.data!.list);
|
|
||||||
} else {
|
|
||||||
toast.error('获取文件列表失败');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
return (
|
return (
|
||||||
<div className='flex flex-col h-full bg-slate-950 text-slate-100'>
|
<div className='flex flex-col h-full bg-slate-950 text-slate-100'>
|
||||||
{/* 顶部工具栏 */}
|
{/* 顶部工具栏 */}
|
||||||
@@ -90,6 +85,8 @@ export default function CodeGraphPage() {
|
|||||||
<NodeInfo />
|
<NodeInfo />
|
||||||
{/* 项目管理弹窗 */}
|
{/* 项目管理弹窗 */}
|
||||||
<ProjectDialog />
|
<ProjectDialog />
|
||||||
|
{/* Bot AI 助手弹窗 */}
|
||||||
|
<BotHelperModal />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
27
src/pages/code-graph/store/bot-helper.ts
Normal file
27
src/pages/code-graph/store/bot-helper.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { create } from 'zustand';
|
||||||
|
|
||||||
|
export type BotKey = 'openclaw' | 'opencode';
|
||||||
|
|
||||||
|
export const BOT_KEYS: BotKey[] = ['openclaw', 'opencode'];
|
||||||
|
|
||||||
|
type BotHelperState = {
|
||||||
|
open: boolean;
|
||||||
|
input: string;
|
||||||
|
activeKey: BotKey;
|
||||||
|
setOpen: (open: boolean) => void;
|
||||||
|
setInput: (input: string) => void;
|
||||||
|
setActiveKey: (key: BotKey) => void;
|
||||||
|
openModal: () => void;
|
||||||
|
closeModal: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useBotHelperStore = create<BotHelperState>()((set) => ({
|
||||||
|
open: false,
|
||||||
|
input: '',
|
||||||
|
activeKey: 'opencode',
|
||||||
|
setOpen: (open) => set({ open }),
|
||||||
|
setInput: (input) => set({ input }),
|
||||||
|
setActiveKey: (key) => set({ activeKey: key }),
|
||||||
|
openModal: () => set({ open: true }),
|
||||||
|
closeModal: () => set({ open: false, input: '' }),
|
||||||
|
}));
|
||||||
@@ -1,7 +1,11 @@
|
|||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
import { GraphNode } from '../modules/graph';
|
import { GraphNode } from '../modules/graph';
|
||||||
import { queryApi as projectApi } from '@/modules/project-api';
|
import { queryApi as projectApi } from '@/modules/project-api';
|
||||||
|
import { queryApi as opencodeApi } from '@/modules/opencode-api';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
import { FileProjectData } from '../modules/tree';
|
||||||
|
import { UserInfo } from '@/pages/auth/store';
|
||||||
|
import { Result } from '@kevisual/query';
|
||||||
|
|
||||||
export type ProjectItem = {
|
export type ProjectItem = {
|
||||||
path: string;
|
path: string;
|
||||||
@@ -33,6 +37,8 @@ type State = {
|
|||||||
// 项目列表
|
// 项目列表
|
||||||
projects: ProjectItem[];
|
projects: ProjectItem[];
|
||||||
projectsLoading: boolean;
|
projectsLoading: boolean;
|
||||||
|
files: FileProjectData[];
|
||||||
|
setFiles: (files: FileProjectData[]) => void;
|
||||||
loadProjects: () => Promise<void>;
|
loadProjects: () => Promise<void>;
|
||||||
addProject: (filepath: string, name?: string) => Promise<boolean>;
|
addProject: (filepath: string, name?: string) => Promise<boolean>;
|
||||||
removeProject: (path: string) => Promise<void>;
|
removeProject: (path: string) => Promise<void>;
|
||||||
@@ -42,6 +48,15 @@ type State = {
|
|||||||
nodeInfoPos: { x: number; y: number };
|
nodeInfoPos: { x: number; y: number };
|
||||||
setNodeInfo: (data: NodeInfoData | null, pos?: { x: number; y: number }) => void;
|
setNodeInfo: (data: NodeInfoData | null, pos?: { x: number; y: number }) => void;
|
||||||
closeNodeInfo: () => void;
|
closeNodeInfo: () => void;
|
||||||
|
url?: string;
|
||||||
|
init(user: UserInfo): Promise<void>;
|
||||||
|
getFiles: (opts?: {
|
||||||
|
filepath?: string; // 可选的目录路径,默认为根目录
|
||||||
|
q?: string; // 可选的搜索关键词
|
||||||
|
projectPath?: string; // 项目路径,必填
|
||||||
|
getContent?: boolean; // 是否获取文件内容,默认为 false
|
||||||
|
}) => Promise<Result<{ list: FileProjectData[] }>>;
|
||||||
|
createQuestion: (opts: { question: string, projectPath: string, engine?: 'openclaw' | 'opencode' }) => any;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useCodeGraphStore = create<State>()((set, get) => ({
|
export const useCodeGraphStore = create<State>()((set, get) => ({
|
||||||
@@ -53,10 +68,14 @@ export const useCodeGraphStore = create<State>()((set, get) => ({
|
|||||||
setProjectDialogOpen: (open) => set({ projectDialogOpen: open }),
|
setProjectDialogOpen: (open) => set({ projectDialogOpen: open }),
|
||||||
projects: [],
|
projects: [],
|
||||||
projectsLoading: false,
|
projectsLoading: false,
|
||||||
|
files: [],
|
||||||
|
setFiles: (files) => set({ files }),
|
||||||
|
|
||||||
loadProjects: async () => {
|
loadProjects: async () => {
|
||||||
set({ projectsLoading: true });
|
set({ projectsLoading: true });
|
||||||
|
const url = get().url || API_URL;
|
||||||
try {
|
try {
|
||||||
const res = await projectApi.project.list(undefined, { url: API_URL });
|
const res = await projectApi.project.list(undefined, { url });
|
||||||
if (res.code === 200) {
|
if (res.code === 200) {
|
||||||
set({ projects: (res.data?.list as ProjectItem[]) ?? [] });
|
set({ projects: (res.data?.list as ProjectItem[]) ?? [] });
|
||||||
} else {
|
} else {
|
||||||
@@ -70,9 +89,10 @@ export const useCodeGraphStore = create<State>()((set, get) => ({
|
|||||||
},
|
},
|
||||||
addProject: async (filepath, name) => {
|
addProject: async (filepath, name) => {
|
||||||
try {
|
try {
|
||||||
|
const url = get().url || API_URL;
|
||||||
const res = await projectApi.project.add(
|
const res = await projectApi.project.add(
|
||||||
{ filepath, name: name || undefined },
|
{ filepath, name: name || undefined },
|
||||||
{ url: API_URL },
|
{ url },
|
||||||
);
|
);
|
||||||
if (res.code === 200) {
|
if (res.code === 200) {
|
||||||
toast.success('项目添加成功');
|
toast.success('项目添加成功');
|
||||||
@@ -89,7 +109,8 @@ export const useCodeGraphStore = create<State>()((set, get) => ({
|
|||||||
},
|
},
|
||||||
removeProject: async (path) => {
|
removeProject: async (path) => {
|
||||||
try {
|
try {
|
||||||
const res = await projectApi.project.remove({ filepath: path }, { url: API_URL });
|
const url = get().url || API_URL;
|
||||||
|
const res = await projectApi.project.remove({ filepath: path }, { url });
|
||||||
if (res.code === 200) {
|
if (res.code === 200) {
|
||||||
toast.success('项目已移除');
|
toast.success('项目已移除');
|
||||||
set((s) => ({ projects: s.projects.filter((p) => p.path !== path) }));
|
set((s) => ({ projects: s.projects.filter((p) => p.path !== path) }));
|
||||||
@@ -110,4 +131,47 @@ export const useCodeGraphStore = create<State>()((set, get) => ({
|
|||||||
nodeInfoPos: pos ?? { x: 0, y: 0 },
|
nodeInfoPos: pos ?? { x: 0, y: 0 },
|
||||||
}),
|
}),
|
||||||
closeNodeInfo: () => set({ nodeInfoOpen: false }),
|
closeNodeInfo: () => set({ nodeInfoOpen: false }),
|
||||||
|
url: API_URL,
|
||||||
|
init: async (user) => {
|
||||||
|
// 可以在这里根据用户信息初始化一些数据,比如权限相关的设置等
|
||||||
|
console.log('CodeGraphStore initialized for user:', user.username);
|
||||||
|
const username = user.username;
|
||||||
|
const url = username ? `/${username}/v1/cnb-dev` : API_URL;
|
||||||
|
set({ url });
|
||||||
|
get().loadProjects();
|
||||||
|
const res = await get().getFiles();
|
||||||
|
if (res.code === 200) {
|
||||||
|
set({ files: res.data!.list });
|
||||||
|
} else {
|
||||||
|
toast.error('获取文件列表失败');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getFiles: async (opts?: {
|
||||||
|
filepath?: string; // 可选的目录路径,默认为根目录
|
||||||
|
q?: string; // 可选的搜索关键词
|
||||||
|
projectPath?: string; // 项目路径,必填
|
||||||
|
getContent?: boolean; // 是否获取文件内容,默认为 false
|
||||||
|
}) => {
|
||||||
|
const url = get().url
|
||||||
|
const res = await projectApi["project-search"].files({
|
||||||
|
...opts,
|
||||||
|
q: opts?.q ?? "",
|
||||||
|
}, {
|
||||||
|
url
|
||||||
|
});
|
||||||
|
return res;
|
||||||
|
},
|
||||||
|
createQuestion: async (opts) => {
|
||||||
|
const { question, projectPath, engine = 'opencode' } = opts;
|
||||||
|
const url = get().url
|
||||||
|
const q = `
|
||||||
|
${question}
|
||||||
|
项目路径: ${projectPath}`
|
||||||
|
const res = await opencodeApi["opencode-cnb"].question({
|
||||||
|
question: q,
|
||||||
|
}, {
|
||||||
|
url
|
||||||
|
});
|
||||||
|
return res;
|
||||||
|
},
|
||||||
}));
|
}));
|
||||||
|
|||||||
Reference in New Issue
Block a user