From f0a7daea02b9504228a3b7378d85abf92f4a2516 Mon Sep 17 00:00:00 2001
From: Ida Dittrich
Date: Mon, 23 Feb 2026 07:28:06 +0100
Subject: [PATCH] nicht fertig; Stand Demo Kessler
---
src/App.tsx | 3 +
src/api/chatbotApi.ts | 5 +-
src/api/chatbotV2Api.ts | 240 +++++++
.../UiComponents/Messages/MessagesTypes.ts | 10 +-
src/config/pageRegistry.tsx | 6 +
src/hooks/useChatbot.ts | 25 +-
src/hooks/useChatbotV2.ts | 326 +++++++++
src/pages/FeatureView.tsx | 10 +
src/pages/admin/AdminFeatureAccessPage.tsx | 63 +-
src/pages/admin/ChatbotConfigSection.tsx | 68 +-
.../chatbotV2/ChatbotV2ConversationsView.tsx | 448 +++++++++++++
.../views/chatbotV2/ChatbotV2Views.module.css | 619 ++++++++++++++++++
src/pages/views/chatbotV2/index.ts | 5 +
src/types/mandate.ts | 11 +
14 files changed, 1789 insertions(+), 50 deletions(-)
create mode 100644 src/api/chatbotV2Api.ts
create mode 100644 src/hooks/useChatbotV2.ts
create mode 100644 src/pages/views/chatbotV2/ChatbotV2ConversationsView.tsx
create mode 100644 src/pages/views/chatbotV2/ChatbotV2Views.module.css
create mode 100644 src/pages/views/chatbotV2/index.ts
diff --git a/src/App.tsx b/src/App.tsx
index 32efece..d1c9c5e 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -151,6 +151,9 @@ function App() {
} />
} />
} />
+ } />
+ } />
+ } />
} />
} />
} />
diff --git a/src/api/chatbotApi.ts b/src/api/chatbotApi.ts
index 81d57a4..90f515e 100644
--- a/src/api/chatbotApi.ts
+++ b/src/api/chatbotApi.ts
@@ -16,7 +16,8 @@ export interface UserInputRequest {
export interface ChatbotWorkflow {
id: string;
- mandateId: string;
+ mandateId?: string; // Optional - not in ChatbotConversation
+ featureInstanceId?: string; // From ChatbotConversation
status: string;
name?: string;
currentRound?: number;
@@ -254,7 +255,7 @@ export async function getChatbotThreadsApi(
return {
items: Array.isArray(data.items) ? data.items : [],
- metadata: data.metadata || {}
+ metadata: data.pagination ?? data.metadata ?? {}
};
}
diff --git a/src/api/chatbotV2Api.ts b/src/api/chatbotV2Api.ts
new file mode 100644
index 0000000..2ed4c1b
--- /dev/null
+++ b/src/api/chatbotV2Api.ts
@@ -0,0 +1,240 @@
+/**
+ * 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/components/UiComponents/Messages/MessagesTypes.ts b/src/components/UiComponents/Messages/MessagesTypes.ts
index 240f577..9ab4d6c 100644
--- a/src/components/UiComponents/Messages/MessagesTypes.ts
+++ b/src/components/UiComponents/Messages/MessagesTypes.ts
@@ -22,7 +22,8 @@ export interface MessageDocument {
*/
export interface Message {
id: string;
- workflowId: string;
+ workflowId?: string; // Legacy / backward compat
+ conversationId?: string; // New - from ChatbotMessage
parentMessageId?: string;
documents?: MessageDocument[];
documentsLabel?: string;
@@ -43,6 +44,13 @@ export interface Message {
actionProgress?: string;
}
+/**
+ * Get the conversation/thread ID from a message (supports both workflowId and conversationId)
+ */
+export function getConversationId(message: Message): string {
+ return message.workflowId ?? message.conversationId ?? '';
+}
+
/**
* Message display variant
*/
diff --git a/src/config/pageRegistry.tsx b/src/config/pageRegistry.tsx
index 78a4db5..c52f039 100644
--- a/src/config/pageRegistry.tsx
+++ b/src/config/pageRegistry.tsx
@@ -106,7 +106,13 @@ export const PAGE_ICONS: Record = {
'feature.chatplayground': ,
'feature.codeeditor': ,
'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/useChatbot.ts b/src/hooks/useChatbot.ts
index 6fb79c0..600a34e 100644
--- a/src/hooks/useChatbot.ts
+++ b/src/hooks/useChatbot.ts
@@ -16,7 +16,7 @@ import {
type ChatDataItem,
type StartChatbotRequest
} from '../api/chatbotApi';
-import { Message } from '../components/UiComponents/Messages/MessagesTypes';
+import { Message, getConversationId } from '../components/UiComponents/Messages/MessagesTypes';
import { useInstanceId } from './useCurrentInstance';
export interface ChatbotHookReturn {
@@ -170,7 +170,8 @@ export function useChatbot(): ChatbotHookReturn {
const tempUserMessageId = `temp-user-${Date.now()}`;
const userMessage: Message = {
id: tempUserMessageId,
- workflowId: currentWorkflowId || '',
+ workflowId: currentWorkflowId || undefined,
+ conversationId: currentWorkflowId || undefined,
role: 'user',
message: inputMessageContent,
publishedAt: Date.now()
@@ -211,7 +212,7 @@ export function useChatbot(): ChatbotHookReturn {
return;
}
- // Handle workflow update
+ // Handle workflow update (includes name updates from background task)
if (item.type === 'stat' && item.item?.id) {
newWorkflowId = item.item.id;
setCurrentWorkflowId(item.item.id);
@@ -223,15 +224,19 @@ export function useChatbot(): ChatbotHookReturn {
console.log('Workflow status is stopped');
setIsStreaming(false);
}
+ // Refresh threads when workflow data arrives (e.g. name update from background)
+ if (item.item?.name) {
+ refreshThreads();
+ }
}
// Handle messages
if (item.type === 'message' && item.item) {
const message = item.item as Message;
-
- // Extract workflowId from message if available and not yet set
- if (message.workflowId) {
- const extractedWorkflowId = message.workflowId;
+
+ // Extract conversation/workflow ID from message (supports workflowId and conversationId)
+ const extractedWorkflowId = getConversationId(message);
+ if (extractedWorkflowId) {
// Update local variable and state if not already set
if (!newWorkflowId) {
newWorkflowId = extractedWorkflowId;
@@ -334,10 +339,10 @@ export function useChatbot(): ChatbotHookReturn {
// Try to get workflowId from currentWorkflowId, or from the latest message
let workflowIdToStop = currentWorkflowId;
if (!workflowIdToStop && messages.length > 0) {
- // Try to extract workflowId from the latest message
+ // Try to extract workflowId from the latest message (supports workflowId and conversationId)
const latestMessage = messages[messages.length - 1];
- if (latestMessage.workflowId) {
- workflowIdToStop = latestMessage.workflowId;
+ workflowIdToStop = getConversationId(latestMessage) || undefined;
+ if (workflowIdToStop) {
console.log('Extracted workflowId from latest message:', workflowIdToStop);
}
}
diff --git a/src/hooks/useChatbotV2.ts b/src/hooks/useChatbotV2.ts
new file mode 100644
index 0000000..f3634c3
--- /dev/null
+++ b/src/hooks/useChatbotV2.ts
@@ -0,0 +1,326 @@
+/**
+ * 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 5c906b7..29e13e1 100644
--- a/src/pages/FeatureView.tsx
+++ b/src/pages/FeatureView.tsx
@@ -23,6 +23,9 @@ 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';
@@ -116,6 +119,13 @@ 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/admin/AdminFeatureAccessPage.tsx b/src/pages/admin/AdminFeatureAccessPage.tsx
index 1f7c83c..40727b6 100644
--- a/src/pages/admin/AdminFeatureAccessPage.tsx
+++ b/src/pages/admin/AdminFeatureAccessPage.tsx
@@ -54,6 +54,7 @@ export const AdminFeatureAccessPage: React.FC = () => {
const [chatbotConnectors, setChatbotConnectors] = useState(['preprocessor']); // Array for multiselect (database connectors only)
const [chatbotSystemPrompt, setChatbotSystemPrompt] = useState('');
const [chatbotEnableWebResearch, setChatbotEnableWebResearch] = useState(true); // Enable Tavily web research
+ const [chatbotAllowedProviders, setChatbotAllowedProviders] = useState([]); // Allowed LLM providers (empty = all)
// Ref to track form data for featureCode detection
const formDataRef = useRef>({});
@@ -128,23 +129,15 @@ export const AdminFeatureAccessPage: React.FC = () => {
setIsSubmitting(false);
return;
}
- if (chatbotConnectors.length === 0) {
- showError('Fehler', 'Mindestens ein Connector muss ausgewählt werden.');
- setIsSubmitting(false);
- return;
- }
-
- // Use first connector as primary type (for backward compatibility)
- // Store all connectors in types array
- const primaryConnector = chatbotConnectors.length > 0 ? chatbotConnectors[0] : 'preprocessor';
+ const primaryConnector = chatbotConnectors.length > 0 ? chatbotConnectors[0] : null;
config = {
- connector: {
- types: chatbotConnectors.length > 0 ? chatbotConnectors : ['preprocessor'], // Array of selected connectors
- type: primaryConnector, // Primary connector (for backward compatibility)
+ connector: chatbotConnectors.length > 0 ? {
+ types: chatbotConnectors,
+ type: primaryConnector,
customConnectorClass: null
- },
+ } : undefined,
prompts: {
- useCustomPrompts: true, // Always true since system prompt is required
+ useCustomPrompts: true,
customAnalysisPrompt: chatbotSystemPrompt,
customFinalAnswerPrompt: chatbotSystemPrompt
},
@@ -153,7 +146,8 @@ export const AdminFeatureAccessPage: React.FC = () => {
enableWebResearch: chatbotEnableWebResearch,
enableRetryOnEmpty: true,
maxRetryAttempts: 2
- }
+ },
+ allowedProviders: chatbotAllowedProviders
};
}
@@ -172,6 +166,7 @@ export const AdminFeatureAccessPage: React.FC = () => {
setChatbotConnectors(['preprocessor']);
setChatbotSystemPrompt('');
setChatbotEnableWebResearch(true);
+ setChatbotAllowedProviders([]);
fetchInstances(selectedMandateId);
loadFeatures(); // Refresh global navigation cache
showSuccess('Feature-Instanz erstellt', `Die Instanz "${createLabel}" wurde erfolgreich erstellt.`);
@@ -202,13 +197,16 @@ export const AdminFeatureAccessPage: React.FC = () => {
// Filter out 'websearch' if it exists (legacy)
const connectorTypes = (config?.connector?.types || (config?.connector?.type ? [config.connector.type] : ['preprocessor']))
.filter((c: string) => c !== 'websearch'); // Remove websearch from connectors
- setChatbotConnectors(connectorTypes.length > 0 ? connectorTypes : ['preprocessor']);
+ setChatbotConnectors(connectorTypes);
setChatbotSystemPrompt(config?.prompts?.customAnalysisPrompt || config?.prompts?.customFinalAnswerPrompt || '');
- setChatbotEnableWebResearch(config?.behavior?.enableWebResearch !== false); // Default to true if not set
+ setChatbotEnableWebResearch(config?.behavior?.enableWebResearch !== false);
+ setChatbotAllowedProviders(Array.isArray(config?.allowedProviders) ? config.allowedProviders
+ : Array.isArray(config?.model?.allowedProviders) ? config.model.allowedProviders : []);
} else {
- setChatbotConnectors(['preprocessor']);
+ setChatbotConnectors([]);
setChatbotSystemPrompt('');
setChatbotEnableWebResearch(true);
+ setChatbotAllowedProviders([]);
}
setShowEditModal(true);
};
@@ -227,23 +225,15 @@ export const AdminFeatureAccessPage: React.FC = () => {
setIsSubmitting(false);
return;
}
- if (chatbotConnectors.length === 0) {
- showError('Fehler', 'Mindestens ein Connector muss ausgewählt werden.');
- setIsSubmitting(false);
- return;
- }
-
- // Merge with existing config if it exists
const existingConfig = editingInstance.config as any || {};
- // Use first connector as primary type (for backward compatibility)
- const primaryConnector = chatbotConnectors.length > 0 ? chatbotConnectors[0] : 'preprocessor';
+ const primaryConnector = chatbotConnectors.length > 0 ? chatbotConnectors[0] : null;
config = {
...existingConfig,
- connector: {
- types: chatbotConnectors.length > 0 ? chatbotConnectors : ['preprocessor'], // Array of selected connectors
- type: primaryConnector, // Primary connector (for backward compatibility)
+ connector: chatbotConnectors.length > 0 ? {
+ types: chatbotConnectors,
+ type: primaryConnector,
customConnectorClass: existingConfig.connector?.customConnectorClass || null
- },
+ } : undefined,
prompts: {
useCustomPrompts: true, // Always true since system prompt is required
customAnalysisPrompt: chatbotSystemPrompt,
@@ -255,7 +245,8 @@ export const AdminFeatureAccessPage: React.FC = () => {
enableWebResearch: chatbotEnableWebResearch,
enableRetryOnEmpty: existingConfig.behavior?.enableRetryOnEmpty !== false,
maxRetryAttempts: existingConfig.behavior?.maxRetryAttempts || 2
- }
+ },
+ allowedProviders: chatbotAllowedProviders
};
}
@@ -269,6 +260,7 @@ export const AdminFeatureAccessPage: React.FC = () => {
setEditingInstance(null);
setChatbotConnectors(['preprocessor']);
setChatbotSystemPrompt('');
+ setChatbotAllowedProviders([]);
fetchInstances(selectedMandateId);
loadFeatures(); // Refresh global navigation cache
showSuccess('Feature-Instanz aktualisiert', `Die Instanz "${data.label}" wurde erfolgreich aktualisiert.`);
@@ -547,6 +539,7 @@ export const AdminFeatureAccessPage: React.FC = () => {
setChatbotConnectors(['preprocessor']);
setChatbotSystemPrompt('');
setChatbotEnableWebResearch(true);
+ setChatbotAllowedProviders([]);
}}
placeholder="Feature auswählen (erforderlich)"
className={styles.configSelect}
@@ -589,9 +582,11 @@ export const AdminFeatureAccessPage: React.FC = () => {
connectors={chatbotConnectors}
systemPrompt={chatbotSystemPrompt}
enableWebResearch={chatbotEnableWebResearch}
+ allowedProviders={chatbotAllowedProviders}
onConnectorsChange={setChatbotConnectors}
onSystemPromptChange={setChatbotSystemPrompt}
onEnableWebResearchChange={setChatbotEnableWebResearch}
+ onAllowedProvidersChange={setChatbotAllowedProviders}
/>
)}
@@ -610,6 +605,7 @@ export const AdminFeatureAccessPage: React.FC = () => {
setChatbotConnectors(['preprocessor']);
setChatbotSystemPrompt('');
setChatbotEnableWebResearch(true);
+ setChatbotAllowedProviders([]);
}}
submitButtonText="Erstellen"
cancelButtonText="Abbrechen"
@@ -663,6 +659,7 @@ export const AdminFeatureAccessPage: React.FC = () => {
setChatbotConnectors(['preprocessor']);
setChatbotSystemPrompt('');
setChatbotEnableWebResearch(true);
+ setChatbotAllowedProviders([]);
}}
submitButtonText="Speichern"
cancelButtonText="Abbrechen"
@@ -674,9 +671,11 @@ export const AdminFeatureAccessPage: React.FC = () => {
connectors={chatbotConnectors}
systemPrompt={chatbotSystemPrompt}
enableWebResearch={chatbotEnableWebResearch}
+ allowedProviders={chatbotAllowedProviders}
onConnectorsChange={setChatbotConnectors}
onSystemPromptChange={setChatbotSystemPrompt}
onEnableWebResearchChange={setChatbotEnableWebResearch}
+ onAllowedProvidersChange={setChatbotAllowedProviders}
/>
)}
diff --git a/src/pages/admin/ChatbotConfigSection.tsx b/src/pages/admin/ChatbotConfigSection.tsx
index e03b634..7d208b8 100644
--- a/src/pages/admin/ChatbotConfigSection.tsx
+++ b/src/pages/admin/ChatbotConfigSection.tsx
@@ -5,10 +5,20 @@
* Only shown when featureCode is "chatbot"
*/
-import React from 'react';
+import React, { useEffect } from 'react';
import { TextField } from '../../components/UiComponents/TextField';
+import { useBilling } from '../../hooks/useBilling';
import styles from './Admin.module.css';
+const PROVIDER_LABELS: Record = {
+ anthropic: 'Anthropic (Claude)',
+ openai: 'OpenAI (GPT)',
+ perplexity: 'Perplexity',
+ tavily: 'Tavily (Web Search)',
+ privatellm: 'Private LLM',
+ internal: 'Internal',
+};
+
export interface ChatbotConfig {
connector: string;
systemPrompt: string;
@@ -18,32 +28,50 @@ export interface ChatbotConfigSectionProps {
connectors: string[]; // Array of selected connector types (database connectors only)
systemPrompt: string;
enableWebResearch: boolean; // Enable Tavily web research
+ allowedProviders: string[]; // Selected LLM providers (empty = all allowed)
onConnectorsChange: (connectors: string[]) => void;
onSystemPromptChange: (prompt: string) => void;
onEnableWebResearchChange: (enabled: boolean) => void;
+ onAllowedProvidersChange: (providers: string[]) => void;
}
export const ChatbotConfigSection: React.FC = ({
connectors,
systemPrompt,
enableWebResearch,
+ allowedProviders,
onConnectorsChange,
onSystemPromptChange,
- onEnableWebResearchChange
+ onEnableWebResearchChange,
+ onAllowedProvidersChange,
}) => {
+ const { allowedProviders: availableProviders, loadAllowedProviders, loading: providersLoading } = useBilling();
+
+ useEffect(() => {
+ if (availableProviders.length === 0 && !providersLoading) {
+ loadAllowedProviders();
+ }
+ }, []);
+
const availableConnectors = [
{ id: 'preprocessor', label: 'Althaus Preprocessor', value: 'preprocessor' }
];
const handleConnectorToggle = (connectorValue: string) => {
if (connectors.includes(connectorValue)) {
- // Remove connector
onConnectorsChange(connectors.filter(c => c !== connectorValue));
} else {
- // Add connector
onConnectorsChange([...connectors, connectorValue]);
}
};
+
+ const handleProviderToggle = (provider: string) => {
+ if (allowedProviders.includes(provider)) {
+ onAllowedProvidersChange(allowedProviders.filter(p => p !== provider));
+ } else {
+ onAllowedProvidersChange([...allowedProviders, provider]);
+ }
+ };
return (
@@ -69,7 +97,7 @@ export const ChatbotConfigSection: React.FC = ({
{connectors.length === 0 && (
- Bitte wählen Sie mindestens einen Connector aus.
+ Ohne Connector werden keine SQL-Abfragen unterstützt.
)}
@@ -89,6 +117,36 @@ export const ChatbotConfigSection: React.FC = ({
+
+
+
+ {providersLoading ? (
+ Lade Anbieter...
+ ) : (
+ availableProviders.map(provider => (
+
+ ))
+ )}
+
+ {allowedProviders.length === 0 && !providersLoading && (
+
+ Keine Einschränkung – alle verfügbaren Anbieter werden verwendet.
+
+ )}
+
+