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 {
|
export interface ChatDataItem {
|
||||||
type: 'message' | 'log' | 'stat' | 'document' | 'stopped';
|
type: 'message' | 'log' | 'stat' | 'document' | 'stopped' | 'status';
|
||||||
createdAt: number;
|
createdAt: number;
|
||||||
item: Message | any;
|
item: Message | any;
|
||||||
|
label?: string; // For status events
|
||||||
}
|
}
|
||||||
|
|
||||||
// Type for the request function passed to API functions
|
// 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
|
const jsonStr = line.slice(6); // Remove 'data: ' prefix
|
||||||
if (jsonStr.trim()) {
|
if (jsonStr.trim()) {
|
||||||
const item: ChatDataItem = JSON.parse(jsonStr);
|
const item: ChatDataItem = JSON.parse(jsonStr);
|
||||||
|
console.log('[SSE] Received event:', item.type, item);
|
||||||
onEvent(item);
|
onEvent(item);
|
||||||
}
|
}
|
||||||
} catch (parseError) {
|
} catch (parseError) {
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ interface TextFieldProps extends BaseTextFieldProps {
|
||||||
step?: string;
|
step?: string;
|
||||||
min?: string | number;
|
min?: string | number;
|
||||||
max?: string | number;
|
max?: string | number;
|
||||||
|
rows?: number; // For textarea
|
||||||
onKeyDown?: (e: React.KeyboardEvent<HTMLInputElement | HTMLTextAreaElement>) => void;
|
onKeyDown?: (e: React.KeyboardEvent<HTMLInputElement | HTMLTextAreaElement>) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -30,6 +31,7 @@ const TextField: React.FC<TextFieldProps> = ({
|
||||||
autoFocus = false,
|
autoFocus = false,
|
||||||
name,
|
name,
|
||||||
id,
|
id,
|
||||||
|
rows,
|
||||||
...props
|
...props
|
||||||
}) => {
|
}) => {
|
||||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||||
|
|
@ -95,7 +97,7 @@ const TextField: React.FC<TextFieldProps> = ({
|
||||||
autoFocus={autoFocus}
|
autoFocus={autoFocus}
|
||||||
name={name}
|
name={name}
|
||||||
id={id}
|
id={id}
|
||||||
rows={1}
|
rows={rows ?? 1}
|
||||||
{...(props as React.TextareaHTMLAttributes<HTMLTextAreaElement>)}
|
{...(props as React.TextareaHTMLAttributes<HTMLTextAreaElement>)}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@ export interface ChatbotHookReturn {
|
||||||
// Current workflow state
|
// Current workflow state
|
||||||
currentWorkflowId: string | null;
|
currentWorkflowId: string | null;
|
||||||
isStreaming: boolean;
|
isStreaming: boolean;
|
||||||
|
streamingStatus: string | null; // Current streaming status message
|
||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
selectThread: (workflowId: string) => Promise<void>;
|
selectThread: (workflowId: string) => Promise<void>;
|
||||||
|
|
@ -66,6 +67,7 @@ export function useChatbot(): ChatbotHookReturn {
|
||||||
// Current workflow state
|
// Current workflow state
|
||||||
const [currentWorkflowId, setCurrentWorkflowId] = useState<string | null>(null);
|
const [currentWorkflowId, setCurrentWorkflowId] = useState<string | null>(null);
|
||||||
const [isStreaming, setIsStreaming] = useState(false);
|
const [isStreaming, setIsStreaming] = useState(false);
|
||||||
|
const [streamingStatus, setStreamingStatus] = useState<string | null>(null);
|
||||||
|
|
||||||
// Error state
|
// Error state
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
@ -159,6 +161,7 @@ export function useChatbot(): ChatbotHookReturn {
|
||||||
|
|
||||||
setError(null);
|
setError(null);
|
||||||
setIsStreaming(true);
|
setIsStreaming(true);
|
||||||
|
setStreamingStatus(null); // Reset status
|
||||||
|
|
||||||
// Store the input message content to track duplicates
|
// Store the input message content to track duplicates
|
||||||
const inputMessageContent = input.trim();
|
const inputMessageContent = input.trim();
|
||||||
|
|
@ -196,6 +199,15 @@ export function useChatbot(): ChatbotHookReturn {
|
||||||
if (item.type === 'stopped') {
|
if (item.type === 'stopped') {
|
||||||
console.log('Received stopped event from backend');
|
console.log('Received stopped event from backend');
|
||||||
setIsStreaming(false);
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -297,6 +309,7 @@ export function useChatbot(): ChatbotHookReturn {
|
||||||
() => {
|
() => {
|
||||||
if (isMountedRef.current) {
|
if (isMountedRef.current) {
|
||||||
setIsStreaming(false);
|
setIsStreaming(false);
|
||||||
|
setStreamingStatus(null); // Clear status on completion
|
||||||
// Refresh threads to get updated list
|
// Refresh threads to get updated list
|
||||||
refreshThreads();
|
refreshThreads();
|
||||||
}
|
}
|
||||||
|
|
@ -376,21 +389,27 @@ export function useChatbot(): ChatbotHookReturn {
|
||||||
const deleteThread = useCallback(async (workflowId: string) => {
|
const deleteThread = useCallback(async (workflowId: string) => {
|
||||||
if (!instanceId) return;
|
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 {
|
try {
|
||||||
await deleteChatbotWorkflowApi(request, instanceId, workflowId);
|
await deleteChatbotWorkflowApi(request, instanceId, workflowId);
|
||||||
|
|
||||||
// If deleted thread was selected, clear selection
|
// Refresh threads list to sync with server
|
||||||
if (selectedThreadId === workflowId) {
|
|
||||||
createNewThread();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Refresh threads list
|
|
||||||
await refreshThreads();
|
await refreshThreads();
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error('Error deleting thread:', err);
|
console.error('Error deleting thread:', err);
|
||||||
|
// Restore threads on error
|
||||||
|
setThreads(previousThreads);
|
||||||
setError(err.message || 'Fehler beim Löschen der Konversation');
|
setError(err.message || 'Fehler beim Löschen der Konversation');
|
||||||
}
|
}
|
||||||
}, [request, instanceId, selectedThreadId, createNewThread, refreshThreads]);
|
}, [request, instanceId, selectedThreadId, threads, createNewThread, refreshThreads]);
|
||||||
|
|
||||||
// Initial load
|
// Initial load
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -408,6 +427,7 @@ export function useChatbot(): ChatbotHookReturn {
|
||||||
loadingMessages,
|
loadingMessages,
|
||||||
currentWorkflowId,
|
currentWorkflowId,
|
||||||
isStreaming,
|
isStreaming,
|
||||||
|
streamingStatus,
|
||||||
selectThread,
|
selectThread,
|
||||||
createNewThread,
|
createNewThread,
|
||||||
sendMessage,
|
sendMessage,
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,7 @@ export interface FeatureInstance {
|
||||||
mandateId: string;
|
mandateId: string;
|
||||||
label: string;
|
label: string;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
|
config?: Record<string, any>; // Instance-specific configuration (JSONB)
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FeatureAccess {
|
export interface FeatureAccess {
|
||||||
|
|
@ -76,6 +77,7 @@ export interface FeatureInstanceCreate {
|
||||||
label: string;
|
label: string;
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
copyTemplateRoles?: 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 (
|
const updateInstance = useCallback(async (
|
||||||
mandateId: string,
|
mandateId: string,
|
||||||
instanceId: string,
|
instanceId: string,
|
||||||
data: { label?: string; enabled?: boolean }
|
data: { label?: string; enabled?: boolean; config?: Record<string, any> }
|
||||||
): Promise<{ success: boolean; data?: FeatureInstance; error?: string }> => {
|
): Promise<{ success: boolean; data?: FeatureInstance; error?: string }> => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
|
||||||
|
|
@ -699,6 +699,79 @@
|
||||||
border-color: var(--primary-color);
|
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 {
|
.rolesList {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
* Allows creating, viewing, and managing feature instances.
|
* 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 { useFeatureAccess, type FeatureInstance } from '../../hooks/useFeatureAccess';
|
||||||
import { useUserMandates, type Mandate } from '../../hooks/useUserMandates';
|
import { useUserMandates, type Mandate } from '../../hooks/useUserMandates';
|
||||||
import { FormGeneratorTable } from '../../components/FormGenerator/FormGeneratorTable';
|
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 { FaPlus, FaSync, FaCube, FaBuilding, FaCogs, FaEdit } from 'react-icons/fa';
|
||||||
import { useToast } from '../../contexts/ToastContext';
|
import { useToast } from '../../contexts/ToastContext';
|
||||||
import api from '../../api';
|
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';
|
import styles from './Admin.module.css';
|
||||||
|
|
||||||
export const AdminFeatureAccessPage: React.FC = () => {
|
export const AdminFeatureAccessPage: React.FC = () => {
|
||||||
|
|
@ -42,6 +45,16 @@ export const AdminFeatureAccessPage: React.FC = () => {
|
||||||
const [, setIsSubmitting] = useState(false);
|
const [, setIsSubmitting] = useState(false);
|
||||||
const [syncingInstance, setSyncingInstance] = useState<string | null>(null);
|
const [syncingInstance, setSyncingInstance] = useState<string | null>(null);
|
||||||
const [backendAttributes, setBackendAttributes] = useState<AttributeDefinition[]>([]);
|
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
|
// Load features, mandates, and attributes on mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -80,41 +93,85 @@ export const AdminFeatureAccessPage: React.FC = () => {
|
||||||
], [features]);
|
], [features]);
|
||||||
|
|
||||||
// Form attributes from backend - merge with dynamic feature options
|
// Form attributes from backend - merge with dynamic feature options
|
||||||
|
// Exclude featureCode, config, and label since we handle them separately
|
||||||
const createFields: AttributeDefinition[] = useMemo(() => {
|
const createFields: AttributeDefinition[] = useMemo(() => {
|
||||||
const excludedFields = ['id', 'mandateId'];
|
const excludedFields = ['id', 'mandateId', 'featureCode', 'config', 'label']; // Exclude featureCode, config, and label - handled separately
|
||||||
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)
|
|
||||||
}));
|
|
||||||
|
|
||||||
return backendAttributes
|
return backendAttributes
|
||||||
.filter(attr => !excludedFields.includes(attr.name))
|
.filter(attr => !excludedFields.includes(attr.name))
|
||||||
.map(attr => ({
|
.map(attr => ({
|
||||||
...attr,
|
...attr,
|
||||||
// Override featureCode: make editable for create and add dynamic options
|
editable: attr.name === 'enabled' ? true : attr.editable,
|
||||||
readonly: attr.name === 'featureCode' ? false : attr.readonly,
|
|
||||||
editable: attr.name === 'featureCode' || attr.name === 'enabled' ? true : attr.editable,
|
|
||||||
options: attr.name === 'featureCode' ? featureOptions : attr.options,
|
|
||||||
})) as AttributeDefinition[];
|
})) as AttributeDefinition[];
|
||||||
}, [features, backendAttributes]);
|
}, [backendAttributes]);
|
||||||
|
|
||||||
// Handle create instance
|
// 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;
|
if (!selectedMandateId) return;
|
||||||
setIsSubmitting(true);
|
setIsSubmitting(true);
|
||||||
try {
|
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, {
|
const result = await createInstance(selectedMandateId, {
|
||||||
featureCode: data.featureCode,
|
featureCode: createFeatureCode,
|
||||||
label: data.label,
|
label: createLabel,
|
||||||
enabled: data.enabled !== false,
|
enabled: data.enabled !== false,
|
||||||
copyTemplateRoles: data.copyTemplateRoles !== false
|
copyTemplateRoles: data.copyTemplateRoles !== false,
|
||||||
|
config: config
|
||||||
});
|
});
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
setShowCreateModal(false);
|
setShowCreateModal(false);
|
||||||
|
setCreateFeatureCode('');
|
||||||
|
setCreateLabel('');
|
||||||
|
formDataRef.current = {};
|
||||||
|
setChatbotConnectors(['preprocessor']);
|
||||||
|
setChatbotSystemPrompt('');
|
||||||
|
setChatbotEnableWebResearch(true);
|
||||||
fetchInstances(selectedMandateId);
|
fetchInstances(selectedMandateId);
|
||||||
showSuccess('Feature-Instanz erstellt', `Die Instanz "${data.label}" wurde erfolgreich erstellt.`);
|
showSuccess('Feature-Instanz erstellt', `Die Instanz "${createLabel}" wurde erfolgreich erstellt.`);
|
||||||
} else {
|
} else {
|
||||||
showError('Fehler', result.error || 'Fehler beim Erstellen der Feature-Instanz');
|
showError('Fehler', result.error || 'Fehler beim Erstellen der Feature-Instanz');
|
||||||
}
|
}
|
||||||
|
|
@ -122,10 +179,34 @@ export const AdminFeatureAccessPage: React.FC = () => {
|
||||||
setIsSubmitting(false);
|
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
|
// Handle edit click
|
||||||
const handleEditClick = (instance: FeatureInstance) => {
|
const handleEditClick = (instance: FeatureInstance) => {
|
||||||
setEditingInstance(instance);
|
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);
|
setShowEditModal(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -134,13 +215,57 @@ export const AdminFeatureAccessPage: React.FC = () => {
|
||||||
if (!selectedMandateId || !editingInstance) return;
|
if (!selectedMandateId || !editingInstance) return;
|
||||||
setIsSubmitting(true);
|
setIsSubmitting(true);
|
||||||
try {
|
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, {
|
const result = await updateInstance(selectedMandateId, editingInstance.id, {
|
||||||
label: data.label,
|
label: data.label,
|
||||||
enabled: data.enabled
|
enabled: data.enabled,
|
||||||
|
config: config
|
||||||
});
|
});
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
setShowEditModal(false);
|
setShowEditModal(false);
|
||||||
setEditingInstance(null);
|
setEditingInstance(null);
|
||||||
|
setChatbotConnectors(['preprocessor']);
|
||||||
|
setChatbotSystemPrompt('');
|
||||||
fetchInstances(selectedMandateId);
|
fetchInstances(selectedMandateId);
|
||||||
showSuccess('Feature-Instanz aktualisiert', `Die Instanz "${data.label}" wurde erfolgreich aktualisiert.`);
|
showSuccess('Feature-Instanz aktualisiert', `Die Instanz "${data.label}" wurde erfolgreich aktualisiert.`);
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -375,14 +500,98 @@ export const AdminFeatureAccessPage: React.FC = () => {
|
||||||
<span>Lade Formular...</span>
|
<span>Lade Formular...</span>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<FormGeneratorForm
|
<div>
|
||||||
attributes={createFields}
|
{/* Feature Code Selector - Required for chatbot config */}
|
||||||
mode="create"
|
<div className={styles.configField} style={{ marginBottom: '1.5rem', paddingBottom: '1rem', borderBottom: '1px solid var(--border-color)' }}>
|
||||||
onSubmit={handleCreateInstance}
|
<label className={styles.configLabel} style={{ fontWeight: 600 }}>
|
||||||
onCancel={() => setShowCreateModal(false)}
|
Feature auswählen: <span style={{ color: 'var(--error-color)' }}>*</span>
|
||||||
submitButtonText="Erstellen"
|
</label>
|
||||||
cancelButtonText="Abbrechen"
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -423,10 +632,28 @@ export const AdminFeatureAccessPage: React.FC = () => {
|
||||||
data={editingInstance}
|
data={editingInstance}
|
||||||
mode="edit"
|
mode="edit"
|
||||||
onSubmit={handleUpdateInstance}
|
onSubmit={handleUpdateInstance}
|
||||||
onCancel={() => { setShowEditModal(false); setEditingInstance(null); }}
|
onCancel={() => {
|
||||||
|
setShowEditModal(false);
|
||||||
|
setEditingInstance(null);
|
||||||
|
setChatbotConnectors(['preprocessor']);
|
||||||
|
setChatbotSystemPrompt('');
|
||||||
|
setChatbotEnableWebResearch(true);
|
||||||
|
}}
|
||||||
submitButtonText="Speichern"
|
submitButtonText="Speichern"
|
||||||
cancelButtonText="Abbrechen"
|
cancelButtonText="Abbrechen"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Chatbot Configuration Section */}
|
||||||
|
{editingInstance?.featureCode === 'chatbot' && (
|
||||||
|
<ChatbotConfigSection
|
||||||
|
connectors={chatbotConnectors}
|
||||||
|
systemPrompt={chatbotSystemPrompt}
|
||||||
|
enableWebResearch={chatbotEnableWebResearch}
|
||||||
|
onConnectorsChange={setChatbotConnectors}
|
||||||
|
onSystemPromptChange={setChatbotSystemPrompt}
|
||||||
|
onEnableWebResearchChange={setChatbotEnableWebResearch}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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 { Button } from '../../../components/UiComponents/Button';
|
||||||
import { AutoScroll } from '../../../components/UiComponents/AutoScroll';
|
import { AutoScroll } from '../../../components/UiComponents/AutoScroll';
|
||||||
import { ChatMessage } from '../../../components/UiComponents/Messages/ChatMessages/ChatMessage';
|
import { ChatMessage } from '../../../components/UiComponents/Messages/ChatMessages/ChatMessage';
|
||||||
|
import { formatUnixTimestamp } from '../../../utils/time';
|
||||||
import { IoMdSend } from 'react-icons/io';
|
import { IoMdSend } from 'react-icons/io';
|
||||||
import { MdStop } from 'react-icons/md';
|
import { MdStop } from 'react-icons/md';
|
||||||
import { LuMessageSquare, LuTrash2 } from 'react-icons/lu';
|
import { LuMessageSquare, LuTrash2 } from 'react-icons/lu';
|
||||||
|
|
@ -26,6 +27,7 @@ export const ChatbotConversationsView: React.FC = () => {
|
||||||
messages,
|
messages,
|
||||||
loadingMessages,
|
loadingMessages,
|
||||||
isStreaming,
|
isStreaming,
|
||||||
|
streamingStatus,
|
||||||
currentWorkflowId,
|
currentWorkflowId,
|
||||||
selectThread,
|
selectThread,
|
||||||
createNewThread,
|
createNewThread,
|
||||||
|
|
@ -45,6 +47,15 @@ export const ChatbotConversationsView: React.FC = () => {
|
||||||
await sendMessage(inputValue);
|
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 () => {
|
const handleStop = async () => {
|
||||||
console.log('Stop button clicked', {
|
console.log('Stop button clicked', {
|
||||||
isStreaming,
|
isStreaming,
|
||||||
|
|
@ -79,7 +90,9 @@ export const ChatbotConversationsView: React.FC = () => {
|
||||||
|
|
||||||
const formatDate = (timestamp?: number) => {
|
const formatDate = (timestamp?: number) => {
|
||||||
if (!timestamp) return '';
|
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 now = new Date();
|
||||||
const diffMs = now.getTime() - date.getTime();
|
const diffMs = now.getTime() - date.getTime();
|
||||||
const diffMins = Math.floor(diffMs / 60000);
|
const diffMins = Math.floor(diffMs / 60000);
|
||||||
|
|
@ -91,7 +104,13 @@ export const ChatbotConversationsView: React.FC = () => {
|
||||||
if (diffHours < 24) return `Vor ${diffHours} Std`;
|
if (diffHours < 24) return `Vor ${diffHours} Std`;
|
||||||
if (diffDays < 7) return `Vor ${diffDays} Tagen`;
|
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) => {
|
const getThreadTitle = (thread: any) => {
|
||||||
|
|
@ -195,11 +214,18 @@ export const ChatbotConversationsView: React.FC = () => {
|
||||||
{isStreaming && (
|
{isStreaming && (
|
||||||
<div className={styles.typingIndicator}>
|
<div className={styles.typingIndicator}>
|
||||||
<div className={styles.typingBubble}>
|
<div className={styles.typingBubble}>
|
||||||
<div className={styles.typingDots}>
|
{streamingStatus ? (
|
||||||
<span></span>
|
<div className={styles.streamingStatus}>
|
||||||
<span></span>
|
<div className={styles.statusSpinner} />
|
||||||
<span></span>
|
<span className={styles.statusText}>{streamingStatus}</span>
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className={styles.typingDots}>
|
||||||
|
<span></span>
|
||||||
|
<span></span>
|
||||||
|
<span></span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -213,6 +239,7 @@ export const ChatbotConversationsView: React.FC = () => {
|
||||||
<TextField
|
<TextField
|
||||||
value={inputValue}
|
value={inputValue}
|
||||||
onChange={setInputValue}
|
onChange={setInputValue}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
placeholder="Nachricht eingeben..."
|
placeholder="Nachricht eingeben..."
|
||||||
disabled={isStreaming}
|
disabled={isStreaming}
|
||||||
className={styles.inputField}
|
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 */
|
/* Dark theme support for typing indicator */
|
||||||
:global(.dark-theme) .typingBubble {
|
:global(.dark-theme) .typingBubble {
|
||||||
background-color: var(--surface-dark, #2a2a2a);
|
background-color: var(--surface-dark, #2a2a2a);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue