first commit
This commit is contained in:
parent
5c5d88c92f
commit
eb4f62c56d
41 changed files with 3851 additions and 19 deletions
4
docker_svc/real-time-chat-app/.dockerignore
Normal file
4
docker_svc/real-time-chat-app/.dockerignore
Normal file
|
@ -0,0 +1,4 @@
|
|||
**/node_modules
|
||||
**/out/
|
||||
**/.history/
|
||||
**/__pycache__/
|
37
docker_svc/real-time-chat-app/Dockerfile
Normal file
37
docker_svc/real-time-chat-app/Dockerfile
Normal file
|
@ -0,0 +1,37 @@
|
|||
# Use the official Node.js image as a base
|
||||
FROM node:20
|
||||
|
||||
# Set the working directory inside the container
|
||||
WORKDIR /app
|
||||
|
||||
# Install pnpm globally with setup
|
||||
ENV PNPM_HOME=/usr/local/bin
|
||||
RUN npm install -g pnpm
|
||||
|
||||
# Copy package.json and package-lock.json to the working directory
|
||||
COPY app/package*.json ./
|
||||
|
||||
# Install dependencies using pnpm
|
||||
RUN pnpm install
|
||||
|
||||
# Set environment variables for the build
|
||||
ARG REACT_APP_API_BASE_URL
|
||||
ENV REACT_APP_API_BASE_URL=${REACT_APP_API_BASE_URL:-http://rag-service:8000}
|
||||
|
||||
# Copy specific folders to avoid node_modules
|
||||
COPY app/public ./public
|
||||
COPY app/src ./src
|
||||
COPY app/*.json ./
|
||||
|
||||
# Build the React application with pnpm - show env vars for debugging
|
||||
RUN echo "Building with API URL: $REACT_APP_API_BASE_URL" && \
|
||||
pnpm run build
|
||||
|
||||
# Use npm for global installs (more reliable in Docker)
|
||||
RUN npm install -g serve
|
||||
|
||||
# Expose the port the app runs on
|
||||
EXPOSE 3000
|
||||
|
||||
# Command to run the application
|
||||
CMD ["serve", "-s", "build"]
|
70
docker_svc/real-time-chat-app/README.md
Normal file
70
docker_svc/real-time-chat-app/README.md
Normal file
|
@ -0,0 +1,70 @@
|
|||
# Getting Started with Create React App
|
||||
|
||||
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
|
||||
|
||||
## Available Scripts
|
||||
|
||||
In the project directory, you can run:
|
||||
|
||||
### `npm start`
|
||||
|
||||
Runs the app in the development mode.\
|
||||
Open [http://localhost:3000](http://localhost:3000) to view it in your browser.
|
||||
|
||||
The page will reload when you make changes.\
|
||||
You may also see any lint errors in the console.
|
||||
|
||||
### `npm test`
|
||||
|
||||
Launches the test runner in the interactive watch mode.\
|
||||
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
|
||||
|
||||
### `npm run build`
|
||||
|
||||
Builds the app for production to the `build` folder.\
|
||||
It correctly bundles React in production mode and optimizes the build for the best performance.
|
||||
|
||||
The build is minified and the filenames include the hashes.\
|
||||
Your app is ready to be deployed!
|
||||
|
||||
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
|
||||
|
||||
### `npm run eject`
|
||||
|
||||
**Note: this is a one-way operation. Once you `eject`, you can't go back!**
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
## Learn More
|
||||
|
||||
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
|
||||
|
||||
To learn React, check out the [React documentation](https://reactjs.org/).
|
||||
|
||||
### Code Splitting
|
||||
|
||||
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)
|
||||
|
||||
### Analyzing the Bundle Size
|
||||
|
||||
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)
|
||||
|
||||
### Making a Progressive Web App
|
||||
|
||||
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)
|
||||
|
||||
### Advanced Configuration
|
||||
|
||||
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)
|
||||
|
||||
### Deployment
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment)
|
||||
|
||||
### `npm run build` fails to minify
|
||||
|
||||
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)
|
28
docker_svc/real-time-chat-app/app/nginx.conf
Normal file
28
docker_svc/real-time-chat-app/app/nginx.conf
Normal file
|
@ -0,0 +1,28 @@
|
|||
server {
|
||||
listen 80;
|
||||
|
||||
# Compression
|
||||
gzip on;
|
||||
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
|
||||
|
||||
# Root directory and index file
|
||||
root /usr/share/nginx/html;
|
||||
index index.html index.htm;
|
||||
|
||||
# Handle React Router
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
# Cache static assets
|
||||
location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg)$ {
|
||||
expires 30d;
|
||||
add_header Cache-Control "public, no-transform";
|
||||
}
|
||||
|
||||
# Error pages
|
||||
error_page 500 502 503 504 /50x.html;
|
||||
location = /50x.html {
|
||||
root /usr/share/nginx/html;
|
||||
}
|
||||
}
|
42
docker_svc/real-time-chat-app/app/package.json
Normal file
42
docker_svc/real-time-chat-app/app/package.json
Normal file
|
@ -0,0 +1,42 @@
|
|||
{
|
||||
"name": "real-time-chat-app",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@testing-library/dom": "^10.4.0",
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"@testing-library/react": "^16.2.0",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-scripts": "5.0.1",
|
||||
"web-vitals": "^2.1.4",
|
||||
"websocket": "^1.0.35",
|
||||
"react-syntax-highlighter": "^15.5.0",
|
||||
"react-markdown": "^7.0.1"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
}
|
||||
}
|
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 17 KiB |
60
docker_svc/real-time-chat-app/app/public/chad-logo-white.svg
Normal file
60
docker_svc/real-time-chat-app/app/public/chad-logo-white.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 16 KiB |
BIN
docker_svc/real-time-chat-app/app/public/favicon.ico
Normal file
BIN
docker_svc/real-time-chat-app/app/public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.8 KiB |
43
docker_svc/real-time-chat-app/app/public/index.html
Normal file
43
docker_svc/real-time-chat-app/app/public/index.html
Normal file
|
@ -0,0 +1,43 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Web site created using create-react-app"
|
||||
/>
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
Only files inside the `public` folder can be referenced from the HTML.
|
||||
|
||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<title>React App</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
If you open it directly in the browser, you will see an empty page.
|
||||
|
||||
You can add webfonts, meta tags, or analytics to this file.
|
||||
The build step will place the bundled scripts into the <body> tag.
|
||||
|
||||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
-->
|
||||
</body>
|
||||
</html>
|
BIN
docker_svc/real-time-chat-app/app/public/logo192.png
Normal file
BIN
docker_svc/real-time-chat-app/app/public/logo192.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.2 KiB |
BIN
docker_svc/real-time-chat-app/app/public/logo512.png
Normal file
BIN
docker_svc/real-time-chat-app/app/public/logo512.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 9.4 KiB |
25
docker_svc/real-time-chat-app/app/public/manifest.json
Normal file
25
docker_svc/real-time-chat-app/app/public/manifest.json
Normal file
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"short_name": "React App",
|
||||
"name": "Create React App Sample",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
},
|
||||
{
|
||||
"src": "logo192.png",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192"
|
||||
},
|
||||
{
|
||||
"src": "logo512.png",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512"
|
||||
}
|
||||
],
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#ffffff"
|
||||
}
|
3
docker_svc/real-time-chat-app/app/public/robots.txt
Normal file
3
docker_svc/real-time-chat-app/app/public/robots.txt
Normal file
|
@ -0,0 +1,3 @@
|
|||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
Disallow:
|
1074
docker_svc/real-time-chat-app/app/src/App.css
Normal file
1074
docker_svc/real-time-chat-app/app/src/App.css
Normal file
File diff suppressed because it is too large
Load diff
912
docker_svc/real-time-chat-app/app/src/App.js
Normal file
912
docker_svc/real-time-chat-app/app/src/App.js
Normal file
|
@ -0,0 +1,912 @@
|
|||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import './App.css';
|
||||
// Code highlighting support:
|
||||
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
|
||||
import { tomorrow } from 'react-syntax-highlighter/dist/esm/styles/prism';
|
||||
// Markdown parsing:
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
|
||||
// Custom renderer for code blocks to use our SyntaxHighlighter
|
||||
const MarkdownComponents = {
|
||||
code({node, inline, className, children, ...props}) {
|
||||
const match = /language-(\w+)/.exec(className || '');
|
||||
return !inline && match ? (
|
||||
<SyntaxHighlighter
|
||||
style={tomorrow}
|
||||
language={match[1]}
|
||||
PreTag="div"
|
||||
{...props}
|
||||
>
|
||||
{String(children).replace(/\n$/, '')}
|
||||
</SyntaxHighlighter>
|
||||
) : (
|
||||
<code className={className} {...props}>
|
||||
{children}
|
||||
</code>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// Helper to format messages using ReactMarkdown to support Markdown syntax and code highlighting
|
||||
const formatMessage = (content) => {
|
||||
return (
|
||||
<ReactMarkdown components={MarkdownComponents}>
|
||||
{content}
|
||||
</ReactMarkdown>
|
||||
);
|
||||
};
|
||||
|
||||
// New helper to process thinking content
|
||||
const processThinkingContent = (message) => {
|
||||
// Check if the message contains thinking tags
|
||||
const thinkPattern = /<think>([\s\S]*?)<\/think>/;
|
||||
const thinkMatch = message.match(thinkPattern);
|
||||
|
||||
if (thinkMatch) {
|
||||
// Extract thinking part and remaining content
|
||||
const thinkingContent = thinkMatch[1].trim();
|
||||
const regularContent = message.replace(thinkPattern, '').trim();
|
||||
|
||||
return {
|
||||
hasThinking: true,
|
||||
thinking: thinkingContent,
|
||||
response: regularContent
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
hasThinking: false,
|
||||
thinking: '',
|
||||
response: message
|
||||
};
|
||||
};
|
||||
|
||||
// Determine API base URL from environment or use dynamic fallbacks
|
||||
const getApiBaseUrl = () => {
|
||||
// When running locally, use localhost port (as published by docker-compose)
|
||||
if (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') {
|
||||
console.log('Using local API URL: http://localhost:8000');
|
||||
return 'http://localhost:8000';
|
||||
}
|
||||
|
||||
// Otherwise, if an environment variable is defined, use that
|
||||
if (process.env.REACT_APP_API_BASE_URL) {
|
||||
console.log(`Using API URL from env: ${process.env.REACT_APP_API_BASE_URL}`);
|
||||
return process.env.REACT_APP_API_BASE_URL;
|
||||
}
|
||||
|
||||
// Fallback to relative URLs (for production with reverse proxy)
|
||||
console.log('Using relative URL for API');
|
||||
return '';
|
||||
};
|
||||
|
||||
// Moon and Sun icons for theme toggle
|
||||
const MoonIcon = () => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const SunIcon = () => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<circle cx="12" cy="12" r="5"></circle>
|
||||
<line x1="12" y1="1" x2="12" y2="3"></line>
|
||||
<line x1="12" y1="21" x2="12" y2="23"></line>
|
||||
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line>
|
||||
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line>
|
||||
<line x1="1" y1="12" x2="3" y2="12"></line>
|
||||
<line x1="21" y1="12" x2="23" y2="12"></line>
|
||||
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line>
|
||||
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line>
|
||||
</svg>
|
||||
);
|
||||
|
||||
// Icone per l'invio e il caricamento
|
||||
const SendIcon = () => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<line x1="22" y1="2" x2="11" y2="13"></line>
|
||||
<polygon points="22 2 15 22 11 13 2 9 22 2"></polygon>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const LoadingIcon = () => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="loading-spinner">
|
||||
<circle cx="12" cy="12" r="10"></circle>
|
||||
<path d="M12 6v6l4 2"></path>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const AttachmentIcon = () => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M21.44 11.05l-9.19 9.19a5.5 5.5 0 0 1-7.78-7.78l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.19 9.19a2.5 2.5 0 0 1-3.54-3.54l8.48-8.48"></path>
|
||||
</svg>
|
||||
);
|
||||
|
||||
// Icona cervello attivo per la modalità "thinking"
|
||||
const ThinkingIcon = () => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M9.5 2A2.5 2.5 0 0 1 12 4.5v15a2.5 2.5 0 0 1-4.96.44A2.5 2.5 0 0 1 5.5 17v-2.5a2.5 2.5 0 0 1-.64-4.9 2.5 2.5 0 0 1 2.14-4.5 2.5 2.5 0 0 1 4.5-3"></path>
|
||||
<path d="M14.5 2A2.5 2.5 0 0 0 12 4.5v15a2.5 2.5 0 0 0 4.96.44A2.5 2.5 0 0 0 18.5 17v-2.5a2.5 2.5 0 0 0 .64-4.9 2.5 2.5 0 0 0-2.14-4.5 2.5 2.5 0 0 0-4.5-3"></path>
|
||||
</svg>
|
||||
);
|
||||
|
||||
// Icona cervello disattivato (con traccia sopra)
|
||||
const ThinkingDisabledIcon = () => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M9.5 2A2.5 2.5 0 0 1 12 4.5v15a2.5 2.5 0 0 1-4.96.44A2.5 2.5 0 0 1 5.5 17v-2.5a2.5 2.5 0 0 1-.64-4.9 2.5 2.5 0 0 1 2.14-4.5 2.5 2.5 0 0 1 4.5-3"></path>
|
||||
<path d="M14.5 2A2.5 2.5 0 0 0 12 4.5v15a2.5 2.5 0 0 0 4.96.44A2.5 2.5 0 0 0 18.5 17v-2.5a2.5 2.5 0 0 0 .64-4.9 2.5 2.5 0 0 0-2.14-4.5 2.5 2.5 0 0 0-4.5-3"></path>
|
||||
<line x1="2" y1="2" x2="22" y2="22" strokeWidth="1.5"></line>
|
||||
</svg>
|
||||
);
|
||||
|
||||
// Add Trash icon for delete functionality
|
||||
const TrashIcon = () => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<polyline points="3 6 5 6 21 6"></polyline>
|
||||
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
|
||||
</svg>
|
||||
);
|
||||
|
||||
// Replace the Clear History icon with a chat bubble icon
|
||||
const ClearHistoryIcon = () => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path>
|
||||
<line x1="9" y1="10" x2="15" y2="10"></line>
|
||||
<line x1="12" y1="7" x2="12" y2="13"></line>
|
||||
</svg>
|
||||
);
|
||||
|
||||
// Aggiungiamo le icone per le personalità
|
||||
const PersonalityIcon = () => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<circle cx="12" cy="12" r="10"></circle>
|
||||
<path d="M8 14s1.5 2 4 2 4-2 4-2"></path>
|
||||
<line x1="9" y1="9" x2="9.01" y2="9"></line>
|
||||
<line x1="15" y1="9" x2="15.01" y2="9"></line>
|
||||
</svg>
|
||||
);
|
||||
|
||||
// Aggiorniamo le icone per le personalità con versioni più moderne
|
||||
const CoolIcon = () => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<circle cx="12" cy="12" r="10"></circle>
|
||||
<path d="M8 14s1.5 2 4 2 4-2 4-2"></path>
|
||||
<line x1="8" y1="9" x2="9" y2="9"></line>
|
||||
<line x1="15" y1="9" x2="16" y2="9"></line>
|
||||
<path d="M3 8l2-1"></path>
|
||||
<path d="M21 8l-2-1"></path>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const CynicalIcon = () => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<circle cx="12" cy="12" r="10"></circle>
|
||||
<path d="M8 16s1.5 -1 4 -1 4 1 4 1"></path>
|
||||
<line x1="9" y1="9" x2="9.01" y2="9"></line>
|
||||
<line x1="15" y1="9" x2="15.01" y2="9"></line>
|
||||
<path d="M17 5l2 -2"></path>
|
||||
<path d="M7 5l-2 -2"></path>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const SupportiveIcon = () => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<circle cx="12" cy="12" r="10"></circle>
|
||||
<path d="M8 13s1.5 2 4 2 4-2 4-2"></path>
|
||||
<path d="M9 9h.01"></path>
|
||||
<path d="M15 9h.01"></path>
|
||||
<path d="M12 7v0.01"></path>
|
||||
<path d="M12 17v.01"></path>
|
||||
<path d="M12 3v2"></path>
|
||||
<path d="M12 19v2"></path>
|
||||
</svg>
|
||||
);
|
||||
|
||||
function App() {
|
||||
const [messages, setMessages] = useState(() => {
|
||||
// Carica i messaggi precedenti dal localStorage o inizializza un array vuoto
|
||||
const savedMessages = localStorage.getItem('chatMessages');
|
||||
return savedMessages ? JSON.parse(savedMessages) : [];
|
||||
});
|
||||
|
||||
const [messageInput, setMessageInput] = useState('');
|
||||
|
||||
const [language, setLanguage] = useState(() => {
|
||||
// Carica la lingua precedente dal localStorage o usa auto-detect come predefinito
|
||||
return localStorage.getItem('selectedLanguage') || 'auto';
|
||||
});
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
// Aggiungo un ref per il container dei messaggi per gestire lo scroll
|
||||
const messagesEndRef = useRef(null);
|
||||
|
||||
const [darkTheme, setDarkTheme] = useState(() => {
|
||||
// Carica il tema precedente dal localStorage o usa dark come predefinito
|
||||
const savedTheme = localStorage.getItem('darkTheme');
|
||||
return savedTheme !== null ? JSON.parse(savedTheme) : true;
|
||||
});
|
||||
|
||||
const fileInputRef = useRef(null);
|
||||
|
||||
const [reasoning, setReasoning] = useState(() => {
|
||||
// Carica lo stato reasoning precedente dal localStorage
|
||||
const savedReasoning = localStorage.getItem('reasoningEnabled');
|
||||
return savedReasoning !== null ? JSON.parse(savedReasoning) : false;
|
||||
});
|
||||
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
|
||||
const apiBaseUrl = getApiBaseUrl();
|
||||
|
||||
// Nomi localizzati delle lingue
|
||||
const languageLabels = {
|
||||
french: "Français",
|
||||
italian: "Italiano",
|
||||
english: "English",
|
||||
german: "Deutsch",
|
||||
auto: "Auto-detect"
|
||||
};
|
||||
|
||||
const [personality, setPersonality] = useState(() => {
|
||||
// Carica la personalità precedente dal localStorage o usa supportive come predefinito
|
||||
const savedPersonality = localStorage.getItem('personalityType');
|
||||
return savedPersonality || 'supportive';
|
||||
});
|
||||
|
||||
// Rimuovo il dropdown state che non serve più
|
||||
// const [showPersonalityDropdown, setShowPersonalityDropdown] = useState(false);
|
||||
|
||||
// Effect to set initial theme and adjust container height
|
||||
useEffect(() => {
|
||||
// Apply theme class on first load
|
||||
if (darkTheme) {
|
||||
document.body.classList.add('dark-theme');
|
||||
} else {
|
||||
document.body.classList.remove('dark-theme');
|
||||
}
|
||||
|
||||
// Save theme to localStorage
|
||||
localStorage.setItem('darkTheme', JSON.stringify(darkTheme));
|
||||
|
||||
// Function to adjust chat container height
|
||||
const adjustHeight = () => {
|
||||
const vh = window.innerHeight;
|
||||
const headerHeight = document.querySelector('.App-header')?.offsetHeight || 0;
|
||||
const messagesContainer = document.querySelector('.messages-container');
|
||||
if (messagesContainer) {
|
||||
const inputContainer = document.querySelector('.input-container')?.offsetHeight || 0;
|
||||
const fileUpload = document.querySelector('.file-upload')?.offsetHeight || 0;
|
||||
const availableHeight = vh - headerHeight - inputContainer - fileUpload - 60; // 60px for padding/margins
|
||||
messagesContainer.style.height = `${Math.max(400, availableHeight)}px`;
|
||||
}
|
||||
};
|
||||
|
||||
// Run once and add event listener for resize
|
||||
adjustHeight();
|
||||
window.addEventListener('resize', adjustHeight);
|
||||
|
||||
// Cleanup
|
||||
return () => window.removeEventListener('resize', adjustHeight);
|
||||
}, [darkTheme]);
|
||||
|
||||
// Save messages to localStorage whenever they change
|
||||
useEffect(() => {
|
||||
localStorage.setItem('chatMessages', JSON.stringify(messages));
|
||||
}, [messages]);
|
||||
|
||||
// Toggle between light and dark themes
|
||||
const toggleTheme = () => {
|
||||
setDarkTheme(prevTheme => !prevTheme);
|
||||
};
|
||||
|
||||
// Aggiorna il setLanguage per salvare nel localStorage
|
||||
const handleLanguageChange = (newLanguage) => {
|
||||
setLanguage(newLanguage);
|
||||
localStorage.setItem('selectedLanguage', newLanguage);
|
||||
};
|
||||
|
||||
// Aggiorna il setReasoning per salvare nel localStorage
|
||||
const toggleReasoning = () => {
|
||||
setReasoning(prev => {
|
||||
const newValue = !prev;
|
||||
localStorage.setItem('reasoningEnabled', JSON.stringify(newValue));
|
||||
return newValue;
|
||||
});
|
||||
};
|
||||
|
||||
// Funzione per scorrere al fondo della chat
|
||||
const scrollToBottom = () => {
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
||||
};
|
||||
|
||||
// Effect per far scorrere la chat quando cambia il contenuto dei messaggi
|
||||
useEffect(() => {
|
||||
scrollToBottom();
|
||||
}, [messages]);
|
||||
|
||||
// Send message directly to FastAPI endpoint
|
||||
const sendMessage = async () => {
|
||||
if (!messageInput.trim()) return;
|
||||
|
||||
// Add user message to the chat
|
||||
const newUserMessage = { role: 'user', content: messageInput };
|
||||
setMessages(prevMessages => [...prevMessages, newUserMessage]);
|
||||
|
||||
// Create the complete message history for the API
|
||||
const messageHistory = [...messages, newUserMessage];
|
||||
|
||||
// Show loading state
|
||||
setIsLoading(true);
|
||||
|
||||
// Clear the input field
|
||||
setMessageInput('');
|
||||
|
||||
try {
|
||||
// Build URL - if apiBaseUrl is empty, this becomes a relative URL
|
||||
const url = apiBaseUrl ? `${apiBaseUrl}/chat` : '/chat';
|
||||
|
||||
// Aggiungo log per vedere qual è la personalità inviata
|
||||
console.log(`Sending message with personality: ${personality}`);
|
||||
console.log(`Connecting to API URL: ${url}`);
|
||||
|
||||
// Aggiungi headers per CORS più espliciti
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'text/event-stream',
|
||||
'Origin': window.location.origin
|
||||
},
|
||||
credentials: 'include', // Include cookies se necessario
|
||||
body: JSON.stringify({
|
||||
messages: messageHistory,
|
||||
language: language,
|
||||
temperature: 0.7,
|
||||
reasoning: reasoning,
|
||||
personality: personality, // Confermo che la personalità viene inviata
|
||||
stream: true
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`API Error: ${response.statusText}`);
|
||||
}
|
||||
|
||||
// Streaming mode (sempre attivo)
|
||||
let currentResponseContent = '';
|
||||
let isInThinkingMode = false;
|
||||
let currentThinkingContent = '';
|
||||
|
||||
// Add a placeholder for the streaming response
|
||||
setMessages(prevMessages => [
|
||||
...prevMessages,
|
||||
{
|
||||
role: 'coach',
|
||||
content: '',
|
||||
streaming: true,
|
||||
thinking: '',
|
||||
isThinking: false
|
||||
}
|
||||
]);
|
||||
|
||||
const responseStream = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'text/event-stream', // Aggiunto header per specificamente accettare eventi SSE
|
||||
},
|
||||
body: JSON.stringify({
|
||||
messages: messageHistory,
|
||||
language: language,
|
||||
temperature: 0.7,
|
||||
reasoning: reasoning,
|
||||
personality: personality, // Confermo che la personalità viene inviata
|
||||
stream: true
|
||||
}),
|
||||
});
|
||||
|
||||
if (!responseStream.ok) {
|
||||
throw new Error(`API Error: ${responseStream.statusText}`);
|
||||
}
|
||||
|
||||
// Process the streaming response
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder("utf-8");
|
||||
|
||||
// Helper function to update the last message
|
||||
const updateLastMessage = (content, thinking = null, isThinking = null) => {
|
||||
setMessages(prevMessages => {
|
||||
const newMessages = [...prevMessages];
|
||||
const lastIndex = newMessages.length - 1;
|
||||
const lastMessage = {...newMessages[lastIndex]};
|
||||
|
||||
if (content !== null) {
|
||||
lastMessage.content = content;
|
||||
}
|
||||
|
||||
if (thinking !== null) {
|
||||
lastMessage.thinking = thinking;
|
||||
}
|
||||
|
||||
if (isThinking !== null) {
|
||||
lastMessage.isThinking = isThinking;
|
||||
}
|
||||
|
||||
newMessages[lastIndex] = lastMessage;
|
||||
return newMessages;
|
||||
});
|
||||
};
|
||||
|
||||
// Process chunks of text
|
||||
const processChunk = (text) => {
|
||||
// Check for thinking tags
|
||||
if (!isInThinkingMode && text.includes('<think>')) {
|
||||
isInThinkingMode = true;
|
||||
const parts = text.split('<think>');
|
||||
|
||||
// Add any text before the <think> tag to the regular response
|
||||
if (parts[0].length > 0) {
|
||||
currentResponseContent += parts[0];
|
||||
}
|
||||
|
||||
// Add the text after <think> to thinking content
|
||||
currentThinkingContent = parts[1] || '';
|
||||
|
||||
// Update the message with thinking mode enabled
|
||||
updateLastMessage(currentResponseContent, currentThinkingContent, true);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for end of thinking
|
||||
if (isInThinkingMode && text.includes('</think>')) {
|
||||
isInThinkingMode = false;
|
||||
const parts = text.split('</think>');
|
||||
|
||||
// Add any remaining text to thinking
|
||||
currentThinkingContent += parts[0];
|
||||
|
||||
// Add any text after </think> to the response
|
||||
if (parts[1] && parts[1].length > 0) {
|
||||
currentResponseContent += parts[1];
|
||||
}
|
||||
|
||||
// Update the message
|
||||
updateLastMessage(currentResponseContent, currentThinkingContent, true);
|
||||
return;
|
||||
}
|
||||
|
||||
// If we're in thinking mode, add to thinking content
|
||||
if (isInThinkingMode) {
|
||||
currentThinkingContent += text;
|
||||
updateLastMessage(null, currentThinkingContent, true);
|
||||
} else {
|
||||
// Otherwise add to normal content
|
||||
currentResponseContent += text;
|
||||
updateLastMessage(currentResponseContent);
|
||||
}
|
||||
};
|
||||
|
||||
const parser = createParser((event) => {
|
||||
if (event.type === "event") {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
|
||||
if (data.content) {
|
||||
// Process the chunk for thinking tags
|
||||
processChunk(data.content);
|
||||
}
|
||||
|
||||
if (data.done) {
|
||||
// Streaming completed, remove streaming flag but preserve thinking state
|
||||
setMessages(prevMessages => {
|
||||
const newMessages = [...prevMessages];
|
||||
const lastIndex = newMessages.length - 1;
|
||||
newMessages[lastIndex] = {
|
||||
...newMessages[lastIndex],
|
||||
streaming: false
|
||||
// Manteniamo i campi thinking e isThinking esistenti
|
||||
};
|
||||
return newMessages;
|
||||
});
|
||||
setIsLoading(false);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Error parsing SSE data", e, event.data);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Read the stream
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
|
||||
const text = decoder.decode(value);
|
||||
parser.feed(text);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to send message:', error);
|
||||
|
||||
// Aggiungi un messaggio più descrittivo sull'errore CORS
|
||||
let errorMessage = 'Error: Could not connect to the server. Please try again later.';
|
||||
|
||||
if (error.message && error.message.includes('NetworkError')) {
|
||||
errorMessage = 'Error: Network error occurred. This might be due to a CORS issue or the backend server being unavailable. Please contact the administrator.';
|
||||
}
|
||||
|
||||
setMessages(prevMessages => [...prevMessages,
|
||||
{ role: 'coach', content: errorMessage }
|
||||
]);
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Helper function for SSE parsing - migliorato per gestire più tipi di eventi SSE
|
||||
function createParser(onParse) {
|
||||
let buffer = '';
|
||||
|
||||
return {
|
||||
feed(chunk) {
|
||||
buffer += chunk;
|
||||
|
||||
// Cerca per pattern di fine del messaggio SSE (doppio newline)
|
||||
let delimiterIndex;
|
||||
while ((delimiterIndex = buffer.indexOf('\n\n')) !== -1) {
|
||||
const rawEvent = buffer.slice(0, delimiterIndex);
|
||||
buffer = buffer.slice(delimiterIndex + 2);
|
||||
|
||||
// Estrai i dati dall'evento
|
||||
const dataMatch = /^data: (.+)$/m.exec(rawEvent);
|
||||
if (dataMatch) {
|
||||
try {
|
||||
const jsonData = dataMatch[1];
|
||||
onParse({ type: "event", data: jsonData });
|
||||
} catch (e) {
|
||||
console.error("Error parsing SSE event:", e, rawEvent);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Add this function to trigger file selection
|
||||
const triggerFileSelect = () => {
|
||||
fileInputRef.current.click();
|
||||
};
|
||||
|
||||
// File upload functionality - modified to add chat messages instead of alerts
|
||||
const uploadFile = async (files) => {
|
||||
if (!files.length) return;
|
||||
|
||||
// Add validation for PDF files only
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
if (files[i].type !== 'application/pdf') {
|
||||
// Add error message to chat instead of alert
|
||||
setMessages(prevMessages => [...prevMessages,
|
||||
{ role: 'coach', content: 'Error: Only PDF files are accepted. Please try again with PDF documents only.' }
|
||||
]);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Create list of file names for success message
|
||||
const fileNames = Array.from(files).map(file => file.name).join(', ');
|
||||
|
||||
const formData = new FormData();
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
formData.append('files', files[i]);
|
||||
}
|
||||
|
||||
try {
|
||||
// Build URL - if apiBaseUrl is empty, this becomes a relative URL
|
||||
const url = apiBaseUrl ? `${apiBaseUrl}/upload` : '/upload';
|
||||
|
||||
// Show loading state
|
||||
setIsLoading(true);
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
// Add success message to chat instead of alert
|
||||
setMessages(prevMessages => [...prevMessages,
|
||||
{ role: 'coach', content: `Files uploaded successfully: ${fileNames}` }
|
||||
]);
|
||||
} else {
|
||||
console.error('Upload failed:', response.statusText);
|
||||
// Add error message to chat instead of alert
|
||||
setMessages(prevMessages => [...prevMessages,
|
||||
{ role: 'coach', content: `Error: Failed to upload files. ${response.statusText}` }
|
||||
]);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to upload files:', error);
|
||||
// Add error message to chat instead of alert
|
||||
setMessages(prevMessages => [...prevMessages,
|
||||
{ role: 'coach', content: 'Error: Could not connect to the server. Failed to upload files.' }
|
||||
]);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Add function to handle deleting all documents
|
||||
const handleDeleteAllDocs = async () => {
|
||||
if (window.confirm('Are you sure you want to delete all documents? This action cannot be undone.')) {
|
||||
setIsDeleting(true);
|
||||
|
||||
try {
|
||||
const url = apiBaseUrl ? `${apiBaseUrl}/docs` : '/docs';
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
// Add confirmation message to chat
|
||||
setMessages(prevMessages => [...prevMessages,
|
||||
{ role: 'coach', content: 'Memory has been cleared. All documents have been deleted from the database.' }
|
||||
]);
|
||||
} else {
|
||||
console.error('Delete failed:', response.statusText);
|
||||
setMessages(prevMessages => [...prevMessages,
|
||||
{ role: 'coach', content: `Error: Failed to delete documents. ${response.statusText}` }
|
||||
]);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to delete documents:', error);
|
||||
setMessages(prevMessages => [...prevMessages,
|
||||
{ role: 'coach', content: 'Error: Could not connect to the server. Failed to delete documents.' }
|
||||
]);
|
||||
} finally {
|
||||
setIsDeleting(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Add function to clear chat history
|
||||
const clearChatHistory = () => {
|
||||
setMessages([]);
|
||||
localStorage.removeItem('chatMessages');
|
||||
};
|
||||
|
||||
// Aggiorna la personalità ciclando tra le opzioni disponibili
|
||||
const togglePersonality = () => {
|
||||
setPersonality(prevPersonality => {
|
||||
// Cicla tra le tre personalità
|
||||
let newPersonality;
|
||||
switch (prevPersonality) {
|
||||
case 'cool':
|
||||
newPersonality = 'cynical';
|
||||
break;
|
||||
case 'cynical':
|
||||
newPersonality = 'supportive';
|
||||
break;
|
||||
default: // 'supportive' o qualsiasi altro valore
|
||||
newPersonality = 'cool';
|
||||
}
|
||||
|
||||
localStorage.setItem('personalityType', newPersonality);
|
||||
return newPersonality;
|
||||
});
|
||||
};
|
||||
|
||||
// Funzione per ottenere l'icona della personalità corrente
|
||||
const getCurrentPersonalityIcon = () => {
|
||||
switch (personality) {
|
||||
case 'cool': return <CoolIcon />;
|
||||
case 'cynical': return <CynicalIcon />;
|
||||
case 'supportive': return <SupportiveIcon />;
|
||||
default: return <PersonalityIcon />;
|
||||
}
|
||||
};
|
||||
|
||||
// Funzione per ottenere il titolo della personalità
|
||||
const getPersonalityTitle = () => {
|
||||
switch (personality) {
|
||||
case 'cool': return "Cool & Confident style";
|
||||
case 'cynical': return "Cynical & Direct style";
|
||||
case 'supportive': return "Supportive & Empathetic style";
|
||||
default: return "Select AI personality style";
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="App">
|
||||
<header className="App-header">
|
||||
<div className="logo-container">
|
||||
<img
|
||||
src={darkTheme ? "/chad-logo-white.svg" : "/chad-logo-white-bg-black.svg"}
|
||||
alt="AI Coach Logo"
|
||||
className="app-logo"
|
||||
/>
|
||||
<h1>MediChaiD - ChaD GPT (Conversational Health Assistance & Direction) </h1>
|
||||
</div>
|
||||
<div className="header-controls">
|
||||
<button
|
||||
className="theme-toggle"
|
||||
onClick={toggleTheme}
|
||||
title={darkTheme ? "Switch to light theme" : "Switch to dark theme"}
|
||||
>
|
||||
{darkTheme ? <SunIcon/> : <MoonIcon/>}
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="chat-container">
|
||||
<div className="messages-container">
|
||||
{messages.map((msg, index) => {
|
||||
// Process content for both streaming and complete messages
|
||||
let processedContent;
|
||||
|
||||
if (msg.role === 'coach') {
|
||||
if (msg.isThinking) {
|
||||
// Per i messaggi con stato thinking già elaborato durante lo streaming
|
||||
processedContent = {
|
||||
hasThinking: true,
|
||||
thinking: msg.thinking || '',
|
||||
response: msg.content
|
||||
};
|
||||
} else {
|
||||
// Per i messaggi normali o quelli completati senza thinking
|
||||
processedContent = processThinkingContent(msg.content);
|
||||
}
|
||||
} else {
|
||||
// Per i messaggi utente
|
||||
processedContent = {
|
||||
hasThinking: false,
|
||||
response: msg.content
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={index} className={`message ${msg.role}`}>
|
||||
{/* Render thinking box if present */}
|
||||
{processedContent.hasThinking && (
|
||||
<div className="thinking-content">
|
||||
<div className="thinking-header">Reasoning Process:</div>
|
||||
<div className="thinking-body">
|
||||
{formatMessage(processedContent.thinking)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Always render the main response */}
|
||||
<div className={`message-content ${msg.streaming ? 'streaming' : ''}`}>
|
||||
{/* Mostra i puntini di caricamento quando è l'ultimo messaggio, è del coach, è vuoto e isLoading è true */}
|
||||
{msg.streaming && msg.content === '' && index === messages.length - 1 ? (
|
||||
<div className="loading-dots">
|
||||
<span></span>
|
||||
<span></span>
|
||||
<span></span>
|
||||
</div>
|
||||
) : (
|
||||
formatMessage(processedContent.response)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Aggiungiamo un elemento invisibile per lo scroll */}
|
||||
<div ref={messagesEndRef} />
|
||||
|
||||
</div>
|
||||
|
||||
<div className="input-container">
|
||||
{/* Primary container for input and send button */}
|
||||
<div className="primary-input-container">
|
||||
<input
|
||||
type="text"
|
||||
value={messageInput}
|
||||
onChange={(e) => setMessageInput(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && messageInput.trim() && !isLoading && sendMessage()}
|
||||
placeholder="Type your message..."
|
||||
className="message-input"
|
||||
disabled={isLoading}
|
||||
/>
|
||||
|
||||
<button
|
||||
onClick={sendMessage}
|
||||
className={`send-button ${messageInput.trim() ? 'enabled' : 'disabled'} ${isLoading ? 'loading' : ''}`}
|
||||
disabled={!messageInput.trim() || isLoading}
|
||||
aria-label="Send message"
|
||||
>
|
||||
{isLoading ? <LoadingIcon /> : <SendIcon />}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Secondary container for all other buttons */}
|
||||
<div className="secondary-buttons-container">
|
||||
{/* Left button group */}
|
||||
<div className="left-buttons-group">
|
||||
<div className="language-select-container">
|
||||
<select
|
||||
value={language}
|
||||
onChange={(e) => handleLanguageChange(e.target.value)}
|
||||
className="language-selector"
|
||||
>
|
||||
<option value="auto">{languageLabels.auto}</option>
|
||||
<option value="english">{languageLabels.english}</option>
|
||||
<option value="french">{languageLabels.french}</option>
|
||||
<option value="italian">{languageLabels.italian}</option>
|
||||
<option value="german">{languageLabels.german}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<button
|
||||
className={`personality-button ${personality}`}
|
||||
onClick={togglePersonality}
|
||||
aria-label={`AI personality: ${personality}`}
|
||||
title={getPersonalityTitle()}
|
||||
>
|
||||
{getCurrentPersonalityIcon()}
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={toggleReasoning}
|
||||
className={`thinking-button ${reasoning ? 'active' : ''}`}
|
||||
title={reasoning ? "Disable thinking mode" : "Enable thinking mode"}
|
||||
aria-label={reasoning ? "Disable thinking mode" : "Enable thinking mode"}
|
||||
>
|
||||
{reasoning ? <ThinkingIcon /> : <ThinkingDisabledIcon />}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Right button group */}
|
||||
<div className="right-buttons-group">
|
||||
<button
|
||||
onClick={clearChatHistory}
|
||||
className="clear-history-button"
|
||||
disabled={isLoading || isDeleting || messages.length === 0}
|
||||
aria-label="Clear chat history"
|
||||
title="Clear chat history"
|
||||
>
|
||||
<ClearHistoryIcon />
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={triggerFileSelect}
|
||||
className="attachment-button"
|
||||
disabled={isLoading || isDeleting}
|
||||
aria-label="Upload PDF files"
|
||||
title="Upload PDF files"
|
||||
>
|
||||
<AttachmentIcon />
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={handleDeleteAllDocs}
|
||||
className="delete-button"
|
||||
disabled={isLoading || isDeleting}
|
||||
aria-label="Delete all documents"
|
||||
title="Delete all documents"
|
||||
>
|
||||
{isDeleting ? <LoadingIcon /> : <TrashIcon />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="file-upload">
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
multiple
|
||||
accept="application/pdf"
|
||||
onChange={(e) => uploadFile(e.target.files)}
|
||||
className="file-input hidden"
|
||||
disabled={isLoading}
|
||||
style={{ display: 'none' }} // Hide the input
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
8
docker_svc/real-time-chat-app/app/src/App.test.js
Normal file
8
docker_svc/real-time-chat-app/app/src/App.test.js
Normal file
|
@ -0,0 +1,8 @@
|
|||
import { render, screen } from '@testing-library/react';
|
||||
import App from './App';
|
||||
|
||||
test('renders learn react link', () => {
|
||||
render(<App />);
|
||||
const linkElement = screen.getByText(/learn react/i);
|
||||
expect(linkElement).toBeInTheDocument();
|
||||
});
|
13
docker_svc/real-time-chat-app/app/src/index.css
Normal file
13
docker_svc/real-time-chat-app/app/src/index.css
Normal file
|
@ -0,0 +1,13 @@
|
|||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||
monospace;
|
||||
}
|
13
docker_svc/real-time-chat-app/app/src/index.js
Normal file
13
docker_svc/real-time-chat-app/app/src/index.js
Normal file
|
@ -0,0 +1,13 @@
|
|||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import './index.css';
|
||||
import App from './App';
|
||||
import reportWebVitals from './reportWebVitals';
|
||||
|
||||
const root = ReactDOM.createRoot(document.getElementById('root'));
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
||||
reportWebVitals();
|
1
docker_svc/real-time-chat-app/app/src/logo.svg
Normal file
1
docker_svc/real-time-chat-app/app/src/logo.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>
|
After Width: | Height: | Size: 2.6 KiB |
13
docker_svc/real-time-chat-app/app/src/reportWebVitals.js
Normal file
13
docker_svc/real-time-chat-app/app/src/reportWebVitals.js
Normal file
|
@ -0,0 +1,13 @@
|
|||
const reportWebVitals = onPerfEntry => {
|
||||
if (onPerfEntry && onPerfEntry instanceof Function) {
|
||||
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
|
||||
getCLS(onPerfEntry);
|
||||
getFID(onPerfEntry);
|
||||
getFCP(onPerfEntry);
|
||||
getLCP(onPerfEntry);
|
||||
getTTFB(onPerfEntry);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export default reportWebVitals;
|
5
docker_svc/real-time-chat-app/app/src/setupTests.js
Normal file
5
docker_svc/real-time-chat-app/app/src/setupTests.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
// jest-dom adds custom jest matchers for asserting on DOM nodes.
|
||||
// allows you to do things like:
|
||||
// expect(element).toHaveTextContent(/react/i)
|
||||
// learn more: https://github.com/testing-library/jest-dom
|
||||
import '@testing-library/jest-dom';
|
Loading…
Add table
Add a link
Reference in a new issue