working on new frontend and backend integration

This commit is contained in:
Ida Dittrich 2025-08-06 15:39:16 +02:00
parent 84764f932b
commit 860fbd51f0
105 changed files with 8638 additions and 6859 deletions

2890
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -10,7 +10,6 @@
"preview": "vite preview"
},
"dependencies": {
<<<<<<< Updated upstream
"@azure/msal-browser": "^4.12.0",
"@azure/msal-react": "^3.0.12",
"@xstate/react": "^5.0.0",
@ -24,10 +23,12 @@
"jwt-decode": "^4.0.0",
"motion": "^12.7.3",
"pg": "^8.8.0",
=======
>>>>>>> Stashed changes
"react": "^19.1.0",
"react-dom": "^19.1.0"
"react-dom": "^19.1.0",
"react-dropzone": "^14.3.8",
"react-icons": "^5.5.0",
"react-router-dom": "^7.7.1",
"xstate": "^5.20.1"
},
"devDependencies": {
"@eslint/js": "^9.30.1",

View file

@ -10,13 +10,16 @@ import Register from './pages/Register';
import { AuthProvider } from './auth/authProvider';
import { ProtectedRoute } from './auth/ProtectedRoute';
import { LanguageProvider } from './contexts/LanguageContext';
import Home from './pages/Home';
import Dateien from './pages/Dateien/Dateien';
import TeamBereich from './pages/TeamBereich/TeamBereich';
import Dashboard from './pages/Dashboard';
import Einstellungen from './pages/Einstellungen/Einstellungen';
import Home from './pages/Home/Home';
import Dateien from './pages/Home/Dateien';
import TeamBereich from './pages/Home/TeamBereich';
import Dashboard from './pages/Home/Dashboard';
import Einstellungen from './pages/Home/Einstellungen';
// Import the global light theme CSS variables as default
import './assets/styles/light.css';
import Connections from './pages/Home/Connections';
import Workflows from './pages/Home/Workflows';
import TestSharepoint from './pages/Home/TestSharepoint';
function App() {
// Load saved theme preference on app mount
@ -49,7 +52,10 @@ function App() {
<Route path="dashboard" element={<Dashboard />} />
<Route path="dateien" element={<Dateien />} />
<Route path="team-bereich" element={<TeamBereich />} />
<Route path="connections" element={<Connections />} />
<Route path="workflows" element={<Workflows />} />
<Route path="einstellungen" element={<Einstellungen />} />
<Route path="testSharepoint" element={<TestSharepoint />} />
</Route>
</Routes>
</Router>

View file

@ -11,9 +11,9 @@
--color-secondary-hover: #FF6A55;
--color-secondary-disabled: #F5B0A4;
--color-red: #D85B65;
--color-red-hover: #E77A81;
--color-red-disabled: #F3C0C4;
--color-red: #dc3545;
--color-red-hover: #f5c6cb;
--color-red-disabled: #f8d7da;
--color-secondary-red: #B94A55;
--color-secondary-red-hover: #D46872;
@ -40,9 +40,9 @@
--color-secondary-hover: #FF715C;
--color-secondary-disabled: #6E3E36;
--color-red: #FF6F7A;
--color-red-hover: #FF8B94;
--color-red-disabled: #80383E;
--color-red: #dc3545;
--color-red-hover: #f5c6cb;
--color-red-disabled: #f8d7da;
--color-secondary-red: #D65D6A;
--color-secondary-red-hover: #E17683;

View file

@ -0,0 +1,61 @@
.editModal {
/* Custom styling for the edit modal */
}
.editForm {
/* Form-specific styling within the modal */
min-width: 400px;
}
/* Ensure proper spacing for form elements */
.editForm :global(.fieldGroup) {
margin-bottom: 18px;
}
.editForm :global(.floatingLabelInput) {
margin-bottom: 18px;
}
/* Style the readonly fields to blend better with the modal */
.editForm :global(.readonlyField) {
background-color: var(--color-bg);
opacity: 0.8;
font-style: italic;
}
/* Enhance button styling within the modal */
.editForm :global(.buttonGroup) {
margin-top: 32px;
padding-top: 20px;
border-top: 1px solid var(--color-primary);
}
.editForm :global(.saveButton) {
background-color: var(--color-secondary);
min-width: 120px;
}
.editForm :global(.saveButton:hover) {
background-color: var(--color-secondary-hover);
}
.editForm :global(.cancelButton) {
min-width: 100px;
}
/* Responsive design */
@media (max-width: 640px) {
.editForm {
min-width: 300px;
}
.editForm :global(.buttonGroup) {
gap: 8px;
}
.editForm :global(.saveButton),
.editForm :global(.cancelButton) {
flex: 1;
min-width: unset;
}
}

View file

@ -0,0 +1,50 @@
import React from 'react';
import { Popup, EditForm } from '../Popup';
import styles from './ConnectionEditModal.module.css';
import { ConnectionEditModalProps } from './interfaces';
import { useLanguage } from '../../contexts/LanguageContext';
export function ConnectionEditModal({
isOpen,
connection,
fields,
onSave,
onCancel
}: ConnectionEditModalProps) {
const { t, isLoading } = useLanguage();
if (!connection) {
return null;
}
const authorityName = connection.authority?.charAt(0).toUpperCase() + connection.authority?.slice(1);
// Simplified approach for the title
const baseTitle = t('connections.edit_connection_title', `Edit {authority} Connection`);
const modalTitle = baseTitle.includes('{authority}')
? baseTitle.replace('{authority}', authorityName || '')
: `Edit ${authorityName} Connection`;
return (
<Popup
isOpen={isOpen}
title={modalTitle}
onClose={onCancel}
size="medium"
className={styles.editModal}
>
<EditForm
data={connection}
fields={fields}
onSave={onSave}
onCancel={onCancel}
saveButtonText={t('connections.update_connection', 'Update Connection')}
cancelButtonText={t('common.cancel', 'Cancel')}
showButtons={true}
className={styles.editForm}
/>
</Popup>
);
}
export default ConnectionEditModal;

View file

@ -0,0 +1,47 @@
.errorContainer {
margin-bottom: 20px;
}
.errorMessage {
color: var(--color-red, #ef4444);
padding: 12px 16px;
background-color: var(--color-red-hover, rgba(239, 68, 68, 0.1));
border: 1px solid var(--color-red, #ef4444);
border-radius: 25px;
margin-bottom: 12px;
font-size: 14px;
line-height: 1.4;
}
.errorMessage:last-child {
margin-bottom: 0;
}
.errorMessage strong {
font-weight: 600;
margin-right: 4px;
}
/* Animation for error appearance */
.errorMessage {
animation: slideIn 0.3s ease-out;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Responsive design */
@media (max-width: 480px) {
.errorMessage {
padding: 10px 14px;
font-size: 13px;
}
}

View file

@ -0,0 +1,41 @@
import React from 'react';
import styles from './ConnectionsErrorDisplay.module.css';
import { ConnectionsErrorDisplayProps } from './interfaces';
import { useLanguage } from '../../contexts/LanguageContext';
export function ConnectionsErrorDisplay({
error,
connectError,
disconnectError
}: ConnectionsErrorDisplayProps) {
const { t } = useLanguage();
// Don't render anything if no errors
if (!error && !connectError && !disconnectError) {
return null;
}
return (
<div className={styles.errorContainer}>
{error && (
<div className={styles.errorMessage}>
<strong>{t('connections.error', 'Error')}:</strong> {error}
</div>
)}
{connectError && (
<div className={styles.errorMessage}>
<strong>{t('connections.connection_error', 'Connection Error')}:</strong> {connectError}
</div>
)}
{disconnectError && (
<div className={styles.errorMessage}>
<strong>{t('connections.disconnect_error', 'Disconnect Error')}:</strong> {disconnectError}
</div>
)}
</div>
);
}
export default ConnectionsErrorDisplay;

View file

@ -0,0 +1,50 @@
.tableContainer {
width: 100%;
background: var(--color-bg);
border-radius: 8px;
overflow: hidden;
}
.connectionsTable {
width: 100%;
border-radius: 8px;
}
/* Override FormGenerator styles for connections-specific styling */
.connectionsTable :global(.table) {
background-color: var(--color-bg);
color: var(--color-text);
}
.connectionsTable :global(.th) {
background-color: var(--color-bg);
color: var(--color-text);
border-bottom: 2px solid var(--color-primary);
}
.connectionsTable :global(.td) {
background-color: var(--color-bg);
color: var(--color-text);
border-bottom: 1px solid var(--color-primary);
}
.connectionsTable :global(.tr:hover) {
background-color: var(--color-primary-hover);
}
/* Responsive design */
@media (max-width: 768px) {
.tableContainer {
border-radius: 4px;
margin: 0 -8px;
}
}
@media (max-width: 480px) {
.tableContainer {
margin: 0 -16px;
border-radius: 0;
border-left: none;
border-right: none;
}
}

View file

@ -0,0 +1,42 @@
import React from 'react';
import { FormGenerator } from '../FormGenerator';
import styles from './ConnectionsTable.module.css';
import { ConnectionsTableProps } from './interfaces';
import { useLanguage } from '../../contexts/LanguageContext';
export function ConnectionsTable({
connections,
columns,
actions,
isLoading = false,
isConnecting = false,
isDisconnecting = false,
onRowSelect
}: ConnectionsTableProps) {
const { t } = useLanguage();
const loading = isLoading || isConnecting || isDisconnecting;
return (
<div className={styles.tableContainer}>
<FormGenerator
data={connections}
columns={columns}
title={t('connections.service_connections', 'Service Connections')}
loading={loading}
searchable={true}
filterable={true}
sortable={true}
resizable={true}
pagination={true}
pageSize={10}
selectable={false}
onRowSelect={onRowSelect}
actions={actions}
className={styles.connectionsTable}
/>
</div>
);
}
export default ConnectionsTable;

View file

@ -0,0 +1,10 @@
// Export all components
export { ConnectionsTable } from './ConnectionsTable';
export { ConnectionEditModal } from './ConnectionEditModal';
export { ConnectionsErrorDisplay } from './ConnectionsErrorDisplay';
// Export logic hook
export { useConnectionsLogic } from './logic';
// Export all interfaces and types
export * from './interfaces';

View file

@ -0,0 +1,72 @@
import { ColumnConfig } from '../FormGenerator';
import { EditFieldConfig } from '../Popup';
// Re-export connection-related interfaces from hooks
export type { Connection, CreateConnectionData } from '../../hooks/useConnections';
// Import React for component types
import React from 'react';
// Component Props Interfaces
export interface ConnectionsTableProps {
connections: Connection[];
columns: ColumnConfig[];
actions: TableAction[];
isLoading?: boolean;
isConnecting?: boolean;
isDisconnecting?: boolean;
onRowSelect?: (selectedRows: Connection[]) => void;
}
export interface ConnectionEditModalProps {
isOpen: boolean;
connection: Connection | null;
fields: EditFieldConfig[];
onSave: (updatedConnection: Connection) => Promise<void>;
onCancel: () => void;
}
export interface ConnectionsErrorDisplayProps {
error?: string | null;
connectError?: string | null;
disconnectError?: string | null;
}
// Table Action Interface
export interface TableAction {
label: string;
onClick: (connection: Connection) => Promise<void> | void;
icon: React.ReactNode | ((connection: Connection) => React.ReactNode);
}
// Connection Status Types
export type ConnectionStatus = 'active' | 'pending' | 'expired' | 'revoked';
export type ConnectionAuthority = 'local' | 'google' | 'msft';
// Handler Function Types
export interface ConnectionHandlers {
handleCreateConnection: (type: 'msft' | 'google') => Promise<void>;
handleConnect: (connection: Connection) => Promise<void>;
handleDisconnect: (connection: Connection) => Promise<void>;
handleDelete: (connection: Connection) => Promise<void>;
handleConnectOrDisconnect: (connection: Connection) => Promise<void>;
handleEditConnection: (connection: Connection) => Promise<void>;
handleSaveConnection: (updatedConnection: Connection) => Promise<void>;
handleCancelEdit: () => void;
}
// Hook Return Types
export interface ConnectionsLogicReturn extends ConnectionHandlers {
connections: Connection[];
isLoading: boolean;
isConnecting: boolean;
isDisconnecting: boolean;
error: string | null;
connectError: string | null;
disconnectError: string | null;
editPopupOpen: boolean;
editingConnection: Connection | null;
connectionColumns: ColumnConfig[];
connectionEditFields: EditFieldConfig[];
tableActions: TableAction[];
}

View file

@ -0,0 +1,339 @@
import { useEffect, useState } from 'react';
import { IoIosLink, IoIosTrash } from 'react-icons/io';
import { MdModeEdit } from 'react-icons/md';
import { GoUnlink } from 'react-icons/go';
import React from 'react';
import { useConnections, useOAuthConnect, useDisconnect } from '../../hooks/useConnections';
import { useLanguage } from '../../contexts/LanguageContext';
import { ColumnConfig } from '../FormGenerator';
import { EditFieldConfig } from '../Popup';
import {
Connection,
CreateConnectionData,
ConnectionsLogicReturn,
TableAction
} from './interfaces';
export function useConnectionsLogic(): ConnectionsLogicReturn {
const { t } = useLanguage();
// Hooks
const {
connections,
fetchConnections,
createConnection,
updateConnection,
connectService,
disconnectService,
deleteConnection,
isLoading,
error
} = useConnections();
const {
connectWithPopup,
isConnecting,
error: connectError
} = useOAuthConnect();
const {
disconnect,
isDisconnecting,
error: disconnectError
} = useDisconnect();
// Local state
const [editPopupOpen, setEditPopupOpen] = useState(false);
const [editingConnection, setEditingConnection] = useState<Connection | null>(null);
// Define field configuration for editing connections
const connectionEditFields: EditFieldConfig[] = [
{
key: 'authority',
label: t('connections.field.service', 'Service'),
type: 'readonly',
editable: false,
formatter: (value: string) => {
if (!value) return t('connections.unknown', 'Unknown');
const labels = {
'google': t('connections.service.google', 'Google'),
'msft': t('connections.service.microsoft', 'Microsoft'),
'local': t('connections.service.local', 'Local')
};
return labels[value as keyof typeof labels] || value;
}
},
{
key: 'status',
label: t('connections.field.status', 'Status'),
type: 'readonly',
editable: false,
formatter: (value: string) => value ? value.charAt(0).toUpperCase() + value.slice(1) : t('connections.unknown', 'Unknown')
},
{
key: 'externalUsername',
label: t('connections.field.external_username', 'External Username'),
type: 'string',
editable: true,
required: false,
placeholder: t('connections.placeholder.external_username', 'Enter external username')
},
{
key: 'externalEmail',
label: t('connections.field.external_email', 'External Email'),
type: 'email',
editable: true,
required: false,
placeholder: t('connections.placeholder.external_email', 'Enter external email address')
},
{
key: 'connectedAt',
label: t('connections.field.connected_at', 'Connected At'),
type: 'readonly',
editable: false,
formatter: (value: string) => {
if (!value) return t('connections.not_available', 'N/A');
try {
return new Date(value).toLocaleString();
} catch {
return t('connections.invalid_date', 'Invalid Date');
}
}
},
{
key: 'lastChecked',
label: t('connections.field.last_checked', 'Last Checked'),
type: 'readonly',
editable: false,
formatter: (value: string) => {
if (!value) return t('connections.not_available', 'N/A');
try {
return new Date(value).toLocaleString();
} catch {
return t('connections.invalid_date', 'Invalid Date');
}
}
}
];
// Define custom columns for the connections table
const connectionColumns: ColumnConfig[] = [
{
key: 'authority',
label: t('connections.field.service', 'Service'),
type: 'enum',
filterOptions: ['google', 'msft', 'local'],
formatter: (value: string) => {
if (!value) return t('connections.unknown', 'Unknown');
const labels = {
'google': t('connections.service.google', 'Google'),
'msft': t('connections.service.microsoft', 'Microsoft'),
'local': t('connections.service.local', 'Local')
};
return labels[value as keyof typeof labels] || value;
},
width: 150,
sortable: true,
filterable: true
},
{
key: 'status',
label: t('connections.field.status', 'Status'),
type: 'enum',
filterOptions: ['active', 'pending', 'expired', 'revoked'],
formatter: (value: string) => {
return value?.charAt(0).toUpperCase() + value?.slice(1) || t('connections.unknown', 'Unknown');
},
width: 120,
sortable: true,
filterable: true
},
{
key: 'externalUsername',
label: t('connections.field.external_username', 'External Username'),
type: 'string',
width: 200,
sortable: true,
filterable: false,
searchable: true
},
{
key: 'externalEmail',
label: t('connections.field.external_email', 'External Email'),
type: 'string',
width: 250,
sortable: true,
filterable: false,
searchable: true
},
{
key: 'connectedAt',
label: t('connections.field.connected_at', 'Connected At'),
type: 'date',
width: 150,
sortable: true,
filterable: false,
searchable: true
},
{
key: 'lastChecked',
label: t('connections.field.last_checked', 'Last Checked'),
type: 'date',
width: 150,
sortable: true,
filterable: true,
searchable: true
},
{
key: 'expiresAt',
label: t('connections.field.expires_at', 'Expires At'),
type: 'date',
width: 150,
sortable: true,
filterable: true
}
];
// Fetch connections on mount
useEffect(() => {
fetchConnections();
}, []);
// Handler functions
const handleCreateConnection = async (type: 'msft' | 'google') => {
console.log('Creating connection for type:', type);
try {
const connectionData: CreateConnectionData = {
type: type,
status: 'pending',
connectedAt: new Date().toISOString(),
lastChecked: new Date().toISOString()
};
console.log('Sending connection data to backend:', connectionData);
const newConnection = await createConnection(connectionData);
console.log('Connection created successfully:', newConnection);
await fetchConnections();
} catch (error) {
console.error('Error creating connection:', error);
}
};
const handleConnect = async (connection: Connection) => {
console.log('Connecting to service:', connection);
try {
await connectWithPopup(connection.id);
await fetchConnections();
} catch (error) {
console.error('Error connecting to service:', error);
}
};
const handleDisconnect = async (connection: Connection) => {
console.log('Disconnecting from service:', connection);
try {
await disconnect(connection.id);
await fetchConnections();
} catch (error) {
console.error('Error disconnecting from service:', error);
}
};
const handleDelete = async (connection: Connection) => {
const serviceName = connection.authority?.charAt(0).toUpperCase() + connection.authority?.slice(1) || t('connections.unknown', 'Unknown');
const confirmMessage = t('connections.confirm_delete', 'Are you sure you want to delete the {service} connection?').replace('{service}', serviceName);
if (window.confirm(confirmMessage)) {
try {
await deleteConnection(connection.id);
await fetchConnections();
} catch (error) {
console.error('Error deleting connection:', error);
}
}
};
const handleConnectOrDisconnect = async (connection: Connection) => {
if (connection.status === 'active') {
await handleDisconnect(connection);
} else {
await handleConnect(connection);
}
};
const handleEditConnection = async (connection: Connection) => {
console.log('Editing connection:', connection);
setEditingConnection(connection);
setEditPopupOpen(true);
};
const handleSaveConnection = async (updatedConnection: Connection) => {
if (!editingConnection) return;
try {
const updateData = {
externalUsername: updatedConnection.externalUsername,
externalEmail: updatedConnection.externalEmail
};
await updateConnection(editingConnection.id, updateData);
console.log('Connection updated successfully');
await fetchConnections();
setEditPopupOpen(false);
setEditingConnection(null);
} catch (error) {
console.error('Error updating connection:', error);
}
};
const handleCancelEdit = () => {
setEditPopupOpen(false);
setEditingConnection(null);
};
// Table actions
const tableActions: TableAction[] = [
{
label: t('connections.action.edit', 'Edit'),
onClick: handleEditConnection,
icon: <MdModeEdit />
},
{
label: t('connections.action.toggle_connection', 'Toggle Connection'),
onClick: handleConnectOrDisconnect,
icon: (connection: Connection) => connection.status === 'active' ? <GoUnlink /> : <IoIosLink />
},
{
label: t('connections.action.delete', 'Delete'),
onClick: handleDelete,
icon: <IoIosTrash />
}
];
return {
// Data
connections,
isLoading,
isConnecting,
isDisconnecting,
error,
connectError,
disconnectError,
editPopupOpen,
editingConnection,
connectionColumns,
connectionEditFields,
tableActions,
// Handlers
handleCreateConnection,
handleConnect,
handleDisconnect,
handleDelete,
handleConnectOrDisconnect,
handleEditConnection,
handleSaveConnection,
handleCancelEdit
};
}

View file

@ -1,13 +1,10 @@
import React, { useState } from "react";
import { BsArrowsAngleExpand, BsArrowsAngleContract } from "react-icons/bs";
import { motion, AnimatePresence } from "framer-motion";
import { Prompt } from "../../../hooks/usePrompts";
import { useLanguage } from '../../../contexts/LanguageContext';
import DashboardChatArea from './DashboardChatArea/DashboardChatArea';
import DashboardChatHistory from './DashboardChatHistory/DashboardChatHistory';
import DashboardChatArea from './DashboardChatArea';
import styles from './DashboardChat.module.css';
import styles from './DashboardChatAreaStyles/DashboardChat.module.css';
interface DashboardChatProps {
isExpanded: boolean;
@ -20,8 +17,6 @@ interface DashboardChatProps {
}
const DashboardChat: React.FC<DashboardChatProps> = ({
isExpanded,
onToggleExpand,
selectedPrompt,
onPromptUsed,
onWorkflowIdChange,

View file

@ -25,13 +25,20 @@ const DashboardChatArea: React.FC<DashboardChatAreaProps> = ({
// Workflow state
const [currentWorkflowId, setCurrentWorkflowId] = useState<string | null>(resumeWorkflowId || null);
// Update current workflow ID when resumeWorkflowId changes
React.useEffect(() => {
if (resumeWorkflowId !== currentWorkflowId) {
setCurrentWorkflowId(resumeWorkflowId);
}
}, [resumeWorkflowId, currentWorkflowId]);
// Handle workflow ID changes
const handleWorkflowIdChange = (workflowId: string | null) => {
const handleWorkflowIdChange = React.useCallback((workflowId: string | null) => {
setCurrentWorkflowId(workflowId);
if (onWorkflowIdChange) {
onWorkflowIdChange(workflowId);
}
};
}, [onWorkflowIdChange]);
// Handle resizing
const handleMouseDown = (direction: 'horizontal' | 'vertical') => (e: React.MouseEvent) => {

View file

@ -1,127 +0,0 @@
import React from "react";
import { motion, AnimatePresence } from "framer-motion";
import { WorkflowStatusDisplayProps } from "./dashboardChatAreaTypes";
import { useLanguage } from "../../../../contexts/LanguageContext";
import styles from './DashboardChatArea.module.css';
const WorkflowStatusDisplay: React.FC<WorkflowStatusDisplayProps> = ({
currentWorkflowId,
workflowStatus,
workflowCompleted,
onStartNewWorkflow,
handleRetry,
shouldShowRetryButton
}) => {
const { t } = useLanguage();
return (
<AnimatePresence>
{currentWorkflowId && shouldShowRetryButton() && (
<motion.div
className={styles.workflow_status}
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.8 }}
transition={{ duration: 0.3, ease: "easeOut" }}
>
<div className={styles.retry_container}>
<span className={styles.failed_message}>
{t('chat.workflow_failed')}
</span>
<button
onClick={handleRetry}
className={styles.retry_button}
>
{t('chat.retry_workflow')}
</button>
</div>
</motion.div>
)}
{currentWorkflowId && !workflowCompleted && !shouldShowRetryButton() && (
<motion.div
className={styles.workflow_status}
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.8 }}
transition={{ duration: 0.3, ease: "easeOut" }}
>
<div
className={styles.dots_container}
style={{ display: 'flex', alignItems: 'center', gap: '4px', height: '20px' }}
>
<motion.span
initial={{ opacity: 0, scale: 0 }}
animate={{
opacity: 1,
scale: 1,
y: [0, -7, 0]
}}
exit={{ opacity: 0, scale: 0 }}
transition={{
opacity: { duration: 0.4, ease: "easeOut" },
scale: { duration: 0.4, ease: "easeOut" },
y: {
duration: 1.2,
repeat: Infinity,
ease: "easeInOut",
times: [0, 0.5, 1],
delay: 0.2
}
}}
style={{ fontSize: '20px', display: 'inline-block' }}
>
</motion.span>
<motion.span
initial={{ opacity: 0, scale: 0 }}
animate={{
opacity: 1,
scale: 1,
y: [0, -7, 0]
}}
exit={{ opacity: 0, scale: 0 }}
transition={{
opacity: { duration: 0.4, ease: "easeOut", delay: 0.1 },
scale: { duration: 0.4, ease: "easeOut", delay: 0.1 },
y: {
duration: 1.2,
repeat: Infinity,
ease: "easeInOut",
times: [0, 0.5, 1],
delay: 0.4
}
}}
style={{ fontSize: '20px', display: 'inline-block' }}
>
</motion.span>
<motion.span
initial={{ opacity: 0, scale: 0 }}
animate={{
opacity: 1,
scale: 1,
y: [0, -7, 0]
}}
exit={{ opacity: 0, scale: 0 }}
transition={{
opacity: { duration: 0.4, ease: "easeOut", delay: 0.2 },
scale: { duration: 0.4, ease: "easeOut", delay: 0.2 },
y: {
duration: 1.2,
repeat: Infinity,
ease: "easeInOut",
times: [0, 0.5, 1],
delay: 0.6
}
}}
style={{ fontSize: '20px', display: 'inline-block' }}
>
</motion.span>
</div>
</motion.div>
)}
</AnimatePresence>
);
};
export default WorkflowStatusDisplay;

View file

@ -1,536 +0,0 @@
.chat_area {
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
overflow: hidden;
font-family: var(--font-family);
}
.chat_messages {
flex: 1;
padding: 15px;
border-radius: 15px;
margin-bottom: 15px;
min-height: 0;
overflow-y: auto;
overflow-x: hidden;
scroll-behavior: smooth;
}
.chat_messages::-webkit-scrollbar {
width: 6px;
}
.chat_messages::-webkit-scrollbar-track {
background: transparent;
}
.chat_messages::-webkit-scrollbar-thumb {
background: var(--color-gray-disabled);
border-radius: 3px;
}
.chat_messages::-webkit-scrollbar-thumb:hover {
background: var(--color-gray);
}
.messages_container {
min-height: 100%;
display: flex;
flex-direction: column;
justify-content: flex-start;
}
.messages_spacer {
flex: 1;
min-height: 20px;
}
.chat_input {
display: flex;
gap: 10px;
align-items: flex-end;
flex-shrink: 0;
flex-direction: column;
transition: all 0.2s ease;
}
.chat_input.drag_over {
background-color: var(--color-secondary-disabled);
border: 2px dashed var(--color-secondary);
border-radius: 12px;
padding: 8px;
}
.input_row {
display: flex;
gap: 10px;
align-items: flex-end;
width: 100%;
}
.message_input {
flex: 1;
padding: 12px 16px;
border: 1px solid var(--color-gray-disabled);
border-radius: 12px;
outline: none;
font-size: 14px;
font-family: var(--font-family);
background-color: var(--color-bg);
color: var(--color-text);
}
.message_input:focus {
border-color: var(--color-secondary);
}
.message_input:disabled {
background-color: var(--color-surface);
cursor: not-allowed;
opacity: 0.6;
}
.attachment_button {
height: 48px;
width: 48px;
background-color: var(--color-secondary-disabled);
color: var(--color-secondary);
border: none;
border-radius: 12px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
display: flex;
align-items: center;
justify-content: center;
transition: background-color 0.2s ease, border-color 0.2s ease;
font-family: var(--font-family);
}
.attachment_button:hover {
background-color: var(--color-secondary-hover);
color: var(--color-bg);
}
.attachment_button:disabled {
background-color: var(--color-surface);
cursor: not-allowed;
opacity: 0.6;
}
.attached_files {
display: flex;
flex-wrap: wrap;
gap: 8px;
width: 100%;
margin-bottom: 5px;
}
.attached_file {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 8px;
background-color: var(--color-secondary-disabled);
border: 1px solid var(--color-secondary);
border-radius: 8px;
font-size: 12px;
color: var(--color-secondary);
font-family: var(--font-family);
}
.attached_file_icon {
font-size: 16px;
}
.attached_file_name {
max-width: 150px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.attached_file_remove {
background: none;
border: none;
color: var(--color-gray);
cursor: pointer;
padding: 0;
margin-left: 4px;
display: flex;
align-items: center;
justify-content: center;
width: 16px;
height: 16px;
border-radius: 50%;
transition: background-color 0.2s ease, color 0.2s ease;
}
.attached_file_remove:hover {
background-color: var(--color-gray-disabled);
color: var(--color-text);
}
.send_button {
height: 48px;
width: 48px;
background-color: var(--color-secondary);
color: var(--color-bg);
border: 1px solid var(--color-secondary);
border-radius: 12px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
display: flex;
align-items: center;
justify-content: center;
font-family: var(--font-family);
}
.send_button:hover {
background-color: var(--color-secondary-hover);
}
.send_button_icon {
height: 60%;
width: 60%;
margin: none;
padding: none;
}
.send_button:disabled {
background-color: var(--color-gray-disabled);
cursor: not-allowed;
opacity: 0.6;
border: 1px solid var(--color-gray-disabled);
}
.stop_button {
padding: 12px 12px;
height: 48px;
width: 48px;
background-color: var(--color-red);
color: var(--color-bg);
border: none;
border-radius: 12px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
display: flex;
align-items: center;
justify-content: center;
font-family: var(--font-family);
}
.stop_button:hover {
background-color: var(--color-red-hover);
}
.stop_button:disabled {
background-color: var(--color-gray-disabled);
cursor: not-allowed;
opacity: 0.6;
}
.loading_message {
padding: 10px;
background-color: var(--color-secondary-disabled);
border-left: 4px solid var(--color-secondary);
border-radius: 4px;
margin-bottom: 10px;
}
.loading_message p {
margin: 0;
color: var(--color-secondary);
font-size: 14px;
font-family: var(--font-family);
}
.error_message {
padding: 10px;
background-color: var(--color-red-disabled);
border-left: 4px solid var(--color-red);
border-radius: 4px;
margin-bottom: 10px;
}
.error_message p {
margin: 0;
color: var(--color-red);
font-size: 14px;
font-family: var(--font-family);
}
.message {
margin-bottom: 15px;
padding: 12px;
border-radius: 12px;
max-width: 80%;
font-family: var(--font-family);
}
.message_user {
background-color: var(--color-secondary);
color: var(--color-bg);
margin-left: auto;
margin-right: 0;
}
.message_assistant {
background-color: var(--color-surface);
color: var(--color-text);
margin-left: 0;
margin-right: auto;
}
.message_system {
background-color: var(--color-primary-disabled);
color: var(--color-primary);
margin-left: auto;
margin-right: auto;
text-align: center;
}
.message_role {
font-size: 12px;
font-weight: 600;
margin-bottom: 4px;
opacity: 0.8;
font-family: var(--font-family);
}
.message_content {
font-size: 14px;
line-height: 1.4;
white-space: pre-wrap;
word-wrap: break-word;
font-family: var(--font-family);
}
.message_timestamp {
font-size: 11px;
margin-top: 4px;
opacity: 0.6;
font-family: var(--font-family);
}
.placeholder_text {
text-align: center;
color: var(--color-gray);
font-style: italic;
margin: 20px 0;
font-family: var(--font-family);
}
.workflow_status {
display: flex;
justify-content: center;
align-items: center;
padding: 10px;
margin: 5px 0;
}
.workflow_status p {
margin: 0;
font-size: 14px;
color: var(--color-gray);
font-family: var(--font-family);
}
.retry_container {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 16px;
background-color: var(--color-red-disabled);
border: 1px solid var(--color-red);
border-radius: 12px;
}
.failed_message {
font-size: 14px;
color: var(--color-red);
font-family: var(--font-family);
font-weight: 500;
}
.retry_button {
background-color: var(--color-primary);
color: var(--color-bg);
border: none;
border-radius: 8px;
padding: 8px 16px;
font-size: 12px;
font-weight: 500;
cursor: pointer;
font-family: var(--font-family);
transition: background-color 0.2s ease, transform 0.2s ease;
white-space: nowrap;
}
.retry_button:hover {
background-color: var(--color-primary-hover);
transform: translateY(-1px);
}
.retry_button:disabled {
background-color: var(--color-gray-disabled);
cursor: not-allowed;
transform: none;
opacity: 0.6;
}
.completion_message {
padding: 10px 12px;
background-color: var(--color-secondary-disabled);
border-left: 4px solid var(--color-secondary);
border-radius: 4px;
margin-bottom: 10px;
text-align: center;
}
.completion_message p {
margin: 0 0 10px 0;
color: var(--color-secondary);
font-size: 14px;
font-weight: 600;
font-family: var(--font-family);
}
.new_workflow_button {
background-color: var(--color-secondary);
color: var(--color-bg);
border: none;
border-radius: 8px;
padding: 8px 16px;
font-size: 12px;
font-weight: 500;
cursor: pointer;
transition: background-color 0.2s ease;
font-family: var(--font-family);
}
.new_workflow_button:hover {
background-color: var(--color-secondary-hover);
}
.message_documents {
margin-top: 8px;
display: flex;
flex-direction: column;
gap: 6px;
}
.document_item {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 10px;
background-color: var(--color-bg);
border: 1px solid var(--color-gray-disabled);
border-radius: 6px;
cursor: pointer;
transition: all 0.2s ease;
font-size: 13px;
}
.document_item:hover {
border-color: var(--color-primary);
background-color: var(--color-surface);
}
.message_assistant .document_item {
background-color: var(--color-bg);
border-color: var(--color-gray-disabled);
}
.message_assistant .document_item:hover {
border-color: var(--color-primary);
background-color: var(--color-surface);
}
.document_icon {
font-size: 16px;
color: var(--color-secondary);
flex-shrink: 0;
}
.document_info {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 2px;
}
.document_name {
font-weight: 500;
color: var(--color-text);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-family: var(--font-family);
}
.document_meta {
display: flex;
gap: 8px;
font-size: 11px;
color: var(--color-gray);
font-family: var(--font-family);
}
.document_size {
font-size: 11px;
color: var(--color-gray);
}
.document_type {
font-size: 11px;
color: var(--color-gray);
}
.document_actions {
display: flex;
gap: 4px;
opacity: 1;
transition: opacity 0.2s ease;
}
.document_item:hover .document_actions {
opacity: 1;
}
.document_action_button {
background: none;
border: none;
color: var(--color-gray);
cursor: pointer;
padding: 4px;
border-radius: 4px;
font-size: 14px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
}
.document_action_button:hover {
background-color: var(--color-surface);
color: var(--color-text);
}
.message_assistant .document_action_button {
color: var(--color-gray);
}
.message_assistant .document_action_button:hover {
background-color: var(--color-surface);
color: var(--color-text);
}

View file

@ -1,104 +0,0 @@
import React, { useEffect } from "react";
import { DashboardChatAreaProps } from "./dashboardChatAreaTypes";
import { useChatLogic } from "./dashboardChatAreaLogic";
import { useLanguage } from "../../../../contexts/LanguageContext";
import MessageList from "./DashboardChatAreaMessageList";
import ChatInput from "./DashboardChatAreaInput";
import styles from './DashboardChatArea.module.css';
const DashboardChatArea: React.FC<DashboardChatAreaProps> = ({
selectedPrompt,
onPromptUsed,
onWorkflowIdChange,
onWorkflowCompletedChange,
resumeWorkflowId
}) => {
const {
// State
inputValue,
setInputValue,
currentWorkflowId,
workflowCompleted,
attachedFiles,
// Refs
inputRef,
messagesEndRef,
// Data from hooks
messages,
messagesLoading,
messagesError,
startingWorkflow,
startError,
workflowStatus,
// Handlers
handleSend,
handleKeyPress,
startNewWorkflow,
handleStopWorkflow,
handleFileAttach,
handleFileRemove,
handleFilesSelect,
handleRetry,
// Workflow state
isWorkflowRunning,
isStoppingWorkflow,
shouldShowRetryButton
} = useChatLogic({
selectedPrompt,
onPromptUsed,
onWorkflowIdChange,
resumeWorkflowId
});
const { t } = useLanguage();
// Notify parent component when workflow completion status changes
useEffect(() => {
if (onWorkflowCompletedChange) {
onWorkflowCompletedChange(workflowCompleted);
}
}, [workflowCompleted, onWorkflowCompletedChange]);
const placeholder = workflowCompleted ? t('chat.continue_conversation') : t('chat.enter_message');
return (
<div className={styles.chat_area}>
<MessageList
messages={messages}
currentWorkflowId={currentWorkflowId}
workflowStatus={workflowStatus}
workflowCompleted={workflowCompleted}
startingWorkflow={startingWorkflow}
startError={startError}
messagesError={messagesError}
messagesLoading={messagesLoading}
onStartNewWorkflow={startNewWorkflow}
messagesEndRef={messagesEndRef}
handleRetry={handleRetry}
shouldShowRetryButton={shouldShowRetryButton}
/>
<ChatInput
inputValue={inputValue}
setInputValue={setInputValue}
onSend={handleSend}
onKeyPress={handleKeyPress}
isDisabled={startingWorkflow}
placeholder={placeholder}
inputRef={inputRef}
isWorkflowRunning={isWorkflowRunning}
onStopWorkflow={handleStopWorkflow}
isStoppingWorkflow={isStoppingWorkflow}
attachedFiles={attachedFiles}
onFileAttach={handleFileAttach}
onFileRemove={handleFileRemove}
onFilesSelect={handleFilesSelect}
/>
</div>
);
};
export default DashboardChatArea;

View file

@ -1,239 +0,0 @@
import React, { useState, useEffect } from "react";
import { motion } from "framer-motion";
import { LuSendHorizontal } from "react-icons/lu";
import { FaStop } from "react-icons/fa";
import { IoAttach, IoClose } from "react-icons/io5";
import { ChatInputProps } from "./dashboardChatAreaTypes";
import { FileInfo } from "../../../../hooks/useFiles";
import { useLanguage } from "../../../../contexts/LanguageContext";
import DateienSelector from "../../../Dateien/DateienHinzufügen/DateienSelector";
import styles from './DashboardChatArea.module.css';
// Helper function to get file icon based on type
const getFileIcon = (mimeType?: string): string => {
if (!mimeType) return '📄';
const type = mimeType.toLowerCase();
if (type.includes('image')) return '🖼️';
if (type.includes('video')) return '🎥';
if (type.includes('audio')) return '🎵';
if (type.includes('pdf')) return '📕';
if (type.includes('word') || type.includes('document')) return '📘';
if (type.includes('excel') || type.includes('spreadsheet')) return '📊';
if (type.includes('powerpoint') || type.includes('presentation')) return '📋';
if (type.includes('text')) return '📝';
if (type.includes('zip') || type.includes('archive')) return '📦';
if (type.includes('javascript') || type.includes('json') || type.includes('html') || type.includes('css')) return '💻';
return '📄';
};
const ChatInput: React.FC<ChatInputProps> = ({
inputValue,
setInputValue,
onSend,
onKeyPress,
isDisabled,
placeholder,
inputRef,
isWorkflowRunning,
onStopWorkflow,
isStoppingWorkflow,
attachedFiles,
onFileAttach,
onFileRemove,
onFilesSelect
}) => {
const { t } = useLanguage();
const [isUploadModalOpen, setIsUploadModalOpen] = useState(false);
const [isDragOver, setIsDragOver] = useState(false);
// Auto-resize textarea functionality
useEffect(() => {
if (inputRef?.current) {
const textarea = inputRef.current;
textarea.style.height = 'auto';
// Calculate line height - approximately 1.5em per line
const lineHeight = 24; // Adjust this value based on your CSS line-height
const maxHeight = lineHeight * 8; // 8 lines maximum
const newHeight = Math.min(textarea.scrollHeight, maxHeight);
textarea.style.height = `${newHeight}px`;
// Enable/disable scroll based on content height
if (textarea.scrollHeight > maxHeight) {
textarea.style.overflowY = 'auto';
} else {
textarea.style.overflowY = 'hidden';
}
}
}, [inputValue, inputRef]);
const handleAttachmentClick = () => {
setIsUploadModalOpen(true);
};
const handleFilesSelected = (files: FileInfo[]) => {
onFilesSelect(files);
setIsUploadModalOpen(false);
};
const handleFileRemove = (fileId: number) => {
onFileRemove(fileId);
};
// Handle Enter key press for sending message (without Shift)
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
if (!isDisabled && (inputValue.trim() || attachedFiles.length > 0)) {
onSend();
}
}
// Call original onKeyPress if it exists (for backward compatibility)
if (onKeyPress && e.key !== 'Enter') {
onKeyPress(e as any);
}
};
// Drag and drop handlers
const handleDragEnter = (e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
if (!isDisabled && !isWorkflowRunning) {
setIsDragOver(true);
}
};
const handleDragOver = (e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
};
const handleDragLeave = (e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
// Only set drag over to false if we're leaving the entire input area
if (!e.currentTarget.contains(e.relatedTarget as Node)) {
setIsDragOver(false);
}
};
const handleDrop = (e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
setIsDragOver(false);
if (isDisabled || isWorkflowRunning) {
return;
}
const files = Array.from(e.dataTransfer.files);
if (files.length > 0) {
// Convert File objects to FileInfo objects
const fileInfos: FileInfo[] = files.map((file, index) => ({
id: Date.now() + index, // Generate unique IDs
name: file.name,
mimeType: file.type,
size: file.size,
creationDate: new Date().toISOString(),
source: 'user_uploaded'
}));
onFilesSelect(fileInfos);
}
};
return (
<motion.div
className={`${styles.chat_input} ${isDragOver ? styles.drag_over : ''}`}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.3, duration: 0.3, ease: "easeOut" }}
onDragEnter={handleDragEnter}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
>
{/* Show attached files if any */}
{attachedFiles.length > 0 && (
<div className={styles.attached_files}>
{attachedFiles.map((file) => (
<div key={file.id} className={styles.attached_file}>
<span className={styles.attached_file_icon}>
{getFileIcon(file.mimeType)}
</span>
<span className={styles.attached_file_name}>
{file.name}
</span>
<button
className={styles.attached_file_remove}
onClick={() => handleFileRemove(file.id)}
title={t('chat.remove_file')}
>
<IoClose size={12} />
</button>
</div>
))}
</div>
)}
{/* Input row with text input, attachment button, and send button */}
<div className={styles.input_row}>
<textarea
ref={inputRef}
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onKeyDown={handleKeyDown}
placeholder={placeholder}
className={styles.message_input}
disabled={isDisabled}
rows={1}
style={{
resize: 'none',
minHeight: '24px',
lineHeight: '24px'
}}
/>
{/* Attachment button */}
<motion.button
className={styles.attachment_button}
onClick={handleAttachmentClick}
disabled={isDisabled || isWorkflowRunning}
whileTap={{ scale: 0.95 }}
transition={{ duration: 0.2, ease: "easeOut" }}
title={t('chat.attach_file')}
>
<IoAttach size={26} />
</motion.button>
{/* Send/Stop button */}
<motion.button
className={isWorkflowRunning ? styles.stop_button : styles.send_button}
onClick={isWorkflowRunning ? onStopWorkflow : onSend}
disabled={isWorkflowRunning ? isStoppingWorkflow : (isDisabled || (!inputValue.trim() && attachedFiles.length === 0))}
whileTap={{ scale: 0.95 }}
transition={{ duration: 0.2, ease: "easeOut" }}
>
{isWorkflowRunning ? (
<FaStop className={styles.send_button_icon}/>
) : (
<LuSendHorizontal className={styles.send_button_icon}/>
)}
</motion.button>
</div>
{/* Upload Modal */}
<DateienSelector
isOpen={isUploadModalOpen}
onClose={() => setIsUploadModalOpen(false)}
onFilesSelected={handleFilesSelected}
/>
</motion.div>
);
};
export default ChatInput;

View file

@ -1,193 +0,0 @@
import React, { useState } from "react";
import { FaDownload } from "react-icons/fa";
import { MdOutlineRemoveRedEye } from "react-icons/md";
import { Message, Document } from "./dashboardChatAreaTypes";
import FilePreviewPopup from "./FilePreviewPopup";
import { useFileDownload } from "../../../../hooks/useWorkflows";
import { useLanguage } from "../../../../contexts/LanguageContext";
import styles from './DashboardChatArea.module.css';
interface MessageItemProps {
message: Message;
index: number;
}
// Helper function to format file size
const formatFileSize = (bytes?: number): string => {
if (!bytes) return '';
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(1024));
return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i];
};
// Helper function to get file icon based on type or extension
const getFileIcon = (type?: string, ext?: string): string => {
// Use extension first if available, then fall back to MIME type
const extension = ext?.toLowerCase();
const mimeType = type?.toLowerCase();
// Check extension first
if (extension) {
// Images
if (['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg'].includes(extension)) return '🖼️';
// Videos
if (['mp4', 'avi', 'mov', 'wmv', 'flv', 'webm', 'mkv'].includes(extension)) return '🎥';
// Audio
if (['mp3', 'wav', 'aac', 'flac', 'ogg', 'wma'].includes(extension)) return '🎵';
// Documents
if (extension === 'pdf') return '📕';
if (['doc', 'docx'].includes(extension)) return '📘';
if (['xls', 'xlsx'].includes(extension)) return '📊';
if (['ppt', 'pptx'].includes(extension)) return '📋';
if (['txt', 'md', 'rtf'].includes(extension)) return '📝';
// Archives
if (['zip', 'rar', '7z', 'tar', 'gz'].includes(extension)) return '📦';
// Code files
if (['js', 'ts', 'jsx', 'tsx', 'html', 'css', 'py', 'java', 'cpp', 'c', 'php'].includes(extension)) return '💻';
}
// Fall back to MIME type if extension didn't match
if (mimeType) {
if (mimeType.includes('image')) return '🖼️';
if (mimeType.includes('video')) return '🎥';
if (mimeType.includes('audio')) return '🎵';
if (mimeType.includes('pdf')) return '📕';
if (mimeType.includes('text')) return '📝';
if (mimeType.includes('word') || mimeType.includes('document')) return '📘';
if (mimeType.includes('excel') || mimeType.includes('spreadsheet')) return '📊';
if (mimeType.includes('powerpoint') || mimeType.includes('presentation')) return '📋';
if (mimeType.includes('zip') || mimeType.includes('archive')) return '📦';
}
return '📄';
};
const MessageItem: React.FC<MessageItemProps> = ({ message, index }) => {
const { t } = useLanguage();
const [previewDocument, setPreviewDocument] = useState<Document | null>(null);
const [isPreviewOpen, setIsPreviewOpen] = useState(false);
const { downloadFile, isDownloading, error: downloadError } = useFileDownload();
const handleDocumentClick = (document: Document) => {
// If there's a downloadUrl, use it; otherwise try the url
const downloadLink = document.downloadUrl || document.url;
if (downloadLink) {
// Open the document in a new tab
window.open(downloadLink, '_blank');
}
};
const handlePreview = (document: Document, e: React.MouseEvent) => {
e.stopPropagation();
// Use fileId if available, otherwise try to use id as fallback
const fileId = document.fileId || document.id;
if (!fileId) {
return;
}
setPreviewDocument(document);
setIsPreviewOpen(true);
};
const handleClosePreview = () => {
setIsPreviewOpen(false);
setPreviewDocument(null);
};
const handleDownload = async (document: Document, e: React.MouseEvent) => {
e.stopPropagation();
// Use fileId if available, otherwise try to use id as fallback
const fileId = document.fileId || document.id;
if (!fileId) {
return;
}
// Construct filename with extension if available
const fileName = document.ext ? `${document.name}.${document.ext}` : document.name;
await downloadFile(fileId, fileName);
};
return (
<div
key={message.id || index}
className={`${styles.message} ${styles[`message_${message.role}`]}`}
>
<div className={styles.message_role}>
{message.role === 'user' ? t('chat.you') : message.agentName}
</div>
<div className={styles.message_content}>
{message.content}
</div>
{message.documents && message.documents.length > 0 && (
<div className={styles.message_documents}>
{message.documents.map((document, docIndex) => (
<div
key={document.id || docIndex}
className={styles.document_item}
onClick={() => handleDocumentClick(document)}
title={`${t('chat.click_to_open')} ${document.name}`}
>
<span className={styles.document_icon}>
{getFileIcon(document.type, document.ext)}
</span>
<div className={styles.document_info}>
<div className={styles.document_name}>
{document.ext ? `${document.name}.${document.ext}` : document.name}
</div>
<div className={styles.document_meta}>
{document.size && (
<span className={styles.document_size}>
{formatFileSize(document.size)}
</span>
)}
</div>
</div>
<div className={styles.document_actions}>
<button
className={styles.document_action_button}
onClick={(e) => handlePreview(document, e)}
title={t('chat.preview_document')}
>
<MdOutlineRemoveRedEye />
</button>
<button
className={styles.document_action_button}
onClick={(e) => handleDownload(document, e)}
title={t('chat.download_document')}
>
<FaDownload />
</button>
</div>
</div>
))}
</div>
)}
{message.timestamp && (
<div className={styles.message_timestamp}>
{new Date(message.timestamp).toLocaleTimeString()}
</div>
)}
{/* File Preview Popup */}
{previewDocument && (
<FilePreviewPopup
document={previewDocument}
isOpen={isPreviewOpen}
onClose={handleClosePreview}
/>
)}
</div>
);
};
export default MessageItem;

View file

@ -1,83 +0,0 @@
import React from "react";
import { motion } from "framer-motion";
import { MessageListProps } from "./dashboardChatAreaTypes";
import MessageItem from "./DashboardChatAreaMessageItem";
import WorkflowStatusDisplay from "./DashbaordChatAreaStatusDisplayold";
import { useLanguage } from "../../../../contexts/LanguageContext";
import styles from './DashboardChatArea.module.css';
const MessageList: React.FC<MessageListProps> = ({
messages,
currentWorkflowId,
workflowStatus,
workflowCompleted,
startingWorkflow,
startError,
messagesError,
messagesLoading,
onStartNewWorkflow,
messagesEndRef,
handleRetry,
shouldShowRetryButton
}) => {
const { t } = useLanguage();
return (
<motion.div
className={styles.chat_messages}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.2, duration: 0.3, ease: "easeOut" }}
>
<div className={styles.messages_container}>
{startingWorkflow && (
<div className={styles.loading_message}>
<p>{workflowCompleted && currentWorkflowId ? t('chat.sending_followup', 'Sending follow-up message...') : t('chat.sending_message', 'Sending message...')}</p>
</div>
)}
{startError && (
<div className={styles.error_message}>
<p>{t('chat.error_prefix', 'Error:')} {startError}</p>
</div>
)}
{messagesError && (
<div className={styles.error_message}>
<p>{t('chat.error_loading_messages', 'Error loading messages:')} {messagesError}</p>
</div>
)}
{currentWorkflowId && messagesLoading && messages.length === 0 && (
<div className={styles.loading_message}>
<p>{t('chat.loading_workflow_messages', 'Loading workflow messages...')}</p>
</div>
)}
{messages.length > 0 ? (
messages.map((message, index) => (
<MessageItem
key={message.id || index}
message={message}
index={index}
/>
))
) : !currentWorkflowId ? (
<p className={styles.placeholder_text}>{t('chat.start_conversation', 'Start a conversation by entering a message, selecting a template, or continuing a previous workflow...')}</p>
) : null}
{/* Spacer to push workflow status to bottom when there are fewer messages */}
{messages.length < 3 && <div className={styles.messages_spacer} />}
<WorkflowStatusDisplay
currentWorkflowId={currentWorkflowId}
workflowStatus={workflowStatus}
workflowCompleted={workflowCompleted}
onStartNewWorkflow={onStartNewWorkflow}
handleRetry={handleRetry}
shouldShowRetryButton={shouldShowRetryButton}
/>
<div ref={messagesEndRef} />
</div>
</motion.div>
);
};
export default MessageList;

View file

@ -1,455 +0,0 @@
.overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: var(--color-bg);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
padding: 20px;
}
.popup {
background: var(--color-bg);
border-radius: 12px;
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
max-width: 90vw;
height: 90vh;
width: 80vw;
display: flex;
flex-direction: column;
overflow: hidden;
font-family: var(--font-family);
}
.header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px;
border-bottom: 1px solid var(--color-gray-disabled);
background-color: var(--color-surface);
flex-shrink: 0;
}
.title {
margin: 0;
font-size: 18px;
font-weight: 600;
color: var(--color-text);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
flex: 1;
margin-right: 16px;
font-family: var(--font-family);
}
.close_button {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
border: none;
border-radius: 8px;
background-color: transparent;
color: var(--color-gray);
cursor: pointer;
transition: all 0.2s ease;
font-size: 18px;
flex-shrink: 0;
}
.close_button:hover {
background-color: var(--color-gray-disabled);
color: var(--color-text);
}
.content {
flex: 1;
display: flex;
align-items: flex-start;
justify-content: center;
padding: 20px;
overflow: hidden;
height: 0;
}
.loading {
display: flex;
align-items: center;
justify-content: center;
color: var(--color-gray);
font-size: 16px;
width: 100%;
height: 100%;
font-family: var(--font-family);
}
.error {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: var(--color-red);
font-size: 16px;
text-align: center;
width: 100%;
height: 100%;
font-family: var(--font-family);
}
.no_preview {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: var(--color-gray);
font-size: 16px;
font-style: italic;
width: 100%;
height: 100%;
font-family: var(--font-family);
}
.image_preview {
max-width: 100%;
max-height: 100%;
object-fit: contain;
border-radius: 8px;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
}
.pdf_preview {
width: 100%;
height: 100%;
border: none;
border-radius: 8px;
}
.text_preview {
width: 100%;
height: 100%;
background-color: var(--color-bg);
border: 1px solid var(--color-gray-disabled);
border-radius: 8px;
padding: 16px;
font-family: 'Courier New', monospace;
font-size: 14px;
line-height: 1.5;
color: var(--color-text);
overflow: auto;
white-space: pre-wrap;
word-wrap: break-word;
box-sizing: border-box;
}
.enhanced_text_preview {
width: 100%;
height: 100%;
background-color: var(--color-bg);
border: 1px solid var(--color-gray-disabled);
border-radius: 8px;
padding: 32px;
overflow: auto;
font-family: var(--font-family);
box-sizing: border-box;
line-height: 1.7;
color: var(--color-text);
}
.text_line {
margin-bottom: 4px;
word-wrap: break-word;
white-space: pre-wrap;
}
.text_line_break {
height: 16px;
}
.text_header {
font-weight: 600;
font-size: 1.1em;
color: var(--color-text);
margin: 16px 0 8px 0;
padding-bottom: 4px;
border-bottom: 1px solid var(--color-gray-disabled);
font-family: var(--font-family);
}
.text_numbered {
margin: 8px 0;
padding-left: 8px;
color: var(--color-text);
font-family: var(--font-family);
}
.text_bullet {
margin: 4px 0;
padding-left: 8px;
color: var(--color-text);
font-family: var(--font-family);
}
.text_indented {
background-color: var(--color-surface);
padding: 8px 12px;
margin: 4px 0;
border-left: 3px solid var(--color-gray-disabled);
border-radius: 4px;
font-family: 'Courier New', monospace;
font-size: 0.9em;
color: var(--color-text);
}
.code_preview {
width: 100%;
height: 100%;
background-color: var(--color-surface);
border: 1px solid var(--color-gray-disabled);
border-radius: 8px;
overflow: auto;
font-family: 'Courier New', monospace;
font-size: 14px;
line-height: 1.4;
color: var(--color-text);
box-sizing: border-box;
display: flex;
flex-direction: column;
}
.python_code_preview {
width: 100%;
height: 100%;
background-color: var(--color-surface);
border: 1px solid var(--color-gray-disabled);
border-radius: 8px;
overflow: auto;
box-sizing: border-box;
display: flex;
flex-direction: column;
}
.code_header {
background-color: var(--color-gray-disabled);
padding: 8px 16px;
border-bottom: 1px solid var(--color-gray-disabled);
display: flex;
justify-content: space-between;
align-items: center;
flex-shrink: 0;
}
.code_language {
font-size: 12px;
font-weight: 600;
color: var(--color-text);
text-transform: uppercase;
letter-spacing: 0.5px;
font-family: var(--font-family);
}
.code_filename {
font-size: 12px;
color: var(--color-gray);
font-family: var(--font-family);
}
.python_code_content {
padding: 16px;
overflow: auto;
flex: 1;
background-color: var(--color-bg);
font-family: 'Courier New', monospace;
font-size: 14px;
line-height: 1.4;
color: var(--color-text);
white-space: pre;
word-wrap: break-word;
}
.python_code_content code {
font-family: 'Courier New', monospace;
font-size: 14px;
line-height: 1.4;
color: var(--color-text);
background: none;
padding: 0;
}
.markdown_preview {
width: 100%;
height: 100%;
background-color: var(--color-bg);
border: 1px solid var(--color-gray-disabled);
border-radius: 8px;
padding: 32px;
overflow: auto;
font-family: var(--font-family);
box-sizing: border-box;
line-height: 1.6;
color: var(--color-text);
}
.markdown_preview h1 {
font-size: 2em;
font-weight: 700;
margin: 0 0 16px 0;
color: var(--color-text);
border-bottom: 2px solid var(--color-gray-disabled);
padding-bottom: 8px;
}
.markdown_preview h2 {
font-size: 1.5em;
font-weight: 600;
margin: 24px 0 12px 0;
color: var(--color-text);
}
.markdown_preview h3 {
font-size: 1.25em;
font-weight: 600;
margin: 20px 0 10px 0;
color: var(--color-text);
}
.markdown_preview h4,
.markdown_preview h5,
.markdown_preview h6 {
font-size: 1.1em;
font-weight: 600;
margin: 16px 0 8px 0;
color: var(--color-text);
}
.markdown_preview p {
margin: 0 0 16px 0;
line-height: 1.7;
}
.markdown_preview ul,
.markdown_preview ol {
margin: 0 0 16px 20px;
line-height: 1.7;
}
.markdown_preview li {
margin: 4px 0;
line-height: 1.6;
}
.markdown_preview blockquote {
margin: 16px 0;
padding: 12px 16px;
border-left: 4px solid var(--color-primary);
background-color: var(--color-surface);
font-style: italic;
color: var(--color-gray);
}
.markdown_preview code {
background-color: var(--color-surface);
padding: 2px 6px;
border-radius: 4px;
font-family: 'Courier New', monospace;
font-size: 0.9em;
color: var(--color-secondary);
}
.markdown_preview pre {
background-color: var(--color-surface);
border: 1px solid var(--color-gray-disabled);
border-radius: 8px;
padding: 16px;
margin: 16px 0;
overflow-x: auto;
font-family: 'Courier New', monospace;
font-size: 14px;
line-height: 1.4;
}
.markdown_preview pre code {
background: none;
padding: 0;
border-radius: 0;
color: var(--color-text);
}
.markdown_preview table {
border-collapse: collapse;
width: 100%;
margin: 16px 0;
border: 1px solid var(--color-gray-disabled);
}
.markdown_preview th,
.markdown_preview td {
border: 1px solid var(--color-gray-disabled);
padding: 8px 12px;
text-align: left;
}
.markdown_preview th {
background-color: var(--color-surface);
font-weight: 600;
}
.markdown_preview td {
background-color: var(--color-bg);
}
.markdown_preview a {
color: var(--color-secondary);
text-decoration: underline;
}
.markdown_preview a:hover {
color: var(--color-secondary-hover);
}
.markdown_preview hr {
border: none;
height: 1px;
background-color: var(--color-gray-disabled);
margin: 24px 0;
}
.markdown_preview strong {
font-weight: 700;
color: var(--color-text);
}
.markdown_preview em {
font-style: italic;
color: var(--color-gray);
}
/* Responsive design */
@media (max-width: 768px) {
.popup {
width: 95vw;
height: 85vh;
}
.header {
padding: 12px 16px;
}
.title {
font-size: 16px;
}
.content {
padding: 16px;
}
.pdf_preview {
height: calc(100% - 60px);
}
}

View file

@ -1,247 +0,0 @@
import React, { useEffect } from "react";
import ReactMarkdown from 'react-markdown';
import { MdClose } from "react-icons/md";
import { Document } from "./dashboardChatAreaTypes";
import { useFilePreview } from "../../../../hooks/useWorkflows";
import { useLanguage } from "../../../../contexts/LanguageContext";
import styles from './FilePreviewPopup.module.css';
interface FilePreviewPopupProps {
document: Document;
isOpen: boolean;
onClose: () => void;
}
const FilePreviewPopup: React.FC<FilePreviewPopupProps> = ({ document, isOpen, onClose }) => {
const { t } = useLanguage();
const { previewContent, fileMetadata, isLoading, error, fetchPreview, clearPreview } = useFilePreview();
useEffect(() => {
if (isOpen && document) {
// Use fileId if available, otherwise try to use id as fallback
const fileId = document.fileId || document.id;
if (fileId) {
console.log('FilePreviewPopup: calling fetchPreview with fileId:', fileId);
fetchPreview(String(fileId));
} else {
console.error('FilePreviewPopup: No fileId or id available on document:', document);
}
} else if (!isOpen) {
clearPreview();
}
}, [isOpen, document.fileId, document.id]);
const handleBackdropClick = (e: React.MouseEvent) => {
if (e.target === e.currentTarget) {
onClose();
}
};
const getPreviewComponent = () => {
if (isLoading) {
return <div className={styles.loading}>{t('file_preview.loading')}</div>;
}
if (error) {
return (
<div className={styles.error}>
<div>{t('file_preview.error')}: {error}</div>
{fileMetadata && (
<div style={{ marginTop: '10px', fontSize: '12px', opacity: 0.7 }}>
Debug: {JSON.stringify(fileMetadata, null, 2)}
</div>
)}
</div>
);
}
if (!previewContent) {
return (
<div className={styles.no_preview}>
<div>{t('file_preview.no_preview')}</div>
{fileMetadata && (
<div style={{ marginTop: '10px', fontSize: '12px', opacity: 0.7 }}>
Available metadata: {Object.keys(fileMetadata).join(', ')}
<br />
Preview field: {fileMetadata.preview ? 'has data' : 'empty/null'}
<br />
Base64Encoded: {String(fileMetadata.base64Encoded)}
<br />
MimeType: {fileMetadata.mimeType}
</div>
)}
</div>
);
}
// Use metadata from backend response, but prioritize file extension over potentially incorrect MIME type
const mimeType = fileMetadata?.mimeType;
const isBase64Encoded = fileMetadata?.base64Encoded;
const fileExtension = document.ext?.toLowerCase();
// Check if this is a markdown file by extension first (more reliable than backend MIME type)
const isMarkdownByType = fileExtension === 'md' ||
fileExtension === 'markdown' ||
mimeType === 'text/markdown' ||
mimeType === 'text/x-markdown';
// Content-based markdown detection for .txt files with markdown content
// BUT NOT for specific code file types
const isCodeFile = fileExtension === 'py' ||
fileExtension === 'js' ||
fileExtension === 'ts' ||
fileExtension === 'jsx' ||
fileExtension === 'tsx' ||
fileExtension === 'java' ||
fileExtension === 'cpp' ||
fileExtension === 'c' ||
fileExtension === 'php' ||
fileExtension === 'html' ||
fileExtension === 'css';
const hasMarkdownContent = !isCodeFile && previewContent && (
previewContent.includes('# ') || // Headers
previewContent.includes('## ') || // Headers
previewContent.includes('**') || // Bold
previewContent.includes('__') || // Bold
previewContent.includes('- ') || // Lists
previewContent.includes('* ') || // Lists
previewContent.includes('1. ') || // Numbered lists
previewContent.includes('```') || // Code blocks
previewContent.includes('[') && previewContent.includes('](') // Links
);
// For .txt files or text MIME types, check for markdown content
const isTxtWithMarkdown = (fileExtension === 'txt' || mimeType?.startsWith('text/')) && hasMarkdownContent;
const isMarkdown = isMarkdownByType || isTxtWithMarkdown;
// Debug logging
console.log('FilePreviewPopup preview detection:', {
fileExtension,
mimeType,
isMarkdownByType,
hasMarkdownContent,
isTxtWithMarkdown,
isMarkdown,
isCodeFile
});
if (mimeType?.startsWith('image/')) {
// Image preview
const imageSrc = isBase64Encoded
? `data:${mimeType};base64,${previewContent}`
: previewContent;
return (
<img
src={imageSrc}
alt={document.name}
className={styles.image_preview}
/>
);
} else if (fileExtension === 'pdf' || mimeType === 'application/pdf') {
// PDF preview
const pdfSrc = isBase64Encoded
? `data:application/pdf;base64,${previewContent}`
: previewContent;
return (
<iframe
src={pdfSrc}
className={styles.pdf_preview}
title={document.name}
/>
);
} else if (isMarkdown) {
// Markdown preview
console.log('Rendering markdown with ReactMarkdown:', previewContent?.substring(0, 200));
return (
<div className={styles.markdown_preview}>
<ReactMarkdown>{previewContent}</ReactMarkdown>
</div>
);
} else if (fileExtension === 'py') {
// Python code preview
return (
<div className={styles.python_code_preview}>
<div className={styles.code_header}>
<span className={styles.code_language}>{t('file_preview.python')}</span>
<span className={styles.code_filename}>
{document.ext ? `${document.name}.${document.ext}` : document.name}
</span>
</div>
<pre className={styles.python_code_content}>
<code>{previewContent}</code>
</pre>
</div>
);
} else if ((mimeType?.startsWith('text/') || fileExtension === 'txt') && !isMarkdown) {
// Enhanced text preview for text files that are not markdown
return (
<div className={styles.enhanced_text_preview}>
{previewContent?.split('\n').map((line, index) => {
// Handle empty lines
if (line.trim() === '') {
return <div key={index} className={styles.text_line_break}></div>;
}
// Check if line looks like a header (all caps, starts with numbers, etc.)
const isHeader = line.match(/^[A-Z\s\d\.\-_]+:?\s*$/) && line.length < 80;
const isNumberedItem = line.match(/^\s*\d+\.\s/);
const isBulletItem = line.match(/^\s*[-*•]\s/);
const isIndented = line.match(/^\s{4,}/);
return (
<div
key={index}
className={`${styles.text_line} ${
isHeader ? styles.text_header :
isNumberedItem ? styles.text_numbered :
isBulletItem ? styles.text_bullet :
isIndented ? styles.text_indented : ''
}`}
>
{line}
</div>
);
})}
</div>
);
} else {
// Code/raw text preview for non-text files
return (
<pre className={styles.code_preview}>
{previewContent}
</pre>
);
}
};
if (!isOpen) return null;
return (
<div className={styles.overlay} onClick={handleBackdropClick}>
<div className={styles.popup}>
<div className={styles.header}>
<h3 className={styles.title}>
{document.ext ? `${document.name}.${document.ext}` : document.name}
</h3>
<button
className={styles.close_button}
onClick={onClose}
title={t('file_preview.close_preview')}
>
<MdClose />
</button>
</div>
<div className={styles.content}>
{getPreviewComponent()}
</div>
</div>
</div>
);
};
export default FilePreviewPopup;

View file

@ -1,316 +0,0 @@
import { useState, useEffect, useRef } from "react";
import { Prompt } from "../../../../hooks/usePrompts";
import { FileInfo, useFileOperations } from "../../../../hooks/useFiles";
import { useWorkflowOperations, useWorkflowMessages, useWorkflowStatus } from "../../../../hooks/useWorkflows";
interface UseChatLogicProps {
selectedPrompt?: Prompt | null;
onPromptUsed?: () => void;
onWorkflowIdChange?: (workflowId: string | null) => void;
resumeWorkflowId?: string | null;
}
export const useChatLogic = ({
selectedPrompt,
onPromptUsed,
onWorkflowIdChange,
resumeWorkflowId
}: UseChatLogicProps) => {
const [inputValue, setInputValue] = useState("");
const [currentWorkflowId, setCurrentWorkflowId] = useState<string | null>(null);
const [workflowCompleted, setWorkflowCompleted] = useState(false);
const [attachedFiles, setAttachedFiles] = useState<FileInfo[]>([]);
const inputRef = useRef<HTMLInputElement>(null);
const messagesEndRef = useRef<HTMLDivElement>(null);
const { startWorkflow, startingWorkflow, startError, stopWorkflow, stoppingWorkflows } = useWorkflowOperations();
const { messages, loading: messagesLoading, error: messagesError, refetch: refetchMessages } = useWorkflowMessages(currentWorkflowId);
const { status: workflowStatus, refetch: refetchStatus } = useWorkflowStatus(currentWorkflowId);
const { handleFileUpload } = useFileOperations();
// Update input value when a prompt is selected
useEffect(() => {
if (selectedPrompt) {
setInputValue(selectedPrompt.content);
// Focus the input field
if (inputRef.current) {
inputRef.current.focus();
}
}
}, [selectedPrompt]);
// Auto-scroll to bottom when new messages arrive or workflow status changes
useEffect(() => {
if (messagesEndRef.current) {
messagesEndRef.current.scrollIntoView({ behavior: 'smooth' });
}
}, [messages, workflowCompleted, workflowStatus]);
// Polling logic for fetching messages and status
useEffect(() => {
if (!currentWorkflowId || workflowCompleted) return;
const interval = setInterval(() => {
refetchMessages();
refetchStatus();
}, 1000); // Poll every second
return () => clearInterval(interval);
}, [currentWorkflowId, workflowCompleted, refetchMessages, refetchStatus]);
// Simple workflow completion detection
useEffect(() => {
const isCompleted = workflowStatus && (
workflowStatus.status === 'completed' ||
workflowStatus.status === 'finished' ||
workflowStatus.status === 'done' ||
workflowStatus.status === 'stopped' ||
workflowStatus.status === 'error'
);
setWorkflowCompleted(!!isCompleted);
}, [workflowStatus]);
// Handle workflow ID changes
useEffect(() => {
if (currentWorkflowId && onWorkflowIdChange) {
onWorkflowIdChange(currentWorkflowId);
}
}, [currentWorkflowId, onWorkflowIdChange]);
// Handle workflow resumption
useEffect(() => {
if (resumeWorkflowId && resumeWorkflowId !== currentWorkflowId) {
setCurrentWorkflowId(resumeWorkflowId);
setWorkflowCompleted(false);
setInputValue("");
setAttachedFiles([]);
}
}, [resumeWorkflowId, currentWorkflowId]);
const handleFileAttach = async (file: File) => {
try {
console.log('Uploading file:', file.name);
const result = await handleFileUpload(file, currentWorkflowId || undefined);
if (result.success && result.fileData) {
// Add the uploaded file to the attached files list
const fileInfo: FileInfo = {
id: result.fileData.id,
name: result.fileData.name || file.name,
mimeType: result.fileData.mimeType || file.type,
size: result.fileData.size || file.size,
creationDate: result.fileData.creationDate || new Date().toISOString(),
workflowId: currentWorkflowId || undefined
};
setAttachedFiles(prev => [...prev, fileInfo]);
console.log('File uploaded successfully:', fileInfo);
} else {
console.error('Failed to upload file:', result.error);
}
} catch (error) {
console.error('Error uploading file:', error);
}
};
const handleFileRemove = (fileId: number) => {
setAttachedFiles(prev => prev.filter(file => file.id !== fileId));
};
const handleFilesSelect = (selectedFiles: FileInfo[]) => {
// Add selected files to the attached files list, avoiding duplicates
setAttachedFiles(prev => {
const existingIds = new Set(prev.map(f => f.id));
const newFiles = selectedFiles.filter(f => !existingIds.has(f.id));
return [...prev, ...newFiles];
});
};
const handleSend = async () => {
if (inputValue.trim() || attachedFiles.length > 0) {
console.log('Sending message:', inputValue, 'with files:', attachedFiles.map(f => f.id));
try {
let result;
// Prepare file IDs for the request
const listFileId = attachedFiles.map(file => file.id);
// If we have a completed workflow, send as follow-up using the existing workflow ID
if (workflowCompleted && currentWorkflowId) {
console.log('Sending follow-up message to workflow:', currentWorkflowId);
result = await startWorkflow({
prompt: inputValue || "Files attached", // Provide a default message if only files are sent
listFileId: listFileId
}, currentWorkflowId);
if (result.success) {
console.log('Follow-up message sent successfully');
// Reset workflow completion state to resume polling for messages/status
// but DON'T reset logPollingCompleted - logs should stay stopped
setWorkflowCompleted(false);
}
} else {
// Start a new workflow
console.log('Starting new workflow');
// Reset previous workflow state when starting a new one
setCurrentWorkflowId(null);
setWorkflowCompleted(false);
result = await startWorkflow({
prompt: inputValue || "Files attached", // Provide a default message if only files are sent
listFileId: listFileId
});
if (result.success && result.data) {
console.log('Workflow started successfully:', result.data);
// Set the workflow ID to start polling for messages
setCurrentWorkflowId(result.data.id);
setWorkflowCompleted(false);
}
}
if (result.success) {
// Clear the input and attached files after successful send
setInputValue("");
setAttachedFiles([]);
// Call onPromptUsed if a prompt was used
if (selectedPrompt && onPromptUsed) {
onPromptUsed();
}
} else {
console.error('Failed to send message:', result.error);
}
} catch (error) {
console.error('Error sending message:', error);
}
}
};
const handleKeyPress = (e: React.KeyboardEvent) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
handleSend();
}
};
const startNewWorkflow = () => {
setCurrentWorkflowId(null);
setWorkflowCompleted(false);
setInputValue("");
setAttachedFiles([]);
if (onWorkflowIdChange) {
onWorkflowIdChange(null);
}
};
const handleStopWorkflow = async () => {
if (currentWorkflowId) {
console.log('Stopping workflow:', currentWorkflowId);
const success = await stopWorkflow(currentWorkflowId);
if (success) {
console.log('Workflow stopped successfully');
// Refresh status to get updated workflow state
refetchStatus();
} else {
console.error('Failed to stop workflow');
}
}
};
// Determine if workflow is currently running
const isWorkflowRunning = !!(currentWorkflowId && !workflowCompleted && workflowStatus &&
workflowStatus.status !== 'completed' &&
workflowStatus.status !== 'finished' &&
workflowStatus.status !== 'done' &&
workflowStatus.status !== 'stopped' &&
workflowStatus.status !== 'error');
const isStoppingWorkflow = currentWorkflowId ? stoppingWorkflows.has(currentWorkflowId) : false;
const handleRetry = async () => {
if (!currentWorkflowId || !messages.length) {
console.error('No workflow ID or messages available for retry');
return;
}
// Find the last user message to retry
const userMessages = messages.filter(msg => msg.role === 'user');
const lastUserMessage = userMessages[userMessages.length - 1];
if (!lastUserMessage) {
console.error('No user message found to retry');
return;
}
console.log('Retrying workflow with last user message:', lastUserMessage.content);
try {
// Extract file IDs if available from the message
const fileIds = lastUserMessage.fileIds || [];
// Start the workflow again with the same prompt and files
const result = await startWorkflow({
prompt: lastUserMessage.content,
listFileId: fileIds
}, currentWorkflowId);
if (result.success) {
console.log('Workflow retry started successfully');
// Reset workflow completion state to resume polling
setWorkflowCompleted(false);
} else {
console.error('Failed to retry workflow:', result.error);
}
} catch (error) {
console.error('Error retrying workflow:', error);
}
};
const shouldShowRetryButton = () => {
if (!workflowStatus) return false;
const statusLower = workflowStatus.status.toLowerCase();
return statusLower === 'error' ||
statusLower === 'failed' ||
statusLower === 'stopped' ||
statusLower === 'cancelled';
};
return {
// State
inputValue,
setInputValue,
currentWorkflowId,
workflowCompleted,
attachedFiles,
// Refs
inputRef,
messagesEndRef,
// Data from hooks
messages,
messagesLoading,
messagesError,
startingWorkflow,
startError,
workflowStatus,
// Handlers
handleSend,
handleKeyPress,
startNewWorkflow,
handleStopWorkflow,
handleFileAttach,
handleFileRemove,
handleFilesSelect,
handleRetry,
// Workflow state
isWorkflowRunning,
isStoppingWorkflow,
shouldShowRetryButton
};
};

View file

@ -1,76 +0,0 @@
import { Prompt } from "../../../../hooks/usePrompts";
import { FileInfo } from "../../../../hooks/useFiles";
export interface DashboardChatAreaProps {
selectedPrompt?: Prompt | null;
onPromptUsed?: () => void;
onWorkflowIdChange?: (workflowId: string | null) => void;
onWorkflowCompletedChange?: (completed: boolean) => void;
resumeWorkflowId?: string | null;
}
export interface Document {
id?: string;
fileId?: number;
name: string;
url?: string;
type?: string;
size?: number;
downloadUrl?: string;
ext?: string;
}
export interface Message {
id?: string;
role: 'user' | 'assistant' | 'system';
agentName: string;
content: string;
timestamp?: string;
documents?: Document[];
}
export interface WorkflowStatus {
status: string;
currentRound?: number;
}
export interface ChatInputProps {
inputValue: string;
setInputValue: (value: string) => void;
onSend: () => void;
onKeyPress: (e: React.KeyboardEvent) => void;
isDisabled: boolean;
placeholder: string;
inputRef: React.RefObject<HTMLTextAreaElement | null>;
isWorkflowRunning: boolean;
onStopWorkflow: () => void;
isStoppingWorkflow: boolean;
attachedFiles: FileInfo[];
onFileAttach: (file: File) => void;
onFileRemove: (fileId: number) => void;
onFilesSelect: (files: FileInfo[]) => void;
}
export interface MessageListProps {
messages: Message[];
currentWorkflowId: string | null;
workflowStatus: WorkflowStatus | null;
workflowCompleted: boolean;
startingWorkflow: boolean;
startError: string | null;
messagesError: string | null;
messagesLoading: boolean;
onStartNewWorkflow: () => void;
messagesEndRef: React.RefObject<HTMLDivElement | null>;
handleRetry: () => Promise<void>;
shouldShowRetryButton: () => boolean;
}
export interface WorkflowStatusDisplayProps {
currentWorkflowId: string | null;
workflowStatus: WorkflowStatus | null;
workflowCompleted: boolean;
onStartNewWorkflow: () => void;
handleRetry: () => Promise<void>;
shouldShowRetryButton: () => boolean;
}

View file

@ -1,109 +0,0 @@
# DashboardChatArea - Modular Structure
This directory contains the refactored `DashboardChatArea` component, broken down into manageable modules for better maintainability and separation of concerns.
## File Structure
```
DashboardChatArea/
├── index.ts # Main export file
├── types.ts # TypeScript interfaces and types
├── DashboardChatArea.tsx # Main orchestrating component
├── useChatLogic.ts # Custom hook with all business logic
├── MessageList.tsx # Component for displaying messages
├── MessageItem.tsx # Individual message component
├── ChatInput.tsx # Input field and send button component
├── WorkflowStatusDisplay.tsx # Workflow status and completion UI
├── DashboardChatArea.module.css # Shared styles
└── README.md # This documentation
```
## Component Responsibilities
### `DashboardChatArea.tsx` (Main Component)
- **Purpose**: Orchestrates all child components
- **Responsibilities**:
- Uses the `useChatLogic` hook
- Renders `MessageList` and `ChatInput` components
- Passes props between components
- **Size**: ~73 lines (reduced from 278 lines)
### `useChatLogic.ts` (Custom Hook)
- **Purpose**: Contains all business logic and state management
- **Responsibilities**:
- State management (input value, workflow ID, completion status)
- Effects for polling, auto-scroll, prompt handling
- Workflow operations (send messages, start workflows)
- Event handlers
- **Size**: ~196 lines
### `MessageList.tsx` (Message Display)
- **Purpose**: Handles the display of all messages and status indicators
- **Responsibilities**:
- Renders loading and error states
- Maps through messages using `MessageItem`
- Includes `WorkflowStatusDisplay`
- Handles auto-scroll reference
- **Size**: ~73 lines
### `MessageItem.tsx` (Individual Message)
- **Purpose**: Renders a single message
- **Responsibilities**:
- Message content display
- Role-based styling
- Timestamp formatting
- **Size**: ~32 lines
### `ChatInput.tsx` (Input Interface)
- **Purpose**: Handles user input and send functionality
- **Responsibilities**:
- Input field with ref handling
- Send button with animations
- Keyboard event handling
- Disabled states
- **Size**: ~46 lines
### `WorkflowStatusDisplay.tsx` (Status UI)
- **Purpose**: Shows workflow status and completion states
- **Responsibilities**:
- Running workflow status
- Completion message
- "Start New Workflow" button
- **Size**: ~38 lines
### `types.ts` (Type Definitions)
- **Purpose**: Centralized TypeScript interfaces
- **Responsibilities**:
- Component prop interfaces
- Data structure types
- Shared type definitions
- **Size**: ~50 lines
## Benefits of This Structure
1. **Separation of Concerns**: Each file has a single, clear responsibility
2. **Reusability**: Components can be easily reused or tested independently
3. **Maintainability**: Easier to locate and modify specific functionality
4. **Readability**: Smaller files are easier to understand and navigate
5. **Testing**: Individual components can be unit tested in isolation
6. **Type Safety**: Centralized types ensure consistency across components
## Usage
Import the main component as before:
```typescript
import DashboardChatArea from './DashboardChatArea';
// or
import DashboardChatArea, { DashboardChatAreaProps } from './DashboardChatArea';
```
The API remains exactly the same - this refactoring is purely internal and doesn't affect how the component is used by parent components.
## Development Guidelines
- **Adding new features**: Consider which component/file is most appropriate
- **State changes**: Most state logic should go in `useChatLogic.ts`
- **UI changes**: Modify the relevant component file
- **New types**: Add to `types.ts`
- **Styling**: All styles remain in `DashboardChatArea.module.css`

View file

@ -1,2 +0,0 @@
export { default } from './DashboardChatArea';
export type { DashboardChatAreaProps } from './dashboardChatAreaTypes';

View file

@ -1,5 +1,5 @@
import React, { useState, useEffect } from 'react';
import { useFileDownload } from '../../../../hooks/useWorkflows';
import { useFileDownload } from '../../../hooks/useWorkflows';
import { FileInfo } from './dashboardChatAreaTypes';
interface AttachedFile {

View file

@ -1,5 +1,5 @@
import React from 'react';
import { useFilePreview } from '../../../../hooks/useWorkflows';
import { useFilePreview } from '../../../hooks/useWorkflows';
import { FileInfo } from './dashboardChatAreaTypes';
interface AttachedFileWithData extends FileInfo {

View file

@ -1,6 +1,6 @@
import React, { useState, useEffect } from 'react';
import { useWorkflowOperations } from '../../../../hooks/useWorkflows';
import { Prompt } from '../../../../hooks/usePrompts';
import { useWorkflowOperations } from '../../../hooks/useWorkflows';
import { Prompt } from '../../../hooks/usePrompts';
import FileAttachmentPopup from './FileAttachmentPopup';
interface InputAreaProps {

View file

@ -1,8 +1,8 @@
import React, { useState } from "react";
import { FaDownload } from "react-icons/fa";
import { MdOutlineRemoveRedEye } from "react-icons/md";
import { useFileDownload } from "../../../../hooks/useWorkflows";
import { useLanguage } from "../../../../contexts/LanguageContext";
import { useFileDownload } from "../../../hooks/useWorkflows";
import { useLanguage } from "../../../contexts/LanguageContext";
import { Message, Document } from "./dashboardChatAreaTypes";
interface MessageItemProps {

View file

@ -1,7 +1,7 @@
import React, { useEffect, useRef, useState, useCallback } from 'react';
import { useWorkflowStatus } from '../../../../hooks/useWorkflows';
import { Prompt } from '../../../../hooks/usePrompts';
import { useApiRequest } from '../../../../hooks/useApi';
import { useWorkflowStatus } from '../../../hooks/useWorkflows';
import { Prompt } from '../../../hooks/usePrompts';
import { useApiRequest } from '../../../hooks/useApi';
import MessageItem from './DashboardChatAreaMessageItem';
import { Message, Document, WorkflowMessage } from './dashboardChatAreaTypes';

View file

@ -1,127 +0,0 @@
.chat_history {
height: 100%;
display: flex;
flex-direction: column;
overflow: hidden;
font-family: var(--font-family);
}
.container {
height: 100%;
display: flex;
flex-direction: column;
padding: 20px;
overflow: hidden;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
flex-shrink: 0;
}
.history_title {
font-size: 18px;
font-weight: 600;
color: var(--color-text);
margin: 0;
font-family: var(--font-family);
}
.workflowCount {
font-size: 14px;
color: var(--color-gray);
background-color: var(--color-surface);
padding: 4px 12px;
border-radius: 12px;
font-family: var(--font-family);
}
.scrollableContent {
flex: 1;
overflow-y: auto;
min-height: 0;
}
.workflowsList {
display: flex;
flex-direction: column;
gap: 0;
}
.emptyState {
display: flex;
align-items: center;
justify-content: center;
height: 200px;
color: var(--color-gray);
font-size: 16px;
text-align: center;
font-family: var(--font-family);
}
.loadingContainer {
display: flex;
align-items: center;
justify-content: center;
height: 200px;
}
.loadingText {
color: var(--color-gray);
font-size: 16px;
font-family: var(--font-family);
}
.errorContainer {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 200px;
gap: 16px;
}
.errorText {
color: var(--color-red);
font-size: 16px;
text-align: center;
font-family: var(--font-family);
}
.retryButton {
background-color: var(--color-secondary);
color: var(--color-bg);
border: none;
padding: 8px 16px;
border-radius: 8px;
cursor: pointer;
font-size: 14px;
transition: background-color 0.2s ease;
font-family: var(--font-family);
}
.retryButton:hover {
background-color: var(--color-secondary-hover);
}
/* Scrollbar styling */
.scrollableContent::-webkit-scrollbar {
width: 6px;
}
.scrollableContent::-webkit-scrollbar-track {
background: var(--color-surface);
border-radius: 3px;
}
.scrollableContent::-webkit-scrollbar-thumb {
background: var(--color-gray-disabled);
border-radius: 3px;
}
.scrollableContent::-webkit-scrollbar-thumb:hover {
background: var(--color-gray);
}

View file

@ -1,98 +0,0 @@
import React from "react";
import { motion } from "framer-motion";
import { useWorkflows } from "../../../../hooks/useWorkflows";
import { useLanguage } from "../../../../contexts/LanguageContext";
import DashboardChatHistoryItem from "./DashboardChatHistoryItem";
import styles from './DashboardChatHistory.module.css';
interface DashboardChatHistoryProps {
onWorkflowResume?: (workflowId: string) => void;
}
const DashboardChatHistory: React.FC<DashboardChatHistoryProps> = ({ onWorkflowResume }) => {
const { workflows, loading, error, refetch } = useWorkflows();
const { t } = useLanguage();
const handleWorkflowResume = (workflowId: string) => {
if (onWorkflowResume) {
onWorkflowResume(workflowId);
}
console.log('Resuming workflow:', workflowId);
};
if (loading) {
return (
<motion.div
className={styles.chat_history}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.2, duration: 0.3, ease: "easeOut" }}
>
<div className={styles.loadingContainer}>
<div className={styles.loadingText}>{t('chat_history.loading', 'Loading workflows...')}</div>
</div>
</motion.div>
);
}
if (error) {
return (
<motion.div
className={styles.chat_history}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.2, duration: 0.3, ease: "easeOut" }}
>
<div className={styles.errorContainer}>
<div className={styles.errorText}>{t('chat_history.error_loading', 'Error loading workflows:')} {error}</div>
<button
onClick={refetch}
className={styles.retryButton}
>
{t('chat_history.try_again', 'Try Again')}
</button>
</div>
</motion.div>
);
}
return (
<motion.div
className={styles.chat_history}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.2, duration: 0.3, ease: "easeOut" }}
>
<div className={styles.container}>
<div className={styles.header}>
<h2 className={styles.history_title}>{t('chat_history.title', 'Workflow History')}</h2>
<div className={styles.workflowCount}>
{workflows.length} {workflows.length === 1 ? t('chat_history.workflow_count', 'Workflow') : t('chat_history.workflow_count_plural', 'Workflows')}
</div>
</div>
<div className={styles.scrollableContent}>
{workflows.length === 0 ? (
<div className={styles.emptyState}>
{t('chat_history.empty_state', 'No workflows available')}
</div>
) : (
<div className={styles.workflowsList}>
{workflows.map((workflow) => (
<DashboardChatHistoryItem
key={workflow.id}
workflow={workflow}
onDelete={refetch}
onResume={handleWorkflowResume}
/>
))}
</div>
)}
</div>
</div>
</motion.div>
);
};
export default DashboardChatHistory;

View file

@ -1,177 +0,0 @@
.workflowItem {
background: var(--color-bg);
border-radius: 12px;
border: 1px solid var(--color-gray-disabled);
margin-bottom: 12px;
transition: all 0.2s ease;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
font-family: var(--font-family);
}
.workflowItem:hover {
border-color: var(--color-gray);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
.workflowMain {
display: flex;
align-items: flex-start;
padding: 16px;
gap: 16px;
}
.workflowContent {
flex: 1;
min-width: 0;
}
.workflowInfo {
margin-bottom: 8px;
}
.workflowId {
font-size: 16px;
font-weight: 600;
color: var(--color-text);
margin: 0 0 8px 0;
line-height: 1.2;
font-family: var(--font-family);
}
.workflowMeta {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 8px;
}
.workflowStatus {
font-size: 12px;
font-weight: 600;
padding: 4px 8px;
border-radius: 12px;
background-color: var(--color-secondary-disabled);
color: var(--color-secondary);
font-family: var(--font-family);
}
.workflowRound {
font-size: 12px;
color: var(--color-gray);
background-color: var(--color-surface);
padding: 2px 6px;
border-radius: 8px;
font-family: var(--font-family);
}
.workflowDates {
display: flex;
flex-direction: column;
gap: 2px;
}
.workflowDate {
font-size: 12px;
color: var(--color-gray);
margin: 0;
line-height: 1.3;
font-family: var(--font-family);
}
.workflowDescription {
margin-top: 8px;
}
.messagePreview {
margin-bottom: 8px;
padding: 8px;
background-color: var(--color-surface);
border-radius: 6px;
border-left: 3px solid var(--color-gray);
}
.previewText {
font-size: 13px;
color: var(--color-gray);
margin: 0;
line-height: 1.4;
word-break: break-word;
font-style: italic;
font-family: var(--font-family);
}
.workflowName {
font-size: 14px;
color: var(--color-gray);
margin: 0;
line-height: 1.4;
word-break: break-word;
font-family: var(--font-family);
}
.actionButtons {
display: flex;
flex-direction: column;
gap: 8px;
flex-shrink: 0;
}
.actionButton {
width: 36px;
height: 36px;
border: none;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s ease;
font-size: 14px;
}
.actionButton:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.resumeButton {
background-color: var(--color-secondary);
color: var(--color-bg);
}
.resumeButton:hover:not(:disabled) {
background-color: var(--color-secondary-hover);
transform: translateY(-1px);
}
.deleteButton {
background-color: var(--color-red);
color: var(--color-bg);
}
.deleteButton:hover:not(:disabled) {
background-color: var(--color-red-hover);
transform: translateY(-1px);
}
.deletingMessage {
padding: 8px 16px;
background-color: var(--color-primary-disabled);
border-top: 1px solid var(--color-gray-disabled);
color: var(--color-primary);
font-size: 12px;
text-align: center;
font-family: var(--font-family);
}
@media (max-width: 768px) {
.workflowMain {
flex-direction: column;
gap: 12px;
}
.actionButtons {
flex-direction: row;
justify-content: flex-end;
}
}

View file

@ -1,212 +0,0 @@
import React, { useState, useEffect } from 'react';
import { FaArrowRight } from 'react-icons/fa';
import { AiOutlineDelete } from 'react-icons/ai';
import { motion } from 'framer-motion';
import { useWorkflowOperations, useWorkflowMessages, Workflow } from '../../../../hooks/useWorkflows';
import { useLanguage } from '../../../../contexts/LanguageContext';
import styles from './DashboardChatHistoryItem.module.css';
interface DashboardChatHistoryItemProps {
workflow: Workflow;
onDelete?: () => void;
onResume: (workflowId: string) => void;
}
function DashboardChatHistoryItem({ workflow, onDelete, onResume }: DashboardChatHistoryItemProps) {
const { deleteWorkflow, deletingWorkflows } = useWorkflowOperations();
const { messages } = useWorkflowMessages(workflow.id);
const { t } = useLanguage();
const isDeleting = deletingWorkflows.has(workflow.id);
// Get the first user message as preview
const firstUserMessage = messages.find(msg => msg.role === 'user');
const messagePreview = firstUserMessage?.content || t('chat_history.no_message_content', 'No message content available');
const handleDelete = async () => {
const workflowName = workflow.title || `Workflow ${workflow.id.substring(0, 8)}...`;
const confirmMessage = t('chat_history.confirm_delete', 'Are you sure you want to delete "{name}"?').replace('{name}', workflowName);
if (window.confirm(confirmMessage)) {
const success = await deleteWorkflow(workflow.id);
if (success && onDelete) {
onDelete();
}
}
};
const handleResume = () => {
onResume(workflow.id);
};
const formatDate = (dateString?: string) => {
if (!dateString) return t('chat_history.unknown_date', 'Unknown date');
try {
return new Date(dateString).toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
} catch (error) {
return t('chat_history.invalid_date', 'Invalid date');
}
};
const getStatusColor = (status: string) => {
switch (status.toLowerCase()) {
case 'error':
case 'failed':
case 'stopped':
case 'cancelled':
return 'var(--color-red)';
default:
return 'var(--color-gray)';
}
};
const getStatusBackgroundColor = (status: string) => {
switch (status.toLowerCase()) {
case 'error':
case 'failed':
case 'stopped':
case 'cancelled':
return 'var(--color-red-disabled)';
default:
return 'var(--color-gray-disabled)';
}
};
const shouldShowStatus = (status: string) => {
const statusLower = status.toLowerCase();
return statusLower === 'error' ||
statusLower === 'failed' ||
statusLower === 'stopped' ||
statusLower === 'cancelled';
};
const isRunning = (status: string) => {
const statusLower = status.toLowerCase();
return statusLower === 'running' || statusLower === 'processing';
};
const getTranslatedStatus = (status: string): string => {
const statusKey = `status.${status.toLowerCase()}`;
return t(statusKey, status.toUpperCase());
};
const renderStatusIndicator = () => {
if (isRunning(workflow.status)) {
return (
<motion.div
className={styles.loadingSpinner}
animate={{ rotate: 360 }}
transition={{
duration: 1,
repeat: Infinity,
ease: "linear"
}}
style={{
width: '16px',
height: '16px',
border: '2px solid var(--color-gray-disabled)',
borderTop: '2px solid var(--color-secondary)',
borderRadius: '50%',
display: 'inline-block'
}}
/>
);
}
if (shouldShowStatus(workflow.status)) {
return (
<span
className={styles.workflowStatus}
style={{
color: getStatusColor(workflow.status),
backgroundColor: getStatusBackgroundColor(workflow.status)
}}
>
{getTranslatedStatus(workflow.status)}
</span>
);
}
return null;
};
const truncateMessage = (message: string, maxLength: number = 150) => {
if (message.length <= maxLength) return message;
return message.substring(0, maxLength) + '...';
};
return (
<div className={styles.workflowItem}>
<div className={styles.workflowMain}>
<div className={styles.workflowContent}>
<div className={styles.workflowInfo}>
<h3 className={styles.workflowId}>
{workflow.title || `Workflow ${workflow.id.substring(0, 8)}...`}
</h3>
<div className={styles.workflowMeta}>
{renderStatusIndicator()}
{workflow.currentRound && (
<span className={styles.workflowRound}>
{t('chat_history.round', 'Round')} {workflow.currentRound}
</span>
)}
</div>
<div className={styles.workflowDates}>
{workflow.startedAt && (
<p className={styles.workflowDate}>
{t('chat_history.started', 'Started:')} {formatDate(workflow.startedAt)}
</p>
)}
{workflow.lastActivity && (
<p className={styles.workflowDate}>
{t('chat_history.last_activity', 'Last Activity:')} {formatDate(workflow.lastActivity)}
</p>
)}
</div>
</div>
<div className={styles.workflowDescription}>
<div className={styles.messagePreview}>
<p className={styles.previewText}>
{truncateMessage(messagePreview)}
</p>
</div>
</div>
</div>
<div className={styles.actionButtons}>
<button
onClick={handleResume}
className={`${styles.actionButton} ${styles.resumeButton}`}
title={t('chat_history.resume_tooltip', 'Resume workflow')}
>
<FaArrowRight size={16} />
</button>
<button
onClick={handleDelete}
disabled={isDeleting}
className={`${styles.actionButton} ${styles.deleteButton}`}
title={t('chat_history.delete_tooltip', 'Delete workflow')}
>
<AiOutlineDelete size={16} />
</button>
</div>
</div>
{isDeleting && (
<div className={styles.deletingMessage}>
{t('chat_history.deleting', 'Deleting workflow...')}
</div>
)}
</div>
);
}
export default DashboardChatHistoryItem;

View file

@ -1,5 +1,5 @@
import React, { useState, useRef, useEffect } from 'react';
import { useUserFiles, UserFile } from '../../../../hooks/useFiles';
import { useUserFiles, UserFile } from '../../../hooks/useFiles';
import DateienAll from '../../../Dateien/DateienAll';
import DateienShared from '../../../Dateien/DateienShared';
import DateienCreated from '../../../Dateien/DateienCreated';

View file

@ -3,8 +3,8 @@ import { MdOutlineRemoveRedEye } from "react-icons/md";
import styles from "./DateienItem.module.css";
import { useState } from "react";
import { useFileOperations } from "../../hooks/useFiles";
import FilePreviewPopup from "../Dashboard/DashboardChat/DashboardChatArea/DashboardChatAreaFilePreview";
import { Document } from "../Dashboard/DashboardChat/DashboardChatArea/dashboardChatAreaTypes";
import FilePreviewPopup from "../Dashboard/DashboardChat/DashboardChatAreaFilePreview";
import { Document } from "../Dashboard/DashboardChat/dashboardChatAreaTypes";
import { useLanguage } from "../../contexts/LanguageContext";
type DateienItemProps = {

View file

@ -0,0 +1,578 @@
.formGenerator {
display: flex;
flex-direction: column;
gap: 20px;
width: 100%;
font-family: var(--font-family);
}
.title {
font-size: 1.5rem;
font-weight: 400;
color: var(--color-text);
margin: 0;
margin-bottom: 10px;
}
/* Controls Section */
.controls {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
gap: 20px;
padding: 15px;
background: var(--color-bg);
border: 1px solid var(--color-primary);
border-radius: 25px;
}
.searchContainer {
display: flex;
align-items: center;
gap: 10px;
flex-shrink: 0;
}
.floatingLabelInput {
position: relative;
width: 250px;
}
.label {
position: absolute;
left: 16px;
top: 50%;
transform: translateY(-50%);
color: var(--color-text);
opacity: 0.6;
font-size: 14px;
pointer-events: none;
transition: all 0.3s ease;
background-color: transparent;
font-family: var(--font-family);
}
.focusedLabel {
position: absolute;
left: 12px;
top: -8px;
transform: translateY(0);
color: var(--color-secondary);
font-size: 12px;
pointer-events: none;
transition: all 0.3s ease;
background-color: var(--color-bg);
padding: 0 4px;
font-family: var(--font-family);
font-weight: 500;
}
.searchInput {
width: 100%;
height: 40px;
padding: 8px 12px;
border: 1px solid var(--color-primary);
border-radius: 25px;
font-size: 14px;
font-family: var(--font-family);
background: var(--color-bg);
color: var(--color-text);
transition: all 0.2s ease;
box-sizing: border-box;
}
.searchInput:focus {
border-color: var(--color-secondary);
box-shadow: 0 0 0 2px rgba(var(--color-secondary), 0.1);
}
.searchInput::placeholder {
color: transparent;
}
.filtersContainer {
display: flex;
flex-wrap: wrap;
gap: 15px;
align-items: center;
}
.filterGroup {
display: flex;
align-items: center;
gap: 8px;
}
.filterGroup .floatingLabelInput {
width: 160px;
}
.customSelectContainer {
position: relative;
display: inline-block;
min-width: 120px;
}
.filterInput {
width: 100%;
height: 40px;
padding: 6px 10px;
border: 1px solid var(--color-primary);
border-radius: 25px;
font-size: 14px;
font-family: var(--font-family);
background: var(--color-bg);
color: var(--color-text);
opacity: 0.6;
min-width: 120px;
transition: all 0.2s ease;
box-sizing: border-box;
}
.filterInput::placeholder {
color: transparent;
}
.filterSelect {
height: 40px;
padding: 6px 10px;
border: 1px solid var(--color-primary);
border-radius: 25px;
font-size: 14px;
font-family: var(--font-family);
background: var(--color-bg);
color: var(--color-text);
opacity: 0.6;
min-width: 120px;
box-sizing: border-box;
}
.filterSelect {
appearance: none;
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6,9 12,15 18,9'%3e%3c/polyline%3e%3c/svg%3e");
background-repeat: no-repeat;
background-position: right 10px center;
background-size: 16px;
padding-right: 35px;
}
/* Hide dropdown arrow when filter has a value */
.filterSelect.hasValue {
background-image: none;
color: var(--color-secondary);
border-color: var(--color-secondary);
opacity: 1;
}
.filterInput:focus {
outline: none;
border-color: var(--color-secondary);
opacity: 1;
box-shadow: 0 0 0 2px rgba(var(--color-secondary), 0.1);
}
.filterSelect:focus {
outline: none;
border-color: var(--color-secondary);
opacity: 1;
}
.clearFilterButton {
position: absolute;
right: 8px;
top: 50%;
transform: translateY(-50%);
background: none;
border: none;
color: var(--color-primary);
cursor: pointer;
font-size: 16px;
padding: 2px;
border-radius: 50%;
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
}
.clearFilterButton:hover {
background: none;
color: var(--color-secondary);
}
/* Table Container */
.tableContainer {
position: relative;
overflow: auto;
border: 1px solid var(--color-primary);
border-radius: 25px;
background: var(--color-bg);
max-height: 600px;
}
.loading {
display: flex;
justify-content: center;
align-items: center;
padding: 40px;
font-size: 16px;
color: var(--color-text);
}
/* Table Styles */
.table {
width: 100%;
border-collapse: collapse;
font-size: 14px;
background: var(--color-bg);
table-layout: fixed;
}
.th {
position: sticky;
top: 0;
background: var(--color-bg);
padding: 10px 16px;
text-align: left;
font-weight: 400;
color: var(--color-text);
white-space: nowrap;
user-select: none;
position: relative;
z-index: 10;
}
.th.actionsColumn {
text-align: center;
}
.th.sortable {
cursor: pointer;
transition: background-color 0.2s ease;
}
.th.sortable:hover {
background: var(--color-gray-disabled);
}
.headerContent {
display: flex;
align-items: center;
justify-content: left;
gap: 8px;
}
.sortIcon {
font-size: 12px;
color: var(--color-secondary);
opacity: 1.;
}
.resizeHandle {
position: absolute;
top: 0;
right: 0;
width: 4px;
height: 100%;
cursor: col-resize;
z-index: 11;
}
.resizeHandle:hover {
background: var(--color-secondary);
}
.td {
padding: 12px 16px;
border-top: 1px solid var(--color-primary);
color: var(--color-text);
vertical-align: middle;
}
.tr {
transition: background-color 0.2s ease;
}
.tr:hover {
background: var(--color-gray-disabled);
}
.tr.selected {
background: rgba(var(--color-secondary-rgb), 0.1);
}
.tr.clickable {
cursor: pointer;
}
/* Selection Column */
.selectColumn {
text-align: center;
padding: 8px !important;
}
.selectColumn input[type="checkbox"] {
cursor: pointer;
transform: scale(1.2);
}
/* Actions Column */
.actionsColumn {
white-space: nowrap;
text-align: center;
padding: 12px 8px !important;
font-weight: 400;
}
/* Actions Column border only on body cells, not header */
tbody .actionsColumn {
border-top: 1px solid var(--color-primary);
}
.actionButtons {
display: flex;
gap: 4px;
justify-content: center;
align-items: center;
width: fit-content;
margin: 0 auto;
}
.actionButton {
display: flex;
align-items: center;
justify-content: center;
padding: 8px;
border: none;
border-radius: 50%;
font-size: 12px;
font-family: var(--font-family);
cursor: pointer;
transition: all 0.2s ease;
white-space: nowrap;
position: relative;
min-width: 32px;
min-height: 32px;
background: var(--color-secondary);
color: var(--color-bg);
}
.actionButton:hover {
background: var(--color-secondary-hover);
transform: scale(1.05);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
.actionIcon {
font-size: 16px;
height: 16px;
width: 16px;
display: flex;
align-items: center;
justify-content: center;
}
/* Custom Tooltip */
.tooltip {
position: absolute;
bottom: 120%;
left: 50%;
transform: translateX(-50%);
background: var(--color-text);
color: var(--color-bg);
padding: 6px 10px;
border-radius: 6px;
font-size: 12px;
white-space: nowrap;
opacity: 0;
visibility: hidden;
transition: opacity 0.2s ease, visibility 0.2s ease;
z-index: 1000;
pointer-events: none;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
}
/* Tooltip arrow */
.tooltip::after {
content: '';
position: absolute;
top: 100%;
left: 50%;
transform: translateX(-50%);
border: 5px solid transparent;
border-top-color: var(--color-text);
}
/* Show tooltip on button hover */
.actionButton:hover .tooltip {
opacity: 1;
visibility: visible;
}
/* Pagination */
.pagination {
display: flex;
justify-content: center;
align-items: center;
gap: 10px;
padding: 15px;
border-top: 1px solid var(--color-gray-disabled);
background: var(--color-bg);
border-radius: 0 0 8px 8px;
}
.paginationButton {
padding: 8px 12px;
border: 1px solid var(--color-gray-disabled);
background: var(--color-bg);
color: var(--color-text);
border-radius: 4px;
cursor: pointer;
font-family: var(--font-family);
transition: all 0.2s ease;
}
.paginationButton:hover:not(:disabled) {
background: var(--color-gray-disabled);
border-color: var(--color-secondary);
}
.paginationButton:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.paginationInfo {
font-size: 14px;
color: var(--color-text);
margin: 0 15px;
white-space: nowrap;
}
/* Responsive Design */
@media (max-width: 768px) {
.controls {
flex-direction: column;
align-items: stretch;
gap: 15px;
padding: 10px;
}
.filtersContainer {
flex-direction: column;
align-items: stretch;
gap: 10px;
}
.filterGroup {
flex-direction: column;
align-items: flex-start;
gap: 4px;
}
.filterInput,
.filterSelect {
width: 100%;
min-width: auto;
}
.floatingLabelInput {
max-width: none;
}
.filterGroup .floatingLabelInput {
width: 100%;
}
.tableContainer {
max-height: 400px;
}
.th,
.td {
padding: 8px 12px;
font-size: 13px;
}
.actionButtons {
flex-direction: column;
gap: 4px;
}
.actionButton {
padding: 4px 8px;
font-size: 11px;
}
.pagination {
flex-wrap: wrap;
gap: 5px;
padding: 10px;
}
.paginationInfo {
order: -1;
width: 100%;
text-align: center;
margin: 0 0 10px 0;
font-size: 13px;
}
}
/* Dark theme support */
@media (prefers-color-scheme: dark) {
.th.sortable:hover {
background: rgba(255, 255, 255, 0.1);
}
.tr:hover {
background: rgba(255, 255, 255, 0.05);
}
.tr.selected {
background: rgba(var(--color-secondary-rgb), 0.2);
}
}
/* Accessibility */
.actionButton:focus,
.paginationButton:focus,
.searchInput:focus,
.filterInput:focus,
.filterSelect:focus {
outline: none;
}
/* Custom scrollbar for table container */
.tableContainer::-webkit-scrollbar {
width: 8px;
height: 8px;
}
.tableContainer::-webkit-scrollbar-track {
background: var(--color-gray-disabled);
border-radius: 4px;
}
.tableContainer::-webkit-scrollbar-thumb {
background: var(--color-gray);
border-radius: 4px;
}
.tableContainer::-webkit-scrollbar-thumb:hover {
background: var(--color-secondary);
}

View file

@ -0,0 +1,660 @@
import React, { useState, useMemo, useRef, useEffect } from 'react';
import styles from './FormGenerator.module.css';
// Types for the FormGenerator
export interface ColumnConfig {
key: string;
label: string;
type?: 'string' | 'number' | 'date' | 'boolean' | 'enum';
width?: number;
minWidth?: number;
maxWidth?: number;
sortable?: boolean;
filterable?: boolean;
searchable?: boolean;
formatter?: (value: any, row: any) => React.ReactNode;
filterOptions?: string[]; // For enum/select filters
}
export interface FormGeneratorProps<T = any> {
data: T[];
columns?: ColumnConfig[];
title?: string;
searchable?: boolean;
filterable?: boolean;
sortable?: boolean;
resizable?: boolean;
pagination?: boolean;
pageSize?: number;
onRowClick?: (row: T, index: number) => void;
onRowSelect?: (selectedRows: T[]) => void;
selectable?: boolean;
loading?: boolean;
actions?: {
label: string;
onClick: (row: T) => void;
icon?: string | React.ReactNode | ((row: T) => React.ReactNode);
}[];
className?: string;
}
export function FormGenerator<T extends Record<string, any>>({
data,
columns: providedColumns,
title,
searchable = true,
filterable = true,
sortable = true,
resizable = true,
pagination = true,
pageSize = 10,
onRowClick,
onRowSelect,
selectable = false,
loading = false,
actions = [],
className = ''
}: FormGeneratorProps<T>) {
// Auto-detect columns if not provided
const detectedColumns = useMemo((): ColumnConfig[] => {
if (providedColumns) return providedColumns;
if (data.length === 0) return [];
const sampleRow = data[0];
return Object.keys(sampleRow).map(key => {
const value = sampleRow[key];
let type: ColumnConfig['type'] = 'string';
// Auto-detect type based on value
if (typeof value === 'number') {
type = 'number';
} else if (typeof value === 'boolean') {
type = 'boolean';
} else if (value instanceof Date || (typeof value === 'string' && !isNaN(Date.parse(value)) && value.includes('-'))) {
type = 'date';
}
return {
key,
label: key.charAt(0).toUpperCase() + key.slice(1).replace(/([A-Z])/g, ' $1'),
type,
sortable: true,
filterable: true,
searchable: type === 'string',
width: 150,
minWidth: 100,
maxWidth: 400
};
});
}, [data, providedColumns]);
// State management
const [searchTerm, setSearchTerm] = useState('');
const [searchFocused, setSearchFocused] = useState(false);
const [filterFocused, setFilterFocused] = useState<Record<string, boolean>>({});
const [sortConfig, setSortConfig] = useState<{ key: string; direction: 'asc' | 'desc' } | null>(null);
const [filters, setFilters] = useState<Record<string, any>>({});
const [columnWidths, setColumnWidths] = useState<Record<string, number>>({});
const [selectedRows, setSelectedRows] = useState<Set<number>>(new Set());
const [currentPage, setCurrentPage] = useState(1);
// Refs for resizing
const tableRef = useRef<HTMLTableElement>(null);
const resizingColumn = useRef<string | null>(null);
const startX = useRef<number>(0);
const startWidth = useRef<number>(0);
// Initialize column widths
useEffect(() => {
const initialWidths: Record<string, number> = {};
detectedColumns.forEach(col => {
// Set a default width if none specified to ensure all columns have explicit widths
initialWidths[col.key] = col.width || 150;
});
setColumnWidths(initialWidths);
}, [detectedColumns]);
// Filter and search data
const filteredData = useMemo(() => {
let result = [...data];
// Apply search filter
if (searchTerm && searchable) {
const searchLower = searchTerm.toLowerCase();
result = result.filter(row => {
return detectedColumns.some(col => {
if (!col.searchable) return false;
const value = row[col.key];
return String(value).toLowerCase().includes(searchLower);
});
});
}
// Apply column filters
Object.entries(filters).forEach(([key, filterValue]) => {
if (filterValue !== undefined && filterValue !== '') {
result = result.filter(row => {
const value = row[key];
const column = detectedColumns.find(col => col.key === key);
if (column?.type === 'boolean') {
return Boolean(value) === Boolean(filterValue);
} else if (column?.type === 'number') {
return Number(value) === Number(filterValue);
} else if (column?.type === 'date') {
// Convert DD.MM.YYYY to comparable format
const parseDate = (dateStr: string) => {
if (dateStr.includes('.')) {
const [day, month, year] = dateStr.split('.');
return new Date(parseInt(year), parseInt(month) - 1, parseInt(day));
}
return new Date(dateStr);
};
// Convert row value to DD.MM.YYYY format for comparison
const rowDate = new Date(value);
const rowFormatted = `${rowDate.getDate().toString().padStart(2, '0')}.${(rowDate.getMonth() + 1).toString().padStart(2, '0')}.${rowDate.getFullYear()}`;
// Check if filter value is complete (DD.MM.YYYY)
if (filterValue.length === 10 && filterValue.match(/^\d{2}\.\d{2}\.\d{4}$/)) {
return rowFormatted === filterValue;
} else {
// Partial matching for incomplete dates
return rowFormatted.startsWith(filterValue);
}
} else {
return String(value).toLowerCase().includes(String(filterValue).toLowerCase());
}
});
}
});
// Apply sorting
if (sortConfig) {
result.sort((a, b) => {
const aVal = a[sortConfig.key];
const bVal = b[sortConfig.key];
if (aVal < bVal) return sortConfig.direction === 'asc' ? -1 : 1;
if (aVal > bVal) return sortConfig.direction === 'asc' ? 1 : -1;
return 0;
});
}
return result;
}, [data, searchTerm, filters, sortConfig, detectedColumns, searchable]);
// Pagination
const paginatedData = useMemo(() => {
if (!pagination) return filteredData;
const startIndex = (currentPage - 1) * pageSize;
return filteredData.slice(startIndex, startIndex + pageSize);
}, [filteredData, currentPage, pageSize, pagination]);
const totalPages = Math.ceil(filteredData.length / pageSize);
// Handle sorting
const handleSort = (key: string) => {
if (!sortable) return;
setSortConfig(current => {
if (current?.key === key) {
return current.direction === 'asc'
? { key, direction: 'desc' }
: null;
}
return { key, direction: 'asc' };
});
};
// Handle filtering
const handleFilter = (key: string, value: any) => {
setFilters(prev => ({
...prev,
[key]: value
}));
setCurrentPage(1); // Reset to first page when filtering
};
// Handle filter input focus
const handleFilterFocus = (key: string, focused: boolean) => {
setFilterFocused(prev => ({
...prev,
[key]: focused
}));
};
// Handle row selection
const handleRowSelect = (index: number) => {
if (!selectable) return;
const newSelected = new Set(selectedRows);
if (newSelected.has(index)) {
newSelected.delete(index);
} else {
newSelected.add(index);
}
setSelectedRows(newSelected);
if (onRowSelect) {
const selectedData = Array.from(newSelected).map(i => paginatedData[i]);
onRowSelect(selectedData);
}
};
// Handle select all
const handleSelectAll = () => {
if (!selectable) return;
if (selectedRows.size === paginatedData.length) {
setSelectedRows(new Set());
onRowSelect?.([]);
} else {
const allIndices = new Set(paginatedData.map((_, index) => index));
setSelectedRows(allIndices);
onRowSelect?.(paginatedData);
}
};
// Handle column resizing
const handleMouseDown = (e: React.MouseEvent, columnKey: string) => {
if (!resizable) return;
e.preventDefault();
resizingColumn.current = columnKey;
startX.current = e.clientX;
startWidth.current = columnWidths[columnKey] || 150;
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
};
const handleMouseMove = (e: MouseEvent) => {
if (!resizingColumn.current) return;
const diff = e.clientX - startX.current;
let newWidth = Math.max(100, startWidth.current + diff);
// Prevent extending beyond table container
const tableContainer = tableRef.current?.parentElement;
if (tableContainer) {
const containerWidth = tableContainer.clientWidth;
const actionsColumnWidth = actions.length > 0 ? 120 : 0;
const selectColumnWidth = selectable ? 40 : 0;
const fixedWidth = actionsColumnWidth + selectColumnWidth;
// Calculate total width of all OTHER data columns (excluding the one being resized)
const otherDataColumnsWidth = detectedColumns.reduce((total, col) => {
if (col.key !== resizingColumn.current) {
return total + (columnWidths[col.key] || col.width || 150);
}
return total;
}, 0);
// Maximum allowed width for this column
const maxAllowedWidth = containerWidth - fixedWidth - otherDataColumnsWidth - 40; // 40px buffer
newWidth = Math.min(newWidth, Math.max(100, maxAllowedWidth));
}
setColumnWidths(prev => ({
...prev,
[resizingColumn.current!]: newWidth
}));
};
const handleMouseUp = () => {
resizingColumn.current = null;
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUp);
};
// Format cell value
const formatCellValue = (value: any, column: ColumnConfig, row: T) => {
if (column.formatter) {
return column.formatter(value, row);
}
if (value === null || value === undefined) {
return '-';
}
switch (column.type) {
case 'date':
return new Date(value).toLocaleDateString();
case 'boolean':
return value ? '✓' : '✗';
case 'number':
return typeof value === 'number' ? value.toLocaleString() : value;
default:
return String(value);
}
};
return (
<div className={`${styles.formGenerator} ${className}`}>
{(searchable || filterable) && (
<div className={styles.controls}>
{searchable && (
<div className={styles.searchContainer}>
<div className={styles.floatingLabelInput}>
<input
type="text"
placeholder=" "
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
onFocus={() => setSearchFocused(true)}
onBlur={() => setSearchFocused(false)}
className={`${styles.searchInput} ${searchFocused || searchTerm ? styles.focused : ''}`}
/>
<label className={searchFocused || searchTerm ? styles.focusedLabel : styles.label}>Search...</label>
</div>
</div>
)}
{filterable && (
<div className={styles.filtersContainer}>
{detectedColumns.filter(col => col.filterable).map(column => (
<div key={column.key} className={styles.filterGroup}>
{column.type === 'boolean' ? (
<div className={styles.customSelectContainer}>
<select
value={filters[column.key] || ''}
onChange={(e) => handleFilter(column.key, e.target.value === '' ? undefined : e.target.value === 'true')}
className={`${styles.filterSelect} ${filters[column.key] ? styles.hasValue : ''}`}
>
<option value="" disabled hidden>{column.label}</option>
<option value="true">Yes</option>
<option value="false">No</option>
</select>
{filters[column.key] && (
<button
type="button"
onClick={() => handleFilter(column.key, '')}
className={styles.clearFilterButton}
title="Clear filter"
>
</button>
)}
</div>
) : column.filterOptions ? (
<div className={styles.customSelectContainer}>
<select
value={filters[column.key] || ''}
onChange={(e) => handleFilter(column.key, e.target.value)}
className={`${styles.filterSelect} ${filters[column.key] ? styles.hasValue : ''}`}
>
<option value="" disabled hidden>{column.label}</option>
{column.filterOptions.map(option => (
<option key={option} value={option}>{option}</option>
))}
</select>
{filters[column.key] && (
<button
type="button"
onClick={() => handleFilter(column.key, '')}
className={styles.clearFilterButton}
title="Clear filter"
>
</button>
)}
</div>
) : column.type === 'date' ? (
<div className={styles.floatingLabelInput}>
<input
type="text"
placeholder=" "
value={filters[column.key] || ''}
onChange={(e) => {
let value = e.target.value;
const currentValue = filters[column.key] || '';
// Check if user is deleting (new value is shorter)
const isDeleting = value.length < currentValue.length;
if (isDeleting) {
// When deleting, preserve the exact input without auto-formatting
handleFilter(column.key, value);
return;
}
// Auto-pad single digits followed by dot (e.g., "4." -> "04.")
value = value.replace(/^(\d)\./, '0$1.');
value = value.replace(/\.(\d)\./, '.0$1.');
// Allow typing and format as DD.MM.YYYY
const digitsOnly = value.replace(/\D/g, ''); // Remove non-digits
let formatted = '';
if (digitsOnly.length >= 8) {
// Full format: DDMMYYYY -> DD.MM.YYYY
const day = digitsOnly.slice(0, 2);
const month = digitsOnly.slice(2, 4);
const year = digitsOnly.slice(4, 8);
// Validate day (01-31) and month (01-12)
if (parseInt(day) > 31 || parseInt(month) > 12 || parseInt(day) === 0 || parseInt(month) === 0) {
return; // Don't update if invalid
}
formatted = `${day}.${month}.${year}`;
} else if (digitsOnly.length >= 4) {
// Partial format: DDMM -> DD.MM.
const day = digitsOnly.slice(0, 2);
const month = digitsOnly.slice(2, 4);
const remaining = digitsOnly.slice(4);
// Validate day (01-31) and month (01-12)
if (parseInt(day) > 31 || parseInt(month) > 12 || parseInt(day) === 0 || parseInt(month) === 0) {
return; // Don't update if invalid
}
formatted = `${day}.${month}.${remaining}`;
} else if (digitsOnly.length >= 2) {
// Start format: DD -> DD.
const day = digitsOnly.slice(0, 2);
const remaining = digitsOnly.slice(2);
// Validate day (01-31)
if (parseInt(day) > 31 || parseInt(day) === 0) {
return; // Don't update if invalid
}
formatted = `${day}.${remaining}`;
} else {
// Just digits
formatted = digitsOnly;
}
handleFilter(column.key, formatted);
}}
onFocus={() => handleFilterFocus(column.key, true)}
onBlur={() => handleFilterFocus(column.key, false)}
className={`${styles.filterInput} ${filterFocused[column.key] || filters[column.key] ? styles.focused : ''}`}
maxLength={10}
/>
<label className={filterFocused[column.key] || filters[column.key] ? styles.focusedLabel : styles.label}>
{column.label}
</label>
</div>
) : (
<div className={styles.floatingLabelInput}>
<input
type="text"
placeholder=" "
value={filters[column.key] || ''}
onChange={(e) => handleFilter(column.key, e.target.value)}
onFocus={() => handleFilterFocus(column.key, true)}
onBlur={() => handleFilterFocus(column.key, false)}
className={`${styles.filterInput} ${filterFocused[column.key] || filters[column.key] ? styles.focused : ''}`}
/>
<label className={filterFocused[column.key] || filters[column.key] ? styles.focusedLabel : styles.label}>
Filter {column.label}
</label>
</div>
)}
</div>
))}
</div>
)}
</div>
)}
{/* Table */}
<div className={styles.tableContainer}>
{(
<table ref={tableRef} className={styles.table}>
<thead>
<tr>
{selectable && (
<th className={styles.selectColumn} style={{ width: '40px', minWidth: '40px', maxWidth: '40px' }}>
<input
type="checkbox"
checked={selectedRows.size === paginatedData.length && paginatedData.length > 0}
onChange={handleSelectAll}
/>
</th>
)}
{actions.length > 0 && (
<th className={styles.actionsColumn} style={{ width: '120px', minWidth: '120px', maxWidth: '120px' }}>
Actions
</th>
)}
{detectedColumns.map(column => (
<th
key={column.key}
className={`${styles.th} ${sortable && column.sortable ? styles.sortable : ''}`}
style={{
width: columnWidths[column.key] || column.width || 150,
minWidth: columnWidths[column.key] || column.width || 150,
maxWidth: columnWidths[column.key] || column.width || 150
}}
onClick={() => column.sortable && handleSort(column.key)}
>
<div className={styles.headerContent}>
<span>{column.label}</span>
{sortable && column.sortable && (
<span className={styles.sortIcon}>
{sortConfig?.key === column.key ? (
sortConfig.direction === 'asc' ? '↑' : '↓'
) : '↕'}
</span>
)}
</div>
{resizable && (
<div
className={styles.resizeHandle}
onMouseDown={(e) => handleMouseDown(e, column.key)}
/>
)}
</th>
))}
</tr>
</thead>
<tbody>
{paginatedData.map((row, index) => (
<tr
key={index}
className={`${styles.tr} ${selectedRows.has(index) ? styles.selected : ''} ${onRowClick ? styles.clickable : ''}`}
onClick={() => onRowClick?.(row, index)}
>
{selectable && (
<td className={styles.selectColumn} style={{ width: '40px', minWidth: '40px', maxWidth: '40px' }}>
<input
type="checkbox"
checked={selectedRows.has(index)}
onChange={() => handleRowSelect(index)}
onClick={(e) => e.stopPropagation()}
/>
</td>
)}
{actions.length > 0 && (
<td className={styles.actionsColumn} style={{ width: '120px', minWidth: '120px', maxWidth: '120px' }}>
<div className={styles.actionButtons}>
{actions.map((action, actionIndex) => (
<button
key={actionIndex}
onClick={(e) => {
e.stopPropagation();
action.onClick(row);
}}
className={styles.actionButton}
title={action.label}
>
{action.icon && (
<span className={styles.actionIcon}>
{typeof action.icon === 'function' ? action.icon(row) : action.icon}
</span>
)}
<span className={styles.tooltip}>{action.label}</span>
</button>
))}
</div>
</td>
)}
{detectedColumns.map(column => (
<td
key={column.key}
className={styles.td}
style={{
width: columnWidths[column.key] || column.width || 150,
minWidth: columnWidths[column.key] || column.width || 150,
maxWidth: columnWidths[column.key] || column.width || 150
}}
>
{formatCellValue(row[column.key], column, row)}
</td>
))}
</tr>
))}
</tbody>
</table>
)}
</div>
{/* Pagination */}
{pagination && totalPages > 1 && (
<div className={styles.pagination}>
<button
onClick={() => setCurrentPage(1)}
disabled={currentPage === 1}
className={styles.paginationButton}
>
««
</button>
<button
onClick={() => setCurrentPage(currentPage - 1)}
disabled={currentPage === 1}
className={styles.paginationButton}
>
«
</button>
<span className={styles.paginationInfo}>
Page {currentPage} of {totalPages} ({filteredData.length} items)
</span>
<button
onClick={() => setCurrentPage(currentPage + 1)}
disabled={currentPage === totalPages}
className={styles.paginationButton}
>
»
</button>
<button
onClick={() => setCurrentPage(totalPages)}
disabled={currentPage === totalPages}
className={styles.paginationButton}
>
»»
</button>
</div>
)}
</div>
);
}
export default FormGenerator;

View file

@ -0,0 +1,2 @@
export { default as FormGenerator } from './FormGenerator';
export type { ColumnConfig, FormGeneratorProps } from './FormGenerator';

View file

@ -0,0 +1,197 @@
/* EditForm container */
.editForm {
width: 100%;
}
/* Field styling */
.fieldGroup {
margin-bottom: 20px;
}
.fieldLabel {
display: block;
font-weight: 500;
color: var(--color-text);
margin-bottom: 6px;
font-size: 14px;
}
/* Floating label container */
.floatingLabelInput {
position: relative;
margin-bottom: 20px;
}
.fieldInput {
width: 100%;
padding: 12px 12px 8px 12px;
border: 1px solid var(--color-primary);
border-radius: 25px;
font-size: 14px;
transition: all 0.2s ease;
background-color: var(--color-bg);
box-sizing: border-box;
color: var(--color-text);
}
.fieldInput:focus {
outline: none;
border-color: var(--color-secondary);
}
.fieldInput.fieldError {
border-color: #ef4444;
box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.1);
}
/* Floating label styles */
.label {
position: absolute;
left: 12px;
top: 50%;
transform: translateY(-50%);
color: var(--color-primary);
opacity: 0.7;
font-size: 14px;
transition: all 0.2s ease;
pointer-events: none;
z-index: 1;
}
.focusedLabel {
position: absolute;
left: 12px;
top: -8px;
transform: translateY(0);
color: var(--color-primary);
opacity: 1;
font-size: 12px;
font-weight: 500;
background-color: var(--color-bg);
padding: 0 4px;
transition: all 0.2s ease;
pointer-events: none;
z-index: 2;
}
.activeFocusedLabel {
position: absolute;
left: 12px;
top: -8px;
transform: translateY(0);
color: var(--color-secondary);
opacity: 1;
font-size: 12px;
font-weight: 500;
background-color: var(--color-bg);
padding: 0 4px;
transition: all 0.2s ease;
pointer-events: none;
z-index: 2;
}
.readonlyField {
padding: 12px 12px 8px 12px;
background-color: var(--color-bg);
border: 1px solid var(--color-primary);
border-radius: 25px;
color: var(--color-text);
font-size: 14px;
min-height: 20px;
opacity: 0.7;
width: 100%;
box-sizing: border-box;
}
/* Checkbox styling */
.checkboxLabel {
display: flex;
align-items: center;
cursor: pointer;
font-weight: 500;
color: var(--color-text);
font-size: 14px;
}
.checkboxInput {
margin-right: 8px;
width: 16px;
height: 16px;
cursor: pointer;
}
/* Required field indicator */
.required {
color: #ef4444;
margin-left: 4px;
}
/* Error text */
.errorText {
color: #ef4444;
font-size: 12px;
margin-top: 4px;
display: block;
}
/* Button group */
.buttonGroup {
display: flex;
justify-content: flex-end;
gap: 12px;
margin-top: 24px;
padding-top: 16px;
border-top: 1px solid var(--color-primary);
}
.cancelButton {
padding: 8px 16px;
border: 1px solid var(--color-primary);
background-color: var(--color-bg);
color: var(--color-text);
border-radius: 25px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
}
.cancelButton:hover {
background-color: var(--color-primary-hover);
border-color: var(--color-primary);
color: #181818;
}
.saveButton {
padding: 8px 16px;
border: none;
background-color: var(--color-secondary);
color: var(--color-bg);
border-radius: 25px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
}
.saveButton:hover {
background-color: var(--color-secondary-hover);
}
.saveButton:disabled {
background-color: #9ca3af;
cursor: not-allowed;
}
/* Responsive design */
@media (max-width: 640px) {
.buttonGroup {
flex-direction: column-reverse;
}
.cancelButton,
.saveButton {
width: 100%;
padding: 12px;
}
}

View file

@ -0,0 +1,250 @@
import React, { useState, useEffect } from 'react';
import styles from './EditForm.module.css';
// Field configuration interface (moved from EditPopup)
export interface EditFieldConfig {
key: string;
label: string;
type: 'string' | 'email' | 'date' | 'enum' | 'boolean' | 'readonly';
editable: boolean;
required?: boolean;
options?: string[]; // For enum types
formatter?: (value: any) => string; // For display formatting
validator?: (value: any) => string | null; // Returns error message or null
placeholder?: string;
}
// EditForm props
export interface EditFormProps<T = any> {
data: T;
fields: EditFieldConfig[];
onSave: (updatedData: T) => void;
onCancel?: () => void;
saveButtonText?: string;
cancelButtonText?: string;
showButtons?: boolean;
className?: string;
}
// EditForm component - handles form logic
export function EditForm<T extends Record<string, any>>({
data,
fields,
onSave,
onCancel,
saveButtonText = 'Save',
cancelButtonText = 'Cancel',
showButtons = true,
className = ''
}: EditFormProps<T>) {
const [editedData, setEditedData] = useState<T>(data);
const [errors, setErrors] = useState<Record<string, string>>({});
const [fieldFocused, setFieldFocused] = useState<Record<string, boolean>>({});
// Reset data when data changes
useEffect(() => {
setEditedData({ ...data });
setErrors({});
setFieldFocused({});
}, [data]);
// Handle field focus
const handleFieldFocus = (fieldKey: string, focused: boolean) => {
setFieldFocused(prev => ({
...prev,
[fieldKey]: focused
}));
};
// Handle field value changes
const handleFieldChange = (fieldKey: string, value: any) => {
setEditedData(prev => ({
...prev,
[fieldKey]: value
}));
// Clear error for this field when user starts typing
if (errors[fieldKey]) {
setErrors(prev => {
const newErrors = { ...prev };
delete newErrors[fieldKey];
return newErrors;
});
}
};
// Validate all fields
const validateFields = (): boolean => {
const newErrors: Record<string, string> = {};
fields.forEach(field => {
const value = editedData[field.key];
// Check required fields
if (field.required && (!value || value.toString().trim() === '')) {
newErrors[field.key] = `${field.label} is required`;
return;
}
// Run custom validator
if (field.validator && value) {
const error = field.validator(value);
if (error) {
newErrors[field.key] = error;
}
}
// Basic email validation
if (field.type === 'email' && value && !/\S+@\S+\.\S+/.test(value)) {
newErrors[field.key] = 'Invalid email format';
}
});
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
// Handle save
const handleSave = () => {
if (validateFields()) {
onSave(editedData);
}
};
// Handle cancel
const handleCancel = () => {
setEditedData({ ...data });
setErrors({});
onCancel?.();
};
// Helper function to get label class
const getLabelClass = (fieldKey: string, value: any) => {
const isFocused = fieldFocused[fieldKey];
const hasValue = value && value.toString().trim() !== '';
if (isFocused) {
return styles.activeFocusedLabel; // Secondary color when actively focused
} else if (hasValue) {
return styles.focusedLabel; // Primary color when has value but not focused
} else {
return styles.label; // Regular position when empty and not focused
}
};
// Render field based on its type
const renderField = (field: EditFieldConfig) => {
const value = editedData[field.key];
const hasError = errors[field.key];
if (field.type === 'readonly' || !field.editable) {
return (
<div className={styles.floatingLabelInput} key={field.key}>
<div className={styles.readonlyField}>
{field.formatter ? field.formatter(value) : (value || 'N/A')}
</div>
<label className={styles.focusedLabel}>
{field.label}
</label>
</div>
);
}
if (field.type === 'enum') {
return (
<div className={styles.floatingLabelInput} key={field.key}>
<select
value={value || ''}
onChange={(e) => handleFieldChange(field.key, e.target.value)}
onFocus={() => handleFieldFocus(field.key, true)}
onBlur={() => handleFieldFocus(field.key, false)}
className={`${styles.fieldInput} ${hasError ? styles.fieldError : ''}`}
>
<option value="" disabled hidden></option>
{field.options?.map(option => (
<option key={option} value={option}>{option}</option>
))}
</select>
<label className={getLabelClass(field.key, value)}>
{field.label}
{field.required && <span className={styles.required}>*</span>}
</label>
{hasError && <span className={styles.errorText}>{hasError}</span>}
</div>
);
}
if (field.type === 'boolean') {
return (
<div className={styles.fieldGroup} key={field.key}>
<label className={styles.checkboxLabel}>
<input
type="checkbox"
checked={!!value}
onChange={(e) => handleFieldChange(field.key, e.target.checked)}
onFocus={() => handleFieldFocus(field.key, true)}
onBlur={() => handleFieldFocus(field.key, false)}
className={styles.checkboxInput}
/>
{field.label}
{field.required && <span className={styles.required}>*</span>}
</label>
{hasError && <span className={styles.errorText}>{hasError}</span>}
</div>
);
}
// Default to text input for string, email, date types
const inputType = field.type === 'email' ? 'email' :
field.type === 'date' ? 'date' : 'text';
return (
<div className={styles.floatingLabelInput} key={field.key}>
<input
type={inputType}
value={value || ''}
onChange={(e) => handleFieldChange(field.key, e.target.value)}
onFocus={() => handleFieldFocus(field.key, true)}
onBlur={() => handleFieldFocus(field.key, false)}
className={`${styles.fieldInput} ${hasError ? styles.fieldError : ''}`}
/>
<label className={getLabelClass(field.key, value)}>
{field.label}
{field.required && <span className={styles.required}>*</span>}
</label>
{hasError && <span className={styles.errorText}>{hasError}</span>}
</div>
);
};
return (
<div className={`${styles.editForm} ${className}`}>
<form onSubmit={(e) => { e.preventDefault(); handleSave(); }}>
{fields.map(field => renderField(field))}
</form>
{showButtons && (
<div className={styles.buttonGroup}>
{onCancel && (
<button
type="button"
className={styles.cancelButton}
onClick={handleCancel}
>
{cancelButtonText}
</button>
)}
<button
type="button"
className={styles.saveButton}
onClick={handleSave}
>
{saveButtonText}
</button>
</div>
)}
</div>
);
}
export default EditForm;

View file

@ -0,0 +1,148 @@
/* Overlay that covers the entire screen */
.overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
padding: 20px;
}
/* Main popup container */
.popup {
border: 1px solid var(--color-primary);
background: var(--color-bg);
border-radius: 25px;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2);
max-height: 90vh;
display: flex;
flex-direction: column;
overflow: hidden;
animation: popupSlideIn 0.2s ease-out;
}
/* Size variants */
.small {
max-width: 400px;
min-width: 300px;
}
.medium {
max-width: 600px;
min-width: 400px;
}
.large {
max-width: 900px;
min-width: 600px;
}
.fullscreen {
width: 95vw;
height: 95vh;
max-width: none;
max-height: none;
}
/* Popup animation */
@keyframes popupSlideIn {
from {
opacity: 0;
transform: scale(0.9) translateY(-20px);
}
to {
opacity: 1;
transform: scale(1) translateY(0);
}
}
/* Header section */
.header {
padding: 20px 24px 16px;
border-bottom: 1px solid var(--color-primary);
display: flex;
justify-content: space-between;
align-items: center;
background: var(--color-bg);
flex-shrink: 0;
}
.title {
margin: 0;
font-size: 18px;
font-weight: 400;
color: var(--color-text);
}
.closeButton {
background: none;
border: none;
font-size: 24px;
color: var(--color-primary);
cursor: pointer;
padding: 4px;
border-radius: 4px;
transition: all 0.2s ease;
line-height: 1;
}
.closeButton:hover {
background-color: var(--color-bg);
color: var(--color-primary-hover);
}
/* Content section */
.content {
padding: 24px;
overflow-y: auto;
flex: 1;
}
/* Footer section */
.footer {
padding: 16px 24px;
border-top: 1px solid var(--color-primary);
display: flex;
justify-content: flex-end;
gap: 12px;
background: var(--color-bg);
flex-shrink: 0;
}
/* Responsive design */
@media (max-width: 640px) {
.popup {
margin: 10px;
}
.small,
.medium,
.large {
min-width: 280px;
width: 100%;
}
.fullscreen {
width: 100vw;
height: 100vh;
border-radius: 0;
}
.content {
padding: 16px;
}
.header {
padding: 16px;
}
.footer {
padding: 12px 16px;
flex-direction: column-reverse;
}
}

View file

@ -0,0 +1,90 @@
import React from 'react';
import styles from './Popup.module.css';
// Generic popup props
export interface PopupProps {
isOpen: boolean;
title: string;
onClose: () => void;
children: React.ReactNode;
footerContent?: React.ReactNode;
className?: string;
size?: 'small' | 'medium' | 'large' | 'fullscreen';
closable?: boolean;
}
// Generic Popup component - can be used for any content
export function Popup({
isOpen,
title,
onClose,
children,
footerContent,
className = '',
size = 'medium',
closable = true
}: PopupProps) {
// Handle escape key
React.useEffect(() => {
const handleEscape = (e: KeyboardEvent) => {
if (e.key === 'Escape' && closable) {
onClose();
}
};
if (isOpen) {
document.addEventListener('keydown', handleEscape);
// Prevent body scroll when popup is open
document.body.style.overflow = 'hidden';
}
return () => {
document.removeEventListener('keydown', handleEscape);
document.body.style.overflow = 'unset';
};
}, [isOpen, closable, onClose]);
if (!isOpen) return null;
// Handle backdrop click
const handleBackdropClick = (e: React.MouseEvent) => {
if (e.target === e.currentTarget && closable) {
onClose();
}
};
return (
<div className={styles.overlay} onClick={handleBackdropClick}>
<div className={`${styles.popup} ${styles[size]} ${className}`}>
{/* Header */}
<div className={styles.header}>
<h2 className={styles.title}>{title}</h2>
{closable && (
<button
className={styles.closeButton}
onClick={onClose}
aria-label="Close"
>
×
</button>
)}
</div>
{/* Content */}
<div className={styles.content}>
{children}
</div>
{/* Footer (optional) */}
{footerContent && (
<div className={styles.footer}>
{footerContent}
</div>
)}
</div>
</div>
);
}
export default Popup;

View file

@ -0,0 +1,56 @@
/* ViewForm container */
.viewForm {
width: 100%;
}
/* Field styling */
.fieldGroup {
margin-bottom: 16px;
padding-bottom: 12px;
border-bottom: 1px solid #f3f4f6;
}
.fieldGroup:last-child {
border-bottom: none;
margin-bottom: 0;
}
.fieldLabel {
display: block;
font-weight: 600;
color: #374151;
margin-bottom: 6px;
font-size: 14px;
text-transform: capitalize;
}
.fieldValue {
color: #6b7280;
font-size: 14px;
line-height: 1.5;
word-break: break-word;
padding: 4px 0;
}
/* Special styling for different value types */
.fieldValue:empty::before {
content: 'N/A';
color: #9ca3af;
font-style: italic;
}
/* Responsive design */
@media (max-width: 640px) {
.fieldGroup {
margin-bottom: 12px;
padding-bottom: 8px;
}
.fieldLabel {
font-size: 13px;
}
.fieldValue {
font-size: 13px;
}
}

View file

@ -0,0 +1,40 @@
import React from 'react';
import styles from './ViewForm.module.css';
import { EditFieldConfig } from './EditForm';
// ViewForm props - for display-only purposes
export interface ViewFormProps<T = any> {
data: T;
fields: EditFieldConfig[];
className?: string;
}
// ViewForm component - displays data in read-only format
export function ViewForm<T extends Record<string, any>>({
data,
fields,
className = ''
}: ViewFormProps<T>) {
// Render field in view-only mode
const renderField = (field: EditFieldConfig) => {
const value = data[field.key];
return (
<div className={styles.fieldGroup} key={field.key}>
<label className={styles.fieldLabel}>{field.label}</label>
<div className={styles.fieldValue}>
{field.formatter ? field.formatter(value) : (value || 'N/A')}
</div>
</div>
);
};
return (
<div className={`${styles.viewForm} ${className}`}>
{fields.map(field => renderField(field))}
</div>
);
}
export default ViewForm;

View file

@ -0,0 +1,11 @@
// Generic Popup components
export { Popup, default as DefaultPopup } from './Popup';
export type { PopupProps } from './Popup';
// EditForm component
export { EditForm } from './EditForm';
export type { EditFormProps, EditFieldConfig } from './EditForm';
// ViewForm component
export { ViewForm } from './ViewForm';
export type { ViewFormProps } from './ViewForm';

View file

@ -1,66 +1,35 @@
import React from 'react'
import { motion, AnimatePresence } from 'framer-motion'
import styles from './Sidebar.module.css'
import styles from './SidebarStyles/Sidebar.module.css'
import SidebarItem from './SidebarItem';
import useSidebarData from '../../contexts/SidebarData';
import SidebarUser from './SidebarUser';
import { useCurrentUser } from '../../hooks/useUsers';
import { useSidebarMachine } from '../../hooks/machines/useSidebarMachine';
import { useSidebarLogic } from './sidebarLogic';
import { SidebarProps } from './sidebarTypes';
import { GoSidebarExpand, GoSidebarCollapse } from 'react-icons/go';
interface SidebarItemType {
id: string;
name: string;
link?: string;
submenu?: SidebarItemType[];
}
interface SidebarProps {
data: SidebarItemType[];
}
const Sidebar: React.FC<SidebarProps> = ({ data }) => {
const { user, isLoading, error } = useCurrentUser();
const sidebarMachine = useSidebarMachine();
const sidebar = useSidebarLogic();
return (
<motion.div
className={`${styles.sidebarContainer} ${sidebarMachine.isMinimized ? styles.minimized : ''}`}
animate={{
width: sidebarMachine.isMinimized ? 80 : 240
}}
transition={{
duration: 0.3,
ease: "easeInOut"
}}
<div
className={`${styles.sidebarContainer} ${sidebar.state.isMinimized ? styles.minimized : ''}`}
>
<div className={styles.logoContainer}>
<AnimatePresence mode="wait">
{!sidebarMachine.isMinimized && (
<motion.div
key="logo"
className={styles.logoWrapper}
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.8 }}
transition={{ duration: 0.2 }}
>
<div className={styles.logoWrapper}>
<div className={styles.logoText}>
<span className={styles.logoPower}>Power</span>
<span className={styles.logoOn}>On</span>
</div>
</motion.div>
)}
</AnimatePresence>
</div>
{/* Minimize/Expand Toggle Button */}
<button
className={styles.toggleButton}
onClick={sidebarMachine.isMinimized ? sidebarMachine.expandSidebar : sidebarMachine.minimizeSidebar}
title={sidebarMachine.isMinimized ? "Expand Sidebar" : "Minimize Sidebar"}
onClick={sidebar.state.isMinimized ? sidebar.expandSidebar : sidebar.minimizeSidebar}
title={sidebar.state.isMinimized ? "Expand Sidebar" : "Minimize Sidebar"}
>
{sidebarMachine.isMinimized ? (
{sidebar.state.isMinimized ? (
<GoSidebarCollapse size={20} />
) : (
<GoSidebarExpand size={20} />
@ -68,44 +37,27 @@ const Sidebar: React.FC<SidebarProps> = ({ data }) => {
</button>
</div>
<div>
<SidebarUser
user={{
id: user?.id || 0,
name: user?.fullName || user?.username || '',
role: user?.privilege || ''
}}
isLoading={isLoading}
error={error}
isMinimized={sidebarMachine.isMinimized}
/>
</div>
<motion.div
<div
className={styles.sidebar}
animate={{
width: sidebarMachine.isMinimized ? 80 : 240
}}
transition={{
duration: 0.3,
ease: "easeInOut"
}}
>
{data.map(item => {
return (
<SidebarItem
key={item.id}
item={item}
isOpen={sidebarMachine.isItemOpen(item.id)}
onToggle={() => sidebarMachine.toggleItem(item.id)}
isActive={sidebarMachine.isItemActive(item.link)}
isMinimized={sidebarMachine.isMinimized}
isOpen={sidebar.isItemOpen(item.id)}
onToggle={() => sidebar.toggleItem(item.id)}
isActive={sidebar.isItemActive(item.link)}
isMinimized={sidebar.state.isMinimized}
/>
);
})}
</motion.div>
</div>
</motion.div>
<SidebarUser
isMinimized={sidebar.state.isMinimized}
/>
</div>
)
}

View file

@ -3,26 +3,9 @@ import { Link } from "react-router-dom";
import { IoIosArrowDown } from "react-icons/io";
import { motion, AnimatePresence } from "framer-motion";
import styles from './SidebarItem.module.css';
import styles from './SidebarStyles/SidebarItem.module.css';
import SidebarSubmenu from "./SidebarSubmenu";
interface SidebarItemProps {
item: {
id: string;
name: string;
link?: string;
icon?: React.ComponentType;
submenu?: {
id: string;
name: string;
link?: string;
}[];
};
isOpen: boolean;
onToggle: () => void;
isActive: boolean;
isMinimized: boolean;
}
import { SidebarItemProps } from "./sidebarTypes";
const SidebarItem: React.FC<SidebarItemProps> = ({
item,
@ -59,7 +42,7 @@ const SidebarItem: React.FC<SidebarItemProps> = ({
</Link>
)}
{/* Overlay for minimized state to ensure full area is clickable */}
{isMinimized && (
<Link
to={item.link || "#"}

View file

@ -1,30 +1,37 @@
/* Allgemeine Stile */
.sidebarContainer {
border-radius: 0px;
border-right: 1px solid var(--color-primary);
background: var(--color-bg);
box-shadow: 0px 2px 6px 0px rgba(194, 194, 194, 0.10);
width: 240px;
padding-bottom: 1px;
display: flex;
justify-content: top;
align-items: center;
justify-content: flex-start;
align-items: stretch;
flex-direction: column;
height: 100vh;
transition: width 0.3s ease-in-out;
position: relative;
border-right: 1px solid var(--color-primary);
}
.sidebar {
display: flex;
flex-direction: column;
align-items: flex-start;
flex-shrink: 0;
flex: 1;
font-family: var(--font-family);
overflow-y: auto;
overflow-x: hidden; /* Disable horizontal scrolling */
padding: 0;
margin: 0;
}
.logoContainer {
display: flex;
height: 80px; /* Fixed height instead of auto */
padding: 30px 20px 7px 20px;
height: 80px;
padding: 30px 20px 7px 27px;
justify-content: space-between;
align-items: center;
flex-shrink: 0;
@ -36,7 +43,19 @@
display: flex;
justify-content: left;
align-items: center;
flex: 1;
opacity: 1;
transition: opacity 0.3s ease-in-out;
white-space: nowrap;
overflow: hidden;
position: absolute;
left: 20px;
top: 50%;
transform: translateY(-50%); /* Center vertically - prevents jumping */
width: calc(100% - 90px); /* Full width minus button space */
}
.logo {
@ -52,6 +71,7 @@
align-items: center;
letter-spacing: -0.5px;
font-weight: 200;
}
.logoPower {
@ -73,23 +93,28 @@
height: 50px;
display: flex;
align-items: center;
justify-content: right;
justify-content: center;
cursor: pointer;
transition: all 0.3s ease;
flex-shrink: 0;
position: absolute;
right: 20px;
top: 50%;
transform: translateY(-50%);
}
.toggleButton:hover {
background: none;
transform: scale(1.05);
top: 50%;
transform: translateY(-50%);
}
/* Minimized Sidebar Styles */
.sidebarContainer.minimized {
width: 80px;
display: flex;
justify-content: top;
align-items: center;
}
.sidebarContainer.minimized .sidebar {
@ -97,19 +122,36 @@
align-items: center;
}
.sidebarContainer.minimized .logoContainer {
height: 80px; /* Same fixed height as expanded */
height: 80px;
padding: 15px 10px 7px 10px;
justify-content: center;
box-sizing: border-box;
}
.sidebarContainer.minimized .logoWrapper {
opacity: 0;
left: 20px; /* Keep same left position */
top: 50%;
transform: translateY(-50%); /* Keep same vertical centering */
}
.sidebarContainer.minimized .menuText {
opacity: 0; /* Same rule as logo - fade out when sidebar minimized */
}
.sidebarContainer.minimized .hassubmenu {
opacity: 0; /* Same rule as logo - fade out when sidebar minimized */
}
.sidebarContainer.minimized .text_content {
opacity: 0; /* Same rule as logo and menu text - fade out when sidebar minimized */
}
.sidebarContainer.minimized .toggleButton {
margin: 0 auto;
justify-content: center;
/* Center the toggle button */
right: 15px; /* Center in 80px width: (80-50)/2 = 15px */
top: 50%;
transform: translateY(-50%); /* Keep same vertical centering */
}

View file

@ -3,23 +3,28 @@
flex-direction: column;
align-items: flex-start;
position: relative;
margin: 0;
padding: 0;
}
.menu li {
display: flex;
width: 220px;
height: 44px;
padding: 0 3px 0 15px;
padding: 0 3px 0 27px;
margin: 0;
align-items: center;
gap: 9px;
color: var(--color-text);
list-style: none;
position: relative;
}
.menu li:hover, .menu li.active {
background: var(--color-secondary);
color: var(--color-bg);
border-top-right-radius: 12px;
border-bottom-right-radius: 12px;
border-top-right-radius: 25px;
border-bottom-right-radius: 25px;
}
.menu li:hover a, .menu li.active a {
@ -30,14 +35,23 @@
.menu li a {
text-decoration: none;
font-family: var(--font-family);
font-size: 14px;
font-size: 0.9rem;
font-style: normal;
font-weight: 500;
line-height: normal;
color: inherit;
display: flex;
align-items: center;
flex: 1;
justify-content: left;
position: absolute;
height: 100%;
opacity: 1;
white-space: nowrap;
overflow: hidden;
transition: opacity 0.3s ease-in-out;
}
.menuLink {
@ -46,6 +60,7 @@
justify-content: space-between;
width: 100%;
padding-right: 15px;
padding-left: 35px;
}
.icon {
@ -56,26 +71,32 @@
justify-content: center;
align-items: center;
flex-shrink: 0;
flex-grow: 0; /* Prevent growth during transitions */
position: absolute;
top: 50%;
transform: translateY(-50%); /* Center vertically */
}
.hassubmenu {
width: 20px;
height: 20px;
transition: transform 0.2s ease;
opacity: 1;
margin-left: auto; /* Push to right side, replacing gap spacing */
transition: opacity 0.3s ease-in-out;
}
.rotated {
transform: rotate(180deg);
}
/* Text content styling */
.menuText {
transition: opacity 0.3s ease, width 0.3s ease;
white-space: nowrap;
overflow: hidden;
opacity: 1;
margin-left: 5px; /* Replace gap with margin to prevent layout shifts */
transition: opacity 0.3s ease-in-out;
}
/* Minimized overlay */
.minimizedOverlay {
position: absolute;
top: 0;
@ -87,35 +108,22 @@
/* Minimized Menu Styles */
.menu.minimized li {
/* Keep the same height as expanded */
width: 46px;
padding: 0 3px 0 15px; /* Keep same padding structure */
justify-content: flex-start; /* Keep icons in same position */
margin: 0; /* Remove auto centering that causes jumping */
padding: 0 3px 0 11px;
justify-content: flex-start;
position: relative;
}
.menu.minimized .icon {
/* Keep icon in exact same position as expanded */
margin-left: -4px;
}
.menu.minimized .menuText {
/* Hide text content */
.menu.minimized li a{
opacity: 0;
width: 0;
overflow: hidden;
}
.menu.minimized .hassubmenu {
/* Hide arrow */
opacity: 0;
width: 0;
}
.menu.minimized li:hover,
.menu.minimized li.active {
/* Keep same hover/active styles but adjust border radius for smaller width */
border-radius: 15px;
color: var(--color-text);
color: var(--color-bg);
}

View file

@ -17,12 +17,7 @@
padding: 5px 0;
}
.verticalLine {
width: 1px;
background-color: var(--color-gray-disabled);
margin-left: 46px;
margin-right: -40px;
}
.submenuList {
margin: 5px 0;

View file

@ -0,0 +1,317 @@
.user_section {
display: flex;
width: 240px;
flex-direction: column;
align-items: left;
font-family: var(--font-family);
box-sizing: border-box;
position: relative;
margin-top: auto; /* Push to bottom */
margin-bottom: 7px;
padding-left: 5px;
}
.user_info {
display: flex;
flex-direction: left;
align-items: center;
gap: 12px;
width: 100%;
border-top-right-radius: 25px;
border-bottom-right-radius : 25px;
padding: 0 3px 0 15px;
opacity: 1;
white-space: nowrap;
overflow: hidden;
transition: opacity 0.3s ease-in-out;
}
.user_info.notClickable {
cursor: pointer;
padding: 8px;
margin: -8px;
background-color: var(--color-secondary, rgba(0, 0, 0, 0.05)); /* Compensate for padding to maintain original size */
}
.user_info.clickable {
cursor: pointer;
padding: 8px;
margin: -8px;
background-color: none; /* Compensate for padding to maintain original size */
}
.user_info.clickable:hover {
background-color: var(--color-secondary, rgba(0, 0, 0, 0.05));
}
.user_header {
display: flex;
align-items: center;
gap: 12px;
width: 100%;
transition: justify-content 0.3s ease;
padding: 0 3px 0 15px;
}
.user_avatar {
width: 40px;
height: 40px;
border-radius: 50%;
background-color: var(--color-primary);
color: var(--color-bg);
display: flex;
align-items: center;
justify-content: center;
font-size: 0.9rem;
font-weight: 600;
text-transform: uppercase;
flex-shrink: 0;
transition: all 0.3s ease;
letter-spacing: 0.5px;
}
.text_content {
display: flex;
flex-direction: column;
gap: 4px;
overflow: hidden;
white-space: nowrap;
overflow: hidden;
opacity: 0; /* Start hidden */
animation: delayedFadeIn 0.3s ease-in-out forwards; /* 0.3s delay + 0.3s fade in */
}
.username {
font-size: 0.8rem !important;
color: var(--color-text) !important;
font-style: italic;
}
.logout_popup {
position: absolute;
bottom: 100%;
left: 0px;
right: 10px;
background: var(--color-primary);
border: none;
border-top-right-radius: 25px;
border-bottom-right-radius: 25px;
z-index: 1000;
margin-bottom: 8px;
overflow: hidden;
}
.logout_menu_button {
width: 100%;
padding: 12px 25px;
background: none;
border: none;
text-align: left;
cursor: pointer;
font-family: var(--font-family);
font-size: 0.9rem;
color: #181818;
display: flex;
align-items: center;
gap: 8px;
}
.logout_menu_button:hover:not(:disabled) {
background-color: var(--color-hover, rgba(0, 0, 0, 0.05));
}
.logout_menu_button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.logout_icon {
font-size: 0.9rem;
color: var(--color-gray);
}
.user_section h1 {
margin: 0;
font-size: 0.9rem;
line-height: 1.;
color: var(--color-text);
font-family: var(--font-family);
font-weight: 400;
opacity: 0; /* Start hidden */
animation: delayedFadeIn 0.3s ease-in-out forwards; /* 0.3s delay + 0.3s fade in */
white-space: nowrap;
}
.user_section p {
margin: 0;
font-family: var(--font-family);
opacity: 0; /* Start hidden */
animation: delayedFadeIn 0.3s ease-in-out forwards; /* 0.3s delay + 0.3s fade in */
white-space: nowrap;
}
.userContainer {
padding: 12px;
text-align: center;
color: transparent;
background-color: transparent;
border: transparent;
}
/* Minimized User Section Styles */
.user_section.minimized {
width: 100%; /* Match menu item width */
padding: 20px 15px 8px 15px; /* Match menu item padding structure */
align-items: center;
justify-content: center;
color: transparent;
background-color: transparent;
border: transparent;
}
.user_section.minimized .user_info {
justify-content: center; /* Center the content when minimized */
width: 100%;
height: 40px;
color: transparent;
background-color: transparent;
border: transparent;
}
.user_section.minimized .user_info.clickable:hover {
background-color: transparent; /* Remove orange background on hover when minimized */
}
.user_section.minimized .user_info.notClickable {
background-color: transparent; /* Remove orange background on hover when minimized */
}
.user_section.minimized .user_header {
justify-content: center; /* Center the avatar when minimized */
width: 100%;
padding: 0; /* Remove padding to center properly */
}
.user_section.minimized .user_avatar {
width: 40px;
height: 40px;
}
.user_section.minimized .text_content {
opacity: 0;
width: 0;
color: transparent;
background-color: transparent;
border: transparent;
animation: none; /* Disable animation when minimized */
}
.user_section.minimized h1 {
opacity: 0;
color: transparent;
background-color: transparent;
border: transparent;
animation: none; /* Disable animation when minimized */
}
.user_section.minimized p {
opacity: 0;
color: transparent;
background-color: transparent;
border: transparent;
animation: none; /* Disable animation when minimized */
}
/* Minimized logout popup styles */
.logout_popup_minimized {
left: 50%; /* Center horizontally over the user icon */
bottom: calc(100% + 8px); /* Position above the user icon */
transform: translateX(-50%) translateY(20px); /* Center and start position for animation */
width: 40px; /* Same size as user avatar */
height: 40px;
margin-left: 0;
margin-bottom: 0;
border-radius: 50%; /* Make it circular */
background: var(--color-primary);
border: 1px solid var(--color-primary);
overflow: visible; /* Allow tooltip to show */
opacity: 0; /* Start invisible */
animation: flyUpFadeIn 0.3s ease-out forwards; /* Animation */
transition: opacity 0.2s ease-out; /* Smooth fade out when disappearing */
}
.logout_popup_minimized .logout_menu_button {
justify-content: center;
align-items: center;
padding: 0;
width: 100%;
height: 100%;
border-radius: 50%; /* Make button circular */
position: relative;
}
.logout_popup_minimized .logout_menu_button::after {
content: 'Abmelden';
position: absolute;
bottom: calc(100% + 8px);
left: 50%;
transform: translateX(-50%);
color: white;
padding: 4px 8px;
border-radius: 4px;
font-size: 0.75rem;
white-space: nowrap;
opacity: 0;
pointer-events: none;
transition: opacity 0.3s ease;
z-index: 1001;
}
.logout_popup_minimized .logout_menu_button:hover::after {
opacity: 1;
}
.logout_popup_minimized .logout_icon {
margin: 0;
font-size: 1rem;
}
/* Animation keyframes for delayed text appearance */
@keyframes delayedFadeIn {
0% {
opacity: 0;
}
50% {
opacity: 0; /* Stay hidden for first 50% (0.3s) */
}
100% {
opacity: 1; /* Fade in during last 50% (0.3s) */
}
}
/* Animation keyframes for fly up and fade in effect */
@keyframes flyUpFadeIn {
from {
opacity: 0;
transform: translateX(-50%) translateY(20px);
}
to {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
}

View file

@ -1,20 +1,8 @@
import styles from './SidebarSubmenu.module.css';
import styles from './SidebarStyles/SidebarSubmenu.module.css';
import { Link } from 'react-router-dom';
import { motion, AnimatePresence } from 'framer-motion';
import { useRef, useEffect, useState } from 'react';
interface SidebarSubmenuProps {
item: {
id: string;
name: string;
submenu?: {
id: string;
name: string;
link?: string;
}[];
};
isOpen: boolean;
}
import { SidebarSubmenuProps } from './sidebarTypes';
const SidebarSubmenu: React.FC<SidebarSubmenuProps> = ({ item, isOpen }) => {
if (!item.submenu) return null;
@ -30,7 +18,6 @@ const SidebarSubmenu: React.FC<SidebarSubmenuProps> = ({ item, isOpen }) => {
className={styles.submenu}
>
<div className={styles.submenuLineContainer}>
<div className={styles.verticalLine}></div>
<ul className={styles.submenuList}>
{item.submenu.map(subitem => {
const textRef = useRef<HTMLSpanElement>(null);

View file

@ -1,124 +0,0 @@
.user_section {
display: flex;
width: 240px;
padding: 20px;
flex-direction: column;
align-items: left;
gap: 8px;
font-family: var(--font-family);
box-sizing: border-box;
}
.user_info {
display: flex;
align-items: center;
gap: 12px;
width: 100%;
transition: justify-content 0.3s ease;
}
.user_icon {
font-size: 40px;
color: var(--color-gray);
flex-shrink: 0;
transition: all 0.3s ease;
}
.text_content {
display: flex;
flex-direction: column;
gap: 4px;
transition: opacity 0.4s ease, width 0.4s ease;
overflow: hidden;
}
.user_section h1 {
margin: 0;
font-size: 16pt;
line-height: 1.;
color: var(--color-text);
font-family: var(--font-family);
font-weight: 400;
transition: opacity 0.3s ease;
white-space: nowrap;
}
.user_section p {
margin: 0;
font-size: 0.9rem;
color: var(--color-gray);
font-family: var(--font-family);
transition: opacity 0.35s ease;
white-space: nowrap;
}
.logout_button {
margin-top: 4px;
padding: 8px 16px;
background-color: var(--color-secondary);
color: var(--color-bg);
border: none;
border-radius: 15px;
cursor: pointer;
font-size: 14px;
font-family: var(--font-family);
transition: all 0.4s ease;
width: 100%;
}
.logout_button:hover {
background-color: var(--color-secondary-hover);
cursor: pointer;
}
.logout_button:active {
background-color: var(--color-red);
}
.logout_text {
transition: opacity 0.3s ease, width 0.3s ease;
white-space: nowrap;
overflow: hidden;
}
/* Minimized User Section Styles */
.user_section.minimized {
width: 46px; /* Match menu item width */
padding: 20px 15px 20px 15px; /* Match menu item padding structure */
align-items: center;
box-sizing: border-box;
justify-content: center;
}
.user_section.minimized .user_info {
justify-content: flex-start; /* Match menu items positioning */
width: 100%;
}
.user_section.minimized .user_icon {
margin-left: -12px; /* Align with menu item icons at margin-left: -4px */
font-size: 40px; /* Keep same size as expanded state */
}
.user_section.minimized .text_content {
opacity: 0;
width: 0;
}
.user_section.minimized h1 {
opacity: 0;
}
.user_section.minimized p {
opacity: 0;
}
.user_section.minimized .logout_button {
opacity: 0;
width: 0;
height: 0;
overflow: hidden;
margin: 0;
padding: 0;
}

View file

@ -1,53 +1,122 @@
import React from 'react'
import React, { useState, useEffect, useRef } from 'react'
import { useMsal } from '@azure/msal-react'
import { FaUserCircle } from 'react-icons/fa'
import styles from './SidebarUser.module.css'
import { FaSignOutAlt } from 'react-icons/fa'
import styles from './SidebarStyles/SidebarUser.module.css'
import { useCurrentUser } from '../../hooks/useUsers'
import { SidebarUserProps } from './sidebarTypes';
interface User {
id: number;
name: string;
role: string;
}
interface SidebarUserProps {
user: User;
isLoading?: boolean;
error?: string | null;
isMinimized?: boolean;
}
const SidebarUser: React.FC<SidebarUserProps> = ({ user, isLoading, error, isMinimized = false }) => {
const SidebarUser: React.FC<SidebarUserProps> = ({ isMinimized = false }) => {
const { instance } = useMsal();
const { user, isLoading, error, logout } = useCurrentUser();
const [showLogoutMenu, setShowLogoutMenu] = useState(false);
const [isLoggingOut, setIsLoggingOut] = useState(false);
const userSectionRef = useRef<HTMLDivElement>(null);
// Function to get initials from full name
const getInitials = (fullName: string): string => {
return fullName
.split(' ')
.map(name => name.charAt(0).toUpperCase())
.join('')
.substring(0, 2); // Limit to 2 characters
};
const handleUserClick = () => {
setShowLogoutMenu(!showLogoutMenu);
};
const handleLogout = async () => {
if (!user || isLoggingOut) return;
setIsLoggingOut(true);
try {
// Clear MSAL cache and sign out
await instance.logoutRedirect({
onRedirectNavigate: () => {
// Clear any application-specific data from localStorage
localStorage.clear();
return true;
// Pass MSAL instance for Microsoft authentication
if (user.authenticationAuthority === 'msft') {
await logout(instance);
} else {
await logout();
}
});
setShowLogoutMenu(false);
} catch (error) {
console.error('Logout failed:', error);
// Keep the menu open if logout fails so user can try again
setIsLoggingOut(false);
}
};
// Close popup when clicking outside
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (userSectionRef.current && !userSectionRef.current.contains(event.target as Node)) {
setShowLogoutMenu(false);
}
};
if (showLogoutMenu) {
document.addEventListener('mousedown', handleClickOutside);
}
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, [showLogoutMenu]);
if (isLoading) {
return <div className={styles.userContainer}>Lädt...</div>;
return (
<div className={`${styles.user_section} ${isMinimized ? styles.minimized : ''}`}>
<div className={styles.userContainer}>Lädt...</div>
</div>
);
}
if (error) {
return <div className={styles.userContainer}>Fehler beim Laden des Benutzerprofils</div>;
return (
<div className={`${styles.user_section} ${isMinimized ? styles.minimized : ''}`}>
<div className={styles.userContainer}>Fehler beim Laden des Benutzerprofils</div>
</div>
);
}
if (!user) {
return (
<div className={`${styles.user_section} ${isMinimized ? styles.minimized : ''}`}>
<div className={styles.userContainer}>Kein Benutzer gefunden</div>
</div>
);
}
return (
<div className={`${styles.user_section} ${isMinimized ? styles.minimized : ''}`}>
<div className={styles.user_info}>
<div
ref={userSectionRef}
className={`${styles.user_section} ${isMinimized ? styles.minimized : ''}`}
>
{showLogoutMenu && (
<div className={`${styles.logout_popup} ${isMinimized ? styles.logout_popup_minimized : ''}`}>
<button
className={styles.logout_menu_button}
onClick={handleLogout}
disabled={isLoggingOut}
>
<FaSignOutAlt className={styles.logout_icon} />
{!isMinimized && (isLoggingOut ? 'Abmelden...' : 'Abmelden')}
</button>
</div>
)}
<div
className={`${styles.user_info} ${showLogoutMenu ? styles.notClickable : styles.clickable}`}
onClick={handleUserClick}
>
<div className={styles.user_header}>
<div className={styles.user_avatar}>
{getInitials(user.fullName)}
</div>
{!isMinimized && (
<div className={styles.text_content}>
<h1>{ user.name }</h1>
<p>Rolle: {user.role}</p>
<h1>{user.fullName}</h1>
<p className={styles.username}>{user.username}</p>
</div>
)}
</div>
</div>
</div>

View file

@ -1,3 +1,7 @@
import Sidebar from "./Sidebar";
export default Sidebar;
export { default as SidebarItem } from './SidebarItem';
export { default as SidebarUser } from './SidebarUser';
export * from './sidebarTypes';
export { useSidebarLogic } from './sidebarLogic';

View file

@ -0,0 +1,68 @@
import { useState, useCallback } from 'react';
import { useLocation } from 'react-router-dom';
import { SidebarState, SidebarContextType } from './sidebarTypes';
// Custom hook for sidebar logic
export const useSidebarLogic = (): SidebarContextType => {
const location = useLocation();
// Simple React state instead of state machine
const [state, setState] = useState<SidebarState>({
openItemId: null,
isMinimized: false,
});
// Toggle a specific menu item
const toggleItem = useCallback((itemId: string) => {
setState(prevState => ({
...prevState,
openItemId: prevState.openItemId === itemId ? null : itemId,
}));
}, []);
// Close all submenus
const closeAll = useCallback(() => {
setState(prevState => ({
...prevState,
openItemId: null,
}));
}, []);
// Check if a specific item is open
const isItemOpen = useCallback((itemId: string) => {
return state.openItemId === itemId;
}, [state.openItemId]);
// Check if an item is the active route
const isItemActive = useCallback((itemPath?: string) => {
if (!itemPath) return false;
return location.pathname === itemPath;
}, [location.pathname]);
// Minimize sidebar
const minimizeSidebar = useCallback(() => {
setState(prevState => ({
...prevState,
isMinimized: true,
openItemId: null, // Close any open submenu when minimizing
}));
}, []);
// Expand sidebar
const expandSidebar = useCallback(() => {
setState(prevState => ({
...prevState,
isMinimized: false,
}));
}, []);
return {
state,
toggleItem,
closeAll,
isItemOpen,
isItemActive,
minimizeSidebar,
expandSidebar,
};
};

View file

@ -0,0 +1,56 @@
import React from 'react';
// Base sidebar item interface
export interface SidebarItemData {
id: string;
name: string;
link?: string;
icon?: React.ComponentType<React.SVGProps<SVGSVGElement>>;
submenu?: SidebarSubmenuItemData[];
}
// Submenu item interface
export interface SidebarSubmenuItemData {
id: string;
name: string;
link?: string;
}
// Sidebar state interface
export interface SidebarState {
openItemId: string | null;
isMinimized: boolean;
}
// Sidebar context interface
export interface SidebarContextType {
state: SidebarState;
toggleItem: (itemId: string) => void;
closeAll: () => void;
isItemOpen: (itemId: string) => boolean;
isItemActive: (itemPath?: string) => boolean;
minimizeSidebar: () => void;
expandSidebar: () => void;
}
// Component props interfaces
export interface SidebarProps {
data: SidebarItemData[];
}
export interface SidebarItemProps {
item: SidebarItemData;
isOpen: boolean;
onToggle: () => void;
isActive: boolean;
isMinimized: boolean;
}
export interface SidebarSubmenuProps {
item: SidebarItemData;
isOpen: boolean;
}
export interface SidebarUserProps {
isMinimized?: boolean;
}

View file

@ -1,5 +1,5 @@
import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react';
import { Language, translations } from './languageContextData';
import { Language, TranslationKeys, loadLanguage } from '../locales';
// Re-export Language type for convenience
export type { Language };
@ -8,6 +8,8 @@ interface LanguageContextType {
currentLanguage: Language;
setLanguage: (language: Language) => void;
t: (key: string, fallback?: string) => string;
isLoading: boolean;
reloadLanguage: () => Promise<void>;
}
const LanguageContext = createContext<LanguageContextType | undefined>(undefined);
@ -16,39 +18,68 @@ interface LanguageProviderProps {
children: ReactNode;
}
export const LanguageProvider: React.FC<LanguageProviderProps> = ({ children }) => {
const [currentLanguage, setCurrentLanguage] = useState<Language>('de');
const [translations, setTranslations] = useState<TranslationKeys>({});
const [isLoading, setIsLoading] = useState(true);
// Function to load and set a language
const loadAndSetLanguage = async (language: Language) => {
setIsLoading(true);
try {
const newTranslations = await loadLanguage(language);
setTranslations(newTranslations);
setCurrentLanguage(language);
} catch (error) {
console.error('Failed to load language:', error);
// Keep current language and translations if loading fails
} finally {
setIsLoading(false);
}
};
// Load saved language preference on mount
useEffect(() => {
const initializeLanguage = async () => {
const savedLanguage = localStorage.getItem('language') as Language;
let initialLanguage: Language = 'de';
if (savedLanguage && ['de', 'en', 'fr'].includes(savedLanguage)) {
setCurrentLanguage(savedLanguage);
initialLanguage = savedLanguage;
} else {
// Detect browser language
const browserLang = navigator.language.split('-')[0] as Language;
if (['de', 'en', 'fr'].includes(browserLang)) {
setCurrentLanguage(browserLang);
initialLanguage = browserLang;
}
}
await loadAndSetLanguage(initialLanguage);
};
initializeLanguage();
}, []);
const setLanguage = (language: Language) => {
setCurrentLanguage(language);
const setLanguage = async (language: Language) => {
localStorage.setItem('language', language);
await loadAndSetLanguage(language);
};
const reloadLanguage = async () => {
await loadAndSetLanguage(currentLanguage);
};
const t = (key: string, fallback?: string): string => {
const translation = translations[currentLanguage]?.[key] || translations['de'][key] || fallback || key;
const translation = translations[key] || fallback || key;
return translation;
};
const contextValue: LanguageContextType = {
currentLanguage,
setLanguage,
t
t,
isLoading,
reloadLanguage
};
return (

View file

@ -1,11 +1,12 @@
import { MdOutlineWorkOutline } from 'react-icons/md';
import { BsChatDots } from "react-icons/bs";
import { LuTicket } from "react-icons/lu";
import { LuWorkflow, LuTicket } from "react-icons/lu";
import { RiTeamLine } from "react-icons/ri";
import { BiInfoSquare } from "react-icons/bi";
import { GoGear } from "react-icons/go";
import { FaRegFileAlt } from "react-icons/fa";
import { FaPlug, FaRegFileAlt } from "react-icons/fa";
import { TbLogs } from "react-icons/tb";
import { FaShare } from "react-icons/fa";
import { useMemo } from 'react';
import { useLanguage } from './LanguageContext';
@ -16,40 +17,48 @@ const useSidebarData = () => {
return useMemo(() => [
{
id: '1',
name: t('nav.team'),
link: '/team-bereich',
icon: MdOutlineWorkOutline,
},
{
id: '2',
name: t('nav.dashboard'),
link: '/dashboard',
icon: LuTicket,
},
{
id: '3',
name: t('nav.files'),
link: '/dateien',
icon: FaRegFileAlt,
},
/*{
id: '6',
name: 'Logs',
link: '',
icon: TbLogs ,
},*/
{
id: '4',
name: t('nav.workflows'),
link: '/workflows',
icon: LuWorkflow ,
},
{
id: '5',
name: t('nav.connections'),
link: '/connections',
icon: FaPlug ,
},
{
id: '2',
name: t('nav.team'),
link: '/team-bereich',
icon: MdOutlineWorkOutline,
},
{
id: '7',
name: t('nav.settings'),
link: '/einstellungen',
icon: GoGear,
},
/*{
{
id: '8',
name: 'Help',
link: '',
icon: BiInfoSquare,
},*/
name: t('nav.testSharepoint'),
link: '/testSharepoint',
icon: FaShare,
},
], [t]);
}

View file

@ -1,683 +0,0 @@
// Language type definition
export type Language = 'de' | 'en' | 'fr';
// Translation keys and their values for each language
export type TranslationKeys = {
[key: string]: string;
};
export type Translations = {
[K in Language]: TranslationKeys;
};
export const translations: Translations = {
de: {
// Navigation
'nav.dashboard': 'Aktivitätszentrum',
'nav.files': 'Dateien',
'nav.team': 'Team-Bereich',
'nav.settings': 'Einstellungen',
// Settings page
'settings.title': 'Einstellungen',
'settings.appearance': 'Darstellung',
'settings.language': 'Sprache',
'settings.about': 'Über',
'settings.version': 'Version',
'settings.theme': 'Theme',
'settings.theme.description': 'Wechseln Sie zwischen hellem und dunklem Modus',
'settings.language.description': 'Wählen Sie Ihre bevorzugte Sprache',
'settings.theme.light': 'Hell',
'settings.theme.dark': 'Dunkel',
'settings.theme.toggle.light': 'Zu hellem Modus wechseln',
'settings.theme.toggle.dark': 'Zu dunklem Modus wechseln',
// Languages
'language.german': 'Deutsch',
'language.english': 'English',
'language.french': 'Français',
// Common
'common.loading': 'Laden...',
'common.error': 'Fehler',
'common.success': 'Erfolgreich',
'common.cancel': 'Abbrechen',
'common.save': 'Speichern',
'common.delete': 'Löschen',
'common.edit': 'Bearbeiten',
'common.close': 'Schließen',
// Auth
'auth.login': 'Anmelden',
'auth.register': 'Registrieren',
'auth.logout': 'Abmelden',
'auth.email': 'E-Mail',
'auth.password': 'Passwort',
// Dashboard
'dashboard.prompt.template': 'Prompt Vorlage',
'dashboard.prompt.settings': 'Einstellungen',
'dashboard.chat.area': 'Chatbereich',
'dashboard.chat.history': 'Workflow-Verlauf',
'dashboard.log.title': 'Log',
'dashboard.log.workflow': 'Workflow',
'dashboard.log.no_workflow': 'Kein Workflow ausgewählt',
'dashboard.log.loading': 'Logs werden geladen...',
'dashboard.log.error': 'Fehler beim Laden der Logs',
'dashboard.log.no_logs': 'Keine Logs für diesen Workflow verfügbar',
'dashboard.log.waiting': 'Workflow läuft... Warte auf Logs...',
'dashboard.log.fetch_failed': 'Logs konnten nicht geladen werden',
'dashboard.log.level.info': 'INFO',
// Prompt Set
'promptset.loading': 'Prompts werden geladen...',
'promptset.error.loading': 'Fehler beim Laden der Prompts',
'promptset.retry': 'Erneut versuchen',
'promptset.new_prompt': 'Neuer Prompt',
'promptset.prompt_count': 'Prompt',
'promptset.prompt_count_plural': 'Prompts',
'promptset.no_prompts': 'Keine Prompts verfügbar',
'promptset.created': 'Erstellt',
'promptset.run_tooltip': 'Prompt ausführen',
'promptset.share_tooltip': 'Prompt teilen',
'promptset.delete_tooltip': 'Prompt löschen',
'promptset.confirm_delete': 'Klicken Sie erneut zum Bestätigen',
'promptset.deleting': 'Löschen...',
'promptset.confirm_click': 'Zum Bestätigen klicken',
'promptset.delete_error': 'Fehler beim Löschen',
'promptset.deleting_message': 'Prompt wird gelöscht...',
// Prompt Modal
'modal.create_prompt': 'Neuen Prompt erstellen',
'modal.name_required': 'Name ist erforderlich',
'modal.content_required': 'Inhalt ist erforderlich',
'modal.create_error': 'Fehler beim Erstellen des Prompts',
'modal.name_label': 'Name',
'modal.content_label': 'Inhalt',
'modal.name_placeholder': 'Geben Sie einen Namen für den Prompt ein',
'modal.content_placeholder': 'Geben Sie den Inhalt des Prompts ein',
'modal.cancel': 'Abbrechen',
'modal.creating': 'Erstellen...',
'modal.create': 'Prompt erstellen',
// Share Modal
'share_modal.title': 'Prompt teilen',
'share_modal.select_users': 'Benutzer auswählen',
'share_modal.select_all': 'Alle auswählen',
'share_modal.deselect_all': 'Alle abwählen',
'share_modal.loading_users': 'Benutzer werden geladen...',
'share_modal.error_loading_users': 'Fehler beim Laden der Benutzer',
'share_modal.no_users_available': 'Keine Benutzer verfügbar',
'share_modal.no_users_selected': 'Bitte wählen Sie mindestens einen Benutzer aus',
'share_modal.one_user_selected': '1 Benutzer ausgewählt',
'share_modal.multiple_users_selected': '{count} Benutzer ausgewählt',
'share_modal.custom_title': 'Benutzerdefinierter Titel (optional)',
'share_modal.title_placeholder': 'Geben Sie einen benutzerdefinierten Titel ein',
'share_modal.message': 'Nachricht (optional)',
'share_modal.message_placeholder': 'Fügen Sie eine Nachricht für die Empfänger hinzu',
'share_modal.share': 'Teilen',
'share_modal.sharing': 'Wird geteilt...',
'share_modal.share_error': 'Fehler beim Teilen des Prompts',
// Prompt Settings
'prompt_settings.title': 'Prompt Einstellungen',
'prompt_settings.content_placeholder': 'Einstellungen werden in zukünftigen Updates hinzugefügt.',
// Chat Area
'chat.continue_conversation': 'Gespräch fortsetzen...',
'chat.enter_message': 'Nachricht eingeben...',
'chat.remove_file': 'Datei entfernen',
'chat.attach_file': 'Datei anhängen',
'chat.you': 'You',
'chat.click_to_open': 'Klicken Sie, um zu öffnen',
'chat.preview_document': 'Dokument vorschauen',
'chat.download_document': 'Dokument herunterladen',
'chat.workflow_failed': 'Workflow fehlgeschlagen.',
'chat.retry_workflow': 'Nochmal versuchen',
'chat.sending_followup': 'Folgenachricht wird gesendet...',
'chat.sending_message': 'Nachricht wird gesendet...',
'chat.error_prefix': 'Fehler:',
'chat.error_loading_messages': 'Fehler beim Laden der Nachrichten:',
'chat.loading_workflow_messages': 'Workflow-Nachrichten werden geladen...',
'chat.start_conversation': 'Beginne ein Gespräch, indem du eine Nachricht eingibst, eine Vorlage auswählst oder einen vorherigen Workflow fortsetzt …',
// File Preview
'file_preview.loading': 'Vorschau wird geladen...',
'file_preview.error': 'Fehler',
'file_preview.no_preview': 'Keine Vorschau verfügbar',
'file_preview.close_preview': 'Vorschau schließen',
'file_preview.python': 'Python',
// Chat History
'chat_history.loading': 'Workflows werden geladen...',
'chat_history.error_loading': 'Fehler beim Laden der Workflows:',
'chat_history.try_again': 'Nochmal versuchen',
'chat_history.title': 'Workflow-Verlauf',
'chat_history.workflow_count': 'Workflow',
'chat_history.workflow_count_plural': 'Workflows',
'chat_history.empty_state': 'Keine Workflows verfügbar',
'chat_history.confirm_delete': 'Sind Sie sicher, dass Sie Workflow "{id}..." löschen möchten?',
'chat_history.no_message_content': 'Kein Nachrichteninhalt verfügbar',
'chat_history.unknown_date': 'Unbekanntes Datum',
'chat_history.invalid_date': 'Ungültiges Datum',
'chat_history.started': 'Gestartet:',
'chat_history.last_activity': 'Letzte Aktivität:',
'chat_history.round': 'Runde',
'chat_history.resume_tooltip': 'Workflow fortsetzen',
'chat_history.delete_tooltip': 'Workflow löschen',
'chat_history.deleting': 'Workflow wird gelöscht...',
// Workflow Status
'status.error': 'FEHLER',
'status.failed': 'FEHLGESCHLAGEN',
'status.stopped': 'GESTOPPT',
'status.cancelled': 'ABGEBROCHEN',
'status.running': 'LÄUFT',
'status.processing': 'VERARBEITUNG',
'status.completed': 'ABGESCHLOSSEN',
'status.pending': 'WARTEND',
// Files
'files.unknown_size': 'Unbekannte Größe',
'files.unknown_date': 'Unbekanntes Datum',
'files.source.uploaded': 'Hochgeladen',
'files.source.ai_created': 'KI-erstellt',
'files.source.shared': 'Geteilt',
'files.source.unknown': 'Unbekannt',
'files.preview_tooltip': 'Datei vorschauen',
'files.download_tooltip': 'Datei herunterladen',
'files.delete_tooltip': 'Datei löschen',
'files.delete_confirm_tooltip': 'Klicken Sie erneut zum Bestätigen der Löschung',
'files.downloading': 'Laden...',
'files.deleting': 'Löschen...',
'files.delete_confirm': 'Zum Bestätigen klicken...',
'files.no_files': 'Keine Dateien gefunden.',
'files.no_shared_files': 'Keine mit Ihnen geteilten Dateien gefunden.',
'files.no_ai_files': 'Keine von der KI erstellten Dateien gefunden.',
'files.no_uploaded_files': 'Keine hochgeladenen Dateien gefunden.',
'files.header.name': 'Name',
'files.header.type': 'Typ',
'files.header.size': 'Größe',
'files.header.date': 'Datum',
'files.selector.title': 'Dateien auswählen',
'files.selector.tab.all': 'Alle Dateien',
'files.selector.tab.uploads': 'Hochgeladen',
'files.selector.tab.created': 'KI-erstellt',
'files.selector.tab.shared': 'Geteilt',
'files.selector.select_all': 'Alle auswählen',
'files.selector.deselect_all': 'Alle abwählen',
'files.selector.file_selected': 'Datei',
'files.selector.files_selected': 'Dateien',
'files.selector.selected_suffix': 'ausgewählt',
'files.selector.upload_new': 'Neue Datei hochladen',
'files.selector.loading': 'Dateien werden geladen...',
'files.selector.error_loading': 'Fehler beim Laden der Dateien:',
'files.upload.title': 'Datei hochladen',
'files.upload.drop_here': 'Datei hier ablegen...',
'files.upload.uploading': 'Lädt hoch...',
'files.upload.drag_files': 'Dateien hierher ziehen',
'files.upload.or': 'oder',
'files.upload.browse': 'Durchsuchen',
'files.upload.selected_file': 'Ausgewählte Datei:',
'files.upload.upload_button': 'Hochladen',
'files.upload.uploading_button': 'Wird hochgeladen...',
'files.upload.success': 'Datei erfolgreich hochgeladen!',
'files.upload.error': 'Beim Hochladen ist ein Fehler aufgetreten.',
'files.upload.unexpected_error': 'Beim Hochladen ist ein unerwarteter Fehler aufgetreten.',
// Files Page
'files.page.tab.all': 'Alle Dateien',
'files.page.tab.uploads': 'Meine Uploads',
'files.page.tab.created': 'Erstellte Dateien',
'files.page.tab.shared': 'Geteilte Dateien',
'files.page.add_file': 'Datei hinzufügen',
'files.page.loading': 'Dateien werden geladen...',
'files.page.error': 'Fehler:',
},
en: {
// Navigation
'nav.dashboard': 'Activity Center',
'nav.files': 'Files',
'nav.team': 'Team Area',
'nav.settings': 'Settings',
// Settings page
'settings.title': 'Settings',
'settings.appearance': 'Appearance',
'settings.language': 'Language',
'settings.about': 'About',
'settings.version': 'Version',
'settings.theme': 'Theme',
'settings.theme.description': 'Switch between light and dark mode',
'settings.language.description': 'Choose your preferred language',
'settings.theme.light': 'Light',
'settings.theme.dark': 'Dark',
'settings.theme.toggle.light': 'Switch to light mode',
'settings.theme.toggle.dark': 'Switch to dark mode',
// Languages
'language.german': 'Deutsch',
'language.english': 'English',
'language.french': 'Français',
// Common
'common.loading': 'Loading...',
'common.error': 'Error',
'common.success': 'Success',
'common.cancel': 'Cancel',
'common.save': 'Save',
'common.delete': 'Delete',
'common.edit': 'Edit',
'common.close': 'Close',
// Auth
'auth.login': 'Login',
'auth.register': 'Register',
'auth.logout': 'Logout',
'auth.email': 'Email',
'auth.password': 'Password',
// Dashboard
'dashboard.prompt.template': 'Prompt Template',
'dashboard.prompt.settings': 'Settings',
'dashboard.chat.area': 'Chat Area',
'dashboard.chat.history': 'Workflow History',
'dashboard.log.title': 'Log',
'dashboard.log.workflow': 'Workflow',
'dashboard.log.no_workflow': 'No workflow selected',
'dashboard.log.loading': 'Loading logs...',
'dashboard.log.error': 'Error loading logs',
'dashboard.log.no_logs': 'No logs available for this workflow',
'dashboard.log.waiting': 'Workflow running... Waiting for logs...',
'dashboard.log.fetch_failed': 'Failed to fetch logs',
'dashboard.log.level.info': 'INFO',
// Prompt Set
'promptset.loading': 'Loading prompts...',
'promptset.error.loading': 'Error loading prompts',
'promptset.retry': 'Try again',
'promptset.new_prompt': 'New Prompt',
'promptset.prompt_count': 'Prompt',
'promptset.prompt_count_plural': 'Prompts',
'promptset.no_prompts': 'No prompts available',
'promptset.created': 'Created',
'promptset.run_tooltip': 'Run prompt',
'promptset.share_tooltip': 'Share prompt',
'promptset.delete_tooltip': 'Delete prompt',
'promptset.confirm_delete': 'Click again to confirm',
'promptset.deleting': 'Deleting...',
'promptset.confirm_click': 'Click to confirm',
'promptset.delete_error': 'Error deleting',
'promptset.deleting_message': 'Deleting prompt...',
// Prompt Modal
'modal.create_prompt': 'Create New Prompt',
'modal.name_required': 'Name is required',
'modal.content_required': 'Content is required',
'modal.create_error': 'Error creating prompt',
'modal.name_label': 'Name',
'modal.content_label': 'Content',
'modal.name_placeholder': 'Enter a name for the prompt',
'modal.content_placeholder': 'Enter the prompt content',
'modal.cancel': 'Cancel',
'modal.creating': 'Creating...',
'modal.create': 'Create Prompt',
// Share Modal
'share_modal.title': 'Share Prompt',
'share_modal.select_users': 'Select Users',
'share_modal.select_all': 'Select All',
'share_modal.deselect_all': 'Deselect All',
'share_modal.loading_users': 'Loading users...',
'share_modal.error_loading_users': 'Error loading users',
'share_modal.no_users_available': 'No users available',
'share_modal.no_users_selected': 'Please select at least one user',
'share_modal.one_user_selected': '1 user selected',
'share_modal.multiple_users_selected': '{count} users selected',
'share_modal.custom_title': 'Custom Title (optional)',
'share_modal.title_placeholder': 'Enter a custom title',
'share_modal.message': 'Message (optional)',
'share_modal.message_placeholder': 'Add a message for recipients',
'share_modal.share': 'Share',
'share_modal.sharing': 'Sharing...',
'share_modal.share_error': 'Error sharing prompt',
// Prompt Settings
'prompt_settings.title': 'Prompt Settings',
'prompt_settings.content_placeholder': 'Settings content will be added here in future updates.',
// Chat Area
'chat.continue_conversation': 'Continue conversation...',
'chat.enter_message': 'Enter message...',
'chat.remove_file': 'Remove file',
'chat.attach_file': 'Attach file',
'chat.you': 'You',
'chat.click_to_open': 'Click to open',
'chat.preview_document': 'Preview document',
'chat.download_document': 'Download document',
'chat.workflow_failed': 'Workflow failed.',
'chat.retry_workflow': 'Try again',
'chat.sending_followup': 'Sending follow-up message...',
'chat.sending_message': 'Sending message...',
'chat.error_prefix': 'Error:',
'chat.error_loading_messages': 'Error loading messages:',
'chat.loading_workflow_messages': 'Loading workflow messages...',
'chat.start_conversation': 'Start a conversation by entering a message, selecting a template, or continuing a previous workflow...',
// File Preview
'file_preview.loading': 'Loading preview...',
'file_preview.error': 'Error',
'file_preview.no_preview': 'No preview available',
'file_preview.close_preview': 'Close preview',
'file_preview.python': 'Python',
// Chat History
'chat_history.loading': 'Loading workflows...',
'chat_history.error_loading': 'Error loading workflows:',
'chat_history.try_again': 'Try Again',
'chat_history.title': 'Workflow History',
'chat_history.workflow_count': 'Workflow',
'chat_history.workflow_count_plural': 'Workflows',
'chat_history.empty_state': 'No workflows available',
'chat_history.confirm_delete': 'Are you sure you want to delete workflow "{id}..."?',
'chat_history.no_message_content': 'No message content available',
'chat_history.unknown_date': 'Unknown date',
'chat_history.invalid_date': 'Invalid date',
'chat_history.started': 'Started:',
'chat_history.last_activity': 'Last Activity:',
'chat_history.round': 'Round',
'chat_history.resume_tooltip': 'Resume workflow',
'chat_history.delete_tooltip': 'Delete workflow',
'chat_history.deleting': 'Deleting workflow...',
// Workflow Status
'status.error': 'ERROR',
'status.failed': 'FAILED',
'status.stopped': 'STOPPED',
'status.cancelled': 'CANCELLED',
'status.running': 'RUNNING',
'status.processing': 'PROCESSING',
'status.completed': 'COMPLETED',
'status.pending': 'PENDING',
// Files
'files.unknown_size': 'Unknown Size',
'files.unknown_date': 'Unknown Date',
'files.source.uploaded': 'Uploaded',
'files.source.ai_created': 'AI-created',
'files.source.shared': 'Shared',
'files.source.unknown': 'Unknown',
'files.preview_tooltip': 'Preview file',
'files.download_tooltip': 'Download file',
'files.delete_tooltip': 'Delete file',
'files.delete_confirm_tooltip': 'Click again to confirm deletion',
'files.downloading': 'Downloading...',
'files.deleting': 'Deleting...',
'files.delete_confirm': 'Click to confirm...',
'files.no_files': 'No files found.',
'files.no_shared_files': 'No shared files found.',
'files.no_ai_files': 'No AI-created files found.',
'files.no_uploaded_files': 'No uploaded files found.',
'files.header.name': 'Name',
'files.header.type': 'Type',
'files.header.size': 'Size',
'files.header.date': 'Date',
'files.selector.title': 'Select files',
'files.selector.tab.all': 'All files',
'files.selector.tab.uploads': 'Uploaded',
'files.selector.tab.created': 'AI-created',
'files.selector.tab.shared': 'Shared',
'files.selector.select_all': 'Select all',
'files.selector.deselect_all': 'Deselect all',
'files.selector.file_selected': 'File',
'files.selector.files_selected': 'Files',
'files.selector.selected_suffix': 'selected',
'files.selector.upload_new': 'Upload new file',
'files.selector.loading': 'Loading files...',
'files.selector.error_loading': 'Error loading files:',
'files.upload.title': 'Upload file',
'files.upload.drop_here': 'Drop file here...',
'files.upload.uploading': 'Uploading...',
'files.upload.drag_files': 'Drag files here',
'files.upload.or': 'or',
'files.upload.browse': 'Browse',
'files.upload.selected_file': 'Selected file:',
'files.upload.upload_button': 'Upload',
'files.upload.uploading_button': 'Uploading...',
'files.upload.success': 'File uploaded successfully!',
'files.upload.error': 'An error occurred while uploading.',
'files.upload.unexpected_error': 'An unexpected error occurred while uploading.',
// Files Page
'files.page.tab.all': 'All Files',
'files.page.tab.uploads': 'My Uploads',
'files.page.tab.created': 'Created Files',
'files.page.tab.shared': 'Shared Files',
'files.page.add_file': 'Add File',
'files.page.loading': 'Loading files...',
'files.page.error': 'Error:',
},
fr: {
// Navigation
'nav.dashboard': 'Centre d\'activité',
'nav.files': 'Fichiers',
'nav.team': 'Espace équipe',
'nav.settings': 'Paramètres',
// Settings page
'settings.title': 'Paramètres',
'settings.appearance': 'Apparence',
'settings.language': 'Langue',
'settings.about': 'À propos',
'settings.version': 'Version',
'settings.theme': 'Thème',
'settings.theme.description': 'Basculer entre le mode clair et sombre',
'settings.language.description': 'Choisissez votre langue préférée',
'settings.theme.light': 'Clair',
'settings.theme.dark': 'Sombre',
'settings.theme.toggle.light': 'Passer en mode clair',
'settings.theme.toggle.dark': 'Passer en mode sombre',
// Languages
'language.german': 'Deutsch',
'language.english': 'English',
'language.french': 'Français',
// Common
'common.loading': 'Chargement...',
'common.error': 'Erreur',
'common.success': 'Succès',
'common.cancel': 'Annuler',
'common.save': 'Enregistrer',
'common.delete': 'Supprimer',
'common.edit': 'Modifier',
'common.close': 'Fermer',
// Auth
'auth.login': 'Se connecter',
'auth.register': 'S\'inscrire',
'auth.logout': 'Se déconnecter',
'auth.email': 'E-mail',
'auth.password': 'Mot de passe',
// Dashboard
'dashboard.prompt.template': 'Modèle de prompt',
'dashboard.prompt.settings': 'Paramètres',
'dashboard.chat.area': 'Zone de chat',
'dashboard.chat.history': 'Historique des workflows',
'dashboard.log.title': 'Journal',
'dashboard.log.workflow': 'Workflow',
'dashboard.log.no_workflow': 'Aucun workflow sélectionné',
'dashboard.log.loading': 'Chargement des logs...',
'dashboard.log.error': 'Erreur lors du chargement des logs',
'dashboard.log.no_logs': 'Aucun log disponible pour ce workflow',
'dashboard.log.waiting': 'Workflow en cours... En attente des logs...',
'dashboard.log.fetch_failed': 'Échec du chargement des logs',
'dashboard.log.level.info': 'INFO',
// Prompt Set
'promptset.loading': 'Chargement des prompts...',
'promptset.error.loading': 'Erreur lors du chargement des prompts',
'promptset.retry': 'Réessayer',
'promptset.new_prompt': 'Nouveau prompt',
'promptset.prompt_count': 'Prompt',
'promptset.prompt_count_plural': 'Prompts',
'promptset.no_prompts': 'Aucun prompt disponible',
'promptset.created': 'Créé',
'promptset.run_tooltip': 'Exécuter le prompt',
'promptset.share_tooltip': 'Partager le prompt',
'promptset.delete_tooltip': 'Supprimer le prompt',
'promptset.confirm_delete': 'Cliquez à nouveau pour confirmer',
'promptset.deleting': 'Suppression...',
'promptset.confirm_click': 'Cliquez pour confirmer',
'promptset.delete_error': 'Erreur lors de la suppression',
'promptset.deleting_message': 'Suppression du prompt...',
// Prompt Modal
'modal.create_prompt': 'Créer un nouveau prompt',
'modal.name_required': 'Le nom est requis',
'modal.content_required': 'Le contenu est requis',
'modal.create_error': 'Erreur lors de la création du prompt',
'modal.name_label': 'Nom',
'modal.content_label': 'Contenu',
'modal.name_placeholder': 'Entrez un nom pour le prompt',
'modal.content_placeholder': 'Entrez le contenu du prompt',
'modal.cancel': 'Annuler',
'modal.creating': 'Création...',
'modal.create': 'Créer le prompt',
// Share Modal
'share_modal.title': 'Partager le prompt',
'share_modal.select_users': 'Sélectionner les utilisateurs',
'share_modal.select_all': 'Tout sélectionner',
'share_modal.deselect_all': 'Tout désélectionner',
'share_modal.loading_users': 'Chargement des utilisateurs...',
'share_modal.error_loading_users': 'Erreur lors du chargement des utilisateurs',
'share_modal.no_users_available': 'Aucun utilisateur disponible',
'share_modal.no_users_selected': 'Veuillez sélectionner au moins un utilisateur',
'share_modal.one_user_selected': '1 utilisateur sélectionné',
'share_modal.multiple_users_selected': '{count} utilisateurs sélectionnés',
'share_modal.custom_title': 'Titre personnalisé (facultatif)',
'share_modal.title_placeholder': 'Entrez un titre personnalisé',
'share_modal.message': 'Message (facultatif)',
'share_modal.message_placeholder': 'Ajoutez un message pour les destinataires',
'share_modal.share': 'Partager',
'share_modal.sharing': 'Partage en cours...',
'share_modal.share_error': 'Erreur lors du partage du prompt',
// Prompt Settings
'prompt_settings.title': 'Paramètres de prompt',
'prompt_settings.content_placeholder': 'Le contenu des paramètres sera ajouté dans les futures mises à jour.',
// Chat Area
'chat.continue_conversation': 'Continuer la conversation...',
'chat.enter_message': 'Entrez votre message...',
'chat.remove_file': 'Supprimer le fichier',
'chat.attach_file': 'Joindre un fichier',
'chat.you': 'Vous',
'chat.click_to_open': 'Cliquez pour ouvrir',
'chat.preview_document': 'Aperçu du document',
'chat.download_document': 'Télécharger le document',
'chat.workflow_failed': 'Échec du workflow.',
'chat.retry_workflow': 'Réessayer',
'chat.sending_followup': 'Envoi du message de suivi...',
'chat.sending_message': 'Envoi du message...',
'chat.error_prefix': 'Erreur:',
'chat.error_loading_messages': 'Erreur lors du chargement des messages:',
'chat.loading_workflow_messages': 'Chargement des messages de workflow...',
'chat.start_conversation': 'Commencez une conversation en entrant un message, en sélectionnant un modèle ou en continuant un workflow précédent...',
// File Preview
'file_preview.loading': 'Chargement de l\'aperçu...',
'file_preview.error': 'Erreur',
'file_preview.no_preview': 'Aucun aperçu disponible',
'file_preview.close_preview': 'Fermer l\'aperçu',
'file_preview.python': 'Python',
// Chat History
'chat_history.loading': 'Chargement des workflows...',
'chat_history.error_loading': 'Erreur lors du chargement des workflows:',
'chat_history.try_again': 'Réessayer',
'chat_history.title': 'Historique des workflows',
'chat_history.workflow_count': 'Workflow',
'chat_history.workflow_count_plural': 'Workflows',
'chat_history.empty_state': 'Aucun workflow disponible',
'chat_history.confirm_delete': 'Êtes-vous sûr de vouloir supprimer le workflow "{id}..."?',
'chat_history.no_message_content': 'Aucun contenu de message disponible',
'chat_history.unknown_date': 'Date inconnue',
'chat_history.invalid_date': 'Date invalide',
'chat_history.started': 'Démarré:',
'chat_history.last_activity': 'Dernière activité:',
'chat_history.round': 'Tour',
'chat_history.resume_tooltip': 'Reprendre le workflow',
'chat_history.delete_tooltip': 'Supprimer le workflow',
'chat_history.deleting': 'Suppression du workflow...',
// Workflow Status
'status.error': 'ERREUR',
'status.failed': 'ÉCHEC',
'status.stopped': 'ARRÊTÉ',
'status.cancelled': 'ANNULÉ',
'status.running': 'EN COURS',
'status.processing': 'TRAITEMENT',
'status.completed': 'TERMINÉ',
'status.pending': 'EN ATTENTE',
// Files
'files.unknown_size': 'Taille inconnue',
'files.unknown_date': 'Date inconnue',
'files.source.uploaded': 'Téléchargé',
'files.source.ai_created': 'Créé par IA',
'files.source.shared': 'Partagé',
'files.source.unknown': 'Inconnu',
'files.preview_tooltip': 'Aperçu du fichier',
'files.download_tooltip': 'Télécharger le fichier',
'files.delete_tooltip': 'Supprimer le fichier',
'files.delete_confirm_tooltip': 'Cliquez à nouveau pour confirmer la suppression',
'files.downloading': 'Téléchargement...',
'files.deleting': 'Suppression...',
'files.delete_confirm': 'Cliquez pour confirmer...',
'files.no_files': 'Aucun fichier trouvé.',
'files.no_shared_files': 'Aucun fichier partagé trouvé.',
'files.no_ai_files': 'Aucun fichier créé par IA trouvé.',
'files.no_uploaded_files': 'Aucun fichier téléchargé trouvé.',
'files.header.name': 'Nom',
'files.header.type': 'Type',
'files.header.size': 'Taille',
'files.header.date': 'Date',
'files.selector.title': 'Sélectionner des fichiers',
'files.selector.tab.all': 'Tous les fichiers',
'files.selector.tab.uploads': 'Téléchargés',
'files.selector.tab.created': 'Créés par IA',
'files.selector.tab.shared': 'Partagés',
'files.selector.select_all': 'Tout sélectionner',
'files.selector.deselect_all': 'Tout désélectionner',
'files.selector.file_selected': 'Fichier',
'files.selector.files_selected': 'Fichiers',
'files.selector.selected_suffix': 'sélectionné(s)',
'files.selector.upload_new': 'Télécharger un nouveau fichier',
'files.selector.loading': 'Chargement des fichiers...',
'files.selector.error_loading': 'Erreur lors du chargement des fichiers:',
'files.upload.title': 'Télécharger un fichier',
'files.upload.drop_here': 'Déposer le fichier ici...',
'files.upload.uploading': 'Téléchargement...',
'files.upload.drag_files': 'Glisser les fichiers ici',
'files.upload.or': 'ou',
'files.upload.browse': 'Parcourir',
'files.upload.selected_file': 'Fichier sélectionné:',
'files.upload.upload_button': 'Télécharger',
'files.upload.uploading_button': 'Téléchargement...',
'files.upload.success': 'Fichier téléchargé avec succès!',
'files.upload.error': 'Une erreur s\'est produite lors du téléchargement.',
'files.upload.unexpected_error': 'Une erreur inattendue s\'est produite lors du téléchargement.',
// Files Page
'files.page.tab.all': 'Tous les fichiers',
'files.page.tab.uploads': 'Mes téléchargements',
'files.page.tab.created': 'Fichiers créés',
'files.page.tab.shared': 'Fichiers partagés',
'files.page.add_file': 'Ajouter un fichier',
'files.page.loading': 'Chargement des fichiers...',
'files.page.error': 'Erreur:',
}
};

View file

@ -1,91 +0,0 @@
import { useActor } from '@xstate/react';
import { useLocation } from 'react-router-dom';
import { useEffect } from 'react';
import { sidebarMachine, getSidebarState } from '../../machines/sidebarMachine';
// Custom hook that provides all sidebar functionality
export const useSidebarMachine = () => {
// Step 1: Create the machine actor (this is like useState but for state machines)
const [state, send] = useActor(sidebarMachine);
// Step 2: Get the current route from React Router
const location = useLocation();
// Step 3: Update the machine when the route changes
useEffect(() => {
send({
type: 'NAVIGATE',
path: location.pathname,
});
}, [location.pathname, send]);
// Step 4: Get derived state using our helper function
const sidebarState = getSidebarState(state.context);
// Step 5: Create easy-to-use action functions
const actions = {
// Toggle a specific menu item
toggleItem: (itemId: string) => {
send({
type: 'TOGGLE_ITEM',
itemId,
});
},
// Close all submenus
closeAll: () => {
send({
type: 'CLOSE_ALL',
});
},
// Check if a specific item is open
isItemOpen: (itemId: string) => {
return sidebarState.isItemOpen(itemId);
},
// Check if an item is the active route
isItemActive: (itemPath?: string) => {
if (!itemPath) return false;
return location.pathname === itemPath;
},
// Minimize/expand sidebar
minimizeSidebar: () => {
send({
type: 'MINIMIZE_SIDEBAR',
});
},
expandSidebar: () => {
send({
type: 'EXPAND_SIDEBAR',
});
},
};
// Step 6: Return everything the components need
return {
// State queries (read-only)
hasOpenSubmenu: sidebarState.hasOpenSubmenu,
openItemId: sidebarState.openItemId,
activePath: sidebarState.activePath,
isMinimized: sidebarState.isMinimized,
currentState: state.value, // For debugging: 'collapsed' or 'expanded'
// Actions (functions to call)
toggleItem: actions.toggleItem,
closeAll: actions.closeAll,
isItemOpen: actions.isItemOpen,
isItemActive: actions.isItemActive,
minimizeSidebar: actions.minimizeSidebar,
expandSidebar: actions.expandSidebar,
// Raw state machine state (for debugging)
_debugState: state,
_debugSend: send,
};
};
// Step 7: Type export for components that need it
export type UseSidebarMachine = ReturnType<typeof useSidebarMachine>;

View file

@ -1,4 +1,4 @@
import { useState } from 'react';
import { useState, useCallback } from 'react';
import api from '../api';
// Generic API error handling
@ -26,7 +26,7 @@ export function useApiRequest<RequestData = any, ResponseData = any>() {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const request = async ({
const request = useCallback(async ({
url,
method,
data,
@ -52,7 +52,7 @@ export function useApiRequest<RequestData = any, ResponseData = any>() {
} finally {
setIsLoading(false);
}
};
}, []);
return {
request,

View file

@ -30,23 +30,43 @@ export function useAuth() {
params.append('client_id', '');
params.append('client_secret', '');
// Create a custom axios instance for this request
const instance = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL,
withCredentials: true,
// Generate a simple CSRF token (in production, this should come from the server)
const csrfToken = Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
// Use the existing api instance with custom headers for this request
const response = await api.post('/api/local/login', params, {
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
'Content-Type': 'application/x-www-form-urlencoded',
'X-CSRF-Token': csrfToken
}
});
const response = await instance.post('/api/token', params);
// Store the entire auth response
if (response.data.accessToken) {
localStorage.setItem('auth_data', JSON.stringify(response.data));
// Normalize the response structure to match what the frontend expects
let normalizedAuthData;
if (response.data.token_data) {
// Backend returns token_data with tokenAccess field, normalize to accessToken
normalizedAuthData = {
accessToken: response.data.token_data.tokenAccess || response.data.access_token,
tokenType: response.data.token_data.tokenType || 'bearer',
userId: response.data.token_data.userId,
expiresAt: response.data.token_data.expiresAt,
createdAt: response.data.token_data.createdAt
};
} else {
// Fallback to old structure if needed
normalizedAuthData = {
accessToken: response.data.access_token,
tokenType: response.data.token_type || 'bearer'
};
}
return response.data;
// Store the normalized auth response
localStorage.setItem('auth_data', JSON.stringify(normalizedAuthData));
return {
accessToken: normalizedAuthData.accessToken,
tokenType: normalizedAuthData.tokenType
};
} catch (error: any) {
let errorMessage = 'An error occurred during login';
@ -85,8 +105,6 @@ interface MsalAuthResponse {
}
export function useMsalAuth() {
const { instance, accounts } = useMsal();
const { request, isLoading, error } = useApiRequest<null, MsalAuthResponse>();
const [msalError, setMsalError] = useState<string | null>(null);
const [isMsalLoading, setIsMsalLoading] = useState(false);
@ -95,75 +113,93 @@ export function useMsalAuth() {
setMsalError(null);
try {
let msalToken;
return new Promise((resolve, reject) => {
// Open popup to backend Microsoft login route
const popup = window.open(
`${import.meta.env.VITE_API_BASE_URL}/api/msft/login?state=login`,
'msft-login',
'width=500,height=600,scrollbars=yes,resizable=yes'
);
// If we have an account, try to get the token silently
if (accounts.length > 0) {
const silentRequest = {
scopes: ['user.read'],
account: accounts[0]
if (!popup) {
setMsalError('Popup was blocked. Please allow popups and try again.');
setIsMsalLoading(false);
reject(new Error('Popup was blocked'));
return;
}
// Listen for messages from the popup
const messageListener = (event: MessageEvent) => {
// Verify origin for security
const apiUrl = new URL(import.meta.env.VITE_API_BASE_URL);
if (event.origin !== apiUrl.origin) {
return;
}
if (event.data.type === 'msft_auth_success') {
// Store the auth data with normalized field names
if (event.data.token_data) {
const normalizedTokenData = {
accessToken: event.data.token_data.tokenAccess, // Convert tokenAccess to accessToken
tokenType: event.data.token_data.tokenType,
userId: event.data.token_data.userId,
expiresAt: event.data.token_data.expiresAt,
createdAt: event.data.token_data.createdAt
};
localStorage.setItem('auth_data', JSON.stringify(normalizedTokenData));
}
// Clean up
window.removeEventListener('message', messageListener);
popup.close();
setIsMsalLoading(false);
// Resolve with the token data
resolve({
accessToken: event.data.token_data.tokenAccess,
tokenType: event.data.token_data.tokenType || 'bearer',
user: {
username: '', // Will be populated by the backend
email: '',
fullName: '',
mandateId: 0
}
});
} else if (event.data.type === 'msft_connection_error') {
// Handle error
window.removeEventListener('message', messageListener);
popup.close();
setIsMsalLoading(false);
setMsalError(event.data.error || 'Microsoft authentication failed');
reject(new Error(event.data.error || 'Microsoft authentication failed'));
}
};
try {
const response = await instance.acquireTokenSilent(silentRequest);
msalToken = response.accessToken;
} catch (e) {
// If silent token acquisition fails, fall back to popup
const response = await instance.acquireTokenPopup(silentRequest);
msalToken = response.accessToken;
}
} else {
// No account, do popup login
const response = await instance.loginPopup({
scopes: ['user.read']
});
// Add message listener
window.addEventListener('message', messageListener);
if (response.account) {
const tokenResponse = await instance.acquireTokenSilent({
scopes: ['user.read'],
account: response.account
});
msalToken = tokenResponse.accessToken;
} else {
throw new Error('Failed to get account after login');
}
}
// Exchange MSAL token for backend token
const response = await api.post('/api/msft/token', null, {
headers: {
'Authorization': `Bearer ${msalToken}`
}
});
// Store the backend token
if (response.data.accessToken) {
localStorage.setItem('auth_data', JSON.stringify(response.data));
}
return response.data;
} catch (error: any) {
let errorMessage = 'MSAL Login fehlgeschlagen';
if (error.response) {
errorMessage = error.response.data?.detail || error.response.data?.message || errorMessage;
} else if (error.request) {
errorMessage = 'Keine Antwort vom Server erhalten';
} else {
errorMessage = error.message || errorMessage;
}
setMsalError(errorMessage);
throw new Error(errorMessage);
} finally {
// Handle popup closing without completing auth
const checkClosed = setInterval(() => {
if (popup.closed) {
clearInterval(checkClosed);
window.removeEventListener('message', messageListener);
setIsMsalLoading(false);
setMsalError('Authentication was cancelled');
reject(new Error('Authentication was cancelled'));
}
}, 1000);
});
} catch (error: any) {
setMsalError(error.message || 'Microsoft authentication failed');
setIsMsalLoading(false);
throw error;
}
};
return {
loginWithMsal,
error: msalError || error,
isLoading: isMsalLoading || isLoading
error: msalError,
isLoading: isMsalLoading
};
}
@ -174,47 +210,83 @@ interface RegisterData {
email: string;
fullName: string;
language?: string;
enabled?: boolean;
privilege?: string;
}
interface RegisterResponse {
success: boolean;
message?: string;
user?: {
id: string;
username: string;
email: string;
fullName: string;
language: string;
enabled: boolean;
privilege: string;
};
}
export function useRegister() {
const { request, isLoading, error } = useApiRequest<RegisterData, any>();
const [error, setError] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(false);
const register = async (userData: RegisterData): Promise<RegisterResponse> => {
setIsLoading(true);
setError(null);
try {
// Add default language if not provided
// Prepare data to match backend expectations
// Backend expects userData as object and password as embedded field
const dataToSend = {
...userData,
language: userData.language || 'de'
userData: {
username: userData.username,
email: userData.email,
fullName: userData.fullName,
language: userData.language || 'de',
enabled: userData.enabled !== undefined ? userData.enabled : true,
privilege: userData.privilege || 'user'
},
password: userData.password
};
const response = await request({
url: '/api/users/register',
method: 'post',
data: dataToSend,
additionalConfig: {
const response = await api.post('/api/local/register', dataToSend, {
headers: {
'Content-Type': 'application/json'
}
}
});
return {
success: true,
message: 'Registration successful',
user: response
user: response.data
};
} catch (error: any) {
let errorMessage = 'An error occurred during registration';
if (error.response) {
// Handle validation errors from FastAPI
if (error.response.data?.detail) {
if (Array.isArray(error.response.data.detail)) {
// Handle FastAPI validation errors array
errorMessage = error.response.data.detail.map((err: any) => err.msg).join(', ');
} else {
errorMessage = error.response.data.detail;
}
} else {
errorMessage = 'Registration failed';
}
} else if (error.request) {
errorMessage = 'No response received from server';
} else {
errorMessage = error.message;
}
setError(errorMessage);
throw error;
} finally {
setIsLoading(false);
}
};
@ -262,7 +334,7 @@ export function useMsalRegister() {
// Register the user through our backend
const response = await request({
url: '/api/users/register-with-msal',
url: '/api/msft/register',
method: 'post',
data: userData,
additionalConfig: {
@ -288,3 +360,121 @@ export function useMsalRegister() {
isLoading
};
}
// Username availability check
export function useUsernameAvailability() {
const [isChecking, setIsChecking] = useState(false);
const [error, setError] = useState<string | null>(null);
const checkAvailability = async (username: string, authenticationAuthority: string = 'local'): Promise<{
username: string;
authenticationAuthority: string;
available: boolean;
message: string;
}> => {
setIsChecking(true);
setError(null);
try {
const response = await api.get('/api/local/available', {
params: {
username,
authenticationAuthority
}
});
return response.data;
} catch (error: any) {
let errorMessage = 'Failed to check username availability';
if (error.response) {
errorMessage = error.response.data?.detail || errorMessage;
}
setError(errorMessage);
throw error;
} finally {
setIsChecking(false);
}
};
return {
checkAvailability,
isChecking,
error
};
}
// Logout function
export function useLogout() {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const logout = async (): Promise<void> => {
setIsLoading(true);
setError(null);
try {
await api.post('/api/local/logout');
// Clear local storage
localStorage.removeItem('auth_data');
// Redirect to login page
window.location.href = '/login';
} catch (error: any) {
let errorMessage = 'Logout failed';
if (error.response) {
errorMessage = error.response.data?.detail || errorMessage;
}
setError(errorMessage);
throw error;
} finally {
setIsLoading(false);
}
};
return {
logout,
isLoading,
error
};
}
// Get current user
export function useCurrentUser() {
const [user, setUser] = useState<any>(null);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const getCurrentUser = async (): Promise<any> => {
setIsLoading(true);
setError(null);
try {
const response = await api.get('/api/local/me');
setUser(response.data);
return response.data;
} catch (error: any) {
let errorMessage = 'Failed to get current user';
if (error.response) {
errorMessage = error.response.data?.detail || errorMessage;
}
setError(errorMessage);
throw error;
} finally {
setIsLoading(false);
}
};
return {
user,
getCurrentUser,
isLoading,
error
};
}

336
src/hooks/useConnections.ts Normal file
View file

@ -0,0 +1,336 @@
import { useState, useEffect } from 'react';
import { useApiRequest } from './useApi';
// Connection interfaces based on backend UserConnection model
export interface Connection {
id: string;
userId: string;
authority: 'local' | 'google' | 'msft';
externalId: string;
externalUsername: string;
externalEmail?: string;
status: 'active' | 'expired' | 'revoked' | 'pending';
connectedAt: string;
lastChecked: string;
expiresAt?: string;
}
export interface CreateConnectionData {
id?: string;
userId?: string;
authority?: 'msft' | 'google'; // Keep for compatibility with existing code
type?: 'msft' | 'google'; // Backend expects this field
externalId?: string;
externalUsername?: string;
externalEmail?: string;
status?: 'active' | 'expired' | 'revoked' | 'pending';
connectedAt?: string;
lastChecked?: string;
expiresAt?: string;
}
export interface ConnectResponse {
authUrl: string;
}
// Hook for managing connections
export function useConnections() {
const [connections, setConnections] = useState<Connection[]>([]);
const { request, isLoading, error } = useApiRequest<any, any>();
// Fetch all connections
const fetchConnections = async (): Promise<Connection[]> => {
try {
const data = await request({
url: '/api/connections/',
method: 'get'
});
setConnections(data);
return data;
} catch (error) {
console.error('Error fetching connections:', error);
setConnections([]);
throw error;
}
};
// Create a new connection
const createConnection = async (connectionData: CreateConnectionData): Promise<Connection> => {
try {
const data = await request({
url: '/api/connections/',
method: 'post',
data: connectionData
});
// Update local state
setConnections(prev => {
const existing = prev.find(conn => conn.id === data.id);
if (existing) {
return prev.map(conn => conn.id === data.id ? data : conn);
} else {
return [...prev, data];
}
});
return data;
} catch (error) {
console.error('Error creating connection:', error);
throw error;
}
};
// Connect to a service (initiate OAuth)
const connectService = async (connectionId: string): Promise<ConnectResponse> => {
try {
const data = await request({
url: `/api/connections/${connectionId}/connect`,
method: 'post'
});
return data;
} catch (error) {
console.error('Error connecting service:', error);
throw error;
}
};
// Disconnect from a service
const disconnectService = async (connectionId: string): Promise<{ message: string }> => {
try {
const data = await request({
url: `/api/connections/${connectionId}/disconnect`,
method: 'post'
});
// Update local state
setConnections(prev =>
prev.map(conn =>
conn.id === connectionId
? { ...conn, status: 'inactive' as any, lastChecked: new Date().toISOString() }
: conn
)
);
return data;
} catch (error) {
console.error('Error disconnecting service:', error);
throw error;
}
};
// Delete a connection
const deleteConnection = async (connectionId: string): Promise<{ message: string }> => {
try {
const data = await request({
url: `/api/connections/${connectionId}`,
method: 'delete'
});
// Update local state
setConnections(prev => prev.filter(conn => conn.id !== connectionId));
return data;
} catch (error) {
console.error('Error deleting connection:', error);
throw error;
}
};
// Update a connection
const updateConnection = async (connectionId: string, updateData: Partial<Connection>): Promise<Connection> => {
try {
// Use PUT endpoint for updating connections
const data = await request({
url: `/api/connections/${connectionId}`,
method: 'put',
data: updateData
});
// Update local state
setConnections(prev =>
prev.map(conn => conn.id === connectionId ? { ...conn, ...data } : conn)
);
return data;
} catch (error) {
console.error('Error updating connection:', error);
throw error;
}
};
return {
connections,
fetchConnections,
createConnection,
updateConnection,
connectService,
disconnectService,
deleteConnection,
isLoading,
error
};
}
// Hook for OAuth connection popup flow (similar to useMsalAuth)
export function useOAuthConnect() {
const { connectService, fetchConnections } = useConnections();
const [isConnecting, setIsConnecting] = useState(false);
const [connectError, setConnectError] = useState<string | null>(null);
const connectWithPopup = async (connectionId: string): Promise<void> => {
setIsConnecting(true);
setConnectError(null);
try {
// Get the OAuth URL from backend
const response = await connectService(connectionId);
if (!response.authUrl) {
throw new Error('No OAuth URL received from backend');
}
console.log('OAuth URL from backend:', response.authUrl);
return new Promise((resolve, reject) => {
// Convert relative URL to absolute URL if needed
let authUrl = response.authUrl;
if (authUrl.startsWith('/')) {
authUrl = `${import.meta.env.VITE_API_BASE_URL}${authUrl}`;
}
// Open popup using the same pattern as useAuthentication.ts
const popup = window.open(
authUrl,
'oauth-connect',
'width=500,height=600,scrollbars=yes,resizable=yes'
);
if (!popup) {
setConnectError('Popup was blocked. Please allow popups and try again.');
setIsConnecting(false);
reject(new Error('Popup was blocked'));
return;
}
// Handle popup closing without completing auth
const checkClosed = setInterval(() => {
if (popup.closed) {
clearInterval(checkClosed);
window.removeEventListener('message', messageListener);
setIsConnecting(false);
console.log('OAuth popup closed');
// Refresh connections anyway in case it succeeded
fetchConnections();
setConnectError('Authentication was cancelled');
reject(new Error('Authentication was cancelled'));
}
}, 1000);
// Listen for messages from the popup (similar to useMsalAuth)
const messageListener = (event: MessageEvent) => {
// Verify origin for security
const apiUrl = new URL(import.meta.env.VITE_API_BASE_URL);
if (event.origin !== apiUrl.origin) {
return;
}
if (event.data.type === 'msft_connection_success' || event.data.type === 'google_connection_success') {
// Clean up - IMPORTANT: clear the checkClosed interval first
clearInterval(checkClosed);
window.removeEventListener('message', messageListener);
popup.close();
setIsConnecting(false);
console.log('OAuth connection successful');
// Refresh connections
fetchConnections();
resolve();
} else if (event.data.type === 'msft_connection_error' || event.data.type === 'google_connection_error') {
// Handle error - also clear the checkClosed interval
clearInterval(checkClosed);
window.removeEventListener('message', messageListener);
popup.close();
setIsConnecting(false);
setConnectError(event.data.error || 'OAuth connection failed');
reject(new Error(event.data.error || 'OAuth connection failed'));
}
};
// Add message listener
window.addEventListener('message', messageListener);
});
} catch (error: any) {
setConnectError(error.message || 'OAuth connection failed');
setIsConnecting(false);
throw error;
}
};
return {
connectWithPopup,
isConnecting,
error: connectError
};
}
// Hook for disconnecting services
export function useDisconnect() {
const { disconnectService, fetchConnections } = useConnections();
const [isDisconnecting, setIsDisconnecting] = useState(false);
const [disconnectError, setDisconnectError] = useState<string | null>(null);
const disconnect = async (connectionId: string): Promise<void> => {
setIsDisconnecting(true);
setDisconnectError(null);
try {
await disconnectService(connectionId);
console.log('Service disconnected successfully');
// Refresh connections to update the status
await fetchConnections();
} catch (error: any) {
setDisconnectError(error.message || 'Disconnect failed');
console.error('Error disconnecting service:', error);
throw error;
} finally {
setIsDisconnecting(false);
}
};
return {
disconnect,
isDisconnecting,
error: disconnectError
};
}
// Hook for individual connection operations
export function useConnection(connectionId?: string) {
const [connection, setConnection] = useState<Connection | null>(null);
const { request, isLoading, error } = useApiRequest<any, Connection[]>();
const fetchConnection = async (id: string = connectionId!): Promise<Connection | null> => {
if (!id) return null;
try {
// Since there's no individual connection endpoint, fetch all and filter
const data: Connection[] = await request({
url: '/api/connections/',
method: 'get'
});
const foundConnection = data.find((conn: Connection) => conn.id === id);
setConnection(foundConnection || null);
return foundConnection || null;
} catch (error) {
console.error('Error fetching connection:', error);
setConnection(null);
throw error;
}
};
return {
connection,
fetchConnection,
isLoading,
error
};
}

View file

@ -0,0 +1,277 @@
import { useState } from 'react';
import { useApiRequest } from './useApi';
// SharePoint interfaces
export interface SharePointConnection {
id: string;
name: string;
description: string;
status: string;
authority: string;
lastChecked: string | null;
expiresAt: string | null;
externalUsername: string;
externalEmail: string;
}
export interface SharePointDocument {
documentName: string;
documentData: any;
mimeType: string;
}
export interface SharePointResponse {
success: boolean;
data?: {
documents?: SharePointDocument[];
};
error?: string;
message: string;
}
export interface SharePointListRequest {
connectionReference: string;
siteUrl: string;
folderPaths: string[];
includeSubfolders?: boolean;
expectedDocumentFormats?: Array<{
extension: string;
mimeType: string;
description?: string;
}>;
}
export interface SharePointFindRequest {
connectionReference: string;
siteUrl: string;
query: string;
searchScope?: string;
expectedDocumentFormats?: Array<{
extension: string;
mimeType: string;
description?: string;
}>;
}
export interface SharePointReadRequest {
documentList: string;
connectionReference: string;
siteUrl: string;
documentPaths: string[];
includeMetadata?: boolean;
expectedDocumentFormats?: Array<{
extension: string;
mimeType: string;
description?: string;
}>;
}
export interface SharePointUploadRequest {
connectionReference: string;
siteUrl: string;
documentPaths: string[];
documentList: string;
fileNames: string[];
expectedDocumentFormats?: Array<{
extension: string;
mimeType: string;
description?: string;
}>;
}
// Hook for SharePoint testing operations
export function useSharePointTest() {
const { request, isLoading, error } = useApiRequest<any, any>();
const [lastResponse, setLastResponse] = useState<SharePointResponse | null>(null);
// Get user's Microsoft connections
const getConnections = async (): Promise<SharePointConnection[]> => {
try {
const response = await request({
url: '/api/test-sharepoint/connections',
method: 'get'
});
return response.data || [];
} catch (error) {
console.error('Error fetching SharePoint connections:', error);
throw error;
}
};
// Test a specific connection
const testConnection = async (connectionId: string): Promise<any> => {
try {
const response = await request({
url: `/api/test-sharepoint/test-connection/${connectionId}`,
method: 'get'
});
return response;
} catch (error) {
console.error('Error testing connection:', error);
throw error;
}
};
// List documents in SharePoint folders
const listDocuments = async (data: SharePointListRequest): Promise<SharePointResponse> => {
try {
const response = await request({
url: '/api/test-sharepoint/list-documents',
method: 'post',
data
});
setLastResponse(response);
return response;
} catch (error) {
console.error('Error listing SharePoint documents:', error);
const errorResponse: SharePointResponse = {
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
message: 'Failed to list documents'
};
setLastResponse(errorResponse);
throw error;
}
};
// Find documents by query
const findDocuments = async (data: SharePointFindRequest): Promise<SharePointResponse> => {
try {
const response = await request({
url: '/api/test-sharepoint/find-documents',
method: 'post',
data
});
setLastResponse(response);
return response;
} catch (error) {
console.error('Error finding SharePoint documents:', error);
const errorResponse: SharePointResponse = {
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
message: 'Failed to find documents'
};
setLastResponse(errorResponse);
throw error;
}
};
// Read documents from SharePoint
const readDocuments = async (data: SharePointReadRequest): Promise<SharePointResponse> => {
try {
const response = await request({
url: '/api/test-sharepoint/read-documents',
method: 'post',
data
});
setLastResponse(response);
return response;
} catch (error) {
console.error('Error reading SharePoint documents:', error);
const errorResponse: SharePointResponse = {
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
message: 'Failed to read documents'
};
setLastResponse(errorResponse);
throw error;
}
};
// Upload documents to SharePoint
const uploadDocuments = async (data: SharePointUploadRequest): Promise<SharePointResponse> => {
try {
const response = await request({
url: '/api/test-sharepoint/upload-documents',
method: 'post',
data
});
setLastResponse(response);
return response;
} catch (error) {
console.error('Error uploading SharePoint documents:', error);
const errorResponse: SharePointResponse = {
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
message: 'Failed to upload documents'
};
setLastResponse(errorResponse);
throw error;
}
};
// Get example request bodies
const getExamples = async (): Promise<any> => {
try {
const response = await request({
url: '/api/test-sharepoint/example-requests',
method: 'get'
});
return response.examples || {};
} catch (error) {
console.error('Error getting example requests:', error);
throw error;
}
};
// Debug user's authentication tokens
const debugTokens = async (): Promise<any> => {
try {
const response = await request({
url: '/api/test-sharepoint/debug-tokens',
method: 'get'
});
return response;
} catch (error) {
console.error('Error debugging tokens:', error);
throw error;
}
};
// Debug detailed token information
const debugTokenDetails = async (): Promise<any> => {
try {
const response = await request({
url: '/api/test-sharepoint/debug-token-details',
method: 'get'
});
return response;
} catch (error) {
console.error('Error debugging token details:', error);
throw error;
}
};
// Discover SharePoint sites
const discoverSites = async (): Promise<any> => {
try {
const response = await request({
url: '/api/test-sharepoint/discover-sites',
method: 'get'
});
return response;
} catch (error) {
console.error('Error discovering SharePoint sites:', error);
throw error;
}
};
return {
// API methods
getConnections,
testConnection,
listDocuments,
findDocuments,
readDocuments,
uploadDocuments,
getExamples,
debugTokens,
debugTokenDetails,
discoverSites,
// State
isLoading,
error,
lastResponse
};
}

View file

@ -3,12 +3,15 @@ import { useApiRequest } from './useApi';
// User interfaces
export interface User {
id: number;
id: string;
username: string;
email: string;
fullName: string;
language: string;
enabled: boolean;
privilege: string;
mandateId: number;
authenticationAuthority: string;
mandateId: string;
}
export type UserUpdateData = Partial<Omit<User, 'id' | 'mandateId'>>;
@ -21,7 +24,7 @@ export function useCurrentUser() {
const fetchCurrentUser = async () => {
try {
const data = await request({
url: '/api/user/me',
url: '/api/local/me',
method: 'get'
});
setUser(data);
@ -30,6 +33,54 @@ export function useCurrentUser() {
}
};
const logout = async (msalInstance?: any) => {
if (!user) {
throw new Error('No user to logout');
}
try {
let logoutEndpoint = '/api/local/logout';
// Determine the correct logout endpoint based on authentication authority
if (user.authenticationAuthority === 'msft') {
logoutEndpoint = '/api/msft/logout';
} else if (user.authenticationAuthority === 'local') {
logoutEndpoint = '/api/local/logout';
}
await request({
url: logoutEndpoint,
method: 'post'
});
// Clear user state after successful logout
setUser(null);
// Clear any local storage data
localStorage.clear();
// Handle MSAL logout for Microsoft authentication
if (user.authenticationAuthority === 'msft' && msalInstance) {
try {
await msalInstance.logoutRedirect({
onRedirectNavigate: () => true
});
return; // MSAL will handle the redirect
} catch (msalError) {
console.error('MSAL logout failed:', msalError);
// Continue with regular redirect if MSAL logout fails
}
}
// Redirect to login or home page
window.location.href = '/login';
} catch (error) {
console.error('Logout failed:', error);
throw error;
}
};
useEffect(() => {
fetchCurrentUser();
}, []);
@ -38,7 +89,8 @@ export function useCurrentUser() {
user,
error,
isLoading,
refetch: fetchCurrentUser
refetch: fetchCurrentUser,
logout
};
}

263
src/locales/de.ts Normal file
View file

@ -0,0 +1,263 @@
export default {
// Navigation
'nav.dashboard': 'Zentrale',
'nav.files': 'Dateien',
'nav.team': 'Team-Bereich',
'nav.connections': 'Verbindungen',
'nav.workflows': 'Workflows',
'nav.settings': 'Einstellungen',
// Settings page
'settings.title': 'Einstellungen',
'settings.appearance': 'Darstellung',
'settings.language': 'Sprache',
'settings.about': 'Über',
'settings.version': 'Version',
'settings.theme': 'Theme',
'settings.theme.description': 'Wechseln Sie zwischen hellem und dunklem Modus',
'settings.language.description': 'Wählen Sie Ihre bevorzugte Sprache',
'settings.theme.light': 'Hell',
'settings.theme.dark': 'Dunkel',
'settings.theme.toggle.light': 'Zu hellem Modus wechseln',
'settings.theme.toggle.dark': 'Zu dunklem Modus wechseln',
// Languages
'language.german': 'Deutsch',
'language.english': 'English',
'language.french': 'Français',
// Common
'common.loading': 'Laden...',
'common.error': 'Fehler',
'common.success': 'Erfolgreich',
'common.cancel': 'Abbrechen',
'common.save': 'Speichern',
'common.delete': 'Löschen',
'common.edit': 'Bearbeiten',
'common.close': 'Schließen',
// Auth
'auth.login': 'Anmelden',
'auth.register': 'Registrieren',
'auth.logout': 'Abmelden',
'auth.email': 'E-Mail',
'auth.password': 'Passwort',
// Dashboard
'dashboard.prompt.template': 'Prompt Vorlage',
'dashboard.prompt.settings': 'Einstellungen',
'dashboard.chat.area': 'Chatbereich',
'dashboard.chat.history': 'Workflow-Verlauf',
'dashboard.log.title': 'Log',
'dashboard.log.workflow': 'Workflow',
'dashboard.log.no_workflow': 'Kein Workflow ausgewählt',
'dashboard.log.loading': 'Logs werden geladen...',
'dashboard.log.error': 'Fehler beim Laden der Logs',
'dashboard.log.no_logs': 'Keine Logs für diesen Workflow verfügbar',
'dashboard.log.waiting': 'Workflow läuft... Warte auf Logs...',
'dashboard.log.fetch_failed': 'Logs konnten nicht geladen werden',
'dashboard.log.level.info': 'INFO',
// Prompt Set
'promptset.loading': 'Prompts werden geladen...',
'promptset.error.loading': 'Fehler beim Laden der Prompts',
'promptset.retry': 'Erneut versuchen',
'promptset.new_prompt': 'Neuer Prompt',
'promptset.prompt_count': 'Prompt',
'promptset.prompt_count_plural': 'Prompts',
'promptset.no_prompts': 'Keine Prompts verfügbar',
'promptset.created': 'Erstellt',
'promptset.run_tooltip': 'Prompt ausführen',
'promptset.share_tooltip': 'Prompt teilen',
'promptset.delete_tooltip': 'Prompt löschen',
'promptset.confirm_delete': 'Klicken Sie erneut zum Bestätigen',
'promptset.deleting': 'Löschen...',
'promptset.confirm_click': 'Zum Bestätigen klicken',
'promptset.delete_error': 'Fehler beim Löschen',
'promptset.deleting_message': 'Prompt wird gelöscht...',
// Connections
'connections.title': 'Verbindungen',
'connections.connect_google': 'Google verbinden',
'connections.connect_microsoft': 'Microsoft verbinden',
'connections.edit_connection_title': '{authority} Verbindung bearbeiten',
'connections.update_connection': 'Verbindung aktualisieren',
'connections.service_connections': 'Service-Verbindungen',
'connections.error': 'Fehler',
'connections.connection_error': 'Verbindungsfehler',
'connections.disconnect_error': 'Trennungsfehler',
'connections.unknown': 'Unbekannt',
'connections.not_available': 'Nicht verfügbar',
'connections.invalid_date': 'Ungültiges Datum',
'connections.confirm_delete': 'Sind Sie sicher, dass Sie die {service} Verbindung löschen möchten?',
// Connection Fields
'connections.field.service': 'Service',
'connections.field.status': 'Status',
'connections.field.external_username': 'Externer Benutzername',
'connections.field.external_email': 'Externe E-Mail',
'connections.field.connected_at': 'Verbunden am',
'connections.field.last_checked': 'Zuletzt geprüft',
'connections.field.expires_at': 'Läuft ab am',
// Connection Services
'connections.service.google': 'Google',
'connections.service.microsoft': 'Microsoft',
'connections.service.local': 'Lokal',
// Connection Placeholders
'connections.placeholder.external_username': 'Externen Benutzernamen eingeben',
'connections.placeholder.external_email': 'Externe E-Mail-Adresse eingeben',
// Connection Actions
'connections.action.edit': 'Bearbeiten',
'connections.action.toggle_connection': 'Verbindung umschalten',
'connections.action.delete': 'Löschen',
// Prompt Modal
'modal.create_prompt': 'Neuen Prompt erstellen',
'modal.name_required': 'Name ist erforderlich',
'modal.content_required': 'Inhalt ist erforderlich',
'modal.create_error': 'Fehler beim Erstellen des Prompts',
'modal.name_label': 'Name',
'modal.content_label': 'Inhalt',
'modal.name_placeholder': 'Geben Sie einen Namen für den Prompt ein',
'modal.content_placeholder': 'Geben Sie den Inhalt des Prompts ein',
'modal.cancel': 'Abbrechen',
'modal.creating': 'Erstellen...',
'modal.create': 'Prompt erstellen',
// Share Modal
'share_modal.title': 'Prompt teilen',
'share_modal.select_users': 'Benutzer auswählen',
'share_modal.select_all': 'Alle auswählen',
'share_modal.deselect_all': 'Alle abwählen',
'share_modal.loading_users': 'Benutzer werden geladen...',
'share_modal.error_loading_users': 'Fehler beim Laden der Benutzer',
'share_modal.no_users_available': 'Keine Benutzer verfügbar',
'share_modal.no_users_selected': 'Bitte wählen Sie mindestens einen Benutzer aus',
'share_modal.one_user_selected': '1 Benutzer ausgewählt',
'share_modal.multiple_users_selected': '{count} Benutzer ausgewählt',
'share_modal.custom_title': 'Benutzerdefinierter Titel (optional)',
'share_modal.title_placeholder': 'Geben Sie einen benutzerdefinierten Titel ein',
'share_modal.message': 'Nachricht (optional)',
'share_modal.message_placeholder': 'Fügen Sie eine Nachricht für die Empfänger hinzu',
'share_modal.share': 'Teilen',
'share_modal.sharing': 'Wird geteilt...',
'share_modal.share_error': 'Fehler beim Teilen des Prompts',
// Prompt Settings
'prompt_settings.title': 'Prompt Einstellungen',
'prompt_settings.content_placeholder': 'Einstellungen werden in zukünftigen Updates hinzugefügt.',
// Chat Area
'chat.continue_conversation': 'Gespräch fortsetzen...',
'chat.enter_message': 'Nachricht eingeben...',
'chat.remove_file': 'Datei entfernen',
'chat.attach_file': 'Datei anhängen',
'chat.you': 'You',
'chat.click_to_open': 'Klicken Sie, um zu öffnen',
'chat.preview_document': 'Dokument vorschauen',
'chat.download_document': 'Dokument herunterladen',
'chat.workflow_failed': 'Workflow fehlgeschlagen.',
'chat.retry_workflow': 'Nochmal versuchen',
'chat.sending_followup': 'Folgenachricht wird gesendet...',
'chat.sending_message': 'Nachricht wird gesendet...',
'chat.error_prefix': 'Fehler:',
'chat.error_loading_messages': 'Fehler beim Laden der Nachrichten:',
'chat.loading_workflow_messages': 'Workflow-Nachrichten werden geladen...',
'chat.start_conversation': 'Beginne ein Gespräch, indem du eine Nachricht eingibst, eine Vorlage auswählst oder einen vorherigen Workflow fortsetzt …',
// File Preview
'file_preview.loading': 'Vorschau wird geladen...',
'file_preview.error': 'Fehler',
'file_preview.no_preview': 'Keine Vorschau verfügbar',
'file_preview.close_preview': 'Vorschau schließen',
'file_preview.python': 'Python',
// Chat History
'chat_history.loading': 'Workflows werden geladen...',
'chat_history.error_loading': 'Fehler beim Laden der Workflows:',
'chat_history.try_again': 'Nochmal versuchen',
'chat_history.title': 'Workflow-Verlauf',
'chat_history.workflow_count': 'Workflow',
'chat_history.workflow_count_plural': 'Workflows',
'chat_history.empty_state': 'Keine Workflows verfügbar',
'chat_history.confirm_delete': 'Sind Sie sicher, dass Sie Workflow "{id}..." löschen möchten?',
'chat_history.no_message_content': 'Kein Nachrichteninhalt verfügbar',
'chat_history.unknown_date': 'Unbekanntes Datum',
'chat_history.invalid_date': 'Ungültiges Datum',
'chat_history.started': 'Gestartet:',
'chat_history.last_activity': 'Letzte Aktivität:',
'chat_history.round': 'Runde',
'chat_history.resume_tooltip': 'Workflow fortsetzen',
'chat_history.delete_tooltip': 'Workflow löschen',
'chat_history.deleting': 'Workflow wird gelöscht...',
// Workflow Status
'status.error': 'FEHLER',
'status.failed': 'FEHLGESCHLAGEN',
'status.stopped': 'GESTOPPT',
'status.cancelled': 'ABGEBROCHEN',
'status.running': 'LÄUFT',
'status.processing': 'VERARBEITUNG',
'status.completed': 'ABGESCHLOSSEN',
'status.pending': 'WARTEND',
// Files
'files.unknown_size': 'Unbekannte Größe',
'files.unknown_date': 'Unbekanntes Datum',
'files.source.uploaded': 'Hochgeladen',
'files.source.ai_created': 'KI-erstellt',
'files.source.shared': 'Geteilt',
'files.source.unknown': 'Unbekannt',
'files.preview_tooltip': 'Datei vorschauen',
'files.download_tooltip': 'Datei herunterladen',
'files.delete_tooltip': 'Datei löschen',
'files.delete_confirm_tooltip': 'Klicken Sie erneut zum Bestätigen der Löschung',
'files.downloading': 'Laden...',
'files.deleting': 'Löschen...',
'files.delete_confirm': 'Zum Bestätigen klicken...',
'files.no_files': 'Keine Dateien gefunden.',
'files.no_shared_files': 'Keine mit Ihnen geteilten Dateien gefunden.',
'files.no_ai_files': 'Keine von der KI erstellten Dateien gefunden.',
'files.no_uploaded_files': 'Keine hochgeladenen Dateien gefunden.',
'files.header.name': 'Name',
'files.header.type': 'Typ',
'files.header.size': 'Größe',
'files.header.date': 'Datum',
'files.selector.title': 'Dateien auswählen',
'files.selector.tab.all': 'Alle Dateien',
'files.selector.tab.uploads': 'Hochgeladen',
'files.selector.tab.created': 'KI-erstellt',
'files.selector.tab.shared': 'Geteilt',
'files.selector.select_all': 'Alle auswählen',
'files.selector.deselect_all': 'Alle abwählen',
'files.selector.file_selected': 'Datei',
'files.selector.files_selected': 'Dateien',
'files.selector.selected_suffix': 'ausgewählt',
'files.selector.upload_new': 'Neue Datei hochladen',
'files.selector.loading': 'Dateien werden geladen...',
'files.selector.error_loading': 'Fehler beim Laden der Dateien:',
'files.upload.title': 'Datei hochladen',
'files.upload.drop_here': 'Datei hier ablegen...',
'files.upload.uploading': 'Lädt hoch...',
'files.upload.drag_files': 'Dateien hierher ziehen',
'files.upload.or': 'oder',
'files.upload.browse': 'Durchsuchen',
'files.upload.selected_file': 'Ausgewählte Datei:',
'files.upload.upload_button': 'Hochladen',
'files.upload.uploading_button': 'Wird hochgeladen...',
'files.upload.success': 'Datei erfolgreich hochgeladen!',
'files.upload.error': 'Beim Hochladen ist ein Fehler aufgetreten.',
'files.upload.unexpected_error': 'Beim Hochladen ist ein unerwarteter Fehler aufgetreten.',
// Files Page
'files.page.tab.all': 'Alle Dateien',
'files.page.tab.uploads': 'Meine Uploads',
'files.page.tab.created': 'Erstellte Dateien',
'files.page.tab.shared': 'Geteilte Dateien',
'files.page.add_file': 'Datei hinzufügen',
'files.page.loading': 'Dateien werden geladen...',
'files.page.error': 'Fehler:',
};

264
src/locales/en.ts Normal file
View file

@ -0,0 +1,264 @@
export default {
// Navigation
'nav.dashboard': 'Dashboard',
'nav.files': 'Files',
'nav.team': 'Team Area',
'nav.workflows': 'Workflows',
'nav.connections': 'Connections',
'nav.settings': 'Settings',
// Settings page
'settings.title': 'Settings',
'settings.appearance': 'Appearance',
'settings.language': 'Language',
'settings.about': 'About',
'settings.version': 'Version',
'settings.theme': 'Theme',
'settings.theme.description': 'Switch between light and dark mode',
'settings.language.description': 'Choose your preferred language',
'settings.theme.light': 'Light',
'settings.theme.dark': 'Dark',
'settings.theme.toggle.light': 'Switch to light mode',
'settings.theme.toggle.dark': 'Switch to dark mode',
// Languages
'language.german': 'Deutsch',
'language.english': 'English',
'language.french': 'Français',
// Common
'common.loading': 'Loading...',
'common.error': 'Error',
'common.success': 'Success',
'common.cancel': 'Cancel',
'common.save': 'Save',
'common.delete': 'Delete',
'common.edit': 'Edit',
'common.close': 'Close',
// Auth
'auth.login': 'Login',
'auth.register': 'Register',
'auth.logout': 'Logout',
'auth.email': 'Email',
'auth.password': 'Password',
// Dashboard
'dashboard.prompt.template': 'Prompt Template',
'dashboard.prompt.settings': 'Settings',
'dashboard.chat.area': 'Chat Area',
'dashboard.chat.history': 'Workflow History',
'dashboard.log.title': 'Log',
'dashboard.log.workflow': 'Workflow',
'dashboard.log.no_workflow': 'No workflow selected',
'dashboard.log.loading': 'Loading logs...',
'dashboard.log.error': 'Error loading logs',
'dashboard.log.no_logs': 'No logs available for this workflow',
'dashboard.log.waiting': 'Workflow running... Waiting for logs...',
'dashboard.log.fetch_failed': 'Failed to fetch logs',
'dashboard.log.level.info': 'INFO',
// Prompt Set
'promptset.loading': 'Loading prompts...',
'promptset.error.loading': 'Error loading prompts',
'promptset.retry': 'Try again',
'promptset.new_prompt': 'New Prompt',
'promptset.prompt_count': 'Prompt',
'promptset.prompt_count_plural': 'Prompts',
'promptset.no_prompts': 'No prompts available',
'promptset.created': 'Created',
'promptset.run_tooltip': 'Run prompt',
'promptset.share_tooltip': 'Share prompt',
'promptset.delete_tooltip': 'Delete prompt',
'promptset.confirm_delete': 'Click again to confirm',
'promptset.deleting': 'Deleting...',
'promptset.confirm_click': 'Click to confirm',
'promptset.delete_error': 'Error deleting',
'promptset.deleting_message': 'Deleting prompt...',
// Connections
'connections.title': 'Connections',
'connections.connect_google': 'Connect Google',
'connections.connect_microsoft': 'Connect Microsoft',
'connections.edit_connection_title': 'Edit {authority} Connection',
'connections.update_connection': 'Update Connection',
'connections.service_connections': 'Service Connections',
'connections.error': 'Error',
'connections.connection_error': 'Connection Error',
'connections.disconnect_error': 'Disconnect Error',
'connections.unknown': 'Unknown',
'connections.not_available': 'N/A',
'connections.invalid_date': 'Invalid Date',
'connections.confirm_delete': 'Are you sure you want to delete the {service} connection?',
// Connection Fields
'connections.field.service': 'Service',
'connections.field.status': 'Status',
'connections.field.external_username': 'External Username',
'connections.field.external_email': 'External Email',
'connections.field.connected_at': 'Connected At',
'connections.field.last_checked': 'Last Checked',
'connections.field.expires_at': 'Expires At',
// Connection Services
'connections.service.google': 'Google',
'connections.service.microsoft': 'Microsoft',
'connections.service.local': 'Local',
// Connection Placeholders
'connections.placeholder.external_username': 'Enter external username',
'connections.placeholder.external_email': 'Enter external email address',
// Connection Actions
'connections.action.edit': 'Edit',
'connections.action.toggle_connection': 'Toggle Connection',
'connections.action.delete': 'Delete',
// Prompt Modal
'modal.create_prompt': 'Create New Prompt',
'modal.name_required': 'Name is required',
'modal.content_required': 'Content is required',
'modal.create_error': 'Error creating prompt',
'modal.name_label': 'Name',
'modal.content_label': 'Content',
'modal.name_placeholder': 'Enter a name for the prompt',
'modal.content_placeholder': 'Enter the prompt content',
'modal.cancel': 'Cancel',
'modal.creating': 'Creating...',
'modal.create': 'Create Prompt',
// Share Modal
'share_modal.title': 'Share Prompt',
'share_modal.select_users': 'Select Users',
'share_modal.select_all': 'Select All',
'share_modal.deselect_all': 'Deselect All',
'share_modal.loading_users': 'Loading users...',
'share_modal.error_loading_users': 'Error loading users',
'share_modal.no_users_available': 'No users available',
'share_modal.no_users_selected': 'Please select at least one user',
'share_modal.one_user_selected': '1 user selected',
'share_modal.multiple_users_selected': '{count} users selected',
'share_modal.custom_title': 'Custom Title (optional)',
'share_modal.title_placeholder': 'Enter a custom title',
'share_modal.message': 'Message (optional)',
'share_modal.message_placeholder': 'Add a message for recipients',
'share_modal.share': 'Share',
'share_modal.sharing': 'Sharing...',
'share_modal.share_error': 'Error sharing prompt',
// Prompt Settings
'prompt_settings.title': 'Prompt Settings',
'prompt_settings.content_placeholder': 'Settings content will be added here in future updates.',
// Chat Area
'chat.continue_conversation': 'Continue conversation...',
'chat.enter_message': 'Enter message...',
'chat.remove_file': 'Remove file',
'chat.attach_file': 'Attach file',
'chat.you': 'You',
'chat.click_to_open': 'Click to open',
'chat.preview_document': 'Preview document',
'chat.download_document': 'Download document',
'chat.workflow_failed': 'Workflow failed.',
'chat.retry_workflow': 'Try again',
'chat.sending_followup': 'Sending follow-up message...',
'chat.sending_message': 'Sending message...',
'chat.error_prefix': 'Error:',
'chat.error_loading_messages': 'Error loading messages:',
'chat.loading_workflow_messages': 'Loading workflow messages...',
'chat.start_conversation': 'Start a conversation by entering a message, selecting a template, or continuing a previous workflow...',
// File Preview
'file_preview.loading': 'Loading preview...',
'file_preview.error': 'Error',
'file_preview.no_preview': 'No preview available',
'file_preview.close_preview': 'Close preview',
'file_preview.python': 'Python',
// Chat History
'chat_history.loading': 'Loading workflows...',
'chat_history.error_loading': 'Error loading workflows:',
'chat_history.try_again': 'Try Again',
'chat_history.title': 'Workflow History',
'chat_history.workflow_count': 'Workflow',
'chat_history.workflow_count_plural': 'Workflows',
'chat_history.empty_state': 'No workflows available',
'chat_history.confirm_delete': 'Are you sure you want to delete workflow "{id}..."?',
'chat_history.no_message_content': 'No message content available',
'chat_history.unknown_date': 'Unknown date',
'chat_history.invalid_date': 'Invalid date',
'chat_history.started': 'Started:',
'chat_history.last_activity': 'Last Activity:',
'chat_history.round': 'Round',
'chat_history.resume_tooltip': 'Resume workflow',
'chat_history.delete_tooltip': 'Delete workflow',
'chat_history.deleting': 'Deleting workflow...',
// Workflow Status
'status.error': 'ERROR',
'status.failed': 'FAILED',
'status.stopped': 'STOPPED',
'status.cancelled': 'CANCELLED',
'status.running': 'RUNNING',
'status.processing': 'PROCESSING',
'status.completed': 'COMPLETED',
'status.pending': 'PENDING',
// Files
'files.unknown_size': 'Unknown Size',
'files.unknown_date': 'Unknown Date',
'files.source.uploaded': 'Uploaded',
'files.source.ai_created': 'AI-created',
'files.source.shared': 'Shared',
'files.source.unknown': 'Unknown',
'files.preview_tooltip': 'Preview file',
'files.download_tooltip': 'Download file',
'files.delete_tooltip': 'Delete file',
'files.delete_confirm_tooltip': 'Click again to confirm deletion',
'files.downloading': 'Downloading...',
'files.deleting': 'Deleting...',
'files.delete_confirm': 'Click to confirm...',
'files.no_files': 'No files found.',
'files.no_shared_files': 'No shared files found.',
'files.no_ai_files': 'No AI-created files found.',
'files.no_uploaded_files': 'No uploaded files found.',
'files.header.name': 'Name',
'files.header.type': 'Type',
'files.header.size': 'Size',
'files.header.date': 'Date',
'files.selector.title': 'Select files',
'files.selector.tab.all': 'All files',
'files.selector.tab.uploads': 'Uploaded',
'files.selector.tab.created': 'AI-created',
'files.selector.tab.shared': 'Shared',
'files.selector.select_all': 'Select all',
'files.selector.deselect_all': 'Deselect all',
'files.selector.file_selected': 'File',
'files.selector.files_selected': 'Files',
'files.selector.selected_suffix': 'selected',
'files.selector.upload_new': 'Upload new file',
'files.selector.loading': 'Loading files...',
'files.selector.error_loading': 'Error loading files:',
'files.upload.title': 'Upload file',
'files.upload.drop_here': 'Drop file here...',
'files.upload.uploading': 'Uploading...',
'files.upload.drag_files': 'Drag files here',
'files.upload.or': 'or',
'files.upload.browse': 'Browse',
'files.upload.selected_file': 'Selected file:',
'files.upload.upload_button': 'Upload',
'files.upload.uploading_button': 'Uploading...',
'files.upload.success': 'File uploaded successfully!',
'files.upload.error': 'An error occurred while uploading.',
'files.upload.unexpected_error': 'An unexpected error occurred while uploading.',
// Files Page
'files.page.tab.all': 'All Files',
'files.page.tab.uploads': 'My Uploads',
'files.page.tab.created': 'Created Files',
'files.page.tab.shared': 'Shared Files',
'files.page.add_file': 'Add File',
'files.page.loading': 'Loading files...',
'files.page.error': 'Error:',
};

263
src/locales/fr.ts Normal file
View file

@ -0,0 +1,263 @@
export default {
// Navigation
'nav.dashboard': 'Centre d\'activité',
'nav.files': 'Fichiers',
'nav.team': 'Espace équipe',
'nav.workflows': 'Workflows',
'nav.connections': 'Connections',
'nav.settings': 'Paramètres',
// Settings page
'settings.title': 'Paramètres',
'settings.appearance': 'Apparence',
'settings.language': 'Langue',
'settings.about': 'À propos',
'settings.version': 'Version',
'settings.theme': 'Thème',
'settings.theme.description': 'Basculer entre le mode clair et sombre',
'settings.language.description': 'Choisissez votre langue préférée',
'settings.theme.light': 'Clair',
'settings.theme.dark': 'Sombre',
'settings.theme.toggle.light': 'Passer en mode clair',
'settings.theme.toggle.dark': 'Passer en mode sombre',
// Languages
'language.german': 'Deutsch',
'language.english': 'English',
'language.french': 'Français',
// Common
'common.loading': 'Chargement...',
'common.error': 'Erreur',
'common.success': 'Succès',
'common.cancel': 'Annuler',
'common.save': 'Enregistrer',
'common.delete': 'Supprimer',
'common.edit': 'Modifier',
'common.close': 'Fermer',
// Auth
'auth.login': 'Se connecter',
'auth.register': 'S\'inscrire',
'auth.logout': 'Se déconnecter',
'auth.email': 'E-mail',
'auth.password': 'Mot de passe',
// Dashboard
'dashboard.prompt.template': 'Modèle de prompt',
'dashboard.prompt.settings': 'Paramètres',
'dashboard.chat.area': 'Zone de chat',
'dashboard.chat.history': 'Historique des workflows',
'dashboard.log.title': 'Journal',
'dashboard.log.workflow': 'Workflow',
'dashboard.log.no_workflow': 'Aucun workflow sélectionné',
'dashboard.log.loading': 'Chargement des logs...',
'dashboard.log.error': 'Erreur lors du chargement des logs',
'dashboard.log.no_logs': 'Aucun log disponible pour ce workflow',
'dashboard.log.waiting': 'Workflow en cours... En attente des logs...',
'dashboard.log.fetch_failed': 'Échec du chargement des logs',
'dashboard.log.level.info': 'INFO',
// Prompt Set
'promptset.loading': 'Chargement des prompts...',
'promptset.error.loading': 'Erreur lors du chargement des prompts',
'promptset.retry': 'Réessayer',
'promptset.new_prompt': 'Nouveau prompt',
'promptset.prompt_count': 'Prompt',
'promptset.prompt_count_plural': 'Prompts',
'promptset.no_prompts': 'Aucun prompt disponible',
'promptset.created': 'Créé',
'promptset.run_tooltip': 'Exécuter le prompt',
'promptset.share_tooltip': 'Partager le prompt',
'promptset.delete_tooltip': 'Supprimer le prompt',
'promptset.confirm_delete': 'Cliquez à nouveau pour confirmer',
'promptset.deleting': 'Suppression...',
'promptset.confirm_click': 'Cliquez pour confirmer',
'promptset.delete_error': 'Erreur lors de la suppression',
'promptset.deleting_message': 'Suppression du prompt...',
// Connections
'connections.title': 'Connexions',
'connections.connect_google': 'Connecter Google',
'connections.connect_microsoft': 'Connecter Microsoft',
'connections.edit_connection_title': 'Modifier la connexion {authority}',
'connections.update_connection': 'Mettre à jour la connexion',
'connections.service_connections': 'Connexions de service',
'connections.error': 'Erreur',
'connections.connection_error': 'Erreur de connexion',
'connections.disconnect_error': 'Erreur de déconnexion',
'connections.unknown': 'Inconnu',
'connections.not_available': 'N/D',
'connections.invalid_date': 'Date invalide',
'connections.confirm_delete': 'Êtes-vous sûr de vouloir supprimer la connexion {service} ?',
// Connection Fields
'connections.field.service': 'Service',
'connections.field.status': 'Statut',
'connections.field.external_username': 'Nom d\'utilisateur externe',
'connections.field.external_email': 'E-mail externe',
'connections.field.connected_at': 'Connecté le',
'connections.field.last_checked': 'Dernière vérification',
'connections.field.expires_at': 'Expire le',
// Connection Services
'connections.service.google': 'Google',
'connections.service.microsoft': 'Microsoft',
'connections.service.local': 'Local',
// Connection Placeholders
'connections.placeholder.external_username': 'Entrez le nom d\'utilisateur externe',
'connections.placeholder.external_email': 'Entrez l\'adresse e-mail externe',
// Connection Actions
'connections.action.edit': 'Modifier',
'connections.action.toggle_connection': 'Basculer la connexion',
'connections.action.delete': 'Supprimer',
// Prompt Modal
'modal.create_prompt': 'Créer un nouveau prompt',
'modal.name_required': 'Le nom est requis',
'modal.content_required': 'Le contenu est requis',
'modal.create_error': 'Erreur lors de la création du prompt',
'modal.name_label': 'Nom',
'modal.content_label': 'Contenu',
'modal.name_placeholder': 'Entrez un nom pour le prompt',
'modal.content_placeholder': 'Entrez le contenu du prompt',
'modal.cancel': 'Annuler',
'modal.creating': 'Création...',
'modal.create': 'Créer le prompt',
// Share Modal
'share_modal.title': 'Partager le prompt',
'share_modal.select_users': 'Sélectionner les utilisateurs',
'share_modal.select_all': 'Tout sélectionner',
'share_modal.deselect_all': 'Tout désélectionner',
'share_modal.loading_users': 'Chargement des utilisateurs...',
'share_modal.error_loading_users': 'Erreur lors du chargement des utilisateurs',
'share_modal.no_users_available': 'Aucun utilisateur disponible',
'share_modal.no_users_selected': 'Veuillez sélectionner au moins un utilisateur',
'share_modal.one_user_selected': '1 utilisateur sélectionné',
'share_modal.multiple_users_selected': '{count} utilisateurs sélectionnés',
'share_modal.custom_title': 'Titre personnalisé (facultatif)',
'share_modal.title_placeholder': 'Entrez un titre personnalisé',
'share_modal.message': 'Message (facultatif)',
'share_modal.message_placeholder': 'Ajoutez un message pour les destinataires',
'share_modal.share': 'Partager',
'share_modal.sharing': 'Partage en cours...',
'share_modal.share_error': 'Erreur lors du partage du prompt',
// Prompt Settings
'prompt_settings.title': 'Paramètres de prompt',
'prompt_settings.content_placeholder': 'Le contenu des paramètres sera ajouté dans les futures mises à jour.',
// Chat Area
'chat.continue_conversation': 'Continuer la conversation...',
'chat.enter_message': 'Entrez votre message...',
'chat.remove_file': 'Supprimer le fichier',
'chat.attach_file': 'Joindre un fichier',
'chat.you': 'Vous',
'chat.click_to_open': 'Cliquez pour ouvrir',
'chat.preview_document': 'Aperçu du document',
'chat.download_document': 'Télécharger le document',
'chat.workflow_failed': 'Échec du workflow.',
'chat.retry_workflow': 'Réessayer',
'chat.sending_followup': 'Envoi du message de suivi...',
'chat.sending_message': 'Envoi du message...',
'chat.error_prefix': 'Erreur:',
'chat.error_loading_messages': 'Erreur lors du chargement des messages:',
'chat.loading_workflow_messages': 'Chargement des messages de workflow...',
'chat.start_conversation': 'Commencez une conversation en entrant un message, en sélectionnant un modèle ou en continuant un workflow précédent...',
// File Preview
'file_preview.loading': 'Chargement de l\'aperçu...',
'file_preview.error': 'Erreur',
'file_preview.no_preview': 'Aucun aperçu disponible',
'file_preview.close_preview': 'Fermer l\'aperçu',
'file_preview.python': 'Python',
// Chat History
'chat_history.loading': 'Chargement des workflows...',
'chat_history.error_loading': 'Erreur lors du chargement des workflows:',
'chat_history.try_again': 'Réessayer',
'chat_history.title': 'Historique des workflows',
'chat_history.workflow_count': 'Workflow',
'chat_history.workflow_count_plural': 'Workflows',
'chat_history.empty_state': 'Aucun workflow disponible',
'chat_history.confirm_delete': 'Êtes-vous sûr de vouloir supprimer le workflow "{id}..."?',
'chat_history.no_message_content': 'Aucun contenu de message disponible',
'chat_history.unknown_date': 'Date inconnue',
'chat_history.invalid_date': 'Date invalide',
'chat_history.started': 'Démarré:',
'chat_history.last_activity': 'Dernière activité:',
'chat_history.round': 'Tour',
'chat_history.resume_tooltip': 'Reprendre le workflow',
'chat_history.delete_tooltip': 'Supprimer le workflow',
'chat_history.deleting': 'Suppression du workflow...',
// Workflow Status
'status.error': 'ERREUR',
'status.failed': 'ÉCHEC',
'status.stopped': 'ARRÊTÉ',
'status.cancelled': 'ANNULÉ',
'status.running': 'EN COURS',
'status.processing': 'TRAITEMENT',
'status.completed': 'TERMINÉ',
'status.pending': 'EN ATTENTE',
// Files
'files.unknown_size': 'Taille inconnue',
'files.unknown_date': 'Date inconnue',
'files.source.uploaded': 'Téléchargé',
'files.source.ai_created': 'Créé par IA',
'files.source.shared': 'Partagé',
'files.source.unknown': 'Inconnu',
'files.preview_tooltip': 'Aperçu du fichier',
'files.download_tooltip': 'Télécharger le fichier',
'files.delete_tooltip': 'Supprimer le fichier',
'files.delete_confirm_tooltip': 'Cliquez à nouveau pour confirmer la suppression',
'files.downloading': 'Téléchargement...',
'files.deleting': 'Suppression...',
'files.delete_confirm': 'Cliquez pour confirmer...',
'files.no_files': 'Aucun fichier trouvé.',
'files.no_shared_files': 'Aucun fichier partagé trouvé.',
'files.no_ai_files': 'Aucun fichier créé par IA trouvé.',
'files.no_uploaded_files': 'Aucun fichier téléchargé trouvé.',
'files.header.name': 'Nom',
'files.header.type': 'Type',
'files.header.size': 'Taille',
'files.header.date': 'Date',
'files.selector.title': 'Sélectionner des fichiers',
'files.selector.tab.all': 'Tous les fichiers',
'files.selector.tab.uploads': 'Téléchargés',
'files.selector.tab.created': 'Créés par IA',
'files.selector.tab.shared': 'Partagés',
'files.selector.select_all': 'Tout sélectionner',
'files.selector.deselect_all': 'Tout désélectionner',
'files.selector.file_selected': 'Fichier',
'files.selector.files_selected': 'Fichiers',
'files.selector.selected_suffix': 'sélectionné(s)',
'files.selector.upload_new': 'Télécharger un nouveau fichier',
'files.selector.loading': 'Chargement des fichiers...',
'files.selector.error_loading': 'Erreur lors du chargement des fichiers:',
'files.upload.title': 'Télécharger un fichier',
'files.upload.drop_here': 'Déposer le fichier ici...',
'files.upload.uploading': 'Téléchargement...',
'files.upload.drag_files': 'Glisser les fichiers ici',
'files.upload.or': 'ou',
'files.upload.browse': 'Parcourir',
'files.upload.selected_file': 'Fichier sélectionné:',
'files.upload.upload_button': 'Télécharger',
'files.upload.uploading_button': 'Téléchargement...',
'files.upload.success': 'Fichier téléchargé avec succès!',
'files.upload.error': 'Une erreur s\'est produite lors du téléchargement.',
'files.upload.unexpected_error': 'Une erreur inattendue s\'est produite lors du téléchargement.',
// Files Page
'files.page.tab.all': 'Tous les fichiers',
'files.page.tab.uploads': 'Mes téléchargements',
'files.page.tab.created': 'Fichiers créés',
'files.page.tab.shared': 'Fichiers partagés',
'files.page.add_file': 'Ajouter un fichier',
'files.page.loading': 'Chargement des fichiers...',
'files.page.error': 'Erreur:',
};

37
src/locales/index.ts Normal file
View file

@ -0,0 +1,37 @@
import { Language, TranslationKeys } from './types';
// Dynamic language loader
export const loadLanguage = async (language: Language): Promise<TranslationKeys> => {
try {
let translations: TranslationKeys;
switch (language) {
case 'de':
const de = await import('./de');
translations = de.default;
break;
case 'en':
const en = await import('./en');
translations = en.default;
break;
case 'fr':
const fr = await import('./fr');
translations = fr.default;
break;
default:
// Fallback to German if language not found
const fallback = await import('./de');
translations = fallback.default;
}
return translations;
} catch (error) {
console.error(`Failed to load language ${language}:`, error);
// Fallback to German in case of error
const fallback = await import('./de');
return fallback.default;
}
};
// Re-export types
export type { Language, TranslationKeys } from './types';

12
src/locales/types.ts Normal file
View file

@ -0,0 +1,12 @@
// Language type definition
export type Language = 'de' | 'en' | 'fr';
// Translation keys and their values
export type TranslationKeys = {
[key: string]: string;
};
// Language loader interface
export interface LanguageLoader {
loadLanguage: (language: Language) => Promise<TranslationKeys>;
}

View file

@ -1,102 +0,0 @@
import { setup, assign } from 'xstate';
export type SidebarEvent =
| { type: 'TOGGLE_ITEM'; itemId: string }
| { type: 'CLOSE_ALL' }
| { type: 'NAVIGATE'; path: string }
| { type: 'MINIMIZE_SIDEBAR' }
| { type: 'EXPAND_SIDEBAR' };
export interface SidebarContext {
openItemId: string | null;
activePath: string;
isMinimized: boolean;
}
export const sidebarMachine = setup({
types: {
context: {} as SidebarContext,
events: {} as SidebarEvent,
},
}).createMachine({
id: 'sidebar',
initial: 'collapsed',
context: {
openItemId: null,
activePath: '/',
isMinimized: false,
},
states: {
collapsed: {
on: {
TOGGLE_ITEM: {
guard: ({ context }) => !context.isMinimized,
target: 'expanded',
actions: assign({
openItemId: ({ event }) => event.itemId,
}),
},
CLOSE_ALL: {
},
},
},
expanded: {
on: {
TOGGLE_ITEM: [
{
guard: ({ context, event }) => !context.isMinimized && context.openItemId === event.itemId,
target: 'collapsed',
actions: assign({
openItemId: null,
}),
},
{
guard: ({ context }) => !context.isMinimized,
target: 'expanded',
actions: assign({
openItemId: ({ event }) => event.itemId,
}),
},
],
CLOSE_ALL: {
target: 'collapsed',
actions: assign({
openItemId: null,
}),
},
},
},
},
on: {
NAVIGATE: {
actions: assign({
activePath: ({ event }) => event.path,
}),
},
MINIMIZE_SIDEBAR: {
actions: assign({
isMinimized: true,
openItemId: null, // Close any open submenu when minimizing
}),
},
EXPAND_SIDEBAR: {
actions: assign({
isMinimized: false,
}),
},
},
});
export const getSidebarState = (context: SidebarContext) => {
return {
hasOpenSubmenu: context.openItemId !== null,
openItemId: context.openItemId,
isItemOpen: (itemId: string) => context.openItemId === itemId,
activePath: context.activePath,
isMinimized: context.isMinimized,
};
};
export type SidebarMachineState = ReturnType<typeof sidebarMachine.transition>;

View file

@ -1,123 +0,0 @@
.dateienContainer {
margin: 51px 49px 0 36px;
display: flex;
padding: 0px 30px 30px 30px;
flex-direction: column;
align-self: stretch;
border-radius: 30px;
background: var(--color-bg);
position: relative;
box-shadow: 0px 2px 6px 0px rgba(194, 194, 194, 0.10);
max-height: calc(100vh - 100px);
overflow: hidden;
font-family: var(--font-family);
}
.horizontalLineLight {
width: calc(100% + 60px);
background-color: var(--color-gray-disabled);
height: 1px;
margin-left: -30px;
margin-bottom: 0;
flex-shrink: 0;
}
/* Combined Header with Tabs and Add Button */
.combinedHeader {
display: flex;
justify-content: space-between;
align-items: center;
flex-shrink: 0;
min-height: 62px;
}
.datei_hinzufügen_button {
border-radius: 30px;
background: var(--color-secondary);
color: var(--color-bg);
border: none;
outline: none;
text-align: left;
padding-left: 20px;
padding-right: 20px;
padding-top: 10px;
padding-bottom: 10px;
display: flex;
gap: 10px;
align-items: center;
flex-shrink: 0;
transition: background-color 0.2s ease;
font-family: var(--font-family);
}
.datei_hinzufügen_button:hover {
cursor: pointer;
background-color: var(--color-secondary-hover);
}
.add_icon {
font-size: 16px;
}
/* Tab Navigation Styles */
.tabButtonDiv {
display: flex;
gap: 12px;
align-items: center;
justify-content: flex-start;
flex: 1;
}
.tabButtonWrapper {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
}
.tabButton {
background: transparent;
border: none;
outline: none;
padding: 20px 0px;
font-size: 14px;
font-weight: 500;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s ease;
white-space: nowrap;
position: relative;
font-family: var(--font-family);
}
.tabButtonActive {
color: var(--color-text);
}
.tabButtonInactive {
color: var(--color-gray);
}
.tabButtonInactive:hover {
color: var(--color-text);
}
.tabUnderline {
position: absolute;
bottom: -2px;
left: 0;
height: 1px;
background-color: var(--color-text);
border-radius: 1px;
}
/* Content area */
.contentArea {
flex: 1;
overflow-y: auto;
min-height: 0;
height: calc(100vh - 300px);
display: flex;
flex-direction: column;
}

View file

@ -0,0 +1,98 @@
import React, { useState } from 'react';
import styles from './HomeStyles/Connections.module.css';
import sharedStyles from './HomeStyles/pages.module.css';
import { IoIosLink } from 'react-icons/io';
import {
ConnectionsTable,
ConnectionEditModal,
ConnectionsErrorDisplay,
useConnectionsLogic,
Connection
} from '../../components/Connections';
import { useLanguage } from '../../contexts/LanguageContext';
function Connections() {
const { t } = useLanguage();
// Use the custom hook for all business logic
const {
connections,
isLoading,
isConnecting,
isDisconnecting,
error,
connectError,
disconnectError,
editPopupOpen,
editingConnection,
connectionColumns,
connectionEditFields,
tableActions,
handleCreateConnection,
handleSaveConnection,
handleCancelEdit
} = useConnectionsLogic();
// Local state for selected connections (if needed)
const [selectedConnections, setSelectedConnections] = useState<Connection[]>([]);
return (
<div className={sharedStyles.pageContainer}>
<div className={sharedStyles.pageCard}>
{/* Page Header */}
<div className={sharedStyles.pageHeader}>
<h1 className={sharedStyles.pageTitle}>{t('connections.title')}</h1>
<div style={{ display: 'flex', gap: '10px' }}>
<button
className={sharedStyles.primaryButton}
onClick={() => handleCreateConnection('google')}
disabled={isLoading || isConnecting || isDisconnecting}
>
<span className={sharedStyles.buttonIcon}><IoIosLink /></span>
{t('connections.connect_google')}
</button>
<button
className={sharedStyles.primaryButton}
onClick={() => handleCreateConnection('msft')}
disabled={isLoading || isConnecting || isDisconnecting}
>
<span className={sharedStyles.buttonIcon}><IoIosLink /></span>
{t('connections.connect_microsoft')}
</button>
</div>
</div>
<div className={sharedStyles.horizontalDivider}></div>
<div className={sharedStyles.contentArea}>
{/* Error Display */}
<ConnectionsErrorDisplay
error={error}
connectError={connectError}
disconnectError={disconnectError}
/>
{/* Connections Table */}
<ConnectionsTable
connections={connections}
columns={connectionColumns}
actions={tableActions}
isLoading={isLoading}
onRowSelect={setSelectedConnections}
/>
</div>
</div>
{/* Edit Connection Modal */}
<ConnectionEditModal
isOpen={editPopupOpen}
connection={editingConnection}
fields={connectionEditFields}
onSave={handleSaveConnection}
onCancel={handleCancelEdit}
/>
</div>
);
}
export default Connections;

View file

@ -1,16 +1,12 @@
import React, { useState, useCallback, useMemo } from 'react';
import DashboardPrompt from '../components/Dashboard/DashboardPrompt/DashboardPrompt';
import DashboardChat from '../components/Dashboard/DashboardChat/DashboardChat';
import DashboardLog from '../components/Dashboard/DashboardLog/DashboardLog';
import { Prompt } from '../hooks/usePrompts';
import styles from './Dashboard.module.css'
import { Prompt } from '../../hooks/usePrompts';
import styles from './HomeStyles/Dashboard.module.css'
import DashboardChat from '../../components/Dashboard/DashboardChat/DashboardChat';
function Dashboard () {
const [isChatExpanded, setIsChatExpanded] = useState(false);
const [selectedPrompt, setSelectedPrompt] = useState<Prompt | null>(null);
const [isPromptAreaCollapsed, setIsPromptAreaCollapsed] = useState(false);
const [currentWorkflowId, setCurrentWorkflowId] = useState<string | null>(null);
const [workflowCompleted, setWorkflowCompleted] = useState(false);
const handleChatToggleExpand = () => {
setIsChatExpanded(!isChatExpanded);
@ -22,12 +18,14 @@ function Dashboard () {
};
const handleWorkflowIdChange = useCallback((workflowId: string | null) => {
setCurrentWorkflowId(workflowId);
setCurrentWorkflowId(prevId => {
// Reset completion status when workflow changes
if (workflowId !== currentWorkflowId) {
if (workflowId !== prevId) {
setWorkflowCompleted(false);
}
}, [currentWorkflowId]);
return workflowId;
});
}, []);
const handleWorkflowCompletedChange = useCallback((completed: boolean) => {
setWorkflowCompleted(completed);
@ -64,3 +62,11 @@ function Dashboard () {
}
export default Dashboard;
function setCurrentWorkflowId(arg0: (prevId: any) => string | null) {
throw new Error('Function not implemented.');
}
function setWorkflowCompleted(arg0: boolean) {
throw new Error('Function not implemented.');
}

View file

@ -1,4 +1,5 @@
import styles from './Dateien.module.css'
import styles from './HomeStyles/Dateien.module.css'
import sharedStyles from './HomeStyles/pages.module.css'
import { IoAddCircleOutline } from "react-icons/io5";
import DateienUpload from '../../components/Dateien/DateienHinzufügen/DateienUploadTool';
import DateienAll from '../../components/Dateien/DateienAll';
@ -73,10 +74,14 @@ function Dateien() {
};
return (
<div className={styles.dateienContainer}>
<div className={sharedStyles.pageContainer}>
<div className={sharedStyles.pageCard}>
<h1 className={sharedStyles.pageTitle}>Dateien</h1>
<div className={sharedStyles.horizontalDivider}></div>
{/* Combined Header with Tabs and Add Button */}
<motion.div
className={styles.combinedHeader}
className={`${sharedStyles.pageHeader} ${styles.combinedHeader}`}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.3 }}
@ -110,16 +115,16 @@ function Dateien() {
</div>
<button
className={styles.datei_hinzufügen_button}
className={`${sharedStyles.primaryButton} ${styles.datei_hinzufügen_button}`}
onClick={() => setIsUploadOpen(true)}
>
<IoAddCircleOutline className={styles.add_icon}/>
<IoAddCircleOutline className={sharedStyles.buttonIcon}/>
{t('files.page.add_file', 'Add File')}
</button>
</motion.div>
<div className={styles.horizontalLineLight}></div>
<div className={sharedStyles.horizontalDivider}></div>
<div className={styles.contentArea}>
<div className={`${sharedStyles.contentArea} ${styles.contentArea}`}>
<DateienUpload
isOpen={isUploadOpen}
onClose={handleUploadClose}
@ -148,6 +153,7 @@ function Dateien() {
)}
</div>
</div>
</div>
);
}

View file

@ -1,10 +1,11 @@
import React, { useState, useEffect } from 'react';
import styles from './Einstellungen.module.css';
import styles from './HomeStyles/Einstellungen.module.css';
import sharedStyles from './HomeStyles/pages.module.css';
import { useLanguage, Language } from '../../contexts/LanguageContext';
function Einstellungen() {
const [isDarkMode, setIsDarkMode] = useState(false);
const { currentLanguage, setLanguage, t } = useLanguage();
const { currentLanguage, setLanguage, t, isLoading } = useLanguage();
// Sync component state with current theme on mount
useEffect(() => {
@ -31,6 +32,16 @@ function Einstellungen() {
localStorage.setItem('theme', newIsDarkMode ? 'dark' : 'light');
};
const handleLanguageChange = async (language: Language) => {
if (language === currentLanguage) return;
try {
await setLanguage(language);
} catch (error) {
console.error('Failed to change language:', error);
}
};
const getLanguageLabel = (lang: Language): string => {
switch (lang) {
case 'de': return t('language.german');
@ -40,15 +51,25 @@ function Einstellungen() {
}
};
if (isLoading) {
return (
<div className={styles.einstellungenContainer}>
<div className={styles.contentWrapper}>
<div className={styles.settingsCard}>
<h1 className={styles.title}>{t('settings.title')}</h1>
<div className={sharedStyles.pageContainer}>
<div className={sharedStyles.pageCard}>
<div style={{ padding: '2rem', textAlign: 'center' }}>
{t('common.loading')}
</div>
</div>
</div>
);
}
<div className={styles.settingsSection}>
<h2 className={styles.sectionTitle}>{t('settings.appearance')}</h2>
return (
<div className={sharedStyles.pageContainer}>
<div className={sharedStyles.pageCard}>
<h1 className={sharedStyles.pageTitle}>{t('settings.title')}</h1>
<div className={sharedStyles.horizontalDivider}></div>
<div className={sharedStyles.contentArea}>
<div className={styles.settingItem}>
<div className={styles.settingInfo}>
<span className={styles.settingLabel}>{t('settings.theme')}</span>
@ -84,7 +105,7 @@ function Einstellungen() {
<select
className={styles.languageSelect}
value={currentLanguage}
onChange={(e) => setLanguage(e.target.value as Language)}
onChange={(e) => handleLanguageChange(e.target.value as Language)}
aria-label={t('settings.language')}
>
<option value="de">{getLanguageLabel('de')}</option>
@ -105,7 +126,7 @@ function Einstellungen() {
</div>
</div>
</div>
</div>
);
}

View file

@ -1,9 +1,9 @@
import { useEffect } from 'react';
import { Outlet, useLocation } from 'react-router-dom';
import styles from './Home.module.css'
import styles from './HomeStyles/Home.module.css'
import Sidebar from '../components/Sidebar';
import Sidebar from '../../components/Sidebar';
import { AnimatePresence, motion } from "framer-motion";

View file

@ -0,0 +1,13 @@
.connectionsTable {
width: 100%;
}
.errorMessage {
color: var(--color-red);
padding: 10px;
background-color: var(--color-red-hover);
border: 1px solid var(--color-red);
border-radius: 25px;
margin-bottom: 10px;
}

View file

@ -0,0 +1,79 @@
/* Dateien Page Styles
*
* This page now uses shared styles from pages.module.css for consistency.
* Only file management-specific styles should be defined here.
*/
/* Remove old styles - now using shared styles */
/* .dateienContainer - now using sharedStyles.pageContainer + sharedStyles.pageCardWithContentPadding */
/* .horizontalLineLight - now using sharedStyles.horizontalDivider */
/* .datei_hinzufügen_button - now using sharedStyles.primaryButton */
/* .add_icon - now using sharedStyles.buttonIcon */
/* Dateien-specific header customization */
.combinedHeader {
/* Additional styling for the header with tabs - extends sharedStyles.pageHeader */
}
/* Additional button customization if needed */
.datei_hinzufügen_button {
/* Any Dateien-specific button customizations go here */
}
/* Tab Navigation Styles */
.tabButtonDiv {
display: flex;
gap: 12px;
align-items: center;
justify-content: flex-start;
flex: 1;
}
.tabButtonWrapper {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
}
.tabButton {
background: transparent;
border: none;
outline: none;
padding: 20px 0px;
font-size: 14px;
font-weight: 500;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s ease;
white-space: nowrap;
position: relative;
font-family: var(--font-family);
}
.tabButtonActive {
color: var(--color-text);
}
.tabButtonInactive {
color: var(--color-gray);
}
.tabButtonInactive:hover {
color: var(--color-text);
}
.tabUnderline {
position: absolute;
bottom: -2px;
left: 0;
height: 1px;
background-color: var(--color-text);
border-radius: 1px;
}
/* Content area customization */
.contentArea {
/* Any Dateien-specific content area customizations go here */
/* Base styling now comes from sharedStyles.contentArea */
}

View file

@ -1,42 +1,3 @@
.einstellungenContainer {
margin: 51px 49px 0 36px;
display: flex;
flex-direction: column;
align-self: stretch;
justify-content: top;
max-height: calc(100vh - 100px);
overflow: hidden;
font-family: var(--font-family);
width: 98%;
}
.contentWrapper {
flex: 1;
overflow-y: auto;
padding: 0px 0;
}
.settingsCard {
display: flex;
padding: 30px;
flex-direction: column;
align-self: stretch;
border-radius: 30px;
background: var(--color-bg);
position: relative;
box-shadow: 0px 2px 6px 0px rgba(194, 194, 194, 0.10);
width: 100%;
margin: 0 auto;
gap: 30px;
}
.title {
font-size: 2rem;
font-weight: 600;
color: var(--color-text);
margin: 0 0 20px 0;
font-family: var(--font-family);
}
.settingsSection {
display: flex;
@ -44,15 +5,6 @@
gap: 20px;
}
.sectionTitle {
font-size: 1.25rem;
font-weight: 500;
color: var(--color-text);
margin: 0;
padding-bottom: 10px;
border-bottom: 1px solid var(--color-gray-disabled);
font-family: var(--font-family);
}
.settingItem {
display: flex;

View file

@ -0,0 +1,28 @@
.membersList {
list-style: none;
padding: 0;
margin: 0; /* Custom top margin for spacing after divider */
width: 100%;
overflow-y: auto;
}
/* Keep specific height requirement for member list items */
.membersList li {
display: flex;
align-items: center;
height: 60px; /* Specific height requirement for each member item */
padding: 0 16px;
border-bottom: 1px solid var(--color-gray-disabled);
font-size: 16px;
transition: background-color 0.2s ease;
color: var(--color-text);
font-family: var(--font-family);
}
.actions {
display: flex;
gap: 10px;
align-items: center;
justify-content: center;
}

View file

@ -0,0 +1,16 @@
/* Workflows Page Styles
*
* This page now uses shared styles from pages.module.css for consistency.
* Only workflow-specific styles should be defined here.
*/
/* Remove old .workflowsContainer - now using sharedStyles.pageContainer + sharedStyles.pageCard */
/* Workflow-specific styles go here */
.workflowItem {
/* Future workflow item styling */
}
.workflowActions {
/* Future workflow action styling */
}

View file

@ -0,0 +1,146 @@
.pageContainer {
display: flex;
flex-direction: column;
align-self: top;
height: calc(100vh);
overflow: hidden;
font-family: var(--font-family);
}
/* Content wrapper for scrollable content */
.contentWrapper {
flex: 1;
overflow-y: auto;
padding: 0;
}
/* Card-style container with background and shadow */
.pageCard {
display: flex;
padding: 25px;
flex-direction: column;
align-self: top;
background: var(--color-bg);
box-shadow: 0px 2px 6px 0px rgba(194, 194, 194, 0.30);
gap: 20px;
height: 100%;
}
/* Page headers with consistent spacing */
.pageHeader {
display: flex;
justify-content: space-between;
align-items: center;
flex-shrink: 0;
min-height: 62px;
gap: 30px;
}
/* Page titles */
.pageTitle {
font-size: 1.5rem;
font-weight: 400;
color: var(--color-text);
margin: 0;
font-family: var(--font-family);
}
/* Common button styles */
.primaryButton {
border-radius: 30px;
background: var(--color-secondary);
color: var(--color-bg);
border: none;
outline: none;
padding: 10px 20px;
display: flex;
gap: 10px;
align-items: center;
flex-shrink: 0;
transition: background-color 0.2s ease;
font-family: var(--font-family);
cursor: pointer;
}
.primaryButton:hover {
background-color: var(--color-secondary-hover);
}
.secondaryButton {
border-radius: 30px;
background: var(--color-gray-disabled);
color: var(--color-text);
border: none;
outline: none;
padding: 10px 20px;
display: flex;
gap: 10px;
align-items: center;
flex-shrink: 0;
transition: background-color 0.2s ease;
font-family: var(--font-family);
cursor: pointer;
}
.secondaryButton:hover {
background-color: var(--color-gray);
}
/* Common icon styles for buttons */
.buttonIcon {
font-size: 16px;
}
/* Horizontal divider lines */
.horizontalDivider {
width: calc(100% + 60px);
background-color: var(--color-primary);
height: 1px;
margin-left: -30px;
margin-bottom: 0;
flex-shrink: 0;
}
/* Content areas */
.contentArea {
min-height: 0;
display: flex;
flex-direction: column;
gap: 10px;
}
.scrollableContent {
overflow-y: auto;
flex: 1;
min-height: 0;
}
/* Responsive design for smaller screens */
@media (max-width: 768px) {
.pageContainer {
margin: 10px;
height: calc(100vh - 20px);
max-height: calc(100vh - 20px);
}
.pageCard,
.pageCardWithContentPadding {
padding: 20px;
}
.pageHeader {
flex-direction: column;
align-items: flex-start;
gap: 15px;
min-height: auto;
}
.pageTitle {
font-size: 1.5rem;
}
}

View file

@ -0,0 +1,63 @@
import styles from './HomeStyles/TeamBereich.module.css'
import sharedStyles from './HomeStyles/pages.module.css'
import MitgliederItem from '../../components/Mitglieder/MitgliederItem';
import { IoPersonAddSharp } from "react-icons/io5";
import { useOrgUsers } from '../../hooks/useUsers';
function TeamBereich () {
const { users, loading, error, refetch } = useOrgUsers();
if (loading) {
return (
<div className={sharedStyles.pageContainer}>
<div className={`${sharedStyles.pageCard} ${styles.mitgliederContainer}`}>
<h1 className={sharedStyles.pageTitle}>Team-Bereich</h1>
<div className={sharedStyles.horizontalDivider}></div>
<p>Lade Mitglieder...</p>
</div>
</div>
);
}
if (error) {
return (
<div className={sharedStyles.pageContainer}>
<div className={`${sharedStyles.pageCard}`}>
<h1 className={sharedStyles.pageTitle}>Team-Bereich</h1>
<div className={sharedStyles.horizontalDivider}></div>
<p>Fehler beim Laden der Mitglieder: {error}</p>
</div>
</div>
);
}
return (
<div className={sharedStyles.pageContainer}>
<div className={`${sharedStyles.pageCard} ${styles.mitgliederContainer}`}>
<h1 className={sharedStyles.pageTitle}>Team-Bereich</h1>
<div className={sharedStyles.horizontalDivider}></div>
<button className={`${sharedStyles.secondaryButton} ${styles.mitglieder_hinzufügen_button}`}>
<IoPersonAddSharp className={sharedStyles.buttonIcon} />
Mitglied hinzufügen
</button>
<ul className={styles.membersList}>
{users.map((user) => (
<MitgliederItem
key={user.id}
user={user}
refetchUsers={refetch}
totalUsers={users.length}
/>
))}
</ul>
{users.length === 0 && (
<p>Keine Mitglieder gefunden.</p>
)}
</div>
</div>
);
}
export default TeamBereich;

View file

@ -0,0 +1,317 @@
.container {
padding: 20px;
max-width: 1400px;
margin: 0 auto;
}
.title {
font-size: 28px;
font-weight: 600;
margin-bottom: 20px;
color: var(--text-primary);
}
.section {
background: var(--bg-secondary);
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
border: 1px solid var(--border-primary);
}
.sectionTitle {
font-size: 18px;
font-weight: 500;
margin-bottom: 15px;
color: var(--text-primary);
display: flex;
align-items: center;
gap: 8px;
}
.connectionsGrid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 15px;
}
.connectionCard {
background: var(--bg-primary);
border: 1px solid var(--border-secondary);
border-radius: 6px;
padding: 15px;
transition: border-color 0.2s;
}
.connectionCard:hover {
border-color: var(--border-accent);
}
.connectionCard.active {
border-color: var(--success);
background: var(--bg-success-subtle);
}
.connectionInfo {
margin-bottom: 10px;
}
.connectionName {
font-weight: 500;
color: var(--text-primary);
margin-bottom: 4px;
}
.connectionStatus {
font-size: 12px;
color: var(--text-secondary);
}
.connectionActions {
display: flex;
gap: 8px;
}
.testArea {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
}
.requestPanel,
.responsePanel {
background: var(--bg-primary);
border: 1px solid var(--border-secondary);
border-radius: 6px;
padding: 15px;
}
.panelTitle {
font-size: 16px;
font-weight: 500;
margin-bottom: 15px;
color: var(--text-primary);
}
.formGroup {
margin-bottom: 15px;
}
.label {
display: block;
font-size: 14px;
font-weight: 500;
margin-bottom: 5px;
color: var(--text-primary);
}
.input,
.textarea,
.select {
width: 100%;
padding: 8px 12px;
border: 1px solid var(--border-secondary);
border-radius: 4px;
background: var(--bg-secondary);
color: var(--text-primary);
font-size: 14px;
transition: border-color 0.2s;
}
.input:focus,
.textarea:focus,
.select:focus {
outline: none;
border-color: var(--border-accent);
}
.textarea {
min-height: 120px;
resize: vertical;
font-family: 'Courier New', monospace;
}
.buttonGroup {
display: flex;
gap: 10px;
margin-bottom: 15px;
}
.button {
padding: 8px 16px;
border: 1px solid var(--border-secondary);
border-radius: 4px;
background: var(--bg-primary);
color: var(--text-primary);
cursor: pointer;
font-size: 14px;
transition: all 0.2s;
}
.button:hover {
background: var(--bg-secondary);
border-color: var(--border-accent);
}
.button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.button.primary {
background: var(--primary);
color: white;
border-color: var(--primary);
}
.button.primary:hover {
background: var(--primary-dark);
}
.button.danger {
background: var(--error);
color: white;
border-color: var(--error);
}
.button.danger:hover {
background: var(--error-dark);
}
.responseArea {
background: var(--bg-code);
border: 1px solid var(--border-secondary);
border-radius: 4px;
padding: 15px;
font-family: 'Courier New', monospace;
font-size: 12px;
color: var(--text-primary);
white-space: pre-wrap;
max-height: 400px;
overflow-y: auto;
}
.responseArea.success {
border-color: var(--success);
background: var(--bg-success-subtle);
}
.responseArea.error {
border-color: var(--error);
background: var(--bg-error-subtle);
}
.loading {
text-align: center;
padding: 20px;
color: var(--text-secondary);
}
.errorMessage {
background: var(--bg-error-subtle);
border: 1px solid var(--error);
border-radius: 4px;
padding: 10px;
color: var(--error);
font-size: 14px;
margin-bottom: 15px;
}
.successMessage {
background: var(--bg-success-subtle);
border: 1px solid var(--success);
border-radius: 4px;
padding: 10px;
color: var(--success);
font-size: 14px;
margin-bottom: 15px;
}
.exampleButton {
background: var(--bg-accent);
color: var(--text-accent);
border: 1px solid var(--border-accent);
}
.exampleButton:hover {
background: var(--bg-accent-hover);
}
.statusBadge {
display: inline-block;
padding: 2px 6px;
border-radius: 3px;
font-size: 11px;
font-weight: 500;
text-transform: uppercase;
}
.statusBadge.active {
background: var(--bg-success-subtle);
color: var(--success);
}
.statusBadge.pending {
background: var(--bg-warning-subtle);
color: var(--warning);
}
.statusBadge.expired {
background: var(--bg-error-subtle);
color: var(--error);
}
.statusBadge.revoked {
background: var(--bg-error-subtle);
color: var(--error);
}
.tabs {
display: flex;
border-bottom: 1px solid var(--border-secondary);
margin-bottom: 15px;
}
.tab {
padding: 10px 20px;
border: none;
background: none;
color: var(--text-secondary);
cursor: pointer;
border-bottom: 2px solid transparent;
transition: all 0.2s;
}
.tab:hover {
color: var(--text-primary);
}
.tab.active {
color: var(--primary);
border-bottom-color: var(--primary);
}
.tabContent {
min-height: 300px;
}
.helpText {
font-size: 12px;
color: var(--text-secondary);
margin-top: 5px;
line-height: 1.4;
}
.requestPreview {
background: var(--bg-code);
border: 1px solid var(--border-secondary);
border-radius: 4px;
padding: 10px;
font-family: 'Courier New', monospace;
font-size: 12px;
color: var(--text-primary);
white-space: pre-wrap;
max-height: 200px;
overflow-y: auto;
margin-top: 10px;
}

View file

@ -0,0 +1,602 @@
import { useState, useEffect } from 'react';
import {
useSharePointTest,
SharePointConnection,
SharePointListRequest,
SharePointFindRequest,
SharePointReadRequest,
SharePointUploadRequest,
SharePointResponse
} from '../../hooks/useSharePointTest';
import styles from './TestSharepoint.module.css';
type TestOperation = 'list' | 'find' | 'read' | 'upload';
interface FormData {
connectionReference: string;
siteUrl: string;
folderPaths: string[];
query: string;
searchScope: string;
documentList: string;
documentPaths: string[];
fileNames: string[];
includeMetadata: boolean;
includeSubfolders: boolean;
}
function TestSharepoint() {
const {
getConnections,
testConnection,
listDocuments,
findDocuments,
readDocuments,
uploadDocuments,
getExamples,
debugTokens,
debugTokenDetails,
discoverSites,
isLoading,
error,
lastResponse
} = useSharePointTest();
// State
const [connections, setConnections] = useState<SharePointConnection[]>([]);
const [selectedConnection, setSelectedConnection] = useState<string>('');
const [activeTab, setActiveTab] = useState<TestOperation>('list');
const [examples, setExamples] = useState<any>({});
const [testResults, setTestResults] = useState<any>({});
const [tokenDebugInfo, setTokenDebugInfo] = useState<any>(null);
// Form data
const [formData, setFormData] = useState<FormData>({
connectionReference: '',
siteUrl: 'https://your-tenant.sharepoint.com/sites/your-site',
folderPaths: ['/'], // Start at root folder
query: 'quarterly report 2024',
searchScope: 'all',
documentList: 'document_list_reference_from_chat',
documentPaths: ['/Shared Documents/file1.docx', '/Documents/file2.pdf'],
fileNames: ['uploaded_file1.docx', 'uploaded_file2.pdf'],
includeMetadata: true,
includeSubfolders: false // Default to false for better navigation UX
});
// Load connections and examples on mount
useEffect(() => {
loadConnections();
loadExamples();
}, []);
// Update connection reference when selected connection changes
useEffect(() => {
setFormData(prev => ({ ...prev, connectionReference: selectedConnection }));
}, [selectedConnection]);
const loadConnections = async () => {
try {
const conns = await getConnections();
setConnections(conns);
if (conns.length > 0) {
setSelectedConnection(conns[0].id);
}
} catch (error) {
console.error('Failed to load connections:', error);
}
};
const loadExamples = async () => {
try {
const exampleData = await getExamples();
setExamples(exampleData);
} catch (error) {
console.error('Failed to load examples:', error);
}
};
const handleTestConnection = async (connectionId: string) => {
try {
const result = await testConnection(connectionId);
setTestResults((prev: any) => ({ ...prev, [connectionId]: result }));
} catch (error) {
console.error('Connection test failed:', error);
}
};
const handleDebugTokens = async () => {
try {
const result = await debugTokens();
setTokenDebugInfo(result);
} catch (error) {
console.error('Token debug failed:', error);
}
};
const handleDebugTokenDetails = async () => {
try {
const result = await debugTokenDetails();
setTokenDebugInfo(result);
} catch (error) {
console.error('Token details debug failed:', error);
}
};
const handleFormChange = (field: keyof FormData, value: any) => {
setFormData(prev => ({ ...prev, [field]: value }));
};
const handleArrayInputChange = (field: 'folderPaths' | 'documentPaths' | 'fileNames', value: string) => {
const array = value.split('\n').filter(item => item.trim() !== '');
setFormData(prev => ({ ...prev, [field]: array }));
};
const loadExample = (operation: TestOperation) => {
const exampleKey = operation === 'list' ? 'listDocuments' :
operation === 'find' ? 'findDocuments' :
operation === 'read' ? 'readDocuments' : 'uploadDocuments';
if (examples[exampleKey]) {
const example = examples[exampleKey];
setFormData(prev => ({
...prev,
connectionReference: selectedConnection || prev.connectionReference,
siteUrl: example.siteUrl || prev.siteUrl,
folderPaths: example.folderPaths || prev.folderPaths,
query: example.query || prev.query,
searchScope: example.searchScope || prev.searchScope,
documentList: example.documentList || prev.documentList,
documentPaths: example.documentPaths || prev.documentPaths,
fileNames: example.fileNames || prev.fileNames,
includeMetadata: example.includeMetadata !== undefined ? example.includeMetadata : prev.includeMetadata,
includeSubfolders: example.includeSubfolders !== undefined ? example.includeSubfolders : prev.includeSubfolders
}));
}
};
const executeTest = async () => {
if (!selectedConnection) {
alert('Please select a connection first');
return;
}
// Find the selected connection to build proper reference
const selectedConn = connections.find(conn => conn.id === selectedConnection);
if (!selectedConn) {
alert('Selected connection not found');
return;
}
// Build connection reference in expected format: connection:{authority}:{username}:{id}
const connectionReference = `connection:${selectedConn.authority}:${selectedConn.externalUsername}:${selectedConn.id}`;
try {
let result: SharePointResponse;
switch (activeTab) {
case 'list':
const listRequest: SharePointListRequest = {
connectionReference: connectionReference,
siteUrl: formData.siteUrl,
folderPaths: formData.folderPaths,
includeSubfolders: formData.includeSubfolders,
expectedDocumentFormats: [{ extension: '.json', mimeType: 'application/json' }]
};
result = await listDocuments(listRequest);
break;
case 'find':
const findRequest: SharePointFindRequest = {
connectionReference: connectionReference,
siteUrl: formData.siteUrl,
query: formData.query,
searchScope: formData.searchScope,
expectedDocumentFormats: [{ extension: '.json', mimeType: 'application/json' }]
};
result = await findDocuments(findRequest);
break;
case 'read':
const readRequest: SharePointReadRequest = {
documentList: formData.documentList,
connectionReference: connectionReference,
siteUrl: formData.siteUrl,
documentPaths: formData.documentPaths,
includeMetadata: formData.includeMetadata,
expectedDocumentFormats: [{ extension: '.json', mimeType: 'application/json' }]
};
result = await readDocuments(readRequest);
break;
case 'upload':
const uploadRequest: SharePointUploadRequest = {
connectionReference: connectionReference,
siteUrl: formData.siteUrl,
documentPaths: formData.folderPaths,
documentList: formData.documentList,
fileNames: formData.fileNames,
expectedDocumentFormats: [{ extension: '.json', mimeType: 'application/json' }]
};
result = await uploadDocuments(uploadRequest);
break;
default:
throw new Error('Invalid operation');
}
console.log('Test result:', result);
} catch (error) {
console.error('Test execution failed:', error);
}
};
const renderConnectionCard = (connection: SharePointConnection) => {
const testResult = testResults[connection.id];
return (
<div
key={connection.id}
className={`${styles.connectionCard} ${selectedConnection === connection.id ? styles.active : ''}`}
onClick={() => setSelectedConnection(connection.id)}
>
<div className={styles.connectionInfo}>
<div className={styles.connectionName}>
{connection.externalUsername || connection.id}
</div>
<div className={styles.connectionStatus}>
<span className={`${styles.statusBadge} ${styles[connection.status]}`}>
{connection.status}
</span>
{connection.externalEmail && (
<span style={{ marginLeft: '8px' }}>{connection.externalEmail}</span>
)}
</div>
</div>
<div className={styles.connectionActions}>
<button
className={styles.button}
onClick={(e) => {
e.stopPropagation();
handleTestConnection(connection.id);
}}
disabled={isLoading}
>
Test
</button>
{testResult && (
<span className={testResult.success ? styles.successMessage : styles.errorMessage}>
{testResult.success ? '✓' : '✗'}
</span>
)}
</div>
</div>
);
};
const renderTestForm = () => {
return (
<div className={styles.requestPanel}>
<div className={styles.panelTitle}>Request Configuration</div>
<div className={styles.formGroup}>
<label className={styles.label}>Connection</label>
<select
className={styles.select}
value={selectedConnection}
onChange={(e) => setSelectedConnection(e.target.value)}
>
<option value="">Select a connection</option>
{connections.map(conn => (
<option key={conn.id} value={conn.id}>
{conn.externalUsername || conn.id} ({conn.status})
</option>
))}
</select>
</div>
<div className={styles.formGroup}>
<label className={styles.label}>SharePoint Site URL</label>
<input
type="text"
className={styles.input}
value={formData.siteUrl}
onChange={(e) => handleFormChange('siteUrl', e.target.value)}
placeholder="https://your-tenant.sharepoint.com/sites/your-site"
/>
</div>
{activeTab === 'list' && (
<>
<div className={styles.formGroup}>
<label className={styles.label}>Folder Paths (one per line)</label>
<textarea
className={styles.textarea}
value={formData.folderPaths.join('\n')}
onChange={(e) => handleArrayInputChange('folderPaths', e.target.value)}
placeholder="/Shared Documents&#10;/Documents"
/>
</div>
<div className={styles.formGroup}>
<label>
<input
type="checkbox"
checked={formData.includeSubfolders}
onChange={(e) => handleFormChange('includeSubfolders', e.target.checked)}
/>
Include Subfolders
</label>
</div>
</>
)}
{activeTab === 'find' && (
<>
<div className={styles.formGroup}>
<label className={styles.label}>Search Query</label>
<input
type="text"
className={styles.input}
value={formData.query}
onChange={(e) => handleFormChange('query', e.target.value)}
placeholder="quarterly report 2024"
/>
</div>
<div className={styles.formGroup}>
<label className={styles.label}>Search Scope</label>
<select
className={styles.select}
value={formData.searchScope}
onChange={(e) => handleFormChange('searchScope', e.target.value)}
>
<option value="all">All</option>
<option value="documents">Documents Only</option>
<option value="pages">Pages Only</option>
</select>
</div>
</>
)}
{activeTab === 'read' && (
<>
<div className={styles.formGroup}>
<label className={styles.label}>Document List Reference</label>
<input
type="text"
className={styles.input}
value={formData.documentList}
onChange={(e) => handleFormChange('documentList', e.target.value)}
placeholder="document_list_reference_from_chat"
/>
<div className={styles.helpText}>
This should be a reference from a chat session or document management system
</div>
</div>
<div className={styles.formGroup}>
<label className={styles.label}>Document Paths (one per line)</label>
<textarea
className={styles.textarea}
value={formData.documentPaths.join('\n')}
onChange={(e) => handleArrayInputChange('documentPaths', e.target.value)}
placeholder="/Shared Documents/file1.docx&#10;/Documents/file2.pdf"
/>
</div>
<div className={styles.formGroup}>
<label>
<input
type="checkbox"
checked={formData.includeMetadata}
onChange={(e) => handleFormChange('includeMetadata', e.target.checked)}
/>
Include Metadata
</label>
</div>
</>
)}
{activeTab === 'upload' && (
<>
<div className={styles.formGroup}>
<label className={styles.label}>Document List Reference</label>
<input
type="text"
className={styles.input}
value={formData.documentList}
onChange={(e) => handleFormChange('documentList', e.target.value)}
placeholder="document_list_reference_from_chat"
/>
</div>
<div className={styles.formGroup}>
<label className={styles.label}>Upload Destination Paths (one per line)</label>
<textarea
className={styles.textarea}
value={formData.folderPaths.join('\n')}
onChange={(e) => handleArrayInputChange('folderPaths', e.target.value)}
placeholder="/Shared Documents/&#10;/Documents/"
/>
</div>
<div className={styles.formGroup}>
<label className={styles.label}>File Names (one per line)</label>
<textarea
className={styles.textarea}
value={formData.fileNames.join('\n')}
onChange={(e) => handleArrayInputChange('fileNames', e.target.value)}
placeholder="uploaded_file1.docx&#10;uploaded_file2.pdf"
/>
</div>
</>
)}
<div className={styles.buttonGroup}>
<button
className={`${styles.button} ${styles.primary}`}
onClick={executeTest}
disabled={isLoading || !selectedConnection}
>
{isLoading ? 'Testing...' : `Test ${activeTab.charAt(0).toUpperCase() + activeTab.slice(1)}`}
</button>
<button
className={`${styles.button} ${styles.exampleButton}`}
onClick={() => loadExample(activeTab)}
>
Load Example
</button>
</div>
<div className={styles.requestPreview}>
<strong>Request Preview:</strong>
{JSON.stringify({
endpoint: `/api/test-sharepoint/${activeTab === 'list' ? 'list-documents' :
activeTab === 'find' ? 'find-documents' :
activeTab === 'read' ? 'read-documents' : 'upload-documents'}`,
method: 'POST',
body: activeTab === 'list' ? {
connectionReference: selectedConnection ?
`connection:${connections.find(c => c.id === selectedConnection)?.authority}:${connections.find(c => c.id === selectedConnection)?.externalUsername}:${selectedConnection}` :
'No connection selected',
siteUrl: formData.siteUrl,
folderPaths: formData.folderPaths,
includeSubfolders: formData.includeSubfolders
} : activeTab === 'find' ? {
connectionReference: selectedConnection ?
`connection:${connections.find(c => c.id === selectedConnection)?.authority}:${connections.find(c => c.id === selectedConnection)?.externalUsername}:${selectedConnection}` :
'No connection selected',
siteUrl: formData.siteUrl,
query: formData.query,
searchScope: formData.searchScope
} : activeTab === 'read' ? {
documentList: formData.documentList,
connectionReference: selectedConnection ?
`connection:${connections.find(c => c.id === selectedConnection)?.authority}:${connections.find(c => c.id === selectedConnection)?.externalUsername}:${selectedConnection}` :
'No connection selected',
siteUrl: formData.siteUrl,
documentPaths: formData.documentPaths,
includeMetadata: formData.includeMetadata
} : {
connectionReference: selectedConnection ?
`connection:${connections.find(c => c.id === selectedConnection)?.authority}:${connections.find(c => c.id === selectedConnection)?.externalUsername}:${selectedConnection}` :
'No connection selected',
siteUrl: formData.siteUrl,
documentPaths: formData.folderPaths,
documentList: formData.documentList,
fileNames: formData.fileNames
}
}, null, 2)}
</div>
</div>
);
};
const renderResponse = () => {
return (
<div className={styles.responsePanel}>
<div className={styles.panelTitle}>Response</div>
{error && (
<div className={styles.errorMessage}>
Error: {error}
</div>
)}
{lastResponse && (
<div className={`${styles.responseArea} ${lastResponse.success ? styles.success : styles.error}`}>
{JSON.stringify(lastResponse, null, 2)}
</div>
)}
{!lastResponse && !error && (
<div className={styles.responseArea}>
No response yet. Execute a test to see results here.
</div>
)}
</div>
);
};
return (
<div className={styles.container}>
<h1 className={styles.title}>SharePoint Method Testing</h1>
<div className={styles.section}>
<h2 className={styles.sectionTitle}>
Microsoft Connections ({connections.length})
</h2>
<div className={styles.buttonGroup} style={{ marginBottom: '15px' }}>
<button
className={`${styles.button} ${styles.exampleButton}`}
onClick={handleDebugTokens}
disabled={isLoading}
>
Debug Authentication Tokens
</button>
<button
className={`${styles.button} ${styles.exampleButton}`}
onClick={handleDebugTokenDetails}
disabled={isLoading}
>
Debug Token Details
</button>
</div>
{tokenDebugInfo && (
<div className={styles.responseArea} style={{ marginBottom: '15px' }}>
<strong>Token Debug Info:</strong>
<pre>{JSON.stringify(tokenDebugInfo, null, 2)}</pre>
</div>
)}
{connections.length === 0 ? (
<div className={styles.loading}>
{isLoading ? 'Loading connections...' : 'No Microsoft connections found. Please create a connection first.'}
</div>
) : (
<div className={styles.connectionsGrid}>
{connections.map(renderConnectionCard)}
</div>
)}
</div>
<div className={styles.section}>
<h2 className={styles.sectionTitle}>SharePoint Operations Testing</h2>
<div className={styles.tabs}>
<button
className={`${styles.tab} ${activeTab === 'list' ? styles.active : ''}`}
onClick={() => setActiveTab('list')}
>
List Documents
</button>
<button
className={`${styles.tab} ${activeTab === 'find' ? styles.active : ''}`}
onClick={() => setActiveTab('find')}
>
Find Documents
</button>
<button
className={`${styles.tab} ${activeTab === 'read' ? styles.active : ''}`}
onClick={() => setActiveTab('read')}
>
Read Documents
</button>
<button
className={`${styles.tab} ${activeTab === 'upload' ? styles.active : ''}`}
onClick={() => setActiveTab('upload')}
>
Upload Documents
</button>
</div>
<div className={styles.tabContent}>
<div className={styles.testArea}>
{renderTestForm()}
{renderResponse()}
</div>
</div>
</div>
</div>
);
}
export default TestSharepoint;

View file

@ -0,0 +1,20 @@
import styles from './HomeStyles/Workflows.module.css'
import sharedStyles from './HomeStyles/pages.module.css'
function Workflows () {
return (
<div className={sharedStyles.pageContainer}>
<div className={sharedStyles.pageCard}>
<h1 className={sharedStyles.pageTitle}>Workflows</h1>
<div className={sharedStyles.horizontalDivider}></div>
<div className={sharedStyles.contentArea}>
{/* Workflow content will go here */}
<p>Workflow management coming soon...</p>
</div>
</div>
</div>
);
}
export default Workflows;

View file

@ -1,27 +1,42 @@
.container {
display: flex;
min-height: 100vh;
background-color: var(--color-bg);
font-family: "DM Sans", sans-serif;
color: #181818;
}
.leftPanel {
.mainContent {
flex: 1;
display: flex;
flex-direction: column;
padding: 3rem;
background-color: var(--color-bg);
background-color: #181818;
}
.rightPanel {
.loginSection {
display: flex;
flex-direction: column;
align-items: center;
flex: 1;
background-color: var(--color-bg);
}
.logoText {
font-size: 35px;
display: flex;
align-items: center;
justify-content: center;
letter-spacing: -0.5px;
font-weight: 200;
}
.logo {
margin-bottom: 2rem;
.logoPower {
color: #E5E7EB;
}
.logoOn {
color: #F25843;
font-weight: 700;
}
.logo img {
@ -29,17 +44,24 @@
}
.loginBox {
max-width: 400px;
margin: auto;
width: 100%;
background-color: #181818;
width: 25%;
height: auto;
margin-top: 5%;
padding: 2rem;
border-radius: 25px;
border: 1px solid rgba(199, 197, 178, 0.15); /* washed-out color */
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.02),
0 0 10px rgba(0, 0, 0, 0.1);
}
.title {
font-size: 2rem;
font-weight: 600;
margin-bottom: 2rem;
color: var(--color-text);
font-family: var(--font-family);
font-family: "DM Sans", sans-serif;
color: #E5E7EB;
}
.loginForm {
@ -48,64 +70,142 @@
gap: 1rem;
}
.floatingLabelInput {
position: relative;
}
.label {
position: absolute;
left: 16px;
top: 50%;
transform: translateY(-50%);
color: #C7C5B2;
font-size: 1rem;
pointer-events: none;
transition: all 0.3s ease;
background-color: transparent;
font-family: var(--font-family);
}
.focusedLabel {
position: absolute;
left: 12px;
top: -8px;
transform: translateY(0);
color: #F25843;
font-size: 0.85rem;
pointer-events: none;
transition: all 0.3s ease;
background-color: #181818;
padding: 0 4px;
font-family: var(--font-family);
font-weight: 500;
}
.input {
width: 100%;
height: 50px;
padding: 12px 16px;
border: 1px solid var(--color-gray-disabled);
border-radius: 8px;
border-radius: 25px;
font-size: 1rem;
transition: all 0.2s ease;
background-color: var(--color-bg);
color: var(--color-text);
background-color: #181818;
color: #C7C5B2;
font-family: var(--font-family);
}
.input:focus {
outline: none;
border-color: var(--color-secondary);
box-shadow: 0 0 0 2px rgba(63, 81, 181, 0.1);
border-color: #F25843;
box-shadow: 0 0 0 2px rgba(242, 88, 67, 0.1);
}
.input::placeholder {
color: var(--color-gray);
color: transparent;
}
/* Fix browser autocomplete styling */
.input:-webkit-autofill,
.input:-webkit-autofill:hover,
.input:-webkit-autofill:focus,
.input:-webkit-autofill:active {
-webkit-box-shadow: 0 0 0 30px #181818 inset !important;
-webkit-text-fill-color: #E5E7EB !important;
background-color: #181818 !important;
transition: background-color 5000s ease-in-out 0s;
}
/* Ensure label background matches when autofilled */
.input:-webkit-autofill + .label,
.input:-webkit-autofill + .focusedLabel {
background-color: #181818 !important;
}
.disclaimer {
font-size: 0.8rem;
color: #E5E7EB;
text-align: center;
}
.button {
width: 100%;
height: 50px;
padding: 12px 20px;
border-radius: 8px;
border-radius: 25px;
font-size: 1rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
border: none;
text-align: center;
font-family: var(--font-family);
}
.primaryButton {
background-color: var(--color-secondary);
color: var(--color-bg);
.buttonContent {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.primaryButton:hover {
.microsoftIcon {
width: 18px;
height: 18px;
}
.loginButton {
background-color: #F25843;
color: #E5E7EB;
}
.loginButton:hover {
background-color: var(--color-secondary-hover);
}
.microsoftButton {
background-color: var(--color-text);
color: var(--color-bg);
background-color: #C7C5B2;
color: #181818;
}
.microsoftButton:hover {
background-color: var(--color-gray);
background-color: #D9D7C6;
}
.googleButton {
background-color: #C7C5B2;
color: #181818;
}
.googleButton:hover {
background-color: #D9D7C6;
}
.divider {
display: flex;
align-items: center;
text-align: center;
margin: 1rem 0;
}
.divider::before,
@ -117,9 +217,8 @@
.divider span {
padding: 0 1rem;
color: var(--color-gray);
font-size: 0.9rem;
font-family: var(--font-family);
color: #E5E7EB;
font-size: 0.8rem;
}
.registerLink {
@ -127,13 +226,11 @@
align-items: center;
justify-content: center;
gap: 0.5rem;
margin-top: 1rem;
}
.registerLink span {
color: var(--color-gray);
font-size: 0.9rem;
font-family: var(--font-family);
color: #E5E7EB;
font-size: 0.8rem;
}
.textButton {
@ -156,24 +253,12 @@ button:disabled {
cursor: not-allowed;
}
.rightContent {
max-width: 80%;
padding: 2rem;
}
.rightContent img {
width: 100%;
height: auto;
max-width: 500px;
}
.error {
color: var(--color-red);
background-color: var(--color-red-disabled);
border: 1px solid var(--color-red);
border-radius: 8px;
padding: 12px;
margin-bottom: 1rem;
font-size: 0.9rem;
text-align: center;
font-family: var(--font-family);

Some files were not shown because too many files have changed in this diff Show more