fixed chatbot functionality
This commit is contained in:
parent
3388002330
commit
1342bdbcca
9 changed files with 548 additions and 46 deletions
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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>)}
|
||||
/>
|
||||
) : (
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
113
src/pages/admin/ChatbotConfigSection.tsx
Normal file
113
src/pages/admin/ChatbotConfigSection.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Reference in a new issue