update
This commit is contained in:
@@ -11,6 +11,7 @@ const api = {
|
||||
"create": {
|
||||
"path": "opencode",
|
||||
"key": "create",
|
||||
"id": "b662fba3c0a8a593",
|
||||
"description": "创建 OpenCode 客户端",
|
||||
"metadata": {
|
||||
"tags": [
|
||||
@@ -40,6 +41,7 @@ const api = {
|
||||
"close": {
|
||||
"path": "opencode",
|
||||
"key": "close",
|
||||
"id": "49672adea9daa837",
|
||||
"description": "关闭 OpenCode 客户端",
|
||||
"metadata": {
|
||||
"tags": [
|
||||
@@ -69,6 +71,7 @@ const api = {
|
||||
"restart": {
|
||||
"path": "opencode",
|
||||
"key": "restart",
|
||||
"id": "e0b1564a796ea88b",
|
||||
"description": "重启 OpenCode 客户端",
|
||||
"metadata": {
|
||||
"tags": [
|
||||
@@ -98,6 +101,7 @@ const api = {
|
||||
"getUrl": {
|
||||
"path": "opencode",
|
||||
"key": "getUrl",
|
||||
"id": "c611acf038e41279",
|
||||
"description": "获取 OpenCode 服务 URL",
|
||||
"metadata": {
|
||||
"tags": [
|
||||
@@ -121,6 +125,7 @@ const api = {
|
||||
"ls-projects": {
|
||||
"path": "opencode",
|
||||
"key": "ls-projects",
|
||||
"id": "ee72cd09da63d13d",
|
||||
"metadata": {
|
||||
"url": "/root/v1/cnb-dev",
|
||||
"source": "query-proxy-api"
|
||||
@@ -135,6 +140,7 @@ const api = {
|
||||
"runProject": {
|
||||
"path": "opencode",
|
||||
"key": "runProject",
|
||||
"id": "112127fa82fe1d9d",
|
||||
"metadata": {
|
||||
"tags": [
|
||||
"opencode"
|
||||
@@ -163,13 +169,17 @@ const api = {
|
||||
* @param data.question - {string} 问题
|
||||
* @param data.baseUrl - {string} OpenCode 服务地址,默认为 http://localhost:4096
|
||||
* @param data.directory - {string} 运行目录,默认为根目录
|
||||
* @param data.messageID - {string} 消息 ID,选填
|
||||
* @param data.messageId - {string} 消息 ID,选填
|
||||
* @param data.sessionId - {string} 会话 ID,选填
|
||||
* @param data.providerId - {string} 指定使用的提供商 ID,默认为空,表示使用默认提供商
|
||||
* @param data.modelId - {string} 指定使用的模型 ID,默认为空,表示使用默认模型
|
||||
* @param data.parts - {array} 消息内容的分块,优先于 question 参数
|
||||
* @param data.awaitAnswer - {boolean} 是否等待回答完成,默认为 false,开启后会在回答完成后返回完整回答内容
|
||||
*/
|
||||
"question": {
|
||||
"path": "opencode-cnb",
|
||||
"key": "question",
|
||||
"id": "193c9c63bc50cbbc",
|
||||
"description": "创建 OpenCode 客户端",
|
||||
"metadata": {
|
||||
"args": {
|
||||
@@ -190,7 +200,7 @@ const api = {
|
||||
"type": "string",
|
||||
"optional": true
|
||||
},
|
||||
"messageID": {
|
||||
"messageId": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"description": "消息 ID,选填",
|
||||
"type": "string",
|
||||
@@ -202,17 +212,430 @@ const api = {
|
||||
"type": "string",
|
||||
"optional": true
|
||||
},
|
||||
"providerId": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"description": "指定使用的提供商 ID,默认为空,表示使用默认提供商",
|
||||
"type": "string",
|
||||
"optional": true
|
||||
},
|
||||
"modelId": {
|
||||
"$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
|
||||
},
|
||||
"awaitAnswer": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"description": "是否等待回答完成,默认为 false,开启后会在回答完成后返回完整回答内容",
|
||||
"type": "boolean",
|
||||
"optional": true
|
||||
}
|
||||
},
|
||||
"url": "/root/v1/cnb-dev",
|
||||
"source": "query-proxy-api"
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 获取 OpenCode 可用模型列表,返回 providerID 和 modelID
|
||||
*
|
||||
* @param data - Request parameters
|
||||
* @param data.baseUrl - {string} OpenCode 服务地址,默认为 http://localhost:4096
|
||||
*/
|
||||
"models": {
|
||||
"path": "opencode-cnb",
|
||||
"key": "models",
|
||||
"id": "a66f19f8427e7085",
|
||||
"description": "获取 OpenCode 可用模型列表,返回 providerID 和 modelID",
|
||||
"metadata": {
|
||||
"args": {
|
||||
"baseUrl": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"description": "OpenCode 服务地址,默认为 http://localhost:4096",
|
||||
"type": "string",
|
||||
"optional": true
|
||||
}
|
||||
},
|
||||
"url": "/root/v1/cnb-dev",
|
||||
"source": "query-proxy-api"
|
||||
}
|
||||
}
|
||||
},
|
||||
"opencode-session": {
|
||||
/**
|
||||
* 在指定目录创建一个新的 OpenCode 会话
|
||||
*
|
||||
* @param data - Request parameters
|
||||
* @param data.directory - {string} 工作目录,默认为 /workspace
|
||||
* @param data.port - {number} OpenCode 服务端口,默认为 4096
|
||||
*/
|
||||
"create": {
|
||||
"path": "opencode-session",
|
||||
"key": "create",
|
||||
"id": "f07990a69e2a1eaf",
|
||||
"description": "创建 OpenCode Session",
|
||||
"metadata": {
|
||||
"tags": [
|
||||
"session"
|
||||
],
|
||||
"args": {
|
||||
"directory": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"description": "工作目录,默认为 /workspace",
|
||||
"type": "string",
|
||||
"optional": true
|
||||
},
|
||||
"port": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"description": "OpenCode 服务端口,默认为 4096",
|
||||
"type": "number",
|
||||
"optional": true
|
||||
}
|
||||
},
|
||||
"skill": "create-opencode-session",
|
||||
"title": "创建 Session",
|
||||
"summary": "在指定目录创建一个新的 OpenCode 会话",
|
||||
"url": "/root/v1/cnb-dev",
|
||||
"source": "query-proxy-api"
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 更新指定 OpenCode 会话的属性,如标题
|
||||
*
|
||||
* @param data - Request parameters
|
||||
* @param data.sessionId - {string} Session ID
|
||||
* @param data.title - {string} 新的会话标题
|
||||
* @param data.port - {number} OpenCode 服务端口,默认为 4096
|
||||
*/
|
||||
"update": {
|
||||
"path": "opencode-session",
|
||||
"key": "update",
|
||||
"id": "0a6f7cd78fa5ff20",
|
||||
"description": "更新 OpenCode Session",
|
||||
"metadata": {
|
||||
"tags": [
|
||||
"session"
|
||||
],
|
||||
"args": {
|
||||
"sessionId": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"type": "string",
|
||||
"description": "Session ID"
|
||||
},
|
||||
"title": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"description": "新的会话标题",
|
||||
"type": "string",
|
||||
"optional": true
|
||||
},
|
||||
"port": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"description": "OpenCode 服务端口,默认为 4096",
|
||||
"type": "number",
|
||||
"optional": true
|
||||
}
|
||||
},
|
||||
"skill": "update-opencode-session",
|
||||
"title": "更新 Session",
|
||||
"summary": "更新指定 OpenCode 会话的属性,如标题",
|
||||
"url": "/root/v1/cnb-dev",
|
||||
"source": "query-proxy-api"
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 根据 ID 删除指定的 OpenCode 会话及其所有数据
|
||||
*
|
||||
* @param data - Request parameters
|
||||
* @param data.sessionId - {string} Session ID
|
||||
* @param data.port - {number} OpenCode 服务端口,默认为 4096
|
||||
*/
|
||||
"delete": {
|
||||
"path": "opencode-session",
|
||||
"key": "delete",
|
||||
"id": "c7bd762b2eccdfc2",
|
||||
"description": "删除 OpenCode Session",
|
||||
"metadata": {
|
||||
"tags": [
|
||||
"session"
|
||||
],
|
||||
"args": {
|
||||
"sessionId": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"type": "string",
|
||||
"description": "Session ID"
|
||||
},
|
||||
"port": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"description": "OpenCode 服务端口,默认为 4096",
|
||||
"type": "number",
|
||||
"optional": true
|
||||
}
|
||||
},
|
||||
"skill": "delete-opencode-session",
|
||||
"title": "删除 Session",
|
||||
"summary": "根据 ID 删除指定的 OpenCode 会话及其所有数据",
|
||||
"url": "/root/v1/cnb-dev",
|
||||
"source": "query-proxy-api"
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 中止正在运行的 OpenCode 会话
|
||||
*
|
||||
* @param data - Request parameters
|
||||
* @param data.sessionId - {string} Session ID
|
||||
* @param data.port - {number} OpenCode 服务端口,默认为 4096
|
||||
*/
|
||||
"abort": {
|
||||
"path": "opencode-session",
|
||||
"key": "abort",
|
||||
"id": "0b89922558164ffd",
|
||||
"description": "中止 OpenCode Session",
|
||||
"metadata": {
|
||||
"tags": [
|
||||
"session"
|
||||
],
|
||||
"args": {
|
||||
"sessionId": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"type": "string",
|
||||
"description": "Session ID"
|
||||
},
|
||||
"port": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"description": "OpenCode 服务端口,默认为 4096",
|
||||
"type": "number",
|
||||
"optional": true
|
||||
}
|
||||
},
|
||||
"skill": "abort-opencode-session",
|
||||
"title": "中止 Session",
|
||||
"summary": "中止正在运行的 OpenCode 会话",
|
||||
"url": "/root/v1/cnb-dev",
|
||||
"source": "query-proxy-api"
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 对指定的 OpenCode 会话进行内容总结
|
||||
*
|
||||
* @param data - Request parameters
|
||||
* @param data.sessionId - {string} Session ID
|
||||
* @param data.port - {number} OpenCode 服务端口,默认为 4096
|
||||
*/
|
||||
"summarize": {
|
||||
"path": "opencode-session",
|
||||
"key": "summarize",
|
||||
"id": "c51ae8a43b269383",
|
||||
"description": "总结 OpenCode Session",
|
||||
"metadata": {
|
||||
"tags": [
|
||||
"session"
|
||||
],
|
||||
"args": {
|
||||
"sessionId": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"type": "string",
|
||||
"description": "Session ID"
|
||||
},
|
||||
"port": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"description": "OpenCode 服务端口,默认为 4096",
|
||||
"type": "number",
|
||||
"optional": true
|
||||
}
|
||||
},
|
||||
"skill": "summarize-opencode-session",
|
||||
"title": "总结 Session",
|
||||
"summary": "对指定的 OpenCode 会话进行内容总结",
|
||||
"url": "/root/v1/cnb-dev",
|
||||
"source": "query-proxy-api"
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 获取当前 OpenCode 会话的运行状态,可按目录过滤
|
||||
*
|
||||
* @param data - Request parameters
|
||||
* @param data.directory - {string} 工作目录
|
||||
* @param data.port - {number} OpenCode 服务端口,默认为 4096
|
||||
*/
|
||||
"status": {
|
||||
"path": "opencode-session",
|
||||
"key": "status",
|
||||
"id": "a2507055e8e1ed42",
|
||||
"description": "获取 OpenCode Session 状态",
|
||||
"metadata": {
|
||||
"tags": [
|
||||
"session"
|
||||
],
|
||||
"args": {
|
||||
"directory": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"description": "工作目录",
|
||||
"type": "string",
|
||||
"optional": true
|
||||
},
|
||||
"port": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"description": "OpenCode 服务端口,默认为 4096",
|
||||
"type": "number",
|
||||
"optional": true
|
||||
}
|
||||
},
|
||||
"skill": "get-opencode-session-status",
|
||||
"title": "获取 Session 状态",
|
||||
"summary": "获取当前 OpenCode 会话的运行状态,可按目录过滤",
|
||||
"url": "/root/v1/cnb-dev",
|
||||
"source": "query-proxy-api"
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 列出指定 OpenCode 会话的所有消息记录
|
||||
*
|
||||
* @param data - Request parameters
|
||||
* @param data.sessionId - {string} Session ID
|
||||
* @param data.port - {number} OpenCode 服务端口,默认为 4096
|
||||
*/
|
||||
"messages": {
|
||||
"path": "opencode-session",
|
||||
"key": "messages",
|
||||
"id": "04e78517e6e9144e",
|
||||
"description": "列出 OpenCode Session 消息",
|
||||
"metadata": {
|
||||
"tags": [
|
||||
"session"
|
||||
],
|
||||
"args": {
|
||||
"sessionId": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"type": "string",
|
||||
"description": "Session ID"
|
||||
},
|
||||
"port": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"description": "OpenCode 服务端口,默认为 4096",
|
||||
"type": "number",
|
||||
"optional": true
|
||||
}
|
||||
},
|
||||
"skill": "list-opencode-session-messages",
|
||||
"title": "列出 Session 消息",
|
||||
"summary": "列出指定 OpenCode 会话的所有消息记录",
|
||||
"url": "/root/v1/cnb-dev",
|
||||
"source": "query-proxy-api"
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 列出 OpenCode 中的所有会话,可按目录过滤
|
||||
*
|
||||
* @param data - Request parameters
|
||||
* @param data.port - {number} OpenCode 服务端口,默认为 4096
|
||||
*/
|
||||
"list": {
|
||||
"path": "opencode-session",
|
||||
"key": "list",
|
||||
"id": "a44e79adfcb199dd",
|
||||
"description": "列出所有 OpenCode Session",
|
||||
"metadata": {
|
||||
"tags": [
|
||||
"session"
|
||||
],
|
||||
"args": {
|
||||
"port": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"description": "OpenCode 服务端口,默认为 4096",
|
||||
"type": "number",
|
||||
"optional": true
|
||||
}
|
||||
},
|
||||
"skill": "list-opencode-sessions",
|
||||
"title": "列出所有 Session",
|
||||
"summary": "列出 OpenCode 中的所有会话,可按目录过滤",
|
||||
"url": "/root/v1/cnb-dev",
|
||||
"source": "query-proxy-api"
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 根据 ID 获取指定的 OpenCode 会话信息
|
||||
*
|
||||
* @param data - Request parameters
|
||||
* @param data.id - {string} Session ID
|
||||
* @param data.port - {number} OpenCode 服务端口,默认为 4096
|
||||
*/
|
||||
"get": {
|
||||
"path": "opencode-session",
|
||||
"key": "get",
|
||||
"id": "7acea53865affb10",
|
||||
"description": "获取指定 OpenCode Session",
|
||||
"metadata": {
|
||||
"tags": [
|
||||
"session"
|
||||
],
|
||||
"args": {
|
||||
"id": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"type": "string",
|
||||
"description": "Session ID"
|
||||
},
|
||||
"port": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"description": "OpenCode 服务端口,默认为 4096",
|
||||
"type": "number",
|
||||
"optional": true
|
||||
}
|
||||
},
|
||||
"skill": "get-opencode-session",
|
||||
"title": "获取 Session",
|
||||
"summary": "根据 ID 获取指定的 OpenCode 会话信息",
|
||||
"url": "/root/v1/cnb-dev",
|
||||
"source": "query-proxy-api"
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 从指定消息处 Fork 一个 OpenCode 会话
|
||||
*
|
||||
* @param data - Request parameters
|
||||
* @param data.sessionId - {string} Session ID
|
||||
* @param data.messageId - {string} 从该消息处开始 Fork
|
||||
* @param data.port - {number} OpenCode 服务端口,默认为 4096
|
||||
*/
|
||||
"fork": {
|
||||
"path": "opencode-session",
|
||||
"key": "fork",
|
||||
"id": "d43a8e2282412078",
|
||||
"description": "Fork OpenCode Session",
|
||||
"metadata": {
|
||||
"tags": [
|
||||
"session"
|
||||
],
|
||||
"args": {
|
||||
"sessionId": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"type": "string",
|
||||
"description": "Session ID"
|
||||
},
|
||||
"messageId": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"type": "string",
|
||||
"description": "从该消息处开始 Fork"
|
||||
},
|
||||
"port": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"description": "OpenCode 服务端口,默认为 4096",
|
||||
"type": "number",
|
||||
"optional": true
|
||||
}
|
||||
},
|
||||
"skill": "fork-opencode-session",
|
||||
"title": "Fork Session",
|
||||
"summary": "从指定消息处 Fork 一个 OpenCode 会话",
|
||||
"url": "/root/v1/cnb-dev",
|
||||
"source": "query-proxy-api"
|
||||
}
|
||||
}
|
||||
}
|
||||
} as const;
|
||||
|
||||
126
src/pages/chat-dev/components/OpencodeChat.tsx
Normal file
126
src/pages/chat-dev/components/OpencodeChat.tsx
Normal file
@@ -0,0 +1,126 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import { FileIcon, FolderIcon, RefreshCwIcon, TrashIcon } from 'lucide-react';
|
||||
import { useChatDevStore } from '../store';
|
||||
import { useCodeGraphStore } from '@/pages/code-graph/store';
|
||||
|
||||
export const OpencodeChat = () => {
|
||||
const {
|
||||
question, projectInfo,
|
||||
sessionId, isLoading,
|
||||
saveSessionInfo, loadSessionInfo, fetchSession, fetchMessages, clearSession,
|
||||
setData,
|
||||
} = useChatDevStore(
|
||||
useShallow((s) => ({
|
||||
question: s.question,
|
||||
projectInfo: s.projectInfo,
|
||||
setData: s.setData,
|
||||
sessionId: s.sessionId,
|
||||
isLoading: s.isLoading,
|
||||
saveSessionInfo: s.saveSessionInfo,
|
||||
loadSessionInfo: s.loadSessionInfo,
|
||||
fetchSession: s.fetchSession,
|
||||
fetchMessages: s.fetchMessages,
|
||||
clearSession: s.clearSession,
|
||||
})),
|
||||
);
|
||||
const codeGraphStore = useCodeGraphStore(useShallow((s) => ({
|
||||
createQuestion: s.createQuestion,
|
||||
url: s.url,
|
||||
})));
|
||||
|
||||
// 初始化后尝试从 sessionStorage 恢复 opencode session 并加载历史消息
|
||||
useEffect(() => {
|
||||
if (!codeGraphStore.url) return;
|
||||
const info = loadSessionInfo();
|
||||
if (info) {
|
||||
fetchSession(info.sessionId);
|
||||
fetchMessages(info.sessionId);
|
||||
}
|
||||
}, [codeGraphStore.url]);
|
||||
|
||||
const relativePath = projectInfo
|
||||
? (projectInfo.filepath || '').replace((projectInfo.projectPath || '') + '/', '') || '/'
|
||||
: null;
|
||||
|
||||
const onSend = async () => {
|
||||
const res = await codeGraphStore.createQuestion({
|
||||
question,
|
||||
projectPath: projectInfo?.projectPath,
|
||||
engine: 'opencode',
|
||||
sessionId: sessionId || undefined,
|
||||
});
|
||||
console.log(res);
|
||||
if (res?.code === 200 && res?.data) {
|
||||
const { sessionId: newSessionId, messageId: newMessageId } = res.data as any;
|
||||
if (newSessionId) {
|
||||
saveSessionInfo(newSessionId, newMessageId || '');
|
||||
fetchMessages(newSessionId);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='w-full max-w-2xl rounded-xl border border-white/10 bg-slate-900 shadow-2xl flex flex-col'>
|
||||
{/* Session 信息栏 */}
|
||||
{sessionId && (
|
||||
<div className='flex items-center gap-2 px-4 py-2 border-b border-white/10'>
|
||||
<span className='flex items-center gap-1 text-xs text-slate-400 ml-auto'>
|
||||
<span className='truncate max-w-[160px]' title={sessionId}>
|
||||
Session: {sessionId.slice(0, 8)}...
|
||||
</span>
|
||||
<button
|
||||
title='刷新消息'
|
||||
onClick={() => fetchMessages(sessionId)}
|
||||
className='p-1 rounded hover:text-emerald-400 transition-colors'>
|
||||
<RefreshCwIcon className='size-3' />
|
||||
</button>
|
||||
<button
|
||||
title='清除 Session'
|
||||
onClick={clearSession}
|
||||
className='p-1 rounded hover:text-red-400 transition-colors'>
|
||||
<TrashIcon className='size-3' />
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 内容区 */}
|
||||
<div className='px-4 py-4 flex flex-col gap-4'>
|
||||
{/* 节点信息 */}
|
||||
{relativePath && (
|
||||
<div className='flex items-center gap-2 px-3 py-2 rounded-lg bg-slate-800/60 border border-white/5 min-w-0'>
|
||||
<FileIcon className='size-4 shrink-0 text-slate-400' />
|
||||
<span className='text-xs text-slate-300 font-mono truncate' title={relativePath}>
|
||||
{relativePath}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{projectInfo?.projectPath && !relativePath && (
|
||||
<div className='flex items-center gap-2 px-3 py-2 rounded-lg bg-slate-800/60 border border-white/5 min-w-0'>
|
||||
<FolderIcon className='size-4 shrink-0 text-slate-400' />
|
||||
<span className='text-xs text-slate-300 font-mono truncate'>
|
||||
{projectInfo.projectPath}
|
||||
</span>
|
||||
</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={6}
|
||||
placeholder='请输入内容...'
|
||||
value={question}
|
||||
onChange={(e) => setData({ question: e.target.value })}
|
||||
/>
|
||||
|
||||
<button
|
||||
disabled={isLoading}
|
||||
className='w-full rounded-lg bg-emerald-600 hover:bg-emerald-500 active:bg-emerald-700 disabled:opacity-50 disabled:cursor-not-allowed text-white text-sm font-medium py-2 transition-colors'
|
||||
onClick={onSend}>
|
||||
{isLoading ? '处理中...' : '发送'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,35 +1,36 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useSearch } from '@tanstack/react-router';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import { BotIcon, FileIcon, FolderIcon } from 'lucide-react';
|
||||
import { BotIcon } from 'lucide-react';
|
||||
import { useChatDevStore } from './store';
|
||||
import { BOT_KEYS, BotKey } from '@/pages/code-graph/store/bot-helper';
|
||||
import openclawSvg from '@/pages/code-graph/assets/openclaw.svg';
|
||||
import opencodePng from '@/pages/code-graph/assets/opencode.png';
|
||||
import { useCodeGraphStore } from '../code-graph/store';
|
||||
import { useLayoutStore } from '../auth/store';
|
||||
import { OpencodeChat } from './components/OpencodeChat';
|
||||
|
||||
const BOT_ICONS: Record<BotKey, string> = {
|
||||
openclaw: openclawSvg,
|
||||
opencode: opencodePng,
|
||||
};
|
||||
|
||||
const BOT_LABELS: Record<BotKey, string> = {
|
||||
openclaw: 'Openclaw',
|
||||
opencode: 'Opencode',
|
||||
};
|
||||
|
||||
export const App = () => {
|
||||
const { timestamp } = useSearch({ from: '/chat-dev' });
|
||||
const { question, engine, projectInfo, initFromTimestamp, setData } = useChatDevStore(
|
||||
const { engine, initFromTimestamp, setData } = useChatDevStore(
|
||||
useShallow((s) => ({
|
||||
question: s.question,
|
||||
engine: s.engine,
|
||||
projectInfo: s.projectInfo,
|
||||
initFromTimestamp: s.initFromTimestamp,
|
||||
setData: s.setData,
|
||||
})),
|
||||
);
|
||||
const layoutStore = useLayoutStore(useShallow((s) => ({
|
||||
me: s.me,
|
||||
})));
|
||||
const layoutStore = useLayoutStore(useShallow((s) => ({ me: s.me })));
|
||||
const codeGraphStore = useCodeGraphStore(useShallow((s) => ({
|
||||
createQuestion: s.createQuestion,
|
||||
init: s.init,
|
||||
})));
|
||||
|
||||
@@ -37,84 +38,45 @@ export const App = () => {
|
||||
if (!layoutStore.me?.username) return;
|
||||
codeGraphStore.init(layoutStore.me, { load: false });
|
||||
}, [layoutStore.me]);
|
||||
|
||||
useEffect(() => {
|
||||
if (timestamp) {
|
||||
initFromTimestamp(timestamp);
|
||||
}
|
||||
}, [timestamp]);
|
||||
|
||||
const relativePath = projectInfo
|
||||
? (projectInfo.filepath || '').replace((projectInfo.projectPath || '') + '/', '') || '/'
|
||||
: null;
|
||||
const onSend = async () => {
|
||||
if (projectInfo) {
|
||||
const res = await codeGraphStore.createQuestion({
|
||||
question,
|
||||
projectPath: projectInfo.projectPath,
|
||||
engine,
|
||||
});
|
||||
console.log(res);
|
||||
}
|
||||
}
|
||||
return (
|
||||
<div className='h-full bg-slate-950 text-slate-100 flex flex-col items-center py-10 px-4'>
|
||||
<div className='w-full max-w-2xl rounded-xl border border-white/10 bg-slate-900 shadow-2xl flex flex-col'>
|
||||
{/* 标题栏 */}
|
||||
<div className='flex items-center gap-2 px-4 py-3 border-b border-white/10'>
|
||||
<BotIcon className='size-4 text-emerald-400' />
|
||||
<span className='text-sm font-medium text-slate-100'>AI 助手</span>
|
||||
</div>
|
||||
|
||||
{/* 内容区 */}
|
||||
<div className='px-4 py-4 flex flex-col gap-4'>
|
||||
{/* 节点信息 + Bot 切换 */}
|
||||
<div className='flex items-center gap-2'>
|
||||
{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'>
|
||||
<FileIcon className='size-4 shrink-0 text-slate-400' />
|
||||
<span className='text-xs text-slate-300 font-mono truncate' title={relativePath}>
|
||||
{relativePath}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{projectInfo?.projectPath && !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'>
|
||||
<FolderIcon className='size-4 shrink-0 text-slate-400' />
|
||||
<span className='text-xs text-slate-300 font-mono truncate'>
|
||||
{projectInfo.projectPath}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{/* Bot 切换按钮组 */}
|
||||
<div className='flex items-center gap-1 shrink-0 ml-auto'>
|
||||
<div className='h-full bg-slate-950 text-slate-100 flex flex-col items-center py-10 px-4 overflow-auto'>
|
||||
<div className='w-full max-w-2xl flex flex-col gap-4'>
|
||||
{/* Bot 切换按钮 —— 最顶层 */}
|
||||
<div className='flex items-center gap-2 px-1'>
|
||||
<BotIcon className='size-4 text-emerald-400 shrink-0' />
|
||||
<span className='text-sm font-medium text-slate-400 mr-1'>Bot</span>
|
||||
<div className='flex items-center gap-1'>
|
||||
{BOT_KEYS.map((key) => (
|
||||
<button
|
||||
key={key}
|
||||
title={key}
|
||||
onClick={() => setData({ engine: key })}
|
||||
className={`p-1.5 rounded-lg border transition-colors ${engine === key
|
||||
? 'border-emerald-500/60 bg-emerald-500/10'
|
||||
: 'border-white/5 bg-slate-800/60 opacity-40 hover:opacity-70'
|
||||
className={`flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg border text-xs font-medium transition-colors ${
|
||||
engine === key
|
||||
? 'border-emerald-500/60 bg-emerald-500/10 text-emerald-300'
|
||||
: 'border-white/5 bg-slate-800/60 text-slate-400 opacity-50 hover:opacity-80'
|
||||
}`}>
|
||||
<img src={BOT_ICONS[key]} alt={key} className='size-5 object-contain' />
|
||||
<img src={BOT_ICONS[key]} alt={key} className='size-4 object-contain' />
|
||||
{BOT_LABELS[key]}
|
||||
</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={6}
|
||||
placeholder='请输入内容...'
|
||||
value={question}
|
||||
onChange={(e) => setData({ question: e.target.value })}
|
||||
/>
|
||||
|
||||
<button 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' onClick={onSend}>
|
||||
发送
|
||||
</button>
|
||||
{/* Bot 对应的聊天面板 */}
|
||||
{engine === 'opencode' && <OpencodeChat />}
|
||||
{engine === 'openclaw' && (
|
||||
<div className='w-full rounded-xl border border-white/10 bg-slate-900 shadow-2xl flex items-center justify-center py-16 text-slate-500 text-sm'>
|
||||
Openclaw 即将支持,敬请期待
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,35 +1,116 @@
|
||||
import { create } from 'zustand';
|
||||
import { BotKey } from '@/pages/code-graph/store/bot-helper';
|
||||
import { queryApi as opencodeApi } from '@/modules/opencode-api';
|
||||
import { getApiUrl } from '@/pages/code-graph/store';
|
||||
export type ChatDevData = {
|
||||
question: string;
|
||||
engine: BotKey;
|
||||
projectInfo: {
|
||||
filepath: string;
|
||||
projectPath: string;
|
||||
kind: 'file' | 'dir' | 'root';
|
||||
} | null;
|
||||
};
|
||||
|
||||
export type SessionMessage = {
|
||||
info: any;
|
||||
parts: Array<any>;
|
||||
};
|
||||
|
||||
export type SessionInfo = {
|
||||
sessionId: string;
|
||||
messageId: string;
|
||||
};
|
||||
|
||||
type ChatDevState = {
|
||||
question: string;
|
||||
engine: BotKey;
|
||||
projectInfo: {
|
||||
filepath: string;
|
||||
projectPath: string;
|
||||
kind?: 'file' | 'dir' | 'root';
|
||||
} | null;
|
||||
sessionId: string | null;
|
||||
messageId: string | null;
|
||||
messages: SessionMessage[];
|
||||
isLoading: boolean;
|
||||
setData: (data: Partial<ChatDevData>) => void;
|
||||
initFromTimestamp: (timestamp: string) => void;
|
||||
saveSessionInfo: (sessionId: string, messageId: string) => void;
|
||||
loadSessionInfo: () => SessionInfo | null;
|
||||
fetchSession: (sessionId: string) => Promise<void>;
|
||||
fetchMessages: (sessionId: string) => Promise<void>;
|
||||
clearSession: () => void;
|
||||
};
|
||||
|
||||
const SESSION_KEY_PREFIX = 'chat-dev-';
|
||||
const SESSION_KEY = 'chat-dev';
|
||||
const SESSION_INFO_KEY = 'chat-dev-session-info';
|
||||
|
||||
export const useChatDevStore = create<ChatDevState>()((set) => ({
|
||||
export const useChatDevStore = create<ChatDevState>()((set, get) => ({
|
||||
question: '',
|
||||
engine: 'opencode',
|
||||
projectInfo: null,
|
||||
sessionId: null,
|
||||
messageId: null,
|
||||
messages: [],
|
||||
isLoading: false,
|
||||
|
||||
setData: (data) => set((s) => ({ ...s, ...data })),
|
||||
|
||||
saveSessionInfo: (sessionId: string, messageId: string) => {
|
||||
const info: SessionInfo = { sessionId, messageId };
|
||||
sessionStorage.setItem(SESSION_INFO_KEY, JSON.stringify(info));
|
||||
set({ sessionId, messageId });
|
||||
},
|
||||
|
||||
loadSessionInfo: () => {
|
||||
const raw = sessionStorage.getItem(SESSION_INFO_KEY);
|
||||
if (!raw) return null;
|
||||
try {
|
||||
return JSON.parse(raw) as SessionInfo;
|
||||
} catch {
|
||||
sessionStorage.removeItem(SESSION_INFO_KEY);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
fetchSession: async (sessionId: string) => {
|
||||
set({ isLoading: true });
|
||||
try {
|
||||
const url = getApiUrl();
|
||||
const res = await opencodeApi['opencode-session'].get({ id: sessionId }, { url });
|
||||
if (res.code === 200 && res.data) {
|
||||
set({ sessionId });
|
||||
}
|
||||
} catch {
|
||||
// 静默失败,不影响主流程
|
||||
} finally {
|
||||
set({ isLoading: false });
|
||||
}
|
||||
},
|
||||
|
||||
fetchMessages: async (sessionId: string) => {
|
||||
set({ isLoading: true });
|
||||
try {
|
||||
const url = getApiUrl();
|
||||
const res = await opencodeApi['opencode-session'].messages({ sessionId: sessionId }, { url });
|
||||
if (res.code === 200 && res.data) {
|
||||
const msgs: SessionMessage[] = Array.isArray(res.data) ? res.data : (res.data as any).list ?? [];
|
||||
set({ messages: msgs });
|
||||
}
|
||||
} catch {
|
||||
// 静默失败,不影响主流程
|
||||
} finally {
|
||||
set({ isLoading: false });
|
||||
}
|
||||
},
|
||||
|
||||
clearSession: () => {
|
||||
sessionStorage.removeItem(SESSION_INFO_KEY);
|
||||
set({ sessionId: null, messageId: null, messages: [] });
|
||||
},
|
||||
|
||||
initFromTimestamp: (timestamp: string) => {
|
||||
const key = SESSION_KEY_PREFIX + timestamp;
|
||||
|
||||
|
||||
@@ -31,23 +31,25 @@ export function BotHelperModal() {
|
||||
projectInfo: s.projectInfo,
|
||||
})),
|
||||
);
|
||||
const { nodeInfoData, createQuestion } = useCodeGraphStore(useShallow((s) => ({
|
||||
nodeInfoData: s.nodeInfoData,
|
||||
const { createQuestion } = useCodeGraphStore(useShallow((s) => ({
|
||||
createQuestion: s.createQuestion,
|
||||
})));
|
||||
const location = useLocation();
|
||||
console.log('BotHelperModal render', location);
|
||||
console.log('BotHelperModal render', botHelperStore.projectInfo);
|
||||
const basename = location.publicHref.replace(location.href, '');
|
||||
const relativePath = nodeInfoData
|
||||
? nodeInfoData.fullPath.replace((nodeInfoData.projectPath || '') + '/', '') || '/'
|
||||
const projectPath = botHelperStore.projectInfo?.projectPath;
|
||||
const filepath = botHelperStore.projectInfo?.filepath;
|
||||
const relativePath = botHelperStore.projectInfo
|
||||
? filepath?.replace((projectPath || '') + '/', '') || '/'
|
||||
: null;
|
||||
|
||||
const kind = botHelperStore.projectInfo?.kind || 'dir';
|
||||
const handleConfirm = async () => {
|
||||
if (nodeInfoData) {
|
||||
const projectPath = botHelperStore.projectInfo?.projectPath;
|
||||
if (projectPath) {
|
||||
const res = await createQuestion({
|
||||
question: botHelperStore.input,
|
||||
projectPath: nodeInfoData.projectPath,
|
||||
filePath: nodeInfoData.kind === 'file' ? nodeInfoData.fullPath : undefined,
|
||||
projectPath,
|
||||
filePath: botHelperStore.projectInfo?.kind === 'file' ? botHelperStore.projectInfo.filepath : undefined,
|
||||
engine: botHelperStore.activeKey,
|
||||
});
|
||||
console.log(res);
|
||||
@@ -102,9 +104,9 @@ export function BotHelperModal() {
|
||||
<div className='px-4 py-4 flex flex-col gap-4'>
|
||||
{/* 节点信息 + 按钮组 */}
|
||||
<div className='flex items-center gap-2'>
|
||||
{nodeInfoData && relativePath && (
|
||||
{botHelperStore.projectInfo && 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} />
|
||||
<NodeIcon kind={kind} color={kind === 'dir' ? 'blue' : 'green'} />
|
||||
<span
|
||||
className='text-xs text-slate-300 font-mono truncate'
|
||||
title={relativePath}>
|
||||
|
||||
@@ -185,7 +185,7 @@ interface Code3DGraphProps {
|
||||
files: FileProjectData[];
|
||||
className?: string;
|
||||
type?: "map" | 'minimap';
|
||||
onProjectFocus?: (projectPath: string) => void;
|
||||
onProjectFocus?: string;
|
||||
}
|
||||
|
||||
// ─── 主组件 ───────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -13,7 +13,7 @@ function KindIcon({ kind, color }: { kind: NodeInfoData['kind']; color: string }
|
||||
}
|
||||
|
||||
const KIND_LABEL: Record<NodeInfoData['kind'], string> = {
|
||||
root: '项目根目录',
|
||||
root: '根目录',
|
||||
dir: '目录',
|
||||
file: '文件',
|
||||
};
|
||||
@@ -57,7 +57,6 @@ export const NodeInfoContainer = () => {
|
||||
codePodOpen: s.codePodOpen,
|
||||
nodeInfoData: s.nodeInfoData,
|
||||
nodeInfoPos: s.nodeInfoPos,
|
||||
setNodeInfoOpen: s.setNodeInfo,
|
||||
closeNodeInfo: s.closeNodeInfo,
|
||||
setCodePodOpen: s.setCodePodOpen,
|
||||
setCodePodAttrs: s.setCodePodAttrs,
|
||||
@@ -175,6 +174,7 @@ export const NodeInfoContainer = () => {
|
||||
botHelperStore.setProjectInfo({
|
||||
filepath: nodeInfoData.fullPath,
|
||||
projectPath: nodeInfoData.projectPath,
|
||||
kind: nodeInfoData.kind,
|
||||
});
|
||||
botHelperStore.openModal();
|
||||
}}
|
||||
|
||||
@@ -1,14 +1,22 @@
|
||||
import { useState, useRef, useEffect, useCallback } from 'react';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import { GripVertical, X } from 'lucide-react';
|
||||
import { GripVertical, X, Sparkles, MoreHorizontal, StopCircle, PlayCircle } from 'lucide-react';
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu';
|
||||
import { useCodeGraphStore } from '../store';
|
||||
import { FileProjectData } from '../modules/tree';
|
||||
import { useBotHelperStore } from '../store/bot-helper';
|
||||
|
||||
interface ProjectPanelProps {
|
||||
onProjectClick?: (projectPath: string, files: FileProjectData[]) => void;
|
||||
onOpenCodePod?: (projectPath: string) => void;
|
||||
onStopProject?: (projectPath: string) => void;
|
||||
}
|
||||
|
||||
export function ProjectPanel({ onProjectClick }: ProjectPanelProps) {
|
||||
export function ProjectPanel({
|
||||
onProjectClick,
|
||||
onOpenCodePod,
|
||||
onStopProject,
|
||||
}: ProjectPanelProps) {
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const [position, setPosition] = useState({ x: 20, y: 80 });
|
||||
const dragOffset = useRef({ x: 0, y: 0 });
|
||||
@@ -20,9 +28,69 @@ export function ProjectPanel({ onProjectClick }: ProjectPanelProps) {
|
||||
files: s.files,
|
||||
})),
|
||||
);
|
||||
const botHelperStore = useBotHelperStore(useShallow((s) => ({
|
||||
openModal: s.openModal,
|
||||
setProjectInfo: s.setProjectInfo,
|
||||
})));
|
||||
const codeGraphStore = useCodeGraphStore(useShallow((s) => {
|
||||
return {
|
||||
setCodePodAttrs: s.setCodePodAttrs,
|
||||
setCodePodOpen: s.setCodePodOpen,
|
||||
setNodeInfo: s.setNodeInfo
|
||||
};
|
||||
}));
|
||||
|
||||
const activeProjects = projects.filter((p) => p.status === 'active');
|
||||
|
||||
const handleMouseDown = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
if (!panelRef.current) return;
|
||||
const rect = panelRef.current.getBoundingClientRect();
|
||||
dragOffset.current = {
|
||||
x: e.clientX - rect.left,
|
||||
y: e.clientY - rect.top,
|
||||
};
|
||||
setIsDragging(true);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const handleMouseMove = useCallback(
|
||||
(e: MouseEvent) => {
|
||||
if (!isDragging) return;
|
||||
setPosition({
|
||||
x: e.clientX - dragOffset.current.x,
|
||||
y: e.clientY - dragOffset.current.y,
|
||||
});
|
||||
},
|
||||
[isDragging],
|
||||
);
|
||||
|
||||
const handleMouseUp = useCallback(() => {
|
||||
setIsDragging(false);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (isDragging) {
|
||||
document.addEventListener('mousemove', handleMouseMove);
|
||||
document.addEventListener('mouseup', handleMouseUp);
|
||||
}
|
||||
return () => {
|
||||
document.removeEventListener('mousemove', handleMouseMove);
|
||||
document.removeEventListener('mouseup', handleMouseUp);
|
||||
};
|
||||
}, [isDragging, handleMouseMove, handleMouseUp]);
|
||||
|
||||
const handleProjectClick = useCallback(
|
||||
(projectPath: string) => {
|
||||
const project = projects.find((p) => p.path === projectPath);
|
||||
if (project && onProjectClick) {
|
||||
onProjectClick(projectPath, files);
|
||||
}
|
||||
},
|
||||
[projects, files, onProjectClick],
|
||||
);
|
||||
|
||||
const [isLargeScreen, setIsLargeScreen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -67,16 +135,78 @@ export function ProjectPanel({ onProjectClick }: ProjectPanelProps) {
|
||||
<div className='flex flex-col py-1 max-h-80 overflow-y-auto'>
|
||||
{activeProjects.map((project) => {
|
||||
const projectName = project.name || project.path.split('/').pop() || project.path;
|
||||
const nodeInfoData = {
|
||||
fullPath: project.path,
|
||||
projectPath: project.path,
|
||||
kind: 'dir',
|
||||
}
|
||||
return (
|
||||
<button
|
||||
<div
|
||||
key={project.path}
|
||||
onClick={() => handleProjectClick(project.path)}
|
||||
className='flex items-center gap-2 px-3 py-2 text-left text-sm text-slate-300 hover:bg-white/5 hover:text-white transition-colors border-l-2 border-transparent hover:border-indigo-500'
|
||||
className='flex items-center gap-2 px-3 py-2 text-left text-sm text-slate-300 hover:bg-white/5 hover:text-white transition-colors border-l-2 border-transparent hover:border-indigo-500 group'
|
||||
>
|
||||
<span className='truncate flex-1' title={projectName}>
|
||||
<button
|
||||
onClick={() => handleProjectClick(project.path)}
|
||||
className='flex-1 flex items-center gap-2 text-left'
|
||||
>
|
||||
<span className='truncate' title={projectName}>
|
||||
{projectName}
|
||||
</span>
|
||||
</button>
|
||||
<div className='flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity'>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
botHelperStore.setProjectInfo({
|
||||
filepath: nodeInfoData.fullPath,
|
||||
projectPath: nodeInfoData.projectPath,
|
||||
kind: 'dir',
|
||||
});
|
||||
botHelperStore.openModal();
|
||||
e.stopPropagation();
|
||||
}}
|
||||
title='AI 助手'
|
||||
className='p-1 rounded-md text-slate-400 hover:text-slate-200 hover:bg-white/10 transition-colors'>
|
||||
<Sparkles className='w-3.5 h-3.5' />
|
||||
</button>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger >
|
||||
<div
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
title='更多'
|
||||
className='p-1 rounded-md text-slate-400 hover:text-slate-200 hover:bg-white/10 transition-colors'>
|
||||
<MoreHorizontal className='w-3.5 h-3.5' />
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align='end' className='bg-slate-900 border-white/10'>
|
||||
<DropdownMenuItem
|
||||
onClick={() => onStopProject?.(project.path)}
|
||||
className='text-slate-300 focus:bg-white/5 focus:text-slate-100 cursor-pointer'>
|
||||
<StopCircle className='w-4 h-4 mr-2' />
|
||||
停止
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
const projectPath = project.path;
|
||||
codeGraphStore.setCodePodAttrs({
|
||||
label: projectPath.split('/').pop() || projectPath,
|
||||
size: 0,
|
||||
color: '',
|
||||
x: 0,
|
||||
y: 0,
|
||||
fullPath: projectPath,
|
||||
projectPath,
|
||||
kind: 'dir',
|
||||
});
|
||||
codeGraphStore.setCodePodOpen(true);
|
||||
}}
|
||||
className='text-slate-300 focus:bg-white/5 focus:text-slate-100 cursor-pointer'>
|
||||
<PlayCircle className='w-4 h-4 mr-2' />
|
||||
打开 CodePod
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import { DatabaseIcon, RefreshCw } from 'lucide-react';
|
||||
import { CodePod } from './components/CodePod';
|
||||
import { useCodeGraphStore } from './store';
|
||||
import CodeGraphView from './components/CodeGraph';
|
||||
import { Code3DGraph } from './components/Code3DGraph';
|
||||
import { NodeInfoContainer } from './components/NodeInfo';
|
||||
import { ProjectDialog } from './components/ProjectDialog';
|
||||
import { BotHelperModal } from './components/BotHelperModal';
|
||||
import { ProjectPanel } from './components/ProjectPanel';
|
||||
import { useBotHelperStore } from './store/bot-helper';
|
||||
import { useLayoutStore } from '../auth/store';
|
||||
import type { FileProjectData } from './modules/tree';
|
||||
import { useCodeGraphStore } from './store';
|
||||
import { Code3DGraph } from './components/Code3DGraph';
|
||||
import CodeGraphView from './components/CodeGraph';
|
||||
import CodePod from './components/CodePod';
|
||||
import NodeInfoContainer from './components/NodeInfo';
|
||||
import { ProjectDialog } from './components/ProjectDialog';
|
||||
type ViewMode = '2d' | '3d';
|
||||
|
||||
export default function CodeGraphPage() {
|
||||
@@ -19,16 +20,18 @@ export default function CodeGraphPage() {
|
||||
const layoutStore = useLayoutStore(useShallow((s) => ({
|
||||
me: s.me,
|
||||
})));
|
||||
const { codePodOpen, setCodePodOpen, codePodAttrs, setProjectDialogOpen, init, files, fetchProjects, setFiles } = useCodeGraphStore(
|
||||
const { codePodOpen, setCodePodOpen, codePodAttrs, setCodePodAttrs, setProjectDialogOpen, init, files, fetchProjects, setFiles, toggleProjectStatus } = useCodeGraphStore(
|
||||
useShallow((s) => ({
|
||||
files: s.files,
|
||||
setFiles: s.setFiles,
|
||||
codePodOpen: s.codePodOpen,
|
||||
setCodePodOpen: s.setCodePodOpen,
|
||||
codePodAttrs: s.codePodAttrs,
|
||||
setCodePodAttrs: s.setCodePodAttrs,
|
||||
setProjectDialogOpen: s.setProjectDialogOpen,
|
||||
init: s.init,
|
||||
fetchProjects: s.fetchProjects,
|
||||
toggleProjectStatus: s.toggleProjectStatus,
|
||||
})),
|
||||
);
|
||||
|
||||
@@ -92,7 +95,23 @@ export default function CodeGraphPage() {
|
||||
{viewMode === '3d' ? (
|
||||
<>
|
||||
<Code3DGraph files={files} className='h-full' onProjectFocus={projectFocus ?? undefined} />
|
||||
<ProjectPanel onProjectClick={handleProjectClick} />
|
||||
<ProjectPanel
|
||||
onProjectClick={handleProjectClick}
|
||||
onOpenCodePod={(projectPath) => {
|
||||
setCodePodAttrs({
|
||||
label: projectPath.split('/').pop() || projectPath,
|
||||
size: 0,
|
||||
color: '',
|
||||
x: 0,
|
||||
y: 0,
|
||||
fullPath: projectPath,
|
||||
projectPath,
|
||||
kind: 'dir',
|
||||
});
|
||||
setCodePodOpen(true);
|
||||
}}
|
||||
onStopProject={(projectPath) => toggleProjectStatus(projectPath)}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<CodeGraphView files={files} className='h-full' />
|
||||
|
||||
@@ -14,8 +14,9 @@ type BotHelperState = {
|
||||
projectInfo: {
|
||||
filepath: string;
|
||||
projectPath: string;
|
||||
kind: 'file' | 'dir' | 'root';
|
||||
} | null;
|
||||
setProjectInfo: (info: { filepath: string; projectPath: string } | null) => void;
|
||||
setProjectInfo: (info: { filepath: string; projectPath: string; kind: 'file' | 'dir' | 'root' } | null) => void;
|
||||
openModal: () => void;
|
||||
closeModal: () => void;
|
||||
};
|
||||
@@ -30,6 +31,8 @@ export const useBotHelperStore = create<BotHelperState>()((set) => ({
|
||||
setActiveKey: (key) => set({ activeKey: key }),
|
||||
projectInfo: null,
|
||||
setProjectInfo: (info) => set({ projectInfo: info }),
|
||||
openModal: () => set({ open: true }),
|
||||
openModal: () => set({
|
||||
open: true,
|
||||
}),
|
||||
closeModal: () => set({ open: false, input: '', projectInfo: null }),
|
||||
}));
|
||||
|
||||
@@ -64,7 +64,7 @@ type State = {
|
||||
projectPath?: string; // 项目路径,必填
|
||||
getContent?: boolean; // 是否获取文件内容,默认为 false
|
||||
}) => Promise<Result<{ list: FileProjectData[] }>>;
|
||||
createQuestion: (opts: { question: string, projectPath: string, filePath?: string, engine?: 'openclaw' | 'opencode', }) => any;
|
||||
createQuestion: (opts: { question: string, projectPath?: string, filePath?: string, engine?: 'openclaw' | 'opencode', sessionId?: string }) => any;
|
||||
saveFile: (filepath: string, content: string) => Promise<void>;
|
||||
isMobile: boolean;
|
||||
setIsMobile: (isMobile: boolean) => void;
|
||||
@@ -274,11 +274,13 @@ export const useCodeGraphStore = create<State>()((set, get) => ({
|
||||
isMobile: false,
|
||||
setIsMobile: (isMobile) => set({ isMobile }),
|
||||
createQuestion: async (opts) => {
|
||||
const { question, projectPath, filePath, engine = 'opencode' } = opts;
|
||||
const { question, projectPath, filePath, engine = 'opencode', sessionId } = opts;
|
||||
const url = get().url
|
||||
let q = `
|
||||
${question}
|
||||
项目路径: ${projectPath}`
|
||||
let q = question;
|
||||
if (projectPath) {
|
||||
q += `
|
||||
项目路径: ${projectPath}`;
|
||||
}
|
||||
if (filePath && filePath !== projectPath) {
|
||||
q += `
|
||||
文件路径: ${filePath}`;
|
||||
@@ -286,6 +288,7 @@ export const useCodeGraphStore = create<State>()((set, get) => ({
|
||||
const res = await opencodeApi["opencode-cnb"].question({
|
||||
question: q,
|
||||
directory: projectPath,
|
||||
sessionId: sessionId || undefined,
|
||||
}, {
|
||||
url,
|
||||
timeout: 60 * 1000 * 15, // 15分钟
|
||||
@@ -293,3 +296,8 @@ export const useCodeGraphStore = create<State>()((set, get) => ({
|
||||
return res;
|
||||
},
|
||||
}));
|
||||
|
||||
export const getApiUrl = () => {
|
||||
const store = useCodeGraphStore.getState();
|
||||
return store.url || API_URL;
|
||||
}
|
||||
Reference in New Issue
Block a user