Compare commits

...

7 Commits

150 changed files with 34573 additions and 428 deletions

@ -11,6 +11,13 @@ JWT_SECRET=sdfj94mfm430f72m3487rdsjiy7834n9rnf934n8r3n490fn4u83fh894hr9nf0
# SERVER_BASEPATH_API=v1/
# TIMEZONE=Europe/Amsterdam
# Default Admin User
DEFAULT_ADMIN_USERNAME=admin
DEFAULT_ADMIN_EMAIL=darren@fusero.nl
DEFAULT_ADMIN_PASSWORD=admin123
FASTIFY_PORT=14000
# [ Database ]

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/

503
README.md

@ -1,346 +1,211 @@
# Fusero Boilerplate App
# Fusero App Boilerplate
This is a Fastify + MikroORM + PostgreSQL boilerplate, designed to run in Docker for local development.
It is set up to run alongside other projects without port or database conflicts.
A full-stack application boilerplate with React frontend and Node.js backend.
---
## Project Structure
## 1. Prerequisites
- [Node.js](https://nodejs.org/) (v18+ recommended)
- [Docker](https://www.docker.com/get-started)
- [npm](https://www.npmjs.com/)
---
## 2. Clone the Repo
```bash
git clone <your-repo-url>
cd fusero-app-boilerplate
```
fusero-app-boilerplate/
├── frontend/ # React frontend application
├── backend/ # Node.js backend application
├── docker-compose.yml # Production Docker configuration
└── docker-compose.dev.yml # Development Docker configuration
```
---
## Prerequisites
## 3. Setup the `.env` File
- Node.js (v18 or higher)
- npm (v9 or higher)
- Docker and Docker Compose
- Git
Copy `.env.example` to `.env` (or create `.env` if not present):
## Development Setup
```env
# Database connection for Docker
POSTGRES_NAME=fusero-boilerplate-db
POSTGRES_HOSTNAME=localhost
POSTGRES_PORT=19095
POSTGRES_USER=root
POSTGRES_PASSWORD=root123
### Important Note: Database Must Run in Docker
The PostgreSQL database must always run in Docker, regardless of your development setup choice. This ensures consistent database behavior across all environments.
# Test Database connection
POSTGRES_TEST_NAME=test-db
POSTGRES_TEST_PORT=19096
# Default admin user for seeding
DEFAULT_ADMIN_USERNAME=darren
DEFAULT_ADMIN_EMAIL=darren@fusero.nl
DEFAULT_ADMIN_PASSWORD=admin123
# JWT secret
JWT_SECRET=your_jwt_secret_here
To start the database:
```bash
docker-compose -f docker-compose.dev.yml up db
```
---
### Option 1: Running Everything in Docker (Recommended for Development)
## 4. Start the Database (Docker)
```bash
docker-compose -f docker-compose.dev.yml up -d
```
This will start two Postgres instances:
- Main database on port 19095
- Test database on port 19096
Both with dedicated Docker volumes.
---
## 5. Install Dependencies
1. **Start the Development Environment**
```bash
docker-compose -f docker-compose.dev.yml up
```
This will start:
- Frontend on http://localhost:3000
- Backend on http://localhost:14000
- PostgreSQL database on port 19090
### Option 2: Running Services Separately (Recommended for Debugging)
For better debugging experience, you can run the frontend and backend in separate terminal windows, while keeping the database in Docker:
1. **First, ensure the database is running in Docker**
```bash
docker-compose -f docker-compose.dev.yml up db
```
2. **Then, in separate terminal windows:**
#### Terminal 1: Backend Service
```bash
cd backend
npm install
```
---
## 6. Run Migrations
```bash
npm run migration:create # Create a new migration
npm run migration:up # Run migrations
```
This will create all tables in the database.
---
## 7. Seed the Database
```bash
npm run seed
```
This will create the default admin user and roles as specified in your `.env`.
---
## 8. Start the App
```bash
npm run dev
```
The backend will be available at http://localhost:14000
The app will be available at [http://localhost:14000](http://localhost:14000).
---
## 9. API Endpoints
### Authentication
#### Login
#### Terminal 2: Frontend Service
```bash
curl -X POST http://localhost:14000/api/v1/auth/login \
-H "Content-Type: application/json" \
-d '{
"username": "darren",
"password": "admin123"
}'
cd frontend
npm install
npm run dev
```
The frontend will be available at http://localhost:3000
### Environment Setup
1. **Backend Environment**
- Copy `.env.example` to `.env` in the backend directory
- Configure your environment variables:
```
PORT=14000
DB_HOST=localhost
DB_PORT=19090
DB_USER=postgres
DB_PASSWORD=postgres
DB_NAME=fusero
JWT_SECRET=your_jwt_secret_key_here
```
2. **Frontend Environment**
- Copy `.env.example` to `.env` in the frontend directory
- Set the API base URL:
```
VITE_API_BASE_URL=http://localhost:14000/api/v1
```
## Production Deployment
1. **Build and Run with Docker**
```bash
docker-compose up --build
```
2. **Environment Variables**
- Ensure all environment variables are properly set in your production environment
- Never commit `.env` files to version control
## Development Best Practices
1. **Database Management**
- Always run the database in Docker
- Use `docker-compose.dev.yml` for development
- Never run PostgreSQL directly on your host machine
2. **Running Services Separately**
- For development, it's recommended to run frontend and backend in separate terminal windows
- This allows for better debugging and hot-reloading
- You can see logs from each service clearly
3. **Code Organization**
- Frontend code should be in the `frontend/` directory
- Backend code should be in the `backend/` directory
- Shared types and utilities should be in their respective directories
4. **Version Control**
- Commit `package-lock.json` files
- Don't commit `.env` files
- Use meaningful commit messages
## API Documentation
The backend API is documented using Swagger/OpenAPI. After starting the backend service, you can access the API documentation at:
- Development: http://localhost:14000/api-docs
- Production: http://your-domain/api-docs
## Troubleshooting
1. **Port Conflicts**
- If you encounter port conflicts, check which services are running:
```bash
docker ps
```
- Or check for processes using the ports:
```bash
lsof -i :3000
lsof -i :14000
```
2. **Database Issues**
- Ensure PostgreSQL is running in Docker
- Check database connection settings in `.env`
- Verify database migrations are up to date
- If database issues persist, try:
```bash
docker-compose -f docker-compose.dev.yml down
docker-compose -f docker-compose.dev.yml up db
```
3. **CORS Issues**
- If you see CORS errors, verify the frontend's API base URL
- Check backend CORS configuration
- Ensure both services are running on the correct ports
## Contributing
1. Create a new branch for your feature
2. Make your changes
3. Submit a pull request
4. Ensure all tests pass
5. Update documentation as needed
## License
This project is licensed under the MIT License - see the LICENSE file for details.
## Technical Documentation: ChatGPT-Powered Endpoint Creation
### Overview
Developers can leverage the ChatGPT modal in the Canvas Endpoints UI to create new Canvas API endpoints using natural language prompts. When a user enters a prompt like "Create a course endpoint for Canvas", the system uses ChatGPT to:
1. Interpret the intent and generate a JSON object with the required fields for the endpoint (name, method, path, description, etc.).
2. Automatically submit this JSON to the backend endpoint creation API (`/api/v1/canvas-api/endpoints`).
3. Refresh the endpoint list in the UI and display a success message.
### How it Works
- **Prompt Handling:**
- The frontend sends the user's prompt to `/api/v1/canvas-api/chatgpt/completions`.
- ChatGPT is instructed to return only a JSON object suitable for the endpoint creation form.
- **Auto-Creation:**
- If the response is a valid endpoint JSON (with `name`, `method`, and `path`), the frontend posts it to `/api/v1/canvas-api/endpoints`.
- The endpoint list is refreshed and a toast notification is shown.
- **Fallback:**
- If the response is not a valid endpoint JSON, it is displayed as a normal chat message.
### Example Prompt
```
Create a course endpoint for Canvas. Use the Canvas API docs to determine the correct path and required fields.
```
Response:
```json
### Example ChatGPT Response
```
{
"success": true,
"message": "Authentication successful",
"data": {
"token": "your.jwt.token",
"user": {
"id": 1,
"username": "darren",
"email": "darren@fusero.nl",
"roles": ["admin"]
}
}
"name": "Create Course",
"method": "POST",
"path": "/courses",
"description": "Creates a new course in Canvas."
}
```
### User Management
#### Create User
```bash
curl -X POST http://localhost:14000/api/v1/app/users \
-H "Content-Type: application/json" \
-H "Authorization: Bearer your.jwt.token" \
-d '{
"username": "newuser",
"password": "userpass123",
"email": "user@example.com",
"roleName": "user"
}'
```
Response:
```json
{
"success": true,
"message": "User created successfully",
"data": {
"id": 2,
"username": "newuser",
"email": "user@example.com",
"roles": ["user"]
}
}
```
#### Get All Users (Requires admin role)
```bash
curl -X GET http://localhost:14000/api/v1/app/users \
-H "Authorization: Bearer your.jwt.token"
```
#### Get User by ID
```bash
curl -X GET http://localhost:14000/api/v1/app/users/1 \
-H "Authorization: Bearer your.jwt.token"
```
### Canvas Dummy Grades API
The canvas API provides endpoints for managing dummy grades. All endpoints are prefixed with `/api/v1/canvas-api/`.
#### Get All Dummy Grades
```bash
GET /api/v1/canvas-api/
```
Response:
```json
{
"success": true,
"message": "Dummy grades retrieved successfully",
"data": {
"grades": [
{
"id": 1,
"student_id": 101,
"course_id": 1,
"assignment_id": 1,
"score": 85,
"grade": "B",
"submitted_at": "2024-03-15T10:00:00Z",
"graded_at": "2024-03-16T14:30:00Z"
}
]
}
}
```
#### Add a New Dummy Grade
```bash
POST /api/v1/canvas-api/add
Content-Type: application/json
{
"student_id": 123,
"course_id": 456,
"assignment_id": 789,
"score": 85
}
```
Response:
```json
{
"success": true,
"message": "Dummy grade added successfully",
"data": {
"id": 3,
"student_id": 123,
"course_id": 456,
"assignment_id": 789,
"score": 85,
"grade": "B",
"submitted_at": "2024-03-15T10:00:00Z",
"graded_at": "2024-03-16T14:30:00Z"
}
}
```
#### Update a Dummy Grade
```bash
PUT /api/v1/canvas-api/update
Content-Type: application/json
{
"id": 1,
"student_id": 123,
"course_id": 456,
"assignment_id": 789,
"score": 90
}
```
Response:
```json
{
"success": true,
"message": "Dummy grade updated successfully",
"data": {
"id": 1,
"student_id": 123,
"course_id": 456,
"assignment_id": 789,
"score": 90,
"grade": "A",
"submitted_at": "2024-03-15T10:00:00Z",
"graded_at": "2024-03-16T14:30:00Z"
}
}
```
#### Delete a Dummy Grade
```bash
DELETE /api/v1/canvas-api/delete
Content-Type: application/json
{
"id": 1
}
```
Response:
```json
{
"success": true,
"message": "Dummy grade deleted successfully"
}
```
---
## 10. Testing
Run tests with:
```bash
npm test # Run all tests
npm run test:watch # Run tests in watch mode
npm run test:coverage # Run tests with coverage
```
The test database is automatically used for running tests.
---
## 11. Authentication & Authorization
- All endpoints except login require a valid JWT token
- The JWT token should be included in the Authorization header as: `Bearer your.jwt.token`
- Some endpoints require specific roles (admin/user)
- JWT tokens expire after 1 hour
---
## 12. Troubleshooting
### Common Issues
1. **Database Connection Issues**
- Ensure Docker is running
- Check if the Postgres containers are up: `docker ps`
- Verify database credentials in `.env`
2. **Authentication Issues**
- Ensure JWT_SECRET is set in `.env`
- Check if the user exists in the database
- Verify the password is correct
3. **Role-based Access Issues**
- Ensure the user has the required role
- Check if the JWT token includes the correct roles
- Verify the token hasn't expired
### Logs
- Application logs can be viewed in the terminal where `npm run dev` was executed
- Database logs can be viewed using: `docker logs fusero-boilerplate-db`
---
## 13. Notes
- The app uses separate databases, ports, and Docker volumes from any other Fusero projects, so it can run in parallel.
- The default admin user is created by the seed script and can be changed via `.env`.
- For production, use `docker-compose.yml` and adjust ports/credentials as needed.
- The app includes TypeScript, ESLint, and Prettier for code quality.
### Developer Notes
- The ChatGPT modal logic is in `frontend/src/components/CanvasEndpoints.tsx`.
- The backend endpoint creation API is `/api/v1/canvas-api/endpoints`.
- The system expects ChatGPT to return a JSON object with at least `name`, `method`, and `path`.
- The endpoint list is auto-refreshed after creation.
---

@ -1,3 +1,5 @@
version: '3.8'
services:
fusero-boilerplate-db:
image: postgres:15

@ -1,91 +1,97 @@
version: '3.8'
services:
fusero-frontend:
container_name: fusero-frontend
env_file: ../fusero-frontend/.env
build:
context: ../fusero-frontend
dockerfile: Dockerfile.dev
ports:
- '3000:80'
networks:
- fusero-network
fusero-app-frontend:
container_name: fusero-app-frontend
build:
context: ./frontend
dockerfile: Dockerfile
ports:
- '3000:80'
networks:
- fusero-network
depends_on:
- fusero-app-backend
fusero-app-boilerplate:
environment:
- POSTGRES_HOST=fusero-app-db
build:
context: .
dockerfile: Dockerfile
env_file: .env
restart: always
ports:
- '5000:14000'
depends_on:
- fusero-app-db
container_name: fusero-app-boilerplate
networks:
- fusero-network
fusero-app-frontend-dev:
container_name: fusero-app-frontend-dev
build:
context: ./frontend
dockerfile: Dockerfile
ports:
- '8080:8080'
volumes:
- ./frontend:/app
- /app/node_modules
environment:
- NODE_ENV=development
command: npm run dev
networks:
- fusero-network
depends_on:
- fusero-app-backend
fusero-app-db:
image: postgres:15
env_file: .env
restart: always
volumes:
- fusero_app_pgdata:/var/lib/postgresql/data
ports:
- '19090:5432'
container_name: fusero-app-db
networks:
- fusero-network
fusero-app-backend:
build:
context: .
dockerfile: Dockerfile
env_file: .env
restart: always
ports:
- '5000:14000'
depends_on:
- fusero-boilerplate-db
container_name: fusero-app-backend
networks:
- fusero-network
fusero-app-test-db:
image: postgres:15
env_file: .env
restart: always
volumes:
- fusero_app_test_pgdata:/var/lib/postgresql/data
ports:
- '19091:5432'
container_name: fusero-app-test-db
networks:
- fusero-network
environment:
- POSTGRES_DB=test-db
fusero-boilerplate-db:
image: postgres:15
env_file: .env
restart: always
volumes:
- fusero_boilerplate_pgdata:/var/lib/postgresql/data
ports:
- '19095:5432'
container_name: fusero-boilerplate-db
networks:
- fusero-network
# 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-boilerplate-test-db:
image: postgres:15
env_file: .env
restart: always
volumes:
- fusero_boilerplate_test_pgdata:/var/lib/postgresql/data
ports:
- '19096:5432'
container_name: fusero-boilerplate-test-db
networks:
- fusero-network
environment:
- POSTGRES_DB=test-db
# fusero-redis:
# image: redis:7-alpine
# restart: always
# ports:
# - '6379:6379'
# volumes:
# - redis_data:/data
# container_name: fusero-redis
# networks:
# - fusero-network
nginx:
image: nginx:alpine
container_name: fusero-nginx
ports:
- '14001:80'
- '14443:443'
volumes:
- ./nginx/nginx.conf.prod:/etc/nginx/conf.d/default.conf:ro
- ./nginx/certs:/etc/nginx/certs:ro
depends_on:
- fusero-app-frontend
- fusero-app-backend
networks:
- fusero-network
volumes:
redis_data:
fusero_app_pgdata:
external: true
fusero_app_test_pgdata:
external: false
fusero_boilerplate_pgdata:
external: true
fusero_boilerplate_test_pgdata:
external: false
networks:
fusero-network:
name: fusero-network
fusero-network:
name: fusero-network

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="/favicon/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon/favicon-16x16.png">
<link rel="manifest" href="/favicon/site.webmanifest">
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Fusero</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

19
frontend/nginx.conf Normal file

@ -0,0 +1,19 @@
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
location /favicon/ {
alias /usr/share/nginx/html/dist/favicon/;
access_log off;
expires max;
}
# DO NOT proxy /api here let the global Nginx handle it
}

27
frontend/nginx.conf.bkup Normal file

@ -0,0 +1,27 @@
server {
listen 80;
server_name _;
location / {
root /usr/share/nginx/html;
index index.html;
try_files $uri $uri/ /index.html;
}
# Serve favicon files
location /favicon/ {
alias /usr/share/nginx/html/dist/favicon/;
access_log off;
expires max;
}
# Proxy API requests to the backend
location /api {
proxy_pass http://fusero-app-backend:14000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
}

10239
frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

45
frontend/package.json Normal file

@ -0,0 +1,45 @@
{
"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/node": "^22.15.18",
"@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: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

@ -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,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,55 @@
import React, { ReactNode } from 'react';
import { ModalConfig } from '../core/types';
import { useModal } from '../context/ModalContext';
interface ModalProps extends Omit<ModalConfig, 'content'> {
content: ReactNode;
}
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,23 @@
import React, { ReactNode } from 'react';
import { Modal } from './Modal';
import { useModal } from '../context/ModalContext';
import { ModalConfig } from '../core/types';
interface ModalWithContent extends ModalConfig {
content: ReactNode;
}
export const ModalRenderer: React.FC = () => {
const { modals } = useModal();
return (
<>
{modals.map((modal) => (
<Modal
key={modal.id}
{...modal as ModalWithContent}
/>
))}
</>
);
};

@ -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,478 @@
import { useCallback, useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { Card } from 'primereact/card';
import { Dialog } from 'primereact/dialog';
import { InputText } from 'primereact/inputtext';
import { InputTextarea } from 'primereact/inputtextarea';
import { FileUpload } from 'primereact/fileupload';
import Button from '../shared/components/_V2/Button';
import Dropdown from '../shared/components/_V2/Dropdown';
import Divider from '../shared/components/_V1/Divider';
import fusero_logo from '../assets/fusero_logo.svg';
import roc_logo from '../assets/roc_logo.png';
import { useSystemStore } from '../state/stores/useSystemStore';
export type SystemDropdownOption = {
id: number;
label: string;
name: string;
businessLabel: string;
urlSlug: string;
logo: string;
category: string;
method?: string;
path?: string;
description?: string;
publicPath?: string;
enterprises?: Enterprise[];
};
export type Enterprise = {
id: number;
name: string;
};
export interface SystemSelectionModalProps {
systemDetails: SystemDropdownOption | null;
onSystemSelected: (systemData: any) => void;
onLogoChange: (newLogoPath: string) => void;
}
// Add system default routes configuration
const systemDefaultRoutes = {
council: 'chat',
fusemind: 'home',
};
const HARDCODED_SYSTEMS = [
{
id: 1,
label: 'Canvas API',
name: 'Canvas API',
businessLabel: 'Canvas',
urlSlug: 'canvas',
logo: roc_logo,
category: 'Apps',
method: 'GET',
path: '/api/v1/canvas-endpoints',
description: 'Integration with Canvas LMS',
publicPath: '/canvas-endpoints',
enterprises: [],
},
{
id: 2,
label: 'FuseMind',
name: 'FuseMind',
businessLabel: 'FuseMind',
urlSlug: 'fusemind',
logo: fusero_logo,
category: 'Apps',
method: 'GET',
path: '/api/v1/fusemind',
description: 'FuseMind AI Platform',
publicPath: '/fusemind',
enterprises: [],
},
];
const SystemSelectionModal = ({ systemDetails }: SystemSelectionModalProps) => {
const navigate = useNavigate();
const [selectedSystem, setSelectedSystem] = useState<SystemDropdownOption | null>(null);
const [selectedEnterprise, setSelectedEnterprise] = useState<number | null>(null);
const [showAddSystemDialog, setShowAddSystemDialog] = useState(false);
const [showEditLogoDialog, setShowEditLogoDialog] = useState(false);
const [systems, setSystems] = useState<SystemDropdownOption[]>(HARDCODED_SYSTEMS);
const [isLoading, setIsLoading] = useState(false);
const [uploadedLogo, setUploadedLogo] = useState<string | null>(null);
const [newSystem, setNewSystem] = useState({
name: '',
businessLabel: '',
urlSlug: '',
method: '',
path: '',
description: '',
publicPath: '',
});
const { setSystem } = useSystemStore();
useEffect(() => {
[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}`);
}
};
const handleEnterpriseChange = useCallback((e: { value: number }) => {
setSelectedEnterprise(e.value);
}, []);
const handleLogoUpload = async (event: any) => {
const file = event.files[0];
if (file) {
const reader = new FileReader();
reader.onload = (e) => {
const base64 = e.target?.result as string;
setUploadedLogo(base64);
};
reader.readAsDataURL(file);
}
};
const handleUpdateLogo = async () => {
if (!selectedSystem || !uploadedLogo) return;
try {
// Upload the new logo
const logoResponse = await fetch('/api/v1/assets', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: `${selectedSystem.name}-logo`,
type: 'image',
data: uploadedLogo,
mimeType: 'image/png',
assetType: 'system_logo',
}),
});
if (!logoResponse.ok) {
throw new Error('Failed to upload logo');
}
const logoData = await logoResponse.json();
// Update the system with the new logo ID
const response = await fetch(`/api/v1/apps/${selectedSystem.id}`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
logoId: logoData.id,
}),
});
if (!response.ok) {
throw new Error('Failed to update system logo');
}
const updatedSystem = await response.json();
// Update the local state
setSystems(prev => prev.map(system =>
system.id === selectedSystem.id
? { ...system, logo: updatedSystem.logo || fusero_logo }
: system
));
setSelectedSystem(prev => prev ? { ...prev, logo: updatedSystem.logo || fusero_logo } : null);
setShowEditLogoDialog(false);
setUploadedLogo(null);
} catch (error) {
console.error('Error updating logo:', error);
}
};
const handleAddSystem = async () => {
try {
// First upload the logo if exists
let logoId = null;
if (uploadedLogo) {
const logoResponse = await fetch('/api/v1/assets', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: `${newSystem.name}-logo`,
type: 'image',
data: uploadedLogo,
mimeType: 'image/png',
assetType: 'system_logo',
}),
});
if (!logoResponse.ok) {
throw new Error('Failed to upload logo');
}
const logoData = await logoResponse.json();
logoId = logoData.id;
}
// Then create the system
const response = await fetch('/api/v1/apps', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
...newSystem,
logoId,
}),
});
if (!response.ok) {
throw new Error('Failed to create system');
}
const createdSystem = await response.json();
const systemOption: SystemDropdownOption = {
...createdSystem,
label: createdSystem.name,
businessLabel: createdSystem.businessLabel || createdSystem.name,
logo: createdSystem.logo || fusero_logo,
category: 'Apps',
};
setSystems(prev => [...prev, systemOption]);
setShowAddSystemDialog(false);
setNewSystem({
name: '',
businessLabel: '',
urlSlug: '',
method: '',
path: '',
description: '',
publicPath: '',
});
setUploadedLogo(null);
} catch (error) {
console.error('Error creating system:', error);
}
};
return (
<>
<Card
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 gap-4'>
<Dropdown
id='system'
value={selectedSystem}
options={systems}
onChange={handleSystemChange}
optionLabel='businessLabel'
placeholder='Select an integration'
className='flex-1'
loading={isLoading}
/>
{/* <Button
label='Add System'
onClick={() => setShowAddSystemDialog(true)}
className='px-4 py-2 text-white transition-colors bg-blue-500 border-2 border-blue-500 rounded-md hover:bg-white hover:text-blue-500'
/> */}
</div>
{selectedSystem && selectedSystem.enterprises && (
<Dropdown
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 && (
<div className="relative">
<img
src={selectedSystem.logo}
alt={`${selectedSystem.label} Logo`}
className='w-48'
/>
<Button
label="Edit Logo"
onClick={() => setShowEditLogoDialog(true)}
className="absolute bottom-0 right-0 px-2 py-1 text-sm text-white bg-blue-500 rounded hover:bg-blue-600"
/>
</div>
)}
<Dialog
header="Add New System"
visible={showAddSystemDialog}
style={{ width: '50vw' }}
onHide={() => setShowAddSystemDialog(false)}
footer={
<div>
<Button
label="Cancel"
onClick={() => setShowAddSystemDialog(false)}
className="mr-2"
/>
<Button
label="Create"
onClick={handleAddSystem}
className="bg-blue-500 text-white"
/>
</div>
}
>
<div className="flex flex-col gap-4">
<div>
<label className="block mb-2">Name</label>
<InputText
value={newSystem.name}
onChange={(e) => setNewSystem({ ...newSystem, name: e.target.value })}
className="w-full"
/>
</div>
<div>
<label className="block mb-2">Business Label</label>
<InputText
value={newSystem.businessLabel}
onChange={(e) => setNewSystem({ ...newSystem, businessLabel: e.target.value })}
className="w-full"
/>
</div>
<div>
<label className="block mb-2">URL Slug</label>
<InputText
value={newSystem.urlSlug}
onChange={(e) => setNewSystem({ ...newSystem, urlSlug: e.target.value })}
className="w-full"
/>
</div>
<div>
<label className="block mb-2">Method</label>
<InputText
value={newSystem.method}
onChange={(e) => setNewSystem({ ...newSystem, method: e.target.value })}
className="w-full"
/>
</div>
<div>
<label className="block mb-2">Path</label>
<InputText
value={newSystem.path}
onChange={(e) => setNewSystem({ ...newSystem, path: e.target.value })}
className="w-full"
/>
</div>
<div>
<label className="block mb-2">Description</label>
<InputTextarea
value={newSystem.description}
onChange={(e) => setNewSystem({ ...newSystem, description: e.target.value })}
className="w-full"
/>
</div>
<div>
<label className="block mb-2">Public Path</label>
<InputText
value={newSystem.publicPath}
onChange={(e) => setNewSystem({ ...newSystem, publicPath: e.target.value })}
className="w-full"
/>
</div>
<div>
<label className="block mb-2">Logo</label>
<FileUpload
mode="basic"
name="logo"
accept="image/*"
maxFileSize={1000000}
chooseLabel="Upload Logo"
onUpload={handleLogoUpload}
auto
customUpload
/>
{uploadedLogo && (
<div className="mt-2">
<img src={uploadedLogo} alt="Uploaded logo" className="w-32 h-32 object-contain" />
</div>
)}
</div>
</div>
</Dialog>
<Dialog
header="Update System Logo"
visible={showEditLogoDialog}
style={{ width: '30vw' }}
onHide={() => setShowEditLogoDialog(false)}
footer={
<div>
<Button
label="Cancel"
onClick={() => setShowEditLogoDialog(false)}
className="mr-2"
/>
<Button
label="Update"
onClick={handleUpdateLogo}
className="bg-blue-500 text-white"
disabled={!uploadedLogo}
/>
</div>
}
>
<div className="flex flex-col gap-4">
<div>
<label className="block mb-2">Current Logo</label>
{selectedSystem && (
<img
src={selectedSystem.logo}
alt="Current logo"
className="w-32 h-32 object-contain mb-4"
/>
)}
</div>
<div>
<label className="block mb-2">New Logo</label>
<FileUpload
mode="basic"
name="logo"
accept="image/*"
maxFileSize={1000000}
chooseLabel="Upload New Logo"
onUpload={handleLogoUpload}
auto
customUpload
/>
{uploadedLogo && (
<div className="mt-2">
<img src={uploadedLogo} alt="New logo preview" className="w-32 h-32 object-contain" />
</div>
)}
</div>
</div>
</Dialog>
</>
);
};
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;

@ -0,0 +1,904 @@
import { useEffect, useState, useRef } from 'react';
import { Column } from 'primereact/column';
import { FilterMatchMode } from 'primereact/api';
import TableGroupHeader from '../../shared/components/_V1/TableGroupHeader';
import TableGroup from '../../shared/components/_V1/TableGroup';
import { TSizeOptionValue } from './DataTable.types';
import { api } from '../../services/api';
import { Button } from 'primereact/button';
import { Dialog } from 'primereact/dialog';
import { InputText } from 'primereact/inputtext';
import { Dropdown } from 'primereact/dropdown';
import { ProgressSpinner } from 'primereact/progressspinner';
import ChatGptModal from './ChatGPTModal';
import { Toast } from 'primereact/toast';
import { useAuthStore } from '../../state/stores/useAuthStore';
import SettingsMenu, { SettingsMenuItem } from '../../shared/components/SettingsMenu';
import ManageDocsModal from './ManageDocsModal';
import PromptCreatorModal from './PromptCreatorModal';
import { Tooltip } from 'primereact/tooltip';
const CANVAS_APP_ID = 1; // Hardcoded Canvas app ID
const CANVAS_API_BASE_URL = import.meta.env.VITE_CANVAS_API_BASE_URL || 'https://canvas.instructure.com/api/v1';
export default function CanvasEndpoints() {
const [endpoints, setEndpoints] = useState<any[]>([]);
const [filters, setFilters] = useState<any>(null);
const [globalFilterValue, setGlobalFilterValue] = useState('');
const [showCreateModal, setShowCreateModal] = useState(false);
const [newEndpoint, setNewEndpoint] = useState({ name: '', method: '', path: '', description: '', publicPath: '' });
const sizeOptions: { label: string; value: TSizeOptionValue }[] = [
{ label: 'Small', value: 'small' },
{ label: 'Normal', value: 'normal' },
{ label: 'Large', value: 'large' },
];
const [size, setSize] = useState<TSizeOptionValue>(sizeOptions[0].value);
const [showCallModal, setShowCallModal] = useState(false);
const [callModalData, setCallModalData] = useState<any[]>([]);
const [callModalColumns, setCallModalColumns] = useState<string[]>([]);
const [callModalLoading, setCallModalLoading] = useState(false);
const [callEndpoint, setCallEndpoint] = useState<any>(null);
const [callModalFilters, setCallModalFilters] = useState<any>(null);
const [callModalGlobalFilterValue, setCallModalGlobalFilterValue] = useState('');
const [callModalSize, setCallModalSize] = useState<TSizeOptionValue>(sizeOptions[0].value);
const [callModalFirst, setCallModalFirst] = useState(0);
const [callModalRows, setCallModalRows] = useState(10);
const [showChatGptModal, setShowChatGptModal] = useState(false);
const [chatPrompt, setChatPrompt] = useState('');
const [chatMessages, setChatMessages] = useState<{ role: 'user' | 'assistant'; content: string }[]>([]);
const [chatLoading, setChatLoading] = useState(false);
const [chatError, setChatError] = useState<string | null>(null);
const toast = useRef(null);
const [editEndpointId, setEditEndpointId] = useState<number | null>(null);
const [apiKey, setApiKey] = useState<string | null>(null);
const [apiKeyLoading, setApiKeyLoading] = useState(false);
const [apiKeyError, setApiKeyError] = useState<string | null>(null);
const [appId, setAppId] = useState<number | null>(CANVAS_APP_ID);
const [showApiKeyModal, setShowApiKeyModal] = useState(false);
const [editByVoiceEndpoint, setEditByVoiceEndpoint] = useState<any>(null);
const [showVoiceModal, setShowVoiceModal] = useState(false);
const [liveTranscript, setLiveTranscript] = useState('');
const [showVoiceInfoModal, setShowVoiceInfoModal] = useState(false);
const [fullTranscript, setFullTranscript] = useState('');
const [showCogMenu, setShowCogMenu] = useState(false);
const cogMenuRef = useRef<any>(null);
const [showManageDocs, setShowManageDocs] = useState(false);
const [showPromptCreator, setShowPromptCreator] = useState(false);
useEffect(() => {
// Load API key from localStorage if available
const storedKey = localStorage.getItem('canvas_api_key');
if (storedKey) setApiKey(storedKey);
initFilters();
fetchEndpoints();
}, []);
const fetchEndpoints = async () => {
try {
const response = await api('get', '/api/v1/canvas-api/endpoints');
setEndpoints(response.data);
} catch (error) {
console.error('Failed to fetch endpoints:', error);
}
};
const initFilters = () => {
setFilters({
global: { value: null, matchMode: FilterMatchMode.CONTAINS },
});
setGlobalFilterValue('');
};
const clearFilter = () => {
initFilters();
};
const onGlobalFilterChange = (e: { target: { value: any } }) => {
const value = e.target.value;
let _filters = { ...filters };
_filters['global'].value = value;
setFilters(_filters);
setGlobalFilterValue(value);
};
// Utility to ensure path never starts with /api/v1
function sanitizeCanvasPath(path: string) {
return path.replace(/^\/api\/v1/, '');
}
const handleCreateEndpoint = async () => {
try {
const sanitizedEndpoint = { ...newEndpoint, path: sanitizeCanvasPath(newEndpoint.path) };
await api('post', '/api/v1/canvas-api/endpoints', sanitizedEndpoint);
setShowCreateModal(false);
setNewEndpoint({ name: '', method: '', path: '', description: '', publicPath: '' });
fetchEndpoints();
} catch (error) {
console.error('Failed to create endpoint:', error);
}
};
const handleCallEndpoint = async (endpoint: any) => {
setShowCallModal(true);
setCallModalLoading(true);
setCallEndpoint(endpoint);
try {
const res = await api('post', '/api/v1/canvas-api/proxy-external', {
path: endpoint.path,
method: endpoint.method || 'GET',
});
let data = res.data;
if (!Array.isArray(data)) data = [data];
setCallModalData(data);
setCallModalColumns(data.length > 0 ? Object.keys(data[0]) : []);
} catch (error) {
setCallModalData([]);
setCallModalColumns([]);
} finally {
setCallModalLoading(false);
}
};
const initCallModalFilters = () => {
setCallModalFilters({
global: { value: null, matchMode: FilterMatchMode.CONTAINS },
});
setCallModalGlobalFilterValue('');
};
const clearCallModalFilter = () => {
initCallModalFilters();
};
const onCallModalGlobalFilterChange = (e: { target: { value: any } }) => {
const value = e.target.value;
let _filters = { ...callModalFilters };
_filters['global'].value = value;
setCallModalFilters(_filters);
setCallModalGlobalFilterValue(value);
};
const handleChatPromptSend = async (promptOverride?: string) => {
const prompt = promptOverride !== undefined ? promptOverride : chatPrompt;
if (!prompt.trim()) return;
setChatMessages(prev => [...prev, { role: 'user', content: prompt }]);
setChatLoading(true);
setChatError(null);
try {
const formattedRequest = { data: prompt };
const response = await api(
'post',
'/api/v1/canvas-api/chatgpt/completions',
formattedRequest,
undefined,
'json',
60,
true
);
const data = response.data;
let isEndpointJson = false;
if (data && data.responseText) {
let parsed;
try {
parsed = typeof data.responseText === 'string' ? JSON.parse(data.responseText) : data.responseText;
} catch (e) {
parsed = null;
}
if (parsed && parsed.name && parsed.method && parsed.path) {
// Auto-create endpoint
const sanitizedParsed = { ...parsed, path: sanitizeCanvasPath(parsed.path) };
await api('post', '/api/v1/canvas-api/endpoints', sanitizedParsed);
fetchEndpoints();
isEndpointJson = true;
if (toast.current) {
toast.current.show({ severity: 'success', summary: 'Endpoint Created', detail: `Endpoint "${parsed.name}" created successfully!` });
}
}
}
if (!isEndpointJson) {
setChatMessages(prev => [...prev, { role: 'assistant', content: typeof data.responseText === 'string' ? data.responseText : JSON.stringify(data.responseText, null, 2) }]);
}
} catch (err) {
setChatError('Failed to get response from server.');
setChatMessages(prev => [...prev, { role: 'assistant', content: 'Failed to get response from server.' }]);
} finally {
setChatLoading(false);
setChatPrompt('');
}
};
const handleChatPromptKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
handleChatPromptSend();
}
};
const handleDeleteEndpoint = async (endpoint: any) => {
try {
await api('delete', `/api/v1/canvas-api/endpoints/${endpoint.id}`);
fetchEndpoints();
if (toast.current) {
toast.current.show({ severity: 'success', summary: 'Endpoint Deleted', detail: `Endpoint "${endpoint.name}" deleted successfully!` });
}
} catch (error) {
if (toast.current) {
toast.current.show({ severity: 'error', summary: 'Delete Failed', detail: 'Failed to delete endpoint.' });
}
}
};
const openEditModal = (endpoint: any) => {
setEditEndpointId(endpoint.id);
setNewEndpoint({
name: endpoint.name,
method: endpoint.method,
path: endpoint.path,
description: endpoint.description || '',
publicPath: endpoint.publicPath || '',
});
setShowCreateModal(true);
};
const handleSaveEndpoint = async () => {
try {
const sanitizedEndpoint = { ...newEndpoint, path: sanitizeCanvasPath(newEndpoint.path) };
if (editEndpointId) {
await api('put', `/api/v1/canvas-api/endpoints/${editEndpointId}`, sanitizedEndpoint);
} else {
await api('post', '/api/v1/canvas-api/endpoints', sanitizedEndpoint);
}
setShowCreateModal(false);
setNewEndpoint({ name: '', method: '', path: '', description: '', publicPath: '' });
setEditEndpointId(null);
fetchEndpoints();
} catch (error) {
console.error('Failed to save endpoint:', error);
}
};
const handleModalHide = () => {
setShowCreateModal(false);
setNewEndpoint({ name: '', method: '', path: '', description: '', publicPath: '' });
setEditEndpointId(null);
};
const handleApiKey = async () => {
console.log('handleApiKey called');
setApiKeyLoading(true);
setApiKeyError(null);
try {
const user = useAuthStore.getState().user;
console.log('User:', user, 'AppId:', appId);
if (!user) throw new Error('User not logged in');
if (!appId) throw new Error('App ID not loaded');
const response = await api('post', '/api/v1/app/apikey/generate', { userId: user.id, appId });
setApiKey(response.data.apiKey);
localStorage.setItem('canvas_api_key', response.data.apiKey);
} catch (error: any) {
const errorMessage = error.response?.data?.error || error.message || 'Failed to fetch or generate API key';
setApiKeyError(errorMessage);
console.error('API Key Error:', error);
} finally {
setApiKeyLoading(false);
}
};
const fetchApiKey = async () => {
try {
const user = useAuthStore.getState().user;
if (!user || !appId) return;
const response = await api('post', '/api/v1/app/apikey/get', { userId: user.id, appId });
setApiKey(response.data.apiKey || null);
if (response.data.apiKey) {
localStorage.setItem('canvas_api_key', response.data.apiKey);
}
setApiKeyError(null);
} catch (error: any) {
setApiKey(null);
setApiKeyError(error.response?.data?.error || error.message || 'Failed to fetch API key');
}
};
useEffect(() => {
if (showApiKeyModal) {
fetchApiKey();
}
// eslint-disable-next-line
}, [showApiKeyModal, appId]);
const tryDeleteFromSpeech = async (transcript) => {
const lower = transcript.toLowerCase();
if (lower.startsWith('create')) return false; // let normal create/chat logic handle it
if (lower.includes('delete')) {
// Try to match "delete endpoint 5" or "remove endpoint 5"
const idMatch = transcript.match(/delete endpoint (\d+)/i) || transcript.match(/remove endpoint (\d+)/i);
if (idMatch) {
const id = parseInt(idMatch[1], 10);
await handleDeleteEndpoint({ id });
setChatMessages(prev => [...prev, { role: 'assistant', content: `Endpoint with ID ${id} deleted (if it existed).` }]);
return true;
}
// Try to match "delete endpoint called X" or "remove endpoint named X"
const nameMatch = transcript.match(/delete endpoint (called|named) (.+)/i) || transcript.match(/remove endpoint (called|named) (.+)/i);
if (nameMatch) {
const name = nameMatch[2].trim();
const endpoint = endpoints.find(e => e.name.toLowerCase() === name.toLowerCase());
if (endpoint) {
await handleDeleteEndpoint(endpoint);
setChatMessages(prev => [...prev, { role: 'assistant', content: `Endpoint \"${name}\" deleted.` }]);
} else {
setChatMessages(prev => [...prev, { role: 'assistant', content: `No endpoint found with name \"${name}\".` }]);
}
return true;
}
// Try to match "delete endpoint [name]"
const nameAfterEndpointMatch = transcript.match(/delete endpoint ([a-zA-Z0-9 _-]+)/i);
if (nameAfterEndpointMatch) {
const name = nameAfterEndpointMatch[1].trim();
const endpoint = endpoints.find(e => e.name.toLowerCase() === name.toLowerCase());
if (endpoint) {
await handleDeleteEndpoint(endpoint);
setChatMessages(prev => [...prev, { role: 'assistant', content: `Endpoint \"${name}\" deleted.` }]);
} else {
setChatMessages(prev => [...prev, { role: 'assistant', content: `No endpoint found with name \"${name}\".` }]);
}
return true;
}
// If just "delete endpoint" with no id or name, show a message
if (lower.includes('delete endpoint')) {
setChatMessages(prev => [...prev, { role: 'assistant', content: `Please specify the endpoint ID or name to delete.` }]);
return true;
}
}
return false;
};
const tryShowFromSpeech = async (transcript) => {
const lower = transcript.toLowerCase();
const viewKeywords = ['show', 'view', 'open'];
// Find which keyword (if any) is present
const action = viewKeywords.find(kw => lower.includes(kw));
if (!action) return false;
// Try "<action> endpoint 5"
const idMatch = lower.match(/(?:show|view|open) endpoint (\d+)/i);
if (idMatch) {
const id = parseInt(idMatch[1], 10);
const endpoint = endpoints.find(e => e.id === id);
if (endpoint) {
handleCallEndpoint(endpoint);
setChatMessages(prev => [...prev, { role: 'assistant', content: `${action.charAt(0).toUpperCase() + action.slice(1)}ing endpoint with ID ${id}.` }]);
} else {
setChatMessages(prev => [...prev, { role: 'assistant', content: `No endpoint found with ID ${id}.` }]);
}
return true;
}
// Try "<action> endpoint [name]"
const nameAfterEndpointMatch = lower.match(/(?:show|view|open) endpoint ([a-zA-Z0-9 _-]+)/i);
if (nameAfterEndpointMatch) {
const name = nameAfterEndpointMatch[1].trim();
const endpoint = endpoints.find(e => e.name.toLowerCase() === name.toLowerCase());
if (endpoint) {
handleCallEndpoint(endpoint);
setChatMessages(prev => [...prev, { role: 'assistant', content: `${action.charAt(0).toUpperCase() + action.slice(1)}ing endpoint \"${name}\".` }]);
} else {
setChatMessages(prev => [...prev, { role: 'assistant', content: `No endpoint found with name \"${name}\".` }]);
}
return true;
}
// Try "<action> [name] endpoint"
const nameEndpointMatch = lower.match(/(?:show|view|open) (.+) endpoint/i);
if (nameEndpointMatch) {
const name = nameEndpointMatch[1].trim();
const endpoint = endpoints.find(e => e.name.toLowerCase() === name.toLowerCase());
if (endpoint) {
handleCallEndpoint(endpoint);
setChatMessages(prev => [...prev, { role: 'assistant', content: `${action.charAt(0).toUpperCase() + action.slice(1)}ing endpoint \"${name}\".` }]);
} else {
setChatMessages(prev => [...prev, { role: 'assistant', content: `No endpoint found with name \"${name}\".` }]);
}
return true;
}
// Try "<action> [name]"
const nameMatch = lower.match(/(?:show|view|open) (.+)/i);
if (nameMatch) {
const name = nameMatch[1].trim();
const endpoint = endpoints.find(e => e.name.toLowerCase() === name.toLowerCase());
if (endpoint) {
handleCallEndpoint(endpoint);
setChatMessages(prev => [...prev, { role: 'assistant', content: `${action.charAt(0).toUpperCase() + action.slice(1)}ing endpoint \"${name}\".` }]);
} else {
setChatMessages(prev => [...prev, { role: 'assistant', content: `No endpoint found with name \"${name}\".` }]);
}
return true;
}
return false;
};
const trySearchFromSpeech = (transcript) => {
const lower = transcript.toLowerCase().trim();
// Match "search [term]", "find [term]", or "filter by [term]"
const searchMatch = lower.match(/^(search|find|filter by)\s+(.+)$/i);
if (searchMatch) {
const term = searchMatch[2].trim();
setGlobalFilterValue(term);
setChatMessages(prev => [...prev, { role: 'assistant', content: `Filtered endpoints by: "${term}"` }]);
return true;
}
return false;
};
const tryModifyFromSpeech = (transcript) => {
const lower = transcript.toLowerCase();
const match = lower.match(/modify endpoint (\d+)/i);
if (match) {
const id = parseInt(match[1], 10);
const endpoint = endpoints.find(e => e.id === id);
if (endpoint) {
setEditByVoiceEndpoint({ ...endpoint });
setChatMessages(prev => [...prev, { role: 'assistant', content: `Ready to modify endpoint with ID ${id}. Say 'method is GET' or 'path is slash course slash id'.` }]);
} else {
setChatMessages(prev => [...prev, { role: 'assistant', content: `No endpoint found with ID ${id}.` }]);
}
return true;
}
return false;
};
const tryUpdateEditByVoice = async (transcript) => {
if (!editByVoiceEndpoint) return false;
let updated = { ...editByVoiceEndpoint };
let didUpdate = false;
// Method update
const methodMatch = transcript.match(/method is (get|post|put|delete|patch)/i);
if (methodMatch) {
updated.method = methodMatch[1].toUpperCase();
didUpdate = true;
}
// Path update
const pathMatch = transcript.match(/path is (.+)/i);
if (pathMatch) {
let path = pathMatch[1]
.replace(/slash/gi, '/')
.replace(/colon id|id/gi, ':id')
.replace(/\s+/g, '');
updated.path = path;
didUpdate = true;
}
if (didUpdate) {
setEditByVoiceEndpoint(updated);
setChatMessages(prev => [...prev, { role: 'assistant', content: `Updated endpoint: method=${updated.method}, path=${updated.path}` }]);
// Auto-save
await api('put', `/api/v1/canvas-api/endpoints/${updated.id}`, updated);
fetchEndpoints();
setEditByVoiceEndpoint(null);
setChatMessages(prev => [...prev, { role: 'assistant', content: `Endpoint ${updated.id} saved.` }]);
return true;
}
return false;
};
const handleVoiceInput = () => {
// @ts-ignore
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
if (!SpeechRecognition) {
alert('Speech recognition not supported in this browser.');
return;
}
setShowVoiceModal(true);
setLiveTranscript('');
setFullTranscript('');
const recognition = new SpeechRecognition();
recognition.lang = 'en-US';
recognition.interimResults = true;
recognition.maxAlternatives = 1;
recognition.onresult = async (event: any) => {
const transcript = event.results[0][0].transcript;
setLiveTranscript(transcript);
setFullTranscript(prev => prev + (prev && !prev.endsWith(' ') ? ' ' : '') + transcript);
setChatPrompt(transcript);
if (event.results[0].isFinal) {
setShowVoiceModal(false);
setLiveTranscript('');
setFullTranscript('');
if (transcript.toLowerCase().includes('open command list')) {
setShowVoiceInfoModal(true);
return;
}
if (await tryDeleteFromSpeech(transcript)) return;
if (await tryShowFromSpeech(transcript)) return;
if (trySearchFromSpeech(transcript)) return;
if (tryModifyFromSpeech(transcript)) return;
if (await tryUpdateEditByVoice(transcript)) return;
handleChatPromptSend(transcript);
}
};
recognition.onerror = (event: any) => {
setShowVoiceModal(false);
setLiveTranscript('');
setFullTranscript('');
alert('Speech recognition error: ' + event.error);
};
recognition.onend = () => {
setShowVoiceModal(false);
setLiveTranscript('');
setFullTranscript('');
};
recognition.start();
};
const settingsItems = [
{
label: 'Manage API Key',
icon: 'pi pi-key',
command: () => setShowApiKeyModal(true),
},
{
label: 'Voice Command List',
icon: 'pi pi-info-circle',
command: () => setShowVoiceInfoModal(true),
},
{
label: 'Manage API Docs',
icon: 'pi pi-link',
command: () => setShowManageDocs(true),
},
{
label: 'Create Prompt',
icon: 'pi pi-plus-circle',
command: () => setShowPromptCreator(true),
},
];
const renderHeader = () => (
<TableGroupHeader
size={size}
setSize={setSize}
sizeOptions={sizeOptions}
globalFilterValue={globalFilterValue}
onGlobalFilterChange={onGlobalFilterChange}
onRefresh={fetchEndpoints}
clearFilter={clearFilter}
extraButtonsTemplate={() => (
<div className="flex gap-2">
<Button
icon="pi pi-plus"
onClick={() => setShowCreateModal(true)}
className="p-button-secondary"
/>
<Button
icon="pi pi-comments"
onClick={() => setShowChatGptModal(true)}
className="p-button-primary"
/>
<SettingsMenu
items={settingsItems}
buttonClassName="p-button-secondary"
/>
<Button
icon="pi pi-volume-up"
onClick={handleVoiceInput}
className="p-button-secondary"
disabled={chatLoading}
/>
</div>
)}
/>
);
const serverPath = import.meta.env.VITE_API_BASE_URL || window.location.origin;
const generateCurlCommand = (endpoint: any, apiKey: string) => {
const backendUrl = `${serverPath}/api/v1/canvas-api/proxy-external`;
const method = endpoint.method || 'GET';
const path = endpoint.path;
const header = `-H 'Authorization: Bearer ${apiKey}'`;
const data = `-d '{"path": "${path}", "method": "${method}"}'`;
return `curl -X POST ${header} -H 'Content-Type: application/json' ${data} '${backendUrl}'`;
};
const renderCallButton = (rowData: any) => (
<div className="flex gap-2">
<Button icon="pi pi-play" onClick={() => handleCallEndpoint(rowData)} />
<Button icon="pi pi-pencil" className="p-button-warning" onClick={() => openEditModal(rowData)} />
<Button icon="pi pi-trash" className="p-button-danger" onClick={() => handleDeleteEndpoint(rowData)} />
<span>
<Button icon="pi pi-copy" className="p-button-secondary" onClick={() => {
if (!apiKey) {
if (toast.current) {
toast.current.show({ severity: 'warn', summary: 'API Key Required', detail: 'Please generate or enter your API key first.' });
}
return;
}
const curl = generateCurlCommand(rowData, apiKey);
navigator.clipboard.writeText(curl);
if (toast.current) {
toast.current.show({ severity: 'info', summary: 'Copied', detail: 'cURL command copied to clipboard!' });
}
}} data-pr-tooltip="Copy cURL command" />
<Tooltip target=".p-button-secondary" content="Copy cURL command" />
</span>
</div>
);
const renderCallModalHeader = () => (
<TableGroupHeader
size={callModalSize}
setSize={setCallModalSize}
sizeOptions={sizeOptions}
globalFilterValue={callModalGlobalFilterValue}
onGlobalFilterChange={onCallModalGlobalFilterChange}
onRefresh={() => handleCallEndpoint(callEndpoint)}
clearFilter={clearCallModalFilter}
/>
);
// Compute filtered endpoints for search-everything
const filteredEndpoints = globalFilterValue
? endpoints.filter(endpoint =>
Object.values(endpoint)
.join(' ')
.toLowerCase()
.includes(globalFilterValue.toLowerCase())
)
: endpoints;
return (
<div className='w-full h-full p-6'>
<h2 className='text-xl font-semibold mb-4'>Canvas Endpoints</h2>
<Toast ref={toast} position="bottom-right" />
<TableGroup
body={filteredEndpoints}
size={size}
header={renderHeader}
filters={filters}
rows={10}
paginator
showGridlines={true}
removableSort={true}
dragSelection={false}
emptyMessage='No endpoints found.'
>
<Column field='id' header='ID' sortable filter style={{ width: '10%' }} />
<Column field='name' header='Name' sortable filter style={{ width: '20%' }} />
<Column field='method' header='Method' sortable filter style={{ width: '10%' }} />
<Column field='path' header='Path' sortable filter style={{ width: '25%' }} />
<Column field='publicPath' header='Public Path' sortable filter style={{ width: '15%' }} />
<Column header='Action' body={renderCallButton} style={{ width: '20%' }} />
</TableGroup>
<Dialog
visible={showCreateModal}
onHide={handleModalHide}
header={editEndpointId ? 'Edit Endpoint' : 'Create Endpoint'}
style={{ width: '40vw' }}
modal
footer={
<div>
<Button label="Cancel" onClick={handleModalHide} className="mr-2" />
<Button label={editEndpointId ? 'Save' : 'Create'} onClick={handleSaveEndpoint} className="bg-blue-500 text-white" />
</div>
}
>
<div className="p-fluid">
<div className="p-field">
<label htmlFor="name">Name</label>
<InputText id="name" value={newEndpoint.name} onChange={(e) => setNewEndpoint({ ...newEndpoint, name: e.target.value })} />
</div>
<div className="p-field">
<label htmlFor="method">Method</label>
<InputText id="method" value={newEndpoint.method} onChange={(e) => setNewEndpoint({ ...newEndpoint, method: e.target.value })} />
</div>
<div className="p-field">
<label htmlFor="path">Path</label>
<InputText id="path" value={newEndpoint.path} onChange={(e) => setNewEndpoint({ ...newEndpoint, path: e.target.value })} />
</div>
<div className="p-field">
<label htmlFor="publicPath">Public Path</label>
<InputText id="publicPath" value={newEndpoint.publicPath || ''} onChange={(e) => setNewEndpoint({ ...newEndpoint, publicPath: e.target.value })} />
</div>
<div className="p-field">
<label htmlFor="description">Description</label>
<InputText id="description" value={newEndpoint.description} onChange={(e) => setNewEndpoint({ ...newEndpoint, description: e.target.value })} />
</div>
</div>
</Dialog>
<Dialog
visible={showCallModal}
onHide={() => setShowCallModal(false)}
header={callEndpoint ? `Response for ${callEndpoint.name}` : 'Endpoint Response'}
style={{ width: '96vw', maxWidth: 1800, minHeight: 600 }}
modal
>
{callModalLoading ? (
<div className="flex justify-center items-center" style={{ minHeight: 200 }}>
<ProgressSpinner />
</div>
) : callModalData.length > 0 ? (
<div style={{ display: 'flex', flexDirection: 'column', height: '70vh' }}>
<div style={{ flex: 1, overflow: 'auto', maxWidth: '100%' }}>
<TableGroup
body={callModalData.slice(callModalFirst, callModalFirst + callModalRows)}
size={callModalSize}
header={renderCallModalHeader}
filters={callModalFilters}
rows={callModalRows}
paginator={false}
showGridlines={true}
removableSort={true}
dragSelection={false}
emptyMessage='No data found.'
>
{callModalColumns.map((col) => (
<Column
key={col}
field={col}
header={col}
sortable
filter
body={rowData => {
const value = rowData[col];
if (typeof value === 'object' && value !== null) {
return <span style={{ fontFamily: 'monospace', fontSize: 12 }}>{JSON.stringify(value)}</span>;
}
return value;
}}
/>
))}
</TableGroup>
</div>
<div style={{ position: 'sticky', bottom: 0, background: '#fff', padding: '8px 0 0 0', zIndex: 2, borderTop: '1px solid #eee' }}>
<div style={{ width: '100%' }}>
<span style={{ float: 'right' }}>
<span style={{ marginRight: 16 }}>
Showing {callModalFirst + 1} to {Math.min(callModalFirst + callModalRows, callModalData.length)} of {callModalData.length} entries
</span>
<select
value={callModalRows}
onChange={e => setCallModalRows(Number(e.target.value))}
style={{ marginRight: 8 }}
>
{[10, 25, 50].map(opt => (
<option key={opt} value={opt}>{opt}</option>
))}
</select>
<button onClick={() => setCallModalFirst(0)} disabled={callModalFirst === 0}>{'<<'}</button>
<button onClick={() => setCallModalFirst(Math.max(0, callModalFirst - callModalRows))} disabled={callModalFirst === 0}>{'<'}</button>
<span style={{ margin: '0 8px' }}>{Math.floor(callModalFirst / callModalRows) + 1}</span>
<button onClick={() => setCallModalFirst(Math.min(callModalFirst + callModalRows, callModalData.length - callModalRows))} disabled={callModalFirst + callModalRows >= callModalData.length}>{'>'}</button>
<button onClick={() => setCallModalFirst(Math.max(0, callModalData.length - callModalRows))} disabled={callModalFirst + callModalRows >= callModalData.length}>{'>>'}</button>
</span>
</div>
</div>
</div>
) : (
<div>No data found or error.</div>
)}
</Dialog>
<Dialog
visible={showChatGptModal}
onHide={() => setShowChatGptModal(false)}
header="ChatGPT Assistant"
style={{ width: '50vw' }}
>
<div className="flex flex-col h-[60vh]">
<div className="flex-1 overflow-auto mb-4 p-2 bg-gray-50 rounded">
{chatMessages.length === 0 && <div className="text-gray-400 text-center mt-8">Start a conversation with ChatGPT...</div>}
{chatMessages.map((msg, idx) => (
<div key={idx} className={`my-2 p-2 rounded-lg max-w-[80%] ${msg.role === 'user' ? 'bg-blue-100 ml-auto' : 'bg-gray-200'}`}>
<span>{msg.content}</span>
</div>
))}
</div>
<div className="flex gap-2 mt-auto">
<input
type="text"
className="flex-1 p-2 border rounded"
placeholder="Type your message..."
value={chatPrompt}
onChange={e => setChatPrompt(e.target.value)}
onKeyDown={handleChatPromptKeyDown}
disabled={chatLoading}
/>
<Button
icon="pi pi-send"
onClick={() => handleChatPromptSend()}
loading={chatLoading}
disabled={!chatPrompt.trim() || chatLoading}
/>
</div>
{chatError && <div className="text-red-500 mt-2">{chatError}</div>}
</div>
</Dialog>
<Dialog
visible={showApiKeyModal}
onHide={() => setShowApiKeyModal(false)}
header="API Key Management"
footer={
<div>
<Button
label={apiKey ? "Regenerate API Key" : "Create API Key"}
icon="pi pi-refresh"
onClick={handleApiKey}
loading={apiKeyLoading}
className="p-button-danger"
/>
<Button
label="Close"
icon="pi pi-times"
onClick={() => setShowApiKeyModal(false)}
className="p-button-text"
/>
</div>
}
>
{apiKey && (
<div>
<div className="mb-2 font-bold">Current API Key:</div>
<div className="font-mono bg-gray-100 p-2 rounded">{apiKey}</div>
<div className="mt-2 text-sm text-red-500">
Regenerating will invalidate the old key.
</div>
</div>
)}
{!apiKey && (
<div>
<div>No API key exists for this app. Click "Create API Key" to generate one.</div>
</div>
)}
{apiKeyError && <div className="text-red-500 mt-2">{apiKeyError}</div>}
</Dialog>
<Dialog
visible={showVoiceModal}
onHide={() => setShowVoiceModal(false)}
header="Listening..."
modal
style={{ minWidth: 400, textAlign: 'center' }}
closable={false}
>
<div style={{ fontSize: 24, minHeight: 60 }}>{fullTranscript || <span className="text-gray-400">Say something</span>}</div>
<div className="mt-4 text-sm text-gray-500">Speak your command or query.</div>
</Dialog>
<Dialog
visible={showVoiceInfoModal}
onHide={() => setShowVoiceInfoModal(false)}
header="Voice Command List"
modal
style={{ minWidth: 400 }}
>
<ul style={{ textAlign: 'left', lineHeight: 2 }}>
<li><b>show/view/open endpoint [name or id]</b> Open an endpoint's table view</li>
<li><b>delete endpoint [name or id]</b> Delete an endpoint</li>
<li><b>search [term]</b> Filter endpoints</li>
<li><b>modify endpoint [id]</b> Start editing an endpoint by voice</li>
<li><b>method is [GET/POST/PUT/DELETE/PATCH]</b> Set method (in edit mode)</li>
<li><b>path is [your path]</b> Set path (in edit mode, say 'slash' for /, 'colon id' for :id)</li>
<li><b>open command list</b> Show this help</li>
</ul>
</Dialog>
<ManageDocsModal visible={showManageDocs} onHide={() => setShowManageDocs(false)} />
<PromptCreatorModal
visible={showPromptCreator}
onHide={() => setShowPromptCreator(false)}
appId={appId || 0}
/>
</div>
);
}

@ -0,0 +1,314 @@
import { DataTable } from 'primereact/datatable';
import { Column } from 'primereact/column';
import { useState, useEffect } from 'react';
import { useChatStore } from '../../state/stores/useChatStore';
import { Dialog } from 'primereact/dialog';
import { generateTablePrompt } from '../../prompts/table-generation';
import { Toast } from 'primereact/toast';
import { useRef } from 'react';
import { InputText } from 'primereact/inputtext';
interface ChatGPTModalProps {
content?: {
prompt: string;
id?: string;
keywords?: string[];
context?: string;
tableSchema?: {
name: string;
fields: Array<{
name: string;
type: string;
description?: string;
}>;
};
};
}
interface TableData {
columns: Array<{
field: string;
header: string;
}>;
data: any[];
metadata?: {
id?: string;
keywords?: string[];
context?: string;
timestamp: string;
actions?: Array<{
action: string;
target: string;
description: string;
}>;
};
}
export const ChatGPTModal = ({ content }: ChatGPTModalProps) => {
const [tableData, setTableData] = useState<TableData>({
columns: [],
data: [],
metadata: {
timestamp: new Date().toISOString()
}
});
const [loading, setLoading] = useState(false);
const [selectedRow, setSelectedRow] = useState<any>(null);
const [showRowDialog, setShowRowDialog] = useState(false);
const [finalPrompt, setFinalPrompt] = useState<string>('');
const { sendChatRequest } = useChatStore();
const toast = useRef<Toast>(null);
const constructPath = (id: string | number, pathname: string): string => {
// Remove any leading/trailing slashes from pathname
const cleanPathname = pathname.replace(/^\/+|\/+$/g, '');
// Construct the path with proper slashes
return `/${id}/${cleanPathname}`.replace(/\/+/g, '/');
};
const findRowById = (id: string | number) => {
const row = tableData.data.find(row => row.id?.toString() === id.toString());
if (row) {
setSelectedRow(row);
setShowRowDialog(true);
showToast('info', `Viewing details for ID: ${id}`);
} else {
showToast('warn', `No row found with ID: ${id}`);
}
};
const handlePublicPath = (prompt: string) => {
// Check for path construction pattern
const pathMatch = prompt.match(/add\s+public\s+path\s+to\s+(\d+)\s+slash\s+([^\s]+)/i);
if (pathMatch) {
const [, id, pathname] = pathMatch;
const constructedPath = constructPath(id, pathname);
// Find the row with the given ID
const rowIndex = tableData.data.findIndex(row => row.id?.toString() === id.toString());
if (rowIndex !== -1) {
// Update the path of the existing row
setTableData(prev => {
const newData = [...prev.data];
newData[rowIndex] = {
...newData[rowIndex],
path: constructedPath
};
// Add an 'add_path' action to metadata
return {
...prev,
data: newData,
metadata: {
...prev.metadata,
actions: [
...(prev.metadata?.actions || []),
{
action: 'add_path',
target: constructedPath,
description: `added public path ${constructedPath} to endpoint ID ${id}`
}
]
}
};
});
showToast('success', `Updated path for ID ${id}: ${constructedPath}`);
findRowById(id);
} else {
showToast('warn', `No endpoint found with ID: ${id}`);
}
return { id, constructedPath };
}
return null;
};
useEffect(() => {
if (content?.prompt) {
setFinalPrompt(content.prompt);
// Handle public path update only, not endpoint creation
handlePublicPath(content.prompt);
generateTable();
}
}, [content]);
const showToast = (severity: 'success' | 'info' | 'warn' | 'error', message: string) => {
toast.current?.show({
severity,
summary: severity.charAt(0).toUpperCase() + severity.slice(1),
detail: message,
life: 3000
});
};
const generateTable = async () => {
if (!content?.prompt) return;
setLoading(true);
try {
const promptText = generateTablePrompt({
prompt: content.prompt,
context: content.context,
keywords: content.keywords,
id: content.id,
tableSchema: content.tableSchema
});
const response = await sendChatRequest('/chat/completions', {
data: promptText,
responseFormat: 'json'
});
if (response?.responseText) {
try {
const parsedData = JSON.parse(response.responseText);
setTableData({
...parsedData,
metadata: {
...parsedData.metadata,
timestamp: new Date().toISOString()
}
});
// Show success message based on the primary action
const primaryAction = parsedData.metadata?.actions?.[0];
if (primaryAction) {
const actionMessage = {
create: 'Successfully created',
delete: 'Successfully deleted',
update: 'Successfully updated',
show: 'Successfully retrieved',
search: 'Search completed',
view: 'Viewing details'
}[primaryAction.action] || 'Operation completed';
showToast('success', `${actionMessage} ${primaryAction.target}`);
}
} catch (error) {
console.error('Error parsing response:', error);
showToast('error', 'Error processing the response');
}
} else {
showToast('error', 'Invalid response format');
}
} catch (error) {
console.error('Error generating table:', error);
showToast('error', 'Error generating table');
} finally {
setLoading(false);
}
};
const handleRowClick = (event: any) => {
const rowData = event.data;
setSelectedRow(rowData);
setShowRowDialog(true);
// Add view action to metadata if not present
if (tableData.metadata?.actions) {
const hasViewAction = tableData.metadata.actions.some(
action => action.action === 'view' && action.target === rowData.id
);
if (!hasViewAction) {
setTableData(prev => ({
...prev,
metadata: {
...prev.metadata,
actions: [
...(prev.metadata?.actions || []),
{
action: 'view',
target: rowData.id || 'selected row',
description: `view details for ${rowData.id || 'selected row'}`
}
]
}
}));
showToast('info', `Viewing details for ${rowData.id || 'selected row'}`);
}
}
};
const renderRowDialog = () => {
if (!selectedRow) return null;
return (
<Dialog
visible={showRowDialog}
onHide={() => setShowRowDialog(false)}
header={`View Details - ${selectedRow.id || 'Selected Row'}`}
style={{ width: '50vw' }}
>
<div className="grid">
{tableData.columns.map(col => (
<div key={col.field} className="col-12 md:col-6 p-2">
<div className="font-bold">{col.header}</div>
<div>{selectedRow[col.field] || 'N/A'}</div>
</div>
))}
</div>
</Dialog>
);
};
return (
<>
<Toast ref={toast} position="top-right" />
<div className="flex flex-column gap-3 p-4 relative">
{finalPrompt && (
<div className="mb-3">
<label className="block text-sm font-medium text-gray-700 mb-1">
Your Request
</label>
<InputText
value={finalPrompt}
readOnly
className="w-full p-2 border rounded"
style={{ backgroundColor: '#f8f9fa' }}
/>
</div>
)}
<DataTable
value={tableData.data}
loading={loading}
className="w-full"
onRowClick={handleRowClick}
selectionMode="single"
rowHover
>
{tableData.columns.map(col => (
<Column
key={col.field}
field={col.field}
header={col.header}
body={(rowData) => (
<div className="cursor-pointer hover:bg-gray-100 p-2">
{rowData[col.field] || 'N/A'}
</div>
)}
/>
))}
</DataTable>
{tableData.metadata && (
<div className="text-sm text-gray-500 mt-2">
<p>ID: {tableData.metadata.id}</p>
<p>Keywords: {tableData.metadata.keywords?.join(', ')}</p>
<p>Context: {tableData.metadata.context}</p>
<p>Generated: {new Date(tableData.metadata.timestamp).toLocaleString()}</p>
{tableData.metadata.actions && tableData.metadata.actions.length > 0 && (
<div className="mt-2">
<p className="font-bold">Detected Actions:</p>
{tableData.metadata.actions.map((action, index) => (
<div key={index} className="ml-2">
<span className="font-semibold">{action.action}:</span> {action.target}
</div>
))}
</div>
)}
</div>
)}
{renderRowDialog()}
</div>
</>
);
};
export default ChatGPTModal;

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

@ -0,0 +1,143 @@
import { useState, useEffect } from 'react';
import { Dialog } from 'primereact/dialog';
import { InputText } from 'primereact/inputtext';
import { Button } from 'primereact/button';
import { ListBox } from 'primereact/listbox';
interface ManageDocsModalProps {
visible: boolean;
onHide: () => void;
}
interface ParseResult {
rawHtml: string;
aiOutput: any;
restructured: any;
}
const LOCAL_STORAGE_KEY = 'apiDocUrls';
export function ManageDocsModal({ visible, onHide }: ManageDocsModalProps) {
const [urls, setUrls] = useState<string[]>([]);
const [selectedUrl, setSelectedUrl] = useState<string | null>(null);
const [newUrl, setNewUrl] = useState('');
const [editIndex, setEditIndex] = useState<number | null>(null);
const [editValue, setEditValue] = useState('');
const [parsing, setParsing] = useState(false);
const [parseResult, setParseResult] = useState<ParseResult | null>(null);
const [error, setError] = useState<string | null>(null);
// Load URLs from localStorage
useEffect(() => {
const saved = localStorage.getItem(LOCAL_STORAGE_KEY);
if (saved) setUrls(JSON.parse(saved));
}, []);
// Save URLs to localStorage
useEffect(() => {
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(urls));
}, [urls]);
const addUrl = () => {
if (newUrl && !urls.includes(newUrl)) {
setUrls([...urls, newUrl]);
setNewUrl('');
}
};
const removeUrl = (url: string) => {
setUrls(urls.filter(u => u !== url));
if (selectedUrl === url) setSelectedUrl(null);
};
const startEdit = (idx: number) => {
setEditIndex(idx);
setEditValue(urls[idx]);
};
const saveEdit = () => {
if (editIndex !== null && editValue) {
const newUrls = [...urls];
newUrls[editIndex] = editValue;
setUrls(newUrls);
setEditIndex(null);
setEditValue('');
}
};
const handleParse = async (url: string) => {
setParsing(true);
setError(null);
setParseResult(null);
try {
const response = await fetch('/api/parse-docs', {
method: 'POST',
body: JSON.stringify({ url }),
headers: { 'Content-Type': 'application/json' }
});
if (!response.ok) throw new Error('Failed to fetch or parse docs');
const data = await response.json();
setParseResult(data);
} catch (e: any) {
setError(e.message || 'Unknown error');
} finally {
setParsing(false);
}
};
return (
<Dialog visible={visible} onHide={onHide} header="Manage API Docs" style={{ width: '60vw' }}>
<div className="flex gap-4">
{/* URL List and Controls */}
<div style={{ minWidth: 300 }}>
<h4>Documentation URLs</h4>
<ListBox
value={selectedUrl}
options={urls.map(u => ({ label: u, value: u }))}
onChange={e => setSelectedUrl(e.value)}
style={{ width: '100%', minHeight: 200 }}
/>
<div className="flex gap-2 mt-2">
<InputText
value={newUrl}
onChange={e => setNewUrl(e.target.value)}
placeholder="Add new URL"
className="w-full"
/>
<Button label="Add" onClick={addUrl} disabled={!newUrl} />
</div>
{selectedUrl && (
<div className="flex gap-2 mt-2">
<Button label="Parse" onClick={() => handleParse(selectedUrl)} loading={parsing} />
<Button label="Remove" className="p-button-danger" onClick={() => removeUrl(selectedUrl)} />
<Button label="Edit" onClick={() => startEdit(urls.indexOf(selectedUrl))} />
</div>
)}
{editIndex !== null && (
<div className="flex gap-2 mt-2">
<InputText value={editValue} onChange={e => setEditValue(e.target.value)} className="w-full" />
<Button label="Save" onClick={saveEdit} />
<Button label="Cancel" className="p-button-secondary" onClick={() => setEditIndex(null)} />
</div>
)}
</div>
{/* Parse Results */}
<div style={{ flex: 1 }}>
{error && <div className="text-red-500 mb-2">{error}</div>}
{parseResult && (
<div>
<h4>Raw HTML (first 500 chars):</h4>
<pre style={{ maxHeight: 200, overflow: 'auto', background: '#f8f9fa', padding: 8 }}>{parseResult.rawHtml?.slice(0, 500)}...</pre>
<h4>AI Output:</h4>
<pre style={{ maxHeight: 200, overflow: 'auto', background: '#f8f9fa', padding: 8 }}>{JSON.stringify(parseResult.aiOutput, null, 2)}</pre>
<h4>Restructured Output:</h4>
<pre style={{ maxHeight: 200, overflow: 'auto', background: '#f8f9fa', padding: 8 }}>{JSON.stringify(parseResult.restructured, null, 2)}</pre>
</div>
)}
</div>
</div>
</Dialog>
);
}
export default ManageDocsModal;

@ -0,0 +1,65 @@
import { useState } from 'react';
import { Dialog } from 'primereact/dialog';
import { InputText } from 'primereact/inputtext';
import { Button } from 'primereact/button';
interface ParseDocsModalProps {
visible: boolean;
onHide: () => void;
onParsed?: (endpoints: any[]) => void;
}
export function ParseDocsModal({ visible, onHide, onParsed }: ParseDocsModalProps) {
const [url, setUrl] = useState('');
const [loading, setLoading] = useState(false);
const [results, setResults] = useState<any>(null);
const [error, setError] = useState<string | null>(null);
const handleParse = async () => {
setLoading(true);
setError(null);
setResults(null);
try {
const response = await fetch('/api/parse-docs', {
method: 'POST',
body: JSON.stringify({ url }),
headers: { 'Content-Type': 'application/json' }
});
if (!response.ok) throw new Error('Failed to fetch or parse docs');
const data = await response.json();
setResults(data);
if (onParsed && data.restructured) onParsed(data.restructured);
} catch (e: any) {
setError(e.message || 'Unknown error');
} finally {
setLoading(false);
}
};
return (
<Dialog visible={visible} onHide={onHide} header="Parse API Docs" style={{ width: '50vw' }}>
<div>
<InputText
value={url}
onChange={e => setUrl(e.target.value)}
placeholder="Enter documentation URL"
className="w-full mb-3"
/>
<Button label="Parse" onClick={handleParse} loading={loading} disabled={!url} />
{error && <div className="text-red-500 mt-2">{error}</div>}
</div>
{results && (
<div className="mt-4">
<h4>Raw HTML (first 500 chars):</h4>
<pre style={{ maxHeight: 200, overflow: 'auto', background: '#f8f9fa', padding: 8 }}>{results.rawHtml?.slice(0, 500)}...</pre>
<h4>AI Output:</h4>
<pre style={{ maxHeight: 200, overflow: 'auto', background: '#f8f9fa', padding: 8 }}>{JSON.stringify(results.aiOutput, null, 2)}</pre>
<h4>Restructured Output:</h4>
<pre style={{ maxHeight: 200, overflow: 'auto', background: '#f8f9fa', padding: 8 }}>{JSON.stringify(results.restructured, null, 2)}</pre>
</div>
)}
</Dialog>
);
}
export default ParseDocsModal;

@ -0,0 +1,153 @@
import { useState } from 'react';
import { Dialog } from 'primereact/dialog';
import { InputText } from 'primereact/inputtext';
import { Dropdown } from 'primereact/dropdown';
import { Button } from 'primereact/button';
import { Toast } from 'primereact/toast';
import { useRef } from 'react';
import { api } from '../../services/api';
import { InputTextarea } from 'primereact/inputtextarea';
interface PromptCreatorModalProps {
visible: boolean;
onHide: () => void;
appId: number;
}
const promptTypes = [
{ label: 'API Documentation', value: 'api_documentation' },
{ label: 'Endpoint Analysis', value: 'endpoint_analysis' },
{ label: 'Schema Generation', value: 'schema_generation' },
{ label: 'Custom', value: 'custom' }
];
export default function PromptCreatorModal({ visible, onHide, appId }: PromptCreatorModalProps) {
const [name, setName] = useState('');
const [content, setContent] = useState('');
const [type, setType] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
const toast = useRef<Toast>(null);
const handleSubmit = async () => {
if (!name || !content || !type) {
toast.current?.show({
severity: 'error',
summary: 'Error',
detail: 'Please fill in all required fields',
life: 3000
});
return;
}
setLoading(true);
try {
const response = await api('post', '/api/v1/prompts', {
name,
content,
type,
appId,
metadata: {
source: 'canvas-api',
timestamp: new Date().toISOString()
}
});
toast.current?.show({
severity: 'success',
summary: 'Success',
detail: 'Prompt created successfully',
life: 3000
});
// Process the prompt immediately
await api('post', `/api/v1/prompts/${response.data.id}/process`);
// Reset form
setName('');
setContent('');
setType(null);
onHide();
} catch (error) {
console.error('Error creating prompt:', error);
toast.current?.show({
severity: 'error',
summary: 'Error',
detail: 'Failed to create prompt',
life: 3000
});
} finally {
setLoading(false);
}
};
return (
<>
<Toast ref={toast} />
<Dialog
visible={visible}
onHide={onHide}
header="Create New Prompt"
style={{ width: '50vw' }}
footer={
<div className="flex justify-end gap-2">
<Button
label="Cancel"
icon="pi pi-times"
onClick={onHide}
className="p-button-text"
/>
<Button
label="Create"
icon="pi pi-check"
onClick={handleSubmit}
loading={loading}
disabled={!name || !content || !type}
/>
</div>
}
>
<div className="flex flex-col gap-4 p-4">
<div className="field">
<label htmlFor="name" className="block text-sm font-medium text-gray-700 mb-1">
Prompt Name
</label>
<InputText
id="name"
value={name}
onChange={(e) => setName(e.target.value)}
className="w-full"
placeholder="Enter a name for your prompt..."
/>
</div>
<div className="field">
<label htmlFor="type" className="block text-sm font-medium text-gray-700 mb-1">
Prompt Type
</label>
<Dropdown
id="type"
value={type}
options={promptTypes}
onChange={(e) => setType(e.value)}
placeholder="Select a type"
className="w-full"
/>
</div>
<div className="field">
<label htmlFor="content" className="block text-sm font-medium text-gray-700 mb-1">
Prompt Content
</label>
<InputTextarea
id="content"
value={content}
onChange={(e) => setContent(e.target.value)}
className="w-full"
rows={8}
autoResize
placeholder="Enter your prompt..."
/>
</div>
</div>
</Dialog>
</>
);
}

@ -0,0 +1,61 @@
import axios, { AxiosRequestConfig, AxiosResponse } from 'axios';
interface ApiConfig {
baseURL?: string;
timeout?: number;
headers?: Record<string, string>;
}
// Clean base URL once at top-level
const rawBaseUrl = import.meta.env.VITE_API_BASE_URL || '';
const cleanedBaseUrl = rawBaseUrl.replace(/\/$/, '');
const DEFAULT_CONFIG: ApiConfig = {
baseURL: cleanedBaseUrl,
timeout: 30000,
headers: {
'Content-Type': 'application/json',
},
};
export const api = async <T = any>(
method: 'get' | 'post' | 'put' | 'delete' | 'patch',
url: string,
data?: any,
params?: any,
responseType: 'json' | 'blob' | 'text' = 'json',
timeout?: number,
useAuth: boolean = true
): Promise<AxiosResponse<T>> => {
const config: AxiosRequestConfig = {
...DEFAULT_CONFIG,
method,
url,
data,
params,
responseType,
timeout: timeout || DEFAULT_CONFIG.timeout,
};
if (useAuth) {
const token = localStorage.getItem('token');
if (token) {
config.headers = {
...config.headers,
Authorization: `Bearer ${token}`,
};
}
}
try {
return await axios(config);
} catch (error) {
if (axios.isAxiosError(error)) {
if (error.response?.status === 401) {
localStorage.removeItem('token');
window.location.href = '/login';
}
}
throw error;
}
};

@ -0,0 +1,78 @@
import { api } from './api';
interface CanvasApiConfig {
baseUrl: string;
accessToken: string;
courseId?: number;
}
const DEFAULT_CANVAS_CONFIG: CanvasApiConfig = {
baseUrl: import.meta.env.VITE_CANVAS_API_BASE_URL || 'https://canvas.instructure.com',
accessToken: import.meta.env.VITE_CANVAS_ACCESS_TOKEN || '',
};
/**
* Constructs a Canvas API endpoint URL with optional parameters
*/
export const constructCanvasEndpoint = (path: string, params?: Record<string, any>) => {
const queryString = params ? `?${new URLSearchParams(params).toString()}` : '';
return `${path}${queryString}`;
};
/**
* Makes a request to the Canvas API
*/
export const getCanvasData = async <T = any>(
endpoint: string,
params?: Record<string, any>,
config: Partial<CanvasApiConfig> = {}
): Promise<T> => {
const finalConfig = { ...DEFAULT_CANVAS_CONFIG, ...config };
const url = constructCanvasEndpoint(endpoint, params);
try {
const response = await api<T>('get', url, undefined, undefined, 'json', 60, true);
return response.data;
} catch (error) {
console.error('Canvas API Error:', error);
throw error;
}
};
// Example usage:
// const students = await getCanvasData('/courses/123/users', { enrollment_type: ['student'] });
// const courses = await getCanvasData('/courses', { per_page: 100 });
class CanvasApi {
private accessToken: string;
private baseUrl: string;
constructor(config: CanvasApiConfig) {
this.accessToken = config.accessToken;
this.baseUrl = config.baseUrl || DEFAULT_CANVAS_CONFIG.baseUrl;
}
private getHeaders() {
return {
Authorization: `Bearer ${this.accessToken}`,
'Content-Type': 'application/json',
};
}
async getStudents(courseId: number) {
try {
const response = await getCanvasData('/courses/' + courseId + '/users', {
enrollment_type: ['student'],
per_page: 100,
});
return response;
} catch (error) {
console.error('Error fetching students:', error);
throw error;
}
}
// Add more Canvas API methods here as needed
}
export default CanvasApi;

@ -0,0 +1,35 @@
import { create } from 'zustand';
import { api } from '../../services/api';
interface CanvasRequestData {
endpoint: string;
params?: Record<string, any>;
courseId?: number;
}
type CanvasStore = {
sendCanvasRequest: (data: CanvasRequestData) => Promise<any>;
};
export const useCanvasStore = create<CanvasStore>(() => ({
sendCanvasRequest: async (requestData) => {
try {
if (!requestData.endpoint) {
throw new Error("Invalid input: 'endpoint' is required.");
}
// Construct the full URL with course ID if provided
const url = requestData.courseId
? `/courses/${requestData.courseId}${requestData.endpoint}`
: requestData.endpoint;
console.log('Sending Canvas request:', { url, params: requestData.params });
const response = await api('get', url, undefined, requestData.params, 'json', 60, true);
console.log('Got Canvas response:', response);
return response.data;
} catch (error) {
console.error('Failed to send Canvas request:', error);
throw error;
}
},
}));

@ -0,0 +1,13 @@
interface CanvasConfig {
accessToken: string;
baseUrl: string;
defaultCourseId: number;
}
const canvasConfig: CanvasConfig = {
accessToken: process.env.VITE_CANVAS_ACCESS_TOKEN || '',
baseUrl: process.env.VITE_CANVAS_API_BASE_URL || 'https://canvas.instructure.com/api/v1',
defaultCourseId: parseInt(process.env.VITE_CANVAS_DEFAULT_COURSE_ID || '1', 10),
};
export default canvasConfig;

@ -0,0 +1,75 @@
export interface ActionKeyword {
action: string;
target: string;
description: string;
}
export const ACTION_KEYWORDS: Record<string, string[]> = {
create: ['create', 'new', 'make'],
delete: ['delete', 'remove', 'drop'],
update: ['update', 'modify', 'change', 'edit'],
show: ['show', 'display', 'list', 'get', 'fetch', 'view'],
search: ['search', 'find', 'query', 'filter'],
view: ['view', 'details', 'info', 'expand']
};
export const detectActions = (prompt: string, keywords: string[]): ActionKeyword[] => {
const actions: ActionKeyword[] = [];
const promptLower = prompt.toLowerCase().trim();
// If prompt starts with 'add', treat as 'add' action
if (promptLower.startsWith('add')) {
// Try to match 'add ... to [id] slash [path]'
const match = promptLower.match(/add.*to\s+(\d+)\s+slash\s+([^\s]+)/i);
if (match) {
const [, id, pathname] = match;
actions.push({
action: 'add',
target: `id:${id} path:/${pathname.replace(/^\/+|\/+$/g, '')}`,
description: `add /${pathname.replace(/^\/+|\/+$/g, '')} to endpoint ID ${id}`
});
} else {
// Fallback: just mark as a generic add action
actions.push({
action: 'add',
target: '',
description: 'add action'
});
}
}
// First check for explicit action-target pairs
const words = promptLower.split(' ');
for (let i = 0; i < words.length - 1; i++) {
const word = words[i];
const nextWord = words[i + 1];
for (const [action, triggers] of Object.entries(ACTION_KEYWORDS)) {
if (triggers.includes(word)) {
actions.push({
action,
target: nextWord,
description: `${action} ${nextWord}`
});
}
}
}
// Then check keywords for additional context
keywords.forEach(keyword => {
const keywordWords = keyword.toLowerCase().split(' ');
for (const [action, triggers] of Object.entries(ACTION_KEYWORDS)) {
if (triggers.some(trigger => keywordWords.includes(trigger))) {
const target = keywordWords.find(word => !triggers.includes(word));
if (target) {
actions.push({
action,
target,
description: `${action} ${target}`
});
}
}
}
});
return actions;
};

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

195
frontend/src/index.css Normal file

@ -0,0 +1,195 @@
@import 'primereact/resources/themes/saga-blue/theme.css';
@import 'primereact/resources/primereact.min.css';
@import 'primeicons/primeicons.css';
@import './styles/canvas-theme.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; */
z-index: 100;
}
.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-wrapper {
overflow: initial !important;
margin: 0 0 80px 0;
width: 100%;
}
.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;
}
/* 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;
}
/* Ensure custom header in .p-datatable-header fills the width */
.p-datatable-header > div {
width: 100% !important;
min-width: 0 !important;
background: #fff !important;
display: flex !important;
}

@ -0,0 +1,114 @@
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' data-system={systemId}>
<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,109 @@
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 roc_logo from '../assets/roc_logo.png';
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,
},
canvas: {
routes: [
{ path: 'canvas-endpoints', label: 'Canvas Endpoints', icon: 'pi pi-table' }
],
logo: roc_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='/'>
<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>
);

@ -0,0 +1,75 @@
import { ActionKeyword, detectActions } from '../constants/actions';
interface TableGenerationPromptParams {
prompt: string;
context?: string;
keywords?: string[];
id?: string;
tableSchema?: {
name: string;
fields: Array<{
name: string;
type: string;
description?: string;
}>;
};
}
export const generateTablePrompt = (params: TableGenerationPromptParams): string => {
const {
prompt,
context = 'General information',
keywords = [],
id = 'auto-generated',
tableSchema
} = params;
const detectedActions = detectActions(prompt, keywords);
return `Create a table with data about: ${prompt}
Context: ${context}
Keywords: ${keywords.join(', ') || 'None specified'}
${tableSchema ? `
Table Schema:
Name: ${tableSchema.name}
Fields:
${tableSchema.fields.map(f => `- ${f.name} (${f.type})${f.description ? `: ${f.description}` : ''}`).join('\n')}
` : ''}
Detected Actions:
${detectedActions.map(action => `- ${action.description}`).join('\n')}
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"}
],
"metadata": {
"id": "${id}",
"keywords": ${JSON.stringify(keywords)},
"context": "${context}",
"timestamp": "${new Date().toISOString()}",
"actions": ${JSON.stringify(detectedActions)}
}
}
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. Ensure all data is relevant to the context and keywords
6. Format dates in ISO format if present
7. Use consistent data types within columns
8. If table schema is provided, strictly follow the field names and types
9. Ensure all required fields from the schema are present in the data
10. Format the data according to the specified types in the schema
11. For multiple actions, prioritize the first action mentioned
12. When creating endpoints, include all related actions in the endpoint description
13. For row click actions, use the 'view' action type`;
};

53
frontend/src/router.tsx Normal file

@ -0,0 +1,53 @@
import { createBrowserRouter, Navigate } from 'react-router-dom';
import { useParams } from 'react-router-dom';
import DualModalComponent from './layouts/DualModal';
import CouncilAI from './components/CouncilAI/CouncilAI';
import FuseMindHome from './components/FuseMind/FuseMindHome';
import CanvasEndpoints from './components/canvas-api/CanvasEndpoints';
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: 'endpoints',
element: <CanvasEndpoints />,
},
{
path: 'canvas-endpoints',
element: <CanvasEndpoints />,
},
],
},
],
},
{
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';
// Clean server path, removing trailing slash if present
const rawBaseUrl = import.meta.env.VITE_API_BASE_URL || '';
const serverPath = rawBaseUrl.replace(/\/$/, '');
type THttpMethod = 'get' | 'put' | 'post' | 'delete';
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',
};
if (!skipAuth && token) {
headers['Authorization'] = `Bearer ${token}`;
}
const axiosInstance = axios.create({
baseURL: serverPath,
timeout: timeoutInSeconds * 1000,
headers,
withCredentials: !skipAuth,
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: any) {
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,456 @@
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 = {
period: formationContext.temporalContext.period,
moodState: this.getMoodState(),
cognitiveBias: this.getActiveBiases()
};
// Apply emotional biases
memory.emotional.triggers.push(...formationContext.emotionalInfluence.biasEffects.map(bias => ({
type: 'contextual' as const,
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;
});
}
private async retrieveMemoryFromStorage(query: MemoryRetrieval): Promise<MemoryUnit[]> {
// TODO: Implement actual storage retrieval
return [];
}
async consolidateMemories(consolidation: MemoryConsolidation): Promise<MemoryUnit> {
const memories = await Promise.all(
consolidation.sourceMemories.map(id => this.retrieveMemory({
query: id,
context: {},
filters: {}
}))
).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,
context: {},
filters: {}
}))
).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,37 @@
import { useRef } from 'react';
import { Button } from 'primereact/button';
import { Menu } from 'primereact/menu';
export interface SettingsMenuItem {
label: string;
icon?: string;
command?: () => void;
}
interface SettingsMenuProps {
items: SettingsMenuItem[];
buttonClassName?: string;
}
const SettingsMenu: React.FC<SettingsMenuProps> = ({ items, buttonClassName }) => {
const menuRef = useRef<any>(null);
return (
<>
<Button
icon="pi pi-cog"
aria-label="Settings"
onClick={e => menuRef.current.toggle(e)}
className={buttonClassName || ''}
tooltip="Settings"
/>
<Menu
model={items}
popup
ref={menuRef}
/>
</>
);
};
export default SettingsMenu;

@ -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 '../../../components/canvas-api/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,45 @@
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-4-mini',
response_format: requestData.responseFormat === 'json' ? { type: 'json_object' } : undefined
};
console.log('Sending formatted request:', formattedRequest);
const response = await api('post', endpoint, formattedRequest, undefined, 'json', 60, true);
console.log('Got API response:', response);
if (!response || !response.data) {
throw new Error('Invalid response from server');
}
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,100 @@
/* Canvas API specific theme overrides */
[data-system="canvas"] {
/* Override all teal colors with #e70022 (red) */
--canvas-primary: #e70022;
--canvas-primary-light: #ff1a3d;
--canvas-primary-dark: #cc001f;
--canvas-button-text: #f3f4f6; /* light gray for button text */
}
/* Override teal colors in Canvas API system */
[data-system="canvas"] .p-button {
background-color: var(--canvas-primary) !important;
border-color: var(--canvas-primary) !important;
color: var(--canvas-button-text) !important;
}
[data-system="canvas"] .p-button:hover {
background-color: var(--canvas-primary-dark) !important;
border-color: var(--canvas-primary-dark) !important;
color: var(--canvas-button-text) !important;
}
[data-system="canvas"] .p-button:hover:not(.p-disabled) {
background-color: var(--canvas-primary-dark) !important;
border-color: var(--canvas-primary-dark) !important;
}
[data-system="canvas"] .p-button.p-button-outlined {
color: var(--canvas-primary) !important;
border-color: var(--canvas-primary) !important;
background: transparent !important;
}
[data-system="canvas"] .p-button.p-button-outlined:hover {
background-color: var(--canvas-primary) !important;
color: var(--canvas-button-text) !important;
}
/* Override teal in tabview */
[data-system="canvas"] .p-tabview .p-tabview-nav li.p-highlight .p-tabview-nav-link {
color: var(--canvas-primary) !important;
border-color: var(--canvas-primary) !important;
}
[data-system="canvas"] .p-tabview .p-tabview-nav li .p-tabview-nav-link:hover {
color: var(--canvas-primary) !important;
}
/* Override teal in dropdowns */
[data-system="canvas"] .p-dropdown:not(.p-disabled).p-focus {
border-color: var(--canvas-primary) !important;
box-shadow: 0 0 0 1px var(--canvas-primary) !important;
}
[data-system="canvas"] .p-dropdown-panel .p-dropdown-items .p-dropdown-item.p-highlight {
background-color: var(--canvas-primary) !important;
}
/* Override teal in checkboxes */
[data-system="canvas"] .p-checkbox .p-checkbox-box.p-highlight {
background-color: var(--canvas-primary) !important;
border-color: var(--canvas-primary) !important;
}
/* Override teal in links */
[data-system="canvas"] .link-style {
color: var(--canvas-primary) !important;
}
[data-system="canvas"] .link-style:hover {
color: var(--canvas-primary-dark) !important;
}
/* Override teal in borders */
[data-system="canvas"] .border-teal-500 {
border-color: var(--canvas-primary) !important;
}
/* Override teal in text */
[data-system="canvas"] .text-teal-500,
[data-system="canvas"] .text-teal-600,
[data-system="canvas"] .text-teal-700 {
color: var(--canvas-primary) !important;
}
/* Override teal in backgrounds */
[data-system="canvas"] .bg-teal-500,
[data-system="canvas"] .bg-teal-600,
[data-system="canvas"] .bg-teal-700 {
background-color: var(--canvas-primary) !important;
}
/* Override teal in focus states */
[data-system="canvas"] .focus\:border-teal-500:focus {
border-color: var(--canvas-primary) !important;
}
[data-system="canvas"] .focus\:ring-teal-500:focus {
--tw-ring-color: var(--canvas-primary) !important;
}

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

Some files were not shown because too many files have changed in this diff Show More