Spaces:
Paused
Paused
deployment
Browse files- README.md +5 -0
- champ/service.py +4 -0
- classes/base_models.py +1 -1
- classes/ocr_reader.py +4 -0
- classes/pii_filter.py +3 -1
- exceptions.py +1 -1
- helpers/file_helper.py +5 -3
- main.py +76 -64
- static/app.js +61 -50
- static/style.css +112 -119
- static/translations.js +33 -13
- templates/index.html +40 -21
README.md
CHANGED
|
@@ -81,6 +81,11 @@ To run the tests, simply execute `pytest` at the project root. Make sure your vi
|
|
| 81 |
pip install -r requirements-dev.txt
|
| 82 |
```
|
| 83 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 84 |
## Load testing
|
| 85 |
[k6](https://k6.io/open-source/) is an open-source tool for performing load testing. Test cases are defined in JavaScript files and can be run using the command `k6 run <filename>.js`.
|
| 86 |
|
|
|
|
| 81 |
pip install -r requirements-dev.txt
|
| 82 |
```
|
| 83 |
|
| 84 |
+
Some tests are marked as `resource_intensive`. They take longer to run and might consume significant memory or CPU. To run them:
|
| 85 |
+
```bash
|
| 86 |
+
pytest -m resource_intensive
|
| 87 |
+
```
|
| 88 |
+
|
| 89 |
## Load testing
|
| 90 |
[k6](https://k6.io/open-source/) is an open-source tool for performing load testing. Test cases are defined in JavaScript files and can be run using the command `k6 run <filename>.js`.
|
| 91 |
|
champ/service.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
| 1 |
# app/champ/service.py
|
| 2 |
|
|
|
|
| 3 |
from typing import Any, Dict, List, Literal, Optional, Sequence, Tuple
|
| 4 |
|
| 5 |
from langchain_community.vectorstores import FAISS as LCFAISS
|
|
@@ -8,6 +9,8 @@ from langchain_core.messages import HumanMessage
|
|
| 8 |
from .agent import build_champ_agent
|
| 9 |
from .triage import safety_triage
|
| 10 |
|
|
|
|
|
|
|
| 11 |
|
| 12 |
class ChampService:
|
| 13 |
vector_store: Optional[LCFAISS] = None
|
|
@@ -33,6 +36,7 @@ class ChampService:
|
|
| 33 |
Tuple[str, Dict[str, Any], List[str]]: The replay, the triage_triggered object and the retrieved passages
|
| 34 |
"""
|
| 35 |
if self.agent is None:
|
|
|
|
| 36 |
raise RuntimeError("CHAMP is not initialized yet.")
|
| 37 |
# --- Safety triage micro-layer (before LLM) ---
|
| 38 |
last_user_text = None
|
|
|
|
| 1 |
# app/champ/service.py
|
| 2 |
|
| 3 |
+
import logging
|
| 4 |
from typing import Any, Dict, List, Literal, Optional, Sequence, Tuple
|
| 5 |
|
| 6 |
from langchain_community.vectorstores import FAISS as LCFAISS
|
|
|
|
| 9 |
from .agent import build_champ_agent
|
| 10 |
from .triage import safety_triage
|
| 11 |
|
| 12 |
+
logger = logging.getLogger("uvicorn")
|
| 13 |
+
|
| 14 |
|
| 15 |
class ChampService:
|
| 16 |
vector_store: Optional[LCFAISS] = None
|
|
|
|
| 36 |
Tuple[str, Dict[str, Any], List[str]]: The replay, the triage_triggered object and the retrieved passages
|
| 37 |
"""
|
| 38 |
if self.agent is None:
|
| 39 |
+
logger.error("CHAMP invoked before initialization")
|
| 40 |
raise RuntimeError("CHAMP is not initialized yet.")
|
| 41 |
# --- Safety triage micro-layer (before LLM) ---
|
| 42 |
last_user_text = None
|
classes/base_models.py
CHANGED
|
@@ -7,7 +7,7 @@ from constants import (
|
|
| 7 |
MAX_MESSAGE_LENGTH,
|
| 8 |
)
|
| 9 |
from pydantic import BaseModel, Field, field_validator
|
| 10 |
-
from typing import
|
| 11 |
|
| 12 |
|
| 13 |
class IdentifierBase(BaseModel):
|
|
|
|
| 7 |
MAX_MESSAGE_LENGTH,
|
| 8 |
)
|
| 9 |
from pydantic import BaseModel, Field, field_validator
|
| 10 |
+
from typing import Literal, Set
|
| 11 |
|
| 12 |
|
| 13 |
class IdentifierBase(BaseModel):
|
classes/ocr_reader.py
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
|
|
| 1 |
from typing import Optional
|
| 2 |
import cv2
|
| 3 |
import easyocr
|
| 4 |
import torch
|
| 5 |
|
|
|
|
|
|
|
| 6 |
|
| 7 |
class OCRReader:
|
| 8 |
_instance: Optional["OCRReader"] = None
|
|
@@ -10,6 +13,7 @@ class OCRReader:
|
|
| 10 |
|
| 11 |
def __new__(cls):
|
| 12 |
if cls._instance is None:
|
|
|
|
| 13 |
cls._instance = super(OCRReader, cls).__new__(cls)
|
| 14 |
cls._instance.ocr_reader = easyocr.Reader(
|
| 15 |
["en", "fr"], gpu=torch.cuda.is_available()
|
|
|
|
| 1 |
+
import logging
|
| 2 |
from typing import Optional
|
| 3 |
import cv2
|
| 4 |
import easyocr
|
| 5 |
import torch
|
| 6 |
|
| 7 |
+
logger = logging.getLogger("uvicorn")
|
| 8 |
+
|
| 9 |
|
| 10 |
class OCRReader:
|
| 11 |
_instance: Optional["OCRReader"] = None
|
|
|
|
| 13 |
|
| 14 |
def __new__(cls):
|
| 15 |
if cls._instance is None:
|
| 16 |
+
logger.info("Loading the OCR model into memory...")
|
| 17 |
cls._instance = super(OCRReader, cls).__new__(cls)
|
| 18 |
cls._instance.ocr_reader = easyocr.Reader(
|
| 19 |
["en", "fr"], gpu=torch.cuda.is_available()
|
classes/pii_filter.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
|
|
| 1 |
from typing import List, Optional
|
| 2 |
from presidio_analyzer import AnalyzerEngine, Pattern, PatternRecognizer
|
| 3 |
from presidio_analyzer.nlp_engine import NlpEngineProvider
|
|
@@ -5,6 +6,7 @@ from presidio_anonymizer import AnonymizerEngine
|
|
| 5 |
from presidio_anonymizer.entities import OperatorConfig
|
| 6 |
|
| 7 |
# from lingua import Language, LanguageDetector
|
|
|
|
| 8 |
|
| 9 |
|
| 10 |
def create_ssn_pattern_recognizer():
|
|
@@ -92,7 +94,7 @@ class PIIFilter:
|
|
| 92 |
|
| 93 |
def __new__(cls):
|
| 94 |
if cls._instance is None:
|
| 95 |
-
|
| 96 |
cls._instance = super(PIIFilter, cls).__new__(cls)
|
| 97 |
|
| 98 |
# Define which models to use for which language
|
|
|
|
| 1 |
+
import logging
|
| 2 |
from typing import List, Optional
|
| 3 |
from presidio_analyzer import AnalyzerEngine, Pattern, PatternRecognizer
|
| 4 |
from presidio_analyzer.nlp_engine import NlpEngineProvider
|
|
|
|
| 6 |
from presidio_anonymizer.entities import OperatorConfig
|
| 7 |
|
| 8 |
# from lingua import Language, LanguageDetector
|
| 9 |
+
logger = logging.getLogger("uvicorn")
|
| 10 |
|
| 11 |
|
| 12 |
def create_ssn_pattern_recognizer():
|
|
|
|
| 94 |
|
| 95 |
def __new__(cls):
|
| 96 |
if cls._instance is None:
|
| 97 |
+
logger.info("Loading the prompt sanitizer into memory...")
|
| 98 |
cls._instance = super(PIIFilter, cls).__new__(cls)
|
| 99 |
|
| 100 |
# Define which models to use for which language
|
exceptions.py
CHANGED
|
@@ -53,7 +53,7 @@ class FileExtractionException(Exception):
|
|
| 53 |
|
| 54 |
FILE_EXTRACTION_ERROR_STATUS_CODES = {
|
| 55 |
FileExtractionError.INVALID_MIME_TYPE: STATUS_CODE_UNSUPPORTED_MEDIA_TYPE,
|
| 56 |
-
FileExtractionError.NO_TEXT:
|
| 57 |
FileExtractionError.TEXT_EXTRACTION_TIMEOUT: STATUS_CODE_INTERNAL_SERVER_ERROR,
|
| 58 |
FileExtractionError.UNSAFE_ZIP: STATUS_CODE_INTERNAL_SERVER_ERROR,
|
| 59 |
FileExtractionError.FILE_TOO_LARGE: STATUS_CODE_CONTENT_TOO_LARGE,
|
|
|
|
| 53 |
|
| 54 |
FILE_EXTRACTION_ERROR_STATUS_CODES = {
|
| 55 |
FileExtractionError.INVALID_MIME_TYPE: STATUS_CODE_UNSUPPORTED_MEDIA_TYPE,
|
| 56 |
+
FileExtractionError.NO_TEXT: STATUS_CODE_BAD_REQUEST,
|
| 57 |
FileExtractionError.TEXT_EXTRACTION_TIMEOUT: STATUS_CODE_INTERNAL_SERVER_ERROR,
|
| 58 |
FileExtractionError.UNSAFE_ZIP: STATUS_CODE_INTERNAL_SERVER_ERROR,
|
| 59 |
FileExtractionError.FILE_TOO_LARGE: STATUS_CODE_CONTENT_TOO_LARGE,
|
helpers/file_helper.py
CHANGED
|
@@ -85,15 +85,17 @@ def safe_unzip_check(file_bytes: bytes) -> bool:
|
|
| 85 |
break
|
| 86 |
total += len(chunk)
|
| 87 |
if total > MAX_FILE_SIZE:
|
| 88 |
-
|
|
|
|
|
|
|
| 89 |
return True
|
| 90 |
except zipfile.BadZipFile:
|
| 91 |
-
|
| 92 |
|
| 93 |
|
| 94 |
def extract_text_from_docx(binary_content: bytes):
|
| 95 |
if not safe_unzip_check(binary_content):
|
| 96 |
-
|
| 97 |
|
| 98 |
# Load the binary data into a stream
|
| 99 |
stream = io.BytesIO(binary_content)
|
|
|
|
| 85 |
break
|
| 86 |
total += len(chunk)
|
| 87 |
if total > MAX_FILE_SIZE:
|
| 88 |
+
raise FileExtractionException(
|
| 89 |
+
FileExtractionError.FILE_TOO_LARGE
|
| 90 |
+
)
|
| 91 |
return True
|
| 92 |
except zipfile.BadZipFile:
|
| 93 |
+
raise FileExtractionException(FileExtractionError.UNSAFE_ZIP)
|
| 94 |
|
| 95 |
|
| 96 |
def extract_text_from_docx(binary_content: bytes):
|
| 97 |
if not safe_unzip_check(binary_content):
|
| 98 |
+
return None
|
| 99 |
|
| 100 |
# Load the binary data into a stream
|
| 101 |
stream = io.BytesIO(binary_content)
|
main.py
CHANGED
|
@@ -1,29 +1,31 @@
|
|
| 1 |
-
import os
|
| 2 |
import asyncio
|
| 3 |
-
import
|
| 4 |
-
import
|
| 5 |
-
|
| 6 |
from contextlib import asynccontextmanager
|
|
|
|
| 7 |
|
| 8 |
-
|
| 9 |
-
|
| 10 |
from dotenv import load_dotenv
|
| 11 |
-
|
| 12 |
-
from fastapi import FastAPI, File, Form, Request, BackgroundTasks, Response, UploadFile
|
| 13 |
from fastapi.responses import HTMLResponse, JSONResponse, StreamingResponse
|
| 14 |
from fastapi.staticfiles import StaticFiles
|
| 15 |
from fastapi.templating import Jinja2Templates
|
| 16 |
-
|
|
|
|
|
|
|
| 17 |
from slowapi import Limiter
|
| 18 |
from slowapi.util import get_remote_address
|
| 19 |
-
|
| 20 |
-
from opentelemetry import trace
|
| 21 |
|
| 22 |
from champ.rag import (
|
| 23 |
create_embedding_model,
|
| 24 |
create_session_vector_store,
|
| 25 |
load_vector_store,
|
| 26 |
)
|
|
|
|
|
|
|
|
|
|
| 27 |
from classes.base_models import (
|
| 28 |
ChatMessage,
|
| 29 |
ChatRequest,
|
|
@@ -36,10 +38,12 @@ from classes.ocr_reader import OCRReader
|
|
| 36 |
from classes.pii_filter import PIIFilter
|
| 37 |
from classes.prompt_injection_filter import PromptInjectionFilter
|
| 38 |
from classes.session_conversation_store import SessionConversationStore
|
|
|
|
| 39 |
from classes.session_tracker import SessionTracker
|
| 40 |
from constants import (
|
| 41 |
MAX_ID_LENGTH,
|
| 42 |
MAX_RAM_USAGE_PERCENT,
|
|
|
|
| 43 |
STATUS_CODE_EXCEED_SIZE_LIMIT,
|
| 44 |
STATUS_CODE_INTERNAL_SERVER_ERROR,
|
| 45 |
STATUS_CODE_UNPROCESSABLE_CONTENT,
|
|
@@ -53,28 +57,19 @@ from exceptions import (
|
|
| 53 |
FileValidationException,
|
| 54 |
)
|
| 55 |
from helpers.dynamodb_helper import log_event
|
| 56 |
-
|
| 57 |
-
from openai import AsyncOpenAI
|
| 58 |
-
from google import genai
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
# from lingua import Language, LanguageDetectorBuilder
|
| 62 |
-
|
| 63 |
-
from champ.service import ChampService
|
| 64 |
-
|
| 65 |
from helpers.file_helper import (
|
| 66 |
extract_text_from_file,
|
| 67 |
is_valid_filename,
|
| 68 |
replace_spaces_in_filename,
|
| 69 |
validate_file,
|
| 70 |
)
|
| 71 |
-
from
|
| 72 |
-
from helpers.message_helper import convert_messages
|
| 73 |
-
from helpers.message_helper import convert_messages_langchain
|
| 74 |
from telemetry import setup_telemetry
|
| 75 |
|
| 76 |
load_dotenv()
|
| 77 |
|
|
|
|
|
|
|
| 78 |
# -------------------- Config --------------------
|
| 79 |
DEV = os.getenv("ENV", None) == "dev"
|
| 80 |
|
|
@@ -117,17 +112,17 @@ session_conversation_store = SessionConversationStore()
|
|
| 117 |
|
| 118 |
|
| 119 |
def run_cleanup():
|
| 120 |
-
|
| 121 |
deleted_session_ids = session_tracker.delete_inactive_sessions()
|
| 122 |
if len(deleted_session_ids) > 0:
|
| 123 |
-
|
| 124 |
for session_id in deleted_session_ids:
|
| 125 |
session_document_store.delete_session_documents(session_id)
|
| 126 |
session_conversation_store.delete_session_conversations(session_id)
|
| 127 |
|
| 128 |
while psutil.virtual_memory().percent > MAX_RAM_USAGE_PERCENT:
|
| 129 |
oldest_session_id = session_tracker.delete_oldest_session()
|
| 130 |
-
|
| 131 |
if oldest_session_id is None:
|
| 132 |
break
|
| 133 |
session_document_store.delete_session_documents(oldest_session_id)
|
|
@@ -170,39 +165,44 @@ def _call_gemini(model_id: str, msgs: list[dict], temperature: float) -> str:
|
|
| 170 |
return (resp.text or "").strip()
|
| 171 |
|
| 172 |
|
| 173 |
-
def
|
| 174 |
-
session_id: str,
|
| 175 |
-
|
| 176 |
-
lang: Literal["en", "fr"],
|
| 177 |
-
conversation: List[ChatMessage],
|
| 178 |
-
) -> AsyncGenerator[str, None] | Tuple[str, Dict[str, Any], List[str]]:
|
| 179 |
tracer = trace.get_tracer(__name__)
|
| 180 |
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
base_vector_store, embedding_model, session_documents
|
| 189 |
-
)
|
| 190 |
-
)
|
| 191 |
|
| 192 |
-
|
| 193 |
-
|
| 194 |
|
| 195 |
-
|
| 196 |
-
|
| 197 |
|
| 198 |
-
|
| 199 |
-
|
| 200 |
|
| 201 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 202 |
|
| 203 |
if model_type not in MODEL_MAP:
|
| 204 |
raise ValueError(f"Unknown model_type: {model_type}")
|
| 205 |
|
|
|
|
|
|
|
|
|
|
| 206 |
model_id = MODEL_MAP[model_type]
|
| 207 |
document_contents = session_document_store.get_document_contents(session_id)
|
| 208 |
msgs = convert_messages(conversation, lang=lang, docs_content=document_contents)
|
|
@@ -223,13 +223,24 @@ def call_llm(
|
|
| 223 |
# -------------------- FastAPI setup --------------------
|
| 224 |
@asynccontextmanager
|
| 225 |
async def lifespan(app: FastAPI):
|
| 226 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 227 |
|
| 228 |
-
print("Loading the OCR model into memory...")
|
| 229 |
# Loading the OCR Reader in advance, because loading the model takes time.
|
| 230 |
OCRReader()
|
| 231 |
|
| 232 |
-
print("Loading the prompt sanitizer into memory...")
|
| 233 |
# Idem for the prompt sanitizer.
|
| 234 |
PIIFilter()
|
| 235 |
|
|
@@ -271,14 +282,16 @@ limiter = Limiter(key_func=get_remote_address)
|
|
| 271 |
async def chat_endpoint(
|
| 272 |
payload: ChatRequest, background_tasks: BackgroundTasks, request: Request
|
| 273 |
):
|
| 274 |
-
if not payload.human_message:
|
| 275 |
-
return JSONResponse({"error": "No message provided"}, status_code=400)
|
| 276 |
-
|
| 277 |
session_id = payload.session_id
|
| 278 |
model_type = payload.model_type
|
| 279 |
lang = payload.lang
|
| 280 |
conversation_id = payload.conversation_id
|
| 281 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 282 |
session_tracker.update_session(session_id)
|
| 283 |
|
| 284 |
prompt_injection_filter = PromptInjectionFilter()
|
|
@@ -370,7 +383,6 @@ async def chat_endpoint(
|
|
| 370 |
},
|
| 371 |
)
|
| 372 |
|
| 373 |
-
# Ajouter les passages récupérés
|
| 374 |
background_tasks.add_task(
|
| 375 |
log_event,
|
| 376 |
user_id=payload.user_id,
|
|
@@ -402,7 +414,12 @@ def comment_endpoint(
|
|
| 402 |
payload: CommentRequest, background_tasks: BackgroundTasks, request: Request
|
| 403 |
):
|
| 404 |
if not payload.comment:
|
| 405 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 406 |
|
| 407 |
background_tasks.add_task(
|
| 408 |
log_event,
|
|
@@ -459,6 +476,8 @@ async def upload_file(
|
|
| 459 |
# )
|
| 460 |
pii_filtered_file_text = pii_filter.sanitize(injection_filtered_file_text)
|
| 461 |
|
|
|
|
|
|
|
| 462 |
if session_document_store.create_document(
|
| 463 |
session_id, pii_filtered_file_text, file_name, file_size
|
| 464 |
):
|
|
@@ -478,11 +497,4 @@ def delete_file(
|
|
| 478 |
|
| 479 |
file_name = replace_spaces_in_filename(file_name)
|
| 480 |
|
| 481 |
-
if not is_valid_filename(file_name):
|
| 482 |
-
return Response(status_code=STATUS_CODE_UNPROCESSABLE_CONTENT)
|
| 483 |
-
|
| 484 |
-
_, extension = os.path.splitext(file_name)
|
| 485 |
-
if extension not in SUPPORTED_FILE_EXTENSIONS:
|
| 486 |
-
return Response(status_code=STATUS_CODE_UNSUPPORTED_MEDIA_TYPE)
|
| 487 |
-
|
| 488 |
session_document_store.delete_document(session_id, file_name)
|
|
|
|
|
|
|
| 1 |
import asyncio
|
| 2 |
+
import logging
|
| 3 |
+
import os
|
|
|
|
| 4 |
from contextlib import asynccontextmanager
|
| 5 |
+
from typing import Any, AsyncGenerator, Dict, List, Literal, Tuple
|
| 6 |
|
| 7 |
+
import psutil
|
| 8 |
+
import torch
|
| 9 |
from dotenv import load_dotenv
|
| 10 |
+
from fastapi import BackgroundTasks, FastAPI, File, Form, Request, Response, UploadFile
|
|
|
|
| 11 |
from fastapi.responses import HTMLResponse, JSONResponse, StreamingResponse
|
| 12 |
from fastapi.staticfiles import StaticFiles
|
| 13 |
from fastapi.templating import Jinja2Templates
|
| 14 |
+
from google import genai
|
| 15 |
+
from openai import AsyncOpenAI
|
| 16 |
+
from opentelemetry import trace
|
| 17 |
from slowapi import Limiter
|
| 18 |
from slowapi.util import get_remote_address
|
| 19 |
+
from uvicorn.logging import DefaultFormatter
|
|
|
|
| 20 |
|
| 21 |
from champ.rag import (
|
| 22 |
create_embedding_model,
|
| 23 |
create_session_vector_store,
|
| 24 |
load_vector_store,
|
| 25 |
)
|
| 26 |
+
|
| 27 |
+
# from lingua import Language, LanguageDetectorBuilder
|
| 28 |
+
from champ.service import ChampService
|
| 29 |
from classes.base_models import (
|
| 30 |
ChatMessage,
|
| 31 |
ChatRequest,
|
|
|
|
| 38 |
from classes.pii_filter import PIIFilter
|
| 39 |
from classes.prompt_injection_filter import PromptInjectionFilter
|
| 40 |
from classes.session_conversation_store import SessionConversationStore
|
| 41 |
+
from classes.session_document_store import SessionDocumentStore
|
| 42 |
from classes.session_tracker import SessionTracker
|
| 43 |
from constants import (
|
| 44 |
MAX_ID_LENGTH,
|
| 45 |
MAX_RAM_USAGE_PERCENT,
|
| 46 |
+
STATUS_CODE_BAD_REQUEST,
|
| 47 |
STATUS_CODE_EXCEED_SIZE_LIMIT,
|
| 48 |
STATUS_CODE_INTERNAL_SERVER_ERROR,
|
| 49 |
STATUS_CODE_UNPROCESSABLE_CONTENT,
|
|
|
|
| 57 |
FileValidationException,
|
| 58 |
)
|
| 59 |
from helpers.dynamodb_helper import log_event
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
from helpers.file_helper import (
|
| 61 |
extract_text_from_file,
|
| 62 |
is_valid_filename,
|
| 63 |
replace_spaces_in_filename,
|
| 64 |
validate_file,
|
| 65 |
)
|
| 66 |
+
from helpers.message_helper import convert_messages, convert_messages_langchain
|
|
|
|
|
|
|
| 67 |
from telemetry import setup_telemetry
|
| 68 |
|
| 69 |
load_dotenv()
|
| 70 |
|
| 71 |
+
logger = logging.getLogger("uvicorn")
|
| 72 |
+
|
| 73 |
# -------------------- Config --------------------
|
| 74 |
DEV = os.getenv("ENV", None) == "dev"
|
| 75 |
|
|
|
|
| 112 |
|
| 113 |
|
| 114 |
def run_cleanup():
|
| 115 |
+
logger.info("Running cleanup")
|
| 116 |
deleted_session_ids = session_tracker.delete_inactive_sessions()
|
| 117 |
if len(deleted_session_ids) > 0:
|
| 118 |
+
logger.info(f"{len(deleted_session_ids)} inactive sessions will be deleted.")
|
| 119 |
for session_id in deleted_session_ids:
|
| 120 |
session_document_store.delete_session_documents(session_id)
|
| 121 |
session_conversation_store.delete_session_conversations(session_id)
|
| 122 |
|
| 123 |
while psutil.virtual_memory().percent > MAX_RAM_USAGE_PERCENT:
|
| 124 |
oldest_session_id = session_tracker.delete_oldest_session()
|
| 125 |
+
logger.info(f"Deleting {oldest_session_id} session because of high RAM usage")
|
| 126 |
if oldest_session_id is None:
|
| 127 |
break
|
| 128 |
session_document_store.delete_session_documents(oldest_session_id)
|
|
|
|
| 165 |
return (resp.text or "").strip()
|
| 166 |
|
| 167 |
|
| 168 |
+
def _call_champ(
|
| 169 |
+
session_id: str, lang: Literal["en", "fr"], conversation: List[ChatMessage]
|
| 170 |
+
):
|
|
|
|
|
|
|
|
|
|
| 171 |
tracer = trace.get_tracer(__name__)
|
| 172 |
|
| 173 |
+
session_documents = session_document_store.get_documents(session_id)
|
| 174 |
+
if session_documents is None:
|
| 175 |
+
vector_store = base_vector_store
|
| 176 |
+
else:
|
| 177 |
+
vector_store = create_session_vector_store(
|
| 178 |
+
base_vector_store, embedding_model, session_documents
|
| 179 |
+
)
|
|
|
|
|
|
|
|
|
|
| 180 |
|
| 181 |
+
with tracer.start_as_current_span("ChampService"):
|
| 182 |
+
champ = ChampService(vector_store=vector_store, lang=lang)
|
| 183 |
|
| 184 |
+
with tracer.start_as_current_span("convert_messages_langchain"):
|
| 185 |
+
msgs = convert_messages_langchain(conversation)
|
| 186 |
|
| 187 |
+
with tracer.start_as_current_span("invoke"):
|
| 188 |
+
reply, triage_meta, context = champ.invoke(msgs)
|
| 189 |
|
| 190 |
+
return reply, triage_meta, context
|
| 191 |
+
|
| 192 |
+
|
| 193 |
+
def call_llm(
|
| 194 |
+
session_id: str,
|
| 195 |
+
model_type: str,
|
| 196 |
+
lang: Literal["en", "fr"],
|
| 197 |
+
conversation: List[ChatMessage],
|
| 198 |
+
) -> AsyncGenerator[str, None] | Tuple[str, Dict[str, Any], List[str]]:
|
| 199 |
|
| 200 |
if model_type not in MODEL_MAP:
|
| 201 |
raise ValueError(f"Unknown model_type: {model_type}")
|
| 202 |
|
| 203 |
+
if model_type == "champ":
|
| 204 |
+
return _call_champ(session_id, lang, conversation)
|
| 205 |
+
|
| 206 |
model_id = MODEL_MAP[model_type]
|
| 207 |
document_contents = session_document_store.get_document_contents(session_id)
|
| 208 |
msgs = convert_messages(conversation, lang=lang, docs_content=document_contents)
|
|
|
|
| 223 |
# -------------------- FastAPI setup --------------------
|
| 224 |
@asynccontextmanager
|
| 225 |
async def lifespan(app: FastAPI):
|
| 226 |
+
logger = logging.getLogger("uvicorn")
|
| 227 |
+
|
| 228 |
+
if logger.handlers:
|
| 229 |
+
colored_formatter = DefaultFormatter(
|
| 230 |
+
fmt="%(levelprefix)s %(asctime)s | %(message)s", datefmt="%Y-%m-%d %H:%M:%S"
|
| 231 |
+
)
|
| 232 |
+
logger.handlers[0].setFormatter(colored_formatter)
|
| 233 |
+
|
| 234 |
+
logger.info("Logging configured!")
|
| 235 |
+
|
| 236 |
+
if torch.cuda.is_available():
|
| 237 |
+
logger.info("CUDA is available")
|
| 238 |
+
else:
|
| 239 |
+
logger.warning("CUDA is NOT available")
|
| 240 |
|
|
|
|
| 241 |
# Loading the OCR Reader in advance, because loading the model takes time.
|
| 242 |
OCRReader()
|
| 243 |
|
|
|
|
| 244 |
# Idem for the prompt sanitizer.
|
| 245 |
PIIFilter()
|
| 246 |
|
|
|
|
| 282 |
async def chat_endpoint(
|
| 283 |
payload: ChatRequest, background_tasks: BackgroundTasks, request: Request
|
| 284 |
):
|
|
|
|
|
|
|
|
|
|
| 285 |
session_id = payload.session_id
|
| 286 |
model_type = payload.model_type
|
| 287 |
lang = payload.lang
|
| 288 |
conversation_id = payload.conversation_id
|
| 289 |
|
| 290 |
+
if not payload.human_message:
|
| 291 |
+
return JSONResponse(
|
| 292 |
+
{"error": "No message provided"}, status_code=STATUS_CODE_BAD_REQUEST
|
| 293 |
+
)
|
| 294 |
+
|
| 295 |
session_tracker.update_session(session_id)
|
| 296 |
|
| 297 |
prompt_injection_filter = PromptInjectionFilter()
|
|
|
|
| 383 |
},
|
| 384 |
)
|
| 385 |
|
|
|
|
| 386 |
background_tasks.add_task(
|
| 387 |
log_event,
|
| 388 |
user_id=payload.user_id,
|
|
|
|
| 414 |
payload: CommentRequest, background_tasks: BackgroundTasks, request: Request
|
| 415 |
):
|
| 416 |
if not payload.comment:
|
| 417 |
+
logger.warning("Received empty comment")
|
| 418 |
+
return JSONResponse(
|
| 419 |
+
{"error": "No comment provided"}, status_code=STATUS_CODE_BAD_REQUEST
|
| 420 |
+
)
|
| 421 |
+
|
| 422 |
+
logger.info("Received comment")
|
| 423 |
|
| 424 |
background_tasks.add_task(
|
| 425 |
log_event,
|
|
|
|
| 476 |
# )
|
| 477 |
pii_filtered_file_text = pii_filter.sanitize(injection_filtered_file_text)
|
| 478 |
|
| 479 |
+
# TODO: total session file size should be related to the amount of text in the file
|
| 480 |
+
# instead of the size of the file
|
| 481 |
if session_document_store.create_document(
|
| 482 |
session_id, pii_filtered_file_text, file_name, file_size
|
| 483 |
):
|
|
|
|
| 497 |
|
| 498 |
file_name = replace_spaces_in_filename(file_name)
|
| 499 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 500 |
session_document_store.delete_document(session_id, file_name)
|
static/app.js
CHANGED
|
@@ -14,10 +14,6 @@ const doneFileUploadBtn = document.getElementById('done-file-upload');
|
|
| 14 |
const closeFileUploadBtn = document.getElementById('close-file-upload-btn');
|
| 15 |
const fileListHtml = document.getElementById('file-list');
|
| 16 |
|
| 17 |
-
const langSwitchContainer = document.getElementById('lang-switch-container');
|
| 18 |
-
const enBtn = document.getElementById('btn-en');
|
| 19 |
-
const frBtn = document.getElementById('btn-fr');
|
| 20 |
-
|
| 21 |
document.createElement('svg');
|
| 22 |
|
| 23 |
const HTML_UPLOAD_ICON = `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
@@ -56,6 +52,8 @@ const consentBtn = document.getElementById('consentBtn');
|
|
| 56 |
const frRadioBtn = document.getElementById('lang-fr');
|
| 57 |
const enRadioBtn = document.getElementById('lang-en');
|
| 58 |
const continueLangBtn = document.getElementById('lang-continue-btn');
|
|
|
|
|
|
|
| 59 |
|
| 60 |
const profileModal = document.getElementById('profile-modal');
|
| 61 |
const profileBtn = document.getElementById('profileBtn');
|
|
@@ -78,6 +76,11 @@ const increaseFontSizeBtn = document.getElementById('increase-font-size-btn');
|
|
| 78 |
const decreaseFontSizeBtn = document.getElementById('decrease-font-size-btn');
|
| 79 |
const resetFontSizeBtn = document.getElementById('reset-font-size-btn');
|
| 80 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 81 |
// Local in-browser chat history
|
| 82 |
// We store for each model its chat history and a conversation id.
|
| 83 |
const modelChats = {};
|
|
@@ -98,16 +101,6 @@ document.body.classList.add('no-scroll');
|
|
| 98 |
|
| 99 |
let sessionFiles = [];
|
| 100 |
|
| 101 |
-
function openModal() {
|
| 102 |
-
// Move the translation options at the top right corner of the screen
|
| 103 |
-
langSwitchContainer.classList.add('floating');
|
| 104 |
-
}
|
| 105 |
-
|
| 106 |
-
function closeModal() {
|
| 107 |
-
// Move the translation options in the toolbar
|
| 108 |
-
langSwitchContainer.classList.remove('floating');
|
| 109 |
-
}
|
| 110 |
-
|
| 111 |
function renderMessages() {
|
| 112 |
chatWindow.innerHTML = '';
|
| 113 |
const modelType = systemPresetSelect.value;
|
|
@@ -178,14 +171,14 @@ async function sendMessage() {
|
|
| 178 |
});
|
| 179 |
|
| 180 |
if (!res.ok) {
|
| 181 |
-
statusEl.
|
| 182 |
-
if (
|
| 183 |
-
statusEl.
|
| 184 |
} else {
|
| 185 |
-
statusEl.textContent = "";
|
| 186 |
statusEl.dataset.i18n = "server_error";
|
| 187 |
-
applyTranslation();
|
| 188 |
}
|
|
|
|
|
|
|
| 189 |
return;
|
| 190 |
}
|
| 191 |
|
|
@@ -243,8 +236,6 @@ function openFileUploadOverlay(e) {
|
|
| 243 |
e.preventDefault();
|
| 244 |
// Let the stylesheet take over
|
| 245 |
uploadFileOverlay.style.display = '';
|
| 246 |
-
|
| 247 |
-
openModal();
|
| 248 |
}
|
| 249 |
uploadFileBtn.addEventListener('click', openFileUploadOverlay);
|
| 250 |
|
|
@@ -438,7 +429,23 @@ async function uploadFile(file) {
|
|
| 438 |
});
|
| 439 |
|
| 440 |
if (!res.ok) {
|
| 441 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 442 |
return false;
|
| 443 |
}
|
| 444 |
|
|
@@ -485,14 +492,24 @@ async function deleteFile(file) {
|
|
| 485 |
// Close the overlay
|
| 486 |
closeFileUploadBtn.addEventListener('click', () => {
|
| 487 |
uploadFileOverlay.style.display = 'none';
|
| 488 |
-
closeModal();
|
| 489 |
});
|
| 490 |
doneFileUploadBtn.addEventListener('click', () => {
|
| 491 |
uploadFileOverlay.style.display = 'none';
|
| 492 |
-
closeModal();
|
| 493 |
})
|
| 494 |
|
| 495 |
-
//
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 496 |
|
| 497 |
// Language modal logic
|
| 498 |
continueLangBtn.addEventListener('click', () => {
|
|
@@ -511,6 +528,14 @@ enRadioBtn.addEventListener('change', () => {
|
|
| 511 |
currentLang = enRadioBtn.value;
|
| 512 |
setLanguage();
|
| 513 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 514 |
|
| 515 |
// Consent logic
|
| 516 |
// When the checkbox is toggled, enable or disable the button
|
|
@@ -573,8 +598,6 @@ profileBtn.addEventListener('click', () => {
|
|
| 573 |
gender = document.getElementById('gender').value;
|
| 574 |
roles = Array.from(document.querySelectorAll('input[name="role"]:checked')).map(input => input.value);
|
| 575 |
participantId = participantInput.value.trim();
|
| 576 |
-
|
| 577 |
-
closeModal();
|
| 578 |
});
|
| 579 |
|
| 580 |
sendBtn.addEventListener('click', sendMessage);
|
|
@@ -608,19 +631,15 @@ function openCommentOverlay(e) {
|
|
| 608 |
e.preventDefault();
|
| 609 |
// Let the stylesheet take over
|
| 610 |
commentOverlay.style.display = '';
|
| 611 |
-
|
| 612 |
-
openModal();
|
| 613 |
}
|
| 614 |
leaveCommentText.addEventListener('click', openCommentOverlay);
|
| 615 |
|
| 616 |
// Cancelling or closing the comment overlay simply hides the comment popup
|
| 617 |
closeCommentBtn.addEventListener('click', () => {
|
| 618 |
commentOverlay.style.display = 'none';
|
| 619 |
-
closeModal();
|
| 620 |
});
|
| 621 |
cancelCommentBtn.addEventListener('click', () => {
|
| 622 |
commentOverlay.style.display = 'none';
|
| 623 |
-
closeModal();
|
| 624 |
});
|
| 625 |
|
| 626 |
async function sendComment() {
|
|
@@ -650,7 +669,11 @@ async function sendComment() {
|
|
| 650 |
});
|
| 651 |
|
| 652 |
if (!res.ok) {
|
| 653 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 654 |
statusComment.className = 'status-error';
|
| 655 |
applyTranslation();
|
| 656 |
return;
|
|
@@ -674,24 +697,14 @@ sendCommentBtn.addEventListener('click', sendComment);
|
|
| 674 |
function setLanguage() {
|
| 675 |
applyTranslation();
|
| 676 |
|
| 677 |
-
document.getElementById('btn-en').classList.toggle('active', currentLang === 'en');
|
| 678 |
-
document.getElementById('btn-fr').classList.toggle('active', currentLang === 'fr');
|
| 679 |
-
|
| 680 |
frRadioBtn.checked = currentLang === 'fr';
|
| 681 |
enRadioBtn.checked = currentLang === 'en';
|
| 682 |
-
|
|
|
|
|
|
|
| 683 |
localStorage.setItem('preferredLang', currentLang);
|
| 684 |
};
|
| 685 |
|
| 686 |
-
enBtn.addEventListener('click', () => {
|
| 687 |
-
currentLang = 'en';
|
| 688 |
-
setLanguage();
|
| 689 |
-
});
|
| 690 |
-
frBtn.addEventListener('click', () => {
|
| 691 |
-
currentLang = 'fr';
|
| 692 |
-
setLanguage();
|
| 693 |
-
});
|
| 694 |
-
|
| 695 |
function applyTranslation() {
|
| 696 |
document.querySelectorAll('[data-i18n]').forEach(element => {
|
| 697 |
const key = element.getAttribute('data-i18n');
|
|
@@ -702,7 +715,7 @@ function applyTranslation() {
|
|
| 702 |
};
|
| 703 |
|
| 704 |
const MIN_FONT_SIZE = 0.75;
|
| 705 |
-
const MAX_FONT_SIZE =
|
| 706 |
const FONT_SIZE_STEP = 0.125; // 1/8 rem for smooth increments
|
| 707 |
|
| 708 |
let currentSize = 1; // 1rem = browser default (usually 16px)
|
|
@@ -731,11 +744,11 @@ statusComment.dataset.i18n = "ready";
|
|
| 731 |
statusComment.className = 'status-ok';
|
| 732 |
|
| 733 |
if (currentLang == "en") {
|
| 734 |
-
enBtn.classList.add('active');
|
| 735 |
enRadioBtn.checked = true;
|
|
|
|
| 736 |
} else {
|
| 737 |
-
frBtn.classList.add('active');
|
| 738 |
frRadioBtn.checked = true;
|
|
|
|
| 739 |
}
|
| 740 |
|
| 741 |
applyTranslation();
|
|
@@ -745,5 +758,3 @@ renderFiles();
|
|
| 745 |
if (window.innerWidth >= 460) {
|
| 746 |
document.querySelector('details').setAttribute('open', '');
|
| 747 |
}
|
| 748 |
-
|
| 749 |
-
openModal();
|
|
|
|
| 14 |
const closeFileUploadBtn = document.getElementById('close-file-upload-btn');
|
| 15 |
const fileListHtml = document.getElementById('file-list');
|
| 16 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
document.createElement('svg');
|
| 18 |
|
| 19 |
const HTML_UPLOAD_ICON = `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
|
|
| 52 |
const frRadioBtn = document.getElementById('lang-fr');
|
| 53 |
const enRadioBtn = document.getElementById('lang-en');
|
| 54 |
const continueLangBtn = document.getElementById('lang-continue-btn');
|
| 55 |
+
const frRadioBtnSettings = document.getElementById('lang-fr-settings');
|
| 56 |
+
const enRadioBtnSettings = document.getElementById('lang-en-settings');
|
| 57 |
|
| 58 |
const profileModal = document.getElementById('profile-modal');
|
| 59 |
const profileBtn = document.getElementById('profileBtn');
|
|
|
|
| 76 |
const decreaseFontSizeBtn = document.getElementById('decrease-font-size-btn');
|
| 77 |
const resetFontSizeBtn = document.getElementById('reset-font-size-btn');
|
| 78 |
|
| 79 |
+
const settingsBtn = document.getElementById('settings-btn')
|
| 80 |
+
const settingsModal = document.getElementById('settings-modal');
|
| 81 |
+
const doneSettingsBtn = document.getElementById('done-settings');
|
| 82 |
+
const closeSettingsBtn = document.getElementById('close-settings-btn');
|
| 83 |
+
|
| 84 |
// Local in-browser chat history
|
| 85 |
// We store for each model its chat history and a conversation id.
|
| 86 |
const modelChats = {};
|
|
|
|
| 101 |
|
| 102 |
let sessionFiles = [];
|
| 103 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 104 |
function renderMessages() {
|
| 105 |
chatWindow.innerHTML = '';
|
| 106 |
const modelType = systemPresetSelect.value;
|
|
|
|
| 171 |
});
|
| 172 |
|
| 173 |
if (!res.ok) {
|
| 174 |
+
statusEl.textContent = "";
|
| 175 |
+
if (res.status === 400) {
|
| 176 |
+
statusEl.dataset.i18n = "empty_message_error";
|
| 177 |
} else {
|
|
|
|
| 178 |
statusEl.dataset.i18n = "server_error";
|
|
|
|
| 179 |
}
|
| 180 |
+
statusEl.className = 'status status-error';
|
| 181 |
+
applyTranslation();
|
| 182 |
return;
|
| 183 |
}
|
| 184 |
|
|
|
|
| 236 |
e.preventDefault();
|
| 237 |
// Let the stylesheet take over
|
| 238 |
uploadFileOverlay.style.display = '';
|
|
|
|
|
|
|
| 239 |
}
|
| 240 |
uploadFileBtn.addEventListener('click', openFileUploadOverlay);
|
| 241 |
|
|
|
|
| 429 |
});
|
| 430 |
|
| 431 |
if (!res.ok) {
|
| 432 |
+
if (res.status === 413) {
|
| 433 |
+
showSnackbar(translations[currentLang]["file_upload_failed_file_too_large"], 'error');
|
| 434 |
+
} else if (res.status === 400) {
|
| 435 |
+
// missing filename, filename too large, invalid file name, empty file, malformed file, no text in file
|
| 436 |
+
showSnackbar(translations[currentLang]["file_upload_failed_malformed_file"], 'error');
|
| 437 |
+
} else if (res.status === 415) {
|
| 438 |
+
// invalid mimetype or unsupported extension
|
| 439 |
+
showSnackbar(translations[currentLang]["file_upload_failed_unsupported_mime_type"], 'error');
|
| 440 |
+
} else if (res.status === 419) {
|
| 441 |
+
showSnackbar(translations[currentLang]["file_upload_failed_exceed_session_size"], 'error');
|
| 442 |
+
} else if (res.status === 500) {
|
| 443 |
+
// generic error, timeout, unsafe zip/docx
|
| 444 |
+
showSnackbar(translations[currentLang]["file_upload_failed_server_error"], 'error');
|
| 445 |
+
} else {
|
| 446 |
+
// unknown error occured.
|
| 447 |
+
showSnackbar(translations[currentLang]["file_upload_failed_unknown_error"], 'error');
|
| 448 |
+
}
|
| 449 |
return false;
|
| 450 |
}
|
| 451 |
|
|
|
|
| 492 |
// Close the overlay
|
| 493 |
closeFileUploadBtn.addEventListener('click', () => {
|
| 494 |
uploadFileOverlay.style.display = 'none';
|
|
|
|
| 495 |
});
|
| 496 |
doneFileUploadBtn.addEventListener('click', () => {
|
| 497 |
uploadFileOverlay.style.display = 'none';
|
|
|
|
| 498 |
})
|
| 499 |
|
| 500 |
+
// Settings modal
|
| 501 |
+
function openSettingsModal(e) {
|
| 502 |
+
e.preventDefault();
|
| 503 |
+
settingsModal.style.display = '';
|
| 504 |
+
}
|
| 505 |
+
settingsBtn.addEventListener('click', openSettingsModal);
|
| 506 |
+
|
| 507 |
+
closeSettingsBtn.addEventListener('click', () => {
|
| 508 |
+
settingsModal.style.display = 'none';
|
| 509 |
+
});
|
| 510 |
+
doneSettingsBtn.addEventListener('click', () => {
|
| 511 |
+
settingsModal.style.display = 'none';
|
| 512 |
+
});
|
| 513 |
|
| 514 |
// Language modal logic
|
| 515 |
continueLangBtn.addEventListener('click', () => {
|
|
|
|
| 528 |
currentLang = enRadioBtn.value;
|
| 529 |
setLanguage();
|
| 530 |
});
|
| 531 |
+
frRadioBtnSettings.addEventListener('change', () => {
|
| 532 |
+
currentLang = frRadioBtnSettings.value;
|
| 533 |
+
setLanguage();
|
| 534 |
+
});
|
| 535 |
+
enRadioBtnSettings.addEventListener('change', () => {
|
| 536 |
+
currentLang = enRadioBtnSettings.value;
|
| 537 |
+
setLanguage();
|
| 538 |
+
});
|
| 539 |
|
| 540 |
// Consent logic
|
| 541 |
// When the checkbox is toggled, enable or disable the button
|
|
|
|
| 598 |
gender = document.getElementById('gender').value;
|
| 599 |
roles = Array.from(document.querySelectorAll('input[name="role"]:checked')).map(input => input.value);
|
| 600 |
participantId = participantInput.value.trim();
|
|
|
|
|
|
|
| 601 |
});
|
| 602 |
|
| 603 |
sendBtn.addEventListener('click', sendMessage);
|
|
|
|
| 631 |
e.preventDefault();
|
| 632 |
// Let the stylesheet take over
|
| 633 |
commentOverlay.style.display = '';
|
|
|
|
|
|
|
| 634 |
}
|
| 635 |
leaveCommentText.addEventListener('click', openCommentOverlay);
|
| 636 |
|
| 637 |
// Cancelling or closing the comment overlay simply hides the comment popup
|
| 638 |
closeCommentBtn.addEventListener('click', () => {
|
| 639 |
commentOverlay.style.display = 'none';
|
|
|
|
| 640 |
});
|
| 641 |
cancelCommentBtn.addEventListener('click', () => {
|
| 642 |
commentOverlay.style.display = 'none';
|
|
|
|
| 643 |
});
|
| 644 |
|
| 645 |
async function sendComment() {
|
|
|
|
| 669 |
});
|
| 670 |
|
| 671 |
if (!res.ok) {
|
| 672 |
+
if (res.status === 400) {
|
| 673 |
+
statusComment.dataset.i18n = "empty_message_error";
|
| 674 |
+
} else {
|
| 675 |
+
statusComment.dataset.i18n = "server_error";
|
| 676 |
+
}
|
| 677 |
statusComment.className = 'status-error';
|
| 678 |
applyTranslation();
|
| 679 |
return;
|
|
|
|
| 697 |
function setLanguage() {
|
| 698 |
applyTranslation();
|
| 699 |
|
|
|
|
|
|
|
|
|
|
| 700 |
frRadioBtn.checked = currentLang === 'fr';
|
| 701 |
enRadioBtn.checked = currentLang === 'en';
|
| 702 |
+
frRadioBtnSettings.checked = currentLang === 'fr';
|
| 703 |
+
enRadioBtnSettings.checked = currentLang === 'en';
|
| 704 |
+
|
| 705 |
localStorage.setItem('preferredLang', currentLang);
|
| 706 |
};
|
| 707 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 708 |
function applyTranslation() {
|
| 709 |
document.querySelectorAll('[data-i18n]').forEach(element => {
|
| 710 |
const key = element.getAttribute('data-i18n');
|
|
|
|
| 715 |
};
|
| 716 |
|
| 717 |
const MIN_FONT_SIZE = 0.75;
|
| 718 |
+
const MAX_FONT_SIZE = 1.625;
|
| 719 |
const FONT_SIZE_STEP = 0.125; // 1/8 rem for smooth increments
|
| 720 |
|
| 721 |
let currentSize = 1; // 1rem = browser default (usually 16px)
|
|
|
|
| 744 |
statusComment.className = 'status-ok';
|
| 745 |
|
| 746 |
if (currentLang == "en") {
|
|
|
|
| 747 |
enRadioBtn.checked = true;
|
| 748 |
+
enRadioBtnSettings.checked = true;
|
| 749 |
} else {
|
|
|
|
| 750 |
frRadioBtn.checked = true;
|
| 751 |
+
frRadioBtnSettings.checked = true;
|
| 752 |
}
|
| 753 |
|
| 754 |
applyTranslation();
|
|
|
|
| 758 |
if (window.innerWidth >= 460) {
|
| 759 |
document.querySelector('details').setAttribute('open', '');
|
| 760 |
}
|
|
|
|
|
|
static/style.css
CHANGED
|
@@ -34,7 +34,7 @@ a {
|
|
| 34 |
}
|
| 35 |
|
| 36 |
.chat-header {
|
| 37 |
-
padding:
|
| 38 |
border-bottom: 1px solid #2c3554;
|
| 39 |
}
|
| 40 |
|
|
@@ -54,21 +54,17 @@ a {
|
|
| 54 |
display: flex;
|
| 55 |
flex-wrap: wrap;
|
| 56 |
gap: 12px;
|
| 57 |
-
margin-top: 10px;
|
| 58 |
padding: 8px 4px;
|
| 59 |
border-bottom: 1px solid #2c3554;
|
| 60 |
}
|
| 61 |
|
| 62 |
.control-group {
|
| 63 |
display: flex;
|
| 64 |
-
|
| 65 |
-
gap:
|
| 66 |
-
font-size: 0.85rem;
|
| 67 |
-
color: #d3dbff;
|
| 68 |
}
|
| 69 |
|
| 70 |
-
.control-group select
|
| 71 |
-
.control-group input[type='range'] {
|
| 72 |
background: #0d1324;
|
| 73 |
border-radius: 8px;
|
| 74 |
border: 1px solid #2c3554;
|
|
@@ -77,21 +73,36 @@ a {
|
|
| 77 |
font-size: 0.85rem;
|
| 78 |
}
|
| 79 |
|
| 80 |
-
.
|
| 81 |
-
align-self:
|
| 82 |
padding: 6px 12px;
|
| 83 |
border-radius: 8px;
|
| 84 |
border: 1px solid #2c3554;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 85 |
background: #1f2840;
|
| 86 |
color: #f5f5f5;
|
| 87 |
font-size: 0.85rem;
|
| 88 |
cursor: pointer;
|
|
|
|
| 89 |
}
|
| 90 |
|
| 91 |
-
.
|
| 92 |
background: #273256;
|
| 93 |
}
|
| 94 |
-
|
| 95 |
/* Chat window */
|
| 96 |
.chat-window {
|
| 97 |
flex: 1;
|
|
@@ -325,45 +336,6 @@ svg {
|
|
| 325 |
color: #ff8080;
|
| 326 |
}
|
| 327 |
|
| 328 |
-
/* RESPONSIVE DESIGN */
|
| 329 |
-
@media (max-width: 460px) {
|
| 330 |
-
/* Hide the text descriptions of the file action buttons */
|
| 331 |
-
.file-actions button span {
|
| 332 |
-
display: none;
|
| 333 |
-
}
|
| 334 |
-
|
| 335 |
-
/* Enlarge the chat container on mobile */
|
| 336 |
-
.chat-container {
|
| 337 |
-
margin: 0;
|
| 338 |
-
width: 100dvw;
|
| 339 |
-
height: 100dvh;
|
| 340 |
-
}
|
| 341 |
-
|
| 342 |
-
/* Reduce the font size of the title on mobile */
|
| 343 |
-
/* Also, add a gap between the title and the details */
|
| 344 |
-
.chat-header h1 {
|
| 345 |
-
margin: 0 0 10px 0;
|
| 346 |
-
font-size: 1.4rem;
|
| 347 |
-
}
|
| 348 |
-
|
| 349 |
-
/* Increase the size of the modals on mobile */
|
| 350 |
-
.modal-content {
|
| 351 |
-
width: 90%;
|
| 352 |
-
}
|
| 353 |
-
}
|
| 354 |
-
|
| 355 |
-
@media (min-width: 460px) {
|
| 356 |
-
details {
|
| 357 |
-
display: block;
|
| 358 |
-
}
|
| 359 |
-
details[open] {
|
| 360 |
-
display: block;
|
| 361 |
-
}
|
| 362 |
-
details summary {
|
| 363 |
-
display: none;
|
| 364 |
-
}
|
| 365 |
-
}
|
| 366 |
-
|
| 367 |
/* CONSENT OVERLAY FIXED VERSION */
|
| 368 |
.modal {
|
| 369 |
/* Covers the entier view port */
|
|
@@ -385,6 +357,11 @@ svg {
|
|
| 385 |
background-color: rgba(0, 0, 0, 0.8);
|
| 386 |
}
|
| 387 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 388 |
.slider {
|
| 389 |
/* Smooth scrolling */
|
| 390 |
scroll-snap-type: x mandatory;
|
|
@@ -435,17 +412,17 @@ svg {
|
|
| 435 |
}
|
| 436 |
|
| 437 |
.language-modal,
|
| 438 |
-
.consent-box
|
|
|
|
|
|
|
| 439 |
display: flex;
|
| 440 |
flex-direction: column;
|
| 441 |
justify-content: space-between;
|
| 442 |
-
max-height: 400px;
|
| 443 |
}
|
| 444 |
|
| 445 |
-
.
|
| 446 |
-
|
| 447 |
-
|
| 448 |
-
justify-content: space-between;
|
| 449 |
}
|
| 450 |
|
| 451 |
.form-group {
|
|
@@ -525,7 +502,7 @@ input[type="checkbox"] {
|
|
| 525 |
accent-color: #007bff; /* Modern way to color native inputs */
|
| 526 |
}
|
| 527 |
|
| 528 |
-
.radio-group label, .checkbox-grid label, .checkbox-grid-lang label
|
| 529 |
font-weight: 400;
|
| 530 |
display: flex;
|
| 531 |
align-items: center;
|
|
@@ -634,81 +611,97 @@ input[type='range'].disabled {
|
|
| 634 |
transition: all 0.2s ease;
|
| 635 |
}
|
| 636 |
|
| 637 |
-
|
| 638 |
-
|
| 639 |
-
|
| 640 |
-
display: flex;
|
| 641 |
-
align-items: center;
|
| 642 |
-
font-family: sans-serif;
|
| 643 |
-
gap: 5px;
|
| 644 |
-
|
| 645 |
-
/* By default */
|
| 646 |
-
position: static;
|
| 647 |
-
margin-left: auto;
|
| 648 |
}
|
| 649 |
|
| 650 |
-
|
| 651 |
-
|
| 652 |
-
|
| 653 |
-
|
| 654 |
-
|
| 655 |
-
|
| 656 |
}
|
| 657 |
|
| 658 |
-
.
|
| 659 |
-
|
| 660 |
-
border:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 661 |
cursor: pointer;
|
| 662 |
-
|
| 663 |
-
|
| 664 |
-
padding: 5px;
|
| 665 |
-
transition: color 0.3s;
|
| 666 |
}
|
| 667 |
|
| 668 |
-
.
|
| 669 |
-
|
| 670 |
-
|
| 671 |
-
|
|
|
|
| 672 |
}
|
| 673 |
|
| 674 |
-
.
|
| 675 |
-
|
| 676 |
}
|
| 677 |
|
| 678 |
-
/*
|
| 679 |
-
|
| 680 |
-
/*
|
| 681 |
-
|
| 682 |
-
|
| 683 |
-
|
| 684 |
-
right: 20px;
|
| 685 |
|
| 686 |
-
|
| 687 |
-
|
| 688 |
-
|
| 689 |
-
|
| 690 |
-
|
| 691 |
-
|
| 692 |
-
border-radius: 8px;
|
| 693 |
-
padding: 10px 8px;
|
| 694 |
-
}
|
| 695 |
|
| 696 |
-
|
| 697 |
-
|
| 698 |
-
|
| 699 |
-
|
| 700 |
-
|
| 701 |
-
|
| 702 |
-
border-radius: 6px;
|
| 703 |
-
font-size: 1rem;
|
| 704 |
-
font-family: monospace;
|
| 705 |
-
cursor: pointer;
|
| 706 |
-
transition: background 0.2s, box-shadow 0.2s;
|
| 707 |
|
| 708 |
-
|
|
|
|
|
|
|
|
|
|
| 709 |
}
|
| 710 |
|
| 711 |
-
|
| 712 |
-
|
| 713 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 714 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
}
|
| 35 |
|
| 36 |
.chat-header {
|
| 37 |
+
padding: 0px 4px 12px;
|
| 38 |
border-bottom: 1px solid #2c3554;
|
| 39 |
}
|
| 40 |
|
|
|
|
| 54 |
display: flex;
|
| 55 |
flex-wrap: wrap;
|
| 56 |
gap: 12px;
|
|
|
|
| 57 |
padding: 8px 4px;
|
| 58 |
border-bottom: 1px solid #2c3554;
|
| 59 |
}
|
| 60 |
|
| 61 |
.control-group {
|
| 62 |
display: flex;
|
| 63 |
+
align-items: center;
|
| 64 |
+
gap: 8px;
|
|
|
|
|
|
|
| 65 |
}
|
| 66 |
|
| 67 |
+
.control-group select {
|
|
|
|
| 68 |
background: #0d1324;
|
| 69 |
border-radius: 8px;
|
| 70 |
border: 1px solid #2c3554;
|
|
|
|
| 73 |
font-size: 0.85rem;
|
| 74 |
}
|
| 75 |
|
| 76 |
+
.clear-button {
|
| 77 |
+
align-self: center;
|
| 78 |
padding: 6px 12px;
|
| 79 |
border-radius: 8px;
|
| 80 |
border: 1px solid #2c3554;
|
| 81 |
+
background: #dc2626ba;
|
| 82 |
+
color: #f5f5f5;
|
| 83 |
+
font-size: 0.85rem;
|
| 84 |
+
cursor: pointer;
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
.clear-button:hover {
|
| 88 |
+
background: #dc2626;
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
.settings-button {
|
| 92 |
+
align-self: center;
|
| 93 |
+
padding: 12px 12px;
|
| 94 |
+
border-radius: 8px;
|
| 95 |
+
border: 1px solid #2c3554;
|
| 96 |
background: #1f2840;
|
| 97 |
color: #f5f5f5;
|
| 98 |
font-size: 0.85rem;
|
| 99 |
cursor: pointer;
|
| 100 |
+
margin-left: auto;
|
| 101 |
}
|
| 102 |
|
| 103 |
+
.settings-button:hover {
|
| 104 |
background: #273256;
|
| 105 |
}
|
|
|
|
| 106 |
/* Chat window */
|
| 107 |
.chat-window {
|
| 108 |
flex: 1;
|
|
|
|
| 336 |
color: #ff8080;
|
| 337 |
}
|
| 338 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 339 |
/* CONSENT OVERLAY FIXED VERSION */
|
| 340 |
.modal {
|
| 341 |
/* Covers the entier view port */
|
|
|
|
| 357 |
background-color: rgba(0, 0, 0, 0.8);
|
| 358 |
}
|
| 359 |
|
| 360 |
+
.modal h2 {
|
| 361 |
+
margin-top: 0;
|
| 362 |
+
margin-bottom: 0;
|
| 363 |
+
}
|
| 364 |
+
|
| 365 |
.slider {
|
| 366 |
/* Smooth scrolling */
|
| 367 |
scroll-snap-type: x mandatory;
|
|
|
|
| 412 |
}
|
| 413 |
|
| 414 |
.language-modal,
|
| 415 |
+
.consent-box,
|
| 416 |
+
.profile,
|
| 417 |
+
.settings-modal-content {
|
| 418 |
display: flex;
|
| 419 |
flex-direction: column;
|
| 420 |
justify-content: space-between;
|
|
|
|
| 421 |
}
|
| 422 |
|
| 423 |
+
.language-modal,
|
| 424 |
+
.consent-box {
|
| 425 |
+
max-height: 350px;
|
|
|
|
| 426 |
}
|
| 427 |
|
| 428 |
.form-group {
|
|
|
|
| 502 |
accent-color: #007bff; /* Modern way to color native inputs */
|
| 503 |
}
|
| 504 |
|
| 505 |
+
.radio-group label, .checkbox-grid label, .checkbox-grid-lang label {
|
| 506 |
font-weight: 400;
|
| 507 |
display: flex;
|
| 508 |
align-items: center;
|
|
|
|
| 611 |
transition: all 0.2s ease;
|
| 612 |
}
|
| 613 |
|
| 614 |
+
/* Settings modal */
|
| 615 |
+
.settings-modal-content {
|
| 616 |
+
position: relative;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 617 |
}
|
| 618 |
|
| 619 |
+
/* Font size */
|
| 620 |
+
.font-size-container {
|
| 621 |
+
display: flex;
|
| 622 |
+
gap: 12px;
|
| 623 |
+
margin-top: 8px;
|
| 624 |
+
justify-content: flex-start;
|
| 625 |
}
|
| 626 |
|
| 627 |
+
.font-size-btn {
|
| 628 |
+
padding: 12px 20px;
|
| 629 |
+
border-radius: 8px;
|
| 630 |
+
border: 1px solid #2c3554;
|
| 631 |
+
background: #1f2840;
|
| 632 |
+
color: #f5f5f5;
|
| 633 |
+
font-size: 1rem;
|
| 634 |
+
font-weight: 600;
|
| 635 |
cursor: pointer;
|
| 636 |
+
transition: all 0.2s ease;
|
| 637 |
+
min-width: 60px;
|
|
|
|
|
|
|
| 638 |
}
|
| 639 |
|
| 640 |
+
.font-size-btn:hover {
|
| 641 |
+
background: #273256;
|
| 642 |
+
border-color: #3d4a6e;
|
| 643 |
+
transform: translateY(-1px);
|
| 644 |
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
|
| 645 |
}
|
| 646 |
|
| 647 |
+
.font-size-btn:active {
|
| 648 |
+
transform: translateY(0);
|
| 649 |
}
|
| 650 |
|
| 651 |
+
/* RESPONSIVE DESIGN */
|
| 652 |
+
@media (max-width: 460px) {
|
| 653 |
+
/* Hide the text descriptions of the file action buttons */
|
| 654 |
+
.file-actions button span {
|
| 655 |
+
display: none;
|
| 656 |
+
}
|
|
|
|
| 657 |
|
| 658 |
+
/* Enlarge the chat container on mobile */
|
| 659 |
+
.chat-container {
|
| 660 |
+
margin: 0;
|
| 661 |
+
width: 100dvw;
|
| 662 |
+
height: 100dvh;
|
| 663 |
+
}
|
|
|
|
|
|
|
|
|
|
| 664 |
|
| 665 |
+
/* Reduce the font size of the title on mobile */
|
| 666 |
+
/* Also, add a gap between the title and the details */
|
| 667 |
+
.chat-header h1 {
|
| 668 |
+
margin: 0 0 10px 0;
|
| 669 |
+
font-size: 1.4rem;
|
| 670 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 671 |
|
| 672 |
+
/* Increase the size of the modals on mobile */
|
| 673 |
+
.modal-content {
|
| 674 |
+
width: 90%;
|
| 675 |
+
}
|
| 676 |
}
|
| 677 |
|
| 678 |
+
@media (max-height: 720px) {
|
| 679 |
+
/* Enlarge the chat container on small screens */
|
| 680 |
+
.chat-container {
|
| 681 |
+
margin: 0;
|
| 682 |
+
width: 100dvw;
|
| 683 |
+
height: 100dvh;
|
| 684 |
+
}
|
| 685 |
+
|
| 686 |
+
/* Reduce the font size of the title */
|
| 687 |
+
.chat-header h1 {
|
| 688 |
+
font-size: 1.4rem;
|
| 689 |
+
}
|
| 690 |
+
|
| 691 |
+
/* Increase the size of the modals */
|
| 692 |
+
.modal-content {
|
| 693 |
+
width: 90%;
|
| 694 |
+
}
|
| 695 |
}
|
| 696 |
+
|
| 697 |
+
@media (min-width: 460px) {
|
| 698 |
+
details {
|
| 699 |
+
display: block;
|
| 700 |
+
}
|
| 701 |
+
details[open] {
|
| 702 |
+
display: block;
|
| 703 |
+
}
|
| 704 |
+
details summary {
|
| 705 |
+
display: none;
|
| 706 |
+
}
|
| 707 |
+
}
|
static/translations.js
CHANGED
|
@@ -14,7 +14,9 @@ const translations = {
|
|
| 14 |
conversation_cleared: "Conversation cleared. Start a new chat!",
|
| 15 |
|
| 16 |
choose_language_title: "Choose your language",
|
| 17 |
-
change_language_instructions: "You can change the language at any time
|
|
|
|
|
|
|
| 18 |
|
| 19 |
consent_title: "Before you continue",
|
| 20 |
consent_desc: "By using this demo you agree that your messages will be shared with us for processing. Do not provide sensitive or private details.",
|
|
@@ -47,8 +49,8 @@ const translations = {
|
|
| 47 |
|
| 48 |
file_title: "Add a file",
|
| 49 |
file_inactivity: "Uploaded files are automatically deleted after 4 hours of inactivity.",
|
| 50 |
-
file_format: "Accepted formats: PDF, TXT, DOCX, JPG, JPEG, PNG (Max
|
| 51 |
-
file_size_limit: "The total size of all uploaded files cannot exceed
|
| 52 |
error_file_format: "Please upload a picture or a document in PDF, TXT, or DOCX format. Other file types are not supported.",
|
| 53 |
error_file_size: "File size exceeds limit. Maximum allowed: 10MB.",
|
| 54 |
error_total_file_size: "The total size of the files would exceed the maximum limit of 30 MB. Please free up space by deleting files.",
|
|
@@ -64,12 +66,19 @@ const translations = {
|
|
| 64 |
file_add_instructions_suffix: " to browse",
|
| 65 |
click: "Click",
|
| 66 |
|
| 67 |
-
|
| 68 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
file_upload_success: "File upload successful!",
|
| 70 |
|
| 71 |
-
file_delete_failed_server_error: "File deletion
|
| 72 |
-
file_delete_failed_network_error: "File deletion
|
| 73 |
file_delete_success: "File deletion successful!",
|
| 74 |
|
| 75 |
done_btn: "Done",
|
|
@@ -79,6 +88,7 @@ const translations = {
|
|
| 79 |
model_changed: "Model changed",
|
| 80 |
sending: "Sending...",
|
| 81 |
no_reply: "(No reply)",
|
|
|
|
| 82 |
|
| 83 |
server_error: "Error from server",
|
| 84 |
network_error: "Network error",
|
|
@@ -99,10 +109,12 @@ const translations = {
|
|
| 99 |
gemini_conservative: "Gemini-3 (Prudent)",
|
| 100 |
gemini_creative: "Gemini-3 (Créatif)",
|
| 101 |
btn_clear: "Réinitialiser",
|
| 102 |
-
conversation_cleared: "Conversation réinitialisée.
|
| 103 |
|
| 104 |
choose_language_title: "Choisissez votre langue",
|
| 105 |
-
change_language_instructions: "Vous pouvez changer la langue à tout moment
|
|
|
|
|
|
|
| 106 |
|
| 107 |
consent_title: "Avant de poursuivre",
|
| 108 |
consent_desc: "En interagissant avec cette démo, vous acceptez que vos messages soient partagés avec nous à des fins de traitement. Veillez à ne partager aucune information sensible ou privée.",
|
|
@@ -152,12 +164,19 @@ const translations = {
|
|
| 152 |
file_add_instructions_suffix: " pour parcourir",
|
| 153 |
click: "Cliquez",
|
| 154 |
|
| 155 |
-
|
| 156 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 157 |
file_upload_success: "Téléversement du fichier réussi !",
|
| 158 |
|
| 159 |
-
file_delete_failed_server_error: "
|
| 160 |
-
file_delete_failed_network_error: "
|
| 161 |
file_delete_success: "Suppression du fichier réussie !",
|
| 162 |
|
| 163 |
done_btn: "Terminer",
|
|
@@ -167,6 +186,7 @@ const translations = {
|
|
| 167 |
model_changed: "Changement de modèle",
|
| 168 |
sending: "Envoi...",
|
| 169 |
no_reply: "(Aucune réponse)",
|
|
|
|
| 170 |
|
| 171 |
server_error: "Erreur du serveur",
|
| 172 |
network_error: "Erreur réseau",
|
|
|
|
| 14 |
conversation_cleared: "Conversation cleared. Start a new chat!",
|
| 15 |
|
| 16 |
choose_language_title: "Choose your language",
|
| 17 |
+
change_language_instructions: "You can change the language at any time in the Settings menu located in the toolbar.",
|
| 18 |
+
change_language: "Change language",
|
| 19 |
+
change_font_size: "Change font size",
|
| 20 |
|
| 21 |
consent_title: "Before you continue",
|
| 22 |
consent_desc: "By using this demo you agree that your messages will be shared with us for processing. Do not provide sensitive or private details.",
|
|
|
|
| 49 |
|
| 50 |
file_title: "Add a file",
|
| 51 |
file_inactivity: "Uploaded files are automatically deleted after 4 hours of inactivity.",
|
| 52 |
+
file_format: "Accepted formats: PDF, TXT, DOCX, JPG, JPEG, PNG (Max 10 MB).",
|
| 53 |
+
file_size_limit: "The total size of all uploaded files cannot exceed 30 MB.",
|
| 54 |
error_file_format: "Please upload a picture or a document in PDF, TXT, or DOCX format. Other file types are not supported.",
|
| 55 |
error_file_size: "File size exceeds limit. Maximum allowed: 10MB.",
|
| 56 |
error_total_file_size: "The total size of the files would exceed the maximum limit of 30 MB. Please free up space by deleting files.",
|
|
|
|
| 66 |
file_add_instructions_suffix: " to browse",
|
| 67 |
click: "Click",
|
| 68 |
|
| 69 |
+
settings_title: "Settings",
|
| 70 |
+
|
| 71 |
+
file_upload_failed_server_error: "File upload failed: server error.",
|
| 72 |
+
file_upload_failed_file_too_large: "File upload failed: size exceeds 10 MB limit",
|
| 73 |
+
file_upload_failed_malformed_file: "File upload failed: file invalid",
|
| 74 |
+
file_upload_failed_unsupported_mime_type: "File upload failed: file must be in PDF, TXT, DOCX, JPEG or PNG format",
|
| 75 |
+
file_upload_failed_exceed_session_size: "File upload failed: the total size of all uploaded files exceeds 30 MB",
|
| 76 |
+
file_upload_failed_network_error: "File upload failed: network error",
|
| 77 |
+
file_upload_failed_unknown_error: "File upload failed: unknown error",
|
| 78 |
file_upload_success: "File upload successful!",
|
| 79 |
|
| 80 |
+
file_delete_failed_server_error: "File deletion failed: server error",
|
| 81 |
+
file_delete_failed_network_error: "File deletion failed: network error",
|
| 82 |
file_delete_success: "File deletion successful!",
|
| 83 |
|
| 84 |
done_btn: "Done",
|
|
|
|
| 88 |
model_changed: "Model changed",
|
| 89 |
sending: "Sending...",
|
| 90 |
no_reply: "(No reply)",
|
| 91 |
+
empty_message_error: "Message cannot be empty",
|
| 92 |
|
| 93 |
server_error: "Error from server",
|
| 94 |
network_error: "Network error",
|
|
|
|
| 109 |
gemini_conservative: "Gemini-3 (Prudent)",
|
| 110 |
gemini_creative: "Gemini-3 (Créatif)",
|
| 111 |
btn_clear: "Réinitialiser",
|
| 112 |
+
conversation_cleared: "Conversation réinitialisée.",
|
| 113 |
|
| 114 |
choose_language_title: "Choisissez votre langue",
|
| 115 |
+
change_language_instructions: "Vous pouvez changer la langue à tout moment dans le menu Paramètres situé dans la barre d'outils.",
|
| 116 |
+
change_language: "Changer la langue",
|
| 117 |
+
change_font_size: "Modifier la taille de la police",
|
| 118 |
|
| 119 |
consent_title: "Avant de poursuivre",
|
| 120 |
consent_desc: "En interagissant avec cette démo, vous acceptez que vos messages soient partagés avec nous à des fins de traitement. Veillez à ne partager aucune information sensible ou privée.",
|
|
|
|
| 164 |
file_add_instructions_suffix: " pour parcourir",
|
| 165 |
click: "Cliquez",
|
| 166 |
|
| 167 |
+
settings_title: "Paramètres",
|
| 168 |
+
|
| 169 |
+
file_upload_failed_server_error: "Échec du téléversement: erreur du serveur.",
|
| 170 |
+
file_upload_failed_file_too_large: "Échec du téléversement: la taille du fichier dépasse la limite de 10 Mo",
|
| 171 |
+
file_upload_failed_malformed_file: "Échec du téléversement: le fichier est invalide",
|
| 172 |
+
file_upload_failed_unsupported_mime_type: "Échec du téléversement: le fichier doit être en format PDF, TXT, DOCX, PNG ou JPEG",
|
| 173 |
+
file_upload_failed_exceed_session_size: "Échec du téléversement: la taille totale des fichiers téléversés dépassent 30 Mo",
|
| 174 |
+
file_upload_failed_network_error: "Échec du téléversement: erreur réseau",
|
| 175 |
+
file_upload_failed_unknown_error: "Échec du téléversement: erreur inconnu",
|
| 176 |
file_upload_success: "Téléversement du fichier réussi !",
|
| 177 |
|
| 178 |
+
file_delete_failed_server_error: "Échec de la suppression: erreur du serveur",
|
| 179 |
+
file_delete_failed_network_error: "Échec de la suppression: erreur réseau",
|
| 180 |
file_delete_success: "Suppression du fichier réussie !",
|
| 181 |
|
| 182 |
done_btn: "Terminer",
|
|
|
|
| 186 |
model_changed: "Changement de modèle",
|
| 187 |
sending: "Envoi...",
|
| 188 |
no_reply: "(Aucune réponse)",
|
| 189 |
+
empty_message_error: "Le message ne peut pas être vide.",
|
| 190 |
|
| 191 |
server_error: "Erreur du serveur",
|
| 192 |
network_error: "Erreur réseau",
|
templates/index.html
CHANGED
|
@@ -31,22 +31,47 @@
|
|
| 31 |
<!-- Controls bar -->
|
| 32 |
<div class="controls-bar">
|
| 33 |
<div class="control-group">
|
| 34 |
-
<
|
| 35 |
-
|
| 36 |
-
<
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
|
|
|
|
|
|
|
|
|
| 42 |
</div>
|
| 43 |
|
| 44 |
-
<button id="
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
|
| 46 |
-
|
| 47 |
-
<
|
| 48 |
-
|
| 49 |
-
<
|
| 50 |
</div>
|
| 51 |
</div>
|
| 52 |
|
|
@@ -170,7 +195,7 @@
|
|
| 170 |
<!-- Comment overlay -->
|
| 171 |
<div id="comment-overlay" class="modal" style="display:none">
|
| 172 |
<div class="modal-content comment-area">
|
| 173 |
-
<button id="closeCommentBtn" class="closeBtn"
|
| 174 |
<h2 data-i18n="comment_title"></h2>
|
| 175 |
<textarea
|
| 176 |
id="commentInput"
|
|
@@ -185,7 +210,7 @@
|
|
| 185 |
<!-- Upload file overlay -->
|
| 186 |
<div id="upload-file-overlay" class="modal" style="display:none">
|
| 187 |
<div class="modal-content upload-file-area">
|
| 188 |
-
<button id="close-file-upload-btn" class="closeBtn"
|
| 189 |
<h2 data-i18n="file_title"></h2>
|
| 190 |
<p data-i18n="file_inactivity"></p>
|
| 191 |
<p data-i18n="file_format"></p>
|
|
@@ -216,12 +241,6 @@
|
|
| 216 |
|
| 217 |
<div id="snackbar-container"></div>
|
| 218 |
|
| 219 |
-
<div class="font-size-container">
|
| 220 |
-
<button id="increase-font-size-btn" class="font-size-btn">Aa+</button>
|
| 221 |
-
<button id="reset-font-size-btn" class="font-size-btn">Aa</button>
|
| 222 |
-
<button id="decrease-font-size-btn" class="font-size-btn">Aa-</button>
|
| 223 |
-
</div>
|
| 224 |
-
|
| 225 |
<script src="/static/translations.js"></script>
|
| 226 |
<script src="/static/snackbar.js"></script>
|
| 227 |
<script src="/static/app.js"></script>
|
|
|
|
| 31 |
<!-- Controls bar -->
|
| 32 |
<div class="controls-bar">
|
| 33 |
<div class="control-group">
|
| 34 |
+
<fieldset class="control-group">
|
| 35 |
+
<legend for="systemPreset" data-i18n="model_selection"></legend>
|
| 36 |
+
<select id="systemPreset">
|
| 37 |
+
<option value="champ" selected>CHAMP</option>
|
| 38 |
+
<!-- champ is our model -->
|
| 39 |
+
<option value="openai">GPT-5.2</option>
|
| 40 |
+
<option value="google-conservative" data-i18n="gemini_conservative"></option>
|
| 41 |
+
<option value="google-creative" data-i18n="gemini_creative"></option>
|
| 42 |
+
</select>
|
| 43 |
+
<button id="clearBtn" class="clear-button" data-i18n="btn_clear"></button>
|
| 44 |
+
</fieldset>
|
| 45 |
</div>
|
| 46 |
|
| 47 |
+
<button id="settings-btn" class="settings-button">⚙️</button>
|
| 48 |
+
</div>
|
| 49 |
+
|
| 50 |
+
<!-- Settings overlay -->
|
| 51 |
+
<div id="settings-modal" class="modal" style="display: none;">
|
| 52 |
+
<div class="modal-content settings-modal-content">
|
| 53 |
+
<button id="close-settings-btn" class="closeBtn">×</button>
|
| 54 |
+
<h2 data-i18n="settings_title"></h2>
|
| 55 |
+
<h3 data-i18n="change_language"></h3>
|
| 56 |
+
<div class="form-group">
|
| 57 |
+
<span class="group-label" data-i18n="language"></span>
|
| 58 |
+
<div class="checkbox-grid-lang">
|
| 59 |
+
<label for="lang-fr-settings"><input type="radio" name="lang-settings" value="fr" id="lang-fr-settings"><span>Français</span></label>
|
| 60 |
+
<label for="lang-en-settings"><input type="radio" name="lang-settings" value="en" id="lang-en-settings"><span>English</span></label>
|
| 61 |
+
</div>
|
| 62 |
+
</div>
|
| 63 |
+
|
| 64 |
+
<h3 data-i18n="change_font_size"></h3>
|
| 65 |
+
<div class="font-size-container">
|
| 66 |
+
<button id="increase-font-size-btn" class="font-size-btn" style="font-size: 1.2rem;">Aa+</button>
|
| 67 |
+
<button id="reset-font-size-btn" class="font-size-btn" style="font-size: 1rem;">Aa</button>
|
| 68 |
+
<button id="decrease-font-size-btn" class="font-size-btn" style="font-size: 0.8rem;">Aa-</button>
|
| 69 |
+
</div>
|
| 70 |
|
| 71 |
+
<!-- div to center the button -->
|
| 72 |
+
<div class="center-button">
|
| 73 |
+
<button id="done-settings" class="ok-button" style="margin-top: 20px;" data-i18n="done_btn"></button>
|
| 74 |
+
</div>
|
| 75 |
</div>
|
| 76 |
</div>
|
| 77 |
|
|
|
|
| 195 |
<!-- Comment overlay -->
|
| 196 |
<div id="comment-overlay" class="modal" style="display:none">
|
| 197 |
<div class="modal-content comment-area">
|
| 198 |
+
<button id="closeCommentBtn" class="closeBtn">×</button>
|
| 199 |
<h2 data-i18n="comment_title"></h2>
|
| 200 |
<textarea
|
| 201 |
id="commentInput"
|
|
|
|
| 210 |
<!-- Upload file overlay -->
|
| 211 |
<div id="upload-file-overlay" class="modal" style="display:none">
|
| 212 |
<div class="modal-content upload-file-area">
|
| 213 |
+
<button id="close-file-upload-btn" class="closeBtn">×</button>
|
| 214 |
<h2 data-i18n="file_title"></h2>
|
| 215 |
<p data-i18n="file_inactivity"></p>
|
| 216 |
<p data-i18n="file_format"></p>
|
|
|
|
| 241 |
|
| 242 |
<div id="snackbar-container"></div>
|
| 243 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 244 |
<script src="/static/translations.js"></script>
|
| 245 |
<script src="/static/snackbar.js"></script>
|
| 246 |
<script src="/static/app.js"></script>
|