diff --git a/src/api/chatbotApi.ts b/src/api/chatbotApi.ts index 23e887e..81d57a4 100644 --- a/src/api/chatbotApi.ts +++ b/src/api/chatbotApi.ts @@ -40,9 +40,10 @@ export interface StartChatbotResponse extends ChatbotWorkflow { } export interface ChatDataItem { - type: 'message' | 'log' | 'stat' | 'document' | 'stopped'; + type: 'message' | 'log' | 'stat' | 'document' | 'stopped' | 'status'; createdAt: number; item: Message | any; + label?: string; // For status events } // Type for the request function passed to API functions @@ -155,6 +156,7 @@ export async function startChatbotStreamApi( 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) { diff --git a/src/components/UiComponents/TextField/TextField.tsx b/src/components/UiComponents/TextField/TextField.tsx index b0cfcce..7aaa63c 100644 --- a/src/components/UiComponents/TextField/TextField.tsx +++ b/src/components/UiComponents/TextField/TextField.tsx @@ -9,6 +9,7 @@ interface TextFieldProps extends BaseTextFieldProps { step?: string; min?: string | number; max?: string | number; + rows?: number; // For textarea onKeyDown?: (e: React.KeyboardEvent) => void; } @@ -30,6 +31,7 @@ const TextField: React.FC = ({ autoFocus = false, name, id, + rows, ...props }) => { const textareaRef = useRef(null); @@ -95,7 +97,7 @@ const TextField: React.FC = ({ autoFocus={autoFocus} name={name} id={id} - rows={1} + rows={rows ?? 1} {...(props as React.TextareaHTMLAttributes)} /> ) : ( diff --git a/src/hooks/useChatbot.ts b/src/hooks/useChatbot.ts index f2d06ed..c4db9f8 100644 --- a/src/hooks/useChatbot.ts +++ b/src/hooks/useChatbot.ts @@ -33,6 +33,7 @@ export interface ChatbotHookReturn { // Current workflow state currentWorkflowId: string | null; isStreaming: boolean; + streamingStatus: string | null; // Current streaming status message // Actions selectThread: (workflowId: string) => Promise; @@ -66,6 +67,7 @@ export function useChatbot(): ChatbotHookReturn { // 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); @@ -159,6 +161,7 @@ export function useChatbot(): ChatbotHookReturn { setError(null); setIsStreaming(true); + setStreamingStatus(null); // Reset status // Store the input message content to track duplicates const inputMessageContent = input.trim(); @@ -196,6 +199,15 @@ export function useChatbot(): ChatbotHookReturn { 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; } @@ -297,6 +309,7 @@ export function useChatbot(): ChatbotHookReturn { () => { if (isMountedRef.current) { setIsStreaming(false); + setStreamingStatus(null); // Clear status on completion // Refresh threads to get updated list refreshThreads(); } @@ -376,21 +389,27 @@ export function useChatbot(): ChatbotHookReturn { 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); - // If deleted thread was selected, clear selection - if (selectedThreadId === workflowId) { - createNewThread(); - } - - // Refresh threads list + // 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, createNewThread, refreshThreads]); + }, [request, instanceId, selectedThreadId, threads, createNewThread, refreshThreads]); // Initial load useEffect(() => { @@ -408,6 +427,7 @@ export function useChatbot(): ChatbotHookReturn { loadingMessages, currentWorkflowId, isStreaming, + streamingStatus, selectThread, createNewThread, sendMessage, diff --git a/src/hooks/useFeatureAccess.ts b/src/hooks/useFeatureAccess.ts index 439936c..a8b8cd7 100644 --- a/src/hooks/useFeatureAccess.ts +++ b/src/hooks/useFeatureAccess.ts @@ -37,6 +37,7 @@ export interface FeatureInstance { mandateId: string; label: string; enabled: boolean; + config?: Record; // Instance-specific configuration (JSONB) } export interface FeatureAccess { @@ -76,6 +77,7 @@ export interface FeatureInstanceCreate { label: string; enabled?: boolean; copyTemplateRoles?: boolean; + config?: Record; // Instance-specific configuration (JSONB) } /** @@ -213,12 +215,12 @@ export function useFeatureAccess() { }, []); /** - * Update a feature instance (label, enabled) + * Update a feature instance (label, enabled, config) */ const updateInstance = useCallback(async ( mandateId: string, instanceId: string, - data: { label?: string; enabled?: boolean } + data: { label?: string; enabled?: boolean; config?: Record } ): Promise<{ success: boolean; data?: FeatureInstance; error?: string }> => { setLoading(true); setError(null); diff --git a/src/pages/admin/Admin.module.css b/src/pages/admin/Admin.module.css index b6a3b01..50e63ef 100644 --- a/src/pages/admin/Admin.module.css +++ b/src/pages/admin/Admin.module.css @@ -699,6 +699,79 @@ border-color: var(--primary-color); } +/* ============================================== */ +/* Chatbot Configuration Styles */ +/* ============================================== */ + +.chatbotConfigSection { + margin-top: 0; +} + +.configSectionTitle { + font-size: 1.125rem; + font-weight: 600; + color: var(--text-primary); + margin: 0 0 1.5rem 0; +} + +.configField { + margin-bottom: 1.5rem; +} + +.configLabel { + display: block; + font-size: 0.875rem; + font-weight: 500; + color: var(--text-primary); + margin-bottom: 0.5rem; +} + +.configSelect { + width: 100%; + max-width: 400px; +} + +.configTextArea { + width: 100%; + min-height: 150px; + font-family: 'Courier New', monospace; + font-size: 0.875rem; + line-height: 1.5; + resize: vertical; +} + +.configHelpText { + font-size: 0.75rem; + color: var(--text-secondary); + margin-top: 0.5rem; + font-style: italic; +} + +/* Multiselect styles for chatbot connectors */ +.multiselectContainer { + padding: 0.75rem; + border: 1px solid var(--border-color); + border-radius: 6px; +} + +.multiselectOption { + display: flex; + align-items: center; + gap: 0.5rem; + cursor: pointer; +} + +.multiselectCheckbox { + width: 18px; + height: 18px; + cursor: pointer; +} + +.multiselectLabel { + font-size: 0.875rem; + user-select: none; +} + .rolesList { display: flex; flex-direction: column; diff --git a/src/pages/admin/AdminFeatureAccessPage.tsx b/src/pages/admin/AdminFeatureAccessPage.tsx index ee9546d..682e171 100644 --- a/src/pages/admin/AdminFeatureAccessPage.tsx +++ b/src/pages/admin/AdminFeatureAccessPage.tsx @@ -5,7 +5,7 @@ * Allows creating, viewing, and managing feature instances. */ -import React, { useState, useEffect, useMemo } from 'react'; +import React, { useState, useEffect, useMemo, useRef } from 'react'; import { useFeatureAccess, type FeatureInstance } from '../../hooks/useFeatureAccess'; import { useUserMandates, type Mandate } from '../../hooks/useUserMandates'; import { FormGeneratorTable } from '../../components/FormGenerator/FormGeneratorTable'; @@ -13,6 +13,9 @@ import { FormGeneratorForm, type AttributeDefinition } from '../../components/Fo import { FaPlus, FaSync, FaCube, FaBuilding, FaCogs, FaEdit } from 'react-icons/fa'; import { useToast } from '../../contexts/ToastContext'; import api from '../../api'; +import { ChatbotConfigSection } from './ChatbotConfigSection'; +import { DropdownSelect } from '../../components/UiComponents/DropdownSelect'; +import { TextField } from '../../components/UiComponents/TextField'; import styles from './Admin.module.css'; export const AdminFeatureAccessPage: React.FC = () => { @@ -42,6 +45,16 @@ export const AdminFeatureAccessPage: React.FC = () => { const [, setIsSubmitting] = useState(false); const [syncingInstance, setSyncingInstance] = useState(null); const [backendAttributes, setBackendAttributes] = useState([]); + + // Chatbot configuration 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 + + // Ref to track form data for featureCode detection + const formDataRef = useRef>({}); // Load features, mandates, and attributes on mount useEffect(() => { @@ -80,41 +93,85 @@ export const AdminFeatureAccessPage: React.FC = () => { ], [features]); // Form attributes from backend - merge with dynamic feature options + // Exclude featureCode, config, and label since we handle them separately const createFields: AttributeDefinition[] = useMemo(() => { - const excludedFields = ['id', 'mandateId']; - const featureOptions = features.map(f => ({ - value: f.code, - label: typeof f.label === 'object' - ? (f.label.de || f.label.en || f.code) - : (f.label || f.code) - })); + const excludedFields = ['id', 'mandateId', 'featureCode', 'config', 'label']; // Exclude featureCode, config, and label - handled separately return backendAttributes .filter(attr => !excludedFields.includes(attr.name)) .map(attr => ({ ...attr, - // Override featureCode: make editable for create and add dynamic options - readonly: attr.name === 'featureCode' ? false : attr.readonly, - editable: attr.name === 'featureCode' || attr.name === 'enabled' ? true : attr.editable, - options: attr.name === 'featureCode' ? featureOptions : attr.options, + editable: attr.name === 'enabled' ? true : attr.editable, })) as AttributeDefinition[]; - }, [features, backendAttributes]); + }, [backendAttributes]); // Handle create instance - const handleCreateInstance = async (data: { featureCode: string; label: string; enabled?: boolean; copyTemplateRoles?: boolean }) => { + const handleCreateInstance = async (data: { featureCode: string; enabled?: boolean; copyTemplateRoles?: boolean }) => { if (!selectedMandateId) return; setIsSubmitting(true); try { + // Validate label + if (!createLabel || createLabel.trim() === '') { + showError('Fehler', 'Label ist erforderlich.'); + setIsSubmitting(false); + return; + } + + // Build config for chatbot instances + let config: Record | undefined = undefined; + if (createFeatureCode === 'chatbot') { + // Validate required fields + if (!chatbotSystemPrompt || chatbotSystemPrompt.trim() === '') { + showError('Fehler', 'System Prompt ist erforderlich für Chatbot-Instanzen.'); + 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'; + config = { + connector: { + types: chatbotConnectors.length > 0 ? chatbotConnectors : ['preprocessor'], // Array of selected connectors + type: primaryConnector, // Primary connector (for backward compatibility) + customConnectorClass: null + }, + prompts: { + useCustomPrompts: true, // Always true since system prompt is required + customAnalysisPrompt: chatbotSystemPrompt, + customFinalAnswerPrompt: chatbotSystemPrompt + }, + behavior: { + maxQueries: 5, + enableWebResearch: chatbotEnableWebResearch, + enableRetryOnEmpty: true, + maxRetryAttempts: 2 + } + }; + } + const result = await createInstance(selectedMandateId, { - featureCode: data.featureCode, - label: data.label, + featureCode: createFeatureCode, + label: createLabel, enabled: data.enabled !== false, - copyTemplateRoles: data.copyTemplateRoles !== false + copyTemplateRoles: data.copyTemplateRoles !== false, + config: config }); if (result.success) { setShowCreateModal(false); + setCreateFeatureCode(''); + setCreateLabel(''); + formDataRef.current = {}; + setChatbotConnectors(['preprocessor']); + setChatbotSystemPrompt(''); + setChatbotEnableWebResearch(true); fetchInstances(selectedMandateId); - showSuccess('Feature-Instanz erstellt', `Die Instanz "${data.label}" wurde erfolgreich erstellt.`); + showSuccess('Feature-Instanz erstellt', `Die Instanz "${createLabel}" wurde erfolgreich erstellt.`); } else { showError('Fehler', result.error || 'Fehler beim Erstellen der Feature-Instanz'); } @@ -122,10 +179,34 @@ export const AdminFeatureAccessPage: React.FC = () => { setIsSubmitting(false); } }; + + // Wrapper for form submission that includes featureCode from selector + const handleFormSubmit = (data: Record) => { + // Use label from state and featureCode from selector + handleCreateInstance({ + featureCode: createFeatureCode, + ...(data as { enabled?: boolean; copyTemplateRoles?: boolean }) + }); + }; // 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.length > 0 ? connectorTypes : ['preprocessor']); + setChatbotSystemPrompt(config?.prompts?.customAnalysisPrompt || config?.prompts?.customFinalAnswerPrompt || ''); + setChatbotEnableWebResearch(config?.behavior?.enableWebResearch !== false); // Default to true if not set + } else { + setChatbotConnectors(['preprocessor']); + setChatbotSystemPrompt(''); + setChatbotEnableWebResearch(true); + } setShowEditModal(true); }; @@ -134,13 +215,57 @@ 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('Fehler', 'System Prompt ist erforderlich für Chatbot-Instanzen.'); + 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'; + config = { + ...existingConfig, + connector: { + types: chatbotConnectors.length > 0 ? chatbotConnectors : ['preprocessor'], // Array of selected connectors + type: primaryConnector, // Primary connector (for backward compatibility) + customConnectorClass: existingConfig.connector?.customConnectorClass || null + }, + 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 + } + }; + } + const result = await updateInstance(selectedMandateId, editingInstance.id, { label: data.label, - enabled: data.enabled + enabled: data.enabled, + config: config }); if (result.success) { setShowEditModal(false); setEditingInstance(null); + setChatbotConnectors(['preprocessor']); + setChatbotSystemPrompt(''); fetchInstances(selectedMandateId); showSuccess('Feature-Instanz aktualisiert', `Die Instanz "${data.label}" wurde erfolgreich aktualisiert.`); } else { @@ -375,14 +500,98 @@ export const AdminFeatureAccessPage: React.FC = () => { Lade Formular... ) : ( - setShowCreateModal(false)} - submitButtonText="Erstellen" - cancelButtonText="Abbrechen" - /> +
+ {/* Feature Code Selector - Required for chatbot config */} +
+ + ({ + id: f.code, + label: typeof f.label === 'object' + ? (f.label.de || f.label.en || f.code) + : (f.label || f.code), + value: f.code + }))} + selectedItemId={createFeatureCode} + onSelect={(item) => { + const selectedCode = item?.value || ''; + setCreateFeatureCode(selectedCode); + // Reset chatbot config when switching + setChatbotConnectors(['preprocessor']); + setChatbotSystemPrompt(''); + setChatbotEnableWebResearch(true); + }} + placeholder="Feature auswählen (erforderlich)" + className={styles.configSelect} + /> + {!createFeatureCode && ( +

+ Bitte wählen Sie ein Feature aus, um fortzufahren. +

+ )} +
+ + {/* Chatbot Configuration Title - Show when chatbot is selected */} + {createFeatureCode === 'chatbot' && ( +

+ Chatbot-Konfiguration +

+ )} + + {/* Label Field - Always shown after title */} + {createFeatureCode && ( +
+ + setCreateLabel(value)} + placeholder="Instanz-Bezeichnung eingeben..." + className={styles.configSelect} + size="md" + required={true} + /> +
+ )} + + {/* Chatbot Configuration Section - Show when chatbot is selected */} + {createFeatureCode === 'chatbot' && ( + + )} + + {/* Main Form - Only show if featureCode is selected */} + {createFeatureCode && ( +
+ { + setShowCreateModal(false); + setCreateFeatureCode(''); + setCreateLabel(''); + formDataRef.current = {}; + setChatbotConnectors(['preprocessor']); + setChatbotSystemPrompt(''); + setChatbotEnableWebResearch(true); + }} + submitButtonText="Erstellen" + cancelButtonText="Abbrechen" + /> +
+ )} +
)} @@ -423,10 +632,28 @@ export const AdminFeatureAccessPage: React.FC = () => { data={editingInstance} mode="edit" onSubmit={handleUpdateInstance} - onCancel={() => { setShowEditModal(false); setEditingInstance(null); }} + onCancel={() => { + setShowEditModal(false); + setEditingInstance(null); + setChatbotConnectors(['preprocessor']); + setChatbotSystemPrompt(''); + setChatbotEnableWebResearch(true); + }} submitButtonText="Speichern" cancelButtonText="Abbrechen" /> + + {/* Chatbot Configuration Section */} + {editingInstance?.featureCode === 'chatbot' && ( + + )} diff --git a/src/pages/admin/ChatbotConfigSection.tsx b/src/pages/admin/ChatbotConfigSection.tsx new file mode 100644 index 0000000..e03b634 --- /dev/null +++ b/src/pages/admin/ChatbotConfigSection.tsx @@ -0,0 +1,113 @@ +/** + * ChatbotConfigSection Component + * + * Displays chatbot-specific configuration fields (connector, system prompt) + * Only shown when featureCode is "chatbot" + */ + +import React from 'react'; +import { TextField } from '../../components/UiComponents/TextField'; +import styles from './Admin.module.css'; + +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 + onConnectorsChange: (connectors: string[]) => void; + onSystemPromptChange: (prompt: string) => void; + onEnableWebResearchChange: (enabled: boolean) => void; +} + +export const ChatbotConfigSection: React.FC = ({ + connectors, + systemPrompt, + enableWebResearch, + onConnectorsChange, + onSystemPromptChange, + onEnableWebResearchChange +}) => { + 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]); + } + }; + + return ( +
+
+ +
+ {availableConnectors.map(connector => { + const isSelected = connectors.includes(connector.value); + return ( + + ); + })} +
+ {connectors.length === 0 && ( +

+ Bitte wählen Sie mindestens einen Connector aus. +

+ )} +
+ +
+ +

+ Wenn aktiviert, führt der Chatbot zusätzlich Web-Recherchen mit Tavily durch, um aktuelle Informationen aus dem Internet zu finden. +

+
+ +
+ + +

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

+
+
+ ); +}; diff --git a/src/pages/views/chatbot/ChatbotConversationsView.tsx b/src/pages/views/chatbot/ChatbotConversationsView.tsx index 4deeb01..cfbfa32 100644 --- a/src/pages/views/chatbot/ChatbotConversationsView.tsx +++ b/src/pages/views/chatbot/ChatbotConversationsView.tsx @@ -11,6 +11,7 @@ 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'; @@ -26,6 +27,7 @@ export const ChatbotConversationsView: React.FC = () => { messages, loadingMessages, isStreaming, + streamingStatus, currentWorkflowId, selectThread, createNewThread, @@ -45,6 +47,15 @@ export const ChatbotConversationsView: React.FC = () => { 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, @@ -79,7 +90,9 @@ export const ChatbotConversationsView: React.FC = () => { const formatDate = (timestamp?: number) => { if (!timestamp) return ''; - const date = new Date(timestamp); + // 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); @@ -91,7 +104,13 @@ export const ChatbotConversationsView: React.FC = () => { if (diffHours < 24) return `Vor ${diffHours} Std`; if (diffDays < 7) return `Vor ${diffDays} Tagen`; - return date.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' }); + // 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) => { @@ -195,11 +214,18 @@ export const ChatbotConversationsView: React.FC = () => { {isStreaming && (
-
- - - -
+ {streamingStatus ? ( +
+
+ {streamingStatus} +
+ ) : ( +
+ + + +
+ )}
)} @@ -213,6 +239,7 @@ export const ChatbotConversationsView: React.FC = () => {