feat: hover rename/delete icons in treeview, resizable tree and sidebar panels
This commit is contained in:
@@ -52,6 +52,7 @@ function TreeItem({
|
|||||||
onContextMenu: (e: React.MouseEvent, type: 'session' | 'folder' | 'root', id: string | null) => void;
|
onContextMenu: (e: React.MouseEvent, type: 'session' | 'folder' | 'root', id: string | null) => void;
|
||||||
onDragSession: (sessionId: string) => void;
|
onDragSession: (sessionId: string) => void;
|
||||||
onDropToFolder: (folderId: string | null) => void;
|
onDropToFolder: (folderId: string | null) => void;
|
||||||
|
onDelete: (id: string, type: 'session' | 'folder') => void;
|
||||||
}) {
|
}) {
|
||||||
const [expanded, setExpanded] = useState(true);
|
const [expanded, setExpanded] = useState(true);
|
||||||
const [isDragOver, setIsDragOver] = useState(false);
|
const [isDragOver, setIsDragOver] = useState(false);
|
||||||
@@ -94,6 +95,12 @@ function TreeItem({
|
|||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" />
|
||||||
</svg>
|
</svg>
|
||||||
<span className="flex-1 truncate">{node.folder!.name}</span>
|
<span className="flex-1 truncate">{node.folder!.name}</span>
|
||||||
|
<button onClick={(e) => { e.stopPropagation(); onContextMenu(e.nativeEvent, 'folder', node.folder!.id); }} className="opacity-0 group-hover:opacity-100 text-secondary-400 hover:text-primary-600 p-0.5" title="Umbenennen">
|
||||||
|
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" /></svg>
|
||||||
|
</button>
|
||||||
|
<button onClick={(e) => { e.stopPropagation(); onDelete(node.folder!.id, 'folder'); }} className="opacity-0 group-hover:opacity-100 text-secondary-400 hover:text-red-600 p-0.5" title="Löschen">
|
||||||
|
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" /></svg>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -124,6 +131,12 @@ function TreeItem({
|
|||||||
<span className={clsx('flex-1 truncate', activeSessionId === session.id && 'font-bold text-primary-700')}>
|
<span className={clsx('flex-1 truncate', activeSessionId === session.id && 'font-bold text-primary-700')}>
|
||||||
{session.title}
|
{session.title}
|
||||||
</span>
|
</span>
|
||||||
|
<button onClick={(e) => { e.stopPropagation(); onContextMenu(e.nativeEvent, 'session', session.id); }} className="opacity-0 group-hover:opacity-100 text-secondary-400 hover:text-primary-600 p-0.5" title="Umbenennen">
|
||||||
|
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" /></svg>
|
||||||
|
</button>
|
||||||
|
<button onClick={(e) => { e.stopPropagation(); onDelete(session.id, 'session'); }} className="opacity-0 group-hover:opacity-100 text-secondary-400 hover:text-red-600 p-0.5" title="Löschen">
|
||||||
|
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" /></svg>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -154,6 +167,7 @@ function TreeItem({
|
|||||||
onContextMenu={onContextMenu}
|
onContextMenu={onContextMenu}
|
||||||
onDragSession={onDragSession}
|
onDragSession={onDragSession}
|
||||||
onDropToFolder={onDropToFolder}
|
onDropToFolder={onDropToFolder}
|
||||||
|
onDelete={onDelete}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -313,6 +327,7 @@ export function SessionList({ activeSessionId, onSelectSession, isSidebar, class
|
|||||||
onContextMenu={handleContextMenu}
|
onContextMenu={handleContextMenu}
|
||||||
onDragSession={setDragSessionId}
|
onDragSession={setDragSessionId}
|
||||||
onDropToFolder={handleDropToFolder}
|
onDropToFolder={handleDropToFolder}
|
||||||
|
onDelete={handleDelete}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { ChatWindow } from '@/components/ai/ChatWindow';
|
import { ChatWindow } from '@/components/ai/ChatWindow';
|
||||||
|
import { ResizablePanel } from '@/components/ui/ResizablePanel';
|
||||||
import { createSession, fetchSessions } from '@/api/ai';
|
import { createSession, fetchSessions } from '@/api/ai';
|
||||||
import { useUIStore } from '@/store/uiStore';
|
import { useUIStore } from '@/store/uiStore';
|
||||||
|
|
||||||
@@ -9,7 +10,6 @@ export function AISidebar() {
|
|||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Find or create a sidebar session
|
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
const sessions = await fetchSessions(true);
|
const sessions = await fetchSessions(true);
|
||||||
@@ -28,28 +28,36 @@ export function AISidebar() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-80 flex-shrink-0 border-l border-secondary-200 bg-white flex flex-col h-full">
|
<ResizablePanel
|
||||||
<div className="h-12 flex items-center justify-between px-3 border-b border-secondary-200">
|
initialWidth={320}
|
||||||
<span className="text-sm font-medium text-secondary-700">KI Assistent</span>
|
minWidth={240}
|
||||||
<button
|
maxWidth={600}
|
||||||
onClick={() => setAISidebarOpen(false)}
|
className="border-l border-secondary-200 bg-white"
|
||||||
className="text-secondary-400 hover:text-secondary-600"
|
data-testid="ai-sidebar-pane"
|
||||||
title="Schließen"
|
>
|
||||||
>
|
<div className="h-full flex flex-col">
|
||||||
✕
|
<div className="h-12 flex items-center justify-between px-3 border-b border-secondary-200">
|
||||||
</button>
|
<span className="text-sm font-medium text-secondary-700">KI Assistent</span>
|
||||||
|
<button
|
||||||
|
onClick={() => setAISidebarOpen(false)}
|
||||||
|
className="text-secondary-400 hover:text-secondary-600"
|
||||||
|
title="Schließen"
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 overflow-hidden">
|
||||||
|
{loading ? (
|
||||||
|
<div className="flex items-center justify-center h-full text-sm text-secondary-400">Laden...</div>
|
||||||
|
) : sessionId ? (
|
||||||
|
<ChatWindow sessionId={sessionId} compact className="h-full" />
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center justify-center h-full text-sm text-red-500">
|
||||||
|
Session konnte nicht erstellt werden
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 overflow-hidden">
|
</ResizablePanel>
|
||||||
{loading ? (
|
|
||||||
<div className="flex items-center justify-center h-full text-sm text-secondary-400">Laden...</div>
|
|
||||||
) : sessionId ? (
|
|
||||||
<ChatWindow sessionId={sessionId} compact className="h-full" />
|
|
||||||
) : (
|
|
||||||
<div className="flex items-center justify-center h-full text-sm text-red-500">
|
|
||||||
Session konnte nicht erstellt werden
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { SessionList } from '@/components/ai/SessionList';
|
import { SessionList } from '@/components/ai/SessionList';
|
||||||
import { ChatWindow } from '@/components/ai/ChatWindow';
|
import { ChatWindow } from '@/components/ai/ChatWindow';
|
||||||
|
import { ResizablePanel } from '@/components/ui/ResizablePanel';
|
||||||
import { usePluginToolbarStore } from '@/store/pluginToolbarStore';
|
import { usePluginToolbarStore } from '@/store/pluginToolbarStore';
|
||||||
import { fetchAgents, type AIAgent } from '@/api/ai';
|
import { fetchAgents, type AIAgent } from '@/api/ai';
|
||||||
|
|
||||||
@@ -69,8 +70,16 @@ export function AIAssistantPage() {
|
|||||||
return (
|
return (
|
||||||
<div className="flex h-full" data-testid="ai-assistant-page">
|
<div className="flex h-full" data-testid="ai-assistant-page">
|
||||||
{/* Desktop: two-pane layout */}
|
{/* Desktop: two-pane layout */}
|
||||||
<div className="hidden md:flex w-72 flex-shrink-0 border-r border-secondary-200 bg-white flex-col">
|
<div className="hidden md:flex">
|
||||||
<SessionList key={refreshKey} activeSessionId={activeSessionId} onSelectSession={setActiveSessionId} className="flex-1" />
|
<ResizablePanel
|
||||||
|
initialWidth={288}
|
||||||
|
minWidth={200}
|
||||||
|
maxWidth={500}
|
||||||
|
className="border-r border-secondary-200 bg-white"
|
||||||
|
data-testid="ai-session-pane"
|
||||||
|
>
|
||||||
|
<SessionList key={refreshKey} activeSessionId={activeSessionId} onSelectSession={setActiveSessionId} className="h-full" />
|
||||||
|
</ResizablePanel>
|
||||||
</div>
|
</div>
|
||||||
<div className="hidden md:flex flex-1 flex-col">
|
<div className="hidden md:flex flex-1 flex-col">
|
||||||
{activeSessionId ? <ChatWindow sessionId={activeSessionId} agentId={selectedAgentId} className="flex-1" /> : (
|
{activeSessionId ? <ChatWindow sessionId={activeSessionId} agentId={selectedAgentId} className="flex-1" /> : (
|
||||||
|
|||||||
Reference in New Issue
Block a user