feat: 增强项目 API,添加项目路径和文件过滤功能;更新 BotHelperModal 和 NodeInfo 组件,优化状态管理和交互体验;改进 ProjectDialog,支持多选和项目初始化功能
This commit is contained in:
@@ -276,7 +276,6 @@ const api = {
|
|||||||
*
|
*
|
||||||
* @param data - Request parameters
|
* @param data - Request parameters
|
||||||
* @param data.q - {string} 搜索关键词,选填;留空或不传则返回全部文件
|
* @param data.q - {string} 搜索关键词,选填;留空或不传则返回全部文件
|
||||||
* @param data.projectPath - {string} 按项目根目录路径过滤,仅返回该项目下的文件,选填
|
|
||||||
* @param data.filepath - {string} 按文件绝对路径过滤,选填
|
* @param data.filepath - {string} 按文件绝对路径过滤,选填
|
||||||
* @param data.repo - {string} 按代码仓库标识过滤(如 owner/repo),选填
|
* @param data.repo - {string} 按代码仓库标识过滤(如 owner/repo),选填
|
||||||
* @param data.title - {string} 按人工标注的标题字段过滤,选填
|
* @param data.title - {string} 按人工标注的标题字段过滤,选填
|
||||||
@@ -287,6 +286,7 @@ const api = {
|
|||||||
* @param data.sort - {array} 排序规则数组,格式为 ["字段:asc"] 或 ["字段:desc"],选填,当 q 为空时默认为 ["projectPath:asc"]
|
* @param data.sort - {array} 排序规则数组,格式为 ["字段:asc"] 或 ["字段:desc"],选填,当 q 为空时默认为 ["projectPath:asc"]
|
||||||
* @param data.limit - {number} 返回结果数量上限,选填,当 q 为空时默认为 1000
|
* @param data.limit - {number} 返回结果数量上限,选填,当 q 为空时默认为 1000
|
||||||
* @param data.getContent - {boolean} 是否返回文件内容,默认为 false;如果为 true,则在结果中包含 content 字段,内容以 base64 编码返回,适用于前端预览或下载场景
|
* @param data.getContent - {boolean} 是否返回文件内容,默认为 false;如果为 true,则在结果中包含 content 字段,内容以 base64 编码返回,适用于前端预览或下载场景
|
||||||
|
* @param data.projects - {array} 按项目名称列表过滤,选填,默认不穿,只过滤当前工作区的项目
|
||||||
*/
|
*/
|
||||||
"files": {
|
"files": {
|
||||||
"path": "project-search",
|
"path": "project-search",
|
||||||
@@ -300,12 +300,120 @@ const api = {
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"projectPath": {
|
"filepath": {
|
||||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
"description": "按项目根目录路径过滤,仅返回该项目下的文件,选填",
|
"description": "按文件绝对路径过滤,选填",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
|
"repo": {
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"description": "按代码仓库标识过滤(如 owner/repo),选填",
|
||||||
|
"type": "string",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"description": "按人工标注的标题字段过滤,选填",
|
||||||
|
"type": "string",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"tags": {
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"description": "按人工标注的标签列表过滤,选填",
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"summary": {
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"description": "按人工标注的摘要字段过滤,选填",
|
||||||
|
"type": "string",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"description": "按人工标注的描述字段过滤,选填",
|
||||||
|
"type": "string",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"link": {
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"description": "按人工标注的外部链接字段过滤,选填",
|
||||||
|
"type": "string",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"sort": {
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"description": "排序规则数组,格式为 [\"字段:asc\"] 或 [\"字段:desc\"],选填,当 q 为空时默认为 [\"projectPath:asc\"]",
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"limit": {
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"description": "返回结果数量上限,选填,当 q 为空时默认为 1000",
|
||||||
|
"type": "number",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"getContent": {
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"description": "是否返回文件内容,默认为 false;如果为 true,则在结果中包含 content 字段,内容以 base64 编码返回,适用于前端预览或下载场景",
|
||||||
|
"type": "boolean",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"projects": {
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"description": "按项目名称列表过滤,选填,默认不穿,只过滤当前工作区的项目",
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"url": "/root/v1/cnb-dev",
|
||||||
|
"source": "query-proxy-api"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 在已索引的项目文件中执行全文搜索,支持按仓库、目录、标签等字段过滤,以及自定义排序和数量限制
|
||||||
|
*
|
||||||
|
* @param data - Request parameters
|
||||||
|
* @param data.q - {string} 搜索关键词,选填;留空或不传则返回全部文件
|
||||||
|
* @param data.projectPath - {string} 按项目根目录路径过滤,仅返回该项目下的文件,必填
|
||||||
|
* @param data.filepath - {string} 按文件绝对路径过滤,选填
|
||||||
|
* @param data.repo - {string} 按代码仓库标识过滤(如 owner/repo),选填
|
||||||
|
* @param data.title - {string} 按人工标注的标题字段过滤,选填
|
||||||
|
* @param data.tags - {array} 按人工标注的标签列表过滤,选填
|
||||||
|
* @param data.summary - {string} 按人工标注的摘要字段过滤,选填
|
||||||
|
* @param data.description - {string} 按人工标注的描述字段过滤,选填
|
||||||
|
* @param data.link - {string} 按人工标注的外部链接字段过滤,选填
|
||||||
|
* @param data.sort - {array} 排序规则数组,格式为 ["字段:asc"] 或 ["字段:desc"],选填,当 q 为空时默认为 ["projectPath:asc"]
|
||||||
|
* @param data.limit - {number} 返回结果数量上限,选填,当 q 为空时默认为 1000
|
||||||
|
* @param data.getContent - {boolean} 是否返回文件内容,默认为 false;如果为 true,则在结果中包含 content 字段,内容以 base64 编码返回,适用于前端预览或下载场景
|
||||||
|
*/
|
||||||
|
"search": {
|
||||||
|
"path": "project-search",
|
||||||
|
"key": "search",
|
||||||
|
"description": "在已索引的项目文件中执行全文搜索,支持按仓库、目录、标签等字段过滤,以及自定义排序和数量限制",
|
||||||
|
"metadata": {
|
||||||
|
"args": {
|
||||||
|
"q": {
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"description": "搜索关键词,选填;留空或不传则返回全部文件",
|
||||||
|
"type": "string",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"projectPath": {
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"type": "string",
|
||||||
|
"description": "按项目根目录路径过滤,仅返回该项目下的文件,必填"
|
||||||
|
},
|
||||||
"filepath": {
|
"filepath": {
|
||||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
"description": "按文件绝对路径过滤,选填",
|
"description": "按文件绝对路径过滤,选填",
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { BotIcon, XIcon, FileIcon, FolderIcon, DatabaseIcon } from 'lucide-react';
|
import { BotIcon, XIcon, FileIcon, FolderIcon, DatabaseIcon, MoreHorizontalIcon } from 'lucide-react';
|
||||||
|
import { getDynamicBasename, wrapBasename } from '@/modules/basename';
|
||||||
import { useBotHelperStore, BOT_KEYS, BotKey } from '../store/bot-helper';
|
import { useBotHelperStore, BOT_KEYS, BotKey } from '../store/bot-helper';
|
||||||
import { useShallow } from 'zustand/react/shallow';
|
import { useShallow } from 'zustand/react/shallow';
|
||||||
import { useCodeGraphStore, NodeInfoData } from '../store';
|
import { useCodeGraphStore, NodeInfoData } from '../store';
|
||||||
@@ -18,7 +19,7 @@ function NodeIcon({ kind, color }: { kind: NodeInfoData['kind']; color: string }
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function BotHelperModal() {
|
export function BotHelperModal() {
|
||||||
const { open, input, setInput, closeModal, activeKey, setActiveKey } = useBotHelperStore(
|
const botHelperStore = useBotHelperStore(
|
||||||
useShallow((s) => ({
|
useShallow((s) => ({
|
||||||
open: s.open,
|
open: s.open,
|
||||||
input: s.input,
|
input: s.input,
|
||||||
@@ -40,16 +41,16 @@ export function BotHelperModal() {
|
|||||||
const handleConfirm = async () => {
|
const handleConfirm = async () => {
|
||||||
if (nodeInfoData) {
|
if (nodeInfoData) {
|
||||||
const res = await createQuestion({
|
const res = await createQuestion({
|
||||||
question: input,
|
question: botHelperStore.input,
|
||||||
projectPath: nodeInfoData.projectPath,
|
projectPath: nodeInfoData.projectPath,
|
||||||
engine: activeKey,
|
engine: botHelperStore.activeKey,
|
||||||
});
|
});
|
||||||
console.log(res);
|
console.log(res);
|
||||||
}
|
}
|
||||||
closeModal();
|
botHelperStore.closeModal();
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!open) return null;
|
if (!botHelperStore.open) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='fixed inset-0 z-[200] flex items-center justify-center'>
|
<div className='fixed inset-0 z-[200] flex items-center justify-center'>
|
||||||
@@ -62,12 +63,23 @@ export function BotHelperModal() {
|
|||||||
<BotIcon className='size-4 text-emerald-400' />
|
<BotIcon className='size-4 text-emerald-400' />
|
||||||
<span className='text-sm font-medium text-slate-100'>AI 助手</span>
|
<span className='text-sm font-medium text-slate-100'>AI 助手</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div className='flex items-center gap-1'>
|
||||||
<button
|
<button
|
||||||
onClick={closeModal}
|
onClick={() => {
|
||||||
|
const timestamp = Date.now();
|
||||||
|
window.open(`/code-graph?timestamp=${timestamp}`, '_blank');
|
||||||
|
}}
|
||||||
|
className='text-slate-500 hover:text-slate-200 transition-colors p-1 rounded hover:bg-white/10'
|
||||||
|
title='新窗口打开'>
|
||||||
|
<MoreHorizontalIcon className='size-4' />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={botHelperStore.closeModal}
|
||||||
className='text-slate-500 hover:text-slate-200 transition-colors'>
|
className='text-slate-500 hover:text-slate-200 transition-colors'>
|
||||||
<XIcon className='size-4' />
|
<XIcon className='size-4' />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
{/* 内容区 */}
|
{/* 内容区 */}
|
||||||
<div className='px-4 py-4 flex flex-col gap-4'>
|
<div className='px-4 py-4 flex flex-col gap-4'>
|
||||||
{/* 节点信息 + 按钮组 */}
|
{/* 节点信息 + 按钮组 */}
|
||||||
@@ -88,8 +100,8 @@ export function BotHelperModal() {
|
|||||||
<button
|
<button
|
||||||
key={key}
|
key={key}
|
||||||
title={key}
|
title={key}
|
||||||
onClick={() => setActiveKey(key)}
|
onClick={() => botHelperStore.setActiveKey(key)}
|
||||||
className={`p-1.5 rounded-lg border transition-colors ${activeKey === key
|
className={`p-1.5 rounded-lg border transition-colors ${botHelperStore.activeKey === key
|
||||||
? 'border-emerald-500/60 bg-emerald-500/10'
|
? 'border-emerald-500/60 bg-emerald-500/10'
|
||||||
: 'border-white/5 bg-slate-800/60 opacity-40 hover:opacity-70'
|
: 'border-white/5 bg-slate-800/60 opacity-40 hover:opacity-70'
|
||||||
}`}>
|
}`}>
|
||||||
@@ -102,8 +114,8 @@ export function BotHelperModal() {
|
|||||||
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'
|
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}
|
rows={5}
|
||||||
placeholder='请输入内容...'
|
placeholder='请输入内容...'
|
||||||
value={input}
|
value={botHelperStore.input}
|
||||||
onChange={(e) => setInput(e.target.value)}
|
onChange={(e) => botHelperStore.setInput(e.target.value)}
|
||||||
autoFocus
|
autoFocus
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -180,17 +180,18 @@ function buildGraph3DData(files: FileProjectData[]): Graph3DData {
|
|||||||
interface Code3DGraphProps {
|
interface Code3DGraphProps {
|
||||||
files: FileProjectData[];
|
files: FileProjectData[];
|
||||||
className?: string;
|
className?: string;
|
||||||
|
type?: "map" | 'minimap';
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── 主组件 ───────────────────────────────────────────────────────────────────
|
// ─── 主组件 ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export function Code3DGraph({ files, className }: Code3DGraphProps) {
|
export function Code3DGraph({ files, className, type }: Code3DGraphProps) {
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
const graphRef = useRef<ForceGraph3DInstance | null>(null);
|
const graphRef = useRef<ForceGraph3DInstance | null>(null);
|
||||||
const searchBoxRef = useRef<NodeSearchBoxHandle>(null);
|
const searchBoxRef = useRef<NodeSearchBoxHandle>(null);
|
||||||
const [searchIndex, setSearchIndex] = useState<NodeSearchEntry[]>([]);
|
const [searchIndex, setSearchIndex] = useState<NodeSearchEntry[]>([]);
|
||||||
|
|
||||||
const { setNodeInfo, selectedNodeId } = useCodeGraphStore(
|
const codeGraphStore = useCodeGraphStore(
|
||||||
useShallow((s) => ({
|
useShallow((s) => ({
|
||||||
setNodeInfo: s.setNodeInfo,
|
setNodeInfo: s.setNodeInfo,
|
||||||
selectedNodeId: s.nodeInfoData?.fullPath ?? null,
|
selectedNodeId: s.nodeInfoData?.fullPath ?? null,
|
||||||
@@ -200,8 +201,8 @@ export function Code3DGraph({ files, className }: Code3DGraphProps) {
|
|||||||
// 用 ref 避免 nodeThreeObject 回调的陈旧闭包
|
// 用 ref 避免 nodeThreeObject 回调的陈旧闭包
|
||||||
const selectedNodeIdRef = useRef<string | null>(null);
|
const selectedNodeIdRef = useRef<string | null>(null);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
selectedNodeIdRef.current = selectedNodeId;
|
selectedNodeIdRef.current = codeGraphStore.selectedNodeId;
|
||||||
}, [selectedNodeId]);
|
}, [codeGraphStore.selectedNodeId]);
|
||||||
|
|
||||||
// 节点跳转
|
// 节点跳转
|
||||||
const focusNode = useCallback((nodeKey: string) => {
|
const focusNode = useCallback((nodeKey: string) => {
|
||||||
@@ -301,7 +302,7 @@ export function Code3DGraph({ files, className }: Code3DGraphProps) {
|
|||||||
// 等相机飞行动画结束(800ms)后,用节点投影坐标打开 NodeInfo
|
// 等相机飞行动画结束(800ms)后,用节点投影坐标打开 NodeInfo
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const pos = nodeToScreenPos(n);
|
const pos = nodeToScreenPos(n);
|
||||||
setNodeInfo(
|
codeGraphStore.setNodeInfo(
|
||||||
{
|
{
|
||||||
label: n.label,
|
label: n.label,
|
||||||
fullPath: n.fullPath,
|
fullPath: n.fullPath,
|
||||||
@@ -345,7 +346,7 @@ export function Code3DGraph({ files, className }: Code3DGraphProps) {
|
|||||||
return () => {
|
return () => {
|
||||||
ro.disconnect();
|
ro.disconnect();
|
||||||
};
|
};
|
||||||
}, [files, focusNode, setNodeInfo, nodeToScreenPos]);
|
}, [files, focusNode, codeGraphStore.setNodeInfo, nodeToScreenPos]);
|
||||||
|
|
||||||
// 卸载时销毁
|
// 卸载时销毁
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -361,7 +362,7 @@ export function Code3DGraph({ files, className }: Code3DGraphProps) {
|
|||||||
if (graph) {
|
if (graph) {
|
||||||
graph.refresh();
|
graph.refresh();
|
||||||
}
|
}
|
||||||
}, [selectedNodeId]);
|
}, [codeGraphStore.selectedNodeId]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`relative w-full h-full overflow-hidden ${className ?? ''}`}>
|
<div className={`relative w-full h-full overflow-hidden ${className ?? ''}`}>
|
||||||
@@ -378,7 +379,7 @@ export function Code3DGraph({ files, className }: Code3DGraphProps) {
|
|||||||
if (!n) return;
|
if (!n) return;
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const pos = nodeToScreenPos(n);
|
const pos = nodeToScreenPos(n);
|
||||||
setNodeInfo(
|
codeGraphStore.setNodeInfo(
|
||||||
{
|
{
|
||||||
label: n.label,
|
label: n.label,
|
||||||
fullPath: n.fullPath,
|
fullPath: n.fullPath,
|
||||||
|
|||||||
@@ -184,6 +184,12 @@ export function CodePod({ open, onClose, nodeAttrs }: CodePodProps) {
|
|||||||
getFiles: s.getFiles,
|
getFiles: s.getFiles,
|
||||||
saveFile: s.saveFile,
|
saveFile: s.saveFile,
|
||||||
})));
|
})));
|
||||||
|
const botHelperStore = useBotHelperStore(useShallow((s) => ({
|
||||||
|
openModal: s.openModal,
|
||||||
|
setProjectInfo: s.setProjectInfo,
|
||||||
|
})));
|
||||||
|
const [showFileName, setShowFileName] = useState(true); // 是否显示文件名(移动端默认隐藏)
|
||||||
|
const [showProjectPath, setShowProjectPath] = useState(false); // 是否显示项目路径(移动端默认隐藏)
|
||||||
const projectPath = nodeAttrs?.projectPath ?? '';
|
const projectPath = nodeAttrs?.projectPath ?? '';
|
||||||
const displayPath = rootPath.startsWith(projectPath)
|
const displayPath = rootPath.startsWith(projectPath)
|
||||||
? rootPath.slice(projectPath.length) || '/'
|
? rootPath.slice(projectPath.length) || '/'
|
||||||
@@ -233,7 +239,11 @@ export function CodePod({ open, onClose, nodeAttrs }: CodePodProps) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleAIOpen = () => {
|
const handleAIOpen = () => {
|
||||||
useBotHelperStore.getState().openModal();
|
botHelperStore.setProjectInfo({
|
||||||
|
filepath: nodeAttrs?.fullPath ?? '',
|
||||||
|
projectPath: nodeAttrs?.projectPath ?? '',
|
||||||
|
});
|
||||||
|
botHelperStore.openModal();
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -275,8 +285,8 @@ export function CodePod({ open, onClose, nodeAttrs }: CodePodProps) {
|
|||||||
{/* 标题:显示相对路径(去掉 projectPath) */}
|
{/* 标题:显示相对路径(去掉 projectPath) */}
|
||||||
<div className='px-3 py-2.5 border-b border-white/10 shrink-0 min-w-[14rem]'>
|
<div className='px-3 py-2.5 border-b border-white/10 shrink-0 min-w-[14rem]'>
|
||||||
<div className='text-[10px] text-slate-500 mb-0.5'>路径</div>
|
<div className='text-[10px] text-slate-500 mb-0.5'>路径</div>
|
||||||
<div className='text-xs font-semibold text-slate-300 truncate' title={rootPath}>
|
<div className='text-xs font-semibold text-slate-300 truncate' title={rootPath} onClick={() => setShowProjectPath((v) => !v)} style={{ cursor: 'pointer' }}>
|
||||||
{displayPath}
|
{showProjectPath ? rootPath : displayPath}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* 目录树 */}
|
{/* 目录树 */}
|
||||||
@@ -299,7 +309,7 @@ export function CodePod({ open, onClose, nodeAttrs }: CodePodProps) {
|
|||||||
{/* 编辑器区域 */}
|
{/* 编辑器区域 */}
|
||||||
<div className='flex-1 flex flex-col min-w-0'>
|
<div className='flex-1 flex flex-col min-w-0'>
|
||||||
{/* 编辑器标题栏 */}
|
{/* 编辑器标题栏 */}
|
||||||
<div className='flex items-center gap-2 px-4 py-2.5 border-b border-white/10 text-xs text-slate-400 shrink-0'>
|
<div className='flex items-center gap-2 px-4 py-2.5 border-b border-white/10 text-xs text-slate-400 shrink-0 mr-6'>
|
||||||
{/* 侧边栏切换按钮 */}
|
{/* 侧边栏切换按钮 */}
|
||||||
<button
|
<button
|
||||||
onClick={() => setSidebarOpen((v) => !v)}
|
onClick={() => setSidebarOpen((v) => !v)}
|
||||||
@@ -307,8 +317,8 @@ export function CodePod({ open, onClose, nodeAttrs }: CodePodProps) {
|
|||||||
title={sidebarOpen ? '收起侧边栏' : '展开侧边栏'}>
|
title={sidebarOpen ? '收起侧边栏' : '展开侧边栏'}>
|
||||||
{sidebarOpen ? '◀' : '▶'}
|
{sidebarOpen ? '◀' : '▶'}
|
||||||
</button>
|
</button>
|
||||||
<span className='truncate text-slate-200'>
|
<span className='truncate text-slate-200' title={selectedFile?.filepath ?? nodeAttrs.fullPath} onClick={() => setShowFileName((v) => !v)} style={{ cursor: 'pointer' }}>
|
||||||
{selectedFile?.filepath ?? nodeAttrs.fullPath}
|
{showFileName ? filename : selectedFile?.filepath ?? nodeAttrs.fullPath}
|
||||||
</span>
|
</span>
|
||||||
<div className='ml-1 flex items-center gap-1 shrink-0'>
|
<div className='ml-1 flex items-center gap-1 shrink-0'>
|
||||||
{loading && <span className='text-slate-500 text-xs'>加载中…</span>}
|
{loading && <span className='text-slate-500 text-xs'>加载中…</span>}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { FileIcon, FolderIcon, DatabaseIcon, XIcon, MoveIcon, SquarePenIcon, Bot
|
|||||||
import { useCodeGraphStore, NodeInfoData } from '../store';
|
import { useCodeGraphStore, NodeInfoData } from '../store';
|
||||||
import { useBotHelperStore } from '../store/bot-helper';
|
import { useBotHelperStore } from '../store/bot-helper';
|
||||||
import { useShallow } from 'zustand/react/shallow';
|
import { useShallow } from 'zustand/react/shallow';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
|
||||||
function KindIcon({ kind, color }: { kind: NodeInfoData['kind']; color: string }) {
|
function KindIcon({ kind, color }: { kind: NodeInfoData['kind']; color: string }) {
|
||||||
const cls = 'size-4 shrink-0';
|
const cls = 'size-4 shrink-0';
|
||||||
@@ -21,7 +22,6 @@ export function NodeInfo() {
|
|||||||
const codeGraphStore = useCodeGraphStore(
|
const codeGraphStore = useCodeGraphStore(
|
||||||
useShallow((s) => ({
|
useShallow((s) => ({
|
||||||
nodeInfoData: s.nodeInfoData,
|
nodeInfoData: s.nodeInfoData,
|
||||||
|
|
||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
const projectPath = codeGraphStore.nodeInfoData?.projectPath || '';
|
const projectPath = codeGraphStore.nodeInfoData?.projectPath || '';
|
||||||
@@ -51,14 +51,18 @@ export function NodeInfo() {
|
|||||||
</div></>)
|
</div></>)
|
||||||
}
|
}
|
||||||
export const NodeInfoContainer = () => {
|
export const NodeInfoContainer = () => {
|
||||||
const { nodeInfoOpen, nodeInfoData, nodeInfoPos, closeNodeInfo, setCodePodOpen, setCodePodAttrs } = useCodeGraphStore(
|
const { nodeInfoOpen, nodeInfoData, nodeInfoPos, closeNodeInfo, codePodOpen, setCodePodOpen, setCodePodAttrs, isMobile, setIsMobile } = useCodeGraphStore(
|
||||||
useShallow((s) => ({
|
useShallow((s) => ({
|
||||||
nodeInfoOpen: s.nodeInfoOpen,
|
nodeInfoOpen: s.nodeInfoOpen,
|
||||||
|
codePodOpen: s.codePodOpen,
|
||||||
nodeInfoData: s.nodeInfoData,
|
nodeInfoData: s.nodeInfoData,
|
||||||
nodeInfoPos: s.nodeInfoPos,
|
nodeInfoPos: s.nodeInfoPos,
|
||||||
|
setNodeInfoOpen: s.setNodeInfo,
|
||||||
closeNodeInfo: s.closeNodeInfo,
|
closeNodeInfo: s.closeNodeInfo,
|
||||||
setCodePodOpen: s.setCodePodOpen,
|
setCodePodOpen: s.setCodePodOpen,
|
||||||
setCodePodAttrs: s.setCodePodAttrs,
|
setCodePodAttrs: s.setCodePodAttrs,
|
||||||
|
isMobile: s.isMobile,
|
||||||
|
setIsMobile: s.setIsMobile,
|
||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -75,18 +79,21 @@ export const NodeInfoContainer = () => {
|
|||||||
kind: nodeInfoData.kind,
|
kind: nodeInfoData.kind,
|
||||||
fileId: nodeInfoData.fileId,
|
fileId: nodeInfoData.fileId,
|
||||||
});
|
});
|
||||||
|
|
||||||
setCodePodOpen(true);
|
setCodePodOpen(true);
|
||||||
// 移到左上角,避免遮挡编辑器
|
// 移到左上角,避免遮挡编辑器
|
||||||
setPinLeft(true);
|
setPinLeft(true);
|
||||||
setOffset({ x: 0, y: 0 });
|
setOffset({ x: 0, y: 0 });
|
||||||
};
|
};
|
||||||
|
|
||||||
const openBotModal = useBotHelperStore((s) => s.openModal);
|
const botHelperStore = useBotHelperStore(useShallow((s) => ({
|
||||||
|
openModal: s.openModal,
|
||||||
|
setProjectInfo: s.setProjectInfo,
|
||||||
|
})));
|
||||||
|
|
||||||
// 拖拽偏移
|
// 拖拽偏移
|
||||||
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); // 编辑后固定到右下角
|
||||||
const [isMobile, setIsMobile] = useState(false);
|
|
||||||
|
|
||||||
// 检测屏幕大小
|
// 检测屏幕大小
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -143,11 +150,11 @@ export const NodeInfoContainer = () => {
|
|||||||
: { left: nodeInfoPos.x + offset.x + 40, top: nodeInfoPos.y + offset.y - 40 };
|
: { left: nodeInfoPos.x + offset.x + 40, top: nodeInfoPos.y + offset.y - 40 };
|
||||||
|
|
||||||
const name = nodeInfoData.fullPath.split('/').pop() || nodeInfoData.label;
|
const name = nodeInfoData.fullPath.split('/').pop() || nodeInfoData.label;
|
||||||
const projectPath = nodeInfoData.projectPath || '';
|
|
||||||
const relativePath = nodeInfoData.fullPath.replace(projectPath + '/', '') || '/';
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`fixed z-50 rounded-xl border border-white/10 bg-slate-900/95 backdrop-blur-sm shadow-2xl select-none ${isMobile ? 'w-full h-full' : 'w-72'}`}
|
className={clsx(`fixed z-50 rounded-xl border border-white/10 bg-slate-900/95 backdrop-blur-sm shadow-2xl select-none`, isMobile ? 'w-full h-full' : 'w-72',
|
||||||
|
isMobile && codePodOpen && 'hidden' // 移动端如果 CodePod 打开了,就隐藏 NodeInfo,避免遮挡
|
||||||
|
)}
|
||||||
style={posStyle}
|
style={posStyle}
|
||||||
onMouseDown={isMobile ? undefined : onMouseDown}>
|
onMouseDown={isMobile ? undefined : onMouseDown}>
|
||||||
{/* 标题栏 */}
|
{/* 标题栏 */}
|
||||||
@@ -164,7 +171,13 @@ export const NodeInfoContainer = () => {
|
|||||||
<SquarePenIcon className='size-3.5' />
|
<SquarePenIcon className='size-3.5' />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={openBotModal}
|
onClick={() => {
|
||||||
|
botHelperStore.setProjectInfo({
|
||||||
|
filepath: nodeInfoData.fullPath,
|
||||||
|
projectPath: nodeInfoData.projectPath,
|
||||||
|
});
|
||||||
|
botHelperStore.openModal();
|
||||||
|
}}
|
||||||
title='AI 助手'
|
title='AI 助手'
|
||||||
className='ml-1 text-slate-500 hover:text-emerald-400 transition-colors'>
|
className='ml-1 text-slate-500 hover:text-emerald-400 transition-colors'>
|
||||||
<BotIcon className='size-3.5' />
|
<BotIcon className='size-3.5' />
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useShallow } from 'zustand/react/shallow';
|
import { useShallow } from 'zustand/react/shallow';
|
||||||
import { FolderOpenIcon, PlusIcon, Trash2Icon, RefreshCwIcon, PlayCircleIcon, StopCircleIcon, FolderIcon, AlertCircleIcon, CircleOffIcon } from 'lucide-react';
|
import { FolderOpenIcon, PlusIcon, Trash2Icon, RefreshCwIcon, PlayCircleIcon, StopCircleIcon, FolderIcon, AlertCircleIcon, CircleOffIcon, DownloadIcon, ListTodoIcon, CheckSquareIcon } from 'lucide-react';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '@/components/ui/dialog';
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '@/components/ui/dialog';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
|
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
||||||
import { useCodeGraphStore } from '../store';
|
import { useCodeGraphStore } from '../store';
|
||||||
|
|
||||||
export function ProjectDialog() {
|
export function ProjectDialog() {
|
||||||
@@ -18,6 +19,7 @@ export function ProjectDialog() {
|
|||||||
addProject,
|
addProject,
|
||||||
removeProject,
|
removeProject,
|
||||||
toggleProjectStatus,
|
toggleProjectStatus,
|
||||||
|
initProject,
|
||||||
} = useCodeGraphStore(
|
} = useCodeGraphStore(
|
||||||
useShallow((s) => ({
|
useShallow((s) => ({
|
||||||
projectDialogOpen: s.projectDialogOpen,
|
projectDialogOpen: s.projectDialogOpen,
|
||||||
@@ -28,6 +30,7 @@ export function ProjectDialog() {
|
|||||||
addProject: s.addProject,
|
addProject: s.addProject,
|
||||||
removeProject: s.removeProject,
|
removeProject: s.removeProject,
|
||||||
toggleProjectStatus: s.toggleProjectStatus,
|
toggleProjectStatus: s.toggleProjectStatus,
|
||||||
|
initProject: s.initProject,
|
||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -50,6 +53,64 @@ export function ProjectDialog() {
|
|||||||
const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false);
|
const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false);
|
||||||
const [pendingDeleteProject, setPendingDeleteProject] = useState<{ path: string; name?: string } | null>(null);
|
const [pendingDeleteProject, setPendingDeleteProject] = useState<{ path: string; name?: string } | null>(null);
|
||||||
|
|
||||||
|
// 初始化确认弹窗
|
||||||
|
const [initConfirmOpen, setInitConfirmOpen] = useState(false);
|
||||||
|
const [initLoading, setInitLoading] = useState(false);
|
||||||
|
|
||||||
|
// 多选模式
|
||||||
|
const [multiSelectMode, setMultiSelectMode] = useState(false);
|
||||||
|
const [selectedProjects, setSelectedProjects] = useState<string[]>([]);
|
||||||
|
|
||||||
|
// 切换多选模式
|
||||||
|
const toggleMultiSelectMode = () => {
|
||||||
|
if (multiSelectMode) {
|
||||||
|
// 退出多选模式时清空选择
|
||||||
|
setSelectedProjects([]);
|
||||||
|
}
|
||||||
|
setMultiSelectMode(!multiSelectMode);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 切换项目选中状态
|
||||||
|
const toggleProjectSelection = (path: string) => {
|
||||||
|
setSelectedProjects((prev) =>
|
||||||
|
prev.includes(path) ? prev.filter((p) => p !== path) : [...prev, path]
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 全选
|
||||||
|
const selectAll = () => {
|
||||||
|
setSelectedProjects(projects.map((p) => p.path));
|
||||||
|
};
|
||||||
|
|
||||||
|
// 取消全选
|
||||||
|
const deselectAll = () => {
|
||||||
|
setSelectedProjects([]);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 全部启动
|
||||||
|
const handleStartAll = async () => {
|
||||||
|
for (const path of selectedProjects) {
|
||||||
|
const project = projects.find((p) => p.path === path);
|
||||||
|
if (project && project.status !== 'active') {
|
||||||
|
await toggleProjectStatus(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setSelectedProjects([]);
|
||||||
|
setMultiSelectMode(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 全部关闭
|
||||||
|
const handleStopAll = async () => {
|
||||||
|
for (const path of selectedProjects) {
|
||||||
|
const project = projects.find((p) => p.path === path);
|
||||||
|
if (project && project.status === 'active') {
|
||||||
|
await toggleProjectStatus(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setSelectedProjects([]);
|
||||||
|
setMultiSelectMode(false);
|
||||||
|
};
|
||||||
|
|
||||||
const handleAdd = async () => {
|
const handleAdd = async () => {
|
||||||
if (!newPath.trim()) return;
|
if (!newPath.trim()) return;
|
||||||
setAddLoading(true);
|
setAddLoading(true);
|
||||||
@@ -114,27 +175,42 @@ export function ProjectDialog() {
|
|||||||
setPendingDeleteProject(null);
|
setPendingDeleteProject(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 确认初始化
|
||||||
|
const handleConfirmInit = async () => {
|
||||||
|
setInitLoading(true);
|
||||||
|
await initProject();
|
||||||
|
setInitLoading(false);
|
||||||
|
setInitConfirmOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 取消初始化
|
||||||
|
const handleCancelInit = () => {
|
||||||
|
setInitConfirmOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={projectDialogOpen} onOpenChange={handleOpenChange}>
|
<Dialog open={projectDialogOpen} onOpenChange={handleOpenChange}>
|
||||||
<DialogContent className='sm:max-w-lg bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900 text-slate-100 border border-white/10 shadow-2xl'>
|
<DialogContent className='w-[calc(100%-2rem)] sm:max-w-lg max-h-[85vh] flex flex-col bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900 text-slate-100 border border-white/10 shadow-2xl overflow-hidden'>
|
||||||
{/* 装饰性背景 */}
|
{/* 装饰性背景 */}
|
||||||
<div className='absolute inset-0 overflow-hidden pointer-events-none'>
|
<div className='absolute inset-0 overflow-hidden pointer-events-none'>
|
||||||
<div className='absolute -top-20 -right-20 w-40 h-40 bg-indigo-500/10 rounded-full blur-3xl' />
|
<div className='absolute -top-20 -right-20 w-40 h-40 bg-indigo-500/10 rounded-full blur-3xl' />
|
||||||
<div className='absolute -bottom-20 -left-20 w-40 h-40 bg-purple-500/10 rounded-full blur-3xl' />
|
<div className='absolute -bottom-20 -left-20 w-40 h-40 bg-purple-500/10 rounded-full blur-3xl' />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DialogHeader className='relative'>
|
<DialogHeader className='relative shrink-0'>
|
||||||
<DialogTitle className='flex items-center gap-3 text-slate-100 text-lg'>
|
<DialogTitle className='flex items-center gap-3 text-slate-100 text-lg'>
|
||||||
<div className='p-2 rounded-xl bg-gradient-to-br from-indigo-500 to-purple-600 shadow-lg shadow-indigo-500/25'>
|
<div className='p-2 rounded-xl bg-gradient-to-br from-indigo-500 to-purple-600 shadow-lg shadow-indigo-500/25 shrink-0'>
|
||||||
<FolderOpenIcon className='w-5 h-5 text-white' />
|
<FolderOpenIcon className='w-5 h-5 text-white' />
|
||||||
</div>
|
</div>
|
||||||
项目管理
|
项目管理
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogDescription className='text-slate-400 ml-14'>
|
<DialogDescription className='text-slate-400 ml-0'>
|
||||||
管理已注册的代码分析项目,支持实时监听文件变更
|
管理已注册的代码分析项目,支持实时监听文件变更
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
|
{/* 内容区域 - 可滚动 */}
|
||||||
|
<div className='flex-1 overflow-y-auto space-y-4 pr-1 scrollbar-thin scrollbar-thumb-slate-700 scrollbar-track-transparent -mx-2 px-2'>
|
||||||
{/* 新增项目 */}
|
{/* 新增项目 */}
|
||||||
{showAddProject && (
|
{showAddProject && (
|
||||||
<div className='relative space-y-3 rounded-xl bg-white/5 p-4 border border-white/10 shadow-lg backdrop-blur-sm'>
|
<div className='relative space-y-3 rounded-xl bg-white/5 p-4 border border-white/10 shadow-lg backdrop-blur-sm'>
|
||||||
@@ -241,6 +317,104 @@ export function ProjectDialog() {
|
|||||||
title='刷新'>
|
title='刷新'>
|
||||||
<RefreshCwIcon className={`w-4 h-4 ${projectsLoading ? 'animate-spin' : ''}`} />
|
<RefreshCwIcon className={`w-4 h-4 ${projectsLoading ? 'animate-spin' : ''}`} />
|
||||||
</button>
|
</button>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger>
|
||||||
|
<div
|
||||||
|
onClick={() => setInitConfirmOpen(true)}
|
||||||
|
className='text-slate-500 hover:text-indigo-400 transition-colors p-1.5 rounded-lg hover:bg-white/5 disabled:opacity-50'>
|
||||||
|
<DownloadIcon className={`w-4 h-4 ${initLoading ? 'animate-spin' : ''}`} />
|
||||||
|
</div>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>初始化 workspace/projects 仓库</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
{multiSelectMode ? (
|
||||||
|
<>
|
||||||
|
<div className='w-px h-5 bg-white/20 mx-1' />
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger>
|
||||||
|
<div
|
||||||
|
onClick={selectAll}
|
||||||
|
className='text-slate-500 hover:text-indigo-400 transition-colors p-1.5 rounded-lg hover:bg-white/5'
|
||||||
|
title='全选'>
|
||||||
|
<CheckSquareIcon className='w-4 h-4' />
|
||||||
|
</div>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>全选</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger>
|
||||||
|
<div
|
||||||
|
onClick={deselectAll}
|
||||||
|
className='text-slate-500 hover:text-indigo-400 transition-colors p-1.5 rounded-lg hover:bg-white/5'
|
||||||
|
title='取消全选'>
|
||||||
|
<ListTodoIcon className='w-4 h-4' />
|
||||||
|
</div>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>取消全选</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger>
|
||||||
|
<button
|
||||||
|
onClick={handleStartAll}
|
||||||
|
disabled={selectedProjects.length === 0}
|
||||||
|
className='text-slate-500 hover:text-green-400 transition-colors p-1.5 rounded-lg hover:bg-white/5 disabled:opacity-50'
|
||||||
|
title='全部启动'>
|
||||||
|
<PlayCircleIcon className='w-4 h-4' />
|
||||||
|
</button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>全部启动 ({selectedProjects.length})</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger>
|
||||||
|
<button
|
||||||
|
onClick={handleStopAll}
|
||||||
|
disabled={selectedProjects.length === 0}
|
||||||
|
className='text-slate-500 hover:text-red-400 transition-colors p-1.5 rounded-lg hover:bg-white/5 disabled:opacity-50'
|
||||||
|
title='全部关闭'>
|
||||||
|
<StopCircleIcon className='w-4 h-4' />
|
||||||
|
</button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>全部关闭 ({selectedProjects.length})</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger>
|
||||||
|
<div
|
||||||
|
onClick={toggleMultiSelectMode}
|
||||||
|
className='text-slate-500 hover:text-indigo-400 transition-colors p-1.5 rounded-lg hover:bg-white/5'
|
||||||
|
title='退出多选'>
|
||||||
|
<CheckSquareIcon className='w-4 h-4' />
|
||||||
|
</div>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>退出多选</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger>
|
||||||
|
<div
|
||||||
|
onClick={toggleMultiSelectMode}
|
||||||
|
className='text-slate-500 hover:text-indigo-400 transition-colors p-1.5 rounded-lg hover:bg-white/5'
|
||||||
|
title='多选'>
|
||||||
|
<CheckSquareIcon className='w-4 h-4' />
|
||||||
|
</div>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>多选</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -261,7 +435,19 @@ export function ProjectDialog() {
|
|||||||
{projects.map((p) => (
|
{projects.map((p) => (
|
||||||
<li
|
<li
|
||||||
key={p.path}
|
key={p.path}
|
||||||
className='group flex items-center gap-3 rounded-xl bg-white/5 px-4 py-3 border border-white/5 hover:bg-white/10 hover:border-white/10 transition-all duration-200'>
|
className={`group flex items-center gap-3 rounded-xl bg-white/5 px-4 py-3 border border-white/5 hover:bg-white/10 hover:border-white/10 transition-all duration-200 ${multiSelectMode && selectedProjects.includes(p.path) ? 'border-indigo-500/50 bg-indigo-500/10' : ''}`}>
|
||||||
|
{/* 多选框 */}
|
||||||
|
{multiSelectMode && (
|
||||||
|
<button
|
||||||
|
onClick={() => toggleProjectSelection(p.path)}
|
||||||
|
className={`shrink-0 w-5 h-5 rounded border-2 flex items-center justify-center transition-colors ${
|
||||||
|
selectedProjects.includes(p.path)
|
||||||
|
? 'bg-indigo-500 border-indigo-500 text-white'
|
||||||
|
: 'border-slate-500 hover:border-indigo-400'
|
||||||
|
}`}>
|
||||||
|
{selectedProjects.includes(p.path) && <CheckSquareIcon className='w-3 h-3' />}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
{/* 项目图标 */}
|
{/* 项目图标 */}
|
||||||
<div className={`shrink-0 p-2 rounded-lg ${p.status === 'active' ? 'bg-green-500/20' : p.status === 'unlive' ? 'bg-orange-500/20' : 'bg-slate-700/50'}`}>
|
<div className={`shrink-0 p-2 rounded-lg ${p.status === 'active' ? 'bg-green-500/20' : p.status === 'unlive' ? 'bg-orange-500/20' : 'bg-slate-700/50'}`}>
|
||||||
<FolderIcon className={`w-4 h-4 ${p.status === 'active' ? 'text-green-400' : p.status === 'unlive' ? 'text-orange-400' : 'text-slate-400'}`} />
|
<FolderIcon className={`w-4 h-4 ${p.status === 'active' ? 'text-green-400' : p.status === 'unlive' ? 'text-orange-400' : 'text-slate-400'}`} />
|
||||||
@@ -312,6 +498,7 @@ export function ProjectDialog() {
|
|||||||
</ul>
|
</ul>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|
||||||
{/* 状态切换确认弹窗 */}
|
{/* 状态切换确认弹窗 */}
|
||||||
@@ -382,6 +569,45 @@ export function ProjectDialog() {
|
|||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
|
{/* 初始化确认弹窗 */}
|
||||||
|
<Dialog open={initConfirmOpen} onOpenChange={(open) => !open && handleCancelInit()}>
|
||||||
|
<DialogContent className='sm:max-w-md bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900 text-slate-100 border border-white/10 shadow-2xl'>
|
||||||
|
<DialogHeader>
|
||||||
|
<div className='w-12 h-12 rounded-full flex items-center justify-center mx-auto mb-3 bg-indigo-500/20'>
|
||||||
|
<DownloadIcon className='w-6 h-6 text-indigo-400' />
|
||||||
|
</div>
|
||||||
|
<DialogTitle className='text-center text-lg font-semibold'>
|
||||||
|
初始化项目
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription className='text-center text-slate-400'>
|
||||||
|
确定要初始化 workspace/projects 仓库吗?初始化过程可能需要一些时间。
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<DialogFooter className='gap-2 sm:justify-center'>
|
||||||
|
<Button
|
||||||
|
variant='outline'
|
||||||
|
onClick={handleCancelInit}
|
||||||
|
disabled={initLoading}
|
||||||
|
className='bg-transparent border-white/20 text-slate-300 hover:bg-white/10 hover:border-white/30 flex-1'>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleConfirmInit}
|
||||||
|
disabled={initLoading}
|
||||||
|
className='bg-indigo-600 hover:bg-indigo-500 text-white flex-1 shadow-lg shadow-indigo-500/25 disabled:opacity-50'>
|
||||||
|
{initLoading ? (
|
||||||
|
<span className='flex items-center gap-2'>
|
||||||
|
<span className='w-3 h-3 border-2 border-white/30 border-t-white rounded-full animate-spin' />
|
||||||
|
初始化中…
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
'确认初始化'
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,11 @@ type BotHelperState = {
|
|||||||
setOpen: (open: boolean) => void;
|
setOpen: (open: boolean) => void;
|
||||||
setInput: (input: string) => void;
|
setInput: (input: string) => void;
|
||||||
setActiveKey: (key: BotKey) => void;
|
setActiveKey: (key: BotKey) => void;
|
||||||
|
projectInfo: {
|
||||||
|
filepath: string;
|
||||||
|
projectPath: string;
|
||||||
|
} | null;
|
||||||
|
setProjectInfo: (info: { filepath: string; projectPath: string } | null) => void;
|
||||||
openModal: () => void;
|
openModal: () => void;
|
||||||
closeModal: () => void;
|
closeModal: () => void;
|
||||||
};
|
};
|
||||||
@@ -21,7 +26,10 @@ export const useBotHelperStore = create<BotHelperState>()((set) => ({
|
|||||||
activeKey: 'opencode',
|
activeKey: 'opencode',
|
||||||
setOpen: (open) => set({ open }),
|
setOpen: (open) => set({ open }),
|
||||||
setInput: (input) => set({ input }),
|
setInput: (input) => set({ input }),
|
||||||
|
|
||||||
setActiveKey: (key) => set({ activeKey: key }),
|
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: '' }),
|
closeModal: () => set({ open: false, input: '', projectInfo: null }),
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -43,8 +43,10 @@ type State = {
|
|||||||
addProject: (filepath: string, name?: string, type?: 'filepath' | 'cnb-repo') => Promise<boolean>;
|
addProject: (filepath: string, name?: string, type?: 'filepath' | 'cnb-repo') => Promise<boolean>;
|
||||||
removeProject: (path: string) => Promise<void>;
|
removeProject: (path: string) => Promise<void>;
|
||||||
toggleProjectStatus: (path: string) => Promise<void>;
|
toggleProjectStatus: (path: string) => Promise<void>;
|
||||||
|
initProject: () => Promise<void>;
|
||||||
// NodeInfo 弹窗
|
// NodeInfo 弹窗
|
||||||
nodeInfoOpen: boolean;
|
nodeInfoOpen: boolean;
|
||||||
|
setNodeInfoOpen: (open: boolean) => void;
|
||||||
nodeInfoData: NodeInfoData | null;
|
nodeInfoData: NodeInfoData | null;
|
||||||
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;
|
||||||
@@ -60,6 +62,8 @@ type State = {
|
|||||||
}) => Promise<Result<{ list: FileProjectData[] }>>;
|
}) => Promise<Result<{ list: FileProjectData[] }>>;
|
||||||
createQuestion: (opts: { question: string, projectPath: string, engine?: 'openclaw' | 'opencode' }) => any;
|
createQuestion: (opts: { question: string, projectPath: string, engine?: 'openclaw' | 'opencode' }) => any;
|
||||||
saveFile: (filepath: string, content: string) => Promise<void>;
|
saveFile: (filepath: string, content: string) => Promise<void>;
|
||||||
|
isMobile: boolean;
|
||||||
|
setIsMobile: (isMobile: boolean) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useCodeGraphStore = create<State>()((set, get) => ({
|
export const useCodeGraphStore = create<State>()((set, get) => ({
|
||||||
@@ -117,6 +121,20 @@ export const useCodeGraphStore = create<State>()((set, get) => ({
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
initProject: async () => {
|
||||||
|
try {
|
||||||
|
const url = get().url || API_URL;
|
||||||
|
const res = await projectApi.project.init(undefined, { url });
|
||||||
|
if (res.code === 200) {
|
||||||
|
toast.success('项目初始化成功');
|
||||||
|
await get().loadProjects();
|
||||||
|
} else {
|
||||||
|
toast.error(res.message ?? '项目初始化失败');
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
toast.error('项目初始化失败');
|
||||||
|
}
|
||||||
|
},
|
||||||
removeProject: async (path) => {
|
removeProject: async (path) => {
|
||||||
try {
|
try {
|
||||||
const url = get().url || API_URL;
|
const url = get().url || API_URL;
|
||||||
@@ -165,6 +183,7 @@ export const useCodeGraphStore = create<State>()((set, get) => ({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
nodeInfoOpen: false,
|
nodeInfoOpen: false,
|
||||||
|
setNodeInfoOpen: (open) => set({ nodeInfoOpen: open }),
|
||||||
nodeInfoData: null,
|
nodeInfoData: null,
|
||||||
nodeInfoPos: { x: 0, y: 0 },
|
nodeInfoPos: { x: 0, y: 0 },
|
||||||
setNodeInfo: (data, pos) =>
|
setNodeInfo: (data, pos) =>
|
||||||
@@ -221,6 +240,8 @@ export const useCodeGraphStore = create<State>()((set, get) => ({
|
|||||||
toast.error('保存失败');
|
toast.error('保存失败');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
isMobile: false,
|
||||||
|
setIsMobile: (isMobile) => set({ isMobile }),
|
||||||
createQuestion: async (opts) => {
|
createQuestion: async (opts) => {
|
||||||
const { question, projectPath, engine = 'opencode' } = opts;
|
const { question, projectPath, engine = 'opencode' } = opts;
|
||||||
const url = get().url
|
const url = get().url
|
||||||
@@ -231,7 +252,8 @@ export const useCodeGraphStore = create<State>()((set, get) => ({
|
|||||||
question: q,
|
question: q,
|
||||||
directory: projectPath,
|
directory: projectPath,
|
||||||
}, {
|
}, {
|
||||||
url
|
url,
|
||||||
|
timeout: 60 * 1000 * 15, // 15分钟
|
||||||
});
|
});
|
||||||
return res;
|
return res;
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user