first push frontend
3
.gitignore
vendored
@ -3,8 +3,7 @@ node_modules/
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
|
||||
|
||||
# Build output
|
||||
dist/
|
||||
|
@ -1,3 +1,5 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
fusero-boilerplate-db:
|
||||
image: postgres:15
|
||||
|
@ -1,19 +1,39 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
fusero-frontend:
|
||||
frontend:
|
||||
container_name: fusero-frontend
|
||||
env_file: ../fusero-frontend/.env
|
||||
build:
|
||||
context: ../fusero-frontend
|
||||
dockerfile: Dockerfile.dev
|
||||
context: ./frontend
|
||||
dockerfile: Dockerfile
|
||||
ports:
|
||||
- '3000:80'
|
||||
networks:
|
||||
- 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:
|
||||
environment:
|
||||
- POSTGRES_HOST=fusero-app-db
|
||||
- POSTGRES_HOST=fusero-boilerplate-db
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
@ -22,68 +42,41 @@ services:
|
||||
ports:
|
||||
- '5000:14000'
|
||||
depends_on:
|
||||
- fusero-app-db
|
||||
- fusero-boilerplate-db
|
||||
container_name: fusero-app-boilerplate
|
||||
networks:
|
||||
- fusero-network
|
||||
|
||||
fusero-app-db:
|
||||
fusero-boilerplate-db:
|
||||
image: postgres:15
|
||||
env_file: .env
|
||||
restart: always
|
||||
volumes:
|
||||
- fusero_app_pgdata:/var/lib/postgresql/data
|
||||
- fusero_boilerplate_pgdata:/var/lib/postgresql/data
|
||||
ports:
|
||||
- '19090:5432'
|
||||
container_name: fusero-app-db
|
||||
- '19095:5432'
|
||||
container_name: fusero-boilerplate-db
|
||||
networks:
|
||||
- fusero-network
|
||||
|
||||
fusero-app-test-db:
|
||||
fusero-boilerplate-test-db:
|
||||
image: postgres:15
|
||||
env_file: .env
|
||||
restart: always
|
||||
volumes:
|
||||
- fusero_app_test_pgdata:/var/lib/postgresql/data
|
||||
- fusero_boilerplate_test_pgdata:/var/lib/postgresql/data
|
||||
ports:
|
||||
- '19091:5432'
|
||||
container_name: fusero-app-test-db
|
||||
- '19096:5432'
|
||||
container_name: fusero-boilerplate-test-db
|
||||
networks:
|
||||
- fusero-network
|
||||
environment:
|
||||
- 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:
|
||||
redis_data:
|
||||
fusero_app_pgdata:
|
||||
fusero_boilerplate_pgdata:
|
||||
external: true
|
||||
fusero_app_test_pgdata:
|
||||
fusero_boilerplate_test_pgdata:
|
||||
external: false
|
||||
|
||||
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 });
|
||||
|
||||
// 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(fCookie);
|
||||
|
||||
|