first push frontend

This commit is contained in:
DarrenT~ 2025-04-30 17:34:49 +02:00
parent 48a04969bc
commit b72331288a
96 changed files with 30132 additions and 46 deletions

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

File diff suppressed because it is too large Load Diff

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"
}
}

@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 759 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

@ -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"}

Binary file not shown.

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 266 KiB

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 482 KiB

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

@ -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;

@ -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;

@ -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;

@ -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;

@ -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;

@ -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>
);
};

@ -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>
);
}

@ -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;

@ -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;

@ -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;

@ -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;

@ -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;

@ -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

@ -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;

@ -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;

@ -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;

@ -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;

@ -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;

@ -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`;

@ -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>
);
};

@ -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
};
};

@ -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[][];
};

@ -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 };

@ -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;
}
});
}
}));

@ -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;

@ -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;

@ -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;

@ -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;
};

@ -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 };

@ -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;
}

@ -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;

@ -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;

@ -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;
}
}

@ -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] })),
}));

@ -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();

@ -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();

@ -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;

@ -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;

@ -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;

@ -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;

@ -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;

@ -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;

@ -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;

@ -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;

@ -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;

@ -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;

@ -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;

@ -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);
}

@ -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;
}
},
}));

@ -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 }),
}));

@ -0,0 +1 @@
export type TSizeOptionValue = 'small' | 'normal' | 'large';

@ -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;
}

@ -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';
}[];
};
}

@ -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;
};
}

@ -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" />

@ -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" }]
}

@ -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

File diff suppressed because it is too large Load Diff

@ -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);