updated files related to chat-dev page, added onSend function to handle sending questions, and updated the button's onClick handler to call this function. Also added new types in code-graph store for handling results from opencode AI SDK.

This commit is contained in:
xiongxiao
2026-03-16 00:04:35 +08:00
committed by cnb
parent 1afd39b970
commit b4980ab4f9
7 changed files with 241 additions and 10 deletions

View File

@@ -32,6 +32,7 @@
"@tanstack/react-router": "^1.166.7", "@tanstack/react-router": "^1.166.7",
"@uiw/codemirror-theme-vscode": "^4.25.8", "@uiw/codemirror-theme-vscode": "^4.25.8",
"@uiw/react-codemirror": "^4.25.8", "@uiw/react-codemirror": "^4.25.8",
"ai": "^6.0.116",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"cmdk": "^1.1.1", "cmdk": "^1.1.1",
@@ -62,6 +63,7 @@
"@kevisual/kv-login": "^0.1.17", "@kevisual/kv-login": "^0.1.17",
"@kevisual/query": "0.0.53", "@kevisual/query": "0.0.53",
"@kevisual/types": "^0.0.12", "@kevisual/types": "^0.0.12",
"@opencode-ai/sdk": "^1.2.26",
"@tailwindcss/vite": "^4.2.1", "@tailwindcss/vite": "^4.2.1",
"@tanstack/react-router-devtools": "^1.166.7", "@tanstack/react-router-devtools": "^1.166.7",
"@tanstack/router-plugin": "^1.166.7", "@tanstack/router-plugin": "^1.166.7",

123
src/pages/chat-dev/page.tsx Normal file
View File

@@ -0,0 +1,123 @@
import { useEffect } from 'react';
import { useSearch } from '@tanstack/react-router';
import { useShallow } from 'zustand/react/shallow';
import { BotIcon, FileIcon, FolderIcon } from 'lucide-react';
import { useChatDevStore } from './store';
import { BOT_KEYS, BotKey } from '@/pages/code-graph/store/bot-helper';
import openclawSvg from '@/pages/code-graph/assets/openclaw.svg';
import opencodePng from '@/pages/code-graph/assets/opencode.png';
import { useCodeGraphStore } from '../code-graph/store';
import { useLayoutStore } from '../auth/store';
const BOT_ICONS: Record<BotKey, string> = {
openclaw: openclawSvg,
opencode: opencodePng,
};
export const App = () => {
const { timestamp } = useSearch({ from: '/chat-dev' });
const { question, engine, projectInfo, initFromTimestamp, setData } = useChatDevStore(
useShallow((s) => ({
question: s.question,
engine: s.engine,
projectInfo: s.projectInfo,
initFromTimestamp: s.initFromTimestamp,
setData: s.setData,
})),
);
const layoutStore = useLayoutStore(useShallow((s) => ({
me: s.me,
})));
const codeGraphStore = useCodeGraphStore(useShallow((s) => ({
createQuestion: s.createQuestion,
init: s.init,
})));
useEffect(() => {
if (!layoutStore.me?.username) return;
codeGraphStore.init(layoutStore.me, { load: false });
}, [layoutStore.me]);
useEffect(() => {
if (timestamp) {
initFromTimestamp(timestamp);
}
}, [timestamp]);
const relativePath = projectInfo
? (projectInfo.filepath || '').replace((projectInfo.projectPath || '') + '/', '') || '/'
: null;
const onSend = async () => {
if (projectInfo) {
const res = await codeGraphStore.createQuestion({
question,
projectPath: projectInfo.projectPath,
engine,
});
console.log(res);
}
}
return (
<div className='h-full bg-slate-950 text-slate-100 flex flex-col items-center py-10 px-4'>
<div className='w-full max-w-2xl rounded-xl border border-white/10 bg-slate-900 shadow-2xl flex flex-col'>
{/* 标题栏 */}
<div className='flex items-center gap-2 px-4 py-3 border-b border-white/10'>
<BotIcon className='size-4 text-emerald-400' />
<span className='text-sm font-medium text-slate-100'>AI </span>
</div>
{/* 内容区 */}
<div className='px-4 py-4 flex flex-col gap-4'>
{/* 节点信息 + Bot 切换 */}
<div className='flex items-center gap-2'>
{relativePath && (
<div className='flex-1 flex items-center gap-2 px-3 py-2 rounded-lg bg-slate-800/60 border border-white/5 min-w-0'>
<FileIcon className='size-4 shrink-0 text-slate-400' />
<span className='text-xs text-slate-300 font-mono truncate' title={relativePath}>
{relativePath}
</span>
</div>
)}
{projectInfo?.projectPath && !relativePath && (
<div className='flex-1 flex items-center gap-2 px-3 py-2 rounded-lg bg-slate-800/60 border border-white/5 min-w-0'>
<FolderIcon className='size-4 shrink-0 text-slate-400' />
<span className='text-xs text-slate-300 font-mono truncate'>
{projectInfo.projectPath}
</span>
</div>
)}
{/* Bot 切换按钮组 */}
<div className='flex items-center gap-1 shrink-0 ml-auto'>
{BOT_KEYS.map((key) => (
<button
key={key}
title={key}
onClick={() => setData({ engine: key })}
className={`p-1.5 rounded-lg border transition-colors ${engine === key
? 'border-emerald-500/60 bg-emerald-500/10'
: 'border-white/5 bg-slate-800/60 opacity-40 hover:opacity-70'
}`}>
<img src={BOT_ICONS[key]} alt={key} className='size-5 object-contain' />
</button>
))}
</div>
</div>
{/* 问题输入区 */}
<textarea
className='w-full rounded-lg border border-white/10 bg-slate-800 px-3 py-2 text-sm text-slate-200 placeholder:text-slate-500 focus:outline-none focus:ring-1 focus:ring-emerald-500 resize-none'
rows={6}
placeholder='请输入内容...'
value={question}
onChange={(e) => setData({ question: e.target.value })}
/>
<button className='w-full rounded-lg bg-emerald-600 hover:bg-emerald-500 active:bg-emerald-700 text-white text-sm font-medium py-2 transition-colors' onClick={onSend}>
</button>
</div>
</div>
</div>
);
};
export default App;

View File

@@ -0,0 +1,64 @@
import { create } from 'zustand';
import { BotKey } from '@/pages/code-graph/store/bot-helper';
export type ChatDevData = {
question: string;
engine: BotKey;
projectInfo: {
filepath: string;
projectPath: string;
} | null;
};
type ChatDevState = {
question: string;
engine: BotKey;
projectInfo: {
filepath: string;
projectPath: string;
} | null;
setData: (data: Partial<ChatDevData>) => void;
initFromTimestamp: (timestamp: string) => void;
};
const SESSION_KEY_PREFIX = 'chat-dev-';
const SESSION_KEY = 'chat-dev';
export const useChatDevStore = create<ChatDevState>()((set) => ({
question: '',
engine: 'opencode',
projectInfo: null,
setData: (data) => set((s) => ({ ...s, ...data })),
initFromTimestamp: (timestamp: string) => {
const key = SESSION_KEY_PREFIX + timestamp;
// 优先从 localStorage 读取(首次打开)
const localRaw = localStorage.getItem(key);
if (localRaw) {
try {
const data: ChatDevData = JSON.parse(localRaw);
// 持久化到 sessionStorage供刷新页面使用不带 timestamp
sessionStorage.setItem(SESSION_KEY, localRaw);
// 清除 localStorage
localStorage.removeItem(key);
set({ question: data.question, engine: data.engine, projectInfo: data.projectInfo });
return;
} catch {
localStorage.removeItem(key);
}
}
// 刷新页面时从 sessionStorage 读取(固定 key不带 timestamp
const sessionRaw = sessionStorage.getItem(SESSION_KEY);
if (sessionRaw) {
try {
const data: ChatDevData = JSON.parse(sessionRaw);
set({ question: data.question, engine: data.engine, projectInfo: data.projectInfo });
return;
} catch {
sessionStorage.removeItem(SESSION_KEY);
}
}
},
}));

View File

@@ -1,5 +1,5 @@
import { BotIcon, XIcon, FileIcon, FolderIcon, DatabaseIcon, MoreHorizontalIcon } from 'lucide-react'; import { BotIcon, XIcon, FileIcon, FolderIcon, DatabaseIcon, MoreHorizontalIcon } from 'lucide-react';
import { getDynamicBasename, wrapBasename } from '@/modules/basename'; import { useNavigate, useLocation } from '@tanstack/react-router';
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';
@@ -27,13 +27,16 @@ export function BotHelperModal() {
closeModal: s.closeModal, closeModal: s.closeModal,
activeKey: s.activeKey, activeKey: s.activeKey,
setActiveKey: s.setActiveKey, setActiveKey: s.setActiveKey,
projectInfo: s.projectInfo,
})), })),
); );
const { nodeInfoData, createQuestion } = useCodeGraphStore(useShallow((s) => ({ const { nodeInfoData, createQuestion } = useCodeGraphStore(useShallow((s) => ({
nodeInfoData: s.nodeInfoData, nodeInfoData: s.nodeInfoData,
createQuestion: s.createQuestion, createQuestion: s.createQuestion,
}))); })));
const location = useLocation();
console.log('BotHelperModal render', location);
const basename = location.publicHref.replace(location.href, '');
const relativePath = nodeInfoData const relativePath = nodeInfoData
? nodeInfoData.fullPath.replace((nodeInfoData.projectPath || '') + '/', '') || '/' ? nodeInfoData.fullPath.replace((nodeInfoData.projectPath || '') + '/', '') || '/'
: null; : null;
@@ -67,7 +70,12 @@ export function BotHelperModal() {
<button <button
onClick={() => { onClick={() => {
const timestamp = Date.now(); const timestamp = Date.now();
window.open(`/code-graph?timestamp=${timestamp}`, '_blank'); localStorage.setItem('chat-dev-' + timestamp, JSON.stringify({
question: botHelperStore.input,
engine: botHelperStore.activeKey,
projectInfo: botHelperStore.projectInfo,
}));
window.open(`${basename}/chat-dev?timestamp=${timestamp}`, '_blank');
}} }}
className='text-slate-500 hover:text-slate-200 transition-colors p-1 rounded hover:bg-white/10' className='text-slate-500 hover:text-slate-200 transition-colors p-1 rounded hover:bg-white/10'
title='新窗口打开'> title='新窗口打开'>

View File

@@ -6,7 +6,7 @@ import { toast } from 'sonner';
import { FileProjectData } from '../modules/tree'; 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'
export type ProjectItem = { export type ProjectItem = {
path: string; path: string;
name?: string; name?: string;
@@ -25,6 +25,10 @@ export type NodeInfoData = {
fileId?: string; fileId?: string;
nodeSize?: number; nodeSize?: number;
}; };
export type OpencodeResult = {
info: AssistantMessage;
parts: Array<Part>;
}
type State = { type State = {
codePodOpen: boolean; codePodOpen: boolean;
@@ -52,7 +56,7 @@ type State = {
setNodeInfo: (data: NodeInfoData | null, pos?: { x: number; y: number }) => void; setNodeInfo: (data: NodeInfoData | null, pos?: { x: number; y: number }) => void;
closeNodeInfo: () => void; closeNodeInfo: () => void;
url?: string; url?: string;
init(user: UserInfo): Promise<void>; init(user: UserInfo, opts?: { load?: boolean }): Promise<void>;
fetchProjects: () => Promise<void>; fetchProjects: () => Promise<void>;
getFiles: (opts?: { getFiles: (opts?: {
filepath?: string; // 可选的目录路径,默认为根目录 filepath?: string; // 可选的目录路径,默认为根目录
@@ -194,13 +198,16 @@ export const useCodeGraphStore = create<State>()((set, get) => ({
}), }),
closeNodeInfo: () => set({ nodeInfoOpen: false, nodeInfoData: null }), closeNodeInfo: () => set({ nodeInfoOpen: false, nodeInfoData: null }),
url: API_URL, url: API_URL,
init: async (user) => { init: async (user, opts = {}) => {
// 可以在这里根据用户信息初始化一些数据,比如权限相关的设置等 // 可以在这里根据用户信息初始化一些数据,比如权限相关的设置等
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; const url = username ? `/${username}/v1/cnb-dev` : API_URL;
set({ url }); set({ url });
await get().fetchProjects(); const load = opts.load ?? true;
if (load) {
await get().fetchProjects();
}
}, },
fetchProjects: async () => { fetchProjects: async () => {
get().loadProjects(); get().loadProjects();

View File

@@ -12,6 +12,7 @@ import { Route as rootRouteImport } from './routes/__root'
import { Route as LoginRouteImport } from './routes/login' import { Route as LoginRouteImport } from './routes/login'
import { Route as DemoRouteImport } from './routes/demo' import { Route as DemoRouteImport } from './routes/demo'
import { Route as CodeGraphRouteImport } from './routes/code-graph' import { Route as CodeGraphRouteImport } from './routes/code-graph'
import { Route as ChatDevRouteImport } from './routes/chat-dev'
import { Route as IndexRouteImport } from './routes/index' import { Route as IndexRouteImport } from './routes/index'
const LoginRoute = LoginRouteImport.update({ const LoginRoute = LoginRouteImport.update({
@@ -29,6 +30,11 @@ const CodeGraphRoute = CodeGraphRouteImport.update({
path: '/code-graph', path: '/code-graph',
getParentRoute: () => rootRouteImport, getParentRoute: () => rootRouteImport,
} as any) } as any)
const ChatDevRoute = ChatDevRouteImport.update({
id: '/chat-dev',
path: '/chat-dev',
getParentRoute: () => rootRouteImport,
} as any)
const IndexRoute = IndexRouteImport.update({ const IndexRoute = IndexRouteImport.update({
id: '/', id: '/',
path: '/', path: '/',
@@ -37,12 +43,14 @@ const IndexRoute = IndexRouteImport.update({
export interface FileRoutesByFullPath { export interface FileRoutesByFullPath {
'/': typeof IndexRoute '/': typeof IndexRoute
'/chat-dev': typeof ChatDevRoute
'/code-graph': typeof CodeGraphRoute '/code-graph': typeof CodeGraphRoute
'/demo': typeof DemoRoute '/demo': typeof DemoRoute
'/login': typeof LoginRoute '/login': typeof LoginRoute
} }
export interface FileRoutesByTo { export interface FileRoutesByTo {
'/': typeof IndexRoute '/': typeof IndexRoute
'/chat-dev': typeof ChatDevRoute
'/code-graph': typeof CodeGraphRoute '/code-graph': typeof CodeGraphRoute
'/demo': typeof DemoRoute '/demo': typeof DemoRoute
'/login': typeof LoginRoute '/login': typeof LoginRoute
@@ -50,20 +58,22 @@ export interface FileRoutesByTo {
export interface FileRoutesById { export interface FileRoutesById {
__root__: typeof rootRouteImport __root__: typeof rootRouteImport
'/': typeof IndexRoute '/': typeof IndexRoute
'/chat-dev': typeof ChatDevRoute
'/code-graph': typeof CodeGraphRoute '/code-graph': typeof CodeGraphRoute
'/demo': typeof DemoRoute '/demo': typeof DemoRoute
'/login': typeof LoginRoute '/login': typeof LoginRoute
} }
export interface FileRouteTypes { export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath fileRoutesByFullPath: FileRoutesByFullPath
fullPaths: '/' | '/code-graph' | '/demo' | '/login' fullPaths: '/' | '/chat-dev' | '/code-graph' | '/demo' | '/login'
fileRoutesByTo: FileRoutesByTo fileRoutesByTo: FileRoutesByTo
to: '/' | '/code-graph' | '/demo' | '/login' to: '/' | '/chat-dev' | '/code-graph' | '/demo' | '/login'
id: '__root__' | '/' | '/code-graph' | '/demo' | '/login' id: '__root__' | '/' | '/chat-dev' | '/code-graph' | '/demo' | '/login'
fileRoutesById: FileRoutesById fileRoutesById: FileRoutesById
} }
export interface RootRouteChildren { export interface RootRouteChildren {
IndexRoute: typeof IndexRoute IndexRoute: typeof IndexRoute
ChatDevRoute: typeof ChatDevRoute
CodeGraphRoute: typeof CodeGraphRoute CodeGraphRoute: typeof CodeGraphRoute
DemoRoute: typeof DemoRoute DemoRoute: typeof DemoRoute
LoginRoute: typeof LoginRoute LoginRoute: typeof LoginRoute
@@ -92,6 +102,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof CodeGraphRouteImport preLoaderRoute: typeof CodeGraphRouteImport
parentRoute: typeof rootRouteImport parentRoute: typeof rootRouteImport
} }
'/chat-dev': {
id: '/chat-dev'
path: '/chat-dev'
fullPath: '/chat-dev'
preLoaderRoute: typeof ChatDevRouteImport
parentRoute: typeof rootRouteImport
}
'/': { '/': {
id: '/' id: '/'
path: '/' path: '/'
@@ -104,6 +121,7 @@ declare module '@tanstack/react-router' {
const rootRouteChildren: RootRouteChildren = { const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute, IndexRoute: IndexRoute,
ChatDevRoute: ChatDevRoute,
CodeGraphRoute: CodeGraphRoute, CodeGraphRoute: CodeGraphRoute,
DemoRoute: DemoRoute, DemoRoute: DemoRoute,
LoginRoute: LoginRoute, LoginRoute: LoginRoute,

9
src/routes/chat-dev.tsx Normal file
View File

@@ -0,0 +1,9 @@
import { createFileRoute } from '@tanstack/react-router'
import {App} from '../pages/chat-dev/page'
export const Route = createFileRoute('/chat-dev')({
validateSearch: (search: Record<string, unknown>) => ({
timestamp: search.timestamp as string | undefined,
}),
component: App,
})