Compare commits
6 Commits
56222183d1
...
48b915c4a4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
48b915c4a4 | ||
|
|
6b72054525 | ||
|
|
a65e7b236d | ||
|
|
2a26a3943f | ||
|
|
7bf7c5099b | ||
|
|
155a126c6e |
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "code-graph",
|
"name": "code-graph",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.1",
|
"version": "0.0.2",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"basename": "/root/code-graph",
|
"basename": "/root/code-graph",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"ui": "bunx shadcn@latest add ",
|
"ui": "bunx shadcn@latest add ",
|
||||||
"pub": "envision deploy ./dist -k code-graph -v 0.0.1 -y y -u"
|
"pub": "envision deploy ./dist -k code-graph -v 0.0.2 -y y -u"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist"
|
"dist"
|
||||||
|
|||||||
179
src/components/FileDescriptionDialog.tsx
Normal file
179
src/components/FileDescriptionDialog.tsx
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { queryApi as projectApi } from '@/modules/project-api';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogFooter,
|
||||||
|
} from '@/components/ui/dialog';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Label } from '@/components/ui/label';
|
||||||
|
|
||||||
|
const API_URL = '/root/v1/dev-cnb';
|
||||||
|
|
||||||
|
export type FileDescriptionData = {
|
||||||
|
filepath: string;
|
||||||
|
title?: string;
|
||||||
|
tags?: string[];
|
||||||
|
summary?: string;
|
||||||
|
description?: string;
|
||||||
|
link?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface FileDescriptionDialogProps {
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
data: FileDescriptionData;
|
||||||
|
onSuccess?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FileDescriptionDialog({
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
data,
|
||||||
|
onSuccess,
|
||||||
|
}: FileDescriptionDialogProps) {
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [formData, setFormData] = useState<FileDescriptionData>({
|
||||||
|
filepath: data.filepath,
|
||||||
|
title: '',
|
||||||
|
tags: [],
|
||||||
|
summary: '',
|
||||||
|
description: '',
|
||||||
|
link: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
// 当打开对话框或数据变化时,同步数据
|
||||||
|
useEffect(() => {
|
||||||
|
if (open && data.filepath) {
|
||||||
|
setFormData({
|
||||||
|
filepath: data.filepath,
|
||||||
|
title: data.title || '',
|
||||||
|
tags: data.tags || [],
|
||||||
|
summary: data.summary || '',
|
||||||
|
description: data.description || '',
|
||||||
|
link: data.link || '',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [open, data]);
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
// 过滤掉空值
|
||||||
|
const updateData: Partial<FileDescriptionData> = {
|
||||||
|
filepath: formData.filepath,
|
||||||
|
};
|
||||||
|
if (formData.title) updateData.title = formData.title;
|
||||||
|
if (formData.tags && formData.tags.length > 0) updateData.tags = formData.tags;
|
||||||
|
if (formData.summary) updateData.summary = formData.summary;
|
||||||
|
if (formData.description) updateData.description = formData.description;
|
||||||
|
if (formData.link) updateData.link = formData.link;
|
||||||
|
|
||||||
|
const res = await projectApi['project-file']['update'](updateData, { url: API_URL });
|
||||||
|
if (res.code === 200) {
|
||||||
|
toast.success('保存成功');
|
||||||
|
onOpenChange(false);
|
||||||
|
onSuccess?.();
|
||||||
|
} else {
|
||||||
|
toast.error(res.message ?? '保存失败');
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
toast.error('保存失败');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTagsChange = (value: string) => {
|
||||||
|
// 标签以逗号分隔
|
||||||
|
const tags = value.split(',').map((t) => t.trim()).filter(Boolean);
|
||||||
|
setFormData((prev) => ({ ...prev, tags }));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
|
<DialogContent className="sm:max-w-[500px] max-h-[90vh] overflow-y-auto">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>编辑文件描述</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="grid gap-4 py-4">
|
||||||
|
{/* 文件路径(只读) */}
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<Label className="text-slate-400">文件路径</Label>
|
||||||
|
<Input value={formData.filepath} disabled className="font-mono text-xs" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 标题 */}
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<Label htmlFor="title">标题</Label>
|
||||||
|
<Input
|
||||||
|
id="title"
|
||||||
|
value={formData.title}
|
||||||
|
onChange={(e) => setFormData((prev) => ({ ...prev, title: e.target.value }))}
|
||||||
|
placeholder="输入文件标题"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 标签 */}
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<Label htmlFor="tags">标签</Label>
|
||||||
|
<Input
|
||||||
|
id="tags"
|
||||||
|
value={formData.tags?.join(', ') || ''}
|
||||||
|
onChange={(e) => handleTagsChange(e.target.value)}
|
||||||
|
placeholder="输入标签,用逗号分隔"
|
||||||
|
/>
|
||||||
|
<span className="text-[10px] text-slate-500">多个标签用逗号分隔</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 摘要 */}
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<Label htmlFor="summary">摘要</Label>
|
||||||
|
<Input
|
||||||
|
id="summary"
|
||||||
|
value={formData.summary}
|
||||||
|
onChange={(e) => setFormData((prev) => ({ ...prev, summary: e.target.value }))}
|
||||||
|
placeholder="简短描述文件内容"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 链接 */}
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<Label htmlFor="link">链接</Label>
|
||||||
|
<Input
|
||||||
|
id="link"
|
||||||
|
value={formData.link}
|
||||||
|
onChange={(e) => setFormData((prev) => ({ ...prev, link: e.target.value }))}
|
||||||
|
placeholder="关联的外部链接(如文档、Issue 等)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 描述 */}
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<Label htmlFor="description">详细描述</Label>
|
||||||
|
<Textarea
|
||||||
|
id="description"
|
||||||
|
value={formData.description}
|
||||||
|
onChange={(e) => setFormData((prev) => ({ ...prev, description: e.target.value }))}
|
||||||
|
placeholder="详细描述文件内容、用途等"
|
||||||
|
className="min-h-[120px]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleSave} disabled={loading}>
|
||||||
|
{loading ? '保存中...' : '保存'}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@ import ReactDOM from 'react-dom/client'
|
|||||||
import { RouterProvider, createRouter } from '@tanstack/react-router'
|
import { RouterProvider, createRouter } from '@tanstack/react-router'
|
||||||
import { routeTree } from './routeTree.gen'
|
import { routeTree } from './routeTree.gen'
|
||||||
import './index.css'
|
import './index.css'
|
||||||
import { getDynamicBasename } from './modules/basename'
|
import { getDynamicBasename } from './modules/dynamic-name.ts'
|
||||||
import './agents/index.ts';
|
import './agents/index.ts';
|
||||||
// Set up a Router instance
|
// Set up a Router instance
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
|
|||||||
4
src/modules/cnb.ts
Normal file
4
src/modules/cnb.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export const isCNB = () => {
|
||||||
|
const hostname = window.location.hostname;
|
||||||
|
return hostname.endsWith('.cnb.run') || hostname.includes('localhost');
|
||||||
|
}
|
||||||
16
src/modules/dynamic-name.ts
Normal file
16
src/modules/dynamic-name.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { basename } from "./basename"
|
||||||
|
|
||||||
|
// 动态计算 basename,根据当前 URL 路径
|
||||||
|
export const getDynamicBasename = (): string => {
|
||||||
|
const path = window.location.pathname
|
||||||
|
const origin = window.location.origin
|
||||||
|
const [user, key, id] = path.split('/').filter(Boolean)
|
||||||
|
if (key === 'v1' && id) {
|
||||||
|
return `/${user}/v1/${id}`
|
||||||
|
}
|
||||||
|
if (origin.includes('cnb.kevisual.cn')) {
|
||||||
|
return '/';
|
||||||
|
}
|
||||||
|
// 默认使用构建时的 basename
|
||||||
|
return basename
|
||||||
|
}
|
||||||
@@ -28,7 +28,7 @@ const api = {
|
|||||||
"skill": "create-opencode-client",
|
"skill": "create-opencode-client",
|
||||||
"title": "创建 OpenCode 客户端",
|
"title": "创建 OpenCode 客户端",
|
||||||
"summary": "创建 OpenCode 客户端,如果存在则复用",
|
"summary": "创建 OpenCode 客户端,如果存在则复用",
|
||||||
"url": "/root/v1/cnb-dev",
|
"url": "/root/v1/dev-cnb",
|
||||||
"source": "query-proxy-api"
|
"source": "query-proxy-api"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -58,7 +58,7 @@ const api = {
|
|||||||
"skill": "close-opencode-client",
|
"skill": "close-opencode-client",
|
||||||
"title": "关闭 OpenCode 客户端",
|
"title": "关闭 OpenCode 客户端",
|
||||||
"summary": "关闭 OpenCode 客户端, 未提供端口则关闭默认端口",
|
"summary": "关闭 OpenCode 客户端, 未提供端口则关闭默认端口",
|
||||||
"url": "/root/v1/cnb-dev",
|
"url": "/root/v1/dev-cnb",
|
||||||
"source": "query-proxy-api"
|
"source": "query-proxy-api"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -88,7 +88,7 @@ const api = {
|
|||||||
"skill": "restart-opencode-client",
|
"skill": "restart-opencode-client",
|
||||||
"title": "重启 OpenCode 客户端",
|
"title": "重启 OpenCode 客户端",
|
||||||
"summary": "重启 OpenCode 客户端",
|
"summary": "重启 OpenCode 客户端",
|
||||||
"url": "/root/v1/cnb-dev",
|
"url": "/root/v1/dev-cnb",
|
||||||
"source": "query-proxy-api"
|
"source": "query-proxy-api"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -118,7 +118,7 @@ const api = {
|
|||||||
"skill": "get-opencode-url",
|
"skill": "get-opencode-url",
|
||||||
"title": "获取 OpenCode 服务 URL",
|
"title": "获取 OpenCode 服务 URL",
|
||||||
"summary": "获取当前 OpenCode 服务的 URL 地址",
|
"summary": "获取当前 OpenCode 服务的 URL 地址",
|
||||||
"url": "/root/v1/cnb-dev",
|
"url": "/root/v1/dev-cnb",
|
||||||
"source": "query-proxy-api"
|
"source": "query-proxy-api"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -127,7 +127,7 @@ const api = {
|
|||||||
"key": "ls-projects",
|
"key": "ls-projects",
|
||||||
"id": "ee72cd09da63d13d",
|
"id": "ee72cd09da63d13d",
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"url": "/root/v1/cnb-dev",
|
"url": "/root/v1/dev-cnb",
|
||||||
"source": "query-proxy-api"
|
"source": "query-proxy-api"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -156,7 +156,7 @@ const api = {
|
|||||||
"skill": "run-opencode-project",
|
"skill": "run-opencode-project",
|
||||||
"title": "运行 OpenCode 项目",
|
"title": "运行 OpenCode 项目",
|
||||||
"summary": "运行一个已有的 OpenCode 项目",
|
"summary": "运行一个已有的 OpenCode 项目",
|
||||||
"url": "/root/v1/cnb-dev",
|
"url": "/root/v1/dev-cnb",
|
||||||
"source": "query-proxy-api"
|
"source": "query-proxy-api"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -238,7 +238,7 @@ const api = {
|
|||||||
"optional": true
|
"optional": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"url": "/root/v1/cnb-dev",
|
"url": "/root/v1/dev-cnb",
|
||||||
"source": "query-proxy-api"
|
"source": "query-proxy-api"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -262,7 +262,7 @@ const api = {
|
|||||||
"optional": true
|
"optional": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"url": "/root/v1/cnb-dev",
|
"url": "/root/v1/dev-cnb",
|
||||||
"source": "query-proxy-api"
|
"source": "query-proxy-api"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -301,7 +301,7 @@ const api = {
|
|||||||
"skill": "create-opencode-session",
|
"skill": "create-opencode-session",
|
||||||
"title": "创建 Session",
|
"title": "创建 Session",
|
||||||
"summary": "在指定目录创建一个新的 OpenCode 会话",
|
"summary": "在指定目录创建一个新的 OpenCode 会话",
|
||||||
"url": "/root/v1/cnb-dev",
|
"url": "/root/v1/dev-cnb",
|
||||||
"source": "query-proxy-api"
|
"source": "query-proxy-api"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -344,7 +344,7 @@ const api = {
|
|||||||
"skill": "update-opencode-session",
|
"skill": "update-opencode-session",
|
||||||
"title": "更新 Session",
|
"title": "更新 Session",
|
||||||
"summary": "更新指定 OpenCode 会话的属性,如标题",
|
"summary": "更新指定 OpenCode 会话的属性,如标题",
|
||||||
"url": "/root/v1/cnb-dev",
|
"url": "/root/v1/dev-cnb",
|
||||||
"source": "query-proxy-api"
|
"source": "query-proxy-api"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -380,7 +380,7 @@ const api = {
|
|||||||
"skill": "delete-opencode-session",
|
"skill": "delete-opencode-session",
|
||||||
"title": "删除 Session",
|
"title": "删除 Session",
|
||||||
"summary": "根据 ID 删除指定的 OpenCode 会话及其所有数据",
|
"summary": "根据 ID 删除指定的 OpenCode 会话及其所有数据",
|
||||||
"url": "/root/v1/cnb-dev",
|
"url": "/root/v1/dev-cnb",
|
||||||
"source": "query-proxy-api"
|
"source": "query-proxy-api"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -416,7 +416,7 @@ const api = {
|
|||||||
"skill": "abort-opencode-session",
|
"skill": "abort-opencode-session",
|
||||||
"title": "中止 Session",
|
"title": "中止 Session",
|
||||||
"summary": "中止正在运行的 OpenCode 会话",
|
"summary": "中止正在运行的 OpenCode 会话",
|
||||||
"url": "/root/v1/cnb-dev",
|
"url": "/root/v1/dev-cnb",
|
||||||
"source": "query-proxy-api"
|
"source": "query-proxy-api"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -452,7 +452,7 @@ const api = {
|
|||||||
"skill": "summarize-opencode-session",
|
"skill": "summarize-opencode-session",
|
||||||
"title": "总结 Session",
|
"title": "总结 Session",
|
||||||
"summary": "对指定的 OpenCode 会话进行内容总结",
|
"summary": "对指定的 OpenCode 会话进行内容总结",
|
||||||
"url": "/root/v1/cnb-dev",
|
"url": "/root/v1/dev-cnb",
|
||||||
"source": "query-proxy-api"
|
"source": "query-proxy-api"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -489,7 +489,7 @@ const api = {
|
|||||||
"skill": "get-opencode-session-status",
|
"skill": "get-opencode-session-status",
|
||||||
"title": "获取 Session 状态",
|
"title": "获取 Session 状态",
|
||||||
"summary": "获取当前 OpenCode 会话的运行状态,可按目录过滤",
|
"summary": "获取当前 OpenCode 会话的运行状态,可按目录过滤",
|
||||||
"url": "/root/v1/cnb-dev",
|
"url": "/root/v1/dev-cnb",
|
||||||
"source": "query-proxy-api"
|
"source": "query-proxy-api"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -525,7 +525,7 @@ const api = {
|
|||||||
"skill": "list-opencode-session-messages",
|
"skill": "list-opencode-session-messages",
|
||||||
"title": "列出 Session 消息",
|
"title": "列出 Session 消息",
|
||||||
"summary": "列出指定 OpenCode 会话的所有消息记录",
|
"summary": "列出指定 OpenCode 会话的所有消息记录",
|
||||||
"url": "/root/v1/cnb-dev",
|
"url": "/root/v1/dev-cnb",
|
||||||
"source": "query-proxy-api"
|
"source": "query-proxy-api"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -555,7 +555,7 @@ const api = {
|
|||||||
"skill": "list-opencode-sessions",
|
"skill": "list-opencode-sessions",
|
||||||
"title": "列出所有 Session",
|
"title": "列出所有 Session",
|
||||||
"summary": "列出 OpenCode 中的所有会话,可按目录过滤",
|
"summary": "列出 OpenCode 中的所有会话,可按目录过滤",
|
||||||
"url": "/root/v1/cnb-dev",
|
"url": "/root/v1/dev-cnb",
|
||||||
"source": "query-proxy-api"
|
"source": "query-proxy-api"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -591,7 +591,7 @@ const api = {
|
|||||||
"skill": "get-opencode-session",
|
"skill": "get-opencode-session",
|
||||||
"title": "获取 Session",
|
"title": "获取 Session",
|
||||||
"summary": "根据 ID 获取指定的 OpenCode 会话信息",
|
"summary": "根据 ID 获取指定的 OpenCode 会话信息",
|
||||||
"url": "/root/v1/cnb-dev",
|
"url": "/root/v1/dev-cnb",
|
||||||
"source": "query-proxy-api"
|
"source": "query-proxy-api"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -633,7 +633,7 @@ const api = {
|
|||||||
"skill": "fork-opencode-session",
|
"skill": "fork-opencode-session",
|
||||||
"title": "Fork Session",
|
"title": "Fork Session",
|
||||||
"summary": "从指定消息处 Fork 一个 OpenCode 会话",
|
"summary": "从指定消息处 Fork 一个 OpenCode 会话",
|
||||||
"url": "/root/v1/cnb-dev",
|
"url": "/root/v1/dev-cnb",
|
||||||
"source": "query-proxy-api"
|
"source": "query-proxy-api"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { createQueryApi } from '@kevisual/query/api';
|
import { createQueryApi } from '@kevisual/query/api';
|
||||||
import { query } from '@/modules/query.ts';
|
|
||||||
const api = {
|
const api = {
|
||||||
"project": {
|
"project": {
|
||||||
/**
|
/**
|
||||||
@@ -41,7 +40,15 @@ const api = {
|
|||||||
"optional": true
|
"optional": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"url": "/root/v1/cnb-dev",
|
"viewItem": {
|
||||||
|
"api": {
|
||||||
|
"url": "/root/v1/dev-cnb"
|
||||||
|
},
|
||||||
|
"type": "api",
|
||||||
|
"title": "CNB_BOARD",
|
||||||
|
"routerStatus": "active"
|
||||||
|
},
|
||||||
|
"url": "/root/v1/dev-cnb",
|
||||||
"source": "query-proxy-api"
|
"source": "query-proxy-api"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -64,7 +71,15 @@ const api = {
|
|||||||
"description": "要移除的项目根目录绝对路径,必填"
|
"description": "要移除的项目根目录绝对路径,必填"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"url": "/root/v1/cnb-dev",
|
"viewItem": {
|
||||||
|
"api": {
|
||||||
|
"url": "/root/v1/dev-cnb"
|
||||||
|
},
|
||||||
|
"type": "api",
|
||||||
|
"title": "CNB_BOARD",
|
||||||
|
"routerStatus": "active"
|
||||||
|
},
|
||||||
|
"url": "/root/v1/dev-cnb",
|
||||||
"source": "query-proxy-api"
|
"source": "query-proxy-api"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -87,7 +102,15 @@ const api = {
|
|||||||
"description": "要暂停监听的项目根目录绝对路径,必填"
|
"description": "要暂停监听的项目根目录绝对路径,必填"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"url": "/root/v1/cnb-dev",
|
"viewItem": {
|
||||||
|
"api": {
|
||||||
|
"url": "/root/v1/dev-cnb"
|
||||||
|
},
|
||||||
|
"type": "api",
|
||||||
|
"title": "CNB_BOARD",
|
||||||
|
"routerStatus": "active"
|
||||||
|
},
|
||||||
|
"url": "/root/v1/dev-cnb",
|
||||||
"source": "query-proxy-api"
|
"source": "query-proxy-api"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -110,7 +133,15 @@ const api = {
|
|||||||
"description": "要查询的项目根目录绝对路径,必填"
|
"description": "要查询的项目根目录绝对路径,必填"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"url": "/root/v1/cnb-dev",
|
"viewItem": {
|
||||||
|
"api": {
|
||||||
|
"url": "/root/v1/dev-cnb"
|
||||||
|
},
|
||||||
|
"type": "api",
|
||||||
|
"title": "CNB_BOARD",
|
||||||
|
"routerStatus": "active"
|
||||||
|
},
|
||||||
|
"url": "/root/v1/dev-cnb",
|
||||||
"source": "query-proxy-api"
|
"source": "query-proxy-api"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -122,7 +153,15 @@ const api = {
|
|||||||
"key": "list",
|
"key": "list",
|
||||||
"description": "列出所有已注册的项目及其当前运行状态(路径、仓库名称、监听是否活跃等)",
|
"description": "列出所有已注册的项目及其当前运行状态(路径、仓库名称、监听是否活跃等)",
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"url": "/root/v1/cnb-dev",
|
"viewItem": {
|
||||||
|
"api": {
|
||||||
|
"url": "/root/v1/dev-cnb"
|
||||||
|
},
|
||||||
|
"type": "api",
|
||||||
|
"title": "CNB_BOARD",
|
||||||
|
"routerStatus": "active"
|
||||||
|
},
|
||||||
|
"url": "/root/v1/dev-cnb",
|
||||||
"source": "query-proxy-api"
|
"source": "query-proxy-api"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -190,7 +229,46 @@ const api = {
|
|||||||
"optional": true
|
"optional": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"url": "/root/v1/cnb-dev",
|
"viewItem": {
|
||||||
|
"api": {
|
||||||
|
"url": "/root/v1/dev-cnb"
|
||||||
|
},
|
||||||
|
"type": "api",
|
||||||
|
"title": "CNB_BOARD",
|
||||||
|
"routerStatus": "active"
|
||||||
|
},
|
||||||
|
"url": "/root/v1/dev-cnb",
|
||||||
|
"source": "query-proxy-api"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 列出项目根目录下的所有文件路径,适用于前端展示项目结构
|
||||||
|
*
|
||||||
|
* @param data - Request parameters
|
||||||
|
* @param data.rootPath - {string} 项目根目录绝对路径,默认为 /workspace/projects,指定后只列出该目录下的项目文件
|
||||||
|
*/
|
||||||
|
"project-files": {
|
||||||
|
"path": "project",
|
||||||
|
"key": "project-files",
|
||||||
|
"description": "列出项目根目录下的所有文件路径,适用于前端展示项目结构",
|
||||||
|
"metadata": {
|
||||||
|
"args": {
|
||||||
|
"rootPath": {
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"description": "项目根目录绝对路径,默认为 /workspace/projects,指定后只列出该目录下的项目文件",
|
||||||
|
"type": "string",
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"viewItem": {
|
||||||
|
"api": {
|
||||||
|
"url": "/root/v1/dev-cnb"
|
||||||
|
},
|
||||||
|
"type": "api",
|
||||||
|
"title": "CNB_BOARD",
|
||||||
|
"routerStatus": "active"
|
||||||
|
},
|
||||||
|
"url": "/root/v1/dev-cnb",
|
||||||
"source": "query-proxy-api"
|
"source": "query-proxy-api"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -199,6 +277,7 @@ const api = {
|
|||||||
*
|
*
|
||||||
* @param data - Request parameters
|
* @param data - Request parameters
|
||||||
* @param data.rootPath - {string} 搜索项目的根目录绝对路径,默认为 /workspace/projects
|
* @param data.rootPath - {string} 搜索项目的根目录绝对路径,默认为 /workspace/projects
|
||||||
|
* @param data.projectPaths - {array} 项目路径列表,提供后将直接注册这些路径为项目,忽略rootPath参数
|
||||||
*/
|
*/
|
||||||
"init": {
|
"init": {
|
||||||
"path": "project",
|
"path": "project",
|
||||||
@@ -211,9 +290,26 @@ const api = {
|
|||||||
"description": "搜索项目的根目录绝对路径,默认为 /workspace/projects",
|
"description": "搜索项目的根目录绝对路径,默认为 /workspace/projects",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"optional": true
|
"optional": true
|
||||||
|
},
|
||||||
|
"projectPaths": {
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"description": "项目路径列表,提供后将直接注册这些路径为项目,忽略rootPath参数",
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"optional": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"url": "/root/v1/cnb-dev",
|
"viewItem": {
|
||||||
|
"api": {
|
||||||
|
"url": "/root/v1/dev-cnb"
|
||||||
|
},
|
||||||
|
"type": "api",
|
||||||
|
"title": "CNB_BOARD",
|
||||||
|
"routerStatus": "active"
|
||||||
|
},
|
||||||
|
"url": "/root/v1/dev-cnb",
|
||||||
"source": "query-proxy-api"
|
"source": "query-proxy-api"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -236,7 +332,15 @@ const api = {
|
|||||||
"optional": true
|
"optional": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"url": "/root/v1/cnb-dev",
|
"viewItem": {
|
||||||
|
"api": {
|
||||||
|
"url": "/root/v1/dev-cnb"
|
||||||
|
},
|
||||||
|
"type": "api",
|
||||||
|
"title": "CNB_BOARD",
|
||||||
|
"routerStatus": "active"
|
||||||
|
},
|
||||||
|
"url": "/root/v1/dev-cnb",
|
||||||
"source": "query-proxy-api"
|
"source": "query-proxy-api"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -265,7 +369,15 @@ const api = {
|
|||||||
"description": "代码仓库标识,用于搜索结果展示和过滤,格式如 owner/repo,例如 kevisual/cnb,必填"
|
"description": "代码仓库标识,用于搜索结果展示和过滤,格式如 owner/repo,例如 kevisual/cnb,必填"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"url": "/root/v1/cnb-dev",
|
"viewItem": {
|
||||||
|
"api": {
|
||||||
|
"url": "/root/v1/dev-cnb"
|
||||||
|
},
|
||||||
|
"type": "api",
|
||||||
|
"title": "CNB_BOARD",
|
||||||
|
"routerStatus": "active"
|
||||||
|
},
|
||||||
|
"url": "/root/v1/dev-cnb",
|
||||||
"source": "query-proxy-api"
|
"source": "query-proxy-api"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -376,7 +488,15 @@ const api = {
|
|||||||
"optional": true
|
"optional": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"url": "/root/v1/cnb-dev",
|
"viewItem": {
|
||||||
|
"api": {
|
||||||
|
"url": "/root/v1/dev-cnb"
|
||||||
|
},
|
||||||
|
"type": "api",
|
||||||
|
"title": "CNB_BOARD",
|
||||||
|
"routerStatus": "active"
|
||||||
|
},
|
||||||
|
"url": "/root/v1/dev-cnb",
|
||||||
"source": "query-proxy-api"
|
"source": "query-proxy-api"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -481,7 +601,15 @@ const api = {
|
|||||||
"optional": true
|
"optional": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"url": "/root/v1/cnb-dev",
|
"viewItem": {
|
||||||
|
"api": {
|
||||||
|
"url": "/root/v1/dev-cnb"
|
||||||
|
},
|
||||||
|
"type": "api",
|
||||||
|
"title": "CNB_BOARD",
|
||||||
|
"routerStatus": "active"
|
||||||
|
},
|
||||||
|
"url": "/root/v1/dev-cnb",
|
||||||
"source": "query-proxy-api"
|
"source": "query-proxy-api"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -506,7 +634,15 @@ const api = {
|
|||||||
"description": "要读取的文件绝对路径,必填"
|
"description": "要读取的文件绝对路径,必填"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"url": "/root/v1/cnb-dev",
|
"viewItem": {
|
||||||
|
"api": {
|
||||||
|
"url": "/root/v1/dev-cnb"
|
||||||
|
},
|
||||||
|
"type": "api",
|
||||||
|
"title": "CNB_BOARD",
|
||||||
|
"routerStatus": "active"
|
||||||
|
},
|
||||||
|
"url": "/root/v1/dev-cnb",
|
||||||
"source": "query-proxy-api"
|
"source": "query-proxy-api"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -536,7 +672,15 @@ const api = {
|
|||||||
"description": "文件内容的 base64 编码,必填"
|
"description": "文件内容的 base64 编码,必填"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"url": "/root/v1/cnb-dev",
|
"viewItem": {
|
||||||
|
"api": {
|
||||||
|
"url": "/root/v1/dev-cnb"
|
||||||
|
},
|
||||||
|
"type": "api",
|
||||||
|
"title": "CNB_BOARD",
|
||||||
|
"routerStatus": "active"
|
||||||
|
},
|
||||||
|
"url": "/root/v1/dev-cnb",
|
||||||
"source": "query-proxy-api"
|
"source": "query-proxy-api"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -597,7 +741,15 @@ const api = {
|
|||||||
"optional": true
|
"optional": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"url": "/root/v1/cnb-dev",
|
"viewItem": {
|
||||||
|
"api": {
|
||||||
|
"url": "/root/v1/dev-cnb"
|
||||||
|
},
|
||||||
|
"type": "api",
|
||||||
|
"title": "CNB_BOARD",
|
||||||
|
"routerStatus": "active"
|
||||||
|
},
|
||||||
|
"url": "/root/v1/dev-cnb",
|
||||||
"source": "query-proxy-api"
|
"source": "query-proxy-api"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -620,12 +772,20 @@ const api = {
|
|||||||
"description": "要删除的文件绝对路径,必填"
|
"description": "要删除的文件绝对路径,必填"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"url": "/root/v1/cnb-dev",
|
"viewItem": {
|
||||||
|
"api": {
|
||||||
|
"url": "/root/v1/dev-cnb"
|
||||||
|
},
|
||||||
|
"type": "api",
|
||||||
|
"title": "CNB_BOARD",
|
||||||
|
"routerStatus": "active"
|
||||||
|
},
|
||||||
|
"url": "/root/v1/dev-cnb",
|
||||||
"source": "query-proxy-api"
|
"source": "query-proxy-api"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} as const;
|
} as const;
|
||||||
const queryApi = createQueryApi({ api, query });
|
const queryApi = createQueryApi({ api });
|
||||||
|
|
||||||
export { queryApi };
|
export { queryApi };
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import { FileIcon, FolderIcon, DatabaseIcon, XIcon, MoveIcon, SquarePenIcon, BotIcon } from 'lucide-react';
|
import { FileIcon, FolderIcon, DatabaseIcon, XIcon, MoveIcon, SquarePenIcon, BotIcon, FileTextIcon } from 'lucide-react';
|
||||||
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';
|
import clsx from 'clsx';
|
||||||
|
import { FileDescriptionDialog, FileDescriptionData } from '@/components/FileDescriptionDialog';
|
||||||
|
|
||||||
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';
|
||||||
@@ -94,6 +95,17 @@ export const NodeInfoContainer = () => {
|
|||||||
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 [descriptionOpen, setDescriptionOpen] = useState(false);
|
||||||
|
const [descriptionData, setDescriptionData] = useState<FileDescriptionData>({
|
||||||
|
filepath: '',
|
||||||
|
title: '',
|
||||||
|
tags: [],
|
||||||
|
summary: '',
|
||||||
|
description: '',
|
||||||
|
link: '',
|
||||||
|
});
|
||||||
|
|
||||||
// 检测屏幕大小
|
// 检测屏幕大小
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const checkMobile = () => setIsMobile(window.innerWidth < 768);
|
const checkMobile = () => setIsMobile(window.innerWidth < 768);
|
||||||
@@ -169,6 +181,23 @@ export const NodeInfoContainer = () => {
|
|||||||
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={() => {
|
||||||
|
if (!nodeInfoData) return;
|
||||||
|
setDescriptionData({
|
||||||
|
filepath: nodeInfoData.fullPath,
|
||||||
|
title: nodeInfoData.title,
|
||||||
|
tags: nodeInfoData.tags,
|
||||||
|
summary: nodeInfoData.summary,
|
||||||
|
description: nodeInfoData.description,
|
||||||
|
link: nodeInfoData.link,
|
||||||
|
});
|
||||||
|
setDescriptionOpen(true);
|
||||||
|
}}
|
||||||
|
title='编辑描述'
|
||||||
|
className='ml-1 text-slate-500 hover:text-amber-400 transition-colors'>
|
||||||
|
<FileTextIcon className='size-3.5' />
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
botHelperStore.setProjectInfo({
|
botHelperStore.setProjectInfo({
|
||||||
@@ -194,6 +223,15 @@ export const NodeInfoContainer = () => {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<NodeInfo />
|
<NodeInfo />
|
||||||
|
{/* 描述编辑弹窗 */}
|
||||||
|
<FileDescriptionDialog
|
||||||
|
open={descriptionOpen}
|
||||||
|
onOpenChange={setDescriptionOpen}
|
||||||
|
data={descriptionData}
|
||||||
|
onSuccess={() => {
|
||||||
|
// 可选:刷新节点数据
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { useShallow } from 'zustand/react/shallow';
|
import { useShallow } from 'zustand/react/shallow';
|
||||||
import { FolderOpenIcon, PlusIcon, Trash2Icon, RefreshCwIcon, PlayCircleIcon, StopCircleIcon, FolderIcon, AlertCircleIcon, CircleOffIcon, DownloadIcon, ListTodoIcon, CheckSquareIcon } 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';
|
||||||
@@ -7,8 +7,189 @@ 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 { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
||||||
|
import { Checkbox } from '@/components/ui/checkbox';
|
||||||
|
import { Card, CardContent } from '@/components/ui/card';
|
||||||
import { useCodeGraphStore } from '../store';
|
import { useCodeGraphStore } from '../store';
|
||||||
|
|
||||||
|
// 初始化项目弹窗组件
|
||||||
|
function ProjectInitDialog({
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
}: {
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
}) {
|
||||||
|
const { initProject, fetchProjectFiles } = useCodeGraphStore(
|
||||||
|
useShallow((s) => ({
|
||||||
|
initProject: s.initProject,
|
||||||
|
fetchProjectFiles: s.fetchProjectFiles,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [files, setFiles] = useState<string[]>([]);
|
||||||
|
const [selectedPaths, setSelectedPaths] = useState<string[]>([]);
|
||||||
|
const [rootPath, setRootPath] = useState('/workspace/projects');
|
||||||
|
|
||||||
|
// 加载文件列表
|
||||||
|
const loadFiles = async () => {
|
||||||
|
const data = await fetchProjectFiles(rootPath);
|
||||||
|
setFiles(data);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 打开时加载数据
|
||||||
|
useEffect(() => {
|
||||||
|
if (open) {
|
||||||
|
loadFiles();
|
||||||
|
}
|
||||||
|
}, [open]);
|
||||||
|
|
||||||
|
// 切换选中状态
|
||||||
|
const toggleSelection = (path: string) => {
|
||||||
|
setSelectedPaths((prev) =>
|
||||||
|
prev.includes(path) ? prev.filter((p) => p !== path) : [...prev, path]
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 全选
|
||||||
|
const selectAll = () => setSelectedPaths([...files]);
|
||||||
|
|
||||||
|
// 取消全选
|
||||||
|
const deselectAll = () => setSelectedPaths([]);
|
||||||
|
|
||||||
|
// 确认初始化
|
||||||
|
const handleConfirm = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
await initProject(selectedPaths.length > 0 ? selectedPaths : undefined);
|
||||||
|
setLoading(false);
|
||||||
|
setSelectedPaths([]);
|
||||||
|
onOpenChange(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 取消
|
||||||
|
const handleCancel = () => {
|
||||||
|
setSelectedPaths([]);
|
||||||
|
onOpenChange(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const allSelected = files.length > 0 && selectedPaths.length === files.length;
|
||||||
|
const isIndeterminate = selectedPaths.length > 0 && selectedPaths.length < files.length;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={(open) => !open && handleCancel()}>
|
||||||
|
<DialogContent className='sm:max-w-2xl max-h-[80vh] 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'>
|
||||||
|
<DialogHeader className='text-center'>
|
||||||
|
<div className='flex justify-center mb-3'>
|
||||||
|
<div className='w-12 h-12 rounded-full flex items-center justify-center bg-indigo-500/20'>
|
||||||
|
<DownloadIcon className='w-6 h-6 text-indigo-400' />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<DialogTitle className='text-lg font-semibold'>
|
||||||
|
初始化项目
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription className='text-slate-400'>
|
||||||
|
选择要初始化的项目,选中后点击确认开始初始化
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
{/* 路径输入 */}
|
||||||
|
<div className='px-6'>
|
||||||
|
<div className='flex items-center gap-2'>
|
||||||
|
<div className='relative flex-1'>
|
||||||
|
<FolderIcon className='absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-500' />
|
||||||
|
<Input
|
||||||
|
value={rootPath}
|
||||||
|
onChange={(e) => setRootPath(e.target.value)}
|
||||||
|
placeholder='项目根目录'
|
||||||
|
className='bg-slate-800/80 border-white/10 text-slate-100 placeholder:text-slate-500 h-9 text-sm pl-10 pr-4 focus:border-indigo-500/50 focus:ring-2 focus:ring-indigo-500/20 transition-all'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
size='sm'
|
||||||
|
onClick={loadFiles}
|
||||||
|
className='bg-slate-700 hover:bg-slate-600 shrink-0'>
|
||||||
|
加载
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 项目列表 */}
|
||||||
|
<div className='flex-1 overflow-hidden px-6 py-2'>
|
||||||
|
<Card className='bg-slate-800/50 border-white/10 h-full'>
|
||||||
|
<CardContent className='p-3 h-[280px] overflow-y-auto scrollbar'>
|
||||||
|
{files.length === 0 ? (
|
||||||
|
<div className='flex flex-col items-center justify-center h-full text-slate-500 text-sm gap-2'>
|
||||||
|
<FolderIcon className='w-8 h-8 text-slate-600' />
|
||||||
|
<span>暂无项目,点击加载获取项目列表</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className='space-y-1'>
|
||||||
|
{files.map((path) => (
|
||||||
|
<div
|
||||||
|
key={path}
|
||||||
|
onClick={() => toggleSelection(path)}
|
||||||
|
className={`flex items-center gap-3 rounded-lg px-3 py-2 cursor-pointer transition-all ${selectedPaths.includes(path)
|
||||||
|
? 'bg-indigo-500/20 border border-indigo-500/50'
|
||||||
|
: 'hover:bg-white/5 border border-transparent'
|
||||||
|
}`}>
|
||||||
|
<Checkbox
|
||||||
|
checked={selectedPaths.includes(path)}
|
||||||
|
onCheckedChange={() => toggleSelection(path)}
|
||||||
|
className='border-slate-500 data-[checked]:bg-indigo-500 data-[checked]:border-indigo-500'
|
||||||
|
/>
|
||||||
|
<FolderIcon className='w-4 h-4 text-slate-400 shrink-0' />
|
||||||
|
<span className='text-sm text-slate-200 truncate'>{path}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 操作按钮 */}
|
||||||
|
<div className='flex items-center justify-between px-6 pb-4'>
|
||||||
|
<div className='flex items-center gap-3'>
|
||||||
|
<Button
|
||||||
|
size='sm'
|
||||||
|
variant='outline'
|
||||||
|
onClick={isIndeterminate ? selectAll : allSelected ? deselectAll : selectAll}
|
||||||
|
disabled={files.length === 0}
|
||||||
|
className='bg-transparent border-white/20 text-slate-300 hover:bg-white/10 hover:border-white/30'>
|
||||||
|
{allSelected ? '取消全选' : '全选'}
|
||||||
|
</Button>
|
||||||
|
<span className='text-xs text-slate-400'>
|
||||||
|
已选择 {selectedPaths.length}/{files.length}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className='flex gap-2'>
|
||||||
|
<Button
|
||||||
|
variant='outline'
|
||||||
|
onClick={handleCancel}
|
||||||
|
disabled={loading}
|
||||||
|
className='bg-transparent border-white/20 text-slate-300 hover:bg-white/10 hover:border-white/30'>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleConfirm}
|
||||||
|
disabled={loading}
|
||||||
|
className='bg-indigo-600 hover:bg-indigo-500 text-white shadow-lg shadow-indigo-500/25 disabled:opacity-50'>
|
||||||
|
{loading ? (
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function ProjectDialog() {
|
export function ProjectDialog() {
|
||||||
const {
|
const {
|
||||||
projectDialogOpen,
|
projectDialogOpen,
|
||||||
@@ -19,7 +200,6 @@ export function ProjectDialog() {
|
|||||||
addProject,
|
addProject,
|
||||||
removeProject,
|
removeProject,
|
||||||
toggleProjectStatus,
|
toggleProjectStatus,
|
||||||
initProject,
|
|
||||||
} = useCodeGraphStore(
|
} = useCodeGraphStore(
|
||||||
useShallow((s) => ({
|
useShallow((s) => ({
|
||||||
projectDialogOpen: s.projectDialogOpen,
|
projectDialogOpen: s.projectDialogOpen,
|
||||||
@@ -30,7 +210,6 @@ export function ProjectDialog() {
|
|||||||
addProject: s.addProject,
|
addProject: s.addProject,
|
||||||
removeProject: s.removeProject,
|
removeProject: s.removeProject,
|
||||||
toggleProjectStatus: s.toggleProjectStatus,
|
toggleProjectStatus: s.toggleProjectStatus,
|
||||||
initProject: s.initProject,
|
|
||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -53,9 +232,8 @@ 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 [initConfirmOpen, setInitConfirmOpen] = useState(false);
|
||||||
const [initLoading, setInitLoading] = useState(false);
|
|
||||||
|
|
||||||
// 多选模式
|
// 多选模式
|
||||||
const [multiSelectMode, setMultiSelectMode] = useState(false);
|
const [multiSelectMode, setMultiSelectMode] = useState(false);
|
||||||
@@ -175,19 +353,6 @@ 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='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'>
|
<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'>
|
||||||
@@ -211,293 +376,290 @@ export function ProjectDialog() {
|
|||||||
|
|
||||||
{/* 内容区域 - 可滚动 */}
|
{/* 内容区域 - 可滚动 */}
|
||||||
<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'>
|
<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'>
|
||||||
<div className='flex items-center gap-2 mb-1'>
|
<div className='flex items-center gap-2 mb-1'>
|
||||||
<div className='w-1 h-3 rounded-full bg-gradient-to-b from-indigo-400 to-purple-500' />
|
<div className='w-1 h-3 rounded-full bg-gradient-to-b from-indigo-400 to-purple-500' />
|
||||||
<p className='text-sm font-medium text-slate-200'>添加新项目</p>
|
<p className='text-sm font-medium text-slate-200'>添加新项目</p>
|
||||||
</div>
|
|
||||||
<div className='space-y-2'>
|
|
||||||
{/* 类型选择 */}
|
|
||||||
<div className='flex gap-2 mb-2'>
|
|
||||||
<button
|
|
||||||
type='button'
|
|
||||||
onClick={() => {
|
|
||||||
setProjectType('filepath');
|
|
||||||
if (!newPath.trim()) {
|
|
||||||
setNewPath('/workspace/projects');
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
className={`flex-1 text-xs py-1.5 px-3 rounded-lg border transition-all ${
|
|
||||||
projectType === 'filepath'
|
|
||||||
? 'bg-indigo-500/20 border-indigo-500/50 text-indigo-400'
|
|
||||||
: 'bg-transparent border-white/10 text-slate-400 hover:border-white/20'
|
|
||||||
}`}>
|
|
||||||
文件路径
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type='button'
|
|
||||||
onClick={() => {
|
|
||||||
setProjectType('cnb-repo');
|
|
||||||
setNewPath('');
|
|
||||||
}}
|
|
||||||
className={`flex-1 text-xs py-1.5 px-3 rounded-lg border transition-all ${
|
|
||||||
projectType === 'cnb-repo'
|
|
||||||
? 'bg-indigo-500/20 border-indigo-500/50 text-indigo-400'
|
|
||||||
: 'bg-transparent border-white/10 text-slate-400 hover:border-white/20'
|
|
||||||
}`}>
|
|
||||||
CNB 仓库
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className='space-y-1.5'>
|
|
||||||
<Label className='text-xs text-slate-400 flex items-center gap-1'>
|
|
||||||
<span>{projectType === 'filepath' ? '项目路径' : 'CNB 仓库'}</span>
|
|
||||||
<span className='text-red-400'>*</span>
|
|
||||||
</Label>
|
|
||||||
<div className='relative'>
|
|
||||||
<FolderIcon className='absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-500' />
|
|
||||||
<Input
|
|
||||||
value={newPath}
|
|
||||||
onChange={(e) => setNewPath(e.target.value)}
|
|
||||||
placeholder={projectType === 'filepath' ? '/workspace/projects' : 'kevisual/cnb'}
|
|
||||||
className='bg-slate-800/80 border-white/10 text-slate-100 placeholder:text-slate-500 h-9 text-sm pl-10 pr-4 focus:border-indigo-500/50 focus:ring-2 focus:ring-indigo-500/20 transition-all'
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div className='space-y-2'>
|
||||||
<div className='space-y-1.5'>
|
{/* 类型选择 */}
|
||||||
<Label className='text-xs text-slate-400'>项目名称(可选)</Label>
|
<div className='flex gap-2 mb-2'>
|
||||||
<Input
|
<button
|
||||||
value={newName}
|
type='button'
|
||||||
onChange={(e) => setNewName(e.target.value)}
|
onClick={() => {
|
||||||
placeholder='My Project'
|
setProjectType('filepath');
|
||||||
className='bg-slate-800/80 border-white/10 text-slate-100 placeholder:text-slate-500 h-9 text-sm focus:border-indigo-500/50 focus:ring-2 focus:ring-indigo-500/20 transition-all'
|
if (!newPath.trim()) {
|
||||||
/>
|
setNewPath('/workspace/projects');
|
||||||
</div>
|
}
|
||||||
</div>
|
}}
|
||||||
<Button
|
className={`flex-1 text-xs py-1.5 px-3 rounded-lg border transition-all ${projectType === 'filepath'
|
||||||
size='sm'
|
? 'bg-indigo-500/20 border-indigo-500/50 text-indigo-400'
|
||||||
onClick={handleAdd}
|
: 'bg-transparent border-white/10 text-slate-400 hover:border-white/20'
|
||||||
disabled={addLoading || !newPath.trim()}
|
}`}>
|
||||||
className='w-full bg-gradient-to-r from-indigo-600 to-purple-600 hover:from-indigo-500 hover:to-purple-500 text-white text-sm h-9 mt-2 shadow-lg shadow-indigo-500/25 disabled:opacity-50 disabled:cursor-not-allowed transition-all hover:shadow-indigo-500/40'>
|
文件路径
|
||||||
<PlusIcon className='w-4 h-4 mr-1.5' />
|
</button>
|
||||||
{addLoading ? (
|
<button
|
||||||
<span className='flex items-center gap-2'>
|
type='button'
|
||||||
<span className='w-3 h-3 border-2 border-white/30 border-t-white rounded-full animate-spin' />
|
onClick={() => {
|
||||||
添加中…
|
setProjectType('cnb-repo');
|
||||||
</span>
|
setNewPath('');
|
||||||
) : (
|
}}
|
||||||
'添加项目'
|
className={`flex-1 text-xs py-1.5 px-3 rounded-lg border transition-all ${projectType === 'cnb-repo'
|
||||||
)}
|
? 'bg-indigo-500/20 border-indigo-500/50 text-indigo-400'
|
||||||
</Button>
|
: 'bg-transparent border-white/10 text-slate-400 hover:border-white/20'
|
||||||
</div>
|
}`}>
|
||||||
)}
|
CNB 仓库
|
||||||
|
</button>
|
||||||
{/* 项目列表 */}
|
</div>
|
||||||
<div className='relative'>
|
<div className='space-y-1.5'>
|
||||||
<div className='flex items-center justify-between mb-3'>
|
<Label className='text-xs text-slate-400 flex items-center gap-1'>
|
||||||
<div className='flex items-center gap-2'>
|
<span>{projectType === 'filepath' ? '项目路径' : 'CNB 仓库'}</span>
|
||||||
<div className='w-1 h-3 rounded-full bg-gradient-to-b from-green-400 to-emerald-500' />
|
<span className='text-red-400'>*</span>
|
||||||
<p className='text-sm font-medium text-slate-200'>已注册项目</p>
|
</Label>
|
||||||
<span className='text-xs text-slate-500 bg-slate-800/80 px-1.5 py-0.5 rounded-full'>
|
<div className='relative'>
|
||||||
{projects.length}
|
<FolderIcon className='absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-500' />
|
||||||
</span>
|
<Input
|
||||||
</div>
|
value={newPath}
|
||||||
<div className='flex items-center gap-1'>
|
onChange={(e) => setNewPath(e.target.value)}
|
||||||
<button
|
placeholder={projectType === 'filepath' ? '/workspace/projects' : 'kevisual/cnb'}
|
||||||
onClick={() => setShowAddProject(!showAddProject)}
|
className='bg-slate-800/80 border-white/10 text-slate-100 placeholder:text-slate-500 h-9 text-sm pl-10 pr-4 focus:border-indigo-500/50 focus:ring-2 focus:ring-indigo-500/20 transition-all'
|
||||||
className={`text-slate-500 hover:text-indigo-400 transition-colors p-1.5 rounded-lg hover:bg-white/5 ${showAddProject ? 'text-indigo-400 bg-white/5' : ''}`}
|
/>
|
||||||
title={showAddProject ? '隐藏添加' : '添加项目'}>
|
|
||||||
<PlusIcon className={`w-4 h-4 ${showAddProject ? 'rotate-90' : ''} transition-transform`} />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={loadProjects}
|
|
||||||
disabled={projectsLoading}
|
|
||||||
className='text-slate-500 hover:text-indigo-400 transition-colors p-1.5 rounded-lg hover:bg-white/5'
|
|
||||||
title='刷新'>
|
|
||||||
<RefreshCwIcon className={`w-4 h-4 ${projectsLoading ? 'animate-spin' : ''}`} />
|
|
||||||
</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>
|
</div>
|
||||||
</TooltipTrigger>
|
</div>
|
||||||
<TooltipContent>
|
<div className='space-y-1.5'>
|
||||||
<p>初始化 workspace/projects 仓库</p>
|
<Label className='text-xs text-slate-400'>项目名称(可选)</Label>
|
||||||
</TooltipContent>
|
<Input
|
||||||
</Tooltip>
|
value={newName}
|
||||||
{multiSelectMode ? (
|
onChange={(e) => setNewName(e.target.value)}
|
||||||
<>
|
placeholder='My Project'
|
||||||
<div className='w-px h-5 bg-white/20 mx-1' />
|
className='bg-slate-800/80 border-white/10 text-slate-100 placeholder:text-slate-500 h-9 text-sm focus:border-indigo-500/50 focus:ring-2 focus:ring-indigo-500/20 transition-all'
|
||||||
<Tooltip>
|
/>
|
||||||
<TooltipTrigger>
|
</div>
|
||||||
<div
|
</div>
|
||||||
onClick={selectAll}
|
<Button
|
||||||
className='text-slate-500 hover:text-indigo-400 transition-colors p-1.5 rounded-lg hover:bg-white/5'
|
size='sm'
|
||||||
title='全选'>
|
onClick={handleAdd}
|
||||||
<CheckSquareIcon className='w-4 h-4' />
|
disabled={addLoading || !newPath.trim()}
|
||||||
</div>
|
className='w-full bg-gradient-to-r from-indigo-600 to-purple-600 hover:from-indigo-500 hover:to-purple-500 text-white text-sm h-9 mt-2 shadow-lg shadow-indigo-500/25 disabled:opacity-50 disabled:cursor-not-allowed transition-all hover:shadow-indigo-500/40'>
|
||||||
</TooltipTrigger>
|
<PlusIcon className='w-4 h-4 mr-1.5' />
|
||||||
<TooltipContent>
|
{addLoading ? (
|
||||||
<p>全选</p>
|
<span className='flex items-center gap-2'>
|
||||||
</TooltipContent>
|
<span className='w-3 h-3 border-2 border-white/30 border-t-white rounded-full animate-spin' />
|
||||||
</Tooltip>
|
添加中…
|
||||||
<Tooltip>
|
</span>
|
||||||
<TooltipTrigger>
|
) : (
|
||||||
<div
|
'添加项目'
|
||||||
onClick={deselectAll}
|
)}
|
||||||
className='text-slate-500 hover:text-indigo-400 transition-colors p-1.5 rounded-lg hover:bg-white/5'
|
</Button>
|
||||||
title='取消全选'>
|
</div>
|
||||||
<ListTodoIcon className='w-4 h-4' />
|
)}
|
||||||
</div>
|
|
||||||
</TooltipTrigger>
|
{/* 项目列表 */}
|
||||||
<TooltipContent>
|
<div className='relative'>
|
||||||
<p>取消全选</p>
|
<div className='flex items-center justify-between mb-3'>
|
||||||
</TooltipContent>
|
<div className='flex items-center gap-2'>
|
||||||
</Tooltip>
|
<div className='w-1 h-3 rounded-full bg-gradient-to-b from-green-400 to-emerald-500' />
|
||||||
<Tooltip>
|
<p className='text-sm font-medium text-slate-200'>已注册项目</p>
|
||||||
<TooltipTrigger>
|
<span className='text-xs text-slate-500 bg-slate-800/80 px-1.5 py-0.5 rounded-full'>
|
||||||
<button
|
{projects.length}
|
||||||
onClick={handleStartAll}
|
</span>
|
||||||
disabled={selectedProjects.length === 0}
|
</div>
|
||||||
className='text-slate-500 hover:text-green-400 transition-colors p-1.5 rounded-lg hover:bg-white/5 disabled:opacity-50'
|
<div className='flex items-center gap-1'>
|
||||||
title='全部启动'>
|
<button
|
||||||
<PlayCircleIcon className='w-4 h-4' />
|
onClick={() => setShowAddProject(!showAddProject)}
|
||||||
</button>
|
className={`text-slate-500 hover:text-indigo-400 transition-colors p-1.5 rounded-lg hover:bg-white/5 ${showAddProject ? 'text-indigo-400 bg-white/5' : ''}`}
|
||||||
</TooltipTrigger>
|
title={showAddProject ? '隐藏添加' : '添加项目'}>
|
||||||
<TooltipContent>
|
<PlusIcon className={`w-4 h-4 ${showAddProject ? 'rotate-90' : ''} transition-transform`} />
|
||||||
<p>全部启动 ({selectedProjects.length})</p>
|
</button>
|
||||||
</TooltipContent>
|
<button
|
||||||
</Tooltip>
|
onClick={loadProjects}
|
||||||
<Tooltip>
|
disabled={projectsLoading}
|
||||||
<TooltipTrigger>
|
className='text-slate-500 hover:text-indigo-400 transition-colors p-1.5 rounded-lg hover:bg-white/5'
|
||||||
<button
|
title='刷新'>
|
||||||
onClick={handleStopAll}
|
<RefreshCwIcon className={`w-4 h-4 ${projectsLoading ? 'animate-spin' : ''}`} />
|
||||||
disabled={selectedProjects.length === 0}
|
</button>
|
||||||
className='text-slate-500 hover:text-red-400 transition-colors p-1.5 rounded-lg hover:bg-white/5 disabled:opacity-50'
|
<Tooltip>
|
||||||
title='全部关闭'>
|
<TooltipTrigger>
|
||||||
<StopCircleIcon className='w-4 h-4' />
|
<div
|
||||||
</button>
|
onClick={() => setInitConfirmOpen(true)}
|
||||||
</TooltipTrigger>
|
className='text-slate-500 hover:text-indigo-400 transition-colors p-1.5 rounded-lg hover:bg-white/5 disabled:opacity-50'>
|
||||||
<TooltipContent>
|
<DownloadIcon className='w-4 h-4' />
|
||||||
<p>全部关闭 ({selectedProjects.length})</p>
|
</div>
|
||||||
</TooltipContent>
|
</TooltipTrigger>
|
||||||
</Tooltip>
|
<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>
|
<Tooltip>
|
||||||
<TooltipTrigger>
|
<TooltipTrigger>
|
||||||
<div
|
<div
|
||||||
onClick={toggleMultiSelectMode}
|
onClick={toggleMultiSelectMode}
|
||||||
className='text-slate-500 hover:text-indigo-400 transition-colors p-1.5 rounded-lg hover:bg-white/5'
|
className='text-slate-500 hover:text-indigo-400 transition-colors p-1.5 rounded-lg hover:bg-white/5'
|
||||||
title='退出多选'>
|
title='多选'>
|
||||||
<CheckSquareIcon className='w-4 h-4' />
|
<CheckSquareIcon className='w-4 h-4' />
|
||||||
</div>
|
</div>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
<p>退出多选</p>
|
<p>多选</p>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</>
|
)}
|
||||||
) : (
|
</div>
|
||||||
<Tooltip>
|
</div>
|
||||||
<TooltipTrigger>
|
|
||||||
<div
|
{projectsLoading ? (
|
||||||
onClick={toggleMultiSelectMode}
|
<div className='flex items-center justify-center h-24 text-slate-500 text-sm bg-white/5 rounded-xl border border-white/5'>
|
||||||
className='text-slate-500 hover:text-indigo-400 transition-colors p-1.5 rounded-lg hover:bg-white/5'
|
<span className='flex items-center gap-2'>
|
||||||
title='多选'>
|
<span className='w-4 h-4 border-2 border-indigo-500/30 border-t-indigo-500 rounded-full animate-spin' />
|
||||||
<CheckSquareIcon className='w-4 h-4' />
|
加载中…
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
) : projects.length === 0 ? (
|
||||||
|
<div className='flex flex-col items-center justify-center h-24 text-slate-500 text-sm bg-white/5 rounded-xl border border-white/5 gap-2'>
|
||||||
|
<FolderIcon className='w-8 h-8 text-slate-600' />
|
||||||
|
<span>暂无项目</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<ul className='space-y-2 max-h-72 overflow-y-auto pr-1 scrollbar-thin scrollbar-thumb-slate-700 scrollbar-track-transparent scrollbar'>
|
||||||
|
{projects.map((p) => (
|
||||||
|
<li
|
||||||
|
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 ${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'}`}>
|
||||||
|
<FolderIcon className={`w-4 h-4 ${p.status === 'active' ? 'text-green-400' : p.status === 'unlive' ? 'text-orange-400' : 'text-slate-400'}`} />
|
||||||
</div>
|
</div>
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>
|
|
||||||
<p>多选</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{projectsLoading ? (
|
<div className='flex-1 min-w-0'>
|
||||||
<div className='flex items-center justify-center h-24 text-slate-500 text-sm bg-white/5 rounded-xl border border-white/5'>
|
<p className='text-sm font-medium text-slate-200 truncate'>{p.name ?? p.path.split('/').pop()}</p>
|
||||||
<span className='flex items-center gap-2'>
|
<p className='text-xs text-slate-500 truncate'>{p.path}</p>
|
||||||
<span className='w-4 h-4 border-2 border-indigo-500/30 border-t-indigo-500 rounded-full animate-spin' />
|
</div>
|
||||||
加载中…
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
) : projects.length === 0 ? (
|
|
||||||
<div className='flex flex-col items-center justify-center h-24 text-slate-500 text-sm bg-white/5 rounded-xl border border-white/5 gap-2'>
|
|
||||||
<FolderIcon className='w-8 h-8 text-slate-600' />
|
|
||||||
<span>暂无项目</span>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<ul className='space-y-2 max-h-72 overflow-y-auto pr-1 scrollbar-thin scrollbar-thumb-slate-700 scrollbar-track-transparent scrollbar'>
|
|
||||||
{projects.map((p) => (
|
|
||||||
<li
|
|
||||||
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 ${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'}`}>
|
|
||||||
<FolderIcon className={`w-4 h-4 ${p.status === 'active' ? 'text-green-400' : p.status === 'unlive' ? 'text-orange-400' : 'text-slate-400'}`} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='flex-1 min-w-0'>
|
{/* 状态切换按钮 */}
|
||||||
<p className='text-sm font-medium text-slate-200 truncate'>{p.name ?? p.path.split('/').pop()}</p>
|
{p.status !== undefined && (
|
||||||
<p className='text-xs text-slate-500 truncate'>{p.path}</p>
|
<button
|
||||||
</div>
|
onClick={() => openStatusConfirm(p)}
|
||||||
|
className={`shrink-0 flex items-center gap-1.5 text-xs px-3 py-1.5 rounded-full transition-all duration-200 ${p.status === 'active'
|
||||||
{/* 状态切换按钮 */}
|
|
||||||
{p.status !== undefined && (
|
|
||||||
<button
|
|
||||||
onClick={() => openStatusConfirm(p)}
|
|
||||||
className={`shrink-0 flex items-center gap-1.5 text-xs px-3 py-1.5 rounded-full transition-all duration-200 ${p.status === 'active'
|
|
||||||
? 'bg-green-500/20 text-green-400 hover:bg-green-500/30 hover:scale-105'
|
? 'bg-green-500/20 text-green-400 hover:bg-green-500/30 hover:scale-105'
|
||||||
: p.status === 'unlive'
|
: p.status === 'unlive'
|
||||||
? 'bg-orange-500/20 text-orange-400 hover:bg-orange-500/30 hover:scale-105'
|
? 'bg-orange-500/20 text-orange-400 hover:bg-orange-500/30 hover:scale-105'
|
||||||
: 'bg-slate-700/50 text-slate-400 hover:bg-slate-600/50 hover:scale-105'
|
: 'bg-slate-700/50 text-slate-400 hover:bg-slate-600/50 hover:scale-105'
|
||||||
}`}>
|
}`}>
|
||||||
{p.status === 'active' ? (
|
{p.status === 'active' ? (
|
||||||
<>
|
<>
|
||||||
<StopCircleIcon className='w-3 h-3' />
|
<StopCircleIcon className='w-3 h-3' />
|
||||||
<span>监听中</span>
|
<span>监听中</span>
|
||||||
</>
|
</>
|
||||||
) : p.status === 'unlive' ? (
|
) : p.status === 'unlive' ? (
|
||||||
<>
|
<>
|
||||||
<CircleOffIcon className='w-3 h-3' />
|
<CircleOffIcon className='w-3 h-3' />
|
||||||
<span>未启动</span>
|
<span>未启动</span>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<PlayCircleIcon className='w-3 h-3' />
|
<PlayCircleIcon className='w-3 h-3' />
|
||||||
<span>已停止</span>
|
<span>已停止</span>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 删除按钮 */}
|
{/* 删除按钮 */}
|
||||||
<button
|
<button
|
||||||
onClick={() => openDeleteConfirm(p)}
|
onClick={() => openDeleteConfirm(p)}
|
||||||
className='shrink-0 text-slate-600 hover:text-red-400 transition-all duration-200 p-1.5 rounded-lg hover:bg-red-500/10 opacity-0 group-hover:opacity-100'>
|
className='shrink-0 text-slate-600 hover:text-red-400 transition-all duration-200 p-1.5 rounded-lg hover:bg-red-500/10 opacity-0 group-hover:opacity-100'>
|
||||||
<Trash2Icon className='w-4 h-4' />
|
<Trash2Icon className='w-4 h-4' />
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|
||||||
@@ -531,8 +693,8 @@ export function ProjectDialog() {
|
|||||||
<Button
|
<Button
|
||||||
onClick={handleConfirmStatusChange}
|
onClick={handleConfirmStatusChange}
|
||||||
className={`flex-1 ${pendingStatusProject?.status === 'active'
|
className={`flex-1 ${pendingStatusProject?.status === 'active'
|
||||||
? 'bg-red-600 hover:bg-red-500 shadow-lg shadow-red-500/25'
|
? 'bg-red-600 hover:bg-red-500 shadow-lg shadow-red-500/25'
|
||||||
: 'bg-green-600 hover:bg-green-500 shadow-lg shadow-green-500/25'
|
: 'bg-green-600 hover:bg-green-500 shadow-lg shadow-green-500/25'
|
||||||
}`}>
|
}`}>
|
||||||
{pendingStatusProject?.status === 'active' ? '停止监听' : '开始监听'}
|
{pendingStatusProject?.status === 'active' ? '停止监听' : '开始监听'}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -570,44 +732,8 @@ export function ProjectDialog() {
|
|||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
{/* 初始化确认弹窗 */}
|
{/* 初始化弹窗 */}
|
||||||
<Dialog open={initConfirmOpen} onOpenChange={(open) => !open && handleCancelInit()}>
|
<ProjectInitDialog open={initConfirmOpen} onOpenChange={setInitConfirmOpen} />
|
||||||
<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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,17 +8,16 @@ import { useBotHelperStore } from '../store/bot-helper';
|
|||||||
|
|
||||||
interface ProjectPanelProps {
|
interface ProjectPanelProps {
|
||||||
onProjectClick?: (projectPath: string, files: FileProjectData[]) => void;
|
onProjectClick?: (projectPath: string, files: FileProjectData[]) => void;
|
||||||
onOpenCodePod?: (projectPath: string) => void;
|
|
||||||
onStopProject?: (projectPath: string) => void;
|
onStopProject?: (projectPath: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ProjectPanel({
|
export function ProjectPanel({
|
||||||
onProjectClick,
|
onProjectClick,
|
||||||
onOpenCodePod,
|
|
||||||
onStopProject,
|
onStopProject,
|
||||||
}: ProjectPanelProps) {
|
}: ProjectPanelProps) {
|
||||||
const [isDragging, setIsDragging] = useState(false);
|
const [isDragging, setIsDragging] = useState(false);
|
||||||
const [position, setPosition] = useState({ x: 20, y: 100 });
|
const [position, setPosition] = useState({ x: 20, y: 100 });
|
||||||
|
const [selectedProject, setSelectedProject] = useState<string | null>(null);
|
||||||
const dragOffset = useRef({ x: 0, y: 0 });
|
const dragOffset = useRef({ x: 0, y: 0 });
|
||||||
const panelRef = useRef<HTMLDivElement>(null);
|
const panelRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
@@ -83,16 +82,47 @@ export function ProjectPanel({
|
|||||||
|
|
||||||
const handleProjectClick = useCallback(
|
const handleProjectClick = useCallback(
|
||||||
(projectPath: string) => {
|
(projectPath: string) => {
|
||||||
|
// 如果点击的是已选中的项目,则取消选中
|
||||||
|
if (selectedProject === projectPath) {
|
||||||
|
setSelectedProject(null);
|
||||||
|
window.location.hash = '';
|
||||||
|
useCodeGraphStore.getState().fetchProjects();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从 projects 列表中查找对应项目的 repo
|
||||||
const project = projects.find((p) => p.path === projectPath);
|
const project = projects.find((p) => p.path === projectPath);
|
||||||
|
const repo = project?.repo;
|
||||||
|
|
||||||
|
if (repo) {
|
||||||
|
// 设置 hash 并刷新
|
||||||
|
window.location.hash = `repo=${repo}`;
|
||||||
|
useCodeGraphStore.getState().fetchProjects();
|
||||||
|
}
|
||||||
|
|
||||||
|
setSelectedProject(projectPath);
|
||||||
|
|
||||||
if (project && onProjectClick) {
|
if (project && onProjectClick) {
|
||||||
onProjectClick(projectPath, files);
|
onProjectClick(projectPath, files);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[projects, files, onProjectClick],
|
[projects, files, onProjectClick, selectedProject],
|
||||||
);
|
);
|
||||||
|
|
||||||
const [isLargeScreen, setIsLargeScreen] = useState(false);
|
const [isLargeScreen, setIsLargeScreen] = useState(false);
|
||||||
|
|
||||||
|
// 初始化选中状态
|
||||||
|
useEffect(() => {
|
||||||
|
const hashParams = new URLSearchParams(window.location.hash.slice(1));
|
||||||
|
const repo = hashParams.get('repo');
|
||||||
|
if (repo && projects.length > 0) {
|
||||||
|
const projectItem = projects.find((p) => p.repo === repo);
|
||||||
|
if (projectItem) {
|
||||||
|
setSelectedProject(projectItem.path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [projects]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const checkScreen = () => {
|
const checkScreen = () => {
|
||||||
setIsLargeScreen(window.innerWidth >= 1024);
|
setIsLargeScreen(window.innerWidth >= 1024);
|
||||||
@@ -143,7 +173,11 @@ export function ProjectPanel({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={project.path}
|
key={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 group'
|
className={`flex items-center gap-2 px-3 py-2 text-left text-sm transition-colors border-l-2 group ${
|
||||||
|
selectedProject === project.path
|
||||||
|
? 'bg-indigo-500/20 text-white border-indigo-500'
|
||||||
|
: 'text-slate-300 hover:bg-white/5 hover:text-white border-transparent hover:border-indigo-500'
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
onClick={() => handleProjectClick(project.path)}
|
onClick={() => handleProjectClick(project.path)}
|
||||||
|
|||||||
@@ -97,19 +97,6 @@ export default function CodeGraphPage() {
|
|||||||
<Code3DGraph files={files} className='h-full' onProjectFocus={projectFocus ?? undefined} />
|
<Code3DGraph files={files} className='h-full' onProjectFocus={projectFocus ?? undefined} />
|
||||||
<ProjectPanel
|
<ProjectPanel
|
||||||
onProjectClick={handleProjectClick}
|
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)}
|
onStopProject={(projectPath) => toggleProjectStatus(projectPath)}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { FileProjectData } from '../modules/tree';
|
|||||||
import { UserInfo } from '@/pages/auth/store';
|
import { UserInfo } from '@/pages/auth/store';
|
||||||
import { Result } from '@kevisual/query';
|
import { Result } from '@kevisual/query';
|
||||||
import { AssistantMessage, Part } from '@opencode-ai/sdk'
|
import { AssistantMessage, Part } from '@opencode-ai/sdk'
|
||||||
|
import { isCNB } from '@/modules/cnb';
|
||||||
export type ProjectItem = {
|
export type ProjectItem = {
|
||||||
path: string;
|
path: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
@@ -14,7 +15,7 @@ export type ProjectItem = {
|
|||||||
status?: 'active' | 'inactive' | 'unlive';
|
status?: 'active' | 'inactive' | 'unlive';
|
||||||
};
|
};
|
||||||
|
|
||||||
const API_URL = '/root/v1/cnb-dev';
|
const API_URL = '/root/v1/dev-cnb';
|
||||||
|
|
||||||
export type NodeInfoData = {
|
export type NodeInfoData = {
|
||||||
label: string;
|
label: string;
|
||||||
@@ -24,6 +25,11 @@ export type NodeInfoData = {
|
|||||||
color: string;
|
color: string;
|
||||||
fileId?: string;
|
fileId?: string;
|
||||||
nodeSize?: number;
|
nodeSize?: number;
|
||||||
|
title?: string;
|
||||||
|
tags?: string[];
|
||||||
|
summary?: string;
|
||||||
|
description?: string;
|
||||||
|
link?: string;
|
||||||
};
|
};
|
||||||
export type OpencodeResult = {
|
export type OpencodeResult = {
|
||||||
info: AssistantMessage;
|
info: AssistantMessage;
|
||||||
@@ -47,7 +53,9 @@ 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>;
|
initProject: (projectPaths?: string[]) => Promise<void>;
|
||||||
|
// 获取项目文件列表
|
||||||
|
fetchProjectFiles: (rootPath?: string) => Promise<string[]>;
|
||||||
// NodeInfo 弹窗
|
// NodeInfo 弹窗
|
||||||
nodeInfoOpen: boolean;
|
nodeInfoOpen: boolean;
|
||||||
setNodeInfoOpen: (open: boolean) => void;
|
setNodeInfoOpen: (open: boolean) => void;
|
||||||
@@ -63,6 +71,8 @@ type State = {
|
|||||||
q?: string; // 可选的搜索关键词
|
q?: string; // 可选的搜索关键词
|
||||||
projectPath?: string; // 项目路径,必填
|
projectPath?: string; // 项目路径,必填
|
||||||
getContent?: boolean; // 是否获取文件内容,默认为 false
|
getContent?: boolean; // 是否获取文件内容,默认为 false
|
||||||
|
repo?: string; // 仓库地址
|
||||||
|
projects?: string[]
|
||||||
}) => Promise<Result<{ list: FileProjectData[] }>>;
|
}) => Promise<Result<{ list: FileProjectData[] }>>;
|
||||||
createQuestion: (opts: { question: string, projectPath?: string, filePath?: string, engine?: 'openclaw' | 'opencode', sessionId?: string }) => any;
|
createQuestion: (opts: { question: string, projectPath?: string, filePath?: string, engine?: 'openclaw' | 'opencode', sessionId?: string }) => any;
|
||||||
saveFile: (filepath: string, content: string) => Promise<void>;
|
saveFile: (filepath: string, content: string) => Promise<void>;
|
||||||
@@ -137,11 +147,11 @@ export const useCodeGraphStore = create<State>()((set, get) => ({
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
initProject: async () => {
|
initProject: async (projectPaths) => {
|
||||||
const loadingToast = toast.loading('初始化项目中...');
|
const loadingToast = toast.loading('初始化项目中...');
|
||||||
try {
|
try {
|
||||||
const url = get().url || API_URL;
|
const url = get().url || API_URL;
|
||||||
const res = await projectApi.project.init(undefined, { url });
|
const res = await projectApi.project.init({ projectPaths }, { url });
|
||||||
toast.dismiss(loadingToast);
|
toast.dismiss(loadingToast);
|
||||||
if (res.code === 200) {
|
if (res.code === 200) {
|
||||||
toast.success('项目初始化成功');
|
toast.success('项目初始化成功');
|
||||||
@@ -154,6 +164,15 @@ export const useCodeGraphStore = create<State>()((set, get) => ({
|
|||||||
toast.error('项目初始化失败');
|
toast.error('项目初始化失败');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
fetchProjectFiles: async (rootPath) => {
|
||||||
|
const url = get().url || API_URL;
|
||||||
|
const res = await projectApi['project']['project-files']({ rootPath }, { url });
|
||||||
|
if (res.code === 200) {
|
||||||
|
const data = res.data as { projectFiles?: string[]; list?: string[] };
|
||||||
|
return data?.projectFiles ?? data?.list ?? [];
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
},
|
||||||
removeProject: async (path) => {
|
removeProject: async (path) => {
|
||||||
const loadingToast = toast.loading('移除项目中...');
|
const loadingToast = toast.loading('移除项目中...');
|
||||||
try {
|
try {
|
||||||
@@ -223,7 +242,14 @@ export const useCodeGraphStore = create<State>()((set, get) => ({
|
|||||||
// 可以在这里根据用户信息初始化一些数据,比如权限相关的设置等
|
// 可以在这里根据用户信息初始化一些数据,比如权限相关的设置等
|
||||||
console.log('CodeGraphStore initialized for user:', user.username);
|
console.log('CodeGraphStore initialized for user:', user.username);
|
||||||
const username = user.username;
|
const username = user.username;
|
||||||
const url = username ? `/${username}/v1/cnb-dev` : API_URL;
|
let url = username ? `/${username}/v1/dev-cnb` : API_URL;
|
||||||
|
if (isCNB()) {
|
||||||
|
url = `/client/router`;
|
||||||
|
}
|
||||||
|
const urlFromHash = new URLSearchParams(window.location.hash.slice(1)).get('url');
|
||||||
|
if (urlFromHash) {
|
||||||
|
url = urlFromHash;
|
||||||
|
}
|
||||||
set({ url });
|
set({ url });
|
||||||
const load = opts.load ?? true;
|
const load = opts.load ?? true;
|
||||||
if (load) {
|
if (load) {
|
||||||
@@ -232,7 +258,12 @@ export const useCodeGraphStore = create<State>()((set, get) => ({
|
|||||||
},
|
},
|
||||||
fetchProjects: async () => {
|
fetchProjects: async () => {
|
||||||
get().loadProjects();
|
get().loadProjects();
|
||||||
const res = await get().getFiles();
|
// 从hash中获取repo参数 (#repo=kevisual/cnb 格式)
|
||||||
|
const hashParams = new URLSearchParams(window.location.hash.slice(1));
|
||||||
|
const repo = hashParams.get('repo');
|
||||||
|
const res = await get().getFiles({
|
||||||
|
repo: repo || undefined,
|
||||||
|
});
|
||||||
if (res.code === 200) {
|
if (res.code === 200) {
|
||||||
set({ files: res.data!.list });
|
set({ files: res.data!.list });
|
||||||
} else {
|
} else {
|
||||||
@@ -244,6 +275,8 @@ export const useCodeGraphStore = create<State>()((set, get) => ({
|
|||||||
q?: string; // 可选的搜索关键词
|
q?: string; // 可选的搜索关键词
|
||||||
projectPath?: string; // 项目路径,必填
|
projectPath?: string; // 项目路径,必填
|
||||||
getContent?: boolean; // 是否获取文件内容,默认为 false
|
getContent?: boolean; // 是否获取文件内容,默认为 false
|
||||||
|
projects?: string[];
|
||||||
|
repo?: string; // 仓库地址
|
||||||
}) => {
|
}) => {
|
||||||
const url = get().url
|
const url = get().url
|
||||||
const res = await projectApi["project-search"].files({
|
const res = await projectApi["project-search"].files({
|
||||||
|
|||||||
@@ -32,7 +32,8 @@ export default defineConfig({
|
|||||||
tailwindcss(),
|
tailwindcss(),
|
||||||
VitePWA({
|
VitePWA({
|
||||||
injectRegister: 'auto',
|
injectRegister: 'auto',
|
||||||
workbox:{
|
registerType: 'autoUpdate',
|
||||||
|
workbox: {
|
||||||
maximumFileSizeToCacheInBytes: 10 * 1024 * 1024, // 10MB
|
maximumFileSizeToCacheInBytes: 10 * 1024 * 1024, // 10MB
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|||||||
Reference in New Issue
Block a user