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:
@@ -32,6 +32,7 @@
|
||||
"@tanstack/react-router": "^1.166.7",
|
||||
"@uiw/codemirror-theme-vscode": "^4.25.8",
|
||||
"@uiw/react-codemirror": "^4.25.8",
|
||||
"ai": "^6.0.116",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
@@ -62,6 +63,7 @@
|
||||
"@kevisual/kv-login": "^0.1.17",
|
||||
"@kevisual/query": "0.0.53",
|
||||
"@kevisual/types": "^0.0.12",
|
||||
"@opencode-ai/sdk": "^1.2.26",
|
||||
"@tailwindcss/vite": "^4.2.1",
|
||||
"@tanstack/react-router-devtools": "^1.166.7",
|
||||
"@tanstack/router-plugin": "^1.166.7",
|
||||
|
||||
123
src/pages/chat-dev/page.tsx
Normal file
123
src/pages/chat-dev/page.tsx
Normal 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;
|
||||
64
src/pages/chat-dev/store/index.ts
Normal file
64
src/pages/chat-dev/store/index.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
},
|
||||
}));
|
||||
@@ -1,5 +1,5 @@
|
||||
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 { useShallow } from 'zustand/react/shallow';
|
||||
import { useCodeGraphStore, NodeInfoData } from '../store';
|
||||
@@ -27,13 +27,16 @@ export function BotHelperModal() {
|
||||
closeModal: s.closeModal,
|
||||
activeKey: s.activeKey,
|
||||
setActiveKey: s.setActiveKey,
|
||||
projectInfo: s.projectInfo,
|
||||
})),
|
||||
);
|
||||
const { nodeInfoData, createQuestion } = useCodeGraphStore(useShallow((s) => ({
|
||||
nodeInfoData: s.nodeInfoData,
|
||||
createQuestion: s.createQuestion,
|
||||
})));
|
||||
|
||||
const location = useLocation();
|
||||
console.log('BotHelperModal render', location);
|
||||
const basename = location.publicHref.replace(location.href, '');
|
||||
const relativePath = nodeInfoData
|
||||
? nodeInfoData.fullPath.replace((nodeInfoData.projectPath || '') + '/', '') || '/'
|
||||
: null;
|
||||
@@ -67,7 +70,12 @@ export function BotHelperModal() {
|
||||
<button
|
||||
onClick={() => {
|
||||
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'
|
||||
title='新窗口打开'>
|
||||
|
||||
@@ -6,7 +6,7 @@ import { toast } from 'sonner';
|
||||
import { FileProjectData } from '../modules/tree';
|
||||
import { UserInfo } from '@/pages/auth/store';
|
||||
import { Result } from '@kevisual/query';
|
||||
|
||||
import { AssistantMessage, Part } from '@opencode-ai/sdk'
|
||||
export type ProjectItem = {
|
||||
path: string;
|
||||
name?: string;
|
||||
@@ -25,6 +25,10 @@ export type NodeInfoData = {
|
||||
fileId?: string;
|
||||
nodeSize?: number;
|
||||
};
|
||||
export type OpencodeResult = {
|
||||
info: AssistantMessage;
|
||||
parts: Array<Part>;
|
||||
}
|
||||
|
||||
type State = {
|
||||
codePodOpen: boolean;
|
||||
@@ -52,7 +56,7 @@ type State = {
|
||||
setNodeInfo: (data: NodeInfoData | null, pos?: { x: number; y: number }) => void;
|
||||
closeNodeInfo: () => void;
|
||||
url?: string;
|
||||
init(user: UserInfo): Promise<void>;
|
||||
init(user: UserInfo, opts?: { load?: boolean }): Promise<void>;
|
||||
fetchProjects: () => Promise<void>;
|
||||
getFiles: (opts?: {
|
||||
filepath?: string; // 可选的目录路径,默认为根目录
|
||||
@@ -194,13 +198,16 @@ export const useCodeGraphStore = create<State>()((set, get) => ({
|
||||
}),
|
||||
closeNodeInfo: () => set({ nodeInfoOpen: false, nodeInfoData: null }),
|
||||
url: API_URL,
|
||||
init: async (user) => {
|
||||
init: async (user, opts = {}) => {
|
||||
// 可以在这里根据用户信息初始化一些数据,比如权限相关的设置等
|
||||
console.log('CodeGraphStore initialized for user:', user.username);
|
||||
const username = user.username;
|
||||
const url = username ? `/${username}/v1/cnb-dev` : API_URL;
|
||||
set({ url });
|
||||
await get().fetchProjects();
|
||||
const load = opts.load ?? true;
|
||||
if (load) {
|
||||
await get().fetchProjects();
|
||||
}
|
||||
},
|
||||
fetchProjects: async () => {
|
||||
get().loadProjects();
|
||||
|
||||
@@ -12,6 +12,7 @@ import { Route as rootRouteImport } from './routes/__root'
|
||||
import { Route as LoginRouteImport } from './routes/login'
|
||||
import { Route as DemoRouteImport } from './routes/demo'
|
||||
import { Route as CodeGraphRouteImport } from './routes/code-graph'
|
||||
import { Route as ChatDevRouteImport } from './routes/chat-dev'
|
||||
import { Route as IndexRouteImport } from './routes/index'
|
||||
|
||||
const LoginRoute = LoginRouteImport.update({
|
||||
@@ -29,6 +30,11 @@ const CodeGraphRoute = CodeGraphRouteImport.update({
|
||||
path: '/code-graph',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const ChatDevRoute = ChatDevRouteImport.update({
|
||||
id: '/chat-dev',
|
||||
path: '/chat-dev',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const IndexRoute = IndexRouteImport.update({
|
||||
id: '/',
|
||||
path: '/',
|
||||
@@ -37,12 +43,14 @@ const IndexRoute = IndexRouteImport.update({
|
||||
|
||||
export interface FileRoutesByFullPath {
|
||||
'/': typeof IndexRoute
|
||||
'/chat-dev': typeof ChatDevRoute
|
||||
'/code-graph': typeof CodeGraphRoute
|
||||
'/demo': typeof DemoRoute
|
||||
'/login': typeof LoginRoute
|
||||
}
|
||||
export interface FileRoutesByTo {
|
||||
'/': typeof IndexRoute
|
||||
'/chat-dev': typeof ChatDevRoute
|
||||
'/code-graph': typeof CodeGraphRoute
|
||||
'/demo': typeof DemoRoute
|
||||
'/login': typeof LoginRoute
|
||||
@@ -50,20 +58,22 @@ export interface FileRoutesByTo {
|
||||
export interface FileRoutesById {
|
||||
__root__: typeof rootRouteImport
|
||||
'/': typeof IndexRoute
|
||||
'/chat-dev': typeof ChatDevRoute
|
||||
'/code-graph': typeof CodeGraphRoute
|
||||
'/demo': typeof DemoRoute
|
||||
'/login': typeof LoginRoute
|
||||
}
|
||||
export interface FileRouteTypes {
|
||||
fileRoutesByFullPath: FileRoutesByFullPath
|
||||
fullPaths: '/' | '/code-graph' | '/demo' | '/login'
|
||||
fullPaths: '/' | '/chat-dev' | '/code-graph' | '/demo' | '/login'
|
||||
fileRoutesByTo: FileRoutesByTo
|
||||
to: '/' | '/code-graph' | '/demo' | '/login'
|
||||
id: '__root__' | '/' | '/code-graph' | '/demo' | '/login'
|
||||
to: '/' | '/chat-dev' | '/code-graph' | '/demo' | '/login'
|
||||
id: '__root__' | '/' | '/chat-dev' | '/code-graph' | '/demo' | '/login'
|
||||
fileRoutesById: FileRoutesById
|
||||
}
|
||||
export interface RootRouteChildren {
|
||||
IndexRoute: typeof IndexRoute
|
||||
ChatDevRoute: typeof ChatDevRoute
|
||||
CodeGraphRoute: typeof CodeGraphRoute
|
||||
DemoRoute: typeof DemoRoute
|
||||
LoginRoute: typeof LoginRoute
|
||||
@@ -92,6 +102,13 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof CodeGraphRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/chat-dev': {
|
||||
id: '/chat-dev'
|
||||
path: '/chat-dev'
|
||||
fullPath: '/chat-dev'
|
||||
preLoaderRoute: typeof ChatDevRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/': {
|
||||
id: '/'
|
||||
path: '/'
|
||||
@@ -104,6 +121,7 @@ declare module '@tanstack/react-router' {
|
||||
|
||||
const rootRouteChildren: RootRouteChildren = {
|
||||
IndexRoute: IndexRoute,
|
||||
ChatDevRoute: ChatDevRoute,
|
||||
CodeGraphRoute: CodeGraphRoute,
|
||||
DemoRoute: DemoRoute,
|
||||
LoginRoute: LoginRoute,
|
||||
|
||||
9
src/routes/chat-dev.tsx
Normal file
9
src/routes/chat-dev.tsx
Normal 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,
|
||||
})
|
||||
Reference in New Issue
Block a user