diff --git a/docs/API_ROUTES_DOCUMENTATION.md b/docs/API_ROUTES_DOCUMENTATION.md new file mode 100644 index 0000000..d790c1a --- /dev/null +++ b/docs/API_ROUTES_DOCUMENTATION.md @@ -0,0 +1,623 @@ +# API Routes Documentation + +## Overview + +This documentation covers the Chat Playground and Workflow management API endpoints. These routes provide functionality for managing AI workflows, chat interactions, messages, logs, and related data. + +## Authentication + +All endpoints require authentication via the `getCurrentUser` dependency, which validates the user's session/token. Rate limiting is applied at 120 requests per minute for all endpoints. + +--- + +## Chat Playground Routes + +Base path: `/api/chat/playground` + +### 1. Start Workflow + +**Endpoint:** `POST /api/chat/playground/start` + +**Description:** Starts a new workflow or continues an existing one based on the provided input. + +**Query Parameters:** +- `workflowId` (optional, string): ID of an existing workflow to continue +- `workflowMode` (string, default: "Actionplan"): Workflow execution mode + - `"Actionplan"`: Traditional task planning workflow + - `"React"`: Iterative react-style processing + +**Request Body:** `UserInputRequest` object containing: +```json +{ + "input": "user's input text", + "files": [...], // optional file attachments + "metadata": {...} // optional metadata +} +``` + +**Response:** `ChatWorkflow` object representing the created or continued workflow + +**Example Request:** +```bash +POST /api/chat/playground/start?workflowMode=Actionplan +Content-Type: application/json + +{ + "input": "Analyze the quarterly sales data", + "files": [...], + "metadata": {} +} +``` + +**Example Response:** +```json +{ + "id": "wf_123456", + "userId": "user_abc", + "status": "running", + "mode": "Actionplan", + "messageIds": [...], + "logIds": [...], + "createdAt": 1234567890.0, + ... +} +``` + +**Use Cases:** +- **User:** Initiating a new AI conversation or task +- **AI:** Starting automated workflow processing for scheduled tasks +- **Integration:** Continuing an interrupted workflow from a previous session + +--- + +### 2. Stop Workflow + +**Endpoint:** `POST /api/chat/playground/{workflowId}/stop` + +**Description:** Stops a running workflow, allowing graceful termination of ongoing processes. + +**Path Parameters:** +- `workflowId` (required, string): ID of the workflow to stop + +**Response:** `ChatWorkflow` object with updated status + +**Example Request:** +```bash +POST /api/chat/playground/wf_123456/stop +``` + +**Example Response:** +```json +{ + "id": "wf_123456", + "status": "stopped", + "stoppedAt": 1234567890.0, + ... +} +``` + +**Use Cases:** +- **User:** Manually stopping a long-running task +- **AI:** Implementing timeout mechanisms +- **System:** Cleanup of abandoned workflows + +--- + +### 3. Get Unified Chat Data + +**Endpoint:** `GET /api/chat/playground/{workflowId}/chatData` + +**Description:** Retrieves unified chat data including messages, logs, and statistics in chronological order. Supports incremental data fetching. + +**Path Parameters:** +- `workflowId` (required, string): ID of the workflow + +**Query Parameters:** +- `afterTimestamp` (optional, float): Unix timestamp to fetch only data created after this time + +**Response:** Dictionary containing: +- `messages`: Array of chat messages +- `logs`: Array of log entries +- `stats`: Array of statistics +- `documents`: Array of attached documents +- All sorted by `_createdAt` timestamp + +**Example Request:** +```bash +GET /api/chat/playground/wf_123456/chatData?afterTimestamp=1234567800.0 +``` + +**Example Response:** +```json +{ + "messages": [ + { + "id": "msg_001", + "content": "Hello", + "_createdAt": 1234567890.0, + ... + } + ], + "logs": [ + { + "id": "log_001", + "level": "info", + "message": "Processing started", + "_createdAt": 1234567891.0, + ... + } + ], + "stats": [...], + "documents": [...] +} +``` + +**Use Cases:** +- **User:** Real-time chat UI updates and polling for new data +- **AI:** Monitoring workflow progress and synchronizing state +- **Mobile App:** Efficient incremental data synchronization + +--- + +## Workflow Routes + +Base path: `/api/workflows` + +### 1. List All Workflows + +**Endpoint:** `GET /api/workflows/` + +**Description:** Retrieves all workflows for the currently authenticated user. + +**Response:** Array of `ChatWorkflow` objects + +**Example Request:** +```bash +GET /api/workflows/ +``` + +**Example Response:** +```json +[ + { + "id": "wf_001", + "userId": "user_abc", + "status": "completed", + "createdAt": 1234567890.0, + ... + }, + { + "id": "wf_002", + "userId": "user_abc", + "status": "running", + "createdAt": 1234567990.0, + ... + } +] +``` + +**Use Cases:** +- **User:** Dashboard showing all their workflows +- **AI:** Analyzing user's workflow history and patterns +- **Admin:** User activity monitoring + +--- + +### 2. Get Workflow by ID + +**Endpoint:** `GET /api/workflows/{workflowId}` + +**Description:** Retrieves detailed information about a specific workflow. + +**Path Parameters:** +- `workflowId` (required, string): ID of the workflow + +**Response:** `ChatWorkflow` object + +**Example Request:** +```bash +GET /api/workflows/wf_123456 +``` + +**Example Response:** +```json +{ + "id": "wf_123456", + "userId": "user_abc", + "status": "running", + "mode": "Actionplan", + "messageIds": ["msg_001", "msg_002"], + "logIds": ["log_001", "log_002"], + "createdAt": 1234567890.0, + "updatedAt": 1234567990.0, + ... +} +``` + +**Use Cases:** +- **User:** Viewing specific workflow details +- **AI:** Loading workflow state for processing +- **Debugging:** Investigating workflow behavior + +--- + +### 3. Update Workflow + +**Endpoint:** `PUT /api/workflows/{workflowId}` + +**Description:** Updates workflow properties. Requires ownership or modification permissions. + +**Path Parameters:** +- `workflowId` (required, string): ID of the workflow to update + +**Request Body:** Dictionary with fields to update: +```json +{ + "name": "Updated workflow name", + "description": "Updated description", + "tags": ["tag1", "tag2"], + ... +} +``` + +**Response:** Updated `ChatWorkflow` object + +**Example Request:** +```bash +PUT /api/workflows/wf_123456 +Content-Type: application/json + +{ + "name": "Sales Analysis Q4", + "tags": ["sales", "analysis"] +} +``` + +**Example Response:** +```json +{ + "id": "wf_123456", + "name": "Sales Analysis Q4", + "tags": ["sales", "analysis"], + ... +} +``` + +**Use Cases:** +- **User:** Renaming workflows for better organization +- **User:** Adding tags and metadata +- **AI:** Programmatically updating workflow metadata + +--- + +### 4. Get Workflow Status + +**Endpoint:** `GET /api/workflows/{workflowId}/status` + +**Description:** Gets the current status of a workflow without loading all associated data. + +**Path Parameters:** +- `workflowId` (required, string): ID of the workflow + +**Response:** `ChatWorkflow` object (lightweight status check) + +**Example Request:** +```bash +GET /api/workflows/wf_123456/status +``` + +**Use Response:** +```json +{ + "id": "wf_123456", + "status": "running", + "currentStep": "analyzing", + "progress": 45, + "updatedAt": 1234567990.0 +} +``` + +**Use Cases:** +- **User:** Quick status checks without loading full data +- **AI:** Monitoring workflow progress +- **Mobile:** Efficient polling for status updates + +--- + +### 5. Get Workflow Logs + +**Endpoint:** `GET /api/workflows/{workflowId}/logs` + +**Description:** Retrieves logs for a workflow with support for selective data transfer. + +**Path Parameters:** +- `workflowId` (required, string): ID of the workflow + +**Query Parameters:** +- `logId` (optional, string): Log ID to fetch only newer logs (selective data transfer) + +**Response:** Array of `ChatLog` objects + +**Example Request (All Logs):** +```bash +GET /api/workflows/wf_123456/logs +``` + +**Example Request (Incremental):** +```bash +GET /api/workflows/wf_123456/logs?logId=log_050 +``` + +**Example Response:** +```json +[ + { + "id": "log_001", + "workflowId": "wf_123456", + "level": "info", + "message": "Workflow started", + "timestamp": 1234567890.0, + ... + }, + { + "id": "log_002", + "workflowId": "wf_123456", + "level": "warning", + "message": "Slow response detected", + "timestamp": 1234567900.0, + ... + } +] +``` + +**Use Cases:** +- **User:** Viewing detailed execution logs for debugging +- **AI:** Error analysis and troubleshooting +- **System:** Audit trail and compliance + +--- + +### 6. Get Workflow Messages + +**Endpoint:** `GET /api/workflows/{workflowId}/messages` + +**Description:** Retrieves messages for a workflow with support for selective data transfer. + +**Path Parameters:** +- `workflowId` (required, string): ID of the workflow + +**Query Parameters:** +- `messageId` (optional, string): Message ID to fetch only newer messages + +**Response:** Array of `ChatMessage` objects + +**Example Request (All Messages):** +```bash +GET /api/workflows/wf_123456/messages +``` + +**Example Request (Incremental):** +```bash +GET /api/workflows/wf_123456/messages?messageId=msg_100 +``` + +**Example Response:** +```json +[ + { + "id": "msg_001", + "workflowId": "wf_123456", + "role": "user", + "content": "Analyze the data", + "timestamp": 1234567890.0, + "files": [...], + ... + }, + { + "id": "msg_002", + "workflowId": "wf_123456", + "role": "assistant", + "content": "Processing your request...", + "timestamp": 1234567900.0, + ... + } +] +``` + +**Use Cases:** +- **User:** Viewing conversation history +- **AI:** Context building for responses +- **UI:** Chat interface message display + +--- + +### 7. Delete Workflow + +**Endpoint:** `DELETE /api/workflows/{workflowId}` + +**Description:** Deletes a workflow and all its associated data (messages, logs, etc.). Requires ownership or deletion permissions. + +**Path Parameters:** +- `workflowId` (required, string): ID of the workflow to delete + +**Response:** Dictionary with deletion confirmation + +**Example Request:** +```bash +DELETE /api/workflows/wf_123456 +``` + +**Example Response:** +```json +{ + "id": "wf_123456", + "message": "Workflow and associated data deleted successfully" +} +``` + +**Use Cases:** +- **User:** Cleanup of old or unnecessary workflows +- **AI:** Automated cleanup of expired workflows +- **Admin:** Data management and compliance + +--- + +### 8. Delete Workflow Message + +**Endpoint:** `DELETE /api/workflows/{workflowId}/messages/{messageId}` + +**Description:** Deletes a specific message from a workflow. + +**Path Parameters:** +- `workflowId` (required, string): ID of the workflow +- `messageId` (required, string): ID of the message to delete + +**Response:** Dictionary with deletion confirmation + +**Example Request:** +```bash +DELETE /api/workflows/wf_123456/messages/msg_050 +``` + +**Example Response:** +```json +{ + "workflowId": "wf_123456", + "messageId": "msg_050", + "message": "Message deleted successfully" +} +``` + +**Use Cases:** +- **User:** Removing sensitive or incorrect messages +- **AI:** Content moderation +- **User:** Editing conversation history + +--- + +### 9. Delete File from Message + +**Endpoint:** `DELETE /api/workflows/{workflowId}/messages/{messageId}/files/{fileId}` + +**Description:** Deletes a file reference from a message within a workflow. + +**Path Parameters:** +- `workflowId` (required, string): ID of the workflow +- `messageId` (required, string): ID of the message +- `fileId` (required, string): ID of the file to delete + +**Response:** Dictionary with deletion confirmation + +**Example Request:** +```bash +DELETE /api/workflows/wf_123456/messages/msg_050/files/file_123 +``` + +**Example Response:** +```json +{ + "workflowId": "wf_123456", + "messageId": "msg_050", + "fileId": "file_123", + "message": "File reference deleted successfully" +} +``` + +**Use Cases:** +- **User:** Removing attached files for privacy +- **User:** Fixing incorrect file uploads +- **System:** Managing storage quotas + +--- + +## Error Handling + +All endpoints may return the following HTTP status codes: + +- `200 OK`: Successful request +- `400 Bad Request`: Invalid request parameters +- `401 Unauthorized`: Authentication required +- `403 Forbidden`: Insufficient permissions +- `404 Not Found`: Resource not found +- `500 Internal Server Error`: Server error + +**Example Error Response:** +```json +{ + "detail": "Error message describing what went wrong" +} +``` + +--- + +## Best Practices + +### For Users: + +1. **Polling Strategy**: Use the `afterTimestamp` or `logId`/`messageId` parameters to implement efficient polling without redundant data transfer +2. **Error Handling**: Always implement proper error handling and user feedback +3. **Rate Limiting**: Be aware of the 120 requests/minute limit and implement exponential backoff +4. **Cleanup**: Regularly delete old workflows to manage storage + +### For AI/Automation: + +1. **Incremental Updates**: Always use selective data transfer parameters to minimize bandwidth +2. **Status Checks**: Use lightweight endpoints like `/status` for monitoring +3. **Timeout Handling**: Implement proper stop logic for long-running workflows +4. **Logging**: Monitor logs for debugging and performance analysis + +### For Developers: + +1. **Caching**: Cache workflow status to reduce API calls +2. **Webhooks**: Consider implementing webhooks for real-time updates instead of polling +3. **Batch Operations**: Group related operations to reduce network overhead +4. **Error Recovery**: Implement retry logic with exponential backoff + +--- + +## Integration Examples + +### Starting a Workflow with File Upload + +```bash +# Start workflow +POST /api/chat/playground/start?workflowMode=Actionplan +{ + "input": "Analyze this report", + "files": [{"id": "file_001", "name": "report.pdf"}] +} + +# Poll for updates +GET /api/chat/playground/wf_123456/chatData + +# Continue polling with incremental updates +GET /api/chat/playground/wf_123456/chatData?afterTimestamp=1234567890.0 +``` + +### Monitoring Workflow Progress + +```bash +# Start workflow +POST /api/chat/playground/start + +# Check status periodically +GET /api/workflows/wf_123456/status + +# Get detailed logs if status indicates issues +GET /api/workflows/wf_123456/logs + +# Stop if needed +POST /api/chat/playground/wf_123456/stop +``` + +### Cleanup Old Workflows + +```bash +# List all workflows +GET /api/workflows/ + +# Filter for old/completed workflows and delete +DELETE /api/workflows/wf_old_123 +DELETE /api/workflows/wf_old_124 +``` + diff --git a/package-lock.json b/package-lock.json index 62181f6..4aaff0b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@azure/msal-browser": "^4.12.0", "@azure/msal-react": "^3.0.12", + "@types/leaflet": "^1.9.21", "@xstate/react": "^5.0.0", "axios": "^1.8.3", "dotenv": "^16.0.3", @@ -19,18 +20,22 @@ "js-cookie": "^3.0.5", "jsonwebtoken": "^9.0.2", "jwt-decode": "^4.0.0", + "leaflet": "^1.9.4", "motion": "^12.7.3", "pg": "^8.8.0", + "proj4": "^2.20.2", "react": "^19.1.0", "react-dom": "^19.1.0", "react-dropzone": "^14.3.8", "react-icons": "^5.5.0", + "react-leaflet": "^5.0.0", "react-router-dom": "^7.7.1", "xstate": "^5.20.1" }, "devDependencies": { "@eslint/js": "^9.30.1", "@types/node": "^24.7.2", + "@types/proj4": "^2.5.6", "@types/react": "^19.1.8", "@types/react-dom": "^19.1.6", "@vitejs/plugin-react": "^4.6.0", @@ -1073,6 +1078,17 @@ "node": ">= 8" } }, + "node_modules/@react-leaflet/core": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@react-leaflet/core/-/core-3.0.0.tgz", + "integrity": "sha512-3EWmekh4Nz+pGcr+xjf0KNyYfC3U2JjnkWsh0zcqaexYqmmB5ZhH37kz41JXGmKzpaMZCnPofBBm64i+YrEvGQ==", + "license": "Hippocratic-2.1", + "peerDependencies": { + "leaflet": "^1.9.0", + "react": "^19.0.0", + "react-dom": "^19.0.0" + } + }, "node_modules/@rolldown/pluginutils": { "version": "1.0.0-beta.27", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", @@ -1426,6 +1442,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "license": "MIT" + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -1433,6 +1455,15 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/leaflet": { + "version": "1.9.21", + "resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.9.21.tgz", + "integrity": "sha512-TbAd9DaPGSnzp6QvtYngntMZgcRk+igFELwR2N99XZn7RXUdKgsXMR+28bUO0rPsWp8MIu/f47luLIQuSLYv/w==", + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, "node_modules/@types/node": { "version": "24.7.2", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.7.2.tgz", @@ -1443,6 +1474,13 @@ "undici-types": "~7.14.0" } }, + "node_modules/@types/proj4": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@types/proj4/-/proj4-2.5.6.tgz", + "integrity": "sha512-zfMrPy9fx+8DchqM0kIUGeu2tTVB5ApO1KGAYcSGFS8GoqRIkyL41xq2yCx/iV3sOLzo7v4hEgViSLTiPI1L0w==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/react": { "version": "19.1.8", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.8.tgz", @@ -3683,6 +3721,12 @@ "json-buffer": "3.0.1" } }, + "node_modules/leaflet": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz", + "integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==", + "license": "BSD-2-Clause" + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -3840,6 +3884,12 @@ "node": ">= 0.6" } }, + "node_modules/mgrs": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/mgrs/-/mgrs-1.0.0.tgz", + "integrity": "sha512-awNbTOqCxK1DBGjalK3xqWIstBZgN6fxsMSiXLs9/spqWkF2pAhb2rrYCFSsr1/tT7PhcDGjZndG8SWYn0byYA==", + "license": "MIT" + }, "node_modules/micromatch": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", @@ -4371,6 +4421,19 @@ "node": ">= 0.8.0" } }, + "node_modules/proj4": { + "version": "2.20.2", + "resolved": "https://registry.npmjs.org/proj4/-/proj4-2.20.2.tgz", + "integrity": "sha512-ipfBRfQly0HhHTO7hnC1GfaX8bvroO7VV4KH889ehmADSE8C/qzp2j+Jj6783S9Tj6c2qX/hhYm7oH0kgXzBAA==", + "license": "MIT", + "dependencies": { + "mgrs": "1.0.0", + "wkt-parser": "^1.5.1" + }, + "funding": { + "url": "https://github.com/sponsors/ahocevar" + } + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -4524,6 +4587,20 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "license": "MIT" }, + "node_modules/react-leaflet": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/react-leaflet/-/react-leaflet-5.0.0.tgz", + "integrity": "sha512-CWbTpr5vcHw5bt9i4zSlPEVQdTVcML390TjeDG0cK59z1ylexpqC6M1PJFjV8jD7CF+ACBFsLIDs6DRMoLEofw==", + "license": "Hippocratic-2.1", + "dependencies": { + "@react-leaflet/core": "^3.0.0" + }, + "peerDependencies": { + "leaflet": "^1.9.0", + "react": "^19.0.0", + "react-dom": "^19.0.0" + } + }, "node_modules/react-refresh": { "version": "0.17.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", @@ -5302,6 +5379,12 @@ "node": ">= 8" } }, + "node_modules/wkt-parser": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/wkt-parser/-/wkt-parser-1.5.2.tgz", + "integrity": "sha512-1ZUiV1FTwSiSrgWzV9KXJuOF2BVW91KY/mau04BhnmgOdroRQea7Q0s5TVqwGLm0D2tZwObd/tBYXW49sSxp3Q==", + "license": "MIT" + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", diff --git a/package.json b/package.json index 08dc3aa..7f632da 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "dependencies": { "@azure/msal-browser": "^4.12.0", "@azure/msal-react": "^3.0.12", + "@types/leaflet": "^1.9.21", "@xstate/react": "^5.0.0", "axios": "^1.8.3", "dotenv": "^16.0.3", @@ -25,18 +26,22 @@ "js-cookie": "^3.0.5", "jsonwebtoken": "^9.0.2", "jwt-decode": "^4.0.0", + "leaflet": "^1.9.4", "motion": "^12.7.3", "pg": "^8.8.0", + "proj4": "^2.20.2", "react": "^19.1.0", "react-dom": "^19.1.0", "react-dropzone": "^14.3.8", "react-icons": "^5.5.0", + "react-leaflet": "^5.0.0", "react-router-dom": "^7.7.1", "xstate": "^5.20.1" }, "devDependencies": { "@eslint/js": "^9.30.1", "@types/node": "^24.7.2", + "@types/proj4": "^2.5.6", "@types/react": "^19.1.8", "@types/react-dom": "^19.1.6", "@vitejs/plugin-react": "^4.6.0", diff --git a/src/App.tsx b/src/App.tsx index c53dd79..6bb6130 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -7,9 +7,11 @@ import './index.css'; import Login from './pages/Login'; import Register from './pages/Register'; -import { AuthProvider } from './auth/authProvider'; -import { ProtectedRoute } from './auth/ProtectedRoute'; -import { LanguageProvider } from './contexts/LanguageContext'; +import { AuthProvider } from './providers/auth/AuthProvider'; +import { ProtectedRoute } from './providers/auth/ProtectedRoute'; +import { LanguageProvider } from './providers/language/LanguageContext'; +import { WorkflowSelectionProvider } from './contexts/WorkflowSelectionContext'; +import { FileProvider } from './contexts/FileContext'; import Home from './pages/Home/Home'; function App() { @@ -37,21 +39,25 @@ function App() { return ( - - - {/* Public route */} - } /> - } /> - - - - }> - {/* All page routing is now handled by the Page Loader in Home.tsx */} - - - - + + + + + {/* Public route */} + } /> + } /> + + + + }> + {/* All page routing is now handled by the Page Loader in Home.tsx */} + + + + + + ); diff --git a/src/api.ts b/src/api.ts index 3fc344f..8beb9ac 100644 --- a/src/api.ts +++ b/src/api.ts @@ -1,6 +1,7 @@ // api.ts import axios from 'axios'; -import { addCSRFTokenToHeaders } from './utils/csrfUtils'; +import { addCSRFTokenToHeaders, getCSRFToken, generateAndStoreCSRFToken } from './utils/csrfUtils'; +import { clearUserDataCache } from './utils/userCache'; // Utility function to resolve hostname to IP address const resolveHostnameToIP = async (hostname: string): Promise => { @@ -52,12 +53,29 @@ api.interceptors.request.use( } } - // Authentication is now handled automatically via httpOnly cookies - // Browser will send cookies automatically with credentials: 'include' - console.log('🍪 Using httpOnly cookies for authentication (automatic)'); + // Check for auth token in localStorage and add to headers + const authToken = localStorage.getItem('authToken'); + if (authToken && config.headers) { + config.headers.Authorization = `Bearer ${authToken}`; + console.log('🔑 Using Bearer token for authentication'); + } else { + // Fallback: httpOnly cookies + console.log('🍪 Using httpOnly cookies for authentication (automatic)'); + } - // Add CSRF token to all requests (except GET requests) - if (config.method && ['post', 'put', 'patch', 'delete'].includes(config.method.toLowerCase())) { + // Add CSRF token to all requests (including GET requests for certain endpoints) + // Some endpoints like /api/realestate/* require CSRF tokens even for GET requests + const method = config.method?.toLowerCase(); + const url = config.url || ''; + const requiresCSRF = + ['post', 'put', 'patch', 'delete'].includes(method || '') || + url.includes('/api/realestate/'); + + if (requiresCSRF) { + // Ensure CSRF token exists, generate one if missing + if (!getCSRFToken()) { + generateAndStoreCSRFToken(); + } addCSRFTokenToHeaders(config.headers as Record); } @@ -80,8 +98,8 @@ api.interceptors.response.use( if (!isLoginEndpoint) { // Clear local auth data (httpOnly cookies are cleared by backend) - localStorage.removeItem('auth_authority'); - localStorage.removeItem('currentUser'); + sessionStorage.removeItem('auth_authority'); + clearUserDataCache(); // Redirect to login window.location.href = '/login'; } diff --git a/src/api/workflowApi.ts b/src/api/workflowApi.ts new file mode 100644 index 0000000..ad3f0d0 --- /dev/null +++ b/src/api/workflowApi.ts @@ -0,0 +1,309 @@ +import { ApiRequestOptions } from '../hooks/useApi'; + +// ============================================================================ +// TYPES & INTERFACES +// ============================================================================ + +// Workflow interfaces +export interface Workflow { + id: string; + mandateId: string; + status: string; + name?: string; + workflowMode?: string; + [key: string]: any; // Allow additional properties +} + +export interface WorkflowMessage { + [key: string]: any; +} + +export interface WorkflowStats { + [key: string]: any; +} + +export interface WorkflowDocument { + [key: string]: any; +} + +export interface WorkflowLog { + [key: string]: any; +} + +// Request/Response interfaces based on API documentation +export interface FileAttachment { + id: string; + name: string; +} + +export interface UserInputRequest { + input: string; + files?: FileAttachment[]; // optional file attachments - array of {id, name} objects + metadata?: Record; // optional metadata +} + +export interface StartWorkflowRequest { + prompt: string; + listFileId?: string[]; // Array of file ID strings (files must be uploaded first via /api/files/upload) + userLanguage?: string; // Optional, defaults to "en" + metadata?: Record; +} + +export interface StartWorkflowResponse extends Workflow { + // Workflow object returned from start endpoint +} + +export interface ChatDataResponse { + messages: WorkflowMessage[]; + logs: WorkflowLog[]; + stats: WorkflowStats[]; + documents: WorkflowDocument[]; +} + +// Type for the request function passed to API functions +export type ApiRequestFunction = (options: ApiRequestOptions) => Promise; + +// ============================================================================ +// API REQUEST FUNCTIONS +// ============================================================================ + +/** + * Fetch all workflows for the current user + * Endpoint: GET /api/workflows/ + */ +export async function fetchWorkflows(request: ApiRequestFunction): Promise { + const data = await request({ + url: '/api/workflows/', + method: 'get' + }); + return Array.isArray(data) ? data : []; +} + +/** + * Fetch a single workflow by ID + * Endpoint: GET /api/workflows/{workflowId} + */ +export async function fetchWorkflow( + request: ApiRequestFunction, + workflowId: string +): Promise { + return await request({ + url: `/api/workflows/${workflowId}`, + method: 'get' + }); +} + +/** + * Fetch workflow status (lightweight status check) + * Endpoint: GET /api/workflows/{workflowId}/status + */ +export async function fetchWorkflowStatus( + request: ApiRequestFunction, + workflowId: string +): Promise { + return await request({ + url: `/api/workflows/${workflowId}/status`, + method: 'get' + }); +} + +/** + * Fetch workflow messages + * Endpoint: GET /api/workflows/{workflowId}/messages + * Query params: messageId (optional) - fetch only newer messages + */ +export async function fetchWorkflowMessages( + request: ApiRequestFunction, + workflowId: string, + messageId?: string +): Promise { + const params = messageId ? { messageId } : undefined; + const data = await request({ + url: `/api/workflows/${workflowId}/messages`, + method: 'get', + params + }); + return Array.isArray(data) ? data : []; +} + +/** + * Fetch workflow logs + * Endpoint: GET /api/workflows/{workflowId}/logs + * Query params: logId (optional) - fetch only newer logs + */ +export async function fetchWorkflowLogs( + request: ApiRequestFunction, + workflowId: string, + logId?: string +): Promise { + const params = logId ? { logId } : undefined; + const data = await request({ + url: `/api/workflows/${workflowId}/logs`, + method: 'get', + params + }); + return Array.isArray(data) ? data : []; +} + +/** + * Fetch unified chat data (messages, logs, stats, documents) + * Endpoint: GET /api/chat/playground/{workflowId}/chatData + * Query params: afterTimestamp (optional) - fetch only data created after this time + */ +export async function fetchChatData( + request: ApiRequestFunction, + workflowId: string, + afterTimestamp?: number +): Promise { + const params = afterTimestamp ? { afterTimestamp: afterTimestamp.toString() } : undefined; + const requestConfig = { + url: `/api/chat/playground/${workflowId}/chatData`, + method: 'get' as const, + params + }; + + console.log('📤 fetchChatData request:', requestConfig); + + const data = await request(requestConfig); + + console.log('📥 fetchChatData response:', data); + + // Ensure all arrays exist + return { + messages: Array.isArray(data.messages) ? data.messages : [], + logs: Array.isArray(data.logs) ? data.logs : [], + stats: Array.isArray(data.stats) ? data.stats : [], + documents: Array.isArray(data.documents) ? data.documents : [] + }; +} + +/** + * Start a new workflow or continue an existing one + * Endpoint: POST /api/chat/playground/start + * Query params: workflowId (optional), workflowMode (default: "Actionplan") + */ +export async function startWorkflowApi( + request: ApiRequestFunction, + workflowData: StartWorkflowRequest, + options?: { workflowId?: string; workflowMode?: 'Actionplan' | 'React' } +): Promise { + const params: Record = {}; + if (options?.workflowId) { + params.workflowId = options.workflowId; + } + if (options?.workflowMode) { + params.workflowMode = options.workflowMode; + } + + const requestConfig = { + url: '/api/chat/playground/start', + method: 'post' as const, + data: workflowData, + params: Object.keys(params).length > 0 ? params : undefined + }; + + console.log('📤 startWorkflow request:', requestConfig); + + const response = await request(requestConfig); + + console.log('📥 startWorkflow response:', response); + + return response; +} + +/** + * Stop a running workflow + * Endpoint: POST /api/chat/playground/{workflowId}/stop + */ +export async function stopWorkflowApi( + request: ApiRequestFunction, + workflowId: string +): Promise { + await request({ + url: `/api/chat/playground/${workflowId}/stop`, + method: 'post' + }); +} + +/** + * Update workflow properties + * Endpoint: PUT /api/workflows/{workflowId} + */ +export async function updateWorkflowApi( + request: ApiRequestFunction, + workflowId: string, + updateData: Partial<{ name: string; description?: string; tags?: string[] }> +): Promise { + return await request({ + url: `/api/workflows/${workflowId}`, + method: 'put', + data: updateData + }); +} + +/** + * Delete a workflow and all associated data + * Endpoint: DELETE /api/workflows/{workflowId} + */ +export async function deleteWorkflowApi( + request: ApiRequestFunction, + workflowId: string +): Promise { + await request({ + url: `/api/workflows/${workflowId}`, + method: 'delete' + }); +} + +/** + * Delete multiple workflows + */ +export async function deleteWorkflowsApi( + request: ApiRequestFunction, + workflowIds: string[] +): Promise { + // Delete workflows one by one since there's no bulk delete endpoint + const deletePromises = workflowIds.map(workflowId => + request({ + url: `/api/workflows/${workflowId}`, + method: 'delete' + }).catch(error => { + console.error(`Failed to delete workflow ${workflowId}:`, error); + throw error; + }) + ); + + await Promise.all(deletePromises); +} + +/** + * Delete a message from a workflow + * Endpoint: DELETE /api/workflows/{workflowId}/messages/{messageId} + */ +export async function deleteMessageApi( + request: ApiRequestFunction, + workflowId: string, + messageId: string +): Promise { + await request({ + url: `/api/workflows/${workflowId}/messages/${messageId}`, + method: 'delete' + }); +} + +/** + * Delete a file reference from a message + * Endpoint: DELETE /api/workflows/{workflowId}/messages/{messageId}/files/{fileId} + */ +export async function deleteFileFromMessageApi( + request: ApiRequestFunction, + workflowId: string, + messageId: string, + fileId: string +): Promise { + await request({ + url: `/api/workflows/${workflowId}/messages/${messageId}/files/${fileId}`, + method: 'delete' + }); +} + diff --git a/src/components/Connections/ConnectionEditModal.module.css b/src/components/Connections/ConnectionEditModal.module.css deleted file mode 100644 index bef2cd8..0000000 --- a/src/components/Connections/ConnectionEditModal.module.css +++ /dev/null @@ -1,61 +0,0 @@ -.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; - } -} \ No newline at end of file diff --git a/src/components/Connections/ConnectionEditModal.tsx b/src/components/Connections/ConnectionEditModal.tsx deleted file mode 100644 index 3d4581d..0000000 --- a/src/components/Connections/ConnectionEditModal.tsx +++ /dev/null @@ -1,50 +0,0 @@ - -import { Popup, EditForm } from '../ui/Popup'; -import styles from './ConnectionEditModal.module.css'; -import { ConnectionEditModalProps } from './connectionsInterfaces'; -import { useLanguage } from '../../contexts/LanguageContext'; - -export function ConnectionEditModal({ - isOpen, - connection, - fields, - onSave, - onCancel -}: ConnectionEditModalProps) { - const { t } = 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 ( - - - - ); -} - -export default ConnectionEditModal; \ No newline at end of file diff --git a/src/components/Connections/ConnectionsErrorDisplay.module.css b/src/components/Connections/ConnectionsErrorDisplay.module.css deleted file mode 100644 index 0c0a15e..0000000 --- a/src/components/Connections/ConnectionsErrorDisplay.module.css +++ /dev/null @@ -1,47 +0,0 @@ -.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; - } -} \ No newline at end of file diff --git a/src/components/Connections/ConnectionsErrorDisplay.tsx b/src/components/Connections/ConnectionsErrorDisplay.tsx deleted file mode 100644 index a4a006b..0000000 --- a/src/components/Connections/ConnectionsErrorDisplay.tsx +++ /dev/null @@ -1,41 +0,0 @@ - -import styles from './ConnectionsErrorDisplay.module.css'; -import { ConnectionsErrorDisplayProps } from './connectionsInterfaces'; -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 ( -
- {error && ( -
- {t('connections.error', 'Error')}: {error} -
- )} - - {connectError && ( -
- {t('connections.connection_error', 'Connection Error')}: {connectError} -
- )} - - {disconnectError && ( -
- {t('connections.disconnect_error', 'Disconnect Error')}: {disconnectError} -
- )} -
- ); -} - -export default ConnectionsErrorDisplay; \ No newline at end of file diff --git a/src/components/Connections/ConnectionsTable.module.css b/src/components/Connections/ConnectionsTable.module.css deleted file mode 100644 index 22bd13b..0000000 --- a/src/components/Connections/ConnectionsTable.module.css +++ /dev/null @@ -1,61 +0,0 @@ -.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); -} - -/* Expired connection highlighting */ -.connectionsTable :global(.expired-connection) { - background-color: rgba(255, 193, 7, 0.1) !important; - color: #ff6b35 !important; - font-weight: 600; -} - -.connectionsTable :global(.tr:hover .expired-connection) { - background-color: rgba(255, 193, 7, 0.2) !important; -} - -/* 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; - } -} \ No newline at end of file diff --git a/src/components/Connections/ConnectionsTable.tsx b/src/components/Connections/ConnectionsTable.tsx deleted file mode 100644 index bec3dc3..0000000 --- a/src/components/Connections/ConnectionsTable.tsx +++ /dev/null @@ -1,46 +0,0 @@ -// import React from 'react'; -import { FormGenerator } from '../FormGenerator'; -import styles from './ConnectionsTable.module.css'; -import { ConnectionsTableProps } from './connectionsInterfaces'; -import { useLanguage } from '../../contexts/LanguageContext'; - -export function ConnectionsTable({ - connections, - columns, - actions, - isLoading = false, - isConnecting = false, - isDisconnecting = false, - onRowSelect, - onDelete, - onDeleteMultiple -}: ConnectionsTableProps) { - const { t } = useLanguage(); - - const loading = isLoading || isConnecting || isDisconnecting; - - return ( -
- -
- ); -} - -export default ConnectionsTable; \ No newline at end of file diff --git a/src/components/Connections/connectionsInterfaces.ts b/src/components/Connections/connectionsInterfaces.ts deleted file mode 100644 index 131ea7c..0000000 --- a/src/components/Connections/connectionsInterfaces.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { ColumnConfig } from '../FormGenerator'; -import { EditFieldConfig } from '../ui/Popup'; - -// Re-export connection-related interfaces from hooks -export type { Connection, CreateConnectionData } from '../../hooks/useConnections'; -import type { Connection } from '../../hooks/useConnections'; - -// Component Props Interfaces -export interface ConnectionsTableProps { - connections: Connection[]; - columns: ColumnConfig[]; - actions: TableAction[]; - isLoading?: boolean; - isConnecting?: boolean; - isDisconnecting?: boolean; - onRowSelect?: (selectedRows: Connection[]) => void; - onDelete?: (connection: Connection) => Promise; - onDeleteMultiple?: (connections: Connection[]) => Promise; -} - -export interface ConnectionEditModalProps { - isOpen: boolean; - connection: Connection | null; - fields: EditFieldConfig[]; - onSave: (updatedConnection: Connection) => Promise; - onCancel: () => void; -} - -export interface ConnectionsErrorDisplayProps { - error?: string | null; - connectError?: string | null; - disconnectError?: string | null; -} - -// Table Action Interface -export interface TableAction { - type: 'edit' | 'delete' | 'download' | 'view' | 'copy'; - title?: string | ((connection: Connection) => string); - onAction?: (connection: Connection) => Promise | void; - disabled?: (connection: Connection) => boolean | { disabled: boolean; message?: string }; - loading?: (connection: Connection) => boolean; -} - -// 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; - handleConnect: (connection: Connection) => Promise; - handleDisconnect: (connection: Connection) => Promise; - handleDelete: (connection: Connection) => Promise; - handleDeleteMultiple: (connections: Connection[]) => Promise; - handleUpdateConnection: (connection: Connection) => Promise; - handleEditConnection: (connection: Connection) => Promise; - handleSaveConnection: (updatedConnection: Connection) => Promise; - 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[]; -} \ No newline at end of file diff --git a/src/components/Connections/connectionsLogic.tsx b/src/components/Connections/connectionsLogic.tsx deleted file mode 100644 index c170b98..0000000 --- a/src/components/Connections/connectionsLogic.tsx +++ /dev/null @@ -1,479 +0,0 @@ -import { useEffect, useState } from 'react'; - - - -import { useConnections, useOAuthConnect, useDisconnect } from '../../hooks/useConnections'; -import { useLanguage } from '../../contexts/LanguageContext'; -import { ColumnConfig } from '../FormGenerator'; -import { EditFieldConfig } from '../ui/Popup'; -import { - Connection, - CreateConnectionData, - ConnectionsLogicReturn, - TableAction -} from './connectionsInterfaces'; - -export function useConnectionsLogic(): ConnectionsLogicReturn { - const { t } = useLanguage(); - - // Hooks - const { - connections, - fetchConnections, - createConnection, - updateConnection, - deleteConnection, - refreshMicrosoftToken, - refreshGoogleToken, - 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(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: number) => { - if (!value) return t('connections.not_available', 'N/A'); - try { - // Convert from seconds to milliseconds for Date constructor - const date = new Date(value * 1000); - const year = date.getFullYear(); - const month = String(date.getMonth() + 1).padStart(2, '0'); - const day = String(date.getDate()).padStart(2, '0'); - const hours = String(date.getHours()).padStart(2, '0'); - const minutes = String(date.getMinutes()).padStart(2, '0'); - const seconds = String(date.getSeconds()).padStart(2, '0'); - const timezoneOffset = date.getTimezoneOffset(); - const offsetHours = Math.floor(Math.abs(timezoneOffset) / 60); - const offsetMinutes = Math.abs(timezoneOffset) % 60; - const offsetSign = timezoneOffset <= 0 ? '+' : '-'; - const timezone = `GMT${offsetSign}${offsetHours}${offsetMinutes > 0 ? ':' + offsetMinutes.toString().padStart(2, '0') : ''}`; - return `${year}-${month}-${day} ${hours}:${minutes}:${seconds} ${timezone}`; - } catch { - return t('connections.invalid_date', 'Invalid Date'); - } - } - }, - { - key: 'lastChecked', - label: t('connections.field.last_checked', 'Last Checked'), - type: 'readonly', - editable: false, - formatter: (value: number) => { - if (!value) return t('connections.not_available', 'N/A'); - try { - // Convert from seconds to milliseconds for Date constructor - const date = new Date(value * 1000); - const year = date.getFullYear(); - const month = String(date.getMonth() + 1).padStart(2, '0'); - const day = String(date.getDate()).padStart(2, '0'); - const hours = String(date.getHours()).padStart(2, '0'); - const minutes = String(date.getMinutes()).padStart(2, '0'); - const seconds = String(date.getSeconds()).padStart(2, '0'); - const timezoneOffset = date.getTimezoneOffset(); - const offsetHours = Math.floor(Math.abs(timezoneOffset) / 60); - const offsetMinutes = Math.abs(timezoneOffset) % 60; - const offsetSign = timezoneOffset <= 0 ? '+' : '-'; - const timezone = `GMT${offsetSign}${offsetHours}${offsetMinutes > 0 ? ':' + offsetMinutes.toString().padStart(2, '0') : ''}`; - return `${year}-${month}-${day} ${hours}:${minutes}:${seconds} ${timezone}`; - } catch { - 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) => { - const status = value?.charAt(0).toUpperCase() + value?.slice(1) || t('connections.unknown', 'Unknown'); - return status; - }, - width: 120, - sortable: true, - filterable: true, - cellClassName: (value: string) => { - return value === 'expired' ? 'expired-connection' : ''; - } - }, - { - 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, - formatter: (value: number) => { - if (!value) return t('connections.not_available', 'N/A'); - try { - // Convert from seconds to milliseconds for Date constructor - const date = new Date(value * 1000); - const year = date.getFullYear(); - const month = String(date.getMonth() + 1).padStart(2, '0'); - const day = String(date.getDate()).padStart(2, '0'); - const hours = String(date.getHours()).padStart(2, '0'); - const minutes = String(date.getMinutes()).padStart(2, '0'); - const seconds = String(date.getSeconds()).padStart(2, '0'); - const timezoneOffset = date.getTimezoneOffset(); - const offsetHours = Math.floor(Math.abs(timezoneOffset) / 60); - const offsetMinutes = Math.abs(timezoneOffset) % 60; - const offsetSign = timezoneOffset <= 0 ? '+' : '-'; - const timezone = `GMT${offsetSign}${offsetHours}${offsetMinutes > 0 ? ':' + offsetMinutes.toString().padStart(2, '0') : ''}`; - return `${year}-${month}-${day} ${hours}:${minutes}:${seconds} ${timezone}`; - } catch { - return t('connections.invalid_date', 'Invalid Date'); - } - } - }, - { - key: 'lastChecked', - label: t('connections.field.last_checked', 'Last Checked'), - type: 'date', - width: 150, - sortable: true, - filterable: true, - searchable: true, - formatter: (value: number) => { - if (!value) return t('connections.not_available', 'N/A'); - try { - // Convert from seconds to milliseconds for Date constructor - const date = new Date(value * 1000); - const year = date.getFullYear(); - const month = String(date.getMonth() + 1).padStart(2, '0'); - const day = String(date.getDate()).padStart(2, '0'); - const hours = String(date.getHours()).padStart(2, '0'); - const minutes = String(date.getMinutes()).padStart(2, '0'); - const seconds = String(date.getSeconds()).padStart(2, '0'); - const timezoneOffset = date.getTimezoneOffset(); - const offsetHours = Math.floor(Math.abs(timezoneOffset) / 60); - const offsetMinutes = Math.abs(timezoneOffset) % 60; - const offsetSign = timezoneOffset <= 0 ? '+' : '-'; - const timezone = `GMT${offsetSign}${offsetHours}${offsetMinutes > 0 ? ':' + offsetMinutes.toString().padStart(2, '0') : ''}`; - return `${year}-${month}-${day} ${hours}:${minutes}:${seconds} ${timezone}`; - } catch { - return t('connections.invalid_date', 'Invalid Date'); - } - } - }, - { - key: 'expiresAt', - label: t('connections.field.expires_at', 'Expires At'), - type: 'date', - width: 150, - sortable: true, - filterable: true, - formatter: (value: number) => { - if (!value) return t('connections.not_available', 'N/A'); - try { - // Convert from seconds to milliseconds for Date constructor - const date = new Date(value * 1000); - const year = date.getFullYear(); - const month = String(date.getMonth() + 1).padStart(2, '0'); - const day = String(date.getDate()).padStart(2, '0'); - const hours = String(date.getHours()).padStart(2, '0'); - const minutes = String(date.getMinutes()).padStart(2, '0'); - const seconds = String(date.getSeconds()).padStart(2, '0'); - const timezoneOffset = date.getTimezoneOffset(); - const offsetHours = Math.floor(Math.abs(timezoneOffset) / 60); - const offsetMinutes = Math.abs(timezoneOffset) % 60; - const offsetSign = timezoneOffset <= 0 ? '+' : '-'; - const timezone = `GMT${offsetSign}${offsetHours}${offsetMinutes > 0 ? ':' + offsetMinutes.toString().padStart(2, '0') : ''}`; - return `${year}-${month}-${day} ${hours}:${minutes}:${seconds} ${timezone}`; - } catch { - return t('connections.invalid_date', 'Invalid Date'); - } - } - } - ]; - - // Fetch connections on mount and auto-connect them - useEffect(() => { - const initializeConnections = async () => { - try { - await fetchConnections(); - // Auto-connect all connections that are not active - const connectionsToConnect = connections.filter(conn => - conn.status !== 'active' && conn.authority !== 'local' - ); - - for (const connection of connectionsToConnect) { - try { - await connectWithPopup(connection.id); - } catch (error) { - console.warn(`Failed to auto-connect ${connection.authority} connection:`, error); - } - } - } catch (error) { - console.error('Error initializing connections:', error); - } - }; - - initializeConnections(); - }, []); - - // Handler functions - - 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 handleCreateConnection = async (type: 'msft' | 'google') => { - console.log('Creating connection for type:', type); - try { - // Get current UTC timestamp in seconds (float) to match backend - const currentTimestamp = Math.floor(Date.now() / 1000); - - const connectionData: CreateConnectionData = { - type: type, - status: 'pending', - connectedAt: currentTimestamp, - lastChecked: currentTimestamp - }; - - console.log('Sending connection data to backend:', connectionData); - const newConnection = await createConnection(connectionData); - console.log('Connection created successfully:', newConnection); - handleConnect(newConnection); - await fetchConnections(); - } catch (error) { - console.error('Error creating connection:', 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 handleDeleteMultiple = async (connections: Connection[]) => { - const confirmMessage = t('connections.confirm_delete_multiple', 'Are you sure you want to delete {count} connections?').replace('{count}', connections.length.toString()); - - if (window.confirm(confirmMessage)) { - try { - // Delete all connections in parallel - await Promise.all(connections.map(connection => deleteConnection(connection.id))); - await fetchConnections(); - } catch (error) { - console.error('Error deleting multiple connections:', error); - } - } - }; - - const handleUpdateConnection = async (connection: Connection) => { - console.log('Updating connection:', connection); - try { - if (connection.status === 'expired') { - // Refresh token based on connection type - if (connection.authority === 'msft') { - await refreshMicrosoftToken(connection.id); - } else if (connection.authority === 'google') { - await refreshGoogleToken(connection.id); - } - } else if (connection.status !== 'active') { - // If not active and not expired, try to connect - await handleConnect(connection); - } - await fetchConnections(); - } catch (error) { - console.error('Error updating connection:', error); - } - }; - - 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[] = [ - { - type: 'edit', - title: t('connections.action.edit', 'Edit'), - onAction: handleEditConnection - }, - { - type: 'delete', - title: t('connections.action.delete', 'Delete') - // onAction is handled by FormGenerator for delete confirmation - } - ] as any; - - return { - // Data - connections, - isLoading, - isConnecting, - isDisconnecting, - error, - connectError, - disconnectError, - editPopupOpen, - editingConnection, - connectionColumns, - connectionEditFields, - tableActions, - - // Handlers - handleCreateConnection, - handleConnect, - handleDisconnect, - handleDelete, - handleDeleteMultiple, - handleUpdateConnection, - handleEditConnection, - handleSaveConnection, - handleCancelEdit - }; -} \ No newline at end of file diff --git a/src/components/Connections/index.ts b/src/components/Connections/index.ts deleted file mode 100644 index 50ef52a..0000000 --- a/src/components/Connections/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -// Export all components -export { ConnectionsTable } from './ConnectionsTable'; -export { ConnectionEditModal } from './ConnectionEditModal'; -export { ConnectionsErrorDisplay } from './ConnectionsErrorDisplay'; - -// Export logic hook -export { useConnectionsLogic } from './connectionsLogic'; - -// Export all interfaces and types -export * from './connectionsInterfaces'; \ No newline at end of file diff --git a/src/components/Dashboard/DashboardChat/DashboardChat.tsx b/src/components/Dashboard/DashboardChat/DashboardChat.tsx deleted file mode 100644 index 8f698f0..0000000 --- a/src/components/Dashboard/DashboardChat/DashboardChat.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import React from "react"; -import { DashboardChatProps } from "./dashboardChatAreaTypes"; -import DashboardChatArea from './DashboardChatArea.tsx'; - -import styles from './DashboardChatAreaStyles/DashboardChat.module.css'; - -const DashboardChat: React.FC = ({ - workflowState, - workflowActions -}) => { - return ( -
- -
- ); -}; - -export default DashboardChat; \ No newline at end of file diff --git a/src/components/Dashboard/DashboardChat/DashboardChatArea.tsx b/src/components/Dashboard/DashboardChat/DashboardChatArea.tsx deleted file mode 100644 index 8be897c..0000000 --- a/src/components/Dashboard/DashboardChat/DashboardChatArea.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import React, { useState } from "react"; -import MessageList from "./DashboardChatAreaMessageList"; - -import InputArea from "./DashboardChatAreaInput"; -import ConnectedFiles from "./DashboardChatAreaConnectedFiles"; -import { DashboardChatAreaProps } from "./dashboardChatAreaTypes"; -import styles from './DashboardChatAreaStyles/DashboardChat.module.css'; - -const DashboardChatArea: React.FC = ({ - workflowState, - workflowActions -}) => { - const [selectedFile, setSelectedFile] = useState(null); - const [attachedFiles, setAttachedFiles] = useState([]); - - return ( -
- {/* Top Left: Message List */} -
- -
- - {/*Top Right: File Preview (disabled for now) -
- -
- */} - - {/* Bottom Left: Input Area */} -
- -
- - {/* Bottom Right: Connected Files */} -
- { - // If the removed file is currently selected, clear the selection - if (selectedFile?.id === fileId) { - setSelectedFile(null); - } - // Remove the file from attached files - setAttachedFiles(files => files.filter(f => f.id !== fileId)); - }} - /> -
-
- ); -}; - -export default DashboardChatArea; diff --git a/src/components/Dashboard/DashboardChat/DashboardChatAreaConnectedFiles.tsx b/src/components/Dashboard/DashboardChat/DashboardChatAreaConnectedFiles.tsx deleted file mode 100644 index 48bbc65..0000000 --- a/src/components/Dashboard/DashboardChat/DashboardChatAreaConnectedFiles.tsx +++ /dev/null @@ -1,82 +0,0 @@ -import React from 'react'; - -import { ConnectedFilesProps } from './dashboardChatAreaTypes'; -import styles from './DashboardChatAreaStyles/DashboardChatConnectedFiles.module.css'; -import { IoIosAttach } from 'react-icons/io'; - -const ConnectedFiles: React.FC = ({ - onFileSelect, - selectedFile, - attachedFiles = [], - onRemoveFile -}) => { - const convertedFiles = attachedFiles.map(file => ({ - id: file.id, - name: file.name, - mimeType: file.type, - size: file.size, - creationDate: new Date().toISOString(), - fileData: file.fileData, - objectUrl: file.objectUrl - })); - - - const formatFileSize = (bytes: number) => { - if (bytes < 1024) return bytes + ' B'; - if (bytes < 1024 * 1024) return Math.round(bytes / 1024) + ' KB'; - return Math.round(bytes / (1024 * 1024)) + ' MB'; - }; - - return ( -
- {attachedFiles.length > 0 && ( -
- {attachedFiles.length} file{attachedFiles.length !== 1 ? 's' : ''} attached for workflow -
- )} - - {convertedFiles.length === 0 ? ( -

- No files connected to this workflow -

- ) : ( -
- {convertedFiles.map((file) => ( -
onFileSelect?.(file)} - className={`${styles.fileItem} ${selectedFile?.id === file.id ? styles.selected : ''} ${styles.attached}`} - > - -
-
- {file.name} -
-
- {file.size ? formatFileSize(file.size) : 'Unknown size'} -
-
- -
- {onRemoveFile && ( - - )} -
-
- ))} -
- )} -
- ); -}; - -export default ConnectedFiles; diff --git a/src/components/Dashboard/DashboardChat/DashboardChatAreaFilePreview.tsx b/src/components/Dashboard/DashboardChat/DashboardChatAreaFilePreview.tsx deleted file mode 100644 index fd9bbb1..0000000 --- a/src/components/Dashboard/DashboardChat/DashboardChatAreaFilePreview.tsx +++ /dev/null @@ -1,192 +0,0 @@ -import React from 'react'; -import { useFilePreview } from '../../../hooks/useWorkflows'; -import { FileInfo } from './dashboardChatAreaTypes'; - -interface AttachedFileWithData extends FileInfo { - fileData?: File; - objectUrl?: string; -} - -interface FilePreviewProps { - selectedFile?: AttachedFileWithData | null; -} - -const FilePreview: React.FC = ({ selectedFile }) => { - const { previewContent, fileMetadata, isLoading, error, fetchPreview } = useFilePreview(); - const [imageUrl, setImageUrl] = React.useState(null); - - // Handle base64 image data from backend - React.useEffect(() => { - if (fileMetadata && fileMetadata.base64Encoded && fileMetadata.preview) { - const isImage = fileMetadata.mimeType?.startsWith('image/') || - selectedFile?.name?.toLowerCase().match(/\.(png|jpg|jpeg|gif|bmp|webp)$/); - - if (isImage) { - - const dataUrl = `data:${fileMetadata.mimeType || 'image/png'};base64,${fileMetadata.preview}`; - setImageUrl(dataUrl); - - } - } - }, [fileMetadata, selectedFile]); - - React.useEffect(() => { - // Clean up previous object URL - const currentImageUrl = imageUrl; - if (currentImageUrl && currentImageUrl.startsWith('blob:')) { - URL.revokeObjectURL(currentImageUrl); - } - setImageUrl(null); - - if (selectedFile?.id) { - - // Check if it's an image file (either from mimeType or file extension) - const isImage = selectedFile.mimeType?.startsWith('image/') || - selectedFile.name?.toLowerCase().match(/\.(png|jpg|jpeg|gif|bmp|webp)$/); - - if (isImage) { - // If it's an attached file with file data, create object URL for preview - if (selectedFile.fileData) { - const url = URL.createObjectURL(selectedFile.fileData); - setImageUrl(url); - } else if (selectedFile.objectUrl) { - setImageUrl(selectedFile.objectUrl); - } else if (selectedFile.downloadUrl) { - setImageUrl(selectedFile.downloadUrl); - } else { - // For existing uploaded files, fetch the image data - fetchPreview(selectedFile.id); - } - } else { - // For non-image files, try to fetch preview - fetchPreview(selectedFile.id); - } - } - - // Cleanup function - return () => { - if (currentImageUrl && currentImageUrl.startsWith('blob:')) { - URL.revokeObjectURL(currentImageUrl); - } - }; - }, [selectedFile?.id, selectedFile?.fileData, selectedFile?.objectUrl]); - - // Cleanup on unmount - React.useEffect(() => { - return () => { - if (imageUrl) { - URL.revokeObjectURL(imageUrl); - } - }; - }, []); - - const getFileType = (mimeType?: string) => { - if (!mimeType) return 'Unknown'; - if (mimeType.startsWith('image/')) return 'Image'; - if (mimeType.startsWith('text/')) return 'Text'; - if (mimeType.includes('pdf')) return 'PDF'; - if (mimeType.includes('spreadsheet') || mimeType.includes('excel')) return 'Spreadsheet'; - return 'Document'; - }; - - return ( -
- - - {!selectedFile && ( -

- Select a file to preview -

- )} - - {selectedFile && ( -
-
-

{selectedFile.name}

-
- Type: {getFileType(selectedFile.mimeType)} • - Size: {selectedFile.size ? Math.round(selectedFile.size / 1024) + ' KB' : 'Unknown'} -
-
- - {/* Image Preview - Show first for images */} - {(selectedFile.mimeType?.startsWith('image/') || selectedFile.name?.toLowerCase().match(/\.(png|jpg|jpeg|gif|bmp|webp)$/)) && imageUrl ? ( -
- {selectedFile.name} console.log('Image loaded successfully')} - onError={(e) => { - console.error('Image failed to load:', e); - console.log('Image URL:', imageUrl); - }} - /> -
- ) : (selectedFile.mimeType?.startsWith('image/') || selectedFile.name?.toLowerCase().match(/\.(png|jpg|jpeg|gif|bmp|webp)$/)) ? ( -
-

🖼️ Image preview loading...

- - Debug: imageUrl={imageUrl ? 'yes' : 'no'}, downloadUrl={selectedFile.downloadUrl ? 'yes' : 'no'}, - fileData={selectedFile.fileData ? 'yes' : 'no'}, objectUrl={selectedFile.objectUrl ? 'yes' : 'no'} -
- MimeType: {selectedFile.mimeType} -
-
- ) : null} - - {/* Text/Code Preview - Only for non-images and when we don't have an image URL */} - {!(selectedFile.mimeType?.startsWith('image/') || selectedFile.name?.toLowerCase().match(/\.(png|jpg|jpeg|gif|bmp|webp)$/)) && !imageUrl && ( - <> - {isLoading &&

Loading preview...

} - {error &&

Error: {error}

} - - {previewContent && ( -
- {previewContent} -
- )} - - {!previewContent && !isLoading && !error && ( -

- Preview not available for this file type -

- )} - - )} -
- )} -
- ); -}; - -export default FilePreview; diff --git a/src/components/Dashboard/DashboardChat/DashboardChatAreaInput.tsx b/src/components/Dashboard/DashboardChat/DashboardChatAreaInput.tsx deleted file mode 100644 index 70c6c1a..0000000 --- a/src/components/Dashboard/DashboardChat/DashboardChatAreaInput.tsx +++ /dev/null @@ -1,364 +0,0 @@ -import React, { useState, useEffect, useRef } from 'react'; -import FileAttachmentPopup from './FileAttachmentPopup'; -import { InputAreaProps, AttachedFile } from './dashboardChatAreaTypes'; -import { useLanguage } from '../../../contexts/LanguageContext'; -import { usePrompts } from '../../../hooks/usePrompts'; - -import styles from './DashboardChatAreaStyles/DashboardChatAreaInput.module.css'; -import sharedStyles from './DashboardChatAreaStyles/DashboardChat.module.css'; - - - -const InputArea: React.FC = ({ - workflowState, - workflowActions, - attachedFiles: externalAttachedFiles = [], - onAttachedFilesChange -}) => { - const { t } = useLanguage(); - const { prompts, loading: promptsLoading } = usePrompts(); - const [inputValue, setInputValue] = useState(''); - const [showFilePopup, setShowFilePopup] = useState(false); - const [isSending, setIsSending] = useState(false); - const [sendError, setSendError] = useState(null); - const [isFocused, setIsFocused] = useState(false); - const [isDragOver, setIsDragOver] = useState(false); - const textareaRef = useRef(null); - const dropZoneRef = useRef(null); - - // Always use external attached files from parent component - const currentAttachedFiles = externalAttachedFiles; - - // Auto-resize textarea function - const adjustTextareaHeight = () => { - const textarea = textareaRef.current; - if (!textarea) return; - - // Reset height to auto to get the actual scroll height - textarea.style.height = 'auto'; - - // Calculate the height based on content - const scrollHeight = textarea.scrollHeight; - const lineHeight = 1.5 * 14; // 1.5em * 14px font size - const padding = 32; // 16px top + 16px bottom padding - const minHeight = lineHeight * 4 + padding; // 4 rows - const maxHeight = lineHeight * 8 + padding; // 8 rows - - // Set height within constraints - const newHeight = Math.min(Math.max(scrollHeight, minHeight), maxHeight); - textarea.style.height = `${newHeight}px`; - }; - - // Get the current selected prompt from workflow state - const currentSelectedPrompt = workflowState.selectedPrompt; - - // Auto-fill input when prompt is selected - useEffect(() => { - if (currentSelectedPrompt) { - setInputValue(currentSelectedPrompt.content); - } - }, [currentSelectedPrompt]); - - // Adjust height when input value changes - useEffect(() => { - adjustTextareaHeight(); - }, [inputValue]); - - // Initial resize on mount - useEffect(() => { - adjustTextareaHeight(); - }, []); - - const handleSend = async () => { - if (!inputValue.trim() || isSending) return; - - setIsSending(true); - setSendError(null); - - try { - const fileIds = currentAttachedFiles.map(f => f.id); - let success = false; - - if (workflowState.currentWorkflowId) { - success = await workflowActions.continueWorkflow(inputValue, fileIds); - } else { - const newWorkflowId = await workflowActions.startNewWorkflow(inputValue, fileIds); - success = !!newWorkflowId; - } - - if (success) { - setInputValue(''); - if (onAttachedFilesChange) { - onAttachedFilesChange([]); - } - // Prompt was used - } else { - setSendError('Failed to send message. Please try again.'); - } - } catch (error: any) { - console.error('Failed to send message:', error); - setSendError(error.message || 'Failed to send message. Please try again.'); - } finally { - setIsSending(false); - } - }; - - const handleStop = async () => { - // Stop the workflow but keep it selected - await workflowActions.stopWorkflow(); - }; - - const handleKeyPress = (e: React.KeyboardEvent) => { - if (e.key === 'Enter' && !e.shiftKey) { - e.preventDefault(); - handleSend(); - } - }; - - const handleFilesAttached = (files: AttachedFile[]) => { - setShowFilePopup(false); - if (onAttachedFilesChange) { - onAttachedFilesChange(files); - } - }; - - const handlePromptSelect = (e: React.ChangeEvent) => { - const promptId = e.target.value; - if (promptId === '') { - workflowActions.clearPrompt(); - setInputValue(''); - } else { - const prompt = prompts.find(p => p.id === promptId); - if (prompt) { - workflowActions.selectPrompt(prompt); - } - } - }; - - const handleClearPrompt = () => { - workflowActions.clearPrompt(); - setInputValue(''); - }; - - // Drag and drop handlers - const handleDragEnter = (e: React.DragEvent) => { - e.preventDefault(); - e.stopPropagation(); - if (e.dataTransfer.types.includes('Files')) { - setIsDragOver(true); - } - }; - - const handleDragLeave = (e: React.DragEvent) => { - e.preventDefault(); - e.stopPropagation(); - // Only hide drag over if we're leaving the drop zone entirely - if (dropZoneRef.current && !dropZoneRef.current.contains(e.relatedTarget as Node)) { - setIsDragOver(false); - } - }; - - const handleDragOver = (e: React.DragEvent) => { - e.preventDefault(); - e.stopPropagation(); - // Set the drop effect to copy - e.dataTransfer.dropEffect = 'copy'; - }; - - const handleDrop = (e: React.DragEvent) => { - e.preventDefault(); - e.stopPropagation(); - setIsDragOver(false); - - if (shouldShowStopButton) { - // Don't allow file drops when workflow is active - return; - } - - const files = Array.from(e.dataTransfer.files); - if (files.length > 0) { - // Convert File objects to AttachedFile format - const attachedFiles: AttachedFile[] = files.map((file, index) => ({ - id: (Date.now() + index).toString(), // Simple ID generation - name: file.name, - size: file.size, - type: file.type, - fileData: file, - objectUrl: URL.createObjectURL(file) - })); - - // Add to existing attached files - const updatedFiles = [...currentAttachedFiles, ...attachedFiles]; - if (onAttachedFilesChange) { - onAttachedFilesChange(updatedFiles); - } - } - }; - - - - // Check if workflow is in an active state where we should show stop button - const isWorkflowActive = workflowState.workflow && - ['running', 'processing', 'started'].includes(workflowState.workflow.status); - - const shouldShowStopButton = !!isWorkflowActive; - - - - // Determine if label should be in focused/moved state - const shouldLabelBeFocused = isFocused || inputValue.trim().length > 0; - - // Get placeholder text - const placeholderText = workflowState.currentWorkflowId - ? t('chat.input.continue_workflow') - : t('chat.input.enter_message'); - - return ( -
- - {/* Error messages */} - {(sendError || workflowState.error) && ( -
- {t('chat.input.error_prefix')} {sendError || workflowState.error} -
- )} - - {/* Drag and drop overlay */} - {isDragOver && ( -
-
-
- {shouldShowStopButton ? '🚫' : '📁'} -
-
- {shouldShowStopButton - ? t('chat.input.drop_disabled') - : t('chat.input.drop_files_here') - } -
-
-
- )} - - {/* Prompt selection dropdown */} -
-
- - {currentSelectedPrompt && ( - - )} -
-
- -
-
- -