Compare commits
No commits in common. "1152abd4a55ab6283f110d475019174782a3b912" and "48a04969bcf23f6c83bc0816a9a5afbbfc3e46cb" have entirely different histories.
1152abd4a5
...
48a04969bc
@ -11,13 +11,6 @@ JWT_SECRET=sdfj94mfm430f72m3487rdsjiy7834n9rnf934n8r3n490fn4u83fh894hr9nf0
|
|||||||
# SERVER_BASEPATH_API=v1/
|
# SERVER_BASEPATH_API=v1/
|
||||||
# TIMEZONE=Europe/Amsterdam
|
# TIMEZONE=Europe/Amsterdam
|
||||||
|
|
||||||
|
|
||||||
# Default Admin User
|
|
||||||
DEFAULT_ADMIN_USERNAME=admin
|
|
||||||
DEFAULT_ADMIN_EMAIL=darren@fusero.nl
|
|
||||||
DEFAULT_ADMIN_PASSWORD=admin123
|
|
||||||
|
|
||||||
|
|
||||||
FASTIFY_PORT=14000
|
FASTIFY_PORT=14000
|
||||||
|
|
||||||
# [ Database ]
|
# [ Database ]
|
||||||
|
3
.gitignore
vendored
@ -3,7 +3,8 @@ node_modules/
|
|||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
yarn-debug.log*
|
yarn-debug.log*
|
||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
|
package-lock.json
|
||||||
|
yarn.lock
|
||||||
|
|
||||||
# Build output
|
# Build output
|
||||||
dist/
|
dist/
|
||||||
|
497
README.md
@ -1,211 +1,346 @@
|
|||||||
# Fusero App Boilerplate
|
# Fusero Boilerplate App
|
||||||
|
|
||||||
A full-stack application boilerplate with React frontend and Node.js backend.
|
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.
|
||||||
|
|
||||||
## Project Structure
|
---
|
||||||
|
|
||||||
```
|
## 1. Prerequisites
|
||||||
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
|
- [Node.js](https://nodejs.org/) (v18+ recommended)
|
||||||
|
- [Docker](https://www.docker.com/get-started)
|
||||||
|
- [npm](https://www.npmjs.com/)
|
||||||
|
|
||||||
- Node.js (v18 or higher)
|
---
|
||||||
- npm (v9 or higher)
|
|
||||||
- Docker and Docker Compose
|
|
||||||
- Git
|
|
||||||
|
|
||||||
## Development Setup
|
## 2. Clone the Repo
|
||||||
|
|
||||||
### 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.
|
|
||||||
|
|
||||||
To start the database:
|
|
||||||
```bash
|
```bash
|
||||||
docker-compose -f docker-compose.dev.yml up db
|
git clone <your-repo-url>
|
||||||
|
cd fusero-app-boilerplate
|
||||||
```
|
```
|
||||||
|
|
||||||
### Option 1: Running Everything in Docker (Recommended for Development)
|
---
|
||||||
|
|
||||||
1. **Start the Development Environment**
|
## 3. Setup the `.env` File
|
||||||
```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)
|
Copy `.env.example` to `.env` (or create `.env` if not present):
|
||||||
|
|
||||||
For better debugging experience, you can run the frontend and backend in separate terminal windows, while keeping the database in Docker:
|
```env
|
||||||
|
# Database connection for Docker
|
||||||
|
POSTGRES_NAME=fusero-boilerplate-db
|
||||||
|
POSTGRES_HOSTNAME=localhost
|
||||||
|
POSTGRES_PORT=19095
|
||||||
|
POSTGRES_USER=root
|
||||||
|
POSTGRES_PASSWORD=root123
|
||||||
|
|
||||||
1. **First, ensure the database is running in Docker**
|
# Test Database connection
|
||||||
```bash
|
POSTGRES_TEST_NAME=test-db
|
||||||
docker-compose -f docker-compose.dev.yml up db
|
POSTGRES_TEST_PORT=19096
|
||||||
```
|
|
||||||
|
|
||||||
2. **Then, in separate terminal windows:**
|
# 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
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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
|
||||||
|
|
||||||
#### Terminal 1: Backend Service
|
|
||||||
```bash
|
```bash
|
||||||
cd backend
|
|
||||||
npm install
|
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
|
npm run dev
|
||||||
```
|
```
|
||||||
The backend will be available at http://localhost:14000
|
|
||||||
|
|
||||||
#### Terminal 2: Frontend Service
|
The app will be available at [http://localhost:14000](http://localhost:14000).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. API Endpoints
|
||||||
|
|
||||||
|
### Authentication
|
||||||
|
|
||||||
|
#### Login
|
||||||
```bash
|
```bash
|
||||||
cd frontend
|
curl -X POST http://localhost:14000/api/v1/auth/login \
|
||||||
npm install
|
-H "Content-Type: application/json" \
|
||||||
npm run dev
|
-d '{
|
||||||
```
|
"username": "darren",
|
||||||
The frontend will be available at http://localhost:3000
|
"password": "admin123"
|
||||||
|
}'
|
||||||
### 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.
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Example ChatGPT Response
|
Response:
|
||||||
```
|
```json
|
||||||
{
|
{
|
||||||
"name": "Create Course",
|
"success": true,
|
||||||
"method": "POST",
|
"message": "Authentication successful",
|
||||||
"path": "/courses",
|
"data": {
|
||||||
"description": "Creates a new course in Canvas."
|
"token": "your.jwt.token",
|
||||||
|
"user": {
|
||||||
|
"id": 1,
|
||||||
|
"username": "darren",
|
||||||
|
"email": "darren@fusero.nl",
|
||||||
|
"roles": ["admin"]
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Developer Notes
|
### User Management
|
||||||
- The ChatGPT modal logic is in `frontend/src/components/CanvasEndpoints.tsx`.
|
|
||||||
- The backend endpoint creation API is `/api/v1/canvas-api/endpoints`.
|
#### Create User
|
||||||
- The system expects ChatGPT to return a JSON object with at least `name`, `method`, and `path`.
|
```bash
|
||||||
- The endpoint list is auto-refreshed after creation.
|
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.
|
||||||
|
|
||||||
---
|
---
|
@ -1,5 +1,3 @@
|
|||||||
version: '3.8'
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
fusero-boilerplate-db:
|
fusero-boilerplate-db:
|
||||||
image: postgres:15
|
image: postgres:15
|
||||||
|
@ -1,97 +1,91 @@
|
|||||||
version: '3.8'
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
fusero-app-frontend:
|
fusero-frontend:
|
||||||
container_name: fusero-app-frontend
|
container_name: fusero-frontend
|
||||||
build:
|
env_file: ../fusero-frontend/.env
|
||||||
context: ./frontend
|
build:
|
||||||
dockerfile: Dockerfile
|
context: ../fusero-frontend
|
||||||
ports:
|
dockerfile: Dockerfile.dev
|
||||||
- '3000:80'
|
ports:
|
||||||
networks:
|
- '3000:80'
|
||||||
- fusero-network
|
networks:
|
||||||
depends_on:
|
- fusero-network
|
||||||
- fusero-app-backend
|
|
||||||
|
|
||||||
fusero-app-frontend-dev:
|
fusero-app-boilerplate:
|
||||||
container_name: fusero-app-frontend-dev
|
environment:
|
||||||
build:
|
- POSTGRES_HOST=fusero-app-db
|
||||||
context: ./frontend
|
build:
|
||||||
dockerfile: Dockerfile
|
context: .
|
||||||
ports:
|
dockerfile: Dockerfile
|
||||||
- '8080:8080'
|
env_file: .env
|
||||||
volumes:
|
restart: always
|
||||||
- ./frontend:/app
|
ports:
|
||||||
- /app/node_modules
|
- '5000:14000'
|
||||||
environment:
|
depends_on:
|
||||||
- NODE_ENV=development
|
- fusero-app-db
|
||||||
command: npm run dev
|
container_name: fusero-app-boilerplate
|
||||||
networks:
|
networks:
|
||||||
- fusero-network
|
- fusero-network
|
||||||
depends_on:
|
|
||||||
- fusero-app-backend
|
|
||||||
|
|
||||||
fusero-app-backend:
|
fusero-app-db:
|
||||||
build:
|
image: postgres:15
|
||||||
context: .
|
env_file: .env
|
||||||
dockerfile: Dockerfile
|
restart: always
|
||||||
env_file: .env
|
volumes:
|
||||||
restart: always
|
- fusero_app_pgdata:/var/lib/postgresql/data
|
||||||
ports:
|
ports:
|
||||||
- '5000:14000'
|
- '19090:5432'
|
||||||
depends_on:
|
container_name: fusero-app-db
|
||||||
- fusero-boilerplate-db
|
networks:
|
||||||
container_name: fusero-app-backend
|
- fusero-network
|
||||||
networks:
|
|
||||||
- fusero-network
|
|
||||||
|
|
||||||
fusero-boilerplate-db:
|
fusero-app-test-db:
|
||||||
image: postgres:15
|
image: postgres:15
|
||||||
env_file: .env
|
env_file: .env
|
||||||
restart: always
|
restart: always
|
||||||
volumes:
|
volumes:
|
||||||
- fusero_boilerplate_pgdata:/var/lib/postgresql/data
|
- fusero_app_test_pgdata:/var/lib/postgresql/data
|
||||||
ports:
|
ports:
|
||||||
- '19095:5432'
|
- '19091:5432'
|
||||||
container_name: fusero-boilerplate-db
|
container_name: fusero-app-test-db
|
||||||
networks:
|
networks:
|
||||||
- fusero-network
|
- fusero-network
|
||||||
|
environment:
|
||||||
|
- POSTGRES_DB=test-db
|
||||||
|
|
||||||
fusero-boilerplate-test-db:
|
# ngrok:
|
||||||
image: postgres:15
|
# image: ngrok/ngrok:latest
|
||||||
env_file: .env
|
# restart: unless-stopped
|
||||||
restart: always
|
# command:
|
||||||
volumes:
|
# - 'start'
|
||||||
- fusero_boilerplate_test_pgdata:/var/lib/postgresql/data
|
# - '--all'
|
||||||
ports:
|
# - '--config'
|
||||||
- '19096:5432'
|
# - '/etc/ngrok.yml'
|
||||||
container_name: fusero-boilerplate-test-db
|
# volumes:
|
||||||
networks:
|
# - ./ngrok.yml:/etc/ngrok.yml
|
||||||
- fusero-network
|
# ports:
|
||||||
environment:
|
# - 19095:4040
|
||||||
- POSTGRES_DB=test-db
|
# networks:
|
||||||
|
# - fusero-network
|
||||||
|
|
||||||
nginx:
|
# fusero-redis:
|
||||||
image: nginx:alpine
|
# image: redis:7-alpine
|
||||||
container_name: fusero-nginx
|
# restart: always
|
||||||
ports:
|
# ports:
|
||||||
- '14001:80'
|
# - '6379:6379'
|
||||||
- '14443:443'
|
# volumes:
|
||||||
volumes:
|
# - redis_data:/data
|
||||||
- ./nginx/nginx.conf.prod:/etc/nginx/conf.d/default.conf:ro
|
# container_name: fusero-redis
|
||||||
- ./nginx/certs:/etc/nginx/certs:ro
|
# networks:
|
||||||
depends_on:
|
# - fusero-network
|
||||||
- fusero-app-frontend
|
|
||||||
- fusero-app-backend
|
|
||||||
networks:
|
|
||||||
- fusero-network
|
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
fusero_boilerplate_pgdata:
|
redis_data:
|
||||||
external: true
|
fusero_app_pgdata:
|
||||||
fusero_boilerplate_test_pgdata:
|
external: true
|
||||||
external: false
|
fusero_app_test_pgdata:
|
||||||
|
external: false
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
fusero-network:
|
fusero-network:
|
||||||
name: fusero-network
|
name: fusero-network
|
||||||
|
@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"semi": true,
|
|
||||||
"trailingComma": "es5",
|
|
||||||
"singleQuote": true,
|
|
||||||
"jsxSingleQuote": true,
|
|
||||||
"tabWidth": 2,
|
|
||||||
"useTabs": false,
|
|
||||||
"printWidth": 100,
|
|
||||||
"bracketSpacing": true
|
|
||||||
}
|
|
@ -1,31 +0,0 @@
|
|||||||
# 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;"]
|
|
@ -1,18 +0,0 @@
|
|||||||
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"]
|
|
@ -1,19 +0,0 @@
|
|||||||
<!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>
|
|
@ -1,19 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
@ -1,27 +0,0 @@
|
|||||||
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
@ -1,45 +0,0 @@
|
|||||||
{
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,6 +0,0 @@
|
|||||||
export default {
|
|
||||||
plugins: {
|
|
||||||
tailwindcss: {},
|
|
||||||
autoprefixer: {},
|
|
||||||
},
|
|
||||||
}
|
|
Before Width: | Height: | Size: 26 KiB |
Before Width: | Height: | Size: 80 KiB |
Before Width: | Height: | Size: 24 KiB |
Before Width: | Height: | Size: 759 B |
Before Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 15 KiB |
@ -1 +0,0 @@
|
|||||||
{"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"}
|
|
Before Width: | Height: | Size: 72 KiB |
@ -1 +0,0 @@
|
|||||||
<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>
|
|
Before Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 266 KiB |
@ -1,38 +0,0 @@
|
|||||||
<?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>
|
|
Before Width: | Height: | Size: 3.0 KiB |
Before Width: | Height: | Size: 74 KiB |
Before Width: | Height: | Size: 482 KiB |
@ -1 +0,0 @@
|
|||||||
<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>
|
|
Before Width: | Height: | Size: 4.0 KiB |
Before Width: | Height: | Size: 41 KiB |
Before Width: | Height: | Size: 1.0 MiB |
@ -1,23 +0,0 @@
|
|||||||
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;
|
|
@ -1,40 +0,0 @@
|
|||||||
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;
|
|
@ -1,32 +0,0 @@
|
|||||||
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;
|
|
@ -1,103 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,84 +0,0 @@
|
|||||||
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;
|
|
@ -1,83 +0,0 @@
|
|||||||
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;
|
|
@ -1,54 +0,0 @@
|
|||||||
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;
|
|
@ -1,17 +0,0 @@
|
|||||||
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;
|
|
@ -1,203 +0,0 @@
|
|||||||
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;
|
|
@ -1,189 +0,0 @@
|
|||||||
# 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
|
|
@ -1,117 +0,0 @@
|
|||||||
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;
|
|
@ -1,51 +0,0 @@
|
|||||||
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;
|
|
@ -1,66 +0,0 @@
|
|||||||
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;
|
|
@ -1,17 +0,0 @@
|
|||||||
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;
|
|
@ -1,144 +0,0 @@
|
|||||||
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;
|
|
@ -1,56 +0,0 @@
|
|||||||
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`;
|
|
@ -1,55 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
};
|
|
@ -1,23 +0,0 @@
|
|||||||
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}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
@ -1,63 +0,0 @@
|
|||||||
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;
|
|
||||||
};
|
|
@ -1,10 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { ModalProvider as Provider } from './ModalContext';
|
|
||||||
|
|
||||||
export const ModalProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
|
||||||
return (
|
|
||||||
<Provider>
|
|
||||||
{children}
|
|
||||||
</Provider>
|
|
||||||
);
|
|
||||||
};
|
|
@ -1,44 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
@ -1,113 +0,0 @@
|
|||||||
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
|
|
||||||
};
|
|
||||||
};
|
|
@ -1,21 +0,0 @@
|
|||||||
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[][];
|
|
||||||
};
|
|
@ -1,111 +0,0 @@
|
|||||||
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 };
|
|
@ -1,85 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}));
|
|
@ -1,91 +0,0 @@
|
|||||||
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;
|
|
@ -1,47 +0,0 @@
|
|||||||
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;
|
|
@ -1,478 +0,0 @@
|
|||||||
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;
|
|
@ -1,110 +0,0 @@
|
|||||||
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;
|
|
@ -1,904 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,314 +0,0 @@
|
|||||||
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;
|
|
@ -1 +0,0 @@
|
|||||||
export type TSizeOptionValue = 'small' | 'normal' | 'large';
|
|
@ -1,143 +0,0 @@
|
|||||||
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;
|
|
@ -1,65 +0,0 @@
|
|||||||
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;
|
|
@ -1,153 +0,0 @@
|
|||||||
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>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,61 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,78 +0,0 @@
|
|||||||
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;
|
|
@ -1,35 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}));
|
|
@ -1,13 +0,0 @@
|
|||||||
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;
|
|
@ -1,75 +0,0 @@
|
|||||||
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
@ -1,7 +0,0 @@
|
|||||||
type User = {
|
|
||||||
id: string;
|
|
||||||
username?: string;
|
|
||||||
roles?: string[];
|
|
||||||
token?: string;
|
|
||||||
tenantId: string;
|
|
||||||
};
|
|
@ -1,67 +0,0 @@
|
|||||||
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 };
|
|
@ -1,46 +0,0 @@
|
|||||||
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
|
|
||||||
};
|
|
||||||
};
|
|
@ -1,195 +0,0 @@
|
|||||||
@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;
|
|
||||||
}
|
|
@ -1,114 +0,0 @@
|
|||||||
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;
|
|
@ -1,109 +0,0 @@
|
|||||||
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;
|
|
@ -1,14 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
@ -1,75 +0,0 @@
|
|||||||
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`;
|
|
||||||
};
|
|
@ -1,53 +0,0 @@
|
|||||||
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;
|
|
@ -1,92 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,19 +0,0 @@
|
|||||||
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] })),
|
|
||||||
}));
|
|
@ -1,76 +0,0 @@
|
|||||||
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();
|
|
@ -1,456 +0,0 @@
|
|||||||
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();
|
|
@ -1,37 +0,0 @@
|
|||||||
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;
|
|
@ -1,13 +0,0 @@
|
|||||||
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;
|
|
@ -1,10 +0,0 @@
|
|||||||
import { DividerProps, Divider as PrimeDivider } from "primereact/divider";
|
|
||||||
import { FunctionComponent } from "react";
|
|
||||||
|
|
||||||
const Divider: FunctionComponent<DividerProps> = ({ children }) => {
|
|
||||||
return (
|
|
||||||
<PrimeDivider>{children}</PrimeDivider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Divider;
|
|
@ -1,16 +0,0 @@
|
|||||||
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;
|
|
@ -1,28 +0,0 @@
|
|||||||
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;
|
|
@ -1,85 +0,0 @@
|
|||||||
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;
|
|
@ -1,21 +0,0 @@
|
|||||||
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;
|
|
@ -1,60 +0,0 @@
|
|||||||
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;
|
|
@ -1,16 +0,0 @@
|
|||||||
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;
|
|
@ -1,16 +0,0 @@
|
|||||||
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;
|
|
@ -1,60 +0,0 @@
|
|||||||
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;
|
|
@ -1,78 +0,0 @@
|
|||||||
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;
|
|
@ -1,63 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
@ -1,45 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}));
|
|
@ -1,30 +0,0 @@
|
|||||||
// Adjusting the store to handle SystemDropdownOption properly
|
|
||||||
import { create } from 'zustand';
|
|
||||||
import { Enterprise } from '../../components/SystemSelectionModal';
|
|
||||||
|
|
||||||
export type System = {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
logo: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type SystemDropdownOption = {
|
|
||||||
id: number;
|
|
||||||
label: string;
|
|
||||||
businessLabel: string;
|
|
||||||
urlSlug: string;
|
|
||||||
logo: string;
|
|
||||||
enterprises?: Enterprise[];
|
|
||||||
};
|
|
||||||
|
|
||||||
type SystemStore = {
|
|
||||||
currentSystem: SystemDropdownOption | null;
|
|
||||||
setSystem: (system: SystemDropdownOption) => void;
|
|
||||||
clearSystem: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useSystemStore = create<SystemStore>((set) => ({
|
|
||||||
currentSystem: null,
|
|
||||||
setSystem: (system: SystemDropdownOption) => set({ currentSystem: system }),
|
|
||||||
clearSystem: () => set({ currentSystem: null }),
|
|
||||||
}));
|
|
@ -1,100 +0,0 @@
|
|||||||
/* 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;
|
|
||||||
}
|
|
@ -1,13 +0,0 @@
|
|||||||
export type AvatarPosition = 'left' | 'center' | 'right';
|
|
||||||
|
|
||||||
export interface AvatarState {
|
|
||||||
position: AvatarPosition;
|
|
||||||
isVisible: boolean;
|
|
||||||
isAnimating: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AvatarConfig {
|
|
||||||
size: number;
|
|
||||||
color: string;
|
|
||||||
animationDuration: number;
|
|
||||||
}
|
|