feat: 添加节点信息编辑功能,支持通过 URL 参数传递 filepath 和 projectPath;调整 ProjectPanel 位置

This commit is contained in:
xiongxiao
2026-03-18 00:03:50 +08:00
committed by cnb
parent b288bd179e
commit 56222183d1
3 changed files with 128 additions and 21 deletions

View File

@@ -1,6 +1,6 @@
import { useEffect } from 'react'; import { useEffect, useState } from 'react';
import { useShallow } from 'zustand/react/shallow'; import { useShallow } from 'zustand/react/shallow';
import { FileIcon, FolderIcon, RefreshCwIcon, TrashIcon } from 'lucide-react'; import { FileIcon, FolderIcon, RefreshCwIcon, TrashIcon, PencilIcon, CheckIcon, XIcon } from 'lucide-react';
import { useChatDevStore } from '../store'; import { useChatDevStore } from '../store';
import { useCodeGraphStore } from '@/pages/code-graph/store'; import { useCodeGraphStore } from '@/pages/code-graph/store';
@@ -29,6 +29,36 @@ export const OpencodeChat = () => {
url: s.url, url: s.url,
}))); })));
// 编辑节点信息的状态
const [isEditing, setIsEditing] = useState(false);
const [editFilepath, setEditFilepath] = useState('');
const [editProjectPath, setEditProjectPath] = useState('');
const startEditing = () => {
setEditFilepath(projectInfo?.filepath || '');
setEditProjectPath(projectInfo?.projectPath || '');
setIsEditing(true);
};
const cancelEditing = () => {
setIsEditing(false);
setEditFilepath('');
setEditProjectPath('');
};
const saveEditing = () => {
setData({
projectInfo: {
filepath: editFilepath,
projectPath: editProjectPath,
kind: projectInfo?.kind || 'file',
},
});
setIsEditing(false);
setEditFilepath('');
setEditProjectPath('');
};
// 初始化后尝试从 sessionStorage 恢复 opencode session 并加载历史消息 // 初始化后尝试从 sessionStorage 恢复 opencode session 并加载历史消息
useEffect(() => { useEffect(() => {
if (!codeGraphStore.url) return; if (!codeGraphStore.url) return;
@@ -88,21 +118,69 @@ export const OpencodeChat = () => {
{/* 内容区 */} {/* 内容区 */}
<div className='px-4 py-4 flex flex-col gap-4'> <div className='px-4 py-4 flex flex-col gap-4'>
{/* 节点信息 */} {/* 节点信息 */}
{relativePath && ( {isEditing ? (
<div className='flex flex-col gap-2 px-3 py-2 rounded-lg bg-slate-800/60 border border-white/5'>
<div className='flex items-center gap-2'>
<FolderIcon className='size-4 shrink-0 text-slate-400' />
<input
type='text'
className='flex-1 min-w-0 rounded border border-white/10 bg-slate-700 px-2 py-1 text-xs text-slate-200 font-mono focus:outline-none focus:ring-1 focus:ring-emerald-500'
placeholder='projectPath'
value={editProjectPath}
onChange={(e) => setEditProjectPath(e.target.value)}
/>
</div>
<div className='flex items-center gap-2'>
<FileIcon className='size-4 shrink-0 text-slate-400' />
<input
type='text'
className='flex-1 min-w-0 rounded border border-white/10 bg-slate-700 px-2 py-1 text-xs text-slate-200 font-mono focus:outline-none focus:ring-1 focus:ring-emerald-500'
placeholder='filepath'
value={editFilepath}
onChange={(e) => setEditFilepath(e.target.value)}
/>
</div>
<div className='flex justify-end gap-1 mt-1'>
<button
onClick={cancelEditing}
className='p-1 rounded hover:text-red-400 transition-colors'
title='取消'>
<XIcon className='size-4' />
</button>
<button
onClick={saveEditing}
className='p-1 rounded hover:text-emerald-400 transition-colors'
title='保存'>
<CheckIcon className='size-4' />
</button>
</div>
</div>
) : (
(relativePath || projectInfo?.projectPath) && (
<div className='flex items-center gap-2 px-3 py-2 rounded-lg bg-slate-800/60 border border-white/5 min-w-0'> <div className='flex items-center gap-2 px-3 py-2 rounded-lg bg-slate-800/60 border border-white/5 min-w-0'>
{relativePath ? (
<>
<FileIcon className='size-4 shrink-0 text-slate-400' /> <FileIcon className='size-4 shrink-0 text-slate-400' />
<span className='text-xs text-slate-300 font-mono truncate' title={relativePath}> <span className='text-xs text-slate-300 font-mono truncate' title={relativePath}>
{relativePath} {relativePath}
</span> </span>
</div> </>
)} ) : (
{projectInfo?.projectPath && !relativePath && ( <>
<div className='flex items-center gap-2 px-3 py-2 rounded-lg bg-slate-800/60 border border-white/5 min-w-0'>
<FolderIcon className='size-4 shrink-0 text-slate-400' /> <FolderIcon className='size-4 shrink-0 text-slate-400' />
<span className='text-xs text-slate-300 font-mono truncate'> <span className='text-xs text-slate-300 font-mono truncate'>
{projectInfo.projectPath} {projectInfo?.projectPath}
</span> </span>
</>
)}
<button
onClick={startEditing}
className='p-1 rounded hover:text-emerald-400 transition-colors ml-auto'
title='编辑节点信息'>
<PencilIcon className='size-3' />
</button>
</div> </div>
)
)} )}
{/* 问题输入区 */} {/* 问题输入区 */}

View File

@@ -113,7 +113,6 @@ export const useChatDevStore = create<ChatDevState>()((set, get) => ({
initFromTimestamp: (timestamp: string) => { initFromTimestamp: (timestamp: string) => {
const key = SESSION_KEY_PREFIX + timestamp; const key = SESSION_KEY_PREFIX + timestamp;
// 优先从 localStorage 读取(首次打开) // 优先从 localStorage 读取(首次打开)
const localRaw = localStorage.getItem(key); const localRaw = localStorage.getItem(key);
if (localRaw) { if (localRaw) {
@@ -123,7 +122,22 @@ export const useChatDevStore = create<ChatDevState>()((set, get) => ({
sessionStorage.setItem(SESSION_KEY, localRaw); sessionStorage.setItem(SESSION_KEY, localRaw);
// 清除 localStorage // 清除 localStorage
localStorage.removeItem(key); localStorage.removeItem(key);
// filepath和projectPath可以通过URL参数传递优先级高于存储的内容
const urlParams = new URLSearchParams(window.location.search);
const urlFilepath = urlParams.get('filepath');
const urlProjectPath = urlParams.get('projectPath');
if (urlFilepath || urlProjectPath) {
const projectInfo = {
...data.projectInfo,
filepath: urlFilepath ? decodeURI(urlFilepath) : data.projectInfo?.filepath ?? '',
projectPath: urlProjectPath ? decodeURI(urlProjectPath) : data.projectInfo?.projectPath ?? '',
};
set({ question: data.question, engine: data.engine, projectInfo });
} else {
set({ question: data.question, engine: data.engine, projectInfo: data.projectInfo }); set({ question: data.question, engine: data.engine, projectInfo: data.projectInfo });
}
return; return;
} catch { } catch {
localStorage.removeItem(key); localStorage.removeItem(key);
@@ -135,7 +149,22 @@ export const useChatDevStore = create<ChatDevState>()((set, get) => ({
if (sessionRaw) { if (sessionRaw) {
try { try {
const data: ChatDevData = JSON.parse(sessionRaw); const data: ChatDevData = JSON.parse(sessionRaw);
// filepath和projectPath可以通过URL参数传递优先级高于存储的内容
const urlParams = new URLSearchParams(window.location.search);
const urlFilepath = urlParams.get('filepath');
const urlProjectPath = urlParams.get('projectPath');
if (urlFilepath || urlProjectPath) {
const projectInfo = {
...data.projectInfo,
filepath: urlFilepath ? decodeURI(urlFilepath) : data.projectInfo?.filepath ?? '',
projectPath: urlProjectPath ? decodeURI(urlProjectPath) : data.projectInfo?.projectPath ?? '',
};
set({ question: data.question, engine: data.engine, projectInfo });
} else {
set({ question: data.question, engine: data.engine, projectInfo: data.projectInfo }); set({ question: data.question, engine: data.engine, projectInfo: data.projectInfo });
}
return; return;
} catch { } catch {
sessionStorage.removeItem(SESSION_KEY); sessionStorage.removeItem(SESSION_KEY);

View File

@@ -18,7 +18,7 @@ export function ProjectPanel({
onStopProject, onStopProject,
}: ProjectPanelProps) { }: ProjectPanelProps) {
const [isDragging, setIsDragging] = useState(false); const [isDragging, setIsDragging] = useState(false);
const [position, setPosition] = useState({ x: 20, y: 80 }); const [position, setPosition] = useState({ x: 20, y: 100 });
const dragOffset = useRef({ x: 0, y: 0 }); const dragOffset = useRef({ x: 0, y: 0 });
const panelRef = useRef<HTMLDivElement>(null); const panelRef = useRef<HTMLDivElement>(null);
@@ -132,7 +132,7 @@ export function ProjectPanel({
</div> </div>
{/* 项目列表 */} {/* 项目列表 */}
<div className='flex flex-col py-1 max-h-80 overflow-y-auto'> <div className='flex flex-col py-1 max-h-80 overflow-y-auto scrollbar'>
{activeProjects.map((project) => { {activeProjects.map((project) => {
const projectName = project.name || project.path.split('/').pop() || project.path; const projectName = project.name || project.path.split('/').pop() || project.path;
const nodeInfoData = { const nodeInfoData = {