From ff68307a392402599abc2b47812f5623ae024744 Mon Sep 17 00:00:00 2001 From: ValueOn AG Date: Wed, 3 Jun 2026 09:35:59 +0200 Subject: [PATCH 1/2] fixes private model and udb scoping sources --- src/App.tsx | 4 +- src/api/chatbotApi.ts | 329 ------------ src/api/featuresApi.ts | 38 -- .../UiComponents/Messages/MessagesTypes.ts | 2 +- .../UnifiedDataBar/FilesTab.module.css | 3 - .../UnifiedDataBar/UdbSourcesProvider.tsx | 23 +- .../__tests__/UdbSourcesProvider.test.ts | 23 +- src/config/keepAliveRoutes.tsx | 4 +- src/config/pageRegistry.tsx | 6 +- src/hooks/useChatbot.ts | 486 ------------------ src/hooks/useInstancePermissions.tsx | 32 -- src/main.tsx | 14 + src/pages/FeatureView.tsx | 36 -- src/pages/admin/Admin.module.css | 8 +- src/pages/admin/AdminFeatureAccessPage.tsx | 146 +----- src/pages/admin/ChatbotConfigSection.tsx | 169 ------ src/pages/admin/SttBenchmarkPage.tsx | 258 ---------- src/pages/admin/index.ts | 1 - .../chatbot/ChatbotConversationsView.tsx | 286 ----------- .../views/chatbot/ChatbotViews.module.css | 467 ----------------- src/pages/views/chatbot/index.ts | 5 - src/pages/views/workspace/WorkspacePage.tsx | 9 +- src/pages/views/workspace/useWorkspace.ts | 49 ++ src/stores/featureStore.tsx | 28 - src/styles/pages.module.css | 53 -- src/types/mandate.ts | 13 +- src/utils/sseClient.ts | 2 + staticwebapp.config.json | 20 +- work-around/chatbot.ts | 79 --- 29 files changed, 121 insertions(+), 2472 deletions(-) delete mode 100644 src/api/chatbotApi.ts delete mode 100644 src/hooks/useChatbot.ts delete mode 100644 src/pages/admin/ChatbotConfigSection.tsx delete mode 100644 src/pages/admin/SttBenchmarkPage.tsx delete mode 100644 src/pages/views/chatbot/ChatbotConversationsView.tsx delete mode 100644 src/pages/views/chatbot/ChatbotViews.module.css delete mode 100644 src/pages/views/chatbot/index.ts delete mode 100644 work-around/chatbot.ts diff --git a/src/App.tsx b/src/App.tsx index 5f35085..eb120e0 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -39,7 +39,7 @@ import { GDPRPage } from './pages/GDPR'; import StorePage from './pages/Store'; import { IntegrationsOverviewPage } from './pages/IntegrationsOverviewPage'; import { FeatureViewPage } from './pages/FeatureView'; -import { AccessManagementHub, AdminMandatesPage, AdminUsersPage, AdminUserMandatesPage, AdminFeatureAccessPage, AdminInvitationsPage, AdminMandateRolesPage, AdminFeatureRolesPage, AdminFeatureInstanceUsersPage, AdminMandateRolePermissionsPage, AdminUserAccessOverviewPage, AdminLogsPage, AdminDemoConfigPage, SttBenchmarkPage } from './pages/admin'; +import { AccessManagementHub, AdminMandatesPage, AdminUsersPage, AdminUserMandatesPage, AdminFeatureAccessPage, AdminInvitationsPage, AdminMandateRolesPage, AdminFeatureRolesPage, AdminFeatureInstanceUsersPage, AdminMandateRolePermissionsPage, AdminUserAccessOverviewPage, AdminLogsPage, AdminDemoConfigPage } from './pages/admin'; import { AdminMandateWizardPage, AdminInvitationWizardPage } from './pages/admin/wizards'; import { PromptsPage, FilesPage, ConnectionsPage } from './pages/basedata'; import { BillingDataView, BillingAdmin, BillingMandateView, AdminSubscriptionsPage } from './pages/billing'; @@ -134,7 +134,6 @@ function App() { } /> {/* Legacy top-level routes – redirect to dashboard (migrated to feature-instance routes) */} - } /> } /> } /> @@ -225,7 +224,6 @@ function App() { } /> - } /> } /> } /> diff --git a/src/api/chatbotApi.ts b/src/api/chatbotApi.ts deleted file mode 100644 index 61280a9..0000000 --- a/src/api/chatbotApi.ts +++ /dev/null @@ -1,329 +0,0 @@ -import { ApiRequestOptions } from '../hooks/useApi'; -import api from '../api'; -import { addCSRFTokenToHeaders, getCSRFToken, generateAndStoreCSRFToken } from '../utils/csrfUtils'; -import { Message } from '../components/UiComponents/Messages/MessagesTypes'; - -// ============================================================================ -// TYPES & INTERFACES -// ============================================================================ - -export interface UserInputRequest { - input: string; - workflowId?: string; - files?: Array<{ id: string; name: string }>; - metadata?: Record; -} - -export interface ChatbotWorkflow { - id: string; - mandateId?: string; // Optional - not in ChatbotConversation - featureInstanceId?: string; // From ChatbotConversation - status: string; - name?: string; - currentRound?: number; - currentTask?: number; - currentAction?: number; - startedAt?: number; - lastActivity?: number; - [key: string]: any; -} - -export interface StartChatbotRequest { - prompt: string; - listFileId?: string[]; - userLanguage?: string; - workflowId?: string; - metadata?: Record; -} - -export interface StartChatbotResponse extends ChatbotWorkflow { - // Workflow object returned from start endpoint -} - -export interface ChatDataItem { - type: 'message' | 'log' | 'stat' | 'document' | 'stopped' | 'status' | 'chunk'; - createdAt?: number; - item?: Message | any; - label?: string; // For status events - content?: string; // For chunk events (token-by-token streaming) -} - -// Type for the request function passed to API functions -export type ApiRequestFunction = (options: ApiRequestOptions) => Promise; - -// Type for SSE event handler -export type SSEEventHandler = (item: ChatDataItem) => void; - -// ============================================================================ -// API REQUEST FUNCTIONS -// ============================================================================ - -/** - * Start a new chatbot workflow or continue an existing one with SSE streaming - * Endpoint: POST /api/chatbot/{instanceId}/start/stream - * - * @param instanceId - Feature Instance ID - * @param requestBody - Request body with prompt and optional workflowId - * @param onEvent - Callback function called for each SSE event - * @param onError - Optional error callback - * @param onComplete - Optional completion callback - * @returns Promise that resolves when stream completes - */ -export async function startChatbotStreamApi( - instanceId: string, - requestBody: StartChatbotRequest, - onEvent: SSEEventHandler, - onError?: (error: Error) => void, - onComplete?: () => void -): Promise { - try { - // Prepare request body - console.log('[startChatbotStreamApi] instanceId:', instanceId); - console.log('[startChatbotStreamApi] requestBody received:', JSON.stringify(requestBody, null, 2)); - - const body: any = { - prompt: requestBody.prompt, - ...(requestBody.listFileId && requestBody.listFileId.length > 0 && { listFileId: requestBody.listFileId }), - ...(requestBody.userLanguage && { userLanguage: requestBody.userLanguage }), - ...(requestBody.metadata && { metadata: requestBody.metadata }) - }; - - console.log('[startChatbotStreamApi] body being sent:', JSON.stringify(body, null, 2)); - - // Add workflowId to query params if provided - const url = requestBody.workflowId - ? `/api/chatbot/${instanceId}/start/stream?workflowId=${encodeURIComponent(requestBody.workflowId)}` - : `/api/chatbot/${instanceId}/start/stream`; - - // Get base URL from api instance - const baseURL = api.defaults.baseURL || ''; - const fullURL = baseURL + url; - - // Prepare headers with authentication and CSRF token - const headers: Record = { - 'Content-Type': 'application/json' - }; - - // Add auth token if available - const authToken = localStorage.getItem('authToken'); - if (authToken) { - headers['Authorization'] = `Bearer ${authToken}`; - } - - // Add CSRF token for POST requests - if (!getCSRFToken()) { - generateAndStoreCSRFToken(); - } - addCSRFTokenToHeaders(headers); - - // Use fetch for SSE streaming (POST with body) - const response = await fetch(fullURL, { - method: 'POST', - headers, - body: JSON.stringify(body), - credentials: 'include' // Include cookies for authentication - }); - - 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; - } - - // Decode chunk and add to buffer - buffer += decoder.decode(value, { stream: true }); - - // Process complete SSE messages - const lines = buffer.split('\n'); - buffer = lines.pop() || ''; // Keep incomplete line in buffer - - for (const line of lines) { - if (line.startsWith('data: ')) { - try { - const jsonStr = line.slice(6); // Remove 'data: ' prefix - if (jsonStr.trim()) { - const item: ChatDataItem = JSON.parse(jsonStr); - console.log('[SSE] Received event:', item.type, item); - onEvent(item); - } - } catch (parseError) { - console.warn('Failed to parse SSE event:', line, parseError); - } - } else if (line.startsWith(':')) { - // Comment/keepalive line, ignore - continue; - } - } - } - - // Process any remaining buffer content - 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 (parseError) { - console.warn('Failed to parse SSE event:', line, parseError); - } - } - } - } - - if (onComplete) { - onComplete(); - } - } finally { - reader.releaseLock(); - } - } catch (error: any) { - console.error('Error in startChatbotStreamApi:', error); - if (onError) { - onError(error instanceof Error ? error : new Error(String(error))); - } else { - throw error; - } - } -} - -/** - * Stop a running chatbot workflow - * Endpoint: POST /api/chatbot/{instanceId}/stop/{workflowId} - */ -export async function stopChatbotApi( - request: ApiRequestFunction, - instanceId: string, - workflowId: string -): Promise { - console.log('[stopChatbotApi] Calling stop endpoint:', `/api/chatbot/${instanceId}/stop/${workflowId}`, { instanceId, workflowId }); - const data = await request({ - url: `/api/chatbot/${instanceId}/stop/${workflowId}`, - method: 'post' - }); - - console.log('[stopChatbotApi] Stop response:', data); - return data as ChatbotWorkflow; -} - -/** - * Get chatbot threads/workflows - * Endpoint: GET /api/chatbot/{instanceId}/threads - */ -export async function getChatbotThreadsApi( - request: ApiRequestFunction, - instanceId: string, - pagination?: { page?: number; pageSize?: number } -): Promise<{ items: ChatbotWorkflow[]; metadata: any }> { - const paginationParam = pagination ? JSON.stringify(pagination) : undefined; - const requestParams = paginationParam - ? { pagination: paginationParam } - : undefined; - - console.log(`[getChatbotThreadsApi] instanceId: ${instanceId}, params:`, requestParams); - - const data = await request({ - url: `/api/chatbot/${instanceId}/threads`, - method: 'get', - params: requestParams - }) as any; - - console.log(`[getChatbotThreadsApi] Full response:`, JSON.stringify(data, null, 2)); - console.log(`[getChatbotThreadsApi] Response structure:`, { - hasItems: !!data.items, - itemsLength: Array.isArray(data.items) ? data.items.length : 'not an array', - hasMetadata: !!data.metadata, - metadataKeys: data.metadata ? Object.keys(data.metadata) : [] - }); - - return { - items: Array.isArray(data.items) ? data.items : [], - metadata: data.pagination ?? data.metadata ?? {} - }; -} - -/** - * Get a specific chatbot thread/workflow with its chat data - * Endpoint: GET /api/chatbot/{instanceId}/threads?workflowId={id} - * - * Backend returns: { workflow: ChatbotWorkflow, chatData: { items: ChatDataItem[] } } - * - * @param request - API request function - * @param instanceId - Feature Instance ID - * @param workflowId - ID of the workflow to fetch - * @returns Object containing workflow details and chatData with items array - */ -export async function getChatbotThreadApi( - request: ApiRequestFunction, - instanceId: string, - workflowId: string -): Promise<{ workflow: ChatbotWorkflow; chatData: { items: ChatDataItem[] } }> { - console.log(`[getChatbotThreadApi] instanceId: ${instanceId}, workflowId: ${workflowId}`); - - const data = await request({ - url: `/api/chatbot/${instanceId}/threads`, - method: 'get', - params: { workflowId } - }) as { workflow: ChatbotWorkflow; chatData: { items: ChatDataItem[] } }; - - console.log(`[getChatbotThreadApi] Full response for workflowId ${workflowId}:`, JSON.stringify(data, null, 2)); - console.log(`[getChatbotThreadApi] Response structure:`, { - hasWorkflow: !!data.workflow, - workflowKeys: data.workflow ? Object.keys(data.workflow) : [], - hasChatData: !!data.chatData, - hasItems: !!data.chatData?.items, - chatDataKeys: data.chatData ? Object.keys(data.chatData) : [], - itemsLength: Array.isArray(data.chatData?.items) ? data.chatData.items.length : 'not an array', - chatDataTypes: Array.isArray(data.chatData?.items) ? data.chatData.items.map((item: ChatDataItem) => item?.type).filter(Boolean) : [] - }); - - return { - workflow: data.workflow, - chatData: data.chatData || { items: [] } - }; -} - -/** - * Delete a chatbot workflow - * Endpoint: DELETE /api/chatbot/{instanceId}/{workflowId} - * - * @param request - API request function - * @param instanceId - Feature Instance ID - * @param workflowId - ID of the workflow to delete - * @returns Success status - */ -export async function deleteChatbotWorkflowApi( - request: ApiRequestFunction, - instanceId: string, - workflowId: string -): Promise { - try { - await request({ - url: `/api/chatbot/${instanceId}/${workflowId}`, - method: 'delete' - }); - return true; - } catch (error: any) { - console.error('Error deleting chatbot workflow:', error); - throw error; - } -} - diff --git a/src/api/featuresApi.ts b/src/api/featuresApi.ts index be2818b..8a75d9f 100644 --- a/src/api/featuresApi.ts +++ b/src/api/featuresApi.ts @@ -178,43 +178,6 @@ export async function fetchMyFeatures(): Promise { // Get the actual data (response.data contains the FeaturesMyResponse) const data = response.data; - // DEBUG: Log all chatbot instances and their permissions - console.log('🔍 [DEBUG] featuresApi: Full response received', { - response, - data, - hasMandates: !!data?.mandates, - mandateCount: data?.mandates?.length || 0, - }); - - if (data?.mandates) { - data.mandates.forEach(mandate => { - mandate.features.forEach(feature => { - if (feature.code === 'chatbot') { - console.log('🔍 [DEBUG] featuresApi: Found chatbot feature', { - mandateId: mandate.id, - mandateName: mandateDisplayLabel(mandate), - featureCode: feature.code, - instanceCount: feature.instances.length, - }); - feature.instances.forEach(instance => { - console.log('🔍 [DEBUG] featuresApi: Chatbot Instance Details:', { - instanceId: instance.id, - instanceLabel: instance.instanceLabel, - featureCode: instance.featureCode, - userRoles: instance.userRoles, - permissions: instance.permissions, - views: instance.permissions?.views, - viewKeys: instance.permissions?.views ? Object.keys(instance.permissions.views) : [], - hasConversationsView: instance.permissions?.views?.['chatbot-conversations'] || - instance.permissions?.views?.['ui.feature.chatbot.conversations'] || - instance.permissions?.views?.['_all'], - }); - }); - } - }); - }); - } - console.log('✅ featuresApi: Loaded features:', { mandateCount: data?.mandates?.length || 0, totalInstances: data?.mandates @@ -239,7 +202,6 @@ export async function fetchAvailableFeatures(): Promise { return [ { code: 'trustee', label: 'Treuhand', icon: 'briefcase', instances: [] }, { code: 'chatworkflow', label: 'Workflow', icon: 'play_circle', instances: [] }, - { code: 'chatbot', label: 'Chatbot', icon: 'chat', instances: [] }, ]; } diff --git a/src/components/UiComponents/Messages/MessagesTypes.ts b/src/components/UiComponents/Messages/MessagesTypes.ts index 04ac70a..1ba9bae 100644 --- a/src/components/UiComponents/Messages/MessagesTypes.ts +++ b/src/components/UiComponents/Messages/MessagesTypes.ts @@ -29,7 +29,7 @@ export interface MessageDocument { export interface Message { id: string; workflowId?: string; // Legacy / backward compat - conversationId?: string; // New - from ChatbotMessage + conversationId?: string; // New - from chat conversation message parentMessageId?: string; documents?: MessageDocument[]; documentsLabel?: string; diff --git a/src/components/UnifiedDataBar/FilesTab.module.css b/src/components/UnifiedDataBar/FilesTab.module.css index e8889ae..8999cb2 100644 --- a/src/components/UnifiedDataBar/FilesTab.module.css +++ b/src/components/UnifiedDataBar/FilesTab.module.css @@ -48,7 +48,6 @@ flex-shrink: 0; } -.scopeIcon, .neutralizeIcon { border: none; background: transparent; @@ -60,7 +59,6 @@ transition: opacity 0.15s; } -.scopeIcon:hover, .neutralizeIcon:hover { opacity: 1; background: var(--bg-hover, rgba(0, 0, 0, 0.06)); @@ -139,7 +137,6 @@ .fileRow:hover { background: rgba(255, 255, 255, 0.05); } - .scopeIcon:hover, .neutralizeIcon:hover { background: rgba(255, 255, 255, 0.08); } diff --git a/src/components/UnifiedDataBar/UdbSourcesProvider.tsx b/src/components/UnifiedDataBar/UdbSourcesProvider.tsx index 96ffedd..f1a28c8 100644 --- a/src/components/UnifiedDataBar/UdbSourcesProvider.tsx +++ b/src/components/UnifiedDataBar/UdbSourcesProvider.tsx @@ -35,7 +35,7 @@ import { } from 'react-icons/fa'; import { SiJira } from 'react-icons/si'; import api from '../../api'; -import type { TreeNode, TreeNodeProvider, ScopeValue } from '../FormGenerator/FormGeneratorTree'; +import type { TreeNode, TreeNodeProvider } from '../FormGenerator/FormGeneratorTree'; // --------------------------------------------------------------------------- // Backend contract types @@ -180,14 +180,14 @@ function _mapBackendNode( // neutralizeFields list). Scope and RAG are not field-level concepts. node.neutralize = n.effectiveNeutralize; } else if (_isFdsKind(n.kind)) { - // FDS records have neutralize + ragIndexEnabled, but no scope. node.neutralize = n.effectiveNeutralize; if (n.supportsRag) { node.ragIndexEnabled = n.effectiveRagIndexEnabled; } } else { - // DataSource family carries the full three-flag set. - node.scope = n.effectiveScope as ScopeValue | 'mixed'; + // DataSource family: neutralize + ragIndexEnabled only. + // Scope was removed (personal sources must not be shared across + // scopes — privacy requirement, 2026-06). node.neutralize = n.effectiveNeutralize; if (n.supportsRag) { node.ragIndexEnabled = n.effectiveRagIndexEnabled; @@ -287,7 +287,7 @@ export function createUdbSourcesProvider( * permission check, and applies the cascade-reset. */ async function _patchFlag( ids: string[], - flag: 'neutralize' | 'scope' | 'ragIndexEnabled', + flag: 'neutralize' | 'ragIndexEnabled', value: unknown, ): Promise { for (const nodeKey of ids) { @@ -314,10 +314,8 @@ export function createUdbSourcesProvider( return list.map((n) => _mapBackendNode(n, _onSettingsClick)); }, - canPatchScope(node) { - const data = node.data; - // Scope only exists on DataSource family; FDS / synthetic containers / fields hide it. - return !!data && !_isSyntheticContainer(data.kind) && !_isFdsKind(data.kind); + canPatchScope(_node) { + return false; }, canPatchNeutralize(node) { @@ -331,11 +329,8 @@ export function createUdbSourcesProvider( return !!data && data.supportsRag === true && data.kind !== 'fdsField'; }, - async patchScope(ids, scope, _cascadeChildren) { - // Backend cascades NULL on descendants automatically based on the - // existence of explicit child records; the cascadeChildren flag is the - // FilesTab convention and is irrelevant here. - await _patchFlag(ids, 'scope', scope); + async patchScope(_ids, _scope, _cascadeChildren) { + // Scope removed from personal sources (privacy, 2026-06). No-op. }, async patchNeutralize(ids, neutralize) { diff --git a/src/components/UnifiedDataBar/__tests__/UdbSourcesProvider.test.ts b/src/components/UnifiedDataBar/__tests__/UdbSourcesProvider.test.ts index d878648..72282dd 100644 --- a/src/components/UnifiedDataBar/__tests__/UdbSourcesProvider.test.ts +++ b/src/components/UnifiedDataBar/__tests__/UdbSourcesProvider.test.ts @@ -128,7 +128,7 @@ describe('UdbSourcesProvider.loadChildren', () => { ); }); - it('maps DS-family backend nodes to TreeNode shape with all three flags', async () => { + it('maps DS-family backend nodes to TreeNode shape with neutralize + rag (no scope)', async () => { const conn = _makeBackendNode(); apiMock.post.mockResolvedValue({ data: { nodesByParent: { 'personalRoot': [conn] } } }); const provider = createUdbSourcesProvider(_instanceId, vi.fn()); @@ -138,7 +138,7 @@ describe('UdbSourcesProvider.loadChildren', () => { expect(result).toHaveLength(1); const tn = result[0]; expect(tn.id).toBe('conn|c1'); - expect(tn.scope).toBe('personal'); + expect(tn.scope).toBeUndefined(); expect(tn.neutralize).toBe(false); expect(tn.ragIndexEnabled).toBe(false); }); @@ -193,7 +193,16 @@ describe('UdbSourcesProvider.canPatch*', () => { expect(provider.canPatchRagIndex?.(synthNode)).toBe(false); }); - it('canPatchScope is false for any FDS kind', async () => { + it('canPatchScope is false for all node kinds (scope removed from Sources)', async () => { + apiMock.post.mockResolvedValue({ + data: { nodesByParent: { 'personalRoot': [_makeBackendNode()] } }, + }); + const provider = createUdbSourcesProvider(_instanceId, vi.fn()); + const [conn] = await provider.loadChildren('personalRoot', 'own'); + expect(provider.canPatchScope?.(conn)).toBe(false); + }); + + it('canPatch neutralize and rag on FDS table', async () => { apiMock.post.mockResolvedValue({ data: { nodesByParent: { 'feat|m1|trustee|fi1': [_makeFdsTableNode()] } }, }); @@ -221,16 +230,12 @@ describe('UdbSourcesProvider.canPatch*', () => { // --------------------------------------------------------------------------- describe('UdbSourcesProvider.patchScope', () => { - it('POSTs to /api/udb/node/{key}/flag/scope with the new value', async () => { - apiMock.post.mockResolvedValue({ data: {} }); + it('is a no-op (scope removed from personal sources, 2026-06)', async () => { const provider = createUdbSourcesProvider(_instanceId, vi.fn()); await provider.patchScope?.(['conn|c1'], 'mandate', true); - expect(apiMock.post).toHaveBeenCalledWith( - `/api/udb/node/${encodeURIComponent('conn|c1')}/flag/scope`, - { value: 'mandate' }, - ); + expect(apiMock.post).not.toHaveBeenCalled(); }); }); diff --git a/src/config/keepAliveRoutes.tsx b/src/config/keepAliveRoutes.tsx index e091c5b..784c68f 100644 --- a/src/config/keepAliveRoutes.tsx +++ b/src/config/keepAliveRoutes.tsx @@ -11,8 +11,8 @@ export const KEEP_ALIVE_ROUTES: KeepAliveEntry[] = [ pathRegex: /\/mandates\/[^/]+\/workspace\/[^/]+\/dashboard/, scopeRegex: /\/mandates\/([^/]+)\/workspace\/([^/]+)/, requireMandateForMount: false, - render: ({ instanceId, scopeKey }) => ( - + render: ({ mandateId, instanceId, scopeKey }) => ( + ), }, { diff --git a/src/config/pageRegistry.tsx b/src/config/pageRegistry.tsx index 706dece..d2292cc 100644 --- a/src/config/pageRegistry.tsx +++ b/src/config/pageRegistry.tsx @@ -19,7 +19,7 @@ import { FaHome, FaCog, FaBriefcase, FaPlay, FaBuilding, FaUsers, FaUserTag, FaCubes, FaEnvelopeOpenText, FaKey, FaUsersCog, FaCube, FaShieldAlt, FaLightbulb, FaRegFileAlt, FaLink, FaComments, - FaListAlt, FaChartLine, FaChartBar, FaFileAlt, FaUserShield, FaDatabase, FaMicrophone, + FaListAlt, FaChartLine, FaChartBar, FaFileAlt, FaUserShield, FaDatabase, FaProjectDiagram, FaMapMarkedAlt, FaWallet, FaMoneyBillAlt, FaClock, FaHeadset, FaVideo, FaHatWizard, FaStore, FaUserTie, FaClipboardList, FaFileContract, FaRobot, FaGlobe, FaClipboardCheck, @@ -88,8 +88,6 @@ export const PAGE_ICONS: Record = { 'page.admin.database-health': , 'page.admin.demoConfig': , 'page.admin.demo-config': , - 'page.admin.sttBenchmark': , - 'page.admin.stt-benchmark': , 'page.admin.mandate-wizard': , 'page.admin.mandateWizard': , 'page.admin.invitation-wizard': , @@ -137,8 +135,6 @@ export const PAGE_ICONS: Record = { 'feature.graphicalEditor': , 'page.feature.graphicalEditor.editor': , 'page.feature.graphicalEditor.workflows-tasks': , - 'page.feature.chatbot.conversations': , - 'feature.chatbot': , 'feature.teamsbot': , // Feature pages - Workspace diff --git a/src/hooks/useChatbot.ts b/src/hooks/useChatbot.ts deleted file mode 100644 index 44084eb..0000000 --- a/src/hooks/useChatbot.ts +++ /dev/null @@ -1,486 +0,0 @@ -/** - * useChatbot Hook - * - * Hook for managing chatbot conversations, messages, and chat functionality. - */ - -import { useState, useEffect, useCallback, useRef } from 'react'; -import { useApiRequest } from './useApi'; -import { - getChatbotThreadsApi, - getChatbotThreadApi, - startChatbotStreamApi, - stopChatbotApi, - deleteChatbotWorkflowApi, - type ChatbotWorkflow, - type ChatDataItem, - type StartChatbotRequest -} from '../api/chatbotApi'; -import { Message, getConversationId } from '../components/UiComponents/Messages/MessagesTypes'; -import { useInstanceId } from './useCurrentInstance'; - -export interface ChatbotHookReturn { - // Threads/Conversations - threads: ChatbotWorkflow[]; - selectedThreadId: string | null; - loadingThreads: boolean; - error: string | null; - - // Messages - messages: Message[]; - loadingMessages: boolean; - - // Current workflow state - currentWorkflowId: string | null; - isStreaming: boolean; - streamingStatus: string | null; // Current streaming status message - - // Actions - selectThread: (workflowId: string) => Promise; - createNewThread: () => void; - sendMessage: (input: string, files?: Array<{ id: string; name: string }>) => Promise; - stopStreaming: () => Promise; - deleteThread: (workflowId: string) => Promise; - refreshThreads: () => Promise; - - // Input form state - inputValue: string; - setInputValue: (value: string) => void; -} - -/** - * Main chatbot hook - */ -export function useChatbot(): ChatbotHookReturn { - const { request } = useApiRequest(); - const instanceId = useInstanceId(); - - // Threads state - const [threads, setThreads] = useState([]); - const [selectedThreadId, setSelectedThreadId] = useState(null); - const [loadingThreads, setLoadingThreads] = useState(false); - - // Messages state - const [messages, setMessages] = useState([]); - const [loadingMessages, setLoadingMessages] = useState(false); - - // Current workflow state - const [currentWorkflowId, setCurrentWorkflowId] = useState(null); - const [isStreaming, setIsStreaming] = useState(false); - const [streamingStatus, setStreamingStatus] = useState(null); - - // Error state - const [error, setError] = useState(null); - - // Input state - const [inputValue, setInputValue] = useState(''); - - // Ref to track if component is mounted - const isMountedRef = useRef(true); - - useEffect(() => { - isMountedRef.current = true; - return () => { - isMountedRef.current = false; - }; - }, []); - - // Load threads - const refreshThreads = useCallback(async () => { - if (!instanceId) return; - - setLoadingThreads(true); - setError(null); - try { - const result = await getChatbotThreadsApi(request, instanceId); - if (isMountedRef.current) { - setThreads(result.items || []); - } - } catch (err: any) { - console.error('Error loading threads:', err); - if (isMountedRef.current) { - setError(err.message || 'Fehler beim Laden der Konversationen'); - } - } finally { - if (isMountedRef.current) { - setLoadingThreads(false); - } - } - }, [request, instanceId]); - - // Load messages for a thread - const loadThreadMessages = useCallback(async (workflowId: string) => { - if (!instanceId) return; - - setLoadingMessages(true); - setError(null); - try { - const result = await getChatbotThreadApi(request, instanceId, workflowId); - if (isMountedRef.current) { - // Extract messages from chatData items - 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) { - console.error('Error loading thread messages:', err); - if (isMountedRef.current) { - setError(err.message || 'Fehler beim Laden der Nachrichten'); - setMessages([]); - } - } finally { - if (isMountedRef.current) { - setLoadingMessages(false); - } - } - }, [request, instanceId]); - - // Select a thread - const selectThread = useCallback(async (workflowId: string) => { - setSelectedThreadId(workflowId); - await loadThreadMessages(workflowId); - }, [loadThreadMessages]); - - // Create new thread - const createNewThread = useCallback(() => { - setSelectedThreadId(null); - setMessages([]); - setCurrentWorkflowId(null); - setInputValue(''); - }, []); - - // Send message - const sendMessage = useCallback(async ( - input: string, - files?: Array<{ id: string; name: string }> - ) => { - if (!input.trim() || isStreaming || !instanceId) return; - - setError(null); - setIsStreaming(true); - setStreamingStatus(null); // Reset status - - // Store the input message content to track duplicates - const inputMessageContent = input.trim(); - - // Add user message immediately for better UX - const tempUserMessageId = `temp-user-${Date.now()}`; - const userMessage: Message = { - id: tempUserMessageId, - workflowId: currentWorkflowId || undefined, - conversationId: currentWorkflowId || undefined, - role: 'user', - message: inputMessageContent, - publishedAt: Date.now() - }; - - setMessages(prev => [...prev, userMessage]); - setInputValue(''); - - try { - const requestBody: StartChatbotRequest = { - prompt: input, - workflowId: currentWorkflowId || undefined, - listFileId: files?.map(f => f.id), - userLanguage: navigator.language || 'de' - }; - - let newWorkflowId: string | null = null; - - await startChatbotStreamApi( - instanceId, - requestBody, - (item: ChatDataItem) => { - if (!isMountedRef.current) return; - - // Handle stopped event - if (item.type === 'stopped') { - console.log('Received stopped event from backend'); - setIsStreaming(false); - setStreamingStatus(null); - return; - } - - // Handle status event (streaming progress updates) - if (item.type === 'status') { - const statusLabel = item.label || (item.item as any)?.label || ''; - console.log('Received status update:', statusLabel); - setStreamingStatus(statusLabel); - return; - } - - // Handle chunk events (ChatGPT-like token-by-token streaming) - if (item.type === 'chunk' && item.content) { - setMessages(prev => { - const last = prev[prev.length - 1]; - if (last?.role === 'assistant') { - return prev.map(m => - m.id === last!.id ? { ...m, message: (m.message || '') + item.content } : m - ); - } - return [...prev, { - id: `streaming-${currentWorkflowId || 'new'}-${Date.now()}`, - workflowId: currentWorkflowId || undefined, - conversationId: currentWorkflowId || undefined, - role: 'assistant' as const, - message: item.content, - publishedAt: Date.now() - }]; - }); - return; - } - - // Handle workflow update (includes name updates from background task) - if (item.type === 'stat' && item.item?.id) { - newWorkflowId = item.item.id; - setCurrentWorkflowId(item.item.id); - if (!selectedThreadId) { - setSelectedThreadId(item.item.id); - } - // Check if workflow status is stopped - if (item.item.status === 'stopped') { - 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 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; - console.log('Extracting workflowId from message:', extractedWorkflowId); - } - // Always update state to ensure we have the latest workflowId - setCurrentWorkflowId(prev => { - if (!prev) { - console.log('Setting currentWorkflowId from message:', extractedWorkflowId); - return extractedWorkflowId; - } - return prev; - }); - if (!selectedThreadId) { - setSelectedThreadId(extractedWorkflowId); - } - } - - setMessages(prev => { - // Check if message already exists by ID - if (prev.some(m => m.id === message.id)) { - return prev; - } - - // Backend sends the "first" message with the transformed/normalized user prompt - // Replace the temporary optimistic message with it - if (message.status === 'first') { - return prev.map(m => - m.id === tempUserMessageId ? message : m - ); - } - - // Final assistant message: replace streaming placeholder if we have one - if (message.role === 'assistant') { - const streamingIdx = prev.findIndex(m => m.id?.startsWith('streaming-')); - if (streamingIdx >= 0) { - const before = prev.slice(0, streamingIdx); - const after = prev.slice(streamingIdx + 1); - return [...before, message, ...after]; - } - } - - // For other messages, check for duplicates by role and content (more lenient check) - const isDuplicate = prev.some(m => { - // Exact ID match - if (m.id === message.id) return true; - // For same role and content, check if it's a duplicate - if (m.role === message.role && m.message === message.message) { - // If it's a user message, it's definitely a duplicate - if (message.role === 'user') return true; - // For assistant messages, check if timestamps are very close (within 1 second) - if (m.publishedAt && message.publishedAt) { - return Math.abs(m.publishedAt - message.publishedAt) < 1000; - } - } - return false; - }); - - if (isDuplicate) return prev; - return [...prev, message]; - }); - } - }, - (err: Error) => { - console.error('Stream error:', err); - if (isMountedRef.current) { - setError(err.message || 'Fehler beim Senden der Nachricht'); - setIsStreaming(false); - } - }, - () => { - if (isMountedRef.current) { - setIsStreaming(false); - setStreamingStatus(null); // Clear status on completion - // Refresh threads to get updated list - refreshThreads(); - } - } - ); - - // Refresh threads after completion - if (newWorkflowId) { - await refreshThreads(); - } - } catch (err: any) { - console.error('Error sending message:', err); - if (isMountedRef.current) { - setError(err.message || 'Fehler beim Senden der Nachricht'); - setIsStreaming(false); - } - } - }, [currentWorkflowId, selectedThreadId, isStreaming, instanceId, refreshThreads]); - - // Stop streaming - const stopStreaming = useCallback(async () => { - if (!instanceId) { - console.warn('Cannot stop: missing instanceId', { instanceId }); - return; - } - - if (!isStreaming) { - console.warn('Cannot stop: not currently streaming'); - return; - } - - // Immediately reset UI state for instant feedback - setIsStreaming(false); - console.log('UI reset immediately after stop button click'); - - // 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 (supports workflowId and conversationId) - const latestMessage = messages[messages.length - 1]; - workflowIdToStop = getConversationId(latestMessage) ?? null; - if (workflowIdToStop) { - console.log('Extracted workflowId from latest message:', workflowIdToStop); - } - } - - if (!workflowIdToStop) { - console.warn('Cannot stop: missing workflowId, but UI already reset', { - currentWorkflowId, - messagesCount: messages.length, - latestMessage: messages.length > 0 ? messages[messages.length - 1] : null - }); - // UI already reset above, just return - return; - } - - // Send stop request to backend (fire and forget - UI already reset) - try { - console.log('Sending stop request to backend for workflow:', workflowIdToStop); - // Don't await - let it run in background, UI is already reset - stopChatbotApi(request, instanceId, workflowIdToStop).catch((err: any) => { - console.error('Error stopping stream on backend (non-blocking):', err); - // Optionally show a non-intrusive error notification - if (isMountedRef.current) { - // Don't reset isStreaming again as it's already false - // Just log the error - console.warn('Backend stop request failed, but UI was already reset'); - } - }); - } catch (err: any) { - console.error('Error initiating stop request:', err); - // UI already reset, so just log the error - } - }, [currentWorkflowId, isStreaming, instanceId, request, messages]); - - // Delete thread - const deleteThread = useCallback(async (workflowId: string) => { - if (!instanceId) return; - - // Optimistic UI update - remove thread immediately - const previousThreads = threads; - setThreads(prev => prev.filter(t => t.id !== workflowId)); - - // If deleted thread was selected, clear selection immediately - if (selectedThreadId === workflowId) { - createNewThread(); - } - - try { - await deleteChatbotWorkflowApi(request, instanceId, workflowId); - - // Refresh threads list to sync with server - await refreshThreads(); - } catch (err: any) { - console.error('Error deleting thread:', err); - // Restore threads on error - setThreads(previousThreads); - setError(err.message || 'Fehler beim Löschen der Konversation'); - } - }, [request, instanceId, selectedThreadId, threads, createNewThread, refreshThreads]); - - // Initial load - useEffect(() => { - if (instanceId) { - refreshThreads(); - } - }, [instanceId, refreshThreads]); - - return { - threads, - selectedThreadId, - loadingThreads, - error, - messages, - loadingMessages, - currentWorkflowId, - isStreaming, - streamingStatus, - selectThread, - createNewThread, - sendMessage, - stopStreaming, - deleteThread, - refreshThreads, - inputValue, - setInputValue - }; -} - -/** - * Hook factory for use in GenericPageData inputFormConfig - */ -export function createChatbotHook() { - return () => { - const chatbot = useChatbot(); - - return { - messages: chatbot.messages, - loading: chatbot.loadingMessages || chatbot.isStreaming, - error: chatbot.error, - data: [], - inputValue: chatbot.inputValue, - onInputChange: chatbot.setInputValue, - handleSubmit: async () => { - await chatbot.sendMessage(chatbot.inputValue); - }, - isSubmitting: chatbot.isStreaming, - stopAction: chatbot.stopStreaming, - canStop: chatbot.isStreaming - }; - }; -} diff --git a/src/hooks/useInstancePermissions.tsx b/src/hooks/useInstancePermissions.tsx index 048c27c..bfd7ac9 100644 --- a/src/hooks/useInstancePermissions.tsx +++ b/src/hooks/useInstancePermissions.tsx @@ -127,43 +127,11 @@ export function useCanViewFeatureView(viewCode: string): boolean { const { instance, featureCode } = useCurrentInstance(); if (!instance?.permissions?.views) { - // DEBUG: Log for chatbot - if (featureCode === 'chatbot') { - console.log('🔍 [DEBUG] useCanViewFeatureView: No views permissions', { - viewCode, - featureCode, - instanceId: instance?.id, - hasPermissions: !!instance?.permissions, - hasViews: !!instance?.permissions?.views, - }); - } return false; } const views = instance.permissions.views; - // DEBUG: Log for chatbot - if (featureCode === 'chatbot') { - const parts = viewCode.split('-'); - const viewName = parts.length >= 2 ? parts.slice(1).join('-') : ''; - const fullObjectKey = `ui.feature.${featureCode}.${viewName}`; - - console.log('🔍 [DEBUG] useCanViewFeatureView: Checking permissions', { - viewCode, - featureCode, - viewName, - fullObjectKey, - instanceId: instance.id, - viewKeys: Object.keys(views), - hasWildcard: !!views["_all"], - hasLegacyView: !!views[viewCode], - hasFullObjectKey: !!views[fullObjectKey], - wildcardValue: views["_all"], - legacyValue: views[viewCode], - fullObjectKeyValue: views[fullObjectKey], - }); - } - // Check for wildcard "_all" permission first (item=None in backend = all views) if (views["_all"]) { return true; diff --git a/src/main.tsx b/src/main.tsx index ad8cb6c..b79dae2 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -7,6 +7,20 @@ import './index.css' import './styles/themes/light.css' import './styles/buttons.css' +// Browser extensions (e.g. password managers, ad blockers) inject content scripts +// whose chrome.runtime message listeners reject with this exact message when the +// extension's background context goes away. It is not an app error and cannot be +// fixed from the page — we only silence this one known-benign rejection so it does +// not clutter the console. Any other rejection is left untouched. +const _EXTENSION_NOISE = 'message channel closed before a response was received'; +window.addEventListener('unhandledrejection', (event) => { + const reason = event.reason; + const message = typeof reason === 'string' ? reason : reason?.message; + if (typeof message === 'string' && message.includes(_EXTENSION_NOISE)) { + event.preventDefault(); + } +}); + createRoot(document.getElementById('root')!).render( diff --git a/src/pages/FeatureView.tsx b/src/pages/FeatureView.tsx index b0b564f..bfe6921 100644 --- a/src/pages/FeatureView.tsx +++ b/src/pages/FeatureView.tsx @@ -22,9 +22,6 @@ import { TrusteeAnalyseView } from './views/trustee/TrusteeAnalyseView'; import { TrusteeAbschlussView } from './views/trustee/TrusteeAbschlussView'; import { TrusteeDataTablesView } from './views/trustee/TrusteeDataTablesView'; -// Chatbot Views -import { ChatbotConversationsView } from './views/chatbot/ChatbotConversationsView'; - // RealEstate Views import { RealEstatePekView, RealEstateInstanceRolesPlaceholder } from './views/realestate'; @@ -86,16 +83,6 @@ const ChatworkflowFiles: React.FC = () => { return ; }; -// Chatbot Views -// ChatbotConversationsView is imported above - -const ChatbotSettings: React.FC = () => { - const { t } = useLanguage(); - return ( - - ); -}; - // Generic/Fallback const NotFound: React.FC = () => { const { t } = useLanguage(); @@ -138,10 +125,6 @@ const VIEW_COMPONENTS: Record> = { runs: ChatworkflowRuns, files: ChatworkflowFiles, }, - chatbot: { - conversations: ChatbotConversationsView, - settings: ChatbotSettings, - }, realestate: { dashboard: RealEstatePekView, 'instance-roles': RealEstateInstanceRolesPlaceholder, @@ -197,25 +180,6 @@ export const FeatureViewPage: React.FC = ({ view }) => { const viewCode = `${featureCode}-${view}`; const canView = useCanViewFeatureView(viewCode); - // DEBUG: Log permission check for chatbot - if (featureCode === 'chatbot') { - console.log('🔍 [DEBUG] FeatureView Permission Check:', { - featureCode, - view, - viewCode, - instanceId: instance?.id, - instanceLabel: instance?.instanceLabel, - isValid, - canView, - permissions: instance?.permissions, - views: instance?.permissions?.views, - viewKeys: instance?.permissions?.views ? Object.keys(instance.permissions.views) : [], - hasLegacyView: instance?.permissions?.views?.[viewCode], - hasFullObjectKey: instance?.permissions?.views?.[`ui.feature.${featureCode}.${view}`], - hasWildcard: instance?.permissions?.views?.['_all'], - }); - } - // Nicht valider Kontext if (!isValid || !featureCode || !instance) { return ; diff --git a/src/pages/admin/Admin.module.css b/src/pages/admin/Admin.module.css index 74c98cd..e0dedfa 100644 --- a/src/pages/admin/Admin.module.css +++ b/src/pages/admin/Admin.module.css @@ -801,13 +801,9 @@ } /* ============================================== */ -/* Chatbot Configuration Styles */ +/* Feature Instance Configuration Styles */ /* ============================================== */ -.chatbotConfigSection { - margin-top: 0; -} - .configSectionTitle { font-size: 1.125rem; font-weight: 600; @@ -848,7 +844,7 @@ font-style: italic; } -/* Multiselect styles for chatbot connectors */ +/* Multiselect styles for connector selection */ .multiselectContainer { padding: 0.75rem; border: 1px solid var(--border-color); diff --git a/src/pages/admin/AdminFeatureAccessPage.tsx b/src/pages/admin/AdminFeatureAccessPage.tsx index b0fe120..ac93881 100644 --- a/src/pages/admin/AdminFeatureAccessPage.tsx +++ b/src/pages/admin/AdminFeatureAccessPage.tsx @@ -16,7 +16,6 @@ import { useToast } from '../../contexts/ToastContext'; import api from '../../api'; import { resolveColumnTypes } from '../../utils/columnTypeResolver'; import type { ColumnConfig } from '../../components/FormGenerator/FormGeneratorTable'; -import { ChatbotConfigSection } from './ChatbotConfigSection'; import { TextField } from '../../components/UiComponents/TextField'; import styles from './Admin.module.css'; @@ -56,13 +55,9 @@ export const AdminFeatureAccessPage: React.FC = () => { const [syncingWorkflowsInstance, setSyncingWorkflowsInstance] = useState(null); const [backendAttributes, setBackendAttributes] = useState([]); - // Chatbot configuration state + // Instance creation state const [createFeatureCode, setCreateFeatureCode] = useState(''); const [createLabel, setCreateLabel] = useState(''); // Label field value - 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>({}); @@ -137,53 +132,17 @@ export const AdminFeatureAccessPage: React.FC = () => { return; } - // Build config for chatbot instances - let config: Record | undefined = undefined; - if (createFeatureCode === 'chatbot') { - // Validate required fields - if (!chatbotSystemPrompt || chatbotSystemPrompt.trim() === '') { - showError(t('Fehler'), t('System Prompt ist erforderlich für Chatbot-Instanzen.')); - setIsSubmitting(false); - return; - } - const primaryConnector = chatbotConnectors.length > 0 ? chatbotConnectors[0] : null; - config = { - connector: chatbotConnectors.length > 0 ? { - types: chatbotConnectors, - type: primaryConnector, - customConnectorClass: null - } : undefined, - prompts: { - useCustomPrompts: true, - customAnalysisPrompt: chatbotSystemPrompt, - customFinalAnswerPrompt: chatbotSystemPrompt - }, - behavior: { - maxQueries: 5, - enableWebResearch: chatbotEnableWebResearch, - enableRetryOnEmpty: true, - maxRetryAttempts: 2 - }, - allowedProviders: chatbotAllowedProviders - }; - } - const result = await createInstance(selectedMandateId, { featureCode: createFeatureCode, label: createLabel, enabled: data.enabled !== false, copyTemplateRoles: data.copyTemplateRoles !== false, - config: config }); if (result.success) { setShowCreateModal(false); setCreateFeatureCode(''); setCreateLabel(''); formDataRef.current = {}; - setChatbotConnectors(['preprocessor']); - setChatbotSystemPrompt(''); - setChatbotEnableWebResearch(true); - setChatbotAllowedProviders([]); fetchInstances(selectedMandateId); loadFeatures(); // Refresh global navigation cache showSuccess(t('Feature-Instanz erstellt'), t('Die Instanz "{name}" wurde erfolgreich erstellt.', { name: createLabel })); @@ -207,24 +166,6 @@ export const AdminFeatureAccessPage: React.FC = () => { // Handle edit click const handleEditClick = (instance: FeatureInstance) => { setEditingInstance(instance); - // Load chatbot config if it's a chatbot instance - if (instance.featureCode === 'chatbot' && instance.config) { - const config = instance.config as any; - // Support both new array format and legacy single type format - // 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); - setChatbotSystemPrompt(config?.prompts?.customAnalysisPrompt || config?.prompts?.customFinalAnswerPrompt || ''); - setChatbotEnableWebResearch(config?.behavior?.enableWebResearch !== false); - setChatbotAllowedProviders(Array.isArray(config?.allowedProviders) ? config.allowedProviders - : Array.isArray(config?.model?.allowedProviders) ? config.model.allowedProviders : []); - } else { - setChatbotConnectors([]); - setChatbotSystemPrompt(''); - setChatbotEnableWebResearch(true); - setChatbotAllowedProviders([]); - } setShowEditModal(true); }; @@ -233,51 +174,13 @@ export const AdminFeatureAccessPage: React.FC = () => { if (!selectedMandateId || !editingInstance) return; setIsSubmitting(true); try { - // Build config for chatbot instances - let config: Record | undefined = undefined; - if (editingInstance.featureCode === 'chatbot') { - // Validate required fields - if (!chatbotSystemPrompt || chatbotSystemPrompt.trim() === '') { - showError(t('Fehler'), t('System Prompt ist erforderlich für Chatbot-Instanzen.')); - setIsSubmitting(false); - return; - } - const existingConfig = editingInstance.config as any || {}; - const primaryConnector = chatbotConnectors.length > 0 ? chatbotConnectors[0] : null; - config = { - ...existingConfig, - 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, - customFinalAnswerPrompt: chatbotSystemPrompt - }, - behavior: { - ...(existingConfig.behavior || {}), - maxQueries: existingConfig.behavior?.maxQueries || 5, - enableWebResearch: chatbotEnableWebResearch, - enableRetryOnEmpty: existingConfig.behavior?.enableRetryOnEmpty !== false, - maxRetryAttempts: existingConfig.behavior?.maxRetryAttempts || 2 - }, - allowedProviders: chatbotAllowedProviders - }; - } - const result = await updateInstance(selectedMandateId, editingInstance.id, { label: data.label, enabled: data.enabled, - config: config }); if (result.success) { setShowEditModal(false); setEditingInstance(null); - setChatbotConnectors(['preprocessor']); - setChatbotSystemPrompt(''); - setChatbotAllowedProviders([]); fetchInstances(selectedMandateId); loadFeatures(); // Refresh global navigation cache showSuccess(t('Feature-Instanz aktualisiert'), t('Die Instanz "{name}" wurde erfolgreich aktualisiert.', { name: data.label })); @@ -562,10 +465,6 @@ export const AdminFeatureAccessPage: React.FC = () => { }} onClick={() => { setCreateFeatureCode(f.code); - setChatbotConnectors(['preprocessor']); - setChatbotSystemPrompt(''); - setChatbotEnableWebResearch(true); - setChatbotAllowedProviders([]); }} > {f.label || f.code} @@ -574,13 +473,6 @@ export const AdminFeatureAccessPage: React.FC = () => { - {/* Chatbot Configuration Title - Show when chatbot is selected */} - {createFeatureCode === 'chatbot' && ( -

- {t('Chatbot-Konfiguration')} -

- )} - {/* Label Field - Always shown after title */} {createFeatureCode && (
@@ -599,20 +491,6 @@ export const AdminFeatureAccessPage: React.FC = () => {
)} - {/* Chatbot Configuration Section - Show when chatbot is selected */} - {createFeatureCode === 'chatbot' && ( - - )} - {/* Main Form - Only show if featureCode is selected */} {createFeatureCode && (
@@ -625,10 +503,6 @@ export const AdminFeatureAccessPage: React.FC = () => { setCreateFeatureCode(''); setCreateLabel(''); formDataRef.current = {}; - setChatbotConnectors(['preprocessor']); - setChatbotSystemPrompt(''); - setChatbotEnableWebResearch(true); - setChatbotAllowedProviders([]); }} submitButtonText={t('Erstellen')} cancelButtonText={t('Abbrechen')} @@ -679,28 +553,10 @@ export const AdminFeatureAccessPage: React.FC = () => { onCancel={() => { setShowEditModal(false); setEditingInstance(null); - setChatbotConnectors(['preprocessor']); - setChatbotSystemPrompt(''); - setChatbotEnableWebResearch(true); - setChatbotAllowedProviders([]); }} submitButtonText={t('Speichern')} cancelButtonText={t('Abbrechen')} /> - - {/* Chatbot Configuration Section */} - {editingInstance?.featureCode === 'chatbot' && ( - - )}
diff --git a/src/pages/admin/ChatbotConfigSection.tsx b/src/pages/admin/ChatbotConfigSection.tsx deleted file mode 100644 index 9502d53..0000000 --- a/src/pages/admin/ChatbotConfigSection.tsx +++ /dev/null @@ -1,169 +0,0 @@ -/** - * ChatbotConfigSection Component - * - * Displays chatbot-specific configuration fields (connector, system prompt) - * Only shown when featureCode is "chatbot" - */ - -import React, { useEffect } from 'react'; -import { TextField } from '../../components/UiComponents/TextField'; -import { useBilling } from '../../hooks/useBilling'; -import styles from './Admin.module.css'; - -import { useLanguage } from '../../providers/language/LanguageContext'; - -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; -} - -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, - onAllowedProvidersChange, -}) => { - const { t } = useLanguage(); - const { allowedProviders: availableProviders, loadAllowedProviders, loading: providersLoading } = useBilling(); - - useEffect(() => { - if (availableProviders.length === 0 && !providersLoading) { - loadAllowedProviders(); - } - }, []); - - const availableConnectors = [ - { id: 'preprocessor', label: t('Althaus Preprocessor'), value: 'preprocessor' } - ]; - - const handleConnectorToggle = (connectorValue: string) => { - if (connectors.includes(connectorValue)) { - onConnectorsChange(connectors.filter(c => c !== connectorValue)); - } else { - onConnectorsChange([...connectors, connectorValue]); - } - }; - - const handleProviderToggle = (provider: string) => { - if (allowedProviders.includes(provider)) { - onAllowedProvidersChange(allowedProviders.filter(p => p !== provider)); - } else { - onAllowedProvidersChange([...allowedProviders, provider]); - } - }; - - return ( -
-
- -
- {availableConnectors.map(connector => { - const isSelected = connectors.includes(connector.value); - return ( - - ); - })} -
- {connectors.length === 0 && ( -

- {t('Ohne Connector werden keine SQL-Abfragen unterstützt.')} -

- )} -
- -
- -

- {t('Wenn aktiviert, führt der Chatbot zusätzlich Web-Recherchen mit Tavily durch, um aktuelle Informationen aus dem Internet zu finden.')} -

-
- -
- -
- {providersLoading ? ( - {t('Anbieter laden')} - ) : ( - availableProviders.map(provider => ( - - )) - )} -
- {allowedProviders.length === 0 && !providersLoading && ( -

- {t('Keine Einschränkung – alle verfügbaren Anbieter werden verwendet.')} -

- )} -
- -
- - -

- {t('Dieser Prompt wird für Analyse und Antwort-Generierung verwendet (erforderlich).')}{' '} - {t('Platzhalter')}: {'{userPrompt}'}, {'{context}'}, {'{db_results_part}'}, {'{web_results_part}'} -

-
-
- ); -}; diff --git a/src/pages/admin/SttBenchmarkPage.tsx b/src/pages/admin/SttBenchmarkPage.tsx deleted file mode 100644 index 92b297e..0000000 --- a/src/pages/admin/SttBenchmarkPage.tsx +++ /dev/null @@ -1,258 +0,0 @@ -/** - * SttBenchmarkPage — Compare STT v1 (latest_long) vs v2 (Chirp 2). - * SysAdmin only. Upload audio, run both engines, compare results. - */ - -import React, { useState, useEffect, useRef, useCallback } from 'react'; -import { FaMicrophone, FaUpload, FaPlay, FaStop, FaSpinner } from 'react-icons/fa'; -import { useLanguage } from '../../providers/language/LanguageContext'; -import { useApiRequest } from '../../hooks/useApi'; -import styles from '../admin/Admin.module.css'; - -interface ModelOption { value: string; label: string } -interface BenchmarkResult { - api: string; - model: string; - latencyMs: number; - results: { transcript: string; confidence: number; words: number }[]; - resultCount: number; - location?: string; - error?: string; -} -interface BenchmarkResponse { - filename: string; - fileSizeBytes: number; - language: string; - v1: BenchmarkResult | { error: string }; - v2: BenchmarkResult | { error: string }; -} -interface ModelsResponse { - v1Models: ModelOption[]; - v2Models: ModelOption[]; - locations: ModelOption[]; - languages: ModelOption[]; -} - -export const SttBenchmarkPage: React.FC = () => { - const { t } = useLanguage(); - const { request } = useApiRequest(); - - const [models, setModels] = useState(null); - const [language, setLanguage] = useState('de-DE'); - const [v1Model, setV1Model] = useState('latest_long'); - const [v2Model, setV2Model] = useState('chirp_2'); - const [v2Location, setV2Location] = useState('europe-west4'); - const [running, setRunning] = useState(false); - const [result, setResult] = useState(null); - - const [recording, setRecording] = useState(false); - const [audioBlob, setAudioBlob] = useState(null); - const [audioUrl, setAudioUrl] = useState(null); - const mediaRecorderRef = useRef(null); - const chunksRef = useRef([]); - const fileInputRef = useRef(null); - - useEffect(() => { - request({ url: '/api/admin/stt-benchmark/models', method: 'get' }) - .then((data: any) => setModels(data)) - .catch(() => {}); - }, []); - - const _startRecording = useCallback(async () => { - try { - const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); - const recorder = new MediaRecorder(stream, { mimeType: 'audio/webm;codecs=opus' }); - chunksRef.current = []; - recorder.ondataavailable = (e) => { if (e.data.size > 0) chunksRef.current.push(e.data); }; - recorder.onstop = () => { - const blob = new Blob(chunksRef.current, { type: 'audio/webm' }); - setAudioBlob(blob); - setAudioUrl(URL.createObjectURL(blob)); - stream.getTracks().forEach(t => t.stop()); - }; - mediaRecorderRef.current = recorder; - recorder.start(); - setRecording(true); - } catch (err) { - console.error('Microphone access denied', err); - } - }, []); - - const _stopRecording = useCallback(() => { - mediaRecorderRef.current?.stop(); - setRecording(false); - }, []); - - const _handleFileSelect = useCallback((e: React.ChangeEvent) => { - const file = e.target.files?.[0]; - if (!file) return; - setAudioBlob(file); - setAudioUrl(URL.createObjectURL(file)); - }, []); - - const _runBenchmark = useCallback(async () => { - if (!audioBlob) return; - setRunning(true); - setResult(null); - try { - const formData = new FormData(); - const filename = audioBlob instanceof File ? audioBlob.name : 'recording.webm'; - formData.append('file', audioBlob, filename); - formData.append('language', language); - formData.append('v1Model', v1Model); - formData.append('v2Model', v2Model); - formData.append('v2Location', v2Location); - - const resp = await fetch('/api/admin/stt-benchmark/run', { - method: 'POST', - body: formData, - credentials: 'include', - }); - if (!resp.ok) throw new Error(`HTTP ${resp.status}`); - const data: BenchmarkResponse = await resp.json(); - setResult(data); - } catch (err: any) { - console.error('Benchmark failed:', err); - } finally { - setRunning(false); - } - }, [audioBlob, language, v1Model, v2Model, v2Location]); - - const _renderResult = (label: string, r: BenchmarkResult | { error: string }) => { - if ('error' in r && r.error) { - return ( -
-

{label}

-

{r.error}

-
- ); - } - const res = r as BenchmarkResult; - const topTranscript = res.results?.[0]?.transcript || '(no result)'; - const topConfidence = res.results?.[0]?.confidence ?? 0; - return ( -
-

{label}

-
-
{t('Modell')}: {res.model}
-
{t('Latenz')}: {res.latencyMs} ms
-
{t('Konfidenz')}: {(topConfidence * 100).toFixed(1)}%
-
{t('Alternativen')}: {res.results?.length || 0}
- {res.location &&
{t('Region')}: {res.location}
} -
-
- {topTranscript} -
- {res.results?.length > 1 && ( -
- {t('Weitere Alternativen')} - {res.results.slice(1).map((alt, i) => ( -
- [{(alt.confidence * 100).toFixed(1)}%] {alt.transcript} -
- ))} -
- )} -
- ); - }; - - return ( -
-
-

{t('STT Benchmark')}

-

- {t('Vergleiche Speech-to-Text v1 (latest_long) mit v2 (Chirp 2). Lade eine Audio-Datei hoch oder nimm direkt auf.')} -

-
- -
- - - - -
- -
- {!recording ? ( - - ) : ( - - )} - - - - - {audioBlob && ( - <> - - {audioBlob instanceof File ? audioBlob.name : 'recording.webm'} ({(audioBlob.size / 1024).toFixed(0)} KB) - - {audioUrl &&
- - - - {result && ( -
-

{t('Ergebnis')}

-

- {result.filename} ({(result.fileSizeBytes / 1024).toFixed(0)} KB) — {result.language} -

-
- {_renderResult(`v1 — ${(result.v1 as any).model || v1Model}`, result.v1)} - {_renderResult(`v2 — ${(result.v2 as any).model || v2Model}`, result.v2)} -
-
- )} -
- ); -}; - -export default SttBenchmarkPage; diff --git a/src/pages/admin/index.ts b/src/pages/admin/index.ts index 2f5a6db..74bc916 100644 --- a/src/pages/admin/index.ts +++ b/src/pages/admin/index.ts @@ -19,4 +19,3 @@ export { AdminLogsPage } from './AdminLogsPage'; export { AdminLanguagesPage } from './AdminLanguagesPage'; export { AdminDemoConfigPage } from './AdminDemoConfigPage'; export { AdminDatabaseHealthPage } from './AdminDatabaseHealthPage'; -export { SttBenchmarkPage } from './SttBenchmarkPage'; diff --git a/src/pages/views/chatbot/ChatbotConversationsView.tsx b/src/pages/views/chatbot/ChatbotConversationsView.tsx deleted file mode 100644 index 0dc9cef..0000000 --- a/src/pages/views/chatbot/ChatbotConversationsView.tsx +++ /dev/null @@ -1,286 +0,0 @@ -/** - * ChatbotConversationsView - * - * Chatbot interface with chat history sidebar and messages view. - * Similar to trustee views but hardcoded for chatbot feature. - */ - -import React, { useState, useCallback } from 'react'; -import { useChatbot } from '../../../hooks/useChatbot'; -import { useConfirm } from '../../../hooks/useConfirm'; -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 } from 'react-icons/lu'; -import messagesStyles from '../../../components/UiComponents/Messages/Messages.module.css'; -import styles from './ChatbotViews.module.css'; - -import { useLanguage } from '../../../providers/language/LanguageContext'; - -export const ChatbotConversationsView: React.FC = () => { - const { t } = useLanguage(); - - const { - threads, - selectedThreadId, - loadingThreads, - error, - messages, - loadingMessages, - isStreaming, - streamingStatus, - currentWorkflowId, - selectThread, - createNewThread, - sendMessage, - stopStreaming, - deleteThread, - refreshThreads, - inputValue, - setInputValue - } = useChatbot(); - - const [deletingId, setDeletingId] = useState(null); - const { confirm, ConfirmDialog } = useConfirm(); - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - if (!inputValue.trim() || isStreaming) return; - await sendMessage(inputValue); - }; - - const handleKeyDown = (e: React.KeyboardEvent) => { - // Enter ohne Shift sendet die Nachricht - if (e.key === 'Enter' && !e.shiftKey) { - e.preventDefault(); - if (!inputValue.trim() || isStreaming) return; - sendMessage(inputValue); - } - }; - - const handleStop = async () => { - console.log('Stop button clicked', { - isStreaming, - currentWorkflowId, - selectedThreadId, - hasMessages: messages.length > 0 - }); - if (isStreaming) { - console.log('Calling stopStreaming...'); - try { - await stopStreaming(); - console.log('stopStreaming completed'); - } catch (error) { - console.error('Error in stopStreaming:', error); - } - } else { - console.warn('Stop button clicked but not streaming'); - } - }; - - const handleDeleteThread = useCallback(async (e: React.MouseEvent, workflowId: string) => { - e.stopPropagation(); - const ok = await confirm(t('Möchten Sie diese Konversation wirklich löschen?'), { - title: t('Konversation löschen'), - confirmLabel: t('Löschen'), - variant: 'danger', - }); - if (!ok) return; - setDeletingId(workflowId); - try { - await deleteThread(workflowId); - } finally { - setDeletingId(null); - } - }, [confirm, deleteThread, t]); - - const formatDate = (timestamp?: number) => { - if (!timestamp) return ''; - // Convert Unix timestamp (seconds) to milliseconds using the time utility logic - const milliseconds = timestamp * 1000; - const date = new Date(milliseconds); - 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 t('Gerade eben'); - if (diffMins < 60) return t('Vor {n} Min', { n: diffMins }); - if (diffHours < 24) return t('Vor {n} Std', { n: diffHours }); - if (diffDays < 7) return t('Vor {n} Tagen', { n: diffDays }); - - // For older dates, use the formatUnixTimestamp utility for consistent formatting - const { time } = formatUnixTimestamp(timestamp, 'de-DE', { - day: '2-digit', - month: '2-digit', - year: 'numeric' - }); - return time; - }; - - const getThreadTitle = (thread: any) => { - if (thread.name) return thread.name; - // Try to get first message content as title - return t('Neue Konversation'); - }; - - return ( -
- {/* Chat History Sidebar */} - - - {/* Main Chat Area */} -
- {/* Messages Area */} -
- {loadingMessages && messages.length === 0 ? ( -
-
- {t('Nachrichten laden')} -
- ) : messages.length === 0 ? ( -
-
- {t('Noch keine Nachrichten. Starte eine Konversation!')} -
-
- ) : ( - -
- {messages.map((message) => ( - - ))} - {isStreaming && ( -
-
- {streamingStatus ? ( -
-
- {streamingStatus} -
- ) : ( -
- - - -
- )} -
-
- )} -
- - )} -
- - {/* Input Form */} -
- - {isStreaming ? ( - - ) : ( - - )} - -
- -
- ); -}; - -export default ChatbotConversationsView; diff --git a/src/pages/views/chatbot/ChatbotViews.module.css b/src/pages/views/chatbot/ChatbotViews.module.css deleted file mode 100644 index d7c20ef..0000000 --- a/src/pages/views/chatbot/ChatbotViews.module.css +++ /dev/null @@ -1,467 +0,0 @@ -/** - * Chatbot Views Shared Styles - */ - -.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; -} - -.messagesWrapper { - display: flex; - flex-direction: column; - flex: 1; - position: relative; -} - -/* ============================================================================= - * Typing Indicator (WhatsApp style) - * ============================================================================= */ - -.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; - } -} - -/* ============================================================================= - * Streaming Status (with status message) - * ============================================================================= */ - -.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; -} - -/* Dark theme support for streaming status */ -:global(.dark-theme) .statusSpinner { - border-color: var(--border-dark, #444); - border-top-color: var(--primary-color, #2563eb); -} - -:global(.dark-theme) .statusText { - color: var(--text-secondary-dark, #aaa); -} - -/* Dark theme support for typing indicator */ -:global(.dark-theme) .typingBubble { - background-color: var(--surface-dark, #2a2a2a); -} - -:global(.dark-theme) .typingDots span { - background-color: var(--text-secondary-dark, #aaa); -} - -.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) .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) .typingDots span { - background: var(--text-secondary-dark, #aaa); -} diff --git a/src/pages/views/chatbot/index.ts b/src/pages/views/chatbot/index.ts deleted file mode 100644 index 7a71c9e..0000000 --- a/src/pages/views/chatbot/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -/** - * Chatbot Views Export - */ - -export { ChatbotConversationsView } from './ChatbotConversationsView'; diff --git a/src/pages/views/workspace/WorkspacePage.tsx b/src/pages/views/workspace/WorkspacePage.tsx index 4d5d15c..5738e58 100644 --- a/src/pages/views/workspace/WorkspacePage.tsx +++ b/src/pages/views/workspace/WorkspacePage.tsx @@ -61,18 +61,20 @@ type RightTab = 'activity' | 'preview'; interface WorkspacePageProps { persistentInstanceId?: string; + persistentMandateId?: string; } -export const WorkspacePage: React.FC = ({ persistentInstanceId }) => { +export const WorkspacePage: React.FC = ({ persistentInstanceId, persistentMandateId }) => { const { t } = useLanguage(); const { instance } = useCurrentInstance(); const instanceId = persistentInstanceId || instance?.id || ''; const workspace = useWorkspace(instanceId); const fileOps = useFileOperations(); const navigate = useNavigate(); - const { mandateId, featureCode, instanceId: routeInstanceId } = useParams<{ + const { mandateId: routeMandateId, featureCode, instanceId: routeInstanceId } = useParams<{ mandateId: string; featureCode: string; instanceId: string; }>(); + const mandateId = persistentMandateId || routeMandateId; const [leftCollapsed, setLeftCollapsed] = useState(false); const [rightCollapsed, setRightCollapsed] = useState(false); const _leftResize = _useResizable(280, 200, 800); @@ -350,7 +352,8 @@ export const WorkspacePage: React.FC = ({ persistentInstance const [pendingAttachDsId, setPendingAttachDsId] = useState(''); const _handleAttachDataSource = useCallback((dsId: string) => { setPendingAttachDsId(dsId); - }, []); + workspace.refreshDataSources(); + }, [workspace]); const _handleDataSourceDrop = useCallback(async (params: { connectionId: string; sourceType: string; path: string; label: string; displayPath?: string }) => { try { diff --git a/src/pages/views/workspace/useWorkspace.ts b/src/pages/views/workspace/useWorkspace.ts index c2d35b2..1fec594 100644 --- a/src/pages/views/workspace/useWorkspace.ts +++ b/src/pages/views/workspace/useWorkspace.ts @@ -9,6 +9,7 @@ import { useState, useCallback, useRef, useEffect } from 'react'; import api from '../../../api'; import { startSseStream, SseEvent } from '../../../utils/sseClient'; +import { useLanguage } from '../../../providers/language/LanguageContext'; import type { Message, MessageDocument } from '../../../components/UiComponents/Messages/MessagesTypes'; export interface AgentProgress { @@ -123,6 +124,7 @@ interface UseWorkspaceReturn { } export function useWorkspace(instanceId: string): UseWorkspaceReturn { + const { t } = useLanguage(); const [messages, setMessages] = useState([]); const [isProcessing, setIsProcessing] = useState(false); const [files, setFiles] = useState([]); @@ -364,6 +366,24 @@ export function useWorkspace(instanceId: string): UseWorkspaceReturn { ]); } }, + onRevealDownload: (event) => { + const data = event.item || event.data || {}; + const fileName = _triggerRevealDownload(data); + if (fileName) { + const count = data.placeholderCount ?? 0; + setMessages(prev => [ + ...prev, + { + id: `reveal-${Date.now()}`, + role: 'assistant', + message: t( + 'De-anonymisierte Datei "{name}" heruntergeladen ({count} Platzhalter aufgelöst). Nicht gespeichert.', + { name: fileName, count }, + ), + } as any, + ]); + } + }, onWorkflowUpdated: (event) => { if (event.workflowId) setWorkflowId(event.workflowId); setWorkflowVersion(v => v + 1); @@ -732,3 +752,32 @@ function _buildAudioUrl(event: SseEvent): string | null { return null; } } + +// Transient de-anonymized download: decode inline base64, trigger a browser download, +// and immediately revoke the object URL. The cleartext is never persisted client-side. +function _triggerRevealDownload(data: any): string | null { + const content = data?.content; + if (!content) return null; + const fileName = data.fileName || 'revealed.txt'; + const mimeType = data.mimeType || 'text/plain'; + try { + const byteChars = atob(content); + const byteArray = new Uint8Array(byteChars.length); + for (let i = 0; i < byteChars.length; i++) { + byteArray[i] = byteChars.charCodeAt(i); + } + const blob = new Blob([byteArray], { type: mimeType }); + const url = URL.createObjectURL(blob); + const anchor = document.createElement('a'); + anchor.href = url; + anchor.download = fileName; + document.body.appendChild(anchor); + anchor.click(); + document.body.removeChild(anchor); + URL.revokeObjectURL(url); + return fileName; + } catch (err) { + console.error('Failed to decode reveal download:', err); + return null; + } +} diff --git a/src/stores/featureStore.tsx b/src/stores/featureStore.tsx index cd75899..23af065 100644 --- a/src/stores/featureStore.tsx +++ b/src/stores/featureStore.tsx @@ -97,20 +97,6 @@ export const FeatureProvider: React.FC = ({ children }) => mandate.features.forEach(feature => { feature.instances.forEach(instance => { cache.set(instance.id, instance); - - // DEBUG: Log permissions for chatbot instances - if (instance.featureCode === 'chatbot') { - console.log('🔍 [DEBUG] Chatbot Instance Permissions (loadFeatures):', { - instanceId: instance.id, - instanceLabel: instance.instanceLabel, - featureCode: instance.featureCode, - userRoles: instance.userRoles, - permissions: instance.permissions, - views: instance.permissions?.views, - viewKeys: instance.permissions?.views ? Object.keys(instance.permissions.views) : [], - hasConversationsView: instance.permissions?.views?.['chatbot-conversations'] || instance.permissions?.views?.['ui.feature.chatbot.conversations'] || instance.permissions?.views?.['_all'], - }); - } }); }); }); @@ -144,20 +130,6 @@ export const FeatureProvider: React.FC = ({ children }) => mandate.features.forEach(feature => { feature.instances.forEach(instance => { cache.set(instance.id, instance); - - // DEBUG: Log permissions for chatbot instances - if (instance.featureCode === 'chatbot') { - console.log('🔍 [DEBUG] Chatbot Instance Permissions:', { - instanceId: instance.id, - instanceLabel: instance.instanceLabel, - featureCode: instance.featureCode, - userRoles: instance.userRoles, - permissions: instance.permissions, - views: instance.permissions?.views, - viewKeys: instance.permissions?.views ? Object.keys(instance.permissions.views) : [], - hasConversationsView: instance.permissions?.views?.['chatbot-conversations'] || instance.permissions?.views?.['ui.feature.chatbot.conversations'] || instance.permissions?.views?.['_all'], - }); - } }); }); }); diff --git a/src/styles/pages.module.css b/src/styles/pages.module.css index 90fd4cd..7be9624 100644 --- a/src/styles/pages.module.css +++ b/src/styles/pages.module.css @@ -317,52 +317,6 @@ height: 100%; } -/* Chatbot two-column layout */ -.chatbotTwoColumnLayout { - display: grid; - grid-template-columns: 1fr 3fr; - gap: 1rem; - width: 100%; - flex: 1; - min-height: 0; - overflow: hidden; - margin-bottom: 1rem; - height: 100%; - max-height: 100%; -} - -.chatbotHistoryColumn { - display: flex; - flex-direction: column; - min-height: 0; - overflow: hidden; - height: 100%; -} - -.chatbotChatColumn { - display: flex; - flex-direction: column; - min-height: 0; - overflow: hidden; - height: 100%; - gap: 1rem; -} - -.chatbotMessagesCell { - display: flex; - flex-direction: column; - flex: 1; - min-height: 0; - overflow: hidden; -} - -.chatbotInputCell { - display: flex; - flex-direction: column; - min-height: 0; - flex-shrink: 0; -} - .chatHistorySection { flex: 1; display: flex; @@ -690,13 +644,6 @@ grid-row: 2; } - .chatbotTwoColumnLayout { - grid-template-columns: 1fr; - height: auto; - max-height: none; - overflow: visible; - } - .dashboardInputGrid { grid-template-columns: 1fr; grid-template-rows: auto auto auto; diff --git a/src/types/mandate.ts b/src/types/mandate.ts index 7425e00..3ba7f92 100644 --- a/src/types/mandate.ts +++ b/src/types/mandate.ts @@ -73,7 +73,7 @@ export interface InstancePermissions { */ export interface FeatureInstance { id: string; // UUID der Instanz - featureCode: string; // "trustee", "chatbot", "chatworkflow", etc. + featureCode: string; // "trustee", "workspace", "chatworkflow", etc. mandateId: string; // Zugehöriger Mandant mandateName: string; // Kurzzeichen / Slug des Mandanten (audit-stable) mandateLabel?: string; // Voller Name des Mandanten (UI-Anzeige) — optional fuer Backwards-Compat @@ -91,7 +91,7 @@ export interface FeatureInstance { * Gruppiert alle Instanzen eines Feature-Typs */ export interface MandateFeature { - code: string; // "trustee", "chatbot", "chatworkflow", etc. + code: string; // "trustee", "workspace", "chatworkflow", etc. label: string; // German plaintext i18n key icon: string; // Material/React Icon Name instances: FeatureInstance[]; @@ -223,15 +223,6 @@ export const FEATURE_REGISTRY: Record = { { code: 'files', label: 'Dateien', path: 'files' }, ] }, - chatbot: { - code: 'chatbot', - label: 'Chatbot', - icon: 'chat', - views: [ - { code: 'conversations', label: 'Konversationen', path: 'conversations' }, - { code: 'settings', label: 'Einstellungen', path: 'settings' }, - ] - }, realestate: { code: 'realestate', label: 'Immobilien', diff --git a/src/utils/sseClient.ts b/src/utils/sseClient.ts index 8223918..e7a8204 100644 --- a/src/utils/sseClient.ts +++ b/src/utils/sseClient.ts @@ -27,6 +27,7 @@ export interface SseEventHandlers { onFileCreated?: (event: SseEvent) => void; onDataSourceAccess?: (event: SseEvent) => void; onVoiceResponse?: (event: SseEvent) => void; + onRevealDownload?: (event: SseEvent) => void; onWorkflowUpdated?: (event: SseEvent) => void; onComplete?: (event: SseEvent) => void; onStopped?: (event: SseEvent) => void; @@ -64,6 +65,7 @@ const _EVENT_ROUTER: Record = { fileCreated: 'onFileCreated', dataSourceAccess: 'onDataSourceAccess', voiceResponse: 'onVoiceResponse', + revealDownload: 'onRevealDownload', workflowUpdated: 'onWorkflowUpdated', complete: 'onComplete', stopped: 'onStopped', diff --git a/staticwebapp.config.json b/staticwebapp.config.json index 87325d4..abe16e7 100644 --- a/staticwebapp.config.json +++ b/staticwebapp.config.json @@ -1,9 +1,23 @@ { "navigationFallback": { "rewrite": "/index.html", - "exclude": ["/static/*", "/images/*", "/*.{png,jpg,gif,ico,json}"] + "exclude": ["/assets/*", "/static/*", "/images/*", "/*.{png,jpg,jpeg,gif,svg,ico,webp,json,js,css,map,woff,woff2,ttf}"] }, + "routes": [ + { + "route": "/assets/*", + "headers": { + "Cache-Control": "public, max-age=31536000, immutable" + } + }, + { + "route": "/*", + "headers": { + "Cache-Control": "no-cache, no-store, must-revalidate" + } + } + ], "mimeTypes": { - ".json": "text/json" + ".json": "application/json" } -} \ No newline at end of file +} diff --git a/work-around/chatbot.ts b/work-around/chatbot.ts deleted file mode 100644 index 68c7f76..0000000 --- a/work-around/chatbot.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { GenericPageData } from '../../pageInterface'; -import { LuMessageSquare } from 'react-icons/lu'; -import { IoMdSend } from 'react-icons/io'; -import { MdStop } from 'react-icons/md'; -import { createChatbotHook } from '../../../../hooks/useChatbot'; - -export const chatbotPageData: GenericPageData = { - id: 'start-chatbot', - path: 'start/chatbot', - name: 'Chatbot', - description: 'Simple chatbot interface for conversations', - - // Parent page - parentPath: 'start', - - // Visual - icon: LuMessageSquare, - title: 'Chatbot', - subtitle: 'Chat with AI assistant', - - // No header buttons (simpler than dashboard) - headerButtons: [], - - // Content sections - content: [ - { - id: 'chatbot-history', - type: 'chatHistory', - chatHistoryConfig: { - emptyMessage: 'No chat history yet. Start a conversation to see it here.' - } - }, - { - id: 'chatbot-messages', - type: 'messages', - messagesConfig: { - variant: 'chat', - showDocuments: true, - showMetadata: false, - showProgress: false, - emptyMessage: 'No messages yet. Start a conversation to see messages here.' - } - }, - { - id: 'chatbot-input', - type: 'inputForm', - inputFormConfig: { - hookFactory: createChatbotHook, - placeholder: 'Type your message here...', - buttonLabel: 'Send', - stopButtonLabel: 'Stop', - buttonIcon: IoMdSend, - stopButtonIcon: MdStop, - buttonVariant: 'primary', - stopButtonVariant: 'danger', - buttonSize: 'md', - textFieldSize: 'md', - showFileUpload: false - } - } - ], - - // Page behavior - persistent: true, - preserveState: true, - preload: true, - moduleEnabled: true, - - - - // Lifecycle hooks - onActivate: async () => { - if (import.meta.env.DEV) console.log('Chatbot activated - state preserved'); - }, - onDeactivate: async () => { - if (import.meta.env.DEV) console.log('Chatbot deactivated - keeping state'); - } -}; - From 7876a528f51c32f7bdd15ea26d0b3026cbc4bfd5 Mon Sep 17 00:00:00 2001 From: ValueOn AG Date: Wed, 3 Jun 2026 10:18:03 +0200 Subject: [PATCH 2/2] fixes deploy --- src/api/featuresApi.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/api/featuresApi.ts b/src/api/featuresApi.ts index 8a75d9f..7634b2e 100644 --- a/src/api/featuresApi.ts +++ b/src/api/featuresApi.ts @@ -14,8 +14,6 @@ import type { InstancePermissions, AccessLevel, } from '../types/mandate'; -import { mandateDisplayLabel } from '../utils/mandateDisplayUtils'; - // ============================================================================= // MOCK DATA (Temporär bis Backend bereit) // =============================================================================