first push frontend
3
.gitignore
vendored
@ -3,8 +3,7 @@ node_modules/
|
|||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
yarn-debug.log*
|
yarn-debug.log*
|
||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
package-lock.json
|
|
||||||
yarn.lock
|
|
||||||
|
|
||||||
# Build output
|
# Build output
|
||||||
dist/
|
dist/
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
services:
|
services:
|
||||||
fusero-boilerplate-db:
|
fusero-boilerplate-db:
|
||||||
image: postgres:15
|
image: postgres:15
|
||||||
|
@ -1,19 +1,39 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
services:
|
services:
|
||||||
fusero-frontend:
|
frontend:
|
||||||
container_name: fusero-frontend
|
container_name: fusero-frontend
|
||||||
env_file: ../fusero-frontend/.env
|
|
||||||
build:
|
build:
|
||||||
context: ../fusero-frontend
|
context: ./frontend
|
||||||
dockerfile: Dockerfile.dev
|
dockerfile: Dockerfile
|
||||||
ports:
|
ports:
|
||||||
- '3000:80'
|
- '3000:80'
|
||||||
networks:
|
networks:
|
||||||
- fusero-network
|
- fusero-network
|
||||||
|
depends_on:
|
||||||
|
- fusero-app-boilerplate
|
||||||
|
|
||||||
|
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-boilerplate:
|
fusero-app-boilerplate:
|
||||||
environment:
|
environment:
|
||||||
- POSTGRES_HOST=fusero-app-db
|
- POSTGRES_HOST=fusero-boilerplate-db
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
@ -22,68 +42,41 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- '5000:14000'
|
- '5000:14000'
|
||||||
depends_on:
|
depends_on:
|
||||||
- fusero-app-db
|
- fusero-boilerplate-db
|
||||||
container_name: fusero-app-boilerplate
|
container_name: fusero-app-boilerplate
|
||||||
networks:
|
networks:
|
||||||
- fusero-network
|
- fusero-network
|
||||||
|
|
||||||
fusero-app-db:
|
fusero-boilerplate-db:
|
||||||
image: postgres:15
|
image: postgres:15
|
||||||
env_file: .env
|
env_file: .env
|
||||||
restart: always
|
restart: always
|
||||||
volumes:
|
volumes:
|
||||||
- fusero_app_pgdata:/var/lib/postgresql/data
|
- fusero_boilerplate_pgdata:/var/lib/postgresql/data
|
||||||
ports:
|
ports:
|
||||||
- '19090:5432'
|
- '19095:5432'
|
||||||
container_name: fusero-app-db
|
container_name: fusero-boilerplate-db
|
||||||
networks:
|
networks:
|
||||||
- fusero-network
|
- fusero-network
|
||||||
|
|
||||||
fusero-app-test-db:
|
fusero-boilerplate-test-db:
|
||||||
image: postgres:15
|
image: postgres:15
|
||||||
env_file: .env
|
env_file: .env
|
||||||
restart: always
|
restart: always
|
||||||
volumes:
|
volumes:
|
||||||
- fusero_app_test_pgdata:/var/lib/postgresql/data
|
- fusero_boilerplate_test_pgdata:/var/lib/postgresql/data
|
||||||
ports:
|
ports:
|
||||||
- '19091:5432'
|
- '19096:5432'
|
||||||
container_name: fusero-app-test-db
|
container_name: fusero-boilerplate-test-db
|
||||||
networks:
|
networks:
|
||||||
- fusero-network
|
- fusero-network
|
||||||
environment:
|
environment:
|
||||||
- POSTGRES_DB=test-db
|
- POSTGRES_DB=test-db
|
||||||
|
|
||||||
# ngrok:
|
|
||||||
# image: ngrok/ngrok:latest
|
|
||||||
# restart: unless-stopped
|
|
||||||
# command:
|
|
||||||
# - 'start'
|
|
||||||
# - '--all'
|
|
||||||
# - '--config'
|
|
||||||
# - '/etc/ngrok.yml'
|
|
||||||
# volumes:
|
|
||||||
# - ./ngrok.yml:/etc/ngrok.yml
|
|
||||||
# ports:
|
|
||||||
# - 19095:4040
|
|
||||||
# networks:
|
|
||||||
# - fusero-network
|
|
||||||
|
|
||||||
# fusero-redis:
|
|
||||||
# image: redis:7-alpine
|
|
||||||
# restart: always
|
|
||||||
# ports:
|
|
||||||
# - '6379:6379'
|
|
||||||
# volumes:
|
|
||||||
# - redis_data:/data
|
|
||||||
# container_name: fusero-redis
|
|
||||||
# networks:
|
|
||||||
# - fusero-network
|
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
redis_data:
|
fusero_boilerplate_pgdata:
|
||||||
fusero_app_pgdata:
|
|
||||||
external: true
|
external: true
|
||||||
fusero_app_test_pgdata:
|
fusero_boilerplate_test_pgdata:
|
||||||
external: false
|
external: false
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
|
10
frontend/.prettierrc.json
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"semi": true,
|
||||||
|
"trailingComma": "es5",
|
||||||
|
"singleQuote": true,
|
||||||
|
"jsxSingleQuote": true,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"useTabs": false,
|
||||||
|
"printWidth": 100,
|
||||||
|
"bracketSpacing": true
|
||||||
|
}
|
31
frontend/Dockerfile
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
# Build stage
|
||||||
|
FROM node:18-alpine as build
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy package files
|
||||||
|
COPY package*.json ./
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
RUN npm ci
|
||||||
|
|
||||||
|
# Copy source code
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Build the app
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# Production stage
|
||||||
|
FROM nginx:alpine
|
||||||
|
|
||||||
|
# Copy built assets from build stage
|
||||||
|
COPY --from=build /app/dist /usr/share/nginx/html
|
||||||
|
|
||||||
|
# Copy nginx configuration
|
||||||
|
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
|
|
||||||
|
# Expose port 80
|
||||||
|
EXPOSE 80
|
||||||
|
|
||||||
|
# Start nginx
|
||||||
|
CMD ["nginx", "-g", "daemon off;"]
|
18
frontend/Dockerfile.dev
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
FROM node:18-alpine
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy package files
|
||||||
|
COPY package*.json ./
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
RUN npm install
|
||||||
|
|
||||||
|
# Copy source code
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Expose port 3000
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
# Start development server
|
||||||
|
CMD ["npm", "run", "dev"]
|
19
frontend/index.html
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<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">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Fusero</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
20
frontend/nginx.conf
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name localhost;
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
10206
frontend/package-lock.json
generated
Normal file
44
frontend/package.json
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
{
|
||||||
|
"name": "fusero-frontend",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "tsc && vite build",
|
||||||
|
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@types/jwt-decode": "^3.1.0",
|
||||||
|
"axios": "^1.6.8",
|
||||||
|
"jwt-decode": "^4.0.0",
|
||||||
|
"primeicons": "^7.0.0",
|
||||||
|
"primereact": "^10.6.3",
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-cropper": "^2.3.3",
|
||||||
|
"react-dom": "^18.2.0",
|
||||||
|
"react-hook-form": "^7.51.3",
|
||||||
|
"react-markdown": "^10.1.0",
|
||||||
|
"react-router-dom": "^6.22.3",
|
||||||
|
"remark-gfm": "^4.0.1",
|
||||||
|
"zod": "^3.23.4",
|
||||||
|
"zustand": "^4.5.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/react": "^18.2.66",
|
||||||
|
"@types/react-dom": "^18.2.22",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^7.2.0",
|
||||||
|
"@typescript-eslint/parser": "^7.2.0",
|
||||||
|
"@vitejs/plugin-react": "^4.2.1",
|
||||||
|
"@vitejs/plugin-react-swc": "^3.5.0",
|
||||||
|
"autoprefixer": "^10.4.19",
|
||||||
|
"eslint": "^8.57.0",
|
||||||
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
|
"eslint-plugin-react-refresh": "^0.4.6",
|
||||||
|
"postcss": "^8.4.38",
|
||||||
|
"tailwindcss": "^3.4.3",
|
||||||
|
"typescript": "^5.2.2",
|
||||||
|
"vite": "^5.2.0"
|
||||||
|
}
|
||||||
|
}
|
6
frontend/postcss.config.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
BIN
frontend/public/favicon/android-chrome-192x192.png
Normal file
After Width: | Height: | Size: 26 KiB |
BIN
frontend/public/favicon/android-chrome-512x512.png
Normal file
After Width: | Height: | Size: 80 KiB |
BIN
frontend/public/favicon/apple-touch-icon.png
Normal file
After Width: | Height: | Size: 24 KiB |
BIN
frontend/public/favicon/favicon-16x16.png
Normal file
After Width: | Height: | Size: 759 B |
BIN
frontend/public/favicon/favicon-32x32.png
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
frontend/public/favicon/favicon.ico
Normal file
After Width: | Height: | Size: 15 KiB |
1
frontend/public/favicon/site.webmanifest
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
|
BIN
frontend/public/fusero_logo_alphapng.png
Normal file
After Width: | Height: | Size: 72 KiB |
1
frontend/public/vite.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
After Width: | Height: | Size: 1.5 KiB |
BIN
frontend/src/assets/female-avatar.png
Normal file
After Width: | Height: | Size: 266 KiB |
38
frontend/src/assets/fusero_logo.svg
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 160 160">
|
||||||
|
<!-- Generator: Adobe Illustrator 29.2.1, SVG Export Plug-In . SVG Version: 2.1.0 Build 116) -->
|
||||||
|
<defs>
|
||||||
|
<style>
|
||||||
|
.st0 {
|
||||||
|
fill: #099;
|
||||||
|
}
|
||||||
|
|
||||||
|
.st1 {
|
||||||
|
fill: none;
|
||||||
|
stroke: #000;
|
||||||
|
stroke-miterlimit: 10;
|
||||||
|
stroke-width: 10px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</defs>
|
||||||
|
<polygon points="160 124.58 0 124.58 0 80.36 6.06 80.36 6.06 118.53 153.94 118.53 153.94 80.36 160 80.36 160 124.58"/>
|
||||||
|
<polygon class="st0" points="6.06 71.02 0 71.02 0 32.72 79.08 32.72 79.08 38.78 6.06 38.78 6.06 71.02"/>
|
||||||
|
<polygon points="159.77 71.42 153.71 71.42 153.71 38.78 78.85 38.78 78.85 32.72 159.77 32.72 159.77 71.42"/>
|
||||||
|
<path class="st1" d="M14.28,105.16"/>
|
||||||
|
<g>
|
||||||
|
<rect x="13.34" y="107.13" width="66.8" height="5.05"/>
|
||||||
|
<rect class="st0" x="80.15" y="107.13" width="66.51" height="5.05"/>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<rect class="st0" x="29.75" y="99.34" width="116.9" height="5.05"/>
|
||||||
|
<rect x="13.34" y="99.34" width="16.41" height="5.05"/>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<path d="M13.32,48.95h17.63v6.06h-10.96v11.81h8.6v6.06h-8.6v18.47h-6.66v-42.4Z"/>
|
||||||
|
<path d="M36.45,89.2c-1.7-1.84-2.54-4.47-2.54-7.9v-32.34h6.66v32.83c0,1.45.29,2.5.88,3.15.58.65,1.42.97,2.51.97s1.93-.32,2.51-.97c.58-.65.88-1.7.88-3.15v-32.83h6.42v32.34c0,3.43-.85,6.07-2.54,7.9-1.7,1.84-4.16,2.76-7.39,2.76s-5.69-.92-7.39-2.76Z"/>
|
||||||
|
<path d="M59.83,89.2c-1.66-1.84-2.48-4.47-2.48-7.9v-2.42h6.3v2.91c0,2.75,1.15,4.12,3.45,4.12,1.13,0,1.99-.33,2.57-1,.58-.67.88-1.75.88-3.24,0-1.78-.4-3.34-1.21-4.69-.81-1.35-2.3-2.98-4.48-4.88-2.75-2.42-4.66-4.61-5.75-6.57-1.09-1.96-1.64-4.17-1.64-6.63,0-3.35.85-5.95,2.54-7.78,1.7-1.84,4.16-2.76,7.39-2.76s5.6.92,7.24,2.76c1.64,1.84,2.45,4.47,2.45,7.9v1.76h-6.3v-2.18c0-1.45-.28-2.51-.85-3.18-.57-.67-1.39-1-2.48-1-2.22,0-3.33,1.35-3.33,4.06,0,1.54.41,2.97,1.24,4.3.83,1.33,2.33,2.95,4.51,4.84,2.79,2.42,4.7,4.62,5.75,6.6,1.05,1.98,1.57,4.3,1.57,6.96,0,3.47-.86,6.14-2.57,8-1.72,1.86-4.21,2.79-7.48,2.79s-5.67-.92-7.33-2.76Z"/>
|
||||||
|
<path d="M81.03,48.95h18.17v6.06h-11.51v11.21h9.15v6.06h-9.15v13.02h11.51v6.06h-18.17v-42.4Z"/>
|
||||||
|
<path d="M103.02,48.95h9.87c3.43,0,5.94.8,7.51,2.39,1.58,1.6,2.36,4.05,2.36,7.36v2.6c0,4.4-1.45,7.19-4.36,8.36v.12c1.62.48,2.76,1.47,3.42,2.97.67,1.49,1,3.49,1,6v7.45c0,1.21.04,2.19.12,2.94.08.75.28,1.48.61,2.21h-6.78c-.24-.69-.4-1.33-.48-1.94-.08-.61-.12-1.7-.12-3.27v-7.75c0-1.94-.31-3.29-.94-4.06-.63-.77-1.71-1.15-3.24-1.15h-2.3v18.17h-6.66v-42.4ZM112.1,67.12c1.33,0,2.33-.34,3-1.03.67-.69,1-1.84,1-3.45v-3.27c0-1.53-.27-2.64-.82-3.33-.54-.69-1.4-1.03-2.57-1.03h-3.03v12.11h2.42Z"/>
|
||||||
|
<path d="M129.54,89.17c-1.74-1.86-2.6-4.48-2.6-7.87v-22.29c0-3.39.87-6.02,2.6-7.87,1.74-1.86,4.24-2.79,7.51-2.79s5.77.93,7.51,2.79c1.74,1.86,2.6,4.48,2.6,7.87v22.29c0,3.39-.87,6.02-2.6,7.87-1.74,1.86-4.24,2.79-7.51,2.79s-5.77-.93-7.51-2.79ZM140.51,81.72v-23.14c0-2.79-1.15-4.18-3.45-4.18s-3.45,1.39-3.45,4.18v23.14c0,2.79,1.15,4.18,3.45,4.18s3.45-1.39,3.45-4.18Z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 3.0 KiB |
BIN
frontend/src/assets/fusero_logo.webp
Normal file
After Width: | Height: | Size: 74 KiB |
BIN
frontend/src/assets/male-avatar.png
Normal file
After Width: | Height: | Size: 482 KiB |
1
frontend/src/assets/react.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
|
After Width: | Height: | Size: 4.0 KiB |
BIN
frontend/src/assets/teal_iso_bg.png
Normal file
After Width: | Height: | Size: 1.0 MiB |
215
frontend/src/components/ActivateConnectionModal.tsx
Normal file
@ -0,0 +1,215 @@
|
|||||||
|
// import React, { useEffect, useState, useRef } from 'react';
|
||||||
|
// import { Button } from 'primereact/button';
|
||||||
|
// import { Toast } from 'primereact/toast';
|
||||||
|
// import { api } from '../services/axios';
|
||||||
|
// import { Dialog } from 'primereact/dialog';
|
||||||
|
// import { useNavigate } from 'react-router-dom';
|
||||||
|
// import { useSettingsStore } from '../state/stores/useSettingsStore';
|
||||||
|
|
||||||
|
// type UserInfo = {
|
||||||
|
// role: string;
|
||||||
|
// tenantId: string | number;
|
||||||
|
// };
|
||||||
|
|
||||||
|
// const ActivateConnectionModal = () => {
|
||||||
|
// const [loading, setLoading] = useState(false);
|
||||||
|
// const [showDialog, setShowDialog] = useState(false);
|
||||||
|
// const toast = useRef<Toast>(null);
|
||||||
|
// const [fetched, setFetched] = useState(false);
|
||||||
|
// const [systemName, setSystemName] = useState('');
|
||||||
|
// const tenantId = localStorage.getItem('tenantId');
|
||||||
|
// const systemId = localStorage.getItem('systemId');
|
||||||
|
|
||||||
|
// const navigate = useNavigate();
|
||||||
|
|
||||||
|
// const [userInfo, setUserInfo] = useState<UserInfo>({
|
||||||
|
// role: '',
|
||||||
|
// tenantId: -1,
|
||||||
|
// });
|
||||||
|
|
||||||
|
// const [messageShown, setMessageShown] = useState<boolean>(false);
|
||||||
|
|
||||||
|
// const { setConnectionId } = useSettingsStore();
|
||||||
|
|
||||||
|
// useEffect(() => {
|
||||||
|
// // TODO: similar logic used in other components, refactor into user context/state/hook
|
||||||
|
// const storedUser = localStorage.getItem('user');
|
||||||
|
// const storedSystemName = localStorage.getItem('systemName');
|
||||||
|
|
||||||
|
// if (storedUser) {
|
||||||
|
// const parsedUser = JSON.parse(storedUser);
|
||||||
|
|
||||||
|
// let role = parsedUser.roles?.includes('admin')
|
||||||
|
// ? 'admin'
|
||||||
|
// : parsedUser.roles?.includes('user')
|
||||||
|
// ? 'user'
|
||||||
|
// : '';
|
||||||
|
|
||||||
|
// let tenantId =
|
||||||
|
// role === 'admin' ? localStorage.getItem('tenantId') || -1 : -1;
|
||||||
|
|
||||||
|
// let userInfo: UserInfo = {
|
||||||
|
// role: role,
|
||||||
|
// tenantId: tenantId,
|
||||||
|
// };
|
||||||
|
|
||||||
|
// setUserInfo(userInfo);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if (storedSystemName) {
|
||||||
|
// setSystemName(storedSystemName);
|
||||||
|
// }
|
||||||
|
// }, []);
|
||||||
|
|
||||||
|
// useEffect(() => {
|
||||||
|
// if (userInfo.role && userInfo.tenantId !== undefined && !fetched) {
|
||||||
|
// fetchConnection();
|
||||||
|
// }
|
||||||
|
// }, [userInfo]);
|
||||||
|
|
||||||
|
// const fetchConnection = async () => {
|
||||||
|
// setLoading(true);
|
||||||
|
// try {
|
||||||
|
// let response: any;
|
||||||
|
|
||||||
|
// if (userInfo.role === 'user') {
|
||||||
|
// response = await api('get', `/connections?systemName=${systemName}`);
|
||||||
|
// } else if (userInfo.role === 'admin' && userInfo.tenantId) {
|
||||||
|
// response = await api(
|
||||||
|
// 'get',
|
||||||
|
// `/connections?systemName=${systemName}&tenantId=${userInfo.tenantId}`,
|
||||||
|
// );
|
||||||
|
// } else {
|
||||||
|
// throw new Error('Invalid user role or missing tenantId');
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const connections = response.data.data;
|
||||||
|
|
||||||
|
// // Localstorage saves undefined as string
|
||||||
|
// if (tenantId === 'undefined' || systemId === 'undefined') {
|
||||||
|
// navigate('/systems');
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if (!connections || connections.length === 0) {
|
||||||
|
// setShowDialog(true);
|
||||||
|
// } else if (userInfo.role === 'user') {
|
||||||
|
// setUserInfo((prevUserInfo) => ({
|
||||||
|
// ...prevUserInfo,
|
||||||
|
// tenantId: connections[0].tenant_fleks_id,
|
||||||
|
// }));
|
||||||
|
// }
|
||||||
|
// const connectionId = connections[0].id;
|
||||||
|
// localStorage.setItem('connectionId', connectionId);
|
||||||
|
// setConnectionId(connectionId);
|
||||||
|
// navigate(`/dashboard/${systemName}`);
|
||||||
|
// setLoading(false);
|
||||||
|
// setFetched(true);
|
||||||
|
// } catch (error) {
|
||||||
|
// console.error('Error:', error);
|
||||||
|
// setLoading(false);
|
||||||
|
// if (!messageShown) {
|
||||||
|
// setMessageShown(true);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
|
||||||
|
// const handleActivateClick = async () => {
|
||||||
|
// setLoading(true);
|
||||||
|
// const systemId = localStorage.getItem('systemId') || null;
|
||||||
|
|
||||||
|
// try {
|
||||||
|
// let postResponse: any;
|
||||||
|
// let requestBody: any = {
|
||||||
|
// system_id: Number(systemId),
|
||||||
|
// };
|
||||||
|
|
||||||
|
// if (!systemId) {
|
||||||
|
// throw new Error('Missing systemId');
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if (userInfo.role === 'admin' && userInfo.tenantId) {
|
||||||
|
// requestBody.tenant_id = userInfo.tenantId;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// postResponse = await api('post', '/connections', requestBody);
|
||||||
|
|
||||||
|
// if (postResponse.status === 201) {
|
||||||
|
// const connectionId = postResponse.data.id;
|
||||||
|
// localStorage.setItem('connectionId', connectionId);
|
||||||
|
// setConnectionId(connectionId);
|
||||||
|
|
||||||
|
// navigate(`/dashboard/${systemName}`);
|
||||||
|
|
||||||
|
// setLoading(false);
|
||||||
|
// setShowDialog(false);
|
||||||
|
// toast.current.show({
|
||||||
|
// severity: 'success',
|
||||||
|
// summary: 'Success',
|
||||||
|
// detail: 'Connection established successfully.',
|
||||||
|
// life: 3000,
|
||||||
|
// });
|
||||||
|
// } else {
|
||||||
|
// setLoading(false);
|
||||||
|
// toast.current.show({
|
||||||
|
// severity: 'error',
|
||||||
|
// summary: 'Error',
|
||||||
|
// detail: 'Failed to establish connection.',
|
||||||
|
// life: 3000,
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// } catch (error) {
|
||||||
|
// if (error.message === 'Missing systemId') {
|
||||||
|
// toast.current.show({
|
||||||
|
// severity: 'error',
|
||||||
|
// summary: 'Error',
|
||||||
|
// detail: 'Missing system information. Please verify.',
|
||||||
|
// life: 3000,
|
||||||
|
// });
|
||||||
|
// } else {
|
||||||
|
// console.error('Error:', error);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
|
||||||
|
// const handleCancelClick = () => {
|
||||||
|
// setShowDialog(false);
|
||||||
|
// navigate('/systems');
|
||||||
|
// };
|
||||||
|
|
||||||
|
// return (
|
||||||
|
// <div className="flex items-center justify-center h-screen overflow-y-hidden bg-gradient-to-br from-purple-500 to-indigo-500">
|
||||||
|
// <div className="fixed inset-0 flex items-center justify-center overflow-y-hidden">
|
||||||
|
// <div className="transition-transform" style={{ transform: 'none' }}>
|
||||||
|
// <Dialog
|
||||||
|
// visible={showDialog}
|
||||||
|
// onHide={() => setShowDialog(false)}
|
||||||
|
// header="Activate Connection"
|
||||||
|
// footer={
|
||||||
|
// <div>
|
||||||
|
// <Button
|
||||||
|
// label="Activate"
|
||||||
|
// className="p-button-primary"
|
||||||
|
// onClick={handleActivateClick}
|
||||||
|
// disabled={loading}
|
||||||
|
// />
|
||||||
|
// <Button
|
||||||
|
// label="Cancel"
|
||||||
|
// className="p-button-secondary"
|
||||||
|
// onClick={handleCancelClick}
|
||||||
|
// disabled={loading}
|
||||||
|
// />
|
||||||
|
// </div>
|
||||||
|
// }
|
||||||
|
// >
|
||||||
|
// {loading ? (
|
||||||
|
// <p>Performing the post request, please wait...</p>
|
||||||
|
// ) : (
|
||||||
|
// <p>Are you sure you want to activate the connection?</p>
|
||||||
|
// )}
|
||||||
|
// </Dialog>
|
||||||
|
// </div>
|
||||||
|
// </div>
|
||||||
|
// </div>
|
||||||
|
// );
|
||||||
|
// };
|
||||||
|
// export default ActivateConnectionModal;
|
23
frontend/src/components/ApiCalls.tsx
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { DataTable } from 'primereact/datatable';
|
||||||
|
import { Column } from 'primereact/column';
|
||||||
|
import { useApiLogStore } from '../services/apiLogStore';
|
||||||
|
|
||||||
|
const ApiCalls = () => {
|
||||||
|
const apiLogs = useApiLogStore((state) => state.apiLogs);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='p-4'>
|
||||||
|
<h2 className='text-lg font-semibold'>API Calls</h2>
|
||||||
|
<DataTable value={apiLogs}>
|
||||||
|
{/* <Column field='timestamp' header='Timestamp' sortable /> */}
|
||||||
|
<Column field='method' header='Method' />
|
||||||
|
<Column field='url' header='URL' />
|
||||||
|
<Column field='status' header='Status' />
|
||||||
|
<Column field='responseTime' header='Time (ms)' />
|
||||||
|
</DataTable>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ApiCalls;
|
40
frontend/src/components/AuthWrapper.tsx
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import { useLocation, useNavigate } from 'react-router-dom';
|
||||||
|
import { getExpirationDateFromToken, useAuth } from '../hooks/useAuth';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
|
const AuthWrapper = ({ children }) => {
|
||||||
|
const location = useLocation();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { logout } = useAuth();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const user = localStorage.getItem('user');
|
||||||
|
const isLoginPage = location.pathname === '/login';
|
||||||
|
|
||||||
|
if (!user && !isLoginPage) {
|
||||||
|
navigate('/', { replace: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
checkUserToken();
|
||||||
|
}, [location.pathname, navigate]);
|
||||||
|
|
||||||
|
const checkUserToken = () => {
|
||||||
|
const user = localStorage.getItem('user');
|
||||||
|
if (!user) return navigate('/');
|
||||||
|
|
||||||
|
const parsedUser = JSON.parse(user);
|
||||||
|
if (parsedUser) {
|
||||||
|
let expiredDate = getExpirationDateFromToken(parsedUser.token);
|
||||||
|
const currentDate = new Date();
|
||||||
|
|
||||||
|
if (expiredDate < currentDate) {
|
||||||
|
logout();
|
||||||
|
navigate('/');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return <>{children}</>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AuthWrapper;
|
32
frontend/src/components/Avatar.tsx
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
interface AvatarProps {
|
||||||
|
avatarUrl: string;
|
||||||
|
name: string;
|
||||||
|
type: 'CFO' | 'CPO' | 'CTO'; // Specific types
|
||||||
|
speech?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Avatar: React.FC<AvatarProps> = ({ avatarUrl, name, type, speech }) => (
|
||||||
|
<div className='flex flex-col items-center space-y-2'>
|
||||||
|
{/* Avatar Image */}
|
||||||
|
<div className='w-32 h-32'>
|
||||||
|
<img
|
||||||
|
src={avatarUrl}
|
||||||
|
alt={`${name}'s Avatar`}
|
||||||
|
className='w-full h-full border-2 border-gray-300 rounded-full'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/* Name and Type */}
|
||||||
|
<span className='text-sm font-semibold'>{name}</span>
|
||||||
|
<span className='text-xs font-medium text-gray-600'>{type}</span>
|
||||||
|
{/* Speech Bubble */}
|
||||||
|
{speech && (
|
||||||
|
<div className='px-3 py-2 text-xs text-gray-800 bg-gray-200 border rounded shadow'>
|
||||||
|
{speech}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default Avatar;
|
110
frontend/src/components/Chat.tsx
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
// import React, { useState, useEffect, useRef } from 'react';
|
||||||
|
// import { InputText } from 'primereact/inputtext';
|
||||||
|
// import { Button } from 'primereact/button';
|
||||||
|
// import { Toast } from 'primereact/toast';
|
||||||
|
// import { Dialog } from 'primereact/dialog';
|
||||||
|
// import { Card } from 'primereact/card';
|
||||||
|
// import io, { Socket } from 'socket.io-client';
|
||||||
|
|
||||||
|
// interface ChatProps {
|
||||||
|
// serverUrl: string;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const Chat: React.FC<ChatProps> = ({ serverUrl }) => {
|
||||||
|
// const [messages, setMessages] = useState<string[]>([]);
|
||||||
|
// const [inputText, setInputText] = useState('');
|
||||||
|
// const [showChat, setShowChat] = useState(false);
|
||||||
|
// const socketRef = useRef<Socket | null>(null);
|
||||||
|
// const toastRef = useRef<Toast | null>(null);
|
||||||
|
|
||||||
|
// useEffect(() => {
|
||||||
|
// socketRef.current = io(serverUrl);
|
||||||
|
// socketRef.current.emit('join', 'global');
|
||||||
|
|
||||||
|
// socketRef.current.on('connect', () => {
|
||||||
|
// showToast('success', 'Connected to the chat server');
|
||||||
|
// });
|
||||||
|
|
||||||
|
// socketRef.current.on('disconnect', () => {
|
||||||
|
// showToast('warn', 'Disconnected from the chat server');
|
||||||
|
// });
|
||||||
|
|
||||||
|
// socketRef.current.on('message', (message: string) => {
|
||||||
|
// setMessages((prevMessages) => [...prevMessages, message]);
|
||||||
|
// });
|
||||||
|
|
||||||
|
// return () => {
|
||||||
|
// if (socketRef.current) {
|
||||||
|
// socketRef.current.disconnect();
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
// }, [serverUrl]);
|
||||||
|
|
||||||
|
// const handleSendMessage = () => {
|
||||||
|
// if (inputText.trim() === '') {
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if (socketRef.current) {
|
||||||
|
// socketRef.current.emit('message', inputText);
|
||||||
|
// }
|
||||||
|
// setInputText('');
|
||||||
|
// };
|
||||||
|
|
||||||
|
// const toggleChat = () => {
|
||||||
|
// setShowChat((prevShowChat) => !prevShowChat);
|
||||||
|
// };
|
||||||
|
|
||||||
|
// const showToast = (severity: any, detail: string) => {
|
||||||
|
// if (toastRef.current) {
|
||||||
|
// toastRef.current.show({
|
||||||
|
// severity,
|
||||||
|
// summary: severity === 'success' ? 'Success' : 'Warning',
|
||||||
|
// detail,
|
||||||
|
// life: 3000,
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
|
||||||
|
// const handleInputKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
|
// if (event.key === 'Enter') {
|
||||||
|
// handleSendMessage();
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
|
||||||
|
// return (
|
||||||
|
// <>
|
||||||
|
// <div className="chat-icon" onClick={toggleChat}>
|
||||||
|
// <h3>Click to chat</h3>
|
||||||
|
// </div>
|
||||||
|
// <Dialog
|
||||||
|
// visible={showChat}
|
||||||
|
// onHide={toggleChat}
|
||||||
|
// className="chat-dialog"
|
||||||
|
// modal
|
||||||
|
// >
|
||||||
|
// <Card title="Chat" className="chat-card">
|
||||||
|
// <div className="chat-container">
|
||||||
|
// {messages.map((message, index) => (
|
||||||
|
// <div key={index} className="message">
|
||||||
|
// <span className="message-content">{message}</span>
|
||||||
|
// </div>
|
||||||
|
// ))}
|
||||||
|
// </div>
|
||||||
|
// <div className="input-container">
|
||||||
|
// <InputText
|
||||||
|
// value={inputText}
|
||||||
|
// onChange={(e) => setInputText(e.target.value)}
|
||||||
|
// onKeyDown={handleInputKeyDown}
|
||||||
|
// placeholder="Type a message..."
|
||||||
|
// />
|
||||||
|
// <Button label="Send" onClick={handleSendMessage} />
|
||||||
|
// </div>
|
||||||
|
// </Card>
|
||||||
|
// </Dialog>
|
||||||
|
// <Toast ref={toastRef} position="bottom-left" className="bottom-0" />{' '}
|
||||||
|
// </>
|
||||||
|
// );
|
||||||
|
// };
|
||||||
|
|
||||||
|
// export default Chat;
|
83
frontend/src/components/ChatGPTModal.tsx
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
import { DataTable } from 'primereact/datatable';
|
||||||
|
import { Column } from 'primereact/column';
|
||||||
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
|
import { useChatStore } from '../state/stores/useChatStore';
|
||||||
|
|
||||||
|
interface ChatGPTModalProps {
|
||||||
|
content?: {
|
||||||
|
prompt: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TableData {
|
||||||
|
columns: Array<{
|
||||||
|
field: string;
|
||||||
|
header: string;
|
||||||
|
}>;
|
||||||
|
data: any[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ChatGPTModal = ({ content }: ChatGPTModalProps) => {
|
||||||
|
const [tableData, setTableData] = useState<TableData>({ columns: [], data: [] });
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const { sendChatRequest } = useChatStore();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.log('useEffect triggered with content:', content);
|
||||||
|
if (content?.prompt) {
|
||||||
|
generateTable();
|
||||||
|
}
|
||||||
|
}, [content]);
|
||||||
|
|
||||||
|
const generateTable = async () => {
|
||||||
|
if (!content?.prompt) return;
|
||||||
|
console.log('Generating table for prompt:', content.prompt);
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const response = await sendChatRequest('/chat/completions', {
|
||||||
|
data: `Create a table with data about: ${content.prompt}
|
||||||
|
Return ONLY a JSON object in this format:
|
||||||
|
{
|
||||||
|
"columns": [
|
||||||
|
{"field": "column1", "header": "Column 1"},
|
||||||
|
{"field": "column2", "header": "Column 2"}
|
||||||
|
],
|
||||||
|
"data": [
|
||||||
|
{"column1": "value1", "column2": "value2"},
|
||||||
|
{"column1": "value3", "column2": "value4"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
1. Return ONLY the JSON object, no explanations
|
||||||
|
2. Make sure field names in data match column fields exactly
|
||||||
|
3. Choose appropriate column names based on the topic
|
||||||
|
4. Include at least 5 rows of data`,
|
||||||
|
responseFormat: 'json'
|
||||||
|
});
|
||||||
|
console.log('Got raw response:', response);
|
||||||
|
|
||||||
|
if (response?.responseText) {
|
||||||
|
console.log('Got responseText:', response.responseText);
|
||||||
|
setTableData(JSON.parse(response.responseText));
|
||||||
|
} else {
|
||||||
|
console.error('Invalid response format:', response);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error generating table:', error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-column gap-3 p-4">
|
||||||
|
<DataTable value={tableData.data} loading={loading} className="w-full">
|
||||||
|
{tableData.columns.map(col => (
|
||||||
|
<Column key={col.field} field={col.field} header={col.header} />
|
||||||
|
))}
|
||||||
|
</DataTable>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
103
frontend/src/components/CouncilAI/CouncilAI.tsx
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import Avatar from '../Avatar';
|
||||||
|
import { InputText } from 'primereact/inputtext';
|
||||||
|
import Button from '../../shared/components/_V2/Button';
|
||||||
|
import maleAvatar from '../../assets/male-avatar.png';
|
||||||
|
import femaleAvatar from '../../assets/female-avatar.png';
|
||||||
|
import { useChatStore } from '../../state/stores/useChatStore';
|
||||||
|
|
||||||
|
const PromptBoxModal = ({ response }) => {
|
||||||
|
return response ? (
|
||||||
|
<div className='absolute w-full p-2 mb-2 text-center text-white bg-gray-700 rounded bottom-full'>
|
||||||
|
<strong>Assistant:</strong> {response}
|
||||||
|
</div>
|
||||||
|
) : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const PromptBox = ({ onSubmit, activeRole }) => {
|
||||||
|
const [input, setInput] = useState('');
|
||||||
|
|
||||||
|
const handleSubmit = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (input.trim()) {
|
||||||
|
onSubmit(input);
|
||||||
|
setInput('');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
className='flex items-center w-full gap-4 p-4 bg-gray-500 rounded-lg shadow-md'
|
||||||
|
>
|
||||||
|
<div className='flex items-center flex-grow px-6 py-3 bg-white border border-gray-400 rounded-lg focus-within:ring-2 focus-within:ring-gray-500 h-14'>
|
||||||
|
<InputText
|
||||||
|
type='text'
|
||||||
|
value={input}
|
||||||
|
onChange={(e) => setInput(e.target.value)}
|
||||||
|
className='w-full h-full text-black bg-transparent border-none focus:outline-none'
|
||||||
|
placeholder={`Enter your message for ${activeRole}...`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className='flex h-14'>
|
||||||
|
<Button
|
||||||
|
label='Send'
|
||||||
|
type='submit'
|
||||||
|
className='px-6 bg-blue-500 text-white rounded-lg hover:bg-blue-600 flex items-center justify-center h-full !m-0'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function AvatarPromptScreen() {
|
||||||
|
const [responses, setResponses] = useState({ cfo: '', cpo: '' });
|
||||||
|
const [activeRole, setActiveRole] = useState('CFO');
|
||||||
|
const { sendChatRequest } = useChatStore();
|
||||||
|
|
||||||
|
const handlePromptSubmit = async (message) => {
|
||||||
|
console.log(`Prompt submitted for ${activeRole}:`, message);
|
||||||
|
const requestData = { keywords: ['example', 'test'], data: message };
|
||||||
|
try {
|
||||||
|
const result = await sendChatRequest('/chat/completions', requestData);
|
||||||
|
const responseText = result.responseText || 'No response from server';
|
||||||
|
setResponses((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[activeRole.toLowerCase()]: responseText,
|
||||||
|
}));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('API request failed:', error);
|
||||||
|
setResponses((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[activeRole.toLowerCase()]: 'Error communicating with server',
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='flex flex-col justify-between w-screen h-screen bg-primary-light'>
|
||||||
|
<div className='grid items-end flex-grow w-full grid-cols-6 pb-4'>
|
||||||
|
{/* CFO (Column 2) */}
|
||||||
|
<div className='relative flex flex-col items-center col-span-1 col-start-2'>
|
||||||
|
<PromptBoxModal response={responses.cfo} />
|
||||||
|
<div className='cursor-pointer' onClick={() => setActiveRole('CFO')}>
|
||||||
|
<Avatar avatarUrl={maleAvatar} name='CFO' type='CFO' />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* CPO (Column 5) */}
|
||||||
|
<div className='relative flex flex-col items-center col-span-1 col-start-5'>
|
||||||
|
<PromptBoxModal response={responses.cpo} />
|
||||||
|
<div className='cursor-pointer' onClick={() => setActiveRole('CPO')}>
|
||||||
|
<Avatar avatarUrl={femaleAvatar} name='CPO' type='CPO' />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Full Width Input Box */}
|
||||||
|
<div className='w-full p-4'>
|
||||||
|
<PromptBox onSubmit={handlePromptSubmit} activeRole={activeRole} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
84
frontend/src/components/CouncilAI/PromptBoxModal.tsx
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { Dialog } from 'primereact/dialog';
|
||||||
|
import { InputText } from 'primereact/inputtext';
|
||||||
|
import Button from '../../shared/components/_V2/Button';
|
||||||
|
import { useChatStore } from '../../state/stores/useChatStore';
|
||||||
|
|
||||||
|
const PromptBoxModal = ({ onSubmit }) => {
|
||||||
|
const [promptMessage, setPromptMessage] = useState('');
|
||||||
|
const [latestMessage, setLatestMessage] = useState({ user: '', response: '' });
|
||||||
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
|
const { sendChatRequest } = useChatStore();
|
||||||
|
|
||||||
|
const handleFormSubmit = async (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
if (promptMessage.trim()) {
|
||||||
|
const requestData = {
|
||||||
|
keywords: ['example', 'test'],
|
||||||
|
data: promptMessage,
|
||||||
|
};
|
||||||
|
|
||||||
|
setLatestMessage({ user: promptMessage, response: '' });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await sendChatRequest('/chat/completions', requestData);
|
||||||
|
const responseText = result.responseText || 'No response from server';
|
||||||
|
setLatestMessage({ user: promptMessage, response: responseText });
|
||||||
|
setIsModalOpen(true);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('API request failed:', error);
|
||||||
|
setLatestMessage({ user: promptMessage, response: 'Error communicating with server' });
|
||||||
|
setIsModalOpen(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
onSubmit(requestData);
|
||||||
|
setPromptMessage('');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='flex items-center w-full h-full p-2 bg-gray-800'>
|
||||||
|
<form onSubmit={handleFormSubmit} className='flex items-center w-full space-x-2'>
|
||||||
|
<InputText
|
||||||
|
type='text'
|
||||||
|
value={promptMessage}
|
||||||
|
onChange={(e) => setPromptMessage(e.target.value)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
handleFormSubmit(e);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className='flex-grow p-2 text-black rounded'
|
||||||
|
placeholder='Enter your message...'
|
||||||
|
/>
|
||||||
|
<Button label='Send' type='submit' className='p-2' />
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{/* Modal for Latest Question & Response */}
|
||||||
|
<Dialog
|
||||||
|
visible={isModalOpen}
|
||||||
|
onHide={() => setIsModalOpen(false)}
|
||||||
|
header='Chat Response'
|
||||||
|
className='w-[400px] p-4 bg-white rounded-lg shadow-lg'
|
||||||
|
modal
|
||||||
|
>
|
||||||
|
<div className='space-y-2'>
|
||||||
|
<div className='p-2 text-white bg-blue-500 rounded-lg'>
|
||||||
|
<strong>You:</strong> {latestMessage.user}
|
||||||
|
</div>
|
||||||
|
<div className='p-2 text-white bg-gray-700 rounded-lg'>
|
||||||
|
<strong>Assistant:</strong> {latestMessage.response}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button label='Close' onClick={() => setIsModalOpen(false)} className='mt-3' />
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
PromptBoxModal.propTypes = {
|
||||||
|
onSubmit: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PromptBoxModal;
|
83
frontend/src/components/FuseMind/Avatar/MoleculeAvatar.tsx
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
const MoleculeAvatar: React.FC = () => {
|
||||||
|
return (
|
||||||
|
<div className="molecule-container">
|
||||||
|
<div className="molecule"></div>
|
||||||
|
<div className="molecule"></div>
|
||||||
|
<div className="molecule"></div>
|
||||||
|
<style >{`
|
||||||
|
.molecule-container {
|
||||||
|
position: relative;
|
||||||
|
width: 200px;
|
||||||
|
height: 200px;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
perspective: 500px;
|
||||||
|
animation: rotateContainer 10s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.molecule {
|
||||||
|
position: absolute;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: teal;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes float1 {
|
||||||
|
0% { transform: translate(0px, 0px) translateZ(0px); }
|
||||||
|
50% { transform: translate(30px, 20px) translateZ(20px); }
|
||||||
|
100% { transform: translate(0px, 0px) translateZ(0px); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes float2 {
|
||||||
|
0% { transform: translate(0px, 0px) translateZ(0px); }
|
||||||
|
50% { transform: translate(-25px, 30px) translateZ(-15px); }
|
||||||
|
100% { transform: translate(0px, 0px) translateZ(0px); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes float3 {
|
||||||
|
0% { transform: translate(0px, 0px) translateZ(0px); }
|
||||||
|
50% { transform: translate(20px, -25px) translateZ(10px); }
|
||||||
|
100% { transform: translate(0px, 0px) translateZ(0px); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes rotateContainer {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
100% { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.molecule:nth-child(1) { animation: float1 3s ease-in-out infinite; }
|
||||||
|
.molecule:nth-child(2) { animation: float2 3.5s ease-in-out infinite; }
|
||||||
|
.molecule:nth-child(3) { animation: float3 4s ease-in-out infinite; }
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.molecule-container {
|
||||||
|
width: 150px;
|
||||||
|
height: 150px;
|
||||||
|
}
|
||||||
|
.molecule {
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 400px) {
|
||||||
|
.molecule-container {
|
||||||
|
width: 120px;
|
||||||
|
height: 120px;
|
||||||
|
}
|
||||||
|
.molecule {
|
||||||
|
width: 25px;
|
||||||
|
height: 25px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`}</style>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MoleculeAvatar;
|
54
frontend/src/components/FuseMind/Chat/ChatBubble.tsx
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Message } from '../../../types/fusemind/messages';
|
||||||
|
|
||||||
|
interface ChatBubbleProps {
|
||||||
|
message: Message | null;
|
||||||
|
position: 'left' | 'right';
|
||||||
|
bgColor?: string;
|
||||||
|
isVisible?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ChatBubble: React.FC<ChatBubbleProps> = ({
|
||||||
|
message,
|
||||||
|
position,
|
||||||
|
bgColor = 'white',
|
||||||
|
isVisible = true
|
||||||
|
}) => {
|
||||||
|
const getBubblePositionClasses = () => {
|
||||||
|
const baseClasses = "relative rounded-lg shadow-sm p-3 max-w-[70%]";
|
||||||
|
const positionClass = position === 'right' ? 'ml-[30%]' : 'ml-[30%]';
|
||||||
|
const animationClasses = `transition-opacity duration-500 ${isVisible ? 'opacity-100' : 'opacity-0'}`;
|
||||||
|
|
||||||
|
return `${baseClasses} ${positionClass} ${animationClasses}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatTime = (date: Date) => {
|
||||||
|
return date.toLocaleTimeString('en-US', {
|
||||||
|
hour: 'numeric',
|
||||||
|
minute: '2-digit',
|
||||||
|
hour12: true
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!message) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={getBubblePositionClasses()}
|
||||||
|
style={{ backgroundColor: bgColor }}
|
||||||
|
>
|
||||||
|
<div className="relative">
|
||||||
|
<div className="relative z-10">
|
||||||
|
<p className="text-[13px] leading-[18px] text-[#f8fafa] whitespace-pre-wrap break-words mb-1">
|
||||||
|
{message.content}
|
||||||
|
</p>
|
||||||
|
<p className="text-[10px] text-[#e0f1f1] text-right">
|
||||||
|
{formatTime(message.timestamp)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ChatBubble;
|
17
frontend/src/components/FuseMind/FuseMind.tsx
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useLocation } from 'react-router-dom';
|
||||||
|
import FuseMindHome from './FuseMindHome';
|
||||||
|
|
||||||
|
const FuseMind: React.FC = () => {
|
||||||
|
const location = useLocation();
|
||||||
|
const path = location.pathname.split('/').pop() || 'home';
|
||||||
|
|
||||||
|
// If we're at the home route, render FuseMindHome directly
|
||||||
|
if (path === 'home' || path === 'fusemind') {
|
||||||
|
return <FuseMindHome />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FuseMind;
|
203
frontend/src/components/FuseMind/FuseMindHome.tsx
Normal file
@ -0,0 +1,203 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { Message } from '../../types/fusemind/messages';
|
||||||
|
import { useChatStore } from '../../state/stores/useChatStore';
|
||||||
|
import ChatBubble from './Chat/ChatBubble';
|
||||||
|
import MoleculeAvatar from './Avatar/MoleculeAvatar';
|
||||||
|
import { useModalCommands } from './modals/hooks/useModalCommands';
|
||||||
|
import ModalContainer from './modals/ModalContainer';
|
||||||
|
import { useModalGrid } from './modals/hooks/useModalGrid';
|
||||||
|
import { ModalConfig } from './modals/core/types';
|
||||||
|
|
||||||
|
interface VisibleMessage extends Message {
|
||||||
|
isVisible: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to convert grid coordinates to percentages
|
||||||
|
const gridToPercent = (x: number, y: number, width: number, height: number) => {
|
||||||
|
// Grid is 20x8, so each cell is 5% of width and 12.5% of height
|
||||||
|
// Since we're using transform: translate(-50%, -50%), we need to center the coordinates
|
||||||
|
return {
|
||||||
|
x: (x * 5) + (width * 5 / 2),
|
||||||
|
y: (y * 12.5) + (height * 12.5 / 2),
|
||||||
|
width: `${width * 5}%`,
|
||||||
|
height: `${height * 12.5}%`
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const FuseMindHome: React.FC = () => {
|
||||||
|
const [messages, setMessages] = useState<VisibleMessage[]>([]);
|
||||||
|
const [inputMessage, setInputMessage] = useState('');
|
||||||
|
const { sendChatRequest } = useChatStore();
|
||||||
|
const { modals, createModal, removeModal } = useModalCommands();
|
||||||
|
const { addModal, removeModal: removeGridModal } = useModalGrid();
|
||||||
|
|
||||||
|
const handleSendMessage = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!inputMessage.trim()) return;
|
||||||
|
|
||||||
|
const userMessage: VisibleMessage = {
|
||||||
|
id: Date.now().toString(),
|
||||||
|
type: 'user',
|
||||||
|
content: inputMessage,
|
||||||
|
timestamp: new Date(),
|
||||||
|
isVisible: true
|
||||||
|
};
|
||||||
|
|
||||||
|
setMessages(prev => [...prev, userMessage]);
|
||||||
|
setInputMessage('');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await sendChatRequest('/chat/completions', {
|
||||||
|
data: `Create a comprehensive table about ${inputMessage}. Return ONLY a JSON object in this format:
|
||||||
|
{
|
||||||
|
"columns": [
|
||||||
|
{"field": "column1", "header": "Column 1"},
|
||||||
|
{"field": "column2", "header": "Column 2"}
|
||||||
|
],
|
||||||
|
"data": [
|
||||||
|
{"column1": "value1", "column2": "value2"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
1. Return ONLY the JSON object, no explanations
|
||||||
|
2. Make sure column names are descriptive and relevant to the topic
|
||||||
|
3. Return ONLY the JSON object, no explanations or additional text
|
||||||
|
4. Ensure data is accurate, comprehensive, and relevant to the topic
|
||||||
|
5. Include as many relevant entries as possible to provide a thorough overview
|
||||||
|
6. Make sure all field names in data match column fields exactly
|
||||||
|
7. Each entry should be unique and provide distinct information`,
|
||||||
|
responseFormat: 'json'
|
||||||
|
});
|
||||||
|
|
||||||
|
const tableData = JSON.parse(response.responseText);
|
||||||
|
if (tableData.columns && tableData.data) {
|
||||||
|
const modalId = Date.now().toString();
|
||||||
|
// Try to find a position in the grid (14 columns wide, 5 rows tall)
|
||||||
|
const gridPosition = addModal(modalId, 14, 5);
|
||||||
|
|
||||||
|
if (gridPosition) {
|
||||||
|
// Make modal wider by using 16 columns instead of 14
|
||||||
|
const pos = gridToPercent(gridPosition.x, gridPosition.y, 16, 7);
|
||||||
|
await createModal({
|
||||||
|
type: 'table',
|
||||||
|
position: pos,
|
||||||
|
content: tableData
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const columnCount = tableData.columns.length;
|
||||||
|
const rowCount = tableData.data.length;
|
||||||
|
const botMessage: VisibleMessage = {
|
||||||
|
id: Date.now().toString(),
|
||||||
|
type: 'system',
|
||||||
|
content: `I've created a table showing ${rowCount} entries with ${columnCount} columns of information about ${inputMessage}. You can view it in the window above.`,
|
||||||
|
timestamp: new Date(),
|
||||||
|
isVisible: true
|
||||||
|
};
|
||||||
|
setMessages(prev => [...prev, botMessage]);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error processing response:', error);
|
||||||
|
const errorMessage: VisibleMessage = {
|
||||||
|
id: Date.now().toString(),
|
||||||
|
type: 'system',
|
||||||
|
content: "Sorry, I ran into an error displaying the table. Please try again.",
|
||||||
|
timestamp: new Date(),
|
||||||
|
isVisible: true
|
||||||
|
};
|
||||||
|
setMessages(prev => [...prev, errorMessage]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleModalClose = (id: string) => {
|
||||||
|
removeModal(id);
|
||||||
|
removeGridModal(id);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full h-full flex flex-col">
|
||||||
|
<div className="flex-1 relative w-full">
|
||||||
|
{/* Grid Container */}
|
||||||
|
<div className="absolute inset-0 p-4">
|
||||||
|
<div className="relative w-full h-full border border-gray-200 bg-white">
|
||||||
|
{/* Grid Lines and Labels */}
|
||||||
|
<div className="absolute inset-0 grid grid-cols-20 grid-rows-8">
|
||||||
|
{/* Grid Cells with Coordinates */}
|
||||||
|
{Array.from({ length: 160 }).map((_, i) => {
|
||||||
|
const col = i % 20;
|
||||||
|
const row = Math.floor(i / 20);
|
||||||
|
const colLabel = String.fromCharCode(65 + col);
|
||||||
|
const rowLabel = row + 1;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
className="border border-gray-100 relative hover:bg-gray-50"
|
||||||
|
style={{ gridColumn: `${col + 1}`, gridRow: `${row + 1}` }}
|
||||||
|
>
|
||||||
|
<span className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 text-gray-300 text-xs select-none">
|
||||||
|
{`${colLabel}${rowLabel}`}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
{/* Modal Container */}
|
||||||
|
<ModalContainer modals={modals} onClose={handleModalClose} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="fixed bottom-0 right-8 w-[400px] z-40">
|
||||||
|
<div className="w-full mb-1">
|
||||||
|
{messages.map((message) => (
|
||||||
|
<div key={message.id} className="mb-0.5">
|
||||||
|
<ChatBubble
|
||||||
|
message={message}
|
||||||
|
position={message.type === 'user' ? 'right' : 'left'}
|
||||||
|
bgColor={message.type === 'user' ? '#3b958a' : '#3d8b96'}
|
||||||
|
isVisible={message.isVisible}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="scale-75 origin-bottom-right flex justify-end translate-y-2">
|
||||||
|
<MoleculeAvatar />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="w-full flex justify-center absolute bottom-5">
|
||||||
|
<div className="w-2/3 flex items-center gap-2">
|
||||||
|
<form onSubmit={handleSendMessage} className="flex-1 flex gap-2">
|
||||||
|
<div className="flex-grow relative">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={inputMessage}
|
||||||
|
onChange={(e) => setInputMessage(e.target.value)}
|
||||||
|
placeholder="Example: Show me a table about beer"
|
||||||
|
className="w-full px-4 py-3 rounded-lg border border-gray-200 focus:outline-none focus:border-teal-500 shadow-md bg-white"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="p-3 bg-teal-500 text-white rounded-lg hover:bg-teal-600 transition-colors shadow-md flex items-center justify-center"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="currentColor"
|
||||||
|
className="w-5 h-5"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M3.478 2.404a.75.75 0 0 0-.926.941l2.432 7.905H13.5a.75.75 0 0 1 0 1.5H4.984l-2.432 7.905a.75.75 0 0 0 .926.94 60.519 60.519 0 0 0 18.445-8.986.75.75 0 0 0 0-1.218A60.517 60.517 0 0 0 3.478 2.404Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FuseMindHome;
|
189
frontend/src/components/FuseMind/README.md
Normal file
@ -0,0 +1,189 @@
|
|||||||
|
# FuseMind Memory System Architecture
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
FuseMind implements a sophisticated memory system that combines psychological memory models with modern LLM capabilities. The system is designed to be modular and extensible, allowing for gradual implementation and refinement.
|
||||||
|
|
||||||
|
## Core Concepts
|
||||||
|
|
||||||
|
### Memory Types
|
||||||
|
1. **Episodic Memory**
|
||||||
|
- Stores specific events and experiences
|
||||||
|
- Includes temporal and emotional context
|
||||||
|
- Used for conversation history and user interactions
|
||||||
|
|
||||||
|
2. **Semantic Memory**
|
||||||
|
- Stores general knowledge and facts
|
||||||
|
- Includes relationships between concepts
|
||||||
|
- Used for system knowledge and agent capabilities
|
||||||
|
|
||||||
|
3. **Procedural Memory**
|
||||||
|
- Stores skills and procedures
|
||||||
|
- Includes conditions and exceptions
|
||||||
|
- Used for agent behaviors and capabilities
|
||||||
|
|
||||||
|
4. **Life Event Memory**
|
||||||
|
- Stores significant life events
|
||||||
|
- Tracks emotional impact and phases
|
||||||
|
- Influences memory formation and retrieval
|
||||||
|
|
||||||
|
### Emotional Context
|
||||||
|
- Tracks emotional state (mood, energy, stress, focus)
|
||||||
|
- Manages emotional triggers and biases
|
||||||
|
- Influences memory formation and retrieval
|
||||||
|
- Handles emotional volatility and stability
|
||||||
|
|
||||||
|
### Temporal Context
|
||||||
|
- Tracks life events and their duration
|
||||||
|
- Manages memory decay and importance
|
||||||
|
- Handles period-specific biases
|
||||||
|
- Influences memory relevance
|
||||||
|
|
||||||
|
## Technical Implementation
|
||||||
|
|
||||||
|
### Memory Service
|
||||||
|
The `MemoryService` class handles all memory operations:
|
||||||
|
- Memory storage and retrieval
|
||||||
|
- Emotional state management
|
||||||
|
- Life event tracking
|
||||||
|
- Memory consolidation
|
||||||
|
|
||||||
|
### Database Schema
|
||||||
|
Memories are stored with the following structure:
|
||||||
|
```typescript
|
||||||
|
interface DatabaseMemory {
|
||||||
|
id: string;
|
||||||
|
type: MemoryType;
|
||||||
|
content: string; // JSON stringified
|
||||||
|
temporal: string; // JSON stringified
|
||||||
|
emotional: string; // JSON stringified
|
||||||
|
connections: string; // JSON stringified
|
||||||
|
created_at: Date;
|
||||||
|
updated_at: Date;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### React Integration
|
||||||
|
The system provides a `useFusemindMemory` hook for React components:
|
||||||
|
```typescript
|
||||||
|
const {
|
||||||
|
activeMemories,
|
||||||
|
emotionalState,
|
||||||
|
storeMemory,
|
||||||
|
retrieveMemories,
|
||||||
|
updateEmotionalState
|
||||||
|
} = useFusemindMemory();
|
||||||
|
```
|
||||||
|
|
||||||
|
## Implementation Roadmap
|
||||||
|
|
||||||
|
### Phase 1: Core Memory System
|
||||||
|
- [x] Basic memory types and interfaces
|
||||||
|
- [x] Memory service implementation
|
||||||
|
- [x] React integration
|
||||||
|
- [ ] Database integration
|
||||||
|
|
||||||
|
### Phase 2: Emotional Context
|
||||||
|
- [ ] Emotional state tracking
|
||||||
|
- [ ] Trigger management
|
||||||
|
- [ ] Bias handling
|
||||||
|
- [ ] Emotional influence on memory
|
||||||
|
|
||||||
|
### Phase 3: Life Events
|
||||||
|
- [ ] Life event tracking
|
||||||
|
- [ ] Event phase management
|
||||||
|
- [ ] Recovery and healing
|
||||||
|
- [ ] Temporal context influence
|
||||||
|
|
||||||
|
### Phase 4: LLM Integration
|
||||||
|
- [ ] Context window management
|
||||||
|
- [ ] Embedding storage and retrieval
|
||||||
|
- [ ] Prompt template management
|
||||||
|
- [ ] Memory-augmented generation
|
||||||
|
|
||||||
|
## Usage Examples
|
||||||
|
|
||||||
|
### Storing a Memory
|
||||||
|
```typescript
|
||||||
|
const memory: MemoryUnit = {
|
||||||
|
id: generateId(),
|
||||||
|
type: 'episodic',
|
||||||
|
content: {
|
||||||
|
data: { /* memory content */ },
|
||||||
|
metadata: { /* additional info */ }
|
||||||
|
},
|
||||||
|
temporal: {
|
||||||
|
created: new Date(),
|
||||||
|
modified: new Date(),
|
||||||
|
lastAccessed: new Date(),
|
||||||
|
decayRate: 0.1,
|
||||||
|
importance: 0.8
|
||||||
|
},
|
||||||
|
emotional: {
|
||||||
|
valence: 0.5,
|
||||||
|
arousal: 0.3,
|
||||||
|
emotionalTags: ['positive', 'exciting']
|
||||||
|
},
|
||||||
|
connections: []
|
||||||
|
};
|
||||||
|
|
||||||
|
await memoryService.storeMemory(memory);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Retrieving Memories
|
||||||
|
```typescript
|
||||||
|
const memories = await memoryService.retrieveMemory({
|
||||||
|
query: 'search term',
|
||||||
|
context: {
|
||||||
|
currentTask: 'task description',
|
||||||
|
emotionalState: 'current state'
|
||||||
|
},
|
||||||
|
filters: {
|
||||||
|
type: 'episodic',
|
||||||
|
timeRange: [startDate, endDate]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
1. **Memory Formation**
|
||||||
|
- Always include temporal and emotional context
|
||||||
|
- Consider current life events
|
||||||
|
- Track memory importance and decay
|
||||||
|
|
||||||
|
2. **Memory Retrieval**
|
||||||
|
- Use appropriate filters
|
||||||
|
- Consider emotional context
|
||||||
|
- Account for temporal relevance
|
||||||
|
|
||||||
|
3. **Life Event Management**
|
||||||
|
- Track event phases
|
||||||
|
- Monitor emotional impact
|
||||||
|
- Handle event transitions
|
||||||
|
|
||||||
|
4. **Emotional State**
|
||||||
|
- Update state gradually
|
||||||
|
- Consider multiple factors
|
||||||
|
- Handle state transitions
|
||||||
|
|
||||||
|
## Future Enhancements
|
||||||
|
|
||||||
|
1. **Advanced Memory Processing**
|
||||||
|
- Machine learning for memory importance
|
||||||
|
- Automated memory consolidation
|
||||||
|
- Dynamic decay rates
|
||||||
|
|
||||||
|
2. **Enhanced Emotional Context**
|
||||||
|
- Multi-dimensional emotional states
|
||||||
|
- Complex trigger patterns
|
||||||
|
- Emotional memory networks
|
||||||
|
|
||||||
|
3. **Improved LLM Integration**
|
||||||
|
- Context-aware prompting
|
||||||
|
- Memory-augmented generation
|
||||||
|
- Dynamic context management
|
||||||
|
|
||||||
|
4. **Visualization Tools**
|
||||||
|
- Memory network visualization
|
||||||
|
- Emotional state tracking
|
||||||
|
- Life event timeline
|
117
frontend/src/components/FuseMind/modals/GridCellIndicator.tsx
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
interface GridCellIndicatorProps {
|
||||||
|
startCol: number;
|
||||||
|
startRow: number;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
onResize?: (newWidth: number, newHeight: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const GridCellIndicator: React.FC<GridCellIndicatorProps> = ({
|
||||||
|
startCol,
|
||||||
|
startRow,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
onResize
|
||||||
|
}) => {
|
||||||
|
// Convert grid units to cell indices
|
||||||
|
const endCol = startCol + width - 1;
|
||||||
|
const endRow = startRow + height - 1;
|
||||||
|
|
||||||
|
// Generate array of cells that should be highlighted
|
||||||
|
const cells = [];
|
||||||
|
for (let row = startRow; row <= endRow; row++) {
|
||||||
|
for (let col = startCol; col <= endCol; col++) {
|
||||||
|
cells.push({ row, col });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle resize interactions
|
||||||
|
const handleResizeStart = (e: React.MouseEvent, edge: 'se' | 'e' | 's') => {
|
||||||
|
e.preventDefault();
|
||||||
|
const startX = e.clientX;
|
||||||
|
const startY = e.clientY;
|
||||||
|
const startWidth = width;
|
||||||
|
const startHeight = height;
|
||||||
|
|
||||||
|
const handleMouseMove = (moveEvent: MouseEvent) => {
|
||||||
|
const deltaX = Math.round((moveEvent.clientX - startX) / (window.innerWidth * 0.05)); // 5% per cell
|
||||||
|
const deltaY = Math.round((moveEvent.clientY - startY) / (window.innerHeight * 0.125)); // 12.5% per cell
|
||||||
|
|
||||||
|
let newWidth = startWidth;
|
||||||
|
let newHeight = startHeight;
|
||||||
|
|
||||||
|
if (edge === 'se' || edge === 'e') {
|
||||||
|
newWidth = Math.max(1, startWidth + deltaX);
|
||||||
|
}
|
||||||
|
if (edge === 'se' || edge === 's') {
|
||||||
|
newHeight = Math.max(1, startHeight + deltaY);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure we don't exceed grid boundaries
|
||||||
|
newWidth = Math.min(newWidth, 20 - startCol);
|
||||||
|
newHeight = Math.min(newHeight, 8 - startRow);
|
||||||
|
|
||||||
|
onResize?.(newWidth, newHeight);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseUp = () => {
|
||||||
|
document.removeEventListener('mousemove', handleMouseMove);
|
||||||
|
document.removeEventListener('mouseup', handleMouseUp);
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('mousemove', handleMouseMove);
|
||||||
|
document.addEventListener('mouseup', handleMouseUp);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* Highlight occupied cells */}
|
||||||
|
{cells.map(({ row, col }) => (
|
||||||
|
<div
|
||||||
|
key={`${row}-${col}`}
|
||||||
|
className="absolute border-2 border-teal-200 bg-teal-50/20"
|
||||||
|
style={{
|
||||||
|
left: `${col * 5}%`,
|
||||||
|
top: `${row * 12.5}%`,
|
||||||
|
width: '5%',
|
||||||
|
height: '12.5%',
|
||||||
|
pointerEvents: 'none'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* Resize handles */}
|
||||||
|
<div
|
||||||
|
className="absolute w-3 h-3 bg-teal-500 rounded-full cursor-se-resize hover:scale-125 transition-transform"
|
||||||
|
style={{
|
||||||
|
right: `${(20 - endCol - 1) * 5}%`,
|
||||||
|
bottom: `${(8 - endRow - 1) * 12.5}%`,
|
||||||
|
transform: 'translate(50%, 50%)',
|
||||||
|
}}
|
||||||
|
onMouseDown={(e) => handleResizeStart(e, 'se')}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className="absolute w-3 h-3 bg-teal-500 rounded-full cursor-e-resize hover:scale-125 transition-transform"
|
||||||
|
style={{
|
||||||
|
right: `${(20 - endCol - 1) * 5}%`,
|
||||||
|
top: '50%',
|
||||||
|
transform: 'translate(50%, -50%)',
|
||||||
|
}}
|
||||||
|
onMouseDown={(e) => handleResizeStart(e, 'e')}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className="absolute w-3 h-3 bg-teal-500 rounded-full cursor-s-resize hover:scale-125 transition-transform"
|
||||||
|
style={{
|
||||||
|
bottom: `${(8 - endRow - 1) * 12.5}%`,
|
||||||
|
left: '50%',
|
||||||
|
transform: 'translate(-50%, 50%)',
|
||||||
|
}}
|
||||||
|
onMouseDown={(e) => handleResizeStart(e, 's')}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default GridCellIndicator;
|
51
frontend/src/components/FuseMind/modals/ModalContainer.tsx
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { ModalConfig } from './core/types';
|
||||||
|
import ModalFactory from './ModalFactory';
|
||||||
|
|
||||||
|
interface TableContent {
|
||||||
|
columns: Array<{
|
||||||
|
field: string;
|
||||||
|
header: string;
|
||||||
|
}>;
|
||||||
|
data: Array<Record<string, unknown>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ModalContainerProps {
|
||||||
|
modals: ModalConfig[];
|
||||||
|
onClose: (id: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ModalContainer: React.FC<ModalContainerProps> = ({ modals, onClose }) => {
|
||||||
|
return (
|
||||||
|
<div className="absolute inset-0">
|
||||||
|
<style>{`
|
||||||
|
.modal-transition {
|
||||||
|
transition: all 0.3s ease-in-out;
|
||||||
|
}
|
||||||
|
`}</style>
|
||||||
|
{modals.map((modal) => (
|
||||||
|
<div
|
||||||
|
key={modal.id}
|
||||||
|
className="absolute modal-transition"
|
||||||
|
style={{
|
||||||
|
left: `${modal.position.x}%`,
|
||||||
|
top: `${modal.position.y}%`,
|
||||||
|
width: modal.position.width,
|
||||||
|
height: modal.position.height,
|
||||||
|
transform: 'translate(-50%, -50%)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ModalFactory
|
||||||
|
id={modal.id}
|
||||||
|
type={modal.type}
|
||||||
|
position={modal.position}
|
||||||
|
content={modal.content as TableContent}
|
||||||
|
onClose={onClose}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ModalContainer;
|
66
frontend/src/components/FuseMind/modals/ModalFactory.tsx
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { ModalType, ModalPosition } from './core/types';
|
||||||
|
import TableModal from './TableModal';
|
||||||
|
|
||||||
|
interface TableContent {
|
||||||
|
columns: Array<{
|
||||||
|
field: string;
|
||||||
|
header: string;
|
||||||
|
}>;
|
||||||
|
data: Array<Record<string, unknown>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ModalFactoryProps {
|
||||||
|
id: string;
|
||||||
|
type: ModalType;
|
||||||
|
position: ModalPosition;
|
||||||
|
style?: {
|
||||||
|
backgroundColor?: string;
|
||||||
|
};
|
||||||
|
content?: TableContent | React.ReactNode;
|
||||||
|
onClose: (id: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ModalFactory: React.FC<ModalFactoryProps> = ({ id, type, position, style, content, onClose }) => {
|
||||||
|
const renderContent = (): React.ReactNode => {
|
||||||
|
if (!content) return <div>Empty modal</div>;
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case 'table':
|
||||||
|
if (typeof content === 'object' && 'columns' in content && 'data' in content) {
|
||||||
|
return <TableModal content={content as TableContent} />;
|
||||||
|
}
|
||||||
|
return <div>Invalid table content</div>;
|
||||||
|
default:
|
||||||
|
return content as React.ReactNode;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="bg-white border shadow-lg p-4 absolute"
|
||||||
|
style={{
|
||||||
|
left: `${position.x}%`,
|
||||||
|
top: `${position.y}%`,
|
||||||
|
transform: 'translate(-50%, -50%)',
|
||||||
|
width: position.width,
|
||||||
|
height: position.height,
|
||||||
|
backgroundColor: style?.backgroundColor || 'white'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="flex justify-between items-center mb-4">
|
||||||
|
<div className="font-semibold">Data Table</div>
|
||||||
|
<button className="hover:bg-gray-100 rounded-full p-1" onClick={() => onClose(id)}>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
||||||
|
<path fillRule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clipRule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="h-[calc(100%-4rem)] overflow-hidden">
|
||||||
|
{renderContent()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ModalFactory;
|
17
frontend/src/components/FuseMind/modals/TableModal.d.ts
vendored
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
interface TableContent {
|
||||||
|
columns: Array<{
|
||||||
|
field: string;
|
||||||
|
header: string;
|
||||||
|
}>;
|
||||||
|
data: Array<Record<string, unknown>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TableModalProps {
|
||||||
|
content: TableContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare const TableModal: React.FC<TableModalProps>;
|
||||||
|
|
||||||
|
export default TableModal;
|
144
frontend/src/components/FuseMind/modals/TableModal.tsx
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { DataTable } from 'primereact/datatable';
|
||||||
|
import { Column } from 'primereact/column';
|
||||||
|
|
||||||
|
interface TableModalProps {
|
||||||
|
content: {
|
||||||
|
columns: Array<{
|
||||||
|
field: string;
|
||||||
|
header: string;
|
||||||
|
}>;
|
||||||
|
data: Array<Record<string, any>>;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const TableModal: React.FC<TableModalProps> = ({ content }) => {
|
||||||
|
return (
|
||||||
|
<div className="w-full h-full flex flex-col p-0">
|
||||||
|
<style>{`
|
||||||
|
.modal-table {
|
||||||
|
display: flex !important;
|
||||||
|
flex-direction: column !important;
|
||||||
|
height: auto !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
}
|
||||||
|
.modal-table .p-datatable-wrapper {
|
||||||
|
min-height: 0 !important;
|
||||||
|
overflow: visible !important;
|
||||||
|
}
|
||||||
|
.modal-table .p-datatable-table {
|
||||||
|
width: 100% !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
border-spacing: 0 !important;
|
||||||
|
}
|
||||||
|
.modal-table .p-datatable-thead {
|
||||||
|
position: sticky !important;
|
||||||
|
top: 0 !important;
|
||||||
|
z-index: 1 !important;
|
||||||
|
background: white !important;
|
||||||
|
}
|
||||||
|
.modal-table .p-datatable-thead > tr > th {
|
||||||
|
padding: 0.4rem !important;
|
||||||
|
background: #f8f9fa !important;
|
||||||
|
white-space: normal !important;
|
||||||
|
overflow: visible !important;
|
||||||
|
}
|
||||||
|
.modal-table .p-datatable-tbody > tr > td {
|
||||||
|
padding: 0.3rem 0.4rem !important;
|
||||||
|
line-height: 1 !important;
|
||||||
|
white-space: normal !important;
|
||||||
|
overflow: visible !important;
|
||||||
|
}
|
||||||
|
.modal-table .p-datatable-tbody > tr:hover {
|
||||||
|
background: #f8f9fa !important;
|
||||||
|
}
|
||||||
|
.p-paginator {
|
||||||
|
padding: 0.15rem !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
border-width: 0 !important;
|
||||||
|
background: white !important;
|
||||||
|
border-top: 1px solid #dee2e6 !important;
|
||||||
|
}
|
||||||
|
.p-column-header-content {
|
||||||
|
display: flex !important;
|
||||||
|
align-items: center !important;
|
||||||
|
justify-content: space-between !important;
|
||||||
|
}
|
||||||
|
.p-column-title {
|
||||||
|
font-weight: 600 !important;
|
||||||
|
}
|
||||||
|
.p-sortable-column-icon {
|
||||||
|
width: 0.75rem !important;
|
||||||
|
height: 0.75rem !important;
|
||||||
|
}
|
||||||
|
.p-datatable-tbody > tr:last-child > td {
|
||||||
|
border-bottom: none !important;
|
||||||
|
}
|
||||||
|
.p-datatable.p-datatable-gridlines {
|
||||||
|
display: flex !important;
|
||||||
|
flex-direction: column !important;
|
||||||
|
height: auto !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
}
|
||||||
|
.p-paginator .p-paginator-pages .p-paginator-page {
|
||||||
|
min-width: 1.8rem !important;
|
||||||
|
height: 1.8rem !important;
|
||||||
|
margin: 0 0.1rem !important;
|
||||||
|
}
|
||||||
|
.p-paginator .p-paginator-first,
|
||||||
|
.p-paginator .p-paginator-prev,
|
||||||
|
.p-paginator .p-paginator-next,
|
||||||
|
.p-paginator .p-paginator-last {
|
||||||
|
min-width: 1.8rem !important;
|
||||||
|
height: 1.8rem !important;
|
||||||
|
margin: 0 0.1rem !important;
|
||||||
|
}
|
||||||
|
.p-paginator .p-paginator-current {
|
||||||
|
margin: 0 0.3rem !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
`}</style>
|
||||||
|
<DataTable
|
||||||
|
value={content.data}
|
||||||
|
className="modal-table"
|
||||||
|
scrollable={false}
|
||||||
|
tableStyle={{ width: '100%', margin: 0 }}
|
||||||
|
showGridlines
|
||||||
|
stripedRows
|
||||||
|
sortMode="multiple"
|
||||||
|
removableSort
|
||||||
|
resizableColumns
|
||||||
|
columnResizeMode="fit"
|
||||||
|
size="small"
|
||||||
|
paginator
|
||||||
|
rows={10}
|
||||||
|
rowsPerPageOptions={[10]}
|
||||||
|
paginatorTemplate="FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink CurrentPageReport"
|
||||||
|
currentPageReportTemplate="Showing {first} to {last} of {totalRecords} entries"
|
||||||
|
>
|
||||||
|
{content.columns.map(col => (
|
||||||
|
<Column
|
||||||
|
key={col.field}
|
||||||
|
field={col.field}
|
||||||
|
header={col.header}
|
||||||
|
sortable
|
||||||
|
style={{
|
||||||
|
minWidth: '100px',
|
||||||
|
padding: '0.3rem 0.4rem',
|
||||||
|
whiteSpace: 'normal',
|
||||||
|
overflow: 'visible'
|
||||||
|
}}
|
||||||
|
headerStyle={{
|
||||||
|
fontWeight: 600,
|
||||||
|
padding: '0.4rem',
|
||||||
|
whiteSpace: 'normal',
|
||||||
|
overflow: 'visible'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</DataTable>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TableModal;
|
56
frontend/src/components/FuseMind/modals/commands/parser.ts
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import { ModalType } from '../core/types';
|
||||||
|
|
||||||
|
export interface ParsedCommand {
|
||||||
|
type: 'MODAL_COMMAND';
|
||||||
|
action: 'CREATE' | 'SPLIT' | 'UPDATE' | 'STYLE' | 'HELP' | 'RESIZE';
|
||||||
|
params: {
|
||||||
|
modalType?: ModalType;
|
||||||
|
layout?: 'horizontal' | 'vertical';
|
||||||
|
position?: 'left' | 'right' | 'top' | 'bottom';
|
||||||
|
style?: {
|
||||||
|
backgroundColor?: string;
|
||||||
|
};
|
||||||
|
size?: {
|
||||||
|
width?: string;
|
||||||
|
height?: string;
|
||||||
|
};
|
||||||
|
count?: number;
|
||||||
|
content?: unknown;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const HELP_TEXT = `
|
||||||
|
I can help you with the following commands:
|
||||||
|
- Create modals: "show me a table/text/image"
|
||||||
|
- Split screen: "show two tables side by side"
|
||||||
|
- Style modals: "make right modal blue"
|
||||||
|
- Resize modals: "make modal bigger" or "set modal width to 80%"
|
||||||
|
- Layout: "arrange modals vertically"
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
- "show me options" - displays available commands
|
||||||
|
- "show me a table" - creates a table modal
|
||||||
|
- "make modal 80% wide" - resizes modal width
|
||||||
|
- "split screen horizontally" - arranges modals horizontally
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const COMMAND_PROMPT = `You are a helpful assistant that creates data tables. If the user's message implies they want to see data in a table format (they might mention "table", "data", "information", "stats", etc., or just ask about a topic that would be best shown in a table), create a table about that topic.
|
||||||
|
|
||||||
|
Return ONLY a JSON object in this format:
|
||||||
|
{
|
||||||
|
"columns": [
|
||||||
|
{"field": "column1", "header": "Column 1"},
|
||||||
|
{"field": "column2", "header": "Column 2"}
|
||||||
|
],
|
||||||
|
"data": [
|
||||||
|
{"column1": "value1", "column2": "value2"},
|
||||||
|
{"column1": "value3", "column2": "value4"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
1. Return ONLY the JSON object, no explanations
|
||||||
|
2. Make sure field names in data match column fields exactly
|
||||||
|
3. Choose appropriate column names based on the topic
|
||||||
|
4. Include at least 5 rows of data
|
||||||
|
5. If the user's message doesn't imply they want tabular data, return null`;
|
53
frontend/src/components/FuseMind/modals/components/Modal.tsx
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { ModalConfig } from '../core/types';
|
||||||
|
import { useModal } from '../context/ModalContext';
|
||||||
|
|
||||||
|
interface ModalProps extends ModalConfig {}
|
||||||
|
|
||||||
|
export const Modal: React.FC<ModalProps> = ({
|
||||||
|
id,
|
||||||
|
type,
|
||||||
|
position,
|
||||||
|
style,
|
||||||
|
content
|
||||||
|
}) => {
|
||||||
|
const { removeModal } = useModal();
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
removeModal(id);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="absolute"
|
||||||
|
style={{
|
||||||
|
top: `${position.y}%`,
|
||||||
|
left: `${position.x}%`,
|
||||||
|
width: position.width,
|
||||||
|
height: position.height,
|
||||||
|
transform: 'translate(-50%, -50%)',
|
||||||
|
backgroundColor: 'white',
|
||||||
|
borderRadius: '8px',
|
||||||
|
boxShadow: '0 4px 6px rgba(0, 0, 0, 0.1)',
|
||||||
|
overflow: 'hidden',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="flex justify-between items-center p-4 border-b">
|
||||||
|
<h2 className="text-lg font-semibold">Data Table</h2>
|
||||||
|
<button
|
||||||
|
onClick={handleClose}
|
||||||
|
className="p-1 hover:bg-gray-100 rounded-full"
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
||||||
|
<path fillRule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clipRule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 overflow-auto p-4">
|
||||||
|
{content}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,18 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Modal } from './Modal';
|
||||||
|
import { useModal } from '../context/ModalContext';
|
||||||
|
|
||||||
|
export const ModalRenderer: React.FC = () => {
|
||||||
|
const { modals } = useModal();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{modals.map((modal) => (
|
||||||
|
<Modal
|
||||||
|
key={modal.id}
|
||||||
|
{...modal}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,63 @@
|
|||||||
|
import React, { createContext, useContext, useState, useCallback } from 'react';
|
||||||
|
import { ModalType, ModalPosition, ModalConfig } from '../core/types';
|
||||||
|
|
||||||
|
interface ModalContextType {
|
||||||
|
modals: ModalConfig[];
|
||||||
|
createModal: (params: { type: ModalType; position?: ModalPosition; content: unknown }) => Promise<ModalConfig>;
|
||||||
|
removeModal: (id: string) => void;
|
||||||
|
updateModalStyle: (id: string, style: { backgroundColor?: string }) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ModalContext = createContext<ModalContextType | undefined>(undefined);
|
||||||
|
|
||||||
|
export const ModalProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||||
|
const [modals, setModals] = useState<ModalConfig[]>([]);
|
||||||
|
|
||||||
|
const createModal = useCallback((params: { type: ModalType; position?: ModalPosition; content: unknown }) => {
|
||||||
|
const newModal: ModalConfig = {
|
||||||
|
id: Date.now().toString(),
|
||||||
|
type: params.type,
|
||||||
|
position: params.position || {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: '100%',
|
||||||
|
height: '100%'
|
||||||
|
},
|
||||||
|
content: params.content
|
||||||
|
};
|
||||||
|
|
||||||
|
return new Promise<ModalConfig>((resolve) => {
|
||||||
|
setModals(prev => {
|
||||||
|
const newModals = [...prev, newModal];
|
||||||
|
resolve(newModal);
|
||||||
|
return newModals;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const removeModal = useCallback((id: string) => {
|
||||||
|
setModals(prev => prev.filter(modal => modal.id !== id));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const updateModalStyle = useCallback((id: string, style: { backgroundColor?: string }) => {
|
||||||
|
setModals(prev =>
|
||||||
|
prev.map(modal =>
|
||||||
|
modal.id === id ? { ...modal, style: { ...modal.style, ...style } } : modal
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModalContext.Provider value={{ modals, createModal, removeModal, updateModalStyle }}>
|
||||||
|
{children}
|
||||||
|
</ModalContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useModal = () => {
|
||||||
|
const context = useContext(ModalContext);
|
||||||
|
if (context === undefined) {
|
||||||
|
throw new Error('useModal must be used within a ModalProvider');
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
};
|
@ -0,0 +1,10 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { ModalProvider as Provider } from './ModalContext';
|
||||||
|
|
||||||
|
export const ModalProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||||
|
return (
|
||||||
|
<Provider>
|
||||||
|
{children}
|
||||||
|
</Provider>
|
||||||
|
);
|
||||||
|
};
|
44
frontend/src/components/FuseMind/modals/core/types.ts
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
export type ModalPosition = {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
width: string;
|
||||||
|
height: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ModalType = 'table' | 'default';
|
||||||
|
|
||||||
|
export type ModalId = string;
|
||||||
|
|
||||||
|
export interface Modal {
|
||||||
|
id: ModalId;
|
||||||
|
type: ModalType;
|
||||||
|
position: ModalPosition;
|
||||||
|
style?: {
|
||||||
|
backgroundColor?: string;
|
||||||
|
};
|
||||||
|
content?: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ModalConfig {
|
||||||
|
id: ModalId;
|
||||||
|
type: ModalType;
|
||||||
|
position: ModalPosition;
|
||||||
|
content: unknown;
|
||||||
|
style?: {
|
||||||
|
backgroundColor?: string;
|
||||||
|
borderRadius?: string;
|
||||||
|
padding?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ModalAction =
|
||||||
|
| { type: 'CREATE_MODAL'; payload: Omit<ModalConfig, 'id'> }
|
||||||
|
| { type: 'UPDATE_MODAL'; payload: { id: ModalId } & Partial<ModalConfig> }
|
||||||
|
| { type: 'DELETE_MODAL'; payload: { id: ModalId } }
|
||||||
|
| { type: 'UPDATE_POSITION'; payload: { id: ModalId; position: Partial<ModalPosition> } }
|
||||||
|
| { type: 'UPDATE_STYLE'; payload: { id: ModalId; style: Partial<ModalConfig['style']> } };
|
||||||
|
|
||||||
|
export interface ModalState {
|
||||||
|
modals: Record<ModalId, ModalConfig>;
|
||||||
|
activeModal: ModalId | null;
|
||||||
|
}
|
@ -0,0 +1,113 @@
|
|||||||
|
import { useState, useCallback } from 'react';
|
||||||
|
import { ModalType, ModalPosition, ModalConfig } from '../core/types';
|
||||||
|
|
||||||
|
export const useModalCommands = () => {
|
||||||
|
const [modals, setModals] = useState<ModalConfig[]>([]);
|
||||||
|
const [layout, setLayout] = useState<'horizontal' | 'vertical'>('horizontal');
|
||||||
|
|
||||||
|
const calculatePositions = useCallback((count: number, layout: 'horizontal' | 'vertical') => {
|
||||||
|
const positions: ModalPosition[] = [];
|
||||||
|
const gap = 10;
|
||||||
|
|
||||||
|
if (layout === 'horizontal') {
|
||||||
|
const width = (100 - gap * (count - 1)) / count;
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
positions.push({
|
||||||
|
x: i * (width + gap),
|
||||||
|
y: 0,
|
||||||
|
width: `${width}%`,
|
||||||
|
height: '100%'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const height = (100 - gap * (count - 1)) / count;
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
positions.push({
|
||||||
|
x: 0,
|
||||||
|
y: i * (height + gap),
|
||||||
|
width: '100%',
|
||||||
|
height: `${height}%`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return positions;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const createModal = useCallback((params: { type: ModalType; position?: ModalPosition; content: unknown }) => {
|
||||||
|
const newModal: ModalConfig = {
|
||||||
|
id: Date.now().toString(),
|
||||||
|
type: params.type,
|
||||||
|
position: params.position || {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: '100%',
|
||||||
|
height: '100%'
|
||||||
|
},
|
||||||
|
content: params.content
|
||||||
|
};
|
||||||
|
|
||||||
|
return new Promise<ModalConfig>((resolve) => {
|
||||||
|
setModals(prev => {
|
||||||
|
const newModals = [...prev, newModal];
|
||||||
|
resolve(newModal);
|
||||||
|
return newModals;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const removeModal = useCallback((id: string) => {
|
||||||
|
setModals(prev => prev.filter(modal => modal.id !== id));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const updateModalStyle = useCallback((id: string, style: { backgroundColor?: string }) => {
|
||||||
|
setModals(prev =>
|
||||||
|
prev.map(modal =>
|
||||||
|
modal.id === id ? { ...modal, style: { ...modal.style, ...style } } : modal
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const splitScreen = useCallback((modalIds: string[], newLayout: 'horizontal' | 'vertical') => {
|
||||||
|
setLayout(newLayout);
|
||||||
|
setModals(prev => {
|
||||||
|
const targetModals = prev.filter(modal => modalIds.includes(modal.id));
|
||||||
|
const otherModals = prev.filter(modal => !modalIds.includes(modal.id));
|
||||||
|
|
||||||
|
const positions = calculatePositions(targetModals.length, newLayout);
|
||||||
|
const updatedTargetModals = targetModals.map((modal, index) => ({
|
||||||
|
...modal,
|
||||||
|
position: positions[index]
|
||||||
|
}));
|
||||||
|
|
||||||
|
return [...otherModals, ...updatedTargetModals];
|
||||||
|
});
|
||||||
|
}, [calculatePositions]);
|
||||||
|
|
||||||
|
const getModalsByPosition = useCallback((position: 'left' | 'right' | 'top' | 'bottom') => {
|
||||||
|
return modals.filter(modal => {
|
||||||
|
switch (position) {
|
||||||
|
case 'left':
|
||||||
|
return modal.position.x === 0;
|
||||||
|
case 'right':
|
||||||
|
return modal.position.x > 0;
|
||||||
|
case 'top':
|
||||||
|
return modal.position.y === 0;
|
||||||
|
case 'bottom':
|
||||||
|
return modal.position.y > 0;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, [modals]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
modals,
|
||||||
|
createModal,
|
||||||
|
removeModal,
|
||||||
|
updateModalStyle,
|
||||||
|
splitScreen,
|
||||||
|
getModalsByPosition,
|
||||||
|
layout
|
||||||
|
};
|
||||||
|
};
|
21
frontend/src/components/FuseMind/modals/hooks/useModalGrid.d.ts
vendored
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { ModalPosition } from '../core/types';
|
||||||
|
|
||||||
|
export interface GridPosition {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GridModal {
|
||||||
|
id: string;
|
||||||
|
position: GridPosition;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useModalGrid: () => {
|
||||||
|
addModal: (id: string, width: number, height: number) => GridPosition | null;
|
||||||
|
removeModal: (id: string) => void;
|
||||||
|
moveModal: (id: string, newPos: GridPosition) => boolean;
|
||||||
|
modals: GridModal[];
|
||||||
|
gridState: boolean[][];
|
||||||
|
};
|
111
frontend/src/components/FuseMind/modals/hooks/useModalGrid.ts
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
import { useState, useCallback } from 'react';
|
||||||
|
|
||||||
|
interface GridPosition {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GridModal {
|
||||||
|
id: string;
|
||||||
|
position: GridPosition;
|
||||||
|
}
|
||||||
|
|
||||||
|
const GRID_COLS = 20;
|
||||||
|
const GRID_ROWS = 8;
|
||||||
|
|
||||||
|
export const useModalGrid = () => {
|
||||||
|
const [gridState, setGridState] = useState<boolean[][]>(
|
||||||
|
Array(GRID_ROWS).fill(null).map(() => Array(GRID_COLS).fill(false))
|
||||||
|
);
|
||||||
|
const [modals, setModals] = useState<GridModal[]>([]);
|
||||||
|
|
||||||
|
// Check if a position is available in the grid
|
||||||
|
const isPositionAvailable = useCallback((pos: GridPosition) => {
|
||||||
|
for (let y = pos.y; y < pos.y + pos.height; y++) {
|
||||||
|
for (let x = pos.x; x < pos.x + pos.width; x++) {
|
||||||
|
if (y >= GRID_ROWS || x >= GRID_COLS || gridState[y][x]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}, [gridState]);
|
||||||
|
|
||||||
|
// Find next available position for given dimensions
|
||||||
|
const findNextPosition = useCallback((width: number, height: number): GridPosition | null => {
|
||||||
|
for (let y = 0; y <= GRID_ROWS - height; y++) {
|
||||||
|
for (let x = 0; x <= GRID_COLS - width; x++) {
|
||||||
|
const pos = { x, y, width, height };
|
||||||
|
if (isPositionAvailable(pos)) {
|
||||||
|
return pos;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}, [isPositionAvailable]);
|
||||||
|
|
||||||
|
// Update grid state for a position
|
||||||
|
const updateGridState = useCallback((pos: GridPosition, value: boolean) => {
|
||||||
|
setGridState(prev => {
|
||||||
|
const newState = prev.map(row => [...row]);
|
||||||
|
for (let y = pos.y; y < pos.y + pos.height; y++) {
|
||||||
|
for (let x = pos.x; x < pos.x + pos.width; x++) {
|
||||||
|
newState[y][x] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return newState;
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Add a new modal to the grid
|
||||||
|
const addModal = useCallback((id: string, width: number, height: number) => {
|
||||||
|
const position = findNextPosition(width, height);
|
||||||
|
if (!position) return null;
|
||||||
|
|
||||||
|
const newModal = { id, position };
|
||||||
|
setModals(prev => [...prev, newModal]);
|
||||||
|
updateGridState(position, true);
|
||||||
|
return position;
|
||||||
|
}, [findNextPosition, updateGridState]);
|
||||||
|
|
||||||
|
// Remove a modal from the grid
|
||||||
|
const removeModal = useCallback((id: string) => {
|
||||||
|
setModals(prev => {
|
||||||
|
const modal = prev.find(m => m.id === id);
|
||||||
|
if (modal) {
|
||||||
|
updateGridState(modal.position, false);
|
||||||
|
}
|
||||||
|
return prev.filter(m => m.id !== id);
|
||||||
|
});
|
||||||
|
}, [updateGridState]);
|
||||||
|
|
||||||
|
// Move a modal to a new position
|
||||||
|
const moveModal = useCallback((id: string, newPos: GridPosition) => {
|
||||||
|
if (!isPositionAvailable(newPos)) return false;
|
||||||
|
|
||||||
|
setModals(prev => {
|
||||||
|
const modal = prev.find(m => m.id === id);
|
||||||
|
if (!modal) return prev;
|
||||||
|
|
||||||
|
updateGridState(modal.position, false);
|
||||||
|
updateGridState(newPos, true);
|
||||||
|
|
||||||
|
return prev.map(m =>
|
||||||
|
m.id === id ? { ...m, position: newPos } : m
|
||||||
|
);
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}, [isPositionAvailable, updateGridState]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
addModal,
|
||||||
|
removeModal,
|
||||||
|
moveModal,
|
||||||
|
modals,
|
||||||
|
gridState
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type { GridPosition, GridModal };
|
85
frontend/src/components/FuseMind/modals/store/modalStore.ts
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
import create from 'zustand';
|
||||||
|
import { ModalState, ModalAction, ModalConfig, ModalId } from '../core/types';
|
||||||
|
import { nanoid } from 'nanoid';
|
||||||
|
|
||||||
|
const initialState: ModalState = {
|
||||||
|
modals: {},
|
||||||
|
activeModal: null
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useModalStore = create<ModalState & {
|
||||||
|
dispatch: (action: ModalAction) => void;
|
||||||
|
}>((set) => ({
|
||||||
|
...initialState,
|
||||||
|
dispatch: (action) => {
|
||||||
|
set((state) => {
|
||||||
|
switch (action.type) {
|
||||||
|
case 'CREATE_MODAL':
|
||||||
|
const id = nanoid();
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
modals: {
|
||||||
|
...state.modals,
|
||||||
|
[id]: {
|
||||||
|
...action.payload,
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'UPDATE_MODAL':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
modals: {
|
||||||
|
...state.modals,
|
||||||
|
[action.payload.id]: {
|
||||||
|
...state.modals[action.payload.id],
|
||||||
|
...action.payload
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'DELETE_MODAL':
|
||||||
|
const { [action.payload.id]: _, ...remainingModals } = state.modals;
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
modals: remainingModals,
|
||||||
|
activeModal: state.activeModal === action.payload.id ? null : state.activeModal
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'UPDATE_POSITION':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
modals: {
|
||||||
|
...state.modals,
|
||||||
|
[action.payload.id]: {
|
||||||
|
...state.modals[action.payload.id],
|
||||||
|
position: {
|
||||||
|
...state.modals[action.payload.id].position,
|
||||||
|
...action.payload.position
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'UPDATE_STYLE':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
modals: {
|
||||||
|
...state.modals,
|
||||||
|
[action.payload.id]: {
|
||||||
|
...state.modals[action.payload.id],
|
||||||
|
style: {
|
||||||
|
...state.modals[action.payload.id].style,
|
||||||
|
...action.payload.style
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}));
|
91
frontend/src/components/LoginModal.tsx
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { Card } from 'primereact/card';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import Button from '../shared/components/_V2/Button';
|
||||||
|
import Input from '../shared/components/_V2/InputText';
|
||||||
|
import { useAuthStore } from '../state/stores/useAuthStore';
|
||||||
|
|
||||||
|
interface LoginModalProps {
|
||||||
|
redirectUrl?: string;
|
||||||
|
onLoginSuccess?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const LoginModal: React.FC<LoginModalProps> = ({ redirectUrl = '/dashboard', onLoginSuccess }) => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { login } = useAuthStore();
|
||||||
|
const [username, setUsername] = useState('');
|
||||||
|
const [password, setPassword] = useState('');
|
||||||
|
const [isTransitioning, setIsTransitioning] = useState(false);
|
||||||
|
|
||||||
|
const handleLogin = async () => {
|
||||||
|
setIsTransitioning(true);
|
||||||
|
try {
|
||||||
|
await login(username, password);
|
||||||
|
if (onLoginSuccess) {
|
||||||
|
onLoginSuccess();
|
||||||
|
}
|
||||||
|
navigate(redirectUrl, { replace: true });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Login failed:', error);
|
||||||
|
} finally {
|
||||||
|
setIsTransitioning(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
|
if (event.key === 'Enter') {
|
||||||
|
handleLogin();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const header = () => <h2 className='text-2xl'>Fusero</h2>;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const timeoutId = setTimeout(() => {
|
||||||
|
setIsTransitioning(false);
|
||||||
|
}, 350);
|
||||||
|
return () => clearTimeout(timeoutId);
|
||||||
|
}, [isTransitioning]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='flex items-center justify-center w-full h-full overflow-hidden'>
|
||||||
|
<Card
|
||||||
|
header={header}
|
||||||
|
style={{ width: '100%', height: '100%', overflow: 'hidden' }}
|
||||||
|
className='flex flex-col items-center justify-center gap-4 p-8 bg-white rounded-md shadowlg'
|
||||||
|
|
||||||
|
>
|
||||||
|
<div className='w-full'>
|
||||||
|
<label htmlFor='username' className='text-sm'>
|
||||||
|
Username
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
id='username'
|
||||||
|
value={username}
|
||||||
|
onChange={(e) => setUsername(e.target.value)}
|
||||||
|
className='w-full'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className='w-full'>
|
||||||
|
<label htmlFor='password' className='text-sm'>
|
||||||
|
Password
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
id='password'
|
||||||
|
type='password'
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
className='w-full'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className='pt-8'>
|
||||||
|
<Button label='Login' className='w-full' onClick={handleLogin} />
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LoginModal;
|
47
frontend/src/components/PromptBox.tsx
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
type PromptBoxProps = {
|
||||||
|
onSubmit: (message: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const PromptBox: React.FC<PromptBoxProps> = ({ onSubmit }) => {
|
||||||
|
const [promptMessage, setPromptMessage] = useState<string>('');
|
||||||
|
|
||||||
|
const handleFormSubmit = (event: React.FormEvent<HTMLFormElement>) => {
|
||||||
|
event.preventDefault();
|
||||||
|
if (promptMessage.trim()) {
|
||||||
|
onSubmit(promptMessage);
|
||||||
|
setPromptMessage('');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='p-4 text-white bg-gray-600' style={{ height: '6rem' }}>
|
||||||
|
<form onSubmit={handleFormSubmit} className='flex items-center space-x-2'>
|
||||||
|
<input
|
||||||
|
type='text'
|
||||||
|
value={promptMessage}
|
||||||
|
onChange={(e) => setPromptMessage(e.target.value)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
// set up typing
|
||||||
|
handleFormSubmit(e as unknown as React.FormEvent<HTMLFormElement>); // Trigger submit on Enter
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className='flex-grow p-2 text-black rounded'
|
||||||
|
placeholder='Enter your message...'
|
||||||
|
/>
|
||||||
|
<button type='submit' className='p-4 text-white rounded bg-accent'>
|
||||||
|
Send
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
PromptBox.propTypes = {
|
||||||
|
onSubmit: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PromptBox;
|
140
frontend/src/components/SystemSelectionModal.tsx
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { Card } from 'primereact/card';
|
||||||
|
import Button from '../shared/components/_V2/Button';
|
||||||
|
import Dropdown from '../shared/components/_V2/Dropdown';
|
||||||
|
import Divider from '../shared/components/_V1/Divider';
|
||||||
|
|
||||||
|
import fusero_logo from '../assets/fusero_logo.svg';
|
||||||
|
import { useSystemStore } from '../state/stores/useSystemStore';
|
||||||
|
|
||||||
|
export type SystemDropdownOption = {
|
||||||
|
id: number;
|
||||||
|
label: string;
|
||||||
|
businessLabel: string;
|
||||||
|
urlSlug: string;
|
||||||
|
logo: string;
|
||||||
|
category: string;
|
||||||
|
enterprises?: Enterprise[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Enterprise = {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface SystemSelectionModalProps {
|
||||||
|
systemDetails: SystemDropdownOption | null;
|
||||||
|
onSystemSelected: (systemData: any) => void;
|
||||||
|
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',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// Add system default routes configuration
|
||||||
|
const systemDefaultRoutes = {
|
||||||
|
council: 'chat',
|
||||||
|
fusemind: 'home',
|
||||||
|
};
|
||||||
|
|
||||||
|
const SystemSelectionModal = ({ systemDetails }: SystemSelectionModalProps) => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [selectedSystem, setSelectedSystem] = useState<SystemDropdownOption | null>(null);
|
||||||
|
const [selectedEnterprise, setSelectedEnterprise] = useState<number | null>(null);
|
||||||
|
const { setSystem } = useSystemStore();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
[fusero_logo].forEach((image) => {
|
||||||
|
const img = new Image();
|
||||||
|
img.src = image;
|
||||||
|
});
|
||||||
|
if (systemDetails) {
|
||||||
|
setSelectedSystem(systemDetails);
|
||||||
|
setSystem(systemDetails);
|
||||||
|
}
|
||||||
|
}, [systemDetails, setSystem]);
|
||||||
|
|
||||||
|
const handleSystemChange = useCallback(
|
||||||
|
(e: { value: SystemDropdownOption }) => {
|
||||||
|
setSelectedSystem(e.value);
|
||||||
|
setSystem(e.value);
|
||||||
|
},
|
||||||
|
[setSystem]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleSelectSystem = () => {
|
||||||
|
if (selectedSystem) {
|
||||||
|
navigate(`/dashboard/${selectedSystem.urlSlug}`); // JUST GO TO BASE PATH
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEnterpriseChange = useCallback((e: { value: number }) => {
|
||||||
|
setSelectedEnterprise(e.value);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Card
|
||||||
|
style={{ border: 'none', boxShadow: 'none', overflow: 'hidden' }}
|
||||||
|
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'>
|
||||||
|
<Dropdown
|
||||||
|
id='system'
|
||||||
|
value={selectedSystem}
|
||||||
|
options={mockedSystems}
|
||||||
|
onChange={handleSystemChange}
|
||||||
|
optionLabel='businessLabel'
|
||||||
|
placeholder='Select an integration'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{selectedSystem && selectedSystem.enterprises && (
|
||||||
|
<Dropdown
|
||||||
|
id='enterprise'
|
||||||
|
value={selectedEnterprise}
|
||||||
|
options={selectedSystem.enterprises.map((enterprise) => ({
|
||||||
|
label: enterprise.name,
|
||||||
|
value: enterprise.id,
|
||||||
|
}))}
|
||||||
|
onChange={handleEnterpriseChange}
|
||||||
|
placeholder='Select an enterprise'
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
label='Select System'
|
||||||
|
onClick={handleSelectSystem}
|
||||||
|
className='w-full mt-4 text-white transition-colors bg-teal-500 border-2 border-teal-500 rounded-md hover:bg-white hover:text-teal-500'
|
||||||
|
/>
|
||||||
|
<Divider />
|
||||||
|
</Card>
|
||||||
|
{selectedSystem && (
|
||||||
|
<img
|
||||||
|
src={selectedSystem.logo }
|
||||||
|
alt={`${selectedSystem.label} Logo`}
|
||||||
|
className='w-48'
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SystemSelectionModal;
|
110
frontend/src/components/TenantProfile.tsx
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import logo_fusero from '../assets/fusero_logo.svg';
|
||||||
|
import Divider from '../shared/components/_V1/Divider';
|
||||||
|
import Button from '../shared/components/_V2/Button';
|
||||||
|
import { useAuthStore } from '../state/stores/useAuthStore';
|
||||||
|
import { TabPanel, TabView } from 'primereact/tabview';
|
||||||
|
import 'primereact/resources/themes/saga-blue/theme.css'; // Import theme for TabView
|
||||||
|
import 'primereact/resources/primereact.min.css'; // Core CSS for TabView
|
||||||
|
import ApiCalls from './ApiCalls'; // Import the new component
|
||||||
|
|
||||||
|
const Notifications = () => (
|
||||||
|
<div className='p-4'>
|
||||||
|
<h2 className='text-lg font-semibold'>Notifications</h2>
|
||||||
|
<p>You have no new notifications.</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const ProfileDetails = ({ user }) => {
|
||||||
|
if (!user) {
|
||||||
|
return (
|
||||||
|
<div className='p-4'>
|
||||||
|
<h2 className='text-lg font-semibold'>Profile Details</h2>
|
||||||
|
<p>No user information available.</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='p-4'>
|
||||||
|
<h2 className='text-lg font-semibold'>Profile Details</h2>
|
||||||
|
<p>Tenant ID: {user.id}</p>
|
||||||
|
<p>Username: {user.username}</p>
|
||||||
|
<p>Roles: {user.roles.join(', ')}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const TenantProfile = () => {
|
||||||
|
const { user, logout } = useAuthStore();
|
||||||
|
const [isVisible, setIsVisible] = useState(false);
|
||||||
|
const [activeIndex, setActiveIndex] = useState(0);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const timeout = setTimeout(() => setIsVisible(true), 50);
|
||||||
|
return () => clearTimeout(timeout);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleLogout = () => {
|
||||||
|
setIsVisible(false);
|
||||||
|
setTimeout(() => {
|
||||||
|
logout();
|
||||||
|
}, 500);
|
||||||
|
};
|
||||||
|
|
||||||
|
const username = user ? user.username : 'Guest';
|
||||||
|
const roles = user && user.roles ? user.roles.join(', ') : 'No roles assigned';
|
||||||
|
const tenantId = user ? user.id : 'No tenant ID found';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='container w-full h-full p-4 mx-auto'>
|
||||||
|
<div
|
||||||
|
className={`transition-opacity duration-500 ${isVisible ? 'opacity-100' : 'opacity-0'
|
||||||
|
} flex flex-col items-center justify-center w-full h-full p-4 relative`}
|
||||||
|
>
|
||||||
|
<div className='flex flex-col w-full h-full overflow-hidden'>
|
||||||
|
<div className='flex items-center justify-between w-full px-4 py-2 bg-white'>
|
||||||
|
<div className='flex items-center'>
|
||||||
|
<img src={logo_fusero} alt='Fusero Logo' className='w-8 h-8 mr-2' />
|
||||||
|
<h1 className='text-lg font-semibold text-teal-600'>Tenant Dashboard</h1>
|
||||||
|
</div>
|
||||||
|
<Button label='Logout' onClick={handleLogout} />
|
||||||
|
</div>
|
||||||
|
<div className='w-full'>
|
||||||
|
<TabView
|
||||||
|
activeIndex={activeIndex}
|
||||||
|
onTabChange={(e) => setActiveIndex(e.index)}
|
||||||
|
className='custom-tabview'
|
||||||
|
>
|
||||||
|
<TabPanel header='Home' headerClassName='custom-tab-header'>
|
||||||
|
<div className='flex flex-col items-center justify-center p-4'>
|
||||||
|
<img src={logo_fusero} alt='Fusero Logo' className='w-1/3 h-auto object-contain' />
|
||||||
|
<h2 className='text-lg font-semibold'>Welcome back, {username}!</h2>
|
||||||
|
{/* <p className='text-xs'>Tenant ID: {tenantId}</p>
|
||||||
|
<p className='text-xs'>{roles}</p> */}
|
||||||
|
<Divider />
|
||||||
|
<p className='text-sm text-center'>
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
|
||||||
|
incididunt ut labore et dolore magna aliqua.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</TabPanel>
|
||||||
|
<TabPanel header='Details' headerClassName='custom-tab-header'>
|
||||||
|
<ProfileDetails user={user} />
|
||||||
|
</TabPanel>
|
||||||
|
<TabPanel header='Notifications' headerClassName='custom-tab-header'>
|
||||||
|
<Notifications />
|
||||||
|
</TabPanel>
|
||||||
|
<TabPanel header='Recent Calls' headerClassName='custom-tab-header'>
|
||||||
|
<ApiCalls />
|
||||||
|
</TabPanel>
|
||||||
|
</TabView>
|
||||||
|
</div>
|
||||||
|
<div className='flex flex-col items-center justify-center flex-grow overflow-y-auto'></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TenantProfile;
|
7
frontend/src/global.types.d.ts
vendored
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
type User = {
|
||||||
|
id: string;
|
||||||
|
username?: string;
|
||||||
|
roles?: string[];
|
||||||
|
token?: string;
|
||||||
|
tenantId: string;
|
||||||
|
};
|
67
frontend/src/hooks/useAuth.ts
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { jwtDecode } from 'jwt-decode';
|
||||||
|
import { api } from '../services/api';
|
||||||
|
|
||||||
|
interface AuthHook {
|
||||||
|
user: User | null;
|
||||||
|
login: (username: string, password: string) => Promise<User>;
|
||||||
|
logout: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
type DecodedToken = {
|
||||||
|
tenantId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Token = {
|
||||||
|
user: {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
roles: string[];
|
||||||
|
exp: number;
|
||||||
|
iat: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const useAuth = (): AuthHook => {
|
||||||
|
const [user, setUser] = useState<User | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const storedUser = localStorage.getItem('user');
|
||||||
|
if (storedUser) {
|
||||||
|
setUser(JSON.parse(storedUser));
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const login = async (username: string, password: string): Promise<User> => {
|
||||||
|
const response = await api('post', '/login', { username, password });
|
||||||
|
const user = response;
|
||||||
|
|
||||||
|
const decodedToken: DecodedToken = jwtDecode(user.data.token);
|
||||||
|
user.data.tenantId = decodedToken.tenantId;
|
||||||
|
|
||||||
|
localStorage.setItem('user', JSON.stringify(user.data));
|
||||||
|
setUser(user.data.data);
|
||||||
|
return user.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
const logout = (): void => {
|
||||||
|
localStorage.removeItem('user');
|
||||||
|
localStorage.removeItem('connectionId');
|
||||||
|
setUser(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
return { user, login, logout };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getExpirationDateFromToken = (token: string) => {
|
||||||
|
const decoded: Token = jwtDecode(token);
|
||||||
|
|
||||||
|
if (decoded && decoded.exp) {
|
||||||
|
const expirationTimestamp = decoded.exp;
|
||||||
|
// Convert to miliseconds, Date constructs using miliseconds
|
||||||
|
return new Date(expirationTimestamp * 1000);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export { useAuth };
|
46
frontend/src/hooks/useFusemindMemory.ts
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { memoryService } from '../services/fusemind/memoryService';
|
||||||
|
import { MemoryUnit, MemoryRetrieval, EmotionalState } from '../types/fusemind/memory';
|
||||||
|
|
||||||
|
export const useFusemindMemory = () => {
|
||||||
|
const [activeMemories, setActiveMemories] = useState<MemoryUnit[]>([]);
|
||||||
|
const [emotionalState, setEmotionalState] = useState<EmotionalState>(
|
||||||
|
memoryService.getCurrentEmotionalState()
|
||||||
|
);
|
||||||
|
|
||||||
|
// Store a new memory
|
||||||
|
const storeMemory = async (memory: MemoryUnit) => {
|
||||||
|
const id = await memoryService.storeMemory(memory);
|
||||||
|
return id;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Retrieve memories based on query
|
||||||
|
const retrieveMemories = async (query: MemoryRetrieval) => {
|
||||||
|
const memories = await memoryService.retrieveMemory(query);
|
||||||
|
setActiveMemories(memories);
|
||||||
|
return memories;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update emotional state
|
||||||
|
const updateEmotionalState = (update: Partial<EmotionalState>) => {
|
||||||
|
memoryService.updateEmotionalState(update);
|
||||||
|
setEmotionalState(memoryService.getCurrentEmotionalState());
|
||||||
|
};
|
||||||
|
|
||||||
|
// Effect to sync with memory service
|
||||||
|
useEffect(() => {
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
setEmotionalState(memoryService.getCurrentEmotionalState());
|
||||||
|
}, 1000); // Update every second
|
||||||
|
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
activeMemories,
|
||||||
|
emotionalState,
|
||||||
|
storeMemory,
|
||||||
|
retrieveMemories,
|
||||||
|
updateEmotionalState
|
||||||
|
};
|
||||||
|
};
|
185
frontend/src/index.css
Normal file
@ -0,0 +1,185 @@
|
|||||||
|
@import 'primereact/resources/themes/saga-blue/theme.css';
|
||||||
|
@import 'primereact/resources/primereact.min.css';
|
||||||
|
@import 'primeicons/primeicons.css';
|
||||||
|
|
||||||
|
@layer tailwind-base, primereact, tailwind-utilities;
|
||||||
|
|
||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
/* li > a {
|
||||||
|
margin: 6px 0;
|
||||||
|
} */
|
||||||
|
|
||||||
|
/* src/index.css */
|
||||||
|
.custom-tabview .p-tabview-nav li {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
cursor: pointer;
|
||||||
|
/* color: #0d9488; text-teal-700 */
|
||||||
|
transition: color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-tabview .p-tabview-nav li:hover a {
|
||||||
|
color: #14b8a6; /* text-teal-500 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-tabview .p-tabview-nav li.p-highlight a {
|
||||||
|
color: #0f766e; /* text-teal-600 */
|
||||||
|
/* font-weight: 600; font-semibold */
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-tabview .p-tabview-nav li.p-highlight {
|
||||||
|
border-bottom: 2px solid #14b8a6; /* border-teal-500 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* {
|
||||||
|
overflow: auto;
|
||||||
|
-ms-overflow-style: none;
|
||||||
|
scrollbar-width: none;
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* .p-highlight {
|
||||||
|
background-color: #212f4d !important;
|
||||||
|
} */
|
||||||
|
|
||||||
|
/* ICONS */
|
||||||
|
.p-icon.p-checkbox-icon {
|
||||||
|
border: 1px solid teal;
|
||||||
|
color: #fff;
|
||||||
|
background-color: teal;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* DATA TABLE */
|
||||||
|
.p-dialog .p-dialog-header {
|
||||||
|
padding-top: 32px !important;
|
||||||
|
padding-bottom: 0px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.p-datatable.p-datatable-gridlines .p-datatable-header {
|
||||||
|
/* padding: 10px 8px 10px 8px !important; */
|
||||||
|
}
|
||||||
|
|
||||||
|
.p-datatable-header {
|
||||||
|
top: 0;
|
||||||
|
z-index: 10;
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* .p-datatable-header > div {
|
||||||
|
display: flex;
|
||||||
|
}*/
|
||||||
|
|
||||||
|
.p-datatable-header > div > div {
|
||||||
|
padding: 0 0 18px 0;
|
||||||
|
}
|
||||||
|
.p-datatable-header > div > div > div > button {
|
||||||
|
width: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.p-datatable .p-datatable-header {
|
||||||
|
position: sticky;
|
||||||
|
padding: 0.3rem 1rem !important;
|
||||||
|
width: 100%;
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.p-datatable-thead {
|
||||||
|
position: sticky;
|
||||||
|
/* top: 114px; */
|
||||||
|
top: 87.5px;
|
||||||
|
z-index: 10;
|
||||||
|
margin: 0 !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.p-sortable-column,
|
||||||
|
thead tr th {
|
||||||
|
/* padding: 12px; */
|
||||||
|
box-shadow: inset 0 -1px 0 #dee2e6;
|
||||||
|
/* width: 100%; */
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
.p-dropdownpanel {
|
||||||
|
width: 365px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.p-datatable {
|
||||||
|
width: 100%;
|
||||||
|
/* margin-top: 32px; */
|
||||||
|
}
|
||||||
|
|
||||||
|
.p-datatable-tbody {
|
||||||
|
margin-top: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.p-datatable > .p-datatable-wrapper {
|
||||||
|
overflow: initial !important;
|
||||||
|
margin: 0 0 80px 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* search bar data table */
|
||||||
|
.p-datatable-header .p-inputtext.p-component {
|
||||||
|
padding: 6px 64px;
|
||||||
|
margin-left: -8px;
|
||||||
|
border: 1px solid #222;
|
||||||
|
}
|
||||||
|
|
||||||
|
.p-paginator.p-component.p-paginator-bottom {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
box-shadow: inset 0 1px 0 #dee2e6;
|
||||||
|
width: 86%;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.pi.pi-search {
|
||||||
|
margin: -8px 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.p-inputtext.p-component {
|
||||||
|
/* padding: 6px 64px; */
|
||||||
|
/* margin-left: -8px; */
|
||||||
|
border: 1px solid #222;
|
||||||
|
}
|
||||||
|
|
||||||
|
.p-component.p-highlight {
|
||||||
|
outline: none !important;
|
||||||
|
background-color: teal;
|
||||||
|
border: 0px solid;
|
||||||
|
}
|
||||||
|
.p-button {
|
||||||
|
outline: none !important;
|
||||||
|
/* border: 0px solid; */
|
||||||
|
padding: 10px !important;
|
||||||
|
margin: 8px 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* in place element removal to make own clickable area*/
|
||||||
|
.p-inplace .p-inplace-display {
|
||||||
|
display: block !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* .p-tabview-ink-bar {
|
||||||
|
width: 0 !important;
|
||||||
|
} */
|
||||||
|
|
||||||
|
.link-style {
|
||||||
|
color: blue;
|
||||||
|
text-decoration: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link-style:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
color: blue;
|
||||||
|
}
|
117
frontend/src/layouts/DualModal.tsx
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { useParams, useNavigate, Outlet, Navigate } from 'react-router-dom';
|
||||||
|
import { useAuthStore } from '../state/stores/useAuthStore';
|
||||||
|
import SystemSelectionModal from '../components/SystemSelectionModal';
|
||||||
|
import TenantProfile from '../components/TenantProfile';
|
||||||
|
import Sidebar from './Sidebar';
|
||||||
|
import LoginModal from '../components/LoginModal';
|
||||||
|
|
||||||
|
const DualModalComponent = () => {
|
||||||
|
const { systemId } = useParams();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { isLoggedIn } = useAuthStore();
|
||||||
|
const [isExpanded, setIsExpanded] = useState(false);
|
||||||
|
const [showOutlet, setShowOutlet] = useState(false);
|
||||||
|
const [logoPath, setLogoPath] = useState('');
|
||||||
|
const [systemDetails, setSystemDetails] = useState(null);
|
||||||
|
const [animationFinished, setAnimationFinished] = useState(false);
|
||||||
|
const animationSpeed = 500;
|
||||||
|
|
||||||
|
const [redirectTo, setRedirectTo] = useState<string | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.log(`useEffect triggered: systemId=${systemId}, isLoggedIn=${isLoggedIn}`);
|
||||||
|
if (!isLoggedIn) {
|
||||||
|
setIsExpanded(false);
|
||||||
|
setShowOutlet(false);
|
||||||
|
setAnimationFinished(false); // Reset animation
|
||||||
|
} else if (systemId && systemId !== 'default') {
|
||||||
|
setIsExpanded(true);
|
||||||
|
setShowOutlet(true);
|
||||||
|
setTimeout(() => setAnimationFinished(true), animationSpeed);
|
||||||
|
} else {
|
||||||
|
setIsExpanded(false);
|
||||||
|
setShowOutlet(false);
|
||||||
|
setAnimationFinished(false); // Reset animation
|
||||||
|
}
|
||||||
|
}, [systemId, isLoggedIn]);
|
||||||
|
|
||||||
|
const handleSystemSelected = (systemData) => {
|
||||||
|
console.log(`System selected: ${systemData.urlSlug}`);
|
||||||
|
setRedirectTo(`/dashboard/${systemData.urlSlug}`);
|
||||||
|
setIsExpanded(true);
|
||||||
|
setShowOutlet(true);
|
||||||
|
setLogoPath(systemData.logo);
|
||||||
|
setSystemDetails(systemData);
|
||||||
|
setAnimationFinished(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLogoChange = (newLogoPath) => {
|
||||||
|
setLogoPath(newLogoPath);
|
||||||
|
};
|
||||||
|
|
||||||
|
const [isCollapsed, setIsCollapsed] = useState(false);
|
||||||
|
|
||||||
|
if (redirectTo) {
|
||||||
|
return <Navigate to={redirectTo} replace />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='flex items-center justify-center h-screen bg-gradient-to-br from-gray-700 to-teal-900'>
|
||||||
|
<div
|
||||||
|
className={`transition-all ease-in-out duration-500 rounded ${
|
||||||
|
isExpanded
|
||||||
|
? 'w-1/5 h-screen px-4'
|
||||||
|
: `${isLoggedIn ? 'w-1/2 h-3/4' : 'w-1/3 h-2/4'} my-12 mx-6 px-4`
|
||||||
|
} m-2 bg-white shadow-xl flex justify-center items-center px-4`}
|
||||||
|
>
|
||||||
|
{isLoggedIn ? (
|
||||||
|
showOutlet && systemId && animationFinished ? (
|
||||||
|
<Sidebar
|
||||||
|
systemId={systemId}
|
||||||
|
isCollapsed={isCollapsed}
|
||||||
|
setIsCollapsed={setIsCollapsed}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<p
|
||||||
|
className={`h-full w-full transition-opacity duration-50 ${
|
||||||
|
isExpanded ? 'opacity-0' : 'opacity-100'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<TenantProfile />
|
||||||
|
</p>
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<LoginModal />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={`relative flex-1 w-full overflow-auto transition-all ease-in-out duration-500 flex rounded ${
|
||||||
|
isExpanded ? 'w-1/3 max-w-none h-full' : 'w-1/3 h-1/2 mx-6'
|
||||||
|
} bg-white shadow-xl justify-center items-center ${isLoggedIn ? '' : 'hidden'}`}
|
||||||
|
>
|
||||||
|
{isLoggedIn && showOutlet && systemId ? (
|
||||||
|
<Outlet />
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<SystemSelectionModal
|
||||||
|
onSystemSelected={handleSystemSelected}
|
||||||
|
onLogoChange={handleLogoChange}
|
||||||
|
systemDetails={systemDetails}
|
||||||
|
/>
|
||||||
|
{logoPath && (
|
||||||
|
<img
|
||||||
|
src={logoPath}
|
||||||
|
alt='Selected System Logo'
|
||||||
|
style={{ maxHeight: '150px', padding: '0 40px 0 0', overflow: 'hidden' }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DualModalComponent;
|
102
frontend/src/layouts/Sidebar.tsx
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { Link, useLocation, useNavigate } from 'react-router-dom';
|
||||||
|
import { Button } from 'primereact/button';
|
||||||
|
import fusero_logo from '../assets/fusero_logo.svg';
|
||||||
|
|
||||||
|
import 'primeicons/primeicons.css';
|
||||||
|
|
||||||
|
const systemConfig = {
|
||||||
|
council: {
|
||||||
|
routes: [{ path: 'chat', label: 'chat', icon: 'pi pi-users' }],
|
||||||
|
logo: fusero_logo,
|
||||||
|
},
|
||||||
|
fusemind: {
|
||||||
|
routes: [
|
||||||
|
{ path: 'home', label: 'Home', icon: 'pi pi-home' },
|
||||||
|
{ path: 'chat', label: 'Chat', icon: 'pi pi-comments' },
|
||||||
|
{ path: 'agents', label: 'Agents', icon: 'pi pi-users' },
|
||||||
|
{ path: 'settings', label: 'Settings', icon: 'pi pi-cog' }
|
||||||
|
],
|
||||||
|
logo: fusero_logo,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const Sidebar = ({ systemId, isCollapsed, setIsCollapsed }) => {
|
||||||
|
const location = useLocation();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const system = systemConfig[systemId];
|
||||||
|
|
||||||
|
const [isVisible, setIsVisible] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let timeoutId = setTimeout(() => setIsVisible(true), 50);
|
||||||
|
return () => clearTimeout(timeoutId);
|
||||||
|
}, [systemId, isCollapsed]);
|
||||||
|
|
||||||
|
const handleBackClick = () => {
|
||||||
|
setIsVisible(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleCollapse = () => setIsCollapsed((prevState) => !prevState);
|
||||||
|
|
||||||
|
if (!systemId || !system) {
|
||||||
|
console.error('Sidebar: systemId is undefined or not supported.');
|
||||||
|
return <p>No system selected</p>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`transition-opacity duration-500 ${isVisible ? 'opacity-100' : 'opacity-0'} ${isCollapsed ? 'w-20' : 'w-64'
|
||||||
|
} h-full bg-white flex flex-col items-center pt-2`}
|
||||||
|
onTransitionEnd={() => {
|
||||||
|
if (!isVisible) navigate('/dashboard');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
icon={isCollapsed ? 'pi pi-arrow-circle-right' : 'pi pi-arrow-circle-left'}
|
||||||
|
onClick={toggleCollapse}
|
||||||
|
className='mt-0 p-button-rounded p-button-text'
|
||||||
|
aria-label='Toggle'
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className='flex justify-center w-full p-0 m-0'>
|
||||||
|
<Link to='/dashboard'>
|
||||||
|
<Button
|
||||||
|
label={isCollapsed ? '' : 'Back'}
|
||||||
|
icon='pi pi-arrow-left'
|
||||||
|
onClick={handleBackClick}
|
||||||
|
className='p-0 mt-1 p-button-rounded p-button-outlined'
|
||||||
|
aria-label='Back'
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={`transition-all duration-400 ${isCollapsed ? 'w-32' : 'w-3/4 p-0 m-0'}`}>
|
||||||
|
<img
|
||||||
|
src={system.logo}
|
||||||
|
alt='Logo'
|
||||||
|
className={`transition-all duration-300 ${isCollapsed ? 'w-32' : 'w-3/4 p-0 m-0'}`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr className='w-full mt-2 border-gray-700' />
|
||||||
|
|
||||||
|
<ul className='w-full mt-2 space-y-1'>
|
||||||
|
{system.routes.map(({ path, label, icon }) => (
|
||||||
|
<li key={path}>
|
||||||
|
<Link
|
||||||
|
to={`/dashboard/${systemId}/${path}`}
|
||||||
|
className={`px-6 py-2 ${location.pathname.includes(`/${path}`) ? 'active' : ''
|
||||||
|
} flex items-center justify-${isCollapsed ? 'center' : 'start'}`}
|
||||||
|
>
|
||||||
|
<i className={`mr-2 pi ${icon} ${isCollapsed ? 'text-xl' : ''}`}></i>
|
||||||
|
{isCollapsed ? '' : label}
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Sidebar;
|
14
frontend/src/main.tsx
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom/client';
|
||||||
|
import './index.css';
|
||||||
|
import Router from './router'; // Importing the router configuration
|
||||||
|
import { RouterProvider } from 'react-router-dom';
|
||||||
|
|
||||||
|
const rootElement = document.getElementById('root') as HTMLElement;
|
||||||
|
const root = ReactDOM.createRoot(rootElement);
|
||||||
|
|
||||||
|
root.render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<RouterProvider router={Router} />
|
||||||
|
</React.StrictMode>
|
||||||
|
);
|
43
frontend/src/router.tsx
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import { createBrowserRouter, Navigate } from 'react-router-dom';
|
||||||
|
import DualModalComponent from './layouts/DualModal';
|
||||||
|
|
||||||
|
import CouncilAI from './components/CouncilAI/CouncilAI';
|
||||||
|
import FuseMindHome from './components/FuseMind/FuseMindHome';
|
||||||
|
import { useParams } from 'react-router-dom';
|
||||||
|
|
||||||
|
const FuseMindHomeWrapper = () => {
|
||||||
|
const { systemId } = useParams();
|
||||||
|
return systemId === 'fusemind' ? <FuseMindHome /> : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const router = createBrowserRouter([
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
element: <Navigate to='dashboard' replace />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'dashboard',
|
||||||
|
element: <DualModalComponent />,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: ':systemId',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: 'chat',
|
||||||
|
element: <CouncilAI />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'home',
|
||||||
|
element: <FuseMindHomeWrapper />,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '*',
|
||||||
|
element: <p>Error: Page not found here?</p>,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
export default router;
|
92
frontend/src/services/api.ts
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
import axios, { AxiosResponse, ResponseType } from 'axios';
|
||||||
|
import { useApiLogStore } from './apiLogStore';
|
||||||
|
|
||||||
|
// Server path
|
||||||
|
const serverPath = import.meta.env.VITE_API_BASE_URL as string;
|
||||||
|
|
||||||
|
type THttpMethod = 'get' | 'put' | 'post' | 'delete';
|
||||||
|
|
||||||
|
const DEFAULT_TIMEOUT = 60;
|
||||||
|
|
||||||
|
export type ApiResponse<T> = {
|
||||||
|
[x: string]: any;
|
||||||
|
data: T;
|
||||||
|
status: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function api<T = any>(
|
||||||
|
method: THttpMethod,
|
||||||
|
url: string,
|
||||||
|
data?: any,
|
||||||
|
params?: any,
|
||||||
|
responseType: ResponseType = 'json',
|
||||||
|
timeoutInSeconds: number = DEFAULT_TIMEOUT,
|
||||||
|
skipAuth: boolean = false
|
||||||
|
): Promise<ApiResponse<T>> {
|
||||||
|
const user = JSON.parse(localStorage.getItem('user') || '{}');
|
||||||
|
const token = user?.token || '';
|
||||||
|
|
||||||
|
const headers: Record<string, string> = {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
};
|
||||||
|
|
||||||
|
// Only add auth headers if not skipping auth
|
||||||
|
if (!skipAuth && token) {
|
||||||
|
headers['Authorization'] = `Bearer ${token}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const axiosInstance = axios.create({
|
||||||
|
baseURL: serverPath,
|
||||||
|
timeout: timeoutInSeconds * 1000,
|
||||||
|
headers,
|
||||||
|
withCredentials: !skipAuth, // Only use credentials if not skipping auth
|
||||||
|
responseType,
|
||||||
|
});
|
||||||
|
|
||||||
|
const logApiCall = useApiLogStore.getState().logApiCall;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const requestOptions: any = {
|
||||||
|
method,
|
||||||
|
url,
|
||||||
|
data,
|
||||||
|
params,
|
||||||
|
};
|
||||||
|
|
||||||
|
const start = performance.now();
|
||||||
|
const response: AxiosResponse<T> = await axiosInstance.request(requestOptions);
|
||||||
|
const end = performance.now();
|
||||||
|
|
||||||
|
const apiResponse: ApiResponse<T> = {
|
||||||
|
data: response.data,
|
||||||
|
status: response.status,
|
||||||
|
};
|
||||||
|
|
||||||
|
logApiCall({
|
||||||
|
method,
|
||||||
|
url,
|
||||||
|
status: response.status,
|
||||||
|
responseTime: end - start,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
return apiResponse;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('API Error:', {
|
||||||
|
url,
|
||||||
|
method,
|
||||||
|
error: error.response?.data || error.message,
|
||||||
|
status: error.response?.status,
|
||||||
|
});
|
||||||
|
|
||||||
|
logApiCall({
|
||||||
|
method,
|
||||||
|
url,
|
||||||
|
status: error.response?.status || 500,
|
||||||
|
responseTime: 0,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
19
frontend/src/services/apiLogStore.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { create } from 'zustand';
|
||||||
|
|
||||||
|
type ApiLog = {
|
||||||
|
method: string;
|
||||||
|
url: string;
|
||||||
|
status: number;
|
||||||
|
responseTime: number;
|
||||||
|
timestamp: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ApiLogStore = {
|
||||||
|
apiLogs: ApiLog[];
|
||||||
|
logApiCall: (log: ApiLog) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useApiLogStore = create<ApiLogStore>((set) => ({
|
||||||
|
apiLogs: [],
|
||||||
|
logApiCall: (log) => set((state) => ({ apiLogs: [...state.apiLogs, log] })),
|
||||||
|
}));
|
76
frontend/src/services/fusemind/chatService.ts
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
import { Message, ChatConfig, ChatResponse } from '../../types/fusemind/messages';
|
||||||
|
|
||||||
|
class ChatService {
|
||||||
|
private config: ChatConfig = {
|
||||||
|
model: 'gpt-4',
|
||||||
|
temperature: 0.7,
|
||||||
|
maxTokens: 2000
|
||||||
|
};
|
||||||
|
|
||||||
|
private context: Message[] = [];
|
||||||
|
|
||||||
|
async sendMessage(message: Message): Promise<ChatResponse> {
|
||||||
|
try {
|
||||||
|
// Add message to context
|
||||||
|
this.context.push(message);
|
||||||
|
|
||||||
|
// Prepare the messages for ChatGPT
|
||||||
|
const messages = this.context.map(msg => ({
|
||||||
|
role: msg.type === 'user' ? 'user' : 'assistant',
|
||||||
|
content: msg.content
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Make API call to ChatGPT
|
||||||
|
const response = await fetch('/api/chat', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
messages,
|
||||||
|
...this.config
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to get response from ChatGPT');
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
// Create response message
|
||||||
|
const responseMessage: Message = {
|
||||||
|
id: Date.now().toString(),
|
||||||
|
content: data.message,
|
||||||
|
type: 'system',
|
||||||
|
timestamp: new Date()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add response to context
|
||||||
|
this.context.push(responseMessage);
|
||||||
|
|
||||||
|
// Return formatted response
|
||||||
|
return {
|
||||||
|
message: responseMessage,
|
||||||
|
usage: data.usage
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in ChatService:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setConfig(config: Partial<ChatConfig>) {
|
||||||
|
this.config = { ...this.config, ...config };
|
||||||
|
}
|
||||||
|
|
||||||
|
clearContext() {
|
||||||
|
this.context = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
getContext(): Message[] {
|
||||||
|
return [...this.context];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const chatService = new ChatService();
|
439
frontend/src/services/fusemind/memoryService.ts
Normal file
@ -0,0 +1,439 @@
|
|||||||
|
import {
|
||||||
|
MemoryUnit,
|
||||||
|
MemoryType,
|
||||||
|
WorkingMemory,
|
||||||
|
MemoryRetrieval,
|
||||||
|
MemoryConsolidation,
|
||||||
|
DatabaseMemory,
|
||||||
|
EmotionalState,
|
||||||
|
LLMWorkingMemory,
|
||||||
|
LLMContextMemory,
|
||||||
|
EmbeddingMemory,
|
||||||
|
PromptMemory,
|
||||||
|
TemporalProperties,
|
||||||
|
MemoryFormationContext,
|
||||||
|
EmotionalProperties
|
||||||
|
} from '../../types/fusemind/memory';
|
||||||
|
|
||||||
|
class MemoryService {
|
||||||
|
private workingMemory: LLMWorkingMemory;
|
||||||
|
private emotionalState: EmotionalState;
|
||||||
|
private activeLifeEvents: Map<string, TemporalProperties['lifeEvents'][0]>;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.workingMemory = {
|
||||||
|
activeUnits: [],
|
||||||
|
focus: {
|
||||||
|
primary: '',
|
||||||
|
secondary: [],
|
||||||
|
attentionWeights: {}
|
||||||
|
},
|
||||||
|
context: {
|
||||||
|
currentTask: '',
|
||||||
|
relevantMemories: [],
|
||||||
|
emotionalState: {
|
||||||
|
mood: 0,
|
||||||
|
energy: 1,
|
||||||
|
stress: 0,
|
||||||
|
focus: 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
llmContext: {
|
||||||
|
currentModel: 'gpt-4',
|
||||||
|
contextWindow: {
|
||||||
|
used: 0,
|
||||||
|
available: 8192
|
||||||
|
},
|
||||||
|
recentPrompts: [],
|
||||||
|
temperature: 0.7,
|
||||||
|
systemPrompt: 'You are a helpful AI assistant.'
|
||||||
|
},
|
||||||
|
embeddings: {
|
||||||
|
model: 'text-embedding-ada-002',
|
||||||
|
cache: new Map(),
|
||||||
|
similarityThreshold: 0.8
|
||||||
|
}
|
||||||
|
};
|
||||||
|
this.emotionalState = this.workingMemory.context.emotionalState;
|
||||||
|
this.activeLifeEvents = new Map();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Memory Operations
|
||||||
|
async storeMemory(memory: MemoryUnit, formationContext?: MemoryFormationContext): Promise<string> {
|
||||||
|
// Add life event context if available
|
||||||
|
if (formationContext?.temporalContext.lifeEvent) {
|
||||||
|
const event = this.activeLifeEvents.get(formationContext.temporalContext.lifeEvent);
|
||||||
|
if (event) {
|
||||||
|
memory.temporal.lifeEvents.push(event);
|
||||||
|
memory.temporal.temporalContext = formationContext.temporalContext;
|
||||||
|
|
||||||
|
// Apply emotional biases
|
||||||
|
memory.emotional.triggers.push(...formationContext.emotionalInfluence.biasEffects.map(bias => ({
|
||||||
|
type: 'contextual',
|
||||||
|
source: 'life_event',
|
||||||
|
intensity: bias.strength,
|
||||||
|
associatedEvent: formationContext.temporalContext.lifeEvent,
|
||||||
|
biasEffect: {
|
||||||
|
type: bias.type as 'amplification' | 'distortion' | 'suppression',
|
||||||
|
strength: bias.strength,
|
||||||
|
duration: event.emotionalImpact.persistence
|
||||||
|
}
|
||||||
|
})));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to database format
|
||||||
|
const dbMemory: DatabaseMemory = {
|
||||||
|
id: memory.id,
|
||||||
|
type: memory.type,
|
||||||
|
content: JSON.stringify(memory.content),
|
||||||
|
temporal: JSON.stringify(memory.temporal),
|
||||||
|
emotional: JSON.stringify(memory.emotional),
|
||||||
|
connections: JSON.stringify(memory.connections),
|
||||||
|
created_at: new Date(),
|
||||||
|
updated_at: new Date()
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: Implement database storage
|
||||||
|
// await db.memories.insert(dbMemory);
|
||||||
|
|
||||||
|
// Update working memory if relevant
|
||||||
|
this.updateWorkingMemory(memory);
|
||||||
|
|
||||||
|
return memory.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
async retrieveMemory(query: MemoryRetrieval): Promise<MemoryUnit[]> {
|
||||||
|
const memories = await this.retrieveMemoryFromStorage(query);
|
||||||
|
|
||||||
|
// Apply emotional and temporal context filters
|
||||||
|
return memories.filter(memory => {
|
||||||
|
// Check if memory is relevant to current life events
|
||||||
|
const isRelevantToCurrentEvents = memory.temporal.lifeEvents.some(event =>
|
||||||
|
this.activeLifeEvents.has(event.eventId)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Check emotional triggers
|
||||||
|
const hasActiveTriggers = memory.emotional.triggers.some(trigger =>
|
||||||
|
trigger.associatedEvent && this.activeLifeEvents.has(trigger.associatedEvent)
|
||||||
|
);
|
||||||
|
|
||||||
|
return isRelevantToCurrentEvents || hasActiveTriggers;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async consolidateMemories(consolidation: MemoryConsolidation): Promise<MemoryUnit> {
|
||||||
|
const memories = await Promise.all(
|
||||||
|
consolidation.sourceMemories.map(id => this.retrieveMemory({ query: id }))
|
||||||
|
).then(results => results.flat());
|
||||||
|
|
||||||
|
// Calculate emotional influence
|
||||||
|
const emotionalInfluence = this.calculateEmotionalInfluence(memories);
|
||||||
|
|
||||||
|
// Create new memory with emotional context
|
||||||
|
const newMemory: MemoryUnit = {
|
||||||
|
id: this.generateId(),
|
||||||
|
type: 'semantic',
|
||||||
|
content: this.mergeMemoryContents(memories),
|
||||||
|
temporal: {
|
||||||
|
...this.calculateTemporalProperties(memories),
|
||||||
|
lifeEvents: memories.flatMap(m => m.temporal.lifeEvents),
|
||||||
|
temporalContext: this.getCurrentTemporalContext()
|
||||||
|
},
|
||||||
|
emotional: {
|
||||||
|
...this.calculateEmotionalProperties(memories),
|
||||||
|
triggers: emotionalInfluence.triggers,
|
||||||
|
emotionalState: {
|
||||||
|
baseline: emotionalInfluence.baseline,
|
||||||
|
current: this.emotionalState.mood,
|
||||||
|
volatility: emotionalInfluence.volatility
|
||||||
|
}
|
||||||
|
},
|
||||||
|
connections: this.buildMemoryConnections(memories)
|
||||||
|
};
|
||||||
|
|
||||||
|
await this.storeMemory(newMemory);
|
||||||
|
return newMemory;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Working Memory Management
|
||||||
|
private updateWorkingMemory(memory: MemoryUnit) {
|
||||||
|
// Add to active units if not already present
|
||||||
|
if (!this.workingMemory.activeUnits.find(u => u.id === memory.id)) {
|
||||||
|
this.workingMemory.activeUnits.push(memory);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update focus based on memory importance
|
||||||
|
this.updateFocus(memory);
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateFocus(memory: MemoryUnit) {
|
||||||
|
const importance = memory.temporal.importance;
|
||||||
|
|
||||||
|
if (importance > 0.8) {
|
||||||
|
// High importance memories become primary focus
|
||||||
|
this.workingMemory.focus.primary = memory.id;
|
||||||
|
} else if (importance > 0.5) {
|
||||||
|
// Medium importance memories become secondary focus
|
||||||
|
if (!this.workingMemory.focus.secondary.includes(memory.id)) {
|
||||||
|
this.workingMemory.focus.secondary.push(memory.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update attention weights
|
||||||
|
this.workingMemory.focus.attentionWeights[memory.id] = importance;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper Methods
|
||||||
|
private matchesQuery(memory: MemoryUnit, query: MemoryRetrieval): boolean {
|
||||||
|
// Implement query matching logic
|
||||||
|
return true; // Placeholder
|
||||||
|
}
|
||||||
|
|
||||||
|
private generateId(): string {
|
||||||
|
return Math.random().toString(36).substring(2, 15);
|
||||||
|
}
|
||||||
|
|
||||||
|
private mergeMemoryContents(memories: MemoryUnit[]): any {
|
||||||
|
// Implement memory content merging logic
|
||||||
|
return {}; // Placeholder
|
||||||
|
}
|
||||||
|
|
||||||
|
private calculateTemporalProperties(memories: MemoryUnit[]): any {
|
||||||
|
// Implement temporal properties calculation
|
||||||
|
return {}; // Placeholder
|
||||||
|
}
|
||||||
|
|
||||||
|
private calculateEmotionalProperties(memories: MemoryUnit[]): any {
|
||||||
|
// Implement emotional properties calculation
|
||||||
|
return {}; // Placeholder
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildMemoryConnections(memories: MemoryUnit[]): any[] {
|
||||||
|
// Implement memory connections building
|
||||||
|
return []; // Placeholder
|
||||||
|
}
|
||||||
|
|
||||||
|
// Emotional State Management
|
||||||
|
updateEmotionalState(update: Partial<EmotionalState>) {
|
||||||
|
this.emotionalState = {
|
||||||
|
...this.emotionalState,
|
||||||
|
...update
|
||||||
|
};
|
||||||
|
this.workingMemory.context.emotionalState = this.emotionalState;
|
||||||
|
}
|
||||||
|
|
||||||
|
getCurrentEmotionalState(): EmotionalState {
|
||||||
|
return this.emotionalState;
|
||||||
|
}
|
||||||
|
|
||||||
|
// LLM-specific operations
|
||||||
|
async storeLLMContext(context: LLMContextMemory): Promise<string> {
|
||||||
|
// Update working memory context
|
||||||
|
this.workingMemory.llmContext = {
|
||||||
|
...this.workingMemory.llmContext,
|
||||||
|
currentModel: context.content.data.model,
|
||||||
|
contextWindow: {
|
||||||
|
used: context.content.data.tokens.total,
|
||||||
|
available: context.content.data.contextWindow
|
||||||
|
},
|
||||||
|
temperature: context.content.data.parameters.temperature
|
||||||
|
};
|
||||||
|
|
||||||
|
return this.storeMemory(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
async storeEmbedding(embedding: EmbeddingMemory): Promise<string> {
|
||||||
|
// Cache the embedding
|
||||||
|
this.workingMemory.embeddings.cache.set(
|
||||||
|
embedding.content.data.text,
|
||||||
|
embedding.content.data.embedding
|
||||||
|
);
|
||||||
|
|
||||||
|
return this.storeMemory(embedding);
|
||||||
|
}
|
||||||
|
|
||||||
|
async storePrompt(prompt: PromptMemory): Promise<string> {
|
||||||
|
// Update recent prompts
|
||||||
|
this.workingMemory.llmContext.recentPrompts.push(
|
||||||
|
prompt.content.data.template
|
||||||
|
);
|
||||||
|
|
||||||
|
// Keep only last 10 prompts
|
||||||
|
if (this.workingMemory.llmContext.recentPrompts.length > 10) {
|
||||||
|
this.workingMemory.llmContext.recentPrompts.shift();
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.storeMemory(prompt);
|
||||||
|
}
|
||||||
|
|
||||||
|
async retrieveSimilarMemories(query: string, threshold: number = 0.8): Promise<MemoryUnit[]> {
|
||||||
|
// Get embedding for query
|
||||||
|
const queryEmbedding = await this.getEmbedding(query);
|
||||||
|
|
||||||
|
// Find similar memories
|
||||||
|
const similarMemories: MemoryUnit[] = [];
|
||||||
|
|
||||||
|
for (const [text, embedding] of this.workingMemory.embeddings.cache) {
|
||||||
|
const similarity = this.calculateSimilarity(queryEmbedding, embedding);
|
||||||
|
if (similarity >= threshold) {
|
||||||
|
const memories = await this.retrieveMemory({
|
||||||
|
query: text,
|
||||||
|
context: {},
|
||||||
|
filters: {}
|
||||||
|
});
|
||||||
|
similarMemories.push(...memories);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return similarMemories;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper methods for LLM operations
|
||||||
|
private async getEmbedding(text: string): Promise<number[]> {
|
||||||
|
// Check cache first
|
||||||
|
if (this.workingMemory.embeddings.cache.has(text)) {
|
||||||
|
return this.workingMemory.embeddings.cache.get(text)!;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Implement actual embedding generation
|
||||||
|
// const embedding = await openai.embeddings.create({
|
||||||
|
// model: this.workingMemory.embeddings.model,
|
||||||
|
// input: text
|
||||||
|
// });
|
||||||
|
|
||||||
|
// For now, return placeholder
|
||||||
|
return new Array(1536).fill(0).map(() => Math.random());
|
||||||
|
}
|
||||||
|
|
||||||
|
private calculateSimilarity(embedding1: number[], embedding2: number[]): number {
|
||||||
|
// Implement cosine similarity
|
||||||
|
const dotProduct = embedding1.reduce((sum, val, i) => sum + val * embedding2[i], 0);
|
||||||
|
const norm1 = Math.sqrt(embedding1.reduce((sum, val) => sum + val * val, 0));
|
||||||
|
const norm2 = Math.sqrt(embedding2.reduce((sum, val) => sum + val * val, 0));
|
||||||
|
|
||||||
|
return dotProduct / (norm1 * norm2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enhanced memory consolidation for LLM context
|
||||||
|
async consolidateLLMMemories(
|
||||||
|
sourceMemories: string[],
|
||||||
|
llmContext: Partial<LLMContextMemory['content']>
|
||||||
|
): Promise<MemoryUnit> {
|
||||||
|
const memories = await Promise.all(
|
||||||
|
sourceMemories.map(id => this.retrieveMemory({ query: id }))
|
||||||
|
).then(results => results.flat());
|
||||||
|
|
||||||
|
// Create new consolidated memory with LLM context
|
||||||
|
const newMemory: MemoryUnit = {
|
||||||
|
id: this.generateId(),
|
||||||
|
type: 'llm_context',
|
||||||
|
content: {
|
||||||
|
...this.mergeMemoryContents(memories),
|
||||||
|
...llmContext
|
||||||
|
},
|
||||||
|
temporal: this.calculateTemporalProperties(memories),
|
||||||
|
emotional: this.calculateEmotionalProperties(memories),
|
||||||
|
connections: this.buildMemoryConnections(memories)
|
||||||
|
};
|
||||||
|
|
||||||
|
await this.storeMemory(newMemory);
|
||||||
|
return newMemory;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Life event management
|
||||||
|
async registerLifeEvent(event: TemporalProperties['lifeEvents'][0]) {
|
||||||
|
this.activeLifeEvents.set(event.eventId, event);
|
||||||
|
|
||||||
|
// Update emotional state based on event
|
||||||
|
this.updateEmotionalState({
|
||||||
|
mood: event.emotionalImpact.valence,
|
||||||
|
energy: event.emotionalImpact.arousal,
|
||||||
|
stress: 1 - event.emotionalImpact.persistence,
|
||||||
|
focus: 0.5 // Reduced focus during significant events
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async endLifeEvent(eventId: string) {
|
||||||
|
const event = this.activeLifeEvents.get(eventId);
|
||||||
|
if (event) {
|
||||||
|
event.endDate = new Date();
|
||||||
|
this.activeLifeEvents.delete(eventId);
|
||||||
|
|
||||||
|
// Gradually return to baseline emotional state
|
||||||
|
this.updateEmotionalState({
|
||||||
|
mood: 0, // Neutral
|
||||||
|
energy: 1,
|
||||||
|
stress: 0,
|
||||||
|
focus: 1
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper methods
|
||||||
|
private calculateEmotionalInfluence(memories: MemoryUnit[]): {
|
||||||
|
triggers: EmotionalProperties['triggers'];
|
||||||
|
baseline: number;
|
||||||
|
volatility: number;
|
||||||
|
} {
|
||||||
|
const triggers: EmotionalProperties['triggers'] = [];
|
||||||
|
let totalValence = 0;
|
||||||
|
let totalArousal = 0;
|
||||||
|
let count = 0;
|
||||||
|
|
||||||
|
memories.forEach(memory => {
|
||||||
|
// Collect triggers
|
||||||
|
triggers.push(...memory.emotional.triggers);
|
||||||
|
|
||||||
|
// Calculate average emotional state
|
||||||
|
totalValence += memory.emotional.valence;
|
||||||
|
totalArousal += memory.emotional.arousal;
|
||||||
|
count++;
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
triggers,
|
||||||
|
baseline: count > 0 ? totalValence / count : 0,
|
||||||
|
volatility: count > 0 ? totalArousal / count : 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private getCurrentTemporalContext(): TemporalProperties['temporalContext'] {
|
||||||
|
const activeEvents = Array.from(this.activeLifeEvents.values());
|
||||||
|
return {
|
||||||
|
period: activeEvents.length > 0 ?
|
||||||
|
activeEvents.map(e => e.type).join('-') :
|
||||||
|
'normal',
|
||||||
|
moodState: this.getMoodState(),
|
||||||
|
cognitiveBias: this.getActiveBiases()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private getMoodState(): string {
|
||||||
|
const mood = this.emotionalState.mood;
|
||||||
|
if (mood > 0.7) return 'euphoric';
|
||||||
|
if (mood > 0.3) return 'positive';
|
||||||
|
if (mood < -0.7) return 'depressed';
|
||||||
|
if (mood < -0.3) return 'negative';
|
||||||
|
return 'neutral';
|
||||||
|
}
|
||||||
|
|
||||||
|
private getActiveBiases(): string[] {
|
||||||
|
const biases: string[] = [];
|
||||||
|
const mood = this.emotionalState.mood;
|
||||||
|
|
||||||
|
if (mood > 0.5) {
|
||||||
|
biases.push('optimism', 'overestimation');
|
||||||
|
} else if (mood < -0.5) {
|
||||||
|
biases.push('pessimism', 'catastrophizing');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.emotionalState.stress > 0.7) {
|
||||||
|
biases.push('anxiety', 'hypervigilance');
|
||||||
|
}
|
||||||
|
|
||||||
|
return biases;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const memoryService = new MemoryService();
|
13
frontend/src/shared/components/_V1/BorderSpinner.tsx
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { ProgressSpinner } from 'primereact/progressspinner';
|
||||||
|
const BorderSpinner = () => {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center">
|
||||||
|
<div className="">
|
||||||
|
<ProgressSpinner />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BorderSpinner;
|
10
frontend/src/shared/components/_V1/Divider.tsx
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { DividerProps, Divider as PrimeDivider } from "primereact/divider";
|
||||||
|
import { FunctionComponent } from "react";
|
||||||
|
|
||||||
|
const Divider: FunctionComponent<DividerProps> = ({ children }) => {
|
||||||
|
return (
|
||||||
|
<PrimeDivider>{children}</PrimeDivider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Divider;
|
16
frontend/src/shared/components/_V1/LoadingPage.tsx
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import BorderSpinner from './BorderSpinner';
|
||||||
|
|
||||||
|
const LoadingPage = ({ height = 200, fullScreen = true }) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`flex items-center justify-center ${fullScreen ? 'h-screen' : ''}`}
|
||||||
|
style={!fullScreen ? { height: height } : undefined}
|
||||||
|
>
|
||||||
|
<div className=''>
|
||||||
|
<BorderSpinner />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LoadingPage;
|
28
frontend/src/shared/components/_V1/TableGroup.tsx
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { FunctionComponent } from 'react';
|
||||||
|
import { DataTable, DataTableProps } from 'primereact/datatable';
|
||||||
|
import LoadingPage from './LoadingPage';
|
||||||
|
|
||||||
|
type CustomTableGroupProps = DataTableProps<any> & {
|
||||||
|
body: any[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const TableGroup: FunctionComponent<CustomTableGroupProps> = ({ body, ...dataTableProps }) => {
|
||||||
|
return dataTableProps.loading ? (
|
||||||
|
<LoadingPage />
|
||||||
|
) : (
|
||||||
|
<DataTable
|
||||||
|
value={body}
|
||||||
|
tableStyle={{ minWidth: '50rem', width: '100%' }}
|
||||||
|
{...(dataTableProps.paginator && {
|
||||||
|
paginator: true,
|
||||||
|
paginatorTemplate:
|
||||||
|
'FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink CurrentPageReport RowsPerPageDropdown',
|
||||||
|
rowsPerPageOptions: [10, 25, 50],
|
||||||
|
currentPageReportTemplate: 'Showing {first} to {last} of {totalRecords} entries',
|
||||||
|
})}
|
||||||
|
{...dataTableProps}
|
||||||
|
></DataTable>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TableGroup;
|
85
frontend/src/shared/components/_V1/TableGroupHeader.tsx
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
import { InputText } from 'primereact/inputtext';
|
||||||
|
import { SelectButton } from 'primereact/selectbutton';
|
||||||
|
import { Dispatch, SetStateAction } from 'react';
|
||||||
|
|
||||||
|
import Button from '../_V2/Button';
|
||||||
|
import { TSizeOptionValue } from '../../../types/DataTable.types';
|
||||||
|
|
||||||
|
interface HeaderProps {
|
||||||
|
size: any;
|
||||||
|
setSize: Dispatch<SetStateAction<TSizeOptionValue>>;
|
||||||
|
sizeOptions: { label: string; value: TSizeOptionValue }[];
|
||||||
|
globalFilterValue: string;
|
||||||
|
onGlobalFilterChange: (e: { target: { value: any } }) => void;
|
||||||
|
onRefresh: () => void;
|
||||||
|
clearFilter: () => void;
|
||||||
|
hasExpandButtons?: boolean;
|
||||||
|
setExpandedRows?: Dispatch<SetStateAction<any>>;
|
||||||
|
expandedData?: any[];
|
||||||
|
extraButtonsTemplate?: () => JSX.Element;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TableGroupHeader: React.FC<HeaderProps> = ({
|
||||||
|
size,
|
||||||
|
setSize,
|
||||||
|
sizeOptions,
|
||||||
|
globalFilterValue,
|
||||||
|
onGlobalFilterChange,
|
||||||
|
onRefresh,
|
||||||
|
clearFilter,
|
||||||
|
hasExpandButtons,
|
||||||
|
setExpandedRows,
|
||||||
|
expandedData,
|
||||||
|
extraButtonsTemplate,
|
||||||
|
}) => {
|
||||||
|
if (hasExpandButtons && (!setExpandedRows || !expandedData)) {
|
||||||
|
throw new Error(
|
||||||
|
'When hasExpandButtons is true, setExpandedRows and expandedData are required.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`flex items-center justify-between ${hasExpandButtons ? 'gap-4' : ''}`}>
|
||||||
|
<SelectButton
|
||||||
|
value={size}
|
||||||
|
onChange={(e) => setSize(e.value)}
|
||||||
|
options={sizeOptions}
|
||||||
|
style={{ textAlign: 'center' }}
|
||||||
|
/>
|
||||||
|
{hasExpandButtons ? (
|
||||||
|
<div className='flex flex-wrap gap-2 justify-content-end'>
|
||||||
|
<Button
|
||||||
|
icon='pi pi-plus'
|
||||||
|
label='Expand All'
|
||||||
|
onClick={() => {
|
||||||
|
// Set the expandedRows state with the new object
|
||||||
|
const elements = [];
|
||||||
|
expandedData.forEach((element) => {
|
||||||
|
elements.push(element);
|
||||||
|
});
|
||||||
|
setExpandedRows(elements);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Button icon='pi pi-minus' label='Collapse All' onClick={() => setExpandedRows(null)} />
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
<span className='w-full mx-auto p-input-icon-left md:w-auto'>
|
||||||
|
<i className='pi pi-search' />
|
||||||
|
<InputText
|
||||||
|
value={globalFilterValue}
|
||||||
|
onChange={onGlobalFilterChange}
|
||||||
|
placeholder='Search everything'
|
||||||
|
className='w-full'
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<div className='flex gap-4'>
|
||||||
|
<div className='flex items-center gap-2 '>
|
||||||
|
<Button type='button' label='Refresh' outlined onClick={onRefresh} />
|
||||||
|
<Button type='button' label='Clear Filters' outlined onClick={clearFilter} />
|
||||||
|
{extraButtonsTemplate ? extraButtonsTemplate() : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default TableGroupHeader;
|
21
frontend/src/shared/components/_V2/Button.tsx
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { Button as PrimeButton, ButtonProps } from 'primereact/button';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
interface CustomButtonProps extends ButtonProps {}
|
||||||
|
|
||||||
|
const Button: React.FC<CustomButtonProps> = ({ style, className, disabled, ...buttonProps }) => {
|
||||||
|
const disabledClass = disabled
|
||||||
|
? 'bg-gray-500 hover:bg-gray-500 text-white'
|
||||||
|
: 'bg-teal-500 hover:bg-white hover:text-teal-500';
|
||||||
|
return (
|
||||||
|
<PrimeButton
|
||||||
|
{...buttonProps}
|
||||||
|
unstyled
|
||||||
|
disabled={disabled}
|
||||||
|
className={` py-2 px-4 min-w-1 mt-4 text-white border-2 border-teal-500 rounded-md transition-colors ${disabledClass} ${className} `}
|
||||||
|
style={{ ...style, fontSize: '0.75rem', fontWeight: '700' }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Button;
|
60
frontend/src/shared/components/_V2/CustomAvatar.tsx
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
|
import { Avatar } from 'primereact/avatar';
|
||||||
|
|
||||||
|
const CustomAvatar = () => {
|
||||||
|
const [image, setImage] = useState<string | ArrayBuffer | null>(null);
|
||||||
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
const localStorageKey = 'userAvatarImage';
|
||||||
|
|
||||||
|
const handleAvatarClick = () => {
|
||||||
|
fileInputRef.current?.click();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const file = event.target.files?.[0];
|
||||||
|
if (file) {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onloadend = () => {
|
||||||
|
const result = reader.result;
|
||||||
|
setImage(result);
|
||||||
|
if (typeof result === 'string') {
|
||||||
|
localStorage.setItem(localStorageKey, result);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const savedImage = localStorage.getItem(localStorageKey);
|
||||||
|
if (savedImage) {
|
||||||
|
setImage(savedImage);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
ref={fileInputRef}
|
||||||
|
type='file'
|
||||||
|
accept='image/*'
|
||||||
|
style={{ display: 'none' }}
|
||||||
|
onChange={handleFileChange}
|
||||||
|
/>
|
||||||
|
<Avatar
|
||||||
|
className='p-overlay-badge'
|
||||||
|
icon={!image ? 'pi pi-user' : undefined}
|
||||||
|
image={typeof image === 'string' ? image : undefined}
|
||||||
|
size='xlarge'
|
||||||
|
onClick={handleAvatarClick}
|
||||||
|
style={{
|
||||||
|
cursor: 'pointer',
|
||||||
|
boxShadow: '0px 4px 12px rgba(0, 0, 0, 0.2)',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CustomAvatar;
|
16
frontend/src/shared/components/_V2/Dropdown.tsx
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { Dropdown as PrimeDropdown, DropdownProps } from 'primereact/dropdown';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
interface CustomDropdownProps extends DropdownProps {}
|
||||||
|
|
||||||
|
const Dropdown: React.FC<CustomDropdownProps> = ({ style, className, ...dropdownProps }) => {
|
||||||
|
return (
|
||||||
|
<PrimeDropdown
|
||||||
|
{...dropdownProps}
|
||||||
|
className={`w-full px-6 py-1 mb-4 border-2 border-gray-300 hover:border-teal-600 focus:ring-teal-500' rounded-md transition-colors hover:bg-white hover:text-teal-500 ${className}`}
|
||||||
|
style={{ ...style, fontWeight: '400' }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Dropdown;
|
16
frontend/src/shared/components/_V2/InputText.tsx
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { InputText as PrimeInputText, InputTextProps } from 'primereact/inputtext';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
interface CustomInputTextProps extends InputTextProps {}
|
||||||
|
|
||||||
|
const InputText: React.FC<CustomInputTextProps> = ({ style, className, ...inputProps }) => {
|
||||||
|
return (
|
||||||
|
<PrimeInputText
|
||||||
|
{...inputProps}
|
||||||
|
className={`w-full p-inputtext p-2 bg-white border-2 border-gray-400 rounded-md transition-colors focus:border-teal-700 ${className}`}
|
||||||
|
style={{ ...style, fontWeight: '400' }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default InputText;
|
60
frontend/src/shared/components/modals/ConfirmModal.tsx
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import { Button } from 'primereact/button';
|
||||||
|
import { Dialog } from 'primereact/dialog';
|
||||||
|
import { CSSProperties } from 'react';
|
||||||
|
|
||||||
|
const ConfirmModal = ({
|
||||||
|
visible,
|
||||||
|
header,
|
||||||
|
message,
|
||||||
|
onHide,
|
||||||
|
onConfirm,
|
||||||
|
disableConfirm,
|
||||||
|
disableCancel,
|
||||||
|
confirmLabel,
|
||||||
|
cancelLabel,
|
||||||
|
onCancel = onHide,
|
||||||
|
style,
|
||||||
|
}: {
|
||||||
|
visible: boolean;
|
||||||
|
header: string;
|
||||||
|
message: string | JSX.Element;
|
||||||
|
onHide: () => void;
|
||||||
|
onConfirm: () => void;
|
||||||
|
disableConfirm?: boolean;
|
||||||
|
disableCancel?: boolean;
|
||||||
|
confirmLabel: string;
|
||||||
|
cancelLabel: string;
|
||||||
|
onCancel: () => void;
|
||||||
|
style?: CSSProperties;
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
closable
|
||||||
|
visible={visible}
|
||||||
|
onHide={onHide}
|
||||||
|
header={header}
|
||||||
|
style={{ width: '25vw', ...style }}
|
||||||
|
footer={
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
label={confirmLabel}
|
||||||
|
style={{ minWidth: '4rem' }}
|
||||||
|
onClick={onConfirm}
|
||||||
|
disabled={disableConfirm}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
label={cancelLabel}
|
||||||
|
onClick={onCancel}
|
||||||
|
style={{ minWidth: '4rem' }}
|
||||||
|
severity="danger"
|
||||||
|
disabled={disableCancel}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="mt-2">{message}</div>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ConfirmModal;
|
78
frontend/src/shared/components/modals/DeleteModal.tsx
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { Button } from 'primereact/button';
|
||||||
|
import { Dialog } from 'primereact/dialog';
|
||||||
|
import { InputTextarea } from 'primereact/inputtextarea';
|
||||||
|
|
||||||
|
interface DeleteModalProps {
|
||||||
|
visible: boolean;
|
||||||
|
onHide: () => void;
|
||||||
|
onConfirm: () => void;
|
||||||
|
confirmLabel: string;
|
||||||
|
cancelLabel: string;
|
||||||
|
onCancel: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DeleteModal: React.FC<DeleteModalProps> = ({
|
||||||
|
visible,
|
||||||
|
onHide,
|
||||||
|
onConfirm,
|
||||||
|
confirmLabel,
|
||||||
|
cancelLabel,
|
||||||
|
onCancel,
|
||||||
|
}) => {
|
||||||
|
const [inputValue, setInputValue] = useState('');
|
||||||
|
const deleteKeyword = 'DELETE';
|
||||||
|
|
||||||
|
const isDeleteMatch = inputValue === deleteKeyword;
|
||||||
|
|
||||||
|
const onCloseModal = () => {
|
||||||
|
setInputValue('');
|
||||||
|
onHide();
|
||||||
|
}
|
||||||
|
|
||||||
|
const onCancelModal = () => {
|
||||||
|
setInputValue('');
|
||||||
|
onCancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
const onConfirmModal = () => {
|
||||||
|
setInputValue('');
|
||||||
|
onConfirm();
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
closable
|
||||||
|
visible={visible}
|
||||||
|
onHide={onCloseModal}
|
||||||
|
header={`Type 'DELETE' to confirm`}
|
||||||
|
footer={
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
label={confirmLabel}
|
||||||
|
style={{ width: '5rem' }}
|
||||||
|
onClick={onConfirmModal}
|
||||||
|
disabled={!isDeleteMatch}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
label={cancelLabel}
|
||||||
|
onClick={onCancelModal}
|
||||||
|
style={{ width: '5rem' }}
|
||||||
|
severity="danger"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<InputTextarea
|
||||||
|
style={{ marginTop: '30px' }}
|
||||||
|
placeholder='DELETE'
|
||||||
|
value={inputValue}
|
||||||
|
onChange={(e) => setInputValue(e.target.value)}
|
||||||
|
rows={5}
|
||||||
|
cols={50}
|
||||||
|
/>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DeleteModal;
|
63
frontend/src/state/stores/useAuthStore.ts
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import { create } from 'zustand';
|
||||||
|
import { api } from '../../services/api';
|
||||||
|
|
||||||
|
type User = {
|
||||||
|
id: number;
|
||||||
|
username: string;
|
||||||
|
roles: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type AuthStore = {
|
||||||
|
user: User | null;
|
||||||
|
token: string | null;
|
||||||
|
isLoggedIn: boolean;
|
||||||
|
login: (username: string, password: string) => Promise<void>;
|
||||||
|
logout: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useAuthStore = create<AuthStore>((set) => ({
|
||||||
|
user: JSON.parse(localStorage.getItem('user') || 'null'),
|
||||||
|
token: localStorage.getItem('token'),
|
||||||
|
isLoggedIn: !!localStorage.getItem('token'),
|
||||||
|
|
||||||
|
login: async (username: string, password: string) => {
|
||||||
|
try {
|
||||||
|
const response = await api('post', '/api/v1/app/users/login', { username, password });
|
||||||
|
const { success, data } = response.data;
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
const { token, user } = data;
|
||||||
|
localStorage.setItem('user', JSON.stringify(user));
|
||||||
|
localStorage.setItem('token', token);
|
||||||
|
|
||||||
|
set({ user, token, isLoggedIn: true });
|
||||||
|
} else {
|
||||||
|
throw new Error('Login failed');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to login:', error);
|
||||||
|
throw new Error('Login failed');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
logout: () => {
|
||||||
|
localStorage.removeItem('user');
|
||||||
|
localStorage.removeItem('token');
|
||||||
|
set({ user: null, token: null, isLoggedIn: false });
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
function parseJwt(token) {
|
||||||
|
const base64Url = token.split('.')[1];
|
||||||
|
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
|
||||||
|
const jsonPayload = decodeURIComponent(
|
||||||
|
atob(base64)
|
||||||
|
.split('')
|
||||||
|
.map(function (c) {
|
||||||
|
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
|
||||||
|
})
|
||||||
|
.join('')
|
||||||
|
);
|
||||||
|
|
||||||
|
return JSON.parse(jsonPayload);
|
||||||
|
}
|
41
frontend/src/state/stores/useChatStore.ts
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import { create } from 'zustand';
|
||||||
|
import { api } from '../../services/api';
|
||||||
|
|
||||||
|
interface ChatRequestData {
|
||||||
|
data: string;
|
||||||
|
responseFormat?: 'text' | 'json';
|
||||||
|
model?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type ChatStore = {
|
||||||
|
sendChatRequest: (endpoint: string, data: ChatRequestData) => Promise<any>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useChatStore = create<ChatStore>(() => ({
|
||||||
|
sendChatRequest: async (endpoint, requestData) => {
|
||||||
|
try {
|
||||||
|
if (
|
||||||
|
!requestData.data ||
|
||||||
|
typeof requestData.data !== 'string' ||
|
||||||
|
requestData.data.trim() === ''
|
||||||
|
) {
|
||||||
|
throw new Error("Invalid input: 'data' must be a non-empty string.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const formattedRequest = {
|
||||||
|
data: requestData.data,
|
||||||
|
// model: requestData.model || 'gpt-4o-mini',
|
||||||
|
model: requestData.model || 'o4-mini',
|
||||||
|
response_format: requestData.responseFormat === 'json' ? { type: 'json_object' } : undefined
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('Sending formatted request:', formattedRequest);
|
||||||
|
const response = await api('post', `/council${endpoint}`, formattedRequest, undefined, 'json', 60, true);
|
||||||
|
console.log('Got API response:', response);
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to send chat request to ${endpoint}:`, error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}));
|
30
frontend/src/state/stores/useSystemStore.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
// Adjusting the store to handle SystemDropdownOption properly
|
||||||
|
import { create } from 'zustand';
|
||||||
|
import { Enterprise } from '../../components/SystemSelectionModal';
|
||||||
|
|
||||||
|
export type System = {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
logo: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SystemDropdownOption = {
|
||||||
|
id: number;
|
||||||
|
label: string;
|
||||||
|
businessLabel: string;
|
||||||
|
urlSlug: string;
|
||||||
|
logo: string;
|
||||||
|
enterprises?: Enterprise[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type SystemStore = {
|
||||||
|
currentSystem: SystemDropdownOption | null;
|
||||||
|
setSystem: (system: SystemDropdownOption) => void;
|
||||||
|
clearSystem: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useSystemStore = create<SystemStore>((set) => ({
|
||||||
|
currentSystem: null,
|
||||||
|
setSystem: (system: SystemDropdownOption) => set({ currentSystem: system }),
|
||||||
|
clearSystem: () => set({ currentSystem: null }),
|
||||||
|
}));
|
1
frontend/src/types/DataTable.types.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export type TSizeOptionValue = 'small' | 'normal' | 'large';
|
13
frontend/src/types/fusemind/avatar.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
export type AvatarPosition = 'left' | 'center' | 'right';
|
||||||
|
|
||||||
|
export interface AvatarState {
|
||||||
|
position: AvatarPosition;
|
||||||
|
isVisible: boolean;
|
||||||
|
isAnimating: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AvatarConfig {
|
||||||
|
size: number;
|
||||||
|
color: string;
|
||||||
|
animationDuration: number;
|
||||||
|
}
|
371
frontend/src/types/fusemind/memory.ts
Normal file
@ -0,0 +1,371 @@
|
|||||||
|
// Core memory interfaces
|
||||||
|
export interface MemoryUnit {
|
||||||
|
id: string;
|
||||||
|
type: MemoryType;
|
||||||
|
content: MemoryContent;
|
||||||
|
temporal: TemporalProperties;
|
||||||
|
emotional: EmotionalProperties;
|
||||||
|
connections: MemoryConnection[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type MemoryType = 'episodic' | 'semantic' | 'procedural' | 'sensory' | 'llm_context' | 'embedding' | 'prompt' | 'life_event';
|
||||||
|
|
||||||
|
export interface MemoryContent {
|
||||||
|
data: any;
|
||||||
|
metadata: Record<string, any>;
|
||||||
|
confidence: number; // 0-1 scale
|
||||||
|
source: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TemporalProperties {
|
||||||
|
created: Date;
|
||||||
|
modified: Date;
|
||||||
|
lastAccessed: Date;
|
||||||
|
decayRate: number; // How quickly the memory fades (0-1)
|
||||||
|
importance: number; // 0-1 scale
|
||||||
|
lifeEvents: {
|
||||||
|
eventId: string;
|
||||||
|
type: 'personal' | 'professional' | 'social' | 'health';
|
||||||
|
startDate: Date;
|
||||||
|
endDate?: Date;
|
||||||
|
intensity: number; // 0-1 scale
|
||||||
|
emotionalImpact: {
|
||||||
|
valence: number; // -1 to 1
|
||||||
|
arousal: number; // 0-1
|
||||||
|
persistence: number; // How long the impact lasts
|
||||||
|
};
|
||||||
|
relatedMemories: string[]; // IDs of memories formed during this period
|
||||||
|
}[];
|
||||||
|
temporalContext: {
|
||||||
|
period: string; // e.g., "post-breakup", "during-pandemic"
|
||||||
|
moodState: string; // e.g., "depressed", "euphoric"
|
||||||
|
cognitiveBias: string[]; // e.g., "overestimation", "pessimism"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EmotionalProperties {
|
||||||
|
valence: number; // -1 to 1 (negative to positive)
|
||||||
|
arousal: number; // 0-1 scale
|
||||||
|
emotionalTags: string[];
|
||||||
|
triggers: {
|
||||||
|
type: 'memory' | 'sensory' | 'contextual';
|
||||||
|
source: string;
|
||||||
|
intensity: number;
|
||||||
|
associatedEvent?: string; // ID of related life event
|
||||||
|
biasEffect: {
|
||||||
|
type: 'amplification' | 'distortion' | 'suppression';
|
||||||
|
strength: number;
|
||||||
|
duration: number; // How long the bias lasts
|
||||||
|
};
|
||||||
|
}[];
|
||||||
|
emotionalState: {
|
||||||
|
baseline: number; // Normal emotional state
|
||||||
|
current: number; // Current emotional state
|
||||||
|
volatility: number; // How easily emotions change
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MemoryConnection {
|
||||||
|
targetId: string;
|
||||||
|
type: ConnectionType;
|
||||||
|
strength: number; // 0-1 scale
|
||||||
|
context: string;
|
||||||
|
emotionalContext: {
|
||||||
|
formedDuring: string; // Life event ID
|
||||||
|
emotionalBias: number; // How much the connection is influenced by emotions
|
||||||
|
temporalRelevance: number; // How relevant the connection is to the current period
|
||||||
|
};
|
||||||
|
triggers: {
|
||||||
|
type: string;
|
||||||
|
intensity: number;
|
||||||
|
associatedEvent?: string;
|
||||||
|
}[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ConnectionType = 'association' | 'causation' | 'similarity' | 'contrast';
|
||||||
|
|
||||||
|
// Specialized memory types
|
||||||
|
export interface EpisodicMemory extends MemoryUnit {
|
||||||
|
type: 'episodic';
|
||||||
|
content: {
|
||||||
|
data: {
|
||||||
|
event: {
|
||||||
|
type: 'conversation' | 'interaction' | 'system_event';
|
||||||
|
participants: string[];
|
||||||
|
location?: string;
|
||||||
|
duration?: number;
|
||||||
|
};
|
||||||
|
narrative: string;
|
||||||
|
keyPoints: string[];
|
||||||
|
};
|
||||||
|
metadata: {
|
||||||
|
context: Record<string, any>;
|
||||||
|
emotionalSignificance: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SemanticMemory extends MemoryUnit {
|
||||||
|
type: 'semantic';
|
||||||
|
content: {
|
||||||
|
data: {
|
||||||
|
concept: string;
|
||||||
|
definition: string;
|
||||||
|
examples: string[];
|
||||||
|
relationships: {
|
||||||
|
type: string;
|
||||||
|
targetId: string;
|
||||||
|
description: string;
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
metadata: {
|
||||||
|
category: string;
|
||||||
|
reliability: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProceduralMemory extends MemoryUnit {
|
||||||
|
type: 'procedural';
|
||||||
|
content: {
|
||||||
|
data: {
|
||||||
|
skill: string;
|
||||||
|
steps: string[];
|
||||||
|
conditions: string[];
|
||||||
|
exceptions: string[];
|
||||||
|
};
|
||||||
|
metadata: {
|
||||||
|
proficiency: number;
|
||||||
|
lastPracticed: Date;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Working memory (active processing)
|
||||||
|
export interface WorkingMemory {
|
||||||
|
activeUnits: MemoryUnit[];
|
||||||
|
focus: {
|
||||||
|
primary: string;
|
||||||
|
secondary: string[];
|
||||||
|
attentionWeights: Record<string, number>;
|
||||||
|
};
|
||||||
|
context: {
|
||||||
|
currentTask: string;
|
||||||
|
relevantMemories: string[];
|
||||||
|
emotionalState: EmotionalState;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EmotionalState {
|
||||||
|
mood: number; // -1 to 1
|
||||||
|
energy: number; // 0-1
|
||||||
|
stress: number; // 0-1
|
||||||
|
focus: number; // 0-1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Memory processing interfaces
|
||||||
|
export interface MemoryRetrieval {
|
||||||
|
query: string;
|
||||||
|
context: Record<string, any>;
|
||||||
|
filters: {
|
||||||
|
type?: MemoryType;
|
||||||
|
timeRange?: [Date, Date];
|
||||||
|
emotionalThreshold?: number;
|
||||||
|
confidenceThreshold?: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MemoryConsolidation {
|
||||||
|
sourceMemories: string[];
|
||||||
|
newMemory: Partial<MemoryUnit>;
|
||||||
|
consolidationStrength: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Database schema mapping
|
||||||
|
export interface DatabaseMemory {
|
||||||
|
id: string;
|
||||||
|
type: MemoryType;
|
||||||
|
content: string; // JSON stringified MemoryContent
|
||||||
|
temporal: string; // JSON stringified TemporalProperties
|
||||||
|
emotional: string; // JSON stringified EmotionalProperties
|
||||||
|
connections: string; // JSON stringified MemoryConnection[]
|
||||||
|
created_at: Date;
|
||||||
|
updated_at: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
// LLM-specific memory types
|
||||||
|
export interface LLMContextMemory extends MemoryUnit {
|
||||||
|
type: 'llm_context';
|
||||||
|
content: {
|
||||||
|
data: {
|
||||||
|
model: string;
|
||||||
|
contextWindow: number;
|
||||||
|
tokens: {
|
||||||
|
prompt: string;
|
||||||
|
completion: string;
|
||||||
|
total: number;
|
||||||
|
};
|
||||||
|
parameters: {
|
||||||
|
temperature: number;
|
||||||
|
top_p: number;
|
||||||
|
frequency_penalty: number;
|
||||||
|
presence_penalty: number;
|
||||||
|
};
|
||||||
|
conversation: {
|
||||||
|
role: 'system' | 'user' | 'assistant';
|
||||||
|
content: string;
|
||||||
|
timestamp: Date;
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
metadata: {
|
||||||
|
modelVersion: string;
|
||||||
|
contextType: 'conversation' | 'instruction' | 'retrieval';
|
||||||
|
performance: {
|
||||||
|
latency: number;
|
||||||
|
tokensPerSecond: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EmbeddingMemory extends MemoryUnit {
|
||||||
|
type: 'embedding';
|
||||||
|
content: {
|
||||||
|
data: {
|
||||||
|
text: string;
|
||||||
|
embedding: number[];
|
||||||
|
dimensions: number;
|
||||||
|
model: string;
|
||||||
|
};
|
||||||
|
metadata: {
|
||||||
|
similarityThreshold: number;
|
||||||
|
retrievalMethod: 'cosine' | 'euclidean' | 'dot';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PromptMemory extends MemoryUnit {
|
||||||
|
type: 'prompt';
|
||||||
|
content: {
|
||||||
|
data: {
|
||||||
|
template: string;
|
||||||
|
variables: Record<string, any>;
|
||||||
|
examples: {
|
||||||
|
input: any;
|
||||||
|
output: any;
|
||||||
|
}[];
|
||||||
|
constraints: string[];
|
||||||
|
};
|
||||||
|
metadata: {
|
||||||
|
category: string;
|
||||||
|
successRate: number;
|
||||||
|
averageTokens: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// LLM-specific connection types
|
||||||
|
export type LLMConnectionType = ConnectionType |
|
||||||
|
'token_similarity' |
|
||||||
|
'embedding_distance' |
|
||||||
|
'prompt_chain' |
|
||||||
|
'context_flow';
|
||||||
|
|
||||||
|
// Enhanced working memory for LLM context
|
||||||
|
export interface LLMWorkingMemory extends WorkingMemory {
|
||||||
|
llmContext: {
|
||||||
|
currentModel: string;
|
||||||
|
contextWindow: {
|
||||||
|
used: number;
|
||||||
|
available: number;
|
||||||
|
};
|
||||||
|
recentPrompts: string[];
|
||||||
|
temperature: number;
|
||||||
|
systemPrompt: string;
|
||||||
|
};
|
||||||
|
embeddings: {
|
||||||
|
model: string;
|
||||||
|
cache: Map<string, number[]>;
|
||||||
|
similarityThreshold: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// LLM-specific retrieval interface
|
||||||
|
export interface LLMMemoryRetrieval extends MemoryRetrieval {
|
||||||
|
llmFilters: {
|
||||||
|
model?: string;
|
||||||
|
contextWindow?: number;
|
||||||
|
temperature?: number;
|
||||||
|
embeddingSimilarity?: number;
|
||||||
|
promptTemplate?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Life event memory type
|
||||||
|
export interface LifeEventMemory extends MemoryUnit {
|
||||||
|
type: 'life_event';
|
||||||
|
content: {
|
||||||
|
data: {
|
||||||
|
event: {
|
||||||
|
type: 'breakup' | 'loss' | 'achievement' | 'trauma' | 'transition';
|
||||||
|
description: string;
|
||||||
|
participants: string[];
|
||||||
|
location?: string;
|
||||||
|
duration: {
|
||||||
|
start: Date;
|
||||||
|
end?: Date;
|
||||||
|
phases?: {
|
||||||
|
phase: string;
|
||||||
|
start: Date;
|
||||||
|
end?: Date;
|
||||||
|
characteristics: string[];
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
impact: {
|
||||||
|
emotional: {
|
||||||
|
immediate: string[];
|
||||||
|
longTerm: string[];
|
||||||
|
triggers: string[];
|
||||||
|
};
|
||||||
|
cognitive: {
|
||||||
|
biases: string[];
|
||||||
|
thoughtPatterns: string[];
|
||||||
|
copingMechanisms: string[];
|
||||||
|
};
|
||||||
|
behavioral: {
|
||||||
|
changes: string[];
|
||||||
|
adaptations: string[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
metadata: {
|
||||||
|
recoveryStage?: string;
|
||||||
|
healingProgress: number;
|
||||||
|
relatedEvents: string[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Memory formation context
|
||||||
|
export interface MemoryFormationContext {
|
||||||
|
temporalContext: {
|
||||||
|
period: string;
|
||||||
|
lifeEvent?: string;
|
||||||
|
emotionalState: string;
|
||||||
|
};
|
||||||
|
cognitiveState: {
|
||||||
|
biases: string[];
|
||||||
|
attention: number;
|
||||||
|
clarity: number;
|
||||||
|
};
|
||||||
|
emotionalInfluence: {
|
||||||
|
currentMood: number;
|
||||||
|
emotionalTriggers: string[];
|
||||||
|
biasEffects: {
|
||||||
|
type: string;
|
||||||
|
strength: number;
|
||||||
|
direction: 'positive' | 'negative';
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
}
|
23
frontend/src/types/fusemind/messages.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
export type MessageType = 'user' | 'system';
|
||||||
|
|
||||||
|
export interface Message {
|
||||||
|
id: string;
|
||||||
|
content: string;
|
||||||
|
type: MessageType;
|
||||||
|
timestamp: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChatConfig {
|
||||||
|
model: string;
|
||||||
|
temperature: number;
|
||||||
|
maxTokens: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChatResponse {
|
||||||
|
message: Message;
|
||||||
|
usage?: {
|
||||||
|
promptTokens: number;
|
||||||
|
completionTokens: number;
|
||||||
|
totalTokens: number;
|
||||||
|
};
|
||||||
|
}
|
19
frontend/src/types/systems.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
export enum Systems {
|
||||||
|
piggy = 'piggy',
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TSystem = {
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
name: string;
|
||||||
|
id: number;
|
||||||
|
label: string;
|
||||||
|
src: string;
|
||||||
|
logoSrc?: string;
|
||||||
|
// connections: any[];
|
||||||
|
nextPage: string;
|
||||||
|
hasNextPage: boolean;
|
||||||
|
previousPage: string;
|
||||||
|
hasPreviousPage: boolean;
|
||||||
|
lastPage: number;
|
||||||
|
};
|
1
frontend/src/vite-env.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
/// <reference types="vite/client" />
|
11
frontend/tailwind.config.js
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
export default {
|
||||||
|
content: [
|
||||||
|
"./index.html",
|
||||||
|
"./src/**/*.{js,jsx,ts,tsx}",
|
||||||
|
],
|
||||||
|
theme: {
|
||||||
|
extend: {},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
}
|
37
frontend/tsconfig.json
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ESNext",
|
||||||
|
"lib": ["DOM", "DOM.Iterable", "ESNext", "ES2015"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": false,
|
||||||
|
"noUnusedLocals": false,
|
||||||
|
"noUnusedParameters": false,
|
||||||
|
"noFallthroughCasesInSwitch": false,
|
||||||
|
"noImplicitAny": false,
|
||||||
|
"noImplicitReturns": false,
|
||||||
|
"noImplicitThis": false,
|
||||||
|
"alwaysStrict": false
|
||||||
|
|
||||||
|
/* Additional Options */
|
||||||
|
// "forceConsistentCasingInFileNames": true,
|
||||||
|
// "strictNullChecks": true,
|
||||||
|
// "noImplicitOverride": true,
|
||||||
|
// "noPropertyAccessFromIndexSignature": true,
|
||||||
|
// "useUnknownInCatchVariables": true,
|
||||||
|
// "incremental": true,
|
||||||
|
// "esModuleInterop": true
|
||||||
|
},
|
||||||
|
"include": ["src"],
|
||||||
|
"references": [{ "path": "./tsconfig.node.json" }]
|
||||||
|
}
|
10
frontend/tsconfig.node.json
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowSyntheticDefaultImports": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
26
frontend/vite.config.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import react from '@vitejs/plugin-react-swc';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
base: '/',
|
||||||
|
server: {
|
||||||
|
port: 8080,
|
||||||
|
},
|
||||||
|
build: {
|
||||||
|
rollupOptions: {
|
||||||
|
output: {
|
||||||
|
manualChunks(id) {
|
||||||
|
if (id.includes('node_modules')) {
|
||||||
|
return id.toString().split('node_modules/')[1].split('/')[0].toString();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': '/src',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
14841
package-lock.json
generated
Normal file
@ -79,7 +79,14 @@ const app: FastifyPluginAsync = async (app, opts): Promise<void> => {
|
|||||||
const orm = await MikroORM.init({ ...mikroOrmConfig });
|
const orm = await MikroORM.init({ ...mikroOrmConfig });
|
||||||
|
|
||||||
// Register CORS, JWT, and Cookies
|
// Register CORS, JWT, and Cookies
|
||||||
app.register(fastifyCors, { origin: '*', methods: ['GET', 'POST', 'PUT', 'DELETE'] });
|
app.register(fastifyCors, {
|
||||||
|
origin: ['http://localhost:3000', 'http://localhost:3001', 'http://localhost:8080', 'http://localhost:8081'],
|
||||||
|
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
|
||||||
|
allowedHeaders: ['Content-Type', 'Authorization', 'Accept'],
|
||||||
|
credentials: true,
|
||||||
|
maxAge: 86400
|
||||||
|
});
|
||||||
|
|
||||||
app.register(fjwt, { secret: process.env.JWT_SECRET || 'your-secret-here' });
|
app.register(fjwt, { secret: process.env.JWT_SECRET || 'your-secret-here' });
|
||||||
app.register(fCookie);
|
app.register(fCookie);
|
||||||
|
|
||||||
|