fixed chatbot functionality

This commit is contained in:
Ida Dittrich 2026-01-30 14:04:56 +01:00
parent 3388002330
commit 1342bdbcca
9 changed files with 548 additions and 46 deletions

View file

@ -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) {

View file

@ -9,6 +9,7 @@ interface TextFieldProps extends BaseTextFieldProps {
step?: string;
min?: string | number;
max?: string | number;
rows?: number; // For textarea
onKeyDown?: (e: React.KeyboardEvent<HTMLInputElement | HTMLTextAreaElement>) => void;
}
@ -30,6 +31,7 @@ const TextField: React.FC<TextFieldProps> = ({
autoFocus = false,
name,
id,
rows,
...props
}) => {
const textareaRef = useRef<HTMLTextAreaElement>(null);
@ -95,7 +97,7 @@ const TextField: React.FC<TextFieldProps> = ({
autoFocus={autoFocus}
name={name}
id={id}
rows={1}
rows={rows ?? 1}
{...(props as React.TextareaHTMLAttributes<HTMLTextAreaElement>)}
/>
) : (

View file

@ -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<void>;
@ -66,6 +67,7 @@ export function useChatbot(): ChatbotHookReturn {
// Current workflow state
const [currentWorkflowId, setCurrentWorkflowId] = useState<string | null>(null);
const [isStreaming, setIsStreaming] = useState(false);
const [streamingStatus, setStreamingStatus] = useState<string | null>(null);
// Error state
const [error, setError] = useState<string | null>(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,

View file

@ -37,6 +37,7 @@ export interface FeatureInstance {
mandateId: string;
label: string;
enabled: boolean;
config?: Record<string, any>; // Instance-specific configuration (JSONB)
}
export interface FeatureAccess {
@ -76,6 +77,7 @@ export interface FeatureInstanceCreate {
label: string;
enabled?: boolean;
copyTemplateRoles?: boolean;
config?: Record<string, any>; // 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<string, any> }
): Promise<{ success: boolean; data?: FeatureInstance; error?: string }> => {
setLoading(true);
setError(null);

View file

@ -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;

View file

@ -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<string | null>(null);
const [backendAttributes, setBackendAttributes] = useState<AttributeDefinition[]>([]);
// Chatbot configuration state
const [createFeatureCode, setCreateFeatureCode] = useState<string>('');
const [createLabel, setCreateLabel] = useState<string>(''); // Label field value
const [chatbotConnectors, setChatbotConnectors] = useState<string[]>(['preprocessor']); // Array for multiselect (database connectors only)
const [chatbotSystemPrompt, setChatbotSystemPrompt] = useState<string>('');
const [chatbotEnableWebResearch, setChatbotEnableWebResearch] = useState<boolean>(true); // Enable Tavily web research
// Ref to track form data for featureCode detection
const formDataRef = useRef<Record<string, any>>({});
// 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<string, any> | 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<string, any>) => {
// 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<string, any> | 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 = () => {
<span>Lade Formular...</span>
</div>
) : (
<FormGeneratorForm
attributes={createFields}
mode="create"
onSubmit={handleCreateInstance}
onCancel={() => setShowCreateModal(false)}
submitButtonText="Erstellen"
cancelButtonText="Abbrechen"
/>
<div>
{/* Feature Code Selector - Required for chatbot config */}
<div className={styles.configField} style={{ marginBottom: '1.5rem', paddingBottom: '1rem', borderBottom: '1px solid var(--border-color)' }}>
<label className={styles.configLabel} style={{ fontWeight: 600 }}>
Feature auswählen: <span style={{ color: 'var(--error-color)' }}>*</span>
</label>
<DropdownSelect
items={features.map(f => ({
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 && (
<p style={{ fontSize: '0.75rem', color: 'var(--text-secondary)', marginTop: '0.5rem' }}>
Bitte wählen Sie ein Feature aus, um fortzufahren.
</p>
)}
</div>
{/* Chatbot Configuration Title - Show when chatbot is selected */}
{createFeatureCode === 'chatbot' && (
<h3 className={styles.configSectionTitle} style={{ marginTop: '1.5rem', marginBottom: '1.5rem' }}>
Chatbot-Konfiguration
</h3>
)}
{/* Label Field - Always shown after title */}
{createFeatureCode && (
<div className={styles.configField} style={{ marginBottom: '1.5rem' }}>
<label className={styles.configLabel}>
Label: <span style={{ color: 'var(--error-color)' }}>*</span>
</label>
<TextField
type="text"
value={createLabel}
onChange={(value) => setCreateLabel(value)}
placeholder="Instanz-Bezeichnung eingeben..."
className={styles.configSelect}
size="md"
required={true}
/>
</div>
)}
{/* Chatbot Configuration Section - Show when chatbot is selected */}
{createFeatureCode === 'chatbot' && (
<ChatbotConfigSection
connectors={chatbotConnectors}
systemPrompt={chatbotSystemPrompt}
enableWebResearch={chatbotEnableWebResearch}
onConnectorsChange={setChatbotConnectors}
onSystemPromptChange={setChatbotSystemPrompt}
onEnableWebResearchChange={setChatbotEnableWebResearch}
/>
)}
{/* Main Form - Only show if featureCode is selected */}
{createFeatureCode && (
<div style={{ marginTop: '1.5rem' }}>
<FormGeneratorForm
attributes={createFields}
mode="create"
onSubmit={handleFormSubmit}
onCancel={() => {
setShowCreateModal(false);
setCreateFeatureCode('');
setCreateLabel('');
formDataRef.current = {};
setChatbotConnectors(['preprocessor']);
setChatbotSystemPrompt('');
setChatbotEnableWebResearch(true);
}}
submitButtonText="Erstellen"
cancelButtonText="Abbrechen"
/>
</div>
)}
</div>
)}
</div>
</div>
@ -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' && (
<ChatbotConfigSection
connectors={chatbotConnectors}
systemPrompt={chatbotSystemPrompt}
enableWebResearch={chatbotEnableWebResearch}
onConnectorsChange={setChatbotConnectors}
onSystemPromptChange={setChatbotSystemPrompt}
onEnableWebResearchChange={setChatbotEnableWebResearch}
/>
)}
</div>
</div>
</div>

View file

@ -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<ChatbotConfigSectionProps> = ({
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 (
<div className={styles.chatbotConfigSection}>
<div className={styles.configField}>
<label className={styles.configLabel}>
Connector:
</label>
<div className={styles.multiselectContainer}>
{availableConnectors.map(connector => {
const isSelected = connectors.includes(connector.value);
return (
<label key={connector.id} className={styles.multiselectOption}>
<input
type="checkbox"
checked={isSelected}
onChange={() => handleConnectorToggle(connector.value)}
className={styles.multiselectCheckbox}
/>
<span className={styles.multiselectLabel}>{connector.label}</span>
</label>
);
})}
</div>
{connectors.length === 0 && (
<p style={{ fontSize: '0.75rem', color: 'var(--text-secondary)', marginTop: '0.5rem' }}>
Bitte wählen Sie mindestens einen Connector aus.
</p>
)}
</div>
<div className={styles.configField}>
<label className={styles.configLabel} style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
<input
type="checkbox"
checked={enableWebResearch}
onChange={(e) => onEnableWebResearchChange(e.target.checked)}
className={styles.multiselectCheckbox}
/>
<span>Web Research aktivieren (Tavily)</span>
</label>
<p className={styles.configHelpText}>
Wenn aktiviert, führt der Chatbot zusätzlich Web-Recherchen mit Tavily durch, um aktuelle Informationen aus dem Internet zu finden.
</p>
</div>
<div className={styles.configField}>
<label className={styles.configLabel}>
System Prompt: <span style={{ color: 'var(--error-color)' }}>*</span>
</label>
<TextField
type="text"
value={systemPrompt}
onChange={onSystemPromptChange}
placeholder="Benutzerdefinierter System-Prompt für den Chatbot..."
className={styles.configTextArea}
size="md"
rows={6}
required={true}
/>
<p className={styles.configHelpText}>
Dieser Prompt wird für Analyse und Antwort-Generierung verwendet (erforderlich).
Platzhalter: {'{userPrompt}'}, {'{context}'}, {'{db_results_part}'}, {'{web_results_part}'}
</p>
</div>
</div>
);
};

View file

@ -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<HTMLInputElement | HTMLTextAreaElement>) => {
// 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 && (
<div className={styles.typingIndicator}>
<div className={styles.typingBubble}>
<div className={styles.typingDots}>
<span></span>
<span></span>
<span></span>
</div>
{streamingStatus ? (
<div className={styles.streamingStatus}>
<div className={styles.statusSpinner} />
<span className={styles.statusText}>{streamingStatus}</span>
</div>
) : (
<div className={styles.typingDots}>
<span></span>
<span></span>
<span></span>
</div>
)}
</div>
</div>
)}
@ -213,6 +239,7 @@ export const ChatbotConversationsView: React.FC = () => {
<TextField
value={inputValue}
onChange={setInputValue}
onKeyDown={handleKeyDown}
placeholder="Nachricht eingeben..."
disabled={isStreaming}
className={styles.inputField}

View file

@ -228,6 +228,42 @@
}
}
/* =============================================================================
* 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);