updates across the board
This commit is contained in:
parent
fc403142c2
commit
1152abd4a5
@ -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 ]
|
||||
|
@ -1,8 +1,8 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
frontend:
|
||||
container_name: fusero-frontend
|
||||
fusero-app-frontend:
|
||||
container_name: fusero-app-frontend
|
||||
build:
|
||||
context: ./frontend
|
||||
dockerfile: Dockerfile
|
||||
@ -11,13 +11,13 @@ services:
|
||||
networks:
|
||||
- fusero-network
|
||||
depends_on:
|
||||
- fusero-app-boilerplate
|
||||
- fusero-app-backend
|
||||
|
||||
frontend-dev:
|
||||
container_name: fusero-frontend-dev
|
||||
fusero-app-frontend-dev:
|
||||
container_name: fusero-app-frontend-dev
|
||||
build:
|
||||
context: ./frontend
|
||||
dockerfile: Dockerfile.dev
|
||||
dockerfile: Dockerfile
|
||||
ports:
|
||||
- '8080:8080'
|
||||
volumes:
|
||||
@ -29,11 +29,9 @@ services:
|
||||
networks:
|
||||
- fusero-network
|
||||
depends_on:
|
||||
- fusero-app-boilerplate
|
||||
- fusero-app-backend
|
||||
|
||||
fusero-app-boilerplate:
|
||||
environment:
|
||||
- POSTGRES_HOST=fusero-boilerplate-db
|
||||
fusero-app-backend:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
@ -43,7 +41,7 @@ services:
|
||||
- '5000:14000'
|
||||
depends_on:
|
||||
- fusero-boilerplate-db
|
||||
container_name: fusero-app-boilerplate
|
||||
container_name: fusero-app-backend
|
||||
networks:
|
||||
- fusero-network
|
||||
|
||||
@ -73,6 +71,21 @@ services:
|
||||
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
|
||||
|
@ -3,10 +3,10 @@
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="public/favicon/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="public/favicon/favicon-16x16.png">
|
||||
<link rel="manifest" href="public/favicon/site.webmanifest">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/favicon/apple-touch-icon.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon/favicon-16x16.png">
|
||||
<link rel="manifest" href="/favicon/site.webmanifest">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Fusero</title>
|
||||
</head>
|
||||
|
@ -1,20 +1,19 @@
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
server_name _;
|
||||
|
||||
location / {
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
location / {
|
||||
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;
|
||||
}
|
||||
|
||||
# DO NOT proxy /api here — let the global Nginx handle it
|
||||
}
|
27
frontend/nginx.conf.bkup
Normal file
27
frontend/nginx.conf.bkup
Normal file
@ -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;
|
||||
}
|
||||
}
|
33
frontend/package-lock.json
generated
33
frontend/package-lock.json
generated
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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<ModalConfig, 'content'> {
|
||||
content: ReactNode;
|
||||
}
|
||||
|
||||
export const Modal: React.FC<ModalProps> = ({
|
||||
id,
|
||||
|
@ -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) => (
|
||||
<Modal
|
||||
key={modal.id}
|
||||
{...modal}
|
||||
{...modal as ModalWithContent}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
|
@ -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<SystemDropdownOption | null>(null);
|
||||
const [selectedEnterprise, setSelectedEnterprise] = useState<number | null>(null);
|
||||
const [showAddSystemDialog, setShowAddSystemDialog] = useState(false);
|
||||
const [showEditLogoDialog, setShowEditLogoDialog] = useState(false);
|
||||
const [systems, setSystems] = useState<SystemDropdownOption[]>(HARDCODED_SYSTEMS);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [uploadedLogo, setUploadedLogo] = useState<string | null>(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 (
|
||||
<>
|
||||
<Card
|
||||
@ -106,15 +270,22 @@ const SystemSelectionModal = ({ systemDetails }: SystemSelectionModalProps) => {
|
||||
className='px-8 bg-white'
|
||||
>
|
||||
<h2 className='my-2 text-xl font-semibold text-gray-700'>Apps & Flows</h2>
|
||||
<div className='flex items-center justify-between'>
|
||||
<div className='flex items-center gap-4'>
|
||||
<Dropdown
|
||||
id='system'
|
||||
value={selectedSystem}
|
||||
options={mockedSystems}
|
||||
options={systems}
|
||||
onChange={handleSystemChange}
|
||||
optionLabel='businessLabel'
|
||||
placeholder='Select an integration'
|
||||
className='flex-1'
|
||||
loading={isLoading}
|
||||
/>
|
||||
{/* <Button
|
||||
label='Add System'
|
||||
onClick={() => setShowAddSystemDialog(true)}
|
||||
className='px-4 py-2 text-white transition-colors bg-blue-500 border-2 border-blue-500 rounded-md hover:bg-white hover:text-blue-500'
|
||||
/> */}
|
||||
</div>
|
||||
{selectedSystem && selectedSystem.enterprises && (
|
||||
<Dropdown
|
||||
@ -136,12 +307,170 @@ const SystemSelectionModal = ({ systemDetails }: SystemSelectionModalProps) => {
|
||||
<Divider />
|
||||
</Card>
|
||||
{selectedSystem && (
|
||||
<div className="relative">
|
||||
<img
|
||||
src={selectedSystem.logo}
|
||||
alt={`${selectedSystem.label} Logo`}
|
||||
className='w-48'
|
||||
/>
|
||||
<Button
|
||||
label="Edit Logo"
|
||||
onClick={() => setShowEditLogoDialog(true)}
|
||||
className="absolute bottom-0 right-0 px-2 py-1 text-sm text-white bg-blue-500 rounded hover:bg-blue-600"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Dialog
|
||||
header="Add New System"
|
||||
visible={showAddSystemDialog}
|
||||
style={{ width: '50vw' }}
|
||||
onHide={() => setShowAddSystemDialog(false)}
|
||||
footer={
|
||||
<div>
|
||||
<Button
|
||||
label="Cancel"
|
||||
onClick={() => setShowAddSystemDialog(false)}
|
||||
className="mr-2"
|
||||
/>
|
||||
<Button
|
||||
label="Create"
|
||||
onClick={handleAddSystem}
|
||||
className="bg-blue-500 text-white"
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div>
|
||||
<label className="block mb-2">Name</label>
|
||||
<InputText
|
||||
value={newSystem.name}
|
||||
onChange={(e) => setNewSystem({ ...newSystem, name: e.target.value })}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block mb-2">Business Label</label>
|
||||
<InputText
|
||||
value={newSystem.businessLabel}
|
||||
onChange={(e) => setNewSystem({ ...newSystem, businessLabel: e.target.value })}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block mb-2">URL Slug</label>
|
||||
<InputText
|
||||
value={newSystem.urlSlug}
|
||||
onChange={(e) => setNewSystem({ ...newSystem, urlSlug: e.target.value })}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block mb-2">Method</label>
|
||||
<InputText
|
||||
value={newSystem.method}
|
||||
onChange={(e) => setNewSystem({ ...newSystem, method: e.target.value })}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block mb-2">Path</label>
|
||||
<InputText
|
||||
value={newSystem.path}
|
||||
onChange={(e) => setNewSystem({ ...newSystem, path: e.target.value })}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block mb-2">Description</label>
|
||||
<InputTextarea
|
||||
value={newSystem.description}
|
||||
onChange={(e) => setNewSystem({ ...newSystem, description: e.target.value })}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block mb-2">Public Path</label>
|
||||
<InputText
|
||||
value={newSystem.publicPath}
|
||||
onChange={(e) => setNewSystem({ ...newSystem, publicPath: e.target.value })}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block mb-2">Logo</label>
|
||||
<FileUpload
|
||||
mode="basic"
|
||||
name="logo"
|
||||
accept="image/*"
|
||||
maxFileSize={1000000}
|
||||
chooseLabel="Upload Logo"
|
||||
onUpload={handleLogoUpload}
|
||||
auto
|
||||
customUpload
|
||||
/>
|
||||
{uploadedLogo && (
|
||||
<div className="mt-2">
|
||||
<img src={uploadedLogo} alt="Uploaded logo" className="w-32 h-32 object-contain" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
|
||||
<Dialog
|
||||
header="Update System Logo"
|
||||
visible={showEditLogoDialog}
|
||||
style={{ width: '30vw' }}
|
||||
onHide={() => setShowEditLogoDialog(false)}
|
||||
footer={
|
||||
<div>
|
||||
<Button
|
||||
label="Cancel"
|
||||
onClick={() => setShowEditLogoDialog(false)}
|
||||
className="mr-2"
|
||||
/>
|
||||
<Button
|
||||
label="Update"
|
||||
onClick={handleUpdateLogo}
|
||||
className="bg-blue-500 text-white"
|
||||
disabled={!uploadedLogo}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div>
|
||||
<label className="block mb-2">Current Logo</label>
|
||||
{selectedSystem && (
|
||||
<img
|
||||
src={selectedSystem.logo}
|
||||
alt="Current logo"
|
||||
className="w-32 h-32 object-contain mb-4"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<label className="block mb-2">New Logo</label>
|
||||
<FileUpload
|
||||
mode="basic"
|
||||
name="logo"
|
||||
accept="image/*"
|
||||
maxFileSize={1000000}
|
||||
chooseLabel="Upload New Logo"
|
||||
onUpload={handleLogoUpload}
|
||||
auto
|
||||
customUpload
|
||||
/>
|
||||
{uploadedLogo && (
|
||||
<div className="mt-2">
|
||||
<img src={uploadedLogo} alt="New logo preview" className="w-32 h-32 object-contain" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -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<any[]>([]);
|
||||
@ -47,7 +53,7 @@ export default function CanvasEndpoints() {
|
||||
const [apiKey, setApiKey] = useState<string | null>(null);
|
||||
const [apiKeyLoading, setApiKeyLoading] = useState(false);
|
||||
const [apiKeyError, setApiKeyError] = useState<string | null>(null);
|
||||
const [appId, setAppId] = useState<number | null>(null);
|
||||
const [appId, setAppId] = useState<number | null>(CANVAS_APP_ID);
|
||||
const [showApiKeyModal, setShowApiKeyModal] = useState(false);
|
||||
const [editByVoiceEndpoint, setEditByVoiceEndpoint] = useState<any>(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<any>(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 = () => (
|
||||
<TableGroupHeader
|
||||
size={size}
|
||||
@ -553,18 +582,7 @@ export default function CanvasEndpoints() {
|
||||
className="p-button-primary"
|
||||
/>
|
||||
<SettingsMenu
|
||||
items={[
|
||||
{
|
||||
label: 'Manage API Key',
|
||||
icon: 'pi pi-key',
|
||||
command: () => setShowApiKeyModal(true),
|
||||
},
|
||||
{
|
||||
label: 'Voice Command List',
|
||||
icon: 'pi pi-info-circle',
|
||||
command: () => setShowVoiceInfoModal(true),
|
||||
},
|
||||
]}
|
||||
items={settingsItems}
|
||||
buttonClassName="p-button-secondary"
|
||||
/>
|
||||
<Button
|
||||
@ -578,11 +596,38 @@ export default function CanvasEndpoints() {
|
||||
/>
|
||||
);
|
||||
|
||||
const serverPath = import.meta.env.VITE_API_BASE_URL || window.location.origin;
|
||||
|
||||
const generateCurlCommand = (endpoint: any, apiKey: string) => {
|
||||
const backendUrl = `${serverPath}/api/v1/canvas-api/proxy-external`;
|
||||
const method = endpoint.method || 'GET';
|
||||
const path = endpoint.path;
|
||||
const header = `-H 'Authorization: Bearer ${apiKey}'`;
|
||||
const data = `-d '{"path": "${path}", "method": "${method}"}'`;
|
||||
return `curl -X POST ${header} -H 'Content-Type: application/json' ${data} '${backendUrl}'`;
|
||||
};
|
||||
|
||||
const renderCallButton = (rowData: any) => (
|
||||
<div className="flex gap-2">
|
||||
<Button icon="pi pi-play" onClick={() => handleCallEndpoint(rowData)} />
|
||||
<Button icon="pi pi-pencil" className="p-button-warning" onClick={() => openEditModal(rowData)} />
|
||||
<Button icon="pi pi-trash" className="p-button-danger" onClick={() => handleDeleteEndpoint(rowData)} />
|
||||
<span>
|
||||
<Button icon="pi pi-copy" className="p-button-secondary" onClick={() => {
|
||||
if (!apiKey) {
|
||||
if (toast.current) {
|
||||
toast.current.show({ severity: 'warn', summary: 'API Key Required', detail: 'Please generate or enter your API key first.' });
|
||||
}
|
||||
return;
|
||||
}
|
||||
const curl = generateCurlCommand(rowData, apiKey);
|
||||
navigator.clipboard.writeText(curl);
|
||||
if (toast.current) {
|
||||
toast.current.show({ severity: 'info', summary: 'Copied', detail: 'cURL command copied to clipboard!' });
|
||||
}
|
||||
}} data-pr-tooltip="Copy cURL command" />
|
||||
<Tooltip target=".p-button-secondary" content="Copy cURL command" />
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -635,11 +680,13 @@ export default function CanvasEndpoints() {
|
||||
<Dialog
|
||||
visible={showCreateModal}
|
||||
onHide={handleModalHide}
|
||||
header={editEndpointId ? "Edit" : "Create"}
|
||||
header={editEndpointId ? 'Edit Endpoint' : 'Create Endpoint'}
|
||||
style={{ width: '40vw' }}
|
||||
modal
|
||||
footer={
|
||||
<div>
|
||||
<Button label="Cancel" icon="pi pi-times" onClick={handleModalHide} className="p-button-text" />
|
||||
<Button label={editEndpointId ? "Save" : "Create"} icon="pi pi-check" onClick={handleSaveEndpoint} autoFocus disabled={!(newEndpoint.name && newEndpoint.method && newEndpoint.path)} />
|
||||
<Button label="Cancel" onClick={handleModalHide} className="mr-2" />
|
||||
<Button label={editEndpointId ? 'Save' : 'Create'} onClick={handleSaveEndpoint} className="bg-blue-500 text-white" />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
@ -650,18 +697,7 @@ export default function CanvasEndpoints() {
|
||||
</div>
|
||||
<div className="p-field">
|
||||
<label htmlFor="method">Method</label>
|
||||
<Dropdown
|
||||
id="method"
|
||||
value={newEndpoint.method}
|
||||
options={[
|
||||
{ label: 'GET', value: 'GET' },
|
||||
{ label: 'POST', value: 'POST' },
|
||||
{ label: 'PUT', value: 'PUT' },
|
||||
{ label: 'DELETE', value: 'DELETE' }
|
||||
]}
|
||||
onChange={(e) => setNewEndpoint({ ...newEndpoint, method: e.value })}
|
||||
placeholder="Select a method"
|
||||
/>
|
||||
<InputText id="method" value={newEndpoint.method} onChange={(e) => setNewEndpoint({ ...newEndpoint, method: e.target.value })} />
|
||||
</div>
|
||||
<div className="p-field">
|
||||
<label htmlFor="path">Path</label>
|
||||
@ -855,6 +891,14 @@ export default function CanvasEndpoints() {
|
||||
<li><b>open command list</b> — Show this help</li>
|
||||
</ul>
|
||||
</Dialog>
|
||||
|
||||
<ManageDocsModal visible={showManageDocs} onHide={() => setShowManageDocs(false)} />
|
||||
|
||||
<PromptCreatorModal
|
||||
visible={showPromptCreator}
|
||||
onHide={() => setShowPromptCreator(false)}
|
||||
appId={appId || 0}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
143
frontend/src/components/canvas-api/ManageDocsModal.tsx
Normal file
143
frontend/src/components/canvas-api/ManageDocsModal.tsx
Normal file
@ -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<string[]>([]);
|
||||
const [selectedUrl, setSelectedUrl] = useState<string | null>(null);
|
||||
const [newUrl, setNewUrl] = useState('');
|
||||
const [editIndex, setEditIndex] = useState<number | null>(null);
|
||||
const [editValue, setEditValue] = useState('');
|
||||
const [parsing, setParsing] = useState(false);
|
||||
const [parseResult, setParseResult] = useState<ParseResult | null>(null);
|
||||
const [error, setError] = useState<string | null>(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 (
|
||||
<Dialog visible={visible} onHide={onHide} header="Manage API Docs" style={{ width: '60vw' }}>
|
||||
<div className="flex gap-4">
|
||||
{/* URL List and Controls */}
|
||||
<div style={{ minWidth: 300 }}>
|
||||
<h4>Documentation URLs</h4>
|
||||
<ListBox
|
||||
value={selectedUrl}
|
||||
options={urls.map(u => ({ label: u, value: u }))}
|
||||
onChange={e => setSelectedUrl(e.value)}
|
||||
style={{ width: '100%', minHeight: 200 }}
|
||||
/>
|
||||
<div className="flex gap-2 mt-2">
|
||||
<InputText
|
||||
value={newUrl}
|
||||
onChange={e => setNewUrl(e.target.value)}
|
||||
placeholder="Add new URL"
|
||||
className="w-full"
|
||||
/>
|
||||
<Button label="Add" onClick={addUrl} disabled={!newUrl} />
|
||||
</div>
|
||||
{selectedUrl && (
|
||||
<div className="flex gap-2 mt-2">
|
||||
<Button label="Parse" onClick={() => handleParse(selectedUrl)} loading={parsing} />
|
||||
<Button label="Remove" className="p-button-danger" onClick={() => removeUrl(selectedUrl)} />
|
||||
<Button label="Edit" onClick={() => startEdit(urls.indexOf(selectedUrl))} />
|
||||
</div>
|
||||
)}
|
||||
{editIndex !== null && (
|
||||
<div className="flex gap-2 mt-2">
|
||||
<InputText value={editValue} onChange={e => setEditValue(e.target.value)} className="w-full" />
|
||||
<Button label="Save" onClick={saveEdit} />
|
||||
<Button label="Cancel" className="p-button-secondary" onClick={() => setEditIndex(null)} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{/* Parse Results */}
|
||||
<div style={{ flex: 1 }}>
|
||||
{error && <div className="text-red-500 mb-2">{error}</div>}
|
||||
{parseResult && (
|
||||
<div>
|
||||
<h4>Raw HTML (first 500 chars):</h4>
|
||||
<pre style={{ maxHeight: 200, overflow: 'auto', background: '#f8f9fa', padding: 8 }}>{parseResult.rawHtml?.slice(0, 500)}...</pre>
|
||||
<h4>AI Output:</h4>
|
||||
<pre style={{ maxHeight: 200, overflow: 'auto', background: '#f8f9fa', padding: 8 }}>{JSON.stringify(parseResult.aiOutput, null, 2)}</pre>
|
||||
<h4>Restructured Output:</h4>
|
||||
<pre style={{ maxHeight: 200, overflow: 'auto', background: '#f8f9fa', padding: 8 }}>{JSON.stringify(parseResult.restructured, null, 2)}</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export default ManageDocsModal;
|
153
frontend/src/components/canvas-api/PromptCreatorModal.tsx
Normal file
153
frontend/src/components/canvas-api/PromptCreatorModal.tsx
Normal file
@ -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<string | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const toast = useRef<Toast>(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 (
|
||||
<>
|
||||
<Toast ref={toast} />
|
||||
<Dialog
|
||||
visible={visible}
|
||||
onHide={onHide}
|
||||
header="Create New Prompt"
|
||||
style={{ width: '50vw' }}
|
||||
footer={
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button
|
||||
label="Cancel"
|
||||
icon="pi pi-times"
|
||||
onClick={onHide}
|
||||
className="p-button-text"
|
||||
/>
|
||||
<Button
|
||||
label="Create"
|
||||
icon="pi pi-check"
|
||||
onClick={handleSubmit}
|
||||
loading={loading}
|
||||
disabled={!name || !content || !type}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="flex flex-col gap-4 p-4">
|
||||
<div className="field">
|
||||
<label htmlFor="name" className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Prompt Name
|
||||
</label>
|
||||
<InputText
|
||||
id="name"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
className="w-full"
|
||||
placeholder="Enter a name for your prompt..."
|
||||
/>
|
||||
</div>
|
||||
<div className="field">
|
||||
<label htmlFor="type" className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Prompt Type
|
||||
</label>
|
||||
<Dropdown
|
||||
id="type"
|
||||
value={type}
|
||||
options={promptTypes}
|
||||
onChange={(e) => setType(e.value)}
|
||||
placeholder="Select a type"
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
<div className="field">
|
||||
<label htmlFor="content" className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Prompt Content
|
||||
</label>
|
||||
<InputTextarea
|
||||
id="content"
|
||||
value={content}
|
||||
onChange={(e) => setContent(e.target.value)}
|
||||
className="w-full"
|
||||
rows={8}
|
||||
autoResize
|
||||
placeholder="Enter your prompt..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
}
|
61
frontend/src/components/canvas-api/api.ts
Normal file
61
frontend/src/components/canvas-api/api.ts
Normal file
@ -0,0 +1,61 @@
|
||||
import axios, { AxiosRequestConfig, AxiosResponse } from 'axios';
|
||||
|
||||
interface ApiConfig {
|
||||
baseURL?: string;
|
||||
timeout?: number;
|
||||
headers?: Record<string, string>;
|
||||
}
|
||||
|
||||
// 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 <T = any>(
|
||||
method: 'get' | 'post' | 'put' | 'delete' | 'patch',
|
||||
url: string,
|
||||
data?: any,
|
||||
params?: any,
|
||||
responseType: 'json' | 'blob' | 'text' = 'json',
|
||||
timeout?: number,
|
||||
useAuth: boolean = true
|
||||
): Promise<AxiosResponse<T>> => {
|
||||
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;
|
||||
}
|
||||
};
|
@ -67,7 +67,7 @@ const Sidebar = ({ systemId, isCollapsed, setIsCollapsed }) => {
|
||||
/>
|
||||
|
||||
<div className='flex justify-center w-full p-0 m-0'>
|
||||
<Link to='/dashboard'>
|
||||
<Link to='/'>
|
||||
<Button
|
||||
label={isCollapsed ? '' : 'Back'}
|
||||
icon='pi pi-arrow-left'
|
||||
|
@ -1,8 +1,9 @@
|
||||
import axios, { AxiosResponse, ResponseType } from 'axios';
|
||||
import { useApiLogStore } from './apiLogStore';
|
||||
|
||||
// Server path
|
||||
const serverPath = import.meta.env.VITE_API_BASE_URL as string;
|
||||
// Clean server path, removing trailing slash if present
|
||||
const rawBaseUrl = import.meta.env.VITE_API_BASE_URL || '';
|
||||
const serverPath = rawBaseUrl.replace(/\/$/, '');
|
||||
|
||||
type THttpMethod = 'get' | 'put' | 'post' | 'delete';
|
||||
|
||||
@ -27,10 +28,9 @@ export async function api<T = any>(
|
||||
const token = user?.token || '';
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json'
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
|
||||
// Only add auth headers if not skipping auth
|
||||
if (!skipAuth && token) {
|
||||
headers['Authorization'] = `Bearer ${token}`;
|
||||
}
|
||||
@ -39,7 +39,7 @@ export async function api<T = any>(
|
||||
baseURL: serverPath,
|
||||
timeout: timeoutInSeconds * 1000,
|
||||
headers,
|
||||
withCredentials: !skipAuth, // Only use credentials if not skipping auth
|
||||
withCredentials: !skipAuth,
|
||||
responseType,
|
||||
});
|
||||
|
||||
@ -71,7 +71,7 @@ export async function api<T = any>(
|
||||
});
|
||||
|
||||
return apiResponse;
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
console.error('API Error:', {
|
||||
url,
|
||||
method,
|
||||
|
@ -65,11 +65,15 @@ class MemoryService {
|
||||
const event = this.activeLifeEvents.get(formationContext.temporalContext.lifeEvent);
|
||||
if (event) {
|
||||
memory.temporal.lifeEvents.push(event);
|
||||
memory.temporal.temporalContext = formationContext.temporalContext;
|
||||
memory.temporal.temporalContext = {
|
||||
period: formationContext.temporalContext.period,
|
||||
moodState: this.getMoodState(),
|
||||
cognitiveBias: this.getActiveBiases()
|
||||
};
|
||||
|
||||
// Apply emotional biases
|
||||
memory.emotional.triggers.push(...formationContext.emotionalInfluence.biasEffects.map(bias => ({
|
||||
type: 'contextual',
|
||||
type: 'contextual' as const,
|
||||
source: 'life_event',
|
||||
intensity: bias.strength,
|
||||
associatedEvent: formationContext.temporalContext.lifeEvent,
|
||||
@ -122,9 +126,18 @@ class MemoryService {
|
||||
});
|
||||
}
|
||||
|
||||
private async retrieveMemoryFromStorage(query: MemoryRetrieval): Promise<MemoryUnit[]> {
|
||||
// TODO: Implement actual storage retrieval
|
||||
return [];
|
||||
}
|
||||
|
||||
async consolidateMemories(consolidation: MemoryConsolidation): Promise<MemoryUnit> {
|
||||
const memories = await Promise.all(
|
||||
consolidation.sourceMemories.map(id => this.retrieveMemory({ query: id }))
|
||||
consolidation.sourceMemories.map(id => this.retrieveMemory({
|
||||
query: id,
|
||||
context: {},
|
||||
filters: {}
|
||||
}))
|
||||
).then(results => results.flat());
|
||||
|
||||
// Calculate emotional influence
|
||||
@ -321,7 +334,11 @@ class MemoryService {
|
||||
llmContext: Partial<LLMContextMemory['content']>
|
||||
): Promise<MemoryUnit> {
|
||||
const memories = await Promise.all(
|
||||
sourceMemories.map(id => this.retrieveMemory({ query: id }))
|
||||
sourceMemories.map(id => this.retrieveMemory({
|
||||
query: id,
|
||||
context: {},
|
||||
filters: {}
|
||||
}))
|
||||
).then(results => results.flat());
|
||||
|
||||
// Create new consolidated memory with LLM context
|
||||
|
@ -102,6 +102,8 @@ export interface EpisodicMemory extends MemoryUnit {
|
||||
context: Record<string, any>;
|
||||
emotionalSignificance: number;
|
||||
};
|
||||
confidence: number;
|
||||
source: string;
|
||||
};
|
||||
}
|
||||
|
||||
@ -122,6 +124,8 @@ export interface SemanticMemory extends MemoryUnit {
|
||||
category: string;
|
||||
reliability: number;
|
||||
};
|
||||
confidence: number;
|
||||
source: string;
|
||||
};
|
||||
}
|
||||
|
||||
@ -138,6 +142,8 @@ export interface ProceduralMemory extends MemoryUnit {
|
||||
proficiency: number;
|
||||
lastPracticed: Date;
|
||||
};
|
||||
confidence: number;
|
||||
source: string;
|
||||
};
|
||||
}
|
||||
|
||||
@ -225,6 +231,8 @@ export interface LLMContextMemory extends MemoryUnit {
|
||||
tokensPerSecond: number;
|
||||
};
|
||||
};
|
||||
confidence: number;
|
||||
source: string;
|
||||
};
|
||||
}
|
||||
|
||||
@ -239,8 +247,10 @@ export interface EmbeddingMemory extends MemoryUnit {
|
||||
};
|
||||
metadata: {
|
||||
similarityThreshold: number;
|
||||
retrievalMethod: 'cosine' | 'euclidean' | 'dot';
|
||||
retrievalMethod: 'dot' | 'cosine' | 'euclidean';
|
||||
};
|
||||
confidence: number;
|
||||
source: string;
|
||||
};
|
||||
}
|
||||
|
||||
@ -261,6 +271,8 @@ export interface PromptMemory extends MemoryUnit {
|
||||
successRate: number;
|
||||
averageTokens: number;
|
||||
};
|
||||
confidence: number;
|
||||
source: string;
|
||||
};
|
||||
}
|
||||
|
||||
@ -307,7 +319,7 @@ export interface LifeEventMemory extends MemoryUnit {
|
||||
content: {
|
||||
data: {
|
||||
event: {
|
||||
type: 'breakup' | 'loss' | 'achievement' | 'trauma' | 'transition';
|
||||
type: 'transition' | 'breakup' | 'loss' | 'achievement' | 'trauma';
|
||||
description: string;
|
||||
participants: string[];
|
||||
location?: string;
|
||||
@ -344,6 +356,8 @@ export interface LifeEventMemory extends MemoryUnit {
|
||||
healingProgress: number;
|
||||
relatedEvents: string[];
|
||||
};
|
||||
confidence: number;
|
||||
source: string;
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,3 @@
|
||||
// import 'dotenv/config';
|
||||
|
||||
|
||||
import dotenv from 'dotenv';
|
||||
|
||||
// Force reload `.env` even if it was previously loaded
|
||||
@ -27,12 +24,11 @@ const config: Options = {
|
||||
warnWhenNoEntities: true,
|
||||
disableDynamicFileAccess: false,
|
||||
},
|
||||
dbName: process.env.POSTGRES_NAME,
|
||||
host: process.env.POSTGRES_HOSTNAME,
|
||||
port: Number(process.env.POSTGRES_PORT),
|
||||
// port: parseInt(process.env.POSTGRES_PORT || "5432", 10),
|
||||
user: process.env.POSTGRES_USER,
|
||||
password: process.env.POSTGRES_PASSWORD,
|
||||
dbName: process.env.POSTGRES_NAME || 'fusero-boilerplate-db',
|
||||
host: process.env.POSTGRES_HOSTNAME || 'localhost',
|
||||
port: Number(process.env.POSTGRES_PORT) || 5432,
|
||||
user: process.env.POSTGRES_USER || 'root',
|
||||
password: process.env.POSTGRES_PASSWORD || 'root123',
|
||||
debug: !isProduction,
|
||||
migrations: {
|
||||
tableName: process.env.POSTGRES_NAME,
|
||||
|
27
nginx/README.md
Normal file
27
nginx/README.md
Normal file
@ -0,0 +1,27 @@
|
||||
# Nginx SSL Reverse Proxy Setup
|
||||
|
||||
## Generate Self-Signed Certificates
|
||||
|
||||
Run this script from the `nginx/` directory:
|
||||
|
||||
```bash
|
||||
./generate-selfsigned.sh
|
||||
```
|
||||
|
||||
This will create `certs/fusero-selfsigned.crt` and `certs/fusero-selfsigned.key`.
|
||||
|
||||
## Usage with Docker Compose
|
||||
|
||||
1. Make sure the certs are generated as above.
|
||||
2. Start the stack from the project root:
|
||||
|
||||
```bash
|
||||
docker compose up -d --build
|
||||
```
|
||||
|
||||
- Nginx will listen on ports 443 (HTTPS) and 80 (HTTP, redirected to HTTPS).
|
||||
- All traffic will be routed securely to your frontend and backend containers.
|
||||
|
||||
## Notes
|
||||
- Browsers will warn about the self-signed certificate. You can safely bypass this for development/testing.
|
||||
- For production, use a trusted certificate authority.
|
8
nginx/generate-selfsigned.sh
Normal file
8
nginx/generate-selfsigned.sh
Normal file
@ -0,0 +1,8 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
mkdir -p certs
|
||||
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
|
||||
-keyout certs/fusero-selfsigned.key \
|
||||
-out certs/fusero-selfsigned.crt \
|
||||
-subj "/CN=localhost"
|
||||
echo "Self-signed certificate generated in nginx/certs/"
|
38
nginx/nginx.conf
Normal file
38
nginx/nginx.conf
Normal file
@ -0,0 +1,38 @@
|
||||
# Nginx SSL reverse proxy config
|
||||
|
||||
# Redirect HTTP to HTTPS
|
||||
server {
|
||||
listen 80;
|
||||
server_name _;
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
|
||||
# HTTPS server
|
||||
server {
|
||||
listen 443 ssl;
|
||||
server_name _;
|
||||
|
||||
ssl_certificate /etc/nginx/certs/fusero-selfsigned.crt;
|
||||
ssl_certificate_key /etc/nginx/certs/fusero-selfsigned.key;
|
||||
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_ciphers HIGH:!aNULL:!MD5;
|
||||
|
||||
# Proxy frontend
|
||||
location / {
|
||||
proxy_pass http://fusero-frontend:80;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
# Proxy API (backend)
|
||||
location /api/ {
|
||||
proxy_pass http://fusero-app-boilerplate:14000/api/;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
19
nginx/nginx.conf.dev
Normal file
19
nginx/nginx.conf.dev
Normal file
@ -0,0 +1,19 @@
|
||||
server {
|
||||
listen 80;
|
||||
server_name _;
|
||||
|
||||
location ^~ /api/ {
|
||||
proxy_pass http://fusero-app-backend:14000/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $remote_addr;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
location / {
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
}
|
38
nginx/nginx.conf.prod
Normal file
38
nginx/nginx.conf.prod
Normal file
@ -0,0 +1,38 @@
|
||||
# Nginx SSL reverse proxy config
|
||||
|
||||
# Redirect HTTP to HTTPS
|
||||
server {
|
||||
listen 80;
|
||||
server_name _;
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
|
||||
# HTTPS server
|
||||
server {
|
||||
listen 443 ssl;
|
||||
server_name _;
|
||||
|
||||
ssl_certificate /etc/nginx/certs/fusero-selfsigned.crt;
|
||||
ssl_certificate_key /etc/nginx/certs/fusero-selfsigned.key;
|
||||
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_ciphers HIGH:!aNULL:!MD5;
|
||||
|
||||
# Proxy frontend
|
||||
location / {
|
||||
proxy_pass http://fusero-frontend:80;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
# Proxy API (backend)
|
||||
location /api/ {
|
||||
proxy_pass http://fusero-app-boilerplate:14000/api/;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
124
src/apps/_app/controllers/PromptController.ts
Normal file
124
src/apps/_app/controllers/PromptController.ts
Normal file
@ -0,0 +1,124 @@
|
||||
import { FastifyRequest, FastifyReply } from 'fastify';
|
||||
import { EntityManager } from '@mikro-orm/core';
|
||||
import { PromptService } from '../services/PromptService';
|
||||
import { PromptType } from '../entities/Prompt';
|
||||
import { App } from '../entities/app/_App';
|
||||
import { User } from '../entities/user/_User';
|
||||
|
||||
interface CreatePromptBody {
|
||||
content: string;
|
||||
type: PromptType;
|
||||
appId: number;
|
||||
metadata?: Record<string, any>;
|
||||
}
|
||||
|
||||
interface PromptParams {
|
||||
id: string;
|
||||
}
|
||||
|
||||
interface AppParams {
|
||||
appId: string;
|
||||
}
|
||||
|
||||
export class PromptController {
|
||||
private promptService: PromptService;
|
||||
|
||||
constructor(private readonly em: EntityManager) {
|
||||
this.promptService = new PromptService(em);
|
||||
}
|
||||
|
||||
async createPrompt(
|
||||
request: FastifyRequest<{ Body: CreatePromptBody }>,
|
||||
reply: FastifyReply
|
||||
) {
|
||||
try {
|
||||
const { content, type, appId, metadata } = request.body;
|
||||
const user = request.user as User;
|
||||
const app = await this.em.findOne(App, { id: appId });
|
||||
|
||||
if (!app) {
|
||||
return reply.status(404).send({ error: 'App not found' });
|
||||
}
|
||||
|
||||
const prompt = await this.promptService.createPrompt(
|
||||
content,
|
||||
type,
|
||||
user,
|
||||
app,
|
||||
metadata
|
||||
);
|
||||
|
||||
return reply.status(201).send(prompt);
|
||||
} catch (error) {
|
||||
return reply.status(500).send({ error: 'Failed to create prompt' });
|
||||
}
|
||||
}
|
||||
|
||||
async processPrompt(
|
||||
request: FastifyRequest<{ Params: PromptParams }>,
|
||||
reply: FastifyReply
|
||||
) {
|
||||
try {
|
||||
const id = parseInt(request.params.id, 10);
|
||||
|
||||
if (isNaN(id)) {
|
||||
return reply.status(400).send({ error: 'Invalid prompt ID' });
|
||||
}
|
||||
|
||||
const prompt = await this.promptService.processPrompt(id);
|
||||
return reply.send(prompt);
|
||||
} catch (error) {
|
||||
return reply.status(500).send({ error: 'Failed to process prompt' });
|
||||
}
|
||||
}
|
||||
|
||||
async getPrompt(
|
||||
request: FastifyRequest<{ Params: PromptParams }>,
|
||||
reply: FastifyReply
|
||||
) {
|
||||
try {
|
||||
const id = parseInt(request.params.id, 10);
|
||||
|
||||
if (isNaN(id)) {
|
||||
return reply.status(400).send({ error: 'Invalid prompt ID' });
|
||||
}
|
||||
|
||||
const prompt = await this.promptService.getPromptById(id);
|
||||
if (!prompt) {
|
||||
return reply.status(404).send({ error: 'Prompt not found' });
|
||||
}
|
||||
|
||||
return reply.send(prompt);
|
||||
} catch (error) {
|
||||
return reply.status(500).send({ error: 'Failed to get prompt' });
|
||||
}
|
||||
}
|
||||
|
||||
async getUserPrompts(request: FastifyRequest, reply: FastifyReply) {
|
||||
try {
|
||||
const user = request.user as User;
|
||||
const prompts = await this.promptService.getPromptsByUser(user.id);
|
||||
return reply.send(prompts);
|
||||
} catch (error) {
|
||||
return reply.status(500).send({ error: 'Failed to get user prompts' });
|
||||
}
|
||||
}
|
||||
|
||||
async getAppPrompts(
|
||||
request: FastifyRequest<{ Params: AppParams }>,
|
||||
reply: FastifyReply
|
||||
) {
|
||||
try {
|
||||
const appId = parseInt(request.params.appId, 10);
|
||||
|
||||
if (isNaN(appId)) {
|
||||
return reply.status(400).send({ error: 'Invalid app ID' });
|
||||
}
|
||||
|
||||
const prompts = await this.promptService.getPromptsByApp(appId);
|
||||
return reply.send(prompts);
|
||||
} catch (error) {
|
||||
return reply.status(500).send({ error: 'Failed to get app prompts' });
|
||||
}
|
||||
}
|
||||
}
|
48
src/apps/_app/entities/Prompt.ts
Normal file
48
src/apps/_app/entities/Prompt.ts
Normal file
@ -0,0 +1,48 @@
|
||||
import { Entity, Property, ManyToOne, Enum } from '@mikro-orm/core';
|
||||
import { BaseEntity } from './_BaseEntity';
|
||||
import { User } from './user/_User';
|
||||
import { App } from './app/_App';
|
||||
|
||||
export enum PromptType {
|
||||
API_DOCUMENTATION = 'api_documentation',
|
||||
ENDPOINT_ANALYSIS = 'endpoint_analysis',
|
||||
SCHEMA_GENERATION = 'schema_generation',
|
||||
CUSTOM = 'custom'
|
||||
}
|
||||
|
||||
export enum PromptStatus {
|
||||
PENDING = 'pending',
|
||||
PROCESSING = 'processing',
|
||||
COMPLETED = 'completed',
|
||||
FAILED = 'failed'
|
||||
}
|
||||
|
||||
@Entity()
|
||||
export class Prompt extends BaseEntity {
|
||||
@Property({ type: 'text', nullable: true })
|
||||
name?: string;
|
||||
|
||||
@Property({ type: 'text' })
|
||||
content!: string;
|
||||
|
||||
@Property({ type: 'jsonb', default: '{}' })
|
||||
metadata!: Record<string, any>;
|
||||
|
||||
@Enum(() => PromptType)
|
||||
type!: PromptType;
|
||||
|
||||
@Enum(() => PromptStatus)
|
||||
status: PromptStatus = PromptStatus.PENDING;
|
||||
|
||||
@ManyToOne(() => User)
|
||||
user!: User;
|
||||
|
||||
@ManyToOne(() => App)
|
||||
app!: App;
|
||||
|
||||
@Property({ type: 'jsonb', nullable: true })
|
||||
processedResult?: Record<string, any>;
|
||||
|
||||
@Property({ type: 'text', nullable: true })
|
||||
errorMessage?: string;
|
||||
}
|
@ -7,6 +7,18 @@ export class App extends BaseEntity {
|
||||
@Property()
|
||||
name!: string;
|
||||
|
||||
@Property({ nullable: true })
|
||||
method?: string;
|
||||
|
||||
@Property({ nullable: true })
|
||||
path?: string;
|
||||
|
||||
@Property({ nullable: true })
|
||||
description?: string;
|
||||
|
||||
@Property({ nullable: true })
|
||||
publicPath?: string;
|
||||
|
||||
@OneToMany(() => TenantApp, (tenantApp) => tenantApp.app)
|
||||
tenantApps = new Collection<TenantApp>(this);
|
||||
}
|
||||
|
30
src/apps/_app/entities/asset/_Asset.ts
Normal file
30
src/apps/_app/entities/asset/_Asset.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import { Entity, Property, Enum } from '@mikro-orm/core';
|
||||
import { BaseEntity } from '../_BaseEntity';
|
||||
|
||||
export enum AssetType {
|
||||
SYSTEM_LOGO = 'system_logo',
|
||||
USER_AVATAR = 'user_avatar',
|
||||
DOCUMENT = 'document',
|
||||
OTHER = 'other'
|
||||
}
|
||||
|
||||
@Entity()
|
||||
export class Asset extends BaseEntity {
|
||||
@Property()
|
||||
name!: string;
|
||||
|
||||
@Property()
|
||||
type!: string;
|
||||
|
||||
@Property({ type: 'text' })
|
||||
data!: string; // Base64 encoded data
|
||||
|
||||
@Property()
|
||||
mimeType!: string;
|
||||
|
||||
@Property({ nullable: true })
|
||||
size?: number;
|
||||
|
||||
@Enum(() => AssetType)
|
||||
assetType!: AssetType;
|
||||
}
|
30
src/apps/_app/middleware/auth.middleware.ts
Normal file
30
src/apps/_app/middleware/auth.middleware.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import { FastifyRequest, FastifyReply } from 'fastify';
|
||||
import { EntityManager } from '@mikro-orm/core';
|
||||
import { User } from '../entities/user/_User';
|
||||
import '@fastify/jwt';
|
||||
|
||||
declare module '@fastify/jwt' {
|
||||
interface FastifyJWT {
|
||||
user: User;
|
||||
}
|
||||
}
|
||||
|
||||
export const authMiddleware = async (request: FastifyRequest, reply: FastifyReply) => {
|
||||
try {
|
||||
const em = request.server.orm.em as EntityManager;
|
||||
const userId = request.user?.id;
|
||||
|
||||
if (!userId) {
|
||||
return reply.status(401).send({ error: 'Unauthorized' });
|
||||
}
|
||||
|
||||
const user = await em.findOne(User, { id: userId });
|
||||
if (!user) {
|
||||
return reply.status(401).send({ error: 'User not found' });
|
||||
}
|
||||
|
||||
request.user = user;
|
||||
} catch (error) {
|
||||
return reply.status(500).send({ error: 'Internal server error' });
|
||||
}
|
||||
};
|
115
src/apps/_app/routes/prompt.routes.ts
Normal file
115
src/apps/_app/routes/prompt.routes.ts
Normal file
@ -0,0 +1,115 @@
|
||||
import { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
|
||||
import { PromptController } from '../controllers/PromptController';
|
||||
import { authMiddleware } from '../middleware/auth.middleware';
|
||||
import { PromptType } from '../entities/Prompt';
|
||||
|
||||
interface CreatePromptBody {
|
||||
content: string;
|
||||
type: PromptType;
|
||||
appId: number;
|
||||
metadata?: Record<string, any>;
|
||||
}
|
||||
|
||||
interface PromptParams {
|
||||
id: string;
|
||||
}
|
||||
|
||||
interface AppParams {
|
||||
appId: string;
|
||||
}
|
||||
|
||||
const createPromptSchema = {
|
||||
body: {
|
||||
type: 'object',
|
||||
required: ['content', 'type', 'appId'],
|
||||
properties: {
|
||||
content: { type: 'string' },
|
||||
type: { type: 'string', enum: Object.values(PromptType) },
|
||||
appId: { type: 'number' },
|
||||
metadata: { type: 'object' }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const promptParamsSchema = {
|
||||
params: {
|
||||
type: 'object',
|
||||
required: ['id'],
|
||||
properties: {
|
||||
id: { type: 'string' }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const appParamsSchema = {
|
||||
params: {
|
||||
type: 'object',
|
||||
required: ['appId'],
|
||||
properties: {
|
||||
appId: { type: 'string' }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default async function promptRoutes(fastify: FastifyInstance) {
|
||||
const promptController = new PromptController(fastify.orm.em);
|
||||
|
||||
// Create a new prompt
|
||||
fastify.post<{ Body: CreatePromptBody }>(
|
||||
'/prompts',
|
||||
{
|
||||
schema: createPromptSchema,
|
||||
preHandler: authMiddleware
|
||||
},
|
||||
async (request, reply) => {
|
||||
return promptController.createPrompt(request, reply);
|
||||
}
|
||||
);
|
||||
|
||||
// Process a prompt
|
||||
fastify.post<{ Params: PromptParams }>(
|
||||
'/prompts/:id/process',
|
||||
{
|
||||
schema: promptParamsSchema,
|
||||
preHandler: authMiddleware
|
||||
},
|
||||
async (request, reply) => {
|
||||
return promptController.processPrompt(request, reply);
|
||||
}
|
||||
);
|
||||
|
||||
// Get a prompt by ID
|
||||
fastify.get<{ Params: PromptParams }>(
|
||||
'/prompts/:id',
|
||||
{
|
||||
schema: promptParamsSchema,
|
||||
preHandler: authMiddleware
|
||||
},
|
||||
async (request, reply) => {
|
||||
return promptController.getPrompt(request, reply);
|
||||
}
|
||||
);
|
||||
|
||||
// Get all prompts for the current user
|
||||
fastify.get(
|
||||
'/prompts',
|
||||
{
|
||||
preHandler: authMiddleware
|
||||
},
|
||||
async (request, reply) => {
|
||||
return promptController.getUserPrompts(request, reply);
|
||||
}
|
||||
);
|
||||
|
||||
// Get all prompts for a specific app
|
||||
fastify.get<{ Params: AppParams }>(
|
||||
'/apps/:appId/prompts',
|
||||
{
|
||||
schema: appParamsSchema,
|
||||
preHandler: authMiddleware
|
||||
},
|
||||
async (request, reply) => {
|
||||
return promptController.getAppPrompts(request, reply);
|
||||
}
|
||||
);
|
||||
}
|
122
src/apps/_app/services/PromptService.ts
Normal file
122
src/apps/_app/services/PromptService.ts
Normal file
@ -0,0 +1,122 @@
|
||||
import { EntityManager } from '@mikro-orm/core';
|
||||
import { Prompt, PromptType, PromptStatus } from '../entities/Prompt';
|
||||
import { User } from '../entities/user/_User';
|
||||
import { App } from '../entities/app/_App';
|
||||
|
||||
interface PromptMetadata {
|
||||
endpoints?: Array<{
|
||||
path: string;
|
||||
method: string;
|
||||
statusCodes: number[];
|
||||
}>;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
interface ProcessedResult {
|
||||
endpoints?: Array<{
|
||||
path: string;
|
||||
method: string;
|
||||
statusCodes: number[];
|
||||
description?: string;
|
||||
}>;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export class PromptService {
|
||||
constructor(private readonly em: EntityManager) {}
|
||||
|
||||
async createPrompt(
|
||||
content: string,
|
||||
type: PromptType,
|
||||
user: User,
|
||||
app: App,
|
||||
metadata: PromptMetadata = {}
|
||||
): Promise<Prompt> {
|
||||
const prompt = new Prompt();
|
||||
prompt.content = content;
|
||||
prompt.type = type;
|
||||
prompt.user = user;
|
||||
prompt.app = app;
|
||||
prompt.metadata = metadata;
|
||||
prompt.status = PromptStatus.PENDING;
|
||||
|
||||
await this.em.persistAndFlush(prompt);
|
||||
return prompt;
|
||||
}
|
||||
|
||||
async processPrompt(id: number): Promise<Prompt> {
|
||||
const prompt = await this.getPromptById(id);
|
||||
if (!prompt) {
|
||||
throw new Error('Prompt not found');
|
||||
}
|
||||
|
||||
try {
|
||||
prompt.status = PromptStatus.PROCESSING;
|
||||
await this.em.flush();
|
||||
|
||||
const result = await this.processPromptByType(prompt);
|
||||
|
||||
prompt.processedResult = result;
|
||||
prompt.status = PromptStatus.COMPLETED;
|
||||
await this.em.flush();
|
||||
|
||||
return prompt;
|
||||
} catch (error) {
|
||||
prompt.status = PromptStatus.FAILED;
|
||||
prompt.errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||
await this.em.flush();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private async processPromptByType(prompt: Prompt): Promise<ProcessedResult> {
|
||||
switch (prompt.type) {
|
||||
case PromptType.API_DOCUMENTATION:
|
||||
return this.processApiDocumentation(prompt);
|
||||
case PromptType.ENDPOINT_ANALYSIS:
|
||||
return this.processEndpointAnalysis(prompt);
|
||||
case PromptType.SCHEMA_GENERATION:
|
||||
return this.processSchemaGeneration(prompt);
|
||||
case PromptType.CUSTOM:
|
||||
return this.processCustomPrompt(prompt);
|
||||
default:
|
||||
throw new Error('Invalid prompt type');
|
||||
}
|
||||
}
|
||||
|
||||
private async processApiDocumentation(prompt: Prompt): Promise<ProcessedResult> {
|
||||
// TODO: Implement API documentation processing
|
||||
return {};
|
||||
}
|
||||
|
||||
private async processEndpointAnalysis(prompt: Prompt): Promise<ProcessedResult> {
|
||||
// TODO: Implement endpoint analysis processing
|
||||
return {};
|
||||
}
|
||||
|
||||
private async processSchemaGeneration(prompt: Prompt): Promise<ProcessedResult> {
|
||||
// TODO: Implement schema generation processing
|
||||
return {};
|
||||
}
|
||||
|
||||
private async processCustomPrompt(prompt: Prompt): Promise<ProcessedResult> {
|
||||
// TODO: Implement custom prompt processing
|
||||
return {};
|
||||
}
|
||||
|
||||
async getPromptById(id: number): Promise<Prompt | null> {
|
||||
return this.em.findOne(Prompt, { id });
|
||||
}
|
||||
|
||||
async getPromptsByUser(userId: number): Promise<Prompt[]> {
|
||||
return this.em.find(Prompt, { user: { id: userId } });
|
||||
}
|
||||
|
||||
async getPromptsByApp(appId: number): Promise<Prompt[]> {
|
||||
return this.em.find(Prompt, { app: { id: appId } });
|
||||
}
|
||||
|
||||
async getPromptsByStatus(status: PromptStatus): Promise<Prompt[]> {
|
||||
return this.em.find(Prompt, { status });
|
||||
}
|
||||
}
|
@ -2,7 +2,7 @@ import { FastifyPluginAsync, FastifyRequest, FastifyReply } from 'fastify';
|
||||
import { CanvasApiEndpointController } from '../http/controllers/CanvasApiEndpointController';
|
||||
import axios, { Method } from 'axios';
|
||||
|
||||
const ALLOWED_METHODS: Method[] = ['GET']; // Expand as needed
|
||||
const ALLOWED_METHODS: Method[] = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS', 'HEAD'];
|
||||
|
||||
// Type for proxy request input
|
||||
interface CanvasProxyRequestInput {
|
||||
@ -51,6 +51,7 @@ const endpointsRoutes: FastifyPluginAsync = async (app) => {
|
||||
let baseUrl = process.env.CANVAS_API_URL || 'https://talnet.instructure.com';
|
||||
// Remove trailing /api/v1 if present
|
||||
baseUrl = baseUrl.replace(/\/api\/v1$/, '');
|
||||
// Always use the server's Canvas API key for Canvas requests
|
||||
const apiKey = process.env.CANVAS_API_KEY || '';
|
||||
const { path, method = 'GET', params } = request.body;
|
||||
if (!path) return reply.status(400).send({ error: 'Missing path' });
|
||||
@ -58,6 +59,7 @@ const endpointsRoutes: FastifyPluginAsync = async (app) => {
|
||||
return reply.status(405).send({ error: `Method ${method} not allowed` });
|
||||
}
|
||||
const url = `${baseUrl}/api/v1${path}`;
|
||||
console.log('Proxying to Canvas:', { url, method, apiKey, params });
|
||||
const response = await axios.request({
|
||||
url,
|
||||
method,
|
||||
@ -65,9 +67,11 @@ const endpointsRoutes: FastifyPluginAsync = async (app) => {
|
||||
params: method === 'GET' ? params : undefined,
|
||||
data: method !== 'GET' ? params : undefined,
|
||||
});
|
||||
console.log('Canvas response:', response.data);
|
||||
return reply.send(response.data);
|
||||
} catch (err) {
|
||||
return reply.status(500).send({ error: err.message });
|
||||
console.error('Proxy error:', err);
|
||||
return reply.status(500).send({ error: err.message, details: err.response?.data || err });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
50
src/database/migrations/Migration20240416000001.ts
Normal file
50
src/database/migrations/Migration20240416000001.ts
Normal file
@ -0,0 +1,50 @@
|
||||
import { Migration } from '@mikro-orm/migrations';
|
||||
|
||||
export class Migration20240416000001 extends Migration {
|
||||
async up(): Promise<void> {
|
||||
this.addSql(`
|
||||
create table "prompt" (
|
||||
"id" serial primary key,
|
||||
"created_at" timestamptz not null,
|
||||
"updated_at" timestamptz not null,
|
||||
"name" text null,
|
||||
"content" text not null,
|
||||
"metadata" jsonb not null default '{}',
|
||||
"type" varchar(50) not null,
|
||||
"status" varchar(50) not null default 'pending',
|
||||
"user_id" int not null,
|
||||
"app_id" int not null,
|
||||
"processed_result" jsonb null,
|
||||
"error_message" text null
|
||||
);
|
||||
`);
|
||||
|
||||
this.addSql(`
|
||||
alter table "prompt"
|
||||
add constraint "prompt_user_id_foreign"
|
||||
foreign key ("user_id")
|
||||
references "user" ("id")
|
||||
on update cascade
|
||||
on delete cascade;
|
||||
`);
|
||||
|
||||
this.addSql(`
|
||||
alter table "prompt"
|
||||
add constraint "prompt_app_id_foreign"
|
||||
foreign key ("app_id")
|
||||
references "app" ("id")
|
||||
on update cascade
|
||||
on delete cascade;
|
||||
`);
|
||||
|
||||
// Add indexes for common queries
|
||||
this.addSql('create index "prompt_user_id_index" on "prompt" ("user_id");');
|
||||
this.addSql('create index "prompt_app_id_index" on "prompt" ("app_id");');
|
||||
this.addSql('create index "prompt_status_index" on "prompt" ("status");');
|
||||
this.addSql('create index "prompt_type_index" on "prompt" ("type");');
|
||||
}
|
||||
|
||||
async down(): Promise<void> {
|
||||
this.addSql('drop table if exists "prompt" cascade;');
|
||||
}
|
||||
}
|
@ -3,15 +3,19 @@ import { Migration } from '@mikro-orm/migrations';
|
||||
export class Migration20250502161232 extends Migration {
|
||||
|
||||
override async up(): Promise<void> {
|
||||
this.addSql(`create table "canvas_api_endpoints" ("id" serial primary key, "created_at" timestamptz not null, "updated_at" timestamptz not null, "name" varchar(255) not null, "method" varchar(255) not null, "path" varchar(255) not null, "description" varchar(255) null, "user_id" int null);`);
|
||||
|
||||
this.addSql(`alter table "canvas_api_endpoints" add constraint "canvas_api_endpoints_user_id_foreign" foreign key ("user_id") references "user" ("id") on update cascade on delete set null;`);
|
||||
|
||||
this.addSql('alter table "canvas_api_endpoints" add column "public_path" varchar(255) null;');
|
||||
// Add API-related columns to app table
|
||||
this.addSql('alter table "app" add column "method" varchar(255) null;');
|
||||
this.addSql('alter table "app" add column "path" varchar(255) null;');
|
||||
this.addSql('alter table "app" add column "description" varchar(255) null;');
|
||||
this.addSql('alter table "app" add column "public_path" varchar(255) null;');
|
||||
}
|
||||
|
||||
override async down(): Promise<void> {
|
||||
this.addSql(`drop table if exists "canvas_api_endpoints" cascade;`);
|
||||
// Remove the added columns
|
||||
this.addSql('alter table "app" drop column "method";');
|
||||
this.addSql('alter table "app" drop column "path";');
|
||||
this.addSql('alter table "app" drop column "description";');
|
||||
this.addSql('alter table "app" drop column "public_path";');
|
||||
}
|
||||
|
||||
}
|
||||
|
23
src/database/migrations/Migration20250502161233.ts
Normal file
23
src/database/migrations/Migration20250502161233.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { Migration } from '@mikro-orm/migrations';
|
||||
|
||||
export class Migration20250502161233 extends Migration {
|
||||
override async up(): Promise<void> {
|
||||
this.addSql(`
|
||||
create table "asset" (
|
||||
"id" serial primary key,
|
||||
"created_at" timestamptz not null,
|
||||
"updated_at" timestamptz not null,
|
||||
"name" varchar(255) not null,
|
||||
"type" varchar(255) not null,
|
||||
"data" text not null,
|
||||
"mime_type" varchar(255) not null,
|
||||
"size" int null,
|
||||
"asset_type" varchar(255) not null
|
||||
);
|
||||
`);
|
||||
}
|
||||
|
||||
override async down(): Promise<void> {
|
||||
this.addSql('drop table if exists "asset" cascade;');
|
||||
}
|
||||
}
|
20
src/database/seeds/CanvasAppSeed.ts
Normal file
20
src/database/seeds/CanvasAppSeed.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { EntityManager } from '@mikro-orm/core';
|
||||
import { App } from '@/apps/_app/entities/app/_App';
|
||||
|
||||
export class CanvasAppSeed {
|
||||
constructor(private readonly em: EntityManager) {}
|
||||
|
||||
async run() {
|
||||
console.log('Seeding Canvas app/system...');
|
||||
|
||||
const existing = await this.em.findOne(App, { name: 'Canvas API' });
|
||||
if (!existing) {
|
||||
const app = new App();
|
||||
app.name = 'Canvas API';
|
||||
await this.em.persistAndFlush(app);
|
||||
console.log('Created Canvas app/system.');
|
||||
} else {
|
||||
console.log('Canvas app/system already exists, skipping...');
|
||||
}
|
||||
}
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
import { EntityManager } from '@mikro-orm/core';
|
||||
import { UserSeed } from './UserSeed';
|
||||
import { RoleSeed } from './RoleSeed';
|
||||
import { CanvasAppSeed } from './CanvasAppSeed';
|
||||
|
||||
export class DatabaseSeeder {
|
||||
constructor(private readonly em: EntityManager) {}
|
||||
@ -19,6 +20,10 @@ export class DatabaseSeeder {
|
||||
const userSeeder = new UserSeed(fork);
|
||||
await userSeeder.run();
|
||||
|
||||
// Then seed Canvas app/system
|
||||
const canvasAppSeeder = new CanvasAppSeed(fork);
|
||||
await canvasAppSeeder.run();
|
||||
|
||||
await fork.flush();
|
||||
console.log('Database seeding completed!');
|
||||
} catch (error) {
|
||||
|
@ -1,8 +1,11 @@
|
||||
import { FastifyRequest, FastifyReply } from 'fastify';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { UserRoleType } from '@/constants/roles';
|
||||
import { UserRoleType } from '../constants/roles';
|
||||
import { User } from '../apps/_app/entities/user/_User';
|
||||
import { EntityManager } from '@mikro-orm/core';
|
||||
|
||||
interface UserPayload extends jwt.JwtPayload {
|
||||
id: number;
|
||||
roles: UserRoleType;
|
||||
}
|
||||
|
||||
@ -22,7 +25,15 @@ const auth = (requiredRoles: UserRoleType[]) => async (request: FastifyRequest,
|
||||
return reply.code(403).send({ error: 'Unauthorized: Insufficient role' });
|
||||
}
|
||||
|
||||
request.user = decoded;
|
||||
// Fetch the full user object from the database
|
||||
const em = request.server.orm.em as EntityManager;
|
||||
const user = await em.findOne(User, { id: decoded.id });
|
||||
|
||||
if (!user) {
|
||||
return reply.code(401).send({ error: 'User not found' });
|
||||
}
|
||||
|
||||
request.user = user;
|
||||
} catch (error) {
|
||||
const typedError = error as Error;
|
||||
return reply.code(401).send({ error: 'Authentication failed', info: typedError.message });
|
||||
|
Loading…
Reference in New Issue
Block a user