Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 54d72f8f57 | |||
| dfb7fb81a6 |
@@ -9,11 +9,9 @@ skip =
|
||||
**/e2e/report/**,
|
||||
*.tsbuildinfo,
|
||||
**/uv.lock,
|
||||
./docker/files/etc/mime.types,
|
||||
check-filenames = true
|
||||
ignore-words-list =
|
||||
afterAll,
|
||||
statics,
|
||||
exclude-file =
|
||||
./src/backend/chat/agent_rag/web_search/mocked.py,
|
||||
|
||||
|
||||
@@ -200,7 +200,7 @@ jobs:
|
||||
- name: Install gettext (required to compile messages) and MIME support
|
||||
run: |
|
||||
sudo apt-get install -y gettext pandoc shared-mime-info
|
||||
sudo cp $GITHUB_WORKSPACE/docker/files/etc/mime.types /etc/mime.types
|
||||
sudo wget https://svn.apache.org/repos/asf/httpd/httpd/trunk/docs/conf/mime.types -O /etc/mime.types
|
||||
|
||||
- name: Generate a MO file from strings extracted from the project
|
||||
run: uv run python manage.py compilemessages
|
||||
|
||||
+2
-35
@@ -10,44 +10,13 @@ and this project adheres to
|
||||
|
||||
### Added
|
||||
|
||||
- ✨(waffle) hide the waffle if not fr theme
|
||||
- ✨(front) allow pasting an attachment from clipboard
|
||||
- ✨(array) temporarily adjust array
|
||||
- ✨(tools) add basic translate tool
|
||||
|
||||
### Changed
|
||||
|
||||
- ⚡️(front) optimize streaming markdown rendering performance
|
||||
- ⬆️(back) update pydantic-ai
|
||||
- ♻️(chat) refactor AIAgentService for readability and maintainability
|
||||
|
||||
### Fixed
|
||||
|
||||
- 💚(docker) vendor mime.types file instead of fetching from Apache SVN
|
||||
- 🐛(front) fix math formulas and carousel translations
|
||||
- 🐛(helm) reverse liveness and readiness for backend deployment
|
||||
|
||||
## [0.0.13] - 2026-02-09
|
||||
|
||||
### Added
|
||||
|
||||
- 💄(front) ui fix : update ui-kit
|
||||
- ✨(front) add persistent darkmode
|
||||
- ✨(front) add ui kit #240
|
||||
- 🧱(files) allow to use S3 storage without external access #849
|
||||
- ✨(backend) add FindRagBackend #209
|
||||
- ⬆️(back) update dependencies
|
||||
- ✨(back) use adaptive parsing for pdf documents
|
||||
|
||||
### Changed
|
||||
|
||||
- 💄(darkmode) change color feedback button
|
||||
- 🏗️(back) migrate to uv
|
||||
- ♻️(front) optimize syntax highlighting bundle size
|
||||
|
||||
### Fixed
|
||||
|
||||
- 🐛(back) cast collection Ids to API expected types
|
||||
|
||||
## [0.0.12] - 2026-01-27
|
||||
|
||||
@@ -65,7 +34,7 @@ and this project adheres to
|
||||
### Changed
|
||||
|
||||
- 📦️(front) update react
|
||||
- ✨(chat) generate and edit conversation title
|
||||
- ✨(chat) Generate and edit conversation title
|
||||
|
||||
|
||||
### Fixed
|
||||
@@ -75,7 +44,6 @@ and this project adheres to
|
||||
- ⚰️(back) remove dead code and unused files
|
||||
- 🐛(back) prevent tool call timeouts
|
||||
|
||||
|
||||
### Removed
|
||||
|
||||
- 🔥(chat) remove thinking part from frontend #227
|
||||
@@ -234,8 +202,7 @@ and this project adheres to
|
||||
- 💄(chat) add code highlighting for LLM responses #67
|
||||
|
||||
|
||||
[unreleased]: https://github.com/suitenumerique/conversations/compare/v0.0.13...main
|
||||
[0.0.13]: https://github.com/suitenumerique/conversations/releases/v0.0.13
|
||||
[unreleased]: https://github.com/suitenumerique/conversations/compare/v0.0.12...main
|
||||
[0.0.12]: https://github.com/suitenumerique/conversations/releases/v0.0.12
|
||||
[0.0.11]: https://github.com/suitenumerique/conversations/releases/v0.0.11
|
||||
[0.0.10]: https://github.com/suitenumerique/conversations/releases/v0.0.10
|
||||
|
||||
+1
-1
@@ -93,7 +93,7 @@ RUN apk add \
|
||||
pango \
|
||||
shared-mime-info
|
||||
|
||||
COPY ./docker/files/etc/mime.types /etc/mime.types
|
||||
RUN wget https://svn.apache.org/repos/asf/httpd/httpd/trunk/docs/conf/mime.types -O /etc/mime.types
|
||||
|
||||
# Copy entrypoint
|
||||
COPY ./docker/files/usr/local/bin/entrypoint /usr/local/bin/entrypoint
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,15 +1,11 @@
|
||||
"""Document parsers for RAG backends."""
|
||||
|
||||
import base64
|
||||
import logging
|
||||
import time
|
||||
from io import BytesIO
|
||||
from urllib.parse import urljoin
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
import requests
|
||||
from pypdf import PdfReader, PdfWriter
|
||||
|
||||
from chat.agent_rag.document_converter.markitdown import DocumentConverter
|
||||
|
||||
@@ -67,214 +63,3 @@ class AlbertParser(BaseParser):
|
||||
return DocumentConverter().convert_raw(
|
||||
name=name, content_type=content_type, content=content
|
||||
)
|
||||
|
||||
|
||||
METHOD_TEXT_EXTRACTION = "text_extraction"
|
||||
METHOD_OCR = "ocr"
|
||||
|
||||
|
||||
def analyze_pdf(pdf_data: bytes) -> dict:
|
||||
"""
|
||||
Analyze a PDF to determine if it needs OCR or can use direct text extraction.
|
||||
"""
|
||||
reader = PdfReader(BytesIO(pdf_data))
|
||||
total_pages = len(reader.pages)
|
||||
if total_pages == 0:
|
||||
logger.info("No page found in pdf")
|
||||
return {
|
||||
"total_pages": 0,
|
||||
"pages_with_text": 0,
|
||||
"avg_chars_per_page": 0,
|
||||
"text_coverage": 0,
|
||||
"recommended_method": METHOD_TEXT_EXTRACTION,
|
||||
}
|
||||
|
||||
total_chars = 0
|
||||
pages_with_text = 0
|
||||
for page in reader.pages:
|
||||
text = (page.extract_text() or "").strip()
|
||||
char_count = len(text)
|
||||
total_chars += char_count
|
||||
|
||||
if char_count > 50:
|
||||
pages_with_text += 1
|
||||
|
||||
avg_chars = total_chars / total_pages
|
||||
text_coverage = pages_with_text / total_pages
|
||||
|
||||
# Decision logic
|
||||
if (
|
||||
avg_chars > settings.MIN_AVG_CHARS_FOR_TEXT_EXTRACTION
|
||||
and text_coverage > settings.MIN_TEXT_COVERAGE_FOR_TEXT_EXTRACTION
|
||||
):
|
||||
method = METHOD_TEXT_EXTRACTION
|
||||
|
||||
else:
|
||||
method = METHOD_OCR
|
||||
|
||||
return {
|
||||
"total_pages": total_pages,
|
||||
"pages_with_text": pages_with_text,
|
||||
"avg_chars_per_page": round(avg_chars),
|
||||
"text_coverage": round(text_coverage, 2),
|
||||
"recommended_method": method,
|
||||
}
|
||||
|
||||
|
||||
class AdaptiveParserMixin:
|
||||
"""
|
||||
Mixin that adds adaptive PDF parsing behavior.
|
||||
|
||||
Analyzes PDF content to choose between direct text extraction (fast) and OCR
|
||||
(for scanned/image PDFs). Subclasses must implement `parse_pdf_document_with_ocr`.
|
||||
"""
|
||||
|
||||
def parse_pdf_document(self, name: str, content_type: str, content: bytes) -> str:
|
||||
"""Analyze PDF and route to text extraction or OCR based on content."""
|
||||
analysis = analyze_pdf(content)
|
||||
|
||||
logger.info(
|
||||
"Pdf analysis - pages: %s, pages with text: %s, text_coverage: %s, "
|
||||
"recommended method: %s",
|
||||
analysis["total_pages"],
|
||||
analysis["pages_with_text"],
|
||||
analysis["text_coverage"],
|
||||
analysis["recommended_method"],
|
||||
)
|
||||
|
||||
method = analysis["recommended_method"]
|
||||
if method == METHOD_TEXT_EXTRACTION:
|
||||
return self.extract_text_from_pdf(name=name, content_type=content_type, content=content)
|
||||
return self.parse_pdf_document_with_ocr(name=name, content=content)
|
||||
|
||||
def extract_text_from_pdf(self, name: str, content_type: str, content: bytes) -> str:
|
||||
"""Extract text directly from PDF without OCR (for text-based PDFs)."""
|
||||
logger.info("Parsing pdf with text extraction")
|
||||
return DocumentConverter().convert_raw(
|
||||
name=name, content_type=content_type, content=content
|
||||
)
|
||||
|
||||
def parse_pdf_document_with_ocr(self, name: str, content: bytes) -> str:
|
||||
"""Process PDF through OCR. Must be implemented by subclass."""
|
||||
raise NotImplementedError("Subclass must implement parse_pdf_document_with_ocr")
|
||||
|
||||
|
||||
class AdaptivePdfParser(AdaptiveParserMixin, BaseParser):
|
||||
"""
|
||||
PDF parser with adaptive text extraction / OCR routing.
|
||||
|
||||
Uses Mistral OCR API for scanned/image PDFs, processed in batches with retry logic.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self.endpoint = urljoin(
|
||||
settings.LLM_CONFIGURATIONS[settings.OCR_HRID].provider.base_url, "/v1/ocr"
|
||||
)
|
||||
self.max_retries = settings.OCR_MAX_RETRIES
|
||||
self.retry_delay = settings.OCR_RETRY_DELAY
|
||||
api_key = settings.LLM_CONFIGURATIONS[settings.OCR_HRID].provider.api_key
|
||||
|
||||
self.headers = {
|
||||
"Authorization": f"Bearer {api_key}",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
def extract_page_batch(self, reader: PdfReader, start_index: int, end_index: int) -> bytes:
|
||||
"""Extract a range of pages from PDF as a new PDF bytes object."""
|
||||
writer = PdfWriter()
|
||||
for i in range(start_index, end_index):
|
||||
writer.add_page(reader.pages[i])
|
||||
output = BytesIO()
|
||||
writer.write(output)
|
||||
return output.getvalue()
|
||||
|
||||
def ocr_page_batch(
|
||||
self,
|
||||
name: str,
|
||||
page_content: bytes,
|
||||
start_index: int,
|
||||
end_index: int,
|
||||
) -> list[str]:
|
||||
"""Send page batch to Mistral OCR API with static delay retry."""
|
||||
file_data = base64.standard_b64encode(page_content).decode("utf-8")
|
||||
payload = {
|
||||
"document": {
|
||||
"type": "document_url",
|
||||
"document_name": f"{name}_pages_{start_index + 1}_to_{end_index}",
|
||||
"document_url": f"data:application/pdf;base64,{file_data}",
|
||||
},
|
||||
"model": settings.OCR_MODEL,
|
||||
}
|
||||
|
||||
last_exception = None
|
||||
for attempt in range(self.max_retries):
|
||||
try:
|
||||
response = requests.post(
|
||||
self.endpoint,
|
||||
headers=self.headers,
|
||||
json=payload,
|
||||
timeout=settings.OCR_TIMEOUT,
|
||||
)
|
||||
response.raise_for_status()
|
||||
|
||||
pages = response.json().get("pages", [])
|
||||
return [page.get("markdown", "") for page in pages]
|
||||
|
||||
except (requests.Timeout, requests.RequestException) as e:
|
||||
last_exception = e
|
||||
if attempt < self.max_retries - 1:
|
||||
logger.warning(
|
||||
"OCR attempt %d/%d failed for pages %d-%d: %s. Retrying in %.1fs...",
|
||||
attempt + 1,
|
||||
self.max_retries,
|
||||
start_index + 1,
|
||||
end_index,
|
||||
str(e),
|
||||
self.retry_delay,
|
||||
)
|
||||
time.sleep(self.retry_delay)
|
||||
|
||||
logger.error(
|
||||
"OCR failed for pages %d-%d after %d attempts: %s",
|
||||
start_index + 1,
|
||||
end_index,
|
||||
self.max_retries,
|
||||
str(last_exception),
|
||||
)
|
||||
raise last_exception
|
||||
|
||||
def parse_pdf_document_with_ocr(self, name: str, content: bytes) -> str:
|
||||
"""Process PDF through OCR in batches, returning concatenated markdown."""
|
||||
reader = PdfReader(BytesIO(content))
|
||||
total_pages = len(reader.pages)
|
||||
batch_size = settings.OCR_BATCH_PAGES
|
||||
|
||||
logger.info("Parsing pdf with OCR (%d pages, batch size %d)", total_pages, batch_size)
|
||||
|
||||
results = []
|
||||
for start_index in range(0, total_pages, batch_size):
|
||||
end_index = min(start_index + batch_size, total_pages)
|
||||
batch_content = self.extract_page_batch(reader, start_index, end_index)
|
||||
try:
|
||||
batch_results = self.ocr_page_batch(name, batch_content, start_index, end_index)
|
||||
results.extend(batch_results)
|
||||
logger.debug(
|
||||
"Completed OCR for pages %d-%d/%d", start_index + 1, end_index, total_pages
|
||||
)
|
||||
except Exception as e: # pylint: disable=broad-except #noqa: BLE001
|
||||
logger.error("Failed to OCR pages %d-%d: %s", start_index + 1, end_index, str(e))
|
||||
# Preserve page count with empty placeholders to maintain correct ordering
|
||||
results.extend([""] * (end_index - start_index))
|
||||
|
||||
return "\n\n".join(results)
|
||||
|
||||
def parse_document(self, name: str, content_type: str, content: bytes) -> str:
|
||||
"""Route to PDF parser or DocumentConverter based on content type."""
|
||||
if content_type == "application/pdf":
|
||||
return self.parse_pdf_document(name=name, content_type=content_type, content=content)
|
||||
|
||||
return DocumentConverter().convert_raw(
|
||||
name=name, content_type=content_type, content=content
|
||||
)
|
||||
|
||||
@@ -7,13 +7,13 @@ from typing import List, Optional
|
||||
from urllib.parse import urljoin
|
||||
|
||||
from django.conf import settings
|
||||
from django.utils.module_loading import import_string
|
||||
|
||||
import httpx
|
||||
import requests
|
||||
|
||||
from chat.agent_rag.albert_api_constants import Searches
|
||||
from chat.agent_rag.constants import RAGWebResult, RAGWebResults, RAGWebUsage
|
||||
from chat.agent_rag.document_converter.parser import AlbertParser
|
||||
from chat.agent_rag.document_rag_backends.base_rag_backend import BaseRagBackend
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -45,13 +45,7 @@ class AlbertRagBackend(BaseRagBackend): # pylint: disable=too-many-instance-att
|
||||
self._documents_endpoint = urljoin(self._base_url, "/v1/documents")
|
||||
self._search_endpoint = urljoin(self._base_url, "/v1/search")
|
||||
self._default_collection_description = "Temporary collection for RAG document search"
|
||||
parser_class = import_string(settings.RAG_DOCUMENT_PARSER)
|
||||
self.parser = parser_class()
|
||||
|
||||
@staticmethod
|
||||
def cast_collection_id(collection_id):
|
||||
"""Albert API expects int Ids."""
|
||||
return int(collection_id)
|
||||
self.parser = AlbertParser()
|
||||
|
||||
def create_collection(self, name: str, description: Optional[str] = None) -> str:
|
||||
"""
|
||||
|
||||
@@ -41,11 +41,6 @@ class BaseRagBackend(ABC):
|
||||
self._default_collection_description = "Temporary collection for RAG document search"
|
||||
self.parser: BaseParser = BaseParser()
|
||||
|
||||
@staticmethod
|
||||
def cast_collection_id(collection_id):
|
||||
"""Dummy method to be overridden when needed."""
|
||||
return collection_id
|
||||
|
||||
def get_all_collection_ids(self) -> List[str]:
|
||||
"""
|
||||
Get all collection IDs, including the main collection ID and read-only collection IDs.
|
||||
@@ -60,13 +55,10 @@ class BaseRagBackend(ABC):
|
||||
|
||||
collection_ids = []
|
||||
if self.collection_id:
|
||||
collection_ids.append(self.cast_collection_id(self.collection_id))
|
||||
collection_ids.append(self.collection_id)
|
||||
if self.read_only_collection_id:
|
||||
collection_ids.extend(
|
||||
[
|
||||
self.cast_collection_id(collection_id)
|
||||
for collection_id in self.read_only_collection_id
|
||||
]
|
||||
[int(collection_id) for collection_id in self.read_only_collection_id]
|
||||
)
|
||||
return collection_ids
|
||||
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
"""Build the translation agent."""
|
||||
|
||||
import dataclasses
|
||||
import logging
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
from .base import BaseAgent
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclasses.dataclass(init=False)
|
||||
class TranslationAgent(BaseAgent):
|
||||
"""Create a Pydantic AI translation Agent instance with the configured settings"""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
"""Initialize the agent with the configured model."""
|
||||
super().__init__(
|
||||
model_hrid=settings.LLM_DEFAULT_MODEL_HRID,
|
||||
output_type=str,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
def get_tools(self) -> list:
|
||||
"""Translation does not need any tools."""
|
||||
return []
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,161 +0,0 @@
|
||||
%PDF-1.4
|
||||
%âãÏÓ
|
||||
1 0 obj
|
||||
<<
|
||||
/Producer (pypdf)
|
||||
>>
|
||||
endobj
|
||||
2 0 obj
|
||||
<<
|
||||
/Type /Pages
|
||||
/Count 10
|
||||
/Kids [ 4 0 R 7 0 R 8 0 R 9 0 R 10 0 R 11 0 R 12 0 R 13 0 R 14 0 R 15 0 R ]
|
||||
>>
|
||||
endobj
|
||||
3 0 obj
|
||||
<<
|
||||
/Type /Catalog
|
||||
/Pages 2 0 R
|
||||
>>
|
||||
endobj
|
||||
4 0 obj
|
||||
<<
|
||||
/Type /Page
|
||||
/MediaBox [ 0 0 612 792 ]
|
||||
/Contents 5 0 R
|
||||
/Resources <<
|
||||
/Font <<
|
||||
/F1 6 0 R
|
||||
>>
|
||||
>>
|
||||
/Parent 2 0 R
|
||||
>>
|
||||
endobj
|
||||
5 0 obj
|
||||
<<
|
||||
/Length 132
|
||||
>>
|
||||
stream
|
||||
BT /F1 12 Tf 100 700 Td (AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA) Tj ET
|
||||
endstream
|
||||
endobj
|
||||
6 0 obj
|
||||
<<
|
||||
/Type /Font
|
||||
/Subtype /Type1
|
||||
/BaseFont /Helvetica
|
||||
/Encoding /WinAnsiEncoding
|
||||
>>
|
||||
endobj
|
||||
7 0 obj
|
||||
<<
|
||||
/Type /Page
|
||||
/MediaBox [ 0 0 612 792 ]
|
||||
/Contents 5 0 R
|
||||
/Resources <<
|
||||
/Font <<
|
||||
/F1 6 0 R
|
||||
>>
|
||||
>>
|
||||
/Parent 2 0 R
|
||||
>>
|
||||
endobj
|
||||
8 0 obj
|
||||
<<
|
||||
/Type /Page
|
||||
/Resources <<
|
||||
>>
|
||||
/MediaBox [ 0.0 0.0 612 792 ]
|
||||
/Parent 2 0 R
|
||||
>>
|
||||
endobj
|
||||
9 0 obj
|
||||
<<
|
||||
/Type /Page
|
||||
/Resources <<
|
||||
>>
|
||||
/MediaBox [ 0.0 0.0 612 792 ]
|
||||
/Parent 2 0 R
|
||||
>>
|
||||
endobj
|
||||
10 0 obj
|
||||
<<
|
||||
/Type /Page
|
||||
/Resources <<
|
||||
>>
|
||||
/MediaBox [ 0.0 0.0 612 792 ]
|
||||
/Parent 2 0 R
|
||||
>>
|
||||
endobj
|
||||
11 0 obj
|
||||
<<
|
||||
/Type /Page
|
||||
/Resources <<
|
||||
>>
|
||||
/MediaBox [ 0.0 0.0 612 792 ]
|
||||
/Parent 2 0 R
|
||||
>>
|
||||
endobj
|
||||
12 0 obj
|
||||
<<
|
||||
/Type /Page
|
||||
/Resources <<
|
||||
>>
|
||||
/MediaBox [ 0.0 0.0 612 792 ]
|
||||
/Parent 2 0 R
|
||||
>>
|
||||
endobj
|
||||
13 0 obj
|
||||
<<
|
||||
/Type /Page
|
||||
/Resources <<
|
||||
>>
|
||||
/MediaBox [ 0.0 0.0 612 792 ]
|
||||
/Parent 2 0 R
|
||||
>>
|
||||
endobj
|
||||
14 0 obj
|
||||
<<
|
||||
/Type /Page
|
||||
/Resources <<
|
||||
>>
|
||||
/MediaBox [ 0.0 0.0 612 792 ]
|
||||
/Parent 2 0 R
|
||||
>>
|
||||
endobj
|
||||
15 0 obj
|
||||
<<
|
||||
/Type /Page
|
||||
/Resources <<
|
||||
>>
|
||||
/MediaBox [ 0.0 0.0 612 792 ]
|
||||
/Parent 2 0 R
|
||||
>>
|
||||
endobj
|
||||
xref
|
||||
0 16
|
||||
0000000000 65535 f
|
||||
0000000015 00000 n
|
||||
0000000054 00000 n
|
||||
0000000174 00000 n
|
||||
0000000223 00000 n
|
||||
0000000351 00000 n
|
||||
0000000534 00000 n
|
||||
0000000631 00000 n
|
||||
0000000759 00000 n
|
||||
0000000853 00000 n
|
||||
0000000947 00000 n
|
||||
0000001042 00000 n
|
||||
0000001137 00000 n
|
||||
0000001232 00000 n
|
||||
0000001327 00000 n
|
||||
0000001422 00000 n
|
||||
trailer
|
||||
<<
|
||||
/Size 16
|
||||
/Root 3 0 R
|
||||
/Info 1 0 R
|
||||
>>
|
||||
startxref
|
||||
1517
|
||||
%%EOF
|
||||
@@ -1,355 +0,0 @@
|
||||
%PDF-1.4
|
||||
%âãÏÓ
|
||||
1 0 obj
|
||||
<<
|
||||
/Producer (pypdf)
|
||||
>>
|
||||
endobj
|
||||
2 0 obj
|
||||
<<
|
||||
/Type /Pages
|
||||
/Count 10
|
||||
/Kids [ 4 0 R 7 0 R 10 0 R 13 0 R 16 0 R 19 0 R 22 0 R 25 0 R 28 0 R 31 0 R ]
|
||||
>>
|
||||
endobj
|
||||
3 0 obj
|
||||
<<
|
||||
/Type /Catalog
|
||||
/Pages 2 0 R
|
||||
>>
|
||||
endobj
|
||||
4 0 obj
|
||||
<<
|
||||
/Type /Page
|
||||
/MediaBox [ 0 0 612 792 ]
|
||||
/Contents 5 0 R
|
||||
/Resources <<
|
||||
/Font <<
|
||||
/F1 6 0 R
|
||||
>>
|
||||
>>
|
||||
/Parent 2 0 R
|
||||
>>
|
||||
endobj
|
||||
5 0 obj
|
||||
<<
|
||||
/Length 332
|
||||
>>
|
||||
stream
|
||||
BT /F1 12 Tf 100 700 Td (AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA) Tj ET
|
||||
endstream
|
||||
endobj
|
||||
6 0 obj
|
||||
<<
|
||||
/Type /Font
|
||||
/Subtype /Type1
|
||||
/BaseFont /Helvetica
|
||||
/Encoding /WinAnsiEncoding
|
||||
>>
|
||||
endobj
|
||||
7 0 obj
|
||||
<<
|
||||
/Type /Page
|
||||
/MediaBox [ 0 0 612 792 ]
|
||||
/Contents 8 0 R
|
||||
/Resources <<
|
||||
/Font <<
|
||||
/F1 9 0 R
|
||||
>>
|
||||
>>
|
||||
/Parent 2 0 R
|
||||
>>
|
||||
endobj
|
||||
8 0 obj
|
||||
<<
|
||||
/Length 332
|
||||
>>
|
||||
stream
|
||||
BT /F1 12 Tf 100 700 Td (AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA) Tj ET
|
||||
endstream
|
||||
endobj
|
||||
9 0 obj
|
||||
<<
|
||||
/Type /Font
|
||||
/Subtype /Type1
|
||||
/BaseFont /Helvetica
|
||||
/Encoding /WinAnsiEncoding
|
||||
>>
|
||||
endobj
|
||||
10 0 obj
|
||||
<<
|
||||
/Type /Page
|
||||
/MediaBox [ 0 0 612 792 ]
|
||||
/Contents 11 0 R
|
||||
/Resources <<
|
||||
/Font <<
|
||||
/F1 12 0 R
|
||||
>>
|
||||
>>
|
||||
/Parent 2 0 R
|
||||
>>
|
||||
endobj
|
||||
11 0 obj
|
||||
<<
|
||||
/Length 332
|
||||
>>
|
||||
stream
|
||||
BT /F1 12 Tf 100 700 Td (AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA) Tj ET
|
||||
endstream
|
||||
endobj
|
||||
12 0 obj
|
||||
<<
|
||||
/Type /Font
|
||||
/Subtype /Type1
|
||||
/BaseFont /Helvetica
|
||||
/Encoding /WinAnsiEncoding
|
||||
>>
|
||||
endobj
|
||||
13 0 obj
|
||||
<<
|
||||
/Type /Page
|
||||
/MediaBox [ 0 0 612 792 ]
|
||||
/Contents 14 0 R
|
||||
/Resources <<
|
||||
/Font <<
|
||||
/F1 15 0 R
|
||||
>>
|
||||
>>
|
||||
/Parent 2 0 R
|
||||
>>
|
||||
endobj
|
||||
14 0 obj
|
||||
<<
|
||||
/Length 332
|
||||
>>
|
||||
stream
|
||||
BT /F1 12 Tf 100 700 Td (AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA) Tj ET
|
||||
endstream
|
||||
endobj
|
||||
15 0 obj
|
||||
<<
|
||||
/Type /Font
|
||||
/Subtype /Type1
|
||||
/BaseFont /Helvetica
|
||||
/Encoding /WinAnsiEncoding
|
||||
>>
|
||||
endobj
|
||||
16 0 obj
|
||||
<<
|
||||
/Type /Page
|
||||
/MediaBox [ 0 0 612 792 ]
|
||||
/Contents 17 0 R
|
||||
/Resources <<
|
||||
/Font <<
|
||||
/F1 18 0 R
|
||||
>>
|
||||
>>
|
||||
/Parent 2 0 R
|
||||
>>
|
||||
endobj
|
||||
17 0 obj
|
||||
<<
|
||||
/Length 332
|
||||
>>
|
||||
stream
|
||||
BT /F1 12 Tf 100 700 Td (AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA) Tj ET
|
||||
endstream
|
||||
endobj
|
||||
18 0 obj
|
||||
<<
|
||||
/Type /Font
|
||||
/Subtype /Type1
|
||||
/BaseFont /Helvetica
|
||||
/Encoding /WinAnsiEncoding
|
||||
>>
|
||||
endobj
|
||||
19 0 obj
|
||||
<<
|
||||
/Type /Page
|
||||
/MediaBox [ 0 0 612 792 ]
|
||||
/Contents 20 0 R
|
||||
/Resources <<
|
||||
/Font <<
|
||||
/F1 21 0 R
|
||||
>>
|
||||
>>
|
||||
/Parent 2 0 R
|
||||
>>
|
||||
endobj
|
||||
20 0 obj
|
||||
<<
|
||||
/Length 332
|
||||
>>
|
||||
stream
|
||||
BT /F1 12 Tf 100 700 Td (AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA) Tj ET
|
||||
endstream
|
||||
endobj
|
||||
21 0 obj
|
||||
<<
|
||||
/Type /Font
|
||||
/Subtype /Type1
|
||||
/BaseFont /Helvetica
|
||||
/Encoding /WinAnsiEncoding
|
||||
>>
|
||||
endobj
|
||||
22 0 obj
|
||||
<<
|
||||
/Type /Page
|
||||
/MediaBox [ 0 0 612 792 ]
|
||||
/Contents 23 0 R
|
||||
/Resources <<
|
||||
/Font <<
|
||||
/F1 24 0 R
|
||||
>>
|
||||
>>
|
||||
/Parent 2 0 R
|
||||
>>
|
||||
endobj
|
||||
23 0 obj
|
||||
<<
|
||||
/Length 332
|
||||
>>
|
||||
stream
|
||||
BT /F1 12 Tf 100 700 Td (AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA) Tj ET
|
||||
endstream
|
||||
endobj
|
||||
24 0 obj
|
||||
<<
|
||||
/Type /Font
|
||||
/Subtype /Type1
|
||||
/BaseFont /Helvetica
|
||||
/Encoding /WinAnsiEncoding
|
||||
>>
|
||||
endobj
|
||||
25 0 obj
|
||||
<<
|
||||
/Type /Page
|
||||
/MediaBox [ 0 0 612 792 ]
|
||||
/Contents 26 0 R
|
||||
/Resources <<
|
||||
/Font <<
|
||||
/F1 27 0 R
|
||||
>>
|
||||
>>
|
||||
/Parent 2 0 R
|
||||
>>
|
||||
endobj
|
||||
26 0 obj
|
||||
<<
|
||||
/Length 332
|
||||
>>
|
||||
stream
|
||||
BT /F1 12 Tf 100 700 Td (AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA) Tj ET
|
||||
endstream
|
||||
endobj
|
||||
27 0 obj
|
||||
<<
|
||||
/Type /Font
|
||||
/Subtype /Type1
|
||||
/BaseFont /Helvetica
|
||||
/Encoding /WinAnsiEncoding
|
||||
>>
|
||||
endobj
|
||||
28 0 obj
|
||||
<<
|
||||
/Type /Page
|
||||
/MediaBox [ 0 0 612 792 ]
|
||||
/Contents 29 0 R
|
||||
/Resources <<
|
||||
/Font <<
|
||||
/F1 30 0 R
|
||||
>>
|
||||
>>
|
||||
/Parent 2 0 R
|
||||
>>
|
||||
endobj
|
||||
29 0 obj
|
||||
<<
|
||||
/Length 332
|
||||
>>
|
||||
stream
|
||||
BT /F1 12 Tf 100 700 Td (AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA) Tj ET
|
||||
endstream
|
||||
endobj
|
||||
30 0 obj
|
||||
<<
|
||||
/Type /Font
|
||||
/Subtype /Type1
|
||||
/BaseFont /Helvetica
|
||||
/Encoding /WinAnsiEncoding
|
||||
>>
|
||||
endobj
|
||||
31 0 obj
|
||||
<<
|
||||
/Type /Page
|
||||
/MediaBox [ 0 0 612 792 ]
|
||||
/Contents 32 0 R
|
||||
/Resources <<
|
||||
/Font <<
|
||||
/F1 33 0 R
|
||||
>>
|
||||
>>
|
||||
/Parent 2 0 R
|
||||
>>
|
||||
endobj
|
||||
32 0 obj
|
||||
<<
|
||||
/Length 332
|
||||
>>
|
||||
stream
|
||||
BT /F1 12 Tf 100 700 Td (AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA) Tj ET
|
||||
endstream
|
||||
endobj
|
||||
33 0 obj
|
||||
<<
|
||||
/Type /Font
|
||||
/Subtype /Type1
|
||||
/BaseFont /Helvetica
|
||||
/Encoding /WinAnsiEncoding
|
||||
>>
|
||||
endobj
|
||||
xref
|
||||
0 34
|
||||
0000000000 65535 f
|
||||
0000000015 00000 n
|
||||
0000000054 00000 n
|
||||
0000000176 00000 n
|
||||
0000000225 00000 n
|
||||
0000000353 00000 n
|
||||
0000000736 00000 n
|
||||
0000000833 00000 n
|
||||
0000000961 00000 n
|
||||
0000001344 00000 n
|
||||
0000001441 00000 n
|
||||
0000001572 00000 n
|
||||
0000001956 00000 n
|
||||
0000002054 00000 n
|
||||
0000002185 00000 n
|
||||
0000002569 00000 n
|
||||
0000002667 00000 n
|
||||
0000002798 00000 n
|
||||
0000003182 00000 n
|
||||
0000003280 00000 n
|
||||
0000003411 00000 n
|
||||
0000003795 00000 n
|
||||
0000003893 00000 n
|
||||
0000004024 00000 n
|
||||
0000004408 00000 n
|
||||
0000004506 00000 n
|
||||
0000004637 00000 n
|
||||
0000005021 00000 n
|
||||
0000005119 00000 n
|
||||
0000005250 00000 n
|
||||
0000005634 00000 n
|
||||
0000005732 00000 n
|
||||
0000005863 00000 n
|
||||
0000006247 00000 n
|
||||
trailer
|
||||
<<
|
||||
/Size 34
|
||||
/Root 3 0 R
|
||||
/Info 1 0 R
|
||||
>>
|
||||
startxref
|
||||
6345
|
||||
%%EOF
|
||||
Binary file not shown.
@@ -1,310 +0,0 @@
|
||||
"""Tests for AdaptivePdfParser and AdaptiveParserMixin."""
|
||||
|
||||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
import requests
|
||||
from pypdf import PdfReader
|
||||
|
||||
from chat.agent_rag.document_converter.parser import (
|
||||
METHOD_OCR,
|
||||
METHOD_TEXT_EXTRACTION,
|
||||
AdaptivePdfParser,
|
||||
analyze_pdf,
|
||||
)
|
||||
|
||||
FIXTURES_DIR = Path(__file__).parent / "fixtures"
|
||||
|
||||
|
||||
@pytest.fixture(name="text_pdf_1_page")
|
||||
def provide_text_pdf_1_page():
|
||||
"""Load a 1 page PDF with extractable text."""
|
||||
return (FIXTURES_DIR / "text_pdf_1_page.pdf").read_bytes()
|
||||
|
||||
|
||||
@pytest.fixture(name="text_pdf_10_pages")
|
||||
def provide_text_pdf_10_pages():
|
||||
"""Load a 10-page PDF with extractable text (~300 chars per page)."""
|
||||
return (FIXTURES_DIR / "text_10_pages.pdf").read_bytes()
|
||||
|
||||
|
||||
@pytest.fixture(name="mixed_pdf_10_pages")
|
||||
def provide_mixed_pdf_10_pages():
|
||||
"""Load a 10-page PDF with 2 pages of text and 8 blank pages."""
|
||||
return (FIXTURES_DIR / "mixed_10_pages.pdf").read_bytes()
|
||||
|
||||
|
||||
MIN_AVG_CHARS_FOR_TEXT_EXTRACTION = 200
|
||||
OCR_RETRY_DELAY = 1
|
||||
OCR_MAX_RETRIES = 3
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def ai_settings(settings):
|
||||
"""Mock Django settings for OCR configuration."""
|
||||
settings.MIN_AVG_CHARS_FOR_TEXT_EXTRACTION = MIN_AVG_CHARS_FOR_TEXT_EXTRACTION
|
||||
settings.MIN_TEXT_COVERAGE_FOR_TEXT_EXTRACTION = 0.7
|
||||
settings.OCR_HRID = "test-ocr-hrid"
|
||||
settings.OCR_MODEL = "test-ocr-model"
|
||||
settings.OCR_TIMEOUT = 60
|
||||
settings.OCR_MAX_RETRIES = OCR_MAX_RETRIES
|
||||
settings.OCR_RETRY_DELAY = OCR_RETRY_DELAY
|
||||
settings.OCR_BATCH_PAGES = 10
|
||||
settings.LLM_CONFIGURATIONS = {
|
||||
"test-ocr-hrid": MagicMock(
|
||||
provider=MagicMock(
|
||||
base_url="https://ocr.example.com",
|
||||
api_key="test-api-key",
|
||||
)
|
||||
)
|
||||
}
|
||||
return settings
|
||||
|
||||
|
||||
def test_analyze_pdf_returns_correct_structure(text_pdf_10_pages):
|
||||
"""analyze_pdf should return dict with expected keys."""
|
||||
result = analyze_pdf(text_pdf_10_pages)
|
||||
|
||||
assert "total_pages" in result
|
||||
assert "pages_with_text" in result
|
||||
assert "avg_chars_per_page" in result
|
||||
assert "text_coverage" in result
|
||||
assert "recommended_method" in result
|
||||
|
||||
|
||||
def test_analyze_pdf_with_text_recommends_extraction(text_pdf_1_page):
|
||||
"""PDF with sufficient text should recommend text extraction."""
|
||||
result = analyze_pdf(text_pdf_1_page)
|
||||
|
||||
assert result["total_pages"] == 1
|
||||
assert result["pages_with_text"] == 1
|
||||
assert result["text_coverage"] == pytest.approx(1.0)
|
||||
assert result["avg_chars_per_page"] > MIN_AVG_CHARS_FOR_TEXT_EXTRACTION
|
||||
assert result["recommended_method"] == METHOD_TEXT_EXTRACTION
|
||||
|
||||
|
||||
def test_analyze_multi_page_pdf_with_text_recommends_extraction(text_pdf_10_pages):
|
||||
"""PDF with sufficient text should recommend text extraction."""
|
||||
result = analyze_pdf(text_pdf_10_pages)
|
||||
|
||||
assert result["total_pages"] == 10
|
||||
assert result["pages_with_text"] == 10
|
||||
assert result["text_coverage"] == pytest.approx(1.0)
|
||||
assert result["avg_chars_per_page"] > MIN_AVG_CHARS_FOR_TEXT_EXTRACTION
|
||||
assert result["recommended_method"] == METHOD_TEXT_EXTRACTION
|
||||
|
||||
|
||||
def test_analyze_pdf_mixed_content_recommends_ocr(mixed_pdf_10_pages):
|
||||
"""PDF with low text coverage should recommend OCR."""
|
||||
result = analyze_pdf(mixed_pdf_10_pages)
|
||||
|
||||
assert result["total_pages"] == 10
|
||||
assert result["pages_with_text"] == 2
|
||||
assert result["text_coverage"] == pytest.approx(0.2)
|
||||
assert result["recommended_method"] == METHOD_OCR
|
||||
|
||||
|
||||
def test_extract_page_batch_single_page(text_pdf_10_pages):
|
||||
"""Should extract a single page correctly."""
|
||||
parser = AdaptivePdfParser()
|
||||
reader = PdfReader(BytesIO(text_pdf_10_pages))
|
||||
|
||||
result = parser.extract_page_batch(reader, 0, 1)
|
||||
|
||||
result_reader = PdfReader(BytesIO(result))
|
||||
assert len(result_reader.pages) == 1
|
||||
|
||||
|
||||
def test_extract_page_batch_multiple_pages(text_pdf_10_pages):
|
||||
"""Should extract multiple pages correctly."""
|
||||
parser = AdaptivePdfParser()
|
||||
reader = PdfReader(BytesIO(text_pdf_10_pages))
|
||||
|
||||
result = parser.extract_page_batch(reader, 2, 7)
|
||||
|
||||
result_reader = PdfReader(BytesIO(result))
|
||||
assert len(result_reader.pages) == 5
|
||||
|
||||
|
||||
def test_extract_page_batch_last_batch(text_pdf_10_pages):
|
||||
"""Should handle last batch with fewer pages."""
|
||||
parser = AdaptivePdfParser()
|
||||
reader = PdfReader(BytesIO(text_pdf_10_pages))
|
||||
|
||||
result = parser.extract_page_batch(reader, 7, 10)
|
||||
|
||||
result_reader = PdfReader(BytesIO(result))
|
||||
assert len(result_reader.pages) == 3
|
||||
|
||||
|
||||
def test_ocr_page_batch_success(text_pdf_1_page):
|
||||
"""Should return markdown content on successful OCR."""
|
||||
parser = AdaptivePdfParser()
|
||||
|
||||
with patch("chat.agent_rag.document_converter.parser.requests.post") as mock_post:
|
||||
mock_post.return_value.json.return_value = {
|
||||
"pages": [
|
||||
{"markdown": "# Page 1 content"},
|
||||
]
|
||||
}
|
||||
mock_post.return_value.raise_for_status = MagicMock()
|
||||
|
||||
result = parser.ocr_page_batch("test.pdf", text_pdf_1_page, 0, 1)
|
||||
|
||||
assert result == ["# Page 1 content"]
|
||||
mock_post.assert_called_once()
|
||||
|
||||
|
||||
def test_ocr_page_batch_retry_on_timeout(text_pdf_1_page):
|
||||
"""Should retry on timeout with static delay."""
|
||||
parser = AdaptivePdfParser()
|
||||
|
||||
with patch("chat.agent_rag.document_converter.parser.requests.post") as mock_post:
|
||||
with patch("chat.agent_rag.document_converter.parser.time.sleep") as mock_sleep:
|
||||
mock_post.side_effect = [
|
||||
requests.Timeout("Connection timed out"),
|
||||
MagicMock(
|
||||
json=MagicMock(return_value={"pages": [{"markdown": "# Content"}]}),
|
||||
raise_for_status=MagicMock(),
|
||||
),
|
||||
]
|
||||
|
||||
result = parser.ocr_page_batch("test.pdf", text_pdf_1_page, 0, 1)
|
||||
|
||||
assert result == ["# Content"]
|
||||
assert mock_post.call_count == 2
|
||||
mock_sleep.assert_called_once_with(OCR_RETRY_DELAY)
|
||||
|
||||
|
||||
def test_ocr_page_batch_fails_after_max_retries(text_pdf_1_page):
|
||||
"""Should raise exception after max retries exceeded."""
|
||||
parser = AdaptivePdfParser()
|
||||
|
||||
with patch("chat.agent_rag.document_converter.parser.requests.post") as mock_post:
|
||||
with patch("chat.agent_rag.document_converter.parser.time.sleep"):
|
||||
mock_post.side_effect = requests.Timeout("Connection timed out")
|
||||
|
||||
with pytest.raises(requests.Timeout):
|
||||
parser.ocr_page_batch("test.pdf", text_pdf_1_page, 0, 1)
|
||||
|
||||
assert mock_post.call_count == OCR_MAX_RETRIES
|
||||
|
||||
|
||||
def test_ocr_page_batch_retry_on_request_exception(text_pdf_1_page):
|
||||
"""Should retry on general request exceptions."""
|
||||
parser = AdaptivePdfParser()
|
||||
|
||||
with patch("chat.agent_rag.document_converter.parser.requests.post") as mock_post:
|
||||
with patch("chat.agent_rag.document_converter.parser.time.sleep"):
|
||||
mock_post.side_effect = [
|
||||
requests.RequestException("Network error"),
|
||||
requests.RequestException("Network error"),
|
||||
MagicMock(
|
||||
json=MagicMock(return_value={"pages": [{"markdown": "# Content"}]}),
|
||||
raise_for_status=MagicMock(),
|
||||
),
|
||||
]
|
||||
|
||||
result = parser.ocr_page_batch("test.pdf", text_pdf_1_page, 0, 1)
|
||||
|
||||
assert result == ["# Content"]
|
||||
assert mock_post.call_count == 3
|
||||
|
||||
|
||||
def test_parse_pdf_with_ocr_single_batch(text_pdf_10_pages):
|
||||
"""Should process PDF in single batch when pages <= batch size."""
|
||||
parser = AdaptivePdfParser()
|
||||
|
||||
with patch("chat.agent_rag.document_converter.parser.requests.post") as mock_post:
|
||||
mock_post.return_value.json.return_value = {
|
||||
"pages": [{"markdown": f"Page {i}"} for i in range(1, 11)]
|
||||
}
|
||||
mock_post.return_value.raise_for_status = MagicMock()
|
||||
|
||||
result = parser.parse_pdf_document_with_ocr("test.pdf", text_pdf_10_pages)
|
||||
|
||||
assert "Page 1" in result
|
||||
assert "Page 10" in result
|
||||
mock_post.assert_called_once()
|
||||
|
||||
|
||||
def test_parse_pdf_with_ocr_multiple_batches(text_pdf_10_pages, settings):
|
||||
"""Should process PDF in multiple batches when pages > batch size."""
|
||||
settings.OCR_BATCH_PAGES = 4 # Force multiple batches
|
||||
parser = AdaptivePdfParser()
|
||||
|
||||
with patch("chat.agent_rag.document_converter.parser.requests.post") as mock_post:
|
||||
mock_post.return_value.json.side_effect = [
|
||||
{"pages": [{"markdown": f"Page {i}"} for i in range(1, 5)]},
|
||||
{"pages": [{"markdown": f"Page {i}"} for i in range(5, 9)]},
|
||||
{"pages": [{"markdown": f"Page {i}"} for i in range(9, 11)]},
|
||||
]
|
||||
mock_post.return_value.raise_for_status = MagicMock()
|
||||
|
||||
result = parser.parse_pdf_document_with_ocr("test.pdf", text_pdf_10_pages)
|
||||
|
||||
assert mock_post.call_count == 3
|
||||
assert "Page 1" in result
|
||||
assert "Page 10" in result
|
||||
|
||||
|
||||
def test_parse_pdf_with_ocr_partial_failure(text_pdf_10_pages, settings):
|
||||
"""Should insert empty placeholders for failed batches."""
|
||||
settings.OCR_BATCH_PAGES = 4 # Force multiple batches
|
||||
parser = AdaptivePdfParser()
|
||||
|
||||
success_response = MagicMock()
|
||||
success_response.json.return_value = {"pages": [{"markdown": f"Page {i}"} for i in range(1, 5)]}
|
||||
success_response.raise_for_status = MagicMock()
|
||||
|
||||
with patch("chat.agent_rag.document_converter.parser.requests.post") as mock_post:
|
||||
with patch("chat.agent_rag.document_converter.parser.time.sleep"):
|
||||
# First batch succeeds, then all retries fail for remaining batches
|
||||
mock_post.side_effect = [
|
||||
success_response,
|
||||
requests.Timeout("OCR failed"),
|
||||
requests.Timeout("OCR failed"),
|
||||
requests.Timeout("OCR failed"),
|
||||
requests.Timeout("OCR failed"),
|
||||
requests.Timeout("OCR failed"),
|
||||
requests.Timeout("OCR failed"),
|
||||
]
|
||||
|
||||
result = parser.parse_pdf_document_with_ocr("test.pdf", text_pdf_10_pages)
|
||||
|
||||
parts = result.split("\n\n")
|
||||
# First batch succeeded (4 pages), remaining batches failed (6 pages as placeholders)
|
||||
assert len(parts) == 10
|
||||
assert parts[0] == "Page 1"
|
||||
assert parts[3] == "Page 4"
|
||||
assert parts[4] == "" # Failed batch placeholder
|
||||
|
||||
|
||||
def test_parse_document_pdf_routed_correctly(text_pdf_1_page):
|
||||
"""Should route PDF content type to PDF parser."""
|
||||
parser = AdaptivePdfParser()
|
||||
|
||||
with patch.object(parser, "parse_pdf_document", return_value="pdf content") as mock_parse:
|
||||
result = parser.parse_document("test.pdf", "application/pdf", text_pdf_1_page)
|
||||
|
||||
assert result == "pdf content"
|
||||
mock_parse.assert_called_once_with(
|
||||
name="test.pdf",
|
||||
content_type="application/pdf",
|
||||
content=text_pdf_1_page,
|
||||
)
|
||||
|
||||
|
||||
def test_parse_document_non_pdf_uses_document_converter():
|
||||
"""Should route non-PDF content to DocumentConverter."""
|
||||
parser = AdaptivePdfParser()
|
||||
|
||||
with patch("chat.agent_rag.document_converter.parser.DocumentConverter") as mock_converter:
|
||||
mock_converter.return_value.convert_raw.return_value = "docx content"
|
||||
|
||||
result = parser.parse_document("test.docx", "application/vnd.openxmlformats", b"content")
|
||||
|
||||
assert result == "docx content"
|
||||
mock_converter.return_value.convert_raw.assert_called_once()
|
||||
@@ -15,13 +15,12 @@ import core.urls
|
||||
import chat.views
|
||||
import conversations.urls
|
||||
from chat.agents.summarize import SummarizationAgent
|
||||
from chat.agents.translate import TranslationAgent
|
||||
from chat.clients.pydantic_ai import AIAgentService
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@pytest.fixture(name="today_prompt_date")
|
||||
@pytest.fixture(name="today_promt_date")
|
||||
def today_prompt_date_fixture():
|
||||
"""Fixture to mock date the system prompt when useless to test it."""
|
||||
_formatted_date = formats.date_format(timezone.now(), "l d/m/Y", use_l10n=False)
|
||||
@@ -92,34 +91,6 @@ def mock_summarization_agent_fixture():
|
||||
yield _mock_agent
|
||||
|
||||
|
||||
@pytest.fixture(name="mock_translation_agent")
|
||||
def mock_translation_agent_fixture():
|
||||
"""Fixture to mock TranslationAgent with a custom model."""
|
||||
|
||||
@contextmanager
|
||||
def _mock_agent(model):
|
||||
"""Context manager to mock TranslationAgent with a custom model."""
|
||||
with ExitStack() as stack:
|
||||
|
||||
class TranslationAgentMock(TranslationAgent):
|
||||
"""Mocked TranslationAgent to override the model."""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
logger.info("Overriding TranslationAgent model with %s", model)
|
||||
self._model = model # pylint: disable=protected-access
|
||||
|
||||
stack.enter_context(
|
||||
patch("chat.agents.translate.TranslationAgent", new=TranslationAgentMock)
|
||||
)
|
||||
stack.enter_context(
|
||||
patch("chat.tools.document_translate.TranslationAgent", new=TranslationAgentMock)
|
||||
)
|
||||
yield
|
||||
|
||||
yield _mock_agent
|
||||
|
||||
|
||||
PIXEL_PNG = (
|
||||
b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01\x08\x06\x00"
|
||||
b"\x00\x00\x1f\x15\xc4\x89\x00\x00\x00\nIDATx\x9cc\xf8\xff\xff?\x00\x05\xfe\x02\xfe"
|
||||
|
||||
@@ -1,318 +0,0 @@
|
||||
"""Tests for document_translate functionality."""
|
||||
|
||||
import io
|
||||
from unittest import mock
|
||||
|
||||
from django.core.files.storage import default_storage
|
||||
|
||||
import pytest
|
||||
from pydantic_ai import ModelResponse, RunContext, TextPart
|
||||
from pydantic_ai.exceptions import ModelRetry
|
||||
from pydantic_ai.models.function import FunctionModel
|
||||
from pydantic_ai.usage import RunUsage
|
||||
|
||||
from chat.llm_configuration import LLModel, LLMProvider
|
||||
from chat.tools.document_translate import document_translate
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def fixture_translation_agent_config(settings):
|
||||
"""Fixture to set used settings for agent configuration."""
|
||||
settings.TRANSLATION_MAX_CHARS = 100_000
|
||||
settings.LLM_CONFIGURATIONS = {
|
||||
settings.LLM_DEFAULT_MODEL_HRID: LLModel(
|
||||
hrid="mistral-model",
|
||||
model_name="mistral-medium-2508",
|
||||
human_readable_name="Mistral Medium 2508",
|
||||
profile=None,
|
||||
provider=LLMProvider(
|
||||
hrid="mistral-medium-2508",
|
||||
kind="mistral",
|
||||
base_url="https://api.mistral.ai/v1",
|
||||
api_key="testkey",
|
||||
),
|
||||
is_active=True,
|
||||
system_prompt="direct",
|
||||
tools=[],
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture(name="mocked_context")
|
||||
def fixture_mocked_context():
|
||||
"""Fixture for a mocked RunContext."""
|
||||
mock_ctx = mock.Mock(spec=RunContext)
|
||||
mock_ctx.usage = RunUsage(input_tokens=0, output_tokens=0)
|
||||
mock_ctx.max_retries = 2
|
||||
mock_ctx.retries = {}
|
||||
return mock_ctx
|
||||
|
||||
|
||||
def _mock_attachments_queryset(attachment):
|
||||
"""Create a mock queryset that chains .filter().order_by().afirst() returning the attachment."""
|
||||
mock_qs = mock.Mock()
|
||||
mock_qs.order_by.return_value = mock_qs
|
||||
mock_qs.afirst = mock.AsyncMock(return_value=attachment)
|
||||
|
||||
mock_attachments = mock.Mock()
|
||||
mock_attachments.filter.return_value = mock_qs
|
||||
return mock_attachments
|
||||
|
||||
|
||||
def mocked_translation(_messages, _info=None):
|
||||
"""Mocked translation response."""
|
||||
return ModelResponse(parts=[TextPart(content="Ceci est une traduction du document.")])
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_document_translate_single_document(mocked_context, mock_translation_agent):
|
||||
"""Test document_translate with a single document."""
|
||||
mock_attachment = mock.Mock()
|
||||
mock_attachment.key = "test_doc.txt"
|
||||
mock_attachment.file_name = "test_doc.txt"
|
||||
mock_attachment.content_type = "text/plain"
|
||||
mock_attachment.size = None
|
||||
|
||||
mock_conversation = mock.Mock()
|
||||
mock_conversation.attachments = _mock_attachments_queryset(mock_attachment)
|
||||
|
||||
file_content = "This is a test document. " * 20
|
||||
|
||||
with mock.patch.object(
|
||||
default_storage, "open", return_value=io.BytesIO(file_content.encode("utf-8"))
|
||||
):
|
||||
mocked_context.deps = mock.Mock()
|
||||
mocked_context.deps.conversation = mock_conversation
|
||||
|
||||
def mocked_translate_full(_message, _info=None):
|
||||
"""Mocked translation for full flow."""
|
||||
return ModelResponse(parts=[TextPart(content="Ceci est un document de test.")])
|
||||
|
||||
with mock_translation_agent(FunctionModel(mocked_translate_full)):
|
||||
result = await document_translate(
|
||||
mocked_context, target_language="French", instructions=None
|
||||
)
|
||||
|
||||
assert "Ceci est un document de test." in result.return_value
|
||||
assert result.metadata["sources"] == {"test_doc.txt"}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_document_translate_uses_last_document(mocked_context, mock_translation_agent):
|
||||
"""Test document_translate uses the last uploaded document."""
|
||||
mock_attachment = mock.Mock()
|
||||
mock_attachment.key = "latest_doc.txt"
|
||||
mock_attachment.file_name = "latest_doc.txt"
|
||||
mock_attachment.content_type = "text/plain"
|
||||
mock_attachment.size = None
|
||||
|
||||
mock_conversation = mock.Mock()
|
||||
mock_conversation.attachments = _mock_attachments_queryset(mock_attachment)
|
||||
|
||||
file_content = "Content of the latest document."
|
||||
|
||||
with mock.patch.object(
|
||||
default_storage, "open", return_value=io.BytesIO(file_content.encode("utf-8"))
|
||||
):
|
||||
mocked_context.deps = mock.Mock()
|
||||
mocked_context.deps.conversation = mock_conversation
|
||||
|
||||
with mock_translation_agent(FunctionModel(mocked_translation)):
|
||||
result = await document_translate(
|
||||
mocked_context, target_language="French", instructions=None
|
||||
)
|
||||
|
||||
assert result.metadata["sources"] == {"latest_doc.txt"}
|
||||
# Verify order_by was called with -created_at
|
||||
mock_conversation.attachments.filter.return_value.order_by.assert_called_once_with(
|
||||
"-created_at"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_document_translate_with_custom_instructions(mocked_context, mock_translation_agent):
|
||||
"""Test document_translate with custom instructions."""
|
||||
mock_attachment = mock.Mock()
|
||||
mock_attachment.key = "test.txt"
|
||||
mock_attachment.file_name = "test.txt"
|
||||
mock_attachment.content_type = "text/plain"
|
||||
mock_attachment.size = None
|
||||
|
||||
mock_conversation = mock.Mock()
|
||||
mock_conversation.attachments = _mock_attachments_queryset(mock_attachment)
|
||||
|
||||
file_content = "Test content " * 20
|
||||
|
||||
with mock.patch.object(
|
||||
default_storage, "open", return_value=io.BytesIO(file_content.encode("utf-8"))
|
||||
):
|
||||
mocked_context.deps = mock.Mock()
|
||||
mocked_context.deps.conversation = mock_conversation
|
||||
|
||||
captured_prompts = []
|
||||
|
||||
def mocked_translate_with_instructions(messages, _info=None):
|
||||
"""Mocked translation that captures prompt."""
|
||||
messages_text = messages[0].parts[-1].content
|
||||
captured_prompts.append(messages_text)
|
||||
return ModelResponse(parts=[TextPart(content="Traduction formelle")])
|
||||
|
||||
with mock_translation_agent(FunctionModel(mocked_translate_with_instructions)):
|
||||
result = await document_translate(
|
||||
mocked_context, target_language="French", instructions="Use formal tone"
|
||||
)
|
||||
|
||||
assert result.return_value is not None
|
||||
assert len(captured_prompts) == 1
|
||||
assert "Use formal tone" in captured_prompts[0]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("target_language", [None, ""])
|
||||
async def test_document_translate_no_target_language(
|
||||
target_language, mocked_context, mock_translation_agent
|
||||
):
|
||||
"""Test document_translate asks the user for language when target_language is not specified."""
|
||||
mocked_context.deps = mock.Mock()
|
||||
|
||||
with mock_translation_agent(FunctionModel(mocked_translation)):
|
||||
result = await document_translate(
|
||||
mocked_context, target_language=target_language, instructions=None
|
||||
)
|
||||
|
||||
assert "target language is not specified" in result
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_document_translate_no_text_attachments(mocked_context, mock_translation_agent):
|
||||
"""Test document_translate returns error message when no text documents found."""
|
||||
mock_conversation = mock.Mock()
|
||||
mock_conversation.attachments = _mock_attachments_queryset(None)
|
||||
|
||||
mocked_context.deps = mock.Mock()
|
||||
mocked_context.deps.conversation = mock_conversation
|
||||
|
||||
with mock_translation_agent(FunctionModel(mocked_translation)):
|
||||
result = await document_translate(
|
||||
mocked_context, target_language="French", instructions=None
|
||||
)
|
||||
|
||||
assert "No text documents found in the conversation" in result
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_document_translate_error_reading_document(mocked_context, mock_translation_agent):
|
||||
"""Test document_translate handles errors when reading documents."""
|
||||
mock_attachment = mock.Mock()
|
||||
mock_attachment.key = "test.txt"
|
||||
mock_attachment.file_name = "test.txt"
|
||||
mock_attachment.content_type = "text/plain"
|
||||
mock_attachment.size = None
|
||||
|
||||
mock_conversation = mock.Mock()
|
||||
mock_conversation.attachments = _mock_attachments_queryset(mock_attachment)
|
||||
|
||||
with mock.patch.object(default_storage, "open", side_effect=IOError("File read error")):
|
||||
mocked_context.deps = mock.Mock()
|
||||
mocked_context.deps.conversation = mock_conversation
|
||||
|
||||
with mock_translation_agent(FunctionModel(mocked_translation)):
|
||||
result = await document_translate(
|
||||
mocked_context, target_language="French", instructions=None
|
||||
)
|
||||
|
||||
assert "An unexpected error occurred during document translation" in result
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_document_translate_error_during_translation(mocked_context, mock_translation_agent):
|
||||
"""Test document_translate handles ModelRetry during translation."""
|
||||
mock_attachment = mock.Mock()
|
||||
mock_attachment.key = "test.txt"
|
||||
mock_attachment.file_name = "test.txt"
|
||||
mock_attachment.content_type = "text/plain"
|
||||
mock_attachment.size = None
|
||||
|
||||
mock_conversation = mock.Mock()
|
||||
mock_conversation.attachments = _mock_attachments_queryset(mock_attachment)
|
||||
|
||||
file_content = "Test content " * 20
|
||||
|
||||
with mock.patch.object(
|
||||
default_storage, "open", return_value=io.BytesIO(file_content.encode("utf-8"))
|
||||
):
|
||||
mocked_context.deps = mock.Mock()
|
||||
mocked_context.deps.conversation = mock_conversation
|
||||
|
||||
def mocked_translate_error(_messages, _info=None):
|
||||
"""Mocked translation that raises an error."""
|
||||
raise ValueError("Translation error")
|
||||
|
||||
with mock_translation_agent(FunctionModel(mocked_translate_error)):
|
||||
with pytest.raises(ModelRetry):
|
||||
await document_translate(
|
||||
mocked_context, target_language="French", instructions=None
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_document_translate_too_large(settings, mocked_context, mock_translation_agent):
|
||||
"""Test document_translate rejects documents exceeding max chars."""
|
||||
settings.TRANSLATION_MAX_CHARS = 100 # Very small limit
|
||||
|
||||
mock_attachment = mock.Mock()
|
||||
mock_attachment.key = "large_doc.txt"
|
||||
mock_attachment.file_name = "large_doc.txt"
|
||||
mock_attachment.content_type = "text/plain"
|
||||
mock_attachment.size = None
|
||||
|
||||
mock_conversation = mock.Mock()
|
||||
mock_conversation.attachments = _mock_attachments_queryset(mock_attachment)
|
||||
|
||||
file_content = "This is a word. " * 100 # Much larger than 100 chars
|
||||
|
||||
with mock.patch.object(
|
||||
default_storage, "open", return_value=io.BytesIO(file_content.encode("utf-8"))
|
||||
):
|
||||
mocked_context.deps = mock.Mock()
|
||||
mocked_context.deps.conversation = mock_conversation
|
||||
|
||||
with mock_translation_agent(FunctionModel(mocked_translation)):
|
||||
result = await document_translate(
|
||||
mocked_context, target_language="French", instructions=None
|
||||
)
|
||||
|
||||
assert "too large to translate" in result
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_document_translate_empty_result(mocked_context, mock_translation_agent):
|
||||
"""Test document_translate raises ModelRetry when translation produces empty result."""
|
||||
mock_attachment = mock.Mock()
|
||||
mock_attachment.key = "test.txt"
|
||||
mock_attachment.file_name = "test.txt"
|
||||
mock_attachment.content_type = "text/plain"
|
||||
mock_attachment.size = None
|
||||
|
||||
mock_conversation = mock.Mock()
|
||||
mock_conversation.attachments = _mock_attachments_queryset(mock_attachment)
|
||||
|
||||
file_content = "Test content " * 20
|
||||
|
||||
with mock.patch.object(
|
||||
default_storage, "open", return_value=io.BytesIO(file_content.encode("utf-8"))
|
||||
):
|
||||
mocked_context.deps = mock.Mock()
|
||||
mocked_context.deps.conversation = mock_conversation
|
||||
|
||||
def mocked_empty_translation(_messages, _info=None):
|
||||
"""Mocked translation that returns empty."""
|
||||
return ModelResponse(parts=[TextPart(content=" ")])
|
||||
|
||||
with mock_translation_agent(FunctionModel(mocked_empty_translation)):
|
||||
with pytest.raises(ModelRetry) as exc_info:
|
||||
await document_translate(
|
||||
mocked_context, target_language="French", instructions=None
|
||||
)
|
||||
|
||||
assert "produced an empty result" in str(exc_info.value)
|
||||
@@ -190,7 +190,6 @@ def test_post_conversation_data_protocol(api_client, mock_openai_stream):
|
||||
"Today is Friday 25/07/2025.\n\nAnswer in english."
|
||||
),
|
||||
"kind": "request",
|
||||
"metadata": None,
|
||||
"parts": [
|
||||
{
|
||||
"content": ["Hello"],
|
||||
@@ -199,29 +198,15 @@ def test_post_conversation_data_protocol(api_client, mock_openai_stream):
|
||||
},
|
||||
],
|
||||
"run_id": _run_id,
|
||||
"timestamp": "2025-07-25T10:36:35.297675Z",
|
||||
},
|
||||
{
|
||||
"finish_reason": "stop",
|
||||
"kind": "response",
|
||||
"metadata": None,
|
||||
"model_name": "test-model",
|
||||
"parts": [
|
||||
{
|
||||
"content": "Hello there",
|
||||
"id": None,
|
||||
"part_kind": "text",
|
||||
"provider_details": None,
|
||||
"provider_name": None,
|
||||
}
|
||||
],
|
||||
"provider_details": {
|
||||
"finish_reason": "stop",
|
||||
"timestamp": "2025-07-25T10:36:35.297675Z",
|
||||
},
|
||||
"parts": [{"content": "Hello there", "id": None, "part_kind": "text"}],
|
||||
"provider_details": {"finish_reason": "stop"},
|
||||
"provider_name": "openai",
|
||||
"provider_response_id": "chatcmpl-1234567890",
|
||||
"provider_url": "https://www.external-ai-service.com/",
|
||||
"timestamp": "2025-07-25T10:36:35.297675Z",
|
||||
"usage": {
|
||||
"cache_audio_read_tokens": 0,
|
||||
@@ -332,7 +317,6 @@ def test_post_conversation_data_protocol_triggers_keepalives(
|
||||
"Answer in english."
|
||||
),
|
||||
"kind": "request",
|
||||
"metadata": None,
|
||||
"parts": [
|
||||
{
|
||||
"content": ["Hello"],
|
||||
@@ -341,29 +325,15 @@ def test_post_conversation_data_protocol_triggers_keepalives(
|
||||
},
|
||||
],
|
||||
"run_id": _run_id,
|
||||
"timestamp": "2025-07-25T10:36:35.297675Z",
|
||||
},
|
||||
{
|
||||
"finish_reason": "stop",
|
||||
"kind": "response",
|
||||
"metadata": None,
|
||||
"model_name": "test-model",
|
||||
"parts": [
|
||||
{
|
||||
"content": "Hello there",
|
||||
"id": None,
|
||||
"part_kind": "text",
|
||||
"provider_details": None,
|
||||
"provider_name": None,
|
||||
}
|
||||
],
|
||||
"provider_details": {
|
||||
"finish_reason": "stop",
|
||||
"timestamp": "2025-07-25T10:36:35.297675Z",
|
||||
},
|
||||
"parts": [{"content": "Hello there", "id": None, "part_kind": "text"}],
|
||||
"provider_details": {"finish_reason": "stop"},
|
||||
"provider_name": "openai",
|
||||
"provider_response_id": "chatcmpl-1234567890",
|
||||
"provider_url": "https://www.external-ai-service.com/",
|
||||
"timestamp": "2025-07-25T10:36:35.297675Z",
|
||||
"usage": {
|
||||
"cache_audio_read_tokens": 0,
|
||||
@@ -468,7 +438,6 @@ def test_post_conversation_text_protocol(api_client, mock_openai_stream):
|
||||
"Today is Friday 25/07/2025.\n\nAnswer in english."
|
||||
),
|
||||
"kind": "request",
|
||||
"metadata": None,
|
||||
"parts": [
|
||||
{
|
||||
"content": ["Hello"],
|
||||
@@ -477,29 +446,15 @@ def test_post_conversation_text_protocol(api_client, mock_openai_stream):
|
||||
},
|
||||
],
|
||||
"run_id": _run_id,
|
||||
"timestamp": "2025-07-25T10:36:35.297675Z",
|
||||
},
|
||||
{
|
||||
"finish_reason": "stop",
|
||||
"kind": "response",
|
||||
"metadata": None,
|
||||
"model_name": "test-model",
|
||||
"parts": [
|
||||
{
|
||||
"content": "Hello there",
|
||||
"id": None,
|
||||
"part_kind": "text",
|
||||
"provider_details": None,
|
||||
"provider_name": None,
|
||||
}
|
||||
],
|
||||
"provider_details": {
|
||||
"finish_reason": "stop",
|
||||
"timestamp": "2025-07-25T10:36:35.297675Z",
|
||||
},
|
||||
"parts": [{"content": "Hello there", "id": None, "part_kind": "text"}],
|
||||
"provider_details": {"finish_reason": "stop"},
|
||||
"provider_name": "openai",
|
||||
"provider_response_id": "chatcmpl-1234567890",
|
||||
"provider_url": "https://www.external-ai-service.com/",
|
||||
"timestamp": "2025-07-25T10:36:35.297675Z",
|
||||
"usage": {
|
||||
"cache_audio_read_tokens": 0,
|
||||
@@ -669,7 +624,6 @@ def test_post_conversation_with_image(api_client, mock_openai_stream_image):
|
||||
"Today is Friday 25/07/2025.\n\nAnswer in english."
|
||||
),
|
||||
"kind": "request",
|
||||
"metadata": None,
|
||||
"parts": [
|
||||
{
|
||||
"content": [
|
||||
@@ -690,29 +644,15 @@ def test_post_conversation_with_image(api_client, mock_openai_stream_image):
|
||||
},
|
||||
],
|
||||
"run_id": _run_id,
|
||||
"timestamp": "2025-07-25T10:36:35.297675Z",
|
||||
},
|
||||
{
|
||||
"finish_reason": "stop",
|
||||
"kind": "response",
|
||||
"metadata": None,
|
||||
"model_name": "test-model",
|
||||
"parts": [
|
||||
{
|
||||
"content": "I see a cat in the picture.",
|
||||
"id": None,
|
||||
"part_kind": "text",
|
||||
"provider_details": None,
|
||||
"provider_name": None,
|
||||
}
|
||||
],
|
||||
"provider_details": {
|
||||
"finish_reason": "stop",
|
||||
"timestamp": "2025-07-25T10:36:35.297675Z",
|
||||
},
|
||||
"parts": [{"content": "I see a cat in the picture.", "id": None, "part_kind": "text"}],
|
||||
"provider_details": {"finish_reason": "stop"},
|
||||
"provider_name": "openai",
|
||||
"provider_response_id": "chatcmpl-1234567890",
|
||||
"provider_url": "https://www.external-ai-service.com/",
|
||||
"timestamp": "2025-07-25T10:36:35.297675Z",
|
||||
"usage": {
|
||||
"cache_audio_read_tokens": 0,
|
||||
@@ -850,7 +790,6 @@ def test_post_conversation_tool_call(api_client, mock_openai_stream_tool, settin
|
||||
"Today is Friday 25/07/2025.\n\nAnswer in english."
|
||||
),
|
||||
"kind": "request",
|
||||
"metadata": None,
|
||||
"parts": [
|
||||
{
|
||||
"content": ["Weather in Paris?"],
|
||||
@@ -859,31 +798,23 @@ def test_post_conversation_tool_call(api_client, mock_openai_stream_tool, settin
|
||||
},
|
||||
],
|
||||
"run_id": _run_id,
|
||||
"timestamp": "2025-07-25T10:36:35.297675Z",
|
||||
},
|
||||
{
|
||||
"finish_reason": "tool_call",
|
||||
"kind": "response",
|
||||
"metadata": None,
|
||||
"model_name": "test-model",
|
||||
"parts": [
|
||||
{
|
||||
"args": '{"location":"Paris", "unit":"celsius"}',
|
||||
"id": None,
|
||||
"part_kind": "tool-call",
|
||||
"provider_details": None,
|
||||
"provider_name": None,
|
||||
"tool_call_id": "xLDcIljdsDrz0idal7tATWSMm2jhMj47",
|
||||
"tool_name": "get_current_weather",
|
||||
}
|
||||
],
|
||||
"provider_details": {
|
||||
"finish_reason": "tool_calls",
|
||||
"timestamp": "2025-07-25T10:36:35.297675Z",
|
||||
},
|
||||
"provider_details": {"finish_reason": "tool_calls"},
|
||||
"provider_name": "openai",
|
||||
"provider_response_id": "chatcmpl-tool-call",
|
||||
"provider_url": "https://www.external-ai-service.com/",
|
||||
"timestamp": "2025-07-25T10:36:35.297675Z",
|
||||
"usage": {
|
||||
"cache_audio_read_tokens": 0,
|
||||
@@ -903,7 +834,6 @@ def test_post_conversation_tool_call(api_client, mock_openai_stream_tool, settin
|
||||
"Today is Friday 25/07/2025.\n\nAnswer in english."
|
||||
),
|
||||
"kind": "request",
|
||||
"metadata": None,
|
||||
"parts": [
|
||||
{
|
||||
"content": {"location": "Paris", "temperature": 22, "unit": "celsius"},
|
||||
@@ -915,29 +845,17 @@ def test_post_conversation_tool_call(api_client, mock_openai_stream_tool, settin
|
||||
}
|
||||
],
|
||||
"run_id": _run_id,
|
||||
"timestamp": "2025-07-25T10:36:35.297675Z",
|
||||
},
|
||||
{
|
||||
"finish_reason": "stop",
|
||||
"kind": "response",
|
||||
"metadata": None,
|
||||
"model_name": "test-model",
|
||||
"parts": [
|
||||
{
|
||||
"content": "The current weather in Paris is nice",
|
||||
"id": None,
|
||||
"part_kind": "text",
|
||||
"provider_details": None,
|
||||
"provider_name": None,
|
||||
}
|
||||
{"content": "The current weather in Paris is nice", "id": None, "part_kind": "text"}
|
||||
],
|
||||
"provider_details": {
|
||||
"finish_reason": "stop",
|
||||
"timestamp": "2025-07-25T10:36:35.297675Z",
|
||||
},
|
||||
"provider_details": {"finish_reason": "stop"},
|
||||
"provider_name": "openai",
|
||||
"provider_response_id": "chatcmpl-final",
|
||||
"provider_url": "https://www.external-ai-service.com/",
|
||||
"timestamp": "2025-07-25T10:36:35.297675Z",
|
||||
"usage": {
|
||||
"cache_audio_read_tokens": 0,
|
||||
@@ -1074,7 +992,6 @@ def test_post_conversation_tool_call_fails(api_client, mock_openai_stream_tool,
|
||||
"Today is Friday 25/07/2025.\n\nAnswer in french."
|
||||
),
|
||||
"kind": "request",
|
||||
"metadata": None,
|
||||
"parts": [
|
||||
{
|
||||
"content": ["Weather in Paris?"],
|
||||
@@ -1083,31 +1000,23 @@ def test_post_conversation_tool_call_fails(api_client, mock_openai_stream_tool,
|
||||
},
|
||||
],
|
||||
"run_id": _run_id,
|
||||
"timestamp": "2025-07-25T10:36:35.297675Z",
|
||||
},
|
||||
{
|
||||
"finish_reason": "tool_call",
|
||||
"kind": "response",
|
||||
"metadata": None,
|
||||
"model_name": "test-model",
|
||||
"parts": [
|
||||
{
|
||||
"args": '{"location":"Paris", "unit":"celsius"}',
|
||||
"id": None,
|
||||
"part_kind": "tool-call",
|
||||
"provider_details": None,
|
||||
"provider_name": None,
|
||||
"tool_call_id": "xLDcIljdsDrz0idal7tATWSMm2jhMj47",
|
||||
"tool_name": "get_current_weather",
|
||||
}
|
||||
],
|
||||
"provider_details": {
|
||||
"finish_reason": "tool_calls",
|
||||
"timestamp": "2025-07-25T10:36:35.297675Z",
|
||||
},
|
||||
"provider_details": {"finish_reason": "tool_calls"},
|
||||
"provider_name": "openai",
|
||||
"provider_response_id": "chatcmpl-tool-call",
|
||||
"provider_url": "https://www.external-ai-service.com/",
|
||||
"timestamp": "2025-07-25T10:36:35.297675Z",
|
||||
"usage": {
|
||||
"cache_audio_read_tokens": 0,
|
||||
@@ -1127,7 +1036,6 @@ def test_post_conversation_tool_call_fails(api_client, mock_openai_stream_tool,
|
||||
"Today is Friday 25/07/2025.\n\nAnswer in french."
|
||||
),
|
||||
"kind": "request",
|
||||
"metadata": None,
|
||||
"parts": [
|
||||
{
|
||||
"content": "Unknown tool name: 'get_current_weather'. No tools available.",
|
||||
@@ -1138,29 +1046,17 @@ def test_post_conversation_tool_call_fails(api_client, mock_openai_stream_tool,
|
||||
}
|
||||
],
|
||||
"run_id": _run_id,
|
||||
"timestamp": "2025-07-25T10:36:35.297675Z",
|
||||
},
|
||||
{
|
||||
"finish_reason": "stop",
|
||||
"kind": "response",
|
||||
"metadata": None,
|
||||
"model_name": "test-model",
|
||||
"parts": [
|
||||
{
|
||||
"content": "I cannot give you an answer to that.",
|
||||
"id": None,
|
||||
"part_kind": "text",
|
||||
"provider_details": None,
|
||||
"provider_name": None,
|
||||
}
|
||||
{"content": "I cannot give you an answer to that.", "id": None, "part_kind": "text"}
|
||||
],
|
||||
"provider_details": {
|
||||
"finish_reason": "stop",
|
||||
"timestamp": "2025-07-25T10:36:35.297675Z",
|
||||
},
|
||||
"provider_details": {"finish_reason": "stop"},
|
||||
"provider_name": "openai",
|
||||
"provider_response_id": "chatcmpl-final",
|
||||
"provider_url": "https://www.external-ai-service.com/",
|
||||
"timestamp": "2025-07-25T10:36:35.297675Z",
|
||||
"usage": {
|
||||
"cache_audio_read_tokens": 0,
|
||||
@@ -1406,7 +1302,6 @@ def test_post_conversation_data_protocol_no_stream(
|
||||
"You are an amazing assistant.\n\nToday is Friday 25/07/2025.\n\nAnswer in english."
|
||||
),
|
||||
"kind": "request",
|
||||
"metadata": None,
|
||||
"parts": [
|
||||
{
|
||||
"content": ["Why the sky is blue?"],
|
||||
@@ -1415,12 +1310,10 @@ def test_post_conversation_data_protocol_no_stream(
|
||||
},
|
||||
],
|
||||
"run_id": _run_id,
|
||||
"timestamp": "2025-07-25T10:36:35.297675Z",
|
||||
},
|
||||
{
|
||||
"finish_reason": "stop",
|
||||
"kind": "response",
|
||||
"metadata": None,
|
||||
"model_name": "mistralai/Mistral-Small-3.2-24B-Instruct-2506",
|
||||
"parts": [
|
||||
{
|
||||
@@ -1428,18 +1321,12 @@ def test_post_conversation_data_protocol_no_stream(
|
||||
"Rayleigh scattering.",
|
||||
"id": None,
|
||||
"part_kind": "text",
|
||||
"provider_details": None,
|
||||
"provider_name": None,
|
||||
}
|
||||
],
|
||||
"provider_details": {
|
||||
"finish_reason": "stop",
|
||||
"timestamp": "2025-09-22T14:13:49Z",
|
||||
},
|
||||
"provider_details": {"finish_reason": "stop"},
|
||||
"provider_name": "openai",
|
||||
"provider_response_id": "chatcmpl-92c413bb5a45426299335d0621324654",
|
||||
"provider_url": "https://www.external-ai-service.com",
|
||||
"timestamp": "2025-07-25T10:36:35.297675Z",
|
||||
"timestamp": "2025-09-22T14:13:49Z",
|
||||
"usage": {
|
||||
"cache_audio_read_tokens": 0,
|
||||
"cache_read_tokens": 0,
|
||||
@@ -1555,7 +1442,6 @@ async def test_post_conversation_async(api_client, mock_openai_stream, monkeypat
|
||||
"Today is Friday 25/07/2025.\n\nAnswer in english."
|
||||
),
|
||||
"kind": "request",
|
||||
"metadata": None,
|
||||
"parts": [
|
||||
{
|
||||
"content": ["Hello"],
|
||||
@@ -1564,29 +1450,15 @@ async def test_post_conversation_async(api_client, mock_openai_stream, monkeypat
|
||||
},
|
||||
],
|
||||
"run_id": _run_id,
|
||||
"timestamp": "2025-07-25T10:36:35.297675Z",
|
||||
},
|
||||
{
|
||||
"finish_reason": "stop",
|
||||
"kind": "response",
|
||||
"metadata": None,
|
||||
"model_name": "test-model",
|
||||
"parts": [
|
||||
{
|
||||
"content": "Hello there",
|
||||
"id": None,
|
||||
"part_kind": "text",
|
||||
"provider_details": None,
|
||||
"provider_name": None,
|
||||
}
|
||||
],
|
||||
"provider_details": {
|
||||
"finish_reason": "stop",
|
||||
"timestamp": "2025-07-25T10:36:35.297675Z",
|
||||
},
|
||||
"parts": [{"content": "Hello there", "id": None, "part_kind": "text"}],
|
||||
"provider_details": {"finish_reason": "stop"},
|
||||
"provider_name": "openai",
|
||||
"provider_response_id": "chatcmpl-1234567890",
|
||||
"provider_url": "https://www.external-ai-service.com/",
|
||||
"timestamp": "2025-07-25T10:36:35.297675Z",
|
||||
"usage": {
|
||||
"cache_audio_read_tokens": 0,
|
||||
@@ -1710,7 +1582,6 @@ async def test_post_conversation_async_triggers_keepalive(
|
||||
"Today is Friday 25/07/2025.\n\nAnswer in english."
|
||||
),
|
||||
"kind": "request",
|
||||
"metadata": None,
|
||||
"parts": [
|
||||
{
|
||||
"content": ["Hello"],
|
||||
@@ -1719,29 +1590,15 @@ async def test_post_conversation_async_triggers_keepalive(
|
||||
},
|
||||
],
|
||||
"run_id": _run_id,
|
||||
"timestamp": ANY,
|
||||
},
|
||||
{
|
||||
"finish_reason": "stop",
|
||||
"kind": "response",
|
||||
"metadata": None,
|
||||
"model_name": "test-model",
|
||||
"parts": [
|
||||
{
|
||||
"content": "Hello there",
|
||||
"id": None,
|
||||
"part_kind": "text",
|
||||
"provider_details": None,
|
||||
"provider_name": None,
|
||||
}
|
||||
],
|
||||
"provider_details": {
|
||||
"finish_reason": "stop",
|
||||
"timestamp": ANY,
|
||||
},
|
||||
"parts": [{"content": "Hello there", "id": None, "part_kind": "text"}],
|
||||
"provider_details": {"finish_reason": "stop"},
|
||||
"provider_name": "openai",
|
||||
"provider_response_id": "chatcmpl-1234567890",
|
||||
"provider_url": "https://www.external-ai-service.com/",
|
||||
"timestamp": ANY,
|
||||
"usage": {
|
||||
"cache_audio_read_tokens": 0,
|
||||
|
||||
+7
-35
@@ -273,7 +273,7 @@ def test_post_conversation_with_document_upload(
|
||||
api_client,
|
||||
mock_document_api, # pylint: disable=unused-argument
|
||||
sample_pdf_content,
|
||||
today_prompt_date,
|
||||
today_promt_date,
|
||||
mock_ai_agent_service,
|
||||
):
|
||||
"""
|
||||
@@ -409,7 +409,7 @@ def test_post_conversation_with_document_upload(
|
||||
|
||||
assert chat_conversation.pydantic_messages[0] == {
|
||||
"instructions": "You are a helpful test assistant :)\n\n"
|
||||
f"{today_prompt_date}\n\n"
|
||||
f"{today_promt_date}\n\n"
|
||||
"Answer in english.\n\n"
|
||||
"Use document_search_rag ONLY to retrieve specific passages from "
|
||||
"attached documents. Do NOT use it to summarize; for summaries, "
|
||||
@@ -424,7 +424,6 @@ def test_post_conversation_with_document_upload(
|
||||
"Do not request re-upload of documents; consider them already "
|
||||
"available via the internal store.",
|
||||
"kind": "request",
|
||||
"metadata": None,
|
||||
"parts": [
|
||||
{
|
||||
"content": ["What does the document say?"],
|
||||
@@ -433,12 +432,10 @@ def test_post_conversation_with_document_upload(
|
||||
},
|
||||
],
|
||||
"run_id": _run_id,
|
||||
"timestamp": timezone_now,
|
||||
}
|
||||
assert chat_conversation.pydantic_messages[1] == {
|
||||
"finish_reason": None,
|
||||
"kind": "response",
|
||||
"metadata": None,
|
||||
"model_name": "function::agent_model",
|
||||
"parts": [
|
||||
{
|
||||
@@ -447,14 +444,11 @@ def test_post_conversation_with_document_upload(
|
||||
"part_kind": "tool-call",
|
||||
"tool_call_id": chat_conversation.pydantic_messages[1]["parts"][0]["tool_call_id"],
|
||||
"tool_name": "document_search_rag",
|
||||
"provider_details": None,
|
||||
"provider_name": None,
|
||||
}
|
||||
],
|
||||
"provider_details": None,
|
||||
"provider_name": None,
|
||||
"provider_response_id": None,
|
||||
"provider_url": None,
|
||||
"timestamp": timezone_now,
|
||||
"usage": {
|
||||
"cache_audio_read_tokens": 0,
|
||||
@@ -471,7 +465,7 @@ def test_post_conversation_with_document_upload(
|
||||
assert chat_conversation.pydantic_messages[2] == {
|
||||
"instructions": (
|
||||
"You are a helpful test assistant :)\n\n"
|
||||
f"{today_prompt_date}\n\n"
|
||||
f"{today_promt_date}\n\n"
|
||||
"Answer in english.\n\n"
|
||||
"Use document_search_rag ONLY to retrieve specific passages from "
|
||||
"attached documents. Do NOT use it to summarize; for summaries, "
|
||||
@@ -487,7 +481,6 @@ def test_post_conversation_with_document_upload(
|
||||
"available via the internal store."
|
||||
),
|
||||
"kind": "request",
|
||||
"metadata": None,
|
||||
"parts": [
|
||||
{
|
||||
"content": [
|
||||
@@ -505,26 +498,21 @@ def test_post_conversation_with_document_upload(
|
||||
}
|
||||
],
|
||||
"run_id": _run_id,
|
||||
"timestamp": timezone_now,
|
||||
}
|
||||
assert chat_conversation.pydantic_messages[3] == {
|
||||
"finish_reason": None,
|
||||
"kind": "response",
|
||||
"metadata": None,
|
||||
"model_name": "function::agent_model",
|
||||
"parts": [
|
||||
{
|
||||
"content": "From the document, I can see that it says 'Hello PDF'.",
|
||||
"id": None,
|
||||
"part_kind": "text",
|
||||
"provider_details": None,
|
||||
"provider_name": None,
|
||||
}
|
||||
],
|
||||
"provider_details": None,
|
||||
"provider_name": None,
|
||||
"provider_response_id": None,
|
||||
"provider_url": None,
|
||||
"timestamp": timezone_now,
|
||||
"usage": {
|
||||
"cache_audio_read_tokens": 0,
|
||||
@@ -614,7 +602,7 @@ def test_post_conversation_with_document_upload_summarize( # pylint: disable=to
|
||||
api_client,
|
||||
mock_document_api, # pylint: disable=unused-argument
|
||||
sample_pdf_content,
|
||||
today_prompt_date,
|
||||
today_promt_date,
|
||||
mock_ai_agent_service,
|
||||
mock_summarization_agent, # pylint: disable=unused-argument
|
||||
):
|
||||
@@ -751,7 +739,7 @@ def test_post_conversation_with_document_upload_summarize( # pylint: disable=to
|
||||
assert chat_conversation.pydantic_messages[0] == {
|
||||
"instructions": (
|
||||
"You are a helpful test assistant :)\n\n"
|
||||
f"{today_prompt_date}\n\n"
|
||||
f"{today_promt_date}\n\n"
|
||||
"Answer in english.\n\n"
|
||||
"Use document_search_rag ONLY to retrieve specific passages from "
|
||||
"attached documents. Do NOT use it to summarize; for summaries, "
|
||||
@@ -767,7 +755,6 @@ def test_post_conversation_with_document_upload_summarize( # pylint: disable=to
|
||||
"available via the internal store."
|
||||
),
|
||||
"kind": "request",
|
||||
"metadata": None,
|
||||
"parts": [
|
||||
{
|
||||
"content": ["Make a summary of this document."],
|
||||
@@ -776,12 +763,10 @@ def test_post_conversation_with_document_upload_summarize( # pylint: disable=to
|
||||
},
|
||||
],
|
||||
"run_id": _run_id,
|
||||
"timestamp": timezone_now,
|
||||
}
|
||||
assert chat_conversation.pydantic_messages[1] == {
|
||||
"finish_reason": None,
|
||||
"kind": "response",
|
||||
"metadata": None,
|
||||
"model_name": "function::agent_model",
|
||||
"parts": [
|
||||
{
|
||||
@@ -790,14 +775,11 @@ def test_post_conversation_with_document_upload_summarize( # pylint: disable=to
|
||||
"part_kind": "tool-call",
|
||||
"tool_call_id": chat_conversation.pydantic_messages[1]["parts"][0]["tool_call_id"],
|
||||
"tool_name": "summarize",
|
||||
"provider_details": None,
|
||||
"provider_name": None,
|
||||
}
|
||||
],
|
||||
"provider_details": None,
|
||||
"provider_name": None,
|
||||
"provider_response_id": None,
|
||||
"provider_url": None,
|
||||
"timestamp": timezone_now,
|
||||
"usage": {
|
||||
"cache_audio_read_tokens": 0,
|
||||
@@ -814,7 +796,7 @@ def test_post_conversation_with_document_upload_summarize( # pylint: disable=to
|
||||
assert chat_conversation.pydantic_messages[2] == {
|
||||
"instructions": (
|
||||
"You are a helpful test assistant :)\n\n"
|
||||
f"{today_prompt_date}\n\n"
|
||||
f"{today_promt_date}\n\n"
|
||||
"Answer in english.\n\n"
|
||||
"Use document_search_rag ONLY to retrieve specific passages from "
|
||||
"attached documents. Do NOT use it to summarize; for summaries, "
|
||||
@@ -830,7 +812,6 @@ def test_post_conversation_with_document_upload_summarize( # pylint: disable=to
|
||||
"available via the internal store."
|
||||
),
|
||||
"kind": "request",
|
||||
"metadata": None,
|
||||
"parts": [
|
||||
{
|
||||
"content": "The document discusses various topics.",
|
||||
@@ -842,26 +823,17 @@ def test_post_conversation_with_document_upload_summarize( # pylint: disable=to
|
||||
}
|
||||
],
|
||||
"run_id": _run_id,
|
||||
"timestamp": timezone_now,
|
||||
}
|
||||
assert chat_conversation.pydantic_messages[3] == {
|
||||
"finish_reason": None,
|
||||
"kind": "response",
|
||||
"metadata": None,
|
||||
"model_name": "function::agent_model",
|
||||
"parts": [
|
||||
{
|
||||
"content": "The document discusses various topics.",
|
||||
"id": None,
|
||||
"part_kind": "text",
|
||||
"provider_details": None,
|
||||
"provider_name": None,
|
||||
}
|
||||
{"content": "The document discusses various topics.", "id": None, "part_kind": "text"}
|
||||
],
|
||||
"provider_details": None,
|
||||
"provider_name": None,
|
||||
"provider_response_id": None,
|
||||
"provider_url": None,
|
||||
"timestamp": timezone_now,
|
||||
"usage": {
|
||||
"cache_audio_read_tokens": 0,
|
||||
|
||||
+10
-35
@@ -73,13 +73,12 @@ def test_post_conversation_with_local_pdf_document_url(
|
||||
# pylint: disable=too-many-arguments,too-many-positional-arguments
|
||||
api_client,
|
||||
sample_document_content,
|
||||
today_prompt_date,
|
||||
today_promt_date,
|
||||
mock_ai_agent_service,
|
||||
):
|
||||
"""
|
||||
Test POST to /api/v1/chats/{pk}/conversation/ with a document URL.
|
||||
"""
|
||||
|
||||
responses.post(
|
||||
"https://albert.api.etalab.gouv.fr/v1/collections",
|
||||
json={"id": 123, "object": "collection"},
|
||||
@@ -141,7 +140,7 @@ def test_post_conversation_with_local_pdf_document_url(
|
||||
],
|
||||
instructions=(
|
||||
"You are a helpful test assistant :)\n\n"
|
||||
f"{today_prompt_date}\n\n"
|
||||
f"{today_promt_date}\n\n"
|
||||
"Answer in english.\n\n"
|
||||
"Use document_search_rag ONLY to retrieve specific passages from attached "
|
||||
"documents. Do NOT use it to summarize; for summaries, call the summarize "
|
||||
@@ -157,7 +156,6 @@ def test_post_conversation_with_local_pdf_document_url(
|
||||
"via the internal store."
|
||||
),
|
||||
run_id=messages[0].run_id,
|
||||
timestamp=timezone.now(),
|
||||
)
|
||||
]
|
||||
yield "This is a document about a single pixel."
|
||||
@@ -231,7 +229,7 @@ def test_post_conversation_with_local_pdf_document_url(
|
||||
assert chat_conversation.pydantic_messages == [
|
||||
{
|
||||
"instructions": "You are a helpful test assistant :)\n\n"
|
||||
f"{today_prompt_date}\n\n"
|
||||
f"{today_promt_date}\n\n"
|
||||
"Answer in english.\n"
|
||||
"\n"
|
||||
"Use document_search_rag ONLY to retrieve specific passages "
|
||||
@@ -251,7 +249,6 @@ def test_post_conversation_with_local_pdf_document_url(
|
||||
"conversation. Do not request re-upload of documents; "
|
||||
"consider them already available via the internal store.",
|
||||
"kind": "request",
|
||||
"metadata": None,
|
||||
"parts": [
|
||||
{
|
||||
"content": [
|
||||
@@ -262,26 +259,21 @@ def test_post_conversation_with_local_pdf_document_url(
|
||||
},
|
||||
],
|
||||
"run_id": _run_id,
|
||||
"timestamp": timestamp,
|
||||
},
|
||||
{
|
||||
"finish_reason": None,
|
||||
"kind": "response",
|
||||
"metadata": None,
|
||||
"model_name": "function::agent_model",
|
||||
"parts": [
|
||||
{
|
||||
"content": "This is a document about a single pixel.",
|
||||
"id": None,
|
||||
"part_kind": "text",
|
||||
"provider_details": None,
|
||||
"provider_name": None,
|
||||
}
|
||||
],
|
||||
"provider_details": None,
|
||||
"provider_name": None,
|
||||
"provider_response_id": None,
|
||||
"provider_url": None,
|
||||
"timestamp": timestamp,
|
||||
"usage": {
|
||||
"cache_audio_read_tokens": 0,
|
||||
@@ -553,7 +545,6 @@ def test_post_conversation_with_local_document_url_in_history( # pylint: disabl
|
||||
assert presigned_url.find("X-Amz-Signature=") != -1
|
||||
assert presigned_url.find("X-Amz-Date=") != -1
|
||||
assert presigned_url.find("X-Amz-Expires=") != -1
|
||||
timestamp_now = timezone.now()
|
||||
|
||||
assert messages == [
|
||||
ModelRequest(
|
||||
@@ -567,7 +558,7 @@ def test_post_conversation_with_local_document_url_in_history( # pylint: disabl
|
||||
identifier="sample.pdf",
|
||||
),
|
||||
],
|
||||
timestamp=timestamp_now,
|
||||
timestamp=timezone.now(),
|
||||
),
|
||||
],
|
||||
instructions="You are a helpful test assistant :)\n\n"
|
||||
@@ -579,7 +570,7 @@ def test_post_conversation_with_local_document_url_in_history( # pylint: disabl
|
||||
parts=[TextPart(content="This is a document about a single pixel.")],
|
||||
usage=RequestUsage(input_tokens=50, output_tokens=9),
|
||||
model_name="function::agent_model",
|
||||
timestamp=timestamp_now,
|
||||
timestamp=timezone.now(),
|
||||
run_id=messages[1].run_id,
|
||||
),
|
||||
ModelRequest(
|
||||
@@ -588,10 +579,9 @@ def test_post_conversation_with_local_document_url_in_history( # pylint: disabl
|
||||
content=[
|
||||
"Give more details about this document.",
|
||||
],
|
||||
timestamp=timestamp_now,
|
||||
timestamp=timezone.now(),
|
||||
)
|
||||
],
|
||||
timestamp=timestamp_now,
|
||||
instructions="You are a helpful test assistant :)\n\n"
|
||||
"Today is Saturday 18/10/2025.\n\n"
|
||||
"Answer in english.",
|
||||
@@ -748,7 +738,6 @@ def test_post_conversation_with_local_document_url_in_history( # pylint: disabl
|
||||
"instructions": "You are a helpful test assistant :)\n\n"
|
||||
"Today is Saturday 18/10/2025.\n\n"
|
||||
"Answer in english.",
|
||||
"metadata": None,
|
||||
"kind": "request",
|
||||
"parts": [
|
||||
{
|
||||
@@ -758,26 +747,21 @@ def test_post_conversation_with_local_document_url_in_history( # pylint: disabl
|
||||
}
|
||||
],
|
||||
"run_id": _run_id,
|
||||
"timestamp": "2025-10-18T20:48:20.286204Z",
|
||||
},
|
||||
{
|
||||
"finish_reason": None,
|
||||
"kind": "response",
|
||||
"metadata": None,
|
||||
"model_name": "function::agent_model",
|
||||
"parts": [
|
||||
{
|
||||
"content": "This is a document of square, very small and nice.",
|
||||
"id": None,
|
||||
"part_kind": "text",
|
||||
"provider_details": None,
|
||||
"provider_name": None,
|
||||
}
|
||||
],
|
||||
"provider_details": None,
|
||||
"provider_name": None,
|
||||
"provider_response_id": None,
|
||||
"provider_url": None,
|
||||
"timestamp": "2025-10-18T20:48:20.286204Z",
|
||||
"usage": {
|
||||
"cache_audio_read_tokens": 0,
|
||||
@@ -807,7 +791,7 @@ def test_post_conversation_with_local_document_url_in_history( # pylint: disabl
|
||||
def test_post_conversation_with_local_not_pdf_document_url(
|
||||
# pylint: disable=too-many-arguments,too-many-positional-arguments
|
||||
api_client,
|
||||
today_prompt_date,
|
||||
today_promt_date,
|
||||
mock_ai_agent_service,
|
||||
file_name,
|
||||
content_type,
|
||||
@@ -869,8 +853,6 @@ def test_post_conversation_with_local_not_pdf_document_url(
|
||||
)
|
||||
|
||||
async def agent_model(messages: list[ModelMessage], _info: AgentInfo):
|
||||
timestamp_now = timezone.now()
|
||||
|
||||
assert messages == [
|
||||
ModelRequest(
|
||||
parts=[
|
||||
@@ -879,13 +861,12 @@ def test_post_conversation_with_local_not_pdf_document_url(
|
||||
"What is in this document?",
|
||||
# No presigned URL for non-PDF documents (not supporter by LLM)
|
||||
],
|
||||
timestamp=timestamp_now,
|
||||
timestamp=timezone.now(),
|
||||
),
|
||||
],
|
||||
timestamp=timestamp_now,
|
||||
instructions=(
|
||||
"You are a helpful test assistant :)\n\n"
|
||||
f"{today_prompt_date}\n\n"
|
||||
f"{today_promt_date}\n\n"
|
||||
"Answer in english.\n\n"
|
||||
"Use document_search_rag ONLY to retrieve specific passages from "
|
||||
"attached documents. Do NOT use it to summarize; for summaries, "
|
||||
@@ -976,7 +957,7 @@ def test_post_conversation_with_local_not_pdf_document_url(
|
||||
{
|
||||
"instructions": (
|
||||
"You are a helpful test assistant :)\n\n"
|
||||
f"{today_prompt_date}\n\n"
|
||||
f"{today_promt_date}\n\n"
|
||||
"Answer in english.\n\n"
|
||||
"Use document_search_rag ONLY to retrieve specific passages from "
|
||||
"attached documents. Do NOT use it to summarize; for summaries, "
|
||||
@@ -993,7 +974,6 @@ def test_post_conversation_with_local_not_pdf_document_url(
|
||||
"consider them already available via the internal store."
|
||||
),
|
||||
"kind": "request",
|
||||
"metadata": None,
|
||||
"parts": [
|
||||
{
|
||||
"content": [
|
||||
@@ -1004,26 +984,21 @@ def test_post_conversation_with_local_not_pdf_document_url(
|
||||
},
|
||||
],
|
||||
"run_id": _run_id,
|
||||
"timestamp": timestamp,
|
||||
},
|
||||
{
|
||||
"finish_reason": None,
|
||||
"kind": "response",
|
||||
"metadata": None,
|
||||
"model_name": "function::agent_model",
|
||||
"parts": [
|
||||
{
|
||||
"content": "This is a document about you.",
|
||||
"id": None,
|
||||
"part_kind": "text",
|
||||
"provider_details": None,
|
||||
"provider_name": None,
|
||||
}
|
||||
],
|
||||
"provider_details": None,
|
||||
"provider_name": None,
|
||||
"provider_response_id": None,
|
||||
"provider_url": None,
|
||||
"timestamp": timestamp,
|
||||
"usage": {
|
||||
"cache_audio_read_tokens": 0,
|
||||
|
||||
+90
-223
@@ -29,10 +29,6 @@ from chat.tests.utils import replace_uuids_with_placeholder
|
||||
pytestmark = pytest.mark.django_db(transaction=True)
|
||||
|
||||
|
||||
PYAI_CURRENT = "current"
|
||||
PYAI_V1_17 = "v1.17"
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def ai_settings(settings):
|
||||
"""Fixture to set AI service URLs for testing."""
|
||||
@@ -45,9 +41,17 @@ def ai_settings(settings):
|
||||
return settings
|
||||
|
||||
|
||||
def build__history_conversation_ui_messages(history_timestamp):
|
||||
"""Build ui messages list for fixtures."""
|
||||
return [
|
||||
@pytest.fixture(name="history_conversation")
|
||||
def history_conversation_fixture():
|
||||
"""Create a conversation with existing message history."""
|
||||
# Create a timestamp for the first message
|
||||
history_timestamp = timezone.now().replace(year=2025, month=6, day=15, hour=10, minute=30)
|
||||
|
||||
# Create a conversation with pre-existing messages
|
||||
conversation = ChatConversationFactory()
|
||||
|
||||
# Add previous user and assistant messages
|
||||
conversation.messages = [
|
||||
UIMessage(
|
||||
id="prev-user-msg-1",
|
||||
createdAt=history_timestamp,
|
||||
@@ -116,205 +120,94 @@ def build__history_conversation_ui_messages(history_timestamp):
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
@pytest.fixture(name="history_conversation")
|
||||
def history_conversation_fixture(request):
|
||||
"""Create a conversation with existing message history according to pydantic ai version."""
|
||||
|
||||
# Create a timestamp for the first message
|
||||
history_timestamp = timezone.now().replace(year=2025, month=6, day=15, hour=10, minute=30)
|
||||
|
||||
# Create a conversation with pre-existing messages
|
||||
conversation = ChatConversationFactory()
|
||||
pyai_version = getattr(request, "param", PYAI_CURRENT)
|
||||
# Add previous user and assistant messages
|
||||
if pyai_version == PYAI_V1_17:
|
||||
conversation.pydantic_messages = [
|
||||
{
|
||||
"instructions": None,
|
||||
"kind": "request",
|
||||
"parts": [
|
||||
{
|
||||
"content": "You are a helpful test assistant :)",
|
||||
"dynamic_ref": None,
|
||||
"part_kind": "system-prompt",
|
||||
"timestamp": "2025-06-15T10:30:00.000000Z",
|
||||
},
|
||||
{
|
||||
"content": ["How does machine learning work?"],
|
||||
"part_kind": "user-prompt",
|
||||
"timestamp": "2025-06-15T10:30:00.000000Z",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"kind": "response",
|
||||
"model_name": "test-model",
|
||||
"parts": [
|
||||
{
|
||||
"content": (
|
||||
"Machine learning is a branch of artificial intelligence that "
|
||||
"focuses on building systems that learn from data."
|
||||
),
|
||||
"part_kind": "text",
|
||||
}
|
||||
],
|
||||
"timestamp": "2025-06-15T10:31:00.000000Z",
|
||||
"usage": {
|
||||
"details": None,
|
||||
"request_tokens": 10,
|
||||
"requests": 1,
|
||||
"response_tokens": 20,
|
||||
"total_tokens": 30,
|
||||
},
|
||||
"vendor_details": None,
|
||||
"vendor_id": None,
|
||||
},
|
||||
{
|
||||
"instructions": None,
|
||||
"kind": "request",
|
||||
"parts": [
|
||||
{
|
||||
"content": ["What are neural networks?"],
|
||||
"part_kind": "user-prompt",
|
||||
"timestamp": "2025-06-15T10:32:00.000000Z",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"kind": "response",
|
||||
"model_name": "test-model",
|
||||
"parts": [
|
||||
{
|
||||
"content": (
|
||||
"Neural networks are computing systems inspired by the "
|
||||
"biological neural networks in animal brains."
|
||||
),
|
||||
"part_kind": "text",
|
||||
}
|
||||
],
|
||||
"timestamp": "2025-06-15T10:33:00.000000Z",
|
||||
"usage": {
|
||||
"details": None,
|
||||
"request_tokens": 5,
|
||||
"requests": 1,
|
||||
"response_tokens": 15,
|
||||
"total_tokens": 20,
|
||||
},
|
||||
"vendor_details": None,
|
||||
"vendor_id": None,
|
||||
},
|
||||
]
|
||||
else:
|
||||
conversation.pydantic_messages = [
|
||||
{
|
||||
"instructions": None,
|
||||
"kind": "request",
|
||||
"parts": [
|
||||
{
|
||||
"content": "You are a helpful test assistant :)",
|
||||
"dynamic_ref": None,
|
||||
"part_kind": "system-prompt",
|
||||
"timestamp": "2025-06-15T10:30:00.000000Z",
|
||||
},
|
||||
{
|
||||
"content": ["How does machine learning work?"],
|
||||
"part_kind": "user-prompt",
|
||||
"timestamp": "2025-06-15T10:30:00.000000Z",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"kind": "response",
|
||||
"model_name": "test-model",
|
||||
"parts": [
|
||||
{
|
||||
"content": (
|
||||
"Machine learning is a branch of artificial intelligence that "
|
||||
"focuses on building systems that learn from data."
|
||||
),
|
||||
"part_kind": "text",
|
||||
"provider_details": {
|
||||
"finish_reason": "stop",
|
||||
"timestamp": "2025-07-25T10:36:35.297675Z",
|
||||
},
|
||||
"provider_name": "some model",
|
||||
}
|
||||
],
|
||||
"timestamp": "2025-06-15T10:31:00.000000Z",
|
||||
"usage": {
|
||||
"details": None,
|
||||
"request_tokens": 10,
|
||||
"requests": 1,
|
||||
"response_tokens": 20,
|
||||
"total_tokens": 30,
|
||||
},
|
||||
"provider_details": None,
|
||||
"vendor_id": None,
|
||||
},
|
||||
{
|
||||
"instructions": None,
|
||||
"kind": "request",
|
||||
"parts": [
|
||||
{
|
||||
"content": ["What are neural networks?"],
|
||||
"part_kind": "user-prompt",
|
||||
"timestamp": "2025-06-15T10:32:00.000000Z",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"kind": "response",
|
||||
"metadata": None,
|
||||
"model_name": "test-model",
|
||||
"finish_reason": "stop",
|
||||
"parts": [
|
||||
{
|
||||
"content": (
|
||||
"Neural networks are computing systems inspired by the "
|
||||
"biological neural networks in animal brains."
|
||||
),
|
||||
"part_kind": "text",
|
||||
"provider_details": {
|
||||
"finish_reason": "stop",
|
||||
"timestamp": "2025-07-25T10:36:35.297675Z",
|
||||
},
|
||||
"provider_name": "test-model",
|
||||
"provider_url": "https://www.external-ai-service.com/",
|
||||
}
|
||||
],
|
||||
"timestamp": "2025-06-15T10:33:00.000000Z",
|
||||
"usage": {
|
||||
"details": None,
|
||||
"request_tokens": 5,
|
||||
"requests": 1,
|
||||
"response_tokens": 15,
|
||||
"total_tokens": 20,
|
||||
},
|
||||
"provider_details": {
|
||||
"timestamp": "2025-07-25T10:36:35.297675Z",
|
||||
"finish_reason": "stop",
|
||||
},
|
||||
"provider_name": "test-model",
|
||||
"provider_response_id": "xyz",
|
||||
},
|
||||
]
|
||||
|
||||
# Set up the OpenAI message format as well
|
||||
conversation.messages = build__history_conversation_ui_messages(history_timestamp)
|
||||
conversation.pydantic_messages = [
|
||||
{
|
||||
"instructions": None,
|
||||
"kind": "request",
|
||||
"parts": [
|
||||
{
|
||||
"content": "You are a helpful test assistant :)",
|
||||
"dynamic_ref": None,
|
||||
"part_kind": "system-prompt",
|
||||
"timestamp": "2025-06-15T10:30:00.000000Z",
|
||||
},
|
||||
{
|
||||
"content": ["How does machine learning work?"],
|
||||
"part_kind": "user-prompt",
|
||||
"timestamp": "2025-06-15T10:30:00.000000Z",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"kind": "response",
|
||||
"model_name": "test-model",
|
||||
"parts": [
|
||||
{
|
||||
"content": (
|
||||
"Machine learning is a branch of artificial intelligence that "
|
||||
"focuses on building systems that learn from data."
|
||||
),
|
||||
"part_kind": "text",
|
||||
}
|
||||
],
|
||||
"timestamp": "2025-06-15T10:31:00.000000Z",
|
||||
"usage": {
|
||||
"details": None,
|
||||
"request_tokens": 10,
|
||||
"requests": 1,
|
||||
"response_tokens": 20,
|
||||
"total_tokens": 30,
|
||||
},
|
||||
"vendor_details": None,
|
||||
"vendor_id": None,
|
||||
},
|
||||
{
|
||||
"instructions": None,
|
||||
"kind": "request",
|
||||
"parts": [
|
||||
{
|
||||
"content": ["What are neural networks?"],
|
||||
"part_kind": "user-prompt",
|
||||
"timestamp": "2025-06-15T10:32:00.000000Z",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"kind": "response",
|
||||
"model_name": "test-model",
|
||||
"parts": [
|
||||
{
|
||||
"content": (
|
||||
"Neural networks are computing systems inspired by the "
|
||||
"biological neural networks in animal brains."
|
||||
),
|
||||
"part_kind": "text",
|
||||
}
|
||||
],
|
||||
"timestamp": "2025-06-15T10:33:00.000000Z",
|
||||
"usage": {
|
||||
"details": None,
|
||||
"request_tokens": 5,
|
||||
"requests": 1,
|
||||
"response_tokens": 15,
|
||||
"total_tokens": 20,
|
||||
},
|
||||
"vendor_details": None,
|
||||
"vendor_id": None,
|
||||
},
|
||||
]
|
||||
|
||||
conversation.save()
|
||||
return conversation
|
||||
|
||||
|
||||
@pytest.mark.parametrize("history_conversation", [PYAI_CURRENT, PYAI_V1_17], indirect=True)
|
||||
@freeze_time("2025-07-25T10:36:35.297675Z")
|
||||
@respx.mock
|
||||
def test_post_conversation_data_protocol_with_history(
|
||||
api_client, mock_openai_stream, history_conversation
|
||||
):
|
||||
"""Test posting messages to a conversation with history using the 'data' protocol."""
|
||||
|
||||
url = f"/api/v1.0/chats/{history_conversation.pk}/conversation/?protocol=data"
|
||||
data = {
|
||||
"messages": [
|
||||
@@ -1135,7 +1028,6 @@ def history_conversation_with_tool_fixture():
|
||||
},
|
||||
{
|
||||
"kind": "response",
|
||||
"metadata": None,
|
||||
"model_name": "test-model",
|
||||
"parts": [
|
||||
{
|
||||
@@ -1177,7 +1069,6 @@ def history_conversation_with_tool_fixture():
|
||||
},
|
||||
{
|
||||
"kind": "response",
|
||||
"metadata": None,
|
||||
"model_name": "test-model",
|
||||
"parts": [
|
||||
{
|
||||
@@ -1211,7 +1102,6 @@ def history_conversation_with_tool_fixture():
|
||||
},
|
||||
{
|
||||
"kind": "response",
|
||||
"metadata": None,
|
||||
"model_name": "test-model",
|
||||
"parts": [
|
||||
{
|
||||
@@ -1258,7 +1148,6 @@ def history_conversation_with_tool_fixture():
|
||||
},
|
||||
{
|
||||
"kind": "response",
|
||||
"metadata": None,
|
||||
"model_name": "test-model",
|
||||
"parts": [
|
||||
{
|
||||
@@ -1495,7 +1384,6 @@ def test_post_conversation_with_existing_tool_history(
|
||||
"Today is Friday 25/07/2025.\n\n"
|
||||
"Answer in dutch.",
|
||||
"kind": "request",
|
||||
"metadata": None,
|
||||
"parts": [
|
||||
{
|
||||
"content": ["How about Paris weather?"],
|
||||
@@ -1504,32 +1392,24 @@ def test_post_conversation_with_existing_tool_history(
|
||||
}
|
||||
],
|
||||
"run_id": _run_id,
|
||||
"timestamp": "2025-07-25T10:36:35.297675Z",
|
||||
}
|
||||
|
||||
assert history_conversation_with_tool.pydantic_messages[9] == {
|
||||
"finish_reason": "tool_call",
|
||||
"kind": "response",
|
||||
"metadata": None,
|
||||
"model_name": "test-model",
|
||||
"parts": [
|
||||
{
|
||||
"args": '{"location":"Paris", "unit":"celsius"}',
|
||||
"id": None,
|
||||
"part_kind": "tool-call",
|
||||
"provider_details": None,
|
||||
"provider_name": None,
|
||||
"tool_call_id": "xLDcIljdsDrz0idal7tATWSMm2jhMj47",
|
||||
"tool_name": "get_current_weather",
|
||||
}
|
||||
],
|
||||
"provider_details": {
|
||||
"finish_reason": "tool_calls",
|
||||
"timestamp": "2025-07-25T10:36:35.297675Z",
|
||||
},
|
||||
"provider_details": {"finish_reason": "tool_calls"},
|
||||
"provider_name": "openai",
|
||||
"provider_response_id": "chatcmpl-tool-call",
|
||||
"provider_url": "https://www.external-ai-service.com/",
|
||||
"timestamp": "2025-07-25T10:36:35.297675Z",
|
||||
"usage": {
|
||||
"cache_audio_read_tokens": 0,
|
||||
@@ -1549,7 +1429,6 @@ def test_post_conversation_with_existing_tool_history(
|
||||
"Today is Friday 25/07/2025.\n\n"
|
||||
"Answer in dutch.",
|
||||
"kind": "request",
|
||||
"metadata": None,
|
||||
"parts": [
|
||||
{
|
||||
"content": {"location": "Paris", "temperature": 22, "unit": "celsius"},
|
||||
@@ -1561,30 +1440,18 @@ def test_post_conversation_with_existing_tool_history(
|
||||
}
|
||||
],
|
||||
"run_id": _run_id,
|
||||
"timestamp": "2025-07-25T10:36:35.297675Z",
|
||||
}
|
||||
|
||||
assert history_conversation_with_tool.pydantic_messages[11] == {
|
||||
"finish_reason": "stop",
|
||||
"kind": "response",
|
||||
"metadata": None,
|
||||
"model_name": "test-model",
|
||||
"parts": [
|
||||
{
|
||||
"content": "The current weather in Paris is nice",
|
||||
"id": None,
|
||||
"part_kind": "text",
|
||||
"provider_details": None,
|
||||
"provider_name": None,
|
||||
}
|
||||
{"content": "The current weather in Paris is nice", "id": None, "part_kind": "text"}
|
||||
],
|
||||
"provider_details": {
|
||||
"finish_reason": "stop",
|
||||
"timestamp": "2025-07-25T10:36:35.297675Z",
|
||||
},
|
||||
"provider_details": {"finish_reason": "stop"},
|
||||
"provider_name": "openai",
|
||||
"provider_response_id": "chatcmpl-final",
|
||||
"provider_url": "https://www.external-ai-service.com/",
|
||||
"timestamp": "2025-07-25T10:36:35.297675Z",
|
||||
"usage": {
|
||||
"cache_audio_read_tokens": 0,
|
||||
|
||||
+11
-34
@@ -92,7 +92,6 @@ def test_post_conversation_with_local_image_url(
|
||||
assert presigned_url.find("X-Amz-Date=") != -1
|
||||
assert presigned_url.find("X-Amz-Expires=") != -1
|
||||
formatted_date = formats.date_format(timezone.now(), "l d/m/Y", use_l10n=False)
|
||||
|
||||
assert messages == [
|
||||
ModelRequest(
|
||||
parts=[
|
||||
@@ -111,7 +110,6 @@ def test_post_conversation_with_local_image_url(
|
||||
instructions="You are a helpful test assistant :)\n\nToday is "
|
||||
f"{formatted_date}.\n\nAnswer in english.",
|
||||
run_id=messages[0].run_id,
|
||||
timestamp=timezone.now(),
|
||||
)
|
||||
]
|
||||
yield "This is an image of a single pixel."
|
||||
@@ -183,7 +181,6 @@ def test_post_conversation_with_local_image_url(
|
||||
"instructions": "You are a helpful test assistant :)\n\n"
|
||||
"Today is Saturday 18/10/2025.\n\nAnswer in english.",
|
||||
"kind": "request",
|
||||
"metadata": None,
|
||||
"parts": [
|
||||
{
|
||||
"content": [
|
||||
@@ -202,26 +199,17 @@ def test_post_conversation_with_local_image_url(
|
||||
},
|
||||
],
|
||||
"run_id": _run_id,
|
||||
"timestamp": "2025-10-18T20:48:20.286204Z",
|
||||
},
|
||||
{
|
||||
"finish_reason": None,
|
||||
"kind": "response",
|
||||
"metadata": None,
|
||||
"model_name": "function::agent_model",
|
||||
"parts": [
|
||||
{
|
||||
"content": "This is an image of a single pixel.",
|
||||
"id": None,
|
||||
"part_kind": "text",
|
||||
"provider_details": None,
|
||||
"provider_name": None,
|
||||
}
|
||||
{"content": "This is an image of a single pixel.", "id": None, "part_kind": "text"}
|
||||
],
|
||||
"provider_details": None,
|
||||
"provider_name": None,
|
||||
"provider_response_id": None,
|
||||
"provider_url": None,
|
||||
"timestamp": "2025-10-18T20:48:20.286204Z",
|
||||
"usage": {
|
||||
"cache_audio_read_tokens": 0,
|
||||
@@ -241,7 +229,7 @@ def test_post_conversation_with_local_image_url(
|
||||
@freeze_time()
|
||||
def test_post_conversation_with_local_image_wrong_url(
|
||||
api_client,
|
||||
today_prompt_date,
|
||||
today_promt_date,
|
||||
mock_ai_agent_service,
|
||||
):
|
||||
"""
|
||||
@@ -287,8 +275,7 @@ def test_post_conversation_with_local_image_wrong_url(
|
||||
timestamp=timezone.now(),
|
||||
),
|
||||
],
|
||||
timestamp=timezone.now(),
|
||||
instructions=f"You are a helpful test assistant :)\n\n{today_prompt_date}"
|
||||
instructions=f"You are a helpful test assistant :)\n\n{today_promt_date}"
|
||||
"\n\nAnswer in english.",
|
||||
run_id=messages[0].run_id,
|
||||
)
|
||||
@@ -327,7 +314,7 @@ def test_post_conversation_with_local_image_wrong_url(
|
||||
@freeze_time()
|
||||
def test_post_conversation_with_remote_image_url(
|
||||
api_client,
|
||||
today_prompt_date,
|
||||
today_promt_date,
|
||||
mock_ai_agent_service,
|
||||
):
|
||||
"""
|
||||
@@ -374,9 +361,8 @@ def test_post_conversation_with_remote_image_url(
|
||||
),
|
||||
],
|
||||
instructions="You are a helpful test assistant :)\n\n"
|
||||
f"{today_prompt_date}\n\nAnswer in english.",
|
||||
f"{today_promt_date}\n\nAnswer in english.",
|
||||
run_id=messages[0].run_id,
|
||||
timestamp=timezone.now(),
|
||||
)
|
||||
]
|
||||
yield "This is an image of a single pixel."
|
||||
@@ -446,7 +432,7 @@ def test_post_conversation_with_remote_image_url(
|
||||
@freeze_time("2025-10-18T20:48:20.286204Z")
|
||||
def test_post_conversation_with_local_image_url_in_history(
|
||||
api_client,
|
||||
today_prompt_date,
|
||||
today_promt_date,
|
||||
mock_ai_agent_service,
|
||||
):
|
||||
"""
|
||||
@@ -489,7 +475,7 @@ def test_post_conversation_with_local_image_url_in_history(
|
||||
],
|
||||
pydantic_messages=[
|
||||
{
|
||||
"instructions": f"You are a helpful test assistant :)\n\n{today_prompt_date}"
|
||||
"instructions": f"You are a helpful test assistant :)\n\n{today_promt_date}"
|
||||
"\n\nAnswer in english.",
|
||||
"kind": "request",
|
||||
"parts": [
|
||||
@@ -561,8 +547,6 @@ def test_post_conversation_with_local_image_url_in_history(
|
||||
assert presigned_url.find("X-Amz-Date=") != -1
|
||||
assert presigned_url.find("X-Amz-Expires=") != -1
|
||||
|
||||
timestamp_now = timezone.now()
|
||||
|
||||
assert messages == [
|
||||
ModelRequest(
|
||||
parts=[
|
||||
@@ -575,11 +559,11 @@ def test_post_conversation_with_local_image_url_in_history(
|
||||
identifier="sample.png",
|
||||
),
|
||||
],
|
||||
timestamp=timestamp_now,
|
||||
timestamp=timezone.now(),
|
||||
),
|
||||
],
|
||||
instructions="You are a helpful test assistant :)\n\n"
|
||||
f"{today_prompt_date}\n\nAnswer in english.",
|
||||
f"{today_promt_date}\n\nAnswer in english.",
|
||||
),
|
||||
ModelResponse(
|
||||
parts=[TextPart(content="This is an image of a single pixel.")],
|
||||
@@ -593,13 +577,12 @@ def test_post_conversation_with_local_image_url_in_history(
|
||||
content=[
|
||||
"Give more details about this image.",
|
||||
],
|
||||
timestamp=timestamp_now,
|
||||
timestamp=timezone.now(),
|
||||
)
|
||||
],
|
||||
run_id=messages[2].run_id,
|
||||
instructions="You are a helpful test assistant :)\n\n"
|
||||
"Today is Saturday 18/10/2025.\n\nAnswer in english.",
|
||||
timestamp=timestamp_now,
|
||||
),
|
||||
]
|
||||
yield "This is an image of square, very small and nice."
|
||||
@@ -698,7 +681,7 @@ def test_post_conversation_with_local_image_url_in_history(
|
||||
_run_id = chat_conversation.pydantic_messages[2]["run_id"]
|
||||
assert chat_conversation.pydantic_messages == [
|
||||
{
|
||||
"instructions": f"You are a helpful test assistant :)\n\n{today_prompt_date}"
|
||||
"instructions": f"You are a helpful test assistant :)\n\n{today_promt_date}"
|
||||
"\n\nAnswer in english.",
|
||||
"kind": "request",
|
||||
"parts": [
|
||||
@@ -745,7 +728,6 @@ def test_post_conversation_with_local_image_url_in_history(
|
||||
"instructions": "You are a helpful test assistant :)\n\nToday is Saturday 18/10/2025."
|
||||
"\n\nAnswer in english.",
|
||||
"kind": "request",
|
||||
"metadata": None,
|
||||
"parts": [
|
||||
{
|
||||
"content": ["Give more details about this image."],
|
||||
@@ -754,26 +736,21 @@ def test_post_conversation_with_local_image_url_in_history(
|
||||
}
|
||||
],
|
||||
"run_id": _run_id,
|
||||
"timestamp": "2025-10-18T20:48:20.286204Z",
|
||||
},
|
||||
{
|
||||
"finish_reason": None,
|
||||
"kind": "response",
|
||||
"metadata": None,
|
||||
"model_name": "function::agent_model",
|
||||
"parts": [
|
||||
{
|
||||
"content": "This is an image of square, very small and nice.",
|
||||
"id": None,
|
||||
"part_kind": "text",
|
||||
"provider_details": None,
|
||||
"provider_name": None,
|
||||
}
|
||||
],
|
||||
"provider_details": None,
|
||||
"provider_name": None,
|
||||
"provider_response_id": None,
|
||||
"provider_url": None,
|
||||
"timestamp": "2025-10-18T20:48:20.286204Z",
|
||||
"usage": {
|
||||
"cache_audio_read_tokens": 0,
|
||||
|
||||
@@ -4,6 +4,7 @@ import asyncio
|
||||
import logging
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.files.storage import default_storage
|
||||
|
||||
import semchunk
|
||||
from asgiref.sync import sync_to_async
|
||||
@@ -13,11 +14,18 @@ from pydantic_ai.messages import ToolReturn
|
||||
|
||||
from chat.agents.summarize import SummarizationAgent
|
||||
from chat.tools.exceptions import ModelCannotRetry
|
||||
from chat.tools.utils import last_model_retry_soft_fail, read_document_content
|
||||
from chat.tools.utils import last_model_retry_soft_fail
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@sync_to_async
|
||||
def read_document_content(doc):
|
||||
"""Read document content asynchronously."""
|
||||
with default_storage.open(doc.key) as f:
|
||||
return doc.file_name, f.read().decode("utf-8")
|
||||
|
||||
|
||||
async def summarize_chunk(idx, chunk, total_chunks, summarization_agent, ctx):
|
||||
"""Summarize a single chunk of text."""
|
||||
sum_prompt = (
|
||||
@@ -43,26 +51,33 @@ async def summarize_chunk(idx, chunk, total_chunks, summarization_agent, ctx):
|
||||
|
||||
|
||||
@last_model_retry_soft_fail
|
||||
async def document_summarize( # pylint: disable=too-many-locals
|
||||
ctx: RunContext, *, instructions: str | None = None
|
||||
async def document_summarize( # pylint: disable=too-many-locals, too-many-statements
|
||||
ctx: RunContext,
|
||||
*,
|
||||
instructions: str | None = None,
|
||||
doc_index: int | None = None,
|
||||
) -> ToolReturn:
|
||||
"""
|
||||
Generate a complete, ready-to-use summary of the documents in context
|
||||
(do not request the documents to the user).
|
||||
Return this summary directly to the user WITHOUT any modification,
|
||||
or additional summarization.
|
||||
The summary is already optimized and MUST be presented as-is in the final response
|
||||
or translated preserving the information.
|
||||
|
||||
Instructions are optional but should reflect the user's request.
|
||||
|
||||
Examples:
|
||||
"Summarize this doc in 2 paragraphs" -> instructions = "summary in 2 paragraphs"
|
||||
"Summarize this doc in English" -> instructions = "In English"
|
||||
"Summarize this doc" -> instructions = "" (default)
|
||||
|
||||
Args:
|
||||
instructions (str | None): The instructions the user gave to use for the summarization
|
||||
Produce a final, user-ready summary for one or more text documents from the conversation.
|
||||
|
||||
Builds per-chunk summaries for the selected documents, synthesizes them into a single coherent
|
||||
markdown-formatted summary that is intended to be returned to the user verbatim.
|
||||
|
||||
Parameters:
|
||||
instructions (str | None): Optional user instructions to guide the summary (e.g., length,
|
||||
language, style). When omitted, a default hint is used.
|
||||
doc_index (int | None): If provided, summarize only the document at this index (0-based;
|
||||
negative indices allowed, e.g. -1 for the last document). If `None`, all text documents
|
||||
found in the conversation are summarized.
|
||||
|
||||
Returns:
|
||||
str: The final synthesized summary formatted in Markdown.
|
||||
|
||||
Raises:
|
||||
ModelCannotRetry: If no text documents are found in the conversation or on unexpected errors
|
||||
that should stop processing and be reported to the user.
|
||||
ModelRetry: For retryable errors such as an out-of-range `doc_index`, errors during chunk
|
||||
processing, merge-generation failures, or when the summarization produces an empty result.
|
||||
"""
|
||||
try:
|
||||
instructions_hint = (
|
||||
@@ -83,6 +98,15 @@ async def document_summarize( # pylint: disable=too-many-locals
|
||||
"You must explain this to the user and ask them to provide documents."
|
||||
)
|
||||
|
||||
if doc_index is not None:
|
||||
try:
|
||||
text_attachment = [text_attachment[doc_index]]
|
||||
except IndexError as exc:
|
||||
raise ModelRetry(
|
||||
f"Document index {doc_index} is out of range. "
|
||||
f"There are {len(text_attachment)} documents available."
|
||||
) from exc
|
||||
|
||||
documents = [await read_document_content(doc) for doc in text_attachment]
|
||||
|
||||
# Chunk documents and summarize each chunk
|
||||
@@ -178,4 +202,4 @@ async def document_summarize( # pylint: disable=too-many-locals
|
||||
raise ModelCannotRetry(
|
||||
f"An unexpected error occurred during document summarization: {type(exc).__name__}. "
|
||||
"You must explain this to the user and not try to answer based on your knowledge."
|
||||
) from exc
|
||||
) from exc
|
||||
@@ -1,136 +0,0 @@
|
||||
"""Translation tool used for uploaded documents."""
|
||||
|
||||
import logging
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
from pydantic_ai import RunContext
|
||||
from pydantic_ai.exceptions import ModelRetry
|
||||
from pydantic_ai.messages import ToolReturn
|
||||
|
||||
from chat.agents.translate import TranslationAgent
|
||||
from chat.tools.exceptions import ModelCannotRetry
|
||||
from chat.tools.utils import last_model_retry_soft_fail, read_document_content
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@last_model_retry_soft_fail
|
||||
async def document_translate(
|
||||
ctx: RunContext,
|
||||
*,
|
||||
target_language: str | None = None,
|
||||
instructions: str | None = None,
|
||||
) -> ToolReturn:
|
||||
"""
|
||||
Translate the full content of the last uploaded document into the specified target language.
|
||||
Preserve the original markdown formatting unless the instructions say otherwise.
|
||||
Return this translation directly to the user WITHOUT any modification
|
||||
or additional summarization.
|
||||
The translation is already complete and MUST be presented as-is in the final response.
|
||||
|
||||
If target_language isn't specified or unknown, the target language should be asked
|
||||
to the user.
|
||||
|
||||
Instructions are optional but should reflect the user's request.
|
||||
|
||||
Examples:
|
||||
"Translate this doc to English" -> target_language = "English", instructions = ""
|
||||
"Translate to Spanish, in formal tone" -> target_language = "Spanish",
|
||||
instructions = "Use formal tone"
|
||||
"Traduis ce document en français" -> target_language = "French", instructions = ""
|
||||
"Translate to German, keep technical terms in English" -> target_language = "German",
|
||||
instructions = "Keep technical terms in English"
|
||||
"Translate this" -> ask the user: "Which language would you like the document
|
||||
translated into?"
|
||||
|
||||
Args:
|
||||
target_language (str | None): The language to translate the document into.
|
||||
If None, ask the user.
|
||||
instructions (str | None): Optional instructions for the translation style or preferences
|
||||
"""
|
||||
try:
|
||||
if not target_language:
|
||||
raise ModelCannotRetry(
|
||||
"The target language is not specified. "
|
||||
"You must ask the user which language they want the document translated into."
|
||||
)
|
||||
instructions_hint = (
|
||||
f"Follow these instructions: {instructions.strip()}" if instructions else ""
|
||||
)
|
||||
translation_agent = TranslationAgent()
|
||||
|
||||
# Get the last uploaded text document
|
||||
last_attachment = await (
|
||||
ctx.deps.conversation.attachments.filter(
|
||||
content_type__startswith="text/",
|
||||
)
|
||||
.order_by("-created_at")
|
||||
.afirst()
|
||||
)
|
||||
|
||||
if not last_attachment:
|
||||
raise ModelCannotRetry(
|
||||
"No text documents found in the conversation. "
|
||||
"You must explain this to the user and ask them to provide documents."
|
||||
)
|
||||
|
||||
doc_name, content = await read_document_content(last_attachment)
|
||||
|
||||
max_chars = settings.TRANSLATION_MAX_CHARS
|
||||
if len(content) > max_chars:
|
||||
raise ModelCannotRetry(
|
||||
f"The document is too large to translate ({len(content):,} characters, "
|
||||
f"limit is {max_chars:,}). "
|
||||
"You must explain this to the user, without providing numerical details. "
|
||||
"Suggest them to reduce the document size by summarizing it or "
|
||||
"by splitting it into smaller parts. "
|
||||
"Also offer them to summarize the document in the target language instead, "
|
||||
"which can be a good alternative to translation for large documents."
|
||||
)
|
||||
|
||||
logger.info(
|
||||
"[translate] translating '%s', %s chars, target_language='%s', instructions='%s'",
|
||||
doc_name,
|
||||
len(content),
|
||||
target_language,
|
||||
instructions_hint,
|
||||
)
|
||||
|
||||
# Translate the document directly
|
||||
translate_prompt = (
|
||||
f"You are an agent specializing in text translation. "
|
||||
f"Translate the following document to {target_language}. "
|
||||
f"Preserve all markdown formatting exactly as-is. "
|
||||
f"{instructions_hint}\n\n"
|
||||
f"'''\n{content}\n'''\n\n"
|
||||
f"Respond directly with the translated text only, no commentary."
|
||||
)
|
||||
|
||||
logger.debug("[translate] prompt for '%s'=> %s", doc_name, translate_prompt[:100] + "...")
|
||||
|
||||
try:
|
||||
resp = await translation_agent.run(translate_prompt, usage=ctx.usage)
|
||||
except Exception as exc:
|
||||
logger.warning("Error during translation of '%s': %s", doc_name, exc, exc_info=True)
|
||||
raise ModelRetry(f"An error occurred while translating document '{doc_name}'.") from exc
|
||||
|
||||
translated_text = (resp.output or "").strip()
|
||||
if not translated_text:
|
||||
raise ModelRetry(f"The translation of '{doc_name}' produced an empty result.")
|
||||
|
||||
logger.debug("[translate] final translation length: %s chars", len(translated_text))
|
||||
|
||||
return ToolReturn(
|
||||
return_value=translated_text,
|
||||
metadata={"sources": {doc_name}},
|
||||
)
|
||||
|
||||
except (ModelCannotRetry, ModelRetry):
|
||||
raise
|
||||
except Exception as exc:
|
||||
logger.exception("Unexpected error in document_translate: %s", exc)
|
||||
raise ModelCannotRetry(
|
||||
f"An unexpected error occurred during document translation: {type(exc).__name__}. "
|
||||
"You must explain this to the user and not try to answer based on your knowledge."
|
||||
) from exc
|
||||
@@ -4,9 +4,6 @@ import functools
|
||||
import logging
|
||||
from typing import Any, Callable
|
||||
|
||||
from django.core.files.storage import default_storage
|
||||
|
||||
from asgiref.sync import sync_to_async
|
||||
from pydantic_ai import ModelRetry, RunContext
|
||||
|
||||
from chat.tools.exceptions import ModelCannotRetry
|
||||
@@ -51,10 +48,3 @@ def last_model_retry_soft_fail(
|
||||
raise # Re-raise to allow retrying
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
@sync_to_async
|
||||
def read_document_content(doc):
|
||||
"""Read document content asynchronously."""
|
||||
with default_storage.open(doc.key) as f:
|
||||
return doc.file_name, f.read().decode("utf-8")
|
||||
|
||||
@@ -743,11 +743,6 @@ class Base(BraveSettings, Configuration):
|
||||
environ_name="RAG_DOCUMENT_SEARCH_BACKEND",
|
||||
environ_prefix=None,
|
||||
)
|
||||
RAG_DOCUMENT_PARSER = values.Value(
|
||||
"chat.agent_rag.document_converter.parser.AlbertParser",
|
||||
environ_name="RAG_DOCUMENT_PARSER",
|
||||
environ_prefix=None,
|
||||
)
|
||||
SPECIFIC_RAG_DOCUMENT_SEARCH_TOOLS = values.DictValue(
|
||||
default={},
|
||||
environ_name="SPECIFIC_RAG_DOCUMENT_SEARCH_TOOLS",
|
||||
@@ -813,51 +808,6 @@ USER QUESTION:
|
||||
environ_prefix=None,
|
||||
)
|
||||
|
||||
# OCR settings for AdaptivePdfParser
|
||||
OCR_HRID = values.Value(
|
||||
default="etalab-plateform-mistral-medium-2508",
|
||||
environ_name="OCR_HRID",
|
||||
environ_prefix=None,
|
||||
)
|
||||
# Specific Mistral OCR model - Designates which Mistral vision model to use for OCR
|
||||
OCR_MODEL = values.Value(
|
||||
default="mistral-ocr-2512",
|
||||
environ_name="OCR_MODEL",
|
||||
environ_prefix=None,
|
||||
)
|
||||
OCR_TIMEOUT = values.PositiveIntegerValue(
|
||||
default=240,
|
||||
environ_name="OCR_TIMEOUT",
|
||||
environ_prefix=None,
|
||||
)
|
||||
OCR_MAX_RETRIES = values.PositiveIntegerValue(
|
||||
default=3,
|
||||
environ_name="OCR_MAX_RETRIES",
|
||||
environ_prefix=None,
|
||||
)
|
||||
|
||||
OCR_RETRY_DELAY = values.PositiveIntegerValue(
|
||||
default=5,
|
||||
environ_name="OCR_RETRY_DELAY",
|
||||
environ_prefix=None,
|
||||
)
|
||||
|
||||
OCR_BATCH_PAGES = values.PositiveIntegerValue(
|
||||
default=10,
|
||||
environ_name="OCR_BATCH_PAGES",
|
||||
environ_prefix=None,
|
||||
)
|
||||
MIN_AVG_CHARS_FOR_TEXT_EXTRACTION = values.PositiveIntegerValue(
|
||||
default=200,
|
||||
environ_name="MIN_AVG_CHARS_FOR_TEXT_EXTRACTION",
|
||||
environ_prefix=None,
|
||||
)
|
||||
MIN_TEXT_COVERAGE_FOR_TEXT_EXTRACTION = values.FloatValue(
|
||||
default=0.7,
|
||||
environ_name="MIN_TEXT_COVERAGE_FOR_TEXT_EXTRACTION",
|
||||
environ_prefix=None,
|
||||
)
|
||||
|
||||
# Summarization
|
||||
SUMMARIZATION_SYSTEM_PROMPT = values.Value(
|
||||
(
|
||||
@@ -883,13 +833,6 @@ USER QUESTION:
|
||||
environ_prefix=None,
|
||||
)
|
||||
|
||||
# Translation
|
||||
TRANSLATION_MAX_CHARS = values.PositiveIntegerValue(
|
||||
default=100_000, # ~100k characters, roughly half a 128k context window
|
||||
environ_name="TRANSLATION_MAX_CHARS",
|
||||
environ_prefix=None,
|
||||
)
|
||||
|
||||
# Tavily API
|
||||
TAVILY_API_KEY = values.Value(
|
||||
None, # Tavily API key is not set by default
|
||||
@@ -1171,17 +1114,6 @@ USER QUESTION:
|
||||
"OIDC_STORE_ACCESS_TOKEN and OIDC_STORE_REFRESH_TOKEN to be set."
|
||||
)
|
||||
|
||||
# OCR configuration validation
|
||||
# Note: we call load_llm_configuration directly because LLM_CONFIGURATIONS is a
|
||||
# @property returning a lazy object, which cannot be accessed via cls in a classmethod.
|
||||
if cls.RAG_DOCUMENT_PARSER == "chat.agent_rag.document_converter.parser.AdaptivePdfParser":
|
||||
llm_configs = load_llm_configuration(cls._llm_configuration_file_path)
|
||||
if cls.OCR_HRID not in llm_configs:
|
||||
raise ValueError(
|
||||
f"OCR_HRID '{cls.OCR_HRID}' not found in LLM_CONFIGURATIONS. "
|
||||
"Please add a matching provider entry or set OCR_HRID to an existing key."
|
||||
)
|
||||
|
||||
# Langfuse initialization
|
||||
if cls.LANGFUSE_ENABLED:
|
||||
if not cls.LANGFUSE_MEDIA_UPLOAD_ENABLED:
|
||||
|
||||
@@ -3,7 +3,7 @@ msgstr ""
|
||||
"Project-Id-Version: la-suite-conversations\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-10-20 21:48+0000\n"
|
||||
"PO-Revision-Date: 2026-02-09 13:05\n"
|
||||
"PO-Revision-Date: 2026-01-27 15:38\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: German\n"
|
||||
"Language: de_DE\n"
|
||||
|
||||
@@ -3,7 +3,7 @@ msgstr ""
|
||||
"Project-Id-Version: la-suite-conversations\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-10-20 21:48+0000\n"
|
||||
"PO-Revision-Date: 2026-02-09 13:05\n"
|
||||
"PO-Revision-Date: 2026-01-27 15:38\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: English\n"
|
||||
"Language: en_US\n"
|
||||
|
||||
@@ -3,7 +3,7 @@ msgstr ""
|
||||
"Project-Id-Version: la-suite-conversations\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-10-20 21:48+0000\n"
|
||||
"PO-Revision-Date: 2026-02-09 13:05\n"
|
||||
"PO-Revision-Date: 2026-01-27 15:38\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: French\n"
|
||||
"Language: fr_FR\n"
|
||||
|
||||
@@ -3,7 +3,7 @@ msgstr ""
|
||||
"Project-Id-Version: la-suite-conversations\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-10-20 21:48+0000\n"
|
||||
"PO-Revision-Date: 2026-02-09 13:05\n"
|
||||
"PO-Revision-Date: 2026-01-27 15:38\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Dutch\n"
|
||||
"Language: nl_NL\n"
|
||||
|
||||
@@ -3,7 +3,7 @@ msgstr ""
|
||||
"Project-Id-Version: la-suite-conversations\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-10-20 21:48+0000\n"
|
||||
"PO-Revision-Date: 2026-02-09 13:05\n"
|
||||
"PO-Revision-Date: 2026-01-27 15:38\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Russian\n"
|
||||
"Language: ru_RU\n"
|
||||
|
||||
@@ -3,7 +3,7 @@ msgstr ""
|
||||
"Project-Id-Version: la-suite-conversations\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-10-20 21:48+0000\n"
|
||||
"PO-Revision-Date: 2026-02-09 13:05\n"
|
||||
"PO-Revision-Date: 2026-01-27 15:38\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Ukrainian\n"
|
||||
"Language: uk_UA\n"
|
||||
|
||||
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "conversations"
|
||||
version = "0.0.13"
|
||||
version = "0.0.12"
|
||||
authors = [{ "name" = "DINUM", "email" = "dev@mail.numerique.gouv.fr" }]
|
||||
classifiers = [
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
@@ -39,7 +39,7 @@ dependencies = [
|
||||
"django-redis==6.0.0",
|
||||
"django-storages[s3]==1.14.6",
|
||||
"django-timezone-field>=5.1",
|
||||
"django==5.2.11",
|
||||
"django==5.2.9",
|
||||
"djangorestframework==3.16.1",
|
||||
"drf_spectacular==0.29.0",
|
||||
"dockerflow==2024.4.2",
|
||||
@@ -56,7 +56,7 @@ dependencies = [
|
||||
"nested-multipart-parser==1.6.0",
|
||||
"posthog==7.0.0",
|
||||
"pydantic==2.12.4",
|
||||
"pydantic-ai-slim[openai,mistral,mcp,evals,logfire]==1.56.0",
|
||||
"pydantic-ai-slim[openai,mistral,mcp,evals,logfire]==1.17.0",
|
||||
"psycopg[binary]==3.2.12",
|
||||
"PyJWT==2.10.1",
|
||||
"python-magic==0.4.27",
|
||||
@@ -67,7 +67,6 @@ dependencies = [
|
||||
"trafilatura==2.0.0",
|
||||
"uvicorn==0.38.0",
|
||||
"whitenoise==6.11.0",
|
||||
"pypdf>=6.6.2",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
@@ -108,11 +107,6 @@ zip-safe = true
|
||||
[tool.distutils.bdist_wheel]
|
||||
universal = true
|
||||
|
||||
[tool.uv]
|
||||
override-dependencies = [
|
||||
"cryptography>=46.0.5", # CVE-2026-26007
|
||||
]
|
||||
|
||||
[tool.uv.build-backend]
|
||||
module-root = ""
|
||||
source-exclude = [
|
||||
@@ -152,8 +146,8 @@ select = [
|
||||
]
|
||||
|
||||
[tool.ruff.lint.isort]
|
||||
section-order = ["future", "standard-library", "django", "third-party", "conversations", "first-party", "local-folder"]
|
||||
sections = { conversations = ["core"], django = ["django"] }
|
||||
section-order = ["future","standard-library","django","third-party","conversations","first-party","local-folder"]
|
||||
sections = { conversations=["core"], django=["django"] }
|
||||
extra-standard-library = ["tomllib"]
|
||||
|
||||
[tool.ruff.lint.per-file-ignores]
|
||||
|
||||
Generated
+47
-86
@@ -7,9 +7,6 @@ resolution-markers = [
|
||||
"sys_platform != 'emscripten' and sys_platform != 'win32'",
|
||||
]
|
||||
|
||||
[manifest]
|
||||
overrides = [{ name = "cryptography", specifier = ">=46.0.5" }]
|
||||
|
||||
[[package]]
|
||||
name = "amqp"
|
||||
version = "5.3.1"
|
||||
@@ -394,7 +391,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "conversations"
|
||||
version = "0.0.13"
|
||||
version = "0.0.12"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "beautifulsoup4" },
|
||||
@@ -431,7 +428,6 @@ dependencies = [
|
||||
{ name = "pydantic" },
|
||||
{ name = "pydantic-ai-slim", extra = ["evals", "logfire", "mcp", "mistral", "openai"] },
|
||||
{ name = "pyjwt" },
|
||||
{ name = "pypdf" },
|
||||
{ name = "python-magic" },
|
||||
{ name = "redis" },
|
||||
{ name = "requests" },
|
||||
@@ -474,7 +470,7 @@ requires-dist = [
|
||||
{ name = "brotli", specifier = "==1.2.0" },
|
||||
{ name = "deprecated" },
|
||||
{ name = "dirty-equals", marker = "extra == 'dev'", specifier = "==0.10.0" },
|
||||
{ name = "django", specifier = "==5.2.11" },
|
||||
{ name = "django", specifier = "==5.2.9" },
|
||||
{ name = "django-configurations", specifier = "==2.5.1" },
|
||||
{ name = "django-cors-headers", specifier = "==4.9.0" },
|
||||
{ name = "django-countries", specifier = "==8.1.0" },
|
||||
@@ -508,13 +504,12 @@ requires-dist = [
|
||||
{ name = "posthog", specifier = "==7.0.0" },
|
||||
{ name = "psycopg", extras = ["binary"], specifier = "==3.2.12" },
|
||||
{ name = "pydantic", specifier = "==2.12.4" },
|
||||
{ name = "pydantic-ai-slim", extras = ["openai", "mistral", "mcp", "evals", "logfire"], specifier = "==1.56.0" },
|
||||
{ name = "pydantic-ai-slim", extras = ["openai", "mistral", "mcp", "evals", "logfire"], specifier = "==1.17.0" },
|
||||
{ name = "pyfakefs", marker = "extra == 'dev'", specifier = "==5.10.2" },
|
||||
{ name = "pyjwt", specifier = "==2.10.1" },
|
||||
{ name = "pylint", marker = "extra == 'dev'", specifier = "==3.3.9" },
|
||||
{ name = "pylint-django", marker = "extra == 'dev'", specifier = "==2.6.1" },
|
||||
{ name = "pylint-pydantic", marker = "extra == 'dev'", specifier = "==0.4.1" },
|
||||
{ name = "pypdf", specifier = ">=6.6.2" },
|
||||
{ name = "pytest", marker = "extra == 'dev'", specifier = "==9.0.1" },
|
||||
{ name = "pytest-asyncio", marker = "extra == 'dev'", specifier = "==1.3.0" },
|
||||
{ name = "pytest-cov", marker = "extra == 'dev'", specifier = "==7.0.0" },
|
||||
@@ -587,41 +582,43 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "cryptography"
|
||||
version = "46.0.5"
|
||||
version = "46.0.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/60/04/ee2a9e8542e4fa2773b81771ff8349ff19cdd56b7258a0cc442639052edb/cryptography-46.0.5.tar.gz", hash = "sha256:abace499247268e3757271b2f1e244b36b06f8515cf27c4d49468fc9eb16e93d", size = 750064, upload-time = "2026-02-10T19:18:38.255Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/9f/33/c00162f49c0e2fe8064a62cb92b93e50c74a72bc370ab92f86112b33ff62/cryptography-46.0.3.tar.gz", hash = "sha256:a8b17438104fed022ce745b362294d9ce35b4c2e45c1d958ad4a4b019285f4a1", size = 749258, upload-time = "2025-10-15T23:18:31.74Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f7/81/b0bb27f2ba931a65409c6b8a8b358a7f03c0e46eceacddff55f7c84b1f3b/cryptography-46.0.5-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:351695ada9ea9618b3500b490ad54c739860883df6c1f555e088eaf25b1bbaad", size = 7176289, upload-time = "2026-02-10T19:17:08.274Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/9e/6b4397a3e3d15123de3b1806ef342522393d50736c13b20ec4c9ea6693a6/cryptography-46.0.5-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c18ff11e86df2e28854939acde2d003f7984f721eba450b56a200ad90eeb0e6b", size = 4275637, upload-time = "2026-02-10T19:17:10.53Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/63/e7/471ab61099a3920b0c77852ea3f0ea611c9702f651600397ac567848b897/cryptography-46.0.5-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d7e3d356b8cd4ea5aff04f129d5f66ebdc7b6f8eae802b93739ed520c47c79b", size = 4424742, upload-time = "2026-02-10T19:17:12.388Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/37/53/a18500f270342d66bf7e4d9f091114e31e5ee9e7375a5aba2e85a91e0044/cryptography-46.0.5-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:50bfb6925eff619c9c023b967d5b77a54e04256c4281b0e21336a130cd7fc263", size = 4277528, upload-time = "2026-02-10T19:17:13.853Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/22/29/c2e812ebc38c57b40e7c583895e73c8c5adb4d1e4a0cc4c5a4fdab2b1acc/cryptography-46.0.5-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:803812e111e75d1aa73690d2facc295eaefd4439be1023fefc4995eaea2af90d", size = 4947993, upload-time = "2026-02-10T19:17:15.618Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6b/e7/237155ae19a9023de7e30ec64e5d99a9431a567407ac21170a046d22a5a3/cryptography-46.0.5-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ee190460e2fbe447175cda91b88b84ae8322a104fc27766ad09428754a618ed", size = 4456855, upload-time = "2026-02-10T19:17:17.221Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/87/fc628a7ad85b81206738abbd213b07702bcbdada1dd43f72236ef3cffbb5/cryptography-46.0.5-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:f145bba11b878005c496e93e257c1e88f154d278d2638e6450d17e0f31e558d2", size = 3984635, upload-time = "2026-02-10T19:17:18.792Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/84/29/65b55622bde135aedf4565dc509d99b560ee4095e56989e815f8fd2aa910/cryptography-46.0.5-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e9251e3be159d1020c4030bd2e5f84d6a43fe54b6c19c12f51cde9542a2817b2", size = 4277038, upload-time = "2026-02-10T19:17:20.256Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/36/45e76c68d7311432741faf1fbf7fac8a196a0a735ca21f504c75d37e2558/cryptography-46.0.5-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:47fb8a66058b80e509c47118ef8a75d14c455e81ac369050f20ba0d23e77fee0", size = 4912181, upload-time = "2026-02-10T19:17:21.825Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/1a/c1ba8fead184d6e3d5afcf03d569acac5ad063f3ac9fb7258af158f7e378/cryptography-46.0.5-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:4c3341037c136030cb46e4b1e17b7418ea4cbd9dd207e4a6f3b2b24e0d4ac731", size = 4456482, upload-time = "2026-02-10T19:17:25.133Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/e5/3fb22e37f66827ced3b902cf895e6a6bc1d095b5b26be26bd13c441fdf19/cryptography-46.0.5-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:890bcb4abd5a2d3f852196437129eb3667d62630333aacc13dfd470fad3aaa82", size = 4405497, upload-time = "2026-02-10T19:17:26.66Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/df/9d58bb32b1121a8a2f27383fabae4d63080c7ca60b9b5c88be742be04ee7/cryptography-46.0.5-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:80a8d7bfdf38f87ca30a5391c0c9ce4ed2926918e017c29ddf643d0ed2778ea1", size = 4667819, upload-time = "2026-02-10T19:17:28.569Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ea/ed/325d2a490c5e94038cdb0117da9397ece1f11201f425c4e9c57fe5b9f08b/cryptography-46.0.5-cp311-abi3-win32.whl", hash = "sha256:60ee7e19e95104d4c03871d7d7dfb3d22ef8a9b9c6778c94e1c8fcc8365afd48", size = 3028230, upload-time = "2026-02-10T19:17:30.518Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/5a/ac0f49e48063ab4255d9e3b79f5def51697fce1a95ea1370f03dc9db76f6/cryptography-46.0.5-cp311-abi3-win_amd64.whl", hash = "sha256:38946c54b16c885c72c4f59846be9743d699eee2b69b6988e0a00a01f46a61a4", size = 3480909, upload-time = "2026-02-10T19:17:32.083Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e2/fa/a66aa722105ad6a458bebd64086ca2b72cdd361fed31763d20390f6f1389/cryptography-46.0.5-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:4108d4c09fbbf2789d0c926eb4152ae1760d5a2d97612b92d508d96c861e4d31", size = 7170514, upload-time = "2026-02-10T19:17:56.267Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0f/04/c85bdeab78c8bc77b701bf0d9bdcf514c044e18a46dcff330df5448631b0/cryptography-46.0.5-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d1f30a86d2757199cb2d56e48cce14deddf1f9c95f1ef1b64ee91ea43fe2e18", size = 4275349, upload-time = "2026-02-10T19:17:58.419Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/32/9b87132a2f91ee7f5223b091dc963055503e9b442c98fc0b8a5ca765fab0/cryptography-46.0.5-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:039917b0dc418bb9f6edce8a906572d69e74bd330b0b3fea4f79dab7f8ddd235", size = 4420667, upload-time = "2026-02-10T19:18:00.619Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/a6/a7cb7010bec4b7c5692ca6f024150371b295ee1c108bdc1c400e4c44562b/cryptography-46.0.5-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ba2a27ff02f48193fc4daeadf8ad2590516fa3d0adeeb34336b96f7fa64c1e3a", size = 4276980, upload-time = "2026-02-10T19:18:02.379Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8e/7c/c4f45e0eeff9b91e3f12dbd0e165fcf2a38847288fcfd889deea99fb7b6d/cryptography-46.0.5-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:61aa400dce22cb001a98014f647dc21cda08f7915ceb95df0c9eaf84b4b6af76", size = 4939143, upload-time = "2026-02-10T19:18:03.964Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/37/19/e1b8f964a834eddb44fa1b9a9976f4e414cbb7aa62809b6760c8803d22d1/cryptography-46.0.5-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ce58ba46e1bc2aac4f7d9290223cead56743fa6ab94a5d53292ffaac6a91614", size = 4453674, upload-time = "2026-02-10T19:18:05.588Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/db/ed/db15d3956f65264ca204625597c410d420e26530c4e2943e05a0d2f24d51/cryptography-46.0.5-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:420d0e909050490d04359e7fdb5ed7e667ca5c3c402b809ae2563d7e66a92229", size = 3978801, upload-time = "2026-02-10T19:18:07.167Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/e2/df40a31d82df0a70a0daf69791f91dbb70e47644c58581d654879b382d11/cryptography-46.0.5-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:582f5fcd2afa31622f317f80426a027f30dc792e9c80ffee87b993200ea115f1", size = 4276755, upload-time = "2026-02-10T19:18:09.813Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/33/45/726809d1176959f4a896b86907b98ff4391a8aa29c0aaaf9450a8a10630e/cryptography-46.0.5-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:bfd56bb4b37ed4f330b82402f6f435845a5f5648edf1ad497da51a8452d5d62d", size = 4901539, upload-time = "2026-02-10T19:18:11.263Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/0f/a3076874e9c88ecb2ecc31382f6e7c21b428ede6f55aafa1aa272613e3cd/cryptography-46.0.5-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:a3d507bb6a513ca96ba84443226af944b0f7f47dcc9a399d110cd6146481d24c", size = 4452794, upload-time = "2026-02-10T19:18:12.914Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/02/ef/ffeb542d3683d24194a38f66ca17c0a4b8bf10631feef44a7ef64e631b1a/cryptography-46.0.5-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9f16fbdf4da055efb21c22d81b89f155f02ba420558db21288b3d0035bafd5f4", size = 4404160, upload-time = "2026-02-10T19:18:14.375Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/96/93/682d2b43c1d5f1406ed048f377c0fc9fc8f7b0447a478d5c65ab3d3a66eb/cryptography-46.0.5-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:ced80795227d70549a411a4ab66e8ce307899fad2220ce5ab2f296e687eacde9", size = 4667123, upload-time = "2026-02-10T19:18:15.886Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/45/2d/9c5f2926cb5300a8eefc3f4f0b3f3df39db7f7ce40c8365444c49363cbda/cryptography-46.0.5-cp38-abi3-win32.whl", hash = "sha256:02f547fce831f5096c9a567fd41bc12ca8f11df260959ecc7c3202555cc47a72", size = 3010220, upload-time = "2026-02-10T19:18:17.361Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/ef/0c2f4a8e31018a986949d34a01115dd057bf536905dca38897bacd21fac3/cryptography-46.0.5-cp38-abi3-win_amd64.whl", hash = "sha256:556e106ee01aa13484ce9b0239bca667be5004efb0aabbed28d353df86445595", size = 3467050, upload-time = "2026-02-10T19:18:18.899Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1d/42/9c391dd801d6cf0d561b5890549d4b27bafcc53b39c31a817e69d87c625b/cryptography-46.0.3-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:109d4ddfadf17e8e7779c39f9b18111a09efb969a301a31e987416a0191ed93a", size = 7225004, upload-time = "2025-10-15T23:16:52.239Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1c/67/38769ca6b65f07461eb200e85fc1639b438bdc667be02cf7f2cd6a64601c/cryptography-46.0.3-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:09859af8466b69bc3c27bdf4f5d84a665e0f7ab5088412e9e2ec49758eca5cbc", size = 4296667, upload-time = "2025-10-15T23:16:54.369Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/49/498c86566a1d80e978b42f0d702795f69887005548c041636df6ae1ca64c/cryptography-46.0.3-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:01ca9ff2885f3acc98c29f1860552e37f6d7c7d013d7334ff2a9de43a449315d", size = 4450807, upload-time = "2025-10-15T23:16:56.414Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/0a/863a3604112174c8624a2ac3c038662d9e59970c7f926acdcfaed8d61142/cryptography-46.0.3-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6eae65d4c3d33da080cff9c4ab1f711b15c1d9760809dad6ea763f3812d254cb", size = 4299615, upload-time = "2025-10-15T23:16:58.442Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/64/02/b73a533f6b64a69f3cd3872acb6ebc12aef924d8d103133bb3ea750dc703/cryptography-46.0.3-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5bf0ed4490068a2e72ac03d786693adeb909981cc596425d09032d372bcc849", size = 4016800, upload-time = "2025-10-15T23:17:00.378Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/25/d5/16e41afbfa450cde85a3b7ec599bebefaef16b5c6ba4ec49a3532336ed72/cryptography-46.0.3-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5ecfccd2329e37e9b7112a888e76d9feca2347f12f37918facbb893d7bb88ee8", size = 4984707, upload-time = "2025-10-15T23:17:01.98Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/56/e7e69b427c3878352c2fb9b450bd0e19ed552753491d39d7d0a2f5226d41/cryptography-46.0.3-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a2c0cd47381a3229c403062f764160d57d4d175e022c1df84e168c6251a22eec", size = 4482541, upload-time = "2025-10-15T23:17:04.078Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/f6/50736d40d97e8483172f1bb6e698895b92a223dba513b0ca6f06b2365339/cryptography-46.0.3-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:549e234ff32571b1f4076ac269fcce7a808d3bf98b76c8dd560e42dbc66d7d91", size = 4299464, upload-time = "2025-10-15T23:17:05.483Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/de/d8e26b1a855f19d9994a19c702fa2e93b0456beccbcfe437eda00e0701f2/cryptography-46.0.3-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:c0a7bb1a68a5d3471880e264621346c48665b3bf1c3759d682fc0864c540bd9e", size = 4950838, upload-time = "2025-10-15T23:17:07.425Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8f/29/798fc4ec461a1c9e9f735f2fc58741b0daae30688f41b2497dcbc9ed1355/cryptography-46.0.3-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:10b01676fc208c3e6feeb25a8b83d81767e8059e1fe86e1dc62d10a3018fa926", size = 4481596, upload-time = "2025-10-15T23:17:09.343Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/15/8d/03cd48b20a573adfff7652b76271078e3045b9f49387920e7f1f631d125e/cryptography-46.0.3-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0abf1ffd6e57c67e92af68330d05760b7b7efb243aab8377e583284dbab72c71", size = 4426782, upload-time = "2025-10-15T23:17:11.22Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/b1/ebacbfe53317d55cf33165bda24c86523497a6881f339f9aae5c2e13e57b/cryptography-46.0.3-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a04bee9ab6a4da801eb9b51f1b708a1b5b5c9eb48c03f74198464c66f0d344ac", size = 4698381, upload-time = "2025-10-15T23:17:12.829Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/96/92/8a6a9525893325fc057a01f654d7efc2c64b9de90413adcf605a85744ff4/cryptography-46.0.3-cp311-abi3-win32.whl", hash = "sha256:f260d0d41e9b4da1ed1e0f1ce571f97fe370b152ab18778e9e8f67d6af432018", size = 3055988, upload-time = "2025-10-15T23:17:14.65Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/bf/80fbf45253ea585a1e492a6a17efcb93467701fa79e71550a430c5e60df0/cryptography-46.0.3-cp311-abi3-win_amd64.whl", hash = "sha256:a9a3008438615669153eb86b26b61e09993921ebdd75385ddd748702c5adfddb", size = 3514451, upload-time = "2025-10-15T23:17:16.142Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2e/af/9b302da4c87b0beb9db4e756386a7c6c5b8003cd0e742277888d352ae91d/cryptography-46.0.3-cp311-abi3-win_arm64.whl", hash = "sha256:5d7f93296ee28f68447397bf5198428c9aeeab45705a55d53a6343455dcb2c3c", size = 2928007, upload-time = "2025-10-15T23:17:18.04Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/23/45fe7f376a7df8daf6da3556603b36f53475a99ce4faacb6ba2cf3d82021/cryptography-46.0.3-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:cb3d760a6117f621261d662bccc8ef5bc32ca673e037c83fbe565324f5c46936", size = 7218248, upload-time = "2025-10-15T23:17:46.294Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/27/32/b68d27471372737054cbd34c84981f9edbc24fe67ca225d389799614e27f/cryptography-46.0.3-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4b7387121ac7d15e550f5cb4a43aef2559ed759c35df7336c402bb8275ac9683", size = 4294089, upload-time = "2025-10-15T23:17:48.269Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/26/42/fa8389d4478368743e24e61eea78846a0006caffaf72ea24a15159215a14/cryptography-46.0.3-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:15ab9b093e8f09daab0f2159bb7e47532596075139dd74365da52ecc9cb46c5d", size = 4440029, upload-time = "2025-10-15T23:17:49.837Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/eb/f483db0ec5ac040824f269e93dd2bd8a21ecd1027e77ad7bdf6914f2fd80/cryptography-46.0.3-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:46acf53b40ea38f9c6c229599a4a13f0d46a6c3fa9ef19fc1a124d62e338dfa0", size = 4297222, upload-time = "2025-10-15T23:17:51.357Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/cf/da9502c4e1912cb1da3807ea3618a6829bee8207456fbbeebc361ec38ba3/cryptography-46.0.3-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10ca84c4668d066a9878890047f03546f3ae0a6b8b39b697457b7757aaf18dbc", size = 4012280, upload-time = "2025-10-15T23:17:52.964Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6b/8f/9adb86b93330e0df8b3dcf03eae67c33ba89958fc2e03862ef1ac2b42465/cryptography-46.0.3-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:36e627112085bb3b81b19fed209c05ce2a52ee8b15d161b7c643a7d5a88491f3", size = 4978958, upload-time = "2025-10-15T23:17:54.965Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/a0/5fa77988289c34bdb9f913f5606ecc9ada1adb5ae870bd0d1054a7021cc4/cryptography-46.0.3-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1000713389b75c449a6e979ffc7dcc8ac90b437048766cef052d4d30b8220971", size = 4473714, upload-time = "2025-10-15T23:17:56.754Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/14/e5/fc82d72a58d41c393697aa18c9abe5ae1214ff6f2a5c18ac470f92777895/cryptography-46.0.3-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:b02cf04496f6576afffef5ddd04a0cb7d49cf6be16a9059d793a30b035f6b6ac", size = 4296970, upload-time = "2025-10-15T23:17:58.588Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/06/5663ed35438d0b09056973994f1aec467492b33bd31da36e468b01ec1097/cryptography-46.0.3-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:71e842ec9bc7abf543b47cf86b9a743baa95f4677d22baa4c7d5c69e49e9bc04", size = 4940236, upload-time = "2025-10-15T23:18:00.897Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/59/873633f3f2dcd8a053b8dd1d38f783043b5fce589c0f6988bf55ef57e43e/cryptography-46.0.3-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:402b58fc32614f00980b66d6e56a5b4118e6cb362ae8f3fda141ba4689bd4506", size = 4472642, upload-time = "2025-10-15T23:18:02.749Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/39/8e71f3930e40f6877737d6f69248cf74d4e34b886a3967d32f919cc50d3b/cryptography-46.0.3-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ef639cb3372f69ec44915fafcd6698b6cc78fbe0c2ea41be867f6ed612811963", size = 4423126, upload-time = "2025-10-15T23:18:04.85Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cd/c7/f65027c2810e14c3e7268353b1681932b87e5a48e65505d8cc17c99e36ae/cryptography-46.0.3-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3b51b8ca4f1c6453d8829e1eb7299499ca7f313900dd4d89a24b8b87c0a780d4", size = 4686573, upload-time = "2025-10-15T23:18:06.908Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/6e/1c8331ddf91ca4730ab3086a0f1be19c65510a33b5a441cb334e7a2d2560/cryptography-46.0.3-cp38-abi3-win32.whl", hash = "sha256:6276eb85ef938dc035d59b87c8a7dc559a232f954962520137529d77b18ff1df", size = 3036695, upload-time = "2025-10-15T23:18:08.672Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/90/45/b0d691df20633eff80955a0fc7695ff9051ffce8b69741444bd9ed7bd0db/cryptography-46.0.3-cp38-abi3-win_amd64.whl", hash = "sha256:416260257577718c05135c55958b674000baef9a1c7d9e8f306ec60d71db850f", size = 3501720, upload-time = "2025-10-15T23:18:10.632Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/cb/2da4cc83f5edb9c3257d09e1e7ab7b23f049c7962cae8d842bbef0a9cec9/cryptography-46.0.3-cp38-abi3-win_arm64.whl", hash = "sha256:d89c3468de4cdc4f08a57e214384d0471911a3830fcdaf7a8cc587e42a866372", size = 2918740, upload-time = "2025-10-15T23:18:12.277Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -698,16 +695,16 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "django"
|
||||
version = "5.2.11"
|
||||
version = "5.2.9"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "asgiref" },
|
||||
{ name = "sqlparse" },
|
||||
{ name = "tzdata", marker = "sys_platform == 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/17/f2/3e57ef696b95067e05ae206171e47a8e53b9c84eec56198671ef9eaa51a6/django-5.2.11.tar.gz", hash = "sha256:7f2d292ad8b9ee35e405d965fbbad293758b858c34bbf7f3df551aeeac6f02d3", size = 10885017, upload-time = "2026-02-03T13:52:50.554Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/eb/1c/188ce85ee380f714b704283013434976df8d3a2df8e735221a02605b6794/django-5.2.9.tar.gz", hash = "sha256:16b5ccfc5e8c27e6c0561af551d2ea32852d7352c67d452ae3e76b4f6b2ca495", size = 10848762, upload-time = "2025-12-02T14:01:08.418Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/91/a7/2b112ab430575bf3135b8304ac372248500d99c352f777485f53fdb9537e/django-5.2.11-py3-none-any.whl", hash = "sha256:e7130df33ada9ab5e5e929bc19346a20fe383f5454acb2cc004508f242ee92c0", size = 8291375, upload-time = "2026-02-03T13:52:42.47Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/17/b0/7f42bfc38b8f19b78546d47147e083ed06e12fc29c42da95655e0962c6c2/django-5.2.9-py3-none-any.whl", hash = "sha256:3a4ea88a70370557ab1930b332fd2887a9f48654261cdffda663fef5976bb00a", size = 8290652, upload-time = "2025-12-02T14:01:03.485Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2164,7 +2161,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "pydantic-ai-slim"
|
||||
version = "1.56.0"
|
||||
version = "1.17.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "genai-prices" },
|
||||
@@ -2175,9 +2172,9 @@ dependencies = [
|
||||
{ name = "pydantic-graph" },
|
||||
{ name = "typing-inspection" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ce/5c/3a577825b9c1da8f287be7f2ee6fe9aab48bc8a80e65c8518052c589f51c/pydantic_ai_slim-1.56.0.tar.gz", hash = "sha256:9f9f9c56b1c735837880a515ae5661b465b40207b25f3a3434178098b2137f05", size = 415265, upload-time = "2026-02-06T01:13:23.58Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/fe/bc/39f1dca02883372ccccd82e55b21a0bf4d248e69f40a22a6e177a285781a/pydantic_ai_slim-1.17.0.tar.gz", hash = "sha256:7c6a10b0842819cd1328dc6d0b64faba4ae59b78d9a97b53910aff1a28108e0a", size = 301616, upload-time = "2025-11-14T00:40:17.329Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/62/4b/34682036528eeb9aaf093c2073540ddf399ab37b99d282a69ca41356f1aa/pydantic_ai_slim-1.56.0-py3-none-any.whl", hash = "sha256:d657e4113485020500b23b7390b0066e2a0277edc7577eaad2290735ca5dd7d5", size = 542270, upload-time = "2026-02-06T01:13:14.918Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/34/16/be20a655d6a9323165306b0889b2f04b985025a7f195c486e4292fd51f74/pydantic_ai_slim-1.17.0-py3-none-any.whl", hash = "sha256:2fc64bad8b6396a2af32c1ff04d73f4cde62ba15c28329cc98dbae47651cad90", size = 401934, upload-time = "2025-11-14T00:40:03.326Z" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
@@ -2195,7 +2192,6 @@ mistral = [
|
||||
]
|
||||
openai = [
|
||||
{ name = "openai" },
|
||||
{ name = "tiktoken" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2225,7 +2221,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "pydantic-evals"
|
||||
version = "1.56.0"
|
||||
version = "1.17.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
@@ -2235,14 +2231,14 @@ dependencies = [
|
||||
{ name = "pyyaml" },
|
||||
{ name = "rich" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/98/f2/8c59284a2978af3fbda45ae3217218eaf8b071207a9290b54b7613983e5d/pydantic_evals-1.56.0.tar.gz", hash = "sha256:206635107127af6a3ee4b1fc8f77af6afb14683615a2d6b3609f79467c1c0d28", size = 47210, upload-time = "2026-02-06T01:13:25.714Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/51/f5/4126cedb4c65e54c13d44b03a9551ed8cf6d4b1c0551e61a2ed9f700c73c/pydantic_evals-1.17.0.tar.gz", hash = "sha256:54e24324fb99b453b27817a8c51510a56282a41bc7968e1e5585355cf0c1aea1", size = 46978, upload-time = "2025-11-14T00:40:18.719Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/89/51/9875d19ff6d584aaeb574aba76b49d931b822546fc60b29c4fc0da98170d/pydantic_evals-1.56.0-py3-none-any.whl", hash = "sha256:d1efb410c97135aabd2a22453b10c981b2b9851985e9354713af67ae0973b7a9", size = 56407, upload-time = "2026-02-06T01:13:17.098Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/33/04/2ddffbd5b45388e7e0f6019670e4cd6cf524fef07597938107f0e6aeb431/pydantic_evals-1.17.0-py3-none-any.whl", hash = "sha256:a7447c99ca86bf68880c3078f1b751c418145a725185bce99b604dab9479bf1a", size = 56134, upload-time = "2025-11-14T00:40:06.793Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pydantic-graph"
|
||||
version = "1.56.0"
|
||||
version = "1.17.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "httpx" },
|
||||
@@ -2250,9 +2246,9 @@ dependencies = [
|
||||
{ name = "pydantic" },
|
||||
{ name = "typing-inspection" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ff/03/f92881cdb12d6f43e60e9bfd602e41c95408f06e2324d3729f7a194e2bcd/pydantic_graph-1.56.0.tar.gz", hash = "sha256:5e22972dbb43dbc379ab9944252ff864019abf3c7d465dcdf572fc8aec9a44a1", size = 58460, upload-time = "2026-02-06T01:13:26.708Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/bd/c8/7d41e6f07d2e81851036552a6a4cd62b100509bfeafceb2e46051beea7b0/pydantic_graph-1.17.0.tar.gz", hash = "sha256:0e673f049fa5e86443ea90f0b79d8e24696b2d49545d31556933a08b9363633b", size = 57983, upload-time = "2025-11-14T00:40:19.876Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/08/07/8c823eb4d196137c123d4d67434e185901d3cbaea3b0c2b7667da84e72c1/pydantic_graph-1.56.0-py3-none-any.whl", hash = "sha256:ec3f0a1d6fcedd4eb9c59fef45079c2ee4d4185878d70dae26440a9c974c6bb3", size = 72346, upload-time = "2026-02-06T01:13:18.792Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0f/6c/a3ccda382ad69d5549da186fe349b17efd1f4ad7bf871584186381a1bc31/pydantic_graph-1.17.0-py3-none-any.whl", hash = "sha256:436e11e8ca5bd6a99e5e2ba50a42f958bfff65aeb8a40aef83ae5da1bb8b5891", size = 72003, upload-time = "2025-11-14T00:40:08.329Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2366,15 +2362,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/ac/5de3c91c7f9354444af251f053cc9953c89cce1defa74b907f67be4f770a/pylint_pydantic-0.4.1-py3-none-any.whl", hash = "sha256:d1b937abe5c346d38de69ee1ada80c93d38ee2356addbabb687e2eb44036ac93", size = 16161, upload-time = "2025-10-27T08:03:33.641Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pypdf"
|
||||
version = "6.6.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b8/bb/a44bab1ac3c54dbcf653d7b8bcdee93dddb2d3bf025a3912cacb8149a2f2/pypdf-6.6.2.tar.gz", hash = "sha256:0a3ea3b3303982333404e22d8f75d7b3144f9cf4b2970b96856391a516f9f016", size = 5281850, upload-time = "2026-01-26T11:57:55.964Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/be/549aaf1dfa4ab4aed29b09703d2fb02c4366fc1f05e880948c296c5764b9/pypdf-6.6.2-py3-none-any.whl", hash = "sha256:44c0c9811cfb3b83b28f1c3d054531d5b8b81abaedee0d8cb403650d023832ba", size = 329132, upload-time = "2026-01-26T11:57:54.099Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pytest"
|
||||
version = "9.0.1"
|
||||
@@ -2890,32 +2877,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/81/0d/13d1d239a25cbfb19e740db83143e95c772a1fe10202dda4b76792b114dd/starlette-0.52.1-py3-none-any.whl", hash = "sha256:0029d43eb3d273bc4f83a08720b4912ea4b071087a3b48db01b7c839f7954d74", size = 74272, upload-time = "2026-01-18T13:34:09.188Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tiktoken"
|
||||
version = "0.12.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "regex" },
|
||||
{ name = "requests" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/7d/ab/4d017d0f76ec3171d469d80fc03dfbb4e48a4bcaddaa831b31d526f05edc/tiktoken-0.12.0.tar.gz", hash = "sha256:b18ba7ee2b093863978fcb14f74b3707cdc8d4d4d3836853ce7ec60772139931", size = 37806, upload-time = "2025-10-06T20:22:45.419Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/00/61/441588ee21e6b5cdf59d6870f86beb9789e532ee9718c251b391b70c68d6/tiktoken-0.12.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:775c2c55de2310cc1bc9a3ad8826761cbdc87770e586fd7b6da7d4589e13dab3", size = 1050802, upload-time = "2025-10-06T20:22:00.96Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/05/dcf94486d5c5c8d34496abe271ac76c5b785507c8eae71b3708f1ad9b45a/tiktoken-0.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a01b12f69052fbe4b080a2cfb867c4de12c704b56178edf1d1d7b273561db160", size = 993995, upload-time = "2025-10-06T20:22:02.788Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/70/5163fe5359b943f8db9946b62f19be2305de8c3d78a16f629d4165e2f40e/tiktoken-0.12.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:01d99484dc93b129cd0964f9d34eee953f2737301f18b3c7257bf368d7615baa", size = 1128948, upload-time = "2025-10-06T20:22:03.814Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/da/c028aa0babf77315e1cef357d4d768800c5f8a6de04d0eac0f377cb619fa/tiktoken-0.12.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:4a1a4fcd021f022bfc81904a911d3df0f6543b9e7627b51411da75ff2fe7a1be", size = 1151986, upload-time = "2025-10-06T20:22:05.173Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/5a/886b108b766aa53e295f7216b509be95eb7d60b166049ce2c58416b25f2a/tiktoken-0.12.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:981a81e39812d57031efdc9ec59fa32b2a5a5524d20d4776574c4b4bd2e9014a", size = 1194222, upload-time = "2025-10-06T20:22:06.265Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/f8/4db272048397636ac7a078d22773dd2795b1becee7bc4922fe6207288d57/tiktoken-0.12.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9baf52f84a3f42eef3ff4e754a0db79a13a27921b457ca9832cf944c6be4f8f3", size = 1255097, upload-time = "2025-10-06T20:22:07.403Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8e/32/45d02e2e0ea2be3a9ed22afc47d93741247e75018aac967b713b2941f8ea/tiktoken-0.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:b8a0cd0c789a61f31bf44851defbd609e8dd1e2c8589c614cc1060940ef1f697", size = 879117, upload-time = "2025-10-06T20:22:08.418Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/76/994fc868f88e016e6d05b0da5ac24582a14c47893f4474c3e9744283f1d5/tiktoken-0.12.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d5f89ea5680066b68bcb797ae85219c72916c922ef0fcdd3480c7d2315ffff16", size = 1050309, upload-time = "2025-10-06T20:22:10.939Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/b8/57ef1456504c43a849821920d582a738a461b76a047f352f18c0b26c6516/tiktoken-0.12.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b4e7ed1c6a7a8a60a3230965bdedba8cc58f68926b835e519341413370e0399a", size = 993712, upload-time = "2025-10-06T20:22:12.115Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/72/90/13da56f664286ffbae9dbcfadcc625439142675845baa62715e49b87b68b/tiktoken-0.12.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:fc530a28591a2d74bce821d10b418b26a094bf33839e69042a6e86ddb7a7fb27", size = 1128725, upload-time = "2025-10-06T20:22:13.541Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/df/4f80030d44682235bdaecd7346c90f67ae87ec8f3df4a3442cb53834f7e4/tiktoken-0.12.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:06a9f4f49884139013b138920a4c393aa6556b2f8f536345f11819389c703ebb", size = 1151875, upload-time = "2025-10-06T20:22:14.559Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/22/1f/ae535223a8c4ef4c0c1192e3f9b82da660be9eb66b9279e95c99288e9dab/tiktoken-0.12.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:04f0e6a985d95913cabc96a741c5ffec525a2c72e9df086ff17ebe35985c800e", size = 1194451, upload-time = "2025-10-06T20:22:15.545Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/a7/f8ead382fce0243cb625c4f266e66c27f65ae65ee9e77f59ea1653b6d730/tiktoken-0.12.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0ee8f9ae00c41770b5f9b0bb1235474768884ae157de3beb5439ca0fd70f3e25", size = 1253794, upload-time = "2025-10-06T20:22:16.624Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/93/e0/6cc82a562bc6365785a3ff0af27a2a092d57c47d7a81d9e2295d8c36f011/tiktoken-0.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:dc2dd125a62cb2b3d858484d6c614d136b5b848976794edfb63688d539b8b93f", size = 878777, upload-time = "2025-10-06T20:22:18.036Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tld"
|
||||
version = "0.13.1"
|
||||
|
||||
@@ -145,8 +145,10 @@ export const commonGlobals = {
|
||||
|
||||
export const whiteLabelGlobals = {
|
||||
colors: {
|
||||
'logo-1': '#4844AD',
|
||||
'logo-2': '#4844AD',
|
||||
'logo-1-light': '#4844AD',
|
||||
'logo-2-light': '#4844AD',
|
||||
'logo-1-dark': '#BEC5F0',
|
||||
'logo-2-dark': '#BEC5F0',
|
||||
'brand-050': '#EEF1FA',
|
||||
'brand-100': '#DDE2F5',
|
||||
'brand-150': '#CED3F1',
|
||||
@@ -839,9 +841,7 @@ const dsfrThemes = getThemesFromGlobals(dsfrGlobals, {
|
||||
overrides: commonTokenOverrides,
|
||||
});
|
||||
|
||||
if (dsfrThemes.dark) {
|
||||
dsfrThemes.dark.globals.components.logo.src =
|
||||
'/assets/logo-gouv-darkmode.svg';
|
||||
if (dsfrThemes.dark?.globals?.colors) {
|
||||
dsfrThemes.dark.globals.colors['logo-1'] = '#95ABFF';
|
||||
dsfrThemes.dark.globals.colors['logo-2'] = '#E78087';
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "app-conversations",
|
||||
"version": "0.0.13",
|
||||
"version": "0.0.12",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
@@ -16,6 +16,7 @@
|
||||
"test:watch": "jest --watch"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ag-media/react-pdf-table": "2.0.3",
|
||||
"@ai-sdk/react": "1.2.12",
|
||||
"@ai-sdk/ui-utils": "1.2.11",
|
||||
"@emoji-mart/data": "1.2.1",
|
||||
@@ -23,10 +24,10 @@
|
||||
"@fontsource/material-icons": "5.2.5",
|
||||
"@gouvfr-lasuite/cunningham-tokens": "^3.1.0",
|
||||
"@gouvfr-lasuite/integration": "1.0.3",
|
||||
"@gouvfr-lasuite/ui-kit": "0.18.7",
|
||||
"@gouvfr-lasuite/ui-kit": "0.18.4",
|
||||
"@openfun/cunningham-react": "4.0.0",
|
||||
"@react-pdf/renderer": "4.3.0",
|
||||
"@sentry/nextjs": "9.26.0",
|
||||
"@shikijs/rehype": "^3.21.0",
|
||||
"@tanstack/react-query": "5.80.5",
|
||||
"canvg": "4.0.3",
|
||||
"clsx": "2.1.1",
|
||||
@@ -36,7 +37,7 @@
|
||||
"i18next": "25.2.1",
|
||||
"i18next-browser-languagedetector": "8.1.0",
|
||||
"idb": "8.0.3",
|
||||
"lodash": "4.17.23",
|
||||
"lodash": "4.17.21",
|
||||
"lottie-react": "^2.4.1",
|
||||
"luxon": "3.6.1",
|
||||
"micromark-extension-llm-math": "3.1.1-20250610",
|
||||
@@ -48,10 +49,12 @@
|
||||
"react-i18next": "15.5.2",
|
||||
"react-intersection-observer": "9.16.0",
|
||||
"react-markdown": "10.1.0",
|
||||
"react-select": "5.10.1",
|
||||
"rehype-katex": "7.0.1",
|
||||
"rehype-pretty-code": "^0.14.1",
|
||||
"remark-gfm": "4.0.1",
|
||||
"remark-math": "6.0.0",
|
||||
"shiki": "^3.21.0",
|
||||
"shiki": "^3.13.0",
|
||||
"styled-components": "6.1.18",
|
||||
"use-debounce": "10.0.4",
|
||||
"zod": "^3.25.67",
|
||||
|
||||
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 34 KiB |
@@ -1,5 +1,4 @@
|
||||
@import url('@gouvfr-lasuite/ui-kit/style');
|
||||
@import url('@gouvfr-lasuite/ui-kit/fonts/Marianne');
|
||||
@import url('./cunningham-tokens.css');
|
||||
|
||||
:root {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
:root {
|
||||
--c--globals--colors--logo-1-light: #377fdb;
|
||||
--c--globals--colors--logo-2-light: #377fdb;
|
||||
--c--globals--colors--logo-1-dark: #c1d6f2;
|
||||
--c--globals--colors--logo-2-dark: #c1d6f2;
|
||||
--c--globals--colors--logo-1-light: #4844ad;
|
||||
--c--globals--colors--logo-2-light: #4844ad;
|
||||
--c--globals--colors--logo-1-dark: #bec5f0;
|
||||
--c--globals--colors--logo-2-dark: #bec5f0;
|
||||
--c--globals--colors--brand-050: #eef1fa;
|
||||
--c--globals--colors--brand-100: #dde2f5;
|
||||
--c--globals--colors--brand-150: #ced3f1;
|
||||
@@ -332,8 +332,6 @@
|
||||
--c--globals--colors--white-900: #f8f8f9e5;
|
||||
--c--globals--colors--white-950: #f8f8f9f2;
|
||||
--c--globals--colors--white-975: #f8f8f9f9;
|
||||
--c--globals--colors--logo-1: #4844ad;
|
||||
--c--globals--colors--logo-2: #4844ad;
|
||||
--c--globals--transitions--ease-in: cubic-bezier(0.32, 0, 0.67, 0);
|
||||
--c--globals--transitions--ease-out: cubic-bezier(0.33, 1, 0.68, 1);
|
||||
--c--globals--transitions--ease-in-out: cubic-bezier(0.65, 0, 0.35, 1);
|
||||
@@ -895,10 +893,10 @@
|
||||
}
|
||||
|
||||
.cunningham-theme--dark {
|
||||
--c--globals--colors--logo-1-light: #377fdb;
|
||||
--c--globals--colors--logo-2-light: #377fdb;
|
||||
--c--globals--colors--logo-1-dark: #c1d6f2;
|
||||
--c--globals--colors--logo-2-dark: #c1d6f2;
|
||||
--c--globals--colors--logo-1-light: #4844ad;
|
||||
--c--globals--colors--logo-2-light: #4844ad;
|
||||
--c--globals--colors--logo-1-dark: #bec5f0;
|
||||
--c--globals--colors--logo-2-dark: #bec5f0;
|
||||
--c--globals--colors--brand-050: #eef1fa;
|
||||
--c--globals--colors--brand-100: #dde2f5;
|
||||
--c--globals--colors--brand-150: #ced3f1;
|
||||
@@ -1228,8 +1226,6 @@
|
||||
--c--globals--colors--white-900: #f8f8f9e5;
|
||||
--c--globals--colors--white-950: #f8f8f9f2;
|
||||
--c--globals--colors--white-975: #f8f8f9f9;
|
||||
--c--globals--colors--logo-1: #4844ad;
|
||||
--c--globals--colors--logo-2: #4844ad;
|
||||
--c--globals--transitions--ease-in: cubic-bezier(0.32, 0, 0.67, 0);
|
||||
--c--globals--transitions--ease-out: cubic-bezier(0.33, 1, 0.68, 1);
|
||||
--c--globals--transitions--ease-in-out: cubic-bezier(0.65, 0, 0.35, 1);
|
||||
@@ -2661,7 +2657,7 @@
|
||||
.cunningham-theme--dsfr-dark {
|
||||
--c--globals--components--la-gaufre: true;
|
||||
--c--globals--components--home-proconnect: true;
|
||||
--c--globals--components--logo--src: /assets/logo-gouv-darkmode.svg;
|
||||
--c--globals--components--logo--src: /assets/logo-gouv.svg;
|
||||
--c--globals--components--logo--widthHeader: 110px;
|
||||
--c--globals--components--logo--widthFooter: 220px;
|
||||
--c--globals--components--logo--alt: gouvernement logo;
|
||||
@@ -4858,14 +4854,6 @@
|
||||
color: var(--c--globals--colors--white-975);
|
||||
}
|
||||
|
||||
.clr-logo-1 {
|
||||
color: var(--c--globals--colors--logo-1);
|
||||
}
|
||||
|
||||
.clr-logo-2 {
|
||||
color: var(--c--globals--colors--logo-2);
|
||||
}
|
||||
|
||||
.bg-logo-1-light {
|
||||
background-color: var(--c--globals--colors--logo-1-light);
|
||||
}
|
||||
@@ -6198,14 +6186,6 @@
|
||||
background-color: var(--c--globals--colors--white-975);
|
||||
}
|
||||
|
||||
.bg-logo-1 {
|
||||
background-color: var(--c--globals--colors--logo-1);
|
||||
}
|
||||
|
||||
.bg-logo-2 {
|
||||
background-color: var(--c--globals--colors--logo-2);
|
||||
}
|
||||
|
||||
.bg-surface-primary {
|
||||
background-color: var(--c--contextuals--background--surface--primary);
|
||||
}
|
||||
|
||||
@@ -3,10 +3,10 @@ export const tokens = {
|
||||
default: {
|
||||
globals: {
|
||||
colors: {
|
||||
'logo-1-light': '#377FDB',
|
||||
'logo-2-light': '#377FDB',
|
||||
'logo-1-dark': '#C1D6F2',
|
||||
'logo-2-dark': '#C1D6F2',
|
||||
'logo-1-light': '#4844AD',
|
||||
'logo-2-light': '#4844AD',
|
||||
'logo-1-dark': '#BEC5F0',
|
||||
'logo-2-dark': '#BEC5F0',
|
||||
'brand-050': '#EEF1FA',
|
||||
'brand-100': '#DDE2F5',
|
||||
'brand-150': '#CED3F1',
|
||||
@@ -336,8 +336,6 @@ export const tokens = {
|
||||
'white-900': '#F8F8F9E5',
|
||||
'white-950': '#F8F8F9F2',
|
||||
'white-975': '#F8F8F9F9',
|
||||
'logo-1': '#4844AD',
|
||||
'logo-2': '#4844AD',
|
||||
},
|
||||
transitions: {
|
||||
'ease-in': 'cubic-bezier(0.32, 0, 0.67, 0)',
|
||||
@@ -549,8 +547,8 @@ export const tokens = {
|
||||
},
|
||||
},
|
||||
content: {
|
||||
logo1: '#377FDB',
|
||||
logo2: '#377FDB',
|
||||
logo1: '#4844AD',
|
||||
logo2: '#4844AD',
|
||||
semantic: {
|
||||
contextual: { primary: '#F8F8F9F2' },
|
||||
overlay: { primary: '#F8F8F9F2' },
|
||||
@@ -683,10 +681,10 @@ export const tokens = {
|
||||
dark: {
|
||||
globals: {
|
||||
colors: {
|
||||
'logo-1-light': '#377FDB',
|
||||
'logo-2-light': '#377FDB',
|
||||
'logo-1-dark': '#C1D6F2',
|
||||
'logo-2-dark': '#C1D6F2',
|
||||
'logo-1-light': '#4844AD',
|
||||
'logo-2-light': '#4844AD',
|
||||
'logo-1-dark': '#BEC5F0',
|
||||
'logo-2-dark': '#BEC5F0',
|
||||
'brand-050': '#EEF1FA',
|
||||
'brand-100': '#DDE2F5',
|
||||
'brand-150': '#CED3F1',
|
||||
@@ -1016,8 +1014,6 @@ export const tokens = {
|
||||
'white-900': '#F8F8F9E5',
|
||||
'white-950': '#F8F8F9F2',
|
||||
'white-975': '#F8F8F9F9',
|
||||
'logo-1': '#4844AD',
|
||||
'logo-2': '#4844AD',
|
||||
},
|
||||
transitions: {
|
||||
'ease-in': 'cubic-bezier(0.32, 0, 0.67, 0)',
|
||||
@@ -1229,8 +1225,8 @@ export const tokens = {
|
||||
},
|
||||
},
|
||||
content: {
|
||||
logo1: '#C1D6F2',
|
||||
logo2: '#C1D6F2',
|
||||
logo1: '#BEC5F0',
|
||||
logo2: '#BEC5F0',
|
||||
semantic: {
|
||||
contextual: { primary: '#1B1B23D9' },
|
||||
overlay: { primary: '#1B1B23D9' },
|
||||
@@ -1880,8 +1876,8 @@ export const tokens = {
|
||||
},
|
||||
},
|
||||
content: {
|
||||
logo1: '#377FDB',
|
||||
logo2: '#377FDB',
|
||||
logo1: '#4844AD',
|
||||
logo2: '#4844AD',
|
||||
semantic: {
|
||||
contextual: { primary: '#F8F8F9F2' },
|
||||
overlay: { primary: '#F8F8F9F2' },
|
||||
@@ -2017,7 +2013,7 @@ export const tokens = {
|
||||
'la-gaufre': true,
|
||||
'home-proconnect': true,
|
||||
logo: {
|
||||
src: '/assets/logo-gouv-darkmode.svg',
|
||||
src: '/assets/logo-gouv.svg',
|
||||
widthHeader: '110px',
|
||||
widthFooter: '220px',
|
||||
alt: 'Gouvernement Logo',
|
||||
@@ -2531,8 +2527,8 @@ export const tokens = {
|
||||
},
|
||||
},
|
||||
content: {
|
||||
logo1: '#C1D6F2',
|
||||
logo2: '#C1D6F2',
|
||||
logo1: '#BEC5F0',
|
||||
logo2: '#BEC5F0',
|
||||
semantic: {
|
||||
contextual: { primary: '#1B1B23D9' },
|
||||
overlay: { primary: '#1B1B23D9' },
|
||||
|
||||
@@ -2,9 +2,6 @@ import merge from 'lodash/merge';
|
||||
import { create } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
|
||||
import { useChatPreferencesStore } from '@/features/chat/stores/useChatPreferencesStore';
|
||||
import { safeLocalStorage } from '@/utils/storages';
|
||||
|
||||
import { tokens } from './cunningham-tokens';
|
||||
|
||||
type Tokens = typeof tokens.themes.default &
|
||||
@@ -29,7 +26,6 @@ interface ThemeStore {
|
||||
setTheme: (theme: Theme) => void;
|
||||
spacingsTokens: Partial<SpacingsTokens>;
|
||||
theme: Theme;
|
||||
baseTheme: Theme; // 'default' or 'dsfr' (not persisted)
|
||||
themeTokens: Partial<Tokens['globals']>;
|
||||
isDarkMode: boolean;
|
||||
toggleDarkMode: () => void;
|
||||
@@ -53,15 +49,6 @@ const getComponentTokens = (
|
||||
const DEFAULT_THEME: Theme = 'default';
|
||||
const defaultTokens = getMergedTokens(DEFAULT_THEME);
|
||||
|
||||
// Helper to get isDarkMode from useChatPreferencesStore
|
||||
const getIsDarkModeFromPreferences = (): boolean => {
|
||||
try {
|
||||
return useChatPreferencesStore.getState().isDarkModePreference ?? false;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const initialState: ThemeStore = {
|
||||
colorsTokens: defaultTokens.globals.colors,
|
||||
componentTokens: getComponentTokens(defaultTokens),
|
||||
@@ -71,9 +58,8 @@ const initialState: ThemeStore = {
|
||||
setTheme: () => {},
|
||||
spacingsTokens: defaultTokens.globals.spacings,
|
||||
theme: DEFAULT_THEME,
|
||||
baseTheme: DEFAULT_THEME,
|
||||
themeTokens: defaultTokens.globals,
|
||||
isDarkMode: getIsDarkModeFromPreferences(),
|
||||
isDarkMode: false,
|
||||
toggleDarkMode: () => {},
|
||||
};
|
||||
|
||||
@@ -82,51 +68,30 @@ export const useCunninghamTheme = create<ThemeStore>()(
|
||||
(set) => ({
|
||||
...initialState,
|
||||
setTheme: (theme: Theme) => {
|
||||
// Extract base theme (default or dsfr)
|
||||
const baseTheme: Theme =
|
||||
theme === 'dark' || theme === 'dsfr-dark'
|
||||
? theme === 'dark'
|
||||
? 'default'
|
||||
: 'dsfr'
|
||||
: theme;
|
||||
|
||||
const isDarkMode =
|
||||
getIsDarkModeFromPreferences() ??
|
||||
(theme === 'dark' || theme === 'dsfr-dark');
|
||||
|
||||
// Apply dark mode based on stored preference or theme
|
||||
const finalTheme: Theme = isDarkMode
|
||||
? baseTheme === 'dsfr'
|
||||
? 'dsfr-dark'
|
||||
: 'dark'
|
||||
: baseTheme;
|
||||
|
||||
const newTokens = getMergedTokens(finalTheme);
|
||||
const newTokens = getMergedTokens(theme);
|
||||
|
||||
set({
|
||||
colorsTokens: newTokens.globals.colors,
|
||||
componentTokens: getComponentTokens(newTokens),
|
||||
contextualTokens: newTokens.contextuals,
|
||||
currentTokens: tokens.themes[finalTheme] as Partial<Tokens>,
|
||||
currentTokens: tokens.themes[theme] as Partial<Tokens>,
|
||||
fontSizesTokens: newTokens.globals.font.sizes,
|
||||
spacingsTokens: newTokens.globals.spacings,
|
||||
theme: finalTheme,
|
||||
baseTheme,
|
||||
theme,
|
||||
themeTokens: newTokens.globals,
|
||||
isDarkMode,
|
||||
isDarkMode: theme === 'dark' || theme === 'dsfr-dark',
|
||||
});
|
||||
},
|
||||
toggleDarkMode: () => {
|
||||
useChatPreferencesStore.getState().toggleDarkModePreferences();
|
||||
|
||||
set((state) => {
|
||||
const newIsDarkMode = getIsDarkModeFromPreferences();
|
||||
const newTheme: Theme = newIsDarkMode
|
||||
? state.baseTheme === 'dsfr'
|
||||
? 'dsfr-dark'
|
||||
: 'dark'
|
||||
: state.baseTheme;
|
||||
|
||||
const newTheme =
|
||||
state.theme === 'default'
|
||||
? 'dark'
|
||||
: state.theme === 'dark'
|
||||
? 'default'
|
||||
: state.theme === 'dsfr'
|
||||
? 'dsfr-dark'
|
||||
: 'dsfr';
|
||||
const newTokens = getMergedTokens(newTheme);
|
||||
|
||||
return {
|
||||
@@ -137,27 +102,15 @@ export const useCunninghamTheme = create<ThemeStore>()(
|
||||
fontSizesTokens: newTokens.globals.font.sizes,
|
||||
spacingsTokens: newTokens.globals.spacings,
|
||||
theme: newTheme,
|
||||
baseTheme: state.baseTheme,
|
||||
themeTokens: newTokens.globals,
|
||||
isDarkMode: newIsDarkMode,
|
||||
isDarkMode: newTheme === 'dark' || newTheme === 'dsfr-dark',
|
||||
};
|
||||
});
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: 'cunningham-theme',
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment
|
||||
storage: safeLocalStorage as any,
|
||||
partialize: (state) => ({ isDarkMode: state.isDarkMode }),
|
||||
onRehydrateStorage: () => (state, error) => {
|
||||
if (error) {
|
||||
console.error('[useCunninghamTheme] Rehydration error:', error);
|
||||
return;
|
||||
}
|
||||
if (state) {
|
||||
state.isDarkMode = getIsDarkModeFromPreferences();
|
||||
}
|
||||
},
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
import { Message, SourceUIPart } from '@ai-sdk/ui-utils';
|
||||
import { Message, SourceUIPart, ToolInvocationUIPart } from '@ai-sdk/ui-utils';
|
||||
import { Modal, ModalSize } from '@openfun/cunningham-react';
|
||||
import 'katex/dist/katex.min.css'; // `rehype-katex` does not import the CSS for you
|
||||
import { useRouter } from 'next/router';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import type { ChangeEvent, FormEvent } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { MarkdownHooks } from 'react-markdown';
|
||||
import rehypeKatex from 'rehype-katex';
|
||||
import rehypePrettyCode from 'rehype-pretty-code';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import remarkMath from 'remark-math';
|
||||
|
||||
import { APIError, errorCauses, fetchAPI } from '@/api';
|
||||
import { Box, Loader, Text } from '@/components';
|
||||
import { Box, Icon, Loader, Text } from '@/components';
|
||||
import { useUploadFile } from '@/features/attachments/hooks/useUploadFile';
|
||||
import { useChat } from '@/features/chat/api/useChat';
|
||||
import { getConversation } from '@/features/chat/api/useConversation';
|
||||
@@ -16,9 +21,13 @@ import {
|
||||
LLMModel,
|
||||
useLLMConfiguration,
|
||||
} from '@/features/chat/api/useLLMConfiguration';
|
||||
import { AttachmentList } from '@/features/chat/components/AttachmentList';
|
||||
import { ChatError } from '@/features/chat/components/ChatError';
|
||||
import { CodeBlock } from '@/features/chat/components/CodeBlock';
|
||||
import { FeedbackButtons } from '@/features/chat/components/FeedbackButtons';
|
||||
import { InputChat } from '@/features/chat/components/InputChat';
|
||||
import { MessageItem } from '@/features/chat/components/MessageItem';
|
||||
import { SourceItemList } from '@/features/chat/components/SourceItemList';
|
||||
import { ToolInvocationItem } from '@/features/chat/components/ToolInvocationItem';
|
||||
import { useClipboard } from '@/hook';
|
||||
import { useResponsiveStore } from '@/stores';
|
||||
|
||||
@@ -280,21 +289,21 @@ export const Chat = ({
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [messages]);
|
||||
|
||||
const openSources = useCallback((messageId: string) => {
|
||||
// Source-parts guard is handled at the call site (MessageItem only shows the button when sourceParts.length > 0),
|
||||
// so we just toggle it here.
|
||||
setIsSourceOpen((prev) => (prev === messageId ? null : messageId));
|
||||
}, []);
|
||||
|
||||
// Memoize the last assistant message index to avoid recalculating in render
|
||||
const lastAssistantMessageIndex = useMemo(() => {
|
||||
return messages.findLastIndex((msg) => msg.role === 'assistant');
|
||||
}, [messages]);
|
||||
|
||||
// Memoize whether this is the first conversation (2 or fewer messages)
|
||||
const isFirstConversationMessage = useMemo(() => {
|
||||
return messages.length <= 2;
|
||||
}, [messages.length]);
|
||||
const openSources = (messageId: string) => {
|
||||
if (isSourceOpen === messageId) {
|
||||
setIsSourceOpen(null);
|
||||
return;
|
||||
}
|
||||
const message = messages.find((msg) => msg.id === messageId);
|
||||
if (message?.parts) {
|
||||
const sourceParts = message.parts.filter(
|
||||
(part): part is SourceUIPart => part.type === 'source',
|
||||
);
|
||||
if (sourceParts.length > 0) {
|
||||
setIsSourceOpen(messageId);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Calculer la hauteur pour le message de streaming
|
||||
const calculateStreamingHeight = useCallback(() => {
|
||||
@@ -633,26 +642,317 @@ export const Chat = ({
|
||||
>
|
||||
{messages.length > 0 && (
|
||||
<Box>
|
||||
{messages.map((message, index) => (
|
||||
<MessageItem
|
||||
key={message.id}
|
||||
message={message}
|
||||
isLastMessage={index === messages.length - 1}
|
||||
isLastAssistantMessage={
|
||||
message.role === 'assistant' &&
|
||||
index === lastAssistantMessageIndex
|
||||
}
|
||||
isFirstConversationMessage={isFirstConversationMessage}
|
||||
streamingMessageHeight={streamingMessageHeight}
|
||||
status={status}
|
||||
conversationId={conversationId}
|
||||
isSourceOpen={isSourceOpen}
|
||||
isMobile={isMobile}
|
||||
onCopyToClipboard={copyToClipboard}
|
||||
onOpenSources={openSources}
|
||||
getMetadata={getMetadata}
|
||||
/>
|
||||
))}
|
||||
{messages.map((message, index) => {
|
||||
const isLastMessage = index === messages.length - 1;
|
||||
const isLastAssistantMessageInConversation =
|
||||
message.role === 'assistant' &&
|
||||
index ===
|
||||
messages.findLastIndex((msg) => msg.role === 'assistant');
|
||||
const isFirstConversationMessage = messages.length <= 2;
|
||||
const shouldApplyStreamingHeight =
|
||||
isLastAssistantMessageInConversation &&
|
||||
isLastMessage &&
|
||||
streamingMessageHeight &&
|
||||
!isFirstConversationMessage;
|
||||
const isCurrentlyStreaming =
|
||||
isLastAssistantMessageInConversation &&
|
||||
(status === 'streaming' || status === 'submitted');
|
||||
|
||||
return (
|
||||
<Box
|
||||
key={message.id}
|
||||
data-message-id={message.id}
|
||||
$css={`
|
||||
display: flex;
|
||||
width: 100%;
|
||||
margin: auto;
|
||||
margin-bottom: ${isLastAssistantMessageInConversation ? '30px' : '0px'};
|
||||
color: var(--c--theme--colors--greyscale-850);
|
||||
padding-left: 12px;
|
||||
padding-right: 12px;
|
||||
max-width: 750px;
|
||||
text-align: left;
|
||||
overflow-wrap: anywhere;
|
||||
flex-direction: ${message.role === 'user' ? 'row-reverse' : 'row'};
|
||||
`}
|
||||
>
|
||||
<Box
|
||||
$display="block"
|
||||
$width={`${message.role === 'user' ? 'auto' : '100%'}`}
|
||||
>
|
||||
{message.experimental_attachments &&
|
||||
message.experimental_attachments.length > 0 && (
|
||||
<Box>
|
||||
<AttachmentList
|
||||
attachments={message.experimental_attachments}
|
||||
isReadOnly={true}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
<Box
|
||||
className={`chatMessage ${message.role === 'user' ? 'chatMessage--user' : 'chatMessage--assistant'}`}
|
||||
style={
|
||||
shouldApplyStreamingHeight
|
||||
? { minHeight: `${streamingMessageHeight}px` }
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{/* Message content */}
|
||||
{message.content && (
|
||||
<Box
|
||||
className="mainContent-chat"
|
||||
data-testid={
|
||||
message.role === 'assistant'
|
||||
? 'assistant-message-content'
|
||||
: undefined
|
||||
}
|
||||
$padding={{ all: 'xxs' }}
|
||||
>
|
||||
<p className="sr-only">
|
||||
{message.role === 'user'
|
||||
? t('You said: ')
|
||||
: t('Assistant IA replied: ')}
|
||||
</p>
|
||||
{message.role === 'user' ? (
|
||||
<Text
|
||||
as="p"
|
||||
$css="white-space: pre-wrap; display: block;"
|
||||
$theme="greyscale"
|
||||
$variation="850"
|
||||
>
|
||||
{message.content}
|
||||
</Text>
|
||||
) : (
|
||||
<MarkdownHooks
|
||||
remarkPlugins={[remarkGfm, remarkMath]}
|
||||
rehypePlugins={[
|
||||
[
|
||||
rehypePrettyCode,
|
||||
{
|
||||
theme: 'github-dark-dimmed',
|
||||
},
|
||||
],
|
||||
rehypeKatex,
|
||||
]}
|
||||
components={{
|
||||
// Custom components for Markdown rendering
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
p: ({ node, ...props }) => (
|
||||
<Text
|
||||
as="p"
|
||||
$css="display: block"
|
||||
$theme="greyscale"
|
||||
$variation="850"
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
a: ({ children, ...props }) => (
|
||||
<a target="_blank" {...props}>
|
||||
{children}
|
||||
</a>
|
||||
),
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
pre: ({ node, children, ...props }) => (
|
||||
<CodeBlock {...props}>{children}</CodeBlock>
|
||||
),
|
||||
}}
|
||||
>
|
||||
{message.content}
|
||||
</MarkdownHooks>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Box $direction="column" $gap="2">
|
||||
{isCurrentlyStreaming &&
|
||||
isLastAssistantMessageInConversation &&
|
||||
status === 'streaming' &&
|
||||
message.parts?.some(
|
||||
(part) =>
|
||||
part.type === 'tool-invocation' &&
|
||||
part.toolInvocation.toolName !==
|
||||
'document_parsing',
|
||||
) && (
|
||||
<Box
|
||||
$direction="row"
|
||||
$align="center"
|
||||
$gap="6px"
|
||||
$width="100%"
|
||||
$maxWidth="750px"
|
||||
$margin={{
|
||||
all: 'auto',
|
||||
top: 'base',
|
||||
bottom: 'md',
|
||||
}}
|
||||
>
|
||||
<Loader />
|
||||
<Text $variation="600" $size="md">
|
||||
{(() => {
|
||||
const toolInvocation = message.parts?.find(
|
||||
(part) =>
|
||||
part.type === 'tool-invocation' &&
|
||||
part.toolInvocation.toolName !==
|
||||
'document_parsing',
|
||||
);
|
||||
if (
|
||||
toolInvocation?.type ===
|
||||
'tool-invocation' &&
|
||||
toolInvocation.toolInvocation.toolName ===
|
||||
'summarize'
|
||||
) {
|
||||
return t('Summarizing...');
|
||||
}
|
||||
return t('Search...');
|
||||
})()}
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
{message.parts
|
||||
?.filter((part) => part.type === 'tool-invocation')
|
||||
.map(
|
||||
(part: ToolInvocationUIPart, partIndex: number) =>
|
||||
part.type === 'tool-invocation' &&
|
||||
isCurrentlyStreaming &&
|
||||
isLastAssistantMessageInConversation ? (
|
||||
<ToolInvocationItem
|
||||
key={`tool-invocation-${partIndex}`}
|
||||
toolInvocation={part.toolInvocation}
|
||||
status={status}
|
||||
hideSearchLoader={true}
|
||||
/>
|
||||
) : null,
|
||||
)}
|
||||
</Box>
|
||||
{message.role === 'assistant' &&
|
||||
!(
|
||||
isLastAssistantMessageInConversation &&
|
||||
status === 'streaming'
|
||||
) && (
|
||||
<Box
|
||||
$css="font-size: 12px;"
|
||||
$direction="row"
|
||||
$align="center"
|
||||
className="clr-content-semantic-neutral-secondary"
|
||||
$justify="space-between"
|
||||
$gap="6px"
|
||||
$margin={{ top: 'base' }}
|
||||
>
|
||||
<Box $direction="row" $gap="4px">
|
||||
<Box
|
||||
$theme="neutral"
|
||||
$variation="secondary"
|
||||
$direction="row"
|
||||
$align="center"
|
||||
$gap="4px"
|
||||
className="c__button c__button--brand c__button--brand--tertiary c__button--nano clr-content-semantic-neutral-secondary"
|
||||
onClick={() => copyToClipboard(message.content)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
copyToClipboard(message.content);
|
||||
}
|
||||
}}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
>
|
||||
<Icon
|
||||
iconName="content_copy"
|
||||
$variation="550"
|
||||
$size="16px"
|
||||
/>
|
||||
{!isMobile && (
|
||||
<Text $theme="neutral" $variation="secondary">
|
||||
{t('Copy')}
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
{message.parts?.some(
|
||||
(part) => part.type === 'source',
|
||||
) &&
|
||||
(() => {
|
||||
const sourceCount =
|
||||
message.parts?.filter(
|
||||
(part) => part.type === 'source',
|
||||
).length || 0;
|
||||
return (
|
||||
<Box
|
||||
$direction="row"
|
||||
$align="center"
|
||||
$gap="4px"
|
||||
className={`c__button c__button--brand c__button--brand--tertiary c__button--nano ${isSourceOpen === message.id ? 'action-chat-button--open' : ''}`}
|
||||
onClick={() => openSources(message.id)}
|
||||
onKeyDown={(e) => {
|
||||
if (
|
||||
e.key === 'Enter' ||
|
||||
e.key === ' '
|
||||
) {
|
||||
e.preventDefault();
|
||||
openSources(message.id);
|
||||
}
|
||||
}}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
>
|
||||
<Icon
|
||||
iconName="book"
|
||||
$theme="greyscale"
|
||||
$variation="550"
|
||||
$size="16px"
|
||||
className="action-chat-button-icon"
|
||||
/>
|
||||
<Text
|
||||
$theme="greyscale"
|
||||
$variation="550"
|
||||
$weight="500"
|
||||
$size="12px"
|
||||
>
|
||||
{t('Show')} {sourceCount}{' '}
|
||||
{sourceCount !== 1
|
||||
? t('sources')
|
||||
: t('source')}
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
})()}
|
||||
</Box>
|
||||
<Box $direction="row" $gap="4px">
|
||||
{/* We should display the button, but disabled if no trace linked */}
|
||||
{conversationId &&
|
||||
message.id &&
|
||||
message.id.startsWith('trace-') && (
|
||||
<FeedbackButtons
|
||||
conversationId={conversationId}
|
||||
messageId={message.id}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
{message.parts &&
|
||||
isSourceOpen === message.id &&
|
||||
(() => {
|
||||
const sourceParts = message.parts.filter(
|
||||
(part): part is SourceUIPart =>
|
||||
part.type === 'source',
|
||||
);
|
||||
return (
|
||||
<Box
|
||||
$css={`
|
||||
animation: fade-in 0.2s ease-out;
|
||||
`}
|
||||
>
|
||||
<SourceItemList
|
||||
parts={sourceParts}
|
||||
getMetadata={getMetadata}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
})()}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
)}
|
||||
{(status !== 'ready' && status !== 'streaming' && status !== 'error') ||
|
||||
|
||||
@@ -71,12 +71,10 @@ export const CodeBlock = ({ children, ...props }: CodeBlockProps) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<figure data-rehype-pretty-code-figure="">
|
||||
<CopyCodeButton onCopy={handleCopy} />
|
||||
<Box ref={preRef} $position="relative" as="pre" {...props}>
|
||||
{children}
|
||||
</Box>
|
||||
</figure>
|
||||
<CopyCodeButton onCopy={handleCopy} />
|
||||
<Box ref={preRef} $position="relative" as="pre" {...props}>
|
||||
{children}
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -213,56 +213,39 @@ export const InputChat = ({
|
||||
}
|
||||
}, [status, input]);
|
||||
|
||||
const validateAndAddFiles = useCallback(
|
||||
(filesToAdd: File[]) => {
|
||||
const acceptedFiles: File[] = [];
|
||||
const rejectedFiles: File[] = [];
|
||||
const handleFilesAccepted = useCallback(
|
||||
(acceptedFiles: File[]) => {
|
||||
setFiles((prev) => {
|
||||
const dt = new DataTransfer();
|
||||
|
||||
filesToAdd.forEach((file) => {
|
||||
if (isFileAccepted(file)) {
|
||||
acceptedFiles.push(file);
|
||||
} else {
|
||||
rejectedFiles.push(file);
|
||||
// Keep existing files
|
||||
if (prev) {
|
||||
Array.from(prev).forEach((f) => dt.items.add(f));
|
||||
}
|
||||
});
|
||||
|
||||
if (rejectedFiles.length > 0) {
|
||||
showToastError();
|
||||
}
|
||||
|
||||
if (acceptedFiles.length > 0) {
|
||||
setFiles((prev) => {
|
||||
const dt = new DataTransfer();
|
||||
|
||||
// Keep existing files
|
||||
if (prev) {
|
||||
Array.from(prev).forEach((f) => dt.items.add(f));
|
||||
// Add new files (avoiding duplicates)
|
||||
acceptedFiles.forEach((f) => {
|
||||
const isDuplicate = Array.from(prev || []).some(
|
||||
(pf) =>
|
||||
pf.name === f.name &&
|
||||
pf.size === f.size &&
|
||||
pf.lastModified === f.lastModified,
|
||||
);
|
||||
if (!isDuplicate) {
|
||||
dt.items.add(f);
|
||||
}
|
||||
|
||||
// Add new files (avoiding duplicates)
|
||||
acceptedFiles.forEach((f) => {
|
||||
const isDuplicate = Array.from(prev || []).some(
|
||||
(pf) =>
|
||||
pf.name === f.name &&
|
||||
pf.size === f.size &&
|
||||
pf.lastModified === f.lastModified,
|
||||
);
|
||||
if (!isDuplicate) {
|
||||
dt.items.add(f);
|
||||
}
|
||||
});
|
||||
|
||||
return dt.files;
|
||||
});
|
||||
}
|
||||
|
||||
return dt.files;
|
||||
});
|
||||
},
|
||||
[isFileAccepted, showToastError, setFiles],
|
||||
[setFiles],
|
||||
);
|
||||
|
||||
const { isDragActive } = useFileDragDrop({
|
||||
enabled: fileUploadEnabled,
|
||||
isFileAccepted,
|
||||
onFilesAccepted: validateAndAddFiles,
|
||||
onFilesAccepted: handleFilesAccepted,
|
||||
onFilesRejected: () => showToastError(),
|
||||
});
|
||||
|
||||
@@ -310,41 +293,6 @@ export const InputChat = ({
|
||||
[],
|
||||
);
|
||||
|
||||
const handlePaste = useCallback(
|
||||
(e: React.ClipboardEvent<HTMLTextAreaElement>) => {
|
||||
if (!fileUploadEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const clipboardData = e.clipboardData;
|
||||
if (!clipboardData) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Due to browser limitations, only one file can be pasted at a time
|
||||
// Check files first (for files from file system)
|
||||
let file: File | null = null;
|
||||
|
||||
if (clipboardData.files && clipboardData.files.length > 0) {
|
||||
file = clipboardData.files[0];
|
||||
} else if (clipboardData.items) {
|
||||
for (let i = 0; i < clipboardData.items.length; i++) {
|
||||
const item = clipboardData.items[i];
|
||||
if (item.kind === 'file') {
|
||||
file = item.getAsFile();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (file) {
|
||||
e.preventDefault();
|
||||
validateAndAddFiles([file]);
|
||||
}
|
||||
},
|
||||
[fileUploadEnabled, validateAndAddFiles],
|
||||
);
|
||||
|
||||
const handleAttachClick = useCallback(() => {
|
||||
fileInputRef.current?.click();
|
||||
}, []);
|
||||
@@ -361,10 +309,46 @@ export const InputChat = ({
|
||||
return;
|
||||
}
|
||||
|
||||
validateAndAddFiles(Array.from(fileList));
|
||||
const acceptedFiles: File[] = [];
|
||||
const rejectedFiles: string[] = [];
|
||||
|
||||
Array.from(fileList).forEach((file) => {
|
||||
if (isFileAccepted(file)) {
|
||||
acceptedFiles.push(file);
|
||||
} else {
|
||||
rejectedFiles.push(file.name);
|
||||
}
|
||||
});
|
||||
|
||||
if (rejectedFiles.length > 0) {
|
||||
showToastError();
|
||||
}
|
||||
|
||||
if (acceptedFiles.length > 0) {
|
||||
setFiles((prev) => {
|
||||
const dt = new DataTransfer();
|
||||
if (prev) {
|
||||
Array.from(prev).forEach((f) => dt.items.add(f));
|
||||
}
|
||||
acceptedFiles.forEach((f) => {
|
||||
if (
|
||||
!Array.from(prev || []).some(
|
||||
(pf) =>
|
||||
pf.name === f.name &&
|
||||
pf.size === f.size &&
|
||||
pf.lastModified === f.lastModified,
|
||||
)
|
||||
) {
|
||||
dt.items.add(f);
|
||||
}
|
||||
});
|
||||
return dt.files;
|
||||
});
|
||||
}
|
||||
|
||||
e.target.value = '';
|
||||
},
|
||||
[validateAndAddFiles],
|
||||
[isFileAccepted, showToastError, setFiles],
|
||||
);
|
||||
|
||||
const handleAttachmentRemove = useCallback(
|
||||
@@ -459,7 +443,6 @@ export const InputChat = ({
|
||||
name="inputchat-textarea"
|
||||
onChange={handleTextareaChange}
|
||||
onKeyDown={handleTextareaKeyDown}
|
||||
onPaste={handlePaste}
|
||||
disabled={isInputDisabled}
|
||||
rows={1}
|
||||
style={textareaStyle}
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
// Memoized components for a single completed markdown blocks - only re-renders when content changes
|
||||
import rehypeShikiFromHighlighter from '@shikijs/rehype/core';
|
||||
import React, { use } from 'react';
|
||||
import { Components, MarkdownHooks } from 'react-markdown';
|
||||
import rehypeKatex from 'rehype-katex';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import remarkMath from 'remark-math';
|
||||
|
||||
import { Text } from '@/components';
|
||||
import { CodeBlock } from '@/features/chat/components/CodeBlock';
|
||||
|
||||
// Memoized markdown plugins - created once at module level
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const REMARK_PLUGINS: any[] = [remarkGfm, remarkMath];
|
||||
// // eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
|
||||
import { getHighlighter } from '../utils/shiki';
|
||||
|
||||
const highlighterPromise = getHighlighter();
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const rehypePluginsPromise: Promise<any[]> = highlighterPromise.then(
|
||||
(highlighter) => [
|
||||
rehypeKatex,
|
||||
[
|
||||
rehypeShikiFromHighlighter,
|
||||
highlighter,
|
||||
{
|
||||
theme: 'github-dark-dimmed',
|
||||
fallbackLanguage: 'plaintext',
|
||||
},
|
||||
],
|
||||
],
|
||||
);
|
||||
|
||||
// Memoized markdown components - created once at module level
|
||||
const MARKDOWN_COMPONENTS: Components = {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
p: ({ node, ...props }) => (
|
||||
<Text
|
||||
as="p"
|
||||
$css="display: block"
|
||||
$theme="greyscale"
|
||||
$variation="850"
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
a: ({ children, ...props }) => (
|
||||
<a target="_blank" {...props}>
|
||||
{children}
|
||||
</a>
|
||||
),
|
||||
|
||||
pre: ({
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
node,
|
||||
children,
|
||||
...props
|
||||
}) => <CodeBlock {...props}>{children}</CodeBlock>,
|
||||
};
|
||||
|
||||
export const CompletedMarkdownBlock = React.memo(
|
||||
({ content }: { content: string }) => {
|
||||
const rehypePlugins = use(rehypePluginsPromise);
|
||||
return (
|
||||
<MarkdownHooks
|
||||
remarkPlugins={REMARK_PLUGINS}
|
||||
rehypePlugins={rehypePlugins}
|
||||
components={MARKDOWN_COMPONENTS}
|
||||
>
|
||||
{content}
|
||||
</MarkdownHooks>
|
||||
);
|
||||
},
|
||||
(prev, next) => prev.content === next.content,
|
||||
);
|
||||
|
||||
CompletedMarkdownBlock.displayName = 'CompletedMarkdownBlock';
|
||||
|
||||
export const RawTextBlock = ({ content }: { content: string }) => (
|
||||
<Text
|
||||
as="div"
|
||||
$css="white-space: pre-wrap; display: block;"
|
||||
$theme="greyscale"
|
||||
$variation="850"
|
||||
>
|
||||
{content}
|
||||
</Text>
|
||||
);
|
||||
@@ -1,554 +0,0 @@
|
||||
import { Message, SourceUIPart, ToolInvocationUIPart } from '@ai-sdk/ui-utils';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Box, Icon, Loader, Text } from '@/components';
|
||||
import { AttachmentList } from '@/features/chat/components/AttachmentList';
|
||||
import { FeedbackButtons } from '@/features/chat/components/FeedbackButtons';
|
||||
import {
|
||||
CompletedMarkdownBlock,
|
||||
RawTextBlock,
|
||||
} from '@/features/chat/components/MessageBlock';
|
||||
import { SourceItemList } from '@/features/chat/components/SourceItemList';
|
||||
import { ToolInvocationItem } from '@/features/chat/components/ToolInvocationItem';
|
||||
|
||||
// Memoized blocks list to prevent parent re-renders from causing block remounts
|
||||
const BlocksList = React.memo(
|
||||
({ blocks, pending }: { blocks: string[]; pending: string }) => (
|
||||
<div>
|
||||
{/* key={index} is safe here: blocks are append-only during streaming
|
||||
and a completed block's content never changes once finalized. */}
|
||||
{blocks.map((block, index) => (
|
||||
<CompletedMarkdownBlock key={index} content={block} />
|
||||
))}
|
||||
{pending && <RawTextBlock content={pending} />}
|
||||
</div>
|
||||
),
|
||||
(prev, next) => {
|
||||
const lengthChanged = prev.blocks.length !== next.blocks.length;
|
||||
const pendingChanged = prev.pending !== next.pending;
|
||||
|
||||
let blocksChanged = false;
|
||||
for (let i = 0; i < Math.min(prev.blocks.length, next.blocks.length); i++) {
|
||||
if (prev.blocks[i] !== next.blocks[i]) {
|
||||
blocksChanged = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (lengthChanged || pendingChanged || blocksChanged) {
|
||||
return false; // needs re-render
|
||||
}
|
||||
return true;
|
||||
},
|
||||
);
|
||||
BlocksList.displayName = 'BlocksList';
|
||||
|
||||
export interface StreamingContent {
|
||||
completedBlocks: string[];
|
||||
pending: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Splits content into blocks by double newlines, respecting code fences.
|
||||
* Code fences may contain double newlines, so we merge blocks until fences are balanced.
|
||||
*/
|
||||
export const splitIntoBlocks = (content: string): string[] => {
|
||||
if (!content) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const rawBlocks = content.split('\n\n');
|
||||
const blocks: string[] = [];
|
||||
let currentBlock = '';
|
||||
let fenceCount = 0;
|
||||
|
||||
for (const rawBlock of rawBlocks) {
|
||||
const fences = (rawBlock.match(/```/g) || []).length;
|
||||
|
||||
currentBlock = currentBlock ? currentBlock + '\n\n' + rawBlock : rawBlock;
|
||||
fenceCount += fences;
|
||||
|
||||
// Balanced fences = complete block
|
||||
if (fenceCount % 2 === 0) {
|
||||
if (currentBlock.trim()) {
|
||||
blocks.push(currentBlock);
|
||||
}
|
||||
currentBlock = '';
|
||||
fenceCount = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (currentBlock.trim()) {
|
||||
blocks.push(currentBlock);
|
||||
}
|
||||
|
||||
return blocks;
|
||||
};
|
||||
|
||||
/**
|
||||
* Splits streaming content into completed blocks (safe and ready to render as markdown)
|
||||
* + a pending content (still being streamed, rendered as raw text).
|
||||
*
|
||||
* A block is considered completed when followed by a double newline.
|
||||
* Each block is returned separately to enable independent memoization.
|
||||
* NB: it respects code fences (``` ... ```) that may contain double newlines.
|
||||
*/
|
||||
export const splitStreamingContent = (content: string): StreamingContent => {
|
||||
if (!content) {
|
||||
return { completedBlocks: [], pending: '' };
|
||||
}
|
||||
|
||||
// Find all code fence positions
|
||||
// Note: this counts all ``` occurrences including those inside inline code spans.
|
||||
// In practice this is unlikely to cause issues since inline code rarely contains ```.
|
||||
const fenceRegex = /```/g;
|
||||
const fences: number[] = [];
|
||||
let match;
|
||||
while ((match = fenceRegex.exec(content)) !== null) {
|
||||
fences.push(match.index);
|
||||
}
|
||||
|
||||
// Check if we're inside an unclosed code fence
|
||||
const isInsideCodeFence = fences.length % 2 === 1;
|
||||
|
||||
let completedContent: string;
|
||||
let pendingContent: string;
|
||||
|
||||
if (isInsideCodeFence) {
|
||||
// Find the last opening fence
|
||||
const lastFenceStart = fences[fences.length - 1];
|
||||
// Everything before the unclosed fence is potentially complete
|
||||
const beforeFence = content.slice(0, lastFenceStart);
|
||||
const fenceAndAfter = content.slice(lastFenceStart);
|
||||
|
||||
// Find the last complete block boundary before the fence
|
||||
const lastDoubleNewline = beforeFence.lastIndexOf('\n\n');
|
||||
if (lastDoubleNewline !== -1) {
|
||||
completedContent = beforeFence.slice(0, lastDoubleNewline);
|
||||
pendingContent = beforeFence.slice(lastDoubleNewline) + fenceAndAfter;
|
||||
} else {
|
||||
// No complete blocks before fence
|
||||
return { completedBlocks: [], pending: content };
|
||||
}
|
||||
} else {
|
||||
// Not inside a code fence - find the last double newline as block boundary
|
||||
const lastDoubleNewline = content.lastIndexOf('\n\n');
|
||||
if (lastDoubleNewline === -1) {
|
||||
// No double newline yet - everything is pending
|
||||
return { completedBlocks: [], pending: content };
|
||||
}
|
||||
|
||||
// Content up to the last \n\n is complete
|
||||
completedContent = content.slice(0, lastDoubleNewline);
|
||||
// Content after the last \n\n is pending (may be empty if content ends with \n\n)
|
||||
pendingContent = content.slice(lastDoubleNewline + 2);
|
||||
}
|
||||
|
||||
const completedBlocks = splitIntoBlocks(completedContent);
|
||||
return { completedBlocks, pending: pendingContent };
|
||||
};
|
||||
|
||||
interface SourceMetadata {
|
||||
title: string | null;
|
||||
favicon: string | null;
|
||||
loading: boolean;
|
||||
error: boolean;
|
||||
}
|
||||
|
||||
export interface MessageItemProps {
|
||||
message: Message;
|
||||
isLastMessage: boolean;
|
||||
isLastAssistantMessage: boolean;
|
||||
isFirstConversationMessage: boolean;
|
||||
streamingMessageHeight: number | null;
|
||||
status: 'submitted' | 'streaming' | 'ready' | 'error';
|
||||
conversationId: string | undefined;
|
||||
isSourceOpen: string | null;
|
||||
isMobile: boolean;
|
||||
onCopyToClipboard: (content: string) => void;
|
||||
onOpenSources: (messageId: string) => void;
|
||||
getMetadata: (url: string) => SourceMetadata | undefined;
|
||||
}
|
||||
|
||||
const MessageItemComponent: React.FC<MessageItemProps> = ({
|
||||
message,
|
||||
isLastMessage,
|
||||
isLastAssistantMessage,
|
||||
isFirstConversationMessage,
|
||||
streamingMessageHeight,
|
||||
status,
|
||||
conversationId,
|
||||
isSourceOpen,
|
||||
isMobile,
|
||||
onCopyToClipboard,
|
||||
onOpenSources,
|
||||
getMetadata,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const shouldApplyStreamingHeight =
|
||||
isLastAssistantMessage &&
|
||||
isLastMessage &&
|
||||
streamingMessageHeight &&
|
||||
!isFirstConversationMessage;
|
||||
|
||||
const isCurrentlyStreaming =
|
||||
isLastAssistantMessage &&
|
||||
(status === 'streaming' || status === 'submitted');
|
||||
|
||||
const sourceParts = React.useMemo(() => {
|
||||
if (!message.parts) {
|
||||
return [];
|
||||
}
|
||||
return message.parts.filter(
|
||||
(part): part is SourceUIPart => part.type === 'source',
|
||||
);
|
||||
}, [message.parts]);
|
||||
|
||||
const toolInvocationParts = React.useMemo(() => {
|
||||
if (!message.parts) {
|
||||
return [];
|
||||
}
|
||||
return message.parts.filter(
|
||||
(part): part is ToolInvocationUIPart => part.type === 'tool-invocation',
|
||||
);
|
||||
}, [message.parts]);
|
||||
|
||||
const hasNonDocumentParsingTool = React.useMemo(() => {
|
||||
return toolInvocationParts.some(
|
||||
(part) => part.toolInvocation.toolName !== 'document_parsing',
|
||||
);
|
||||
}, [toolInvocationParts]);
|
||||
|
||||
const activeToolInvocation = React.useMemo(() => {
|
||||
const tool = toolInvocationParts.find(
|
||||
(part) => part.toolInvocation.toolName !== 'document_parsing',
|
||||
);
|
||||
return tool?.toolInvocation;
|
||||
}, [toolInvocationParts]);
|
||||
|
||||
const activeToolInvocationDisplayName = React.useMemo(() => {
|
||||
if (!activeToolInvocation) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (activeToolInvocation.toolName === 'summarize') {
|
||||
return t('Summarizing...');
|
||||
}
|
||||
if (activeToolInvocation.toolName === 'translate') {
|
||||
return t('Translation in progress...');
|
||||
}
|
||||
|
||||
return t('Search...');
|
||||
}, [activeToolInvocation, t]);
|
||||
|
||||
// Memoize the streaming content split to avoid recreating components in JSX
|
||||
const { completedBlocks, pending } = React.useMemo(() => {
|
||||
// When not streaming, everything is completed as a single block array
|
||||
if (!isCurrentlyStreaming) {
|
||||
return {
|
||||
completedBlocks: splitIntoBlocks(message.content),
|
||||
pending: '',
|
||||
};
|
||||
}
|
||||
return splitStreamingContent(message.content);
|
||||
}, [isCurrentlyStreaming, message.content]);
|
||||
|
||||
const handleCopy = React.useCallback(() => {
|
||||
onCopyToClipboard(message.content);
|
||||
}, [onCopyToClipboard, message.content]);
|
||||
|
||||
const handleCopyKeyDown = React.useCallback(
|
||||
(e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
onCopyToClipboard(message.content);
|
||||
}
|
||||
},
|
||||
[onCopyToClipboard, message.content],
|
||||
);
|
||||
|
||||
const handleOpenSources = React.useCallback(() => {
|
||||
onOpenSources(message.id);
|
||||
}, [onOpenSources, message.id]);
|
||||
|
||||
const handleOpenSourcesKeyDown = React.useCallback(
|
||||
(e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
onOpenSources(message.id);
|
||||
}
|
||||
},
|
||||
[onOpenSources, message.id],
|
||||
);
|
||||
|
||||
return (
|
||||
<Box
|
||||
data-message-id={message.id}
|
||||
data-testid={message.id}
|
||||
$css={`
|
||||
display: flex;
|
||||
width: 100%;
|
||||
margin: auto;
|
||||
margin-bottom: ${isLastAssistantMessage ? '30px' : '0px'};
|
||||
color: var(--c--theme--colors--greyscale-850);
|
||||
padding-left: 12px;
|
||||
padding-right: 12px;
|
||||
max-width: 750px;
|
||||
text-align: left;
|
||||
overflow-wrap: anywhere;
|
||||
flex-direction: ${message.role === 'user' ? 'row-reverse' : 'row'};
|
||||
`}
|
||||
>
|
||||
<Box
|
||||
$display="block"
|
||||
$width={`${message.role === 'user' ? 'auto' : '100%'}`}
|
||||
>
|
||||
{message.experimental_attachments &&
|
||||
message.experimental_attachments.length > 0 && (
|
||||
<Box>
|
||||
<AttachmentList
|
||||
attachments={message.experimental_attachments}
|
||||
isReadOnly={true}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
<Box
|
||||
$radius="8px"
|
||||
$width={`${message.role === 'user' ? 'auto' : '100%'}`}
|
||||
$maxWidth="100%"
|
||||
$padding={`${message.role === 'user' ? '12px' : '0'}`}
|
||||
$margin={{ vertical: 'base' }}
|
||||
$background={`${message.role === 'user' ? '#EEF1F4' : 'white'}`}
|
||||
$css={`
|
||||
display: inline-block;
|
||||
float: right;
|
||||
${shouldApplyStreamingHeight ? `min-height: ${streamingMessageHeight}px;` : ''}`}
|
||||
>
|
||||
{/* Message content */}
|
||||
{message.content && (
|
||||
<Box
|
||||
className="mainContent-chat"
|
||||
data-testid={
|
||||
message.role === 'assistant'
|
||||
? 'assistant-message-content'
|
||||
: undefined
|
||||
}
|
||||
$padding={{ all: 'xxs' }}
|
||||
>
|
||||
<p className="sr-only">
|
||||
{message.role === 'user'
|
||||
? t('You said: ')
|
||||
: t('Assistant IA replied: ')}
|
||||
</p>
|
||||
{message.role === 'user' ? (
|
||||
<Text
|
||||
as="p"
|
||||
$css="white-space: pre-wrap; display: block;"
|
||||
$theme="greyscale"
|
||||
$variation="850"
|
||||
>
|
||||
{message.content}
|
||||
</Text>
|
||||
) : (
|
||||
// Render completed blocks as markdown, pending block as plain text
|
||||
<BlocksList blocks={completedBlocks} pending={pending} />
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Box $direction="column" $gap="2">
|
||||
{isCurrentlyStreaming &&
|
||||
isLastAssistantMessage &&
|
||||
status === 'streaming' &&
|
||||
hasNonDocumentParsingTool && (
|
||||
<Box
|
||||
$direction="row"
|
||||
$align="center"
|
||||
$gap="6px"
|
||||
$width="100%"
|
||||
$maxWidth="750px"
|
||||
$margin={{
|
||||
all: 'auto',
|
||||
top: 'base',
|
||||
bottom: 'md',
|
||||
}}
|
||||
>
|
||||
<Loader />
|
||||
<Text $variation="600" $size="md">
|
||||
{activeToolInvocationDisplayName}
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
{toolInvocationParts.map((part, partIndex) =>
|
||||
isCurrentlyStreaming && isLastAssistantMessage ? (
|
||||
<ToolInvocationItem
|
||||
key={`tool-invocation-${partIndex}`}
|
||||
toolInvocation={part.toolInvocation}
|
||||
status={status}
|
||||
hideSearchLoader={true}
|
||||
/>
|
||||
) : null,
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{message.role === 'assistant' &&
|
||||
!(isLastAssistantMessage && status === 'streaming') && (
|
||||
<Box
|
||||
$css="color: #222631; font-size: 12px;"
|
||||
$direction="row"
|
||||
$align="center"
|
||||
$justify="space-between"
|
||||
$gap="6px"
|
||||
$margin={{ top: 'base' }}
|
||||
>
|
||||
<Box $direction="row" $gap="4px">
|
||||
<Box
|
||||
$direction="row"
|
||||
$align="center"
|
||||
$gap="4px"
|
||||
className="c__button--neutral action-chat-button"
|
||||
onClick={handleCopy}
|
||||
onKeyDown={handleCopyKeyDown}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
>
|
||||
<Icon
|
||||
iconName="content_copy"
|
||||
$theme="greyscale"
|
||||
$variation="550"
|
||||
$size="16px"
|
||||
className="action-chat-button-icon"
|
||||
/>
|
||||
{!isMobile && (
|
||||
<Text $theme="greyscale" $variation="550">
|
||||
{t('Copy')}
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
{sourceParts.length > 0 && (
|
||||
<Box
|
||||
$direction="row"
|
||||
$align="center"
|
||||
$gap="4px"
|
||||
className={`c__button--neutral action-chat-button ${isSourceOpen === message.id ? 'action-chat-button--open' : ''}`}
|
||||
onClick={handleOpenSources}
|
||||
onKeyDown={handleOpenSourcesKeyDown}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
>
|
||||
<Icon
|
||||
iconName="book"
|
||||
$theme="greyscale"
|
||||
$variation="550"
|
||||
$size="16px"
|
||||
className="action-chat-button-icon"
|
||||
/>
|
||||
<Text
|
||||
$theme="greyscale"
|
||||
$variation="550"
|
||||
$weight="500"
|
||||
$size="12px"
|
||||
>
|
||||
{t('Show')} {sourceParts.length}{' '}
|
||||
{sourceParts.length !== 1 ? t('sources') : t('source')}
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
<Box $direction="row" $gap="4px">
|
||||
{conversationId &&
|
||||
message.id &&
|
||||
message.id.startsWith('trace-') && (
|
||||
<FeedbackButtons
|
||||
conversationId={conversationId}
|
||||
messageId={message.id}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{isSourceOpen === message.id && sourceParts.length > 0 && (
|
||||
<Box
|
||||
$css={`
|
||||
animation: fade-in 0.2s ease-out;
|
||||
`}
|
||||
>
|
||||
<SourceItemList parts={sourceParts} getMetadata={getMetadata} />
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
MessageItemComponent.displayName = 'MessageItem';
|
||||
|
||||
// Custom comparison function for React.memo
|
||||
// Only re-render when props that affect rendering change
|
||||
const arePropsEqual = (
|
||||
prevProps: MessageItemProps,
|
||||
nextProps: MessageItemProps,
|
||||
): boolean => {
|
||||
// Always re-render if message content changed
|
||||
if (prevProps.message.id !== nextProps.message.id) {
|
||||
return false;
|
||||
}
|
||||
if (prevProps.message.content !== nextProps.message.content) {
|
||||
return false;
|
||||
}
|
||||
if (prevProps.message.role !== nextProps.message.role) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check parts changes (for streaming tool invocations and sources)
|
||||
const prevPartsLength = prevProps.message.parts?.length ?? 0;
|
||||
const nextPartsLength = nextProps.message.parts?.length ?? 0;
|
||||
if (prevPartsLength !== nextPartsLength) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check attachments
|
||||
const prevAttachmentsLength =
|
||||
prevProps.message.experimental_attachments?.length ?? 0;
|
||||
const nextAttachmentsLength =
|
||||
nextProps.message.experimental_attachments?.length ?? 0;
|
||||
if (prevAttachmentsLength !== nextAttachmentsLength) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check rendering flags
|
||||
if (prevProps.isLastMessage !== nextProps.isLastMessage) {
|
||||
return false;
|
||||
}
|
||||
if (prevProps.isLastAssistantMessage !== nextProps.isLastAssistantMessage) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
prevProps.isFirstConversationMessage !==
|
||||
nextProps.isFirstConversationMessage
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (prevProps.streamingMessageHeight !== nextProps.streamingMessageHeight) {
|
||||
return false;
|
||||
}
|
||||
if (prevProps.status !== nextProps.status) {
|
||||
return false;
|
||||
}
|
||||
if (prevProps.isSourceOpen !== nextProps.isSourceOpen) {
|
||||
return false;
|
||||
}
|
||||
if (prevProps.isMobile !== nextProps.isMobile) {
|
||||
return false;
|
||||
}
|
||||
if (prevProps.conversationId !== nextProps.conversationId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
export const MessageItem = React.memo(MessageItemComponent, arePropsEqual);
|
||||
@@ -23,6 +23,7 @@ const SourceItemListComponent: React.FC<SourceItemListProps> = ({
|
||||
if (parts.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
$direction="column"
|
||||
|
||||
+8
-10
@@ -3,7 +3,13 @@ import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Box } from '@/components';
|
||||
|
||||
const SUGGESTIONS_COUNT = 4;
|
||||
const SUGGESTION_KEYS = [
|
||||
'Ask a question',
|
||||
'Turn this list into bullet points',
|
||||
'Write a short product description',
|
||||
'Find recent news about...',
|
||||
] as const;
|
||||
const SUGGESTIONS_COUNT = SUGGESTION_KEYS.length;
|
||||
|
||||
const WRAPPER_CSS = `position: absolute;
|
||||
top: 1rem;
|
||||
@@ -36,15 +42,7 @@ export const SuggestionCarousel = ({
|
||||
const [currentSuggestionIndex, setCurrentSuggestionIndex] = useState(0);
|
||||
const [isResetting, setIsResetting] = useState(false);
|
||||
|
||||
const suggestions = useMemo(
|
||||
() => [
|
||||
t('Ask a question'),
|
||||
t('Turn this list into bullet points'),
|
||||
t('Write a short product description'),
|
||||
t('Find recent news about...'),
|
||||
],
|
||||
[t],
|
||||
);
|
||||
const suggestions = useMemo(() => SUGGESTION_KEYS.map((key) => t(key)), [t]);
|
||||
const carouselSuggestions = useMemo(
|
||||
() => [...suggestions, suggestions[0]],
|
||||
[suggestions],
|
||||
|
||||
+12
-7
@@ -29,15 +29,13 @@ export const ToolInvocationItem: React.FC<ToolInvocationItemProps> = ({
|
||||
?.documents;
|
||||
const documentIdentifiers: string[] =
|
||||
Array.isArray(documents) &&
|
||||
documents.every(
|
||||
(doc): doc is { identifier: string } =>
|
||||
typeof doc === 'object' && doc !== null && 'identifier' in doc,
|
||||
)
|
||||
documents.every(
|
||||
(doc): doc is { identifier: string } =>
|
||||
typeof doc === 'object' && doc !== null && 'identifier' in doc,
|
||||
)
|
||||
? documents.map((doc) => doc.identifier)
|
||||
: [];
|
||||
|
||||
const label = documentIdentifiers.length > 1 ? 'Extracting documents...' : 'Extracting document...';
|
||||
|
||||
return (
|
||||
<Box
|
||||
$direction="row"
|
||||
@@ -47,10 +45,17 @@ export const ToolInvocationItem: React.FC<ToolInvocationItemProps> = ({
|
||||
$width="100%"
|
||||
$maxWidth="750px"
|
||||
$margin={{ all: 'auto', top: 'base', bottom: 'md' }}
|
||||
$background="var(--c--contextuals--background--surface--tertiary)"
|
||||
$color="var(--c--contextuals--content--semantic--neutral--secondary)"
|
||||
$padding={{ all: 'sm' }}
|
||||
$radius="8px"
|
||||
$css="font-size: 0.9em; width: 100%; white-space: pre-wrap; word-wrap: break-word;"
|
||||
>
|
||||
<Loader />
|
||||
<Text $variation="600" $size="md">
|
||||
{t(label, { documents: documentIdentifiers.join(', ') })}
|
||||
{t('Extracting documents: {{documents}} ...', {
|
||||
documents: documentIdentifiers.join(', '),
|
||||
})}
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
|
||||
-171
@@ -1,171 +0,0 @@
|
||||
/* eslint-disable testing-library/no-unnecessary-act, @typescript-eslint/require-await */
|
||||
import { CunninghamProvider } from '@openfun/cunningham-react';
|
||||
import { act, render, screen } from '@testing-library/react';
|
||||
import { Suspense } from 'react';
|
||||
|
||||
import { CompletedMarkdownBlock, RawTextBlock } from '../MessageBlock';
|
||||
|
||||
// Mock react-markdown (ESM module not compatible with Jest)
|
||||
jest.mock('react-markdown', () => ({
|
||||
MarkdownHooks: ({ children }: { children: string }) => {
|
||||
// Simple mock that renders markdown-like content
|
||||
// This tests the component integration, not the markdown parsing itself
|
||||
return <div data-testid="markdown-content">{children}</div>;
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock rehype/remark plugins
|
||||
jest.mock('@shikijs/rehype/core', () => () => {});
|
||||
jest.mock('../../utils/shiki', () => ({
|
||||
getHighlighter: () => Promise.resolve({}),
|
||||
}));
|
||||
jest.mock('rehype-katex', () => () => {});
|
||||
jest.mock('remark-gfm', () => () => {});
|
||||
jest.mock('remark-math', () => () => {});
|
||||
|
||||
jest.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => key,
|
||||
}),
|
||||
}));
|
||||
|
||||
const renderWithProviders = (ui: React.ReactNode) => {
|
||||
return render(
|
||||
<CunninghamProvider>
|
||||
<Suspense fallback={null}>{ui}</Suspense>
|
||||
</CunninghamProvider>,
|
||||
);
|
||||
};
|
||||
|
||||
describe('CompletedMarkdownBlock', () => {
|
||||
it('renders content through MarkdownHooks', async () => {
|
||||
await act(async () => {
|
||||
renderWithProviders(<CompletedMarkdownBlock content="Hello world" />);
|
||||
});
|
||||
|
||||
expect(screen.getByText('Hello world')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('passes content to markdown renderer', async () => {
|
||||
const content = '# Header Some **bold** text';
|
||||
await act(async () => {
|
||||
renderWithProviders(<CompletedMarkdownBlock content={content} />);
|
||||
});
|
||||
|
||||
expect(screen.getByTestId('markdown-content')).toHaveTextContent(content);
|
||||
});
|
||||
|
||||
it('does not re-render when content is the same (memoization)', async () => {
|
||||
let rerender: ReturnType<typeof render>['rerender'];
|
||||
await act(async () => {
|
||||
({ rerender } = renderWithProviders(
|
||||
<CompletedMarkdownBlock content="Same content" />,
|
||||
));
|
||||
});
|
||||
|
||||
const firstRender = screen.getByTestId('markdown-content');
|
||||
|
||||
rerender!(
|
||||
<CunninghamProvider>
|
||||
<Suspense fallback={null}>
|
||||
<CompletedMarkdownBlock content="Same content" />
|
||||
</Suspense>
|
||||
</CunninghamProvider>,
|
||||
);
|
||||
|
||||
const secondRender = screen.getByTestId('markdown-content');
|
||||
|
||||
// The DOM element should be the same instance (not recreated)
|
||||
expect(firstRender).toBe(secondRender);
|
||||
});
|
||||
|
||||
it('re-renders when content changes', async () => {
|
||||
let rerender: ReturnType<typeof render>['rerender'];
|
||||
await act(async () => {
|
||||
({ rerender } = renderWithProviders(
|
||||
<CompletedMarkdownBlock content="Original content" />,
|
||||
));
|
||||
});
|
||||
|
||||
expect(screen.getByText('Original content')).toBeInTheDocument();
|
||||
|
||||
rerender!(
|
||||
<CunninghamProvider>
|
||||
<Suspense fallback={null}>
|
||||
<CompletedMarkdownBlock content="New content" />
|
||||
</Suspense>
|
||||
</CunninghamProvider>,
|
||||
);
|
||||
|
||||
expect(screen.queryByText('Original content')).not.toBeInTheDocument();
|
||||
expect(screen.getByText('New content')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('handles empty content', async () => {
|
||||
let container: HTMLElement;
|
||||
await act(async () => {
|
||||
({ container } = renderWithProviders(
|
||||
<CompletedMarkdownBlock content="" />,
|
||||
));
|
||||
});
|
||||
|
||||
expect(container!).toBeInTheDocument();
|
||||
expect(screen.getByTestId('markdown-content')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('handles content with special characters', async () => {
|
||||
await act(async () => {
|
||||
renderWithProviders(
|
||||
<CompletedMarkdownBlock content={`Special chars: < > & " '`} />,
|
||||
);
|
||||
});
|
||||
|
||||
expect(screen.getByText(/Special chars:/)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('RawTextBlock', () => {
|
||||
it('renders plain text content', () => {
|
||||
renderWithProviders(<RawTextBlock content="Raw text content" />);
|
||||
|
||||
expect(screen.getByText('Raw text content')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('preserves whitespace and newlines', () => {
|
||||
renderWithProviders(
|
||||
<RawTextBlock content="Line 1\n Indented line\nLine 3" />,
|
||||
);
|
||||
|
||||
const textElement = screen.getByText(/Line 1/);
|
||||
expect(textElement).toHaveStyle('white-space: pre-wrap');
|
||||
});
|
||||
|
||||
it('renders as a div element', () => {
|
||||
renderWithProviders(<RawTextBlock content="Test content" />);
|
||||
|
||||
const element = screen.getByText('Test content');
|
||||
// Text component renders with as="div", check it's in the DOM
|
||||
expect(element).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not parse markdown syntax', () => {
|
||||
renderWithProviders(<RawTextBlock content="**not bold** *not italic*" />);
|
||||
|
||||
// Should render the raw markdown syntax, not parsed
|
||||
expect(screen.getByText('**not bold** *not italic*')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('handles empty content', () => {
|
||||
const { container } = renderWithProviders(<RawTextBlock content="" />);
|
||||
|
||||
expect(container).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('handles content with code fence markers', () => {
|
||||
renderWithProviders(
|
||||
<RawTextBlock content="```python\npsquares = [x**2 for x in range(10)]" />,
|
||||
);
|
||||
|
||||
expect(screen.getByText(/```python/)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
-536
@@ -1,536 +0,0 @@
|
||||
/* eslint-disable testing-library/no-unnecessary-act, @typescript-eslint/require-await */
|
||||
import { CunninghamProvider } from '@openfun/cunningham-react';
|
||||
import { act, render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { Suspense } from 'react';
|
||||
|
||||
import {
|
||||
MessageItem,
|
||||
splitIntoBlocks,
|
||||
splitStreamingContent,
|
||||
} from '../MessageItem';
|
||||
|
||||
// Mock react-markdown (ESM module)
|
||||
jest.mock('react-markdown', () => ({
|
||||
MarkdownHooks: ({ children }: { children: string }) => (
|
||||
<div data-testid="markdown-content">{children}</div>
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock('@shikijs/rehype/core', () => () => {});
|
||||
jest.mock('../../utils/shiki', () => ({
|
||||
getHighlighter: () => Promise.resolve({}),
|
||||
}));
|
||||
jest.mock('rehype-katex', () => () => {});
|
||||
jest.mock('remark-gfm', () => () => {});
|
||||
jest.mock('remark-math', () => () => {});
|
||||
|
||||
jest.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => key,
|
||||
}),
|
||||
}));
|
||||
|
||||
// Mock child components
|
||||
jest.mock('../AttachmentList', () => ({
|
||||
AttachmentList: () => <div data-testid="attachment-list" />,
|
||||
}));
|
||||
|
||||
jest.mock('../FeedbackButtons', () => ({
|
||||
FeedbackButtons: () => <div data-testid="feedback-buttons" />,
|
||||
}));
|
||||
|
||||
jest.mock('../SourceItemList', () => ({
|
||||
SourceItemList: () => <div data-testid="source-item-list" />,
|
||||
}));
|
||||
|
||||
jest.mock('../ToolInvocationItem', () => ({
|
||||
ToolInvocationItem: () => <div data-testid="tool-invocation-item" />,
|
||||
}));
|
||||
|
||||
describe('splitIntoBlocks', () => {
|
||||
describe('basic splitting', () => {
|
||||
it('returns empty array for empty content', () => {
|
||||
expect(splitIntoBlocks('')).toEqual([]);
|
||||
});
|
||||
|
||||
it('returns empty array for null/undefined content', () => {
|
||||
expect(splitIntoBlocks(null as unknown as string)).toEqual([]);
|
||||
expect(splitIntoBlocks(undefined as unknown as string)).toEqual([]);
|
||||
});
|
||||
|
||||
it('returns single block for content without double newlines', () => {
|
||||
expect(splitIntoBlocks('Hello world')).toEqual(['Hello world']);
|
||||
});
|
||||
|
||||
it('splits content by double newlines', () => {
|
||||
expect(splitIntoBlocks('Block 1\n\nBlock 2')).toEqual([
|
||||
'Block 1',
|
||||
'Block 2',
|
||||
]);
|
||||
});
|
||||
|
||||
it('splits multiple blocks', () => {
|
||||
expect(splitIntoBlocks('Block 1\n\nBlock 2\n\nBlock 3')).toEqual([
|
||||
'Block 1',
|
||||
'Block 2',
|
||||
'Block 3',
|
||||
]);
|
||||
});
|
||||
|
||||
it('ignores single newlines', () => {
|
||||
expect(splitIntoBlocks('Line 1\nLine 2\n\nBlock 2')).toEqual([
|
||||
'Line 1\nLine 2',
|
||||
'Block 2',
|
||||
]);
|
||||
});
|
||||
|
||||
it('filters out empty blocks', () => {
|
||||
expect(splitIntoBlocks('Block 1\n\n\n\nBlock 2')).toEqual([
|
||||
'Block 1',
|
||||
'Block 2',
|
||||
]);
|
||||
});
|
||||
|
||||
it('filters out whitespace-only blocks', () => {
|
||||
expect(splitIntoBlocks('Block 1\n\n \n\nBlock 2')).toEqual([
|
||||
'Block 1',
|
||||
'Block 2',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('code fence handling', () => {
|
||||
it('keeps code block with internal double newlines as single block', () => {
|
||||
const content = '```python\nline1\n\nline2\n```';
|
||||
expect(splitIntoBlocks(content)).toEqual([content]);
|
||||
});
|
||||
|
||||
it('keeps code block intact when followed by other content', () => {
|
||||
const content = '```python\ncode\n```\n\nText after';
|
||||
expect(splitIntoBlocks(content)).toEqual([
|
||||
'```python\ncode\n```',
|
||||
'Text after',
|
||||
]);
|
||||
});
|
||||
|
||||
it('keeps code block intact when preceded by other content', () => {
|
||||
const content = 'Text before\n\n```python\ncode\n```';
|
||||
expect(splitIntoBlocks(content)).toEqual([
|
||||
'Text before',
|
||||
'```python\ncode\n```',
|
||||
]);
|
||||
});
|
||||
|
||||
it('handles multiple code blocks', () => {
|
||||
const content = '```js\ncode1\n```\n\nText\n\n```python\ncode2\n```';
|
||||
expect(splitIntoBlocks(content)).toEqual([
|
||||
'```js\ncode1\n```',
|
||||
'Text',
|
||||
'```python\ncode2\n```',
|
||||
]);
|
||||
});
|
||||
|
||||
it('handles code block with multiple double newlines inside', () => {
|
||||
const content = '```\nline1\n\nline2\n\nline3\n```';
|
||||
expect(splitIntoBlocks(content)).toEqual([content]);
|
||||
});
|
||||
|
||||
it('handles nested backticks inside code block', () => {
|
||||
const content = '```markdown\nSome `inline` code\n\nMore text\n```';
|
||||
expect(splitIntoBlocks(content)).toEqual([content]);
|
||||
});
|
||||
|
||||
it('handles unclosed code fence', () => {
|
||||
const content = 'Text\n\n```python\ncode without closing';
|
||||
const result = splitIntoBlocks(content);
|
||||
// The unclosed fence should be kept with the text before it
|
||||
expect(result).toEqual(['Text', '```python\ncode without closing']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('handles content ending with double newline', () => {
|
||||
expect(splitIntoBlocks('Block 1\n\nBlock 2\n\n')).toEqual([
|
||||
'Block 1',
|
||||
'Block 2',
|
||||
]);
|
||||
});
|
||||
|
||||
it('handles content starting with double newline', () => {
|
||||
expect(splitIntoBlocks('\n\nBlock 1\n\nBlock 2')).toEqual([
|
||||
'Block 1',
|
||||
'Block 2',
|
||||
]);
|
||||
});
|
||||
|
||||
it('handles markdown headers', () => {
|
||||
expect(splitIntoBlocks('# Header\n\nParagraph')).toEqual([
|
||||
'# Header',
|
||||
'Paragraph',
|
||||
]);
|
||||
});
|
||||
|
||||
it('handles markdown lists', () => {
|
||||
const content = '- Item 1\n- Item 2\n\nParagraph';
|
||||
expect(splitIntoBlocks(content)).toEqual([
|
||||
'- Item 1\n- Item 2',
|
||||
'Paragraph',
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('splitStreamingContent', () => {
|
||||
describe('basic splitting', () => {
|
||||
it('returns empty blocks and empty pending for empty content', () => {
|
||||
expect(splitStreamingContent('')).toEqual({
|
||||
completedBlocks: [],
|
||||
pending: '',
|
||||
});
|
||||
});
|
||||
|
||||
it('returns all content as pending when no double newline', () => {
|
||||
expect(splitStreamingContent('Partial content')).toEqual({
|
||||
completedBlocks: [],
|
||||
pending: 'Partial content',
|
||||
});
|
||||
});
|
||||
|
||||
it('splits completed and pending content', () => {
|
||||
expect(splitStreamingContent('Block 1\n\nPending')).toEqual({
|
||||
completedBlocks: ['Block 1'],
|
||||
pending: 'Pending',
|
||||
});
|
||||
});
|
||||
|
||||
it('handles multiple completed blocks with pending', () => {
|
||||
expect(splitStreamingContent('Block 1\n\nBlock 2\n\nPending')).toEqual({
|
||||
completedBlocks: ['Block 1', 'Block 2'],
|
||||
pending: 'Pending',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('content ending with double newline (bug fix)', () => {
|
||||
it('keeps blocks when content ends with double newline', () => {
|
||||
// This was the bug: content ending with \n\n would return empty blocks
|
||||
const result = splitStreamingContent('Block 1\n\nBlock 2\n\n');
|
||||
expect(result.completedBlocks).toEqual(['Block 1', 'Block 2']);
|
||||
expect(result.pending).toBe('');
|
||||
});
|
||||
|
||||
it('keeps single block when content ends with double newline', () => {
|
||||
const result = splitStreamingContent('Block 1\n\n');
|
||||
expect(result.completedBlocks).toEqual(['Block 1']);
|
||||
expect(result.pending).toBe('');
|
||||
});
|
||||
|
||||
it('handles multiple trailing double newlines', () => {
|
||||
// 'Block 1\n\n\n\n' - lastIndexOf('\n\n') finds the last pair
|
||||
// completedContent = 'Block 1\n\n', pending = ''
|
||||
const result = splitStreamingContent('Block 1\n\n\n\n');
|
||||
expect(result.completedBlocks).toEqual(['Block 1']);
|
||||
expect(result.pending).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('code fence handling', () => {
|
||||
it('treats unclosed code fence as pending', () => {
|
||||
const result = splitStreamingContent('Text\n\n```python\ncode');
|
||||
expect(result.completedBlocks).toEqual(['Text']);
|
||||
expect(result.pending).toBe('\n\n```python\ncode');
|
||||
});
|
||||
|
||||
it('handles closed code fence as completed', () => {
|
||||
const result = splitStreamingContent('```python\ncode\n```\n\nPending');
|
||||
expect(result.completedBlocks).toEqual(['```python\ncode\n```']);
|
||||
expect(result.pending).toBe('Pending');
|
||||
});
|
||||
|
||||
it('handles unclosed fence with no content before', () => {
|
||||
const result = splitStreamingContent('```python\ncode');
|
||||
expect(result.completedBlocks).toEqual([]);
|
||||
expect(result.pending).toBe('```python\ncode');
|
||||
});
|
||||
|
||||
it('handles unclosed fence with double newline inside', () => {
|
||||
const result = splitStreamingContent('Text\n\n```python\nline1\n\nline2');
|
||||
expect(result.completedBlocks).toEqual(['Text']);
|
||||
expect(result.pending).toContain('```python');
|
||||
});
|
||||
|
||||
it('handles multiple code blocks', () => {
|
||||
const result = splitStreamingContent(
|
||||
'```js\ncode1\n```\n\n```python\ncode2\n```\n\nPending',
|
||||
);
|
||||
expect(result.completedBlocks).toEqual([
|
||||
'```js\ncode1\n```',
|
||||
'```python\ncode2\n```',
|
||||
]);
|
||||
expect(result.pending).toBe('Pending');
|
||||
});
|
||||
});
|
||||
|
||||
describe('streaming simulation', () => {
|
||||
it('maintains block stability as content grows', () => {
|
||||
// Simulate streaming: content grows over time
|
||||
const stream1 = 'Hello';
|
||||
const stream2 = 'Hello world';
|
||||
const stream3 = 'Hello world\n\n';
|
||||
const stream4 = 'Hello world\n\nSecond';
|
||||
const stream5 = 'Hello world\n\nSecond block\n\n';
|
||||
const stream6 = 'Hello world\n\nSecond block\n\nThird';
|
||||
|
||||
// Initially all pending
|
||||
expect(splitStreamingContent(stream1).completedBlocks).toEqual([]);
|
||||
expect(splitStreamingContent(stream2).completedBlocks).toEqual([]);
|
||||
|
||||
// After first \n\n, first block is complete
|
||||
const result3 = splitStreamingContent(stream3);
|
||||
expect(result3.completedBlocks).toEqual(['Hello world']);
|
||||
expect(result3.pending).toBe('');
|
||||
|
||||
// New content arrives
|
||||
const result4 = splitStreamingContent(stream4);
|
||||
expect(result4.completedBlocks).toEqual(['Hello world']);
|
||||
expect(result4.pending).toBe('Second');
|
||||
|
||||
// Second block completes
|
||||
const result5 = splitStreamingContent(stream5);
|
||||
expect(result5.completedBlocks).toEqual(['Hello world', 'Second block']);
|
||||
expect(result5.pending).toBe('');
|
||||
|
||||
// More content
|
||||
const result6 = splitStreamingContent(stream6);
|
||||
expect(result6.completedBlocks).toEqual(['Hello world', 'Second block']);
|
||||
expect(result6.pending).toBe('Third');
|
||||
});
|
||||
|
||||
it('block content remains stable during streaming', () => {
|
||||
// The key test: first block content should not change as more content arrives
|
||||
const stream1 = 'First block\n\nSecond';
|
||||
const stream2 = 'First block\n\nSecond block\n\nThird';
|
||||
|
||||
const result1 = splitStreamingContent(stream1);
|
||||
const result2 = splitStreamingContent(stream2);
|
||||
|
||||
// First block should be identical
|
||||
expect(result1.completedBlocks[0]).toBe(result2.completedBlocks[0]);
|
||||
expect(result1.completedBlocks[0]).toBe('First block');
|
||||
});
|
||||
});
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('handles only whitespace', () => {
|
||||
expect(splitStreamingContent(' ')).toEqual({
|
||||
completedBlocks: [],
|
||||
pending: ' ',
|
||||
});
|
||||
});
|
||||
|
||||
it('handles only newlines', () => {
|
||||
expect(splitStreamingContent('\n\n')).toEqual({
|
||||
completedBlocks: [],
|
||||
pending: '',
|
||||
});
|
||||
});
|
||||
|
||||
it('handles special characters', () => {
|
||||
const result = splitStreamingContent('Special: < > & "\n\nMore');
|
||||
expect(result.completedBlocks).toEqual(['Special: < > & "']);
|
||||
expect(result.pending).toBe('More');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('MessageItem', () => {
|
||||
const defaultProps = {
|
||||
message: {
|
||||
id: 'msg-1',
|
||||
role: 'assistant' as const,
|
||||
content: 'Hello world',
|
||||
},
|
||||
isLastMessage: false,
|
||||
isLastAssistantMessage: false,
|
||||
isFirstConversationMessage: false,
|
||||
streamingMessageHeight: null,
|
||||
status: 'ready' as const,
|
||||
conversationId: 'conv-1',
|
||||
isSourceOpen: null,
|
||||
isMobile: false,
|
||||
onCopyToClipboard: jest.fn(),
|
||||
onOpenSources: jest.fn(),
|
||||
getMetadata: jest.fn(),
|
||||
};
|
||||
|
||||
const renderWithProviders = (ui: React.ReactNode) => {
|
||||
return render(
|
||||
<CunninghamProvider>
|
||||
<Suspense fallback={null}>{ui}</Suspense>
|
||||
</CunninghamProvider>,
|
||||
);
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('rendering', () => {
|
||||
it('renders assistant message content', async () => {
|
||||
await act(async () => {
|
||||
renderWithProviders(<MessageItem {...defaultProps} />);
|
||||
});
|
||||
|
||||
expect(
|
||||
screen.getByTestId('assistant-message-content'),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders user message content', async () => {
|
||||
await act(async () => {
|
||||
renderWithProviders(
|
||||
<MessageItem
|
||||
{...defaultProps}
|
||||
message={{ ...defaultProps.message, role: 'user' }}
|
||||
/>,
|
||||
);
|
||||
});
|
||||
|
||||
expect(screen.getByText('Hello world')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders message with data-message-id attribute', async () => {
|
||||
await act(async () => {
|
||||
renderWithProviders(<MessageItem {...defaultProps} />);
|
||||
});
|
||||
|
||||
expect(screen.getByTestId('msg-1')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders copy button for non-streaming assistant messages', async () => {
|
||||
await act(async () => {
|
||||
renderWithProviders(<MessageItem {...defaultProps} />);
|
||||
});
|
||||
|
||||
expect(screen.getByText('Copy')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not render copy button while streaming', async () => {
|
||||
await act(async () => {
|
||||
renderWithProviders(
|
||||
<MessageItem
|
||||
{...defaultProps}
|
||||
status="streaming"
|
||||
isLastAssistantMessage={true}
|
||||
/>,
|
||||
);
|
||||
});
|
||||
|
||||
expect(screen.queryByText('Copy')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('hides copy text on mobile', async () => {
|
||||
await act(async () => {
|
||||
renderWithProviders(<MessageItem {...defaultProps} isMobile={true} />);
|
||||
});
|
||||
|
||||
expect(screen.queryByText('Copy')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('interactions', () => {
|
||||
it('calls onCopyToClipboard when copy button is clicked', async () => {
|
||||
const user = userEvent.setup();
|
||||
const onCopyToClipboard = jest.fn();
|
||||
|
||||
await act(async () => {
|
||||
renderWithProviders(
|
||||
<MessageItem
|
||||
{...defaultProps}
|
||||
onCopyToClipboard={onCopyToClipboard}
|
||||
/>,
|
||||
);
|
||||
});
|
||||
|
||||
await user.click(screen.getByText('Copy'));
|
||||
|
||||
expect(onCopyToClipboard).toHaveBeenCalledWith('Hello world');
|
||||
});
|
||||
});
|
||||
|
||||
describe('streaming state', () => {
|
||||
it('uses splitStreamingContent when streaming', async () => {
|
||||
// When streaming, content should be split into blocks + pending
|
||||
await act(async () => {
|
||||
renderWithProviders(
|
||||
<MessageItem
|
||||
{...defaultProps}
|
||||
message={{
|
||||
...defaultProps.message,
|
||||
content: 'Block 1\n\nPending content',
|
||||
}}
|
||||
status="streaming"
|
||||
isLastAssistantMessage={true}
|
||||
/>,
|
||||
);
|
||||
});
|
||||
|
||||
// The markdown content should contain "Block 1" but not necessarily "Pending"
|
||||
// since pending is rendered as raw text
|
||||
expect(
|
||||
screen.getByTestId('assistant-message-content'),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('attachments', () => {
|
||||
it('renders AttachmentList when message has attachments', async () => {
|
||||
const messageWithAttachments = {
|
||||
...defaultProps.message,
|
||||
experimental_attachments: [
|
||||
{ url: 'https://example.com/file.pdf', name: 'file.pdf' },
|
||||
],
|
||||
};
|
||||
|
||||
await act(async () => {
|
||||
renderWithProviders(
|
||||
<MessageItem {...defaultProps} message={messageWithAttachments} />,
|
||||
);
|
||||
});
|
||||
|
||||
expect(screen.getByTestId('attachment-list')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not render AttachmentList when no attachments', async () => {
|
||||
await act(async () => {
|
||||
renderWithProviders(<MessageItem {...defaultProps} />);
|
||||
});
|
||||
|
||||
expect(screen.queryByTestId('attachment-list')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('feedback buttons', () => {
|
||||
it('renders FeedbackButtons for trace messages', async () => {
|
||||
await act(async () => {
|
||||
renderWithProviders(
|
||||
<MessageItem
|
||||
{...defaultProps}
|
||||
message={{ ...defaultProps.message, id: 'trace-123' }}
|
||||
/>,
|
||||
);
|
||||
});
|
||||
|
||||
expect(screen.getByTestId('feedback-buttons')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not render FeedbackButtons for non-trace messages', async () => {
|
||||
await act(async () => {
|
||||
renderWithProviders(<MessageItem {...defaultProps} />);
|
||||
});
|
||||
|
||||
expect(screen.queryByTestId('feedback-buttons')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
+1
-20
@@ -1,5 +1,4 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import i18next from 'i18next';
|
||||
|
||||
import '@/i18n/initI18n';
|
||||
|
||||
@@ -10,26 +9,8 @@ describe('SuggestionCarousel', () => {
|
||||
jest.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
await i18next.changeLanguage('en');
|
||||
});
|
||||
|
||||
it('should render all suggestions in french', async () => {
|
||||
await i18next.changeLanguage('fr');
|
||||
|
||||
render(<SuggestionCarousel messagesLength={0} />);
|
||||
|
||||
expect(screen.getAllByText('Poser une question')).toHaveLength(2);
|
||||
expect(
|
||||
screen.getByText('Transformer cette liste en liste à puces...'),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText('Écrire une description courte du produit...'),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText('Trouver les dernières actualités concernant...'),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render all suggestions', () => {
|
||||
|
||||
@@ -4,10 +4,8 @@ import { persist } from 'zustand/middleware';
|
||||
interface ChatPreferencesState {
|
||||
selectedModelHrid: string | null;
|
||||
forceWebSearch: boolean;
|
||||
isDarkModePreference: boolean;
|
||||
isPanelOpen: boolean;
|
||||
setSelectedModelHrid: (hrid: string | null) => void;
|
||||
toggleDarkModePreferences: () => void;
|
||||
toggleForceWebSearch: () => void;
|
||||
setPanelOpen: (isOpen: boolean) => void;
|
||||
togglePanel: () => void;
|
||||
@@ -18,11 +16,8 @@ export const useChatPreferencesStore = create<ChatPreferencesState>()(
|
||||
(set) => ({
|
||||
selectedModelHrid: null,
|
||||
forceWebSearch: false,
|
||||
isDarkModePreference: false,
|
||||
isPanelOpen: false,
|
||||
setSelectedModelHrid: (hrid) => set({ selectedModelHrid: hrid }),
|
||||
toggleDarkModePreferences: () =>
|
||||
set((state) => ({ isDarkModePreference: !state.isDarkModePreference })),
|
||||
toggleForceWebSearch: () =>
|
||||
set((state) => ({ forceWebSearch: !state.forceWebSearch })),
|
||||
setPanelOpen: (isOpen) => set({ isPanelOpen: isOpen }),
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
import { createHighlighterCore } from 'shiki/core';
|
||||
import { createOnigurumaEngine } from 'shiki/engine/oniguruma';
|
||||
import langBash from 'shiki/langs/bash.mjs';
|
||||
import langCss from 'shiki/langs/css.mjs';
|
||||
import langHtml from 'shiki/langs/html.mjs';
|
||||
import langJavascript from 'shiki/langs/javascript.mjs';
|
||||
import langJson from 'shiki/langs/json.mjs';
|
||||
import langMarkdown from 'shiki/langs/markdown.mjs';
|
||||
import langPython from 'shiki/langs/python.mjs';
|
||||
import langSql from 'shiki/langs/sql.mjs';
|
||||
import langTypescript from 'shiki/langs/typescript.mjs';
|
||||
import langYaml from 'shiki/langs/yaml.mjs';
|
||||
import themeDimmed from 'shiki/themes/github-dark-dimmed.mjs';
|
||||
|
||||
export const getHighlighter = () =>
|
||||
createHighlighterCore({
|
||||
themes: [themeDimmed],
|
||||
langs: [
|
||||
langJavascript,
|
||||
langTypescript,
|
||||
langPython,
|
||||
langBash,
|
||||
langJson,
|
||||
langHtml,
|
||||
langCss,
|
||||
langSql,
|
||||
langYaml,
|
||||
langMarkdown,
|
||||
],
|
||||
|
||||
engine: createOnigurumaEngine(import('shiki/wasm')),
|
||||
});
|
||||
@@ -173,8 +173,25 @@ export const InfoIcon = () => {
|
||||
>
|
||||
<path
|
||||
d="M12 15.5C12.2833 15.5 12.5208 15.4042 12.7125 15.2125C12.9042 15.0208 13 14.7833 13 14.5C13 14.2167 12.9042 13.9792 12.7125 13.7875C12.5208 13.5958 12.2833 13.5 12 13.5C11.7167 13.5 11.4792 13.5958 11.2875 13.7875C11.0958 13.9792 11 14.2167 11 14.5C11 14.7833 11.0958 15.0208 11.2875 15.2125C11.4792 15.4042 11.7167 15.5 12 15.5ZM11 11.5H13V5.5H11V11.5ZM2 22.5V4.5C2 3.95 2.19583 3.47917 2.5875 3.0875C2.97917 2.69583 3.45 2.5 4 2.5H20C20.55 2.5 21.0208 2.69583 21.4125 3.0875C21.8042 3.47917 22 3.95 22 4.5V16.5C22 17.05 21.8042 17.5208 21.4125 17.9125C21.0208 18.3042 20.55 18.5 20 18.5H6L2 22.5ZM5.15 16.5H20V4.5H4V17.625L5.15 16.5Z"
|
||||
fill="currentColor"
|
||||
fill="#265eaa"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
// export const VisioIcon = () => {
|
||||
// return (
|
||||
// <svg
|
||||
// width="42"
|
||||
// height="43"
|
||||
// viewBox="0 0 42 43"
|
||||
// fill="none"
|
||||
// xmlns="http://www.w3.org/2000/svg"
|
||||
// >
|
||||
// <path
|
||||
// d="M14.1288 27.1205C13.8568 27.1205 13.6502 27.0399 13.5092 26.8787C13.3783 26.7074 13.3128 26.4908 13.3128 26.2289C13.3128 25.8562 13.4588 25.3676 13.751 24.7631C14.0431 24.1486 14.4814 23.5341 15.0656 22.9196C15.66 22.3051 16.4105 21.7913 17.3172 21.3783C18.2239 20.9552 19.2867 20.7436 20.5056 20.7436C21.7346 20.7436 22.7975 20.9552 23.694 21.3783C24.6007 21.7913 25.3512 22.3051 25.9456 22.9196C26.5399 23.5341 26.9832 24.1486 27.2754 24.7631C27.5675 25.3676 27.7136 25.8562 27.7136 26.2289C27.7136 26.4908 27.6431 26.7074 27.502 26.8787C27.3711 27.0399 27.1645 27.1205 26.8825 27.1205H14.1288ZM20.5056 19.6103C19.8407 19.6103 19.2363 19.439 18.6923 19.0965C18.1584 18.754 17.7302 18.2906 17.4079 17.7063C17.0855 17.1119 16.9243 16.442 16.9243 15.6965C16.9243 15.0014 17.0855 14.3617 17.4079 13.7774C17.7302 13.1931 18.1584 12.7297 18.6923 12.3872C19.2363 12.0346 19.8407 11.8583 20.5056 11.8583C21.1705 11.8583 21.7699 12.0346 22.3038 12.3872C22.8478 12.7297 23.281 13.1931 23.6034 13.7774C23.9257 14.3617 24.0869 15.0014 24.0869 15.6965C24.0869 16.442 23.9257 17.1119 23.6034 17.7063C23.281 18.2906 22.8478 18.754 22.3038 19.0965C21.7699 19.439 21.1705 19.6103 20.5056 19.6103ZM13.1465 38.1968C12.6227 38.1968 12.2147 38.0205 11.9226 37.6679C11.6405 37.3254 11.4994 36.862 11.4994 36.2777V32.3489H10.7741C9.27309 32.3489 8.0088 32.0819 6.98125 31.548C5.9537 31.004 5.17297 30.2182 4.63904 29.1907C4.1152 28.1631 3.85327 26.9089 3.85327 25.428V13.853C3.85327 12.3721 4.1152 11.1179 4.63904 10.0903C5.17297 9.06278 5.9537 8.28205 6.98125 7.74812C8.0088 7.20413 9.27309 6.93213 10.7741 6.93213H30.2371C31.7381 6.93213 33.0024 7.20413 34.03 7.74812C35.0575 8.28205 35.8332 9.06278 36.3571 10.0903C36.891 11.1179 37.158 12.3721 37.158 13.853V25.428C37.158 26.9089 36.891 28.1631 36.3571 29.1907C35.8332 30.2182 35.0575 31.004 34.03 31.548C33.0024 32.0819 31.7381 32.3489 30.2371 32.3489H20.5358L15.3225 36.9879C14.8692 37.3909 14.4864 37.6931 14.1741 37.8946C13.8618 38.0961 13.5193 38.1968 13.1465 38.1968ZM13.7661 35.4315L18.6016 30.6262C18.8837 30.3341 19.1557 30.1427 19.4176 30.052C19.6795 29.9613 20.0221 29.916 20.4452 29.916H30.2371C31.7583 29.916 32.8866 29.5382 33.622 28.7827C34.3574 28.017 34.7251 26.8938 34.7251 25.4129V13.853C34.7251 12.3822 34.3574 11.269 33.622 10.5134C32.8866 9.74782 31.7583 9.365 30.2371 9.365H10.7741C9.24287 9.365 8.10954 9.74782 7.37414 10.5134C6.64881 11.269 6.28615 12.3822 6.28615 13.853V25.4129C6.28615 26.8938 6.64881 28.017 7.37414 28.7827C8.10954 29.5382 9.24287 29.916 10.7741 29.916H12.6328C13.0458 29.916 13.338 30.0016 13.5092 30.1729C13.6805 30.3441 13.7661 30.6363 13.7661 31.0493V35.4315Z"
|
||||
// fill="currentColor"
|
||||
// />
|
||||
// </svg>
|
||||
// );
|
||||
// };
|
||||
|
||||
@@ -16,6 +16,7 @@ const BlueStripe = styled.div`
|
||||
position: absolute;
|
||||
height: 2px;
|
||||
width: 100%;
|
||||
background: var(--c--theme--colors--primary-600);
|
||||
top: 0;
|
||||
`;
|
||||
|
||||
@@ -128,15 +129,12 @@ export const Footer = () => {
|
||||
gap:0.2rem;
|
||||
transition: box-shadow 0.3s;
|
||||
&:hover {
|
||||
box-shadow: 0px 2px 0 0 var(--c--contextuals--content--semantic--neutral--tertiary);
|
||||
box-shadow: 0px 2px 0 0 var(--c--theme--colors--greyscale-text);
|
||||
}
|
||||
`}
|
||||
>
|
||||
<Text $weight="bold">{label}</Text>
|
||||
<IconLink
|
||||
width={18}
|
||||
color="var(--c--contextuals--content--semantic--neutral--primary)"
|
||||
/>
|
||||
<IconLink width={18} />
|
||||
</StyledLink>
|
||||
))}
|
||||
</Box>
|
||||
@@ -174,9 +172,7 @@ export const Footer = () => {
|
||||
$css={css`
|
||||
&:hover {
|
||||
box-shadow: 0px 2px 0 0
|
||||
var(
|
||||
--c--contextuals--content--semantic--neutral--tertiary
|
||||
);
|
||||
var(--c--theme--colors--greyscale-text);
|
||||
}
|
||||
`}
|
||||
>
|
||||
@@ -203,17 +199,14 @@ export const Footer = () => {
|
||||
$css={css`
|
||||
display: inline-flex;
|
||||
box-shadow: 0px 1px 0 0
|
||||
var(--c--contextuals--content--semantic--neutral--tertiary);
|
||||
var(--c--theme--colors--greyscale-text);
|
||||
gap: 0.2rem;
|
||||
`}
|
||||
>
|
||||
<Text $variation="tertiary" $theme="neutral">
|
||||
{bottomInformation.link.label}
|
||||
</Text>
|
||||
<IconLink
|
||||
width={14}
|
||||
color="var(--c--contextuals--content--semantic--neutral--primary)"
|
||||
/>
|
||||
<IconLink width={14} />
|
||||
</StyledLink>
|
||||
)}
|
||||
</Text>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M10 6v2H5v11h11v-5h2v6a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1h6Zm11-3v8h-2V6.413l-7.793 7.794-1.414-1.414L17.585 5H13V3h8Z"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 243 B After Width: | Height: | Size: 219 B |
@@ -24,10 +24,7 @@ import { LaGaufre } from './LaGaufre';
|
||||
|
||||
export const Header = () => {
|
||||
const { t } = useTranslation();
|
||||
const { spacingsTokens, colorsTokens, componentTokens } =
|
||||
useCunninghamTheme();
|
||||
const showLaGaufre =
|
||||
(componentTokens as Record<string, unknown>)['la-gaufre'] === true;
|
||||
const { spacingsTokens, colorsTokens } = useCunninghamTheme();
|
||||
const { isDesktop } = useResponsiveStore();
|
||||
const { setPanelOpen } = useChatPreferencesStore();
|
||||
const { isAtTop } = useChatScroll();
|
||||
@@ -98,7 +95,7 @@ export const Header = () => {
|
||||
>
|
||||
<ButtonLogin />
|
||||
<LanguagePicker />
|
||||
{showLaGaufre && <LaGaufre />}
|
||||
<LaGaufre />
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
@@ -1,210 +0,0 @@
|
||||
<svg width="386" height="384" viewBox="0 0 1025 1024" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g filter="url(#filter0_n_386_10828)">
|
||||
<path d="M892.092 587C899.824 587 906.092 593.268 906.092 601V667.955C907.978 679.026 913.901 689.799 927.744 693.844C929.9 694.474 929.765 697.546 927.52 697.508C918.813 697.358 908.404 697.081 905.37 696.589C902.117 696.061 899.152 695.182 896.475 693.979C895.096 694.433 893.623 694.68 892.092 694.68H334C326.268 694.68 320 688.412 320 680.68V601C320 593.268 326.268 587 334 587H892.092Z" fill="#626A80"/>
|
||||
<path d="M892.092 587C899.824 587 906.092 593.268 906.092 601V667.955C907.978 679.026 913.901 689.799 927.744 693.844C929.9 694.474 929.765 697.546 927.52 697.508C918.813 697.358 908.404 697.081 905.37 696.589C902.117 696.061 899.152 695.182 896.475 693.979C895.096 694.433 893.623 694.68 892.092 694.68H334C326.268 694.68 320 688.412 320 680.68V601C320 593.268 326.268 587 334 587H892.092Z" fill="#181B24" fill-opacity="0.35"/>
|
||||
<path d="M892.092 587L892.092 585.5H892.092V587ZM906.092 667.955H904.592V668.082L904.613 668.207L906.092 667.955ZM927.744 693.844L928.165 692.404L928.165 692.404L927.744 693.844ZM927.52 697.508L927.494 699.008L927.494 699.008L927.52 697.508ZM905.37 696.589L905.13 698.069L905.13 698.07L905.37 696.589ZM896.475 693.979L897.089 692.61L896.558 692.372L896.005 692.554L896.475 693.979ZM892.092 694.68V696.18H892.092L892.092 694.68ZM334 694.68L334 696.18H334V694.68ZM320 680.68H318.5V680.68L320 680.68ZM334 587V585.5H334L334 587ZM892.092 587L892.092 588.5C898.995 588.5 904.592 594.097 904.592 601H906.092H907.592C907.592 592.44 900.652 585.5 892.092 585.5L892.092 587ZM906.092 601H904.592V667.955H906.092H907.592V601H906.092ZM906.092 667.955L904.613 668.207C906.561 679.638 912.755 691.026 927.323 695.284L927.744 693.844L928.165 692.404C915.047 688.571 909.395 678.413 907.57 667.703L906.092 667.955ZM927.744 693.844L927.323 695.283C927.528 695.343 927.634 695.443 927.693 695.53C927.761 695.63 927.79 695.747 927.784 695.846C927.782 695.894 927.772 695.928 927.763 695.949C927.759 695.96 927.755 695.966 927.753 695.97C927.75 695.973 927.749 695.975 927.748 695.975C927.748 695.975 927.748 695.976 927.746 695.977C927.745 695.978 927.739 695.981 927.729 695.985C927.709 695.993 927.653 696.01 927.545 696.008L927.52 697.508L927.494 699.008C928.419 699.023 929.252 698.71 929.862 698.105C930.454 697.516 930.741 696.749 930.78 696.006C930.859 694.52 929.959 692.928 928.165 692.404L927.744 693.844ZM927.52 697.508L927.545 696.008C923.196 695.933 918.428 695.827 914.372 695.678C910.273 695.527 907.016 695.336 905.61 695.108L905.37 696.589L905.13 698.07C906.758 698.334 910.222 698.527 914.262 698.676C918.346 698.826 923.136 698.933 927.494 699.008L927.52 697.508ZM905.37 696.589L905.61 695.108C902.475 694.599 899.637 693.755 897.089 692.61L896.475 693.979L895.86 695.347C898.666 696.608 901.758 697.522 905.13 698.069L905.37 696.589ZM896.475 693.979L896.005 692.554C894.775 692.959 893.46 693.18 892.092 693.18L892.092 694.68L892.092 696.18C893.785 696.18 895.416 695.906 896.944 695.403L896.475 693.979ZM892.092 694.68V693.18H334V694.68V696.18H892.092V694.68ZM334 694.68L334 693.18C327.096 693.18 321.5 687.583 321.5 680.68L320 680.68L318.5 680.68C318.5 689.24 325.44 696.18 334 696.18L334 694.68ZM320 680.68H321.5V601H320H318.5V680.68H320ZM320 601H321.5C321.5 594.096 327.096 588.5 334 588.5L334 587L334 585.5C325.44 585.5 318.5 592.44 318.5 601H320ZM334 587V588.5H892.092V587V585.5H334V587Z" fill="#626A80"/>
|
||||
<path d="M892.092 587L892.092 585.5H892.092V587ZM906.092 667.955H904.592V668.082L904.613 668.207L906.092 667.955ZM927.744 693.844L928.165 692.404L928.165 692.404L927.744 693.844ZM927.52 697.508L927.494 699.008L927.494 699.008L927.52 697.508ZM905.37 696.589L905.13 698.069L905.13 698.07L905.37 696.589ZM896.475 693.979L897.089 692.61L896.558 692.372L896.005 692.554L896.475 693.979ZM892.092 694.68V696.18H892.092L892.092 694.68ZM334 694.68L334 696.18H334V694.68ZM320 680.68H318.5V680.68L320 680.68ZM334 587V585.5H334L334 587ZM892.092 587L892.092 588.5C898.995 588.5 904.592 594.097 904.592 601H906.092H907.592C907.592 592.44 900.652 585.5 892.092 585.5L892.092 587ZM906.092 601H904.592V667.955H906.092H907.592V601H906.092ZM906.092 667.955L904.613 668.207C906.561 679.638 912.755 691.026 927.323 695.284L927.744 693.844L928.165 692.404C915.047 688.571 909.395 678.413 907.57 667.703L906.092 667.955ZM927.744 693.844L927.323 695.283C927.528 695.343 927.634 695.443 927.693 695.53C927.761 695.63 927.79 695.747 927.784 695.846C927.782 695.894 927.772 695.928 927.763 695.949C927.759 695.96 927.755 695.966 927.753 695.97C927.75 695.973 927.749 695.975 927.748 695.975C927.748 695.975 927.748 695.976 927.746 695.977C927.745 695.978 927.739 695.981 927.729 695.985C927.709 695.993 927.653 696.01 927.545 696.008L927.52 697.508L927.494 699.008C928.419 699.023 929.252 698.71 929.862 698.105C930.454 697.516 930.741 696.749 930.78 696.006C930.859 694.52 929.959 692.928 928.165 692.404L927.744 693.844ZM927.52 697.508L927.545 696.008C923.196 695.933 918.428 695.827 914.372 695.678C910.273 695.527 907.016 695.336 905.61 695.108L905.37 696.589L905.13 698.07C906.758 698.334 910.222 698.527 914.262 698.676C918.346 698.826 923.136 698.933 927.494 699.008L927.52 697.508ZM905.37 696.589L905.61 695.108C902.475 694.599 899.637 693.755 897.089 692.61L896.475 693.979L895.86 695.347C898.666 696.608 901.758 697.522 905.13 698.069L905.37 696.589ZM896.475 693.979L896.005 692.554C894.775 692.959 893.46 693.18 892.092 693.18L892.092 694.68L892.092 696.18C893.785 696.18 895.416 695.906 896.944 695.403L896.475 693.979ZM892.092 694.68V693.18H334V694.68V696.18H892.092V694.68ZM334 694.68L334 693.18C327.096 693.18 321.5 687.583 321.5 680.68L320 680.68L318.5 680.68C318.5 689.24 325.44 696.18 334 696.18L334 694.68ZM320 680.68H321.5V601H320H318.5V680.68H320ZM320 601H321.5C321.5 594.096 327.096 588.5 334 588.5L334 587L334 585.5C325.44 585.5 318.5 592.44 318.5 601H320ZM334 587V588.5H892.092V587V585.5H334V587Z" fill="#181B24" fill-opacity="0.35"/>
|
||||
<path d="M739.321 660.008C741.53 660.008 743.321 661.799 743.321 664.008V666.99C743.321 669.199 741.53 670.99 739.321 670.99H363.174C360.965 670.99 359.174 669.199 359.174 666.99V664.008C359.174 661.799 360.965 660.008 363.174 660.008H739.321ZM862.92 635.35C865.129 635.35 866.92 637.14 866.92 639.35V642.332C866.92 644.541 865.129 646.332 862.92 646.332H363.174C360.965 646.332 359.174 644.541 359.174 642.332V639.35C359.174 637.14 360.965 635.35 363.174 635.35H862.92ZM862.92 610.691C865.129 610.691 866.92 612.482 866.92 614.691V617.674C866.92 619.883 865.129 621.674 862.92 621.674H363.174C360.965 621.674 359.174 619.883 359.174 617.674V614.691C359.174 612.482 360.965 610.691 363.174 610.691H862.92Z" fill="#626A80"/>
|
||||
<path d="M739.321 660.008C741.53 660.008 743.321 661.799 743.321 664.008V666.99C743.321 669.199 741.53 670.99 739.321 670.99H363.174C360.965 670.99 359.174 669.199 359.174 666.99V664.008C359.174 661.799 360.965 660.008 363.174 660.008H739.321ZM862.92 635.35C865.129 635.35 866.92 637.14 866.92 639.35V642.332C866.92 644.541 865.129 646.332 862.92 646.332H363.174C360.965 646.332 359.174 644.541 359.174 642.332V639.35C359.174 637.14 360.965 635.35 363.174 635.35H862.92ZM862.92 610.691C865.129 610.691 866.92 612.482 866.92 614.691V617.674C866.92 619.883 865.129 621.674 862.92 621.674H363.174C360.965 621.674 359.174 619.883 359.174 617.674V614.691C359.174 612.482 360.965 610.691 363.174 610.691H862.92Z" fill="#8B92AA" fill-opacity="0.28"/>
|
||||
<mask id="path-4-inside-1_386_10828" fill="white">
|
||||
<path d="M906.092 47C913.824 47.0002 920.092 53.2682 920.092 61V241.955C921.978 253.026 927.901 263.799 941.744 267.844C943.9 268.474 943.765 271.546 941.52 271.508C932.813 271.358 922.404 271.081 919.37 270.589C916.117 270.061 913.152 269.181 910.476 267.979C909.096 268.433 907.623 268.68 906.092 268.68H348C340.268 268.68 334 262.412 334 254.68V61C334 53.268 340.268 47 348 47H906.092Z"/>
|
||||
</mask>
|
||||
<path d="M906.092 47C913.824 47.0002 920.092 53.2682 920.092 61V241.955C921.978 253.026 927.901 263.799 941.744 267.844C943.9 268.474 943.765 271.546 941.52 271.508C932.813 271.358 922.404 271.081 919.37 270.589C916.117 270.061 913.152 269.181 910.476 267.979C909.096 268.433 907.623 268.68 906.092 268.68H348C340.268 268.68 334 262.412 334 254.68V61C334 53.268 340.268 47 348 47H906.092Z" fill="#626A80"/>
|
||||
<path d="M906.092 47C913.824 47.0002 920.092 53.2682 920.092 61V241.955C921.978 253.026 927.901 263.799 941.744 267.844C943.9 268.474 943.765 271.546 941.52 271.508C932.813 271.358 922.404 271.081 919.37 270.589C916.117 270.061 913.152 269.181 910.476 267.979C909.096 268.433 907.623 268.68 906.092 268.68H348C340.268 268.68 334 262.412 334 254.68V61C334 53.268 340.268 47 348 47H906.092Z" fill="#181B24" fill-opacity="0.35"/>
|
||||
<path d="M906.092 47L906.092 44H906.092V47ZM920.092 61H923.092H920.092ZM920.092 241.955H917.092V242.209L917.134 242.459L920.092 241.955ZM941.744 267.844L942.586 264.964L942.586 264.964L941.744 267.844ZM941.52 271.508L941.468 274.507L941.469 274.507L941.52 271.508ZM919.37 270.589L918.889 273.55L918.89 273.55L919.37 270.589ZM910.476 267.979L911.705 265.242L910.643 264.765L909.537 265.129L910.476 267.979ZM906.092 268.68V271.68H906.092L906.092 268.68ZM334 61L331 61V61H334ZM348 47V44V47ZM906.092 47L906.092 50C912.167 50.0002 917.092 54.9251 917.092 61H920.092H923.092C923.092 51.6112 915.48 44.0003 906.092 44L906.092 47ZM920.092 61H917.092V241.955H920.092H923.092V61H920.092ZM920.092 241.955L917.134 242.459C919.143 254.251 925.608 266.254 940.903 270.723L941.744 267.844L942.586 264.964C930.194 261.343 924.812 251.8 923.049 241.451L920.092 241.955ZM941.744 267.844L940.902 270.723C940.548 270.62 940.263 270.212 940.287 269.766C940.298 269.545 940.391 269.209 940.692 268.911C941.028 268.577 941.405 268.505 941.57 268.508L941.52 271.508L941.469 274.507C942.757 274.529 943.993 274.088 944.918 273.169C945.809 272.286 946.221 271.15 946.278 270.086C946.392 267.956 945.096 265.698 942.586 264.964L941.744 267.844ZM941.52 271.508L941.571 268.508C937.225 268.434 932.469 268.327 928.427 268.179C924.299 268.027 921.144 267.837 919.85 267.628L919.37 270.589L918.89 273.55C920.629 273.832 924.196 274.027 928.207 274.175C932.306 274.325 937.107 274.432 941.468 274.507L941.52 271.508ZM919.37 270.589L919.851 267.628C916.834 267.138 914.124 266.329 911.705 265.242L910.476 267.979L909.246 270.715C912.181 272.034 915.4 272.984 918.889 273.55L919.37 270.589ZM910.476 267.979L909.537 265.129C908.456 265.485 907.299 265.68 906.092 265.68L906.092 268.68L906.092 271.68C907.947 271.68 909.737 271.38 911.414 270.828L910.476 267.979ZM906.092 268.68V265.68H348V268.68V271.68H906.092V268.68ZM348 268.68V265.68C341.925 265.68 337 260.755 337 254.68H334H331C331 264.069 338.611 271.68 348 271.68V268.68ZM334 254.68H337V61H334H331V254.68H334ZM334 61L337 61C337 54.9249 341.925 50 348 50V47V44C338.611 44 331 51.6112 331 61L334 61ZM348 47V50H906.092V47V44H348V47Z" fill="#626A80" mask="url(#path-4-inside-1_386_10828)"/>
|
||||
<path d="M906.092 47L906.092 44H906.092V47ZM920.092 61H923.092H920.092ZM920.092 241.955H917.092V242.209L917.134 242.459L920.092 241.955ZM941.744 267.844L942.586 264.964L942.586 264.964L941.744 267.844ZM941.52 271.508L941.468 274.507L941.469 274.507L941.52 271.508ZM919.37 270.589L918.889 273.55L918.89 273.55L919.37 270.589ZM910.476 267.979L911.705 265.242L910.643 264.765L909.537 265.129L910.476 267.979ZM906.092 268.68V271.68H906.092L906.092 268.68ZM334 61L331 61V61H334ZM348 47V44V47ZM906.092 47L906.092 50C912.167 50.0002 917.092 54.9251 917.092 61H920.092H923.092C923.092 51.6112 915.48 44.0003 906.092 44L906.092 47ZM920.092 61H917.092V241.955H920.092H923.092V61H920.092ZM920.092 241.955L917.134 242.459C919.143 254.251 925.608 266.254 940.903 270.723L941.744 267.844L942.586 264.964C930.194 261.343 924.812 251.8 923.049 241.451L920.092 241.955ZM941.744 267.844L940.902 270.723C940.548 270.62 940.263 270.212 940.287 269.766C940.298 269.545 940.391 269.209 940.692 268.911C941.028 268.577 941.405 268.505 941.57 268.508L941.52 271.508L941.469 274.507C942.757 274.529 943.993 274.088 944.918 273.169C945.809 272.286 946.221 271.15 946.278 270.086C946.392 267.956 945.096 265.698 942.586 264.964L941.744 267.844ZM941.52 271.508L941.571 268.508C937.225 268.434 932.469 268.327 928.427 268.179C924.299 268.027 921.144 267.837 919.85 267.628L919.37 270.589L918.89 273.55C920.629 273.832 924.196 274.027 928.207 274.175C932.306 274.325 937.107 274.432 941.468 274.507L941.52 271.508ZM919.37 270.589L919.851 267.628C916.834 267.138 914.124 266.329 911.705 265.242L910.476 267.979L909.246 270.715C912.181 272.034 915.4 272.984 918.889 273.55L919.37 270.589ZM910.476 267.979L909.537 265.129C908.456 265.485 907.299 265.68 906.092 265.68L906.092 268.68L906.092 271.68C907.947 271.68 909.737 271.38 911.414 270.828L910.476 267.979ZM906.092 268.68V265.68H348V268.68V271.68H906.092V268.68ZM348 268.68V265.68C341.925 265.68 337 260.755 337 254.68H334H331C331 264.069 338.611 271.68 348 271.68V268.68ZM334 254.68H337V61H334H331V254.68H334ZM334 61L337 61C337 54.9249 341.925 50 348 50V47V44C338.611 44 331 51.6112 331 61L334 61ZM348 47V50H906.092V47V44H348V47Z" fill="#181B24" fill-opacity="0.35" mask="url(#path-4-inside-1_386_10828)"/>
|
||||
<path d="M850.174 234C852.383 234 854.174 235.791 854.174 238V241C854.174 243.209 852.383 245 850.174 245H377.174C374.965 245 373.174 243.209 373.174 241V238C373.174 235.791 374.965 234 377.174 234H850.174ZM876.92 209.348C879.129 209.348 880.92 211.139 880.92 213.348V216.33C880.92 218.539 879.129 220.33 876.92 220.33H377.174C374.965 220.33 373.174 218.539 373.174 216.33V213.348C373.174 211.139 374.965 209.348 377.174 209.348H876.92Z" fill="#626A80"/>
|
||||
<path d="M850.174 234C852.383 234 854.174 235.791 854.174 238V241C854.174 243.209 852.383 245 850.174 245H377.174C374.965 245 373.174 243.209 373.174 241V238C373.174 235.791 374.965 234 377.174 234H850.174ZM876.92 209.348C879.129 209.348 880.92 211.139 880.92 213.348V216.33C880.92 218.539 879.129 220.33 876.92 220.33H377.174C374.965 220.33 373.174 218.539 373.174 216.33V213.348C373.174 211.139 374.965 209.348 377.174 209.348H876.92Z" fill="#8B92AA" fill-opacity="0.28"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M373.174 189.33C373.174 191.54 374.965 193.33 377.174 193.33H876.92C879.129 193.33 880.92 191.54 880.92 189.33V186.348C880.92 184.139 879.129 182.348 876.92 182.348H377.174C374.965 182.348 373.174 184.139 373.174 186.348V189.33Z" fill="#626A80"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M373.174 189.33C373.174 191.54 374.965 193.33 377.174 193.33H876.92C879.129 193.33 880.92 191.54 880.92 189.33V186.348C880.92 184.139 879.129 182.348 876.92 182.348H377.174C374.965 182.348 373.174 184.139 373.174 186.348V189.33Z" fill="#8B92AA" fill-opacity="0.28"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M373.174 162.33C373.174 164.54 374.965 166.33 377.174 166.33H876.92C879.129 166.33 880.92 164.54 880.92 162.33V159.348C880.92 157.139 879.129 155.348 876.92 155.348H377.174C374.965 155.348 373.174 157.139 373.174 159.348V162.33Z" fill="#626A80"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M373.174 162.33C373.174 164.54 374.965 166.33 377.174 166.33H876.92C879.129 166.33 880.92 164.54 880.92 162.33V159.348C880.92 157.139 879.129 155.348 876.92 155.348H377.174C374.965 155.348 373.174 157.139 373.174 159.348V162.33Z" fill="#8B92AA" fill-opacity="0.28"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M373.174 108.33C373.174 110.54 374.965 112.33 377.174 112.33H876.92C879.129 112.33 880.92 110.54 880.92 108.33V105.348C880.92 103.139 879.129 101.348 876.92 101.348H377.174C374.965 101.348 373.174 103.139 373.174 105.348V108.33Z" fill="#626A80"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M373.174 108.33C373.174 110.54 374.965 112.33 377.174 112.33H876.92C879.129 112.33 880.92 110.54 880.92 108.33V105.348C880.92 103.139 879.129 101.348 876.92 101.348H377.174C374.965 101.348 373.174 103.139 373.174 105.348V108.33Z" fill="#8B92AA" fill-opacity="0.28"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M373.174 81.3305C373.174 83.5396 374.965 85.3305 377.174 85.3305H876.92C879.129 85.3305 880.92 83.5396 880.92 81.3305V78.3477C880.92 76.1385 879.129 74.3477 876.92 74.3477H377.174C374.965 74.3477 373.174 76.1385 373.174 78.3477V81.3305Z" fill="#626A80"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M373.174 81.3305C373.174 83.5396 374.965 85.3305 377.174 85.3305H876.92C879.129 85.3305 880.92 83.5396 880.92 81.3305V78.3477C880.92 76.1385 879.129 74.3477 876.92 74.3477H377.174C374.965 74.3477 373.174 76.1385 373.174 78.3477V81.3305Z" fill="#8B92AA" fill-opacity="0.28"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M373 135C373 137.209 374.791 139 377 139H614C616.209 139 618 137.209 618 135V132C618 129.791 616.209 128 614 128H377C374.791 128 373 129.791 373 132V135Z" fill="#626A80"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M373 135C373 137.209 374.791 139 377 139H614C616.209 139 618 137.209 618 135V132C618 129.791 616.209 128 614 128H377C374.791 128 373 129.791 373 132V135Z" fill="#8B92AA" fill-opacity="0.28"/>
|
||||
<path d="M737.09 330C744.822 330 751.09 336.268 751.09 344V525.68C751.09 533.412 744.822 539.679 737.09 539.68H178.998C171.266 539.68 164.998 533.412 164.998 525.68V344C164.998 336.268 171.266 330 178.998 330H737.09Z" fill="#626A80"/>
|
||||
<path d="M737.09 330C744.822 330 751.09 336.268 751.09 344V525.68C751.09 533.412 744.822 539.679 737.09 539.68H178.998C171.266 539.68 164.998 533.412 164.998 525.68V344C164.998 336.268 171.266 330 178.998 330H737.09Z" fill="#181B24" fill-opacity="0.35"/>
|
||||
<path d="M737.09 330L737.09 328.5H737.09V330ZM737.09 539.68V541.18H737.09L737.09 539.68ZM164.998 344L163.498 344V344H164.998ZM737.09 330L737.09 331.5C743.993 331.5 749.59 337.097 749.59 344H751.09H752.59C752.59 335.44 745.65 328.5 737.09 328.5L737.09 330ZM751.09 344H749.59V525.68H751.09H752.59V344H751.09ZM751.09 525.68H749.59C749.59 532.583 743.993 538.179 737.09 538.18L737.09 539.68L737.09 541.18C745.65 541.179 752.59 534.24 752.59 525.68H751.09ZM737.09 539.68V538.18H178.998V539.68V541.18H737.09V539.68ZM178.998 539.68V538.18C172.094 538.18 166.498 532.583 166.498 525.68H164.998H163.498C163.498 534.24 170.438 541.18 178.998 541.18V539.68ZM164.998 525.68H166.498V344H164.998H163.498V525.68H164.998ZM164.998 344L166.498 344C166.498 337.096 172.094 331.5 178.998 331.5V330V328.5C170.438 328.5 163.498 335.44 163.498 344L164.998 344ZM178.998 330V331.5H737.09V330V328.5H178.998V330Z" fill="#626A80"/>
|
||||
<path d="M737.09 330L737.09 328.5H737.09V330ZM737.09 539.68V541.18H737.09L737.09 539.68ZM164.998 344L163.498 344V344H164.998ZM737.09 330L737.09 331.5C743.993 331.5 749.59 337.097 749.59 344H751.09H752.59C752.59 335.44 745.65 328.5 737.09 328.5L737.09 330ZM751.09 344H749.59V525.68H751.09H752.59V344H751.09ZM751.09 525.68H749.59C749.59 532.583 743.993 538.179 737.09 538.18L737.09 539.68L737.09 541.18C745.65 541.179 752.59 534.24 752.59 525.68H751.09ZM737.09 539.68V538.18H178.998V539.68V541.18H737.09V539.68ZM178.998 539.68V538.18C172.094 538.18 166.498 532.583 166.498 525.68H164.998H163.498C163.498 534.24 170.438 541.18 178.998 541.18V539.68ZM164.998 525.68H166.498V344H164.998H163.498V525.68H164.998ZM164.998 344L166.498 344C166.498 337.096 172.094 331.5 178.998 331.5V330V328.5C170.438 328.5 163.498 335.44 163.498 344L164.998 344ZM178.998 330V331.5H737.09V330V328.5H178.998V330Z" fill="#181B24" fill-opacity="0.35"/>
|
||||
<path d="M452.998 503C455.207 503 456.998 504.791 456.998 507V510C456.998 512.209 455.207 514 452.998 514H201.998C199.789 514 197.998 512.209 197.998 510V507C197.998 504.791 199.789 503 201.998 503H452.998ZM701.998 479C704.207 479 705.998 480.791 705.998 483V486C705.998 488.209 704.207 490 701.998 490H201.998C199.789 490 197.998 488.209 197.998 486V483C197.998 480.791 199.789 479 201.998 479H701.998ZM701.744 454.658C703.953 454.658 705.744 456.449 705.744 458.658V461.641C705.744 463.85 703.953 465.641 701.744 465.641H201.998C199.789 465.641 197.998 463.85 197.998 461.641V458.658C197.998 456.449 199.789 454.658 201.998 454.658H701.744ZM701.744 430C703.953 430 705.744 431.791 705.744 434V436.982C705.744 439.192 703.953 440.982 701.744 440.982H201.998C199.789 440.982 197.998 439.192 197.998 436.982V434C197.998 431.791 199.789 430 201.998 430H701.744ZM701.998 405C704.207 405 705.998 406.791 705.998 409V412C705.998 414.209 704.207 416 701.998 416H201.998C199.789 416 197.998 414.209 197.998 412V409C197.998 406.791 199.789 405 201.998 405H701.998ZM701.744 380.658C703.953 380.658 705.744 382.449 705.744 384.658V387.641C705.744 389.85 703.953 391.641 701.744 391.641H201.998C199.789 391.641 197.998 389.85 197.998 387.641V384.658C197.998 382.449 199.789 380.658 201.998 380.658H701.744ZM701.744 356C703.953 356 705.744 357.791 705.744 360V362.982C705.744 365.192 703.953 366.982 701.744 366.982H201.998C199.789 366.982 197.998 365.192 197.998 362.982V360C197.998 357.791 199.789 356 201.998 356H701.744Z" fill="#626A80"/>
|
||||
<path d="M452.998 503C455.207 503 456.998 504.791 456.998 507V510C456.998 512.209 455.207 514 452.998 514H201.998C199.789 514 197.998 512.209 197.998 510V507C197.998 504.791 199.789 503 201.998 503H452.998ZM701.998 479C704.207 479 705.998 480.791 705.998 483V486C705.998 488.209 704.207 490 701.998 490H201.998C199.789 490 197.998 488.209 197.998 486V483C197.998 480.791 199.789 479 201.998 479H701.998ZM701.744 454.658C703.953 454.658 705.744 456.449 705.744 458.658V461.641C705.744 463.85 703.953 465.641 701.744 465.641H201.998C199.789 465.641 197.998 463.85 197.998 461.641V458.658C197.998 456.449 199.789 454.658 201.998 454.658H701.744ZM701.744 430C703.953 430 705.744 431.791 705.744 434V436.982C705.744 439.192 703.953 440.982 701.744 440.982H201.998C199.789 440.982 197.998 439.192 197.998 436.982V434C197.998 431.791 199.789 430 201.998 430H701.744ZM701.998 405C704.207 405 705.998 406.791 705.998 409V412C705.998 414.209 704.207 416 701.998 416H201.998C199.789 416 197.998 414.209 197.998 412V409C197.998 406.791 199.789 405 201.998 405H701.998ZM701.744 380.658C703.953 380.658 705.744 382.449 705.744 384.658V387.641C705.744 389.85 703.953 391.641 701.744 391.641H201.998C199.789 391.641 197.998 389.85 197.998 387.641V384.658C197.998 382.449 199.789 380.658 201.998 380.658H701.744ZM701.744 356C703.953 356 705.744 357.791 705.744 360V362.982C705.744 365.192 703.953 366.982 701.744 366.982H201.998C199.789 366.982 197.998 365.192 197.998 362.982V360C197.998 357.791 199.789 356 201.998 356H701.744Z" fill="#8B92AA" fill-opacity="0.28"/>
|
||||
<path d="M136.494 334.852C136.812 336.284 137.647 337 139 337C140.353 337 141.148 336.284 141.387 334.852C142.342 329.84 143.177 325.941 143.893 323.156C144.689 320.292 145.763 318.144 147.115 316.712C148.547 315.2 150.695 314.047 153.56 313.251C156.503 312.455 160.561 311.54 165.733 310.506C167.244 310.188 168 309.353 168 308C168 306.647 167.244 305.812 165.733 305.494C160.561 304.46 156.543 303.505 153.679 302.63C150.894 301.754 148.786 300.601 147.354 299.169C146.001 297.737 144.927 295.628 144.132 292.844C143.416 289.979 142.501 286.081 141.387 281.148C141.148 279.716 140.353 279 139 279C137.647 279 136.812 279.716 136.494 281.148C135.46 286.081 134.545 289.979 133.749 292.844C133.033 295.628 131.959 297.737 130.527 299.169C129.174 300.601 127.066 301.754 124.202 302.63C121.417 303.505 117.439 304.46 112.267 305.494C110.756 305.812 110 306.647 110 308C110 309.353 110.756 310.188 112.267 310.506C117.439 311.54 121.457 312.495 124.321 313.37C127.185 314.166 129.333 315.32 130.765 316.831C132.198 318.263 133.272 320.372 133.988 323.156C134.704 325.941 135.539 329.84 136.494 334.852Z" fill="#3E5DE7"/>
|
||||
<path d="M136.494 334.852C136.812 336.284 137.647 337 139 337C140.353 337 141.148 336.284 141.387 334.852C142.342 329.84 143.177 325.941 143.893 323.156C144.689 320.292 145.763 318.144 147.115 316.712C148.547 315.2 150.695 314.047 153.56 313.251C156.503 312.455 160.561 311.54 165.733 310.506C167.244 310.188 168 309.353 168 308C168 306.647 167.244 305.812 165.733 305.494C160.561 304.46 156.543 303.505 153.679 302.63C150.894 301.754 148.786 300.601 147.354 299.169C146.001 297.737 144.927 295.628 144.132 292.844C143.416 289.979 142.501 286.081 141.387 281.148C141.148 279.716 140.353 279 139 279C137.647 279 136.812 279.716 136.494 281.148C135.46 286.081 134.545 289.979 133.749 292.844C133.033 295.628 131.959 297.737 130.527 299.169C129.174 300.601 127.066 301.754 124.202 302.63C121.417 303.505 117.439 304.46 112.267 305.494C110.756 305.812 110 306.647 110 308C110 309.353 110.756 310.188 112.267 310.506C117.439 311.54 121.457 312.495 124.321 313.37C127.185 314.166 129.333 315.32 130.765 316.831C132.198 318.263 133.272 320.372 133.988 323.156C134.704 325.941 135.539 329.84 136.494 334.852Z" fill="#181B24" fill-opacity="0.35"/>
|
||||
<path d="M136.494 334.852C136.812 336.284 137.647 337 139 337C140.353 337 141.148 336.284 141.387 334.852C142.342 329.84 143.177 325.941 143.893 323.156C144.689 320.292 145.763 318.144 147.115 316.712C148.547 315.2 150.695 314.047 153.56 313.251C156.503 312.455 160.561 311.54 165.733 310.506C167.244 310.188 168 309.353 168 308C168 306.647 167.244 305.812 165.733 305.494C160.561 304.46 156.543 303.505 153.679 302.63C150.894 301.754 148.786 300.601 147.354 299.169C146.001 297.737 144.927 295.628 144.132 292.844C143.416 289.979 142.501 286.081 141.387 281.148C141.148 279.716 140.353 279 139 279C137.647 279 136.812 279.716 136.494 281.148C135.46 286.081 134.545 289.979 133.749 292.844C133.033 295.628 131.959 297.737 130.527 299.169C129.174 300.601 127.066 301.754 124.202 302.63C121.417 303.505 117.439 304.46 112.267 305.494C110.756 305.812 110 306.647 110 308C110 309.353 110.756 310.188 112.267 310.506C117.439 311.54 121.457 312.495 124.321 313.37C127.185 314.166 129.333 315.32 130.765 316.831C132.198 318.263 133.272 320.372 133.988 323.156C134.704 325.941 135.539 329.84 136.494 334.852Z" fill="#7E97FB"/>
|
||||
<path d="M99.2716 375.519C99.4911 376.506 100.067 377 101 377C101.933 377 102.481 376.506 102.646 375.519C103.305 372.062 103.881 369.373 104.374 367.453C104.923 365.477 105.664 363.996 106.597 363.008C107.584 361.966 109.066 361.17 111.041 360.621C113.071 360.073 115.87 359.442 119.436 358.728C120.479 358.509 121 357.933 121 357C121 356.067 120.479 355.491 119.436 355.272C115.87 354.558 113.099 353.9 111.123 353.296C109.203 352.693 107.749 351.897 106.761 350.909C105.829 349.922 105.088 348.468 104.539 346.547C104.045 344.572 103.414 341.883 102.646 338.481C102.481 337.494 101.933 337 101 337C100.067 337 99.4911 337.494 99.2716 338.481C98.5583 341.883 97.9273 344.572 97.3786 346.547C96.8848 348.468 96.144 349.922 95.1564 350.909C94.2236 351.897 92.7695 352.693 90.7942 353.296C88.8738 353.9 86.1303 354.558 82.5638 355.272C81.5213 355.491 81 356.067 81 357C81 357.933 81.5213 358.509 82.5638 358.728C86.1303 359.442 88.9012 360.1 90.8765 360.704C92.8519 361.252 94.3333 362.048 95.321 363.091C96.3086 364.078 97.0494 365.532 97.5432 367.453C98.037 369.373 98.6132 372.062 99.2716 375.519Z" fill="#3E5DE7"/>
|
||||
<path d="M99.2716 375.519C99.4911 376.506 100.067 377 101 377C101.933 377 102.481 376.506 102.646 375.519C103.305 372.062 103.881 369.373 104.374 367.453C104.923 365.477 105.664 363.996 106.597 363.008C107.584 361.966 109.066 361.17 111.041 360.621C113.071 360.073 115.87 359.442 119.436 358.728C120.479 358.509 121 357.933 121 357C121 356.067 120.479 355.491 119.436 355.272C115.87 354.558 113.099 353.9 111.123 353.296C109.203 352.693 107.749 351.897 106.761 350.909C105.829 349.922 105.088 348.468 104.539 346.547C104.045 344.572 103.414 341.883 102.646 338.481C102.481 337.494 101.933 337 101 337C100.067 337 99.4911 337.494 99.2716 338.481C98.5583 341.883 97.9273 344.572 97.3786 346.547C96.8848 348.468 96.144 349.922 95.1564 350.909C94.2236 351.897 92.7695 352.693 90.7942 353.296C88.8738 353.9 86.1303 354.558 82.5638 355.272C81.5213 355.491 81 356.067 81 357C81 357.933 81.5213 358.509 82.5638 358.728C86.1303 359.442 88.9012 360.1 90.8765 360.704C92.8519 361.252 94.3333 362.048 95.321 363.091C96.3086 364.078 97.0494 365.532 97.5432 367.453C98.037 369.373 98.6132 372.062 99.2716 375.519Z" fill="#181B24" fill-opacity="0.35"/>
|
||||
<path d="M99.2716 375.519C99.4911 376.506 100.067 377 101 377C101.933 377 102.481 376.506 102.646 375.519C103.305 372.062 103.881 369.373 104.374 367.453C104.923 365.477 105.664 363.996 106.597 363.008C107.584 361.966 109.066 361.17 111.041 360.621C113.071 360.073 115.87 359.442 119.436 358.728C120.479 358.509 121 357.933 121 357C121 356.067 120.479 355.491 119.436 355.272C115.87 354.558 113.099 353.9 111.123 353.296C109.203 352.693 107.749 351.897 106.761 350.909C105.829 349.922 105.088 348.468 104.539 346.547C104.045 344.572 103.414 341.883 102.646 338.481C102.481 337.494 101.933 337 101 337C100.067 337 99.4911 337.494 99.2716 338.481C98.5583 341.883 97.9273 344.572 97.3786 346.547C96.8848 348.468 96.144 349.922 95.1564 350.909C94.2236 351.897 92.7695 352.693 90.7942 353.296C88.8738 353.9 86.1303 354.558 82.5638 355.272C81.5213 355.491 81 356.067 81 357C81 357.933 81.5213 358.509 82.5638 358.728C86.1303 359.442 88.9012 360.1 90.8765 360.704C92.8519 361.252 94.3333 362.048 95.321 363.091C96.3086 364.078 97.0494 365.532 97.5432 367.453C98.037 369.373 98.6132 372.062 99.2716 375.519Z" fill="#7E97FB"/>
|
||||
<path d="M208.222 569.333C208.321 569.778 208.58 570 209 570C209.42 570 209.667 569.778 209.741 569.333C210.037 567.778 210.296 566.568 210.519 565.704C210.765 564.815 211.099 564.148 211.519 563.704C211.963 563.235 212.63 562.877 213.519 562.63C214.432 562.383 215.691 562.099 217.296 561.778C217.765 561.679 218 561.42 218 561C218 560.58 217.765 560.321 217.296 560.222C215.691 559.901 214.444 559.605 213.556 559.333C212.691 559.062 212.037 558.704 211.593 558.259C211.173 557.815 210.84 557.16 210.593 556.296C210.37 555.407 210.086 554.198 209.741 552.667C209.667 552.222 209.42 552 209 552C208.58 552 208.321 552.222 208.222 552.667C207.901 554.198 207.617 555.407 207.37 556.296C207.148 557.16 206.815 557.815 206.37 558.259C205.951 558.704 205.296 559.062 204.407 559.333C203.543 559.605 202.309 559.901 200.704 560.222C200.235 560.321 200 560.58 200 561C200 561.42 200.235 561.679 200.704 561.778C202.309 562.099 203.556 562.395 204.444 562.667C205.333 562.914 206 563.272 206.444 563.741C206.889 564.185 207.222 564.84 207.444 565.704C207.667 566.568 207.926 567.778 208.222 569.333Z" fill="#3E5DE7"/>
|
||||
<path d="M208.222 569.333C208.321 569.778 208.58 570 209 570C209.42 570 209.667 569.778 209.741 569.333C210.037 567.778 210.296 566.568 210.519 565.704C210.765 564.815 211.099 564.148 211.519 563.704C211.963 563.235 212.63 562.877 213.519 562.63C214.432 562.383 215.691 562.099 217.296 561.778C217.765 561.679 218 561.42 218 561C218 560.58 217.765 560.321 217.296 560.222C215.691 559.901 214.444 559.605 213.556 559.333C212.691 559.062 212.037 558.704 211.593 558.259C211.173 557.815 210.84 557.16 210.593 556.296C210.37 555.407 210.086 554.198 209.741 552.667C209.667 552.222 209.42 552 209 552C208.58 552 208.321 552.222 208.222 552.667C207.901 554.198 207.617 555.407 207.37 556.296C207.148 557.16 206.815 557.815 206.37 558.259C205.951 558.704 205.296 559.062 204.407 559.333C203.543 559.605 202.309 559.901 200.704 560.222C200.235 560.321 200 560.58 200 561C200 561.42 200.235 561.679 200.704 561.778C202.309 562.099 203.556 562.395 204.444 562.667C205.333 562.914 206 563.272 206.444 563.741C206.889 564.185 207.222 564.84 207.444 565.704C207.667 566.568 207.926 567.778 208.222 569.333Z" fill="#181B24" fill-opacity="0.35"/>
|
||||
<path d="M208.222 569.333C208.321 569.778 208.58 570 209 570C209.42 570 209.667 569.778 209.741 569.333C210.037 567.778 210.296 566.568 210.519 565.704C210.765 564.815 211.099 564.148 211.519 563.704C211.963 563.235 212.63 562.877 213.519 562.63C214.432 562.383 215.691 562.099 217.296 561.778C217.765 561.679 218 561.42 218 561C218 560.58 217.765 560.321 217.296 560.222C215.691 559.901 214.444 559.605 213.556 559.333C212.691 559.062 212.037 558.704 211.593 558.259C211.173 557.815 210.84 557.16 210.593 556.296C210.37 555.407 210.086 554.198 209.741 552.667C209.667 552.222 209.42 552 209 552C208.58 552 208.321 552.222 208.222 552.667C207.901 554.198 207.617 555.407 207.37 556.296C207.148 557.16 206.815 557.815 206.37 558.259C205.951 558.704 205.296 559.062 204.407 559.333C203.543 559.605 202.309 559.901 200.704 560.222C200.235 560.321 200 560.58 200 561C200 561.42 200.235 561.679 200.704 561.778C202.309 562.099 203.556 562.395 204.444 562.667C205.333 562.914 206 563.272 206.444 563.741C206.889 564.185 207.222 564.84 207.444 565.704C207.667 566.568 207.926 567.778 208.222 569.333Z" fill="#7E97FB"/>
|
||||
<path d="M236.222 578.333C236.321 578.778 236.58 579 237 579C237.42 579 237.667 578.778 237.741 578.333C238.037 576.778 238.296 575.568 238.519 574.704C238.765 573.815 239.099 573.148 239.519 572.704C239.963 572.235 240.63 571.877 241.519 571.63C242.432 571.383 243.691 571.099 245.296 570.778C245.765 570.679 246 570.42 246 570C246 569.58 245.765 569.321 245.296 569.222C243.691 568.901 242.444 568.605 241.556 568.333C240.691 568.062 240.037 567.704 239.593 567.259C239.173 566.815 238.84 566.16 238.593 565.296C238.37 564.407 238.086 563.198 237.741 561.667C237.667 561.222 237.42 561 237 561C236.58 561 236.321 561.222 236.222 561.667C235.901 563.198 235.617 564.407 235.37 565.296C235.148 566.16 234.815 566.815 234.37 567.259C233.951 567.704 233.296 568.062 232.407 568.333C231.543 568.605 230.309 568.901 228.704 569.222C228.235 569.321 228 569.58 228 570C228 570.42 228.235 570.679 228.704 570.778C230.309 571.099 231.556 571.395 232.444 571.667C233.333 571.914 234 572.272 234.444 572.741C234.889 573.185 235.222 573.84 235.444 574.704C235.667 575.568 235.926 576.778 236.222 578.333Z" fill="#3E5DE7"/>
|
||||
<path d="M236.222 578.333C236.321 578.778 236.58 579 237 579C237.42 579 237.667 578.778 237.741 578.333C238.037 576.778 238.296 575.568 238.519 574.704C238.765 573.815 239.099 573.148 239.519 572.704C239.963 572.235 240.63 571.877 241.519 571.63C242.432 571.383 243.691 571.099 245.296 570.778C245.765 570.679 246 570.42 246 570C246 569.58 245.765 569.321 245.296 569.222C243.691 568.901 242.444 568.605 241.556 568.333C240.691 568.062 240.037 567.704 239.593 567.259C239.173 566.815 238.84 566.16 238.593 565.296C238.37 564.407 238.086 563.198 237.741 561.667C237.667 561.222 237.42 561 237 561C236.58 561 236.321 561.222 236.222 561.667C235.901 563.198 235.617 564.407 235.37 565.296C235.148 566.16 234.815 566.815 234.37 567.259C233.951 567.704 233.296 568.062 232.407 568.333C231.543 568.605 230.309 568.901 228.704 569.222C228.235 569.321 228 569.58 228 570C228 570.42 228.235 570.679 228.704 570.778C230.309 571.099 231.556 571.395 232.444 571.667C233.333 571.914 234 572.272 234.444 572.741C234.889 573.185 235.222 573.84 235.444 574.704C235.667 575.568 235.926 576.778 236.222 578.333Z" fill="#181B24" fill-opacity="0.35"/>
|
||||
<path d="M236.222 578.333C236.321 578.778 236.58 579 237 579C237.42 579 237.667 578.778 237.741 578.333C238.037 576.778 238.296 575.568 238.519 574.704C238.765 573.815 239.099 573.148 239.519 572.704C239.963 572.235 240.63 571.877 241.519 571.63C242.432 571.383 243.691 571.099 245.296 570.778C245.765 570.679 246 570.42 246 570C246 569.58 245.765 569.321 245.296 569.222C243.691 568.901 242.444 568.605 241.556 568.333C240.691 568.062 240.037 567.704 239.593 567.259C239.173 566.815 238.84 566.16 238.593 565.296C238.37 564.407 238.086 563.198 237.741 561.667C237.667 561.222 237.42 561 237 561C236.58 561 236.321 561.222 236.222 561.667C235.901 563.198 235.617 564.407 235.37 565.296C235.148 566.16 234.815 566.815 234.37 567.259C233.951 567.704 233.296 568.062 232.407 568.333C231.543 568.605 230.309 568.901 228.704 569.222C228.235 569.321 228 569.58 228 570C228 570.42 228.235 570.679 228.704 570.778C230.309 571.099 231.556 571.395 232.444 571.667C233.333 571.914 234 572.272 234.444 572.741C234.889 573.185 235.222 573.84 235.444 574.704C235.667 575.568 235.926 576.778 236.222 578.333Z" fill="#7E97FB"/>
|
||||
<path d="M135.506 390.185C135.632 390.728 135.964 391 136.5 391C137.036 391 137.352 390.728 137.447 390.185C137.825 388.284 138.156 386.805 138.44 385.749C138.756 384.663 139.182 383.848 139.718 383.305C140.286 382.731 141.138 382.294 142.274 381.992C143.441 381.69 145.05 381.343 147.101 380.951C147.7 380.83 148 380.513 148 380C148 379.487 147.7 379.17 147.101 379.049C145.05 378.657 143.457 378.295 142.321 377.963C141.217 377.631 140.381 377.193 139.813 376.65C139.276 376.107 138.85 375.307 138.535 374.251C138.251 373.165 137.888 371.686 137.447 369.815C137.352 369.272 137.036 369 136.5 369C135.964 369 135.632 369.272 135.506 369.815C135.096 371.686 134.733 373.165 134.418 374.251C134.134 375.307 133.708 376.107 133.14 376.65C132.604 377.193 131.767 377.631 130.632 377.963C129.527 378.295 127.95 378.657 125.899 379.049C125.3 379.17 125 379.487 125 380C125 380.513 125.3 380.83 125.899 380.951C127.95 381.343 129.543 381.705 130.679 382.037C131.815 382.339 132.667 382.776 133.235 383.35C133.802 383.893 134.228 384.693 134.512 385.749C134.796 386.805 135.128 388.284 135.506 390.185Z" fill="#3E5DE7"/>
|
||||
<path d="M135.506 390.185C135.632 390.728 135.964 391 136.5 391C137.036 391 137.352 390.728 137.447 390.185C137.825 388.284 138.156 386.805 138.44 385.749C138.756 384.663 139.182 383.848 139.718 383.305C140.286 382.731 141.138 382.294 142.274 381.992C143.441 381.69 145.05 381.343 147.101 380.951C147.7 380.83 148 380.513 148 380C148 379.487 147.7 379.17 147.101 379.049C145.05 378.657 143.457 378.295 142.321 377.963C141.217 377.631 140.381 377.193 139.813 376.65C139.276 376.107 138.85 375.307 138.535 374.251C138.251 373.165 137.888 371.686 137.447 369.815C137.352 369.272 137.036 369 136.5 369C135.964 369 135.632 369.272 135.506 369.815C135.096 371.686 134.733 373.165 134.418 374.251C134.134 375.307 133.708 376.107 133.14 376.65C132.604 377.193 131.767 377.631 130.632 377.963C129.527 378.295 127.95 378.657 125.899 379.049C125.3 379.17 125 379.487 125 380C125 380.513 125.3 380.83 125.899 380.951C127.95 381.343 129.543 381.705 130.679 382.037C131.815 382.339 132.667 382.776 133.235 383.35C133.802 383.893 134.228 384.693 134.512 385.749C134.796 386.805 135.128 388.284 135.506 390.185Z" fill="#181B24" fill-opacity="0.35"/>
|
||||
<path d="M135.506 390.185C135.632 390.728 135.964 391 136.5 391C137.036 391 137.352 390.728 137.447 390.185C137.825 388.284 138.156 386.805 138.44 385.749C138.756 384.663 139.182 383.848 139.718 383.305C140.286 382.731 141.138 382.294 142.274 381.992C143.441 381.69 145.05 381.343 147.101 380.951C147.7 380.83 148 380.513 148 380C148 379.487 147.7 379.17 147.101 379.049C145.05 378.657 143.457 378.295 142.321 377.963C141.217 377.631 140.381 377.193 139.813 376.65C139.276 376.107 138.85 375.307 138.535 374.251C138.251 373.165 137.888 371.686 137.447 369.815C137.352 369.272 137.036 369 136.5 369C135.964 369 135.632 369.272 135.506 369.815C135.096 371.686 134.733 373.165 134.418 374.251C134.134 375.307 133.708 376.107 133.14 376.65C132.604 377.193 131.767 377.631 130.632 377.963C129.527 378.295 127.95 378.657 125.899 379.049C125.3 379.17 125 379.487 125 380C125 380.513 125.3 380.83 125.899 380.951C127.95 381.343 129.543 381.705 130.679 382.037C131.815 382.339 132.667 382.776 133.235 383.35C133.802 383.893 134.228 384.693 134.512 385.749C134.796 386.805 135.128 388.284 135.506 390.185Z" fill="#7E97FB"/>
|
||||
<path d="M722.222 567.333C722.321 567.778 722.58 568 723 568C723.42 568 723.667 567.778 723.741 567.333C724.037 565.778 724.296 564.568 724.519 563.704C724.765 562.815 725.099 562.148 725.519 561.704C725.963 561.235 726.63 560.877 727.519 560.63C728.432 560.383 729.691 560.099 731.296 559.778C731.765 559.679 732 559.42 732 559C732 558.58 731.765 558.321 731.296 558.222C729.691 557.901 728.444 557.605 727.556 557.333C726.691 557.062 726.037 556.704 725.593 556.259C725.173 555.815 724.84 555.16 724.593 554.296C724.37 553.407 724.086 552.198 723.741 550.667C723.667 550.222 723.42 550 723 550C722.58 550 722.321 550.222 722.222 550.667C721.901 552.198 721.617 553.407 721.37 554.296C721.148 555.16 720.815 555.815 720.37 556.259C719.951 556.704 719.296 557.062 718.407 557.333C717.543 557.605 716.309 557.901 714.704 558.222C714.235 558.321 714 558.58 714 559C714 559.42 714.235 559.679 714.704 559.778C716.309 560.099 717.556 560.395 718.444 560.667C719.333 560.914 720 561.272 720.444 561.741C720.889 562.185 721.222 562.84 721.444 563.704C721.667 564.568 721.926 565.778 722.222 567.333Z" fill="#3E5DE7"/>
|
||||
<path d="M722.222 567.333C722.321 567.778 722.58 568 723 568C723.42 568 723.667 567.778 723.741 567.333C724.037 565.778 724.296 564.568 724.519 563.704C724.765 562.815 725.099 562.148 725.519 561.704C725.963 561.235 726.63 560.877 727.519 560.63C728.432 560.383 729.691 560.099 731.296 559.778C731.765 559.679 732 559.42 732 559C732 558.58 731.765 558.321 731.296 558.222C729.691 557.901 728.444 557.605 727.556 557.333C726.691 557.062 726.037 556.704 725.593 556.259C725.173 555.815 724.84 555.16 724.593 554.296C724.37 553.407 724.086 552.198 723.741 550.667C723.667 550.222 723.42 550 723 550C722.58 550 722.321 550.222 722.222 550.667C721.901 552.198 721.617 553.407 721.37 554.296C721.148 555.16 720.815 555.815 720.37 556.259C719.951 556.704 719.296 557.062 718.407 557.333C717.543 557.605 716.309 557.901 714.704 558.222C714.235 558.321 714 558.58 714 559C714 559.42 714.235 559.679 714.704 559.778C716.309 560.099 717.556 560.395 718.444 560.667C719.333 560.914 720 561.272 720.444 561.741C720.889 562.185 721.222 562.84 721.444 563.704C721.667 564.568 721.926 565.778 722.222 567.333Z" fill="#181B24" fill-opacity="0.35"/>
|
||||
<path d="M722.222 567.333C722.321 567.778 722.58 568 723 568C723.42 568 723.667 567.778 723.741 567.333C724.037 565.778 724.296 564.568 724.519 563.704C724.765 562.815 725.099 562.148 725.519 561.704C725.963 561.235 726.63 560.877 727.519 560.63C728.432 560.383 729.691 560.099 731.296 559.778C731.765 559.679 732 559.42 732 559C732 558.58 731.765 558.321 731.296 558.222C729.691 557.901 728.444 557.605 727.556 557.333C726.691 557.062 726.037 556.704 725.593 556.259C725.173 555.815 724.84 555.16 724.593 554.296C724.37 553.407 724.086 552.198 723.741 550.667C723.667 550.222 723.42 550 723 550C722.58 550 722.321 550.222 722.222 550.667C721.901 552.198 721.617 553.407 721.37 554.296C721.148 555.16 720.815 555.815 720.37 556.259C719.951 556.704 719.296 557.062 718.407 557.333C717.543 557.605 716.309 557.901 714.704 558.222C714.235 558.321 714 558.58 714 559C714 559.42 714.235 559.679 714.704 559.778C716.309 560.099 717.556 560.395 718.444 560.667C719.333 560.914 720 561.272 720.444 561.741C720.889 562.185 721.222 562.84 721.444 563.704C721.667 564.568 721.926 565.778 722.222 567.333Z" fill="#7E97FB"/>
|
||||
<path d="M750.222 576.333C750.321 576.778 750.58 577 751 577C751.42 577 751.667 576.778 751.741 576.333C752.037 574.778 752.296 573.568 752.519 572.704C752.765 571.815 753.099 571.148 753.519 570.704C753.963 570.235 754.63 569.877 755.519 569.63C756.432 569.383 757.691 569.099 759.296 568.778C759.765 568.679 760 568.42 760 568C760 567.58 759.765 567.321 759.296 567.222C757.691 566.901 756.444 566.605 755.556 566.333C754.691 566.062 754.037 565.704 753.593 565.259C753.173 564.815 752.84 564.16 752.593 563.296C752.37 562.407 752.086 561.198 751.741 559.667C751.667 559.222 751.42 559 751 559C750.58 559 750.321 559.222 750.222 559.667C749.901 561.198 749.617 562.407 749.37 563.296C749.148 564.16 748.815 564.815 748.37 565.259C747.951 565.704 747.296 566.062 746.407 566.333C745.543 566.605 744.309 566.901 742.704 567.222C742.235 567.321 742 567.58 742 568C742 568.42 742.235 568.679 742.704 568.778C744.309 569.099 745.556 569.395 746.444 569.667C747.333 569.914 748 570.272 748.444 570.741C748.889 571.185 749.222 571.84 749.444 572.704C749.667 573.568 749.926 574.778 750.222 576.333Z" fill="#3E5DE7"/>
|
||||
<path d="M750.222 576.333C750.321 576.778 750.58 577 751 577C751.42 577 751.667 576.778 751.741 576.333C752.037 574.778 752.296 573.568 752.519 572.704C752.765 571.815 753.099 571.148 753.519 570.704C753.963 570.235 754.63 569.877 755.519 569.63C756.432 569.383 757.691 569.099 759.296 568.778C759.765 568.679 760 568.42 760 568C760 567.58 759.765 567.321 759.296 567.222C757.691 566.901 756.444 566.605 755.556 566.333C754.691 566.062 754.037 565.704 753.593 565.259C753.173 564.815 752.84 564.16 752.593 563.296C752.37 562.407 752.086 561.198 751.741 559.667C751.667 559.222 751.42 559 751 559C750.58 559 750.321 559.222 750.222 559.667C749.901 561.198 749.617 562.407 749.37 563.296C749.148 564.16 748.815 564.815 748.37 565.259C747.951 565.704 747.296 566.062 746.407 566.333C745.543 566.605 744.309 566.901 742.704 567.222C742.235 567.321 742 567.58 742 568C742 568.42 742.235 568.679 742.704 568.778C744.309 569.099 745.556 569.395 746.444 569.667C747.333 569.914 748 570.272 748.444 570.741C748.889 571.185 749.222 571.84 749.444 572.704C749.667 573.568 749.926 574.778 750.222 576.333Z" fill="#181B24" fill-opacity="0.35"/>
|
||||
<path d="M750.222 576.333C750.321 576.778 750.58 577 751 577C751.42 577 751.667 576.778 751.741 576.333C752.037 574.778 752.296 573.568 752.519 572.704C752.765 571.815 753.099 571.148 753.519 570.704C753.963 570.235 754.63 569.877 755.519 569.63C756.432 569.383 757.691 569.099 759.296 568.778C759.765 568.679 760 568.42 760 568C760 567.58 759.765 567.321 759.296 567.222C757.691 566.901 756.444 566.605 755.556 566.333C754.691 566.062 754.037 565.704 753.593 565.259C753.173 564.815 752.84 564.16 752.593 563.296C752.37 562.407 752.086 561.198 751.741 559.667C751.667 559.222 751.42 559 751 559C750.58 559 750.321 559.222 750.222 559.667C749.901 561.198 749.617 562.407 749.37 563.296C749.148 564.16 748.815 564.815 748.37 565.259C747.951 565.704 747.296 566.062 746.407 566.333C745.543 566.605 744.309 566.901 742.704 567.222C742.235 567.321 742 567.58 742 568C742 568.42 742.235 568.679 742.704 568.778C744.309 569.099 745.556 569.395 746.444 569.667C747.333 569.914 748 570.272 748.444 570.741C748.889 571.185 749.222 571.84 749.444 572.704C749.667 573.568 749.926 574.778 750.222 576.333Z" fill="#7E97FB"/>
|
||||
<path d="M735.222 320.333C735.321 320.778 735.58 321 736 321C736.42 321 736.667 320.778 736.741 320.333C737.037 318.778 737.296 317.568 737.519 316.704C737.765 315.815 738.099 315.148 738.519 314.704C738.963 314.235 739.63 313.877 740.519 313.63C741.432 313.383 742.691 313.099 744.296 312.778C744.765 312.679 745 312.42 745 312C745 311.58 744.765 311.321 744.296 311.222C742.691 310.901 741.444 310.605 740.556 310.333C739.691 310.062 739.037 309.704 738.593 309.259C738.173 308.815 737.84 308.16 737.593 307.296C737.37 306.407 737.086 305.198 736.741 303.667C736.667 303.222 736.42 303 736 303C735.58 303 735.321 303.222 735.222 303.667C734.901 305.198 734.617 306.407 734.37 307.296C734.148 308.16 733.815 308.815 733.37 309.259C732.951 309.704 732.296 310.062 731.407 310.333C730.543 310.605 729.309 310.901 727.704 311.222C727.235 311.321 727 311.58 727 312C727 312.42 727.235 312.679 727.704 312.778C729.309 313.099 730.556 313.395 731.444 313.667C732.333 313.914 733 314.272 733.444 314.741C733.889 315.185 734.222 315.84 734.444 316.704C734.667 317.568 734.926 318.778 735.222 320.333Z" fill="#3E5DE7"/>
|
||||
<path d="M735.222 320.333C735.321 320.778 735.58 321 736 321C736.42 321 736.667 320.778 736.741 320.333C737.037 318.778 737.296 317.568 737.519 316.704C737.765 315.815 738.099 315.148 738.519 314.704C738.963 314.235 739.63 313.877 740.519 313.63C741.432 313.383 742.691 313.099 744.296 312.778C744.765 312.679 745 312.42 745 312C745 311.58 744.765 311.321 744.296 311.222C742.691 310.901 741.444 310.605 740.556 310.333C739.691 310.062 739.037 309.704 738.593 309.259C738.173 308.815 737.84 308.16 737.593 307.296C737.37 306.407 737.086 305.198 736.741 303.667C736.667 303.222 736.42 303 736 303C735.58 303 735.321 303.222 735.222 303.667C734.901 305.198 734.617 306.407 734.37 307.296C734.148 308.16 733.815 308.815 733.37 309.259C732.951 309.704 732.296 310.062 731.407 310.333C730.543 310.605 729.309 310.901 727.704 311.222C727.235 311.321 727 311.58 727 312C727 312.42 727.235 312.679 727.704 312.778C729.309 313.099 730.556 313.395 731.444 313.667C732.333 313.914 733 314.272 733.444 314.741C733.889 315.185 734.222 315.84 734.444 316.704C734.667 317.568 734.926 318.778 735.222 320.333Z" fill="#181B24" fill-opacity="0.35"/>
|
||||
<path d="M735.222 320.333C735.321 320.778 735.58 321 736 321C736.42 321 736.667 320.778 736.741 320.333C737.037 318.778 737.296 317.568 737.519 316.704C737.765 315.815 738.099 315.148 738.519 314.704C738.963 314.235 739.63 313.877 740.519 313.63C741.432 313.383 742.691 313.099 744.296 312.778C744.765 312.679 745 312.42 745 312C745 311.58 744.765 311.321 744.296 311.222C742.691 310.901 741.444 310.605 740.556 310.333C739.691 310.062 739.037 309.704 738.593 309.259C738.173 308.815 737.84 308.16 737.593 307.296C737.37 306.407 737.086 305.198 736.741 303.667C736.667 303.222 736.42 303 736 303C735.58 303 735.321 303.222 735.222 303.667C734.901 305.198 734.617 306.407 734.37 307.296C734.148 308.16 733.815 308.815 733.37 309.259C732.951 309.704 732.296 310.062 731.407 310.333C730.543 310.605 729.309 310.901 727.704 311.222C727.235 311.321 727 311.58 727 312C727 312.42 727.235 312.679 727.704 312.778C729.309 313.099 730.556 313.395 731.444 313.667C732.333 313.914 733 314.272 733.444 314.741C733.889 315.185 734.222 315.84 734.444 316.704C734.667 317.568 734.926 318.778 735.222 320.333Z" fill="#7E97FB"/>
|
||||
<path d="M770.531 557.741C770.717 558.58 771.207 559 772 559C772.793 559 773.259 558.58 773.399 557.741C773.959 554.802 774.449 552.517 774.868 550.885C775.335 549.206 775.964 547.947 776.757 547.107C777.597 546.221 778.856 545.545 780.535 545.078C782.261 544.612 784.639 544.075 787.671 543.469C788.557 543.283 789 542.793 789 542C789 541.207 788.557 540.717 787.671 540.531C784.639 539.925 782.284 539.365 780.605 538.852C778.973 538.339 777.737 537.663 776.897 536.823C776.104 535.984 775.475 534.748 775.008 533.115C774.588 531.436 774.052 529.151 773.399 526.259C773.259 525.42 772.793 525 772 525C771.207 525 770.717 525.42 770.531 526.259C769.925 529.151 769.388 531.436 768.922 533.115C768.502 534.748 767.872 535.984 767.033 536.823C766.24 537.663 765.004 538.339 763.325 538.852C761.693 539.365 759.361 539.925 756.329 540.531C755.443 540.717 755 541.207 755 542C755 542.793 755.443 543.283 756.329 543.469C759.361 544.075 761.716 544.635 763.395 545.148C765.074 545.615 766.333 546.291 767.173 547.177C768.012 548.016 768.642 549.252 769.062 550.885C769.481 552.517 769.971 554.802 770.531 557.741Z" fill="#3E5DE7"/>
|
||||
<path d="M770.531 557.741C770.717 558.58 771.207 559 772 559C772.793 559 773.259 558.58 773.399 557.741C773.959 554.802 774.449 552.517 774.868 550.885C775.335 549.206 775.964 547.947 776.757 547.107C777.597 546.221 778.856 545.545 780.535 545.078C782.261 544.612 784.639 544.075 787.671 543.469C788.557 543.283 789 542.793 789 542C789 541.207 788.557 540.717 787.671 540.531C784.639 539.925 782.284 539.365 780.605 538.852C778.973 538.339 777.737 537.663 776.897 536.823C776.104 535.984 775.475 534.748 775.008 533.115C774.588 531.436 774.052 529.151 773.399 526.259C773.259 525.42 772.793 525 772 525C771.207 525 770.717 525.42 770.531 526.259C769.925 529.151 769.388 531.436 768.922 533.115C768.502 534.748 767.872 535.984 767.033 536.823C766.24 537.663 765.004 538.339 763.325 538.852C761.693 539.365 759.361 539.925 756.329 540.531C755.443 540.717 755 541.207 755 542C755 542.793 755.443 543.283 756.329 543.469C759.361 544.075 761.716 544.635 763.395 545.148C765.074 545.615 766.333 546.291 767.173 547.177C768.012 548.016 768.642 549.252 769.062 550.885C769.481 552.517 769.971 554.802 770.531 557.741Z" fill="#181B24" fill-opacity="0.35"/>
|
||||
<path d="M770.531 557.741C770.717 558.58 771.207 559 772 559C772.793 559 773.259 558.58 773.399 557.741C773.959 554.802 774.449 552.517 774.868 550.885C775.335 549.206 775.964 547.947 776.757 547.107C777.597 546.221 778.856 545.545 780.535 545.078C782.261 544.612 784.639 544.075 787.671 543.469C788.557 543.283 789 542.793 789 542C789 541.207 788.557 540.717 787.671 540.531C784.639 539.925 782.284 539.365 780.605 538.852C778.973 538.339 777.737 537.663 776.897 536.823C776.104 535.984 775.475 534.748 775.008 533.115C774.588 531.436 774.052 529.151 773.399 526.259C773.259 525.42 772.793 525 772 525C771.207 525 770.717 525.42 770.531 526.259C769.925 529.151 769.388 531.436 768.922 533.115C768.502 534.748 767.872 535.984 767.033 536.823C766.24 537.663 765.004 538.339 763.325 538.852C761.693 539.365 759.361 539.925 756.329 540.531C755.443 540.717 755 541.207 755 542C755 542.793 755.443 543.283 756.329 543.469C759.361 544.075 761.716 544.635 763.395 545.148C765.074 545.615 766.333 546.291 767.173 547.177C768.012 548.016 768.642 549.252 769.062 550.885C769.481 552.517 769.971 554.802 770.531 557.741Z" fill="#7E97FB"/>
|
||||
<path d="M708.531 315.741C708.717 316.58 709.207 317 710 317C710.793 317 711.259 316.58 711.399 315.741C711.959 312.802 712.449 310.517 712.868 308.885C713.335 307.206 713.964 305.947 714.757 305.107C715.597 304.221 716.856 303.545 718.535 303.078C720.261 302.612 722.639 302.075 725.671 301.469C726.557 301.283 727 300.793 727 300C727 299.207 726.557 298.717 725.671 298.531C722.639 297.925 720.284 297.365 718.605 296.852C716.973 296.339 715.737 295.663 714.897 294.823C714.104 293.984 713.475 292.748 713.008 291.115C712.588 289.436 712.052 287.151 711.399 284.259C711.259 283.42 710.793 283 710 283C709.207 283 708.717 283.42 708.531 284.259C707.925 287.151 707.388 289.436 706.922 291.115C706.502 292.748 705.872 293.984 705.033 294.823C704.24 295.663 703.004 296.339 701.325 296.852C699.693 297.365 697.361 297.925 694.329 298.531C693.443 298.717 693 299.207 693 300C693 300.793 693.443 301.283 694.329 301.469C697.361 302.075 699.716 302.635 701.395 303.148C703.074 303.615 704.333 304.291 705.173 305.177C706.012 306.016 706.642 307.252 707.062 308.885C707.481 310.517 707.971 312.802 708.531 315.741Z" fill="#3E5DE7"/>
|
||||
<path d="M708.531 315.741C708.717 316.58 709.207 317 710 317C710.793 317 711.259 316.58 711.399 315.741C711.959 312.802 712.449 310.517 712.868 308.885C713.335 307.206 713.964 305.947 714.757 305.107C715.597 304.221 716.856 303.545 718.535 303.078C720.261 302.612 722.639 302.075 725.671 301.469C726.557 301.283 727 300.793 727 300C727 299.207 726.557 298.717 725.671 298.531C722.639 297.925 720.284 297.365 718.605 296.852C716.973 296.339 715.737 295.663 714.897 294.823C714.104 293.984 713.475 292.748 713.008 291.115C712.588 289.436 712.052 287.151 711.399 284.259C711.259 283.42 710.793 283 710 283C709.207 283 708.717 283.42 708.531 284.259C707.925 287.151 707.388 289.436 706.922 291.115C706.502 292.748 705.872 293.984 705.033 294.823C704.24 295.663 703.004 296.339 701.325 296.852C699.693 297.365 697.361 297.925 694.329 298.531C693.443 298.717 693 299.207 693 300C693 300.793 693.443 301.283 694.329 301.469C697.361 302.075 699.716 302.635 701.395 303.148C703.074 303.615 704.333 304.291 705.173 305.177C706.012 306.016 706.642 307.252 707.062 308.885C707.481 310.517 707.971 312.802 708.531 315.741Z" fill="#181B24" fill-opacity="0.35"/>
|
||||
<path d="M708.531 315.741C708.717 316.58 709.207 317 710 317C710.793 317 711.259 316.58 711.399 315.741C711.959 312.802 712.449 310.517 712.868 308.885C713.335 307.206 713.964 305.947 714.757 305.107C715.597 304.221 716.856 303.545 718.535 303.078C720.261 302.612 722.639 302.075 725.671 301.469C726.557 301.283 727 300.793 727 300C727 299.207 726.557 298.717 725.671 298.531C722.639 297.925 720.284 297.365 718.605 296.852C716.973 296.339 715.737 295.663 714.897 294.823C714.104 293.984 713.475 292.748 713.008 291.115C712.588 289.436 712.052 287.151 711.399 284.259C711.259 283.42 710.793 283 710 283C709.207 283 708.717 283.42 708.531 284.259C707.925 287.151 707.388 289.436 706.922 291.115C706.502 292.748 705.872 293.984 705.033 294.823C704.24 295.663 703.004 296.339 701.325 296.852C699.693 297.365 697.361 297.925 694.329 298.531C693.443 298.717 693 299.207 693 300C693 300.793 693.443 301.283 694.329 301.469C697.361 302.075 699.716 302.635 701.395 303.148C703.074 303.615 704.333 304.291 705.173 305.177C706.012 306.016 706.642 307.252 707.062 308.885C707.481 310.517 707.971 312.802 708.531 315.741Z" fill="#7E97FB"/>
|
||||
<circle cx="207" cy="372" r="39" fill="#626A80"/>
|
||||
<circle cx="207" cy="372" r="39" fill="#181B24" fill-opacity="0.35"/>
|
||||
<path d="M217.696 352.771C217.696 351.453 217.696 350.794 217.559 350.193C217.306 349.081 216.714 348.075 215.864 347.314C215.404 346.903 214.828 346.583 213.676 345.944L210.731 344.311C209.642 343.707 209.098 343.405 208.536 343.235C207.498 342.922 206.391 342.922 205.353 343.235C204.792 343.405 204.247 343.707 203.158 344.311L201.944 344.985C200.47 345.802 199.733 346.211 199.395 346.687C198.764 347.577 198.764 348.769 199.397 349.658C199.735 350.134 200.472 350.542 201.947 351.358L202.93 351.902C204.084 352.541 204.661 352.86 205.122 353.271C205.973 354.032 206.567 355.038 206.82 356.151C206.957 356.753 206.957 357.413 206.957 358.732V359.858C206.957 361.456 206.957 362.255 207.197 362.771C207.646 363.734 208.642 364.32 209.702 364.246C210.269 364.206 210.968 363.819 212.366 363.044L213.674 362.319C214.827 361.68 215.403 361.36 215.863 360.949C216.713 360.188 217.306 359.182 217.559 358.07C217.696 357.468 217.696 356.809 217.696 355.491V352.771Z" fill="#3E5DE7"/>
|
||||
<path d="M217.696 352.771C217.696 351.453 217.696 350.794 217.559 350.193C217.306 349.081 216.714 348.075 215.864 347.314C215.404 346.903 214.828 346.583 213.676 345.944L210.731 344.311C209.642 343.707 209.098 343.405 208.536 343.235C207.498 342.922 206.391 342.922 205.353 343.235C204.792 343.405 204.247 343.707 203.158 344.311L201.944 344.985C200.47 345.802 199.733 346.211 199.395 346.687C198.764 347.577 198.764 348.769 199.397 349.658C199.735 350.134 200.472 350.542 201.947 351.358L202.93 351.902C204.084 352.541 204.661 352.86 205.122 353.271C205.973 354.032 206.567 355.038 206.82 356.151C206.957 356.753 206.957 357.413 206.957 358.732V359.858C206.957 361.456 206.957 362.255 207.197 362.771C207.646 363.734 208.642 364.32 209.702 364.246C210.269 364.206 210.968 363.819 212.366 363.044L213.674 362.319C214.827 361.68 215.403 361.36 215.863 360.949C216.713 360.188 217.306 359.182 217.559 358.07C217.696 357.468 217.696 356.809 217.696 355.491V352.771Z" fill="#7E98FE"/>
|
||||
<path d="M195.667 353.075C194.526 352.416 193.956 352.086 193.366 351.904C192.277 351.567 191.109 351.577 190.026 351.933C189.44 352.125 188.875 352.464 187.745 353.143L184.858 354.877C183.791 355.517 183.258 355.838 182.83 356.239C182.039 356.981 181.485 357.94 181.238 358.996C181.104 359.567 181.094 360.19 181.072 361.434L181.048 362.823C181.019 364.508 181.005 365.351 181.248 365.881C181.703 366.873 182.736 367.469 183.822 367.366C184.403 367.311 185.125 366.876 186.569 366.007L187.532 365.428C188.662 364.747 189.227 364.407 189.814 364.214C190.898 363.857 192.066 363.847 193.157 364.184C193.747 364.366 194.318 364.696 195.46 365.356L196.435 365.918C197.82 366.718 198.512 367.117 199.078 367.167C200.136 367.26 201.143 366.69 201.608 365.735C201.857 365.224 201.871 364.425 201.899 362.827L201.925 361.332C201.948 360.014 201.96 359.355 201.833 358.751C201.6 357.634 201.025 356.618 200.188 355.843C199.735 355.423 199.165 355.094 198.023 354.435L195.667 353.075Z" fill="#3E5DE7"/>
|
||||
<path d="M195.667 353.075C194.526 352.416 193.956 352.086 193.366 351.904C192.277 351.567 191.109 351.577 190.026 351.933C189.44 352.125 188.875 352.464 187.745 353.143L184.858 354.877C183.791 355.517 183.258 355.838 182.83 356.239C182.039 356.981 181.485 357.94 181.238 358.996C181.104 359.567 181.094 360.19 181.072 361.434L181.048 362.823C181.019 364.508 181.005 365.351 181.248 365.881C181.703 366.873 182.736 367.469 183.822 367.366C184.403 367.311 185.125 366.876 186.569 366.007L187.532 365.428C188.662 364.747 189.227 364.407 189.814 364.214C190.898 363.857 192.066 363.847 193.157 364.184C193.747 364.366 194.318 364.696 195.46 365.356L196.435 365.918C197.82 366.718 198.512 367.117 199.078 367.167C200.136 367.26 201.143 366.69 201.608 365.735C201.857 365.224 201.871 364.425 201.899 362.827L201.925 361.332C201.948 360.014 201.96 359.355 201.833 358.751C201.6 357.634 201.025 356.618 200.188 355.843C199.735 355.423 199.165 355.094 198.023 354.435L195.667 353.075Z" fill="#7E98FE"/>
|
||||
<path d="M184.916 372.304C183.775 372.963 183.204 373.292 182.752 373.711C181.915 374.486 181.34 375.502 181.107 376.618C180.98 377.222 180.992 377.881 181.014 379.199L181.072 382.566C181.094 383.81 181.104 384.433 181.238 385.004C181.485 386.06 182.039 387.019 182.83 387.761C183.258 388.162 183.791 388.483 184.858 389.123L186.049 389.838C187.494 390.706 188.217 391.14 188.797 391.195C189.884 391.297 190.916 390.7 191.37 389.708C191.613 389.177 191.597 388.334 191.567 386.649L191.546 385.526C191.522 384.207 191.51 383.547 191.636 382.943C191.869 381.825 192.444 380.808 193.282 380.033C193.735 379.613 194.306 379.283 195.448 378.623L196.423 378.061C197.807 377.261 198.5 376.862 198.826 376.396C199.435 375.526 199.445 374.37 198.851 373.489C198.533 373.018 197.848 372.606 196.478 371.783L195.196 371.013C194.066 370.334 193.501 369.994 192.915 369.802C191.831 369.446 190.664 369.436 189.574 369.773C188.984 369.955 188.414 370.285 187.272 370.944L184.916 372.304Z" fill="#3E5DE7"/>
|
||||
<path d="M184.916 372.304C183.775 372.963 183.204 373.292 182.752 373.711C181.915 374.486 181.34 375.502 181.107 376.618C180.98 377.222 180.992 377.881 181.014 379.199L181.072 382.566C181.094 383.81 181.104 384.433 181.238 385.004C181.485 386.06 182.039 387.019 182.83 387.761C183.258 388.162 183.791 388.483 184.858 389.123L186.049 389.838C187.494 390.706 188.217 391.14 188.797 391.195C189.884 391.297 190.916 390.7 191.37 389.708C191.613 389.177 191.597 388.334 191.567 386.649L191.546 385.526C191.522 384.207 191.51 383.547 191.636 382.943C191.869 381.825 192.444 380.808 193.282 380.033C193.735 379.613 194.306 379.283 195.448 378.623L196.423 378.061C197.807 377.261 198.5 376.862 198.826 376.396C199.435 375.526 199.445 374.37 198.851 373.489C198.533 373.018 197.848 372.606 196.478 371.783L195.196 371.013C194.066 370.334 193.501 369.994 192.915 369.802C191.831 369.446 190.664 369.436 189.574 369.773C188.984 369.955 188.414 370.285 187.272 370.944L184.916 372.304Z" fill="#7E98FE"/>
|
||||
<path d="M196.194 391.229C196.194 392.547 196.194 393.206 196.33 393.807C196.583 394.919 197.175 395.925 198.025 396.686C198.485 397.097 199.061 397.417 200.214 398.056L203.158 399.689C204.247 400.293 204.792 400.595 205.353 400.765C206.391 401.078 207.498 401.078 208.536 400.765C209.098 400.595 209.642 400.293 210.731 399.689L211.945 399.015C213.419 398.198 214.156 397.789 214.494 397.313C215.126 396.423 215.125 395.231 214.493 394.342C214.155 393.866 213.417 393.458 211.942 392.642L210.959 392.098C209.805 391.459 209.228 391.14 208.767 390.729C207.916 389.968 207.323 388.962 207.07 387.849C206.933 387.247 206.933 386.587 206.933 385.268V384.142C206.933 382.544 206.933 381.745 206.692 381.229C206.244 380.266 205.248 379.68 204.188 379.754C203.621 379.794 202.922 380.181 201.523 380.956L200.215 381.681C199.062 382.32 198.486 382.64 198.026 383.051C197.176 383.812 196.583 384.818 196.33 385.93C196.194 386.532 196.194 387.191 196.194 388.509V391.229Z" fill="#3E5DE7"/>
|
||||
<path d="M196.194 391.229C196.194 392.547 196.194 393.206 196.33 393.807C196.583 394.919 197.175 395.925 198.025 396.686C198.485 397.097 199.061 397.417 200.214 398.056L203.158 399.689C204.247 400.293 204.792 400.595 205.353 400.765C206.391 401.078 207.498 401.078 208.536 400.765C209.098 400.595 209.642 400.293 210.731 399.689L211.945 399.015C213.419 398.198 214.156 397.789 214.494 397.313C215.126 396.423 215.125 395.231 214.493 394.342C214.155 393.866 213.417 393.458 211.942 392.642L210.959 392.098C209.805 391.459 209.228 391.14 208.767 390.729C207.916 389.968 207.323 388.962 207.07 387.849C206.933 387.247 206.933 386.587 206.933 385.268V384.142C206.933 382.544 206.933 381.745 206.692 381.229C206.244 380.266 205.248 379.68 204.188 379.754C203.621 379.794 202.922 380.181 201.523 380.956L200.215 381.681C199.062 382.32 198.486 382.64 198.026 383.051C197.176 383.812 196.583 384.818 196.33 385.93C196.194 386.532 196.194 387.191 196.194 388.509V391.229Z" fill="#7E98FE"/>
|
||||
<path d="M218.222 390.925C219.363 391.584 219.934 391.914 220.523 392.096C221.613 392.433 222.78 392.423 223.863 392.067C224.449 391.875 225.014 391.536 226.144 390.857L229.031 389.123C230.098 388.483 230.632 388.162 231.06 387.761C231.851 387.019 232.404 386.06 232.651 385.004C232.785 384.433 232.796 383.81 232.817 382.566L232.841 381.177C232.87 379.492 232.885 378.649 232.641 378.119C232.186 377.127 231.154 376.531 230.067 376.634C229.486 376.689 228.764 377.124 227.32 377.993L226.357 378.572C225.227 379.253 224.662 379.593 224.076 379.786C222.991 380.143 221.823 380.153 220.733 379.816C220.143 379.634 219.571 379.304 218.429 378.644L217.454 378.082C216.07 377.282 215.378 376.883 214.811 376.833C213.753 376.74 212.747 377.31 212.281 378.265C212.032 378.776 212.018 379.575 211.99 381.173L211.964 382.668C211.941 383.986 211.93 384.645 212.056 385.249C212.289 386.366 212.864 387.382 213.701 388.157C214.154 388.577 214.725 388.906 215.866 389.565L218.222 390.925Z" fill="#3E5DE7"/>
|
||||
<path d="M218.222 390.925C219.363 391.584 219.934 391.914 220.523 392.096C221.613 392.433 222.78 392.423 223.863 392.067C224.449 391.875 225.014 391.536 226.144 390.857L229.031 389.123C230.098 388.483 230.632 388.162 231.06 387.761C231.851 387.019 232.404 386.06 232.651 385.004C232.785 384.433 232.796 383.81 232.817 382.566L232.841 381.177C232.87 379.492 232.885 378.649 232.641 378.119C232.186 377.127 231.154 376.531 230.067 376.634C229.486 376.689 228.764 377.124 227.32 377.993L226.357 378.572C225.227 379.253 224.662 379.593 224.076 379.786C222.991 380.143 221.823 380.153 220.733 379.816C220.143 379.634 219.571 379.304 218.429 378.644L217.454 378.082C216.07 377.282 215.378 376.883 214.811 376.833C213.753 376.74 212.747 377.31 212.281 378.265C212.032 378.776 212.018 379.575 211.99 381.173L211.964 382.668C211.941 383.986 211.93 384.645 212.056 385.249C212.289 386.366 212.864 387.382 213.701 388.157C214.154 388.577 214.725 388.906 215.866 389.565L218.222 390.925Z" fill="#7E98FE"/>
|
||||
<path d="M228.973 371.696C230.114 371.037 230.685 370.708 231.138 370.289C231.974 369.514 232.549 368.498 232.783 367.382C232.909 366.778 232.898 366.119 232.875 364.801L232.817 361.434C232.796 360.19 232.785 359.567 232.651 358.996C232.404 357.94 231.851 356.981 231.06 356.239C230.632 355.838 230.098 355.517 229.031 354.877L227.84 354.162C226.395 353.294 225.673 352.86 225.092 352.805C224.005 352.703 222.973 353.3 222.519 354.292C222.277 354.823 222.292 355.666 222.323 357.351L222.343 358.474C222.367 359.793 222.379 360.453 222.253 361.057C222.02 362.175 221.445 363.192 220.608 363.967C220.155 364.387 219.584 364.717 218.441 365.377L217.466 365.939C216.082 366.739 215.39 367.138 215.064 367.604C214.454 368.474 214.444 369.63 215.038 370.511C215.356 370.982 216.041 371.394 217.411 372.217L218.694 372.987C219.823 373.666 220.388 374.006 220.975 374.198C222.058 374.554 223.226 374.564 224.315 374.227C224.905 374.045 225.476 373.715 226.617 373.056L228.973 371.696Z" fill="#3E5DE7"/>
|
||||
<path d="M228.973 371.696C230.114 371.037 230.685 370.708 231.138 370.289C231.974 369.514 232.549 368.498 232.783 367.382C232.909 366.778 232.898 366.119 232.875 364.801L232.817 361.434C232.796 360.19 232.785 359.567 232.651 358.996C232.404 357.94 231.851 356.981 231.06 356.239C230.632 355.838 230.098 355.517 229.031 354.877L227.84 354.162C226.395 353.294 225.673 352.86 225.092 352.805C224.005 352.703 222.973 353.3 222.519 354.292C222.277 354.823 222.292 355.666 222.323 357.351L222.343 358.474C222.367 359.793 222.379 360.453 222.253 361.057C222.02 362.175 221.445 363.192 220.608 363.967C220.155 364.387 219.584 364.717 218.441 365.377L217.466 365.939C216.082 366.739 215.39 367.138 215.064 367.604C214.454 368.474 214.444 369.63 215.038 370.511C215.356 370.982 216.041 371.394 217.411 372.217L218.694 372.987C219.823 373.666 220.388 374.006 220.975 374.198C222.058 374.554 223.226 374.564 224.315 374.227C224.905 374.045 225.476 373.715 226.617 373.056L228.973 371.696Z" fill="#7E98FE"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M731.897 795C731.897 795 737.88 801.2 742.681 810.718C747.704 801.422 753.738 795.406 753.779 795.365C753.779 795.365 760.296 807.07 759.999 821.997C768.576 813.66 776.987 808.764 777.021 808.744C777.021 808.744 779.423 825.414 769.34 842.419C765.112 849.549 759.356 855.603 754.102 860.233C755.915 867.377 755.79 872.316 755.79 872.316C755.79 872.316 751.715 870.303 746.134 866.585C742.773 869.003 740.518 870.316 740.504 870.324C740.498 870.282 740.069 867.247 740.352 862.468C734.753 858.203 728.618 852.662 723.768 846.058C711.72 829.654 712.154 812.909 712.154 812.904C712.154 812.904 717.827 815.703 724.949 820.801C725.282 806.26 731.872 795.041 731.897 795Z" fill="#626A80"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M731.897 795C731.897 795 737.88 801.2 742.681 810.718C747.704 801.422 753.738 795.406 753.779 795.365C753.779 795.365 760.296 807.07 759.999 821.997C768.576 813.66 776.987 808.764 777.021 808.744C777.021 808.744 779.423 825.414 769.34 842.419C765.112 849.549 759.356 855.603 754.102 860.233C755.915 867.377 755.79 872.316 755.79 872.316C755.79 872.316 751.715 870.303 746.134 866.585C742.773 869.003 740.518 870.316 740.504 870.324C740.498 870.282 740.069 867.247 740.352 862.468C734.753 858.203 728.618 852.662 723.768 846.058C711.72 829.654 712.154 812.909 712.154 812.904C712.154 812.904 717.827 815.703 724.949 820.801C725.282 806.26 731.872 795.041 731.897 795Z" fill="#181B24" fill-opacity="0.05"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M839.424 896.683C839.424 932.719 797.02 961.933 744.712 961.933C692.404 961.933 650 932.719 650 896.683C650 860.645 692.404 831.432 744.712 831.432C797.02 831.432 839.424 860.645 839.424 896.683Z" fill="#626A80"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M839.424 896.683C839.424 932.719 797.02 961.933 744.712 961.933C692.404 961.933 650 932.719 650 896.683C650 860.645 692.404 831.432 744.712 831.432C797.02 831.432 839.424 860.645 839.424 896.683Z" fill="#181B24" fill-opacity="0.35"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M803.377 1013H695.649C679.116 1013 665.713 999.597 665.713 983.063V977.903C665.713 961.371 679.116 947.967 695.649 947.967H803.377C819.911 947.967 833.314 961.371 833.314 977.903V983.063C833.314 999.597 819.911 1013 803.377 1013Z" fill="#626A80"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M803.377 1013H695.649C679.116 1013 665.713 999.597 665.713 983.063V977.903C665.713 961.371 679.116 947.967 695.649 947.967H803.377C819.911 947.967 833.314 961.371 833.314 977.903V983.063C833.314 999.597 819.911 1013 803.377 1013Z" fill="#181B24" fill-opacity="0.05"/>
|
||||
<path d="M690.591 850.636L673.133 837.105" stroke="#626A80" stroke-width="5.87681"/>
|
||||
<path d="M690.591 850.636L673.133 837.105" stroke="#181B24" stroke-opacity="0.05" stroke-width="5.87681"/>
|
||||
<path d="M665.713 920.645L650 931.406" stroke="#626A80" stroke-width="5.87681"/>
|
||||
<path d="M665.713 920.645L650 931.406" stroke="#181B24" stroke-opacity="0.05" stroke-width="5.87681"/>
|
||||
<path d="M711.379 914.775L738.494 919.175" stroke="#626A80" stroke-width="5.87681"/>
|
||||
<path d="M711.379 914.775L738.494 919.175" stroke="#181B24" stroke-opacity="0.05" stroke-width="5.87681"/>
|
||||
<path d="M780.715 848.435L797.086 828.318" stroke="#626A80" stroke-width="5.87681"/>
|
||||
<path d="M780.715 848.435L797.086 828.318" stroke="#181B24" stroke-opacity="0.05" stroke-width="5.87681"/>
|
||||
<path d="M810.617 870.325L828.512 858.93" stroke="#626A80" stroke-width="5.87681"/>
|
||||
<path d="M810.617 870.325L828.512 858.93" stroke="#181B24" stroke-opacity="0.05" stroke-width="5.87681"/>
|
||||
<path d="M814.109 929.199L831.131 940.111" stroke="#626A80" stroke-width="5.87681"/>
|
||||
<path d="M814.109 929.199L831.131 940.111" stroke="#181B24" stroke-opacity="0.05" stroke-width="5.87681"/>
|
||||
<path d="M719.248 874.832L711.148 856.105" stroke="#626A80" stroke-width="5.87681"/>
|
||||
<path d="M719.248 874.832L711.148 856.105" stroke="#181B24" stroke-opacity="0.7" stroke-width="5.87681"/>
|
||||
<path d="M796.736 895.254L777.271 904.469" stroke="#626A80" stroke-width="5.87681"/>
|
||||
<path d="M796.736 895.254L777.271 904.469" stroke="#181B24" stroke-opacity="0.7" stroke-width="5.87681"/>
|
||||
<path d="M702.58 884.543L684.049 872.283" stroke="#626A80" stroke-width="5.87681"/>
|
||||
<path d="M702.58 884.543L684.049 872.283" stroke="#181B24" stroke-opacity="0.7" stroke-width="5.87681"/>
|
||||
<path d="M665.713 973.949L699.231 987.017L732.75 973.949L766.268 987.017L799.791 973.949L833.314 987.017" stroke="#626A80" stroke-width="5.87681"/>
|
||||
<path d="M665.713 973.949L699.231 987.017L732.75 973.949L766.268 987.017L799.791 973.949L833.314 987.017" stroke="#181B24" stroke-opacity="0.35" stroke-width="5.87681"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M556 1017C556 1019.76 473.844 1022 372.5 1022C271.156 1022 189 1019.76 189 1017C189 1014.24 271.156 1012 372.5 1012C473.844 1012 556 1014.24 556 1017Z" fill="black" fill-opacity="0.12"/>
|
||||
<g filter="url(#filter1_d_386_10828)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M339.459 605.176L355.347 599.147L342.88 514.127L367.441 466.182L334.44 446.037C334.44 446.037 316.551 492.291 315.654 507.546C314.093 534.272 339.459 605.176 339.459 605.176Z" fill="#EAC7B2"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M324.432 455.507L368.658 471.968L383.416 419.723L348.695 408.057L324.432 455.507Z" fill="#DB5962"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M324.432 455.507L368.658 471.968L383.416 419.723L348.695 408.057L324.432 455.507Z" fill="black" fill-opacity="0.2"/>
|
||||
<path d="M573.656 412.516L568.6 458.984" stroke="#3E5DE7" stroke-width="10.2887" stroke-linecap="round"/>
|
||||
<path d="M573.656 412.516L568.6 458.984" stroke="white" stroke-opacity="0.2" stroke-width="10.2887" stroke-linecap="round"/>
|
||||
<path d="M574.928 402.121L573.292 415.825" stroke="#3E5DE7" stroke-width="6.43045" stroke-linecap="round"/>
|
||||
<path d="M574.928 402.121L573.292 415.825" stroke="white" stroke-opacity="0.2" stroke-width="6.43045" stroke-linecap="round"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M576.245 399.962C563.726 399.266 554.269 386.258 555.122 370.907C555.975 355.556 566.814 343.676 579.333 344.371C591.851 345.067 601.308 358.075 600.456 373.426C599.603 388.777 588.763 400.658 576.245 399.962Z" stroke="#3E5DE7" stroke-width="10.2887" stroke-linecap="round"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M576.245 399.962C563.726 399.266 554.269 386.258 555.122 370.907C555.975 355.556 566.814 343.676 579.333 344.371C591.851 345.067 601.308 358.075 600.456 373.426C599.603 388.777 588.763 400.658 576.245 399.962Z" stroke="white" stroke-opacity="0.2" stroke-width="10.2887" stroke-linecap="round"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M392.844 1016.17H296.447L299.896 959.379L327.663 968.727L324.598 988.862C324.598 988.862 370.245 999.743 392.844 1016.17Z" fill="#404553"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M458.361 1016.17H367.47L370.918 959.379L398.695 968.727L395.62 988.862C395.62 988.862 435.762 999.743 458.361 1016.17Z" fill="#404553"/>
|
||||
<path d="M327.196 972.128L324.598 988.852L298.148 988.132L299.27 963.762L327.196 972.128ZM398.228 972.128L395.62 988.852L369.17 988.132L370.292 963.762L398.228 972.128Z" fill="#EAC7B2"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M440.744 472.1C440.744 472.1 485.55 520.429 506.663 514.166C534.29 506.005 571.609 444.767 571.609 444.767L553.59 435.961L502.877 479.691L456.146 436.942L440.744 472.1Z" fill="#EAC7B2"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M491.447 457.115L469.016 506.556L441.912 480.942C441.912 480.942 440.772 488.112 434.277 499.732C428.012 511.241 422.767 523.277 418.603 535.703L330.207 528.42C330.207 528.42 336.917 501.312 338.067 481.681C339.366 459.601 336.132 402.551 361.208 393.988C382.891 386.585 388.807 394.175 418.986 400.064C447.408 405.626 491.447 457.115 491.447 457.115Z" fill="#DB5962"/>
|
||||
<path d="M358.069 530.695L358.056 530.365L418.629 535.703C418.662 535.769 426.591 551.657 430.321 595.286C437.09 675.265 432.34 751.168 432.33 751.331L406.924 966.51L412.611 966.66L412.179 983.088L354.598 981.57L355.03 965.143L361.694 965.317L367.86 752.846L365.982 710.251L361.021 763.97L336.859 964.801L342.014 964.938L341.581 981.365L284 979.848L284.433 963.42L291.931 963.617L300.252 758.67C300.252 758.67 301.692 648.28 305.047 603.167C309.068 549.112 330.14 528.523 330.216 528.449L358.069 530.695Z" fill="#4964A5"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M420.089 399.298C420.089 399.298 446.557 396.868 452.146 389.408C467.867 368.394 447.221 312.045 447.221 312.045L386.2 348.698C386.2 348.698 376.405 399.429 376.573 399.476L415.696 410.516L420.089 399.298Z" fill="#EAC7B2"/>
|
||||
<path d="M372.611 239C389.893 239 403.902 253.012 403.902 270.297C403.902 274.37 403.123 278.26 401.708 281.829C403.564 281.674 405.446 281.602 407.349 281.617C421.324 281.646 434.834 286.647 445.462 295.724C456.574 305.333 463.454 319.046 463.305 334.218C463.288 336.206 463.147 338.192 462.884 340.163C460.583 357.481 449.08 372.142 433.075 379.913L428.06 452.404L369.123 448.403C354.412 447.403 343.356 433.886 344.449 418.145L350.445 331.416C350.479 330.705 350.529 329.999 350.593 329.296L350.646 328.535L350.667 328.536C351.816 317.819 356.48 308.067 363.57 300.265C350.697 296.385 341.32 284.437 341.32 270.297C341.32 253.012 355.33 239 372.611 239Z" fill="#DA8748"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M354.302 599.773L360.47 768.569L364.592 714.79L354.302 599.773Z" fill="black" fill-opacity="0.2"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M437.341 365.701C437.341 365.701 442.192 355.362 434.051 352.978C427.986 351.202 424.5 356.045 423.443 359.129C419.873 369.524 431.584 374.694 433.266 383.481" fill="#EAC7B2"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M550.179 439.279L552.899 433.885C552.899 433.885 557.572 424.294 560.376 420.639C564.909 414.694 570.657 418.994 570.657 418.994C570.657 418.994 581.62 413.021 584.349 414.264C591.882 417.704 590.564 445.505 583.414 448.132C577.881 450.16 574.451 452.74 567.311 454.572C564.283 455.348 560.367 453.871 558.095 454.011" fill="#EAC7B2"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M471.896 500.228L487.551 465.688L478.99 505.239L471.896 500.228Z" fill="black" fill-opacity="0.2"/>
|
||||
</g>
|
||||
<rect x="791.287" y="235.916" width="84.6173" height="112.823" rx="12.5359" transform="rotate(-9.63619 791.287 235.916)" fill="#7E98FF"/>
|
||||
<rect x="791.287" y="235.916" width="84.6173" height="112.823" rx="12.5359" transform="rotate(-9.63619 791.287 235.916)" fill="#181B24" fill-opacity="0.6"/>
|
||||
<g clip-path="url(#clip0_386_10828)">
|
||||
<rect x="788.178" y="232.646" width="84.6173" height="112.823" rx="12.5359" transform="rotate(-9.63619 788.178 232.646)" fill="#2B303D"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M804.921 255.773C804.631 254.067 805.78 252.449 807.486 252.159L835.294 247.438C837 247.148 838.619 248.296 838.908 250.003C839.198 251.709 838.05 253.327 836.343 253.617L808.535 258.338C806.829 258.628 805.211 257.48 804.921 255.773Z" fill="#7E98FF"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M806.695 266.223C806.405 264.516 807.553 262.898 809.26 262.608L861.589 253.723C863.296 253.434 864.914 254.582 865.204 256.288C865.494 257.995 864.345 259.613 862.639 259.903L810.309 268.788C808.602 269.077 806.984 267.929 806.695 266.223ZM808.268 275.492C807.979 273.785 809.127 272.167 810.833 271.877L863.163 262.993C864.87 262.703 866.488 263.851 866.778 265.558C867.067 267.264 865.919 268.882 864.212 269.172L811.883 278.057C810.176 278.347 808.558 277.198 808.268 275.492ZM809.842 284.761C809.552 283.055 810.701 281.436 812.407 281.147L864.737 272.262C866.443 271.972 868.062 273.121 868.351 274.827C868.641 276.533 867.493 278.152 865.786 278.441L813.456 287.326C811.75 287.616 810.132 286.468 809.842 284.761ZM811.416 294.03C811.126 292.324 812.275 290.706 813.981 290.416L866.311 281.531C868.017 281.241 869.635 282.39 869.925 284.096C870.215 285.803 869.066 287.421 867.36 287.711L815.03 296.596C813.324 296.885 811.706 295.737 811.416 294.03Z" fill="#7E98FF"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M806.695 266.223C806.405 264.516 807.553 262.898 809.26 262.608L861.589 253.723C863.296 253.434 864.914 254.582 865.204 256.288C865.494 257.995 864.345 259.613 862.639 259.903L810.309 268.788C808.602 269.077 806.984 267.929 806.695 266.223ZM808.268 275.492C807.979 273.785 809.127 272.167 810.833 271.877L863.163 262.993C864.87 262.703 866.488 263.851 866.778 265.558C867.067 267.264 865.919 268.882 864.212 269.172L811.883 278.057C810.176 278.347 808.558 277.198 808.268 275.492ZM809.842 284.761C809.552 283.055 810.701 281.436 812.407 281.147L864.737 272.262C866.443 271.972 868.062 273.121 868.351 274.827C868.641 276.533 867.493 278.152 865.786 278.441L813.456 287.326C811.75 287.616 810.132 286.468 809.842 284.761ZM811.416 294.03C811.126 292.324 812.275 290.706 813.981 290.416L866.311 281.531C868.017 281.241 869.635 282.39 869.925 284.096C870.215 285.803 869.066 287.421 867.36 287.711L815.03 296.596C813.324 296.885 811.706 295.737 811.416 294.03Z" fill="#181B24" fill-opacity="0.45"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M813.008 303.409C812.719 301.702 813.867 300.084 815.573 299.795L867.903 290.91C869.61 290.62 871.228 291.768 871.518 293.475C871.807 295.181 870.659 296.799 868.952 297.089L816.623 305.974C814.916 306.264 813.298 305.115 813.008 303.409ZM814.582 312.678C814.292 310.972 815.441 309.354 817.147 309.064L869.477 300.179C871.183 299.889 872.802 301.038 873.091 302.744C873.381 304.45 872.233 306.069 870.526 306.358L818.196 315.243C816.49 315.533 814.872 314.385 814.582 312.678ZM816.156 321.947C815.866 320.241 817.015 318.623 818.721 318.333L871.051 309.448C872.757 309.158 874.375 310.307 874.665 312.013C874.955 313.72 873.806 315.338 872.1 315.628L819.77 324.513C818.064 324.802 816.446 323.654 816.156 321.947ZM817.73 331.217C817.44 329.51 818.588 327.892 820.295 327.602L872.625 318.717C874.331 318.428 875.949 319.576 876.239 321.283C876.529 322.989 875.38 324.607 873.674 324.897L821.344 333.782C819.638 334.072 818.019 332.923 817.73 331.217Z" fill="#7E98FF"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M813.008 303.409C812.719 301.702 813.867 300.084 815.573 299.795L867.903 290.91C869.61 290.62 871.228 291.768 871.518 293.475C871.807 295.181 870.659 296.799 868.952 297.089L816.623 305.974C814.916 306.264 813.298 305.115 813.008 303.409ZM814.582 312.678C814.292 310.972 815.441 309.354 817.147 309.064L869.477 300.179C871.183 299.889 872.802 301.038 873.091 302.744C873.381 304.45 872.233 306.069 870.526 306.358L818.196 315.243C816.49 315.533 814.872 314.385 814.582 312.678ZM816.156 321.947C815.866 320.241 817.015 318.623 818.721 318.333L871.051 309.448C872.757 309.158 874.375 310.307 874.665 312.013C874.955 313.72 873.806 315.338 872.1 315.628L819.77 324.513C818.064 324.802 816.446 323.654 816.156 321.947ZM817.73 331.217C817.44 329.51 818.588 327.892 820.295 327.602L872.625 318.717C874.331 318.428 875.949 319.576 876.239 321.283C876.529 322.989 875.38 324.607 873.674 324.897L821.344 333.782C819.638 334.072 818.019 332.923 817.73 331.217Z" fill="#181B24" fill-opacity="0.45"/>
|
||||
</g>
|
||||
<rect x="789.601" y="233.657" width="82.1493" height="110.355" rx="11.3019" transform="rotate(-9.63619 789.601 233.657)" stroke="#7E98FF" stroke-width="2.46801"/>
|
||||
<rect x="789.601" y="233.657" width="82.1493" height="110.355" rx="11.3019" transform="rotate(-9.63619 789.601 233.657)" stroke="#181B24" stroke-opacity="0.45" stroke-width="2.46801"/>
|
||||
<circle cx="834.077" cy="226.439" r="12.563" transform="rotate(-9.63619 834.077 226.439)" fill="#687FE7"/>
|
||||
<circle cx="834.077" cy="226.439" r="12.563" transform="rotate(-9.63619 834.077 226.439)" fill="#181B24" fill-opacity="0.35"/>
|
||||
<circle cx="829.19" cy="224.955" r="12.563" transform="rotate(-9.63619 829.19 224.955)" fill="#687FE7"/>
|
||||
<rect x="484.484" y="657.703" width="84.2274" height="112.303" rx="12.4781" transform="rotate(-9.63619 484.484 657.703)" fill="#7E98FF"/>
|
||||
<rect x="484.484" y="657.703" width="84.2274" height="112.303" rx="12.4781" transform="rotate(-9.63619 484.484 657.703)" fill="#181B24" fill-opacity="0.6"/>
|
||||
<g clip-path="url(#clip1_386_10828)">
|
||||
<rect x="481.391" y="654.445" width="84.2274" height="112.303" rx="12.4781" transform="rotate(-9.63619 481.391 654.445)" fill="#2B303D"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M498.059 677.466C497.771 675.768 498.914 674.157 500.613 673.868L528.292 669.169C529.991 668.88 531.602 670.024 531.89 671.722C532.178 673.421 531.035 675.031 529.337 675.32L501.657 680.019C499.958 680.308 498.348 679.165 498.059 677.466Z" fill="#7E98FF"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M499.823 687.865C499.535 686.166 500.678 684.555 502.376 684.267L554.465 675.423C556.163 675.135 557.774 676.278 558.063 677.976C558.351 679.675 557.208 681.286 555.509 681.574L503.421 690.418C501.722 690.706 500.111 689.563 499.823 687.865ZM501.39 697.091C501.101 695.393 502.244 693.782 503.943 693.493L556.031 684.649C557.73 684.361 559.341 685.504 559.629 687.203C559.918 688.901 558.774 690.512 557.076 690.801L504.987 699.644C503.289 699.933 501.678 698.79 501.39 697.091ZM502.956 706.318C502.668 704.619 503.811 703.008 505.509 702.72L557.598 693.876C559.297 693.588 560.907 694.731 561.196 696.429C561.484 698.128 560.341 699.739 558.642 700.027L506.554 708.871C504.855 709.159 503.244 708.016 502.956 706.318ZM504.523 715.544C504.234 713.846 505.377 712.235 507.076 711.947L559.165 703.103C560.863 702.814 562.474 703.957 562.762 705.656C563.051 707.354 561.908 708.965 560.209 709.254L508.12 718.098C506.422 718.386 504.811 717.243 504.523 715.544Z" fill="#7E98FF"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M499.823 687.865C499.535 686.166 500.678 684.555 502.376 684.267L554.465 675.423C556.163 675.135 557.774 676.278 558.063 677.976C558.351 679.675 557.208 681.286 555.509 681.574L503.421 690.418C501.722 690.706 500.111 689.563 499.823 687.865ZM501.39 697.091C501.101 695.393 502.244 693.782 503.943 693.493L556.031 684.649C557.73 684.361 559.341 685.504 559.629 687.203C559.918 688.901 558.774 690.512 557.076 690.801L504.987 699.644C503.289 699.933 501.678 698.79 501.39 697.091ZM502.956 706.318C502.668 704.619 503.811 703.008 505.509 702.72L557.598 693.876C559.297 693.588 560.907 694.731 561.196 696.429C561.484 698.128 560.341 699.739 558.642 700.027L506.554 708.871C504.855 709.159 503.244 708.016 502.956 706.318ZM504.523 715.544C504.234 713.846 505.377 712.235 507.076 711.947L559.165 703.103C560.863 702.814 562.474 703.957 562.762 705.656C563.051 707.354 561.908 708.965 560.209 709.254L508.12 718.098C506.422 718.386 504.811 717.243 504.523 715.544Z" fill="#181B24" fill-opacity="0.45"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M506.108 724.88C505.819 723.181 506.962 721.57 508.661 721.282L560.75 712.438C562.448 712.149 564.059 713.293 564.347 714.991C564.636 716.69 563.493 718.301 561.794 718.589L509.705 727.433C508.007 727.721 506.396 726.578 506.108 724.88ZM507.674 734.106C507.386 732.408 508.529 730.797 510.228 730.508L562.316 721.664C564.015 721.376 565.625 722.519 565.914 724.218C566.202 725.916 565.059 727.527 563.361 727.815L511.272 736.659C509.573 736.948 507.963 735.805 507.674 734.106ZM509.241 743.333C508.952 741.634 510.096 740.023 511.794 739.735L563.883 730.891C565.581 730.603 567.192 731.746 567.48 733.444C567.769 735.143 566.626 736.754 564.927 737.042L512.838 745.886C511.14 746.174 509.529 745.031 509.241 743.333ZM510.807 752.559C510.519 750.861 511.662 749.25 513.361 748.962L565.449 740.118C567.148 739.829 568.759 740.972 569.047 742.671C569.335 744.369 568.192 745.98 566.494 746.269L514.405 755.113C512.706 755.401 511.096 754.258 510.807 752.559Z" fill="#7E98FF"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M506.108 724.88C505.819 723.181 506.962 721.57 508.661 721.282L560.75 712.438C562.448 712.149 564.059 713.293 564.347 714.991C564.636 716.69 563.493 718.301 561.794 718.589L509.705 727.433C508.007 727.721 506.396 726.578 506.108 724.88ZM507.674 734.106C507.386 732.408 508.529 730.797 510.228 730.508L562.316 721.664C564.015 721.376 565.625 722.519 565.914 724.218C566.202 725.916 565.059 727.527 563.361 727.815L511.272 736.659C509.573 736.948 507.963 735.805 507.674 734.106ZM509.241 743.333C508.952 741.634 510.096 740.023 511.794 739.735L563.883 730.891C565.581 730.603 567.192 731.746 567.48 733.444C567.769 735.143 566.626 736.754 564.927 737.042L512.838 745.886C511.14 746.174 509.529 745.031 509.241 743.333ZM510.807 752.559C510.519 750.861 511.662 749.25 513.361 748.962L565.449 740.118C567.148 739.829 568.759 740.972 569.047 742.671C569.335 744.369 568.192 745.98 566.494 746.269L514.405 755.113C512.706 755.401 511.096 754.258 510.807 752.559Z" fill="#181B24" fill-opacity="0.45"/>
|
||||
</g>
|
||||
<rect x="482.807" y="655.451" width="81.7708" height="109.847" rx="11.2498" transform="rotate(-9.63619 482.807 655.451)" stroke="#7E98FF" stroke-width="2.45663"/>
|
||||
<rect x="482.807" y="655.451" width="81.7708" height="109.847" rx="11.2498" transform="rotate(-9.63619 482.807 655.451)" stroke="#181B24" stroke-opacity="0.45" stroke-width="2.45663"/>
|
||||
<circle cx="527.076" cy="648.271" r="12.5051" transform="rotate(-9.63619 527.076 648.271)" fill="#687FE7"/>
|
||||
<circle cx="527.076" cy="648.271" r="12.5051" transform="rotate(-9.63619 527.076 648.271)" fill="#181B24" fill-opacity="0.35"/>
|
||||
<circle cx="522.211" cy="646.788" r="12.5051" transform="rotate(-9.63619 522.211 646.788)" fill="#687FE7"/>
|
||||
<rect x="307.186" y="84.9434" width="85.801" height="114.401" rx="12.7113" transform="rotate(11.9505 307.186 84.9434)" fill="#7E98FF"/>
|
||||
<rect x="307.186" y="84.9434" width="85.801" height="114.401" rx="12.7113" transform="rotate(11.9505 307.186 84.9434)" fill="#181B24" fill-opacity="0.6"/>
|
||||
<g clip-path="url(#clip2_386_10828)">
|
||||
<rect x="305.475" y="80.6992" width="85.801" height="114.401" rx="12.7113" transform="rotate(11.9505 305.475 80.6992)" fill="#2B303D"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M312.635 108.753C312.998 107.036 314.685 105.939 316.402 106.303L344.382 112.225C346.099 112.588 347.197 114.275 346.833 115.992C346.47 117.709 344.783 118.806 343.066 118.443L315.086 112.52C313.369 112.157 312.272 110.47 312.635 108.753Z" fill="#7E98FF"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M310.41 119.265C310.774 117.548 312.46 116.451 314.177 116.814L366.832 127.959C368.549 128.322 369.646 130.009 369.283 131.726C368.919 133.443 367.233 134.54 365.516 134.177L312.861 123.032C311.144 122.669 310.047 120.982 310.41 119.265ZM308.436 128.592C308.8 126.875 310.486 125.778 312.203 126.141L364.858 137.286C366.575 137.649 367.672 139.336 367.309 141.053C366.945 142.77 365.259 143.867 363.542 143.504L310.887 132.359C309.17 131.996 308.073 130.309 308.436 128.592ZM306.462 137.919C306.826 136.202 308.512 135.105 310.229 135.468L362.884 146.613C364.601 146.976 365.698 148.662 365.335 150.38C364.971 152.097 363.285 153.194 361.568 152.83L308.913 141.686C307.196 141.322 306.099 139.636 306.462 137.919ZM304.488 147.246C304.852 145.529 306.538 144.431 308.255 144.795L360.91 155.939C362.627 156.303 363.724 157.989 363.361 159.706C362.997 161.423 361.311 162.521 359.594 162.157L306.939 151.013C305.222 150.649 304.125 148.963 304.488 147.246Z" fill="#7E98FF"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M310.41 119.265C310.774 117.548 312.46 116.451 314.177 116.814L366.832 127.959C368.549 128.322 369.646 130.009 369.283 131.726C368.919 133.443 367.233 134.54 365.516 134.177L312.861 123.032C311.144 122.669 310.047 120.982 310.41 119.265ZM308.436 128.592C308.8 126.875 310.486 125.778 312.203 126.141L364.858 137.286C366.575 137.649 367.672 139.336 367.309 141.053C366.945 142.77 365.259 143.867 363.542 143.504L310.887 132.359C309.17 131.996 308.073 130.309 308.436 128.592ZM306.462 137.919C306.826 136.202 308.512 135.105 310.229 135.468L362.884 146.613C364.601 146.976 365.698 148.662 365.335 150.38C364.971 152.097 363.285 153.194 361.568 152.83L308.913 141.686C307.196 141.322 306.099 139.636 306.462 137.919ZM304.488 147.246C304.852 145.529 306.538 144.431 308.255 144.795L360.91 155.939C362.627 156.303 363.724 157.989 363.361 159.706C362.997 161.423 361.311 162.521 359.594 162.157L306.939 151.013C305.222 150.649 304.125 148.963 304.488 147.246Z" fill="#181B24" fill-opacity="0.45"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M302.491 156.682C302.854 154.965 304.541 153.868 306.258 154.232L358.912 165.376C360.629 165.74 361.727 167.426 361.363 169.143C361 170.86 359.313 171.957 357.596 171.594L304.942 160.449C303.225 160.086 302.127 158.399 302.491 156.682ZM300.517 166.009C300.88 164.292 302.567 163.195 304.284 163.558L356.938 174.703C358.655 175.066 359.753 176.753 359.389 178.47C359.026 180.187 357.339 181.284 355.622 180.921L302.968 169.776C301.251 169.413 300.153 167.726 300.517 166.009ZM298.543 175.336C298.906 173.619 300.593 172.522 302.31 172.885L354.964 184.03C356.681 184.393 357.779 186.08 357.415 187.797C357.052 189.514 355.365 190.611 353.648 190.248L300.994 179.103C299.277 178.74 298.179 177.053 298.543 175.336ZM296.569 184.663C296.932 182.946 298.619 181.849 300.336 182.212L352.99 193.357C354.707 193.72 355.805 195.407 355.441 197.124C355.078 198.841 353.391 199.938 351.674 199.574L299.02 188.43C297.303 188.066 296.205 186.38 296.569 184.663Z" fill="#7E98FF"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M302.491 156.682C302.854 154.965 304.541 153.868 306.258 154.232L358.912 165.376C360.629 165.74 361.727 167.426 361.363 169.143C361 170.86 359.313 171.957 357.596 171.594L304.942 160.449C303.225 160.086 302.127 158.399 302.491 156.682ZM300.517 166.009C300.88 164.292 302.567 163.195 304.284 163.558L356.938 174.703C358.655 175.066 359.753 176.753 359.389 178.47C359.026 180.187 357.339 181.284 355.622 180.921L302.968 169.776C301.251 169.413 300.153 167.726 300.517 166.009ZM298.543 175.336C298.906 173.619 300.593 172.522 302.31 172.885L354.964 184.03C356.681 184.393 357.779 186.08 357.415 187.797C357.052 189.514 355.365 190.611 353.648 190.248L300.994 179.103C299.277 178.74 298.179 177.053 298.543 175.336ZM296.569 184.663C296.932 182.946 298.619 181.849 300.336 182.212L352.99 193.357C354.707 193.72 355.805 195.407 355.441 197.124C355.078 198.841 353.391 199.938 351.674 199.574L299.02 188.43C297.303 188.066 296.205 186.38 296.569 184.663Z" fill="#181B24" fill-opacity="0.45"/>
|
||||
</g>
|
||||
<rect x="306.44" y="82.1825" width="83.2985" height="111.899" rx="11.46" transform="rotate(11.9505 306.44 82.1825)" stroke="#7E98FF" stroke-width="2.50253"/>
|
||||
<rect x="306.44" y="82.1825" width="83.2985" height="111.899" rx="11.46" transform="rotate(11.9505 306.44 82.1825)" stroke="#181B24" stroke-opacity="0.45" stroke-width="2.50253"/>
|
||||
<circle cx="351.065" cy="91.9657" r="12.7387" transform="rotate(11.9505 351.065 91.9657)" fill="#687FE7"/>
|
||||
<circle cx="351.065" cy="91.9657" r="12.7387" transform="rotate(11.9505 351.065 91.9657)" fill="#181B24" fill-opacity="0.35"/>
|
||||
<circle cx="347.012" cy="88.743" r="12.7387" transform="rotate(11.9505 347.012 88.743)" fill="#687FE7"/>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_n_386_10828" x="0" y="0" width="1025" height="1024" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feTurbulence type="fractalNoise" baseFrequency="1 1" stitchTiles="stitch" numOctaves="3" result="noise" seed="1493" />
|
||||
<feColorMatrix in="noise" type="luminanceToAlpha" result="alphaNoise" />
|
||||
<feComponentTransfer in="alphaNoise" result="coloredNoise1">
|
||||
<feFuncA type="discrete" tableValues="0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 "/>
|
||||
</feComponentTransfer>
|
||||
<feComposite operator="in" in2="shape" in="coloredNoise1" result="noise1Clipped" />
|
||||
<feFlood flood-color="rgba(131, 131, 131, 0.27)" result="color1Flood" />
|
||||
<feComposite operator="in" in2="noise1Clipped" in="color1Flood" result="color1" />
|
||||
<feMerge result="effect1_noise_386_10828">
|
||||
<feMergeNode in="shape" />
|
||||
<feMergeNode in="color1" />
|
||||
</feMerge>
|
||||
</filter>
|
||||
<filter id="filter1_d_386_10828" x="284" y="239" width="324.65" height="781.168" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dx="3" dy="4"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.0941176 0 0 0 0 0.105882 0 0 0 0 0.141176 0 0 0 0.25 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_386_10828"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_386_10828" result="shape"/>
|
||||
</filter>
|
||||
<clipPath id="clip0_386_10828">
|
||||
<rect x="788.178" y="232.646" width="84.6173" height="112.823" rx="12.5359" transform="rotate(-9.63619 788.178 232.646)" fill="white"/>
|
||||
</clipPath>
|
||||
<clipPath id="clip1_386_10828">
|
||||
<rect x="481.391" y="654.445" width="84.2274" height="112.303" rx="12.4781" transform="rotate(-9.63619 481.391 654.445)" fill="white"/>
|
||||
</clipPath>
|
||||
<clipPath id="clip2_386_10828">
|
||||
<rect x="305.475" y="80.6992" width="85.801" height="114.401" rx="12.7113" transform="rotate(11.9505 305.475 80.6992)" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 95 KiB |
@@ -12,14 +12,13 @@ import { gotoLogin } from '@/features/auth';
|
||||
// import { ProConnectButton } from '@/features/auth';
|
||||
import { useResponsiveStore } from '@/stores';
|
||||
|
||||
import BannerDarkMode from '../assets/banner-dark.svg';
|
||||
import Banner from '../assets/banner.svg';
|
||||
|
||||
import { getHeaderHeight } from './HomeHeader';
|
||||
|
||||
export default function HomeBanner() {
|
||||
const { t } = useTranslation();
|
||||
const { spacingsTokens, colorsTokens, componentTokens, isDarkMode } =
|
||||
const { spacingsTokens, colorsTokens, componentTokens } =
|
||||
useCunninghamTheme();
|
||||
const { isMobile, isSmallMobile } = useResponsiveStore();
|
||||
const withProConnect = Boolean(
|
||||
@@ -108,7 +107,7 @@ export default function HomeBanner() {
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
{!isMobile && (isDarkMode ? <BannerDarkMode /> : <Banner />)}
|
||||
{!isMobile && <Banner />}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -26,8 +26,6 @@ export const HomeHeader = () => {
|
||||
const logo = (componentTokens as Record<string, unknown>).logo as
|
||||
| { src: string; alt: string; widthHeader: string; widthFooter: string }
|
||||
| undefined;
|
||||
const showLaGaufre =
|
||||
(componentTokens as Record<string, unknown>)['la-gaufre'] === true;
|
||||
|
||||
return (
|
||||
<Box
|
||||
@@ -81,7 +79,7 @@ export const HomeHeader = () => {
|
||||
<Box $direction="row" $gap="1rem" $align="center">
|
||||
<ButtonLogin />
|
||||
<LanguagePicker />
|
||||
{showLaGaufre && <LaGaufre />}
|
||||
<LaGaufre />
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
+42
-32
@@ -2,6 +2,7 @@ import { Modal, ModalSize } from '@openfun/cunningham-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Box, StyledLink, Text, ToggleSwitch, useToast } from '@/components';
|
||||
import { HorizontalSeparator } from '@/components/separators/HorizontalSeparator';
|
||||
import { useUserUpdate } from '@/core/api/useUserUpdate';
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
import { useAuthQuery } from '@/features/auth/api';
|
||||
@@ -55,7 +56,7 @@ export const SettingsModal = ({ onClose, isOpen }: SettingsModalProps) => {
|
||||
title={t('Assistant settings')}
|
||||
>
|
||||
<Box aria-label={t('Assistant settings')}>
|
||||
<Box $justify="space-between">
|
||||
<Box $align="center" $justify="space-between">
|
||||
<Box $gap="2xs">
|
||||
<Text
|
||||
$size="xs"
|
||||
@@ -65,10 +66,49 @@ export const SettingsModal = ({ onClose, isOpen }: SettingsModalProps) => {
|
||||
$padding={{ top: 'sm', bottom: 'sm' }}
|
||||
>
|
||||
{t(
|
||||
'The Assistant is a sovereign AI for public servants. It helps with daily tasks (rephrasing, summarising, translating, information search). Your data stays in France on secure, state-compliant infrastructure and is never used for commercial purposes.',
|
||||
'The Assistant is a sovereign conversational AI designed for public servants. It helps you save time on daily tasks like rephrasing, summarising, translating, or searching information. Your data never leaves France and is stored on secure, state-compliant infrastructures. It is never used for commercial purposes.',
|
||||
)}
|
||||
</Text>
|
||||
|
||||
<Box $gap="2xs" $padding={{ top: 'md' }}>
|
||||
<Text
|
||||
$size="md"
|
||||
$weight="500"
|
||||
$theme="greyscale"
|
||||
$variation="850"
|
||||
>
|
||||
{t('Dark mode')}
|
||||
</Text>
|
||||
<Box
|
||||
$direction="row"
|
||||
$justify="space-between"
|
||||
$align="flex-start"
|
||||
>
|
||||
<Box $css="max-width: 70%;">
|
||||
<Text
|
||||
$css={`
|
||||
display: inline-block;
|
||||
`}
|
||||
$size="xs"
|
||||
$theme="greyscale"
|
||||
$variation="600"
|
||||
$weight="400"
|
||||
>
|
||||
{t(
|
||||
'Enable dark mode to reduce eye strain in low-light environments.',
|
||||
)}
|
||||
</Text>
|
||||
</Box>
|
||||
<ToggleSwitch
|
||||
checked={isDarkMode}
|
||||
onChange={() => toggleDarkMode()}
|
||||
aria-label={t('Dark mode')}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<HorizontalSeparator />
|
||||
|
||||
<Text
|
||||
$size="md"
|
||||
$weight="500"
|
||||
@@ -121,36 +161,6 @@ export const SettingsModal = ({ onClose, isOpen }: SettingsModalProps) => {
|
||||
aria-label={t('Allow conversation analysis')}
|
||||
/>
|
||||
</Box>
|
||||
<Box
|
||||
$display="block"
|
||||
$justify="space-between"
|
||||
$align="flex-end"
|
||||
$gap="2xs"
|
||||
$padding={{ top: 'md' }}
|
||||
$css="min-width: 100%;"
|
||||
>
|
||||
<Box
|
||||
$direction="row"
|
||||
$justify="space-between"
|
||||
$css="min-width: 70%;"
|
||||
>
|
||||
<Box $css="min-width: 70%;">
|
||||
<Text
|
||||
$size="md"
|
||||
$weight="500"
|
||||
$theme="greyscale"
|
||||
$variation="850"
|
||||
>
|
||||
{t('Dark mode')}
|
||||
</Text>
|
||||
</Box>
|
||||
<ToggleSwitch
|
||||
checked={isDarkMode}
|
||||
onChange={() => toggleDarkMode()}
|
||||
aria-label={t('Dark mode')}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Modal>
|
||||
|
||||
@@ -1,15 +1,6 @@
|
||||
{
|
||||
"de": {
|
||||
"translation": {
|
||||
"ABC-1234-XY": "ABC-1234-XY"
|
||||
}
|
||||
},
|
||||
"en": {
|
||||
"translation": {
|
||||
"Login": "Login",
|
||||
"Logout": "Logout"
|
||||
}
|
||||
},
|
||||
"de": { "translation": { "ABC-1234-XY": "ABC-1234-XY" } },
|
||||
"en": { "translation": { "Login": "Login", "Logout": "Logout" } },
|
||||
"fr": {
|
||||
"translation": {
|
||||
"30 sec to tell us what you think or report a bug": "Prenez 30 secondes pour partager votre avis ou signaler un bug",
|
||||
@@ -25,13 +16,13 @@
|
||||
"An error occurred while renaming the conversation": "Une erreur est survenue lors du renommage de la conversation",
|
||||
"An error occurred. Please try again.": "Une erreur s'est produite. Veuillez réessayer.",
|
||||
"Are you sure you want to delete this conversation ?": "Êtes-vous sûr de vouloir supprimer cette conversation ?",
|
||||
"Ask a question": "Poser une question",
|
||||
"Assistant IA replied: ": "Assistant IA a répondu : ",
|
||||
"Assistant is already available, log in to use it now.": "L'Assistant est déjà disponible, connectez-vous pour l'utiliser maintenant.",
|
||||
"Assistant is in development: your feedback matters! Choose how to share your ideas:": "L'assistant est en cours de développement : vos commentaires sont importants ! Choisissez comment partager vos avis :",
|
||||
"Assistant settings": "Paramètres de l'Assistant",
|
||||
"Attach file": "Joindre un fichier",
|
||||
"Attachment summary not supported": "Résumé des pièces jointes non pris en charge",
|
||||
"Available soon": "Disponible prochainement",
|
||||
"Cancel": "Annuler",
|
||||
"Clear search": "Effacer la recherche",
|
||||
"Close model selector": "Fermer le sélecteur de modèle",
|
||||
@@ -46,7 +37,6 @@
|
||||
"Copied": "Copié",
|
||||
"Copy": "Copier",
|
||||
"Copy code": "Copier le code",
|
||||
"Dark mode": "Mode sombre",
|
||||
"Default": "Par défaut",
|
||||
"Delete": "Supprimer",
|
||||
"Delete a conversation": "Supprimer une conversation",
|
||||
@@ -54,8 +44,6 @@
|
||||
"Direct exchange with our team": "Échange direct avec notre équipe",
|
||||
"Enter your message or a question": "Entrez votre message ou une question",
|
||||
"Explore other LaSuite apps": "Explorer les autres applications de LaSuite",
|
||||
"Extracting document: {{documents}} ...": "Extraction du document : {{documents}}...",
|
||||
"Extracting documents: {{documents}} ...": "Extraction des documents : {{documents}}...",
|
||||
"Failed to activate account. Please try again.": "Échec de l'activation du compte. Veuillez réessayer.",
|
||||
"Failed to copy": "Échec de la copie",
|
||||
"Failed to register for notifications. Please try again.": "Échec de l'inscription aux notifications. Veuillez réessayer.",
|
||||
@@ -66,7 +54,6 @@
|
||||
"Feedback Négatif": "Retour négatif",
|
||||
"Feedback positif": "Retour positif",
|
||||
"File type not supported": "Type de fichier non pris en charge",
|
||||
"Find recent news about...": "Trouver les dernières actualités concernant...",
|
||||
"Get notified about the Public Beta.": "Soyez informé de la Bêta publique.",
|
||||
"Get notified for the public beta": "Être notifié pour la bêta publique",
|
||||
"Give a quick opinion": "Donner un avis rapide",
|
||||
@@ -74,11 +61,13 @@
|
||||
"History": "Historique",
|
||||
"Home": "Accueil",
|
||||
"If enabled, this allows us to analyse your exchanges to improve the Assistant. If disabled, all conversations remain confidential and are not used in any way. ": "Si cette option est activée, cela nous permet d'analyser vos conversations afin d'améliorer l'Assistant. Si elle est désactivée, toutes les conversations restent confidentielles et ne sont utilisées d'aucune manière. ",
|
||||
"Illustration": "Image",
|
||||
"Image 401": "Image 401",
|
||||
"Image 403": "Image 403",
|
||||
"Invalid activation code. Please check and try again.": "Code d'activation invalide. Veuillez le vérifier et réessayer.",
|
||||
"It seems that the page you are looking for does not exist or cannot be displayed correctly.": "Il semble que la page que vous cherchez n'existe pas ou ne puisse pas être affichée correctement.",
|
||||
"Know more": "En savoir plus",
|
||||
"Language": "Langue",
|
||||
"Learn more about data usage.": "En savoir plus sur l’usage des données.",
|
||||
"Load more": "Afficher plus",
|
||||
"Log in to access this page.": "Connectez-vous pour accéder à cette page.",
|
||||
@@ -108,7 +97,6 @@
|
||||
"Search results": "Résultats de la recherche",
|
||||
"Search...": "Recherche...",
|
||||
"Select": "Sélectionner",
|
||||
"Select language": "Sélectionnez une langue",
|
||||
"Select model": "Sélectionner un modèle",
|
||||
"Send": "Envoyer",
|
||||
"Settings": "Réglages",
|
||||
@@ -120,15 +108,13 @@
|
||||
"Start conversation": "Entamer la conversation",
|
||||
"Stop": "Stop",
|
||||
"Summarizing...": "Résumé en cours...",
|
||||
"Translation in progress...": "Traduction en cours...",
|
||||
"The Assistant is a sovereign AI for public servants. It helps with daily tasks (rephrasing, summarising, translating, information search). Your data stays in France on secure, state-compliant infrastructure and is never used for commercial purposes.": "L'Assistant est une IA souveraine destinée aux fonctionnaires. Il les aide dans leurs tâches quotidiennes (reformulation, résumé, traduction, recherche d'informations). Vos données restent en France, sur une infrastructure sécurisée et conforme aux normes nationales, et ne sont jamais utilisées à des fins commerciales.",
|
||||
"The Assistant is a sovereign conversational AI designed for public servants. It helps you save time on daily tasks like rephrasing, summarising, translating, or searching information. Your data never leaves France and is stored on secure, state-compliant infrastructures. It is never used for commercial purposes.": "L'Assistant est une IA souveraine conçue pour les fonctionnaires. Il vous permet de gagner du temps sur des tâches quotidiennes telles que la reformulation, le résumé, la traduction ou la recherche d'informations. Vos données ne quittent jamais la France et sont stockées sur des infrastructures sûres et conformes à l'état et ne sont jamais utilisées à des fins commerciales.",
|
||||
"The Assistant is in Beta": "L'Assistant est en Bêta",
|
||||
"The conversation has been deleted.": "La conversation a été supprimée.",
|
||||
"The conversation has been renamed.": "La conversation a été renommée.",
|
||||
"The summary feature is not supported yet.": "La fonctionnalité de résumé n'est pas encore prise en charge.",
|
||||
"Thinking...": "Réflexion...",
|
||||
"To add a file to the conversation, drop it here.": "Pour ajouter un fichier à la conversation, déposez-le ici.",
|
||||
"Turn this list into bullet points": "Transformer cette liste en liste à puces...",
|
||||
"Unlock access": "Déverrouiller l'accès",
|
||||
"Unlocking...": "Déverrouillage en cours...",
|
||||
"Untitled conversation": "Conversation sans titre",
|
||||
@@ -138,7 +124,6 @@
|
||||
"We'll email you when the public beta opens.": "Nous vous enverrons un email quand la bêta publique sera ouverte.",
|
||||
"Web": "Web",
|
||||
"What is on your mind?": "Qu’avez-vous en tête ?",
|
||||
"Write a short product description": "Écrire une description courte du produit...",
|
||||
"Write on Tchap": "Écrire sur Tchap",
|
||||
"You are on the list": "Vous êtes dans la liste",
|
||||
"You do not have permission to view this page.": "Vous n’avez pas la permission de voir cette page.",
|
||||
@@ -166,13 +151,13 @@
|
||||
"An error occurred while renaming the conversation": "Er is een fout opgetreden bij het hernoemen van het gesprek",
|
||||
"An error occurred. Please try again.": "Er is een fout opgetreden. Probeer het opnieuw.",
|
||||
"Are you sure you want to delete this conversation ?": "Weet u zeker dat u dit gesprek wilt verwijderen?",
|
||||
"Ask a question": "Stel een vraag",
|
||||
"Assistant IA replied: ": "AI Assistent antwoordde: ",
|
||||
"Assistant is already available, log in to use it now.": "Assistent is al beschikbaar, log in om het nu te gebruiken.",
|
||||
"Assistant is in development: your feedback matters! Choose how to share your ideas:": "Assistent is in ontwikkeling: uw feedback telt! Kies hoe u uw ideeën wilt delen:",
|
||||
"Assistant settings": "Assistentinstellingen",
|
||||
"Attach file": "Bestand bijvoegen",
|
||||
"Attachment summary not supported": "Bijlageoverzicht niet ondersteund",
|
||||
"Available soon": "Binnenkort beschikbaar",
|
||||
"Cancel": "Annuleren",
|
||||
"Clear search": "Zoekopdracht wissen",
|
||||
"Close model selector": "Sluit modelselector",
|
||||
@@ -187,7 +172,6 @@
|
||||
"Copied": "Gekopieerd",
|
||||
"Copy": "Kopiëren",
|
||||
"Copy code": "Kopieer code",
|
||||
"Dark mode": "Donkere modus",
|
||||
"Default": "Standaard",
|
||||
"Delete": "Verwijderen",
|
||||
"Delete a conversation": "Een gesprek verwijderen",
|
||||
@@ -195,8 +179,6 @@
|
||||
"Direct exchange with our team": "Directe uitwisseling met ons team",
|
||||
"Enter your message or a question": "Voer uw bericht of een vraag in",
|
||||
"Explore other LaSuite apps": "Ontdek andere LaSuite-apps",
|
||||
"Extracting document: {{documents}} ...": "Document uitpakken: {{documents}}...",
|
||||
"Extracting documents: {{documents}} ...": "Documenten uitpakken: {{documents}}...",
|
||||
"Failed to activate account. Please try again.": "Account activeren mislukt. Probeer het opnieuw.",
|
||||
"Failed to copy": "Kopiëren mislukt",
|
||||
"Failed to register for notifications. Please try again.": "Registratie voor meldingen mislukt. Probeer het opnieuw.",
|
||||
@@ -207,7 +189,6 @@
|
||||
"Feedback Négatif": "Negatieve feedback",
|
||||
"Feedback positif": "Positieve feedback",
|
||||
"File type not supported": "Bestandstype niet ondersteund",
|
||||
"Find recent news about...": "Vind het laatste nieuws over...",
|
||||
"Get notified about the Public Beta.": "Ontvang een melding over de openbare bèta.",
|
||||
"Get notified for the public beta": "Ontvang een melding voor de openbare bètaversie",
|
||||
"Give a quick opinion": "Geef snel een mening",
|
||||
@@ -215,11 +196,13 @@
|
||||
"History": "Geschiedenis",
|
||||
"Home": "Thuis",
|
||||
"If enabled, this allows us to analyse your exchanges to improve the Assistant. If disabled, all conversations remain confidential and are not used in any way. ": "Indien ingeschakeld, kunnen we uw gesprekken analyseren om de Assistent te verbeteren. Indien uitgeschakeld, blijven alle gesprekken vertrouwelijk en worden ze op geen enkele manier gebruikt. ",
|
||||
"Illustration": "Illustratie",
|
||||
"Image 401": "Afbeelding 401",
|
||||
"Image 403": "Afbeelding 403",
|
||||
"Invalid activation code. Please check and try again.": "Ongeldige activeringscode. Controleer en probeer het opnieuw.",
|
||||
"It seems that the page you are looking for does not exist or cannot be displayed correctly.": "Het lijkt erop dat de pagina die u zoekt niet bestaat of niet correct kan worden weergegeven.",
|
||||
"Know more": "Meer weten",
|
||||
"Language": "Taal",
|
||||
"Learn more about data usage.": "Meer informatie over datagebruik.",
|
||||
"Load more": "Laad Meer",
|
||||
"Log in to access this page.": "Meld u aan om deze pagina te zien.",
|
||||
@@ -249,7 +232,6 @@
|
||||
"Search results": "Zoekresultaten",
|
||||
"Search...": "Zoek...",
|
||||
"Select": "Selecteer",
|
||||
"Select language": "Selecteer taal",
|
||||
"Select model": "Selecteer model",
|
||||
"Send": "Verstuur",
|
||||
"Settings": "Instellingen",
|
||||
@@ -261,15 +243,13 @@
|
||||
"Start conversation": "Begin een gesprek",
|
||||
"Stop": "Stop",
|
||||
"Summarizing...": "Samenvatten...",
|
||||
"Translation in progress...": "Vertaling bezig...",
|
||||
"The Assistant is a sovereign AI for public servants. It helps with daily tasks (rephrasing, summarising, translating, information search). Your data stays in France on secure, state-compliant infrastructure and is never used for commercial purposes.": "De assistent is een soevereine AI voor overheidsambtenaren. Het helpt met dagelijkse taken (herformuleren, samenvatten, vertalen, informatiezoeken). Je gegevens blijven in Nederland op een veilige, door de staat compatibele infrastructuur en worden nooit voor commerciële doeleinden gebruikt.",
|
||||
"The Assistant is a sovereign conversational AI designed for public servants. It helps you save time on daily tasks like rephrasing, summarising, translating, or searching information. Your data never leaves France and is stored on secure, state-compliant infrastructures. It is never used for commercial purposes.": "De Assistent is een soevereine conversationele AI, ontworpen voor ambtenaren. Het helpt je tijd te besparen bij dagelijkse taken zoals het herformuleren, samenvatten, vertalen of zoeken van informatie. Je gegevens verlaten het land nooit en worden opgeslagen op beveiligde, door de overheid goedgekeurde infrastructuren. Ze worden nooit gebruikt voor commerciële doeleinden.",
|
||||
"The Assistant is in Beta": "De Assistent is in bèta",
|
||||
"The conversation has been deleted.": "Het gesprek is verwijderd.",
|
||||
"The conversation has been renamed.": "Het gesprek is verwijderd.",
|
||||
"The summary feature is not supported yet.": "De samenvattingsfunctie wordt nog niet ondersteund.",
|
||||
"Thinking...": "Denken...",
|
||||
"To add a file to the conversation, drop it here.": "Als u een bestand aan het gesprek wilt toevoegen, sleept u het hierheen.",
|
||||
"Turn this list into bullet points": "Zet deze lijst om in opsommingstekens",
|
||||
"Unlock access": "Toegang ontgrendelen",
|
||||
"Unlocking...": "Ontgrendelen...",
|
||||
"Untitled conversation": "Ongetiteld gesprek",
|
||||
@@ -279,7 +259,6 @@
|
||||
"We'll email you when the public beta opens.": "We sturen u een e-mail zodra de openbare bètaversie beschikbaar is.",
|
||||
"Web": "Internet",
|
||||
"What is on your mind?": "Waar denk je aan?",
|
||||
"Write a short product description": "Schrijf een korte productbeschrijving",
|
||||
"Write on Tchap": "Schrijf op Tchap",
|
||||
"You are on the list": "Je staat op de lijst",
|
||||
"You do not have permission to view this page.": "U heeft geen toestemming om deze pagina te bekijken.",
|
||||
@@ -307,13 +286,13 @@
|
||||
"An error occurred while renaming the conversation": "Ошибка при переименовании беседы",
|
||||
"An error occurred. Please try again.": "Произошла ошибка. Пожалуйста, повторите попытку.",
|
||||
"Are you sure you want to delete this conversation ?": "Вы действительно хотите удалить эту беседу?",
|
||||
"Ask a question": "Задать вопрос",
|
||||
"Assistant IA replied: ": "Помощник ИИ ответил: ",
|
||||
"Assistant is already available, log in to use it now.": "Помощник уже доступен, просто войдите в систему.",
|
||||
"Assistant is in development: your feedback matters! Choose how to share your ideas:": "Помощник находится в разработке: ваши отзывы важны! Выберите, как поделиться своими идеями:",
|
||||
"Assistant settings": "Настройки помощника",
|
||||
"Attach file": "Прикрепить файл",
|
||||
"Attachment summary not supported": "Сводка о вложениях не поддерживается",
|
||||
"Available soon": "Скоро будет доступно",
|
||||
"Cancel": "Отмена",
|
||||
"Clear search": "Очистить поиск",
|
||||
"Close model selector": "Закрыть выбор модели",
|
||||
@@ -328,7 +307,6 @@
|
||||
"Copied": "Скопировано",
|
||||
"Copy": "Копировать",
|
||||
"Copy code": "Скопировать код",
|
||||
"Dark mode": "Тёмный режим",
|
||||
"Default": "По-умолчанию",
|
||||
"Delete": "Удалить",
|
||||
"Delete a conversation": "Удалить беседу",
|
||||
@@ -336,8 +314,6 @@
|
||||
"Direct exchange with our team": "Прямое общение с нашей командой",
|
||||
"Enter your message or a question": "Введите сообщение или вопрос",
|
||||
"Explore other LaSuite apps": "Посмотреть другие приложения LaSuite",
|
||||
"Extracting document: {{documents}} ...": "Извлечение документа: {{documents}}...",
|
||||
"Extracting documents: {{documents}} ...": "Извлечение документов: {{documents}}...",
|
||||
"Failed to activate account. Please try again.": "Не удалось активировать учётную запись. Пожалуйста, попробуйте снова.",
|
||||
"Failed to copy": "Не удалось скопировать",
|
||||
"Failed to register for notifications. Please try again.": "Не удалось зарегистрироваться для получения уведомлений. Пожалуйста, попробуйте ещё раз.",
|
||||
@@ -348,7 +324,6 @@
|
||||
"Feedback Négatif": "Отрицательный отзыв",
|
||||
"Feedback positif": "Положительный отзыв",
|
||||
"File type not supported": "Тип файла не поддерживается",
|
||||
"Find recent news about...": "Найти последние новости...",
|
||||
"Get notified about the Public Beta.": "Получать уведомления о публичной бета-версии.",
|
||||
"Get notified for the public beta": "Получать уведомления о публичной бета-версии",
|
||||
"Give a quick opinion": "Оставить быстрый отзыв",
|
||||
@@ -356,11 +331,13 @@
|
||||
"History": "История",
|
||||
"Home": "Главная",
|
||||
"If enabled, this allows us to analyse your exchanges to improve the Assistant. If disabled, all conversations remain confidential and are not used in any way. ": "Если этот параметр включён, мы можем анализировать ваши беседы для улучшения интеллекта помощника. Если отключён, то все разговоры остаются конфиденциальными и никак не используются. ",
|
||||
"Illustration": "Иллюстрация",
|
||||
"Image 401": "Изображение 401",
|
||||
"Image 403": "Изображение 403",
|
||||
"Invalid activation code. Please check and try again.": "Неверный код активации. Пожалуйста, проверьте его и повторите попытку.",
|
||||
"It seems that the page you are looking for does not exist or cannot be displayed correctly.": "Похоже, страница, которую вы ищете, не существует или не может быть правильно отображена.",
|
||||
"Know more": "Подробнее",
|
||||
"Language": "Язык",
|
||||
"Learn more about data usage.": "Узнайте больше об использовании данных.",
|
||||
"Load more": "Загрузить ещё",
|
||||
"Log in to access this page.": "Войдите, чтобы получить доступ к этой странице.",
|
||||
@@ -390,7 +367,6 @@
|
||||
"Search results": "Результаты поиска",
|
||||
"Search...": "Поиск...",
|
||||
"Select": "Выбрать",
|
||||
"Select language": "Выберите язык",
|
||||
"Select model": "Выберите модель",
|
||||
"Send": "Отправить",
|
||||
"Settings": "Настройки",
|
||||
@@ -402,15 +378,13 @@
|
||||
"Start conversation": "Начать беседу",
|
||||
"Stop": "Остановить",
|
||||
"Summarizing...": "Обобщение...",
|
||||
"Translation in progress...": "Перевод выполняется...",
|
||||
"The Assistant is a sovereign AI for public servants. It helps with daily tasks (rephrasing, summarising, translating, information search). Your data stays in France on secure, state-compliant infrastructure and is never used for commercial purposes.": "Помощник — независимый ИИ для государственных служащих. Он помогает в выполнении повседневных задач (перефразирование, резюмирование, перевод, поиск информации). Ваши данные хранятся во Франции на безопасной инфраструктуре, соответствующей государственным требованиям, и никогда не используются в коммерческих целях.",
|
||||
"The Assistant is a sovereign conversational AI designed for public servants. It helps you save time on daily tasks like rephrasing, summarising, translating, or searching information. Your data never leaves France and is stored on secure, state-compliant infrastructures. It is never used for commercial purposes.": "Помощник - собеседник на основе ИИ для государственных служащих. Он поможет вам сэкономить время на ежедневных задачах, таких как перефразирование, обобщение, перевод или поиск информации. Ваши данные никогда не покидают Францию и хранятся в охраняемой государственной инфраструктуре, которая никогда не используется в коммерческих целях.",
|
||||
"The Assistant is in Beta": "Помощник находится на этапе Бета-версии",
|
||||
"The conversation has been deleted.": "Беседа была удалена.",
|
||||
"The conversation has been renamed.": "Беседа была переименована.",
|
||||
"The summary feature is not supported yet.": "Функция сводки пока не поддерживается.",
|
||||
"Thinking...": "Размышление...",
|
||||
"To add a file to the conversation, drop it here.": "Чтобы добавить файл в беседу, поместите его сюда.",
|
||||
"Turn this list into bullet points": "Преобразовать этот список в маркированный",
|
||||
"Unlock access": "Разблокировать",
|
||||
"Unlocking...": "Разблокировка...",
|
||||
"Untitled conversation": "Беседа без названия",
|
||||
@@ -420,7 +394,6 @@
|
||||
"We'll email you when the public beta opens.": "Мы отправим вам письмо, когда станет доступна публичная бета-версия.",
|
||||
"Web": "Интернет",
|
||||
"What is on your mind?": "О чём вы думаете?",
|
||||
"Write a short product description": "Введите краткое описание продукта",
|
||||
"Write on Tchap": "Написать в Tchap",
|
||||
"You are on the list": "Вы в списке",
|
||||
"You do not have permission to view this page.": "У вас недостаточно прав для просмотра этой страницы.",
|
||||
@@ -448,13 +421,13 @@
|
||||
"An error occurred while renaming the conversation": "Сталася помилка під час перейменування розмови",
|
||||
"An error occurred. Please try again.": "Сталась помилка. Спробуйте ще раз.",
|
||||
"Are you sure you want to delete this conversation ?": "Ви дійсно бажаєте видалити цю розмову?",
|
||||
"Ask a question": "Задати питання",
|
||||
"Assistant IA replied: ": "Відповідь помічника ШІ: ",
|
||||
"Assistant is already available, log in to use it now.": "Помічник вже доступний, увійдіть щоб почати використання.",
|
||||
"Assistant is in development: your feedback matters! Choose how to share your ideas:": "Помічник в розробці: ваші відгуки мають значення! Оберіть, як поділитися своїми ідеями:",
|
||||
"Assistant settings": "Налаштування помічника",
|
||||
"Attach file": "Додати файл",
|
||||
"Attachment summary not supported": "Узагальнення вкладень не підтримується",
|
||||
"Available soon": "Незабаром буде доступно",
|
||||
"Cancel": "Скасувати",
|
||||
"Clear search": "Очистити вікно пошуку",
|
||||
"Close model selector": "Закрити вікно вибору моделі",
|
||||
@@ -469,7 +442,6 @@
|
||||
"Copied": "Скопійовано",
|
||||
"Copy": "Копіювати",
|
||||
"Copy code": "Скопіювати код",
|
||||
"Dark mode": "Темний режим",
|
||||
"Default": "За замовчуванням",
|
||||
"Delete": "Видалити",
|
||||
"Delete a conversation": "Видалити розмову",
|
||||
@@ -477,8 +449,6 @@
|
||||
"Direct exchange with our team": "Пряме спілкування з нашою командою",
|
||||
"Enter your message or a question": "Введіть ваше повідомлення або питання",
|
||||
"Explore other LaSuite apps": "Ознайомтесь з іншими застосунками LaSuite",
|
||||
"Extracting document: {{documents}} ...": "Видобування документа: {{documents}}...",
|
||||
"Extracting documents: {{documents}} ...": "Видобування документів: {{documents}}...",
|
||||
"Failed to activate account. Please try again.": "Не вдалося активувати обліковий запис. Спробуйте ще раз.",
|
||||
"Failed to copy": "Не вдалось скопіювати",
|
||||
"Failed to register for notifications. Please try again.": "Не вдалося виконати реєстрацію для повідомлень. Будь ласка, спробуйте ще раз.",
|
||||
@@ -489,7 +459,6 @@
|
||||
"Feedback Négatif": "Негативний відгук",
|
||||
"Feedback positif": "Позитивний відгук",
|
||||
"File type not supported": "Тип файлу не підтримується",
|
||||
"Find recent news about...": "Знайти останні новини про...",
|
||||
"Get notified about the Public Beta.": "Отримувати повідомлення про публічну бета-версію.",
|
||||
"Get notified for the public beta": "Отримувати повідомлення про публічну бета-версію",
|
||||
"Give a quick opinion": "Дати швидкий відгук",
|
||||
@@ -497,11 +466,13 @@
|
||||
"History": "Історія",
|
||||
"Home": "Головна",
|
||||
"If enabled, this allows us to analyse your exchanges to improve the Assistant. If disabled, all conversations remain confidential and are not used in any way. ": "Якщо увімкнуто, це дозволить нам аналізувати ваші розмови для покращення помічника. Якщо вимкнено, всі розмови залишаються конфіденційними та не використовуються жодним чином. ",
|
||||
"Illustration": "Ілюстрація",
|
||||
"Image 401": "Зображення 401",
|
||||
"Image 403": "Зображення 403",
|
||||
"Invalid activation code. Please check and try again.": "Невірний код активації. Будь ласка, перевірте та повторіть спробу.",
|
||||
"It seems that the page you are looking for does not exist or cannot be displayed correctly.": "Схоже, що сторінка, яку ви шукаєте, не існує або не може бути показаною правильно.",
|
||||
"Know more": "Докладніше",
|
||||
"Language": "Мова",
|
||||
"Learn more about data usage.": "Дізнатися більше про використання даних.",
|
||||
"Load more": "Завантажити ще",
|
||||
"Log in to access this page.": "Увійдіть, щоб отримати доступ до цієї сторінки.",
|
||||
@@ -531,7 +502,6 @@
|
||||
"Search results": "Результати пошуку",
|
||||
"Search...": "Пошук...",
|
||||
"Select": "Вибрати",
|
||||
"Select language": "Оберіть мову",
|
||||
"Select model": "Виберіть модель",
|
||||
"Send": "Відправити",
|
||||
"Settings": "Налаштування",
|
||||
@@ -543,15 +513,13 @@
|
||||
"Start conversation": "Почати розмову",
|
||||
"Stop": "Зупинити",
|
||||
"Summarizing...": "Узагальнення...",
|
||||
"Translation in progress...": "Переклад виконується...",
|
||||
"The Assistant is a sovereign AI for public servants. It helps with daily tasks (rephrasing, summarising, translating, information search). Your data stays in France on secure, state-compliant infrastructure and is never used for commercial purposes.": "Помічник — це незалежний штучний інтелект для державних службовців. Він допомагає у виконанні щоденних завдань (перефразування, узагальнення, переклад, пошук інформації). Ваші дані зберігаються у Франції на безпечній інфраструктурі, що відповідає державним вимогам, і ніколи не використовуються в комерційних цілях.",
|
||||
"The Assistant is a sovereign conversational AI designed for public servants. It helps you save time on daily tasks like rephrasing, summarising, translating, or searching information. Your data never leaves France and is stored on secure, state-compliant infrastructures. It is never used for commercial purposes.": "Помічник - це розмовний ШІ, призначений для державних службовців. Він допоможе вам зберегти час в таких щоденних завданнях, як рефразування, узагальнення, переклад або пошукова інформація. Ваші дані ніколи не покидають Францію та зберігаються на захищеній державній інфраструктурі. Вони ніколи не використовуються для комерційних цілей.",
|
||||
"The Assistant is in Beta": "Помічник у бета-версії",
|
||||
"The conversation has been deleted.": "Розмова була видалена.",
|
||||
"The conversation has been renamed.": "Розмова була перейменована.",
|
||||
"The summary feature is not supported yet.": "Функція узагальнення ще не підтримується.",
|
||||
"Thinking...": "Мислення...",
|
||||
"To add a file to the conversation, drop it here.": "Щоб додати файл до розмови, покладіть його сюди.",
|
||||
"Turn this list into bullet points": "Перетворити цей список на маркований",
|
||||
"Unlock access": "Розблокувати доступ",
|
||||
"Unlocking...": "Розблоковування...",
|
||||
"Untitled conversation": "Розмова без назви",
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useRouter } from 'next/router';
|
||||
import { type ReactElement, Suspense } from 'react';
|
||||
import type { ReactElement } from 'react';
|
||||
|
||||
import { Loader } from '@/components';
|
||||
import { Chat } from '@/features/chat/components/Chat';
|
||||
import { MainLayout } from '@/layouts';
|
||||
import { NextPageWithLayout } from '@/types/next';
|
||||
@@ -10,11 +9,7 @@ const Page: NextPageWithLayout = () => {
|
||||
const router = useRouter();
|
||||
const { id } = router.query;
|
||||
|
||||
return (
|
||||
<Suspense fallback={<Loader />}>
|
||||
<Chat initialConversationId={id as string} />
|
||||
</Suspense>
|
||||
);
|
||||
return <Chat initialConversationId={id as string} />;
|
||||
};
|
||||
|
||||
Page.getLayout = function getLayout(page: ReactElement) {
|
||||
|
||||
@@ -1,16 +1,11 @@
|
||||
import { type ReactElement, Suspense } from 'react';
|
||||
import type { ReactElement } from 'react';
|
||||
|
||||
import { Loader } from '@/components';
|
||||
import { Chat } from '@/features/chat/components/Chat';
|
||||
import { MainLayout } from '@/layouts';
|
||||
import { NextPageWithLayout } from '@/types/next';
|
||||
|
||||
const Page: NextPageWithLayout = () => {
|
||||
return (
|
||||
<Suspense fallback={<Loader />}>
|
||||
<Chat initialConversationId={undefined} />
|
||||
</Suspense>
|
||||
);
|
||||
return <Chat initialConversationId={undefined} />;
|
||||
};
|
||||
|
||||
Page.getLayout = function getLayout(page: ReactElement) {
|
||||
|
||||
@@ -30,7 +30,7 @@ main ::-webkit-scrollbar-track,
|
||||
|
||||
main ::-webkit-scrollbar-thumb,
|
||||
.ReactModalPortal ::-webkit-scrollbar-thumb {
|
||||
background-color: var(--c--contextuals--background--surface--tertiary);
|
||||
background-color: #d6dee1;
|
||||
border-radius: 20px;
|
||||
border: 6px solid transparent;
|
||||
background-clip: content-box;
|
||||
@@ -38,7 +38,7 @@ main ::-webkit-scrollbar-thumb,
|
||||
|
||||
main ::-webkit-scrollbar-thumb:hover,
|
||||
.ReactModalPortal ::-webkit-scrollbar-thumb:hover {
|
||||
background-color: var(--c--contextuals--background--surface--primary);
|
||||
background-color: #a8bbbf;
|
||||
}
|
||||
|
||||
.btn-unstyled {
|
||||
@@ -234,6 +234,21 @@ ul a:hover {
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.c__button--info {
|
||||
background-color: #cfe5ff !important;
|
||||
color: #265eaa !important;
|
||||
font-weight: 500 !important;
|
||||
}
|
||||
|
||||
.c__button--info:hover,
|
||||
.c__button--info:active,
|
||||
.c__button--info:focus {
|
||||
outline: none !important;
|
||||
background-color: #b7d7ff !important;
|
||||
color: #265eaa !important;
|
||||
border-color: #b7d7ff !important;
|
||||
}
|
||||
|
||||
.research-web-button:focus:not(:focus-visible) {
|
||||
box-shadow: none !important;
|
||||
}
|
||||
@@ -301,40 +316,6 @@ figure[data-rehype-pretty-code-figure] > pre > code {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.mainContent-chat table {
|
||||
display: block;
|
||||
width: 100%;
|
||||
margin-top: 32px;
|
||||
overflow-x: scroll;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.mainContent-chat table th {
|
||||
color: var(--c--contextuals--content--semantic--neutral--secondary);
|
||||
font-weight: 500;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.mainContent-chat table tr,
|
||||
.mainContent-chat table th {
|
||||
display: flex;
|
||||
border-bottom: 1px solid var(--c--contextuals--border--surface--primary);
|
||||
}
|
||||
|
||||
.mainContent-chat tbody tr,
|
||||
.mainContent-chat thead th {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.mainContent-chat table tr:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.mainContent-chat table td,
|
||||
.mainContent-chat table th {
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
|
||||
@@ -63,7 +63,7 @@ export function PostHogProvider({
|
||||
},
|
||||
capture_pageview: false,
|
||||
capture_pageleave: true,
|
||||
__add_tracing_headers: false,
|
||||
__add_tracing_headers: true,
|
||||
});
|
||||
|
||||
const handleRouteChange = () => posthog?.capture('$pageview');
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
import { overrideConfig } from './common';
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/home/');
|
||||
});
|
||||
@@ -71,71 +69,4 @@ test.describe('Chat page', () => {
|
||||
await chatHistoryLink.click();
|
||||
await expect(messageContent).toBeVisible();
|
||||
});
|
||||
|
||||
test('the user can paste a document into the chat input', async ({ page }) => {
|
||||
await overrideConfig(page, {
|
||||
FEATURE_FLAGS: {
|
||||
'document-upload': 'enabled',
|
||||
'web-search': 'enabled',
|
||||
},
|
||||
});
|
||||
|
||||
await page.goto('/');
|
||||
|
||||
const chatInput = page.getByRole('textbox', {
|
||||
name: 'Enter your message or a',
|
||||
});
|
||||
await expect(chatInput).toBeVisible();
|
||||
await chatInput.click();
|
||||
|
||||
// Create a test file content
|
||||
const fileContent = 'Test document content for paste';
|
||||
const fileName = 'test-document.txt';
|
||||
const fileType = 'text/plain';
|
||||
|
||||
// Simulate paste event with file
|
||||
await page.evaluate(
|
||||
({ content, name, type }) => {
|
||||
const textarea = document.querySelector(
|
||||
'textarea[name="inputchat-textarea"]',
|
||||
) as HTMLTextAreaElement;
|
||||
if (!textarea) return;
|
||||
|
||||
// Create a File object
|
||||
const file = new File([content], name, { type });
|
||||
|
||||
// Create a DataTransfer object to simulate clipboard
|
||||
const dataTransfer = new DataTransfer();
|
||||
dataTransfer.items.add(file);
|
||||
|
||||
// Create a paste event - ClipboardEvent constructor doesn't accept clipboardData
|
||||
// so we create a regular Event and add clipboardData property
|
||||
const pasteEvent = new Event('paste', {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
}) as unknown as ClipboardEvent;
|
||||
|
||||
// Define clipboardData property to make it accessible
|
||||
Object.defineProperty(pasteEvent, 'clipboardData', {
|
||||
value: {
|
||||
files: dataTransfer.files,
|
||||
items: dataTransfer.items,
|
||||
types: Array.from(dataTransfer.types),
|
||||
getData: () => '',
|
||||
setData: () => {},
|
||||
},
|
||||
writable: false,
|
||||
configurable: true,
|
||||
});
|
||||
|
||||
textarea.dispatchEvent(pasteEvent);
|
||||
},
|
||||
{ content: fileContent, name: fileName, type: fileType },
|
||||
);
|
||||
|
||||
// Wait for the file to be processed and appear in the attachment list
|
||||
// The attachment should be visible with the file name
|
||||
const attachment = page.getByText(fileName, { exact: false }).first();
|
||||
await expect(attachment).toBeVisible({ timeout: 5000 });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "app-e2e",
|
||||
"version": "0.0.13",
|
||||
"version": "0.0.12",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"lint": "eslint . --ext .ts",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "conversations",
|
||||
"version": "0.0.13",
|
||||
"version": "0.0.12",
|
||||
"private": true,
|
||||
"workspaces": {
|
||||
"packages": [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "eslint-config-conversations",
|
||||
"version": "0.0.13",
|
||||
"version": "0.0.12",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"lint": "eslint --ext .js ."
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "packages-i18n",
|
||||
"version": "0.0.13",
|
||||
"version": "0.0.12",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"extract-translation": "yarn extract-translation:conversations",
|
||||
|
||||
+635
-109
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
||||
apiVersion: v2
|
||||
type: application
|
||||
name: conversations
|
||||
version: 0.0.5
|
||||
version: 0.0.4
|
||||
appVersion: latest
|
||||
|
||||
@@ -72,11 +72,6 @@ spec:
|
||||
subPath: {{ .subPath | default "" }}
|
||||
readOnly: {{ .readOnly }}
|
||||
{{- end }}
|
||||
{{- if .Values.backend.llmConfiguration.enabled }}
|
||||
- name: llm-configuration
|
||||
mountPath: {{ .Values.backend.llmConfiguration.mount_path }}
|
||||
readOnly: true
|
||||
{{- end }}
|
||||
{{- with .Values.backend.nodeSelector }}
|
||||
nodeSelector:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
@@ -126,9 +121,4 @@ spec:
|
||||
emptyDir: {}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- if .Values.backend.llmConfiguration.enabled }}
|
||||
- name: llm-configuration
|
||||
configMap:
|
||||
name: conversations-llm-configuration
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
@@ -73,11 +73,6 @@ spec:
|
||||
subPath: {{ .subPath | default "" }}
|
||||
readOnly: {{ .readOnly }}
|
||||
{{- end }}
|
||||
{{- if .Values.backend.llmConfiguration.enabled }}
|
||||
- name: llm-configuration
|
||||
mountPath: {{ .Values.backend.llmConfiguration.mount_path }}
|
||||
readOnly: true
|
||||
{{- end }}
|
||||
{{- with .Values.backend.nodeSelector }}
|
||||
nodeSelector:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
@@ -127,8 +122,3 @@ spec:
|
||||
emptyDir: {}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- if .Values.backend.llmConfiguration.enabled }}
|
||||
- name: llm-configuration
|
||||
configMap:
|
||||
name: conversations-llm-configuration
|
||||
{{- end }}
|
||||
|
||||
@@ -73,11 +73,6 @@ spec:
|
||||
subPath: {{ .subPath | default "" }}
|
||||
readOnly: {{ .readOnly }}
|
||||
{{- end }}
|
||||
{{- if .Values.backend.llmConfiguration.enabled }}
|
||||
- name: llm-configuration
|
||||
mountPath: {{ .Values.backend.llmConfiguration.mount_path }}
|
||||
readOnly: true
|
||||
{{- end }}
|
||||
{{- with .Values.backend.nodeSelector }}
|
||||
nodeSelector:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
@@ -127,8 +122,3 @@ spec:
|
||||
emptyDir: {}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- if .Values.backend.llmConfiguration.enabled }}
|
||||
- name: llm-configuration
|
||||
configMap:
|
||||
name: conversations-llm-configuration
|
||||
{{- end }}
|
||||
|
||||
@@ -234,10 +234,10 @@ backend:
|
||||
## @param backend.probes.readiness.initialDelaySeconds [nullable] Configure timeout for backend readiness probe
|
||||
probes:
|
||||
liveness:
|
||||
path: /__lbheartbeat__
|
||||
path: /__heartbeat__
|
||||
initialDelaySeconds: 10
|
||||
readiness:
|
||||
path: /__heartbeat__
|
||||
path: /__lbheartbeat__
|
||||
initialDelaySeconds: 10
|
||||
|
||||
## @param backend.resources Resource requirements for the backend container
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "mail_mjml",
|
||||
"version": "0.0.13",
|
||||
"version": "0.0.12",
|
||||
"description": "An util to generate html and text django's templates from mjml templates",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
|
||||
+1
-1
@@ -399,7 +399,7 @@ glob-parent@~5.1.2:
|
||||
dependencies:
|
||||
is-glob "^4.0.1"
|
||||
|
||||
glob@^10.3.10, glob@^10.3.3:
|
||||
glob@^10.5.0:
|
||||
version "10.5.0"
|
||||
resolved "https://registry.yarnpkg.com/glob/-/glob-10.5.0.tgz#8ec0355919cd3338c28428a23d4f24ecc5fe738c"
|
||||
integrity sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==
|
||||
|
||||
Reference in New Issue
Block a user