Lädt...
diff --git a/src/components/Speech/SpeechConfirmation.tsx b/src/components/Speech/SpeechConfirmation.tsx
index 96085f8..b245fdd 100644
--- a/src/components/Speech/SpeechConfirmation.tsx
+++ b/src/components/Speech/SpeechConfirmation.tsx
@@ -2,7 +2,7 @@ import { IoIosCheckmarkCircle, IoIosMail, IoIosCall, IoIosTime, IoIosRefresh } f
import { useNavigate } from 'react-router-dom';
import sharedStyles from '../../core/PageManager/pages.module.css';
import styles from './SpeechConfirmation.module.css';
-import { useLanguage } from '../../contexts/LanguageContext';
+import { useLanguage } from '../../providers/language/LanguageContext';
interface MandateData {
id: string;
diff --git a/src/components/Speech/SpeechInfo.tsx b/src/components/Speech/SpeechInfo.tsx
index 41ff633..b690dfe 100644
--- a/src/components/Speech/SpeechInfo.tsx
+++ b/src/components/Speech/SpeechInfo.tsx
@@ -1,6 +1,6 @@
import { IoIosLink, IoIosCall, IoIosAnalytics, IoIosFingerPrint, IoIosBook, IoIosChatbubbles, IoIosDesktop } from 'react-icons/io';
import styles from './SpeechInfo.module.css';
-import { useLanguage } from '../../contexts/LanguageContext';
+import { useLanguage } from '../../providers/language/LanguageContext';
function SpeechInfo() {
const { t } = useLanguage();
diff --git a/src/components/Speech/SpeechSettings.tsx b/src/components/Speech/SpeechSettings.tsx
index 4276c89..013243b 100644
--- a/src/components/Speech/SpeechSettings.tsx
+++ b/src/components/Speech/SpeechSettings.tsx
@@ -2,7 +2,7 @@ import { useState, useEffect } from 'react';
import { IoIosBusiness, IoIosContact, IoIosTime, IoIosRefresh } from 'react-icons/io';
import sharedStyles from '../../core/PageManager/pages.module.css';
import styles from './SpeechSettings.module.css';
-import { useLanguage } from '../../contexts/LanguageContext';
+import { useLanguage } from '../../providers/language/LanguageContext';
interface MandateData {
id: string;
diff --git a/src/components/Speech/SpeechSignUp.tsx b/src/components/Speech/SpeechSignUp.tsx
index ca65ce9..0c98f34 100644
--- a/src/components/Speech/SpeechSignUp.tsx
+++ b/src/components/Speech/SpeechSignUp.tsx
@@ -2,7 +2,7 @@ import { useState } from 'react';
import { IoIosArrowBack, IoIosBusiness, IoIosPeople } from 'react-icons/io';
import sharedStyles from '../../core/PageManager/pages.module.css';
import styles from './SpeechSignUp.module.css';
-import { useLanguage } from '../../contexts/LanguageContext';
+import { useLanguage } from '../../providers/language/LanguageContext';
interface SpeechSignUpProps {
onBack: () => void;
diff --git a/src/components/TestSharepoint/TestSharepointTable.module.css b/src/components/TestSharepoint/TestSharepointTable.module.css
deleted file mode 100644
index 8b8b048..0000000
--- a/src/components/TestSharepoint/TestSharepointTable.module.css
+++ /dev/null
@@ -1,40 +0,0 @@
-.testSharepointTable {
- width: 100%;
- display: flex;
- flex-direction: column;
- gap: 20px;
-}
-
-.errorState {
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- padding: 40px 20px;
- background: var(--color-bg);
- border: 1px solid var(--color-error, #dc3545);
- border-radius: 8px;
- color: var(--color-error, #dc3545);
- text-align: center;
- gap: 15px;
-}
-
-.retryButton {
- padding: 8px 16px;
- background: var(--color-error, #dc3545);
- color: white;
- border: none;
- border-radius: 4px;
- cursor: pointer;
- font-family: var(--font-family);
- font-size: 14px;
- transition: background-color 0.2s ease;
-}
-
-.retryButton:hover {
- background: var(--color-error-hover, #c82333);
-}
-
-.sharepointFormGenerator {
- width: 100%;
-}
diff --git a/src/components/TestSharepoint/TestSharepointTable.tsx b/src/components/TestSharepoint/TestSharepointTable.tsx
deleted file mode 100644
index 36e9b02..0000000
--- a/src/components/TestSharepoint/TestSharepointTable.tsx
+++ /dev/null
@@ -1,72 +0,0 @@
-import { FormGenerator } from '../FormGenerator';
-import { useLanguage } from '../../contexts/LanguageContext';
-import styles from './TestSharepointTable.module.css';
-import { useTestSharepointLogic } from './testSharepointLogic';
-import type { TestSharepointTableProps } from './testSharepointInterfaces';
-
-export function TestSharepointTable({
- className = '',
- documents,
- documentsLoading,
- documentsError,
- columns,
- actions,
- onRowClick
-}: TestSharepointTableProps) {
- const { t } = useLanguage();
-
- // Get fallback data from hook if props not provided (for backwards compatibility)
- const hookData = useTestSharepointLogic();
-
- // Use props data if provided, otherwise fall back to hook data
- const actualDocuments = documents ?? hookData.documents;
- const actualLoading = documentsLoading ?? hookData.documentsLoading;
- const actualError = documentsError ?? hookData.documentsError;
- const actualColumns = columns ?? hookData.columns;
- const actualActions = actions ?? hookData.actions;
-
- // Debug the data being passed to FormGenerator
- console.log('TestSharepointTable - actualDocuments:', actualDocuments);
- console.log('TestSharepointTable - actualLoading:', actualLoading);
- console.log('TestSharepointTable - actualError:', actualError);
- console.log('TestSharepointTable - actualColumns:', actualColumns);
-
- // Show error state
- if (actualError) {
- return (
-
-
-
{t('sharepoint.error.loading', 'Error loading SharePoint documents:')} {actualError}
-
window.location.reload()} className={styles.retryButton}>
- {t('sharepoint.button.retry', 'Retry')}
-
-
-
- );
- }
-
- return (
-
-
-
- );
-}
-
-export default TestSharepointTable;
diff --git a/src/components/TestSharepoint/index.ts b/src/components/TestSharepoint/index.ts
deleted file mode 100644
index ea7a8e3..0000000
--- a/src/components/TestSharepoint/index.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-export { TestSharepointTable } from './TestSharepointTable';
-export { useTestSharepointLogic } from './testSharepointLogic';
-export type {
- TestSharepointTableProps,
- TableAction,
- SharePointHandlers,
- SharePointOperationsReturn,
- SharePointConnectionsReturn,
- SharePointTableConfig,
- TestSharepointLogicReturn
-} from './testSharepointInterfaces';
diff --git a/src/components/TestSharepoint/testSharepointInterfaces.ts b/src/components/TestSharepoint/testSharepointInterfaces.ts
deleted file mode 100644
index 515dff0..0000000
--- a/src/components/TestSharepoint/testSharepointInterfaces.ts
+++ /dev/null
@@ -1,112 +0,0 @@
-import { ColumnConfig } from '../FormGenerator';
-import React from 'react';
-
-// Re-export SharePoint-related interfaces from hooks
-export type {
- SharePointConnection,
- SharePointDocument,
- SharePointResponse,
- SharePointListRequest,
- SharePointFindRequest,
- SharePointReadRequest,
- SharePointUploadRequest
-} from '../../hooks/useSharePointTest';
-
-// Import for local use
-import type { SharePointConnection, SharePointDocument } from '../../hooks/useSharePointTest';
-
-// Component Props Interfaces
-export interface TestSharepointTableProps {
- className?: string;
- documents?: SharePointDocument[];
- documentsLoading?: boolean;
- documentsError?: string | null;
- columns?: any[];
- actions?: any[];
- onRowClick?: (row: any) => void;
-}
-
-// Table Action Interface
-export interface TableAction {
- label: string;
- onClick?: (document: SharePointDocument) => Promise
| void;
- icon: React.ReactNode | ((document: SharePointDocument) => React.ReactNode);
-}
-
-// SharePoint Operation Handler Types
-export interface SharePointHandlers {
- handleConnectionTest: (connectionId: string) => Promise;
- handleListDocuments: (connectionId: string, siteUrl: string, folderPaths: string[]) => Promise;
- handleFindDocuments: (connectionId: string, siteUrl: string, query: string) => Promise;
- handleReadDocument: (connectionId: string, siteUrl: string, documentPath: string) => Promise<{ success: boolean; data?: any; error?: string }>;
-}
-
-// Hook Return Types for SharePoint Operations
-export interface SharePointOperationsReturn extends SharePointHandlers {
- testingConnections: Set;
- loadingDocuments: boolean;
- connectionError: string | null;
- documentsError: string | null;
- isLoading: boolean;
-}
-
-// Hook Return Types for SharePoint Connections
-export interface SharePointConnectionsReturn {
- connections: SharePointConnection[];
- loading: boolean;
- error: string | null;
- refetch: () => Promise;
- testConnection: (connectionId: string) => Promise;
-}
-
-// SharePoint Table Configuration
-export interface SharePointTableConfig {
- columns: ColumnConfig[];
- actions: TableAction[];
- pageSize: number;
- searchable: boolean;
- filterable: boolean;
- sortable: boolean;
- resizable: boolean;
- pagination: boolean;
-}
-
-// Hook Return Type for TestSharepoint Logic
-export interface TestSharepointLogicReturn {
- // Connection data
- connections: SharePointConnection[];
- selectedConnection: SharePointConnection | null;
- connectionLoading: boolean;
- connectionError: string | null;
-
- // Document data
- documents: SharePointDocument[];
- documentsLoading: boolean;
- documentsError: string | null;
-
- // Table configuration
- columns: ColumnConfig[];
- actions: TableAction[];
-
- // Connection testing
- testingConnections: Set;
- connectionTestResults: Record;
-
- // Site discovery
- discoveredSites: any[];
- sitesDiscovered: boolean;
-
- // Token debug
- tokenDebugInfo: any;
-
- // Handlers
- handleSelectConnection: (connectionId: string) => void;
- handleTestConnection: (connectionId: string) => Promise;
- handleListDocuments: (siteUrl?: string, folderPaths?: string[]) => Promise;
- handleDiscoverSites: () => Promise;
- handleSelectSite: (siteUrl: string) => void;
- handleDebugTokens: () => Promise;
- handleCleanupTokens: () => Promise;
- handleFolderNavigation: (document: SharePointDocument, currentPath: string) => string;
- refetchConnections: () => Promise;
-}
diff --git a/src/components/TestSharepoint/testSharepointLogic.tsx b/src/components/TestSharepoint/testSharepointLogic.tsx
deleted file mode 100644
index addec42..0000000
--- a/src/components/TestSharepoint/testSharepointLogic.tsx
+++ /dev/null
@@ -1,395 +0,0 @@
-import { useMemo, useState, useEffect } from 'react';
-import { IoIosLink, IoIosCloudDownload } from 'react-icons/io';
-
-import { ColumnConfig } from '../FormGenerator';
-import { useSharePointTest } from '../../hooks/useSharePointTest';
-import { useLanguage } from '../../contexts/LanguageContext';
-import type {
- TableAction,
- SharePointConnection,
- SharePointDocument,
- TestSharepointLogicReturn
-} from './testSharepointInterfaces';
-
-export function useTestSharepointLogic(): TestSharepointLogicReturn {
- const {
- getConnections,
- testConnection,
- listDocuments,
- discoverSites,
- debugTokenDetails,
- cleanupTokens,
- isLoading
- } = useSharePointTest();
- const { t } = useLanguage();
-
- // State management
- const [connections, setConnections] = useState([]);
- const [selectedConnection, setSelectedConnection] = useState(null);
- const [connectionLoading, setConnectionLoading] = useState(false);
- const [connectionError, setConnectionError] = useState(null);
-
- const [documents, setDocuments] = useState([]);
- const [documentsLoading, setDocumentsLoading] = useState(false);
- const [documentsError, setDocumentsError] = useState(null);
-
- const [testingConnections, setTestingConnections] = useState>(new Set());
- const [connectionTestResults, setConnectionTestResults] = useState>({});
-
- const [discoveredSites, setDiscoveredSites] = useState([]);
- const [sitesDiscovered, setSitesDiscovered] = useState(false);
- const [tokenDebugInfo, setTokenDebugInfo] = useState(null);
-
- // Load connections on mount
- useEffect(() => {
- loadConnections();
- }, []);
-
- const loadConnections = async () => {
- setConnectionLoading(true);
- setConnectionError(null);
- try {
- const conns = await getConnections();
- setConnections(conns);
- if (conns.length > 0 && !selectedConnection) {
- setSelectedConnection(conns[0]);
- }
- } catch (error) {
- console.error('Failed to load connections:', error);
- setConnectionError(error instanceof Error ? error.message : 'Failed to load connections');
- } finally {
- setConnectionLoading(false);
- }
- };
-
- // Configure columns for the SharePoint documents table
- const columns: ColumnConfig[] = useMemo(() => [
- {
- key: 'documentName',
- label: t('sharepoint.column.documentName', 'Document Name'),
- type: 'string',
- width: 300,
- minWidth: 200,
- maxWidth: 400,
- sortable: true,
- filterable: true,
- searchable: true,
- formatter: (value: string, row: any) => (
-
- {row?.type === 'folder' ? '📁' : '📄'} {value}
-
- )
- },
- {
- key: 'mimeType',
- label: t('sharepoint.column.mimeType', 'MIME Type'),
- type: 'string',
- width: 200,
- minWidth: 150,
- maxWidth: 300,
- sortable: true,
- filterable: true,
- searchable: true,
- },
- {
- key: 'size',
- label: t('sharepoint.column.size', 'Size'),
- type: 'number',
- width: 140,
- minWidth: 120,
- maxWidth: 180,
- sortable: true,
- filterable: false,
- formatter: (value: number | string | undefined) => {
- if (!value || value === 0) return '-';
-
- const sizeInBytes = typeof value === 'string' ? parseInt(value, 10) : value;
- const units = ['Bytes', 'KB', 'MB', 'GB'];
- let size = sizeInBytes;
- let unitIndex = 0;
-
- while (size >= 1024 && unitIndex < units.length - 1) {
- size /= 1024;
- unitIndex++;
- }
-
- return (
-
- {`${size.toFixed(1)} ${units[unitIndex]}`}
-
- );
- }
- },
- {
- key: 'path',
- label: t('sharepoint.column.path', 'Path'),
- type: 'string',
- width: 250,
- minWidth: 200,
- maxWidth: 400,
- sortable: true,
- filterable: true,
- searchable: true,
- },
- ], [t]);
-
- // Handle connection selection
- const handleSelectConnection = (connectionId: string) => {
- const connection = connections.find(conn => conn.id === connectionId);
- if (connection) {
- setSelectedConnection(connection);
- // Clear documents when changing connection
- setDocuments([]);
- setDocumentsError(null);
- }
- };
-
- // Handle connection testing
- const handleTestConnection = async (connectionId: string) => {
- setTestingConnections(prev => new Set(prev).add(connectionId));
- try {
- const result = await testConnection(connectionId);
- setConnectionTestResults(prev => ({ ...prev, [connectionId]: result }));
- } catch (error) {
- console.error('Connection test failed:', error);
- setConnectionTestResults(prev => ({
- ...prev,
- [connectionId]: {
- success: false,
- error: error instanceof Error ? error.message : 'Connection test failed'
- }
- }));
- } finally {
- setTestingConnections(prev => {
- const newSet = new Set(prev);
- newSet.delete(connectionId);
- return newSet;
- });
- }
- };
-
- // Handle listing documents
- const handleListDocuments = async (siteUrl?: string, folderPaths?: string[]) => {
- if (!selectedConnection) {
- setDocumentsError('No connection selected');
- return;
- }
-
- setDocumentsLoading(true);
- setDocumentsError(null);
-
- try {
- const connectionReference = `connection:${selectedConnection.authority}:${selectedConnection.externalUsername}:${selectedConnection.id}`;
-
- const response = await listDocuments({
- connectionReference,
- siteUrl: siteUrl || 'https://your-tenant.sharepoint.com/sites/your-site',
- folderPaths: folderPaths || ['/'],
- includeSubfolders: false,
- expectedDocumentFormats: [{ extension: '.json', mimeType: 'application/json' }]
- });
-
- console.log('SharePoint response:', response);
-
- if (response.success && response.data?.documents) {
- // Extract the actual files from the nested structure
- const documents = response.data.documents;
- if (documents.length > 0 && documents[0].documentData?.listResults) {
- // Flatten all files from all folder results
- const allFiles: SharePointDocument[] = [];
- documents[0].documentData.listResults.forEach((folderResult: any) => {
- if (folderResult.items && Array.isArray(folderResult.items)) {
- folderResult.items.forEach((item: any) => {
- // Convert SharePoint item to our document format
- allFiles.push({
- documentName: item.name || 'Unknown',
- mimeType: item.file?.mimeType || (item.type === 'folder' ? 'folder' : 'unknown'),
- size: item.size || 0,
- path: folderResult.folderPath || '/',
- type: item.type || (item.folder ? 'folder' : 'file'),
- id: item.id,
- documentData: item // Store the full item data
- });
- });
- }
- });
- console.log('Extracted files:', allFiles);
- console.log('Sample file structure:', allFiles[0]);
- setDocuments(allFiles);
- } else {
- console.log('No listResults found in response');
- setDocuments([]);
- }
- } else {
- console.log('Response error or no documents:', response);
- setDocumentsError(response.error || 'Failed to list documents');
- setDocuments([]);
- }
- } catch (error) {
- console.error('Failed to list documents:', error);
- setDocumentsError(error instanceof Error ? error.message : 'Failed to list documents');
- setDocuments([]);
- } finally {
- setDocumentsLoading(false);
- }
- };
-
- // Handle site discovery
- const handleDiscoverSites = async () => {
- if (!selectedConnection) {
- return;
- }
-
- try {
- const result = await discoverSites();
- if (result.success && result.data && result.data.sites) {
- setDiscoveredSites(result.data.sites);
- setSitesDiscovered(true);
- setDocumentsError(null); // Clear any previous errors
- } else {
- console.error('Site discovery failed:', result);
- setDiscoveredSites([]);
- setSitesDiscovered(true); // Set to true so we show the "no sites" message
- // Set error message to help user understand what went wrong
- const errorMsg = result.error || result.message || 'Unknown error occurred';
- setDocumentsError(`Site discovery failed: ${errorMsg}`);
- }
- } catch (error) {
- console.error('Site discovery failed:', error);
- setDiscoveredSites([]);
- setSitesDiscovered(true);
- setDocumentsError(`Site discovery error: ${error instanceof Error ? error.message : 'Network or authentication error'}`);
- }
- };
-
- // Handle site selection
- const handleSelectSite = (siteUrl: string) => {
- // This will be used by the parent component
- console.log('Site selected:', siteUrl);
- };
-
- // Handle token debug
- const handleDebugTokens = async () => {
- try {
- const result = await debugTokenDetails();
- setTokenDebugInfo(result);
- console.log('Token debug info:', result);
- } catch (error) {
- console.error('Token debug failed:', error);
- setTokenDebugInfo({ error: error instanceof Error ? error.message : 'Failed to get token info' });
- }
- };
-
- // Handle token cleanup
- const handleCleanupTokens = async () => {
- try {
- const result = await cleanupTokens();
- console.log('Token cleanup result:', result);
- // Clear the debug info to force refresh
- setTokenDebugInfo(null);
- // Show success message
- setDocumentsError(null);
- alert(`Success! Deleted ${result.data?.tokensDeleted || 0} tokens. Please reconnect your Microsoft account now.`);
- } catch (error) {
- console.error('Token cleanup failed:', error);
- alert(`Token cleanup failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
- }
- };
-
- // Handle folder navigation
- const handleFolderNavigation = (document: SharePointDocument, currentPath: string) => {
- if (document.type === 'folder') {
- // Build the new path by combining current path with folder name
- const newPath = currentPath === '/' ? `/${document.documentName}` : `${currentPath}/${document.documentName}`;
- console.log('Navigating to folder:', newPath);
- return newPath;
- }
- return currentPath;
- };
-
- // Handle document actions
- const handleViewDocument = async (document: SharePointDocument) => {
- console.log('View document:', document);
- // TODO: Implement document viewing
- };
-
- const handleDownloadDocument = async (document: SharePointDocument) => {
- console.log('Download document:', document);
- // TODO: Implement document download
- };
-
- // Configure action buttons
- const actions: TableAction[] = useMemo(() => [
- {
- label: t('sharepoint.action.view', 'View'),
- icon: ,
- onClick: (document: SharePointDocument) => {
- handleViewDocument(document);
- }
- },
- {
- label: t('sharepoint.action.download', 'Download'),
- icon: ,
- onClick: (document: SharePointDocument) => {
- handleDownloadDocument(document);
- }
- }
- ], [t]);
-
- // Refetch connections
- const refetchConnections = async () => {
- await loadConnections();
- };
-
- return {
- // Connection data
- connections,
- selectedConnection,
- connectionLoading,
- connectionError,
-
- // Document data
- documents,
- documentsLoading: documentsLoading || isLoading,
- documentsError,
-
- // Table configuration
- columns,
- actions,
-
- // Connection testing
- testingConnections,
- connectionTestResults,
-
- // Site discovery
- discoveredSites,
- sitesDiscovered,
-
- // Token debug
- tokenDebugInfo,
-
- // Handlers
- handleSelectConnection,
- handleTestConnection,
- handleListDocuments,
- handleDiscoverSites,
- handleSelectSite,
- handleDebugTokens,
- handleCleanupTokens,
- handleFolderNavigation,
- refetchConnections
- };
-}
diff --git a/src/components/ui/Button/Button.tsx b/src/components/UiComponents/Button/Button.tsx
similarity index 100%
rename from src/components/ui/Button/Button.tsx
rename to src/components/UiComponents/Button/Button.tsx
diff --git a/src/components/ui/Button/ButtonTypes.ts b/src/components/UiComponents/Button/ButtonTypes.ts
similarity index 100%
rename from src/components/ui/Button/ButtonTypes.ts
rename to src/components/UiComponents/Button/ButtonTypes.ts
diff --git a/src/components/ui/Button/CreateButton/CreateButton.tsx b/src/components/UiComponents/Button/CreateButton/CreateButton.tsx
similarity index 97%
rename from src/components/ui/Button/CreateButton/CreateButton.tsx
rename to src/components/UiComponents/Button/CreateButton/CreateButton.tsx
index c82d0d2..541854a 100644
--- a/src/components/ui/Button/CreateButton/CreateButton.tsx
+++ b/src/components/UiComponents/Button/CreateButton/CreateButton.tsx
@@ -2,7 +2,7 @@ import React, { useState } from 'react';
import { CreateButtonProps } from '../ButtonTypes';
import Button from '../Button';
import { Popup, EditForm } from '../../Popup';
-import { useLanguage } from '../../../../contexts/LanguageContext';
+import { useLanguage } from '../../../../providers/language/LanguageContext';
const CreateButton: React.FC = ({
onCreate,
diff --git a/src/components/ui/Button/CreateButton/index.ts b/src/components/UiComponents/Button/CreateButton/index.ts
similarity index 100%
rename from src/components/ui/Button/CreateButton/index.ts
rename to src/components/UiComponents/Button/CreateButton/index.ts
diff --git a/src/components/ui/Button/UploadButton/UploadButton.tsx b/src/components/UiComponents/Button/UploadButton/UploadButton.tsx
similarity index 100%
rename from src/components/ui/Button/UploadButton/UploadButton.tsx
rename to src/components/UiComponents/Button/UploadButton/UploadButton.tsx
diff --git a/src/components/ui/Button/UploadButton/index.ts b/src/components/UiComponents/Button/UploadButton/index.ts
similarity index 100%
rename from src/components/ui/Button/UploadButton/index.ts
rename to src/components/UiComponents/Button/UploadButton/index.ts
diff --git a/src/components/ui/Button/index.ts b/src/components/UiComponents/Button/index.ts
similarity index 100%
rename from src/components/ui/Button/index.ts
rename to src/components/UiComponents/Button/index.ts
diff --git a/src/components/UiComponents/ConnectedFilesList/ConnectedFilesList.module.css b/src/components/UiComponents/ConnectedFilesList/ConnectedFilesList.module.css
new file mode 100644
index 0000000..20323a7
--- /dev/null
+++ b/src/components/UiComponents/ConnectedFilesList/ConnectedFilesList.module.css
@@ -0,0 +1,133 @@
+.container {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ background: var(--background-secondary, #f5f5f5);
+ border-radius: 8px;
+ padding: 1rem;
+ overflow: hidden;
+}
+
+.header {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ margin-bottom: 1rem;
+ padding-bottom: 0.75rem;
+ border-bottom: 1px solid var(--border-color, #e0e0e0);
+}
+
+.title {
+ font-size: 1rem;
+ font-weight: 600;
+ margin: 0;
+ color: var(--text-primary, #333);
+}
+
+.count {
+ font-size: 0.875rem;
+ color: var(--text-secondary, #666);
+}
+
+.fileList {
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
+ overflow-y: auto;
+ flex: 1;
+}
+
+.fileItem {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 0.75rem;
+ background: var(--background-primary, #fff);
+ border: 1px solid var(--border-color, #e0e0e0);
+ border-radius: 6px;
+ transition: all 0.2s ease;
+}
+
+.fileItem:hover {
+ border-color: var(--border-hover, #ccc);
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
+}
+
+.fileInfo {
+ display: flex;
+ flex-direction: column;
+ gap: 0.25rem;
+ flex: 1;
+ min-width: 0;
+}
+
+.fileName {
+ font-size: 0.875rem;
+ font-weight: 500;
+ color: var(--text-primary, #333);
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.fileMeta {
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+ font-size: 0.75rem;
+ color: var(--text-secondary, #666);
+}
+
+.fileSize {
+ font-weight: 400;
+}
+
+.fileSource {
+ padding: 0.125rem 0.5rem;
+ background: var(--background-tertiary, #f0f0f0);
+ border-radius: 4px;
+ font-size: 0.7rem;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+}
+
+.fileActions {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ flex-shrink: 0;
+ margin-left: 0.75rem;
+}
+
+.emptyState {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 2rem;
+ color: var(--text-secondary, #666);
+ font-size: 0.875rem;
+ text-align: center;
+ flex: 1;
+}
+
+/* Scrollbar styling */
+.fileList::-webkit-scrollbar {
+ width: 6px;
+}
+
+.fileList::-webkit-scrollbar-track {
+ background: transparent;
+}
+
+.fileList::-webkit-scrollbar-thumb {
+ background: var(--border-color, #ccc);
+ border-radius: 3px;
+}
+
+.fileList::-webkit-scrollbar-thumb:hover {
+ background: var(--border-hover, #999);
+}
+
+
+
+
diff --git a/src/components/UiComponents/ConnectedFilesList/ConnectedFilesList.tsx b/src/components/UiComponents/ConnectedFilesList/ConnectedFilesList.tsx
new file mode 100644
index 0000000..139b7cb
--- /dev/null
+++ b/src/components/UiComponents/ConnectedFilesList/ConnectedFilesList.tsx
@@ -0,0 +1,218 @@
+import React, { useMemo } from 'react';
+import { ViewActionButton, DeleteActionButton, RemoveActionButton } from '../../FormGenerator/ActionButtons';
+import { WorkflowFile } from '../../../hooks/usePlayground';
+import styles from './ConnectedFilesList.module.css';
+
+export interface ConnectedFilesListProps {
+ files: WorkflowFile[];
+ pendingFiles?: WorkflowFile[];
+ onDelete: (file: WorkflowFile) => Promise;
+ onRemove?: (file: WorkflowFile) => Promise;
+ onAttach?: (fileId: string) => Promise; // New: attach file for next message
+ deletingFiles?: Set;
+ previewingFiles?: Set;
+ removingFiles?: Set;
+ workflowId?: string;
+ emptyMessage?: string;
+}
+
+export function ConnectedFilesList({
+ files,
+ pendingFiles = [],
+ onDelete,
+ onRemove,
+ onAttach,
+ deletingFiles = new Set(),
+ previewingFiles = new Set(),
+ removingFiles = new Set(),
+ workflowId,
+ emptyMessage = 'No files connected to this workflow'
+}: ConnectedFilesListProps) {
+ // Combine workflow files and pending files, deduplicating by fileId
+ const allFiles = useMemo(() => {
+ const fileMap = new Map();
+
+ // Add workflow files first (filter out files without fileId)
+ files.forEach(file => {
+ if (file.fileId && file.fileId.trim() !== '') {
+ fileMap.set(file.fileId, file);
+ }
+ });
+
+ // Add pending files (may override workflow files if same fileId)
+ pendingFiles.forEach(file => {
+ if (file.fileId && file.fileId.trim() !== '') {
+ fileMap.set(file.fileId, file);
+ }
+ });
+
+ return Array.from(fileMap.values());
+ }, [files, pendingFiles]);
+
+ // Create hookData object for action buttons
+ const hookData = useMemo(() => ({
+ handleDelete: async (fileId: string) => {
+ const file = allFiles.find(f => f.fileId === fileId);
+ if (file) {
+ await onDelete(file);
+ return true;
+ }
+ return false;
+ },
+ removeOptimistically: (fileId: string) => {
+ // This will be handled by the parent component's state
+ },
+ refetch: async () => {
+ // Refetch handled by parent
+ },
+ deletingItems: deletingFiles,
+ previewingFiles: previewingFiles
+ }), [allFiles, onDelete, deletingFiles, previewingFiles]);
+
+ const handleView = async (file: WorkflowFile) => {
+ // View is handled by ViewActionButton's FilePreview component
+ return Promise.resolve();
+ };
+
+ const handleRemove = async (file: WorkflowFile) => {
+ // Remove file from workflow (not delete from backend)
+ if (onRemove) {
+ await onRemove(file);
+ }
+ };
+
+ if (allFiles.length === 0) {
+ return (
+
+ );
+ }
+
+ return (
+
+
+
Connected Files
+ ({allFiles.length})
+
+
+ {allFiles
+ .filter(file => file.fileId && file.fileId.trim() !== '') // Ensure fileId exists
+ .map((file, index) => {
+ const isDeleting = deletingFiles.has(file.fileId!);
+ const isPreviewing = previewingFiles.has(file.fileId!);
+ const isRemoving = removingFiles.has(file.fileId!);
+ // Use fileId as key since we've filtered out files without it
+ const uniqueKey = file.fileId!;
+
+ // Check if file is in pending files (can be removed) or in messages (already sent)
+ const isPendingFile = pendingFiles.some(f => f.fileId === file.fileId);
+
+ // Handle clicking on file item to attach/detach for next message
+ const handleFileItemClick = async (e: React.MouseEvent) => {
+ // Don't trigger if clicking on action buttons - they handle their own clicks
+ const target = e.target as HTMLElement;
+ const fileActionsElement = target.closest(`.${styles.fileActions}`);
+ const buttonElement = target.closest('button');
+
+ if (fileActionsElement || buttonElement) {
+ e.stopPropagation();
+ return;
+ }
+
+ // Prevent default and stop propagation to ensure click handler fires
+ e.preventDefault();
+ e.stopPropagation();
+
+ if (onAttach && file.fileId) {
+ console.log('🖱️ ConnectedFilesList: Clicking file to attach/detach:', file.fileId);
+ await onAttach(file.fileId);
+ }
+ };
+
+ return (
+
+
+
+ {file.fileName}
+ {onAttach && (
+
+ {isPendingFile ? '(click to detach)' : '(click to attach)'}
+
+ )}
+
+
+
+ {formatFileSize(file.fileSize)}
+
+ {file.source && (
+
+ {file.source === 'user_uploaded' ? 'Uploaded' : 'AI Created'}
+
+ )}
+ {isPendingFile && (
+
+ • Attached
+
+ )}
+
+
+
e.stopPropagation()}>
+
+ {isPendingFile && onRemove && (
+
+ )}
+
+
+
+ );
+ })}
+
+
+ );
+}
+
+function formatFileSize(bytes: number): string {
+ if (bytes === 0) return '0 B';
+ const k = 1024;
+ const sizes = ['B', 'KB', 'MB', 'GB'];
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
+ return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
+}
+
+export default ConnectedFilesList;
+
diff --git a/src/components/UiComponents/ConnectedFilesList/index.ts b/src/components/UiComponents/ConnectedFilesList/index.ts
new file mode 100644
index 0000000..90bd23f
--- /dev/null
+++ b/src/components/UiComponents/ConnectedFilesList/index.ts
@@ -0,0 +1,6 @@
+export { default as ConnectedFilesList } from './ConnectedFilesList';
+export type { ConnectedFilesListProps } from './ConnectedFilesList';
+
+
+
+
diff --git a/src/components/ui/DragDropOverlay/DragDropOverlay.module.css b/src/components/UiComponents/DragDropOverlay/DragDropOverlay.module.css
similarity index 100%
rename from src/components/ui/DragDropOverlay/DragDropOverlay.module.css
rename to src/components/UiComponents/DragDropOverlay/DragDropOverlay.module.css
diff --git a/src/components/ui/DragDropOverlay/DragDropOverlay.tsx b/src/components/UiComponents/DragDropOverlay/DragDropOverlay.tsx
similarity index 98%
rename from src/components/ui/DragDropOverlay/DragDropOverlay.tsx
rename to src/components/UiComponents/DragDropOverlay/DragDropOverlay.tsx
index b6a1161..e4e8339 100644
--- a/src/components/ui/DragDropOverlay/DragDropOverlay.tsx
+++ b/src/components/UiComponents/DragDropOverlay/DragDropOverlay.tsx
@@ -1,5 +1,5 @@
import React, { useState, useCallback } from 'react';
-import { useLanguage } from '../../../contexts/LanguageContext';
+import { useLanguage } from '../../../providers/language/LanguageContext';
import styles from './DragDropOverlay.module.css';
import { IoFolderOpen } from 'react-icons/io5';
diff --git a/src/components/ui/DragDropOverlay/index.ts b/src/components/UiComponents/DragDropOverlay/index.ts
similarity index 100%
rename from src/components/ui/DragDropOverlay/index.ts
rename to src/components/UiComponents/DragDropOverlay/index.ts
diff --git a/src/components/UiComponents/DropdownSelect/DropdownSelect.module.css b/src/components/UiComponents/DropdownSelect/DropdownSelect.module.css
new file mode 100644
index 0000000..6fd5806
--- /dev/null
+++ b/src/components/UiComponents/DropdownSelect/DropdownSelect.module.css
@@ -0,0 +1,270 @@
+.dropdownContainer {
+ position: relative;
+ display: inline-block;
+}
+
+.dropdownButton {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 8px;
+ padding: 12px 16px;
+ border: 1px solid var(--color-primary);
+ border-radius: 25px;
+ font-size: 14px;
+ font-family: var(--font-family);
+ cursor: pointer;
+ transition: all 0.2s ease;
+ background-color: var(--color-bg);
+ color: var(--color-text);
+ min-width: 100%;
+ box-sizing: border-box;
+}
+
+.dropdownButton:hover:not(.disabled):not(:disabled) {
+ border-color: var(--color-secondary);
+ background-color: var(--color-secondary);
+ color: white;
+}
+
+.dropdownButton:focus {
+ outline: none;
+ box-shadow: 0 0 0 3px rgba(var(--color-primary-rgb, 0, 123, 255), 0.1);
+}
+
+.dropdownButton.disabled,
+.dropdownButton:disabled {
+ opacity: 0.6;
+ cursor: not-allowed;
+ background-color: var(--color-bg-disabled, #f5f5f5);
+}
+
+.dropdownButton.loading {
+ cursor: wait;
+}
+
+/* Button variants */
+.buttonPrimary {
+ border-color: var(--color-primary);
+ color: var(--color-text);
+}
+
+.buttonPrimary:hover:not(.disabled):not(:disabled) {
+ background-color: var(--color-primary);
+ color: white;
+}
+
+.buttonSecondary {
+ border-color: var(--color-secondary);
+ color: var(--color-text);
+}
+
+.buttonSecondary:hover:not(.disabled):not(:disabled) {
+ background-color: var(--color-secondary);
+ color: white;
+}
+
+.buttonDanger {
+ border-color: #ef4444;
+ color: var(--color-text);
+}
+
+.buttonDanger:hover:not(.disabled):not(:disabled) {
+ background-color: #ef4444;
+ color: white;
+}
+
+.buttonSuccess {
+ border-color: #10b981;
+ color: var(--color-text);
+}
+
+.buttonSuccess:hover:not(.disabled):not(:disabled) {
+ background-color: #10b981;
+ color: white;
+}
+
+.buttonWarning {
+ border-color: #f59e0b;
+ color: var(--color-text);
+}
+
+.buttonWarning:hover:not(.disabled):not(:disabled) {
+ background-color: #f59e0b;
+ color: white;
+}
+
+/* Button sizes */
+.buttonSm {
+ padding: 8px 12px;
+ font-size: 13px;
+}
+
+.buttonMd {
+ padding: 12px 16px;
+ font-size: 14px;
+}
+
+.buttonLg {
+ padding: 16px 20px;
+ font-size: 16px;
+}
+
+.buttonIcon {
+ font-size: 16px;
+ flex-shrink: 0;
+}
+
+.buttonText {
+ flex: 1;
+ text-align: left;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.chevronIcon {
+ font-size: 16px;
+ flex-shrink: 0;
+ transition: transform 0.2s ease;
+}
+
+.chevronOpen {
+ transform: rotate(180deg);
+}
+
+.clearIcon {
+ font-size: 18px;
+ flex-shrink: 0;
+ transition: transform 0.2s ease;
+}
+
+.clearIcon:hover {
+ transform: scale(1.1);
+}
+
+.buttonSpinner {
+ width: 16px;
+ height: 16px;
+ border: 2px solid var(--color-primary);
+ border-top-color: transparent;
+ border-radius: 50%;
+ animation: spin 0.6s linear infinite;
+}
+
+@keyframes spin {
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+.dropdownMenu {
+ position: absolute;
+ top: calc(100% + 4px);
+ left: 0;
+ right: 0;
+ background-color: var(--color-bg);
+ border: 1px solid var(--color-primary);
+ border-radius: 8px;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+ z-index: 1000;
+ overflow: hidden;
+ min-width: 100%;
+}
+
+.dropdownHeader {
+ padding: 12px 16px;
+ font-size: 12px;
+ font-weight: 600;
+ color: var(--color-text);
+ background-color: var(--color-bg);
+ border-bottom: 1px solid var(--color-primary);
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+}
+
+.dropdownItems {
+ max-height: inherit;
+ overflow-y: auto;
+}
+
+.dropdownEmpty {
+ padding: 12px 16px;
+ font-size: 14px;
+ color: var(--color-text);
+ font-style: italic;
+ text-align: center;
+}
+
+.dropdownItem {
+ width: 100%;
+ padding: 12px 16px;
+ background: none;
+ border: none;
+ text-align: left;
+ cursor: pointer;
+ font-family: var(--font-family);
+ transition: all 0.2s ease;
+ border-bottom: 1px solid var(--color-primary);
+ color: var(--color-text);
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 8px;
+}
+
+.dropdownItem:last-child {
+ border-bottom: none;
+}
+
+.dropdownItem:hover {
+ background-color: var(--color-secondary);
+ color: white;
+}
+
+.dropdownItemSelected {
+ background-color: var(--color-primary);
+ color: white;
+ font-weight: 500;
+}
+
+.dropdownItemSelected:hover {
+ background-color: var(--color-primary);
+ opacity: 0.9;
+}
+
+.itemLabel {
+ flex: 1;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.selectedIndicator {
+ font-size: 16px;
+ font-weight: bold;
+ flex-shrink: 0;
+}
+
+/* Dark theme support */
+[data-theme="dark"] .dropdownButton {
+ border-color: var(--color-primary);
+ background-color: var(--color-bg);
+}
+
+[data-theme="dark"] .dropdownMenu {
+ background-color: var(--color-bg);
+ border-color: var(--color-primary);
+}
+
+[data-theme="dark"] .dropdownItem {
+ color: var(--color-text);
+}
+
+/* Responsive design */
+@media (max-width: 640px) {
+ .dropdownButton {
+ font-size: 16px; /* Prevents zoom on iOS */
+ }
+}
+
diff --git a/src/components/UiComponents/DropdownSelect/DropdownSelect.tsx b/src/components/UiComponents/DropdownSelect/DropdownSelect.tsx
new file mode 100644
index 0000000..bef6a04
--- /dev/null
+++ b/src/components/UiComponents/DropdownSelect/DropdownSelect.tsx
@@ -0,0 +1,240 @@
+import React, { useState, useRef, useEffect } from 'react';
+import { IconType } from 'react-icons';
+import { IoChevronDown, IoClose } from 'react-icons/io5';
+import styles from './DropdownSelect.module.css';
+import { ButtonVariant, ButtonSize } from '../Button/ButtonTypes';
+
+export interface DropdownSelectItem {
+ id: string | number;
+ label: string;
+ value: T;
+ metadata?: Record; // Additional data for custom rendering
+}
+
+export interface DropdownSelectProps {
+ items: DropdownSelectItem[];
+ selectedItemId?: string | number | null;
+ onSelect: (item: DropdownSelectItem | null) => void;
+ placeholder?: string;
+ emptyMessage?: string;
+ headerText?: string;
+ variant?: ButtonVariant;
+ size?: ButtonSize;
+ icon?: IconType;
+ disabled?: boolean;
+ loading?: boolean;
+ className?: string;
+ renderItem?: (item: DropdownSelectItem, isSelected: boolean) => React.ReactNode;
+ renderButton?: (selectedItem: DropdownSelectItem | null, isOpen: boolean) => React.ReactNode;
+ renderClearButton?: (selectedItem: DropdownSelectItem, onClear: () => void) => React.ReactNode;
+ minWidth?: string;
+ maxHeight?: string;
+ showClearButton?: boolean; // Enable/disable clear button feature
+ clearButtonLabel?: string; // Label for clear button (defaults to showing selected item name)
+}
+
+function DropdownSelect({
+ items = [],
+ selectedItemId,
+ onSelect,
+ placeholder = 'Select an item',
+ emptyMessage = 'No items available',
+ headerText,
+ variant = 'primary',
+ size = 'md',
+ icon: Icon,
+ disabled = false,
+ loading = false,
+ className = '',
+ renderItem,
+ renderButton,
+ renderClearButton,
+ minWidth = '180px',
+ maxHeight = '300px',
+ showClearButton = true,
+ clearButtonLabel
+}: DropdownSelectProps) {
+ const [isOpen, setIsOpen] = useState(false);
+ const dropdownRef = useRef(null);
+
+ // Close dropdown when clicking outside
+ useEffect(() => {
+ const handleClickOutside = (event: MouseEvent) => {
+ if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
+ setIsOpen(false);
+ }
+ };
+
+ if (isOpen) {
+ document.addEventListener('mousedown', handleClickOutside);
+ }
+
+ return () => {
+ document.removeEventListener('mousedown', handleClickOutside);
+ };
+ }, [isOpen]);
+
+ // Find selected item
+ const selectedItem = selectedItemId !== null && selectedItemId !== undefined
+ ? items.find(item => item.id === selectedItemId) || null
+ : null;
+
+ // Handle item selection
+ const handleItemClick = (item: DropdownSelectItem) => {
+ onSelect(item);
+ setIsOpen(false);
+ };
+
+ // Handle clear selection
+ const handleClear = () => {
+ if (!disabled && !loading) {
+ onSelect(null);
+ setIsOpen(false);
+ }
+ };
+
+ // Toggle dropdown
+ const toggleDropdown = () => {
+ if (!disabled && !loading) {
+ setIsOpen(!isOpen);
+ }
+ };
+
+ // Build button classes
+ const buttonClasses = [
+ styles.dropdownButton,
+ styles[`button${variant.charAt(0).toUpperCase() + variant.slice(1)}`],
+ styles[`button${size.charAt(0).toUpperCase() + size.slice(1)}`],
+ loading ? styles.loading : '',
+ disabled ? styles.disabled : '',
+ className
+ ].filter(Boolean).join(' ');
+
+ // Render default button content
+ const renderDefaultButton = () => {
+ if (loading) {
+ return (
+ <>
+
+ {placeholder}
+ >
+ );
+ }
+
+ if (selectedItem) {
+ return (
+ <>
+ {Icon && }
+ {selectedItem.label}
+
+ >
+ );
+ }
+
+ return (
+ <>
+ {Icon && }
+ {placeholder}
+
+ >
+ );
+ };
+
+ // Render clear button when item is selected and showClearButton is true
+ const renderClearButtonContent = () => {
+ if (renderClearButton && selectedItem) {
+ return renderClearButton(selectedItem, handleClear);
+ }
+
+ if (selectedItem && showClearButton) {
+ return (
+
+ {Icon && }
+
+ {clearButtonLabel || selectedItem.label}
+
+
+
+ );
+ }
+
+ return null;
+ };
+
+ return (
+
+ {/* Show clear button if item is selected and showClearButton is enabled */}
+ {selectedItem && showClearButton ? (
+ renderClearButtonContent()
+ ) : renderButton ? (
+
+ {renderButton(selectedItem, isOpen)}
+
+ ) : (
+
+ {renderDefaultButton()}
+
+ )}
+
+ {isOpen && (
+
+ {headerText && (
+
+ {headerText}
+
+ )}
+
+ {items.length === 0 ? (
+
+ {emptyMessage}
+
+ ) : (
+
+ {items.map((item) => {
+ const isSelected = selectedItemId === item.id;
+
+ if (renderItem) {
+ return (
+
handleItemClick(item)}
+ >
+ {renderItem(item, isSelected)}
+
+ );
+ }
+
+ return (
+
handleItemClick(item)}
+ >
+ {item.label}
+ {isSelected && ✓ }
+
+ );
+ })}
+
+ )}
+
+ )}
+
+ );
+}
+
+export default DropdownSelect;
+
diff --git a/src/components/UiComponents/DropdownSelect/index.ts b/src/components/UiComponents/DropdownSelect/index.ts
new file mode 100644
index 0000000..691cb06
--- /dev/null
+++ b/src/components/UiComponents/DropdownSelect/index.ts
@@ -0,0 +1,3 @@
+export { default as DropdownSelect } from './DropdownSelect';
+export type { DropdownSelectItem, DropdownSelectProps } from './DropdownSelect';
+
diff --git a/src/components/UiComponents/EditFields/SelectField/SelectField.tsx b/src/components/UiComponents/EditFields/SelectField/SelectField.tsx
new file mode 100644
index 0000000..1241f16
--- /dev/null
+++ b/src/components/UiComponents/EditFields/SelectField/SelectField.tsx
@@ -0,0 +1,95 @@
+import React from 'react';
+import { DropdownSelect, DropdownSelectItem } from '../../DropdownSelect';
+import { ButtonSize } from '../../Button/ButtonTypes';
+
+export interface SelectFieldOption {
+ id: string | number;
+ label: string;
+ value: any;
+}
+
+export interface SelectFieldProps {
+ value?: string | number | null;
+ onChange?: (value: any) => void;
+ label?: string;
+ options: SelectFieldOption[];
+ placeholder?: string;
+ required?: boolean;
+ disabled?: boolean;
+ description?: string;
+ className?: string;
+ size?: ButtonSize;
+}
+
+const SelectField: React.FC = ({
+ value,
+ onChange,
+ label,
+ options,
+ placeholder = 'Select an option',
+ required = false,
+ disabled = false,
+ description,
+ className = '',
+ size = 'md'
+}) => {
+ // Convert options to DropdownSelectItem format
+ const items: DropdownSelectItem[] = options.map(opt => ({
+ id: opt.id,
+ label: opt.label,
+ value: opt.value
+ }));
+
+ // Find selected item ID from value
+ const selectedItemId = value !== null && value !== undefined
+ ? options.find(opt => opt.value === value || opt.id === value)?.id ?? null
+ : null;
+
+ const handleSelect = (item: DropdownSelectItem | null) => {
+ if (onChange) {
+ onChange(item ? item.value : null);
+ }
+ };
+
+ return (
+
+ {label && (
+
+
+ {label}
+ {required && * }
+
+ {description && (
+
+ {description}
+
+ )}
+
+ )}
+
+
+ );
+};
+
+export default SelectField;
+
diff --git a/src/components/UiComponents/EditFields/SelectField/index.ts b/src/components/UiComponents/EditFields/SelectField/index.ts
new file mode 100644
index 0000000..207392e
--- /dev/null
+++ b/src/components/UiComponents/EditFields/SelectField/index.ts
@@ -0,0 +1,3 @@
+export { default } from './SelectField';
+export type { SelectFieldProps, SelectFieldOption } from './SelectField';
+
diff --git a/src/components/UiComponents/EditFields/TextInputField/TextInputField.tsx b/src/components/UiComponents/EditFields/TextInputField/TextInputField.tsx
new file mode 100644
index 0000000..3c8e1c9
--- /dev/null
+++ b/src/components/UiComponents/EditFields/TextInputField/TextInputField.tsx
@@ -0,0 +1,76 @@
+import React from 'react';
+import TextField from '../../TextField';
+import { TextFieldSize } from '../../TextField/TextFieldTypes';
+
+export interface TextInputFieldProps {
+ value?: string;
+ onChange?: (value: string) => void;
+ label?: string;
+ placeholder?: string;
+ required?: boolean;
+ disabled?: boolean;
+ readonly?: boolean;
+ type?: 'text' | 'email' | 'tel';
+ description?: string;
+ error?: string;
+ className?: string;
+ size?: TextFieldSize;
+}
+
+const TextInputField: React.FC = ({
+ value = '',
+ onChange,
+ label,
+ placeholder,
+ required = false,
+ disabled = false,
+ readonly = false,
+ type = 'text',
+ description,
+ error,
+ className = '',
+ size = 'md'
+}) => {
+ return (
+
+ {label && (
+
+
+ {label}
+ {required && * }
+
+ {description && (
+
+ {description}
+
+ )}
+
+ )}
+
+
+ );
+};
+
+export default TextInputField;
+
diff --git a/src/components/UiComponents/EditFields/TextInputField/index.ts b/src/components/UiComponents/EditFields/TextInputField/index.ts
new file mode 100644
index 0000000..d1a3517
--- /dev/null
+++ b/src/components/UiComponents/EditFields/TextInputField/index.ts
@@ -0,0 +1,3 @@
+export { default } from './TextInputField';
+export type { TextInputFieldProps } from './TextInputField';
+
diff --git a/src/components/UiComponents/EditFields/ToggleField/ToggleField.tsx b/src/components/UiComponents/EditFields/ToggleField/ToggleField.tsx
new file mode 100644
index 0000000..7cf4878
--- /dev/null
+++ b/src/components/UiComponents/EditFields/ToggleField/ToggleField.tsx
@@ -0,0 +1,111 @@
+import React from 'react';
+
+export interface ToggleFieldProps {
+ value?: boolean;
+ onChange?: (value: boolean) => void;
+ label?: string;
+ description?: string;
+ disabled?: boolean;
+ className?: string;
+}
+
+const ToggleField: React.FC = ({
+ value = false,
+ onChange,
+ label,
+ description,
+ disabled = false,
+ className = ''
+}) => {
+ const handleToggle = () => {
+ if (!disabled && onChange) {
+ onChange(!value);
+ }
+ };
+
+ return (
+
+ {label && (
+
+
+ {label}
+
+ {description && (
+
+ {description}
+
+ )}
+
+ )}
+
{
+ if (!disabled) {
+ e.currentTarget.style.boxShadow = '0 4px 12px rgba(63, 81, 181, 0.2)';
+ }
+ }}
+ onMouseLeave={(e) => {
+ e.currentTarget.style.boxShadow = 'none';
+ }}
+ >
+
+ {value ? 'Enabled' : 'Disabled'}
+
+
+ );
+};
+
+export default ToggleField;
+
diff --git a/src/components/UiComponents/EditFields/ToggleField/index.ts b/src/components/UiComponents/EditFields/ToggleField/index.ts
new file mode 100644
index 0000000..2b63eb1
--- /dev/null
+++ b/src/components/UiComponents/EditFields/ToggleField/index.ts
@@ -0,0 +1,3 @@
+export { default } from './ToggleField';
+export type { ToggleFieldProps } from './ToggleField';
+
diff --git a/src/components/UiComponents/EditFields/index.ts b/src/components/UiComponents/EditFields/index.ts
new file mode 100644
index 0000000..1c86b79
--- /dev/null
+++ b/src/components/UiComponents/EditFields/index.ts
@@ -0,0 +1,7 @@
+export { default as TextInputField } from './TextInputField';
+export { default as SelectField } from './SelectField';
+export { default as ToggleField } from './ToggleField';
+export type { TextInputFieldProps } from './TextInputField';
+export type { SelectFieldProps, SelectFieldOption } from './SelectField';
+export type { ToggleFieldProps } from './ToggleField';
+
diff --git a/src/components/ui/MessageOverlay/MessageOverlay.module.css b/src/components/UiComponents/InfoMessageOverlay/MessageOverlay.module.css
similarity index 100%
rename from src/components/ui/MessageOverlay/MessageOverlay.module.css
rename to src/components/UiComponents/InfoMessageOverlay/MessageOverlay.module.css
diff --git a/src/components/ui/MessageOverlay/MessageOverlay.tsx b/src/components/UiComponents/InfoMessageOverlay/MessageOverlay.tsx
similarity index 100%
rename from src/components/ui/MessageOverlay/MessageOverlay.tsx
rename to src/components/UiComponents/InfoMessageOverlay/MessageOverlay.tsx
diff --git a/src/components/ui/MessageOverlay/index.ts b/src/components/UiComponents/InfoMessageOverlay/index.ts
similarity index 100%
rename from src/components/ui/MessageOverlay/index.ts
rename to src/components/UiComponents/InfoMessageOverlay/index.ts
diff --git a/src/components/UiComponents/LocationInput/LocationInput.module.css b/src/components/UiComponents/LocationInput/LocationInput.module.css
new file mode 100644
index 0000000..f591a54
--- /dev/null
+++ b/src/components/UiComponents/LocationInput/LocationInput.module.css
@@ -0,0 +1,30 @@
+.locationInputContainer {
+ width: 100%;
+}
+
+.inputRow {
+ display: flex;
+ gap: 1rem;
+ align-items: flex-start;
+}
+
+.inputWrapper {
+ flex: 1;
+}
+
+.locationButton {
+ white-space: nowrap;
+ margin-top: 1.5rem; /* Align with input field */
+}
+
+@media (max-width: 768px) {
+ .inputRow {
+ flex-direction: column;
+ }
+
+ .locationButton {
+ margin-top: 0;
+ width: 100%;
+ }
+}
+
diff --git a/src/components/UiComponents/LocationInput/LocationInput.tsx b/src/components/UiComponents/LocationInput/LocationInput.tsx
new file mode 100644
index 0000000..7c31198
--- /dev/null
+++ b/src/components/UiComponents/LocationInput/LocationInput.tsx
@@ -0,0 +1,73 @@
+import React, { useState } from 'react';
+import { Button, TextField } from '../index';
+import { FaLocationArrow } from 'react-icons/fa';
+import styles from './LocationInput.module.css';
+
+export interface LocationInputProps {
+ value: string;
+ onChange: (value: string) => void;
+ onUseCurrentLocation: () => void;
+ isGettingLocation?: boolean;
+ placeholder?: string;
+ label?: string;
+ error?: string;
+ helperText?: string;
+ disabled?: boolean;
+}
+
+const LocationInput: React.FC = ({
+ value,
+ onChange,
+ onUseCurrentLocation,
+ isGettingLocation = false,
+ placeholder = 'Kanton, Gemeinde, Adresse oder Parzelle',
+ label = 'Standort',
+ error,
+ helperText,
+ disabled = false
+}) => {
+ const [isRequestingLocation, setIsRequestingLocation] = useState(false);
+
+ const handleUseCurrentLocation = async () => {
+ setIsRequestingLocation(true);
+ try {
+ await onUseCurrentLocation();
+ } finally {
+ setIsRequestingLocation(false);
+ }
+ };
+
+ return (
+
+
+
+
+
+
+ Meine Position verwenden
+
+
+
+ );
+};
+
+export default LocationInput;
+
diff --git a/src/components/UiComponents/LocationInput/index.ts b/src/components/UiComponents/LocationInput/index.ts
new file mode 100644
index 0000000..80f8a43
--- /dev/null
+++ b/src/components/UiComponents/LocationInput/index.ts
@@ -0,0 +1,3 @@
+export { default as LocationInput } from './LocationInput';
+export type { LocationInputProps } from './LocationInput';
+
diff --git a/src/components/UiComponents/MapView/LV95Converter.ts b/src/components/UiComponents/MapView/LV95Converter.ts
new file mode 100644
index 0000000..19a8b8b
--- /dev/null
+++ b/src/components/UiComponents/MapView/LV95Converter.ts
@@ -0,0 +1,25 @@
+import proj4 from 'proj4';
+
+// Define LV95 projection (EPSG:2056) and WGS84 (EPSG:4326)
+// LV95 / Swiss TM 35
+const LV95_PROJ = '+proj=somerc +lat_0=46.95240555555556 +lon_0=7.439583333333333 +k_0=1 +x_0=2600000 +y_0=1200000 +ellps=bessel +towgs84=674.374,15.056,405.346,0,0,0,0 +units=m +no_defs';
+const WGS84_PROJ = 'EPSG:4326';
+
+/**
+ * Convert LV95 coordinates to WGS84 (lat/lon)
+ * Uses proj4 for accurate coordinate transformation
+ */
+export function lv95ToWGS84(x: number, y: number): { lat: number; lon: number } {
+ const [lon, lat] = proj4(LV95_PROJ, WGS84_PROJ, [x, y]);
+ return { lat, lon };
+}
+
+/**
+ * Convert WGS84 (lat/lon) to LV95 coordinates
+ * Uses proj4 for accurate coordinate transformation
+ */
+export function wgs84ToLV95(lat: number, lon: number): { x: number; y: number } {
+ const [x, y] = proj4(WGS84_PROJ, LV95_PROJ, [lon, lat]);
+ return { x, y };
+}
+
diff --git a/src/components/UiComponents/MapView/MapView.module.css b/src/components/UiComponents/MapView/MapView.module.css
new file mode 100644
index 0000000..fc4eb44
--- /dev/null
+++ b/src/components/UiComponents/MapView/MapView.module.css
@@ -0,0 +1,49 @@
+.mapViewContainer {
+ width: 100%;
+ position: relative;
+ border: 1px solid #e5e7eb;
+ border-radius: 8px;
+ overflow: hidden;
+ background-color: #f5f5f5;
+ min-height: 400px;
+}
+
+.mapCanvas {
+ width: 100%;
+ height: 100%;
+ cursor: crosshair;
+ display: block;
+}
+
+.mapCanvas:active {
+ cursor: grabbing;
+}
+
+.emptyState {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ height: 100%;
+ color: #6b7280;
+ font-size: 1rem;
+ text-align: center;
+ padding: 2rem;
+}
+
+.emptyStateOverlay {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ background-color: rgba(255, 255, 255, 0.95);
+ padding: 1.5rem 2rem;
+ border-radius: 8px;
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
+ color: #6b7280;
+ font-size: 1rem;
+ text-align: center;
+ pointer-events: none;
+ z-index: 10;
+ max-width: 80%;
+}
+
diff --git a/src/components/UiComponents/MapView/MapView.tsx b/src/components/UiComponents/MapView/MapView.tsx
new file mode 100644
index 0000000..b76c662
--- /dev/null
+++ b/src/components/UiComponents/MapView/MapView.tsx
@@ -0,0 +1,33 @@
+// Export types
+export interface MapPoint {
+ x: number; // LV95 X coordinate
+ y: number; // LV95 Y coordinate
+}
+
+export interface ParcelGeometry {
+ id: string;
+ egrid?: string;
+ number?: string;
+ coordinates: MapPoint[];
+ isSelected?: boolean;
+ isAdjacent?: boolean;
+}
+
+export interface MapViewProps {
+ parcels?: ParcelGeometry[];
+ center?: MapPoint;
+ zoomBounds?: {
+ min_x: number;
+ min_y: number;
+ max_x: number;
+ max_y: number;
+ };
+ onMapClick?: (point: MapPoint) => void;
+ onParcelClick?: (parcelId: string) => void;
+ height?: string;
+ className?: string;
+ emptyMessage?: string;
+}
+
+// Re-export the Leaflet implementation
+export { default } from './MapViewLeaflet';
diff --git a/src/components/UiComponents/MapView/MapViewLeaflet.tsx b/src/components/UiComponents/MapView/MapViewLeaflet.tsx
new file mode 100644
index 0000000..c7d75aa
--- /dev/null
+++ b/src/components/UiComponents/MapView/MapViewLeaflet.tsx
@@ -0,0 +1,218 @@
+import React, { useEffect, useRef } from 'react';
+import L from 'leaflet';
+import 'leaflet/dist/leaflet.css';
+import { lv95ToWGS84, wgs84ToLV95 } from './LV95Converter';
+import type { MapPoint, ParcelGeometry, MapViewProps } from './MapView';
+import styles from './MapView.module.css';
+
+// Fix for default marker icons in Leaflet
+import icon from 'leaflet/dist/images/marker-icon.png';
+import iconShadow from 'leaflet/dist/images/marker-shadow.png';
+
+const DefaultIcon = L.icon({
+ iconUrl: icon,
+ shadowUrl: iconShadow,
+ iconSize: [25, 41],
+ iconAnchor: [12, 41],
+ popupAnchor: [1, -34],
+ shadowSize: [41, 41]
+});
+
+L.Marker.prototype.options.icon = DefaultIcon;
+
+const MapViewLeaflet: React.FC = ({
+ parcels = [],
+ center,
+ zoomBounds,
+ onMapClick,
+ onParcelClick,
+ height = '600px',
+ className = '',
+ emptyMessage = 'Klicken Sie auf die Karte, um einen Standort auszuwählen'
+}) => {
+ const mapRef = useRef(null);
+ const mapContainerRef = useRef(null);
+ const layersRef = useRef([]);
+ const centerMarkerRef = useRef(null);
+
+ // Initialize map
+ useEffect(() => {
+ if (!mapContainerRef.current || mapRef.current) return;
+
+ // Default center: Switzerland (converted from LV95)
+ const defaultCenterLV95 = center || { x: 2600000, y: 1200000 };
+ const defaultCenter = lv95ToWGS84(defaultCenterLV95.x, defaultCenterLV95.y);
+
+ // Create map
+ const map = L.map(mapContainerRef.current, {
+ center: [defaultCenter.lat, defaultCenter.lon],
+ zoom: zoomBounds ? 15 : 8, // Zoom level based on whether we have bounds
+ zoomControl: true,
+ attributionControl: true
+ });
+
+ // Add OpenStreetMap tiles
+ L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
+ attribution: '© OpenStreetMap contributors',
+ maxZoom: 19
+ }).addTo(map);
+
+ mapRef.current = map;
+
+ // Cleanup
+ return () => {
+ map.remove();
+ mapRef.current = null;
+ };
+ }, []); // Only run once on mount
+
+ // Update map center and zoom when center or zoomBounds change
+ useEffect(() => {
+ if (!mapRef.current) return;
+
+ const map = mapRef.current;
+
+ // Remove existing center marker
+ if (centerMarkerRef.current) {
+ map.removeLayer(centerMarkerRef.current);
+ centerMarkerRef.current = null;
+ }
+
+ if (zoomBounds && parcels.length > 0) {
+ // Convert zoom bounds to WGS84
+ const sw = lv95ToWGS84(zoomBounds.min_x, zoomBounds.min_y);
+ const ne = lv95ToWGS84(zoomBounds.max_x, zoomBounds.max_y);
+
+ // Fit map to bounds
+ map.fitBounds([[sw.lat, sw.lon], [ne.lat, ne.lon]], {
+ padding: [20, 20],
+ maxZoom: 18
+ });
+ } else if (center) {
+ // Center on point and add marker
+ const centerWGS84 = lv95ToWGS84(center.x, center.y);
+ map.setView([centerWGS84.lat, centerWGS84.lon], map.getZoom() || 15);
+
+ // Add center marker
+ const marker = L.marker([centerWGS84.lat, centerWGS84.lon], {
+ icon: L.icon({
+ iconUrl: 'data:image/svg+xml;base64,' + btoa(`
+
+
+
+ `),
+ iconSize: [24, 24],
+ iconAnchor: [12, 12]
+ })
+ }).addTo(map);
+ centerMarkerRef.current = marker;
+ } else {
+ // Default center: Switzerland
+ const defaultCenter = lv95ToWGS84(2600000, 1200000);
+ map.setView([defaultCenter.lat, defaultCenter.lon], 8);
+ }
+ }, [center, zoomBounds, parcels.length]);
+
+ // Draw parcels
+ useEffect(() => {
+ if (!mapRef.current) return;
+
+ const map = mapRef.current;
+
+ // Debug logging
+ if (import.meta.env.DEV) {
+ console.log('🗺️ MapView: Drawing parcels', {
+ parcelCount: parcels.length,
+ parcels: parcels.map(p => ({
+ id: p.id,
+ coordCount: p.coordinates.length,
+ isSelected: p.isSelected,
+ isAdjacent: p.isAdjacent
+ }))
+ });
+ }
+
+ // Remove existing parcel layers
+ layersRef.current.forEach((layer) => {
+ map.removeLayer(layer);
+ });
+ layersRef.current = [];
+
+ // Add parcels
+ parcels.forEach((parcel) => {
+ if (parcel.coordinates.length < 3) {
+ if (import.meta.env.DEV) {
+ console.warn(`⚠️ Parcel ${parcel.id} has insufficient coordinates: ${parcel.coordinates.length}`);
+ }
+ return; // Need at least 3 points for a polygon
+ }
+
+ // Convert LV95 coordinates to WGS84
+ const latLngs = parcel.coordinates.map((coord) => {
+ const wgs84 = lv95ToWGS84(coord.x, coord.y);
+ return [wgs84.lat, wgs84.lon] as [number, number];
+ });
+
+ // Create polygon
+ const polygon = L.polygon(latLngs, {
+ color: parcel.isSelected ? '#3b82f6' : parcel.isAdjacent ? '#9ca3af' : '#22c55e',
+ weight: parcel.isSelected ? 3 : parcel.isAdjacent ? 2 : 1,
+ fillColor: parcel.isSelected ? '#3b82f6' : parcel.isAdjacent ? '#9ca3af' : '#22c55e',
+ fillOpacity: parcel.isSelected ? 0.3 : parcel.isAdjacent ? 0.2 : 0.2
+ });
+
+ // Add popup with parcel info
+ const popupContent = `
+
+ Parzelle ${parcel.number || parcel.id}
+ ${parcel.egrid ? `EGRID: ${parcel.egrid} ` : ''}
+ ${parcel.isSelected ? 'Ausgewählt ' : parcel.isAdjacent ? 'Angrenzend ' : ''}
+
+ `;
+ polygon.bindPopup(popupContent);
+
+ // Add click handler
+ if (onParcelClick) {
+ polygon.on('click', () => {
+ onParcelClick(parcel.id);
+ });
+ }
+
+ polygon.addTo(map);
+ layersRef.current.push(polygon);
+ });
+ }, [parcels, onParcelClick]);
+
+ // Handle map clicks
+ useEffect(() => {
+ if (!mapRef.current || !onMapClick) return;
+
+ const map = mapRef.current;
+
+ const handleMapClick = (e: L.LeafletMouseEvent) => {
+ // Convert WGS84 to LV95
+ const lv95 = wgs84ToLV95(e.latlng.lat, e.latlng.lng);
+ onMapClick({ x: lv95.x, y: lv95.y });
+ };
+
+ map.on('click', handleMapClick);
+
+ return () => {
+ map.off('click', handleMapClick);
+ };
+ }, [onMapClick]);
+
+ return (
+
+
+ {parcels.length === 0 && !center && (
+
+ )}
+
+ );
+};
+
+export default MapViewLeaflet;
+
diff --git a/src/components/UiComponents/MapView/index.ts b/src/components/UiComponents/MapView/index.ts
new file mode 100644
index 0000000..b08e17e
--- /dev/null
+++ b/src/components/UiComponents/MapView/index.ts
@@ -0,0 +1,3 @@
+export { default as MapView } from './MapView';
+export type { MapViewProps, MapPoint, ParcelGeometry } from './MapView';
+
diff --git a/src/components/UiComponents/Messages/ChatMessages/ChatMessage.tsx b/src/components/UiComponents/Messages/ChatMessages/ChatMessage.tsx
new file mode 100644
index 0000000..333d0f3
--- /dev/null
+++ b/src/components/UiComponents/Messages/ChatMessages/ChatMessage.tsx
@@ -0,0 +1,102 @@
+import React from 'react';
+import { Message } from '../MessagesTypes';
+import { formatTimestamp } from '../MessageUtils';
+import { DocumentItem, ActionInfo } from '../MessageParts';
+import { WorkflowFile } from '../../../../hooks/usePlayground';
+import styles from '../Messages.module.css';
+
+export interface ChatMessageProps {
+ message: Message;
+ showDocuments?: boolean;
+ renderDocument?: (document: any, message: Message) => React.ReactNode;
+ onFileDelete?: (file: WorkflowFile) => Promise;
+ onFileRemove?: (file: WorkflowFile) => Promise;
+ onFileView?: (file: WorkflowFile) => Promise;
+ deletingFiles?: Set;
+ previewingFiles?: Set;
+ removingFiles?: Set;
+ workflowId?: string;
+}
+
+/**
+ * Renders a single message in chat style (bubble UI)
+ */
+export const ChatMessage: React.FC = ({
+ message,
+ showDocuments = true,
+ renderDocument,
+ onFileDelete,
+ onFileRemove,
+ onFileView,
+ deletingFiles,
+ previewingFiles,
+ removingFiles,
+ workflowId
+}) => {
+ const isUser = message.role?.toLowerCase() === 'user';
+ const messageClass = isUser ? styles.messageUser : styles.messageAssistant;
+
+ // Debug: Log documents if in dev mode
+ if (import.meta.env.DEV && message.documents) {
+ console.log('ChatMessage documents:', message.id, message.documents);
+ }
+
+ return (
+
+
+ {/* Message content */}
+ {message.message && (
+
+ {message.message}
+
+ )}
+
+ {/* Summary if different from message */}
+ {message.summary && message.summary !== message.message && (
+
+ Summary: {message.summary}
+
+ )}
+
+ {/* Documents */}
+ {showDocuments && message.documents && Array.isArray(message.documents) && message.documents.length > 0 && (
+
+ {message.documentsLabel && (
+
{message.documentsLabel}
+ )}
+
+ {message.documents.map((doc) => (
+
+ {renderDocument ? renderDocument(doc, message) : (
+
+ )}
+
+ ))}
+
+
+ )}
+
+ {/* Action information (shown only for assistant messages) */}
+ {!isUser &&
}
+
+ {/* Timestamp */}
+ {message.publishedAt && (
+
+ {formatTimestamp(message.publishedAt)}
+
+ )}
+
+
+ );
+};
+
diff --git a/src/components/UiComponents/Messages/LogMessages/LogMessage.module.css b/src/components/UiComponents/Messages/LogMessages/LogMessage.module.css
new file mode 100644
index 0000000..9f2d6b2
--- /dev/null
+++ b/src/components/UiComponents/Messages/LogMessages/LogMessage.module.css
@@ -0,0 +1,92 @@
+/* Log message styles - list/table layout */
+.logMessage {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ padding: 12px 16px;
+ border-bottom: 1px solid var(--color-primary);
+ background-color: var(--color-surface);
+ transition: background-color 0.2s ease;
+}
+
+.logMessage:hover {
+ background-color: var(--color-highlight-gray);
+}
+
+.logMetadata {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ padding-bottom: 8px;
+ border-bottom: 1px solid rgba(0, 0, 0, 0.1);
+}
+
+.logContent {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+
+.logText {
+ font-size: 14px;
+ line-height: 1.6;
+ color: var(--color-text);
+ white-space: pre-wrap;
+ word-wrap: break-word;
+}
+
+.logSummary {
+ padding: 8px 12px;
+ border-radius: var(--object-radius-small);
+ font-size: 12px;
+ line-height: 1.5;
+ background-color: rgba(0, 0, 0, 0.05);
+ color: var(--color-text);
+}
+
+.logSummary strong {
+ color: var(--color-secondary);
+ font-weight: 600;
+}
+
+.logDocuments {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ margin-top: 8px;
+ padding-top: 8px;
+ border-top: 1px solid rgba(0, 0, 0, 0.1);
+}
+
+/* Dark theme support */
+[data-theme="dark"] .logMessage {
+ border-bottom-color: var(--color-primary);
+}
+
+[data-theme="dark"] .logMessage:hover {
+ background-color: rgba(255, 255, 255, 0.05);
+}
+
+[data-theme="dark"] .logMetadata {
+ border-bottom-color: rgba(255, 255, 255, 0.1);
+}
+
+[data-theme="dark"] .logSummary {
+ background-color: rgba(255, 255, 255, 0.05);
+}
+
+[data-theme="dark"] .logDocuments {
+ border-top-color: rgba(255, 255, 255, 0.1);
+}
+
+/* Responsive design */
+@media (max-width: 640px) {
+ .logMessage {
+ padding: 10px 12px;
+ }
+
+ .logText {
+ font-size: 13px;
+ }
+}
+
diff --git a/src/components/UiComponents/Messages/LogMessages/LogMessage.tsx b/src/components/UiComponents/Messages/LogMessages/LogMessage.tsx
new file mode 100644
index 0000000..f68d34d
--- /dev/null
+++ b/src/components/UiComponents/Messages/LogMessages/LogMessage.tsx
@@ -0,0 +1,95 @@
+import React from 'react';
+import { Message } from '../MessagesTypes';
+import { DocumentItem, MessageMetadata, ActionInfo } from '../MessageParts';
+import { WorkflowFile } from '../../../../hooks/usePlayground';
+import styles from '../Messages.module.css';
+import logStyles from './LogMessage.module.css';
+
+export interface LogMessageProps {
+ message: Message;
+ showDocuments?: boolean;
+ showMetadata?: boolean;
+ showProgress?: boolean;
+ renderDocument?: (document: any, message: Message) => React.ReactNode;
+ onFileDelete?: (file: WorkflowFile) => Promise;
+ onFileRemove?: (file: WorkflowFile) => Promise;
+ onFileView?: (file: WorkflowFile) => Promise;
+ deletingFiles?: Set;
+ previewingFiles?: Set;
+ removingFiles?: Set;
+ workflowId?: string;
+}
+
+/**
+ * Renders a single message in log style (list/table UI)
+ */
+export const LogMessage: React.FC = ({
+ message,
+ showDocuments = true,
+ showMetadata = true,
+ showProgress = true,
+ renderDocument,
+ onFileDelete,
+ onFileRemove,
+ onFileView,
+ deletingFiles,
+ previewingFiles,
+ removingFiles,
+ workflowId
+}) => {
+ return (
+
+ {/* Metadata row */}
+ {showMetadata && (
+
+
+
+ )}
+
+ {/* Content row */}
+
+ {message.message && (
+
{message.message}
+ )}
+
+ {message.summary && message.summary !== message.message && (
+
+ Summary: {message.summary}
+
+ )}
+
+ {/* Action information */}
+
+
+
+ {/* Documents row */}
+ {showDocuments && message.documents && Array.isArray(message.documents) && message.documents.length > 0 && (
+
+ {message.documentsLabel && (
+
{message.documentsLabel}
+ )}
+
+ {message.documents.map((doc) => (
+
+ {renderDocument ? renderDocument(doc, message) : (
+
+ )}
+
+ ))}
+
+
+ )}
+
+ );
+};
+
diff --git a/src/components/UiComponents/Messages/MessageParts/ActionInfo.tsx b/src/components/UiComponents/Messages/MessageParts/ActionInfo.tsx
new file mode 100644
index 0000000..1bff6d4
--- /dev/null
+++ b/src/components/UiComponents/Messages/MessageParts/ActionInfo.tsx
@@ -0,0 +1,34 @@
+import React from 'react';
+import { Message } from '../MessagesTypes';
+import styles from '../Messages.module.css';
+
+export interface ActionInfoProps {
+ message: Message;
+ className?: string;
+}
+
+/**
+ * Renders action information if present
+ */
+export const ActionInfo: React.FC = ({ message, className }) => {
+ if (!message.actionName && !message.actionMethod) return null;
+
+ return (
+
+ {message.actionName && (
+ {message.actionName}
+ )}
+ {message.actionMethod && (
+ {message.actionMethod}
+ )}
+ {(message.roundNumber !== undefined || message.taskNumber !== undefined || message.actionNumber !== undefined) && (
+
+ {message.roundNumber !== undefined && `Round ${message.roundNumber}`}
+ {message.taskNumber !== undefined && ` • Task ${message.taskNumber}`}
+ {message.actionNumber !== undefined && ` • Action ${message.actionNumber}`}
+
+ )}
+
+ );
+};
+
diff --git a/src/components/UiComponents/Messages/MessageParts/DocumentItem.tsx b/src/components/UiComponents/Messages/MessageParts/DocumentItem.tsx
new file mode 100644
index 0000000..3209f8d
--- /dev/null
+++ b/src/components/UiComponents/Messages/MessageParts/DocumentItem.tsx
@@ -0,0 +1,131 @@
+import React, { useMemo } from 'react';
+import { MessageDocument, Message } from '../MessagesTypes';
+import { formatFileSize } from '../MessageUtils';
+import { ViewActionButton, DeleteActionButton, RemoveActionButton } from '../../../FormGenerator/ActionButtons';
+import { WorkflowFile } from '../../../../hooks/usePlayground';
+import styles from '../Messages.module.css';
+
+export interface DocumentItemProps {
+ document: MessageDocument;
+ message: Message;
+ className?: string;
+ onFileDelete?: (file: WorkflowFile) => Promise;
+ onFileRemove?: (file: WorkflowFile) => Promise;
+ onFileView?: (file: WorkflowFile) => Promise;
+ deletingFiles?: Set;
+ previewingFiles?: Set;
+ removingFiles?: Set;
+ workflowId?: string;
+}
+
+/**
+ * Renders a single document attachment with action buttons
+ */
+export const DocumentItem: React.FC = ({
+ document,
+ message,
+ className,
+ onFileDelete,
+ onFileRemove,
+ onFileView,
+ deletingFiles = new Set(),
+ previewingFiles = new Set(),
+ removingFiles = new Set(),
+ workflowId
+}) => {
+ // Convert MessageDocument to WorkflowFile format for compatibility with action buttons
+ const workflowFile: WorkflowFile = useMemo(() => ({
+ id: document.id,
+ fileId: document.fileId,
+ fileName: document.fileName,
+ fileSize: document.fileSize,
+ mimeType: document.mimeType,
+ messageId: document.messageId,
+ source: 'user_uploaded' // Default to user_uploaded, can be enhanced later
+ }), [document]);
+
+ const isDeleting = deletingFiles.has(document.fileId);
+ const isPreviewing = previewingFiles.has(document.fileId);
+ const isRemoving = removingFiles.has(document.fileId);
+
+ // Create hookData object for action buttons
+ const hookData = useMemo(() => ({
+ handleDelete: async (fileId: string) => {
+ if (onFileDelete) {
+ await onFileDelete(workflowFile);
+ return true;
+ }
+ return false;
+ },
+ removeOptimistically: () => {
+ // Handled by parent component
+ },
+ refetch: async () => {
+ // Refetch handled by parent
+ },
+ deletingItems: deletingFiles,
+ previewingFiles: previewingFiles,
+ removingItems: removingFiles
+ }), [onFileDelete, workflowFile, deletingFiles, previewingFiles, removingFiles]);
+
+ const handleView = async () => {
+ if (onFileView) {
+ await onFileView(workflowFile);
+ }
+ };
+
+ const handleRemove = async () => {
+ if (onFileRemove) {
+ await onFileRemove(workflowFile);
+ }
+ };
+
+ return (
+
+
📎
+
+
{document.fileName}
+
+ {formatFileSize(document.fileSize)} • {document.mimeType}
+
+
+ {(onFileView || onFileDelete || onFileRemove) && (
+
+ {onFileView && (
+
+ )}
+ {onFileRemove && (
+
+ )}
+ {onFileDelete && (
+
+ )}
+
+ )}
+
+ );
+};
+
diff --git a/src/components/UiComponents/Messages/MessageParts/MessageMetadata.tsx b/src/components/UiComponents/Messages/MessageParts/MessageMetadata.tsx
new file mode 100644
index 0000000..64e8f00
--- /dev/null
+++ b/src/components/UiComponents/Messages/MessageParts/MessageMetadata.tsx
@@ -0,0 +1,44 @@
+import React from 'react';
+import { Message } from '../MessagesTypes';
+import { formatTimestamp, getStatusClass } from '../MessageUtils';
+import styles from '../Messages.module.css';
+
+export interface MessageMetadataProps {
+ message: Message;
+ showProgress?: boolean;
+ className?: string;
+}
+
+/**
+ * Renders message metadata (role, status, timestamp)
+ */
+export const MessageMetadata: React.FC = ({
+ message,
+ showProgress = true,
+ className
+}) => {
+ return (
+
+ {message.role && (
+
+ {message.role}
+
+ )}
+ {message.status && (
+
+ {message.status}
+
+ )}
+ {message.publishedAt && (
+ {formatTimestamp(message.publishedAt)}
+ )}
+ {showProgress && message.actionProgress && (
+ {message.actionProgress}
+ )}
+ {showProgress && message.taskProgress && (
+ {message.taskProgress}
+ )}
+
+ );
+};
+
diff --git a/src/components/UiComponents/Messages/MessageParts/index.ts b/src/components/UiComponents/Messages/MessageParts/index.ts
new file mode 100644
index 0000000..ba43658
--- /dev/null
+++ b/src/components/UiComponents/Messages/MessageParts/index.ts
@@ -0,0 +1,7 @@
+export { DocumentItem } from './DocumentItem';
+export { MessageMetadata } from './MessageMetadata';
+export { ActionInfo } from './ActionInfo';
+export type { DocumentItemProps } from './DocumentItem';
+export type { MessageMetadataProps } from './MessageMetadata';
+export type { ActionInfoProps } from './ActionInfo';
+
diff --git a/src/components/UiComponents/Messages/MessageUtils.ts b/src/components/UiComponents/Messages/MessageUtils.ts
new file mode 100644
index 0000000..3d40945
--- /dev/null
+++ b/src/components/UiComponents/Messages/MessageUtils.ts
@@ -0,0 +1,85 @@
+/**
+ * Utility functions for message formatting and styling
+ */
+
+/**
+ * Formats a timestamp to a readable date/time string
+ * Handles both Unix timestamps in seconds and milliseconds
+ * Formats exactly like FormGenerator component
+ */
+export const formatTimestamp = (timestamp?: number): string => {
+ if (!timestamp) return '';
+
+ try {
+ // Backend sends UTC timestamp in seconds (Unix timestamp)
+ // JavaScript Date constructor expects milliseconds
+ // Check if timestamp is in seconds (typical Unix timestamp range)
+ // Timestamps less than 10000000000 (year 2286) are likely in seconds
+ let date: Date;
+ if (timestamp < 10000000000) {
+ // Convert seconds to milliseconds
+ date = new Date(timestamp * 1000);
+ } else {
+ // Already in milliseconds
+ date = new Date(timestamp);
+ }
+
+ // Validate date
+ if (isNaN(date.getTime())) {
+ console.warn('Invalid timestamp:', timestamp);
+ return '';
+ }
+
+ // Format exactly like FormGenerator: YYYY-MM-DD HH:MM:SS GMT±HHMM
+ const year = date.getFullYear();
+ const month = String(date.getMonth() + 1).padStart(2, '0');
+ const day = String(date.getDate()).padStart(2, '0');
+ const hours = String(date.getHours()).padStart(2, '0');
+ const minutes = String(date.getMinutes()).padStart(2, '0');
+ const seconds = String(date.getSeconds()).padStart(2, '0');
+ const timezoneOffset = date.getTimezoneOffset();
+ const offsetHours = Math.floor(Math.abs(timezoneOffset) / 60);
+ const offsetMinutes = Math.abs(timezoneOffset) % 60;
+ const offsetSign = timezoneOffset <= 0 ? '+' : '-';
+ const timezone = `GMT${offsetSign}${String(offsetHours).padStart(2, '0')}${offsetMinutes > 0 ? ':' + String(offsetMinutes).padStart(2, '0') : ''}`;
+
+ return `${year}-${month}-${day} ${hours}:${minutes}:${seconds} ${timezone}`;
+ } catch (error) {
+ console.warn('Error formatting timestamp:', timestamp, error);
+ return '';
+ }
+};
+
+/**
+ * Formats file size to human-readable format
+ */
+export const formatFileSize = (bytes: number): string => {
+ if (bytes === 0) return '0 Bytes';
+ const k = 1024;
+ const sizes = ['Bytes', 'KB', 'MB', 'GB'];
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
+ return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
+};
+
+/**
+ * Gets status badge color class based on status
+ */
+export const getStatusClass = (baseStyles: any, status?: string, success?: boolean): string => {
+ if (success === false) return baseStyles.statusError;
+ if (success === true) return baseStyles.statusSuccess;
+
+ switch (status?.toLowerCase()) {
+ case 'completed':
+ case 'success':
+ return baseStyles.statusSuccess;
+ case 'failed':
+ case 'error':
+ return baseStyles.statusError;
+ case 'running':
+ case 'pending':
+ return baseStyles.statusPending;
+ default:
+ return baseStyles.statusDefault;
+ }
+};
+
diff --git a/src/components/UiComponents/Messages/Messages.module.css b/src/components/UiComponents/Messages/Messages.module.css
new file mode 100644
index 0000000..9692e02
--- /dev/null
+++ b/src/components/UiComponents/Messages/Messages.module.css
@@ -0,0 +1,326 @@
+.messagesContainer {
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+ width: 100%;
+ font-family: var(--font-family);
+ flex: 1;
+ overflow-y: auto;
+ min-height: 0;
+ padding: 16px 20px;
+ background-color: var(--color-surface);
+}
+
+.emptyState {
+ padding: 40px 20px;
+ text-align: center;
+ color: var(--color-gray);
+ font-size: 14px;
+ flex: 1;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.messageWrapper {
+ width: 100%;
+}
+
+.message {
+ display: flex;
+ width: 100%;
+ margin-bottom: 8px;
+}
+
+.messageUser {
+ justify-content: flex-end;
+}
+
+.messageAssistant {
+ justify-content: flex-start;
+}
+
+.messageBubble {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ max-width: 75%;
+ padding: 12px 16px;
+ border-radius: 18px;
+ word-wrap: break-word;
+}
+
+.messageUser .messageBubble {
+ background-color: var(--color-secondary);
+ color: white;
+ border-bottom-right-radius: 4px;
+}
+
+.messageAssistant .messageBubble {
+ background-color: var(--color-surface);
+ color: var(--color-text);
+ border-bottom-left-radius: 4px;
+ border: 1px solid var(--color-primary);
+}
+
+/* Message Metadata */
+.messageMetadata {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 8px;
+ font-size: 12px;
+}
+
+.roleBadge {
+ padding: 4px 8px;
+ border-radius: var(--object-radius-small);
+ font-weight: 500;
+ background-color: var(--color-primary);
+ color: var(--color-text);
+ text-transform: capitalize;
+}
+
+.roleBadge[data-role="user"] {
+ background-color: var(--color-secondary);
+ color: white;
+}
+
+.roleBadge[data-role="assistant"] {
+ background-color: var(--color-primary);
+}
+
+.statusBadge {
+ padding: 4px 8px;
+ border-radius: var(--object-radius-small);
+ font-weight: 500;
+ font-size: 11px;
+ text-transform: capitalize;
+}
+
+.statusSuccess {
+ background-color: #28a745;
+ color: white;
+}
+
+.statusError {
+ background-color: #dc3545;
+ color: white;
+}
+
+.statusPending {
+ background-color: #ffc107;
+ color: #212529;
+}
+
+.statusDefault {
+ background-color: var(--color-gray-disabled);
+ color: var(--color-text);
+}
+
+.timestamp {
+ color: var(--color-gray);
+ font-size: 11px;
+}
+
+.progress {
+ color: var(--color-secondary);
+ font-size: 11px;
+ font-weight: 500;
+}
+
+/* Action Info */
+.actionInfo {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 8px;
+ padding: 6px 10px;
+ background-color: rgba(0, 0, 0, 0.05);
+ border-radius: var(--object-radius-small);
+ font-size: 11px;
+ margin-top: 4px;
+}
+
+.actionName {
+ font-weight: 600;
+ color: var(--color-text);
+}
+
+.actionMethod {
+ color: var(--color-gray);
+ font-family: monospace;
+}
+
+.actionNumbers {
+ color: var(--color-gray);
+ font-size: 11px;
+}
+
+/* Message Content */
+.messageContent {
+ font-size: 14px;
+ line-height: 1.5;
+ white-space: pre-wrap;
+ word-wrap: break-word;
+}
+
+.messageUser .messageContent {
+ color: white;
+}
+
+.messageAssistant .messageContent {
+ color: var(--color-text);
+}
+
+.messageTimestamp {
+ font-size: 11px;
+ opacity: 0.7;
+ margin-top: 4px;
+ align-self: flex-end;
+}
+
+.messageUser .messageTimestamp {
+ color: rgba(255, 255, 255, 0.8);
+}
+
+.messageAssistant .messageTimestamp {
+ color: var(--color-gray);
+}
+
+.messageSummary {
+ padding: 8px 12px;
+ border-radius: var(--object-radius-small);
+ font-size: 12px;
+ line-height: 1.5;
+ margin-top: 4px;
+}
+
+.messageUser .messageSummary {
+ background-color: rgba(255, 255, 255, 0.2);
+ color: rgba(255, 255, 255, 0.9);
+}
+
+.messageUser .messageSummary strong {
+ color: white;
+ font-weight: 600;
+}
+
+.messageAssistant .messageSummary {
+ background-color: rgba(0, 0, 0, 0.05);
+ color: var(--color-text);
+}
+
+.messageAssistant .messageSummary strong {
+ color: var(--color-secondary);
+ font-weight: 600;
+}
+
+/* Documents */
+.documentsContainer {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ margin-top: 4px;
+}
+
+.documentsLabel {
+ font-size: 12px;
+ font-weight: 600;
+ color: var(--color-text);
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+}
+
+.documentsList {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+
+.documentItem {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ padding: 10px 12px;
+ background-color: var(--color-surface);
+ border-radius: var(--object-radius-small);
+ transition: all 0.2s ease;
+}
+
+.documentItem:hover {
+ background-color: var(--color-highlight-gray);
+}
+
+.documentIcon {
+ font-size: 18px;
+ flex-shrink: 0;
+}
+
+.documentInfo {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+ flex: 1;
+ min-width: 0;
+}
+
+.documentName {
+ font-size: 13px;
+ font-weight: 500;
+ color: var(--color-text);
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.documentMeta {
+ font-size: 11px;
+ color: var(--color-gray);
+}
+
+/* Dark theme support */
+[data-theme="dark"] .message {
+ background-color: var(--color-surface);
+ border-color: var(--color-primary);
+}
+
+[data-theme="dark"] .message:hover {
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
+}
+
+[data-theme="dark"] .actionInfo {
+ background-color: rgba(255, 255, 255, 0.05);
+}
+
+[data-theme="dark"] .messageSummary {
+ background-color: rgba(255, 255, 255, 0.05);
+}
+
+[data-theme="dark"] .documentItem {
+ background-color: rgba(255, 255, 255, 0.05);
+}
+
+[data-theme="dark"] .documentItem:hover {
+ background-color: rgba(255, 255, 255, 0.08);
+}
+
+/* Responsive design */
+@media (max-width: 640px) {
+ .message {
+ padding: 12px;
+ }
+
+ .messageMetadata {
+ font-size: 11px;
+ }
+
+ .messageContent {
+ font-size: 13px;
+ }
+
+ .documentItem {
+ padding: 8px 10px;
+ }
+}
+
diff --git a/src/components/UiComponents/Messages/Messages.tsx b/src/components/UiComponents/Messages/Messages.tsx
new file mode 100644
index 0000000..53a8725
--- /dev/null
+++ b/src/components/UiComponents/Messages/Messages.tsx
@@ -0,0 +1,112 @@
+import React from 'react';
+import { MessagesProps } from './MessagesTypes';
+import { ChatMessage } from './ChatMessages/ChatMessage';
+import { LogMessage } from './LogMessages/LogMessage';
+import styles from './Messages.module.css';
+
+/**
+ * Generic Messages component for displaying workflow messages
+ * Supports two variants: 'chat' (bubble UI) and 'log' (list/table UI)
+ *
+ * @example
+ * ```tsx
+ * // Chat style (default)
+ *
+ *
+ * // Log style
+ *
+ * ```
+ */
+const Messages: React.FC = ({
+ messages,
+ className = '',
+ variant = 'chat',
+ showDocuments = true,
+ showMetadata = true,
+ showProgress = true,
+ renderMessage,
+ renderDocument,
+ emptyMessage = 'No messages yet',
+ onFileDelete,
+ onFileRemove,
+ onFileView,
+ deletingFiles,
+ previewingFiles,
+ removingFiles,
+ workflowId
+}) => {
+ // Handle empty state
+ if (!messages || messages.length === 0) {
+ return (
+
+ );
+ }
+
+ return (
+
+ {messages.map((message, index) => {
+ // Use custom render function if provided
+ if (renderMessage) {
+ return (
+
+ {renderMessage(message, index)}
+
+ );
+ }
+
+ // Render based on variant
+ if (variant === 'log') {
+ return (
+
+ );
+ }
+
+ // Default to chat variant
+ return (
+
+ );
+ })}
+
+ );
+};
+
+export default Messages;
+
diff --git a/src/components/UiComponents/Messages/MessagesTypes.ts b/src/components/UiComponents/Messages/MessagesTypes.ts
new file mode 100644
index 0000000..f577741
--- /dev/null
+++ b/src/components/UiComponents/Messages/MessagesTypes.ts
@@ -0,0 +1,118 @@
+import type React from 'react';
+import { WorkflowFile } from '../../../hooks/usePlayground';
+
+/**
+ * Document interface representing file attachments in messages
+ */
+export interface MessageDocument {
+ id: string;
+ messageId: string;
+ fileId: string;
+ fileName: string;
+ fileSize: number;
+ mimeType: string;
+ roundNumber: number;
+ taskNumber: number;
+ actionNumber: number;
+ actionId: string;
+}
+
+/**
+ * Message interface matching the API response structure
+ */
+export interface Message {
+ id: string;
+ workflowId: string;
+ parentMessageId?: string;
+ documents?: MessageDocument[];
+ documentsLabel?: string;
+ message?: string;
+ summary?: string;
+ role?: string;
+ status?: string;
+ sequenceNr?: number;
+ publishedAt?: number;
+ success?: boolean;
+ actionId?: string;
+ actionMethod?: string;
+ actionName?: string;
+ roundNumber?: number;
+ taskNumber?: number;
+ actionNumber?: number;
+ taskProgress?: string;
+ actionProgress?: string;
+}
+
+/**
+ * Message display variant
+ */
+export type MessageVariant = 'chat' | 'log';
+
+/**
+ * Props for the Messages component
+ */
+export interface MessagesProps {
+ /**
+ * Array of messages to display
+ */
+ messages: Message[];
+
+ /**
+ * Display variant: 'chat' for bubble UI, 'log' for list/table UI
+ * @default 'chat'
+ */
+ variant?: MessageVariant;
+
+ /**
+ * Optional className for custom styling
+ */
+ className?: string;
+
+ /**
+ * Whether to show document attachments
+ * @default true
+ */
+ showDocuments?: boolean;
+
+ /**
+ * Whether to show metadata (role, status, timestamps)
+ * @default true
+ */
+ showMetadata?: boolean;
+
+ /**
+ * Whether to show progress indicators
+ * @default true
+ */
+ showProgress?: boolean;
+
+ /**
+ * Custom render function for message content
+ * If provided, this will be used instead of the default rendering
+ */
+ renderMessage?: (message: Message, index: number) => React.ReactNode;
+
+ /**
+ * Custom render function for documents
+ * If provided, this will be used instead of the default document rendering
+ */
+ renderDocument?: (document: MessageDocument, message: Message) => React.ReactNode;
+
+ /**
+ * Empty state message when no messages are present
+ * @default "No messages yet"
+ */
+ emptyMessage?: string;
+
+ /**
+ * File operation handlers for documents in messages
+ */
+ onFileDelete?: (file: WorkflowFile) => Promise;
+ onFileRemove?: (file: WorkflowFile) => Promise;
+ onFileView?: (file: WorkflowFile) => Promise;
+ deletingFiles?: Set;
+ previewingFiles?: Set;
+ removingFiles?: Set;
+ workflowId?: string;
+}
+
diff --git a/src/components/UiComponents/Messages/index.ts b/src/components/UiComponents/Messages/index.ts
new file mode 100644
index 0000000..042ba9f
--- /dev/null
+++ b/src/components/UiComponents/Messages/index.ts
@@ -0,0 +1,9 @@
+export { default as Messages } from './Messages';
+export { ChatMessage } from './ChatMessages/ChatMessage';
+export { LogMessage } from './LogMessages/LogMessage';
+export { DocumentItem, MessageMetadata, ActionInfo } from './MessageParts';
+export * from './MessageUtils';
+export type { MessagesProps, Message, MessageDocument, MessageVariant } from './MessagesTypes';
+export type { ChatMessageProps } from './ChatMessages/ChatMessage';
+export type { LogMessageProps } from './LogMessages/LogMessage';
+
diff --git a/src/components/UiComponents/ParcelInfoPanel/ParcelInfoPanel.module.css b/src/components/UiComponents/ParcelInfoPanel/ParcelInfoPanel.module.css
new file mode 100644
index 0000000..496cd69
--- /dev/null
+++ b/src/components/UiComponents/ParcelInfoPanel/ParcelInfoPanel.module.css
@@ -0,0 +1,170 @@
+.backdrop {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background-color: rgba(0, 0, 0, 0.3);
+ z-index: 999;
+ backdrop-filter: blur(2px);
+}
+
+.panel {
+ position: fixed;
+ top: 0;
+ right: 0;
+ width: 400px;
+ max-width: 90vw;
+ height: 100vh;
+ background-color: var(--color-bg, #ffffff);
+ box-shadow: -2px 0 8px rgba(0, 0, 0, 0.1);
+ z-index: 1000;
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+}
+
+.header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 1.5rem;
+ border-bottom: 1px solid var(--color-border, #e5e7eb);
+ background-color: var(--color-bg-secondary, #f9fafb);
+}
+
+.title {
+ margin: 0;
+ font-size: 1.25rem;
+ font-weight: 600;
+ color: var(--color-text, #111827);
+}
+
+.closeButton {
+ background: none;
+ border: none;
+ cursor: pointer;
+ padding: 0.5rem;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: var(--color-text-secondary, #6b7280);
+ border-radius: 4px;
+ transition: background-color 0.2s;
+}
+
+.closeButton:hover {
+ background-color: var(--color-hover, #f3f4f6);
+ color: var(--color-text, #111827);
+}
+
+.content {
+ flex: 1;
+ overflow-y: auto;
+ padding: 1.5rem;
+}
+
+.section {
+ margin-bottom: 2rem;
+}
+
+.section:last-child {
+ margin-bottom: 0;
+}
+
+.sectionTitle {
+ margin: 0 0 1rem 0;
+ font-size: 1.1rem;
+ font-weight: 600;
+ color: var(--color-text, #111827);
+ padding-bottom: 0.5rem;
+ border-bottom: 2px solid var(--color-primary, #3b82f6);
+}
+
+.infoGrid {
+ display: flex;
+ flex-direction: column;
+ gap: 0.75rem;
+}
+
+.infoItem {
+ display: flex;
+ flex-direction: column;
+ gap: 0.25rem;
+}
+
+.label {
+ font-size: 0.875rem;
+ font-weight: 500;
+ color: var(--color-text-secondary, #6b7280);
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+}
+
+.value {
+ font-size: 1rem;
+ color: var(--color-text, #111827);
+ word-break: break-word;
+}
+
+.subValue {
+ color: var(--color-text-secondary, #6b7280);
+ font-size: 0.875rem;
+}
+
+.link {
+ color: var(--color-primary, #3b82f6);
+ text-decoration: none;
+ font-weight: 500;
+ transition: color 0.2s;
+}
+
+.link:hover {
+ color: var(--color-primary-dark, #2563eb);
+ text-decoration: underline;
+}
+
+.adjacentList {
+ display: flex;
+ flex-direction: column;
+ gap: 0.75rem;
+}
+
+.adjacentItem {
+ padding: 0.75rem;
+ background-color: var(--color-bg-secondary, #f9fafb);
+ border: 1px solid var(--color-border, #e5e7eb);
+ border-radius: 6px;
+ transition: background-color 0.2s, border-color 0.2s;
+}
+
+.adjacentItem:hover {
+ background-color: var(--color-hover, #f3f4f6);
+ border-color: var(--color-primary, #3b82f6);
+}
+
+.adjacentHeader {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.adjacentNumber {
+ font-weight: 600;
+ color: var(--color-text, #111827);
+ font-size: 1rem;
+}
+
+.adjacentEgrid {
+ font-size: 0.875rem;
+ color: var(--color-text-secondary, #6b7280);
+ font-family: monospace;
+}
+
+@media (max-width: 768px) {
+ .panel {
+ width: 100vw;
+ max-width: 100vw;
+ }
+}
+
diff --git a/src/components/UiComponents/ParcelInfoPanel/ParcelInfoPanel.tsx b/src/components/UiComponents/ParcelInfoPanel/ParcelInfoPanel.tsx
new file mode 100644
index 0000000..8c813ab
--- /dev/null
+++ b/src/components/UiComponents/ParcelInfoPanel/ParcelInfoPanel.tsx
@@ -0,0 +1,216 @@
+import React from 'react';
+import { motion, AnimatePresence } from 'framer-motion';
+import { FaTimes } from 'react-icons/fa';
+import styles from './ParcelInfoPanel.module.css';
+
+export interface ParcelInfoPanelProps {
+ isOpen: boolean;
+ onClose: () => void;
+ parcelData: any;
+ adjacentParcels?: any[];
+}
+
+const ParcelInfoPanel: React.FC = ({
+ isOpen,
+ onClose,
+ parcelData,
+ adjacentParcels = []
+}) => {
+ if (!parcelData) return null;
+
+ return (
+
+ {isOpen && (
+ <>
+ {/* Backdrop */}
+
+
+ {/* Panel */}
+
+
+
Parzellen-Informationen
+
+
+
+
+
+
+ {/* Main Parcel */}
+
+ Ausgewählte Parzelle
+
+ {parcelData.parcel.id && (
+
+ ID:
+ {parcelData.parcel.id}
+
+ )}
+ {parcelData.parcel.number && (
+
+ Nummer:
+ {parcelData.parcel.number}
+
+ )}
+ {parcelData.parcel.name && (
+
+ Name:
+ {parcelData.parcel.name}
+
+ )}
+ {parcelData.parcel.egrid && (
+
+ EGRID:
+ {parcelData.parcel.egrid}
+
+ )}
+ {parcelData.parcel.identnd && (
+
+ IdentND:
+ {parcelData.parcel.identnd}
+
+ )}
+ {parcelData.parcel.address && (
+
+ Adresse:
+ {parcelData.parcel.address}
+
+ )}
+ {parcelData.parcel.canton && (
+
+ Kanton:
+ {parcelData.parcel.canton}
+
+ )}
+ {parcelData.parcel.municipality_name && (
+
+ Gemeinde:
+ {parcelData.parcel.municipality_name}
+
+ )}
+ {parcelData.parcel.municipality_code && (
+
+ Gemeinde-Code:
+ {parcelData.parcel.municipality_code}
+
+ )}
+ {parcelData.parcel.area_m2 !== undefined && (
+
+ Fläche:
+
+ {parcelData.parcel.area_m2.toFixed(2)} m²
+ {parcelData.parcel.area_m2 >= 10000 && (
+
+ {' '}({(parcelData.parcel.area_m2 / 10000).toFixed(2)} ha)
+
+ )}
+
+
+ )}
+ {parcelData.parcel.realestate_type && (
+
+ Grundstückstyp:
+ {parcelData.parcel.realestate_type}
+
+ )}
+ {parcelData.parcel.centroid && (
+
+ Zentrum (LV95):
+
+ {parcelData.parcel.centroid.x.toFixed(2)}, {parcelData.parcel.centroid.y.toFixed(2)}
+
+
+ )}
+ {parcelData.parcel.geoportal_url && (
+
+ )}
+
+
+
+ {/* Map View Info */}
+ {parcelData.map_view && (
+
+ Kartenansicht
+
+ {parcelData.map_view.center && (
+
+ Zentrum:
+
+ {parcelData.map_view.center.x.toFixed(2)}, {parcelData.map_view.center.y.toFixed(2)}
+
+
+ )}
+ {parcelData.map_view.zoom_bounds && (
+ <>
+
+ Bounds Min:
+
+ {parcelData.map_view.zoom_bounds.min_x.toFixed(2)}, {parcelData.map_view.zoom_bounds.min_y.toFixed(2)}
+
+
+
+ Bounds Max:
+
+ {parcelData.map_view.zoom_bounds.max_x.toFixed(2)}, {parcelData.map_view.zoom_bounds.max_y.toFixed(2)}
+
+
+ >
+ )}
+
+
+ )}
+
+ {/* Adjacent Parcels */}
+ {adjacentParcels.length > 0 && (
+
+
+ Angrenzende Parzellen ({adjacentParcels.length})
+
+
+ {adjacentParcels.map((adjacent, index) => (
+
+
+
+ {adjacent.number || adjacent.id}
+
+ {adjacent.egrid && (
+ {adjacent.egrid}
+ )}
+
+
+ ))}
+
+
+ )}
+
+
+ >
+ )}
+
+ );
+};
+
+export default ParcelInfoPanel;
+
diff --git a/src/components/UiComponents/ParcelInfoPanel/index.ts b/src/components/UiComponents/ParcelInfoPanel/index.ts
new file mode 100644
index 0000000..74bce7f
--- /dev/null
+++ b/src/components/UiComponents/ParcelInfoPanel/index.ts
@@ -0,0 +1,3 @@
+export { default as ParcelInfoPanel } from './ParcelInfoPanel';
+export type { ParcelInfoPanelProps } from './ParcelInfoPanel';
+
diff --git a/src/components/ui/Popup/EditForm.module.css b/src/components/UiComponents/Popup/EditForm.module.css
similarity index 100%
rename from src/components/ui/Popup/EditForm.module.css
rename to src/components/UiComponents/Popup/EditForm.module.css
diff --git a/src/components/ui/Popup/EditForm.tsx b/src/components/UiComponents/Popup/EditForm.tsx
similarity index 100%
rename from src/components/ui/Popup/EditForm.tsx
rename to src/components/UiComponents/Popup/EditForm.tsx
diff --git a/src/components/ui/Popup/Popup.module.css b/src/components/UiComponents/Popup/Popup.module.css
similarity index 100%
rename from src/components/ui/Popup/Popup.module.css
rename to src/components/UiComponents/Popup/Popup.module.css
diff --git a/src/components/ui/Popup/Popup.tsx b/src/components/UiComponents/Popup/Popup.tsx
similarity index 100%
rename from src/components/ui/Popup/Popup.tsx
rename to src/components/UiComponents/Popup/Popup.tsx
diff --git a/src/components/ui/Popup/ViewForm.module.css b/src/components/UiComponents/Popup/ViewForm.module.css
similarity index 100%
rename from src/components/ui/Popup/ViewForm.module.css
rename to src/components/UiComponents/Popup/ViewForm.module.css
diff --git a/src/components/ui/Popup/ViewForm.tsx b/src/components/UiComponents/Popup/ViewForm.tsx
similarity index 100%
rename from src/components/ui/Popup/ViewForm.tsx
rename to src/components/UiComponents/Popup/ViewForm.tsx
diff --git a/src/components/ui/Popup/index.ts b/src/components/UiComponents/Popup/index.ts
similarity index 100%
rename from src/components/ui/Popup/index.ts
rename to src/components/UiComponents/Popup/index.ts
diff --git a/src/components/UiComponents/TextField/TextField.module.css b/src/components/UiComponents/TextField/TextField.module.css
new file mode 100644
index 0000000..0415031
--- /dev/null
+++ b/src/components/UiComponents/TextField/TextField.module.css
@@ -0,0 +1,172 @@
+.textFieldWrapper {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ width: 100%;
+}
+
+.label {
+ font-size: 14px;
+ font-weight: 500;
+ color: var(--color-text);
+ display: block;
+}
+
+.labelRequired {
+ color: var(--color-text);
+}
+
+.required {
+ color: #ef4444;
+ margin-left: 4px;
+}
+
+.inputContainer {
+ position: relative;
+ width: 100%;
+}
+
+.input,
+.textarea {
+ width: 100%;
+ padding: 12px 16px;
+ border: 1px solid var(--color-primary);
+ border-radius: 25px;
+ font-size: 14px;
+ font-family: inherit;
+ color: var(--color-text);
+ background-color: var(--color-bg);
+ transition: all 0.2s ease;
+ box-sizing: border-box;
+ resize: none;
+ overflow-y: auto;
+ line-height: 1.5;
+}
+
+.textarea {
+ min-height: 44px; /* Match input height */
+ height: auto;
+ max-height: calc(1.5em * 5 + 24px); /* 5 lines + padding */
+ overflow-y: auto;
+}
+
+.input:focus,
+.textarea:focus {
+ outline: none;
+ border-color: var(--color-secondary);
+ box-shadow: 0 0 0 3px rgba(var(--color-primary-rgb, 0, 123, 255), 0.1);
+}
+
+.input:disabled,
+.textarea:disabled {
+ opacity: 0.6;
+ cursor: not-allowed;
+ background-color: var(--color-bg-disabled, #f5f5f5);
+}
+
+.input:read-only,
+.textarea:read-only {
+ cursor: default;
+ background-color: var(--color-bg-disabled, #f5f5f5);
+}
+
+.inputFieldError {
+ border-color: #ef4444;
+}
+
+.inputFieldError:focus {
+ border-color: #ef4444;
+ box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.1);
+}
+
+.textarea.inputFieldError:focus {
+ border-color: #ef4444;
+ box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.1);
+}
+
+/* Size variants */
+.sm .input,
+.sm .textarea {
+ padding: 8px 12px;
+ font-size: 13px;
+}
+
+.sm .textarea {
+ min-height: 36px;
+}
+
+.md .input,
+.md .textarea {
+ padding: 12px 16px;
+ font-size: 14px;
+}
+
+.md .textarea {
+ min-height: 44px;
+}
+
+.lg .input,
+.lg .textarea {
+ padding: 16px 20px;
+ font-size: 16px;
+}
+
+.lg .textarea {
+ min-height: 52px;
+}
+
+/* Helper text and error text */
+.helperText {
+ font-size: 12px;
+ color: #6b7280;
+ margin-top: 4px;
+}
+
+.errorText {
+ font-size: 12px;
+ color: #ef4444;
+ margin-top: 4px;
+}
+
+/* Dark theme support */
+[data-theme="dark"] .input,
+[data-theme="dark"] .textarea {
+ border-color: var(--color-primary);
+ background-color: var(--color-bg);
+}
+
+[data-theme="dark"] .input:disabled,
+[data-theme="dark"] .textarea:disabled {
+ background-color: rgba(255, 255, 255, 0.05);
+}
+
+[data-theme="dark"] .input:read-only,
+[data-theme="dark"] .textarea:read-only {
+ background-color: rgba(255, 255, 255, 0.05);
+}
+
+[data-theme="dark"] .label {
+ color: var(--color-text);
+}
+
+[data-theme="dark"] .helperText {
+ color: #9ca3af;
+}
+
+/* Responsive design */
+@media (max-width: 640px) {
+ .input,
+ .textarea {
+ font-size: 16px; /* Prevents zoom on iOS */
+ }
+
+ .label {
+ font-size: 13px;
+ }
+
+ .helperText,
+ .errorText {
+ font-size: 11px;
+ }
+}
+
diff --git a/src/components/UiComponents/TextField/TextField.tsx b/src/components/UiComponents/TextField/TextField.tsx
new file mode 100644
index 0000000..c33a7b1
--- /dev/null
+++ b/src/components/UiComponents/TextField/TextField.tsx
@@ -0,0 +1,131 @@
+import React, { useRef, useEffect } from 'react';
+import { BaseTextFieldProps } from './TextFieldTypes';
+import styles from './TextField.module.css';
+
+interface TextFieldProps extends BaseTextFieldProps {
+ // Allow all standard HTML input attributes
+ autoComplete?: string;
+ pattern?: string;
+ step?: string;
+ min?: string | number;
+ max?: string | number;
+}
+
+const TextField: React.FC = ({
+ value = '',
+ onChange,
+ placeholder,
+ disabled = false,
+ required = false,
+ readonly = false,
+ size = 'md',
+ error,
+ helperText,
+ label,
+ className = '',
+ type = 'text',
+ maxLength,
+ minLength,
+ autoFocus = false,
+ name,
+ id,
+ ...props
+}) => {
+ const textareaRef = useRef(null);
+
+ // Use textarea for text types, input for others
+ const useTextarea = type === 'text' || type === 'search';
+
+ // Simple auto-grow
+ useEffect(() => {
+ if (!useTextarea || !textareaRef.current) return;
+ const textarea = textareaRef.current;
+ textarea.style.height = 'auto';
+ textarea.style.height = `${textarea.scrollHeight}px`;
+ });
+
+ // Handle input change
+ const handleChange = (e: React.ChangeEvent) => {
+ if (!disabled && !readonly && onChange) {
+ onChange(e.target.value);
+ }
+ };
+
+ // Build classes for input container
+ const containerClasses = [
+ styles.inputContainer,
+ styles[size],
+ className
+ ].filter(Boolean).join(' ');
+
+ // Build classes for input/textarea
+ const inputClasses = [
+ useTextarea ? styles.textarea : styles.input,
+ error ? styles.inputFieldError : '',
+ ].filter(Boolean).join(' ');
+
+ return (
+
+ {/* Label */}
+ {label && (
+
+ {label}
+ {required && * }
+
+ )}
+
+ {/* Input or Textarea */}
+
+ {useTextarea ? (
+
+
+ {/* Error or Helper text */}
+ {error ? (
+
{error}
+ ) : helperText ? (
+
{helperText}
+ ) : null}
+
+ );
+};
+
+export default TextField;
+
diff --git a/src/components/UiComponents/TextField/TextFieldTypes.ts b/src/components/UiComponents/TextField/TextFieldTypes.ts
new file mode 100644
index 0000000..df47742
--- /dev/null
+++ b/src/components/UiComponents/TextField/TextFieldTypes.ts
@@ -0,0 +1,22 @@
+export type TextFieldSize = 'sm' | 'md' | 'lg';
+
+export interface BaseTextFieldProps {
+ value?: string;
+ onChange?: (value: string) => void;
+ placeholder?: string;
+ disabled?: boolean;
+ required?: boolean;
+ readonly?: boolean;
+ size?: TextFieldSize;
+ error?: string;
+ helperText?: string;
+ label?: string;
+ className?: string;
+ type?: 'text' | 'email' | 'password' | 'number' | 'tel' | 'url' | 'search';
+ maxLength?: number;
+ minLength?: number;
+ autoFocus?: boolean;
+ name?: string;
+ id?: string;
+}
+
diff --git a/src/components/UiComponents/TextField/index.ts b/src/components/UiComponents/TextField/index.ts
new file mode 100644
index 0000000..26767d5
--- /dev/null
+++ b/src/components/UiComponents/TextField/index.ts
@@ -0,0 +1,4 @@
+export { default as TextField } from './TextField';
+export { default } from './TextField';
+export * from './TextFieldTypes';
+
diff --git a/src/components/UiComponents/index.ts b/src/components/UiComponents/index.ts
new file mode 100644
index 0000000..a32e421
--- /dev/null
+++ b/src/components/UiComponents/index.ts
@@ -0,0 +1,12 @@
+export * from './Button';
+export * from './Button/UploadButton';
+export { default as MessageOverlay } from './InfoMessageOverlay';
+export type { MessageMode } from './InfoMessageOverlay';
+export * from './DragDropOverlay';
+export * from './TextField';
+export * from './Messages';
+export * from './DropdownSelect';
+export * from './EditFields';
+export * from './LocationInput';
+export * from './MapView';
+export * from './ParcelInfoPanel';
diff --git a/src/components/Workflows/WorkflowsTable.module.css b/src/components/Workflows/WorkflowsTable.module.css
deleted file mode 100644
index 164e10e..0000000
--- a/src/components/Workflows/WorkflowsTable.module.css
+++ /dev/null
@@ -1,175 +0,0 @@
-.workflowsTable {
- width: 100%;
- height: 100%;
- display: flex;
- flex-direction: column;
-}
-
-.workflowsFormGenerator {
- flex: 1;
- height: 100%;
-}
-
-/* Error state styling */
-.errorState {
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- padding: 2rem;
- text-align: center;
- color: var(--color-error, #dc3545);
- background-color: var(--color-error-bg, #f8d7da);
- border: 1px solid var(--color-error-border, #f5c6cb);
- border-radius: 8px;
- margin: 1rem;
-}
-
-.retryButton {
- padding: 0.5rem 1rem;
- background-color: var(--color-primary, #007bff);
- color: white;
- border: none;
- border-radius: 4px;
- cursor: pointer;
- margin-top: 1rem;
- transition: background-color 0.2s ease;
-}
-
-.retryButton:hover {
- background-color: var(--color-primary-dark, #0056b3);
-}
-
-/* Table cell styling */
-.workflowId {
- font-family: 'Courier New', monospace;
- font-size: 0.9em;
- color: var(--color-gray);
- cursor: help;
-}
-
-.workflowName {
- font-weight: 500;
- color: var(--color-text);
-}
-
-.statusBadge {
- display: inline-block;
- padding: 0.25rem 0.5rem;
- border-radius: 12px;
- font-size: 0.85em;
- font-weight: 500;
- text-transform: capitalize;
- text-align: center;
- min-width: 60px;
-}
-
-.status-running {
- background-color: var(--color-gray);
- color: white;
-}
-
-.status-completed {
- background-color: var(--color-secondary);
- color: white;
-}
-
-.status-failed {
- background-color: var(--color-red);
- color: white;
-}
-
-.status-stopped {
- background-color: #f5f5f5;
- color: #495057;
- border: 1px solid #dee2e6;
-}
-
-.status-pending {
- background-color: #fff3cd;
- color: #856404;
- border: 1px solid #ffeaa7;
-}
-
-.roundNumber {
- font-weight: 400;
- color: var(--color-text);
- background-color: var(--color-bg);
- padding: 0.25rem 0.5rem;
- font-size: 0.9em;
- min-width: 24px;
- text-align: center;
- display: inline-block;
-}
-
-.messageCount {
- font-weight: 500;
- color: var(--color-text);
- background-color: var(--color-bg);
- padding: 0.25rem 0.5rem;
- border-radius: 8px;
- font-size: 0.9em;
- min-width: 24px;
- text-align: center;
- display: inline-block;
-}
-
-/* Responsive design */
-@media (max-width: 768px) {
- .workflowId {
- font-size: 0.8em;
- }
-
- .statusBadge {
- font-size: 0.8em;
- padding: 0.2rem 0.4rem;
- }
-
- .roundNumber,
- .messageCount {
- font-size: 0.8em;
- padding: 0.2rem 0.4rem;
- }
-}
-
-/* Dark mode support */
-@media (prefers-color-scheme: dark) {
- .workflowId {
- color: var(--color-gray);
- }
-
- .workflowName {
- color: var(--color-text);
- }
-
- .status-running {
- background-color: var(--color-gray);
- color: white;
- }
-
- .status-completed {
- background-color: var(--color-secondary);
- color: white;
- }
-
- .status-failed {
- background-color: var(--color-red);
- color: white;
- }
-
- .status-stopped {
- background-color: var(--color-primary);
- color: white;
- }
-
-
- .roundNumber {
- background-color: var(--color-bg);
- color: var(--color-text);
- }
-
- .messageCount {
- background-color: var(--color-gray);
- color: white;
- }
-}
diff --git a/src/components/Workflows/WorkflowsTable.tsx b/src/components/Workflows/WorkflowsTable.tsx
deleted file mode 100644
index 64ea70f..0000000
--- a/src/components/Workflows/WorkflowsTable.tsx
+++ /dev/null
@@ -1,71 +0,0 @@
-
-
-import { FormGenerator } from '../FormGenerator/FormGenerator';
-import { Popup, EditForm } from '../ui/Popup';
-import { useWorkflowsLogic } from './workflowsLogic';
-import { WorkflowsTableProps } from './workflowsTypes';
-import styles from './WorkflowsTable.module.css';
-
-function WorkflowsTable({ className = '' }: WorkflowsTableProps) {
- const logic = useWorkflowsLogic();
-
- // Show error state
- if (logic.error) {
- return (
-
-
-
Error loading workflows: {logic.error}
-
- Retry
-
-
-
- );
- }
-
- return (
-
-
{
- // TODO: Navigate to workflow detail view
- console.log('Clicked workflow:', workflow);
- }}
- />
-
- {/* Edit Workflow Modal */}
-
- {logic.editingWorkflow && (
-
- )}
-
-
- );
-}
-
-export default WorkflowsTable;
diff --git a/src/components/Workflows/index.ts b/src/components/Workflows/index.ts
deleted file mode 100644
index dc20a54..0000000
--- a/src/components/Workflows/index.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-export { default as WorkflowsTable } from './WorkflowsTable';
-export { useWorkflowsLogic } from './workflowsLogic';
-export * from './workflowsTypes';
\ No newline at end of file
diff --git a/src/components/Workflows/workflowsLogic.tsx b/src/components/Workflows/workflowsLogic.tsx
deleted file mode 100644
index 69f1d59..0000000
--- a/src/components/Workflows/workflowsLogic.tsx
+++ /dev/null
@@ -1,450 +0,0 @@
-import { useState, useEffect, useMemo } from 'react';
-import { useNavigate } from 'react-router-dom';
-
-import { IoIosTrash, IoIosPlay } from 'react-icons/io';
-import { MdModeEdit } from 'react-icons/md';
-
-import { useWorkflows, useWorkflowOperations, Workflow } from '../../hooks/useWorkflows';
-import { useApiRequest } from '../../hooks/useApi';
-import { useLanguage } from '../../contexts/LanguageContext';
-import type { EditFieldConfig } from '../ui/Popup/EditForm';
-
-import type {
- WorkflowsLogicReturn,
- WorkflowMessageCounts,
- WorkflowActionConfig,
- WorkflowColumnConfig
-} from './workflowsTypes';
-import styles from './WorkflowsTable.module.css';
-
-export function useWorkflowsLogic(): WorkflowsLogicReturn {
- const { workflows, loading, error, refetch } = useWorkflows();
- const navigate = useNavigate();
- const { t } = useLanguage();
-
- // State to track message counts for each workflow
- const [workflowMessageCounts, setWorkflowMessageCounts] = useState({});
- const { request } = useApiRequest();
-
- // Debug: Log workflow data to see the actual structure
- console.log('Workflows data:', workflows);
- if (workflows && workflows.length > 0) {
- const firstWorkflow = workflows[0];
- console.log('First workflow object:', firstWorkflow);
- console.log('First workflow keys:', Object.keys(firstWorkflow));
- console.log('First workflow stats:', firstWorkflow.stats);
- if (firstWorkflow.stats) {
- console.log('Stats keys:', Object.keys(firstWorkflow.stats));
- console.log('Stats object:', firstWorkflow.stats);
- }
- console.log('First workflow messages array:', firstWorkflow.messages, 'length:', firstWorkflow.messages?.length);
- }
-
- const {
- deleteWorkflow,
- updateWorkflow,
- deletingWorkflows
- } = useWorkflowOperations();
-
- // Edit modal state
- const [editModalOpen, setEditModalOpen] = useState(false);
- const [editingWorkflow, setEditingWorkflow] = useState(null);
-
- // Function to fetch message count for a single workflow
- const fetchMessageCount = async (workflowId: string) => {
- try {
- console.log(`Fetching messages for workflow: ${workflowId}`);
- const messages = await request({
- url: `/api/workflows/${workflowId}/messages`,
- method: 'get'
- });
-
- console.log(`Messages for ${workflowId}:`, messages, 'length:', messages?.length);
- const count = Array.isArray(messages) ? messages.length : 0;
- console.log(`Setting message count for ${workflowId}:`, count);
- setWorkflowMessageCounts(prev => ({
- ...prev,
- [workflowId]: count
- }));
- } catch (error) {
- console.error(`Failed to fetch message count for workflow ${workflowId}:`, error);
- // Set count to 0 for failed requests
- setWorkflowMessageCounts(prev => ({
- ...prev,
- [workflowId]: 0
- }));
- }
- };
-
- // Effect to fetch message counts when workflows change
- useEffect(() => {
- if (workflows && workflows.length > 0) {
- workflows.forEach(workflow => {
- // Only fetch if we don't already have the count
- if (!(workflow.id in workflowMessageCounts)) {
- fetchMessageCount(workflow.id);
- }
- });
- }
- }, [workflows]); // Don't include workflowMessageCounts to avoid infinite loop
-
- // Configure edit fields for workflow name editing
- const editWorkflowFields: EditFieldConfig[] = useMemo(() => [
- {
- key: 'name',
- label: t('workflows.field.name', 'Workflow Name'),
- type: 'string',
- editable: true,
- required: true,
- validator: (value: string) => {
- if (!value || value.trim() === '') {
- return t('workflows.validation.nameRequired', 'Workflow name cannot be empty');
- }
- if (value.length > 100) {
- return t('workflows.validation.nameTooLong', 'Workflow name cannot exceed 100 characters');
- }
- return null;
- }
- }
- ], [t]);
-
- // Configure columns for the workflows table
- const columns: WorkflowColumnConfig[] = useMemo(() => [
- {
- key: 'id',
- label: t('workflows.column.id'),
- type: 'string',
- width: 180,
- minWidth: 150,
- maxWidth: 250,
- sortable: true,
- filterable: true,
- searchable: true,
- formatter: (value: string) => (
-
- {value.length > 8 ? `${value.substring(0, 8)}...` : value}
-
- )
- },
- {
- key: 'name',
- label: t('workflows.column.name'),
- type: 'string',
- width: 200,
- minWidth: 150,
- maxWidth: 300,
- sortable: true,
- filterable: true,
- searchable: true,
- formatter: (value: string | undefined) => (
-
- {value || t('workflows.unnamed')}
-
- )
- },
- {
- key: 'status',
- label: t('workflows.column.status'),
- type: 'enum',
- width: 120,
- minWidth: 100,
- maxWidth: 150,
- sortable: true,
- filterable: true,
- filterOptions: ['running', 'completed', 'failed', 'stopped', 'pending'],
- formatter: (value: string) => (
-
- {t(`workflows.status.${value}`, value)}
-
- )
- },
- {
- key: 'currentRound',
- label: t('workflows.column.round'),
- type: 'number',
- width: 80,
- minWidth: 60,
- maxWidth: 100,
- sortable: true,
- filterable: true,
- formatter: (value: number | undefined) => (
-
- {value || 1}
-
- )
- },
- {
- key: 'startedAt',
- label: t('workflows.column.started'),
- type: 'date',
- width: 140,
- minWidth: 120,
- maxWidth: 180,
- sortable: true,
- filterable: true,
- formatter: (value: number | undefined) => {
- if (!value) return '-';
- try {
- // Backend sends UTC timestamp in seconds (float), convert to milliseconds for Date constructor
- const date = new Date(value * 1000);
-
- // Check if date is valid
- if (isNaN(date.getTime())) {
- console.warn('Invalid startedAt date:', value);
- return '-';
- }
- const year = date.getFullYear();
- const month = String(date.getMonth() + 1).padStart(2, '0');
- const day = String(date.getDate()).padStart(2, '0');
- const hours = String(date.getHours()).padStart(2, '0');
- const minutes = String(date.getMinutes()).padStart(2, '0');
- const seconds = String(date.getSeconds()).padStart(2, '0');
- const timezoneOffset = date.getTimezoneOffset();
- const offsetHours = Math.floor(Math.abs(timezoneOffset) / 60);
- const offsetMinutes = Math.abs(timezoneOffset) % 60;
- const offsetSign = timezoneOffset <= 0 ? '+' : '-';
- const timezone = `GMT${offsetSign}${offsetHours}${offsetMinutes > 0 ? ':' + offsetMinutes.toString().padStart(2, '0') : ''}`;
- return `${year}-${month}-${day} ${hours}:${minutes}:${seconds} ${timezone}`;
- } catch (error) {
- console.warn('Error parsing startedAt date:', value, error);
- return '-';
- }
- }
- },
- {
- key: 'lastActivity',
- label: t('workflows.column.lastActivity'),
- type: 'date',
- width: 140,
- minWidth: 120,
- maxWidth: 180,
- sortable: true,
- filterable: true,
- formatter: (value: number | undefined) => {
- if (!value) return '-';
- try {
- // Backend sends UTC timestamp in seconds (float), convert to milliseconds for Date constructor
- const date = new Date(value * 1000);
-
- // Check if date is valid
- if (isNaN(date.getTime())) {
- console.warn('Invalid lastActivity date:', value);
- return '-';
- }
- const year = date.getFullYear();
- const month = String(date.getMonth() + 1).padStart(2, '0');
- const day = String(date.getDate()).padStart(2, '0');
- const hours = String(date.getHours()).padStart(2, '0');
- const minutes = String(date.getMinutes()).padStart(2, '0');
- const seconds = String(date.getSeconds()).padStart(2, '0');
- const timezoneOffset = date.getTimezoneOffset();
- const offsetHours = Math.floor(Math.abs(timezoneOffset) / 60);
- const offsetMinutes = Math.abs(timezoneOffset) % 60;
- const offsetSign = timezoneOffset <= 0 ? '+' : '-';
- const timezone = `GMT${offsetSign}${offsetHours}${offsetMinutes > 0 ? ':' + offsetMinutes.toString().padStart(2, '0') : ''}`;
- return `${year}-${month}-${day} ${hours}:${minutes}:${seconds} ${timezone}`;
- } catch (error) {
- console.warn('Error parsing lastActivity date:', value, error);
- return '-';
- }
- }
- },
- {
- key: 'messages',
- label: t('workflows.column.messages'),
- type: 'number',
- width: 100,
- minWidth: 80,
- maxWidth: 120,
- sortable: true,
- filterable: false,
- formatter: (_value: any, row: any) => {
- // Get message count from our fetched data, just like in Dashboard component
- const workflowId = row?.id;
- const messageCount = workflowId ? workflowMessageCounts[workflowId] : undefined;
-
- console.log(`Messages formatter for ${workflowId}:`, {
- workflowId,
- messageCount,
- hasInCache: workflowId in workflowMessageCounts,
- allCounts: workflowMessageCounts
- });
-
- // Show the count if available, otherwise show loading indicator or dash
- let displayValue;
- if (messageCount !== undefined) {
- displayValue = messageCount;
- } else if (workflowId && workflows.some(w => w.id === workflowId)) {
- // We're still loading this count
- displayValue = '...';
- } else {
- displayValue = '-';
- }
-
- return (
-
- {displayValue}
-
- );
- }
- }
- ], [t, workflowMessageCounts, workflows]);
-
- // Handle workflow actions
- const handleDeleteWorkflow = async (workflow: Workflow) => {
- const workflowName = workflow.name || workflow.id;
- if (window.confirm(t('workflows.delete.confirm').replace('{name}', workflowName))) {
- const success = await deleteWorkflow(workflow.id);
- if (success) {
- refetch(); // Refresh the workflows list
- }
- }
- };
-
- // Handle single workflow deletion for bulk delete
- const handleDeleteSingle = async (workflow: Workflow) => {
- const workflowName = workflow.name || workflow.id;
- if (window.confirm(t('workflows.delete.confirm', 'Are you sure you want to delete "{name}"?').replace('{name}', workflowName))) {
- const success = await deleteWorkflow(workflow.id);
- if (success) {
- refetch(); // Refresh the workflows list
- } else {
- console.error('Delete failed for workflow:', workflow.id);
- }
- }
- };
-
- // Handle multiple workflow deletion
- const handleDeleteMultiple = async (workflowsToDelete: Workflow[]) => {
- const workflowCount = workflowsToDelete.length;
- if (window.confirm(t('workflows.delete.confirmMultiple', 'Are you sure you want to delete {count} workflows?').replace('{count}', workflowCount.toString()))) {
- // Start all delete operations simultaneously
- const deletePromises = workflowsToDelete.map(async (workflow) => {
- try {
- const success = await deleteWorkflow(workflow.id);
- return { workflowId: workflow.id, success };
- } catch (error) {
- console.error('Failed to delete workflow:', workflow.id, error);
- return { workflowId: workflow.id, success: false };
- }
- });
-
- // Wait for all deletions to complete
- const results = await Promise.all(deletePromises);
-
- // Check if any deletions failed
- const failedDeletions = results.filter(result => !result.success);
- if (failedDeletions.length > 0) {
- console.error('Some workflow deletions failed:', failedDeletions);
- }
-
- // Refresh the workflow list regardless of individual failures
- refetch();
- }
- };
-
- // Handle edit workflow
- const handleEditWorkflow = (workflow: Workflow) => {
- setEditingWorkflow(workflow);
- setEditModalOpen(true);
- };
-
- // Handle save workflow
- const handleSaveWorkflow = async (updatedWorkflow: Workflow) => {
- if (!editingWorkflow) return;
-
- try {
- // Call API to update workflow name
- const result = await updateWorkflow(editingWorkflow.id, {
- name: updatedWorkflow.name
- });
-
- if (result.success) {
- // Close modal
- setEditModalOpen(false);
- setEditingWorkflow(null);
-
- // Refresh workflow list
- await refetch();
-
- // Notify other components that workflows have been updated
- window.dispatchEvent(new CustomEvent('workflowUpdated', {
- detail: { workflowId: editingWorkflow.id, newName: updatedWorkflow.name }
- }));
- } else {
- console.error('Failed to update workflow:', result.error);
- // TODO: Show error message to user
- }
- } catch (error) {
- console.error('Failed to update workflow:', error);
- // TODO: Show error message to user
- }
- };
-
- // Handle cancel edit
- const handleCancelEdit = () => {
- setEditModalOpen(false);
- setEditingWorkflow(null);
- };
-
- // Handle play workflow - navigate to dashboard with workflow ID
- const handlePlayWorkflow = (workflow: Workflow) => {
- // Navigate to dashboard with workflow ID as URL parameter
- navigate(`/dashboard?workflowId=${workflow.id}`);
- };
-
- // Configure action buttons
- const actions: WorkflowActionConfig[] = useMemo(() => [
- {
- label: t('workflows.action.play'),
- icon: (_row: Workflow) => {
- return ;
- },
- onClick: (row: Workflow) => {
- handlePlayWorkflow(row);
- }
- },
- {
- label: t('workflows.action.edit'),
- icon: (_row: Workflow) => {
- return ;
- },
- onClick: (row: Workflow) => {
- handleEditWorkflow(row);
- }
- },
- {
- label: t('workflows.action.delete'),
- icon: (_row: Workflow) => {
- return ;
- }
- // onClick is handled by FormGenerator for delete confirmation
- },
- ], [t, deletingWorkflows, handleDeleteWorkflow, handleEditWorkflow, handlePlayWorkflow]);
-
- return {
- // Data
- workflows,
- loading,
- error,
- workflowMessageCounts,
- editModalOpen,
- editingWorkflow,
- editWorkflowFields,
-
- // Actions
- handleDeleteSingle,
- handleDeleteMultiple,
- handleEditWorkflow,
- handleSaveWorkflow,
- handleCancelEdit,
- handlePlayWorkflow,
-
- // Refetch function
- refetch,
-
- // Additional data for rendering
- columns: columns as any,
- actions: actions as any
- };
-}
diff --git a/src/components/Workflows/workflowsTypes.ts b/src/components/Workflows/workflowsTypes.ts
deleted file mode 100644
index ed986d0..0000000
--- a/src/components/Workflows/workflowsTypes.ts
+++ /dev/null
@@ -1,59 +0,0 @@
-import React from 'react';
-import { Workflow } from '../../hooks/useWorkflows';
-
-export interface WorkflowsTableProps {
- className?: string;
-}
-
-export interface WorkflowMessageCounts {
- [workflowId: string]: number;
-}
-
-export interface WorkflowEditState {
- editModalOpen: boolean;
- editingWorkflow: Workflow | null;
-}
-
-export interface WorkflowsLogicReturn {
- // Data
- workflows: Workflow[];
- loading: boolean;
- error: string | null;
- workflowMessageCounts: WorkflowMessageCounts;
- editModalOpen: boolean;
- editingWorkflow: Workflow | null;
- editWorkflowFields: any[];
- columns: any[];
- actions: any[];
-
- // Actions
- handleDeleteSingle: (workflow: Workflow) => Promise;
- handleDeleteMultiple: (workflows: Workflow[]) => Promise;
- handleEditWorkflow: (workflow: Workflow) => void;
- handleSaveWorkflow: (updatedWorkflow: Workflow) => Promise;
- handleCancelEdit: () => void;
- handlePlayWorkflow: (workflow: Workflow) => void;
-
- // Refetch function
- refetch: () => Promise;
-}
-
-export interface WorkflowActionConfig {
- label: string;
- icon: (row: Workflow) => React.ReactElement;
- onClick?: (row: Workflow) => void;
-}
-
-export interface WorkflowColumnConfig {
- key: string;
- label: string;
- type: string;
- width: number;
- minWidth: number;
- maxWidth: number;
- sortable: boolean;
- filterable: boolean;
- searchable?: boolean;
- filterOptions?: string[];
- formatter: (value: any, row?: any) => React.ReactElement | string;
-}
diff --git a/src/components/settings/settingsSpeech.module.css b/src/components/settings/settingsSpeech.module.css
deleted file mode 100644
index 8cd4dcf..0000000
--- a/src/components/settings/settingsSpeech.module.css
+++ /dev/null
@@ -1,290 +0,0 @@
-/* Speech Settings Form - matches userInfoForm styling */
-.speechSettingsForm {
- display: flex;
- flex-direction: column;
- gap: 20px;
- padding: 20px;
- background: var(--color-bg);
- border-radius: 25px;
- border: 1px solid var(--color-primary);
- margin-bottom: 20px;
-}
-
-.settingLabel {
- font-size: 1rem;
- font-weight: 500;
- color: var(--color-text);
- font-family: var(--font-family);
-}
-
-.settingDescription {
- font-size: 0.875rem;
- color: var(--color-primary);
- font-family: var(--font-family);
-}
-
-.loading {
- text-align: center;
- padding: 2rem;
- color: var(--color-text);
- font-size: 1rem;
-}
-
-.noData {
- text-align: center;
- padding: 2rem;
- background: var(--color-bg);
- border-radius: 20px;
- border: 1px solid var(--color-secondary);
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
-}
-
-.noData p {
- margin: 0 0 1.5rem 0;
- color: var(--color-text);
- font-size: 1rem;
-}
-
-.signUpButton {
- padding: 12px 24px;
- border-radius: 25px;
- border: none;
- background: var(--color-secondary);
- color: white;
- cursor: pointer;
- transition: all 0.3s ease;
- font-family: var(--font-family);
- font-size: 0.875rem;
- font-weight: 500;
- min-width: 120px;
-}
-
-.signUpButton:hover:not(:disabled) {
- background: var(--color-secondary);
- border-color: var(--color-secondary);
- box-shadow: 0 4px 12px rgba(63, 81, 181, 0.3);
- transform: translateY(-2px);
-}
-
-.updateMessage {
- padding: 12px 16px;
- border-radius: 8px;
- font-size: 0.875rem;
- font-weight: 500;
- margin-bottom: 10px;
-}
-
-.successMessage {
- background-color: rgba(34, 197, 94, 0.1);
- color: #16a34a;
- border: 1px solid rgba(34, 197, 94, 0.2);
-}
-
-.errorMessage {
- background-color: rgba(239, 68, 68, 0.1);
- color: #dc2626;
- border: 1px solid rgba(239, 68, 68, 0.2);
-}
-
-/* Section styling */
-.section {
- display: flex;
- flex-direction: column;
- gap: 15px;
- padding: 15px;
- background: var(--color-bg);
- border-radius: 15px;
- border: 1px solid var(--color-secondary);
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
-}
-
-.sectionHeader {
- display: flex;
- align-items: center;
- gap: 8px;
- padding-bottom: 8px;
- border-bottom: 1px solid var(--color-secondary);
-}
-
-.sectionIcon {
- font-size: 1.1rem;
- color: var(--color-secondary);
-}
-
-.sectionTitle {
- font-size: 1rem;
- font-weight: 600;
- color: var(--color-text);
- margin: 0;
- font-family: var(--font-family);
-}
-
-/* Form styling - matches userInfoForm */
-.formRow {
- display: flex;
- gap: 20px;
- flex-wrap: wrap;
-}
-
-.formField {
- display: flex;
- flex-direction: column;
- gap: 8px;
- flex: 1;
- min-width: 250px;
-}
-
-.fieldLabel {
- font-size: 0.875rem;
- font-weight: 500;
- color: var(--color-text);
- font-family: var(--font-family);
-}
-
-.formInput,
-.formSelect {
- padding: 12px 16px;
- border-radius: 25px;
- border: 1px solid var(--color-primary);
- background: var(--color-bg);
- color: var(--color-text);
- font-family: var(--font-family);
- font-size: 0.875rem;
- transition: all 0.3s ease;
- outline: none;
-}
-
-.formInput:focus,
-.formSelect:focus {
- border-color: var(--color-primary);
- box-shadow: 0 0 0 3px rgba(63, 81, 181, 0.1);
-}
-
-.formInput:hover,
-.formSelect:hover {
- border-color: var(--color-secondary);
-}
-
-.formSelect {
- appearance: none;
- background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='m6 8 4 4 4-4'/%3e%3c/svg%3e");
- background-position: right 0.5rem center;
- background-repeat: no-repeat;
- background-size: 1.5em 1.5em;
- padding-right: 2.5rem;
-}
-
-.formSelect option {
- background: var(--color-bg);
- color: var(--color-text);
- padding: 10px;
-}
-
-/* Actions styling - matches userInfoForm */
-.formActions {
- display: flex;
- justify-content: flex-end;
- gap: 12px;
- padding-top: 10px;
-}
-
-.saveButton {
- padding: 12px 24px;
- border-radius: 25px;
- border: none;
- background: var(--color-secondary);
- color: white;
- cursor: pointer;
- transition: all 0.3s ease;
- font-family: var(--font-family);
- font-size: 0.875rem;
- font-weight: 500;
- min-width: 120px;
-}
-
-.saveButton:hover:not(:disabled) {
- background: var(--color-secondary);
- border-color: var(--color-secondary);
- box-shadow: 0 4px 12px rgba(63, 81, 181, 0.3);
- transform: translateY(-2px);
-}
-
-.saveButton:disabled {
- opacity: 0.6;
- cursor: not-allowed;
- transform: none;
-}
-
-.resetButton {
- padding: 12px 24px;
- border-radius: 25px;
- border: 1px solid var(--color-primary);
- background: var(--color-bg);
- color: var(--color-text);
- cursor: pointer;
- transition: all 0.3s ease;
- font-family: var(--font-family);
- font-size: 0.875rem;
- font-weight: 500;
- min-width: 120px;
- display: flex;
- align-items: center;
- gap: 8px;
-}
-
-.resetButton:hover:not(:disabled) {
- background: var(--color-primary);
- color: white;
- transform: translateY(-2px);
-}
-
-.resetButton:disabled {
- opacity: 0.6;
- cursor: not-allowed;
- transform: none;
-}
-
-.resetIcon {
- font-size: 1rem;
-}
-
-/* Responsive Design */
-@media (max-width: 768px) {
- .formRow {
- flex-direction: column;
- gap: 15px;
- }
-
- .formField {
- min-width: unset;
- }
-
- .formActions {
- justify-content: center;
- flex-direction: column;
- }
-
- .saveButton,
- .resetButton {
- width: 100%;
- }
-}
-
-@media (max-width: 640px) {
- .speechSettingsForm {
- padding: 15px;
- }
-
- .section {
- padding: 12px;
- }
-
- .sectionHeader {
- gap: 6px;
- padding-bottom: 6px;
- }
-
- .formRow {
- gap: 12px;
- }
-}
diff --git a/src/components/settings/settingsSpeech.tsx b/src/components/settings/settingsSpeech.tsx
deleted file mode 100644
index 64c1b30..0000000
--- a/src/components/settings/settingsSpeech.tsx
+++ /dev/null
@@ -1,387 +0,0 @@
-import { useState, useEffect } from 'react';
-import { IoIosBusiness, IoIosContact, IoIosTime, IoIosRefresh } from 'react-icons/io';
-import { useNavigate } from 'react-router-dom';
-import styles from './settingsSpeech.module.css';
-import { useLanguage } from '../../contexts/LanguageContext';
-
-interface MandateData {
- id: string;
- mandate_general: {
- company_name: string;
- industry: string;
- contact_info: {
- email: string;
- phone: string;
- street: string;
- postal_code: string;
- city: string;
- country: string;
- };
- business_hours: string;
- timezone: string;
- };
- setup_contacts: boolean;
-}
-
-interface SettingsSpeechProps {
- onDataUpdate?: (data: MandateData) => void;
-}
-
-function SettingsSpeech({ onDataUpdate }: SettingsSpeechProps) {
- const { t } = useLanguage();
- const navigate = useNavigate();
- const [formData, setFormData] = useState(null);
- const [isLoading, setIsLoading] = useState(true);
- const [isSaving, setIsSaving] = useState(false);
- const [saveMessage, setSaveMessage] = useState<{ type: 'success' | 'error', text: string } | null>(null);
-
- // Load data from localStorage on component mount
- useEffect(() => {
- const loadSpeechData = () => {
- try {
- const savedData = localStorage.getItem('speechSignUpData');
- const timestamp = localStorage.getItem('speechSignUpTimestamp');
-
- if (savedData && timestamp) {
- const parsedData = JSON.parse(savedData);
- const savedTime = parseInt(timestamp);
- const now = Date.now();
- const twentyFourHours = 24 * 60 * 60 * 1000;
-
- // Check if data is still valid (within 24 hours)
- if (now - savedTime < twentyFourHours) {
- setFormData(parsedData);
- } else {
- // Data expired, clear it
- localStorage.removeItem('speechSignUpData');
- localStorage.removeItem('speechSignUpTimestamp');
- }
- }
- } catch (error) {
- console.error('Error loading speech data:', error);
- } finally {
- setIsLoading(false);
- }
- };
-
- loadSpeechData();
- }, []);
-
- const handleInputChange = (field: string, value: string) => {
- if (!formData) return;
-
- const newData = { ...formData };
- const fieldParts = field.split('.');
-
- if (fieldParts.length === 2) {
- // Handle nested fields like mandate_general.company_name
- const [parent, child] = fieldParts;
- if (parent === 'mandate_general' && child in newData.mandate_general) {
- (newData.mandate_general as any)[child] = value;
- }
- } else if (fieldParts.length === 3) {
- // Handle deeply nested fields like mandate_general.contact_info.email
- const [parent, child, grandchild] = fieldParts;
- if (parent === 'mandate_general' && child === 'contact_info' && grandchild in newData.mandate_general.contact_info) {
- (newData.mandate_general.contact_info as any)[grandchild] = value;
- }
- } else if (field === 'setup_contacts') {
- newData.setup_contacts = value === 'true';
- }
-
- setFormData(newData);
- setSaveMessage(null);
- };
-
-
- const handleSave = async () => {
- if (!formData) return;
-
- setIsSaving(true);
- try {
- // Save to localStorage
- localStorage.setItem('speechSignUpData', JSON.stringify(formData));
- localStorage.setItem('speechSignUpTimestamp', Date.now().toString());
-
- // Dispatch event to notify other components
- window.dispatchEvent(new CustomEvent('speechSignUpChanged'));
-
- setSaveMessage({ type: 'success', text: t('speech.settings.save_success') });
-
- // Notify parent component if callback provided
- if (onDataUpdate) {
- onDataUpdate(formData);
- }
-
- // Clear message after 3 seconds
- setTimeout(() => setSaveMessage(null), 3000);
- } catch (error) {
- console.error('Error saving speech settings:', error);
- setSaveMessage({ type: 'error', text: t('speech.settings.save_error') });
- } finally {
- setIsSaving(false);
- }
- };
-
- const handleReset = () => {
- if (window.confirm(t('speech.settings.reset_confirm'))) {
- localStorage.removeItem('speechSignUpData');
- localStorage.removeItem('speechSignUpTimestamp');
- window.dispatchEvent(new CustomEvent('speechSignUpChanged'));
- setFormData(null);
- setSaveMessage({ type: 'success', text: t('speech.settings.reset_success') });
- setTimeout(() => setSaveMessage(null), 3000);
- }
- };
-
- if (isLoading) {
- return (
-
-
- {t('common.loading')}
-
-
- );
- }
-
- if (!formData) {
- return (
-
-
-
{t('speech.settings.no_data')}
-
navigate('/speech')}
- >
- {t('speech.settings.sign_up_now')}
-
-
-
- );
- }
-
- return (
-
-
{t('speech.settings.title')}
-
{t('speech.settings.description')}
-
- {saveMessage && (
-
- {saveMessage.text}
-
- )}
-
- {/* Company Information Section */}
-
-
-
-
{t('speech.settings.company_info')}
-
-
-
-
-
- {/* Contact Information Section */}
-
-
-
-
{t('speech.settings.contact_info')}
-
-
-
-
-
-
-
-
-
- {/* Business Hours Section */}
-
-
-
-
{t('speech.settings.business_hours')}
-
-
-
-
-
- {t('speech.signup.business_hours')} *
-
- handleInputChange('mandate_general.business_hours', e.target.value)}
- required
- />
-
-
-
-
- {t('speech.signup.timezone')} *
-
- handleInputChange('mandate_general.timezone', e.target.value)}
- required
- >
- {t('speech.signup.select_timezone')}
- UTC-12 (Baker Island)
- UTC-11 (American Samoa)
- UTC-10 (Hawaii)
- UTC-9 (Alaska)
- UTC-8 (Pacific Time)
- UTC-7 (Mountain Time)
- UTC-6 (Central Time)
- UTC-5 (Eastern Time)
- UTC-4 (Atlantic Time)
- UTC-3 (Brazil)
- UTC-2 (Mid-Atlantic)
- UTC-1 (Azores)
- UTC+0 (Greenwich Mean Time)
- UTC+1 (Central European Time)
- UTC+2 (Eastern European Time)
- UTC+3 (Moscow Time)
- UTC+4 (Gulf Standard Time)
- UTC+5 (Pakistan Standard Time)
- UTC+6 (Bangladesh Standard Time)
- UTC+7 (Indochina Time)
- UTC+8 (China Standard Time)
- UTC+9 (Japan Standard Time)
- UTC+10 (Australian Eastern Time)
- UTC+11 (Solomon Islands)
- UTC+12 (New Zealand)
-
-
-
-
-
- {/* Actions */}
-
-
-
- {t('speech.settings.reset')}
-
-
-
- {isSaving ? t('speech.settings.saving') : t('speech.settings.save')}
-
-
-
- );
-}
-
-export default SettingsSpeech;
diff --git a/src/components/settings/settingsUser.module.css b/src/components/settings/settingsUser.module.css
deleted file mode 100644
index f239ab8..0000000
--- a/src/components/settings/settingsUser.module.css
+++ /dev/null
@@ -1,168 +0,0 @@
-/* User Information Form Styles */
-.userInfoForm {
- display: flex;
- flex-direction: column;
- gap: 20px;
- padding: 20px;
- background: var(--color-bg);
- border-radius: 25px;
- border: 1px solid var(--color-primary);
- margin-bottom: 20px;
-}
-
-.formRow {
- display: flex;
- gap: 20px;
- flex-wrap: wrap;
-}
-
-.formField {
- display: flex;
- flex-direction: column;
- gap: 8px;
- flex: 1;
- min-width: 250px;
-}
-
-.fieldLabel {
- font-size: 0.875rem;
- font-weight: 500;
- color: var(--color-text);
- font-family: var(--font-family);
-}
-
-.fieldNote {
- font-size: 0.75rem;
- font-weight: 400;
- color: var(--color-primary);
- font-style: italic;
-}
-
-.formInput,
-.formSelect {
- padding: 12px 16px;
- border-radius: 25px;
- border: 1px solid var(--color-primary);
- background: var(--color-bg);
- color: var(--color-text);
- font-family: var(--font-family);
- font-size: 0.875rem;
- transition: all 0.3s ease;
- outline: none;
-}
-
-.formInput:focus,
-.formSelect:focus {
- border-color: var(--color-primary);
- box-shadow: 0 0 0 3px rgba(63, 81, 181, 0.1);
-}
-
-.formInput:hover,
-.formSelect:hover {
- border-color: var(--color-secondary);
-}
-
-.formInput[readonly] {
- background: var(--color-gray-light);
- cursor: not-allowed;
- opacity: 0.7;
-}
-
-.formSelect option {
- background: var(--color-bg);
- color: var(--color-text);
- padding: 10px;
-}
-
-.formActions {
- display: flex;
- justify-content: flex-end;
- padding-top: 10px;
-}
-
-.saveButton {
- padding: 12px 24px;
- border-radius: 25px;
- border: none;
- background: var(--color-secondary);
- color: white;
- cursor: pointer;
- transition: all 0.3s ease;
- font-family: var(--font-family);
- font-size: 0.875rem;
- font-weight: 500;
- min-width: 120px;
-}
-
-.saveButton:hover:not(:disabled) {
- background: var(--color-secondary);
- border-color: var(--color-secondary);
- box-shadow: 0 4px 12px rgba(63, 81, 181, 0.3);
-}
-
-.saveButton:disabled {
- opacity: 0.6;
- cursor: not-allowed;
- transform: none;
- box-shadow: none;
-}
-
-.updateMessage {
- padding: 12px 16px;
- border-radius: 12px;
- font-size: 0.875rem;
- font-weight: 500;
- margin-bottom: 10px;
-}
-
-.successMessage {
- background-color: #e8f5e8;
- color: #2e7d32;
- border: 1px solid #81c784;
-}
-
-.errorMessage {
- background-color: #fce4ec;
- color: #c2185b;
- border: 1px solid #f48fb1;
- padding: 12px 16px;
- border-radius: 12px;
- font-size: 0.875rem;
- font-weight: 500;
- margin-bottom: 20px;
-}
-
-.settingLabel {
- font-size: 1rem;
- font-weight: 500;
- color: var(--color-text);
- font-family: var(--font-family);
-}
-
-.settingDescription {
- font-size: 0.875rem;
- color: var(--color-primary);
- font-family: var(--font-family);
-}
-
-
-/* Responsive design */
-@media (max-width: 768px) {
- .formRow {
- flex-direction: column;
- gap: 15px;
- }
-
- .formField {
- min-width: unset;
- }
-
- .formActions {
- justify-content: center;
- }
-
- .saveButton {
- width: 100%;
- max-width: 200px;
- }
-}
diff --git a/src/components/settings/settingsUser.tsx b/src/components/settings/settingsUser.tsx
deleted file mode 100644
index e3f9fae..0000000
--- a/src/components/settings/settingsUser.tsx
+++ /dev/null
@@ -1,389 +0,0 @@
-import { useState, useEffect, useRef } from 'react';
-import { useLanguage, Language } from '../../contexts/LanguageContext';
-import { useCurrentUser, useUser, User } from '../../hooks/useUsers';
-import styles from './settingsUser.module.css';
-
-interface SettingsUserProps {
- className?: string;
-}
-
-function SettingsUser({ className }: SettingsUserProps) {
- console.log('👤 SettingsUser component loaded');
- const { currentLanguage, setLanguage, t } = useLanguage();
- const { user: currentUser } = useCurrentUser();
- const { getUser, updateUser, isLoading: updateLoading } = useUser();
-
- // Local state for user data fetched directly via API
- const [user, setUser] = useState(null);
- const [userLoading, setUserLoading] = useState(false);
- const [userError, setUserError] = useState(null);
-
- // Form state for user info
- const [userForm, setUserForm] = useState({
- username: '',
- fullName: '',
- email: '',
- language: 'en' as Language,
- privilege: '',
- enabled: true
- });
-
- // Phone name state (stored in localStorage only)
- const [phoneName, setPhoneName] = useState('');
- const [updateMessage, setUpdateMessage] = useState<{ type: 'success' | 'error', text: string } | null>(null);
- const [isUpdating, setIsUpdating] = useState(false); // Flag to prevent form reset during update
- const hasLoadedUser = useRef(false);
-
- // Load phone name from localStorage
- const loadPhoneName = () => {
- try {
- const savedPhoneName = localStorage.getItem('userPhoneName');
- if (savedPhoneName) {
- setPhoneName(savedPhoneName);
- }
- } catch (error) {
- console.error('Failed to load phone name from localStorage:', error);
- }
- };
-
- // Save phone name to localStorage
- const savePhoneName = (name: string) => {
- try {
- if (name.trim()) {
- localStorage.setItem('userPhoneName', name.trim());
- } else {
- localStorage.removeItem('userPhoneName');
- }
- } catch (error) {
- console.error('Failed to save phone name to localStorage:', error);
- }
- };
-
- // Fetch user data directly using the /api/users/{userId} endpoint
- const fetchUserData = async () => {
- if (!currentUser?.id || hasLoadedUser.current) return;
-
- hasLoadedUser.current = true;
- setUserLoading(true);
- setUserError(null);
-
- try {
- const userData = await getUser(currentUser.id);
- setUser(userData);
- } catch (error) {
- console.error('Failed to fetch user data:', error);
- setUserError(typeof error === 'string' ? error : 'Failed to load user data');
- hasLoadedUser.current = false; // Reset on error to allow retry
- } finally {
- setUserLoading(false);
- }
- };
-
- // Load phone name from localStorage on component mount
- useEffect(() => {
- loadPhoneName();
- }, []);
-
- // Fetch user data when currentUser is available
- useEffect(() => {
- if (currentUser?.id && !hasLoadedUser.current) {
- fetchUserData();
- }
- }, [currentUser?.id]);
-
- // Update form when user data is loaded (but not during an active update)
- useEffect(() => {
- if (user && !isUpdating) {
- console.log('🔄 Updating form with user data:', user);
- setUserForm({
- username: user.username || '',
- fullName: user.fullName || '',
- email: user.email || '',
- language: (user.language as Language) || currentLanguage,
- privilege: user.privilege || '',
- enabled: user.enabled
- });
- }
- }, [user, currentLanguage, isUpdating]);
-
- const handleUserFormChange = (field: keyof typeof userForm, value: string | boolean) => {
- setUserForm(prev => ({ ...prev, [field]: value }));
- setUpdateMessage(null); // Clear any previous messages
-
- // Language change will be handled when the form is submitted, not immediately
- };
-
- const handlePhoneNameChange = (value: string) => {
- setPhoneName(value);
- savePhoneName(value); // Save to localStorage immediately
- };
-
- const handleSaveUserInfo = async () => {
- if (!user) return;
-
- setIsUpdating(true); // Prevent form reset during update
-
- try {
- // Create complete User object with updated form data
- // Only include editable fields based on authentication authority
- const completeUserData: User = {
- id: user.id,
- username: user.authenticationAuthority === 'local' ? userForm.username : user.username,
- fullName: user.authenticationAuthority === 'local' ? userForm.fullName : user.fullName,
- email: user.authenticationAuthority === 'local' ? userForm.email : user.email,
- language: userForm.language, // Language is always editable
- privilege: userForm.privilege,
- enabled: userForm.enabled,
- authenticationAuthority: user.authenticationAuthority,
- mandateId: user.mandateId
- };
-
- // Update user via API - this returns the updated user
- const updatedUser = await updateUser(user.id, completeUserData);
-
- if (updatedUser) {
- console.log('✅ User update successful:', updatedUser);
-
- // CRITICAL: Update localStorage with new user data (single source of truth!)
- localStorage.setItem('currentUser', JSON.stringify(updatedUser));
- console.log('💾 Updated user data cached in localStorage');
-
- // Update local user state with the returned data
- setUser(updatedUser);
-
- // Update frontend language if it was changed
- const newLanguage = updatedUser.language as Language;
- if (newLanguage && newLanguage !== currentLanguage) {
- try {
- await setLanguage(newLanguage);
- console.log('🌍 Frontend language updated to:', newLanguage);
- } catch (error) {
- console.error('Failed to change frontend language:', error);
- }
- }
-
- // Success: Update form with the actual returned data to ensure consistency
- setUserForm({
- username: updatedUser.username || '',
- fullName: updatedUser.fullName || '',
- email: updatedUser.email || '',
- language: newLanguage || currentLanguage,
- privilege: updatedUser.privilege || '',
- enabled: updatedUser.enabled
- });
-
- console.log('📝 Form updated with new data');
-
- // Dispatch event to notify other components (like sidebar) that user data was updated
- window.dispatchEvent(new CustomEvent('userInfoUpdated'));
-
- setUpdateMessage({ type: 'success', text: t('settings.userinfo.success') });
- } else {
- throw new Error('No updated user data returned from server');
- }
-
- // Clear message after 3 seconds
- setTimeout(() => setUpdateMessage(null), 3000);
- } catch (error) {
- console.error('Failed to update user info:', error);
- setUpdateMessage({ type: 'error', text: t('settings.userinfo.update_error') });
-
- // Reset form to original user data on error
- if (user) {
- setUserForm({
- username: user.username || '',
- fullName: user.fullName || '',
- email: user.email || '',
- language: (user.language as Language) || currentLanguage,
- privilege: user.privilege || '',
- enabled: user.enabled
- });
- }
- } finally {
- setIsUpdating(false); // Re-enable form sync
- }
- };
-
- const getLanguageLabel = (lang: Language): string => {
- switch (lang) {
- case 'de': return t('language.german');
- case 'en': return t('language.english');
- case 'fr': return t('language.french');
- default: return lang;
- }
- };
-
- if (userLoading) {
- return (
-
-
- {t('settings.userinfo.loading')}
-
-
- );
- }
-
- return (
-
- {userError && (
-
- {t('settings.userinfo.error')}: {typeof userError === 'string' ? userError : 'An error occurred'}
-
- )}
-
- {user && (
- <>
-
{t('settings.userinfo')}
-
- {t('settings.userinfo.description')}
-
-
-
-
-
-
-
-
- {t('settings.language')}
- ({t('settings.language.description')})
-
- handleUserFormChange('language', e.target.value)}
- aria-label={t('settings.language')}
- >
- {getLanguageLabel('de')}
- {getLanguageLabel('en')}
- {getLanguageLabel('fr')}
-
-
-
-
- {/* Empty field to maintain layout */}
-
-
-
-
-
- {updateMessage && (
-
- {updateMessage.text}
-
- )}
-
-
-
- {updateLoading ? t('settings.userinfo.saving') : t('settings.userinfo.save')}
-
-
- >
- )}
-
- );
-}
-
-export default SettingsUser;
diff --git a/src/components/ui/index.ts b/src/components/ui/index.ts
deleted file mode 100644
index 782ab2b..0000000
--- a/src/components/ui/index.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-export * from './Button';
-export * from './Button/UploadButton';
-export { default as MessageOverlay } from './MessageOverlay';
-export type { MessageMode } from './MessageOverlay';
-export * from './DragDropOverlay';
diff --git a/src/contexts/FileContext.tsx b/src/contexts/FileContext.tsx
new file mode 100644
index 0000000..9fc661d
--- /dev/null
+++ b/src/contexts/FileContext.tsx
@@ -0,0 +1,109 @@
+import React, { createContext, useContext, useState, useCallback, useEffect } from 'react';
+import { useUserFiles, useFileOperations, UserFile } from '../hooks/useFiles';
+
+interface FileContextType {
+ files: UserFile[];
+ loading: boolean;
+ error: string | null;
+ refetch: () => Promise;
+ handleFileUpload: (file: File, workflowId?: string) => Promise<{ success: boolean; fileData?: any; error?: string }>;
+ handleFileDelete: (fileId: string, onOptimisticDelete?: () => void) => Promise;
+ uploadingFile: boolean;
+ deletingFiles: Set;
+ previewingFiles: Set;
+}
+
+const FileContext = createContext(undefined);
+
+export function FileProvider({ children }: { children: React.ReactNode }) {
+ const { data: files, loading, error, refetch: refetchFiles, removeFileOptimistically, addFileOptimistically } = useUserFiles();
+ const {
+ handleFileUpload: hookHandleFileUpload,
+ handleFileDelete: hookHandleFileDelete,
+ uploadingFile,
+ deletingFiles,
+ previewingFiles
+ } = useFileOperations();
+
+ // Centralized file upload that updates the shared state
+ const handleFileUpload = useCallback(async (file: File, workflowId?: string) => {
+ const result = await hookHandleFileUpload(file, workflowId);
+
+ if (result.success && result.fileData) {
+ // The API response structure: { message, file: FileInfo, ... }
+ // The file data is nested in the 'file' property
+ const responseData = result.fileData;
+ const fileData = responseData.file || responseData; // Support both nested and direct structure
+
+ if (!fileData || !fileData.id) {
+ console.error('File upload response missing file data:', responseData);
+ return result;
+ }
+
+ // Add file optimistically to the shared state
+ const newFile: UserFile = {
+ id: fileData.id,
+ file_name: fileData.fileName || file.name,
+ mime_type: fileData.mimeType || file.type || 'application/octet-stream',
+ action: 'Document', // Will be determined by mime type in useUserFiles
+ created_at: fileData.creationDate ? new Date(fileData.creationDate * 1000).toISOString() : new Date().toISOString(),
+ size: fileData.fileSize || file.size,
+ source: 'user_uploaded'
+ };
+
+ addFileOptimistically(newFile);
+
+ // Refetch to ensure we have the latest data (this will update all consumers)
+ await refetchFiles();
+ }
+
+ return result;
+ }, [hookHandleFileUpload, addFileOptimistically, refetchFiles]);
+
+ // Centralized file delete that updates the shared state
+ const handleFileDelete = useCallback(async (fileId: string, onOptimisticDelete?: () => void) => {
+ const success = await hookHandleFileDelete(fileId, () => {
+ removeFileOptimistically(fileId);
+ onOptimisticDelete?.();
+ });
+
+ if (success) {
+ // Refetch to ensure we have the latest data
+ await refetchFiles();
+ }
+
+ return success;
+ }, [hookHandleFileDelete, removeFileOptimistically, refetchFiles]);
+
+ // Expose refetch function
+ const refetch = useCallback(async () => {
+ await refetchFiles();
+ }, [refetchFiles]);
+
+ return (
+
+ {children}
+
+ );
+}
+
+export function useFileContext() {
+ const context = useContext(FileContext);
+ if (context === undefined) {
+ throw new Error('useFileContext must be used within a FileProvider');
+ }
+ return context;
+}
+
diff --git a/src/contexts/WorkflowSelectionContext.tsx b/src/contexts/WorkflowSelectionContext.tsx
new file mode 100644
index 0000000..33a2c37
--- /dev/null
+++ b/src/contexts/WorkflowSelectionContext.tsx
@@ -0,0 +1,41 @@
+import React, { createContext, useContext, useState, useCallback, ReactNode } from 'react';
+
+interface WorkflowSelectionContextType {
+ selectedWorkflowId: string | null;
+ selectWorkflow: (workflowId: string | null) => void;
+ clearWorkflow: () => void; // Clear workflow and connected files
+}
+
+const WorkflowSelectionContext = createContext(undefined);
+
+export function WorkflowSelectionProvider({ children }: { children: ReactNode }) {
+ const [selectedWorkflowId, setSelectedWorkflowId] = useState(null);
+
+ const selectWorkflow = useCallback((workflowId: string | null) => {
+ setSelectedWorkflowId(workflowId);
+ // Also dispatch a custom event for components that might not have access to context
+ window.dispatchEvent(new CustomEvent('workflowSelected', { detail: { workflowId } }));
+ }, []);
+
+ const clearWorkflow = useCallback(() => {
+ setSelectedWorkflowId(null);
+ // Dispatch event to notify that workflow is cleared - connected files should also be cleared
+ window.dispatchEvent(new CustomEvent('workflowCleared', { detail: { workflowId: null } }));
+ // Also dispatch workflowSelected with null for backward compatibility
+ window.dispatchEvent(new CustomEvent('workflowSelected', { detail: { workflowId: null } }));
+ }, []);
+
+ return (
+
+ {children}
+
+ );
+}
+
+export function useWorkflowSelection() {
+ const context = useContext(WorkflowSelectionContext);
+ if (context === undefined) {
+ throw new Error('useWorkflowSelection must be used within a WorkflowSelectionProvider');
+ }
+ return context;
+}
diff --git a/src/core/PageManager/PageManager.tsx b/src/core/PageManager/PageManager.tsx
index a6df63f..2f2fe3b 100644
--- a/src/core/PageManager/PageManager.tsx
+++ b/src/core/PageManager/PageManager.tsx
@@ -65,7 +65,7 @@ const PageManager: React.FC = ({
useEffect(() => {
const pageData = getPageDataByPath(currentPath);
- if (!pageData || !pageData.moduleEnabled || !checkSpeechAccess(currentPath)) {
+ if (!pageData || pageData.hide || !pageData.moduleEnabled || !checkSpeechAccess(currentPath)) {
return;
}
@@ -99,8 +99,6 @@ const PageManager: React.FC = ({
{
- console.log(`Button clicked: ${buttonId}`, button);
- // Add global button click handling here
}}
/>
)}
@@ -114,20 +112,11 @@ const PageManager: React.FC = ({
newInstances.set(currentPath, pageInstance);
- if (import.meta.env.DEV) {
- console.log(`🔥 PageManager: Created ${shouldPreserve ? 'PRESERVED' : 'regular'} instance for ${currentPath}`, {
- totalInstances: newInstances.size,
- preservedInstances: Array.from(newInstances.values()).filter(i => i.shouldPreserve).length,
- pageData: pageData.name
- });
- }
+
} else {
if (import.meta.env.DEV) {
const instance = newInstances.get(currentPath);
- console.log(`♻️ PageManager: Reusing ${instance?.shouldPreserve ? 'PRESERVED' : 'regular'} instance for ${currentPath}`, {
- totalInstances: newInstances.size,
- preservedInstances: Array.from(newInstances.values()).filter(i => i.shouldPreserve).length
- });
+
}
}
@@ -148,9 +137,7 @@ const PageManager: React.FC = ({
});
instancesToDelete.forEach(path => {
- if (import.meta.env.DEV) {
- console.log(`🗑️ PageManager: Cleaning up non-preserved instance for ${path}`);
- }
+
updatedInstances.delete(path);
});
@@ -163,7 +150,7 @@ const PageManager: React.FC = ({
const pageData = getPageDataByPath(currentPath);
- if (!pageData || !pageData.moduleEnabled || !checkSpeechAccess(currentPath)) {
+ if (!pageData || pageData.hide || !pageData.moduleEnabled || !checkSpeechAccess(currentPath)) {
return ;
}
diff --git a/src/core/PageManager/PageRenderer.tsx b/src/core/PageManager/PageRenderer.tsx
index 43188af..48d7625 100644
--- a/src/core/PageManager/PageRenderer.tsx
+++ b/src/core/PageManager/PageRenderer.tsx
@@ -1,9 +1,13 @@
-import React from 'react';
-import { GenericPageData, PageButton, PageContent, resolveLanguageText } from './pageInterface';
+import React, { useState, useEffect } from 'react';
+import { GenericPageData, PageButton, PageContent, resolveLanguageText, SettingsFieldConfig, SettingsSectionConfig } from './pageInterface';
import { FormGenerator } from '../../components/FormGenerator';
-import { Button, UploadButton, CreateButton } from '../../components/ui';
-import { DragDropOverlay } from '../../components/ui/DragDropOverlay';
-import { useLanguage } from '../../contexts/LanguageContext';
+import { Button, UploadButton, CreateButton, TextField, Messages, ChatMessage, LogMessage, DropdownSelect, TextInputField, SelectField, ToggleField } from '../../components/UiComponents';
+import { Popup } from '../../components/UiComponents/Popup';
+import { ConnectedFilesList } from '../../components/UiComponents/ConnectedFilesList';
+import type { DropdownSelectItem } from '../../components/UiComponents/DropdownSelect';
+import { DragDropOverlay } from '../../components/UiComponents/DragDropOverlay';
+import { useLanguage } from '../../providers/language/LanguageContext';
+import { FiPaperclip } from 'react-icons/fi';
import styles from '../../styles/pages.module.css';
interface PageRendererProps {
@@ -21,7 +25,11 @@ const PageRenderer: React.FC = ({
// Call the hook at the top level to ensure it persists across renders
// This is CRITICAL - hooks must be called in the same order on every render
const tableContent = pageData.content?.find(content => content.type === 'table');
- const hookFactory = tableContent?.tableConfig?.hookFactory;
+ const inputFormContent = pageData.content?.find(content => content.type === 'inputForm');
+ const settingsContent = pageData.content?.find(content => content.type === 'settings');
+ const hookFactory = tableContent?.tableConfig?.hookFactory
+ || inputFormContent?.inputFormConfig?.hookFactory
+ || settingsContent?.settingsConfig?.hookFactory;
// Create a stable hook instance using React.useMemo
// This ensures the same hook instance is used across re-renders
@@ -36,11 +44,7 @@ const PageRenderer: React.FC = ({
// This will be called on every render, but it's the SAME hook instance
const hookData = useTableData ? useTableData() : null;
- // Debug hook data
- if (import.meta.env.DEV && hookData) {
- console.log('🔍 PageRenderer hookData:', hookData);
- console.log('🔍 PageRenderer has handleUpload:', !!hookData.handleUpload);
- }
+
// Handle button clicks
const handleButtonClick = async (button: PageButton) => {
@@ -68,6 +72,338 @@ const PageRenderer: React.FC = ({
}
};
+ // Helper function to get nested value using dot notation (generic utility)
+ const getNestedValue = (obj: any, path: string): any => {
+ return path.split('.').reduce((current, key) => current?.[key], obj);
+ };
+
+ // Helper function to set nested value using dot notation (generic utility)
+ const setNestedValue = (obj: any, path: string, value: any): any => {
+ const keys = path.split('.');
+ const result = { ...obj };
+ let current = result;
+ for (let i = 0; i < keys.length - 1; i++) {
+ const key = keys[i];
+ if (!(key in current)) {
+ current[key] = {};
+ }
+ current[key] = { ...current[key] };
+ current = current[key];
+ }
+ current[keys[keys.length - 1]] = value;
+ return result;
+ };
+
+ // Generic form section renderer - reusable for any form-based content
+ const FormSectionRenderer: React.FC<{
+ sections: SettingsSectionConfig[];
+ formData: any;
+ fieldsBySection: Record;
+ loadingBySection: Record;
+ errorsBySection: Record;
+ onSave?: (sectionId: string, data: any) => Promise;
+ getNestedValue: (obj: any, path: string) => any;
+ setNestedValue: (obj: any, path: string, value: any) => any;
+ }> = ({ sections, formData, fieldsBySection, loadingBySection, errorsBySection, onSave, getNestedValue, setNestedValue }) => {
+ const [sectionFormData, setSectionFormData] = useState>({});
+ const [sectionSaveLoading, setSectionSaveLoading] = useState>({});
+ const [sectionSaveMessages, setSectionSaveMessages] = useState>({});
+
+ // Initialize form data from formData when it changes
+ useEffect(() => {
+ const newFormData: Record = {};
+ sections.forEach(section => {
+ const allFields = [
+ ...(section.staticFields || []),
+ ...(fieldsBySection[section.sectionId] || [])
+ ];
+
+ if (allFields.length === 0) return;
+
+ const sectionData: any = { ...(sectionFormData[section.id] || {}) };
+ allFields.forEach(field => {
+ const value = getNestedValue(formData, field.dataKey);
+ if (value !== undefined && sectionData[field.dataKey] !== value) {
+ sectionData[field.dataKey] = value;
+ }
+ });
+ newFormData[section.id] = sectionData;
+ });
+
+ const hasChanges = Object.keys(newFormData).some(sectionId => {
+ const newData = newFormData[sectionId];
+ const oldData = sectionFormData[sectionId] || {};
+ return JSON.stringify(newData) !== JSON.stringify(oldData);
+ });
+
+ if (hasChanges) {
+ setSectionFormData(prev => ({ ...prev, ...newFormData }));
+ }
+ }, [formData, sections.length, JSON.stringify(fieldsBySection)]);
+
+ // Generic field renderer
+ const renderField = (field: SettingsFieldConfig, sectionId: string) => {
+ const localFormData = sectionFormData[sectionId] || {};
+ const currentValue = localFormData[field.dataKey] !== undefined
+ ? localFormData[field.dataKey]
+ : getNestedValue(formData, field.dataKey);
+
+ const isDisabled = typeof field.disabled === 'function'
+ ? field.disabled(formData)
+ : field.disabled || false;
+
+ const handleFieldChange = (value: any) => {
+ setSectionFormData(prev => ({
+ ...prev,
+ [sectionId]: setNestedValue(prev[sectionId] || {}, field.dataKey, value)
+ }));
+ setSectionSaveMessages(prev => ({ ...prev, [sectionId]: null }));
+ };
+
+ switch (field.type) {
+ case 'text':
+ return (
+
+ );
+
+ case 'select':
+ const options = (field.options || []).map(opt => ({
+ id: opt.id,
+ label: resolveLanguageText(opt.label, t),
+ value: opt.value
+ }));
+ return (
+
+ );
+
+ case 'toggle':
+ return (
+
+ );
+
+ default:
+ return null;
+ }
+ };
+
+ const handleSectionSave = async (section: typeof sections[0]) => {
+ const localFormData = sectionFormData[section.id] || {};
+ setSectionSaveLoading(prev => ({ ...prev, [section.id]: true }));
+ setSectionSaveMessages(prev => ({ ...prev, [section.id]: null }));
+
+ try {
+ if (onSave) {
+ await onSave(section.id, localFormData);
+ } else if (section.onSave) {
+ await section.onSave(section.id, localFormData);
+ }
+
+ setSectionSaveMessages(prev => ({
+ ...prev,
+ [section.id]: {
+ type: 'success',
+ text: t('settings.save_success') || 'Settings saved successfully'
+ }
+ }));
+
+ setTimeout(() => {
+ setSectionSaveMessages(prev => ({ ...prev, [section.id]: null }));
+ }, 3000);
+ } catch (error: any) {
+ setSectionSaveMessages(prev => ({
+ ...prev,
+ [section.id]: {
+ type: 'error',
+ text: error.message || t('settings.save_error') || 'Failed to save settings'
+ }
+ }));
+ } finally {
+ setSectionSaveLoading(prev => ({ ...prev, [section.id]: false }));
+ }
+ };
+
+ return (
+
+ {sections.map(section => {
+ // Conditional rendering check (generic - can be used for any section)
+ if (section.id === 'speech-settings') {
+ const hasSpeechData = formData.speechData || formData.mandate_general;
+ if (!hasSpeechData) {
+ return (
+
+
+ {resolveLanguageText(section.title, t)}
+
+
+ {t('speech.settings.no_data')}
+
+
window.location.href = '/speech'}
+ >
+ {t('speech.settings.sign_up_now')}
+
+
+ );
+ }
+ }
+
+ const allFields = [
+ ...(section.staticFields || []),
+ ...(fieldsBySection[section.sectionId] || [])
+ ];
+ const isLoading = loadingBySection[section.sectionId] || false;
+ const error = errorsBySection[section.sectionId];
+ const saveLoading = sectionSaveLoading[section.id] || false;
+ const saveMessage = sectionSaveMessages[section.id];
+
+ return (
+
+ {/* Section Header */}
+
+ {section.icon && (
+
+ )}
+
+ {resolveLanguageText(section.title, t)}
+
+ {section.description && (
+
+ {resolveLanguageText(section.description, t)}
+
+ )}
+
+
+ {/* Loading State */}
+ {isLoading && (
+
+ {t('common.loading')}
+
+ )}
+
+ {/* Error State */}
+ {error && !isLoading && (
+
+ {error}
+
+ )}
+
+ {/* Fields */}
+ {!isLoading && !error && allFields.length > 0 && (
+
+ {allFields.map(field => renderField(field, section.id))}
+
+ )}
+
+ {/* Save Message */}
+ {saveMessage && (
+
+ {saveMessage.text}
+
+ )}
+
+ {/* Save Button */}
+ {!isLoading && !error && allFields.length > 0 && (
+
+ handleSectionSave(section)}
+ loading={saveLoading}
+ disabled={saveLoading}
+ >
+ {resolveLanguageText(section.saveButtonLabel || 'settings.save', t)}
+
+
+ )}
+
+ );
+ })}
+
+ );
+ };
+
// Render content based on type
const renderContent = (content: PageContent) => {
switch (content.type) {
@@ -146,13 +482,18 @@ const PageRenderer: React.FC = ({
}
// Use columns from hook data if available, otherwise use config columns
- const columns = hookData.columns || configColumns;
+ // If configColumns is an empty array, treat it as undefined to enable auto-detection
+ // Also check if hookData.columns is an empty array (truthy but not useful)
+ const hookColumns = hookData.columns && hookData.columns.length > 0 ? hookData.columns : undefined;
+ const configCols = configColumns && configColumns.length > 0 ? configColumns : undefined;
+ const columns = hookColumns || configCols;
// CRITICAL: Resolve LanguageText objects in column labels
- const resolvedColumns = columns.map(col => ({
+ // Only map if columns exist, otherwise FormGenerator will auto-detect
+ const resolvedColumns = columns ? columns.map(col => ({
...col,
label: resolveLanguageText(col.label, t)
- }));
+ })) : undefined;
// Convert action buttons to FormGenerator format
// Let each action button handle its own logic using the passed fileOperations
@@ -169,10 +510,27 @@ const PageRenderer: React.FC = ({
nameField: action.nameField,
typeField: action.typeField,
operationName: action.operationName,
- loadingStateName: action.loadingStateName
+ loadingStateName: action.loadingStateName,
+ // Preserve edit fields configuration
+ editFields: action.editFields?.map(field => ({
+ ...field,
+ label: resolveLanguageText(field.label, t)
+ }))
};
}) || [];
+ // Debug logging for table rendering
+ if (import.meta.env.DEV) {
+ console.log('🔍 Rendering FormGenerator:', {
+ dataLength: hookData.data?.length || 0,
+ columnsCount: resolvedColumns?.length || 0,
+ loading: showLoadingSpinner,
+ hasError: !!hookData.error,
+ data: hookData.data,
+ willAutoDetect: !resolvedColumns
+ });
+ }
+
return (
{hookData.isRefetching && (
@@ -195,6 +553,402 @@ const PageRenderer: React.FC
= ({
}
return null;
+ case 'inputForm':
+ if (content.inputFormConfig && hookData) {
+ const config = content.inputFormConfig;
+ const isRunning = hookData.isRunning || false;
+
+ // Determine button props based on workflow state
+ const buttonLabel = isRunning
+ ? (config.stopButtonLabel || config.buttonLabel)
+ : config.buttonLabel;
+ const buttonIcon = isRunning
+ ? (config.stopButtonIcon || config.buttonIcon)
+ : config.buttonIcon;
+ const buttonVariant = isRunning
+ ? (config.stopButtonVariant || config.buttonVariant || 'primary')
+ : (config.buttonVariant || 'primary');
+ const buttonDisabled = hookData.isSubmitting || (!isRunning && !hookData.inputValue?.trim());
+
+ // Handle Enter key press
+ const handleKeyDown = (e: React.KeyboardEvent) => {
+ if (e.key === 'Enter' && !e.shiftKey && hookData.handleSubmit && !hookData.isSubmitting) {
+ e.preventDefault();
+ hookData.handleSubmit();
+ }
+ };
+
+ // Check if we have file management (dashboard workflow)
+ const hasFileManagement = !!(hookData.handleFileUpload && hookData.workflowFiles !== undefined);
+
+ // Grid layout for dashboard with files
+ if (hasFileManagement) {
+ return (
+
+ {/* Left column: Input and buttons */}
+