From 1152abd4a55ab6283f110d475019174782a3b912 Mon Sep 17 00:00:00 2001 From: liquidrinu Date: Thu, 15 May 2025 15:10:07 +0200 Subject: [PATCH] updates across the board --- .env.example | 7 + docker-compose.yml | 159 +++---- frontend/index.html | 8 +- frontend/nginx.conf | 23 +- frontend/nginx.conf.bkup | 27 ++ frontend/package-lock.json | 33 ++ frontend/package.json | 1 + .../FuseMind/modals/components/Modal.tsx | 6 +- .../modals/components/ModalRenderer.tsx | 9 +- .../src/components/SystemSelectionModal.tsx | 401 ++++++++++++++++-- .../components/canvas-api/CanvasEndpoints.tsx | 118 ++++-- .../components/canvas-api/ManageDocsModal.tsx | 143 +++++++ .../canvas-api/PromptCreatorModal.tsx | 153 +++++++ frontend/src/components/canvas-api/api.ts | 61 +++ frontend/src/layouts/Sidebar.tsx | 2 +- frontend/src/services/api.ts | 12 +- .../src/services/fusemind/memoryService.ts | 25 +- frontend/src/types/fusemind/memory.ts | 18 +- mikro-orm.config.ts | 14 +- nginx/README.md | 27 ++ nginx/generate-selfsigned.sh | 8 + nginx/nginx.conf | 38 ++ nginx/nginx.conf.dev | 19 + nginx/nginx.conf.prod | 38 ++ src/apps/_app/controllers/PromptController.ts | 124 ++++++ src/apps/_app/entities/Prompt.ts | 48 +++ src/apps/_app/entities/app/_App.ts | 12 + src/apps/_app/entities/asset/_Asset.ts | 30 ++ src/apps/_app/middleware/auth.middleware.ts | 30 ++ src/apps/_app/routes/prompt.routes.ts | 115 +++++ src/apps/_app/services/PromptService.ts | 122 ++++++ src/apps/canvas-api/routes/EndpointsRoutes.ts | 8 +- .../migrations/Migration20240416000001.ts | 50 +++ .../migrations/Migration20250502161232.ts | 16 +- .../migrations/Migration20250502161233.ts | 23 + src/database/seeds/CanvasAppSeed.ts | 20 + src/database/seeds/index.ts | 5 + src/middleware/Auth.ts | 15 +- 38 files changed, 1770 insertions(+), 198 deletions(-) create mode 100644 frontend/nginx.conf.bkup create mode 100644 frontend/src/components/canvas-api/ManageDocsModal.tsx create mode 100644 frontend/src/components/canvas-api/PromptCreatorModal.tsx create mode 100644 frontend/src/components/canvas-api/api.ts create mode 100644 nginx/README.md create mode 100644 nginx/generate-selfsigned.sh create mode 100644 nginx/nginx.conf create mode 100644 nginx/nginx.conf.dev create mode 100644 nginx/nginx.conf.prod create mode 100644 src/apps/_app/controllers/PromptController.ts create mode 100644 src/apps/_app/entities/Prompt.ts create mode 100644 src/apps/_app/entities/asset/_Asset.ts create mode 100644 src/apps/_app/middleware/auth.middleware.ts create mode 100644 src/apps/_app/routes/prompt.routes.ts create mode 100644 src/apps/_app/services/PromptService.ts create mode 100644 src/database/migrations/Migration20240416000001.ts create mode 100644 src/database/migrations/Migration20250502161233.ts create mode 100644 src/database/seeds/CanvasAppSeed.ts diff --git a/.env.example b/.env.example index dd51b17..70d4270 100644 --- a/.env.example +++ b/.env.example @@ -11,6 +11,13 @@ JWT_SECRET=sdfj94mfm430f72m3487rdsjiy7834n9rnf934n8r3n490fn4u83fh894hr9nf0 # SERVER_BASEPATH_API=v1/ # TIMEZONE=Europe/Amsterdam + +# Default Admin User +DEFAULT_ADMIN_USERNAME=admin +DEFAULT_ADMIN_EMAIL=darren@fusero.nl +DEFAULT_ADMIN_PASSWORD=admin123 + + FASTIFY_PORT=14000 # [ Database ] diff --git a/docker-compose.yml b/docker-compose.yml index 04ba58d..198945e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,84 +1,97 @@ version: '3.8' services: - frontend: - container_name: fusero-frontend - build: - context: ./frontend - dockerfile: Dockerfile - ports: - - '3000:80' - networks: - - fusero-network - depends_on: - - fusero-app-boilerplate + fusero-app-frontend: + container_name: fusero-app-frontend + build: + context: ./frontend + dockerfile: Dockerfile + ports: + - '3000:80' + networks: + - fusero-network + depends_on: + - fusero-app-backend - frontend-dev: - container_name: fusero-frontend-dev - build: - context: ./frontend - dockerfile: Dockerfile.dev - ports: - - '8080:8080' - volumes: - - ./frontend:/app - - /app/node_modules - environment: - - NODE_ENV=development - command: npm run dev - networks: - - fusero-network - depends_on: - - fusero-app-boilerplate + fusero-app-frontend-dev: + container_name: fusero-app-frontend-dev + build: + context: ./frontend + dockerfile: Dockerfile + ports: + - '8080:8080' + volumes: + - ./frontend:/app + - /app/node_modules + environment: + - NODE_ENV=development + command: npm run dev + networks: + - fusero-network + depends_on: + - fusero-app-backend - fusero-app-boilerplate: - environment: - - POSTGRES_HOST=fusero-boilerplate-db - build: - context: . - dockerfile: Dockerfile - env_file: .env - restart: always - ports: - - '5000:14000' - depends_on: - - fusero-boilerplate-db - container_name: fusero-app-boilerplate - networks: - - fusero-network + fusero-app-backend: + build: + context: . + dockerfile: Dockerfile + env_file: .env + restart: always + ports: + - '5000:14000' + depends_on: + - fusero-boilerplate-db + container_name: fusero-app-backend + networks: + - fusero-network - fusero-boilerplate-db: - image: postgres:15 - env_file: .env - restart: always - volumes: - - fusero_boilerplate_pgdata:/var/lib/postgresql/data - ports: - - '19095:5432' - container_name: fusero-boilerplate-db - networks: - - fusero-network + fusero-boilerplate-db: + image: postgres:15 + env_file: .env + restart: always + volumes: + - fusero_boilerplate_pgdata:/var/lib/postgresql/data + ports: + - '19095:5432' + container_name: fusero-boilerplate-db + networks: + - fusero-network - fusero-boilerplate-test-db: - image: postgres:15 - env_file: .env - restart: always - volumes: - - fusero_boilerplate_test_pgdata:/var/lib/postgresql/data - ports: - - '19096:5432' - container_name: fusero-boilerplate-test-db - networks: - - fusero-network - environment: - - POSTGRES_DB=test-db + fusero-boilerplate-test-db: + image: postgres:15 + env_file: .env + restart: always + volumes: + - fusero_boilerplate_test_pgdata:/var/lib/postgresql/data + ports: + - '19096:5432' + container_name: fusero-boilerplate-test-db + networks: + - fusero-network + environment: + - POSTGRES_DB=test-db + + nginx: + image: nginx:alpine + container_name: fusero-nginx + ports: + - '14001:80' + - '14443:443' + volumes: + - ./nginx/nginx.conf.prod:/etc/nginx/conf.d/default.conf:ro + - ./nginx/certs:/etc/nginx/certs:ro + depends_on: + - fusero-app-frontend + - fusero-app-backend + networks: + - fusero-network volumes: - fusero_boilerplate_pgdata: - external: true - fusero_boilerplate_test_pgdata: - external: false + fusero_boilerplate_pgdata: + external: true + fusero_boilerplate_test_pgdata: + external: false networks: - fusero-network: - name: fusero-network + fusero-network: + name: fusero-network diff --git a/frontend/index.html b/frontend/index.html index 603ad34..532215d 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -3,10 +3,10 @@ - - - - + + + + Fusero diff --git a/frontend/nginx.conf b/frontend/nginx.conf index f4c9c4c..b54a604 100644 --- a/frontend/nginx.conf +++ b/frontend/nginx.conf @@ -1,20 +1,19 @@ server { listen 80; - server_name localhost; + server_name _; + + root /usr/share/nginx/html; + index index.html; location / { - root /usr/share/nginx/html; - index index.html; try_files $uri $uri/ /index.html; } - # Proxy API requests to the backend - location /api { - proxy_pass http://backend:14000; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection 'upgrade'; - proxy_set_header Host $host; - proxy_cache_bypass $http_upgrade; + location /favicon/ { + alias /usr/share/nginx/html/dist/favicon/; + access_log off; + expires max; } -} \ No newline at end of file + + # DO NOT proxy /api here — let the global Nginx handle it +} diff --git a/frontend/nginx.conf.bkup b/frontend/nginx.conf.bkup new file mode 100644 index 0000000..7fdce10 --- /dev/null +++ b/frontend/nginx.conf.bkup @@ -0,0 +1,27 @@ +server { + listen 80; + server_name _; + + location / { + root /usr/share/nginx/html; + index index.html; + try_files $uri $uri/ /index.html; + } + + # Serve favicon files + location /favicon/ { + alias /usr/share/nginx/html/dist/favicon/; + access_log off; + expires max; + } + + # Proxy API requests to the backend + location /api { + proxy_pass http://fusero-app-backend:14000; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host $host; + proxy_cache_bypass $http_upgrade; + } +} \ No newline at end of file diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 5e828b6..c32ba7e 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -24,6 +24,7 @@ "zustand": "^4.5.2" }, "devDependencies": { + "@types/node": "^22.15.18", "@types/react": "^18.2.66", "@types/react-dom": "^18.2.22", "@typescript-eslint/eslint-plugin": "^7.2.0", @@ -1696,6 +1697,16 @@ "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", "license": "MIT" }, + "node_modules/@types/node": { + "version": "22.15.18", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.18.tgz", + "integrity": "sha512-v1DKRfUdyW+jJhZNEI1PYy29S2YRxMV5AOO/x/SjKmW0acCIOqmbj6Haf9eHAhsPmrhlHSxEhv/1WszcLWV4cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, "node_modules/@types/prop-types": { "version": "15.7.12", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz", @@ -5796,6 +5807,13 @@ "node": ">=14.17" } }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, "node_modules/unified": { "version": "11.0.5", "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", @@ -7264,6 +7282,15 @@ "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==" }, + "@types/node": { + "version": "22.15.18", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.18.tgz", + "integrity": "sha512-v1DKRfUdyW+jJhZNEI1PYy29S2YRxMV5AOO/x/SjKmW0acCIOqmbj6Haf9eHAhsPmrhlHSxEhv/1WszcLWV4cg==", + "dev": true, + "requires": { + "undici-types": "~6.21.0" + } + }, "@types/prop-types": { "version": "15.7.12", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz", @@ -9969,6 +9996,12 @@ "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", "dev": true }, + "undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true + }, "unified": { "version": "11.0.5", "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", diff --git a/frontend/package.json b/frontend/package.json index 1f3523c..f30af95 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -26,6 +26,7 @@ "zustand": "^4.5.2" }, "devDependencies": { + "@types/node": "^22.15.18", "@types/react": "^18.2.66", "@types/react-dom": "^18.2.22", "@typescript-eslint/eslint-plugin": "^7.2.0", diff --git a/frontend/src/components/FuseMind/modals/components/Modal.tsx b/frontend/src/components/FuseMind/modals/components/Modal.tsx index 9293537..aac7432 100644 --- a/frontend/src/components/FuseMind/modals/components/Modal.tsx +++ b/frontend/src/components/FuseMind/modals/components/Modal.tsx @@ -1,8 +1,10 @@ -import React from 'react'; +import React, { ReactNode } from 'react'; import { ModalConfig } from '../core/types'; import { useModal } from '../context/ModalContext'; -interface ModalProps extends ModalConfig {} +interface ModalProps extends Omit { + content: ReactNode; +} export const Modal: React.FC = ({ id, diff --git a/frontend/src/components/FuseMind/modals/components/ModalRenderer.tsx b/frontend/src/components/FuseMind/modals/components/ModalRenderer.tsx index d4ccb4a..8bd86a4 100644 --- a/frontend/src/components/FuseMind/modals/components/ModalRenderer.tsx +++ b/frontend/src/components/FuseMind/modals/components/ModalRenderer.tsx @@ -1,6 +1,11 @@ -import React from 'react'; +import React, { ReactNode } from 'react'; import { Modal } from './Modal'; import { useModal } from '../context/ModalContext'; +import { ModalConfig } from '../core/types'; + +interface ModalWithContent extends ModalConfig { + content: ReactNode; +} export const ModalRenderer: React.FC = () => { const { modals } = useModal(); @@ -10,7 +15,7 @@ export const ModalRenderer: React.FC = () => { {modals.map((modal) => ( ))} diff --git a/frontend/src/components/SystemSelectionModal.tsx b/frontend/src/components/SystemSelectionModal.tsx index 6413bca..0d7b85f 100644 --- a/frontend/src/components/SystemSelectionModal.tsx +++ b/frontend/src/components/SystemSelectionModal.tsx @@ -1,6 +1,10 @@ import { useCallback, useEffect, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { Card } from 'primereact/card'; +import { Dialog } from 'primereact/dialog'; +import { InputText } from 'primereact/inputtext'; +import { InputTextarea } from 'primereact/inputtextarea'; +import { FileUpload } from 'primereact/fileupload'; import Button from '../shared/components/_V2/Button'; import Dropdown from '../shared/components/_V2/Dropdown'; import Divider from '../shared/components/_V1/Divider'; @@ -12,10 +16,15 @@ import { useSystemStore } from '../state/stores/useSystemStore'; export type SystemDropdownOption = { id: number; label: string; + name: string; businessLabel: string; urlSlug: string; logo: string; category: string; + method?: string; + path?: string; + description?: string; + publicPath?: string; enterprises?: Enterprise[]; }; @@ -30,44 +39,61 @@ export interface SystemSelectionModalProps { onLogoChange: (newLogoPath: string) => void; } -// TODO: removed mocked put in db -const mockedSystems = [ - { - id: 1, - label: 'CouncilAI', - businessLabel: 'CouncilAI', - urlSlug: 'council', - logo: fusero_logo, - category: 'Apps', - }, - { - id: 4, - label: 'FuseMind', - businessLabel: 'FuseMind (AI)', - urlSlug: 'fusemind', - logo: fusero_logo, - category: 'Apps', - }, - { - id: 5, - label: 'Canvas API', - businessLabel: 'Canvas API', - urlSlug: 'canvas', - logo: roc_logo, - category: 'Apps', - }, -]; - // Add system default routes configuration const systemDefaultRoutes = { council: 'chat', fusemind: 'home', }; +const HARDCODED_SYSTEMS = [ + { + id: 1, + label: 'Canvas API', + name: 'Canvas API', + businessLabel: 'Canvas', + urlSlug: 'canvas', + logo: roc_logo, + category: 'Apps', + method: 'GET', + path: '/api/v1/canvas-endpoints', + description: 'Integration with Canvas LMS', + publicPath: '/canvas-endpoints', + enterprises: [], + }, + { + id: 2, + label: 'FuseMind', + name: 'FuseMind', + businessLabel: 'FuseMind', + urlSlug: 'fusemind', + logo: fusero_logo, + category: 'Apps', + method: 'GET', + path: '/api/v1/fusemind', + description: 'FuseMind AI Platform', + publicPath: '/fusemind', + enterprises: [], + }, +]; + const SystemSelectionModal = ({ systemDetails }: SystemSelectionModalProps) => { const navigate = useNavigate(); const [selectedSystem, setSelectedSystem] = useState(null); const [selectedEnterprise, setSelectedEnterprise] = useState(null); + const [showAddSystemDialog, setShowAddSystemDialog] = useState(false); + const [showEditLogoDialog, setShowEditLogoDialog] = useState(false); + const [systems, setSystems] = useState(HARDCODED_SYSTEMS); + const [isLoading, setIsLoading] = useState(false); + const [uploadedLogo, setUploadedLogo] = useState(null); + const [newSystem, setNewSystem] = useState({ + name: '', + businessLabel: '', + urlSlug: '', + method: '', + path: '', + description: '', + publicPath: '', + }); const { setSystem } = useSystemStore(); useEffect(() => { @@ -91,7 +117,7 @@ const SystemSelectionModal = ({ systemDetails }: SystemSelectionModalProps) => { const handleSelectSystem = () => { if (selectedSystem) { - navigate(`/dashboard/${selectedSystem.urlSlug}`); // JUST GO TO BASE PATH + navigate(`/dashboard/${selectedSystem.urlSlug}`); } }; @@ -99,6 +125,144 @@ const SystemSelectionModal = ({ systemDetails }: SystemSelectionModalProps) => { setSelectedEnterprise(e.value); }, []); + const handleLogoUpload = async (event: any) => { + const file = event.files[0]; + if (file) { + const reader = new FileReader(); + reader.onload = (e) => { + const base64 = e.target?.result as string; + setUploadedLogo(base64); + }; + reader.readAsDataURL(file); + } + }; + + const handleUpdateLogo = async () => { + if (!selectedSystem || !uploadedLogo) return; + + try { + // Upload the new logo + const logoResponse = await fetch('/api/v1/assets', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + name: `${selectedSystem.name}-logo`, + type: 'image', + data: uploadedLogo, + mimeType: 'image/png', + assetType: 'system_logo', + }), + }); + + if (!logoResponse.ok) { + throw new Error('Failed to upload logo'); + } + + const logoData = await logoResponse.json(); + + // Update the system with the new logo ID + const response = await fetch(`/api/v1/apps/${selectedSystem.id}`, { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + logoId: logoData.id, + }), + }); + + if (!response.ok) { + throw new Error('Failed to update system logo'); + } + + const updatedSystem = await response.json(); + + // Update the local state + setSystems(prev => prev.map(system => + system.id === selectedSystem.id + ? { ...system, logo: updatedSystem.logo || fusero_logo } + : system + )); + + setSelectedSystem(prev => prev ? { ...prev, logo: updatedSystem.logo || fusero_logo } : null); + setShowEditLogoDialog(false); + setUploadedLogo(null); + } catch (error) { + console.error('Error updating logo:', error); + } + }; + + const handleAddSystem = async () => { + try { + // First upload the logo if exists + let logoId = null; + if (uploadedLogo) { + const logoResponse = await fetch('/api/v1/assets', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + name: `${newSystem.name}-logo`, + type: 'image', + data: uploadedLogo, + mimeType: 'image/png', + assetType: 'system_logo', + }), + }); + + if (!logoResponse.ok) { + throw new Error('Failed to upload logo'); + } + + const logoData = await logoResponse.json(); + logoId = logoData.id; + } + + // Then create the system + const response = await fetch('/api/v1/apps', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + ...newSystem, + logoId, + }), + }); + + if (!response.ok) { + throw new Error('Failed to create system'); + } + + const createdSystem = await response.json(); + const systemOption: SystemDropdownOption = { + ...createdSystem, + label: createdSystem.name, + businessLabel: createdSystem.businessLabel || createdSystem.name, + logo: createdSystem.logo || fusero_logo, + category: 'Apps', + }; + + setSystems(prev => [...prev, systemOption]); + setShowAddSystemDialog(false); + setNewSystem({ + name: '', + businessLabel: '', + urlSlug: '', + method: '', + path: '', + description: '', + publicPath: '', + }); + setUploadedLogo(null); + } catch (error) { + console.error('Error creating system:', error); + } + }; + return ( <> { className='px-8 bg-white' >

Apps & Flows

-
+
+ {/*
{selectedSystem && selectedSystem.enterprises && ( { {selectedSystem && ( - {`${selectedSystem.label} +
+ {`${selectedSystem.label} +
)} + + setShowAddSystemDialog(false)} + footer={ +
+
+ } + > +
+
+ + setNewSystem({ ...newSystem, name: e.target.value })} + className="w-full" + /> +
+
+ + setNewSystem({ ...newSystem, businessLabel: e.target.value })} + className="w-full" + /> +
+
+ + setNewSystem({ ...newSystem, urlSlug: e.target.value })} + className="w-full" + /> +
+
+ + setNewSystem({ ...newSystem, method: e.target.value })} + className="w-full" + /> +
+
+ + setNewSystem({ ...newSystem, path: e.target.value })} + className="w-full" + /> +
+
+ + setNewSystem({ ...newSystem, description: e.target.value })} + className="w-full" + /> +
+
+ + setNewSystem({ ...newSystem, publicPath: e.target.value })} + className="w-full" + /> +
+
+ + + {uploadedLogo && ( +
+ Uploaded logo +
+ )} +
+
+
+ + setShowEditLogoDialog(false)} + footer={ +
+
+ } + > +
+
+ + {selectedSystem && ( + Current logo + )} +
+
+ + + {uploadedLogo && ( +
+ New logo preview +
+ )} +
+
+
); }; diff --git a/frontend/src/components/canvas-api/CanvasEndpoints.tsx b/frontend/src/components/canvas-api/CanvasEndpoints.tsx index eae8c74..6b86017 100644 --- a/frontend/src/components/canvas-api/CanvasEndpoints.tsx +++ b/frontend/src/components/canvas-api/CanvasEndpoints.tsx @@ -14,6 +14,12 @@ import ChatGptModal from './ChatGPTModal'; import { Toast } from 'primereact/toast'; import { useAuthStore } from '../../state/stores/useAuthStore'; import SettingsMenu, { SettingsMenuItem } from '../../shared/components/SettingsMenu'; +import ManageDocsModal from './ManageDocsModal'; +import PromptCreatorModal from './PromptCreatorModal'; +import { Tooltip } from 'primereact/tooltip'; + +const CANVAS_APP_ID = 1; // Hardcoded Canvas app ID +const CANVAS_API_BASE_URL = import.meta.env.VITE_CANVAS_API_BASE_URL || 'https://canvas.instructure.com/api/v1'; export default function CanvasEndpoints() { const [endpoints, setEndpoints] = useState([]); @@ -47,7 +53,7 @@ export default function CanvasEndpoints() { const [apiKey, setApiKey] = useState(null); const [apiKeyLoading, setApiKeyLoading] = useState(false); const [apiKeyError, setApiKeyError] = useState(null); - const [appId, setAppId] = useState(null); + const [appId, setAppId] = useState(CANVAS_APP_ID); const [showApiKeyModal, setShowApiKeyModal] = useState(false); const [editByVoiceEndpoint, setEditByVoiceEndpoint] = useState(null); const [showVoiceModal, setShowVoiceModal] = useState(false); @@ -56,17 +62,13 @@ export default function CanvasEndpoints() { const [fullTranscript, setFullTranscript] = useState(''); const [showCogMenu, setShowCogMenu] = useState(false); const cogMenuRef = useRef(null); + const [showManageDocs, setShowManageDocs] = useState(false); + const [showPromptCreator, setShowPromptCreator] = useState(false); useEffect(() => { - async function fetchAppId() { - try { - const response = await api('get', '/api/v1/app/by-name/Canvas'); - setAppId(response.data.id); - } catch (err) { - console.error('Failed to fetch Canvas app id'); - } - } - fetchAppId(); + // Load API key from localStorage if available + const storedKey = localStorage.getItem('canvas_api_key'); + if (storedKey) setApiKey(storedKey); initFilters(); fetchEndpoints(); }, []); @@ -272,6 +274,7 @@ export default function CanvasEndpoints() { if (!appId) throw new Error('App ID not loaded'); const response = await api('post', '/api/v1/app/apikey/generate', { userId: user.id, appId }); setApiKey(response.data.apiKey); + localStorage.setItem('canvas_api_key', response.data.apiKey); } catch (error: any) { const errorMessage = error.response?.data?.error || error.message || 'Failed to fetch or generate API key'; setApiKeyError(errorMessage); @@ -287,6 +290,9 @@ export default function CanvasEndpoints() { if (!user || !appId) return; const response = await api('post', '/api/v1/app/apikey/get', { userId: user.id, appId }); setApiKey(response.data.apiKey || null); + if (response.data.apiKey) { + localStorage.setItem('canvas_api_key', response.data.apiKey); + } setApiKeyError(null); } catch (error: any) { setApiKey(null); @@ -531,6 +537,29 @@ export default function CanvasEndpoints() { recognition.start(); }; + const settingsItems = [ + { + label: 'Manage API Key', + icon: 'pi pi-key', + command: () => setShowApiKeyModal(true), + }, + { + label: 'Voice Command List', + icon: 'pi pi-info-circle', + command: () => setShowVoiceInfoModal(true), + }, + { + label: 'Manage API Docs', + icon: 'pi pi-link', + command: () => setShowManageDocs(true), + }, + { + label: 'Create Prompt', + icon: 'pi pi-plus-circle', + command: () => setShowPromptCreator(true), + }, + ]; + const renderHeader = () => ( setShowApiKeyModal(true), - }, - { - label: 'Voice Command List', - icon: 'pi pi-info-circle', - command: () => setShowVoiceInfoModal(true), - }, - ]} + items={settingsItems} buttonClassName="p-button-secondary" />
); @@ -635,11 +680,13 @@ export default function CanvasEndpoints() { - + + setShowManageDocs(false)} /> + + setShowPromptCreator(false)} + appId={appId || 0} + /> ); } \ No newline at end of file diff --git a/frontend/src/components/canvas-api/ManageDocsModal.tsx b/frontend/src/components/canvas-api/ManageDocsModal.tsx new file mode 100644 index 0000000..357b79c --- /dev/null +++ b/frontend/src/components/canvas-api/ManageDocsModal.tsx @@ -0,0 +1,143 @@ +import { useState, useEffect } from 'react'; +import { Dialog } from 'primereact/dialog'; +import { InputText } from 'primereact/inputtext'; +import { Button } from 'primereact/button'; +import { ListBox } from 'primereact/listbox'; + +interface ManageDocsModalProps { + visible: boolean; + onHide: () => void; +} + +interface ParseResult { + rawHtml: string; + aiOutput: any; + restructured: any; +} + +const LOCAL_STORAGE_KEY = 'apiDocUrls'; + +export function ManageDocsModal({ visible, onHide }: ManageDocsModalProps) { + const [urls, setUrls] = useState([]); + const [selectedUrl, setSelectedUrl] = useState(null); + const [newUrl, setNewUrl] = useState(''); + const [editIndex, setEditIndex] = useState(null); + const [editValue, setEditValue] = useState(''); + const [parsing, setParsing] = useState(false); + const [parseResult, setParseResult] = useState(null); + const [error, setError] = useState(null); + + // Load URLs from localStorage + useEffect(() => { + const saved = localStorage.getItem(LOCAL_STORAGE_KEY); + if (saved) setUrls(JSON.parse(saved)); + }, []); + + // Save URLs to localStorage + useEffect(() => { + localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(urls)); + }, [urls]); + + const addUrl = () => { + if (newUrl && !urls.includes(newUrl)) { + setUrls([...urls, newUrl]); + setNewUrl(''); + } + }; + + const removeUrl = (url: string) => { + setUrls(urls.filter(u => u !== url)); + if (selectedUrl === url) setSelectedUrl(null); + }; + + const startEdit = (idx: number) => { + setEditIndex(idx); + setEditValue(urls[idx]); + }; + + const saveEdit = () => { + if (editIndex !== null && editValue) { + const newUrls = [...urls]; + newUrls[editIndex] = editValue; + setUrls(newUrls); + setEditIndex(null); + setEditValue(''); + } + }; + + const handleParse = async (url: string) => { + setParsing(true); + setError(null); + setParseResult(null); + try { + const response = await fetch('/api/parse-docs', { + method: 'POST', + body: JSON.stringify({ url }), + headers: { 'Content-Type': 'application/json' } + }); + if (!response.ok) throw new Error('Failed to fetch or parse docs'); + const data = await response.json(); + setParseResult(data); + } catch (e: any) { + setError(e.message || 'Unknown error'); + } finally { + setParsing(false); + } + }; + + return ( + +
+ {/* URL List and Controls */} +
+

Documentation URLs

+ ({ label: u, value: u }))} + onChange={e => setSelectedUrl(e.value)} + style={{ width: '100%', minHeight: 200 }} + /> +
+ setNewUrl(e.target.value)} + placeholder="Add new URL" + className="w-full" + /> +
+ {selectedUrl && ( +
+
+ )} + {editIndex !== null && ( +
+ setEditValue(e.target.value)} className="w-full" /> +
+ )} +
+ {/* Parse Results */} +
+ {error &&
{error}
} + {parseResult && ( +
+

Raw HTML (first 500 chars):

+
{parseResult.rawHtml?.slice(0, 500)}...
+

AI Output:

+
{JSON.stringify(parseResult.aiOutput, null, 2)}
+

Restructured Output:

+
{JSON.stringify(parseResult.restructured, null, 2)}
+
+ )} +
+
+
+ ); +} + +export default ManageDocsModal; \ No newline at end of file diff --git a/frontend/src/components/canvas-api/PromptCreatorModal.tsx b/frontend/src/components/canvas-api/PromptCreatorModal.tsx new file mode 100644 index 0000000..58da396 --- /dev/null +++ b/frontend/src/components/canvas-api/PromptCreatorModal.tsx @@ -0,0 +1,153 @@ +import { useState } from 'react'; +import { Dialog } from 'primereact/dialog'; +import { InputText } from 'primereact/inputtext'; +import { Dropdown } from 'primereact/dropdown'; +import { Button } from 'primereact/button'; +import { Toast } from 'primereact/toast'; +import { useRef } from 'react'; +import { api } from '../../services/api'; +import { InputTextarea } from 'primereact/inputtextarea'; + +interface PromptCreatorModalProps { + visible: boolean; + onHide: () => void; + appId: number; +} + +const promptTypes = [ + { label: 'API Documentation', value: 'api_documentation' }, + { label: 'Endpoint Analysis', value: 'endpoint_analysis' }, + { label: 'Schema Generation', value: 'schema_generation' }, + { label: 'Custom', value: 'custom' } +]; + +export default function PromptCreatorModal({ visible, onHide, appId }: PromptCreatorModalProps) { + const [name, setName] = useState(''); + const [content, setContent] = useState(''); + const [type, setType] = useState(null); + const [loading, setLoading] = useState(false); + const toast = useRef(null); + + const handleSubmit = async () => { + if (!name || !content || !type) { + toast.current?.show({ + severity: 'error', + summary: 'Error', + detail: 'Please fill in all required fields', + life: 3000 + }); + return; + } + + setLoading(true); + try { + const response = await api('post', '/api/v1/prompts', { + name, + content, + type, + appId, + metadata: { + source: 'canvas-api', + timestamp: new Date().toISOString() + } + }); + + toast.current?.show({ + severity: 'success', + summary: 'Success', + detail: 'Prompt created successfully', + life: 3000 + }); + + // Process the prompt immediately + await api('post', `/api/v1/prompts/${response.data.id}/process`); + + // Reset form + setName(''); + setContent(''); + setType(null); + onHide(); + } catch (error) { + console.error('Error creating prompt:', error); + toast.current?.show({ + severity: 'error', + summary: 'Error', + detail: 'Failed to create prompt', + life: 3000 + }); + } finally { + setLoading(false); + } + }; + + return ( + <> + + + + + ); +} \ No newline at end of file diff --git a/frontend/src/components/canvas-api/api.ts b/frontend/src/components/canvas-api/api.ts new file mode 100644 index 0000000..ab88a75 --- /dev/null +++ b/frontend/src/components/canvas-api/api.ts @@ -0,0 +1,61 @@ +import axios, { AxiosRequestConfig, AxiosResponse } from 'axios'; + +interface ApiConfig { + baseURL?: string; + timeout?: number; + headers?: Record; +} + +// Clean base URL once at top-level +const rawBaseUrl = import.meta.env.VITE_API_BASE_URL || ''; +const cleanedBaseUrl = rawBaseUrl.replace(/\/$/, ''); + +const DEFAULT_CONFIG: ApiConfig = { + baseURL: cleanedBaseUrl, + timeout: 30000, + headers: { + 'Content-Type': 'application/json', + }, +}; + +export const api = async ( + method: 'get' | 'post' | 'put' | 'delete' | 'patch', + url: string, + data?: any, + params?: any, + responseType: 'json' | 'blob' | 'text' = 'json', + timeout?: number, + useAuth: boolean = true +): Promise> => { + const config: AxiosRequestConfig = { + ...DEFAULT_CONFIG, + method, + url, + data, + params, + responseType, + timeout: timeout || DEFAULT_CONFIG.timeout, + }; + + if (useAuth) { + const token = localStorage.getItem('token'); + if (token) { + config.headers = { + ...config.headers, + Authorization: `Bearer ${token}`, + }; + } + } + + try { + return await axios(config); + } catch (error) { + if (axios.isAxiosError(error)) { + if (error.response?.status === 401) { + localStorage.removeItem('token'); + window.location.href = '/login'; + } + } + throw error; + } +}; diff --git a/frontend/src/layouts/Sidebar.tsx b/frontend/src/layouts/Sidebar.tsx index 2c09a9b..970e6d4 100644 --- a/frontend/src/layouts/Sidebar.tsx +++ b/frontend/src/layouts/Sidebar.tsx @@ -67,7 +67,7 @@ const Sidebar = ({ systemId, isCollapsed, setIsCollapsed }) => { />
- +