ready for rebase
This commit is contained in:
parent
f0a7daea02
commit
50f3499fda
8 changed files with 0 additions and 1664 deletions
|
|
@ -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<any>) => Promise<any>;
|
|
||||||
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<UploadChatbotV2Response> {
|
|
||||||
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<void> {
|
|
||||||
try {
|
|
||||||
const url = `/api/chatbotv2/${instanceId}/start/stream?workflowId=${encodeURIComponent(requestBody.workflowId)}`;
|
|
||||||
const baseURL = api.defaults.baseURL || '';
|
|
||||||
const fullURL = baseURL + url;
|
|
||||||
|
|
||||||
const headers: Record<string, string> = { '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<ChatbotV2Workflow> {
|
|
||||||
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<void> {
|
|
||||||
await request({
|
|
||||||
url: `/api/chatbotv2/${instanceId}/conversations/${workflowId}`,
|
|
||||||
method: 'delete'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
@ -108,11 +108,6 @@ export const PAGE_ICONS: Record<string, React.ReactNode> = {
|
||||||
'feature.automation': <FaCogs />,
|
'feature.automation': <FaCogs />,
|
||||||
'page.feature.chatbot.conversations': <FaComments />,
|
'page.feature.chatbot.conversations': <FaComments />,
|
||||||
'feature.chatbot': <FaComments />,
|
'feature.chatbot': <FaComments />,
|
||||||
'page.feature.chatbotv2.conversations': <FaComments />,
|
|
||||||
'page.feature.chatbotv2.upload': <FaComments />,
|
|
||||||
'page.feature.chatbotv2.chat': <FaComments />,
|
|
||||||
'page.feature.chatbotv2.threads': <FaComments />,
|
|
||||||
'feature.chatbotv2': <FaComments />,
|
|
||||||
'feature.teamsbot': <FaHeadset />,
|
'feature.teamsbot': <FaHeadset />,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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<void>;
|
|
||||||
addExistingFile: (id: string, fileName: string) => void;
|
|
||||||
submitContext: () => Promise<string | null>; // Returns conversationId or null
|
|
||||||
clearPendingFiles: () => void;
|
|
||||||
removePendingFile: (id: string) => void;
|
|
||||||
|
|
||||||
// Actions
|
|
||||||
selectThread: (workflowId: string) => Promise<void>;
|
|
||||||
createNewThread: () => void;
|
|
||||||
sendMessage: (input: string) => Promise<void>;
|
|
||||||
stopStreaming: () => Promise<void>;
|
|
||||||
deleteThread: (workflowId: string) => Promise<void>;
|
|
||||||
refreshThreads: () => Promise<void>;
|
|
||||||
|
|
||||||
// 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<ChatbotV2Workflow[]>([]);
|
|
||||||
const [selectedThreadId, setSelectedThreadId] = useState<string | null>(null);
|
|
||||||
const [loadingThreads, setLoadingThreads] = useState(false);
|
|
||||||
const [messages, setMessages] = useState<Message[]>([]);
|
|
||||||
const [loadingMessages, setLoadingMessages] = useState(false);
|
|
||||||
const [currentWorkflowId, setCurrentWorkflowId] = useState<string | null>(null);
|
|
||||||
const [isStreaming, setIsStreaming] = useState(false);
|
|
||||||
const [streamingStatus, setStreamingStatus] = useState<string | null>(null);
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
|
||||||
const [inputValue, setInputValue] = useState('');
|
|
||||||
const [uploadingContext, setUploadingContext] = useState(false);
|
|
||||||
const [pendingFiles, setPendingFiles] = useState<Array<{ id: string; name: string }>>([]);
|
|
||||||
|
|
||||||
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<string | null> => {
|
|
||||||
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
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -23,9 +23,6 @@ import { TrusteeAccountingSettingsView } from './views/trustee/TrusteeAccounting
|
||||||
// Chatbot Views
|
// Chatbot Views
|
||||||
import { ChatbotConversationsView } from './views/chatbot/ChatbotConversationsView';
|
import { ChatbotConversationsView } from './views/chatbot/ChatbotConversationsView';
|
||||||
|
|
||||||
// Chatbot V2 Views
|
|
||||||
import { ChatbotV2ConversationsView } from './views/chatbotV2/ChatbotV2ConversationsView';
|
|
||||||
|
|
||||||
// RealEstate Views
|
// RealEstate Views
|
||||||
import { RealEstatePekView, RealEstateInstanceRolesPlaceholder } from './views/realestate';
|
import { RealEstatePekView, RealEstateInstanceRolesPlaceholder } from './views/realestate';
|
||||||
|
|
||||||
|
|
@ -119,13 +116,6 @@ const VIEW_COMPONENTS: Record<string, Record<string, ViewComponent>> = {
|
||||||
conversations: ChatbotConversationsView,
|
conversations: ChatbotConversationsView,
|
||||||
settings: ChatbotSettings,
|
settings: ChatbotSettings,
|
||||||
},
|
},
|
||||||
chatbotv2: {
|
|
||||||
dashboard: ChatbotV2ConversationsView,
|
|
||||||
conversations: ChatbotV2ConversationsView,
|
|
||||||
upload: ChatbotV2ConversationsView,
|
|
||||||
chat: ChatbotV2ConversationsView,
|
|
||||||
threads: ChatbotV2ConversationsView,
|
|
||||||
},
|
|
||||||
realestate: {
|
realestate: {
|
||||||
dashboard: RealEstatePekView,
|
dashboard: RealEstatePekView,
|
||||||
'instance-roles': RealEstateInstanceRolesPlaceholder,
|
'instance-roles': RealEstateInstanceRolesPlaceholder,
|
||||||
|
|
|
||||||
|
|
@ -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<string | null>(null);
|
|
||||||
const [isDragOver, setIsDragOver] = useState(false);
|
|
||||||
const [showExistingPicker, setShowExistingPicker] = useState(false);
|
|
||||||
const [existingFiles, setExistingFiles] = useState<FileInfo[]>([]);
|
|
||||||
const [loadingExistingFiles, setLoadingExistingFiles] = useState(false);
|
|
||||||
const fileInputRef = useRef<HTMLInputElement>(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<HTMLInputElement | HTMLTextAreaElement>) => {
|
|
||||||
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<HTMLInputElement>) => {
|
|
||||||
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 (
|
|
||||||
<div className={styles.chatbotView}>
|
|
||||||
{/* Chat History Sidebar */}
|
|
||||||
<aside className={styles.chatHistory}>
|
|
||||||
<div className={styles.chatHistoryHeader}>
|
|
||||||
<h2 className={styles.chatHistoryTitle}>Konversationen</h2>
|
|
||||||
<button className={styles.newChatButton} onClick={createNewThread} title="Neue Konversation">
|
|
||||||
<LuMessageSquare /> Neu
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{loadingThreads ? (
|
|
||||||
<div className={styles.loading}>
|
|
||||||
<div className={styles.spinner} />
|
|
||||||
<span>Lade Konversationen...</span>
|
|
||||||
</div>
|
|
||||||
) : error ? (
|
|
||||||
<div className={styles.error}>
|
|
||||||
<p>{error}</p>
|
|
||||||
<button className={styles.retryButton} onClick={refreshThreads}>
|
|
||||||
Erneut versuchen
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
) : threads.length === 0 ? (
|
|
||||||
<div className={styles.emptyState}>
|
|
||||||
<LuMessageSquare className={styles.emptyIcon} />
|
|
||||||
<p>Noch keine Konversationen.</p>
|
|
||||||
<p className={styles.emptyHint}>Klicke auf „Neu“ und lade Dateien als Kontext hoch.</p>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className={styles.threadList}>
|
|
||||||
{threads.map((thread) => (
|
|
||||||
<div
|
|
||||||
key={thread.id}
|
|
||||||
className={`${styles.threadItem} ${selectedThreadId === thread.id ? styles.selected : ''}`}
|
|
||||||
onClick={() => selectThread(thread.id)}
|
|
||||||
>
|
|
||||||
<div className={styles.threadContent}>
|
|
||||||
<div className={styles.threadTitle}>{getThreadTitle(thread)}</div>
|
|
||||||
<div className={styles.threadMeta}>
|
|
||||||
{thread.status === 'ready' || thread.status === 'running'
|
|
||||||
? formatDate(thread.lastActivity || thread.startedAt)
|
|
||||||
: thread.status === 'extracting'
|
|
||||||
? 'Wird extrahiert...'
|
|
||||||
: formatDate(thread.lastActivity)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
className={styles.deleteButton}
|
|
||||||
onClick={(e) => handleDeleteThread(e, thread.id)}
|
|
||||||
disabled={deletingId === thread.id}
|
|
||||||
title="Löschen"
|
|
||||||
>
|
|
||||||
{deletingId === thread.id ? <div className={styles.spinner} /> : <LuTrash2 />}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</aside>
|
|
||||||
|
|
||||||
{/* Main Area: Add Context, Extracting, or Chat */}
|
|
||||||
<main className={styles.chatArea}>
|
|
||||||
{showExtracting ? (
|
|
||||||
<div className={styles.extractingPhase}>
|
|
||||||
<div className={styles.spinner} />
|
|
||||||
<span>Kontext wird extrahiert...</span>
|
|
||||||
<p className={styles.extractingHint}>Bitte kurz warten.</p>
|
|
||||||
</div>
|
|
||||||
) : showAddContext ? (
|
|
||||||
/* Add Context Phase */
|
|
||||||
<div className={styles.addContextPhase}>
|
|
||||||
<div className={styles.addContextHeader}>
|
|
||||||
<LuFileText className={styles.addContextIcon} />
|
|
||||||
<h3 className={styles.addContextTitle}>Kontext hinzufügen</h3>
|
|
||||||
<p className={styles.addContextHint}>
|
|
||||||
Lade PDF- oder Textdateien hoch. Sie werden analysiert und als Kontext für den Chat verwendet.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
className={`${styles.dropZone} ${isDragOver ? styles.dropZoneActive : ''}`}
|
|
||||||
onDrop={handleDrop}
|
|
||||||
onDragOver={handleDragOver}
|
|
||||||
onDragLeave={handleDragLeave}
|
|
||||||
onClick={() => fileInputRef.current?.click()}
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
ref={fileInputRef}
|
|
||||||
type="file"
|
|
||||||
multiple
|
|
||||||
accept=".pdf,.txt,application/pdf,text/plain"
|
|
||||||
onChange={handleFileSelect}
|
|
||||||
className={styles.hiddenInput}
|
|
||||||
/>
|
|
||||||
<LuUpload className={styles.dropZoneIcon} />
|
|
||||||
<p>Dateien hier ablegen oder klicken zum Auswählen</p>
|
|
||||||
<p className={styles.dropZoneHint}>PDF und TXT unterstützt</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.existingFilesSection}>
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
size="md"
|
|
||||||
onClick={loadExistingFiles}
|
|
||||||
disabled={loadingExistingFiles}
|
|
||||||
icon={LuFolderOpen}
|
|
||||||
>
|
|
||||||
{loadingExistingFiles ? 'Lade...' : 'Aus bereits hochgeladenen Dateien wählen'}
|
|
||||||
</Button>
|
|
||||||
{showExistingPicker && (
|
|
||||||
<div className={styles.existingFilesList}>
|
|
||||||
{existingFiles.length === 0 ? (
|
|
||||||
<p className={styles.existingFilesEmpty}>Keine Dateien vorhanden. Lade zuerst Dateien hoch.</p>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<div className={styles.existingFilesHeader}>
|
|
||||||
<span>Deine Dateien</span>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={styles.closeExistingBtn}
|
|
||||||
onClick={() => setShowExistingPicker(false)}
|
|
||||||
title="Schließen"
|
|
||||||
>
|
|
||||||
<LuX />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<ul className={styles.existingFilesListInner}>
|
|
||||||
{existingFiles.map((file) => {
|
|
||||||
const isPending = pendingFiles.some((pf) => pf.id === file.id);
|
|
||||||
return (
|
|
||||||
<li key={file.id} className={styles.existingFileItem}>
|
|
||||||
<LuFileText />
|
|
||||||
<span className={styles.existingFileName}>{file.fileName}</span>
|
|
||||||
<Button
|
|
||||||
variant="primary"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => handleAddExistingFile(file)}
|
|
||||||
disabled={isPending}
|
|
||||||
icon={LuPlus}
|
|
||||||
>
|
|
||||||
{isPending ? 'Hinzugefügt' : 'Hinzufügen'}
|
|
||||||
</Button>
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</ul>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{pendingFiles.length > 0 && (
|
|
||||||
<div className={styles.pendingFilesSection}>
|
|
||||||
<h4>Ausgewählte Dateien ({pendingFiles.length})</h4>
|
|
||||||
<ul className={styles.pendingFilesList}>
|
|
||||||
{pendingFiles.map((f) => (
|
|
||||||
<li key={f.id} className={styles.pendingFileItem}>
|
|
||||||
<LuFileText />
|
|
||||||
<span>{f.name}</span>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={styles.removeFileBtn}
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
removePendingFile(f.id);
|
|
||||||
}}
|
|
||||||
title="Entfernen"
|
|
||||||
>
|
|
||||||
<LuX />
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
<div className={styles.extractActions}>
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
size="md"
|
|
||||||
onClick={clearPendingFiles}
|
|
||||||
disabled={uploadingContext}
|
|
||||||
>
|
|
||||||
Zurücksetzen
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="primary"
|
|
||||||
size="md"
|
|
||||||
onClick={handleExtractClick}
|
|
||||||
disabled={uploadingContext || pendingFiles.length === 0}
|
|
||||||
icon={LuUpload}
|
|
||||||
>
|
|
||||||
{uploadingContext ? 'Extrahieren läuft...' : 'Kontext extrahieren & Chat starten'}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
/* Chat Phase */
|
|
||||||
<>
|
|
||||||
<div className={styles.messagesArea}>
|
|
||||||
{loadingMessages && messages.length === 0 ? (
|
|
||||||
<div className={styles.loading}>
|
|
||||||
<div className={styles.spinner} />
|
|
||||||
<span>Lade Nachrichten...</span>
|
|
||||||
</div>
|
|
||||||
) : messages.length === 0 ? (
|
|
||||||
<div className={`${messagesStyles.messagesContainer} ${messagesStyles.emptyContainer}`}>
|
|
||||||
<div className={messagesStyles.emptyState}>
|
|
||||||
Stelle Fragen zu deinen Dokumenten. Der Kontext wurde bereits extrahiert.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<AutoScroll scrollDependency={messages.length + (isStreaming ? 1 : 0)}>
|
|
||||||
<div className={messagesStyles.messagesContainer}>
|
|
||||||
{messages.map((message) => (
|
|
||||||
<ChatMessage key={message.id} message={message} showDocuments={true} />
|
|
||||||
))}
|
|
||||||
{isStreaming && (
|
|
||||||
<div className={styles.typingIndicator}>
|
|
||||||
<div className={styles.typingBubble}>
|
|
||||||
{streamingStatus ? (
|
|
||||||
<div className={styles.streamingStatus}>
|
|
||||||
<div className={styles.statusSpinner} />
|
|
||||||
<span className={styles.statusText}>{streamingStatus}</span>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className={styles.typingDots}>
|
|
||||||
<span></span>
|
|
||||||
<span></span>
|
|
||||||
<span></span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</AutoScroll>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form onSubmit={handleSubmit} className={styles.inputForm}>
|
|
||||||
<TextField
|
|
||||||
value={inputValue}
|
|
||||||
onChange={setInputValue}
|
|
||||||
onKeyDown={handleKeyDown}
|
|
||||||
placeholder="Frage zu deinen Dokumenten..."
|
|
||||||
disabled={isStreaming}
|
|
||||||
className={styles.inputField}
|
|
||||||
size="md"
|
|
||||||
/>
|
|
||||||
{isStreaming ? (
|
|
||||||
<Button type="button" onClick={handleStop} variant="danger" size="md" icon={MdStop} disabled={!isStreaming}>
|
|
||||||
Stoppen
|
|
||||||
</Button>
|
|
||||||
) : (
|
|
||||||
<Button
|
|
||||||
type="submit"
|
|
||||||
disabled={!inputValue.trim() || isStreaming}
|
|
||||||
variant="primary"
|
|
||||||
size="md"
|
|
||||||
icon={IoMdSend}
|
|
||||||
>
|
|
||||||
Senden
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</form>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ChatbotV2ConversationsView;
|
|
||||||
|
|
@ -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); }
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
/**
|
|
||||||
* Chatbot V2 Views Export
|
|
||||||
*/
|
|
||||||
|
|
||||||
export { ChatbotV2ConversationsView } from './ChatbotV2ConversationsView';
|
|
||||||
|
|
@ -232,17 +232,6 @@ export const FEATURE_REGISTRY: Record<string, FeatureConfig> = {
|
||||||
{ code: 'settings', label: { de: 'Einstellungen', en: 'Settings' }, path: 'settings' },
|
{ 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: {
|
realestate: {
|
||||||
code: 'realestate',
|
code: 'realestate',
|
||||||
label: { de: 'Immobilien', en: 'Real Estate' },
|
label: { de: 'Immobilien', en: 'Real Estate' },
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue