Spaces:
Sleeping
Sleeping
pranav8tripathi@gmail.com commited on
Commit ·
222ab06
1
Parent(s): 3e291b7
init
Browse files- .dockerignore +15 -0
- Dockerfile +34 -0
- README.md +6 -77
- package-lock.json +0 -0
- package.json +19 -7
- public/index.html +4 -4
- public/static.json +14 -0
- src/.env +1 -0
- src/App.js +0 -25
- src/{App.test.js → App.test.tsx} +1 -0
- src/App.tsx +61 -0
- src/components/AgentSelector.tsx +107 -0
- src/components/ChatInterface.tsx +284 -0
- src/components/ReportRenderer.tsx +389 -0
- src/contexts/AgentContext.tsx +63 -0
- src/{index.js → index.tsx} +3 -1
- src/react-app-env.d.ts +1 -0
- src/{reportWebVitals.js → reportWebVitals.ts} +3 -1
- src/{setupTests.js → setupTests.ts} +0 -0
- tsconfig.json +26 -0
.dockerignore
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
node_modules
|
| 2 |
+
npm-debug.log
|
| 3 |
+
build
|
| 4 |
+
.git
|
| 5 |
+
.gitignore
|
| 6 |
+
README.md
|
| 7 |
+
.DS_Store
|
| 8 |
+
.env
|
| 9 |
+
.env.local
|
| 10 |
+
.env.development.local
|
| 11 |
+
.env.test.local
|
| 12 |
+
.env.production.local
|
| 13 |
+
.vscode
|
| 14 |
+
.idea
|
| 15 |
+
*.md
|
Dockerfile
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Build stage
|
| 2 |
+
FROM node:18-alpine as build
|
| 3 |
+
|
| 4 |
+
# Set working directory
|
| 5 |
+
WORKDIR /app
|
| 6 |
+
|
| 7 |
+
# Copy package files
|
| 8 |
+
COPY package*.json ./
|
| 9 |
+
|
| 10 |
+
# Install dependencies
|
| 11 |
+
RUN npm install
|
| 12 |
+
|
| 13 |
+
# Copy source code
|
| 14 |
+
COPY . .
|
| 15 |
+
|
| 16 |
+
# Build the app
|
| 17 |
+
RUN npm run build
|
| 18 |
+
|
| 19 |
+
# Production stage
|
| 20 |
+
FROM node:18-alpine
|
| 21 |
+
|
| 22 |
+
WORKDIR /app
|
| 23 |
+
|
| 24 |
+
# Install http-server
|
| 25 |
+
RUN npm install -g http-server
|
| 26 |
+
|
| 27 |
+
# Copy built assets from build stage
|
| 28 |
+
COPY --from=build /app/build ./build
|
| 29 |
+
|
| 30 |
+
# Expose port 3000
|
| 31 |
+
EXPOSE 3000
|
| 32 |
+
|
| 33 |
+
# Start the app using http-server
|
| 34 |
+
CMD ["http-server", "build", "-p", "3000", "-a", "0.0.0.0"]
|
README.md
CHANGED
|
@@ -1,81 +1,10 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
-
sdk:
|
| 7 |
pinned: false
|
| 8 |
-
app_build_command: npm run build
|
| 9 |
-
app_file: build/index.html
|
| 10 |
---
|
| 11 |
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
|
| 15 |
-
|
| 16 |
-
## Available Scripts
|
| 17 |
-
|
| 18 |
-
In the project directory, you can run:
|
| 19 |
-
|
| 20 |
-
### `npm start`
|
| 21 |
-
|
| 22 |
-
Runs the app in the development mode.\
|
| 23 |
-
Open [http://localhost:3000](http://localhost:3000) to view it in your browser.
|
| 24 |
-
|
| 25 |
-
The page will reload when you make changes.\
|
| 26 |
-
You may also see any lint errors in the console.
|
| 27 |
-
|
| 28 |
-
### `npm test`
|
| 29 |
-
|
| 30 |
-
Launches the test runner in the interactive watch mode.\
|
| 31 |
-
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
|
| 32 |
-
|
| 33 |
-
### `npm run build`
|
| 34 |
-
|
| 35 |
-
Builds the app for production to the `build` folder.\
|
| 36 |
-
It correctly bundles React in production mode and optimizes the build for the best performance.
|
| 37 |
-
|
| 38 |
-
The build is minified and the filenames include the hashes.\
|
| 39 |
-
Your app is ready to be deployed!
|
| 40 |
-
|
| 41 |
-
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
|
| 42 |
-
|
| 43 |
-
### `npm run eject`
|
| 44 |
-
|
| 45 |
-
**Note: this is a one-way operation. Once you `eject`, you can't go back!**
|
| 46 |
-
|
| 47 |
-
If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
|
| 48 |
-
|
| 49 |
-
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own.
|
| 50 |
-
|
| 51 |
-
You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it.
|
| 52 |
-
|
| 53 |
-
## Learn More
|
| 54 |
-
|
| 55 |
-
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
|
| 56 |
-
|
| 57 |
-
To learn React, check out the [React documentation](https://reactjs.org/).
|
| 58 |
-
|
| 59 |
-
### Code Splitting
|
| 60 |
-
|
| 61 |
-
This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting)
|
| 62 |
-
|
| 63 |
-
### Analyzing the Bundle Size
|
| 64 |
-
|
| 65 |
-
This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size)
|
| 66 |
-
|
| 67 |
-
### Making a Progressive Web App
|
| 68 |
-
|
| 69 |
-
This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app)
|
| 70 |
-
|
| 71 |
-
### Advanced Configuration
|
| 72 |
-
|
| 73 |
-
This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration)
|
| 74 |
-
|
| 75 |
-
### Deployment
|
| 76 |
-
|
| 77 |
-
This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment)
|
| 78 |
-
|
| 79 |
-
### `npm run build` fails to minify
|
| 80 |
-
|
| 81 |
-
This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify)
|
|
|
|
| 1 |
---
|
| 2 |
+
title: BizFrontend
|
| 3 |
+
emoji: 🦀
|
| 4 |
+
colorFrom: yellow
|
| 5 |
+
colorTo: yellow
|
| 6 |
+
sdk: docker
|
| 7 |
pinned: false
|
|
|
|
|
|
|
| 8 |
---
|
| 9 |
|
| 10 |
+
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
package-lock.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
package.json
CHANGED
|
@@ -1,20 +1,32 @@
|
|
| 1 |
{
|
| 2 |
-
"name": "
|
| 3 |
"version": "0.1.0",
|
| 4 |
"private": true,
|
| 5 |
"dependencies": {
|
| 6 |
-
"@
|
| 7 |
-
"@
|
| 8 |
-
"@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
"@testing-library/user-event": "^13.5.0",
|
| 10 |
-
"
|
| 11 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
"react-scripts": "5.0.1",
|
|
|
|
| 13 |
"web-vitals": "^2.1.4"
|
| 14 |
},
|
|
|
|
| 15 |
"scripts": {
|
| 16 |
"start": "react-scripts start",
|
| 17 |
-
"build": "react-scripts build",
|
| 18 |
"test": "react-scripts test",
|
| 19 |
"eject": "react-scripts eject"
|
| 20 |
},
|
|
|
|
| 1 |
{
|
| 2 |
+
"name": "chat-interface",
|
| 3 |
"version": "0.1.0",
|
| 4 |
"private": true,
|
| 5 |
"dependencies": {
|
| 6 |
+
"@emotion/react": "^11.11.1",
|
| 7 |
+
"@emotion/styled": "^11.11.0",
|
| 8 |
+
"@mui/icons-material": "^5.11.16",
|
| 9 |
+
"@mui/material": "^5.13.0",
|
| 10 |
+
"@testing-library/dom": "^9.3.1",
|
| 11 |
+
"@testing-library/jest-dom": "^5.16.5",
|
| 12 |
+
"@testing-library/react": "^13.4.0",
|
| 13 |
"@testing-library/user-event": "^13.5.0",
|
| 14 |
+
"@types/jest": "^27.5.2",
|
| 15 |
+
"@types/jspdf": "^1.3.3",
|
| 16 |
+
"@types/node": "^16.18.38",
|
| 17 |
+
"@types/react": "^18.2.6",
|
| 18 |
+
"@types/react-dom": "^18.2.4",
|
| 19 |
+
"jspdf": "^3.0.3",
|
| 20 |
+
"react": "^18.2.0",
|
| 21 |
+
"react-dom": "^18.2.0",
|
| 22 |
"react-scripts": "5.0.1",
|
| 23 |
+
"typescript": "^4.9.5",
|
| 24 |
"web-vitals": "^2.1.4"
|
| 25 |
},
|
| 26 |
+
"homepage": ".",
|
| 27 |
"scripts": {
|
| 28 |
"start": "react-scripts start",
|
| 29 |
+
"build": "GENERATE_SOURCEMAP=false INLINE_RUNTIME_CHUNK=false react-scripts build",
|
| 30 |
"test": "react-scripts test",
|
| 31 |
"eject": "react-scripts eject"
|
| 32 |
},
|
public/index.html
CHANGED
|
@@ -2,19 +2,19 @@
|
|
| 2 |
<html lang="en">
|
| 3 |
<head>
|
| 4 |
<meta charset="utf-8" />
|
| 5 |
-
<link rel="icon" href="
|
| 6 |
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
| 7 |
<meta name="theme-color" content="#000000" />
|
| 8 |
<meta
|
| 9 |
name="description"
|
| 10 |
content="Web site created using create-react-app"
|
| 11 |
/>
|
| 12 |
-
<link rel="apple-touch-icon" href="
|
| 13 |
<!--
|
| 14 |
manifest.json provides metadata used when your web app is installed on a
|
| 15 |
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
| 16 |
-->
|
| 17 |
-
<link rel="manifest" href="
|
| 18 |
<!--
|
| 19 |
Notice the use of %PUBLIC_URL% in the tags above.
|
| 20 |
It will be replaced with the URL of the `public` folder during the build.
|
|
@@ -24,7 +24,7 @@
|
|
| 24 |
work correctly both with client-side routing and a non-root public URL.
|
| 25 |
Learn how to configure a non-root public URL by running `npm run build`.
|
| 26 |
-->
|
| 27 |
-
<title>
|
| 28 |
</head>
|
| 29 |
<body>
|
| 30 |
<noscript>You need to enable JavaScript to run this app.</noscript>
|
|
|
|
| 2 |
<html lang="en">
|
| 3 |
<head>
|
| 4 |
<meta charset="utf-8" />
|
| 5 |
+
<link rel="icon" href="favicon.ico" />
|
| 6 |
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
| 7 |
<meta name="theme-color" content="#000000" />
|
| 8 |
<meta
|
| 9 |
name="description"
|
| 10 |
content="Web site created using create-react-app"
|
| 11 |
/>
|
| 12 |
+
<link rel="apple-touch-icon" href="logo192.png" />
|
| 13 |
<!--
|
| 14 |
manifest.json provides metadata used when your web app is installed on a
|
| 15 |
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
| 16 |
-->
|
| 17 |
+
<link rel="manifest" href="manifest.json" />
|
| 18 |
<!--
|
| 19 |
Notice the use of %PUBLIC_URL% in the tags above.
|
| 20 |
It will be replaced with the URL of the `public` folder during the build.
|
|
|
|
| 24 |
work correctly both with client-side routing and a non-root public URL.
|
| 25 |
Learn how to configure a non-root public URL by running `npm run build`.
|
| 26 |
-->
|
| 27 |
+
<title>Agent Marketplace</title>
|
| 28 |
</head>
|
| 29 |
<body>
|
| 30 |
<noscript>You need to enable JavaScript to run this app.</noscript>
|
public/static.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"root": "/",
|
| 3 |
+
"clean_urls": true,
|
| 4 |
+
"routes": {
|
| 5 |
+
"/**": "index.html"
|
| 6 |
+
},
|
| 7 |
+
"headers": {
|
| 8 |
+
"/**": {
|
| 9 |
+
"Cache-Control": "no-cache, no-store, must-revalidate",
|
| 10 |
+
"Pragma": "no-cache",
|
| 11 |
+
"Expires": "0"
|
| 12 |
+
}
|
| 13 |
+
}
|
| 14 |
+
}
|
src/.env
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
VITE_API_BASE_URL=http://localhost:8080
|
src/App.js
DELETED
|
@@ -1,25 +0,0 @@
|
|
| 1 |
-
import logo from './logo.svg';
|
| 2 |
-
import './App.css';
|
| 3 |
-
|
| 4 |
-
function App() {
|
| 5 |
-
return (
|
| 6 |
-
<div className="App">
|
| 7 |
-
<header className="App-header">
|
| 8 |
-
<img src={logo} className="App-logo" alt="logo" />
|
| 9 |
-
<p>
|
| 10 |
-
Edit <code>src/App.js</code> and save to reload.
|
| 11 |
-
</p>
|
| 12 |
-
<a
|
| 13 |
-
className="App-link"
|
| 14 |
-
href="https://reactjs.org"
|
| 15 |
-
target="_blank"
|
| 16 |
-
rel="noopener noreferrer"
|
| 17 |
-
>
|
| 18 |
-
Learn React
|
| 19 |
-
</a>
|
| 20 |
-
</header>
|
| 21 |
-
</div>
|
| 22 |
-
);
|
| 23 |
-
}
|
| 24 |
-
|
| 25 |
-
export default App;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/{App.test.js → App.test.tsx}
RENAMED
|
@@ -1,3 +1,4 @@
|
|
|
|
|
| 1 |
import { render, screen } from '@testing-library/react';
|
| 2 |
import App from './App';
|
| 3 |
|
|
|
|
| 1 |
+
import React from 'react';
|
| 2 |
import { render, screen } from '@testing-library/react';
|
| 3 |
import App from './App';
|
| 4 |
|
src/App.tsx
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from 'react';
|
| 2 |
+
import { AgentProvider, useAgent } from './contexts/AgentContext';
|
| 3 |
+
import ChatInterface from './components/ChatInterface';
|
| 4 |
+
import AgentSelector from './components/AgentSelector';
|
| 5 |
+
import { CssBaseline, ThemeProvider, createTheme } from '@mui/material';
|
| 6 |
+
|
| 7 |
+
const theme = createTheme({
|
| 8 |
+
palette: {
|
| 9 |
+
mode: 'light',
|
| 10 |
+
primary: {
|
| 11 |
+
main: '#7C3AED', // violet
|
| 12 |
+
light: '#A78BFA',
|
| 13 |
+
dark: '#5B21B6',
|
| 14 |
+
contrastText: '#ffffff',
|
| 15 |
+
},
|
| 16 |
+
secondary: {
|
| 17 |
+
main: '#06B6D4', // cyan
|
| 18 |
+
light: '#67E8F9',
|
| 19 |
+
dark: '#0E7490',
|
| 20 |
+
contrastText: '#003543',
|
| 21 |
+
},
|
| 22 |
+
background: {
|
| 23 |
+
default: '#f7f9fc',
|
| 24 |
+
paper: '#ffffff',
|
| 25 |
+
},
|
| 26 |
+
},
|
| 27 |
+
shape: {
|
| 28 |
+
borderRadius: 12,
|
| 29 |
+
},
|
| 30 |
+
typography: {
|
| 31 |
+
fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif',
|
| 32 |
+
},
|
| 33 |
+
});
|
| 34 |
+
|
| 35 |
+
function App() {
|
| 36 |
+
return (
|
| 37 |
+
<ThemeProvider theme={theme}>
|
| 38 |
+
<CssBaseline />
|
| 39 |
+
<AgentProvider>
|
| 40 |
+
<AppContent />
|
| 41 |
+
</AgentProvider>
|
| 42 |
+
</ThemeProvider>
|
| 43 |
+
);
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
// Separate component to use the useAgent hook
|
| 47 |
+
function AppContent() {
|
| 48 |
+
const { selectedAgent } = useAgent();
|
| 49 |
+
|
| 50 |
+
return (
|
| 51 |
+
<>
|
| 52 |
+
{selectedAgent ? (
|
| 53 |
+
<ChatInterface />
|
| 54 |
+
) : (
|
| 55 |
+
<AgentSelector />
|
| 56 |
+
)}
|
| 57 |
+
</>
|
| 58 |
+
);
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
export default App;
|
src/components/AgentSelector.tsx
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from 'react';
|
| 2 |
+
import { useAgent } from '../contexts/AgentContext';
|
| 3 |
+
import {
|
| 4 |
+
Container,
|
| 5 |
+
Typography,
|
| 6 |
+
Grid,
|
| 7 |
+
Card,
|
| 8 |
+
CardActionArea,
|
| 9 |
+
Box,
|
| 10 |
+
Chip,
|
| 11 |
+
Avatar,
|
| 12 |
+
} from '@mui/material';
|
| 13 |
+
|
| 14 |
+
const AgentSelector: React.FC = () => {
|
| 15 |
+
const { availableAgents, setSelectedAgent } = useAgent();
|
| 16 |
+
|
| 17 |
+
return (
|
| 18 |
+
<Box sx={{ minHeight: '100vh', bgcolor: 'background.default' }}>
|
| 19 |
+
{/* Hero Section */}
|
| 20 |
+
<Box
|
| 21 |
+
sx={{
|
| 22 |
+
py: { xs: 6, md: 10 },
|
| 23 |
+
px: 2,
|
| 24 |
+
background: 'linear-gradient(135deg, rgba(25,118,210,0.1) 0%, rgba(220,0,78,0.06) 100%)',
|
| 25 |
+
borderBottom: theme => `1px solid ${theme.palette.divider}`,
|
| 26 |
+
}}
|
| 27 |
+
>
|
| 28 |
+
<Container maxWidth="lg">
|
| 29 |
+
<Typography variant="h2" component="h1" align="center" gutterBottom sx={{ fontWeight: 700 }}>
|
| 30 |
+
Agent Marketplace
|
| 31 |
+
</Typography>
|
| 32 |
+
<Typography variant="h6" color="text.secondary" align="center" sx={{ maxWidth: 800, mx: 'auto' }}>
|
| 33 |
+
Your AI copilot for insights. Choose an agent to get started.
|
| 34 |
+
</Typography>
|
| 35 |
+
</Container>
|
| 36 |
+
</Box>
|
| 37 |
+
|
| 38 |
+
{/* Agents Grid */}
|
| 39 |
+
<Container maxWidth="lg" sx={{ mt: { xs: 3, md: 6 }, pb: 6 }}>
|
| 40 |
+
<Grid container spacing={4}>
|
| 41 |
+
{availableAgents.map((agent) => (
|
| 42 |
+
<Grid item xs={12} sm={6} md={4} key={agent.id}>
|
| 43 |
+
<Card
|
| 44 |
+
elevation={2}
|
| 45 |
+
sx={{
|
| 46 |
+
height: '100%',
|
| 47 |
+
display: 'flex',
|
| 48 |
+
flexDirection: 'column',
|
| 49 |
+
borderRadius: 3,
|
| 50 |
+
border: theme => `1px solid ${theme.palette.divider}`,
|
| 51 |
+
transition: 'transform 0.2s ease, box-shadow 0.2s ease',
|
| 52 |
+
position: 'relative',
|
| 53 |
+
'&:hover': {
|
| 54 |
+
transform: 'translateY(-6px)',
|
| 55 |
+
boxShadow: 8,
|
| 56 |
+
},
|
| 57 |
+
}}
|
| 58 |
+
>
|
| 59 |
+
{agent.upcoming && (
|
| 60 |
+
<Box sx={{ position: 'absolute', top: 12, right: 12 }}>
|
| 61 |
+
<Chip size="small" color="secondary" label="Upcoming" />
|
| 62 |
+
</Box>
|
| 63 |
+
)}
|
| 64 |
+
|
| 65 |
+
<CardActionArea
|
| 66 |
+
disabled={agent.upcoming}
|
| 67 |
+
onClick={() => !agent.upcoming && setSelectedAgent(agent)}
|
| 68 |
+
sx={{
|
| 69 |
+
height: '100%',
|
| 70 |
+
display: 'flex',
|
| 71 |
+
flexDirection: 'column',
|
| 72 |
+
alignItems: 'center',
|
| 73 |
+
textAlign: 'center',
|
| 74 |
+
p: 3,
|
| 75 |
+
opacity: agent.upcoming ? 0.7 : 1,
|
| 76 |
+
cursor: agent.upcoming ? 'not-allowed' : 'pointer',
|
| 77 |
+
}}
|
| 78 |
+
>
|
| 79 |
+
<Avatar
|
| 80 |
+
src={agent.avatar}
|
| 81 |
+
alt={agent.name}
|
| 82 |
+
sx={{
|
| 83 |
+
width: 88,
|
| 84 |
+
height: 88,
|
| 85 |
+
mb: 2,
|
| 86 |
+
border: '3px solid',
|
| 87 |
+
borderColor: 'primary.main',
|
| 88 |
+
boxShadow: 2,
|
| 89 |
+
}}
|
| 90 |
+
/>
|
| 91 |
+
<Typography variant="h6" component="div" gutterBottom>
|
| 92 |
+
{agent.name}
|
| 93 |
+
</Typography>
|
| 94 |
+
<Typography variant="body2" color="text.secondary">
|
| 95 |
+
{agent.description}
|
| 96 |
+
</Typography>
|
| 97 |
+
</CardActionArea>
|
| 98 |
+
</Card>
|
| 99 |
+
</Grid>
|
| 100 |
+
))}
|
| 101 |
+
</Grid>
|
| 102 |
+
</Container>
|
| 103 |
+
</Box>
|
| 104 |
+
);
|
| 105 |
+
};
|
| 106 |
+
|
| 107 |
+
export default AgentSelector;
|
src/components/ChatInterface.tsx
ADDED
|
@@ -0,0 +1,284 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useState, useEffect, useRef } from 'react';
|
| 2 |
+
import { useAgent } from '../contexts/AgentContext';
|
| 3 |
+
import ReportRenderer from './ReportRenderer';
|
| 4 |
+
import {
|
| 5 |
+
Box,
|
| 6 |
+
Typography,
|
| 7 |
+
IconButton,
|
| 8 |
+
Avatar,
|
| 9 |
+
Paper,
|
| 10 |
+
TextField,
|
| 11 |
+
InputAdornment,
|
| 12 |
+
List,
|
| 13 |
+
ListItem,
|
| 14 |
+
ListItemAvatar,
|
| 15 |
+
AppBar,
|
| 16 |
+
Toolbar,
|
| 17 |
+
Chip,
|
| 18 |
+
} from '@mui/material';
|
| 19 |
+
import SendIcon from '@mui/icons-material/Send';
|
| 20 |
+
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
| 21 |
+
|
| 22 |
+
const ChatInterface: React.FC = () => {
|
| 23 |
+
const { selectedAgent, setSelectedAgent } = useAgent();
|
| 24 |
+
type QuickReplyButton = { title: string; payload: string };
|
| 25 |
+
type CustomMultiSelect = {
|
| 26 |
+
type: 'multi_select_chips';
|
| 27 |
+
options: string[];
|
| 28 |
+
columns?: number;
|
| 29 |
+
hint?: string;
|
| 30 |
+
selections?: string[]; // maintained client-side
|
| 31 |
+
};
|
| 32 |
+
type ChatMessage = {
|
| 33 |
+
sender: 'user' | 'bot';
|
| 34 |
+
text?: string;
|
| 35 |
+
buttons?: QuickReplyButton[];
|
| 36 |
+
custom?: CustomMultiSelect;
|
| 37 |
+
};
|
| 38 |
+
|
| 39 |
+
const [messages, setMessages] = useState<ChatMessage[]>([]);
|
| 40 |
+
const [inputValue, setInputValue] = useState('');
|
| 41 |
+
const [isLoading, setIsLoading] = useState(false);
|
| 42 |
+
const messagesEndRef = useRef<HTMLDivElement>(null);
|
| 43 |
+
const chatContainerRef = useRef<HTMLDivElement>(null);
|
| 44 |
+
|
| 45 |
+
const sendMessage = async (outgoing: string) => {
|
| 46 |
+
if (!outgoing.trim()) return;
|
| 47 |
+
setIsLoading(true);
|
| 48 |
+
try {
|
| 49 |
+
console.log('[DEBUG] Sending message to Rasa REST webhook...', outgoing);
|
| 50 |
+
const res = await fetch(`http://localhost:5005/webhooks/rest/webhook`, {
|
| 51 |
+
method: 'POST',
|
| 52 |
+
headers: { 'Content-Type': 'application/json' },
|
| 53 |
+
body: JSON.stringify({ sender: 'web-user', message: outgoing }),
|
| 54 |
+
});
|
| 55 |
+
|
| 56 |
+
if (!res.ok) throw new Error(`Backend error: ${res.status} ${res.statusText}`);
|
| 57 |
+
const data = await res.json();
|
| 58 |
+
console.log('[DEBUG] Rasa response:', data);
|
| 59 |
+
|
| 60 |
+
// Map Rasa messages to our ChatMessage structure (support text + buttons + custom multi-select)
|
| 61 |
+
const botMessages: ChatMessage[] = Array.isArray(data)
|
| 62 |
+
? data
|
| 63 |
+
.map((m: any): ChatMessage | null => {
|
| 64 |
+
const text = typeof m?.text === 'string' ? m.text : undefined;
|
| 65 |
+
const buttons = Array.isArray(m?.buttons)
|
| 66 |
+
? m.buttons
|
| 67 |
+
.filter((b: any) => typeof b?.title === 'string' && typeof b?.payload === 'string')
|
| 68 |
+
.map((b: any) => ({ title: b.title as string, payload: b.payload as string }))
|
| 69 |
+
: undefined;
|
| 70 |
+
let custom: ChatMessage['custom'] = undefined;
|
| 71 |
+
if (m?.custom && typeof m.custom === 'object' && m.custom.type === 'multi_select_chips') {
|
| 72 |
+
const opts = Array.isArray(m.custom.options) ? m.custom.options.filter((o: any) => typeof o === 'string') : [];
|
| 73 |
+
custom = {
|
| 74 |
+
type: 'multi_select_chips',
|
| 75 |
+
options: opts,
|
| 76 |
+
columns: typeof m.custom.columns === 'number' ? m.custom.columns : undefined,
|
| 77 |
+
hint: typeof m.custom.hint === 'string' ? m.custom.hint : undefined,
|
| 78 |
+
selections: [],
|
| 79 |
+
};
|
| 80 |
+
}
|
| 81 |
+
if (!text && (!buttons || buttons.length === 0) && !custom) return null;
|
| 82 |
+
return { sender: 'bot', text, buttons, custom } as ChatMessage;
|
| 83 |
+
})
|
| 84 |
+
.filter(Boolean) as ChatMessage[]
|
| 85 |
+
: [];
|
| 86 |
+
|
| 87 |
+
if (botMessages.length > 0) {
|
| 88 |
+
setMessages(prev => [...prev, ...botMessages]);
|
| 89 |
+
}
|
| 90 |
+
} catch (error) {
|
| 91 |
+
console.error('[DEBUG] sendMessage error:', error);
|
| 92 |
+
setMessages(prev => [...prev, { text: '❌ Something went wrong.', sender: 'bot' }]);
|
| 93 |
+
} finally {
|
| 94 |
+
setIsLoading(false);
|
| 95 |
+
}
|
| 96 |
+
};
|
| 97 |
+
|
| 98 |
+
const handleSendMessage = async (e: React.FormEvent) => {
|
| 99 |
+
e.preventDefault();
|
| 100 |
+
if (!inputValue.trim()) return;
|
| 101 |
+
const userMessage = inputValue;
|
| 102 |
+
setMessages(prev => [...prev, { text: userMessage, sender: 'user' }]);
|
| 103 |
+
setInputValue('');
|
| 104 |
+
await sendMessage(userMessage);
|
| 105 |
+
};
|
| 106 |
+
|
| 107 |
+
const handleQuickReply = async (btn: QuickReplyButton) => {
|
| 108 |
+
// If this is a Continue button after a multi-select, append selections
|
| 109 |
+
const lastMultiIndex = [...messages]
|
| 110 |
+
.map((m, idx) => ({ m, idx }))
|
| 111 |
+
.reverse()
|
| 112 |
+
.find(item => item.m.custom?.type === 'multi_select_chips')?.idx;
|
| 113 |
+
|
| 114 |
+
const lastMulti = lastMultiIndex !== undefined ? messages[lastMultiIndex] : undefined;
|
| 115 |
+
const selections = lastMulti?.custom?.selections ?? [];
|
| 116 |
+
|
| 117 |
+
if (btn.payload.startsWith('/')) {
|
| 118 |
+
if (selections.length > 0 && btn.payload.includes('continue')) {
|
| 119 |
+
// Send Rasa intent payload with inline JSON entity containing selections
|
| 120 |
+
const payloadWithSelections = `${btn.payload}${JSON.stringify({ selections })}`;
|
| 121 |
+
setMessages(prev => [
|
| 122 |
+
...prev,
|
| 123 |
+
{ text: `${btn.title}\n${selections.join(', ')}`, sender: 'user' },
|
| 124 |
+
]);
|
| 125 |
+
await sendMessage(payloadWithSelections);
|
| 126 |
+
return;
|
| 127 |
+
}
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
// Default behavior: show the button title as user message, send payload
|
| 131 |
+
setMessages(prev => [...prev, { text: btn.title, sender: 'user' }]);
|
| 132 |
+
await sendMessage(btn.payload);
|
| 133 |
+
};
|
| 134 |
+
|
| 135 |
+
const toggleMultiSelect = (messageIndex: number, option: string) => {
|
| 136 |
+
setMessages(prev => {
|
| 137 |
+
const next = [...prev];
|
| 138 |
+
const msg = next[messageIndex];
|
| 139 |
+
if (!msg?.custom || msg.custom.type !== 'multi_select_chips') return prev;
|
| 140 |
+
const current = new Set(msg.custom.selections || []);
|
| 141 |
+
if (current.has(option)) {
|
| 142 |
+
current.delete(option);
|
| 143 |
+
} else {
|
| 144 |
+
current.add(option);
|
| 145 |
+
}
|
| 146 |
+
msg.custom = { ...msg.custom, selections: Array.from(current) };
|
| 147 |
+
next[messageIndex] = { ...msg };
|
| 148 |
+
return next;
|
| 149 |
+
});
|
| 150 |
+
};
|
| 151 |
+
const scrollToBottom = () => messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
| 152 |
+
useEffect(() => { scrollToBottom(); }, [messages, isLoading]);
|
| 153 |
+
|
| 154 |
+
if (!selectedAgent) return null;
|
| 155 |
+
|
| 156 |
+
return (
|
| 157 |
+
<Box sx={{ display: 'flex', height: '100vh', justifyContent: 'center', alignItems: 'center', bgcolor: 'background.default', p: { xs: 1.5, md: 2 } }}>
|
| 158 |
+
<Paper elevation={4} sx={{ width: '100%', maxWidth: 900, height: { xs: '90vh', md: '85vh' }, display: 'flex', flexDirection: 'column', borderRadius: 4, overflow: 'hidden' }}>
|
| 159 |
+
<AppBar position="static" color="transparent" elevation={0} sx={{ borderBottom: theme => `1px solid ${theme.palette.divider}`, bgcolor: 'background.paper' }}>
|
| 160 |
+
<Toolbar sx={{ gap: 1 }}>
|
| 161 |
+
<IconButton edge="start" onClick={() => setSelectedAgent(null)} sx={{ mr: 1 }}>
|
| 162 |
+
<ArrowBackIcon />
|
| 163 |
+
</IconButton>
|
| 164 |
+
<Avatar src={selectedAgent.avatar} alt={selectedAgent.name} sx={{ width: 32, height: 32, mr: 2 }} />
|
| 165 |
+
<Typography variant="h6" sx={{ flexGrow: 1 }}>{selectedAgent.name}</Typography>
|
| 166 |
+
</Toolbar>
|
| 167 |
+
</AppBar>
|
| 168 |
+
|
| 169 |
+
<Box ref={chatContainerRef} sx={{ flex: 1, overflowY: 'auto', p: { xs: 1.5, md: 2 }, bgcolor: 'background.paper' }}>
|
| 170 |
+
<List>
|
| 171 |
+
{messages.map((msg, i) => (
|
| 172 |
+
<ListItem key={i} sx={{ justifyContent: msg.sender === 'user' ? 'flex-end' : 'flex-start', alignItems: 'flex-start' }}>
|
| 173 |
+
{msg.sender === 'bot' && <ListItemAvatar><Avatar src={selectedAgent.avatar} /></ListItemAvatar>}
|
| 174 |
+
<Paper sx={{
|
| 175 |
+
p: 1.25,
|
| 176 |
+
px: 1.5,
|
| 177 |
+
maxWidth: '70%',
|
| 178 |
+
bgcolor: 'grey.100',
|
| 179 |
+
color: 'text.primary',
|
| 180 |
+
borderRadius: 3,
|
| 181 |
+
boxShadow: 1,
|
| 182 |
+
}}>
|
| 183 |
+
{msg.text && (
|
| 184 |
+
<ReportRenderer content={msg.text} isBotMessage={msg.sender === 'bot'} />
|
| 185 |
+
)}
|
| 186 |
+
{msg.custom?.type === 'multi_select_chips' && (
|
| 187 |
+
<Box sx={{ mt: msg.text ? 1 : 0 }}>
|
| 188 |
+
{msg.custom.hint && (
|
| 189 |
+
<Typography variant="body2" color="text.secondary" sx={{ mb: 1 }}>
|
| 190 |
+
{msg.custom.hint}
|
| 191 |
+
</Typography>
|
| 192 |
+
)}
|
| 193 |
+
<Box
|
| 194 |
+
sx={{
|
| 195 |
+
display: 'flex',
|
| 196 |
+
flexWrap: 'wrap',
|
| 197 |
+
gap: 1,
|
| 198 |
+
// Each chip takes equal width per column if columns provided
|
| 199 |
+
'& > .multi-chip': {
|
| 200 |
+
flex: msg.custom.columns ? `0 0 ${100 / msg.custom.columns}%` : '0 0 auto',
|
| 201 |
+
},
|
| 202 |
+
}}
|
| 203 |
+
>
|
| 204 |
+
{msg.custom.options.map((opt) => {
|
| 205 |
+
const selected = !!msg.custom?.selections?.includes(opt);
|
| 206 |
+
return (
|
| 207 |
+
<Box key={opt} className="multi-chip">
|
| 208 |
+
<Chip
|
| 209 |
+
label={opt}
|
| 210 |
+
color={selected ? 'primary' : 'default'}
|
| 211 |
+
variant={selected ? 'filled' : 'outlined'}
|
| 212 |
+
onClick={() => toggleMultiSelect(i, opt)}
|
| 213 |
+
sx={{ borderRadius: 999, width: '100%' }}
|
| 214 |
+
/>
|
| 215 |
+
</Box>
|
| 216 |
+
);
|
| 217 |
+
})}
|
| 218 |
+
</Box>
|
| 219 |
+
</Box>
|
| 220 |
+
)}
|
| 221 |
+
{msg.buttons && msg.buttons.length > 0 && (
|
| 222 |
+
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1, mt: msg.text ? 1 : 0 }}>
|
| 223 |
+
{msg.buttons.map((b, idx) => (
|
| 224 |
+
<Chip
|
| 225 |
+
key={idx}
|
| 226 |
+
label={b.title}
|
| 227 |
+
color="primary"
|
| 228 |
+
variant="outlined"
|
| 229 |
+
onClick={() => handleQuickReply(b)}
|
| 230 |
+
sx={{ borderRadius: 999 }}
|
| 231 |
+
/>
|
| 232 |
+
))}
|
| 233 |
+
</Box>
|
| 234 |
+
)}
|
| 235 |
+
</Paper>
|
| 236 |
+
</ListItem>
|
| 237 |
+
))}
|
| 238 |
+
{isLoading && (
|
| 239 |
+
<ListItem>
|
| 240 |
+
<ListItemAvatar><Avatar src={selectedAgent.avatar} /></ListItemAvatar>
|
| 241 |
+
<Paper sx={{ p: 1.25, px: 1.5, bgcolor: 'grey.100', color: 'text.primary', borderRadius: 3 }}>
|
| 242 |
+
<Typography>Typing...</Typography>
|
| 243 |
+
</Paper>
|
| 244 |
+
</ListItem>
|
| 245 |
+
)}
|
| 246 |
+
<div ref={messagesEndRef} />
|
| 247 |
+
</List>
|
| 248 |
+
</Box>
|
| 249 |
+
|
| 250 |
+
<Box component="form" onSubmit={handleSendMessage} sx={{ display: 'flex', gap: 1, p: 1.5, bgcolor: 'background.paper', borderTop: theme => `1px solid ${theme.palette.divider}` }}>
|
| 251 |
+
<TextField
|
| 252 |
+
fullWidth
|
| 253 |
+
variant="outlined"
|
| 254 |
+
placeholder="Type a message..."
|
| 255 |
+
value={inputValue}
|
| 256 |
+
onChange={(e) => setInputValue(e.target.value)}
|
| 257 |
+
onKeyDown={(e) => {
|
| 258 |
+
if (e.key === 'Enter' && !e.altKey) {
|
| 259 |
+
e.preventDefault();
|
| 260 |
+
handleSendMessage(e);
|
| 261 |
+
} else if (e.key === 'Enter' && e.altKey) {
|
| 262 |
+
return;
|
| 263 |
+
}
|
| 264 |
+
}}
|
| 265 |
+
multiline
|
| 266 |
+
maxRows={4}
|
| 267 |
+
InputProps={{
|
| 268 |
+
sx: { borderRadius: 999, bgcolor: 'background.default' },
|
| 269 |
+
endAdornment: (
|
| 270 |
+
<InputAdornment position="end">
|
| 271 |
+
<IconButton type="submit" color="primary" disabled={!inputValue.trim()} sx={{ bgcolor: (theme) => (!inputValue.trim() ? 'transparent' : theme.palette.primary.light + '33'), ':hover': { bgcolor: (theme) => theme.palette.primary.light + '55' } }}>
|
| 272 |
+
<SendIcon />
|
| 273 |
+
</IconButton>
|
| 274 |
+
</InputAdornment>
|
| 275 |
+
),
|
| 276 |
+
}}
|
| 277 |
+
/>
|
| 278 |
+
</Box>
|
| 279 |
+
</Paper>
|
| 280 |
+
</Box>
|
| 281 |
+
);
|
| 282 |
+
};
|
| 283 |
+
|
| 284 |
+
export default ChatInterface;
|
src/components/ReportRenderer.tsx
ADDED
|
@@ -0,0 +1,389 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from 'react';
|
| 2 |
+
import {
|
| 3 |
+
Box,
|
| 4 |
+
Typography,
|
| 5 |
+
Card,
|
| 6 |
+
CardContent,
|
| 7 |
+
Chip,
|
| 8 |
+
List,
|
| 9 |
+
ListItem,
|
| 10 |
+
ListItemText,
|
| 11 |
+
Link,
|
| 12 |
+
Table,
|
| 13 |
+
TableBody,
|
| 14 |
+
TableCell,
|
| 15 |
+
TableContainer,
|
| 16 |
+
TableHead,
|
| 17 |
+
TableRow,
|
| 18 |
+
IconButton,
|
| 19 |
+
Collapse,
|
| 20 |
+
} from '@mui/material';
|
| 21 |
+
import {
|
| 22 |
+
ExpandMore,
|
| 23 |
+
ExpandLess,
|
| 24 |
+
Code as CodeIcon,
|
| 25 |
+
Link as LinkIcon,
|
| 26 |
+
TableChart as TableIcon,
|
| 27 |
+
} from '@mui/icons-material';
|
| 28 |
+
|
| 29 |
+
interface ResponseRendererProps {
|
| 30 |
+
content: string;
|
| 31 |
+
isBotMessage?: boolean;
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
const ResponseRenderer: React.FC<ResponseRendererProps> = ({ content, isBotMessage = true }) => {
|
| 35 |
+
const [expandedSections, setExpandedSections] = React.useState<{ [key: string]: boolean }>({});
|
| 36 |
+
|
| 37 |
+
const toggleSection = (sectionId: string) => {
|
| 38 |
+
setExpandedSections(prev => ({
|
| 39 |
+
...prev,
|
| 40 |
+
[sectionId]: !prev[sectionId]
|
| 41 |
+
}));
|
| 42 |
+
};
|
| 43 |
+
|
| 44 |
+
// Parse different content types
|
| 45 |
+
const parseContent = (text: string) => {
|
| 46 |
+
const sections: Array<{
|
| 47 |
+
type: 'header' | 'subheader' | 'text' | 'list' | 'code' | 'table' | 'link' | 'metric' | 'highlight';
|
| 48 |
+
content?: string;
|
| 49 |
+
level?: number;
|
| 50 |
+
items?: string[];
|
| 51 |
+
language?: string;
|
| 52 |
+
headers?: string[];
|
| 53 |
+
rows?: string[][];
|
| 54 |
+
url?: string;
|
| 55 |
+
title?: string;
|
| 56 |
+
}> = [];
|
| 57 |
+
|
| 58 |
+
const lines = text.split('\n');
|
| 59 |
+
|
| 60 |
+
for (let i = 0; i < lines.length; i++) {
|
| 61 |
+
const line = lines[i].trim();
|
| 62 |
+
|
| 63 |
+
if (!line) {
|
| 64 |
+
sections.push({ type: 'text', content: '' });
|
| 65 |
+
continue;
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
// Headers
|
| 69 |
+
if (line.startsWith('# ')) {
|
| 70 |
+
sections.push({ type: 'header', content: line.substring(2), level: 1 });
|
| 71 |
+
} else if (line.startsWith('## ')) {
|
| 72 |
+
sections.push({ type: 'subheader', content: line.substring(3), level: 2 });
|
| 73 |
+
} else if (line.startsWith('### ')) {
|
| 74 |
+
sections.push({ type: 'subheader', content: line.substring(4), level: 3 });
|
| 75 |
+
}
|
| 76 |
+
// Bold text as highlights
|
| 77 |
+
else if (line.startsWith('**') && line.endsWith('**')) {
|
| 78 |
+
sections.push({ type: 'highlight', content: line.substring(2, line.length - 2) });
|
| 79 |
+
}
|
| 80 |
+
// Lists
|
| 81 |
+
else if (line.startsWith('• ') || line.startsWith('- ') || line.startsWith('* ')) {
|
| 82 |
+
const listItems = [line.substring(2)];
|
| 83 |
+
let j = i + 1;
|
| 84 |
+
while (j < lines.length && (lines[j].trim().startsWith('• ') || lines[j].trim().startsWith('- ') || lines[j].trim().startsWith('* '))) {
|
| 85 |
+
listItems.push(lines[j].trim().substring(2));
|
| 86 |
+
j++;
|
| 87 |
+
}
|
| 88 |
+
i = j - 1; // Skip processed lines
|
| 89 |
+
sections.push({ type: 'list', content: '', items: listItems });
|
| 90 |
+
}
|
| 91 |
+
// Code blocks
|
| 92 |
+
else if (line.startsWith('```')) {
|
| 93 |
+
const language = line.substring(3).trim();
|
| 94 |
+
let codeContent = '';
|
| 95 |
+
let j = i + 1;
|
| 96 |
+
while (j < lines.length && !lines[j].trim().startsWith('```')) {
|
| 97 |
+
codeContent += lines[j] + '\n';
|
| 98 |
+
j++;
|
| 99 |
+
}
|
| 100 |
+
i = j; // Skip processed lines
|
| 101 |
+
sections.push({ type: 'code', content: codeContent.trim(), language });
|
| 102 |
+
}
|
| 103 |
+
// Table detection (simple format: | col1 | col2 |)
|
| 104 |
+
else if (line.includes('|') && lines[i + 1]?.includes('|') && lines[i + 1]?.includes('-')) {
|
| 105 |
+
const headers = line.split('|').map(h => h.trim()).filter(h => h);
|
| 106 |
+
let j = i + 2;
|
| 107 |
+
const rows: string[][] = [];
|
| 108 |
+
while (j < lines.length && lines[j].trim().includes('|')) {
|
| 109 |
+
const row = lines[j].split('|').map(cell => cell.trim()).filter(cell => cell);
|
| 110 |
+
if (row.length > 0) rows.push(row);
|
| 111 |
+
j++;
|
| 112 |
+
}
|
| 113 |
+
i = j - 1;
|
| 114 |
+
sections.push({ type: 'table', content: '', headers, rows });
|
| 115 |
+
}
|
| 116 |
+
// Links
|
| 117 |
+
else if (line.match(/https?:\/\/[^\s]+/)) {
|
| 118 |
+
const urlMatch = line.match(/(https?:\/\/[^\s]+)/);
|
| 119 |
+
if (urlMatch) {
|
| 120 |
+
const url = urlMatch[1];
|
| 121 |
+
const title = line.replace(url, '').trim();
|
| 122 |
+
sections.push({ type: 'link', content: title, url });
|
| 123 |
+
} else {
|
| 124 |
+
sections.push({ type: 'text', content: line });
|
| 125 |
+
}
|
| 126 |
+
}
|
| 127 |
+
// Key metrics (• Key: Value format)
|
| 128 |
+
else if (line.includes('•') && line.includes(':')) {
|
| 129 |
+
sections.push({ type: 'metric', content: line });
|
| 130 |
+
}
|
| 131 |
+
// Regular text
|
| 132 |
+
else {
|
| 133 |
+
sections.push({ type: 'text', content: line });
|
| 134 |
+
}
|
| 135 |
+
}
|
| 136 |
+
|
| 137 |
+
return sections;
|
| 138 |
+
};
|
| 139 |
+
|
| 140 |
+
const sections = parseContent(content);
|
| 141 |
+
|
| 142 |
+
const renderSection = (section: any, index: number) => {
|
| 143 |
+
const sectionId = `section-${index}`;
|
| 144 |
+
|
| 145 |
+
switch (section.type) {
|
| 146 |
+
case 'header':
|
| 147 |
+
return (
|
| 148 |
+
<Box key={index} sx={{ mb: 3 }}>
|
| 149 |
+
<Typography
|
| 150 |
+
variant="h4"
|
| 151 |
+
component="h1"
|
| 152 |
+
sx={{
|
| 153 |
+
color: 'primary.main',
|
| 154 |
+
fontWeight: 'bold',
|
| 155 |
+
mb: 2,
|
| 156 |
+
borderBottom: '2px solid',
|
| 157 |
+
borderColor: 'primary.light',
|
| 158 |
+
pb: 1
|
| 159 |
+
}}
|
| 160 |
+
>
|
| 161 |
+
{section.content}
|
| 162 |
+
</Typography>
|
| 163 |
+
</Box>
|
| 164 |
+
);
|
| 165 |
+
|
| 166 |
+
case 'subheader':
|
| 167 |
+
return (
|
| 168 |
+
<Box key={index} sx={{ mb: 2 }}>
|
| 169 |
+
<Typography
|
| 170 |
+
variant={section.level === 2 ? "h5" : "h6"}
|
| 171 |
+
component="h2"
|
| 172 |
+
sx={{
|
| 173 |
+
color: 'secondary.main',
|
| 174 |
+
fontWeight: '600',
|
| 175 |
+
mb: 1.5,
|
| 176 |
+
display: 'flex',
|
| 177 |
+
alignItems: 'center',
|
| 178 |
+
gap: 1
|
| 179 |
+
}}
|
| 180 |
+
>
|
| 181 |
+
<Box sx={{
|
| 182 |
+
width: section.level === 2 ? 4 : 3,
|
| 183 |
+
height: section.level === 2 ? 4 : 3,
|
| 184 |
+
bgcolor: 'primary.main',
|
| 185 |
+
borderRadius: '50%'
|
| 186 |
+
}} />
|
| 187 |
+
{section.content}
|
| 188 |
+
</Typography>
|
| 189 |
+
</Box>
|
| 190 |
+
);
|
| 191 |
+
|
| 192 |
+
case 'highlight':
|
| 193 |
+
return (
|
| 194 |
+
<Box key={index} sx={{ mb: 2 }}>
|
| 195 |
+
<Chip
|
| 196 |
+
label={section.content}
|
| 197 |
+
color="primary"
|
| 198 |
+
variant="filled"
|
| 199 |
+
sx={{
|
| 200 |
+
fontSize: '0.9rem',
|
| 201 |
+
fontWeight: 'bold',
|
| 202 |
+
mb: 1,
|
| 203 |
+
'& .MuiChip-label': {
|
| 204 |
+
px: 2,
|
| 205 |
+
py: 1
|
| 206 |
+
}
|
| 207 |
+
}}
|
| 208 |
+
/>
|
| 209 |
+
</Box>
|
| 210 |
+
);
|
| 211 |
+
|
| 212 |
+
case 'list':
|
| 213 |
+
return (
|
| 214 |
+
<Card key={index} sx={{ mb: 2, bgcolor: 'background.paper' }}>
|
| 215 |
+
<CardContent sx={{ p: 2 }}>
|
| 216 |
+
<List dense>
|
| 217 |
+
{section.items.map((item: string, itemIndex: number) => (
|
| 218 |
+
<ListItem key={itemIndex} sx={{ px: 0, py: 0.5 }}>
|
| 219 |
+
<Box sx={{
|
| 220 |
+
width: 6,
|
| 221 |
+
height: 6,
|
| 222 |
+
bgcolor: 'primary.main',
|
| 223 |
+
borderRadius: '50%',
|
| 224 |
+
mr: 2,
|
| 225 |
+
mt: 0.5
|
| 226 |
+
}} />
|
| 227 |
+
<ListItemText
|
| 228 |
+
primary={item}
|
| 229 |
+
sx={{
|
| 230 |
+
'& .MuiListItemText-primary': {
|
| 231 |
+
fontSize: '0.9rem',
|
| 232 |
+
lineHeight: 1.4
|
| 233 |
+
}
|
| 234 |
+
}}
|
| 235 |
+
/>
|
| 236 |
+
</ListItem>
|
| 237 |
+
))}
|
| 238 |
+
</List>
|
| 239 |
+
</CardContent>
|
| 240 |
+
</Card>
|
| 241 |
+
);
|
| 242 |
+
|
| 243 |
+
case 'code':
|
| 244 |
+
const isExpanded = expandedSections[sectionId];
|
| 245 |
+
const shouldShowExpand = section.content.length > 200;
|
| 246 |
+
|
| 247 |
+
return (
|
| 248 |
+
<Card key={index} sx={{ mb: 2, bgcolor: 'grey.900', color: 'common.white' }}>
|
| 249 |
+
<CardContent sx={{ p: 2 }}>
|
| 250 |
+
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 1 }}>
|
| 251 |
+
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
| 252 |
+
<CodeIcon sx={{ fontSize: 18, color: 'primary.main' }} />
|
| 253 |
+
<Typography variant="caption" sx={{ color: 'grey.400' }}>
|
| 254 |
+
{section.language || 'Code'}
|
| 255 |
+
</Typography>
|
| 256 |
+
</Box>
|
| 257 |
+
{shouldShowExpand && (
|
| 258 |
+
<IconButton
|
| 259 |
+
size="small"
|
| 260 |
+
onClick={() => toggleSection(sectionId)}
|
| 261 |
+
sx={{ color: 'grey.400' }}
|
| 262 |
+
>
|
| 263 |
+
{isExpanded ? <ExpandLess /> : <ExpandMore />}
|
| 264 |
+
</IconButton>
|
| 265 |
+
)}
|
| 266 |
+
</Box>
|
| 267 |
+
<Collapse in={isExpanded || !shouldShowExpand}>
|
| 268 |
+
<Box sx={{
|
| 269 |
+
bgcolor: 'grey.800',
|
| 270 |
+
p: 1.5,
|
| 271 |
+
borderRadius: 1,
|
| 272 |
+
fontFamily: 'monospace',
|
| 273 |
+
fontSize: '0.8rem',
|
| 274 |
+
overflow: 'auto',
|
| 275 |
+
maxHeight: isExpanded ? 'none' : '150px'
|
| 276 |
+
}}>
|
| 277 |
+
<pre style={{ margin: 0, whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}>
|
| 278 |
+
{section.content}
|
| 279 |
+
</pre>
|
| 280 |
+
</Box>
|
| 281 |
+
</Collapse>
|
| 282 |
+
</CardContent>
|
| 283 |
+
</Card>
|
| 284 |
+
);
|
| 285 |
+
|
| 286 |
+
case 'table':
|
| 287 |
+
return (
|
| 288 |
+
<Card key={index} sx={{ mb: 2 }}>
|
| 289 |
+
<CardContent sx={{ p: 0 }}>
|
| 290 |
+
<Box sx={{ display: 'flex', alignItems: 'center', p: 2, borderBottom: '1px solid', borderColor: 'divider' }}>
|
| 291 |
+
<TableIcon sx={{ mr: 1, color: 'primary.main' }} />
|
| 292 |
+
<Typography variant="subtitle2">Data Table</Typography>
|
| 293 |
+
</Box>
|
| 294 |
+
<TableContainer>
|
| 295 |
+
<Table size="small">
|
| 296 |
+
<TableHead>
|
| 297 |
+
<TableRow sx={{ bgcolor: 'primary.light' }}>
|
| 298 |
+
{section.headers.map((header: string, headerIndex: number) => (
|
| 299 |
+
<TableCell key={headerIndex} sx={{ fontWeight: 'bold', color: 'primary.contrastText' }}>
|
| 300 |
+
{header}
|
| 301 |
+
</TableCell>
|
| 302 |
+
))}
|
| 303 |
+
</TableRow>
|
| 304 |
+
</TableHead>
|
| 305 |
+
<TableBody>
|
| 306 |
+
{section.rows.map((row: string[], rowIndex: number) => (
|
| 307 |
+
<TableRow key={rowIndex} sx={{ '&:nth-of-type(odd)': { bgcolor: 'action.hover' } }}>
|
| 308 |
+
{row.map((cell: string, cellIndex: number) => (
|
| 309 |
+
<TableCell key={cellIndex}>{cell}</TableCell>
|
| 310 |
+
))}
|
| 311 |
+
</TableRow>
|
| 312 |
+
))}
|
| 313 |
+
</TableBody>
|
| 314 |
+
</Table>
|
| 315 |
+
</TableContainer>
|
| 316 |
+
</CardContent>
|
| 317 |
+
</Card>
|
| 318 |
+
);
|
| 319 |
+
|
| 320 |
+
case 'link':
|
| 321 |
+
return (
|
| 322 |
+
<Box key={index} sx={{ mb: 2 }}>
|
| 323 |
+
<Link
|
| 324 |
+
href={section.url}
|
| 325 |
+
target="_blank"
|
| 326 |
+
rel="noopener noreferrer"
|
| 327 |
+
sx={{
|
| 328 |
+
display: 'inline-flex',
|
| 329 |
+
alignItems: 'center',
|
| 330 |
+
gap: 1,
|
| 331 |
+
color: 'primary.main',
|
| 332 |
+
textDecoration: 'none',
|
| 333 |
+
'&:hover': {
|
| 334 |
+
textDecoration: 'underline'
|
| 335 |
+
}
|
| 336 |
+
}}
|
| 337 |
+
>
|
| 338 |
+
<LinkIcon sx={{ fontSize: 16 }} />
|
| 339 |
+
{section.content || section.url}
|
| 340 |
+
</Link>
|
| 341 |
+
</Box>
|
| 342 |
+
);
|
| 343 |
+
|
| 344 |
+
case 'metric':
|
| 345 |
+
const [label, value] = section.content.split(':').map((s: string) => s.trim());
|
| 346 |
+
return (
|
| 347 |
+
<Card key={index} sx={{ mb: 2, bgcolor: 'success.light', color: 'success.contrastText' }}>
|
| 348 |
+
<CardContent sx={{ p: 2 }}>
|
| 349 |
+
<Typography variant="body2" sx={{ mb: 0.5, opacity: 0.8 }}>
|
| 350 |
+
{label.replace('•', '').trim()}
|
| 351 |
+
</Typography>
|
| 352 |
+
<Typography variant="h6" sx={{ fontWeight: 'bold' }}>
|
| 353 |
+
{value}
|
| 354 |
+
</Typography>
|
| 355 |
+
</CardContent>
|
| 356 |
+
</Card>
|
| 357 |
+
);
|
| 358 |
+
|
| 359 |
+
case 'text':
|
| 360 |
+
default:
|
| 361 |
+
return section.content ? (
|
| 362 |
+
<Typography
|
| 363 |
+
key={index}
|
| 364 |
+
variant="body1"
|
| 365 |
+
sx={{
|
| 366 |
+
mb: 2,
|
| 367 |
+
lineHeight: 1.6,
|
| 368 |
+
color: isBotMessage ? 'text.primary' : 'text.secondary'
|
| 369 |
+
}}
|
| 370 |
+
>
|
| 371 |
+
{section.content}
|
| 372 |
+
</Typography>
|
| 373 |
+
) : null;
|
| 374 |
+
}
|
| 375 |
+
};
|
| 376 |
+
|
| 377 |
+
return (
|
| 378 |
+
<Box sx={{
|
| 379 |
+
maxWidth: '100%',
|
| 380 |
+
'& > *': {
|
| 381 |
+
mb: 1.5
|
| 382 |
+
}
|
| 383 |
+
}}>
|
| 384 |
+
{sections.map((section: any, index: number) => renderSection(section, index))}
|
| 385 |
+
</Box>
|
| 386 |
+
);
|
| 387 |
+
};
|
| 388 |
+
|
| 389 |
+
export default ResponseRenderer;
|
src/contexts/AgentContext.tsx
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { createContext, useContext, useState, ReactNode } from 'react';
|
| 2 |
+
|
| 3 |
+
export type Agent = {
|
| 4 |
+
id: string;
|
| 5 |
+
name: string;
|
| 6 |
+
description?: string;
|
| 7 |
+
avatar?: string;
|
| 8 |
+
upcoming?: boolean;
|
| 9 |
+
};
|
| 10 |
+
|
| 11 |
+
type AgentContextValue = {
|
| 12 |
+
selectedAgent: Agent | null;
|
| 13 |
+
setSelectedAgent: (a: Agent | null) => void;
|
| 14 |
+
availableAgents: Agent[]; // Add availableAgents to the context type
|
| 15 |
+
};
|
| 16 |
+
const AgentContext = createContext<AgentContextValue | undefined>(undefined);
|
| 17 |
+
|
| 18 |
+
export const AgentProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
|
| 19 |
+
// Define your agents list
|
| 20 |
+
const [availableAgents] = useState<Agent[]>([
|
| 21 |
+
{
|
| 22 |
+
id: 'agent-grapher',
|
| 23 |
+
name: 'Agent Grapher',
|
| 24 |
+
description: 'Advanced data visualization and analysis assistant',
|
| 25 |
+
avatar: 'https://api.iconify.design/mdi/chart-line.svg?color=%237C3AED',
|
| 26 |
+
},
|
| 27 |
+
{
|
| 28 |
+
id: 'rival-lens',
|
| 29 |
+
name: 'Bizinsight',
|
| 30 |
+
description: 'Business insights and competitive analysis bot',
|
| 31 |
+
avatar: 'https://api.iconify.design/mdi/brain.svg?color=%2306B6D4',
|
| 32 |
+
},
|
| 33 |
+
{
|
| 34 |
+
id: 'lexa-legal',
|
| 35 |
+
name: 'Lexa (Legal Assistant)',
|
| 36 |
+
description: 'Legal Assistant',
|
| 37 |
+
avatar: 'https://api.iconify.design/mdi/scale-balance.svg?color=%235B21B6',
|
| 38 |
+
upcoming: true,
|
| 39 |
+
}
|
| 40 |
+
]);
|
| 41 |
+
|
| 42 |
+
const [selectedAgent, setSelectedAgent] = useState<Agent | null>(null);
|
| 43 |
+
|
| 44 |
+
return (
|
| 45 |
+
<AgentContext.Provider
|
| 46 |
+
value={{
|
| 47 |
+
selectedAgent,
|
| 48 |
+
setSelectedAgent,
|
| 49 |
+
availableAgents // Include availableAgents in the context value
|
| 50 |
+
}}
|
| 51 |
+
>
|
| 52 |
+
{children}
|
| 53 |
+
</AgentContext.Provider>
|
| 54 |
+
);
|
| 55 |
+
};
|
| 56 |
+
|
| 57 |
+
export const useAgent = (): AgentContextValue => {
|
| 58 |
+
const ctx = useContext(AgentContext);
|
| 59 |
+
if (!ctx) {
|
| 60 |
+
throw new Error('useAgent must be used within an AgentProvider');
|
| 61 |
+
}
|
| 62 |
+
return ctx;
|
| 63 |
+
};
|
src/{index.js → index.tsx}
RENAMED
|
@@ -4,7 +4,9 @@ import './index.css';
|
|
| 4 |
import App from './App';
|
| 5 |
import reportWebVitals from './reportWebVitals';
|
| 6 |
|
| 7 |
-
const root = ReactDOM.createRoot(
|
|
|
|
|
|
|
| 8 |
root.render(
|
| 9 |
<React.StrictMode>
|
| 10 |
<App />
|
|
|
|
| 4 |
import App from './App';
|
| 5 |
import reportWebVitals from './reportWebVitals';
|
| 6 |
|
| 7 |
+
const root = ReactDOM.createRoot(
|
| 8 |
+
document.getElementById('root') as HTMLElement
|
| 9 |
+
);
|
| 10 |
root.render(
|
| 11 |
<React.StrictMode>
|
| 12 |
<App />
|
src/react-app-env.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
/// <reference types="react-scripts" />
|
src/{reportWebVitals.js → reportWebVitals.ts}
RENAMED
|
@@ -1,4 +1,6 @@
|
|
| 1 |
-
|
|
|
|
|
|
|
| 2 |
if (onPerfEntry && onPerfEntry instanceof Function) {
|
| 3 |
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
|
| 4 |
getCLS(onPerfEntry);
|
|
|
|
| 1 |
+
import { ReportHandler } from 'web-vitals';
|
| 2 |
+
|
| 3 |
+
const reportWebVitals = (onPerfEntry?: ReportHandler) => {
|
| 4 |
if (onPerfEntry && onPerfEntry instanceof Function) {
|
| 5 |
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
|
| 6 |
getCLS(onPerfEntry);
|
src/{setupTests.js → setupTests.ts}
RENAMED
|
File without changes
|
tsconfig.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"compilerOptions": {
|
| 3 |
+
"target": "es5",
|
| 4 |
+
"lib": [
|
| 5 |
+
"dom",
|
| 6 |
+
"dom.iterable",
|
| 7 |
+
"esnext"
|
| 8 |
+
],
|
| 9 |
+
"allowJs": true,
|
| 10 |
+
"skipLibCheck": true,
|
| 11 |
+
"esModuleInterop": true,
|
| 12 |
+
"allowSyntheticDefaultImports": true,
|
| 13 |
+
"strict": true,
|
| 14 |
+
"forceConsistentCasingInFileNames": true,
|
| 15 |
+
"noFallthroughCasesInSwitch": true,
|
| 16 |
+
"module": "esnext",
|
| 17 |
+
"moduleResolution": "node",
|
| 18 |
+
"resolveJsonModule": true,
|
| 19 |
+
"isolatedModules": true,
|
| 20 |
+
"noEmit": true,
|
| 21 |
+
"jsx": "react-jsx"
|
| 22 |
+
},
|
| 23 |
+
"include": [
|
| 24 |
+
"src"
|
| 25 |
+
]
|
| 26 |
+
}
|