diff --git a/src/api/chatbotV2Api.ts b/src/api/chatbotV2Api.ts deleted file mode 100644 index 2ed4c1b..0000000 --- a/src/api/chatbotV2Api.ts +++ /dev/null @@ -1,240 +0,0 @@ -/** - * Chatbot V2 API - * - * Context-aware chat: upload files for extraction first, then chat. - * Endpoints: /api/chatbotv2/{instanceId}/... - */ - -import { ApiRequestOptions } from '../hooks/useApi'; -import api from '../api'; -import { addCSRFTokenToHeaders, getCSRFToken, generateAndStoreCSRFToken } from '../utils/csrfUtils'; -import { Message } from '../components/UiComponents/Messages/MessagesTypes'; - -// ============================================================================ -// TYPES -// ============================================================================ - -export interface ChatbotV2Workflow { - id: string; - mandateId?: string; - featureInstanceId?: string; - status: string; // extracting | ready | running | stopped - name?: string; - currentRound?: number; - lastActivity?: number; - startedAt?: number; - extractedContextId?: string; - contextFiles?: Array<{ fileId: string; fileName: string; mimeType?: string }>; - [key: string]: any; -} - -export interface UploadChatbotV2Request { - listFileId: string[]; -} - -export interface UploadChatbotV2Response { - conversationId: string; - status: string; - message?: string; -} - -export interface StartChatbotV2Request { - prompt: string; - workflowId: string; // conversationId - required for V2 - userLanguage?: string; -} - -export interface ChatDataItem { - type: 'message' | 'log' | 'stat' | 'document' | 'stopped' | 'status'; - createdAt?: number; - item: Message | any; - label?: string; -} - -export type ApiRequestFunction = (options: ApiRequestOptions) => Promise; -export type SSEEventHandler = (item: ChatDataItem) => void; - -// ============================================================================ -// API FUNCTIONS -// ============================================================================ - -/** - * Upload files as context and start extraction. - * Files must be uploaded to central storage first via /api/files/upload. - */ -export async function uploadChatbotV2Api( - request: ApiRequestFunction, - instanceId: string, - listFileId: string[] -): Promise { - const data = await request({ - url: `/api/chatbotv2/${instanceId}/upload`, - method: 'post', - data: { listFileId } - }); - return data as UploadChatbotV2Response; -} - -/** - * Get chatbot V2 threads (conversations) - */ -export async function getChatbotV2ThreadsApi( - request: ApiRequestFunction, - instanceId: string, - pagination?: { page?: number; pageSize?: number } -): Promise<{ items: ChatbotV2Workflow[]; pagination?: any }> { - const params = pagination ? { pagination: JSON.stringify(pagination) } : undefined; - const data = await request({ - url: `/api/chatbotv2/${instanceId}/threads`, - method: 'get', - params - }) as any; - return { - items: Array.isArray(data.items) ? data.items : [], - pagination: data.pagination ?? data.metadata - }; -} - -/** - * Get a specific thread with chat data - */ -export async function getChatbotV2ThreadApi( - request: ApiRequestFunction, - instanceId: string, - workflowId: string -): Promise<{ workflow: ChatbotV2Workflow; chatData: { items: ChatDataItem[] } }> { - const data = await request({ - url: `/api/chatbotv2/${instanceId}/threads`, - method: 'get', - params: { workflowId } - }) as { workflow: ChatbotV2Workflow; chatData: { items: ChatDataItem[] } }; - return { - workflow: data.workflow, - chatData: data.chatData || { items: [] } - }; -} - -/** - * Start or continue chat with SSE streaming. - * Requires conversationId (workflowId) - must have completed extraction first. - */ -export async function startChatbotV2StreamApi( - instanceId: string, - requestBody: StartChatbotV2Request, - onEvent: SSEEventHandler, - onError?: (error: Error) => void, - onComplete?: () => void -): Promise { - try { - const url = `/api/chatbotv2/${instanceId}/start/stream?workflowId=${encodeURIComponent(requestBody.workflowId)}`; - const baseURL = api.defaults.baseURL || ''; - const fullURL = baseURL + url; - - const headers: Record = { 'Content-Type': 'application/json' }; - const authToken = localStorage.getItem('authToken'); - if (authToken) headers['Authorization'] = `Bearer ${authToken}`; - if (!getCSRFToken()) generateAndStoreCSRFToken(); - addCSRFTokenToHeaders(headers); - - const response = await fetch(fullURL, { - method: 'POST', - headers, - body: JSON.stringify({ - prompt: requestBody.prompt, - userLanguage: requestBody.userLanguage || navigator.language || 'de' - }), - credentials: 'include' - }); - - if (!response.ok) { - const errorText = await response.text(); - throw new Error(`HTTP error! status: ${response.status}, message: ${errorText}`); - } - - if (!response.body) throw new Error('Response body is null'); - - const reader = response.body.getReader(); - const decoder = new TextDecoder(); - let buffer = ''; - - try { - while (true) { - const { done, value } = await reader.read(); - if (done) break; - buffer += decoder.decode(value, { stream: true }); - const lines = buffer.split('\n'); - buffer = lines.pop() || ''; - - for (const line of lines) { - if (line.startsWith('data: ')) { - try { - const jsonStr = line.slice(6); - if (jsonStr.trim()) { - const item: ChatDataItem = JSON.parse(jsonStr); - onEvent(item); - } - } catch { - // ignore parse errors - } - } - } - } - - if (buffer.trim()) { - const lines = buffer.split('\n'); - for (const line of lines) { - if (line.startsWith('data: ')) { - try { - const jsonStr = line.slice(6); - if (jsonStr.trim()) { - const item: ChatDataItem = JSON.parse(jsonStr); - onEvent(item); - } - } catch { - // ignore - } - } - } - } - - onComplete?.(); - } finally { - reader.releaseLock(); - } - } catch (error: any) { - if (onError) { - onError(error instanceof Error ? error : new Error(String(error))); - } else { - throw error; - } - } -} - -/** - * Stop a running chat - */ -export async function stopChatbotV2Api( - request: ApiRequestFunction, - instanceId: string, - workflowId: string -): Promise { - const data = await request({ - url: `/api/chatbotv2/${instanceId}/stop/${workflowId}`, - method: 'post' - }); - return data as ChatbotV2Workflow; -} - -/** - * Delete a conversation - */ -export async function deleteChatbotV2Api( - request: ApiRequestFunction, - instanceId: string, - workflowId: string -): Promise { - await request({ - url: `/api/chatbotv2/${instanceId}/conversations/${workflowId}`, - method: 'delete' - }); -} diff --git a/src/config/pageRegistry.tsx b/src/config/pageRegistry.tsx index c52f039..0c168ee 100644 --- a/src/config/pageRegistry.tsx +++ b/src/config/pageRegistry.tsx @@ -108,11 +108,6 @@ export const PAGE_ICONS: Record = { 'feature.automation': , 'page.feature.chatbot.conversations': , 'feature.chatbot': , - 'page.feature.chatbotv2.conversations': , - 'page.feature.chatbotv2.upload': , - 'page.feature.chatbotv2.chat': , - 'page.feature.chatbotv2.threads': , - 'feature.chatbotv2': , 'feature.teamsbot': , }; diff --git a/src/hooks/useChatbotV2.ts b/src/hooks/useChatbotV2.ts deleted file mode 100644 index f3634c3..0000000 --- a/src/hooks/useChatbotV2.ts +++ /dev/null @@ -1,326 +0,0 @@ -/** - * useChatbotV2 Hook - * - * Context-aware chatbot: add context (upload + extract) first, then chat. - * Flow: Upload files -> Extract context -> Chat with context - */ - -import { useState, useEffect, useCallback, useRef } from 'react'; -import { useApiRequest } from './useApi'; -import { useFileOperations } from './useFiles'; -import { - uploadChatbotV2Api, - getChatbotV2ThreadsApi, - getChatbotV2ThreadApi, - startChatbotV2StreamApi, - stopChatbotV2Api, - deleteChatbotV2Api, - type ChatbotV2Workflow, - type ChatDataItem -} from '../api/chatbotV2Api'; -import { Message, getConversationId } from '../components/UiComponents/Messages/MessagesTypes'; -import { useInstanceId } from './useCurrentInstance'; - -export interface UseChatbotV2Return { - // Threads - threads: ChatbotV2Workflow[]; - selectedThreadId: string | null; - loadingThreads: boolean; - error: string | null; - - // Messages - messages: Message[]; - loadingMessages: boolean; - - // Current workflow - currentWorkflowId: string | null; - selectedThread: ChatbotV2Workflow | null; - isStreaming: boolean; - streamingStatus: string | null; - - // Add context (upload + extract) - uploadingContext: boolean; - pendingFiles: Array<{ id: string; name: string }>; - addContextFiles: (files: File[]) => Promise; - addExistingFile: (id: string, fileName: string) => void; - submitContext: () => Promise; // Returns conversationId or null - clearPendingFiles: () => void; - removePendingFile: (id: string) => void; - - // Actions - selectThread: (workflowId: string) => Promise; - createNewThread: () => void; - sendMessage: (input: string) => Promise; - stopStreaming: () => Promise; - deleteThread: (workflowId: string) => Promise; - refreshThreads: () => Promise; - - // Input - inputValue: string; - setInputValue: (value: string) => void; -} - -export function useChatbotV2(): UseChatbotV2Return { - const { request } = useApiRequest(); - const { handleFileUpload } = useFileOperations(); - const instanceId = useInstanceId(); - const isMountedRef = useRef(true); - - const [threads, setThreads] = useState([]); - const [selectedThreadId, setSelectedThreadId] = useState(null); - const [loadingThreads, setLoadingThreads] = useState(false); - const [messages, setMessages] = useState([]); - const [loadingMessages, setLoadingMessages] = useState(false); - const [currentWorkflowId, setCurrentWorkflowId] = useState(null); - const [isStreaming, setIsStreaming] = useState(false); - const [streamingStatus, setStreamingStatus] = useState(null); - const [error, setError] = useState(null); - const [inputValue, setInputValue] = useState(''); - const [uploadingContext, setUploadingContext] = useState(false); - const [pendingFiles, setPendingFiles] = useState>([]); - - useEffect(() => { - isMountedRef.current = true; - return () => { isMountedRef.current = false; }; - }, []); - - const refreshThreads = useCallback(async () => { - if (!instanceId) return; - setLoadingThreads(true); - setError(null); - try { - const result = await getChatbotV2ThreadsApi(request, instanceId); - if (isMountedRef.current) setThreads(result.items || []); - } catch (err: any) { - if (isMountedRef.current) setError(err.message || 'Fehler beim Laden der Konversationen'); - } finally { - if (isMountedRef.current) setLoadingThreads(false); - } - }, [request, instanceId]); - - const loadThreadMessages = useCallback(async (workflowId: string) => { - if (!instanceId) return; - setLoadingMessages(true); - setError(null); - try { - const result = await getChatbotV2ThreadApi(request, instanceId, workflowId); - if (isMountedRef.current) { - const messageItems = (result.chatData?.items || []) - .filter((item: ChatDataItem) => item.type === 'message') - .map((item: ChatDataItem) => item.item as Message); - setMessages(messageItems); - setCurrentWorkflowId(workflowId); - } - } catch (err: any) { - if (isMountedRef.current) { - setError(err.message || 'Fehler beim Laden der Nachrichten'); - setMessages([]); - } - } finally { - if (isMountedRef.current) setLoadingMessages(false); - } - }, [request, instanceId]); - - const selectThread = useCallback(async (workflowId: string) => { - setSelectedThreadId(workflowId); - await loadThreadMessages(workflowId); - }, [loadThreadMessages]); - - const createNewThread = useCallback(() => { - setSelectedThreadId(null); - setMessages([]); - setCurrentWorkflowId(null); - setPendingFiles([]); - setInputValue(''); - }, []); - - const addContextFiles = useCallback(async (files: File[]) => { - if (!files.length) return; - setError(null); - const added: Array<{ id: string; name: string }> = []; - for (const file of Array.from(files)) { - const result = await handleFileUpload(file); - if (result.success && result.fileData) { - const fd = result.fileData.file || result.fileData; - if (fd?.id) { - added.push({ id: fd.id, name: fd.fileName || file.name }); - } - } - } - if (isMountedRef.current && added.length) { - setPendingFiles(prev => [...prev, ...added]); - } - }, [handleFileUpload]); - - const addExistingFile = useCallback((id: string, fileName: string) => { - setError(null); - setPendingFiles(prev => { - if (prev.some(f => f.id === id)) return prev; - return [...prev, { id, name: fileName }]; - }); - }, []); - - const clearPendingFiles = useCallback(() => setPendingFiles([]), []); - const removePendingFile = useCallback((id: string) => { - setPendingFiles(prev => prev.filter(f => f.id !== id)); - }, []); - - const submitContext = useCallback(async (): Promise => { - if (!instanceId || pendingFiles.length === 0) return null; - setUploadingContext(true); - setError(null); - try { - const listFileId = pendingFiles.map(f => f.id); - const result = await uploadChatbotV2Api(request, instanceId, listFileId); - if (isMountedRef.current && result.conversationId) { - setPendingFiles([]); - setSelectedThreadId(result.conversationId); - setCurrentWorkflowId(result.conversationId); - await loadThreadMessages(result.conversationId); - await refreshThreads(); - return result.conversationId; - } - return null; - } catch (err: any) { - if (isMountedRef.current) { - setError(err.message || 'Fehler beim Extrahieren des Kontexts'); - } - return null; - } finally { - if (isMountedRef.current) setUploadingContext(false); - } - }, [instanceId, pendingFiles, request, loadThreadMessages, refreshThreads]); - - const sendMessage = useCallback(async (input: string) => { - if (!input.trim() || isStreaming || !instanceId || !currentWorkflowId) return; - setError(null); - setIsStreaming(true); - setStreamingStatus(null); - - const tempUserMessageId = `temp-user-${Date.now()}`; - const userMessage: Message = { - id: tempUserMessageId, - conversationId: currentWorkflowId, - role: 'user', - message: input.trim(), - publishedAt: Date.now() - }; - setMessages(prev => [...prev, userMessage]); - setInputValue(''); - - try { - await startChatbotV2StreamApi( - instanceId, - { prompt: input, workflowId: currentWorkflowId, userLanguage: navigator.language || 'de' }, - (item: ChatDataItem) => { - if (!isMountedRef.current) return; - if (item.type === 'stopped') { - setIsStreaming(false); - setStreamingStatus(null); - return; - } - if (item.type === 'status') { - const label = item.label || (item.item as any)?.label || ''; - setStreamingStatus(label); - return; - } - if (item.type === 'message' && item.item) { - const message = item.item as Message; - const extractedId = getConversationId(message); - if (extractedId && !currentWorkflowId) setCurrentWorkflowId(extractedId); - - setMessages(prev => { - if (prev.some(m => m.id === message.id)) return prev; - if (message.status === 'first') { - return prev.map(m => (m.id === tempUserMessageId ? message : m)); - } - const isDup = prev.some(m => - m.id === message.id || - (m.role === message.role && m.message === message.message && - m.publishedAt && message.publishedAt && - Math.abs(m.publishedAt - message.publishedAt) < 1000) - ); - if (isDup) return prev; - return [...prev, message]; - }); - } - }, - (err) => { - if (isMountedRef.current) { - setError(err.message || 'Fehler beim Senden'); - setIsStreaming(false); - } - }, - () => { - if (isMountedRef.current) { - setIsStreaming(false); - setStreamingStatus(null); - refreshThreads(); - } - } - ); - } catch (err: any) { - if (isMountedRef.current) { - setError(err.message || 'Fehler beim Senden'); - setIsStreaming(false); - } - } - }, [currentWorkflowId, isStreaming, instanceId, refreshThreads]); - - const stopStreaming = useCallback(async () => { - if (!instanceId || !isStreaming) return; - setIsStreaming(false); - const workflowId = currentWorkflowId || (messages.length ? getConversationId(messages[messages.length - 1]) : null); - if (workflowId) { - stopChatbotV2Api(request, instanceId, workflowId).catch(() => {}); - } - }, [currentWorkflowId, isStreaming, instanceId, request, messages]); - - const deleteThread = useCallback(async (workflowId: string) => { - if (!instanceId) return; - const previousThreads = threads; - setThreads(prev => prev.filter(t => t.id !== workflowId)); - if (selectedThreadId === workflowId) createNewThread(); - try { - await deleteChatbotV2Api(request, instanceId, workflowId); - await refreshThreads(); - } catch (err: any) { - setThreads(previousThreads); - setError(err.message || 'Fehler beim Löschen'); - } - }, [request, instanceId, selectedThreadId, threads, createNewThread, refreshThreads]); - - useEffect(() => { - if (instanceId) refreshThreads(); - }, [instanceId, refreshThreads]); - - const selectedThread = threads.find(t => t.id === selectedThreadId) || null; - - return { - threads, - selectedThreadId, - loadingThreads, - error, - messages, - loadingMessages, - currentWorkflowId, - selectedThread, - isStreaming, - streamingStatus, - uploadingContext, - pendingFiles, - addContextFiles, - addExistingFile, - submitContext, - clearPendingFiles, - removePendingFile, - selectThread, - createNewThread, - sendMessage, - stopStreaming, - deleteThread, - refreshThreads, - inputValue, - setInputValue - }; -} diff --git a/src/pages/FeatureView.tsx b/src/pages/FeatureView.tsx index 29e13e1..5c906b7 100644 --- a/src/pages/FeatureView.tsx +++ b/src/pages/FeatureView.tsx @@ -23,9 +23,6 @@ import { TrusteeAccountingSettingsView } from './views/trustee/TrusteeAccounting // Chatbot Views import { ChatbotConversationsView } from './views/chatbot/ChatbotConversationsView'; -// Chatbot V2 Views -import { ChatbotV2ConversationsView } from './views/chatbotV2/ChatbotV2ConversationsView'; - // RealEstate Views import { RealEstatePekView, RealEstateInstanceRolesPlaceholder } from './views/realestate'; @@ -119,13 +116,6 @@ const VIEW_COMPONENTS: Record> = { conversations: ChatbotConversationsView, settings: ChatbotSettings, }, - chatbotv2: { - dashboard: ChatbotV2ConversationsView, - conversations: ChatbotV2ConversationsView, - upload: ChatbotV2ConversationsView, - chat: ChatbotV2ConversationsView, - threads: ChatbotV2ConversationsView, - }, realestate: { dashboard: RealEstatePekView, 'instance-roles': RealEstateInstanceRolesPlaceholder, diff --git a/src/pages/views/chatbotV2/ChatbotV2ConversationsView.tsx b/src/pages/views/chatbotV2/ChatbotV2ConversationsView.tsx deleted file mode 100644 index 0007a88..0000000 --- a/src/pages/views/chatbotV2/ChatbotV2ConversationsView.tsx +++ /dev/null @@ -1,448 +0,0 @@ -/** - * ChatbotV2ConversationsView - * - * Context-aware chat: first "Add context" (upload files, extract), then chat. - * Chat history on the left like the original chatbot. - */ - -import React, { useState, useRef, useCallback } from 'react'; -import { useChatbotV2 } from '../../../hooks/useChatbotV2'; -import { useApiRequest } from '../../../hooks/useApi'; -import { fetchFiles, type FileInfo } from '../../../api/fileApi'; -import { TextField } from '../../../components/UiComponents/TextField'; -import { Button } from '../../../components/UiComponents/Button'; -import { AutoScroll } from '../../../components/UiComponents/AutoScroll'; -import { ChatMessage } from '../../../components/UiComponents/Messages/ChatMessages/ChatMessage'; -import { formatUnixTimestamp } from '../../../utils/time'; -import { IoMdSend } from 'react-icons/io'; -import { MdStop } from 'react-icons/md'; -import { LuMessageSquare, LuTrash2, LuUpload, LuFileText, LuX, LuFolderOpen, LuPlus } from 'react-icons/lu'; -import messagesStyles from '../../../components/UiComponents/Messages/Messages.module.css'; -import styles from './ChatbotV2Views.module.css'; - -export const ChatbotV2ConversationsView: React.FC = () => { - const { - threads, - selectedThreadId, - loadingThreads, - error, - messages, - loadingMessages, - selectedThread, - isStreaming, - streamingStatus, - uploadingContext, - pendingFiles, - addContextFiles, - addExistingFile, - submitContext, - clearPendingFiles, - removePendingFile, - selectThread, - createNewThread, - sendMessage, - stopStreaming, - deleteThread, - refreshThreads, - inputValue, - setInputValue - } = useChatbotV2(); - const { request } = useApiRequest(); - - const [deletingId, setDeletingId] = useState(null); - const [isDragOver, setIsDragOver] = useState(false); - const [showExistingPicker, setShowExistingPicker] = useState(false); - const [existingFiles, setExistingFiles] = useState([]); - const [loadingExistingFiles, setLoadingExistingFiles] = useState(false); - const fileInputRef = useRef(null); - - const loadExistingFiles = useCallback(async () => { - setLoadingExistingFiles(true); - try { - const data = await fetchFiles(request); - const items = Array.isArray(data) ? data : (data?.items ?? []); - setExistingFiles(items); - setShowExistingPicker(true); - } catch (err) { - console.error('Fehler beim Laden der Dateien:', err); - } finally { - setLoadingExistingFiles(false); - } - }, [request]); - - const handleAddExistingFile = useCallback( - (file: FileInfo) => { - addExistingFile(file.id, file.fileName); - }, - [addExistingFile] - ); - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - if (!inputValue.trim() || isStreaming) return; - await sendMessage(inputValue); - }; - - const handleKeyDown = (e: React.KeyboardEvent) => { - if (e.key === 'Enter' && !e.shiftKey) { - e.preventDefault(); - if (!inputValue.trim() || isStreaming) return; - sendMessage(inputValue); - } - }; - - const handleStop = async () => { - if (isStreaming) { - try { - await stopStreaming(); - } catch (err) { - console.error('Error stopping:', err); - } - } - }; - - const handleDeleteThread = async (e: React.MouseEvent, workflowId: string) => { - e.stopPropagation(); - if (window.confirm('Möchten Sie diese Konversation wirklich löschen?')) { - setDeletingId(workflowId); - try { - await deleteThread(workflowId); - } finally { - setDeletingId(null); - } - } - }; - - const handleFileSelect = useCallback( - (e: React.ChangeEvent) => { - const files = e.target.files; - if (files && files.length > 0) { - addContextFiles(Array.from(files)); - } - if (fileInputRef.current) fileInputRef.current.value = ''; - }, - [addContextFiles] - ); - - const handleDrop = useCallback( - (e: React.DragEvent) => { - e.preventDefault(); - setIsDragOver(false); - const files = e.dataTransfer.files; - if (files && files.length > 0) { - addContextFiles(Array.from(files)); - } - }, - [addContextFiles] - ); - - const handleDragOver = (e: React.DragEvent) => { - e.preventDefault(); - setIsDragOver(true); - }; - - const handleDragLeave = () => setIsDragOver(false); - - const handleExtractClick = async () => { - await submitContext(); - }; - - const formatDate = (timestamp?: number) => { - if (!timestamp) return ''; - const ms = timestamp * 1000; - const date = new Date(ms); - const now = new Date(); - const diffMs = now.getTime() - date.getTime(); - const diffMins = Math.floor(diffMs / 60000); - const diffHours = Math.floor(diffMs / 3600000); - const diffDays = Math.floor(diffMs / 86400000); - if (diffMins < 1) return 'Gerade eben'; - if (diffMins < 60) return `Vor ${diffMins} Min`; - if (diffHours < 24) return `Vor ${diffHours} Std`; - if (diffDays < 7) return `Vor ${diffDays} Tagen`; - const { time } = formatUnixTimestamp(timestamp, 'de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' }); - return time; - }; - - const getThreadTitle = (thread: any) => thread.name || 'Kontext-Chat'; - - const showExtracting = selectedThreadId && selectedThread?.status === 'extracting'; - const showChat = selectedThreadId && selectedThread && (selectedThread.status === 'ready' || selectedThread.status === 'running'); - const showAddContext = !selectedThreadId; - - return ( -
- {/* Chat History Sidebar */} - - - {/* Main Area: Add Context, Extracting, or Chat */} -
- {showExtracting ? ( -
-
- Kontext wird extrahiert... -

Bitte kurz warten.

-
- ) : showAddContext ? ( - /* Add Context Phase */ -
-
- -

Kontext hinzufügen

-

- Lade PDF- oder Textdateien hoch. Sie werden analysiert und als Kontext für den Chat verwendet. -

-
- -
fileInputRef.current?.click()} - > - - -

Dateien hier ablegen oder klicken zum Auswählen

-

PDF und TXT unterstützt

-
- -
- - {showExistingPicker && ( -
- {existingFiles.length === 0 ? ( -

Keine Dateien vorhanden. Lade zuerst Dateien hoch.

- ) : ( - <> -
- Deine Dateien - -
-
    - {existingFiles.map((file) => { - const isPending = pendingFiles.some((pf) => pf.id === file.id); - return ( -
  • - - {file.fileName} - -
  • - ); - })} -
- - )} -
- )} -
- - {pendingFiles.length > 0 && ( -
-

Ausgewählte Dateien ({pendingFiles.length})

-
    - {pendingFiles.map((f) => ( -
  • - - {f.name} - -
  • - ))} -
-
- - -
-
- )} - -
- ) : ( - /* Chat Phase */ - <> -
- {loadingMessages && messages.length === 0 ? ( -
-
- Lade Nachrichten... -
- ) : messages.length === 0 ? ( -
-
- Stelle Fragen zu deinen Dokumenten. Der Kontext wurde bereits extrahiert. -
-
- ) : ( - -
- {messages.map((message) => ( - - ))} - {isStreaming && ( -
-
- {streamingStatus ? ( -
-
- {streamingStatus} -
- ) : ( -
- - - -
- )} -
-
- )} -
- - )} -
- -
- - {isStreaming ? ( - - ) : ( - - )} - - - )} -
-
- ); -}; - -export default ChatbotV2ConversationsView; diff --git a/src/pages/views/chatbotV2/ChatbotV2Views.module.css b/src/pages/views/chatbotV2/ChatbotV2Views.module.css deleted file mode 100644 index d3e26b3..0000000 --- a/src/pages/views/chatbotV2/ChatbotV2Views.module.css +++ /dev/null @@ -1,619 +0,0 @@ -/** - * Chatbot V2 Views Styles - * Add context + Chat with history on left - */ - -.chatbotView { - display: flex; - height: calc(100vh - 200px); - min-height: 600px; - gap: 1rem; - background: var(--bg-primary, #ffffff); -} - -/* ============================================================================= - * Chat History Sidebar - * ============================================================================= */ - -.chatHistory { - width: 300px; - min-width: 250px; - display: flex; - flex-direction: column; - background: var(--surface-color, #f8f9fa); - border: 1px solid var(--border-color, #e0e0e0); - border-radius: 8px; - overflow: hidden; -} - -.chatHistoryHeader { - display: flex; - align-items: center; - justify-content: space-between; - padding: 1rem; - border-bottom: 1px solid var(--border-color, #e0e0e0); - background: var(--bg-primary, #ffffff); -} - -.chatHistoryTitle { - margin: 0; - font-size: 1rem; - font-weight: 600; - color: var(--text-primary, #1a1a1a); -} - -.newChatButton { - display: flex; - align-items: center; - gap: 0.5rem; - padding: 0.5rem 0.75rem; - border: 1px solid var(--primary-color, #2563eb); - border-radius: 6px; - background: var(--primary-color, #2563eb); - color: white; - font-size: 0.875rem; - font-weight: 500; - cursor: pointer; - transition: background 0.2s; -} - -.newChatButton:hover { - background: var(--primary-hover, #1d4ed8); -} - -.threadList { - flex: 1; - overflow-y: auto; - padding: 0.5rem; -} - -.threadItem { - display: flex; - align-items: center; - justify-content: space-between; - padding: 0.75rem; - margin-bottom: 0.5rem; - background: var(--bg-primary, #ffffff); - border: 1px solid var(--border-color, #e0e0e0); - border-radius: 6px; - cursor: pointer; - transition: all 0.2s; -} - -.threadItem:hover { - background: var(--hover-bg, rgba(0, 0, 0, 0.02)); - border-color: var(--primary-color, #2563eb); -} - -.threadItem.selected { - background: var(--primary-light, #eff6ff); - border-color: var(--primary-color, #2563eb); -} - -.threadContent { - flex: 1; - min-width: 0; -} - -.threadTitle { - font-size: 0.875rem; - font-weight: 500; - color: var(--text-primary, #1a1a1a); - margin-bottom: 0.25rem; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.threadMeta { - font-size: 0.75rem; - color: var(--text-secondary, #666); -} - -.deleteButton { - display: flex; - align-items: center; - justify-content: center; - padding: 0.375rem; - border: none; - border-radius: 4px; - background: transparent; - color: var(--text-secondary, #666); - cursor: pointer; - transition: all 0.2s; - opacity: 0; -} - -.threadItem:hover .deleteButton { - opacity: 1; -} - -.deleteButton:hover { - background: var(--error-light, #fee2e2); - color: var(--error-color, #dc2626); -} - -.deleteButton:disabled { - opacity: 0.5; - cursor: not-allowed; -} - -/* ============================================================================= - * Chat Area - * ============================================================================= */ - -.chatArea { - flex: 1; - display: flex; - flex-direction: column; - background: var(--bg-primary, #ffffff); - border: 1px solid var(--border-color, #e0e0e0); - border-radius: 8px; - overflow: hidden; -} - -.messagesArea { - flex: 1; - overflow-y: auto; - background: var(--bg-primary, #ffffff); - display: flex; - flex-direction: column; -} - -/* ============================================================================= - * Add Context Phase - * ============================================================================= */ - -.addContextPhase { - flex: 1; - display: flex; - flex-direction: column; - align-items: center; - justify-content: flex-start; - padding: 2rem; - overflow-y: auto; -} - -.addContextHeader { - text-align: center; - margin-bottom: 2rem; -} - -.addContextIcon { - font-size: 3rem; - color: var(--primary-color, #2563eb); - margin-bottom: 1rem; -} - -.addContextTitle { - margin: 0 0 0.5rem; - font-size: 1.25rem; - font-weight: 600; - color: var(--text-primary, #1a1a1a); -} - -.addContextHint { - margin: 0; - font-size: 0.875rem; - color: var(--text-secondary, #666); -} - -.dropZone { - width: 100%; - max-width: 480px; - padding: 3rem 2rem; - border: 2px dashed var(--border-color, #e0e0e0); - border-radius: 12px; - background: var(--surface-color, #f8f9fa); - cursor: pointer; - transition: all 0.2s; - display: flex; - flex-direction: column; - align-items: center; - gap: 0.5rem; -} - -.dropZone:hover, -.dropZoneActive { - border-color: var(--primary-color, #2563eb); - background: var(--primary-light, #eff6ff); -} - -.dropZoneIcon { - font-size: 2.5rem; - color: var(--text-tertiary, #999); -} - -.dropZone p { - margin: 0; - font-size: 0.9375rem; - color: var(--text-primary, #1a1a1a); -} - -.dropZoneHint { - font-size: 0.75rem !important; - color: var(--text-tertiary, #888) !important; -} - -.hiddenInput { - position: absolute; - width: 0; - height: 0; - opacity: 0; - pointer-events: none; -} - -/* ============================================================================= - * Existing Files Picker - * ============================================================================= */ - -.existingFilesSection { - width: 100%; - max-width: 480px; - margin-top: 1.5rem; - display: flex; - flex-direction: column; - align-items: center; - gap: 0.75rem; -} - -.existingFilesList { - width: 100%; - border: 1px solid var(--border-color, #e0e0e0); - border-radius: 8px; - background: var(--bg-primary, #ffffff); - max-height: 240px; - overflow: hidden; - display: flex; - flex-direction: column; -} - -.existingFilesHeader { - display: flex; - align-items: center; - justify-content: space-between; - padding: 0.75rem 1rem; - border-bottom: 1px solid var(--border-color, #e0e0e0); - font-size: 0.875rem; - font-weight: 600; - color: var(--text-primary, #1a1a1a); -} - -.closeExistingBtn { - display: flex; - align-items: center; - justify-content: center; - padding: 0.25rem; - border: none; - border-radius: 4px; - background: transparent; - color: var(--text-secondary, #666); - cursor: pointer; - transition: all 0.2s; -} - -.closeExistingBtn:hover { - background: var(--error-light, #fee2e2); - color: var(--error-color, #dc2626); -} - -.existingFilesListInner { - list-style: none; - padding: 0; - margin: 0; - overflow-y: auto; -} - -.existingFileItem { - display: flex; - align-items: center; - gap: 0.5rem; - padding: 0.75rem 1rem; - border-bottom: 1px solid var(--border-color, #e0e0e0); - font-size: 0.875rem; -} - -.existingFileItem:last-child { - border-bottom: none; -} - -.existingFileName { - flex: 1; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.existingFilesEmpty { - padding: 1.5rem; - margin: 0; - font-size: 0.875rem; - color: var(--text-secondary, #666); - text-align: center; -} - -.pendingFilesSection { - width: 100%; - max-width: 480px; - margin-top: 2rem; -} - -.pendingFilesSection h4 { - margin: 0 0 0.75rem; - font-size: 0.9375rem; - font-weight: 600; - color: var(--text-primary, #1a1a1a); -} - -.pendingFilesList { - list-style: none; - padding: 0; - margin: 0 0 1rem; - border: 1px solid var(--border-color, #e0e0e0); - border-radius: 8px; - overflow: hidden; -} - -.pendingFileItem { - display: flex; - align-items: center; - gap: 0.5rem; - padding: 0.75rem 1rem; - background: var(--bg-primary, #ffffff); - border-bottom: 1px solid var(--border-color, #e0e0e0); - font-size: 0.875rem; -} - -.pendingFileItem:last-child { - border-bottom: none; -} - -.pendingFileItem span { - flex: 1; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.removeFileBtn { - display: flex; - align-items: center; - justify-content: center; - padding: 0.25rem; - border: none; - border-radius: 4px; - background: transparent; - color: var(--text-secondary, #666); - cursor: pointer; - transition: all 0.2s; -} - -.removeFileBtn:hover { - background: var(--error-light, #fee2e2); - color: var(--error-color, #dc2626); -} - -.extractActions { - display: flex; - gap: 0.75rem; - justify-content: flex-end; -} - -/* ============================================================================= - * Extracting Phase - * ============================================================================= */ - -.extractingPhase { - flex: 1; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - gap: 1rem; - color: var(--text-secondary, #666); -} - -.extractingHint { - margin: 0; - font-size: 0.875rem; - color: var(--text-tertiary, #888); -} - -/* ============================================================================= - * Typing Indicator & Streaming - * ============================================================================= */ - -.typingIndicator { - display: flex; - width: 100%; - justify-content: flex-start; - padding: 0; - margin: 0; -} - -.typingBubble { - display: inline-flex; - align-items: center; - padding: 12px 16px; - background-color: var(--color-surface, #f0f0f0); - color: var(--color-text, #1a1a1a); - border-radius: 18px; - border-bottom-left-radius: 4px; - max-width: 65%; - box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); -} - -.typingDots { - display: flex; - align-items: center; - gap: 4px; - height: 20px; -} - -.typingDots span { - width: 8px; - height: 8px; - border-radius: 50%; - background-color: var(--color-gray, #999); - animation: typingBounce 1.4s infinite ease-in-out; -} - -.typingDots span:nth-child(1) { animation-delay: 0s; } -.typingDots span:nth-child(2) { animation-delay: 0.2s; } -.typingDots span:nth-child(3) { animation-delay: 0.4s; } - -@keyframes typingBounce { - 0%, 60%, 100% { transform: translateY(0); opacity: 0.7; } - 30% { transform: translateY(-8px); opacity: 1; } -} - -.streamingStatus { - display: flex; - align-items: center; - gap: 10px; - padding: 2px 0; -} - -.statusSpinner { - width: 16px; - height: 16px; - border: 2px solid var(--border-color, #e0e0e0); - border-top-color: var(--primary-color, #2563eb); - border-radius: 50%; - animation: spin 0.8s linear infinite; -} - -.statusText { - font-size: 0.875rem; - color: var(--text-secondary, #666); - font-style: italic; -} - -.inputForm { - display: flex; - gap: 0.75rem; - padding: 1rem; - border-top: 1px solid var(--border-color, #e0e0e0); - background: var(--surface-color, #f8f9fa); -} - -.inputField { - flex: 1; -} - -/* ============================================================================= - * Loading & Error States - * ============================================================================= */ - -.loading { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - padding: 2rem; - color: var(--text-secondary, #666); - gap: 1rem; -} - -.spinner { - width: 24px; - height: 24px; - border: 3px solid var(--border-color, #e0e0e0); - border-top-color: var(--primary-color, #2563eb); - border-radius: 50%; - animation: spin 1s linear infinite; -} - -@keyframes spin { - to { transform: rotate(360deg); } -} - -.error { - padding: 1rem; - color: var(--error-color, #dc2626); - font-size: 0.875rem; -} - -.retryButton { - margin-top: 0.5rem; - padding: 0.5rem 1rem; - border: 1px solid var(--error-color, #dc2626); - border-radius: 6px; - background: transparent; - color: var(--error-color, #dc2626); - font-size: 0.875rem; - cursor: pointer; - transition: background 0.2s; -} - -.retryButton:hover { - background: var(--error-light, #fee2e2); -} - -.emptyState { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - padding: 2rem; - text-align: center; - color: var(--text-secondary, #666); -} - -.emptyIcon { - font-size: 3rem; - color: var(--text-tertiary, #999); - margin-bottom: 1rem; -} - -.emptyState p { - margin: 0.5rem 0; - font-size: 0.875rem; -} - -.emptyHint { - font-size: 0.75rem; - color: var(--text-tertiary, #888); -} - -/* ============================================================================= - * Dark Theme - * ============================================================================= */ - -:global(.dark-theme) .chatbotView { background: var(--surface-dark, #1a1a1a); } -:global(.dark-theme) .chatHistory { background: var(--surface-dark, #1a1a1a); border-color: var(--border-dark, #333); } -:global(.dark-theme) .chatHistoryHeader { background: var(--surface-dark, #1a1a1a); border-bottom-color: var(--border-dark, #333); } -:global(.dark-theme) .chatHistoryTitle { color: var(--text-primary-dark, #ffffff); } -:global(.dark-theme) .threadItem { background: var(--surface-dark, #1a1a1a); border-color: var(--border-dark, #333); } -:global(.dark-theme) .threadItem:hover { background: var(--surface-dark, #2a2a2a); border-color: var(--primary-color, #2563eb); } -:global(.dark-theme) .threadItem.selected { background: var(--primary-dark, #1e3a8a); border-color: var(--primary-color, #2563eb); } -:global(.dark-theme) .threadTitle { color: var(--text-primary-dark, #ffffff); } -:global(.dark-theme) .threadMeta { color: var(--text-secondary-dark, #aaa); } -:global(.dark-theme) .deleteButton { color: var(--text-secondary-dark, #aaa); } -:global(.dark-theme) .deleteButton:hover { background: var(--error-dark, #450a0a); color: var(--error-light, #fef2f2); } -:global(.dark-theme) .chatArea { background: var(--surface-dark, #1a1a1a); border-color: var(--border-dark, #333); } -:global(.dark-theme) .messagesArea { background: var(--surface-dark, #1a1a1a); } -:global(.dark-theme) .inputForm { background: var(--surface-dark, #1a1a1a); border-top-color: var(--border-dark, #333); } -:global(.dark-theme) .addContextTitle { color: var(--text-primary-dark, #ffffff); } -:global(.dark-theme) .addContextHint { color: var(--text-secondary-dark, #aaa); } -:global(.dark-theme) .dropZone { background: var(--surface-dark, #1a1a1a); border-color: var(--border-dark, #333); } -:global(.dark-theme) .dropZone:hover, -:global(.dark-theme) .dropZoneActive { border-color: var(--primary-color, #2563eb); background: var(--primary-dark, #1e3a8a); } -:global(.dark-theme) .dropZone p { color: var(--text-primary-dark, #ffffff); } -:global(.dark-theme) .existingFilesList { background: var(--surface-dark, #1a1a1a); border-color: var(--border-dark, #333); } -:global(.dark-theme) .existingFilesHeader { border-color: var(--border-dark, #333); color: var(--text-primary-dark, #ffffff); } -:global(.dark-theme) .existingFileItem { border-color: var(--border-dark, #333); } -:global(.dark-theme) .existingFilesEmpty { color: var(--text-secondary-dark, #aaa); } -:global(.dark-theme) .pendingFileItem { background: var(--surface-dark, #1a1a1a); border-color: var(--border-dark, #333); } -:global(.dark-theme) .pendingFilesSection h4 { color: var(--text-primary-dark, #ffffff); } -:global(.dark-theme) .loading { color: var(--text-secondary-dark, #aaa); } -:global(.dark-theme) .spinner { border-color: var(--border-dark, #333); border-top-color: var(--primary-color, #2563eb); } -:global(.dark-theme) .error { color: var(--error-light, #fef2f2); } -:global(.dark-theme) .retryButton { border-color: var(--error-color, #dc2626); color: var(--error-light, #fef2f2); } -:global(.dark-theme) .retryButton:hover { background: var(--error-dark, #450a0a); } -:global(.dark-theme) .emptyState { color: var(--text-secondary-dark, #aaa); } -:global(.dark-theme) .emptyIcon { color: var(--text-tertiary-dark, #666); } -:global(.dark-theme) .emptyHint { color: var(--text-tertiary-dark, #666); } -:global(.dark-theme) .typingBubble { background-color: var(--surface-dark, #2a2a2a); } -:global(.dark-theme) .typingDots span { background-color: var(--text-secondary-dark, #aaa); } -:global(.dark-theme) .statusText { color: var(--text-secondary-dark, #aaa); } diff --git a/src/pages/views/chatbotV2/index.ts b/src/pages/views/chatbotV2/index.ts deleted file mode 100644 index 0a85743..0000000 --- a/src/pages/views/chatbotV2/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -/** - * Chatbot V2 Views Export - */ - -export { ChatbotV2ConversationsView } from './ChatbotV2ConversationsView'; diff --git a/src/types/mandate.ts b/src/types/mandate.ts index f8aba04..0d64f97 100644 --- a/src/types/mandate.ts +++ b/src/types/mandate.ts @@ -232,17 +232,6 @@ export const FEATURE_REGISTRY: Record = { { code: 'settings', label: { de: 'Einstellungen', en: 'Settings' }, path: 'settings' }, ] }, - chatbotv2: { - code: 'chatbotv2', - label: { de: 'Chatbot V2', en: 'Chatbot V2' }, - icon: 'chat', - views: [ - { code: 'conversations', label: { de: 'Konversationen', en: 'Conversations' }, path: 'conversations' }, - { code: 'upload', label: { de: 'Upload & Extrahieren', en: 'Upload & Extract' }, path: 'upload' }, - { code: 'chat', label: { de: 'Chat', en: 'Chat' }, path: 'chat' }, - { code: 'threads', label: { de: 'Konversationen', en: 'Threads' }, path: 'threads' }, - ] - }, realestate: { code: 'realestate', label: { de: 'Immobilien', en: 'Real Estate' },