From 3e5f13f516b605813df5207a00a740374ac36099 Mon Sep 17 00:00:00 2001 From: Agent Zero Date: Tue, 25 Aug 2026 19:35:05 +0200 Subject: [PATCH] =?UTF-8?q?fix(i-d):=20ai/sessions-API-Bruche=20behoben=20?= =?UTF-8?q?=E2=80=94=20tote=20Frontend-Calls=20eliminiert=20statt=20Backen?= =?UTF-8?q?d-Shims?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root-Cause: Backend hat KEIN /ai/sessions-CRUD (nur Conversations-Routen im kommunikation/ai_assistant). Frontend-Nutzer war NUR AISidebar — dessen Chat-Tab renderte nie einen echten Chat sondern nur Platzhalter gesteuert von Session-Calls auf 404. Nach AGENTS.md 0.2/0.3 keine Backend-Shims gebaut: (1) Geister-Tests ChatWindow.test.tsx + SessionList.test.tsx geloescht — importierten nicht existierende Komponenten @/components/ai/ChatWindow + SessionList (BUG-099-Muster, Plan sanktioniert Loeschung). (2) AISidebar: tote fetchSessions/createSession-Calls + sessionId/loading-State entfernt; Chat-Tab zeigt jetzt Verweis-Link auf existierende /ai-assistant-Seite (962e0ee). (3) api/ai.ts 253→170 Zeilen: tote Interfaces ChatFolder/ChatSession/ChatMessage/ChatAttachment + Folders/Sessions/Attachments-Sektionen entfernt; fetchMessages/streamChat bleiben (genutzt von AiChatPanel/Communication). Beweise: tsc --noEmit exit=0; vitest src/__tests__/ai/ 26/26 gruen (vorher 2 Geister-Suites mit Import-Error); ruff unberuehrt. --- frontend/src/__tests__/ai/ChatWindow.test.tsx | 113 ------------------ .../src/__tests__/ai/SessionList.test.tsx | 104 ---------------- frontend/src/api/ai.ts | 83 ------------- frontend/src/components/layout/AISidebar.tsx | 46 ++----- 4 files changed, 13 insertions(+), 333 deletions(-) delete mode 100644 frontend/src/__tests__/ai/ChatWindow.test.tsx delete mode 100644 frontend/src/__tests__/ai/SessionList.test.tsx diff --git a/frontend/src/__tests__/ai/ChatWindow.test.tsx b/frontend/src/__tests__/ai/ChatWindow.test.tsx deleted file mode 100644 index 37b274b..0000000 --- a/frontend/src/__tests__/ai/ChatWindow.test.tsx +++ /dev/null @@ -1,113 +0,0 @@ -import React from 'react'; -import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { render, screen, fireEvent, waitFor } from '@testing-library/react'; -import { ChatWindow } from '@/components/ai/ChatWindow'; - -const mockMessages = [ - { id: 'm1', session_id: 's1', role: 'user', content: 'Hello AI', tokens: 10, model_used: '' }, - { id: 'm2', session_id: 's1', role: 'assistant', content: 'Hello User', tokens: 10, model_used: 'gpt-4' }, -]; - -const fetchMessagesMock = vi.fn(); -const fetchAttachmentsMock = vi.fn(); -const streamChatMock = vi.fn(); -const uploadAttachmentMock = vi.fn(); - -vi.mock('@/api/ai', () => ({ - fetchMessages: (...args: any[]) => fetchMessagesMock(...args), - fetchAttachments: (...args: any[]) => fetchAttachmentsMock(...args), - streamChat: (...args: any[]) => streamChatMock(...args), - uploadAttachment: (...args: any[]) => uploadAttachmentMock(...args), - getAttachmentDownloadUrl: (id: string) => `/api/v1/ai/attachments/${id}/download`, -})); - -vi.mock('react-markdown', () => ({ - default: ({ children }: { children: string }) =>
{children}
, -})); - -vi.mock('remark-gfm', () => ({ - default: () => ({}), -})); - -beforeEach(() => { - vi.clearAllMocks(); - fetchMessagesMock.mockResolvedValue(mockMessages); - fetchAttachmentsMock.mockResolvedValue([]); - streamChatMock.mockReturnValue({ - async *[Symbol.asyncIterator]() { - yield { type: 'token', content: 'Hi' }; - yield { type: 'done' }; - }, - }); -}); - -describe('ChatWindow', () => { - it('renders chat window with messages', async () => { - render(); - await waitFor(() => { - expect(screen.getByText('Hello AI')).toBeInTheDocument(); - expect(screen.getByText('Hello User')).toBeInTheDocument(); - }); - }); - - it('renders textarea input for message', async () => { - render(); - await waitFor(() => { - expect(screen.getByPlaceholderText('Nachricht eingeben...')).toBeInTheDocument(); - }); - }); - - it('renders send button', async () => { - render(); - await waitFor(() => { - expect(screen.getByText('Senden')).toBeInTheDocument(); - }); - }); - - it('disables send button when input is empty', async () => { - render(); - await waitFor(() => { - const sendBtn = screen.getByText('Senden'); - expect(sendBtn).toBeDisabled(); - }); - }); - - it('enables send button when input has text', async () => { - render(); - await waitFor(() => { - expect(screen.getByPlaceholderText('Nachricht eingeben...')).toBeInTheDocument(); - }); - const input = screen.getByPlaceholderText('Nachricht eingeben...'); - fireEvent.change(input, { target: { value: 'Test message' } }); - expect(screen.getByText('Senden')).not.toBeDisabled(); - }); - - it('shows empty state when no messages', async () => { - fetchMessagesMock.mockResolvedValueOnce([]); - render(); - await waitFor(() => { - expect(screen.getByText('Starte eine Konversation...')).toBeInTheDocument(); - }); - }); - - it('displays error when fetchMessages fails', async () => { - fetchMessagesMock.mockRejectedValueOnce(new Error('Load failed')); - render(); - await waitFor(() => { - expect(screen.getByText('Load failed')).toBeInTheDocument(); - }); - }); - - it('sends message on button click', async () => { - render(); - await waitFor(() => { - expect(screen.getByPlaceholderText('Nachricht eingeben...')).toBeInTheDocument(); - }); - const input = screen.getByPlaceholderText('Nachricht eingeben...'); - fireEvent.change(input, { target: { value: 'Test message' } }); - fireEvent.click(screen.getByText('Senden')); - await waitFor(() => { - expect(streamChatMock).toHaveBeenCalledWith('s1', 'Test message', undefined); - }); - }); -}); diff --git a/frontend/src/__tests__/ai/SessionList.test.tsx b/frontend/src/__tests__/ai/SessionList.test.tsx deleted file mode 100644 index 523cec1..0000000 --- a/frontend/src/__tests__/ai/SessionList.test.tsx +++ /dev/null @@ -1,104 +0,0 @@ -import React from 'react'; -import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { render, screen, fireEvent, waitFor } from '@testing-library/react'; -import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import { SessionList } from '@/components/ai/SessionList'; - -const queryClient = new QueryClient({ - defaultOptions: { queries: { retry: false } }, -}); - -function renderWithProvider(ui: React.ReactElement) { - return render( - {ui} - ); -} - -const mockSessions = [ - { id: 's1', title: 'First Chat', folder_id: null, agent_id: null, is_sidebar: false, created_at: '2024-01-01T00:00:00Z', updated_at: '2024-01-01T00:00:00Z' }, - { id: 's2', title: 'Second Chat', folder_id: 'f1', agent_id: null, is_sidebar: false, created_at: '2024-01-01T00:00:00Z', updated_at: '2024-01-01T00:00:00Z' }, -]; - -const mockFolders = [ - { id: 'f1', name: 'My Folder', parent_id: null, created_at: '2024-01-01T00:00:00Z' }, -]; - -const fetchSessionsMock = vi.fn(); -const fetchFoldersMock = vi.fn(); -const createSessionMock = vi.fn(); -const deleteSessionMock = vi.fn(); -const updateSessionMock = vi.fn(); -const createFolderMock = vi.fn(); -const deleteFolderMock = vi.fn(); -const updateFolderMock = vi.fn(); - -vi.mock('@/api/ai', () => ({ - fetchSessions: (...args: any[]) => fetchSessionsMock(...args), - fetchFolders: (...args: any[]) => fetchFoldersMock(...args), - createSession: (...args: any[]) => createSessionMock(...args), - deleteSession: (...args: any[]) => deleteSessionMock(...args), - updateSession: (...args: any[]) => updateSessionMock(...args), - createFolder: (...args: any[]) => createFolderMock(...args), - deleteFolder: (...args: any[]) => deleteFolderMock(...args), - updateFolder: (...args: any[]) => updateFolderMock(...args), -})); - -beforeEach(() => { - vi.clearAllMocks(); - fetchSessionsMock.mockResolvedValue(mockSessions); - fetchFoldersMock.mockResolvedValue(mockFolders); - createSessionMock.mockResolvedValue({ id: 's3', title: 'New Chat', folder_id: null, agent_id: null, is_sidebar: false, created_at: '', updated_at: '' }); - deleteSessionMock.mockResolvedValue({}); - updateSessionMock.mockResolvedValue({}); - createFolderMock.mockResolvedValue({ id: 'f2', name: 'New Folder', parent_id: null, created_at: '' }); - deleteFolderMock.mockResolvedValue({}); - updateFolderMock.mockResolvedValue({}); -}); - -describe('SessionList', () => { - it('renders session list with sessions', async () => { - renderWithProvider(); - await waitFor(() => { - expect(screen.getByText('First Chat')).toBeInTheDocument(); - }); - }); - - it('renders folder names', async () => { - renderWithProvider(); - await waitFor(() => { - expect(screen.getByText('My Folder')).toBeInTheDocument(); - }); - }); - - it('renders sessions inside folders', async () => { - renderWithProvider(); - await waitFor(() => { - expect(screen.getByText('Second Chat')).toBeInTheDocument(); - }); - }); - - it('calls onSelectSession when session clicked', async () => { - const onSelect = vi.fn(); - renderWithProvider(); - await waitFor(() => { - expect(screen.getByText('First Chat')).toBeInTheDocument(); - }); - fireEvent.click(screen.getByText('First Chat')); - expect(onSelect).toHaveBeenCalledWith('s1'); - }); - - it('shows loading state initially', () => { - fetchSessionsMock.mockReturnValue(new Promise(() => {})); - fetchFoldersMock.mockReturnValue(new Promise(() => {})); - renderWithProvider(); - expect(screen.getByText('Laden...')).toBeInTheDocument(); - }); - - it('displays error when fetch fails', async () => { - fetchSessionsMock.mockRejectedValueOnce(new Error('Failed to load')); - renderWithProvider(); - await waitFor(() => { - expect(screen.getByText('Failed to load')).toBeInTheDocument(); - }); - }); -}); diff --git a/frontend/src/api/ai.ts b/frontend/src/api/ai.ts index b62571a..8e8f03e 100644 --- a/frontend/src/api/ai.ts +++ b/frontend/src/api/ai.ts @@ -61,47 +61,6 @@ export interface AIAgent { updated_at?: string; } -export interface ChatFolder { - id: string; - name: string; - parent_id: string | null; - user_id: string; - created_at?: string; -} - -export interface ChatSession { - id: string; - user_id: string; - agent_id: string | null; - title: string; - is_pinned: boolean; - is_sidebar: boolean; - folder_id: string | null; - created_at?: string; - updated_at?: string; -} - -export interface ChatMessage { - id: string; - session_id: string; - role: string; - content: string; - tool_calls?: Record[]; - tool_results?: Record[]; - tokens: number; - model_used: string; - created_at?: string; -} - -export interface ChatAttachment { - id: string; - message_id: string | null; - session_id: string; - filename: string; - mime_type: string; - size_bytes: number; -} - export interface AITool { name: string; description: string; @@ -147,53 +106,11 @@ export const fetchTools = async () => { return Array.isArray(res) ? res : res.items ?? []; }; -// ─── Folders ─── - -export const fetchFolders = () => apiGet('/ai/folders'); -export const createFolder = (data: { name: string; parent_id?: string }) => apiPost('/ai/folders', data); -export const updateFolder = (id: string, data: Partial) => apiPut(`/ai/folders/${id}`, data); -export const deleteFolder = (id: string) => apiDelete(`/ai/folders/${id}`); - -// ─── Sessions ─── - -export const fetchSessions = (isSidebar?: boolean) => - apiGet('/ai/sessions', { params: isSidebar !== undefined ? { is_sidebar: isSidebar } : {} }); -export const createSession = (data: { title?: string; agent_id?: string; is_sidebar?: boolean; folder_id?: string }) => - apiPost('/ai/sessions', data); -export const updateSession = (id: string, data: Partial) => - apiPut(`/ai/sessions/${id}`, data); -export const deleteSession = (id: string) => apiDelete(`/ai/sessions/${id}`); - // ─── Messages ─── export const fetchMessages = (conversationId: string) => apiGet<{ role: string; content: string }[]>(`/ai/conversations/${conversationId}/messages`); -// ─── Attachments ─── - -export const fetchAttachments = (sessionId: string) => - apiGet(`/ai/sessions/${sessionId}/attachments`); - -export async function uploadAttachment(sessionId: string, file: File): Promise { - const formData = new FormData(); - formData.append('file', file); - const csrfToken = sessionStorage.getItem('leocrm_csrf_token'); - const response = await fetch(`/api/v1/ai/sessions/${sessionId}/attachments`, { - method: 'POST', - headers: { - ...(csrfToken ? { 'X-CSRF-Token': csrfToken } : {}), - }, - credentials: 'include', - body: formData, - }); - if (!response.ok) throw new Error(`Upload failed: ${response.status}`); - return response.json(); -} - -export function getAttachmentDownloadUrl(attachmentId: string): string { - return `/api/v1/ai/attachments/${attachmentId}/download`; -} - // ─── Streaming Chat ─── export interface StreamEvent { diff --git a/frontend/src/components/layout/AISidebar.tsx b/frontend/src/components/layout/AISidebar.tsx index a3ef5c8..821e7a6 100644 --- a/frontend/src/components/layout/AISidebar.tsx +++ b/frontend/src/components/layout/AISidebar.tsx @@ -1,9 +1,9 @@ -import React, { useState, useEffect } from 'react'; +import React from 'react'; +import { Link } from 'react-router-dom'; import { ResizablePanel } from '@/components/ui/ResizablePanel'; import { SuggestionList } from '@/components/ai/SuggestionSidebar'; import { ImprovementPanel } from '@/components/ai/ImprovementPanel'; -import { createSession, fetchSessions } from '@/api/ai'; import { useUIStore } from '@/store/uiStore'; import { getContributedTabs } from './sidebarTabs'; import { useTranslation } from 'react-i18next'; @@ -56,26 +56,6 @@ function ChatPanel() { export function AISidebar() { const { t } = useTranslation(); const { aiSidebarCollapsed, toggleAISidebar, aiSidebarTab, setAISidebarTab, notifications, removeNotification } = useUIStore(); - const [sessionId, setSessionId] = useState(null); - const [loading, setLoading] = useState(true); - - useEffect(() => { - (async () => { - try { - const sessions = await fetchSessions(true); - if (sessions.length > 0) { - setSessionId(sessions[0].id); - } else { - const session = await createSession({ is_sidebar: true, title: 'Sidebar Chat' }); - setSessionId(session.id); - } - } catch (e) { - console.error('AISidebar init error:', e); - } finally { - setLoading(false); - } - })(); - }, []); const tabs: TabDef[] = [ { key: 'chat', label: 'KI Chat', icon: robotIcon, testId: 'ai-sidebar-tab-chat' }, @@ -166,18 +146,18 @@ export function AISidebar() { const ContributedComponent = contributed.component; return ; } - // chat tab - if (loading) { - return ( -
Laden...
- ); - } - if (sessionId) { - return
KI Chat im Kommunikations-Plugin verfügbar
; - } + // chat tab — voller KI-Chat lebt auf der /ai-assistant-Seite return ( -
- Session konnte nicht erstellt werden +
+

+ Der volle KI-Chat ist auf der AI-Assistant-Seite verfügbar. +

+ + AI Assistant öffnen +
); };