Compare commits

...

30 Commits

Author SHA1 Message Date
natoromano 88be0c94dd (tests) fix tests 2026-02-24 17:39:36 +01:00
natoromano 00f28f4647 🖋️(translate): more fixes 2026-02-23 16:53:53 +01:00
natoromano 34a619a54f chore: small fixes 2026-02-23 16:34:59 +01:00
natoromano bd4b312bba (tools) add basic translate tool 2026-02-20 15:43:27 +01:00
Laurent Paoletti ba712af899 ♻️(back) refactor AIAgentService for readability and maintainability
Extract large _run_agent method into smaller focused methods
Add comprehensive module and method documentation
Minor fixes

Signed-off-by: Laurent Paoletti <lp@providenz.fr>
2026-02-19 23:31:28 +01:00
Manuel Raynaud e60c938d44 🐛(helm) reverse liveness and readiness for backend deployment
The liveness and readiness are reversed. The liveness was using the
heartbeat process that is cheking all django checks and the database
connection.

Signed-off-by: Laurent Paoletti <lp@providenz.fr>
2026-02-19 19:46:45 +01:00
Eléonore Voisin 5d895d15f9 (table) temporarily adjust array
temporarily adjust css table
2026-02-18 09:56:27 +01:00
Eléonore Voisin 96a2963adf (waffle) hide the waffle if not fr theme
show waffe only on fr theme
2026-02-16 18:20:49 +01:00
Laurent Paoletti 224e6f83fd ⬆️(back) update pydantic-ai
Signed-off-by: Laurent Paoletti <lp@providenz.fr>
2026-02-16 11:11:27 +01:00
Laurent Paoletti af6facf4a3 ️(front) optimize streaming markdown rendering with block splitting
Split streaming content into independent memoized blocks to avoid
re-rendering all markdown/katex/syntax-highlighting on each update.
Add some tests for split functions and components.

Signed-off-by: Laurent Paoletti <lp@providenz.fr>
2026-02-16 09:46:23 +01:00
natoromano 9d16460777 ⬆️ (fix): redo after rebase
(front) add e2e pasting an attachment from clipboard

add test e2e paste attachment
2026-02-13 11:53:17 +01:00
Laurent Paoletti 09dceb508d 🐛(front) fix math formulas and carousel translations
Signed-off-by: Laurent Paoletti <lp@providenz.fr>
2026-02-10 10:23:11 +01:00
Laurent Paoletti 63e0e6c383 💚(docker) vendor mime.types file i/o fetching from Apache
Signed-off-by: Laurent Paoletti <lp@providenz.fr>
2026-02-10 09:45:25 +01:00
providenz a919d9a63b 🌐(i18n) update translated strings
Update translated files with new translations

Signed-off-by: Laurent Paoletti <lp@providenz.fr>
2026-02-09 15:14:24 +01:00
Laurent Paoletti 9506df341a 🔖(patch) bump release to 0.0.13
- 💄(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

- 💄(darkmode) change color feedback butto
- 🏗️(back) migrate to uv
- ♻️(front) optimize syntax highlighting bundle size

-  🐛(back) Cast collection Ids to API expected types

Signed-off-by: Laurent Paoletti <lp@providenz.fr>
2026-02-09 15:10:10 +01:00
Laurent Paoletti c87c734e98 🐛(back) cast collection Ids to API expected types
Signed-off-by: Laurent Paoletti <lp@providenz.fr>
2026-02-05 14:56:38 +01:00
Eléonore Voisin 41ade65a49 💄(darkmode) change color feedback button
change the color of the feedback button & add a dark mode illustration
2026-02-05 10:41:58 +01:00
Quentin BEY fd372e1c56 🧱(helm) mount the LLM json config file in jobs
The backend might do some consistency checks for settings,
some of them are the proper LLM configuration...
2026-02-04 21:41:39 +01:00
Laurent Paoletti 3bbe7fb125 (back) Add adaptive pdf parser (text extraction /ocr)
Signed-off-by: Laurent Paoletti <lp@providenz.fr>
2026-02-04 16:01:06 +01:00
Eléonore Voisin 082937e107 💄(front) ui fix : update ui-kit - fix Marianne font
upgrade package ui-kit
add darkmode logo gouv
2026-02-04 11:57:59 +01:00
Laurent Paoletti 54d2d3e807 ⬆️(back) update django
Signed-off-by: Laurent Paoletti <lp@providenz.fr>
2026-02-03 23:19:13 +01:00
Quentin BEY 23964cbf12 ♻️(front) optimize syntax highlighting bundle size
Replace rehype-pretty-code with direct Shiki integration

Migrate syntax highlighting from rehype-pretty-code to @shikijs/rehype for
better bundle optimization. Create a centralized Shiki highlighter that
pre-loads only required languages.

Also remove unused dependencies: @ag-media/react-pdf-table,
@react-pdf/renderer, react-select.

Signed-off-by: Laurent Paoletti <lp@providenz.fr>
2026-02-03 21:30:42 +01:00
Eléonore Voisin ff9e8335f2 (front) add persistent darkmode
add persistent darkmode and change settings modal
2026-02-03 17:37:27 +01:00
Quentin BEY 8c62f5d933 🐛(find) allow the chat completion view w/o access token
Find requires access and refresh tokens, but the current
beahavior using Albert API does not. We need to be able to
still use the completion endpoint without any stored
token.
2026-02-03 10:20:56 +01:00
charles 88bdcc2e60 (backend) handle deleting temporary collections
I handle deleting document in temporary collection
for web_search_brave_with_document_backend
2026-02-02 22:27:26 +01:00
charles 1c573ad3a7 ♻️(backend) refactor document parsers
I refactor document parsing by introducing
AlbertParser and BaseParser
2026-02-02 22:27:26 +01:00
Quentin BEY 3106d5f25f (backend) enhance Find API integration with user sub and tag
I enhance Find API integration with user
access control and configuration options
2026-02-02 22:27:26 +01:00
charles 23fa1d6b9e (backend) implement FindRagBackend
We want to be able to use Find api in rag tools.
I add a new rag backend class to do so.
2026-02-02 22:27:26 +01:00
Eléonore Voisin 8ed72fb305 (front) fix hover button darkmode
fix buttons colors missing -> source item, feedback buttons
2026-02-02 14:18:58 +01:00
Eléonore Voisin c231e69871 (front) add ui kit with dark mode
Add UI kit and dark mode
Fix missing color variables
2026-02-02 11:00:31 +01:00
147 changed files with 23198 additions and 9727 deletions
+2
View File
@@ -9,9 +9,11 @@ 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,
+1 -1
View File
@@ -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 wget https://svn.apache.org/repos/asf/httpd/httpd/trunk/docs/conf/mime.types -O /etc/mime.types
sudo cp $GITHUB_WORKSPACE/docker/files/etc/mime.types /etc/mime.types
- name: Generate a MO file from strings extracted from the project
run: uv run python manage.py compilemessages
+3
View File
@@ -44,6 +44,9 @@ env.d/development/*
!env.d/development/*.dist
env.d/terraform
# Configuration
**/conversations/configuration/llm/dev.json
# npm
node_modules
+38 -3
View File
@@ -10,11 +10,44 @@ and this project adheres to
### Added
- 🧱(files) allow to use S3 storage without external access #849
- ✨(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
@@ -32,7 +65,7 @@ and this project adheres to
### Changed
- 📦️(front) update react
- ✨(chat) Generate and edit conversation title
- ✨(chat) generate and edit conversation title
### Fixed
@@ -42,6 +75,7 @@ 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
@@ -200,7 +234,8 @@ and this project adheres to
- 💄(chat) add code highlighting for LLM responses #67
[unreleased]: https://github.com/suitenumerique/conversations/compare/v0.0.12...main
[unreleased]: https://github.com/suitenumerique/conversations/compare/v0.0.13...main
[0.0.13]: https://github.com/suitenumerique/conversations/releases/v0.0.13
[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
View File
@@ -93,7 +93,7 @@ RUN apk add \
pango \
shared-mime-info
RUN wget https://svn.apache.org/repos/asf/httpd/httpd/trunk/docs/conf/mime.types -O /etc/mime.types
COPY ./docker/files/etc/mime.types /etc/mime.types
# Copy entrypoint
COPY ./docker/files/usr/local/bin/entrypoint /usr/local/bin/entrypoint
+11
View File
@@ -71,6 +71,9 @@ services:
- "host.docker.internal:host-gateway"
ports:
- "8071:8000"
networks:
- default
- lasuite
volumes:
- ./src/backend:/app
- ./data/static:/data/static
@@ -90,6 +93,9 @@ services:
image: nginx:1.25
ports:
- "8083:8083"
networks:
- default
- lasuite
volumes:
- ./docker/files/etc/nginx/conf.d:/etc/nginx/conf.d:ro
depends_on:
@@ -178,3 +184,8 @@ services:
kc_postgresql:
condition: service_healthy
restart: true
networks:
lasuite:
name: lasuite-network
driver: bridge
File diff suppressed because it is too large Load Diff
+3
View File
@@ -95,6 +95,9 @@ These are the environment variables you can set for the `conversations-backend`
| CACHES_KEY_PREFIX | The prefix used to every cache keys. | conversations |
| THEME_CUSTOMIZATION_FILE_PATH | full path to the file customizing the theme. An example is provided in src/backend/conversations/configuration/theme/default.json | BASE_DIR/conversations/configuration/theme/default.json |
| THEME_CUSTOMIZATION_CACHE_TIMEOUT | Cache duration for the customization settings | 86400 |
| FIND_API_KEY | API key of Find | |
| FIND_API_URL | URL of Find | `https://app-find/api` |
| FIND_API_TIMEOUT | Find API timeout | 30 |
## conversations-frontend image
+3 -3
View File
@@ -244,9 +244,9 @@ For Mistral AI models using the Etalab platform:
{
"models": [
{
"hrid": "mistral-large",
"model_name": "mistral-large-latest",
"human_readable_name": "Mistral Large (Etalab)",
"hrid": "mistral-medium",
"model_name": "mistral-medium-2508",
"human_readable_name": "Mistral Medium (Etalab)",
"provider_name": "mistral-etalab",
"profile": null,
"settings": {
+1
View File
@@ -357,6 +357,7 @@ The RAG backend performs semantic search to find the most relevant content:
rag_results = document_store.search(
query,
results_count=settings.BRAVE_RAG_WEB_SEARCH_CHUNK_NUMBER,
**kwargs, # Additional search parameters like session with access_token
)
```
@@ -0,0 +1,280 @@
"""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
logger = logging.getLogger(__name__)
class BaseParser:
"""Base class for document parsers."""
def parse_document(self, name: str, content_type: str, content: bytes) -> str:
"""
Parse the document and prepare it for the search operation.
This method should handle the logic to convert the document
into a format suitable for storage.
Args:
name (str): The name of the document.
content_type (str): The MIME type of the document (e.g., "application/pdf").
content (bytes): The content of the document as a bytes stream.
Returns:
str: The document content in Markdown format.
"""
raise NotImplementedError("Must be implemented in subclass.")
class AlbertParser(BaseParser):
"""Document parser using Albert API for PDFs and DocumentConverter for other formats."""
endpoint = urljoin(settings.ALBERT_API_URL, "/v1/parse-beta")
def parse_pdf_document(self, name: str, content_type: str, content: bytes) -> str:
"""Parse PDF document using Albert API."""
response = requests.post(
self.endpoint,
headers={
"Authorization": f"Bearer {settings.ALBERT_API_KEY}",
},
files={
"file": (name, content, content_type),
"output_format": (None, "markdown"),
},
timeout=settings.ALBERT_API_PARSE_TIMEOUT,
)
response.raise_for_status()
return "\n\n".join(
document_page["content"] for document_page in response.json().get("data", [])
)
def parse_document(self, name: str, content_type: str, content: bytes) -> str:
"""Parse document 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
)
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.markitdown import DocumentConverter
from chat.agent_rag.document_rag_backends.base_rag_backend import BaseRagBackend
logger = logging.getLogger(__name__)
@@ -26,9 +26,6 @@ class AlbertRagBackend(BaseRagBackend): # pylint: disable=too-many-instance-att
It provides methods to:
- Create a collection for the search operation.
- Parse documents and convert them to Markdown format:
+ Handle PDF parsing using the Albert API.
+ Use the DocumentConverter (markitdown) for other formats.
- Store parsed documents in the Albert collection.
- Perform a search operation using the Albert API.
"""
@@ -46,10 +43,15 @@ class AlbertRagBackend(BaseRagBackend): # pylint: disable=too-many-instance-att
}
self._collections_endpoint = urljoin(self._base_url, "/v1/collections")
self._documents_endpoint = urljoin(self._base_url, "/v1/documents")
self._pdf_parser_endpoint = urljoin(self._base_url, "/v1/parse-beta")
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)
def create_collection(self, name: str, description: Optional[str] = None) -> str:
"""
@@ -91,7 +93,7 @@ class AlbertRagBackend(BaseRagBackend): # pylint: disable=too-many-instance-att
self.collection_id = str(response.json()["id"])
return self.collection_id
def delete_collection(self) -> None:
def delete_collection(self, **kwargs) -> None:
"""
Delete the current collection
"""
@@ -102,7 +104,7 @@ class AlbertRagBackend(BaseRagBackend): # pylint: disable=too-many-instance-att
)
response.raise_for_status()
async def adelete_collection(self) -> None:
async def adelete_collection(self, **kwargs) -> None:
"""
Asynchronously delete the current collection
"""
@@ -114,59 +116,7 @@ class AlbertRagBackend(BaseRagBackend): # pylint: disable=too-many-instance-att
)
response.raise_for_status()
def parse_pdf_document(self, name: str, content_type: str, content: BytesIO) -> str:
"""
Parse the PDF document content and return the text content.
This method should handle the logic to convert the PDF into
a format suitable for the Albert API.
"""
response = requests.post(
self._pdf_parser_endpoint,
headers=self._headers,
files={
"file": (
name,
content,
content_type,
), # Use the name as the filename in the request
"output_format": (None, "markdown"), # Specify the output format as Markdown,
},
timeout=settings.ALBERT_API_PARSE_TIMEOUT,
)
response.raise_for_status()
return "\n\n".join(
document_page["content"] for document_page in response.json().get("data", [])
)
def parse_document(self, name: str, content_type: str, content: BytesIO):
"""
Parse the document and prepare it for the search operation.
This method should handle the logic to convert the document
into a format suitable for the Albert API.
Args:
name (str): The name of the document.
content_type (str): The MIME type of the document (e.g., "application/pdf").
content (BytesIO): The content of the document as a BytesIO stream.
Returns:
str: The document content in Markdown format.
"""
# Implement the parsing logic here
if content_type == "application/pdf":
# Handle PDF parsing
markdown_content = self.parse_pdf_document(
name=name, content_type=content_type, content=content
)
else:
markdown_content = DocumentConverter().convert_raw(
name=name, content_type=content_type, content=content
)
return markdown_content
def store_document(self, name: str, content: str) -> None:
def store_document(self, name: str, content: str, **kwargs) -> None:
"""
Store the document content in the Albert collection.
This method should handle the logic to send the document content to the Albert API.
@@ -174,6 +124,7 @@ class AlbertRagBackend(BaseRagBackend): # pylint: disable=too-many-instance-att
Args:
name (str): The name of the document.
content (str): The content of the document in Markdown format.
**kwargs: Additional arguments.
"""
response = requests.post(
urljoin(self._base_url, self._documents_endpoint),
@@ -188,7 +139,7 @@ class AlbertRagBackend(BaseRagBackend): # pylint: disable=too-many-instance-att
logger.debug(response.json())
response.raise_for_status()
async def astore_document(self, name: str, content: str) -> None:
async def astore_document(self, name: str, content: str, **kwargs) -> None:
"""
Store the document content in the Albert collection.
This method should handle the logic to send the document content to the Albert API.
@@ -196,6 +147,7 @@ class AlbertRagBackend(BaseRagBackend): # pylint: disable=too-many-instance-att
Args:
name (str): The name of the document.
content (str): The content of the document in Markdown format.
**kwargs: Additional arguments.
"""
async with httpx.AsyncClient(timeout=settings.ALBERT_API_TIMEOUT) as client:
response = await client.post(
@@ -213,13 +165,14 @@ class AlbertRagBackend(BaseRagBackend): # pylint: disable=too-many-instance-att
logger.debug(response.json())
response.raise_for_status()
def search(self, query, results_count: int = 4) -> RAGWebResults:
def search(self, query: str, results_count: int = 4, **kwargs) -> RAGWebResults:
"""
Perform a search using the Albert API based on the provided query.
Args:
query (str): The search query.
results_count (int): The number of results to return.
**kwargs: Additional arguments.
Returns:
RAGWebResults: The search results.
@@ -256,13 +209,14 @@ class AlbertRagBackend(BaseRagBackend): # pylint: disable=too-many-instance-att
),
)
async def asearch(self, query, results_count: int = 4) -> RAGWebResults:
async def asearch(self, query, results_count: int = 4, **kwargs) -> RAGWebResults:
"""
Perform an asynchronous search using the Albert API based on the provided query.
Args:
query (str): The search query.
results_count (int): The number of results to return.
**kwargs: Additional arguments.
Returns:
RAGWebResults: The search results.
@@ -1,18 +1,19 @@
"""Implementation of the Albert API for RAG document search."""
import logging
from abc import ABC, abstractmethod
from contextlib import asynccontextmanager, contextmanager
from io import BytesIO
from typing import List, Optional
from asgiref.sync import sync_to_async
from chat.agent_rag.constants import RAGWebResults
from chat.agent_rag.document_converter.parser import BaseParser
logger = logging.getLogger(__name__)
class BaseRagBackend:
class BaseRagBackend(ABC):
"""Base class for RAG backends."""
def __init__(
@@ -38,6 +39,12 @@ class BaseRagBackend:
self.collection_id = collection_id
self.read_only_collection_id = read_only_collection_id or []
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]:
"""
@@ -53,13 +60,17 @@ class BaseRagBackend:
collection_ids = []
if self.collection_id:
collection_ids.append(int(self.collection_id))
collection_ids.append(self.cast_collection_id(self.collection_id))
if self.read_only_collection_id:
collection_ids.extend(
[int(collection_id) for collection_id in self.read_only_collection_id]
[
self.cast_collection_id(collection_id)
for collection_id in self.read_only_collection_id
]
)
return collection_ids
@abstractmethod
def create_collection(self, name: str, description: Optional[str] = None) -> str:
"""
Create a temporary collection for the search operation.
@@ -74,7 +85,7 @@ class BaseRagBackend:
"""
return await sync_to_async(self.create_collection)(name=name, description=description)
def parse_document(self, name: str, content_type: str, content: BytesIO):
def parse_document(self, name: str, content_type: str, content: bytes):
"""
Parse the document and prepare it for the search operation.
This method should handle the logic to convert the document
@@ -83,14 +94,15 @@ class BaseRagBackend:
Args:
name (str): The name of the document.
content_type (str): The MIME type of the document (e.g., "application/pdf").
content (BytesIO): The content of the document as a BytesIO stream.
content (bytes): The content of the document as a bytes stream.
Returns:
str: The document content in Markdown format.
"""
raise NotImplementedError("Must be implemented in subclass.")
return self.parser.parse_document(name, content_type, content)
def store_document(self, name: str, content: str) -> None:
@abstractmethod
def store_document(self, name: str, content: str, **kwargs) -> None:
"""
Store the document content in the collection.
This method should handle the logic to send the document content to the API.
@@ -98,10 +110,11 @@ class BaseRagBackend:
Args:
name (str): The name of the document.
content (str): The content of the document in Markdown format.
**kwargs: Additional arguments. ex: "user_sub" for access control.
"""
raise NotImplementedError("Must be implemented in subclass.")
async def astore_document(self, name: str, content: str) -> None:
async def astore_document(self, name: str, content: str, **kwargs) -> None:
"""
Store the document content in the collection.
This method should handle the logic to send the document content to the API.
@@ -109,50 +122,66 @@ class BaseRagBackend:
Args:
name (str): The name of the document.
content (str): The content of the document in Markdown format.
**kwargs: Additional arguments. ex: "user_sub" for access control.
"""
return await sync_to_async(self.store_document)(name=name, content=content)
return await sync_to_async(self.store_document)(name=name, content=content, **kwargs)
def parse_and_store_document(self, name: str, content_type: str, content: BytesIO) -> str:
def parse_and_store_document(
self, name: str, content_type: str, content: bytes, **kwargs
) -> str:
"""
Parse the document and store it in the Albert collection.
Args:
name (str): The name of the document.
content_type (str): The MIME type of the document (e.g., "application/pdf").
content (BytesIO): The content of the document as a BytesIO stream.
content (bytes): The content of the document as a bytes stream.
**kwargs: Additional arguments. ex: "user_sub" for access control.
"""
if not self.collection_id:
raise RuntimeError("The RAG backend requires collection_id")
document_content = self.parse_document(name, content_type, content)
self.store_document(name, document_content)
self.store_document(name, document_content, **kwargs)
return document_content
def delete_collection(self) -> None:
@abstractmethod
def delete_collection(self, **kwargs) -> None:
"""
Delete the collection.
This method should handle the logic to delete the collection from the backend.
"""
raise NotImplementedError("Must be implemented in subclass.")
async def adelete_collection(self) -> None:
async def adelete_collection(self, **kwargs) -> None:
"""
Delete the collection.
This method should handle the logic to delete the collection from the backend.
"""
return await sync_to_async(self.delete_collection)()
return await sync_to_async(self.delete_collection)(**kwargs)
def search(self, query, results_count: int = 4) -> RAGWebResults:
@abstractmethod
def search(self, query: str, results_count: int = 4, **kwargs) -> RAGWebResults:
"""
Search the collection for the given query.
Args:
query: The search query string.
results_count: Number of results to return.
**kwargs: Additional arguments. ex: 'session' for OIDC authentication.
"""
raise NotImplementedError("Must be implemented in subclass.")
async def asearch(self, query, results_count: int = 4) -> RAGWebResults:
async def asearch(self, query: str, results_count: int = 4, **kwargs) -> RAGWebResults:
"""
Search the collection for the given query.
Search the collection for the given query asynchronously.
Args:
query: The search query string.
results_count: Number of results to return.
**kwargs: Additional arguments. ex: 'session' for OIDC authentication.
"""
return await sync_to_async(self.search)(query=query, results_count=results_count)
return await sync_to_async(self.search)(query=query, results_count=results_count, **kwargs)
@classmethod
@contextmanager
@@ -168,7 +197,9 @@ class BaseRagBackend:
@classmethod
@asynccontextmanager
async def temporary_collection_async(cls, name: str, description: Optional[str] = None):
async def temporary_collection_async(
cls, name: str, description: Optional[str] = None, **kwargs
):
"""Context manager for RAG backend with temporary collections."""
backend = cls()
@@ -176,4 +207,4 @@ class BaseRagBackend:
try:
yield backend
finally:
await backend.adelete_collection()
await backend.adelete_collection(**kwargs)
@@ -0,0 +1,160 @@
"""Implementation of the Find API for RAG document search."""
import logging
import uuid
from typing import List, Optional
from urllib.parse import urljoin
from uuid import uuid4
from django.conf import settings
from django.utils import timezone
import requests
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
from utils.oidc import with_fresh_access_token
logger = logging.getLogger(__name__)
SUPPORTED_LANGUAGE_CODES = ["en", "fr", "de", "nl"]
class FindRagBackend(BaseRagBackend):
"""
This class is a placeholder for the Find API implementation.
It is designed to be used with the RAG (Retrieval-Augmented Generation) document search system.
It provides methods to:
- Store parsed documents in the Find index.
- Perform a search operation using the Find API.
"""
def __init__(
self,
collection_id: Optional[str] = None,
read_only_collection_id: Optional[List[str]] = None,
):
# Initialize any necessary parameters or configurations here
super().__init__(collection_id, read_only_collection_id)
self.api_key = settings.FIND_API_KEY
self.search_endpoint = "api/v1.0/documents/search/"
self.indexing_endpoint = "api/v1.0/documents/index/"
self.deleting_endpoint = "api/v1.0/documents/delete/"
self.parser = AlbertParser() # Find Rag relies on Albert parser
def create_collection(self, name: str, description: Optional[str] = None) -> str:
"""
init collection_id
"""
self.collection_id = self.collection_id or str(uuid.uuid4())
return self.collection_id
@with_fresh_access_token
def delete_collection(self, **kwargs) -> None:
"""
Delete the current collection
"""
response = requests.post(
urljoin(settings.FIND_API_URL, self.deleting_endpoint),
headers={"Authorization": f"Bearer {kwargs['session'].get('oidc_access_token')}"},
json={"tags": [f"collection-{self.collection_id}"], "service": "conversations"},
timeout=settings.FIND_API_TIMEOUT,
)
response.raise_for_status()
def store_document(self, name: str, content: str, **kwargs) -> None:
"""
index document in Find
Args:
name (str): The name of the document.
content (str): The content of the document in Markdown format.
user_sub (str): The user subject identifier for access control.
"""
logger.debug("index document '%s' in Find", name)
user_sub = kwargs.get("user_sub")
if not user_sub:
raise ValueError("user_sub is required to store document in FindRagBackend")
response = requests.post(
urljoin(settings.FIND_API_URL, self.indexing_endpoint),
headers={"Authorization": f"Bearer {self.api_key}"},
json={
"id": str(uuid4()),
"title": str(name) or "",
"depth": 0,
"path": str(name) or "",
"numchild": 0,
"content": content or "",
"created_at": timezone.now().isoformat(),
"updated_at": timezone.now().isoformat(),
"tags": [f"collection-{self.collection_id}"],
"size": len(content.encode("utf-8")),
"users": [user_sub],
"groups": [],
"reach": "authenticated",
"is_active": True,
},
timeout=settings.FIND_API_TIMEOUT,
)
response.raise_for_status()
@with_fresh_access_token
def search(self, query: str, results_count: int = 4, **kwargs) -> RAGWebResults:
"""
Perform a search using the Find API.
Uses the user's OIDC token from the request session.
Args:
query: The search query.
results_count: Number of results to return.
**kwargs: Additional arguments. Expected: 'session' containing OIDC tokens,
Returns:
RAGWebResults: The search results.
"""
logger.debug("search documents in Find with query '%s'", query)
response = requests.post(
urljoin(settings.FIND_API_URL, self.search_endpoint),
headers={"Authorization": f"Bearer {kwargs['session'].get('oidc_access_token')}"},
json={
"q": query or "*",
"tags": [
f"collection-{collection_id}" for collection_id in self.get_all_collection_ids()
],
"k": results_count,
},
timeout=settings.FIND_API_TIMEOUT,
)
response.raise_for_status()
return RAGWebResults(
data=[
RAGWebResult(
url=get_language_value(result["_source"], "title"),
content=get_language_value(result["_source"], "content"),
score=result["_score"],
)
for result in response.json()
],
usage=RAGWebUsage(
prompt_tokens=0,
completion_tokens=0,
),
)
def get_language_value(source, language_field):
"""
extract the value of the language field with the correct language_code extension.
"title" and "content" have extensions like "title.en" or "title.fr".
get_language_value will return the value regardless of the extension.
"""
for language_code in SUPPORTED_LANGUAGE_CODES:
if f"{language_field}.{language_code}" in source:
return source[f"{language_field}.{language_code}"]
raise ValueError(f"No '{language_field}' field with any supported language code in object")
+27
View File
@@ -0,0 +1,27 @@
"""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
@@ -0,0 +1,161 @@
%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
@@ -0,0 +1,355 @@
%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
@@ -0,0 +1,310 @@
"""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()
+68 -1
View File
@@ -2,19 +2,26 @@
import logging
from contextlib import ExitStack, contextmanager
from importlib import reload
from unittest.mock import patch
from django.urls import clear_url_caches, set_urlconf
from django.utils import formats, timezone
import pytest
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_promt_date")
@pytest.fixture(name="today_prompt_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)
@@ -85,8 +92,68 @@ 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"
b"\xa7V\xbd\xfa\x00\x00\x00\x00IEND\xaeB`\x82"
)
@pytest.fixture(name="oidc_refresh_token_enabled")
def fixture_oidc_refresh_token_enabled(settings):
"""
Fixture to enable OIDC refresh token storage during the test.
This is not a nice fixture, as it forces reloading views and URL configurations.
Maybe there is a better way to do this, because even the conditional_refresh_oidc_token
function is not nice, but I don't want it to be lazy either.
"""
__initial_value = bool(settings.OIDC_STORE_REFRESH_TOKEN)
settings.OIDC_STORE_REFRESH_TOKEN = True
# force view reload
reload(chat.views)
reload(core.urls)
reload(conversations.urls)
clear_url_caches()
set_urlconf(None)
yield settings
settings.OIDC_STORE_REFRESH_TOKEN = __initial_value
# force view reload
reload(chat.views)
reload(core.urls)
reload(conversations.urls)
clear_url_caches()
set_urlconf(None)
@@ -0,0 +1,318 @@
"""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)
@@ -1,14 +1,19 @@
"""Tests for the Brave web search tool."""
# pylint: disable=too-many-lines
from typing import Sequence
# pylint: disable=too-many-lines
from unittest.mock import AsyncMock, MagicMock, Mock, patch
from urllib.parse import parse_qs
from django.contrib.auth.base_user import AbstractBaseUser
from django.contrib.sessions.backends.cache import SessionStore
import httpx
import pytest
import respx
from pydantic_ai import ModelRetry, RunContext, RunUsage
from pydantic_ai._run_context import RunContextAgentDepsT
from chat.tools.exceptions import ModelCannotRetry
from chat.tools.web_search_brave import (
@@ -38,9 +43,6 @@ def brave_settings(settings):
settings.BRAVE_SEARCH_EXTRA_SNIPPETS = True
settings.BRAVE_SUMMARIZATION_ENABLED = False
settings.BRAVE_CACHE_TTL = 3600
settings.RAG_DOCUMENT_SEARCH_BACKEND = (
"chat.agent_rag.document_rag_backends.albert_rag_backend.AlbertRagBackend"
)
settings.BRAVE_RAG_WEB_SEARCH_CHUNK_NUMBER = 5
@@ -48,6 +50,13 @@ def brave_settings(settings):
def fixture_mocked_context():
"""Fixture for a mocked RunContext."""
mock_ctx = Mock(spec=RunContext)
mock_ctx.deps = Mock(spec=RunContextAgentDepsT)
user = Mock(spec=AbstractBaseUser)
user.sub = Mock(spec=Sequence)
mock_ctx.deps.user = user
session = SessionStore()
session["oidc_access_token"] = "mocked-access-token"
mock_ctx.deps.session = session
mock_ctx.usage = RunUsage(input_tokens=0, output_tokens=0)
mock_ctx.max_retries = 2
mock_ctx.retries = {}
@@ -1011,7 +1020,12 @@ async def test_web_search_brave_with_document_backend_rag_search_params(mocked_c
await web_search_brave_with_document_backend(mocked_context, "test query")
# Verify RAG search was called with correct parameters
mock_document_store.asearch.assert_called_once_with("test query", results_count=5)
mock_document_store.asearch.assert_called_once_with(
query="test query",
results_count=5,
session=mocked_context.deps.session,
user_sub=mocked_context.deps.user.sub,
)
@pytest.mark.asyncio
@@ -3,6 +3,7 @@
import json
import logging
import time
from unittest.mock import ANY, patch
from django.utils import timezone
@@ -189,6 +190,7 @@ 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"],
@@ -197,15 +199,29 @@ 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": {"finish_reason": "stop"},
"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",
},
"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,
@@ -316,6 +332,7 @@ def test_post_conversation_data_protocol_triggers_keepalives(
"Answer in english."
),
"kind": "request",
"metadata": None,
"parts": [
{
"content": ["Hello"],
@@ -324,15 +341,29 @@ 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": {"finish_reason": "stop"},
"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",
},
"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,
@@ -437,6 +468,7 @@ 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"],
@@ -445,15 +477,29 @@ 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": {"finish_reason": "stop"},
"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",
},
"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,
@@ -623,6 +669,7 @@ 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": [
@@ -643,15 +690,29 @@ 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": {"finish_reason": "stop"},
"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",
},
"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,
@@ -789,6 +850,7 @@ 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?"],
@@ -797,23 +859,31 @@ 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"},
"provider_details": {
"finish_reason": "tool_calls",
"timestamp": "2025-07-25T10:36:35.297675Z",
},
"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,
@@ -833,6 +903,7 @@ 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"},
@@ -844,17 +915,29 @@ 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"}
{
"content": "The current weather in Paris is nice",
"id": None,
"part_kind": "text",
"provider_details": None,
"provider_name": None,
}
],
"provider_details": {"finish_reason": "stop"},
"provider_details": {
"finish_reason": "stop",
"timestamp": "2025-07-25T10:36:35.297675Z",
},
"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,
@@ -991,6 +1074,7 @@ 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?"],
@@ -999,23 +1083,31 @@ 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"},
"provider_details": {
"finish_reason": "tool_calls",
"timestamp": "2025-07-25T10:36:35.297675Z",
},
"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,
@@ -1035,6 +1127,7 @@ 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.",
@@ -1045,17 +1138,29 @@ 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"}
{
"content": "I cannot give you an answer to that.",
"id": None,
"part_kind": "text",
"provider_details": None,
"provider_name": None,
}
],
"provider_details": {"finish_reason": "stop"},
"provider_details": {
"finish_reason": "stop",
"timestamp": "2025-07-25T10:36:35.297675Z",
},
"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,
@@ -1301,6 +1406,7 @@ 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?"],
@@ -1309,10 +1415,12 @@ 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": [
{
@@ -1320,12 +1428,18 @@ 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"},
"provider_details": {
"finish_reason": "stop",
"timestamp": "2025-09-22T14:13:49Z",
},
"provider_name": "openai",
"provider_response_id": "chatcmpl-92c413bb5a45426299335d0621324654",
"timestamp": "2025-09-22T14:13:49Z",
"provider_url": "https://www.external-ai-service.com",
"timestamp": "2025-07-25T10:36:35.297675Z",
"usage": {
"cache_audio_read_tokens": 0,
"cache_read_tokens": 0,
@@ -1441,6 +1555,7 @@ 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"],
@@ -1449,15 +1564,29 @@ 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": {"finish_reason": "stop"},
"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",
},
"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,
@@ -1581,6 +1710,7 @@ 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"],
@@ -1589,15 +1719,29 @@ 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": {"finish_reason": "stop"},
"parts": [
{
"content": "Hello there",
"id": None,
"part_kind": "text",
"provider_details": None,
"provider_name": None,
}
],
"provider_details": {
"finish_reason": "stop",
"timestamp": ANY,
},
"provider_name": "openai",
"provider_response_id": "chatcmpl-1234567890",
"provider_url": "https://www.external-ai-service.com/",
"timestamp": ANY,
"usage": {
"cache_audio_read_tokens": 0,
@@ -1612,3 +1756,106 @@ async def test_post_conversation_async_triggers_keepalive(
"run_id": _run_id,
},
]
def test_post_conversation_oidc_refresh_enabled_unrefreshed( # pylint: disable=unused-argument
api_client, oidc_refresh_token_enabled
):
"""Test posting messages to a conversation without fresh access token should be forbidden."""
chat_conversation = ChatConversationFactory(owner__language="en-us")
url = f"/api/v1.0/chats/{chat_conversation.pk}/conversation/?protocol=data"
data = {
"messages": [
{
"id": "yuPoOuBkKA4FnKvk",
"role": "user",
"parts": [{"text": "Hello", "type": "text"}],
"content": "Hello",
"createdAt": "2025-07-03T15:22:17.105Z",
}
]
}
api_client.force_login(
chat_conversation.owner, backend="core.authentication.backends.OIDCAuthenticationBackend"
)
response = api_client.post(url, data, format="json")
assert response.status_code == status.HTTP_401_UNAUTHORIZED
@freeze_time("2025-07-25T10:36:35.297675Z")
@respx.mock
def test_post_conversation_oidc_refresh_enabled( # pylint: disable=unused-argument
api_client, mock_openai_stream, oidc_refresh_token_enabled
):
"""Test posting messages to a conversation using the 'data' protocol."""
chat_conversation = ChatConversationFactory(owner__language="en-us")
url = f"/api/v1.0/chats/{chat_conversation.pk}/conversation/?protocol=data"
data = {
"messages": [
{
"id": "yuPoOuBkKA4FnKvk",
"role": "user",
"parts": [{"text": "Hello", "type": "text"}],
"content": "Hello",
"createdAt": "2025-07-03T15:22:17.105Z",
}
]
}
api_client.force_login(
chat_conversation.owner, backend="core.authentication.backends.OIDCAuthenticationBackend"
)
session = api_client.session
session["oidc_id_token_expiration"] = time.time() + 3600 # valid for 1 hour
session["oidc_token_expiration"] = session["oidc_id_token_expiration"] # ...
session.save()
response = api_client.post(url, data, format="json")
assert response.status_code == status.HTTP_200_OK
assert response.get("Content-Type") == "text/event-stream"
assert response.get("x-vercel-ai-data-stream") == "v1"
assert response.streaming
# Wait for the streaming content to be fully received
response_content = b"".join(response.streaming_content).decode("utf-8")
# Replace UUIDs with placeholders for assertion
response_content = replace_uuids_with_placeholder(response_content)
assert response_content == (
'0:"Hello"\n'
'0:" there"\n'
'f:{"messageId":"<mocked_uuid>"}\n'
'd:{"finishReason":"stop","usage":{"promptTokens":0,"completionTokens":0}}\n'
)
assert mock_openai_stream.called
# ensure instructions are merged as a system prompt
last_request_payload = json.loads(respx.calls.last.request.content)
assert last_request_payload["messages"][0] == {
"content": (
"You are a helpful test assistant :)\n\nToday is Friday 25/07/2025.\n\n"
"Answer in english."
),
"role": "system",
}
chat_conversation.refresh_from_db()
assert chat_conversation.ui_messages == [
{
"content": "Hello",
"createdAt": "2025-07-03T15:22:17.105Z",
"id": "yuPoOuBkKA4FnKvk",
"parts": [{"text": "Hello", "type": "text"}],
"role": "user",
}
]
assert len(chat_conversation.messages) == 2
assert len(chat_conversation.pydantic_messages) == 2
@@ -8,6 +8,7 @@ import logging
from io import BytesIO
from unittest import mock
from django.contrib.sessions.backends.cache import SessionStore
from django.utils import formats, timezone
import httpx
@@ -41,28 +42,49 @@ from chat.tests.utils import replace_uuids_with_placeholder
pytestmark = pytest.mark.django_db(transaction=True)
@pytest.fixture(autouse=True)
def ai_settings(settings):
@pytest.fixture(
autouse=True,
params=[
"chat.agent_rag.document_rag_backends.find_rag_backend.FindRagBackend",
"chat.agent_rag.document_rag_backends.albert_rag_backend.AlbertRagBackend",
],
)
def ai_settings(request, settings):
"""Fixture to set AI service URLs for testing."""
settings.AI_BASE_URL = "https://www.external-ai-service.com/"
settings.AI_API_KEY = "test-api-key"
settings.AI_MODEL = "test-model"
settings.AI_AGENT_INSTRUCTIONS = "You are a helpful test assistant :)"
# Enable Albert API for document search
settings.RAG_DOCUMENT_SEARCH_BACKEND = (
"chat.agent_rag.document_rag_backends.albert_rag_backend.AlbertRagBackend"
)
settings.ALBERT_API_URL = "https://albert.api.etalab.gouv.fr"
settings.ALBERT_API_KEY = "albert-api-key"
# enable on rag document search tool
settings.RAG_DOCUMENT_SEARCH_BACKEND = request.param
settings.RAG_WEB_SEARCH_PROMPT_UPDATE = (
"Based on the following document contents:\n\n{search_results}\n\n"
"Please answer the user's question: {user_prompt}"
)
settings.AI_BASE_URL = "https://www.external-ai-service.com/"
settings.AI_API_KEY = "test-api-key"
settings.AI_MODEL = "test-model"
settings.AI_AGENT_INSTRUCTIONS = "You are a helpful test assistant :)"
# Albert API settings
settings.ALBERT_API_URL = "https://albert.api.etalab.gouv.fr"
settings.ALBERT_API_KEY = "albert-api-key"
# Find API settings
settings.FIND_API_URL = "https://find.api.example.com"
settings.FIND_API_KEY = "find-api-key"
return settings
@pytest.fixture(autouse=True)
def mock_refresh_access_token():
"""Mock refresh_access_token to bypass token refresh in tests."""
with mock.patch("utils.oidc.refresh_access_token") as mocked_refresh_access_token:
session = SessionStore()
session["oidc_access_token"] = "mocked-access-token"
mocked_refresh_access_token.return_value = session
yield mocked_refresh_access_token
@pytest.fixture(name="sample_pdf_content")
def fixture_sample_pdf_content():
"""Create a dummy PDF content as BytesIO."""
@@ -81,10 +103,18 @@ def fixture_sample_pdf_content():
return BytesIO(pdf_data)
@pytest.fixture(name="mock_albert_api")
def fixture_mock_albert_api():
@pytest.fixture(name="mock_document_api")
def fixture_mock_document_api():
"""Fixture to mock the Albert API endpoints."""
# Mock collection creation
document_name = "sample.pdf"
document_content = "This is the content of the PDF."
prompt_tokens = 10
completion_tokens = 20
search_method = "semantic"
search_score = 0.9
responses.post(
"https://albert.api.etalab.gouv.fr/v1/collections",
json={"id": "123", "name": "test-collection"},
@@ -101,7 +131,7 @@ def fixture_mock_albert_api():
"metadata": {"document_name": "sample.pdf"},
}
],
"usage": {"prompt_tokens": 10, "completion_tokens": 20},
"usage": {"prompt_tokens": prompt_tokens, "completion_tokens": completion_tokens},
},
status=status.HTTP_200_OK,
)
@@ -119,20 +149,42 @@ def fixture_mock_albert_api():
json={
"data": [
{
"method": "semantic",
"method": search_method,
"chunk": {
"id": 123,
"content": "This is the content of the PDF.",
"metadata": {"document_name": "sample.pdf"},
"content": document_content,
"metadata": {"document_name": document_name},
},
"score": 0.9,
"score": search_score,
}
],
"usage": {"prompt_tokens": 10, "completion_tokens": 20},
"usage": {"prompt_tokens": prompt_tokens, "completion_tokens": completion_tokens},
},
status=status.HTTP_200_OK,
)
# Mock document indexing (Find API)
responses.post(
"https://find.api.example.com/api/v1.0/documents/index/",
json={"id": "456", "status": "indexed"},
status=status.HTTP_200_OK,
)
# Mock document search (Find API)
responses.post(
"https://find.api.example.com/api/v1.0/documents/search/",
json=[
{
"_source": {
"title.fr": document_name,
"content.fr": document_content,
},
"_score": search_score,
}
],
status=status.HTTP_200_OK,
)
@pytest.fixture(name="mock_summarization_agent")
def fixture_mock_summarization_agent():
@@ -219,9 +271,9 @@ def fixture_mock_openai_stream():
def test_post_conversation_with_document_upload(
# pylint: disable=too-many-arguments,too-many-positional-arguments
api_client,
mock_albert_api, # pylint: disable=unused-argument
mock_document_api, # pylint: disable=unused-argument
sample_pdf_content,
today_promt_date,
today_prompt_date,
mock_ai_agent_service,
):
"""
@@ -357,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_promt_date}\n\n"
f"{today_prompt_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, "
@@ -372,6 +424,7 @@ 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?"],
@@ -380,10 +433,12 @@ 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": [
{
@@ -392,11 +447,14 @@ 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,
@@ -413,7 +471,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_promt_date}\n\n"
f"{today_prompt_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, "
@@ -429,6 +487,7 @@ def test_post_conversation_with_document_upload(
"available via the internal store."
),
"kind": "request",
"metadata": None,
"parts": [
{
"content": [
@@ -446,21 +505,26 @@ 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,
@@ -548,9 +612,9 @@ def test_post_conversation_with_document_upload_feature_disabled(
@freeze_time()
def test_post_conversation_with_document_upload_summarize( # pylint: disable=too-many-arguments,too-many-positional-arguments # noqa: PLR0913
api_client,
mock_albert_api, # pylint: disable=unused-argument
mock_document_api, # pylint: disable=unused-argument
sample_pdf_content,
today_promt_date,
today_prompt_date,
mock_ai_agent_service,
mock_summarization_agent, # pylint: disable=unused-argument
):
@@ -687,7 +751,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_promt_date}\n\n"
f"{today_prompt_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, "
@@ -703,6 +767,7 @@ 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."],
@@ -711,10 +776,12 @@ 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": [
{
@@ -723,11 +790,14 @@ 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,
@@ -744,7 +814,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_promt_date}\n\n"
f"{today_prompt_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, "
@@ -760,6 +830,7 @@ 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.",
@@ -771,17 +842,26 @@ 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"}
{
"content": "The document discusses various topics.",
"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,
@@ -37,11 +37,20 @@ from chat.tests.utils import replace_uuids_with_placeholder
pytestmark = pytest.mark.django_db(transaction=True)
@pytest.fixture(autouse=True)
def ai_settings(settings):
@pytest.fixture(
autouse=True,
params=[
"chat.agent_rag.document_rag_backends.find_rag_backend.FindRagBackend",
"chat.agent_rag.document_rag_backends.albert_rag_backend.AlbertRagBackend",
],
)
def ai_settings(request, settings):
"""Fixture to set AI service URLs for testing."""
settings.RAG_DOCUMENT_SEARCH_BACKEND = request.param
settings.AI_BASE_URL = "https://www.external-ai-service.com/"
settings.AI_API_KEY = "test-api-key"
settings.FIND_API_URL = "https://app-find"
settings.FIND_API_KEY = "find-api-key"
settings.AI_MODEL = "test-model"
settings.AI_AGENT_INSTRUCTIONS = "You are a helpful test assistant :)"
return settings
@@ -64,12 +73,13 @@ def test_post_conversation_with_local_pdf_document_url(
# pylint: disable=too-many-arguments,too-many-positional-arguments
api_client,
sample_document_content,
today_promt_date,
today_prompt_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"},
@@ -85,6 +95,10 @@ def test_post_conversation_with_local_pdf_document_url(
json={"id": "document_id", "object": "document"},
status=200,
)
responses.post(
"https://app-find/api/v1.0/documents/index/",
status=200,
)
chat_conversation = ChatConversationFactory(owner__language="en-us")
api_client.force_authenticate(user=chat_conversation.owner)
@@ -127,7 +141,7 @@ def test_post_conversation_with_local_pdf_document_url(
],
instructions=(
"You are a helpful test assistant :)\n\n"
f"{today_promt_date}\n\n"
f"{today_prompt_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 "
@@ -143,6 +157,7 @@ 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."
@@ -216,7 +231,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_promt_date}\n\n"
f"{today_prompt_date}\n\n"
"Answer in english.\n"
"\n"
"Use document_search_rag ONLY to retrieve specific passages "
@@ -236,6 +251,7 @@ 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": [
@@ -246,21 +262,26 @@ 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,
@@ -532,6 +553,7 @@ 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(
@@ -545,7 +567,7 @@ def test_post_conversation_with_local_document_url_in_history( # pylint: disabl
identifier="sample.pdf",
),
],
timestamp=timezone.now(),
timestamp=timestamp_now,
),
],
instructions="You are a helpful test assistant :)\n\n"
@@ -557,7 +579,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=timezone.now(),
timestamp=timestamp_now,
run_id=messages[1].run_id,
),
ModelRequest(
@@ -566,9 +588,10 @@ def test_post_conversation_with_local_document_url_in_history( # pylint: disabl
content=[
"Give more details about this document.",
],
timestamp=timezone.now(),
timestamp=timestamp_now,
)
],
timestamp=timestamp_now,
instructions="You are a helpful test assistant :)\n\n"
"Today is Saturday 18/10/2025.\n\n"
"Answer in english.",
@@ -725,6 +748,7 @@ 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": [
{
@@ -734,21 +758,26 @@ 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,
@@ -778,7 +807,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_promt_date,
today_prompt_date,
mock_ai_agent_service,
file_name,
content_type,
@@ -801,6 +830,10 @@ def test_post_conversation_with_local_not_pdf_document_url(
json={"id": "document_id", "object": "document"},
status=200,
)
responses.post(
"https://app-find/api/v1.0/documents/index/",
status=200,
)
chat_conversation = ChatConversationFactory(owner__language="en-us")
api_client.force_authenticate(user=chat_conversation.owner)
@@ -836,6 +869,8 @@ 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=[
@@ -844,12 +879,13 @@ 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=timezone.now(),
timestamp=timestamp_now,
),
],
timestamp=timestamp_now,
instructions=(
"You are a helpful test assistant :)\n\n"
f"{today_promt_date}\n\n"
f"{today_prompt_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, "
@@ -940,7 +976,7 @@ def test_post_conversation_with_local_not_pdf_document_url(
{
"instructions": (
"You are a helpful test assistant :)\n\n"
f"{today_promt_date}\n\n"
f"{today_prompt_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, "
@@ -957,6 +993,7 @@ def test_post_conversation_with_local_not_pdf_document_url(
"consider them already available via the internal store."
),
"kind": "request",
"metadata": None,
"parts": [
{
"content": [
@@ -967,21 +1004,26 @@ 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,
@@ -29,6 +29,10 @@ 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."""
@@ -41,17 +45,9 @@ def ai_settings(settings):
return settings
@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 = [
def build__history_conversation_ui_messages(history_timestamp):
"""Build ui messages list for fixtures."""
return [
UIMessage(
id="prev-user-msg-1",
createdAt=history_timestamp,
@@ -120,94 +116,205 @@ def history_conversation_fixture():
),
]
@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.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.messages = build__history_conversation_ui_messages(history_timestamp)
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": [
@@ -1028,6 +1135,7 @@ def history_conversation_with_tool_fixture():
},
{
"kind": "response",
"metadata": None,
"model_name": "test-model",
"parts": [
{
@@ -1069,6 +1177,7 @@ def history_conversation_with_tool_fixture():
},
{
"kind": "response",
"metadata": None,
"model_name": "test-model",
"parts": [
{
@@ -1102,6 +1211,7 @@ def history_conversation_with_tool_fixture():
},
{
"kind": "response",
"metadata": None,
"model_name": "test-model",
"parts": [
{
@@ -1148,6 +1258,7 @@ def history_conversation_with_tool_fixture():
},
{
"kind": "response",
"metadata": None,
"model_name": "test-model",
"parts": [
{
@@ -1384,6 +1495,7 @@ 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?"],
@@ -1392,24 +1504,32 @@ 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"},
"provider_details": {
"finish_reason": "tool_calls",
"timestamp": "2025-07-25T10:36:35.297675Z",
},
"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,
@@ -1429,6 +1549,7 @@ 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"},
@@ -1440,18 +1561,30 @@ 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"}
{
"content": "The current weather in Paris is nice",
"id": None,
"part_kind": "text",
"provider_details": None,
"provider_name": None,
}
],
"provider_details": {"finish_reason": "stop"},
"provider_details": {
"finish_reason": "stop",
"timestamp": "2025-07-25T10:36:35.297675Z",
},
"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,
@@ -92,6 +92,7 @@ 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=[
@@ -110,6 +111,7 @@ 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."
@@ -181,6 +183,7 @@ 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": [
@@ -199,17 +202,26 @@ 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"}
{
"content": "This is an image of 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": "2025-10-18T20:48:20.286204Z",
"usage": {
"cache_audio_read_tokens": 0,
@@ -229,7 +241,7 @@ def test_post_conversation_with_local_image_url(
@freeze_time()
def test_post_conversation_with_local_image_wrong_url(
api_client,
today_promt_date,
today_prompt_date,
mock_ai_agent_service,
):
"""
@@ -275,7 +287,8 @@ def test_post_conversation_with_local_image_wrong_url(
timestamp=timezone.now(),
),
],
instructions=f"You are a helpful test assistant :)\n\n{today_promt_date}"
timestamp=timezone.now(),
instructions=f"You are a helpful test assistant :)\n\n{today_prompt_date}"
"\n\nAnswer in english.",
run_id=messages[0].run_id,
)
@@ -314,7 +327,7 @@ def test_post_conversation_with_local_image_wrong_url(
@freeze_time()
def test_post_conversation_with_remote_image_url(
api_client,
today_promt_date,
today_prompt_date,
mock_ai_agent_service,
):
"""
@@ -361,8 +374,9 @@ def test_post_conversation_with_remote_image_url(
),
],
instructions="You are a helpful test assistant :)\n\n"
f"{today_promt_date}\n\nAnswer in english.",
f"{today_prompt_date}\n\nAnswer in english.",
run_id=messages[0].run_id,
timestamp=timezone.now(),
)
]
yield "This is an image of a single pixel."
@@ -432,7 +446,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_promt_date,
today_prompt_date,
mock_ai_agent_service,
):
"""
@@ -475,7 +489,7 @@ def test_post_conversation_with_local_image_url_in_history(
],
pydantic_messages=[
{
"instructions": f"You are a helpful test assistant :)\n\n{today_promt_date}"
"instructions": f"You are a helpful test assistant :)\n\n{today_prompt_date}"
"\n\nAnswer in english.",
"kind": "request",
"parts": [
@@ -547,6 +561,8 @@ 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=[
@@ -559,11 +575,11 @@ def test_post_conversation_with_local_image_url_in_history(
identifier="sample.png",
),
],
timestamp=timezone.now(),
timestamp=timestamp_now,
),
],
instructions="You are a helpful test assistant :)\n\n"
f"{today_promt_date}\n\nAnswer in english.",
f"{today_prompt_date}\n\nAnswer in english.",
),
ModelResponse(
parts=[TextPart(content="This is an image of a single pixel.")],
@@ -577,12 +593,13 @@ def test_post_conversation_with_local_image_url_in_history(
content=[
"Give more details about this image.",
],
timestamp=timezone.now(),
timestamp=timestamp_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."
@@ -681,7 +698,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_promt_date}"
"instructions": f"You are a helpful test assistant :)\n\n{today_prompt_date}"
"\n\nAnswer in english.",
"kind": "request",
"parts": [
@@ -728,6 +745,7 @@ 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."],
@@ -736,21 +754,26 @@ 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,
@@ -26,7 +26,7 @@ def add_document_rag_search_tool(agent: Agent) -> None:
document_store = document_store_backend(ctx.deps.conversation.collection_id)
rag_results = document_store.search(query)
rag_results = document_store.search(query, session=ctx.deps.session)
ctx.usage += RunUsage(
input_tokens=rag_results.usage.prompt_tokens,
+1 -9
View File
@@ -4,7 +4,6 @@ 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
@@ -14,18 +13,11 @@ 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
from chat.tools.utils import last_model_retry_soft_fail, read_document_content
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 = (
@@ -0,0 +1,136 @@
"""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
+10
View File
@@ -4,6 +4,9 @@ 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
@@ -48,3 +51,10 @@ 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")
+12 -5
View File
@@ -127,7 +127,7 @@ async def _extract_and_summarize_snippets_async(query: str, url: str) -> List[st
return []
async def _fetch_and_store_async(url: str, document_store) -> None:
async def _fetch_and_store_async(url: str, document_store, **kwargs) -> None:
"""Fetch, extract and store text content from the URL in the document store."""
try:
@@ -136,7 +136,7 @@ async def _fetch_and_store_async(url: str, document_store) -> None:
logger.debug("Fetched document: %s", document)
if document:
await document_store.astore_document(url, document)
await document_store.astore_document(url, document, **kwargs)
except DocumentFetchError as e:
logger.warning("Failed to fetch and store %s: %s", url, e)
# Continue with other documents
@@ -307,19 +307,26 @@ async def web_search_brave_with_document_backend(ctx: RunContext, query: str) ->
temp_collection_name = f"tmp-{uuid.uuid4()}"
try:
async with document_store_backend.temporary_collection_async(
temp_collection_name
temp_collection_name, session=ctx.deps.session
) as document_store:
# Fetch and store all documents concurrently
tasks = [
_fetch_and_store_async(result["url"], document_store)
_fetch_and_store_async(
result["url"],
document_store,
user_sub=ctx.deps.user.sub,
session=ctx.deps.session,
)
for result in raw_search_results
]
await asyncio.gather(*tasks, return_exceptions=True)
# Perform RAG search
rag_results = await document_store.asearch(
query,
query=query,
results_count=settings.BRAVE_RAG_WEB_SEARCH_CHUNK_NUMBER,
session=ctx.deps.session,
user_sub=ctx.deps.user.sub,
)
logger.info("RAG search returned: %s", rag_results)
+17
View File
@@ -8,11 +8,13 @@ from django.conf import settings
from django.core.cache import cache
from django.core.files.storage import default_storage
from django.http import Http404, StreamingHttpResponse
from django.utils.decorators import method_decorator
import langfuse
import magic
import posthog
from lasuite.malware_detection import malware_detection
from lasuite.oidc_login.decorators import refresh_oidc_access_token
from rest_framework import decorators, filters, mixins, permissions, status, viewsets
from rest_framework.exceptions import MethodNotAllowed, PermissionDenied, ValidationError
from rest_framework.response import Response
@@ -35,6 +37,19 @@ from chat.serializers import ChatConversationRequestSerializer
logger = logging.getLogger(__name__)
def conditional_refresh_oidc_token(func):
"""
Conditionally apply refresh_oidc_access_token decorator.
The decorator is only applied if OIDC_STORE_REFRESH_TOKEN is True, meaning
we can actually refresh something. Broader settings checks are done in settings.py.
"""
if settings.OIDC_STORE_REFRESH_TOKEN:
return method_decorator(refresh_oidc_access_token)(func)
return func
class ChatConversationFilter(filters.BaseFilterBackend):
"""Filter conversation."""
@@ -126,6 +141,7 @@ class ChatViewSet( # pylint: disable=too-many-ancestors, abstract-method
self.permission_classes = []
return super().get_permissions()
@conditional_refresh_oidc_token
@decorators.action(
methods=["post"],
detail=True,
@@ -177,6 +193,7 @@ class ChatViewSet( # pylint: disable=too-many-ancestors, abstract-method
ai_service = AIAgentService(
conversation=conversation,
user=self.request.user,
session=request.session,
model_hrid=model_hrid,
language=(
self.request.user.language
+1 -1
View File
@@ -22,7 +22,7 @@ def no_http_requests(monkeypatch):
Credits: https://blog.jerrycodes.com/no-http-requests/
"""
allowed_hosts = {"localhost", "minio", "minio:9000"}
allowed_hosts = {"localhost", "127.0.0.1", "minio", "minio:9000"}
original_urlopen = HTTPConnectionPool.urlopen
def urlopen_mock(self, method, url, *args, **kwargs):
+103
View File
@@ -743,6 +743,11 @@ 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",
@@ -808,6 +813,51 @@ 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(
(
@@ -833,6 +883,13 @@ 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
@@ -867,6 +924,23 @@ USER QUESTION:
environ_prefix=None,
)
# Find
FIND_API_KEY = values.Value(
None,
environ_name="FIND_API_KEY",
environ_prefix=None,
)
FIND_API_URL = values.Value(
"https://app-find/api",
environ_name="FIND_API_URL",
environ_prefix=None,
)
FIND_API_TIMEOUT = values.PositiveIntegerValue(
default=30, # seconds
environ_name="FIND_API_TIMEOUT",
environ_prefix=None,
)
# Logging
# We want to make it easy to log to console but by default we log production
# to Sentry and don't want to log to console.
@@ -1079,6 +1153,35 @@ USER QUESTION:
"Please set FILE_BACKEND_URL to a valid URL for backend temporary file access."
)
# Find configuration
if (
cls.RAG_DOCUMENT_SEARCH_BACKEND
== "chat.agent_rag.document_rag_backends.find_rag_backend.FindRagBackend"
and not all(
(
cls.FIND_API_KEY,
cls.FIND_API_URL,
cls.OIDC_STORE_ACCESS_TOKEN,
cls.OIDC_STORE_REFRESH_TOKEN,
)
)
):
raise ValueError(
f"{cls.RAG_DOCUMENT_SEARCH_BACKEND} requires FIND_API_KEY, FIND_API_URL, "
"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-01-27 15:38\n"
"PO-Revision-Date: 2026-02-09 13:05\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-01-27 15:38\n"
"PO-Revision-Date: 2026-02-09 13:05\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-01-27 15:38\n"
"PO-Revision-Date: 2026-02-09 13:05\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-01-27 15:38\n"
"PO-Revision-Date: 2026-02-09 13:05\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-01-27 15:38\n"
"PO-Revision-Date: 2026-02-09 13:05\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-01-27 15:38\n"
"PO-Revision-Date: 2026-02-09 13:05\n"
"Last-Translator: \n"
"Language-Team: Ukrainian\n"
"Language: uk_UA\n"
+11 -5
View File
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "conversations"
version = "0.0.12"
version = "0.0.13"
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.9",
"django==5.2.11",
"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.17.0",
"pydantic-ai-slim[openai,mistral,mcp,evals,logfire]==1.56.0",
"psycopg[binary]==3.2.12",
"PyJWT==2.10.1",
"python-magic==0.4.27",
@@ -67,6 +67,7 @@ dependencies = [
"trafilatura==2.0.0",
"uvicorn==0.38.0",
"whitenoise==6.11.0",
"pypdf>=6.6.2",
]
[project.urls]
@@ -107,6 +108,11 @@ 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 = [
@@ -146,8 +152,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]
+54
View File
@@ -0,0 +1,54 @@
"""Utility functions for OIDC token management."""
from functools import wraps
from django.conf import settings
import requests
from lasuite.oidc_login.backends import get_oidc_refresh_token, store_tokens
from rest_framework.exceptions import AuthenticationFailed
def refresh_access_token(session):
"""Refresh the OIDC access token using the refresh token."""
refresh_token = get_oidc_refresh_token(session)
if not refresh_token:
raise AuthenticationFailed({"error": "Refresh token is missing from session"})
response = requests.post(
settings.OIDC_OP_TOKEN_ENDPOINT,
data={
"grant_type": "refresh_token",
"client_id": settings.OIDC_RP_CLIENT_ID,
"client_secret": settings.OIDC_RP_CLIENT_SECRET,
"refresh_token": refresh_token,
},
timeout=5,
)
response.raise_for_status()
token_info = response.json()
store_tokens(
session,
access_token=token_info.get("access_token"),
id_token=None,
refresh_token=token_info.get("refresh_token"),
)
return session
def with_fresh_access_token(func):
"""
Decorator to handle OIDC token refresh and extraction.
Expects 'session' in kwargs and update it with the fresh token.
"""
@wraps(func)
def wrapper(*args, **kwargs):
session = kwargs.pop("session", None)
if session is None:
raise AuthenticationFailed({"error": "Session is required but not provided"})
refreshed_session = refresh_access_token(session)
return func(*args, session=refreshed_session, **kwargs)
return wrapper
+86 -47
View File
@@ -7,6 +7,9 @@ resolution-markers = [
"sys_platform != 'emscripten' and sys_platform != 'win32'",
]
[manifest]
overrides = [{ name = "cryptography", specifier = ">=46.0.5" }]
[[package]]
name = "amqp"
version = "5.3.1"
@@ -391,7 +394,7 @@ wheels = [
[[package]]
name = "conversations"
version = "0.0.12"
version = "0.0.13"
source = { editable = "." }
dependencies = [
{ name = "beautifulsoup4" },
@@ -428,6 +431,7 @@ dependencies = [
{ name = "pydantic" },
{ name = "pydantic-ai-slim", extra = ["evals", "logfire", "mcp", "mistral", "openai"] },
{ name = "pyjwt" },
{ name = "pypdf" },
{ name = "python-magic" },
{ name = "redis" },
{ name = "requests" },
@@ -470,7 +474,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.9" },
{ name = "django", specifier = "==5.2.11" },
{ name = "django-configurations", specifier = "==2.5.1" },
{ name = "django-cors-headers", specifier = "==4.9.0" },
{ name = "django-countries", specifier = "==8.1.0" },
@@ -504,12 +508,13 @@ 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.17.0" },
{ name = "pydantic-ai-slim", extras = ["openai", "mistral", "mcp", "evals", "logfire"], specifier = "==1.56.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" },
@@ -582,43 +587,41 @@ wheels = [
[[package]]
name = "cryptography"
version = "46.0.3"
version = "46.0.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
]
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" }
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" }
wheels = [
{ 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" },
{ 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" },
]
[[package]]
@@ -695,16 +698,16 @@ wheels = [
[[package]]
name = "django"
version = "5.2.9"
version = "5.2.11"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "asgiref" },
{ name = "sqlparse" },
{ name = "tzdata", marker = "sys_platform == 'win32'" },
]
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" }
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" }
wheels = [
{ 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" },
{ 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" },
]
[[package]]
@@ -2161,7 +2164,7 @@ wheels = [
[[package]]
name = "pydantic-ai-slim"
version = "1.17.0"
version = "1.56.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "genai-prices" },
@@ -2172,9 +2175,9 @@ dependencies = [
{ name = "pydantic-graph" },
{ name = "typing-inspection" },
]
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" }
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" }
wheels = [
{ 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" },
{ 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" },
]
[package.optional-dependencies]
@@ -2192,6 +2195,7 @@ mistral = [
]
openai = [
{ name = "openai" },
{ name = "tiktoken" },
]
[[package]]
@@ -2221,7 +2225,7 @@ wheels = [
[[package]]
name = "pydantic-evals"
version = "1.17.0"
version = "1.56.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
@@ -2231,14 +2235,14 @@ dependencies = [
{ name = "pyyaml" },
{ name = "rich" },
]
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" }
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" }
wheels = [
{ 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" },
{ 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" },
]
[[package]]
name = "pydantic-graph"
version = "1.17.0"
version = "1.56.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "httpx" },
@@ -2246,9 +2250,9 @@ dependencies = [
{ name = "pydantic" },
{ name = "typing-inspection" },
]
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" }
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" }
wheels = [
{ 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" },
{ 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" },
]
[[package]]
@@ -2362,6 +2366,15 @@ 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"
@@ -2877,6 +2890,32 @@ 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"
File diff suppressed because it is too large Load Diff
+7 -9
View File
@@ -1,6 +1,6 @@
{
"name": "app-conversations",
"version": "0.0.12",
"version": "0.0.13",
"private": true,
"scripts": {
"dev": "next dev",
@@ -16,17 +16,17 @@
"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",
"@emoji-mart/react": "1.1.1",
"@fontsource/material-icons": "5.2.5",
"@gouvfr-lasuite/cunningham-tokens": "^3.1.0",
"@gouvfr-lasuite/integration": "1.0.3",
"@gouvfr-lasuite/ui-kit": "0.7.0",
"@openfun/cunningham-react": "3.1.0",
"@react-pdf/renderer": "4.3.0",
"@gouvfr-lasuite/ui-kit": "0.18.7",
"@openfun/cunningham-react": "4.0.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 +36,7 @@
"i18next": "25.2.1",
"i18next-browser-languagedetector": "8.1.0",
"idb": "8.0.3",
"lodash": "4.17.21",
"lodash": "4.17.23",
"lottie-react": "^2.4.1",
"luxon": "3.6.1",
"micromark-extension-llm-math": "3.1.1-20250610",
@@ -48,12 +48,10 @@
"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.13.0",
"shiki": "^3.21.0",
"styled-components": "6.1.18",
"use-debounce": "10.0.4",
"zod": "^3.25.67",
@@ -1,8 +1,8 @@
<svg viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M43.9612 12.1078C43.9612 10.7446 43.9612 10.063 43.8197 9.44065C43.5582 8.29032 42.9454 7.24979 42.0663 6.46318C41.5907 6.03759 40.9946 5.707 39.8025 5.04582L36.7561 3.35621C35.6298 2.73158 35.0667 2.41926 34.4857 2.24352C33.4121 1.91883 32.2665 1.91883 31.1929 2.24352C30.6119 2.41926 30.0488 2.73158 28.9226 3.35621L27.6662 4.05301C26.1413 4.89875 25.3789 5.32161 25.0296 5.81385C24.3763 6.73448 24.3769 7.96763 25.0311 8.88764C25.3808 9.37954 26.1437 9.80168 27.6694 10.646L28.6864 11.2088C29.8804 11.8695 30.4774 12.1998 30.9538 12.6255C31.8343 13.4121 32.4482 14.4534 32.7101 15.6047C32.8519 16.2276 32.8519 16.9099 32.8519 18.2745V19.4391C32.8519 21.0927 32.8519 21.9195 33.1003 22.4525C33.5646 23.4487 34.595 24.0556 35.6913 23.9787C36.278 23.9376 37.0012 23.5368 38.4474 22.7351L39.8008 21.9849C40.9934 21.3238 41.5897 20.9933 42.0655 20.5677C42.945 19.7811 43.558 18.7404 43.8196 17.5898C43.9612 16.9673 43.9612 16.2855 43.9612 14.922V12.1078Z" fill="#3E5DE7"/>
<path d="M10.0512 32.3143C8.87068 32.9959 8.2804 33.3367 7.81215 33.7704C6.94669 34.572 6.35194 35.623 6.11028 36.7776C5.97953 37.4023 5.99126 38.0838 6.01473 39.4468L6.07469 42.9299C6.09685 44.2175 6.10794 44.8614 6.24627 45.4524C6.50185 46.5445 7.07465 47.5366 7.89263 48.304C8.33535 48.7194 8.88738 49.0509 9.99144 49.7139L11.2231 50.4535C12.7179 51.3513 13.4654 51.8001 14.0663 51.8565C15.1902 51.962 16.2579 51.3449 16.7275 50.3183C16.9787 49.7695 16.9628 48.8978 16.9311 47.1543L16.91 45.9921C16.8852 44.6277 16.8728 43.9456 17.0032 43.3202C17.2442 42.1643 17.839 41.1121 18.7051 40.3095C19.1737 39.8753 19.7646 39.5342 20.9464 38.8519L21.9549 38.2696C23.387 37.4428 24.103 37.0294 24.4404 36.5477C25.071 35.6475 25.0814 34.4518 24.4667 33.5407C24.1377 33.0532 23.429 32.6273 22.0116 31.7757L20.6853 30.9787C19.5165 30.2765 18.9321 29.9253 18.3256 29.726C17.2046 29.3577 15.9968 29.3471 14.8696 29.6959C14.2597 29.8845 13.6693 30.2254 12.4884 30.9072L10.0512 32.3143Z" fill="#3E5DE7"/>
<path d="M21.7175 51.8922C21.7175 53.2554 21.7175 53.937 21.8589 54.5594C22.1204 55.7097 22.7332 56.7502 23.6123 57.5368C24.088 57.9624 24.684 58.293 25.8762 58.9542L28.9226 60.6438C30.0488 61.2684 30.6119 61.5807 31.1929 61.7565C32.2665 62.0812 33.4121 62.0812 34.4857 61.7565C35.0667 61.5807 35.6298 61.2684 36.7561 60.6438L38.0124 59.947C39.5373 59.1012 40.2997 58.6784 40.649 58.1862C41.3023 57.2655 41.3017 56.0324 40.6476 55.1124C40.2978 54.6205 39.5349 54.1983 38.0093 53.354L36.9922 52.7912C35.7982 52.1305 35.2012 51.8002 34.7248 51.3745C33.8443 50.5879 33.2305 49.5466 32.9685 48.3953C32.8268 47.7724 32.8268 47.0901 32.8268 45.7255V44.5609C32.8268 42.9073 32.8268 42.0805 32.5783 41.5475C32.114 40.5513 31.0837 39.9444 29.9873 40.0213C29.4006 40.0624 28.6775 40.4632 27.2312 41.2649L25.8778 42.0151C24.6852 42.6761 24.089 43.0067 23.6131 43.4323C22.7336 44.2189 22.1206 45.2596 21.859 46.4102C21.7175 47.0327 21.7175 47.7145 21.7175 49.078V51.8922Z" fill="#3E5DE7"/>
<path d="M44.5055 51.5779C45.6861 52.2595 46.2764 52.6003 46.8861 52.789C48.0131 53.1377 49.2206 53.1273 50.3414 52.7592C50.9477 52.5601 51.5321 52.2092 52.7007 51.5074L55.6872 49.7139C56.7912 49.0509 57.3433 48.7194 57.786 48.304C58.604 47.5366 59.1768 46.5445 59.4324 45.4524C59.5707 44.8614 59.5818 44.2175 59.6039 42.9299L59.6287 41.4935C59.6587 39.75 59.6737 38.8783 59.422 38.3297C58.9514 37.3036 57.8832 36.6875 56.7593 36.794C56.1584 36.851 55.4114 37.3006 53.9174 38.1997L52.9215 38.7991C51.7523 39.5028 51.1677 39.8546 50.5609 40.0544C49.4394 40.4236 48.2307 40.4346 47.1027 40.0858C46.4924 39.8971 45.9015 39.5559 44.7197 38.8736L43.7111 38.2913C42.2791 37.4645 41.563 37.0511 40.9772 36.9998C39.8823 36.9038 38.8415 37.4926 38.3599 38.4806C38.1022 39.0092 38.0878 39.8359 38.0589 41.4892L38.0319 43.0364C38.0081 44.3997 37.9962 45.0814 38.1269 45.7062C38.3684 46.8612 38.9631 47.9125 39.8287 48.7143C40.2971 49.1481 40.8875 49.489 42.0684 50.1708L44.5055 51.5779Z" fill="#3E5DE7"/>
<path d="M55.6274 31.6857C56.8079 31.0041 57.3982 30.6633 57.8665 30.2296C58.7319 29.428 59.3267 28.377 59.5683 27.2224C59.6991 26.5977 59.6874 25.9162 59.6639 24.5532L59.6039 21.0701C59.5818 19.7825 59.5707 19.1386 59.4324 18.5476C59.1768 17.4555 58.604 16.4634 57.786 15.696C57.3433 15.2806 56.7912 14.9491 55.6872 14.2861L54.4556 13.5465C52.9607 12.6488 52.2133 12.1999 51.6123 12.1435C50.4884 12.038 49.4207 12.6551 48.9511 13.6817C48.7 14.2305 48.7158 15.1022 48.7475 16.8457L48.7686 18.0079C48.7934 19.3723 48.8058 20.0544 48.6754 20.6798C48.4344 21.8357 47.8396 22.8879 46.9735 23.6905C46.5049 24.1247 45.914 24.4658 44.7322 25.1481L43.7237 25.7304C42.2916 26.5572 41.5756 26.9706 41.2382 27.4523C40.6076 28.3525 40.5972 29.5482 41.2119 30.4593C41.5409 30.9468 42.2496 31.3727 43.667 32.2243L44.9934 33.0213C46.1622 33.7235 46.7466 34.0747 47.353 34.274C48.474 34.6423 49.6818 34.6529 50.809 34.3041C51.4189 34.1155 52.0094 33.7746 53.1902 33.0928L55.6274 31.6857Z" fill="#3E5DE7"/>
<path d="M21.173 12.4222C19.9925 11.7406 19.4022 11.3998 18.7924 11.2112C17.6655 10.8625 16.458 10.8728 15.3372 11.2409C14.7308 11.44 14.1465 11.7909 12.9778 12.4927L9.99135 14.2862C8.8873 14.9492 8.33527 15.2807 7.89255 15.6961C7.07457 16.4635 6.50177 17.4556 6.24619 18.5477C6.10786 19.1387 6.09677 19.7826 6.07461 21.0702L6.04988 22.5066C6.01987 24.2501 6.00486 25.1218 6.2565 25.6704C6.72713 26.6965 7.79538 27.3126 8.91921 27.2061C9.5201 27.1491 10.2671 26.6995 11.7611 25.8004L12.757 25.201C13.9262 24.4973 14.5108 24.1455 15.1176 23.9457C16.2392 23.5765 17.4478 23.5655 18.5759 23.9143C19.1862 24.103 19.7771 24.4442 20.9589 25.1265L21.9674 25.7088C23.3995 26.5356 24.1155 26.949 24.7014 27.0003C25.7962 27.0963 26.837 26.5075 27.3186 25.5195C27.5764 24.9909 27.5908 24.1642 27.6196 22.5109L27.6466 20.9638C27.6704 19.6004 27.6823 18.9187 27.5517 18.2939C27.3102 17.1389 26.7154 16.0876 25.8498 15.2858C25.3815 14.852 24.791 14.5111 23.6102 13.8293L21.173 12.4222Z" fill="#D3343F"/>
<path d="M43.9612 12.1078C43.9612 10.7446 43.9612 10.063 43.8197 9.44065C43.5582 8.29032 42.9454 7.24979 42.0663 6.46318C41.5907 6.03759 40.9946 5.707 39.8025 5.04582L36.7561 3.35621C35.6298 2.73158 35.0667 2.41926 34.4857 2.24352C33.4121 1.91883 32.2665 1.91883 31.1929 2.24352C30.6119 2.41926 30.0488 2.73158 28.9226 3.35621L27.6662 4.05301C26.1413 4.89875 25.3789 5.32161 25.0296 5.81385C24.3763 6.73448 24.3769 7.96763 25.0311 8.88764C25.3808 9.37954 26.1437 9.80168 27.6694 10.646L28.6864 11.2088C29.8804 11.8695 30.4774 12.1998 30.9538 12.6255C31.8343 13.4121 32.4482 14.4534 32.7101 15.6047C32.8519 16.2276 32.8519 16.9099 32.8519 18.2745V19.4391C32.8519 21.0927 32.8519 21.9195 33.1003 22.4525C33.5646 23.4487 34.595 24.0556 35.6913 23.9787C36.278 23.9376 37.0012 23.5368 38.4474 22.7351L39.8008 21.9849C40.9934 21.3238 41.5897 20.9933 42.0655 20.5677C42.945 19.7811 43.558 18.7404 43.8196 17.5898C43.9612 16.9673 43.9612 16.2855 43.9612 14.922V12.1078Z" fill="var(--c--globals--colors--logo-1)"/>
<path d="M10.0512 32.3143C8.87068 32.9959 8.2804 33.3367 7.81215 33.7704C6.94669 34.572 6.35194 35.623 6.11028 36.7776C5.97953 37.4023 5.99126 38.0838 6.01473 39.4468L6.07469 42.9299C6.09685 44.2175 6.10794 44.8614 6.24627 45.4524C6.50185 46.5445 7.07465 47.5366 7.89263 48.304C8.33535 48.7194 8.88738 49.0509 9.99144 49.7139L11.2231 50.4535C12.7179 51.3513 13.4654 51.8001 14.0663 51.8565C15.1902 51.962 16.2579 51.3449 16.7275 50.3183C16.9787 49.7695 16.9628 48.8978 16.9311 47.1543L16.91 45.9921C16.8852 44.6277 16.8728 43.9456 17.0032 43.3202C17.2442 42.1643 17.839 41.1121 18.7051 40.3095C19.1737 39.8753 19.7646 39.5342 20.9464 38.8519L21.9549 38.2696C23.387 37.4428 24.103 37.0294 24.4404 36.5477C25.071 35.6475 25.0814 34.4518 24.4667 33.5407C24.1377 33.0532 23.429 32.6273 22.0116 31.7757L20.6853 30.9787C19.5165 30.2765 18.9321 29.9253 18.3256 29.726C17.2046 29.3577 15.9968 29.3471 14.8696 29.6959C14.2597 29.8845 13.6693 30.2254 12.4884 30.9072L10.0512 32.3143Z" fill="var(--c--globals--colors--logo-1)"/>
<path d="M21.7175 51.8922C21.7175 53.2554 21.7175 53.937 21.8589 54.5594C22.1204 55.7097 22.7332 56.7502 23.6123 57.5368C24.088 57.9624 24.684 58.293 25.8762 58.9542L28.9226 60.6438C30.0488 61.2684 30.6119 61.5807 31.1929 61.7565C32.2665 62.0812 33.4121 62.0812 34.4857 61.7565C35.0667 61.5807 35.6298 61.2684 36.7561 60.6438L38.0124 59.947C39.5373 59.1012 40.2997 58.6784 40.649 58.1862C41.3023 57.2655 41.3017 56.0324 40.6476 55.1124C40.2978 54.6205 39.5349 54.1983 38.0093 53.354L36.9922 52.7912C35.7982 52.1305 35.2012 51.8002 34.7248 51.3745C33.8443 50.5879 33.2305 49.5466 32.9685 48.3953C32.8268 47.7724 32.8268 47.0901 32.8268 45.7255V44.5609C32.8268 42.9073 32.8268 42.0805 32.5783 41.5475C32.114 40.5513 31.0837 39.9444 29.9873 40.0213C29.4006 40.0624 28.6775 40.4632 27.2312 41.2649L25.8778 42.0151C24.6852 42.6761 24.089 43.0067 23.6131 43.4323C22.7336 44.2189 22.1206 45.2596 21.859 46.4102C21.7175 47.0327 21.7175 47.7145 21.7175 49.078V51.8922Z" fill="var(--c--globals--colors--logo-1)"/>
<path d="M44.5055 51.5779C45.6861 52.2595 46.2764 52.6003 46.8861 52.789C48.0131 53.1377 49.2206 53.1273 50.3414 52.7592C50.9477 52.5601 51.5321 52.2092 52.7007 51.5074L55.6872 49.7139C56.7912 49.0509 57.3433 48.7194 57.786 48.304C58.604 47.5366 59.1768 46.5445 59.4324 45.4524C59.5707 44.8614 59.5818 44.2175 59.6039 42.9299L59.6287 41.4935C59.6587 39.75 59.6737 38.8783 59.422 38.3297C58.9514 37.3036 57.8832 36.6875 56.7593 36.794C56.1584 36.851 55.4114 37.3006 53.9174 38.1997L52.9215 38.7991C51.7523 39.5028 51.1677 39.8546 50.5609 40.0544C49.4394 40.4236 48.2307 40.4346 47.1027 40.0858C46.4924 39.8971 45.9015 39.5559 44.7197 38.8736L43.7111 38.2913C42.2791 37.4645 41.563 37.0511 40.9772 36.9998C39.8823 36.9038 38.8415 37.4926 38.3599 38.4806C38.1022 39.0092 38.0878 39.8359 38.0589 41.4892L38.0319 43.0364C38.0081 44.3997 37.9962 45.0814 38.1269 45.7062C38.3684 46.8612 38.9631 47.9125 39.8287 48.7143C40.2971 49.1481 40.8875 49.489 42.0684 50.1708L44.5055 51.5779Z" fill="var(--c--globals--colors--logo-1)"/>
<path d="M55.6274 31.6857C56.8079 31.0041 57.3982 30.6633 57.8665 30.2296C58.7319 29.428 59.3267 28.377 59.5683 27.2224C59.6991 26.5977 59.6874 25.9162 59.6639 24.5532L59.6039 21.0701C59.5818 19.7825 59.5707 19.1386 59.4324 18.5476C59.1768 17.4555 58.604 16.4634 57.786 15.696C57.3433 15.2806 56.7912 14.9491 55.6872 14.2861L54.4556 13.5465C52.9607 12.6488 52.2133 12.1999 51.6123 12.1435C50.4884 12.038 49.4207 12.6551 48.9511 13.6817C48.7 14.2305 48.7158 15.1022 48.7475 16.8457L48.7686 18.0079C48.7934 19.3723 48.8058 20.0544 48.6754 20.6798C48.4344 21.8357 47.8396 22.8879 46.9735 23.6905C46.5049 24.1247 45.914 24.4658 44.7322 25.1481L43.7237 25.7304C42.2916 26.5572 41.5756 26.9706 41.2382 27.4523C40.6076 28.3525 40.5972 29.5482 41.2119 30.4593C41.5409 30.9468 42.2496 31.3727 43.667 32.2243L44.9934 33.0213C46.1622 33.7235 46.7466 34.0747 47.353 34.274C48.474 34.6423 49.6818 34.6529 50.809 34.3041C51.4189 34.1155 52.0094 33.7746 53.1902 33.0928L55.6274 31.6857Z" fill="var(--c--globals--colors--logo-1)"/>
<path d="M21.173 12.4222C19.9925 11.7406 19.4022 11.3998 18.7924 11.2112C17.6655 10.8625 16.458 10.8728 15.3372 11.2409C14.7308 11.44 14.1465 11.7909 12.9778 12.4927L9.99135 14.2862C8.8873 14.9492 8.33527 15.2807 7.89255 15.6961C7.07457 16.4635 6.50177 17.4556 6.24619 18.5477C6.10786 19.1387 6.09677 19.7826 6.07461 21.0702L6.04988 22.5066C6.01987 24.2501 6.00486 25.1218 6.2565 25.6704C6.72713 26.6965 7.79538 27.3126 8.91921 27.2061C9.5201 27.1491 10.2671 26.6995 11.7611 25.8004L12.757 25.201C13.9262 24.4973 14.5108 24.1455 15.1176 23.9457C16.2392 23.5765 17.4478 23.5655 18.5759 23.9143C19.1862 24.103 19.7771 24.4442 20.9589 25.1265L21.9674 25.7088C23.3995 26.5356 24.1155 26.949 24.7014 27.0003C25.7962 27.0963 26.837 26.5075 27.3186 25.5195C27.5764 24.9909 27.5908 24.1642 27.6196 22.5109L27.6466 20.9638C27.6704 19.6004 27.6823 18.9187 27.5517 18.2939C27.3102 17.1389 26.7154 16.0876 25.8498 15.2858C25.3815 14.852 24.791 14.5111 23.6102 13.8293L21.173 12.4222Z" fill="var(--c--globals--colors--logo-2)"/>
</svg>

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 6.0 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 34 KiB

@@ -1,3 +1,3 @@
<svg width="24" height="25" viewBox="0 0 24 25" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4.04601 20.8381C3.04306 20.8381 2.28465 20.5812 1.77079 20.0673C1.25693 19.5597 1 18.8105 1 17.82V6.51815C1 5.52139 1.25693 4.76917 1.77079 4.2615C2.28465 3.75383 3.04306 3.5 4.04601 3.5H19.9447C20.9538 3.5 21.7154 3.75383 22.2292 4.2615C22.7431 4.76917 23 5.52139 23 6.51815V17.82C23 18.8105 22.7431 19.5597 22.2292 20.0673C21.7154 20.5812 20.9538 20.8381 19.9447 20.8381H4.04601ZM4.15745 19.0087H19.8425C20.2697 19.0087 20.5979 18.8972 20.8269 18.6743C21.056 18.4515 21.1705 18.114 21.1705 17.6621V6.66674C21.1705 6.22098 21.056 5.88666 20.8269 5.66378C20.5979 5.4409 20.2697 5.32946 19.8425 5.32946H4.15745C3.72407 5.32946 3.39285 5.4409 3.16378 5.66378C2.9409 5.88666 2.82946 6.22098 2.82946 6.66674V17.6621C2.82946 18.114 2.9409 18.4515 3.16378 18.6743C3.39285 18.8972 3.72407 19.0087 4.15745 19.0087ZM8.38286 19.3058V5.01372H10.1195V19.3058H8.38286ZM6.56268 8.72837H4.65893C4.49796 8.72837 4.35556 8.66646 4.23174 8.54263C4.11411 8.41881 4.0553 8.28261 4.0553 8.13402C4.0553 7.97305 4.11411 7.83376 4.23174 7.71613C4.35556 7.59849 4.49796 7.53968 4.65893 7.53968H6.56268C6.72984 7.53968 6.87224 7.59849 6.98987 7.71613C7.11369 7.83376 7.1756 7.97305 7.1756 8.13402C7.1756 8.28261 7.11369 8.41881 6.98987 8.54263C6.87224 8.66646 6.72984 8.72837 6.56268 8.72837ZM6.56268 11.2172H4.65893C4.49796 11.2172 4.35556 11.1584 4.23174 11.0407C4.11411 10.9169 4.0553 10.7745 4.0553 10.6136C4.0553 10.4588 4.11411 10.3226 4.23174 10.2049C4.35556 10.0873 4.49796 10.0285 4.65893 10.0285H6.56268C6.72984 10.0285 6.87224 10.0873 6.98987 10.2049C7.11369 10.3226 7.1756 10.4588 7.1756 10.6136C7.1756 10.7745 7.11369 10.9169 6.98987 11.0407C6.87224 11.1584 6.72984 11.2172 6.56268 11.2172ZM6.56268 13.6967H4.65893C4.49796 13.6967 4.35556 13.6379 4.23174 13.5203C4.11411 13.4026 4.0553 13.2664 4.0553 13.1117C4.0553 12.9507 4.11411 12.8114 4.23174 12.6938C4.35556 12.5761 4.49796 12.5173 4.65893 12.5173H6.56268C6.72984 12.5173 6.87224 12.5761 6.98987 12.6938C7.11369 12.8114 7.1756 12.9507 7.1756 13.1117C7.1756 13.2664 7.11369 13.4026 6.98987 13.5203C6.87224 13.6379 6.72984 13.6967 6.56268 13.6967Z" fill="#3251E0"/>
<path d="M4.04601 20.8381C3.04306 20.8381 2.28465 20.5812 1.77079 20.0673C1.25693 19.5597 1 18.8105 1 17.82V6.51815C1 5.52139 1.25693 4.76917 1.77079 4.2615C2.28465 3.75383 3.04306 3.5 4.04601 3.5H19.9447C20.9538 3.5 21.7154 3.75383 22.2292 4.2615C22.7431 4.76917 23 5.52139 23 6.51815V17.82C23 18.8105 22.7431 19.5597 22.2292 20.0673C21.7154 20.5812 20.9538 20.8381 19.9447 20.8381H4.04601ZM4.15745 19.0087H19.8425C20.2697 19.0087 20.5979 18.8972 20.8269 18.6743C21.056 18.4515 21.1705 18.114 21.1705 17.6621V6.66674C21.1705 6.22098 21.056 5.88666 20.8269 5.66378C20.5979 5.4409 20.2697 5.32946 19.8425 5.32946H4.15745C3.72407 5.32946 3.39285 5.4409 3.16378 5.66378C2.9409 5.88666 2.82946 6.22098 2.82946 6.66674V17.6621C2.82946 18.114 2.9409 18.4515 3.16378 18.6743C3.39285 18.8972 3.72407 19.0087 4.15745 19.0087ZM8.38286 19.3058V5.01372H10.1195V19.3058H8.38286ZM6.56268 8.72837H4.65893C4.49796 8.72837 4.35556 8.66646 4.23174 8.54263C4.11411 8.41881 4.0553 8.28261 4.0553 8.13402C4.0553 7.97305 4.11411 7.83376 4.23174 7.71613C4.35556 7.59849 4.49796 7.53968 4.65893 7.53968H6.56268C6.72984 7.53968 6.87224 7.59849 6.98987 7.71613C7.11369 7.83376 7.1756 7.97305 7.1756 8.13402C7.1756 8.28261 7.11369 8.41881 6.98987 8.54263C6.87224 8.66646 6.72984 8.72837 6.56268 8.72837ZM6.56268 11.2172H4.65893C4.49796 11.2172 4.35556 11.1584 4.23174 11.0407C4.11411 10.9169 4.0553 10.7745 4.0553 10.6136C4.0553 10.4588 4.11411 10.3226 4.23174 10.2049C4.35556 10.0873 4.49796 10.0285 4.65893 10.0285H6.56268C6.72984 10.0285 6.87224 10.0873 6.98987 10.2049C7.11369 10.3226 7.1756 10.4588 7.1756 10.6136C7.1756 10.7745 7.11369 10.9169 6.98987 11.0407C6.87224 11.1584 6.72984 11.2172 6.56268 11.2172ZM6.56268 13.6967H4.65893C4.49796 13.6967 4.35556 13.6379 4.23174 13.5203C4.11411 13.4026 4.0553 13.2664 4.0553 13.1117C4.0553 12.9507 4.11411 12.8114 4.23174 12.6938C4.35556 12.5761 4.49796 12.5173 4.65893 12.5173H6.56268C6.72984 12.5173 6.87224 12.5761 6.98987 12.6938C7.11369 12.8114 7.1756 12.9507 7.1756 13.1117C7.1756 13.2664 7.11369 13.4026 6.98987 13.5203C6.87224 13.6379 6.72984 13.6967 6.56268 13.6967Z" fill="var(--c--contextuals--content--semantic--brand--tertiary)"/>
</svg>

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

@@ -1,8 +1,8 @@
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M43.9612 12.1078C43.9612 10.7446 43.9612 10.063 43.8197 9.44065C43.5582 8.29032 42.9454 7.24979 42.0663 6.46318C41.5907 6.03759 40.9946 5.707 39.8025 5.04582L36.7561 3.35621C35.6298 2.73158 35.0667 2.41926 34.4857 2.24352C33.4121 1.91883 32.2665 1.91883 31.1929 2.24352C30.6119 2.41926 30.0488 2.73158 28.9226 3.35621L27.6662 4.05301C26.1413 4.89875 25.3789 5.32161 25.0296 5.81385C24.3763 6.73448 24.3769 7.96764 25.0311 8.88764C25.3808 9.37954 26.1437 9.80168 27.6694 10.646L28.6864 11.2088C29.8804 11.8695 30.4774 12.1998 30.9538 12.6255C31.8343 13.4121 32.4482 14.4534 32.7101 15.6047C32.8519 16.2276 32.8519 16.9099 32.8519 18.2745V19.4391C32.8519 21.0927 32.8519 21.9195 33.1003 22.4525C33.5646 23.4487 34.595 24.0556 35.6913 23.9787C36.278 23.9376 37.0012 23.5368 38.4474 22.7351L39.8008 21.9849C40.9934 21.3238 41.5897 20.9933 42.0655 20.5677C42.945 19.7811 43.558 18.7404 43.8196 17.5898C43.9612 16.9673 43.9612 16.2855 43.9612 14.922V12.1078Z" fill="#2845C1"/>
<path d="M10.0512 32.3143C8.87068 32.9959 8.2804 33.3367 7.81215 33.7704C6.94669 34.572 6.35194 35.623 6.11028 36.7776C5.97953 37.4023 5.99126 38.0838 6.01473 39.4468L6.07469 42.9299C6.09685 44.2175 6.10794 44.8614 6.24627 45.4524C6.50185 46.5445 7.07465 47.5366 7.89263 48.304C8.33535 48.7194 8.88738 49.0509 9.99144 49.7139L11.2231 50.4535C12.7179 51.3512 13.4654 51.8001 14.0663 51.8565C15.1902 51.962 16.2579 51.3449 16.7275 50.3183C16.9787 49.7695 16.9628 48.8978 16.9311 47.1543L16.91 45.9921C16.8852 44.6277 16.8728 43.9456 17.0032 43.3202C17.2442 42.1643 17.839 41.1121 18.7051 40.3095C19.1737 39.8753 19.7646 39.5342 20.9464 38.8519L21.9549 38.2696C23.387 37.4428 24.103 37.0294 24.4404 36.5477C25.071 35.6475 25.0814 34.4518 24.4667 33.5407C24.1377 33.0532 23.429 32.6273 22.0116 31.7757L20.6853 30.9787C19.5165 30.2765 18.9321 29.9253 18.3256 29.726C17.2046 29.3577 15.9968 29.3471 14.8696 29.6959C14.2597 29.8845 13.6693 30.2254 12.4884 30.9072L10.0512 32.3143Z" fill="#2845C1"/>
<path d="M21.7175 51.8922C21.7175 53.2554 21.7175 53.937 21.8589 54.5594C22.1204 55.7097 22.7332 56.7502 23.6123 57.5368C24.088 57.9624 24.684 58.293 25.8762 58.9542L28.9226 60.6438C30.0488 61.2684 30.6119 61.5807 31.1929 61.7565C32.2665 62.0812 33.4121 62.0812 34.4857 61.7565C35.0667 61.5807 35.6298 61.2684 36.7561 60.6438L38.0124 59.947C39.5373 59.1012 40.2997 58.6784 40.649 58.1862C41.3023 57.2655 41.3017 56.0324 40.6476 55.1124C40.2978 54.6205 39.5349 54.1983 38.0093 53.354L36.9922 52.7912C35.7982 52.1305 35.2012 51.8002 34.7248 51.3745C33.8443 50.5879 33.2305 49.5466 32.9685 48.3953C32.8268 47.7724 32.8268 47.0901 32.8268 45.7255V44.5609C32.8268 42.9073 32.8268 42.0805 32.5783 41.5475C32.114 40.5513 31.0837 39.9444 29.9873 40.0213C29.4006 40.0624 28.6775 40.4632 27.2312 41.2649L25.8778 42.0151C24.6852 42.6761 24.089 43.0067 23.6131 43.4323C22.7336 44.2189 22.1206 45.2596 21.859 46.4102C21.7175 47.0327 21.7175 47.7145 21.7175 49.078V51.8922Z" fill="#2845C1"/>
<path d="M44.5055 51.5779C45.6861 52.2595 46.2764 52.6003 46.8861 52.789C48.0131 53.1376 49.2206 53.1273 50.3414 52.7592C50.9478 52.5601 51.5321 52.2092 52.7007 51.5074L55.6872 49.7139C56.7912 49.0509 57.3433 48.7194 57.786 48.304C58.604 47.5366 59.1768 46.5445 59.4324 45.4524C59.5707 44.8614 59.5818 44.2175 59.6039 42.9299L59.6287 41.4935C59.6587 39.75 59.6737 38.8783 59.422 38.3297C58.9514 37.3036 57.8832 36.6875 56.7593 36.794C56.1584 36.851 55.4114 37.3006 53.9174 38.1997L52.9215 38.7991C51.7523 39.5028 51.1677 39.8546 50.5609 40.0544C49.4394 40.4236 48.2307 40.4346 47.1027 40.0858C46.4924 39.8971 45.9015 39.5559 44.7197 38.8736L43.7111 38.2913C42.2791 37.4645 41.563 37.0511 40.9772 36.9998C39.8823 36.9038 38.8415 37.4926 38.3599 38.4806C38.1022 39.0092 38.0878 39.8359 38.0589 41.4892L38.0319 43.0364C38.0081 44.3997 37.9962 45.0814 38.1269 45.7062C38.3684 46.8612 38.9631 47.9125 39.8287 48.7143C40.2971 49.1481 40.8875 49.489 42.0684 50.1708L44.5055 51.5779Z" fill="#2845C1"/>
<path d="M55.6274 31.6857C56.8079 31.0041 57.3982 30.6633 57.8665 30.2296C58.7319 29.428 59.3267 28.3771 59.5683 27.2224C59.6991 26.5977 59.6874 25.9162 59.6639 24.5532L59.6039 21.0701C59.5818 19.7825 59.5707 19.1386 59.4324 18.5476C59.1768 17.4555 58.604 16.4634 57.786 15.696C57.3433 15.2806 56.7912 14.9491 55.6872 14.2861L54.4556 13.5465C52.9607 12.6488 52.2133 12.1999 51.6123 12.1435C50.4884 12.038 49.4207 12.6551 48.9511 13.6817C48.7 14.2305 48.7158 15.1022 48.7475 16.8457L48.7686 18.0079C48.7934 19.3723 48.8058 20.0544 48.6754 20.6798C48.4344 21.8357 47.8396 22.8879 46.9735 23.6905C46.5049 24.1247 45.914 24.4658 44.7322 25.1481L43.7237 25.7304C42.2916 26.5572 41.5756 26.9706 41.2382 27.4523C40.6076 28.3525 40.5972 29.5482 41.2119 30.4593C41.5409 30.9468 42.2496 31.3727 43.667 32.2243L44.9934 33.0213C46.1622 33.7235 46.7466 34.0747 47.353 34.274C48.474 34.6423 49.6818 34.6529 50.809 34.3041C51.4189 34.1155 52.0094 33.7746 53.1902 33.0928L55.6274 31.6857Z" fill="#2845C1"/>
<path d="M21.173 12.4222C19.9925 11.7406 19.4022 11.3998 18.7924 11.2112C17.6655 10.8625 16.458 10.8728 15.3372 11.2409C14.7308 11.44 14.1465 11.7909 12.9778 12.4927L9.99135 14.2862C8.8873 14.9492 8.33527 15.2807 7.89255 15.6961C7.07457 16.4635 6.50177 17.4556 6.24619 18.5477C6.10786 19.1387 6.09677 19.7826 6.07461 21.0702L6.04988 22.5066C6.01987 24.2501 6.00486 25.1218 6.2565 25.6704C6.72713 26.6965 7.79538 27.3126 8.91921 27.2061C9.5201 27.1491 10.2671 26.6995 11.7611 25.8004L12.757 25.201C13.9262 24.4973 14.5108 24.1455 15.1176 23.9457C16.2392 23.5765 17.4478 23.5655 18.5759 23.9143C19.1862 24.103 19.7771 24.4442 20.9589 25.1265L21.9674 25.7088C23.3995 26.5356 24.1155 26.949 24.7014 27.0003C25.7962 27.0963 26.837 26.5075 27.3186 25.5195C27.5764 24.9909 27.5908 24.1642 27.6196 22.5109L27.6466 20.9638C27.6704 19.6004 27.6823 18.9187 27.5517 18.2939C27.3102 17.1389 26.7154 16.0876 25.8498 15.2858C25.3815 14.852 24.791 14.5111 23.6102 13.8293L21.173 12.4222Z" fill="#C83F49"/>
<path d="M43.9612 12.1078C43.9612 10.7446 43.9612 10.063 43.8197 9.44065C43.5582 8.29032 42.9454 7.24979 42.0663 6.46318C41.5907 6.03759 40.9946 5.707 39.8025 5.04582L36.7561 3.35621C35.6298 2.73158 35.0667 2.41926 34.4857 2.24352C33.4121 1.91883 32.2665 1.91883 31.1929 2.24352C30.6119 2.41926 30.0488 2.73158 28.9226 3.35621L27.6662 4.05301C26.1413 4.89875 25.3789 5.32161 25.0296 5.81385C24.3763 6.73448 24.3769 7.96764 25.0311 8.88764C25.3808 9.37954 26.1437 9.80168 27.6694 10.646L28.6864 11.2088C29.8804 11.8695 30.4774 12.1998 30.9538 12.6255C31.8343 13.4121 32.4482 14.4534 32.7101 15.6047C32.8519 16.2276 32.8519 16.9099 32.8519 18.2745V19.4391C32.8519 21.0927 32.8519 21.9195 33.1003 22.4525C33.5646 23.4487 34.595 24.0556 35.6913 23.9787C36.278 23.9376 37.0012 23.5368 38.4474 22.7351L39.8008 21.9849C40.9934 21.3238 41.5897 20.9933 42.0655 20.5677C42.945 19.7811 43.558 18.7404 43.8196 17.5898C43.9612 16.9673 43.9612 16.2855 43.9612 14.922V12.1078Z" fill="var(--c--globals--colors--logo-1)"/>
<path d="M10.0512 32.3143C8.87068 32.9959 8.2804 33.3367 7.81215 33.7704C6.94669 34.572 6.35194 35.623 6.11028 36.7776C5.97953 37.4023 5.99126 38.0838 6.01473 39.4468L6.07469 42.9299C6.09685 44.2175 6.10794 44.8614 6.24627 45.4524C6.50185 46.5445 7.07465 47.5366 7.89263 48.304C8.33535 48.7194 8.88738 49.0509 9.99144 49.7139L11.2231 50.4535C12.7179 51.3512 13.4654 51.8001 14.0663 51.8565C15.1902 51.962 16.2579 51.3449 16.7275 50.3183C16.9787 49.7695 16.9628 48.8978 16.9311 47.1543L16.91 45.9921C16.8852 44.6277 16.8728 43.9456 17.0032 43.3202C17.2442 42.1643 17.839 41.1121 18.7051 40.3095C19.1737 39.8753 19.7646 39.5342 20.9464 38.8519L21.9549 38.2696C23.387 37.4428 24.103 37.0294 24.4404 36.5477C25.071 35.6475 25.0814 34.4518 24.4667 33.5407C24.1377 33.0532 23.429 32.6273 22.0116 31.7757L20.6853 30.9787C19.5165 30.2765 18.9321 29.9253 18.3256 29.726C17.2046 29.3577 15.9968 29.3471 14.8696 29.6959C14.2597 29.8845 13.6693 30.2254 12.4884 30.9072L10.0512 32.3143Z" fill="var(--c--globals--colors--logo-1)"/>
<path d="M21.7175 51.8922C21.7175 53.2554 21.7175 53.937 21.8589 54.5594C22.1204 55.7097 22.7332 56.7502 23.6123 57.5368C24.088 57.9624 24.684 58.293 25.8762 58.9542L28.9226 60.6438C30.0488 61.2684 30.6119 61.5807 31.1929 61.7565C32.2665 62.0812 33.4121 62.0812 34.4857 61.7565C35.0667 61.5807 35.6298 61.2684 36.7561 60.6438L38.0124 59.947C39.5373 59.1012 40.2997 58.6784 40.649 58.1862C41.3023 57.2655 41.3017 56.0324 40.6476 55.1124C40.2978 54.6205 39.5349 54.1983 38.0093 53.354L36.9922 52.7912C35.7982 52.1305 35.2012 51.8002 34.7248 51.3745C33.8443 50.5879 33.2305 49.5466 32.9685 48.3953C32.8268 47.7724 32.8268 47.0901 32.8268 45.7255V44.5609C32.8268 42.9073 32.8268 42.0805 32.5783 41.5475C32.114 40.5513 31.0837 39.9444 29.9873 40.0213C29.4006 40.0624 28.6775 40.4632 27.2312 41.2649L25.8778 42.0151C24.6852 42.6761 24.089 43.0067 23.6131 43.4323C22.7336 44.2189 22.1206 45.2596 21.859 46.4102C21.7175 47.0327 21.7175 47.7145 21.7175 49.078V51.8922Z" fill="var(--c--globals--colors--logo-1)"/>
<path d="M44.5055 51.5779C45.6861 52.2595 46.2764 52.6003 46.8861 52.789C48.0131 53.1376 49.2206 53.1273 50.3414 52.7592C50.9478 52.5601 51.5321 52.2092 52.7007 51.5074L55.6872 49.7139C56.7912 49.0509 57.3433 48.7194 57.786 48.304C58.604 47.5366 59.1768 46.5445 59.4324 45.4524C59.5707 44.8614 59.5818 44.2175 59.6039 42.9299L59.6287 41.4935C59.6587 39.75 59.6737 38.8783 59.422 38.3297C58.9514 37.3036 57.8832 36.6875 56.7593 36.794C56.1584 36.851 55.4114 37.3006 53.9174 38.1997L52.9215 38.7991C51.7523 39.5028 51.1677 39.8546 50.5609 40.0544C49.4394 40.4236 48.2307 40.4346 47.1027 40.0858C46.4924 39.8971 45.9015 39.5559 44.7197 38.8736L43.7111 38.2913C42.2791 37.4645 41.563 37.0511 40.9772 36.9998C39.8823 36.9038 38.8415 37.4926 38.3599 38.4806C38.1022 39.0092 38.0878 39.8359 38.0589 41.4892L38.0319 43.0364C38.0081 44.3997 37.9962 45.0814 38.1269 45.7062C38.3684 46.8612 38.9631 47.9125 39.8287 48.7143C40.2971 49.1481 40.8875 49.489 42.0684 50.1708L44.5055 51.5779Z" fill="var(--c--globals--colors--logo-1)"/>
<path d="M55.6274 31.6857C56.8079 31.0041 57.3982 30.6633 57.8665 30.2296C58.7319 29.428 59.3267 28.3771 59.5683 27.2224C59.6991 26.5977 59.6874 25.9162 59.6639 24.5532L59.6039 21.0701C59.5818 19.7825 59.5707 19.1386 59.4324 18.5476C59.1768 17.4555 58.604 16.4634 57.786 15.696C57.3433 15.2806 56.7912 14.9491 55.6872 14.2861L54.4556 13.5465C52.9607 12.6488 52.2133 12.1999 51.6123 12.1435C50.4884 12.038 49.4207 12.6551 48.9511 13.6817C48.7 14.2305 48.7158 15.1022 48.7475 16.8457L48.7686 18.0079C48.7934 19.3723 48.8058 20.0544 48.6754 20.6798C48.4344 21.8357 47.8396 22.8879 46.9735 23.6905C46.5049 24.1247 45.914 24.4658 44.7322 25.1481L43.7237 25.7304C42.2916 26.5572 41.5756 26.9706 41.2382 27.4523C40.6076 28.3525 40.5972 29.5482 41.2119 30.4593C41.5409 30.9468 42.2496 31.3727 43.667 32.2243L44.9934 33.0213C46.1622 33.7235 46.7466 34.0747 47.353 34.274C48.474 34.6423 49.6818 34.6529 50.809 34.3041C51.4189 34.1155 52.0094 33.7746 53.1902 33.0928L55.6274 31.6857Z" fill="var(--c--globals--colors--logo-1)"/>
<path d="M21.173 12.4222C19.9925 11.7406 19.4022 11.3998 18.7924 11.2112C17.6655 10.8625 16.458 10.8728 15.3372 11.2409C14.7308 11.44 14.1465 11.7909 12.9778 12.4927L9.99135 14.2862C8.8873 14.9492 8.33527 15.2807 7.89255 15.6961C7.07457 16.4635 6.50177 17.4556 6.24619 18.5477C6.10786 19.1387 6.09677 19.7826 6.07461 21.0702L6.04988 22.5066C6.01987 24.2501 6.00486 25.1218 6.2565 25.6704C6.72713 26.6965 7.79538 27.3126 8.91921 27.2061C9.5201 27.1491 10.2671 26.6995 11.7611 25.8004L12.757 25.201C13.9262 24.4973 14.5108 24.1455 15.1176 23.9457C16.2392 23.5765 17.4478 23.5655 18.5759 23.9143C19.1862 24.103 19.7771 24.4442 20.9589 25.1265L21.9674 25.7088C23.3995 26.5356 24.1155 26.949 24.7014 27.0003C25.7962 27.0963 26.837 26.5075 27.3186 25.5195C27.5764 24.9909 27.5908 24.1642 27.6196 22.5109L27.6466 20.9638C27.6704 19.6004 27.6823 18.9187 27.5517 18.2939C27.3102 17.1389 26.7154 16.0876 25.8498 15.2858C25.3815 14.852 24.791 14.5111 23.6102 13.8293L21.173 12.4222Z" fill="var(--c--globals--colors--logo-2)"/>
</svg>

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 6.0 KiB

@@ -1,21 +1,21 @@
<svg width="183" height="41" viewBox="0 0 183 41" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M25.9806 10.5539C25.9806 9.87231 25.9806 9.53151 25.9098 9.22032C25.7791 8.64516 25.4727 8.12489 25.0331 7.73159C24.7953 7.51879 24.4973 7.3535 23.9012 7.02291L22.378 6.1781C21.8149 5.86579 21.5334 5.70963 21.2428 5.62176C20.7061 5.45941 20.1333 5.45941 19.5965 5.62176C19.3059 5.70963 19.0244 5.86579 18.4613 6.1781L17.8331 6.52651C17.0707 6.94937 16.6894 7.16081 16.5148 7.40692C16.1881 7.86724 16.1884 8.48382 16.5155 8.94382C16.6904 9.18977 17.0718 9.40084 17.8347 9.82298L18.3432 10.1044C18.9402 10.4347 19.2387 10.5999 19.4769 10.8127C19.9172 11.2061 20.2241 11.7267 20.3551 12.3023C20.4259 12.6138 20.4259 12.9549 20.4259 13.6372V14.2195C20.4259 15.0463 20.4259 15.4597 20.5502 15.7263C20.7823 16.2243 21.2975 16.5278 21.8457 16.4894C22.139 16.4688 22.5006 16.2684 23.2237 15.8675L23.9004 15.4925C24.4967 15.1619 24.7948 14.9967 25.0327 14.7839C25.4725 14.3905 25.779 13.8702 25.9098 13.2949C25.9806 12.9837 25.9806 12.6428 25.9806 11.961V10.5539Z" fill="#2845C1"/>
<path d="M9.02562 20.6571C8.43534 20.9979 8.1402 21.1683 7.90608 21.3852C7.47335 21.786 7.17597 22.3115 7.05514 22.8888C6.98977 23.2012 6.99563 23.5419 7.00736 24.2234L7.03734 25.9649C7.04843 26.6088 7.05397 26.9307 7.12313 27.2262C7.25092 27.7723 7.53733 28.2683 7.94632 28.652C8.16767 28.8597 8.44369 29.0254 8.99572 29.357L9.61153 29.7268C10.359 30.1756 10.7327 30.4001 11.0331 30.4282C11.5951 30.481 12.1289 30.1724 12.3638 29.6592C12.4893 29.3847 12.4814 28.9489 12.4656 28.0772L12.455 27.4961C12.4426 26.8139 12.4364 26.4728 12.5016 26.1601C12.6221 25.5821 12.9195 25.056 13.3526 24.6548C13.5869 24.4377 13.8823 24.2671 14.4732 23.9259L14.9775 23.6348C15.6935 23.2214 16.0515 23.0147 16.2202 22.7739C16.5355 22.3238 16.5407 21.7259 16.2333 21.2703C16.0689 21.0266 15.7145 20.8137 15.0058 20.3878L14.3426 19.9894C13.7582 19.6382 13.466 19.4627 13.1628 19.363C12.6023 19.1788 11.9984 19.1736 11.4348 19.3479C11.1298 19.4423 10.8346 19.6127 10.2442 19.9536L9.02562 20.6571Z" fill="#2845C1"/>
<path d="M14.8587 30.4461C14.8587 31.1277 14.8587 31.4685 14.9295 31.7797C15.0602 32.3548 15.3666 32.8751 15.8062 33.2684C16.044 33.4812 16.342 33.6465 16.9381 33.9771L18.4613 34.8219C19.0244 35.1342 19.306 35.2904 19.5965 35.3782C20.1333 35.5406 20.7061 35.5406 21.2428 35.3782C21.5334 35.2904 21.8149 35.1342 22.378 34.8219L23.0062 34.4735C23.7686 34.0506 24.1499 33.8392 24.3245 33.5931C24.6512 33.1328 24.6509 32.5162 24.3238 32.0562C24.1489 31.8102 23.7675 31.5992 23.0046 31.177L22.4961 30.8956C21.8991 30.5653 21.6006 30.4001 21.3624 30.1873C20.9222 29.7939 20.6152 29.2733 20.4842 28.6977C20.4134 28.3862 20.4134 28.0451 20.4134 27.3627V26.7805C20.4134 25.9537 20.4134 25.5403 20.2892 25.2737C20.057 24.7757 19.5418 24.4722 18.9936 24.5106C18.7003 24.5312 18.3387 24.7316 17.6156 25.1325L16.9389 25.5075C16.3426 25.8381 16.0445 26.0033 15.8066 26.2161C15.3668 26.6095 15.0603 27.1298 14.9295 27.7051C14.8587 28.0163 14.8587 28.3572 14.8587 29.039V30.4461Z" fill="#2845C1"/>
<path d="M26.2528 30.2889C26.843 30.6297 27.1382 30.8001 27.4431 30.8945C28.0065 31.0688 28.6103 31.0636 29.1707 30.8796C29.4739 30.78 29.766 30.6046 30.3504 30.2537L31.8436 29.3569C32.3956 29.0254 32.6716 28.8597 32.893 28.652C33.302 28.2683 33.5884 27.7723 33.7162 27.2262C33.7853 26.9307 33.7909 26.6088 33.802 25.9649L33.8143 25.2467C33.8293 24.375 33.8368 23.9391 33.711 23.6648C33.4757 23.1518 32.9416 22.8437 32.3797 22.897C32.0792 22.9255 31.7057 23.1503 30.9587 23.5999L30.4608 23.8995C29.8762 24.2514 29.5839 24.4273 29.2805 24.5272C28.7197 24.7118 28.1154 24.7173 27.5513 24.5429C27.2462 24.4485 26.9507 24.278 26.3598 23.9368L25.8556 23.6457C25.1395 23.2323 24.7815 23.0256 24.4886 22.9999C23.9411 22.9519 23.4208 23.2463 23.18 23.7403C23.0511 24.0046 23.0439 24.4179 23.0294 25.2446L23.0159 26.0182C23.004 26.6998 22.9981 27.0407 23.0634 27.3531C23.1842 27.9306 23.4816 28.4562 23.9144 28.8572C24.1485 29.0741 24.4438 29.2445 25.0342 29.5854L26.2528 30.2889Z" fill="#2845C1"/>
<path d="M31.8137 20.3429C32.404 20.0021 32.6991 19.8317 32.9332 19.6148C33.366 19.214 33.6633 18.6885 33.7842 18.1112C33.8495 17.7988 33.8437 17.4581 33.8319 16.7766L33.802 15.0351C33.7909 14.3912 33.7853 14.0693 33.7162 13.7738C33.5884 13.2277 33.302 12.7317 32.893 12.348C32.6716 12.1403 32.3956 11.9746 31.8436 11.6431L31.2278 11.2732C30.4804 10.8244 30.1066 10.5999 29.8062 10.5718C29.2442 10.519 28.7104 10.8276 28.4755 11.3408C28.35 11.6153 28.3579 12.0511 28.3737 12.9228L28.3843 13.5039C28.3967 14.1861 28.4029 14.5272 28.3377 14.8399C28.2172 15.4179 27.9198 15.944 27.4867 16.3452C27.2524 16.5623 26.957 16.7329 26.3661 17.0741L25.8618 17.3652C25.1458 17.7786 24.7878 17.9853 24.6191 18.2261C24.3038 18.6762 24.2986 19.2741 24.606 19.7297C24.7704 19.9734 25.1248 20.1863 25.8335 20.6122L26.4967 21.0106C27.0811 21.3618 27.3733 21.5373 27.6765 21.637C28.237 21.8212 28.8409 21.8264 29.4045 21.6521C29.7095 21.5577 30.0047 21.3873 30.5951 21.0464L31.8137 20.3429Z" fill="#2845C1"/>
<path d="M14.5863 10.7111C13.996 10.3703 13.7008 10.1999 13.396 10.1056C12.8325 9.93123 12.2287 9.93642 11.6683 10.1204C11.3652 10.22 11.073 10.3955 10.4887 10.7464L8.99543 11.6431C8.4434 11.9746 8.16739 12.1404 7.94603 12.348C7.53704 12.7317 7.25064 13.2278 7.12285 13.7738C7.05368 14.0694 7.04814 14.3913 7.03706 15.0351L7.0247 15.7533C7.00969 16.6251 7.00219 17.0609 7.128 17.3352C7.36332 17.8483 7.89744 18.1563 8.45936 18.103C8.75981 18.0746 9.13331 17.8498 9.88031 17.4002L10.3783 17.1005C10.9629 16.7487 11.2552 16.5728 11.5586 16.4729C12.1193 16.2883 12.7237 16.2828 13.2877 16.4572C13.5929 16.5515 13.8883 16.7221 14.4792 17.0632L14.9835 17.3544C15.6995 17.7678 16.0575 17.9745 16.3504 18.0002C16.8979 18.0482 17.4183 17.7537 17.6591 17.2598C17.7879 16.9955 17.7951 16.5821 17.8096 15.7554L17.8231 14.9819C17.835 14.3002 17.8409 13.9594 17.7756 13.6469C17.6548 13.0694 17.3575 12.5438 16.9247 12.1429C16.6905 11.926 16.3953 11.7555 15.8048 11.4146L14.5863 10.7111Z" fill="#C83F49"/>
<path d="M61.8577 16.0576C62.6931 16.0576 63.468 16.24 64.1819 16.6045C64.8463 16.9367 65.3785 17.3509 65.7781 17.8471C65.8437 17.9285 65.8294 18.0466 65.7498 18.1145L64.3459 19.3122C64.2574 19.3877 64.1236 19.3717 64.0503 19.2814C63.7909 18.9622 63.4929 18.6926 63.1565 18.4727C62.7616 18.2145 62.3285 18.085 61.8577 18.085C61.5237 18.085 61.2353 18.1462 60.9924 18.2676C60.7646 18.3891 60.5895 18.5486 60.468 18.7461C60.3617 18.9436 60.3088 19.1642 60.3088 19.4072C60.3089 19.7716 60.4375 20.0754 60.6955 20.3184C60.969 20.5462 61.319 20.744 61.7444 20.9111C62.1696 21.0782 62.6174 21.2606 63.0881 21.458C63.5742 21.6403 64.03 21.8682 64.4553 22.1416C64.8806 22.3998 65.2225 22.7489 65.4807 23.1895C65.7541 23.6147 65.8908 24.1537 65.8909 24.8066C65.8909 25.5812 65.7012 26.2423 65.3215 26.7891C64.957 27.3206 64.471 27.7309 63.8635 28.0195C63.2559 28.3082 62.5868 28.4531 61.8577 28.4531C60.8706 28.4531 60.0124 28.2707 59.2834 27.9062C58.6116 27.5563 58.0171 27.0962 57.5 26.5269C57.4264 26.4459 57.4354 26.3208 57.5176 26.2485L58.921 25.0135C59.0067 24.9381 59.1377 24.9498 59.2123 25.0363C59.5425 25.4194 59.9081 25.7384 60.3088 25.9922C60.7645 26.2807 61.2733 26.4248 61.8352 26.4248C62.3669 26.4248 62.7698 26.2881 63.0432 26.0146C63.3316 25.7261 63.4758 25.3768 63.4758 24.9668C63.4758 24.5871 63.339 24.2835 63.0657 24.0557C62.8074 23.8278 62.4656 23.63 62.0403 23.4629C61.6149 23.2958 61.1592 23.1208 60.6731 22.9385C60.2024 22.7411 59.7545 22.5057 59.3293 22.2324C58.9041 21.959 58.5539 21.6098 58.2805 21.1846C58.0223 20.744 57.8938 20.1816 57.8938 19.498C57.8938 18.8601 58.0606 18.283 58.3948 17.7666C58.729 17.2349 59.192 16.8165 59.7844 16.5127C60.3919 16.209 61.0831 16.0577 61.8577 16.0576Z" fill="#2845C1"/>
<path d="M71.3215 16.0576C72.1569 16.0577 72.9319 16.24 73.6457 16.6045C74.3101 16.9367 74.8424 17.3509 75.2419 17.8471C75.3075 17.9285 75.2933 18.0466 75.2137 18.1144L73.8098 19.3122C73.7213 19.3877 73.5875 19.3717 73.5141 19.2813C73.2548 18.9622 72.9568 18.6927 72.6204 18.4727C72.2255 18.2145 71.7923 18.085 71.3215 18.085C70.9873 18.085 70.6984 18.1461 70.4553 18.2676C70.2275 18.3891 70.0524 18.5486 69.9309 18.7461C69.8246 18.9435 69.7717 19.1642 69.7717 19.4072C69.7718 19.7717 69.9013 20.0754 70.1594 20.3184C70.4328 20.5462 70.782 20.7441 71.2073 20.9111C71.6326 21.0782 72.0811 21.2605 72.552 21.458C73.0379 21.6403 73.494 21.8682 73.9192 22.1416C74.3443 22.3998 74.6864 22.7491 74.9446 23.1895C75.2178 23.6146 75.3547 24.1538 75.3547 24.8066C75.3547 25.5813 75.1641 26.2422 74.7844 26.7891C74.4199 27.3207 73.934 27.7309 73.3264 28.0195C72.7189 28.3081 72.0506 28.4531 71.3215 28.4531C70.3341 28.4531 69.4755 28.2708 68.7463 27.9062C68.0745 27.5563 67.48 27.0962 66.9629 26.5269C66.8893 26.4459 66.8983 26.3208 66.9805 26.2485L68.3839 25.0135C68.4696 24.9381 68.6006 24.9498 68.6752 25.0363C69.0054 25.4194 69.371 25.7384 69.7717 25.9922C70.2273 26.2807 70.7362 26.4247 71.2981 26.4248C71.8298 26.4248 72.2327 26.2881 72.5061 26.0146C72.7946 25.7261 72.9387 25.3768 72.9387 24.9668C72.9387 24.587 72.802 24.2835 72.5286 24.0557C72.2703 23.8278 71.9285 23.63 71.5032 23.4629C71.0779 23.2958 70.622 23.1207 70.136 22.9385C69.6653 22.7411 69.2174 22.5057 68.7922 22.2324C68.3671 21.9591 68.0178 21.6097 67.7444 21.1846C67.4861 20.744 67.3567 20.1816 67.3567 19.498C67.3567 18.8602 67.5236 18.283 67.8577 17.7666C68.1918 17.235 68.655 16.8165 69.2473 16.5127C69.8549 16.2089 70.5468 16.0576 71.3215 16.0576Z" fill="#2845C1"/>
<path d="M86.3313 16.0576C87.1668 16.0576 87.9416 16.2399 88.6555 16.6045C89.32 16.9368 89.8521 17.3508 90.2517 17.8471C90.3173 17.9285 90.303 18.0466 90.2235 18.1144L88.8195 19.3122C88.731 19.3877 88.5973 19.3717 88.5239 19.2814C88.2646 18.9622 87.9666 18.6926 87.6301 18.4727C87.2352 18.2144 86.8022 18.085 86.3313 18.085C85.9971 18.085 85.7081 18.1461 85.4651 18.2676C85.2374 18.3891 85.0631 18.5488 84.9416 18.7461C84.8353 18.9436 84.7815 19.1642 84.7815 19.4072C84.7816 19.7716 84.911 20.0754 85.1692 20.3184C85.4426 20.5462 85.7918 20.7441 86.217 20.9111C86.6424 21.0782 87.0909 21.2605 87.5618 21.458C88.0478 21.6403 88.5036 21.8682 88.9289 22.1416C89.3542 22.3998 89.6961 22.749 89.9543 23.1895C90.2276 23.6147 90.3644 24.1537 90.3645 24.8066C90.3645 25.5811 90.1747 26.2423 89.7952 26.7891C89.4306 27.3207 88.9438 27.7309 88.3362 28.0195C87.7286 28.3081 87.0604 28.4531 86.3313 28.4531C85.3439 28.4531 84.4852 28.2708 83.7561 27.9062C83.0843 27.5563 82.4905 27.0961 81.9736 26.5269C81.9 26.4459 81.909 26.3208 81.9911 26.2485L83.3937 25.0136C83.4793 24.9381 83.6104 24.9498 83.685 25.0363C84.0152 25.4194 84.3808 25.7384 84.7815 25.9922C85.2372 26.2808 85.7468 26.4248 86.3088 26.4248C86.8403 26.4248 87.2425 26.288 87.5159 26.0146C87.8044 25.7261 87.9494 25.3769 87.9495 24.9668C87.9495 24.5872 87.8125 24.2835 87.5393 24.0557C87.2811 23.8279 86.9391 23.63 86.5139 23.4629C86.0886 23.2958 85.6328 23.1207 85.1467 22.9385C84.6758 22.741 84.2273 22.5059 83.802 22.2324C83.3768 21.9591 83.0275 21.6097 82.7541 21.1846C82.4959 20.744 82.3664 20.1816 82.3664 19.498C82.3664 18.86 82.5342 18.2831 82.8684 17.7666C83.2026 17.2349 83.6656 16.8165 84.258 16.5127C84.8656 16.209 85.5567 16.0576 86.3313 16.0576Z" fill="#2845C1"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M106.191 16.2861C107.239 16.2861 108.128 16.4757 108.857 16.8555C109.586 17.2352 110.133 17.7594 110.498 18.4277C110.877 19.0961 111.067 19.8481 111.067 20.6836V28.0256C111.067 28.136 110.977 28.2256 110.867 28.2256H108.76C108.65 28.2256 108.56 28.136 108.56 28.0256V27.2662C108.56 27.1718 108.438 27.1271 108.372 27.1948C108.27 27.2997 108.159 27.4002 108.037 27.4961C107.626 27.8303 107.148 28.0892 106.601 28.2715C106.054 28.4689 105.5 28.5673 104.938 28.5674C103.814 28.5674 102.894 28.2639 102.18 27.6562C101.466 27.0486 101.11 26.2355 101.11 25.2178C101.11 24.352 101.436 23.6154 102.089 23.0078C102.743 22.4002 103.684 21.9973 104.915 21.7998L108.394 21.2089C108.49 21.1926 108.56 21.1093 108.56 21.0118V20.6836C108.56 20.0002 108.333 19.4686 107.877 19.0889C107.422 18.694 106.829 18.4962 106.1 18.4961C105.508 18.4961 104.968 18.6256 104.482 18.8838C104.071 19.1094 103.717 19.4102 103.422 19.7865C103.351 19.876 103.222 19.8965 103.132 19.8273L101.606 18.6598C101.52 18.5942 101.501 18.4719 101.568 18.3865C102.052 17.7617 102.666 17.2665 103.411 16.9014C104.231 16.4912 105.158 16.2861 106.191 16.2861ZM105.553 23.4639C104.87 23.5702 104.376 23.7671 104.072 24.0557C103.768 24.329 103.616 24.6784 103.616 25.1035C103.616 25.5288 103.775 25.8789 104.094 26.1523C104.428 26.4258 104.877 26.5625 105.439 26.5625C106.138 26.5625 106.753 26.403 107.285 26.084C107.721 25.8222 108.09 25.4882 108.393 25.083C108.508 24.9295 108.56 24.7399 108.56 24.5483V23.2008C108.56 23.0772 108.449 22.9832 108.327 23.0035L105.553 23.4639Z" fill="#2845C1"/>
<path d="M95.9868 16.5412C95.9868 16.6517 96.0764 16.7412 96.1868 16.7412H99.5017C99.6121 16.7412 99.7017 16.8308 99.7017 16.9412V18.7971C99.7017 18.9075 99.6121 18.9971 99.5017 18.9971H96.1868C96.0764 18.9971 95.9868 19.0866 95.9868 19.1971V24.1699C95.9868 24.671 96.0708 25.0659 96.2378 25.3545C96.4048 25.6429 96.6403 25.8555 96.9439 25.9922C97.2477 26.1137 97.605 26.1748 98.0151 26.1748C98.4101 26.1748 98.7446 26.152 99.0181 26.1064C99.1739 26.0718 99.3174 26.0347 99.4485 25.9951C99.5829 25.9545 99.7241 26.0522 99.7241 26.1926V27.9814C99.7241 28.0604 99.6778 28.1326 99.6042 28.1613C99.3605 28.2561 99.0888 28.3233 98.7896 28.3623C98.4554 28.423 98.0605 28.4531 97.605 28.4531C96.6936 28.4531 95.926 28.2936 95.3032 27.9746C94.6959 27.6404 94.2325 27.1617 93.9136 26.5391C93.6099 25.9012 93.4585 25.1115 93.4585 24.1699V19.1971C93.4585 19.0866 93.369 18.9971 93.2585 18.9971H91.6087C91.4982 18.9971 91.4087 18.9075 91.4087 18.7971V16.9412C91.4087 16.8308 91.4982 16.7412 91.6087 16.7412H93.2585C93.369 16.7412 93.4585 16.6517 93.4585 16.5412V14.0711C93.4585 13.9606 93.548 13.8711 93.6585 13.8711H95.7868C95.8973 13.8711 95.9868 13.9606 95.9868 14.0711V16.5412Z" fill="#2845C1"/>
<path d="M130.563 16.5412C130.563 16.6517 130.653 16.7412 130.763 16.7412H134.077C134.187 16.7412 134.277 16.8308 134.277 16.9412V18.7971C134.277 18.9075 134.187 18.9971 134.077 18.9971H130.763C130.653 18.9971 130.563 19.0866 130.563 19.1971V24.1699C130.563 24.671 130.646 25.0659 130.813 25.3545C130.98 25.6431 131.216 25.8555 131.52 25.9922C131.824 26.1137 132.18 26.1748 132.59 26.1748C132.985 26.1748 133.32 26.152 133.593 26.1064C133.749 26.0718 133.893 26.0347 134.024 25.9951C134.158 25.9545 134.299 26.0522 134.299 26.1925V27.9813C134.299 28.0604 134.253 28.1326 134.179 28.1613C133.936 28.2561 133.665 28.3233 133.366 28.3623C133.032 28.4231 132.636 28.4531 132.18 28.4531C131.269 28.4531 130.502 28.2936 129.879 27.9746C129.272 27.6404 128.809 27.1619 128.49 26.5391C128.186 25.9011 128.034 25.1116 128.034 24.1699V19.1971C128.034 19.0866 127.944 18.9971 127.834 18.9971H126.092C125.982 18.9971 125.892 18.9075 125.892 18.7971V16.9412C125.892 16.8308 125.982 16.7412 126.092 16.7412H127.834C127.944 16.7412 128.034 16.6517 128.034 16.5412V14.0711C128.034 13.9606 128.123 13.8711 128.234 13.8711H130.363C130.473 13.8711 130.563 13.9606 130.563 14.0711V16.5412Z" fill="#2845C1"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M56.8209 27.9534C56.8716 28.0844 56.7749 28.2256 56.6344 28.2256H54.0801C53.9969 28.2256 53.9224 28.1741 53.893 28.0964L52.3355 23.9798C52.3061 23.902 52.2316 23.8506 52.1485 23.8506H45.6707C45.5876 23.8506 45.5132 23.9019 45.4837 23.9796L43.9196 28.0966C43.8901 28.1743 43.8157 28.2256 43.7326 28.2256H41.2002C41.0598 28.2256 40.9631 28.0846 41.0136 27.9536L47.0107 12.4034C47.0404 12.3263 47.1146 12.2754 47.1973 12.2754H50.6144C50.697 12.2754 50.7711 12.3262 50.8009 12.4032L56.8209 27.9534ZM46.5361 21.2104C46.4864 21.3413 46.5831 21.4814 46.7231 21.4814H51.1006C51.2405 21.4814 51.3372 21.3415 51.2876 21.2106L49.1032 15.4395C49.038 15.2673 48.7946 15.2672 48.7292 15.4392L46.5361 21.2104Z" fill="#2845C1"/>
<path d="M80.1372 28.0256C80.1372 28.136 80.0477 28.2256 79.9372 28.2256H77.8314C77.7209 28.2256 77.6314 28.136 77.6314 28.0256V16.9412C77.6314 16.8308 77.7209 16.7412 77.8314 16.7412H79.9372C80.0477 16.7412 80.1372 16.8308 80.1372 16.9412V28.0256Z" fill="#2845C1"/>
<path d="M120.009 16.2861C120.86 16.2861 121.619 16.4758 122.288 16.8555C122.971 17.2352 123.511 17.7977 123.906 18.542C124.301 19.2863 124.499 20.2208 124.499 21.3447V28.0256C124.499 28.136 124.409 28.2256 124.299 28.2256H122.169C122.059 28.2256 121.969 28.136 121.969 28.0256V21.4355C121.969 20.5546 121.741 19.8709 121.286 19.3848C120.83 18.8835 120.206 18.6328 119.417 18.6328C118.915 18.6329 118.475 18.7313 118.095 18.9287C117.715 19.111 117.389 19.3471 117.116 19.6357C116.919 19.8551 116.745 20.0831 116.592 20.3199C116.498 20.4662 116.455 20.6384 116.455 20.8124V28.0256C116.455 28.136 116.365 28.2256 116.255 28.2256H114.149C114.038 28.2256 113.949 28.136 113.949 28.0256V16.9412C113.949 16.8308 114.038 16.7412 114.149 16.7412H116.255C116.365 16.7412 116.455 16.8308 116.455 16.9412V17.5392C116.455 17.6294 116.566 17.6747 116.631 17.6128C117.008 17.2571 117.443 16.9588 117.936 16.7188C118.528 16.4302 119.219 16.2861 120.009 16.2861Z" fill="#2845C1"/>
<path d="M78.9067 11C79.2105 11 79.484 11.0757 79.7271 11.2275C79.9853 11.3794 80.1831 11.5845 80.3198 11.8428C80.4717 12.0858 80.5474 12.3593 80.5474 12.6631C80.5474 12.9668 80.4716 13.2477 80.3198 13.5059C80.1831 13.7641 79.9853 13.9692 79.7271 14.1211C79.484 14.2578 79.2106 14.3262 78.9067 14.3262C78.603 14.3262 78.3222 14.2578 78.064 14.1211C77.8057 13.9692 77.6006 13.7641 77.4487 13.5059C77.2969 13.2477 77.2212 12.9668 77.2212 12.6631C77.2212 12.2074 77.3807 11.82 77.6997 11.501C78.0338 11.167 78.436 11 78.9067 11Z" fill="#2845C1"/>
<rect x="143" y="12.5" width="40" height="16" rx="8" fill="#2845C1"/>
<path d="M25.9806 10.5539C25.9806 9.87231 25.9806 9.53151 25.9098 9.22032C25.7791 8.64516 25.4727 8.12489 25.0331 7.73159C24.7953 7.51879 24.4973 7.3535 23.9012 7.02291L22.378 6.1781C21.8149 5.86579 21.5334 5.70963 21.2428 5.62176C20.7061 5.45941 20.1333 5.45941 19.5965 5.62176C19.3059 5.70963 19.0244 5.86579 18.4613 6.1781L17.8331 6.52651C17.0707 6.94937 16.6894 7.16081 16.5148 7.40692C16.1881 7.86724 16.1884 8.48382 16.5155 8.94382C16.6904 9.18977 17.0718 9.40084 17.8347 9.82298L18.3432 10.1044C18.9402 10.4347 19.2387 10.5999 19.4769 10.8127C19.9172 11.2061 20.2241 11.7267 20.3551 12.3023C20.4259 12.6138 20.4259 12.9549 20.4259 13.6372V14.2195C20.4259 15.0463 20.4259 15.4597 20.5502 15.7263C20.7823 16.2243 21.2975 16.5278 21.8457 16.4894C22.139 16.4688 22.5006 16.2684 23.2237 15.8675L23.9004 15.4925C24.4967 15.1619 24.7948 14.9967 25.0327 14.7839C25.4725 14.3905 25.779 13.8702 25.9098 13.2949C25.9806 12.9837 25.9806 12.6428 25.9806 11.961V10.5539Z" fill="var(--c--globals--colors--logo-1)"/>
<path d="M9.02562 20.6571C8.43534 20.9979 8.1402 21.1683 7.90608 21.3852C7.47335 21.786 7.17597 22.3115 7.05514 22.8888C6.98977 23.2012 6.99563 23.5419 7.00736 24.2234L7.03734 25.9649C7.04843 26.6088 7.05397 26.9307 7.12313 27.2262C7.25092 27.7723 7.53733 28.2683 7.94632 28.652C8.16767 28.8597 8.44369 29.0254 8.99572 29.357L9.61153 29.7268C10.359 30.1756 10.7327 30.4001 11.0331 30.4282C11.5951 30.481 12.1289 30.1724 12.3638 29.6592C12.4893 29.3847 12.4814 28.9489 12.4656 28.0772L12.455 27.4961C12.4426 26.8139 12.4364 26.4728 12.5016 26.1601C12.6221 25.5821 12.9195 25.056 13.3526 24.6548C13.5869 24.4377 13.8823 24.2671 14.4732 23.9259L14.9775 23.6348C15.6935 23.2214 16.0515 23.0147 16.2202 22.7739C16.5355 22.3238 16.5407 21.7259 16.2333 21.2703C16.0689 21.0266 15.7145 20.8137 15.0058 20.3878L14.3426 19.9894C13.7582 19.6382 13.466 19.4627 13.1628 19.363C12.6023 19.1788 11.9984 19.1736 11.4348 19.3479C11.1298 19.4423 10.8346 19.6127 10.2442 19.9536L9.02562 20.6571Z" fill="var(--c--globals--colors--logo-1)"/>
<path d="M14.8587 30.4461C14.8587 31.1277 14.8587 31.4685 14.9295 31.7797C15.0602 32.3548 15.3666 32.8751 15.8062 33.2684C16.044 33.4812 16.342 33.6465 16.9381 33.9771L18.4613 34.8219C19.0244 35.1342 19.306 35.2904 19.5965 35.3782C20.1333 35.5406 20.7061 35.5406 21.2428 35.3782C21.5334 35.2904 21.8149 35.1342 22.378 34.8219L23.0062 34.4735C23.7686 34.0506 24.1499 33.8392 24.3245 33.5931C24.6512 33.1328 24.6509 32.5162 24.3238 32.0562C24.1489 31.8102 23.7675 31.5992 23.0046 31.177L22.4961 30.8956C21.8991 30.5653 21.6006 30.4001 21.3624 30.1873C20.9222 29.7939 20.6152 29.2733 20.4842 28.6977C20.4134 28.3862 20.4134 28.0451 20.4134 27.3627V26.7805C20.4134 25.9537 20.4134 25.5403 20.2892 25.2737C20.057 24.7757 19.5418 24.4722 18.9936 24.5106C18.7003 24.5312 18.3387 24.7316 17.6156 25.1325L16.9389 25.5075C16.3426 25.8381 16.0445 26.0033 15.8066 26.2161C15.3668 26.6095 15.0603 27.1298 14.9295 27.7051C14.8587 28.0163 14.8587 28.3572 14.8587 29.039V30.4461Z" fill="var(--c--globals--colors--logo-1)"/>
<path d="M26.2528 30.2889C26.843 30.6297 27.1382 30.8001 27.4431 30.8945C28.0065 31.0688 28.6103 31.0636 29.1707 30.8796C29.4739 30.78 29.766 30.6046 30.3504 30.2537L31.8436 29.3569C32.3956 29.0254 32.6716 28.8597 32.893 28.652C33.302 28.2683 33.5884 27.7723 33.7162 27.2262C33.7853 26.9307 33.7909 26.6088 33.802 25.9649L33.8143 25.2467C33.8293 24.375 33.8368 23.9391 33.711 23.6648C33.4757 23.1518 32.9416 22.8437 32.3797 22.897C32.0792 22.9255 31.7057 23.1503 30.9587 23.5999L30.4608 23.8995C29.8762 24.2514 29.5839 24.4273 29.2805 24.5272C28.7197 24.7118 28.1154 24.7173 27.5513 24.5429C27.2462 24.4485 26.9507 24.278 26.3598 23.9368L25.8556 23.6457C25.1395 23.2323 24.7815 23.0256 24.4886 22.9999C23.9411 22.9519 23.4208 23.2463 23.18 23.7403C23.0511 24.0046 23.0439 24.4179 23.0294 25.2446L23.0159 26.0182C23.004 26.6998 22.9981 27.0407 23.0634 27.3531C23.1842 27.9306 23.4816 28.4562 23.9144 28.8572C24.1485 29.0741 24.4438 29.2445 25.0342 29.5854L26.2528 30.2889Z" fill="var(--c--globals--colors--logo-1)"/>
<path d="M31.8137 20.3429C32.404 20.0021 32.6991 19.8317 32.9332 19.6148C33.366 19.214 33.6633 18.6885 33.7842 18.1112C33.8495 17.7988 33.8437 17.4581 33.8319 16.7766L33.802 15.0351C33.7909 14.3912 33.7853 14.0693 33.7162 13.7738C33.5884 13.2277 33.302 12.7317 32.893 12.348C32.6716 12.1403 32.3956 11.9746 31.8436 11.6431L31.2278 11.2732C30.4804 10.8244 30.1066 10.5999 29.8062 10.5718C29.2442 10.519 28.7104 10.8276 28.4755 11.3408C28.35 11.6153 28.3579 12.0511 28.3737 12.9228L28.3843 13.5039C28.3967 14.1861 28.4029 14.5272 28.3377 14.8399C28.2172 15.4179 27.9198 15.944 27.4867 16.3452C27.2524 16.5623 26.957 16.7329 26.3661 17.0741L25.8618 17.3652C25.1458 17.7786 24.7878 17.9853 24.6191 18.2261C24.3038 18.6762 24.2986 19.2741 24.606 19.7297C24.7704 19.9734 25.1248 20.1863 25.8335 20.6122L26.4967 21.0106C27.0811 21.3618 27.3733 21.5373 27.6765 21.637C28.237 21.8212 28.8409 21.8264 29.4045 21.6521C29.7095 21.5577 30.0047 21.3873 30.5951 21.0464L31.8137 20.3429Z" fill="var(--c--globals--colors--logo-1)"/>
<path d="M14.5863 10.7111C13.996 10.3703 13.7008 10.1999 13.396 10.1056C12.8325 9.93123 12.2287 9.93642 11.6683 10.1204C11.3652 10.22 11.073 10.3955 10.4887 10.7464L8.99543 11.6431C8.4434 11.9746 8.16739 12.1404 7.94603 12.348C7.53704 12.7317 7.25064 13.2278 7.12285 13.7738C7.05368 14.0694 7.04814 14.3913 7.03706 15.0351L7.0247 15.7533C7.00969 16.6251 7.00219 17.0609 7.128 17.3352C7.36332 17.8483 7.89744 18.1563 8.45936 18.103C8.75981 18.0746 9.13331 17.8498 9.88031 17.4002L10.3783 17.1005C10.9629 16.7487 11.2552 16.5728 11.5586 16.4729C12.1193 16.2883 12.7237 16.2828 13.2877 16.4572C13.5929 16.5515 13.8883 16.7221 14.4792 17.0632L14.9835 17.3544C15.6995 17.7678 16.0575 17.9745 16.3504 18.0002C16.8979 18.0482 17.4183 17.7537 17.6591 17.2598C17.7879 16.9955 17.7951 16.5821 17.8096 15.7554L17.8231 14.9819C17.835 14.3002 17.8409 13.9594 17.7756 13.6469C17.6548 13.0694 17.3575 12.5438 16.9247 12.1429C16.6905 11.926 16.3953 11.7555 15.8048 11.4146L14.5863 10.7111Z" fill="var(--c--contextuals--background--semantic--neutral--tertiary)"/>
<path d="M61.8577 16.0576C62.6931 16.0576 63.468 16.24 64.1819 16.6045C64.8463 16.9367 65.3785 17.3509 65.7781 17.8471C65.8437 17.9285 65.8294 18.0466 65.7498 18.1145L64.3459 19.3122C64.2574 19.3877 64.1236 19.3717 64.0503 19.2814C63.7909 18.9622 63.4929 18.6926 63.1565 18.4727C62.7616 18.2145 62.3285 18.085 61.8577 18.085C61.5237 18.085 61.2353 18.1462 60.9924 18.2676C60.7646 18.3891 60.5895 18.5486 60.468 18.7461C60.3617 18.9436 60.3088 19.1642 60.3088 19.4072C60.3089 19.7716 60.4375 20.0754 60.6955 20.3184C60.969 20.5462 61.319 20.744 61.7444 20.9111C62.1696 21.0782 62.6174 21.2606 63.0881 21.458C63.5742 21.6403 64.03 21.8682 64.4553 22.1416C64.8806 22.3998 65.2225 22.7489 65.4807 23.1895C65.7541 23.6147 65.8908 24.1537 65.8909 24.8066C65.8909 25.5812 65.7012 26.2423 65.3215 26.7891C64.957 27.3206 64.471 27.7309 63.8635 28.0195C63.2559 28.3082 62.5868 28.4531 61.8577 28.4531C60.8706 28.4531 60.0124 28.2707 59.2834 27.9062C58.6116 27.5563 58.0171 27.0962 57.5 26.5269C57.4264 26.4459 57.4354 26.3208 57.5176 26.2485L58.921 25.0135C59.0067 24.9381 59.1377 24.9498 59.2123 25.0363C59.5425 25.4194 59.9081 25.7384 60.3088 25.9922C60.7645 26.2807 61.2733 26.4248 61.8352 26.4248C62.3669 26.4248 62.7698 26.2881 63.0432 26.0146C63.3316 25.7261 63.4758 25.3768 63.4758 24.9668C63.4758 24.5871 63.339 24.2835 63.0657 24.0557C62.8074 23.8278 62.4656 23.63 62.0403 23.4629C61.6149 23.2958 61.1592 23.1208 60.6731 22.9385C60.2024 22.7411 59.7545 22.5057 59.3293 22.2324C58.9041 21.959 58.5539 21.6098 58.2805 21.1846C58.0223 20.744 57.8938 20.1816 57.8938 19.498C57.8938 18.8601 58.0606 18.283 58.3948 17.7666C58.729 17.2349 59.192 16.8165 59.7844 16.5127C60.3919 16.209 61.0831 16.0577 61.8577 16.0576Z" fill="var(--c--globals--colors--logo-1)"/>
<path d="M71.3215 16.0576C72.1569 16.0577 72.9319 16.24 73.6457 16.6045C74.3101 16.9367 74.8424 17.3509 75.2419 17.8471C75.3075 17.9285 75.2933 18.0466 75.2137 18.1144L73.8098 19.3122C73.7213 19.3877 73.5875 19.3717 73.5141 19.2813C73.2548 18.9622 72.9568 18.6927 72.6204 18.4727C72.2255 18.2145 71.7923 18.085 71.3215 18.085C70.9873 18.085 70.6984 18.1461 70.4553 18.2676C70.2275 18.3891 70.0524 18.5486 69.9309 18.7461C69.8246 18.9435 69.7717 19.1642 69.7717 19.4072C69.7718 19.7717 69.9013 20.0754 70.1594 20.3184C70.4328 20.5462 70.782 20.7441 71.2073 20.9111C71.6326 21.0782 72.0811 21.2605 72.552 21.458C73.0379 21.6403 73.494 21.8682 73.9192 22.1416C74.3443 22.3998 74.6864 22.7491 74.9446 23.1895C75.2178 23.6146 75.3547 24.1538 75.3547 24.8066C75.3547 25.5813 75.1641 26.2422 74.7844 26.7891C74.4199 27.3207 73.934 27.7309 73.3264 28.0195C72.7189 28.3081 72.0506 28.4531 71.3215 28.4531C70.3341 28.4531 69.4755 28.2708 68.7463 27.9062C68.0745 27.5563 67.48 27.0962 66.9629 26.5269C66.8893 26.4459 66.8983 26.3208 66.9805 26.2485L68.3839 25.0135C68.4696 24.9381 68.6006 24.9498 68.6752 25.0363C69.0054 25.4194 69.371 25.7384 69.7717 25.9922C70.2273 26.2807 70.7362 26.4247 71.2981 26.4248C71.8298 26.4248 72.2327 26.2881 72.5061 26.0146C72.7946 25.7261 72.9387 25.3768 72.9387 24.9668C72.9387 24.587 72.802 24.2835 72.5286 24.0557C72.2703 23.8278 71.9285 23.63 71.5032 23.4629C71.0779 23.2958 70.622 23.1207 70.136 22.9385C69.6653 22.7411 69.2174 22.5057 68.7922 22.2324C68.3671 21.9591 68.0178 21.6097 67.7444 21.1846C67.4861 20.744 67.3567 20.1816 67.3567 19.498C67.3567 18.8602 67.5236 18.283 67.8577 17.7666C68.1918 17.235 68.655 16.8165 69.2473 16.5127C69.8549 16.2089 70.5468 16.0576 71.3215 16.0576Z" fill="var(--c--globals--colors--logo-1)"/>
<path d="M86.3313 16.0576C87.1668 16.0576 87.9416 16.2399 88.6555 16.6045C89.32 16.9368 89.8521 17.3508 90.2517 17.8471C90.3173 17.9285 90.303 18.0466 90.2235 18.1144L88.8195 19.3122C88.731 19.3877 88.5973 19.3717 88.5239 19.2814C88.2646 18.9622 87.9666 18.6926 87.6301 18.4727C87.2352 18.2144 86.8022 18.085 86.3313 18.085C85.9971 18.085 85.7081 18.1461 85.4651 18.2676C85.2374 18.3891 85.0631 18.5488 84.9416 18.7461C84.8353 18.9436 84.7815 19.1642 84.7815 19.4072C84.7816 19.7716 84.911 20.0754 85.1692 20.3184C85.4426 20.5462 85.7918 20.7441 86.217 20.9111C86.6424 21.0782 87.0909 21.2605 87.5618 21.458C88.0478 21.6403 88.5036 21.8682 88.9289 22.1416C89.3542 22.3998 89.6961 22.749 89.9543 23.1895C90.2276 23.6147 90.3644 24.1537 90.3645 24.8066C90.3645 25.5811 90.1747 26.2423 89.7952 26.7891C89.4306 27.3207 88.9438 27.7309 88.3362 28.0195C87.7286 28.3081 87.0604 28.4531 86.3313 28.4531C85.3439 28.4531 84.4852 28.2708 83.7561 27.9062C83.0843 27.5563 82.4905 27.0961 81.9736 26.5269C81.9 26.4459 81.909 26.3208 81.9911 26.2485L83.3937 25.0136C83.4793 24.9381 83.6104 24.9498 83.685 25.0363C84.0152 25.4194 84.3808 25.7384 84.7815 25.9922C85.2372 26.2808 85.7468 26.4248 86.3088 26.4248C86.8403 26.4248 87.2425 26.288 87.5159 26.0146C87.8044 25.7261 87.9494 25.3769 87.9495 24.9668C87.9495 24.5872 87.8125 24.2835 87.5393 24.0557C87.2811 23.8279 86.9391 23.63 86.5139 23.4629C86.0886 23.2958 85.6328 23.1207 85.1467 22.9385C84.6758 22.741 84.2273 22.5059 83.802 22.2324C83.3768 21.9591 83.0275 21.6097 82.7541 21.1846C82.4959 20.744 82.3664 20.1816 82.3664 19.498C82.3664 18.86 82.5342 18.2831 82.8684 17.7666C83.2026 17.2349 83.6656 16.8165 84.258 16.5127C84.8656 16.209 85.5567 16.0576 86.3313 16.0576Z" fill="var(--c--globals--colors--logo-1)"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M106.191 16.2861C107.239 16.2861 108.128 16.4757 108.857 16.8555C109.586 17.2352 110.133 17.7594 110.498 18.4277C110.877 19.0961 111.067 19.8481 111.067 20.6836V28.0256C111.067 28.136 110.977 28.2256 110.867 28.2256H108.76C108.65 28.2256 108.56 28.136 108.56 28.0256V27.2662C108.56 27.1718 108.438 27.1271 108.372 27.1948C108.27 27.2997 108.159 27.4002 108.037 27.4961C107.626 27.8303 107.148 28.0892 106.601 28.2715C106.054 28.4689 105.5 28.5673 104.938 28.5674C103.814 28.5674 102.894 28.2639 102.18 27.6562C101.466 27.0486 101.11 26.2355 101.11 25.2178C101.11 24.352 101.436 23.6154 102.089 23.0078C102.743 22.4002 103.684 21.9973 104.915 21.7998L108.394 21.2089C108.49 21.1926 108.56 21.1093 108.56 21.0118V20.6836C108.56 20.0002 108.333 19.4686 107.877 19.0889C107.422 18.694 106.829 18.4962 106.1 18.4961C105.508 18.4961 104.968 18.6256 104.482 18.8838C104.071 19.1094 103.717 19.4102 103.422 19.7865C103.351 19.876 103.222 19.8965 103.132 19.8273L101.606 18.6598C101.52 18.5942 101.501 18.4719 101.568 18.3865C102.052 17.7617 102.666 17.2665 103.411 16.9014C104.231 16.4912 105.158 16.2861 106.191 16.2861ZM105.553 23.4639C104.87 23.5702 104.376 23.7671 104.072 24.0557C103.768 24.329 103.616 24.6784 103.616 25.1035C103.616 25.5288 103.775 25.8789 104.094 26.1523C104.428 26.4258 104.877 26.5625 105.439 26.5625C106.138 26.5625 106.753 26.403 107.285 26.084C107.721 25.8222 108.09 25.4882 108.393 25.083C108.508 24.9295 108.56 24.7399 108.56 24.5483V23.2008C108.56 23.0772 108.449 22.9832 108.327 23.0035L105.553 23.4639Z" fill="var(--c--globals--colors--logo-1)"/>
<path d="M95.9868 16.5412C95.9868 16.6517 96.0764 16.7412 96.1868 16.7412H99.5017C99.6121 16.7412 99.7017 16.8308 99.7017 16.9412V18.7971C99.7017 18.9075 99.6121 18.9971 99.5017 18.9971H96.1868C96.0764 18.9971 95.9868 19.0866 95.9868 19.1971V24.1699C95.9868 24.671 96.0708 25.0659 96.2378 25.3545C96.4048 25.6429 96.6403 25.8555 96.9439 25.9922C97.2477 26.1137 97.605 26.1748 98.0151 26.1748C98.4101 26.1748 98.7446 26.152 99.0181 26.1064C99.1739 26.0718 99.3174 26.0347 99.4485 25.9951C99.5829 25.9545 99.7241 26.0522 99.7241 26.1926V27.9814C99.7241 28.0604 99.6778 28.1326 99.6042 28.1613C99.3605 28.2561 99.0888 28.3233 98.7896 28.3623C98.4554 28.423 98.0605 28.4531 97.605 28.4531C96.6936 28.4531 95.926 28.2936 95.3032 27.9746C94.6959 27.6404 94.2325 27.1617 93.9136 26.5391C93.6099 25.9012 93.4585 25.1115 93.4585 24.1699V19.1971C93.4585 19.0866 93.369 18.9971 93.2585 18.9971H91.6087C91.4982 18.9971 91.4087 18.9075 91.4087 18.7971V16.9412C91.4087 16.8308 91.4982 16.7412 91.6087 16.7412H93.2585C93.369 16.7412 93.4585 16.6517 93.4585 16.5412V14.0711C93.4585 13.9606 93.548 13.8711 93.6585 13.8711H95.7868C95.8973 13.8711 95.9868 13.9606 95.9868 14.0711V16.5412Z" fill="var(--c--globals--colors--logo-1)"/>
<path d="M130.563 16.5412C130.563 16.6517 130.653 16.7412 130.763 16.7412H134.077C134.187 16.7412 134.277 16.8308 134.277 16.9412V18.7971C134.277 18.9075 134.187 18.9971 134.077 18.9971H130.763C130.653 18.9971 130.563 19.0866 130.563 19.1971V24.1699C130.563 24.671 130.646 25.0659 130.813 25.3545C130.98 25.6431 131.216 25.8555 131.52 25.9922C131.824 26.1137 132.18 26.1748 132.59 26.1748C132.985 26.1748 133.32 26.152 133.593 26.1064C133.749 26.0718 133.893 26.0347 134.024 25.9951C134.158 25.9545 134.299 26.0522 134.299 26.1925V27.9813C134.299 28.0604 134.253 28.1326 134.179 28.1613C133.936 28.2561 133.665 28.3233 133.366 28.3623C133.032 28.4231 132.636 28.4531 132.18 28.4531C131.269 28.4531 130.502 28.2936 129.879 27.9746C129.272 27.6404 128.809 27.1619 128.49 26.5391C128.186 25.9011 128.034 25.1116 128.034 24.1699V19.1971C128.034 19.0866 127.944 18.9971 127.834 18.9971H126.092C125.982 18.9971 125.892 18.9075 125.892 18.7971V16.9412C125.892 16.8308 125.982 16.7412 126.092 16.7412H127.834C127.944 16.7412 128.034 16.6517 128.034 16.5412V14.0711C128.034 13.9606 128.123 13.8711 128.234 13.8711H130.363C130.473 13.8711 130.563 13.9606 130.563 14.0711V16.5412Z" fill="var(--c--globals--colors--logo-1)"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M56.8209 27.9534C56.8716 28.0844 56.7749 28.2256 56.6344 28.2256H54.0801C53.9969 28.2256 53.9224 28.1741 53.893 28.0964L52.3355 23.9798C52.3061 23.902 52.2316 23.8506 52.1485 23.8506H45.6707C45.5876 23.8506 45.5132 23.9019 45.4837 23.9796L43.9196 28.0966C43.8901 28.1743 43.8157 28.2256 43.7326 28.2256H41.2002C41.0598 28.2256 40.9631 28.0846 41.0136 27.9536L47.0107 12.4034C47.0404 12.3263 47.1146 12.2754 47.1973 12.2754H50.6144C50.697 12.2754 50.7711 12.3262 50.8009 12.4032L56.8209 27.9534ZM46.5361 21.2104C46.4864 21.3413 46.5831 21.4814 46.7231 21.4814H51.1006C51.2405 21.4814 51.3372 21.3415 51.2876 21.2106L49.1032 15.4395C49.038 15.2673 48.7946 15.2672 48.7292 15.4392L46.5361 21.2104Z" fill="var(--c--globals--colors--logo-1)"/>
<path d="M80.1372 28.0256C80.1372 28.136 80.0477 28.2256 79.9372 28.2256H77.8314C77.7209 28.2256 77.6314 28.136 77.6314 28.0256V16.9412C77.6314 16.8308 77.7209 16.7412 77.8314 16.7412H79.9372C80.0477 16.7412 80.1372 16.8308 80.1372 16.9412V28.0256Z" fill="var(--c--globals--colors--logo-1)"/>
<path d="M120.009 16.2861C120.86 16.2861 121.619 16.4758 122.288 16.8555C122.971 17.2352 123.511 17.7977 123.906 18.542C124.301 19.2863 124.499 20.2208 124.499 21.3447V28.0256C124.499 28.136 124.409 28.2256 124.299 28.2256H122.169C122.059 28.2256 121.969 28.136 121.969 28.0256V21.4355C121.969 20.5546 121.741 19.8709 121.286 19.3848C120.83 18.8835 120.206 18.6328 119.417 18.6328C118.915 18.6329 118.475 18.7313 118.095 18.9287C117.715 19.111 117.389 19.3471 117.116 19.6357C116.919 19.8551 116.745 20.0831 116.592 20.3199C116.498 20.4662 116.455 20.6384 116.455 20.8124V28.0256C116.455 28.136 116.365 28.2256 116.255 28.2256H114.149C114.038 28.2256 113.949 28.136 113.949 28.0256V16.9412C113.949 16.8308 114.038 16.7412 114.149 16.7412H116.255C116.365 16.7412 116.455 16.8308 116.455 16.9412V17.5392C116.455 17.6294 116.566 17.6747 116.631 17.6128C117.008 17.2571 117.443 16.9588 117.936 16.7188C118.528 16.4302 119.219 16.2861 120.009 16.2861Z" fill="var(--c--globals--colors--logo-1)"/>
<path d="M78.9067 11C79.2105 11 79.484 11.0757 79.7271 11.2275C79.9853 11.3794 80.1831 11.5845 80.3198 11.8428C80.4717 12.0858 80.5474 12.3593 80.5474 12.6631C80.5474 12.9668 80.4716 13.2477 80.3198 13.5059C80.1831 13.7641 79.9853 13.9692 79.7271 14.1211C79.484 14.2578 79.2106 14.3262 78.9067 14.3262C78.603 14.3262 78.3222 14.2578 78.064 14.1211C77.8057 13.9692 77.6006 13.7641 77.4487 13.5059C77.2969 13.2477 77.2212 12.9668 77.2212 12.6631C77.2212 12.2074 77.3807 11.82 77.6997 11.501C78.0338 11.167 78.436 11 78.9067 11Z" fill="var(--c--globals--colors--logo-1)"/>
<rect x="143" y="12.5" width="40" height="16" rx="8" fill="var(--c--globals--colors--logo-1)"/>
<rect x="143" y="12.5" width="40" height="16" rx="8" fill="#F7F8FA" fill-opacity="0.9"/>
<path d="M150.133 24.5V16.8H152.399C154.005 16.8 154.984 17.592 154.984 18.89C154.984 19.517 154.698 20.056 154.159 20.408C154.973 20.771 155.424 21.431 155.424 22.234C155.424 23.642 154.335 24.5 152.564 24.5H150.133ZM152.432 18.098H151.695V19.836H152.432C153.026 19.836 153.378 19.517 153.378 18.956C153.378 18.417 153.026 18.098 152.432 18.098ZM152.652 21.134H151.695V23.202H152.652C153.378 23.202 153.818 22.817 153.818 22.168C153.818 21.508 153.378 21.134 152.652 21.134ZM156.917 24.5V16.8H161.405V18.098H158.479V19.924H160.965V21.222H158.479V23.202H161.405V24.5H156.917ZM162.479 18.219V16.8H168.793V18.219H166.417V24.5H164.855V18.219H162.479ZM168.346 24.5L171.261 16.8H173.307L176.222 24.5H174.561L173.824 22.498H170.744L170.007 24.5H168.346ZM171.228 21.167H173.34L172.284 18.274L171.228 21.167Z" fill="#2845C1"/>
<path d="M150.133 24.5V16.8H152.399C154.005 16.8 154.984 17.592 154.984 18.89C154.984 19.517 154.698 20.056 154.159 20.408C154.973 20.771 155.424 21.431 155.424 22.234C155.424 23.642 154.335 24.5 152.564 24.5H150.133ZM152.432 18.098H151.695V19.836H152.432C153.026 19.836 153.378 19.517 153.378 18.956C153.378 18.417 153.026 18.098 152.432 18.098ZM152.652 21.134H151.695V23.202H152.652C153.378 23.202 153.818 22.817 153.818 22.168C153.818 21.508 153.378 21.134 152.652 21.134ZM156.917 24.5V16.8H161.405V18.098H158.479V19.924H160.965V21.222H158.479V23.202H161.405V24.5H156.917ZM162.479 18.219V16.8H168.793V18.219H166.417V24.5H164.855V18.219H162.479ZM168.346 24.5L171.261 16.8H173.307L176.222 24.5H174.561L173.824 22.498H170.744L170.007 24.5H168.346ZM171.228 21.167H173.34L172.284 18.274L171.228 21.167Z" fill="var(--c--globals--colors--logo-1)"/>
</svg>

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 19 KiB

@@ -1,30 +1,30 @@
<svg width="139" height="40" viewBox="0 0 139 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M25.9806 10.0539C25.9806 9.37231 25.9806 9.03151 25.9098 8.72032C25.7791 8.14516 25.4727 7.62489 25.0331 7.23159C24.7953 7.01879 24.4973 6.8535 23.9012 6.52291L22.378 5.6781C21.8149 5.36579 21.5334 5.20963 21.2428 5.12176C20.7061 4.95941 20.1333 4.95941 19.5965 5.12176C19.3059 5.20963 19.0244 5.36579 18.4613 5.6781L17.8331 6.02651C17.0707 6.44937 16.6894 6.66081 16.5148 6.90692C16.1881 7.36724 16.1884 7.98382 16.5155 8.44382C16.6904 8.68977 17.0718 8.90084 17.8347 9.32298L18.3432 9.60438C18.9402 9.93474 19.2387 10.0999 19.4769 10.3127C19.9172 10.7061 20.2241 11.2267 20.3551 11.8023C20.4259 12.1138 20.4259 12.4549 20.4259 13.1372V13.7195C20.4259 14.5463 20.4259 14.9597 20.5502 15.2263C20.7823 15.7243 21.2975 16.0278 21.8457 15.9894C22.139 15.9688 22.5006 15.7684 23.2237 15.3675L23.9004 14.9925C24.4967 14.6619 24.7948 14.4967 25.0327 14.2839C25.4725 13.8905 25.779 13.3702 25.9098 12.7949C25.9806 12.4837 25.9806 12.1428 25.9806 11.461V10.0539Z" fill="#2845C1"/>
<path d="M9.02562 20.1571C8.43534 20.4979 8.1402 20.6683 7.90608 20.8852C7.47335 21.286 7.17597 21.8115 7.05514 22.3888C6.98977 22.7012 6.99563 23.0419 7.00736 23.7234L7.03734 25.4649C7.04843 26.1088 7.05397 26.4307 7.12313 26.7262C7.25092 27.2723 7.53733 27.7683 7.94632 28.152C8.16767 28.3597 8.44369 28.5254 8.99572 28.857L9.61153 29.2268C10.359 29.6756 10.7327 29.9001 11.0331 29.9282C11.5951 29.981 12.1289 29.6724 12.3638 29.1592C12.4893 28.8847 12.4814 28.4489 12.4656 27.5772L12.455 26.9961C12.4426 26.3139 12.4364 25.9728 12.5016 25.6601C12.6221 25.0821 12.9195 24.556 13.3526 24.1548C13.5869 23.9377 13.8823 23.7671 14.4732 23.4259L14.9775 23.1348C15.6935 22.7214 16.0515 22.5147 16.2202 22.2739C16.5355 21.8238 16.5407 21.2259 16.2333 20.7703C16.0689 20.5266 15.7145 20.3137 15.0058 19.8878L14.3426 19.4894C13.7582 19.1382 13.466 18.9627 13.1628 18.863C12.6023 18.6788 11.9984 18.6736 11.4348 18.8479C11.1298 18.9423 10.8346 19.1127 10.2442 19.4536L9.02562 20.1571Z" fill="#2845C1"/>
<path d="M14.8587 29.9461C14.8587 30.6277 14.8587 30.9685 14.9295 31.2797C15.0602 31.8548 15.3666 32.3751 15.8062 32.7684C16.044 32.9812 16.342 33.1465 16.9381 33.4771L18.4613 34.3219C19.0244 34.6342 19.306 34.7904 19.5965 34.8782C20.1333 35.0406 20.7061 35.0406 21.2428 34.8782C21.5334 34.7904 21.8149 34.6342 22.378 34.3219L23.0062 33.9735C23.7686 33.5506 24.1499 33.3392 24.3245 33.0931C24.6512 32.6328 24.6509 32.0162 24.3238 31.5562C24.1489 31.3102 23.7675 31.0992 23.0046 30.677L22.4961 30.3956C21.8991 30.0653 21.6006 29.9001 21.3624 29.6873C20.9222 29.2939 20.6152 28.7733 20.4842 28.1977C20.4134 27.8862 20.4134 27.5451 20.4134 26.8627V26.2805C20.4134 25.4537 20.4134 25.0403 20.2892 24.7737C20.057 24.2757 19.5418 23.9722 18.9936 24.0106C18.7003 24.0312 18.3387 24.2316 17.6156 24.6325L16.9389 25.0075C16.3426 25.3381 16.0445 25.5033 15.8066 25.7161C15.3668 26.1095 15.0603 26.6298 14.9295 27.2051C14.8587 27.5163 14.8587 27.8572 14.8587 28.539V29.9461Z" fill="#2845C1"/>
<path d="M26.2528 29.7889C26.843 30.1297 27.1382 30.3001 27.4431 30.3945C28.0065 30.5688 28.6103 30.5636 29.1707 30.3796C29.4739 30.28 29.766 30.1046 30.3504 29.7537L31.8436 28.8569C32.3956 28.5254 32.6716 28.3597 32.893 28.152C33.302 27.7683 33.5884 27.2723 33.7162 26.7262C33.7853 26.4307 33.7909 26.1088 33.802 25.4649L33.8143 24.7467C33.8293 23.875 33.8368 23.4391 33.711 23.1648C33.4757 22.6518 32.9416 22.3437 32.3797 22.397C32.0792 22.4255 31.7057 22.6503 30.9587 23.0999L30.4608 23.3995C29.8762 23.7514 29.5839 23.9273 29.2805 24.0272C28.7197 24.2118 28.1154 24.2173 27.5513 24.0429C27.2462 23.9485 26.9507 23.778 26.3598 23.4368L25.8556 23.1457C25.1395 22.7323 24.7815 22.5256 24.4886 22.4999C23.9411 22.4519 23.4208 22.7463 23.18 23.2403C23.0511 23.5046 23.0439 23.9179 23.0294 24.7446L23.0159 25.5182C23.004 26.1998 22.9981 26.5407 23.0634 26.8531C23.1842 27.4306 23.4816 27.9562 23.9144 28.3572C24.1485 28.5741 24.4438 28.7445 25.0342 29.0854L26.2528 29.7889Z" fill="#2845C1"/>
<path d="M31.8137 19.8429C32.404 19.5021 32.6991 19.3317 32.9332 19.1148C33.366 18.714 33.6633 18.1885 33.7842 17.6112C33.8495 17.2988 33.8437 16.9581 33.8319 16.2766L33.802 14.5351C33.7909 13.8912 33.7853 13.5693 33.7162 13.2738C33.5884 12.7277 33.302 12.2317 32.893 11.848C32.6716 11.6403 32.3956 11.4746 31.8436 11.1431L31.2278 10.7732C30.4804 10.3244 30.1066 10.0999 29.8062 10.0718C29.2442 10.019 28.7104 10.3276 28.4755 10.8408C28.35 11.1153 28.3579 11.5511 28.3737 12.4228L28.3843 13.0039C28.3967 13.6861 28.4029 14.0272 28.3377 14.3399C28.2172 14.9179 27.9198 15.444 27.4867 15.8452C27.2524 16.0623 26.957 16.2329 26.3661 16.5741L25.8618 16.8652C25.1458 17.2786 24.7878 17.4853 24.6191 17.7261C24.3038 18.1762 24.2986 18.7741 24.606 19.2297C24.7704 19.4734 25.1248 19.6863 25.8335 20.1122L26.4967 20.5106C27.0811 20.8618 27.3733 21.0373 27.6765 21.137C28.237 21.3212 28.8409 21.3264 29.4045 21.1521C29.7095 21.0577 30.0047 20.8873 30.5951 20.5464L31.8137 19.8429Z" fill="#2845C1"/>
<path d="M14.5865 10.2111C13.9962 9.87031 13.7011 9.69991 13.3962 9.60558C12.8327 9.43123 12.229 9.43642 11.6686 9.62044C11.3654 9.72001 11.0732 9.89546 10.4889 10.2464L8.99568 11.1431C8.44365 11.4746 8.16763 11.6404 7.94627 11.848C7.53729 12.2317 7.25088 12.7278 7.12309 13.2738C7.05393 13.5694 7.04839 13.8913 7.0373 14.5351L7.02494 15.2533C7.00993 16.1251 7.00243 16.5609 7.12825 16.8352C7.36357 17.3483 7.89769 17.6563 8.45961 17.603C8.76005 17.5746 9.13355 17.3498 9.88056 16.9002L10.3785 16.6005C10.9631 16.2487 11.2554 16.0728 11.5588 15.9729C12.1196 15.7883 12.7239 15.7828 13.2879 15.9572C13.5931 16.0515 13.8885 16.2221 14.4794 16.5632L14.9837 16.8544C15.6997 17.2678 16.0577 17.4745 16.3507 17.5002C16.8981 17.5482 17.4185 17.2537 17.6593 16.7598C17.7882 16.4955 17.7954 16.0821 17.8098 15.2554L17.8233 14.4819C17.8352 13.8002 17.8412 13.4594 17.7758 13.1469C17.6551 12.5694 17.3577 12.0438 16.9249 11.6429C16.6907 11.426 16.3955 11.2555 15.8051 10.9146L14.5865 10.2111Z" fill="#C83F49"/>
<rect x="8" y="27" width="24" height="9" rx="4.5" fill="#EEF1F4"/>
<path d="M25.9806 10.0539C25.9806 9.37231 25.9806 9.03151 25.9098 8.72032C25.7791 8.14516 25.4727 7.62489 25.0331 7.23159C24.7953 7.01879 24.4973 6.8535 23.9012 6.52291L22.378 5.6781C21.8149 5.36579 21.5334 5.20963 21.2428 5.12176C20.7061 4.95941 20.1333 4.95941 19.5965 5.12176C19.3059 5.20963 19.0244 5.36579 18.4613 5.6781L17.8331 6.02651C17.0707 6.44937 16.6894 6.66081 16.5148 6.90692C16.1881 7.36724 16.1884 7.98382 16.5155 8.44382C16.6904 8.68977 17.0718 8.90084 17.8347 9.32298L18.3432 9.60438C18.9402 9.93474 19.2387 10.0999 19.4769 10.3127C19.9172 10.7061 20.2241 11.2267 20.3551 11.8023C20.4259 12.1138 20.4259 12.4549 20.4259 13.1372V13.7195C20.4259 14.5463 20.4259 14.9597 20.5502 15.2263C20.7823 15.7243 21.2975 16.0278 21.8457 15.9894C22.139 15.9688 22.5006 15.7684 23.2237 15.3675L23.9004 14.9925C24.4967 14.6619 24.7948 14.4967 25.0327 14.2839C25.4725 13.8905 25.779 13.3702 25.9098 12.7949C25.9806 12.4837 25.9806 12.1428 25.9806 11.461V10.0539Z" fill="var(--c--globals--colors--logo-1)"/>
<path d="M9.02562 20.1571C8.43534 20.4979 8.1402 20.6683 7.90608 20.8852C7.47335 21.286 7.17597 21.8115 7.05514 22.3888C6.98977 22.7012 6.99563 23.0419 7.00736 23.7234L7.03734 25.4649C7.04843 26.1088 7.05397 26.4307 7.12313 26.7262C7.25092 27.2723 7.53733 27.7683 7.94632 28.152C8.16767 28.3597 8.44369 28.5254 8.99572 28.857L9.61153 29.2268C10.359 29.6756 10.7327 29.9001 11.0331 29.9282C11.5951 29.981 12.1289 29.6724 12.3638 29.1592C12.4893 28.8847 12.4814 28.4489 12.4656 27.5772L12.455 26.9961C12.4426 26.3139 12.4364 25.9728 12.5016 25.6601C12.6221 25.0821 12.9195 24.556 13.3526 24.1548C13.5869 23.9377 13.8823 23.7671 14.4732 23.4259L14.9775 23.1348C15.6935 22.7214 16.0515 22.5147 16.2202 22.2739C16.5355 21.8238 16.5407 21.2259 16.2333 20.7703C16.0689 20.5266 15.7145 20.3137 15.0058 19.8878L14.3426 19.4894C13.7582 19.1382 13.466 18.9627 13.1628 18.863C12.6023 18.6788 11.9984 18.6736 11.4348 18.8479C11.1298 18.9423 10.8346 19.1127 10.2442 19.4536L9.02562 20.1571Z" fill="var(--c--globals--colors--logo-1)"/>
<path d="M14.8587 29.9461C14.8587 30.6277 14.8587 30.9685 14.9295 31.2797C15.0602 31.8548 15.3666 32.3751 15.8062 32.7684C16.044 32.9812 16.342 33.1465 16.9381 33.4771L18.4613 34.3219C19.0244 34.6342 19.306 34.7904 19.5965 34.8782C20.1333 35.0406 20.7061 35.0406 21.2428 34.8782C21.5334 34.7904 21.8149 34.6342 22.378 34.3219L23.0062 33.9735C23.7686 33.5506 24.1499 33.3392 24.3245 33.0931C24.6512 32.6328 24.6509 32.0162 24.3238 31.5562C24.1489 31.3102 23.7675 31.0992 23.0046 30.677L22.4961 30.3956C21.8991 30.0653 21.6006 29.9001 21.3624 29.6873C20.9222 29.2939 20.6152 28.7733 20.4842 28.1977C20.4134 27.8862 20.4134 27.5451 20.4134 26.8627V26.2805C20.4134 25.4537 20.4134 25.0403 20.2892 24.7737C20.057 24.2757 19.5418 23.9722 18.9936 24.0106C18.7003 24.0312 18.3387 24.2316 17.6156 24.6325L16.9389 25.0075C16.3426 25.3381 16.0445 25.5033 15.8066 25.7161C15.3668 26.1095 15.0603 26.6298 14.9295 27.2051C14.8587 27.5163 14.8587 27.8572 14.8587 28.539V29.9461Z" fill="var(--c--globals--colors--logo-1)"/>
<path d="M26.2528 29.7889C26.843 30.1297 27.1382 30.3001 27.4431 30.3945C28.0065 30.5688 28.6103 30.5636 29.1707 30.3796C29.4739 30.28 29.766 30.1046 30.3504 29.7537L31.8436 28.8569C32.3956 28.5254 32.6716 28.3597 32.893 28.152C33.302 27.7683 33.5884 27.2723 33.7162 26.7262C33.7853 26.4307 33.7909 26.1088 33.802 25.4649L33.8143 24.7467C33.8293 23.875 33.8368 23.4391 33.711 23.1648C33.4757 22.6518 32.9416 22.3437 32.3797 22.397C32.0792 22.4255 31.7057 22.6503 30.9587 23.0999L30.4608 23.3995C29.8762 23.7514 29.5839 23.9273 29.2805 24.0272C28.7197 24.2118 28.1154 24.2173 27.5513 24.0429C27.2462 23.9485 26.9507 23.778 26.3598 23.4368L25.8556 23.1457C25.1395 22.7323 24.7815 22.5256 24.4886 22.4999C23.9411 22.4519 23.4208 22.7463 23.18 23.2403C23.0511 23.5046 23.0439 23.9179 23.0294 24.7446L23.0159 25.5182C23.004 26.1998 22.9981 26.5407 23.0634 26.8531C23.1842 27.4306 23.4816 27.9562 23.9144 28.3572C24.1485 28.5741 24.4438 28.7445 25.0342 29.0854L26.2528 29.7889Z" fill="var(--c--globals--colors--logo-1)"/>
<path d="M31.8137 19.8429C32.404 19.5021 32.6991 19.3317 32.9332 19.1148C33.366 18.714 33.6633 18.1885 33.7842 17.6112C33.8495 17.2988 33.8437 16.9581 33.8319 16.2766L33.802 14.5351C33.7909 13.8912 33.7853 13.5693 33.7162 13.2738C33.5884 12.7277 33.302 12.2317 32.893 11.848C32.6716 11.6403 32.3956 11.4746 31.8436 11.1431L31.2278 10.7732C30.4804 10.3244 30.1066 10.0999 29.8062 10.0718C29.2442 10.019 28.7104 10.3276 28.4755 10.8408C28.35 11.1153 28.3579 11.5511 28.3737 12.4228L28.3843 13.0039C28.3967 13.6861 28.4029 14.0272 28.3377 14.3399C28.2172 14.9179 27.9198 15.444 27.4867 15.8452C27.2524 16.0623 26.957 16.2329 26.3661 16.5741L25.8618 16.8652C25.1458 17.2786 24.7878 17.4853 24.6191 17.7261C24.3038 18.1762 24.2986 18.7741 24.606 19.2297C24.7704 19.4734 25.1248 19.6863 25.8335 20.1122L26.4967 20.5106C27.0811 20.8618 27.3733 21.0373 27.6765 21.137C28.237 21.3212 28.8409 21.3264 29.4045 21.1521C29.7095 21.0577 30.0047 20.8873 30.5951 20.5464L31.8137 19.8429Z" fill="var(--c--globals--colors--logo-1)"/>
<path d="M14.5865 10.2111C13.9962 9.87031 13.7011 9.69991 13.3962 9.60558C12.8327 9.43123 12.229 9.43642 11.6686 9.62044C11.3654 9.72001 11.0732 9.89546 10.4889 10.2464L8.99568 11.1431C8.44365 11.4746 8.16763 11.6404 7.94627 11.848C7.53729 12.2317 7.25088 12.7278 7.12309 13.2738C7.05393 13.5694 7.04839 13.8913 7.0373 14.5351L7.02494 15.2533C7.00993 16.1251 7.00243 16.5609 7.12825 16.8352C7.36357 17.3483 7.89769 17.6563 8.45961 17.603C8.76005 17.5746 9.13355 17.3498 9.88056 16.9002L10.3785 16.6005C10.9631 16.2487 11.2554 16.0728 11.5588 15.9729C12.1196 15.7883 12.7239 15.7828 13.2879 15.9572C13.5931 16.0515 13.8885 16.2221 14.4794 16.5632L14.9837 16.8544C15.6997 17.2678 16.0577 17.4745 16.3507 17.5002C16.8981 17.5482 17.4185 17.2537 17.6593 16.7598C17.7882 16.4955 17.7954 16.0821 17.8098 15.2554L17.8233 14.4819C17.8352 13.8002 17.8412 13.4594 17.7758 13.1469C17.6551 12.5694 17.3577 12.0438 16.9249 11.6429C16.6907 11.426 16.3955 11.2555 15.8051 10.9146L14.5865 10.2111Z" fill="var(--c--globals--colors--logo-2)"/>
<rect x="8" y="27" width="24" height="9" rx="4.5" fill="var(--c--contextuals--background--semantic--neutral--tertiary)"/>
<rect x="8" y="27" width="24" height="9" rx="4.5" fill="url(#paint0_linear_6479_3681)" fill-opacity="0.1"/>
<path d="M12.3425 33.7362C12.2926 33.7362 12.2676 33.7362 12.2486 33.7265C12.2318 33.718 12.2182 33.7044 12.2097 33.6876C12.2 33.6686 12.2 33.6436 12.2 33.5937V29.2425C12.2 29.1926 12.2 29.1677 12.2097 29.1486C12.2182 29.1318 12.2318 29.1182 12.2486 29.1097C12.2676 29.1 12.2926 29.1 12.3425 29.1H13.5643C14.5313 29.1 15.1208 29.5768 15.1208 30.3584C15.1208 30.648 15.0195 30.9064 14.8257 31.1068C14.7572 31.1777 14.7229 31.2131 14.7171 31.2364C14.7109 31.2615 14.7123 31.274 14.7237 31.2972C14.7343 31.3187 14.7739 31.3451 14.8532 31.3978C15.1981 31.6275 15.3857 31.9699 15.3857 32.3719C15.3857 33.2196 14.73 33.7362 13.6637 33.7362H12.3425ZM13.5842 29.8815H13.283C13.2331 29.8815 13.2081 29.8815 13.1891 29.8912C13.1723 29.8998 13.1587 29.9134 13.1502 29.9301C13.1405 29.9492 13.1405 29.9741 13.1405 30.024V30.7855C13.1405 30.8354 13.1405 30.8603 13.1502 30.8794C13.1587 30.8961 13.1723 30.9097 13.1891 30.9183C13.2081 30.928 13.2331 30.928 13.283 30.928H13.5842C13.9419 30.928 14.1538 30.7359 14.1538 30.3981C14.1538 30.0736 13.9419 29.8815 13.5842 29.8815ZM13.7167 31.7095H13.283C13.2331 31.7095 13.2081 31.7095 13.1891 31.7192C13.1723 31.7278 13.1587 31.7414 13.1502 31.7582C13.1405 31.7772 13.1405 31.8022 13.1405 31.852V32.8122C13.1405 32.8621 13.1405 32.887 13.1502 32.9061C13.1587 32.9228 13.1723 32.9365 13.1891 32.945C13.2081 32.9547 13.2331 32.9547 13.283 32.9547H13.7167C14.1538 32.9547 14.4187 32.7229 14.4187 32.3321C14.4187 31.9347 14.1538 31.7095 13.7167 31.7095Z" fill="#2845C1"/>
<path d="M16.4271 33.7362C16.3772 33.7362 16.3523 33.7362 16.3333 33.7265C16.3165 33.718 16.3029 33.7044 16.2943 33.6876C16.2846 33.6686 16.2846 33.6436 16.2846 33.5937V29.2425C16.2846 29.1926 16.2846 29.1677 16.2943 29.1486C16.3029 29.1318 16.3165 29.1182 16.3333 29.1097C16.3523 29.1 16.3772 29.1 16.4271 29.1H18.8444C18.8943 29.1 18.9192 29.1 18.9383 29.1097C18.955 29.1182 18.9687 29.1318 18.9772 29.1486C18.9869 29.1677 18.9869 29.1926 18.9869 29.2425V29.739C18.9869 29.7889 18.9869 29.8138 18.9772 29.8329C18.9687 29.8496 18.955 29.8633 18.9383 29.8718C18.9192 29.8815 18.8943 29.8815 18.8444 29.8815H17.3676C17.3177 29.8815 17.2928 29.8815 17.2738 29.8912C17.257 29.8998 17.2434 29.9134 17.2348 29.9301C17.2251 29.9492 17.2251 29.9741 17.2251 30.024V30.8385C17.2251 30.8884 17.2251 30.9133 17.2348 30.9323C17.2434 30.9491 17.257 30.9627 17.2738 30.9713C17.2928 30.981 17.3177 30.981 17.3676 30.981H18.5795C18.6294 30.981 18.6543 30.981 18.6733 30.9907C18.6901 30.9992 18.7037 31.0128 18.7123 31.0296C18.722 31.0487 18.722 31.0736 18.722 31.1235V31.62C18.722 31.6699 18.722 31.6948 18.7123 31.7139C18.7037 31.7306 18.6901 31.7443 18.6733 31.7528C18.6543 31.7625 18.6294 31.7625 18.5795 31.7625H17.3676C17.3177 31.7625 17.2928 31.7625 17.2738 31.7722C17.257 31.7808 17.2434 31.7944 17.2348 31.8111C17.2251 31.8302 17.2251 31.8551 17.2251 31.905V32.8122C17.2251 32.8621 17.2251 32.887 17.2348 32.9061C17.2434 32.9228 17.257 32.9365 17.2738 32.945C17.2928 32.9547 17.3177 32.9547 17.3676 32.9547H18.8444C18.8943 32.9547 18.9192 32.9547 18.9383 32.9644C18.955 32.9729 18.9687 32.9866 18.9772 33.0033C18.9869 33.0224 18.9869 33.0473 18.9869 33.0972V33.5937C18.9869 33.6436 18.9869 33.6686 18.9772 33.6876C18.9687 33.7044 18.955 33.718 18.9383 33.7265C18.9192 33.7362 18.8943 33.7362 18.8444 33.7362H16.4271Z" fill="#2845C1"/>
<path d="M19.7762 29.9544C19.7263 29.9544 19.7014 29.9544 19.6823 29.9447C19.6656 29.9361 19.652 29.9225 19.6434 29.9057C19.6337 29.8867 19.6337 29.8618 19.6337 29.8119V29.2425C19.6337 29.1926 19.6337 29.1677 19.6434 29.1486C19.652 29.1318 19.6656 29.1182 19.6823 29.1097C19.7014 29.1 19.7263 29.1 19.7762 29.1H23.2929C23.3428 29.1 23.3678 29.1 23.3868 29.1097C23.4036 29.1182 23.4172 29.1318 23.4257 29.1486C23.4354 29.1677 23.4354 29.1926 23.4354 29.2425V29.8119C23.4354 29.8618 23.4354 29.8867 23.4257 29.9057C23.4172 29.9225 23.4036 29.9361 23.3868 29.9447C23.3678 29.9544 23.3428 29.9544 23.2929 29.9544H22.1473C22.0974 29.9544 22.0725 29.9544 22.0535 29.9641C22.0367 29.9726 22.0231 29.9862 22.0145 30.003C22.0048 30.0221 22.0048 30.047 22.0048 30.0969V33.5937C22.0048 33.6436 22.0048 33.6686 21.9951 33.6876C21.9866 33.7044 21.973 33.718 21.9562 33.7265C21.9371 33.7362 21.9122 33.7362 21.8623 33.7362H21.2068C21.1569 33.7362 21.132 33.7362 21.113 33.7265C21.0962 33.718 21.0826 33.7044 21.074 33.6876C21.0643 33.6686 21.0643 33.6436 21.0643 33.5937V30.0969C21.0643 30.047 21.0643 30.0221 21.0546 30.003C21.0461 29.9862 21.0325 29.9726 21.0157 29.9641C20.9966 29.9544 20.9717 29.9544 20.9218 29.9544H19.7762Z" fill="#2845C1"/>
<path d="M23.3724 33.7362C23.3031 33.7362 23.2685 33.7362 23.2463 33.7217C23.2268 33.709 23.2131 33.6891 23.2081 33.6664C23.2024 33.6405 23.2146 33.6081 23.2392 33.5433L24.8864 29.192C24.899 29.1588 24.9053 29.1422 24.9161 29.13C24.9257 29.1192 24.9377 29.1109 24.9512 29.1058C24.9665 29.1 24.9842 29.1 25.0197 29.1H26.0548C26.0903 29.1 26.108 29.1 26.1233 29.1058C26.1368 29.1109 26.1488 29.1192 26.1584 29.13C26.1692 29.1422 26.1755 29.1588 26.188 29.192L27.8353 33.5433C27.8598 33.6081 27.8721 33.6405 27.8664 33.6664C27.8613 33.6891 27.8476 33.709 27.8282 33.7217C27.806 33.7362 27.7713 33.7362 27.702 33.7362H27.0076C26.9718 33.7362 26.9539 33.7362 26.9385 33.7304C26.925 33.7252 26.9129 33.7167 26.9033 33.7058C26.8925 33.6934 26.8863 33.6766 26.8739 33.643L26.4988 32.6241C26.4865 32.5905 26.4803 32.5737 26.4694 32.5613C26.4599 32.5503 26.4478 32.5419 26.4342 32.5367C26.4188 32.5308 26.4009 32.5308 26.3651 32.5308H24.7094C24.6736 32.5308 24.6556 32.5308 24.6403 32.5367C24.6267 32.5419 24.6146 32.5503 24.605 32.5613C24.5942 32.5737 24.588 32.5905 24.5757 32.6241L24.2006 33.643C24.1882 33.6766 24.182 33.6934 24.1712 33.7058C24.1616 33.7167 24.1495 33.7252 24.1359 33.7304C24.1206 33.7362 24.1027 33.7362 24.0668 33.7362H23.3724ZM24.9713 31.538C24.9478 31.6024 24.936 31.6346 24.9419 31.6603C24.9471 31.6828 24.9608 31.7024 24.9802 31.715C25.0023 31.7294 25.0366 31.7294 25.1051 31.7294H25.9694C26.0379 31.7294 26.0721 31.7294 26.0943 31.715C26.1136 31.7024 26.1274 31.6828 26.1325 31.6603C26.1385 31.6346 26.1267 31.6024 26.1032 31.538L25.6711 30.3542C25.6298 30.2411 25.6092 30.1846 25.579 30.1686C25.5529 30.1547 25.5216 30.1547 25.4955 30.1686C25.4653 30.1846 25.4447 30.2411 25.4034 30.3542L24.9713 31.538Z" fill="#2845C1"/>
<path d="M61.8577 15.8076C62.6931 15.8076 63.468 15.99 64.1819 16.3545C64.8463 16.6867 65.3785 17.1009 65.7781 17.5971C65.8437 17.6785 65.8294 17.7966 65.7498 17.8645L64.3459 19.0622C64.2574 19.1377 64.1236 19.1217 64.0503 19.0314C63.7909 18.7122 63.4929 18.4426 63.1565 18.2227C62.7616 17.9645 62.3285 17.835 61.8577 17.835C61.5237 17.835 61.2353 17.8962 60.9924 18.0176C60.7646 18.1391 60.5895 18.2986 60.468 18.4961C60.3617 18.6936 60.3088 18.9142 60.3088 19.1572C60.3089 19.5216 60.4375 19.8254 60.6955 20.0684C60.969 20.2962 61.319 20.494 61.7444 20.6611C62.1696 20.8282 62.6174 21.0106 63.0881 21.208C63.5742 21.3903 64.03 21.6182 64.4553 21.8916C64.8806 22.1498 65.2225 22.4989 65.4807 22.9395C65.7541 23.3647 65.8908 23.9037 65.8909 24.5566C65.8909 25.3312 65.7012 25.9923 65.3215 26.5391C64.957 27.0706 64.471 27.4809 63.8635 27.7695C63.2559 28.0582 62.5868 28.2031 61.8577 28.2031C60.8706 28.2031 60.0124 28.0207 59.2834 27.6562C58.6116 27.3063 58.0171 26.8462 57.5 26.2769C57.4264 26.1959 57.4354 26.0708 57.5176 25.9985L58.921 24.7635C59.0067 24.6881 59.1377 24.6998 59.2123 24.7863C59.5425 25.1694 59.9081 25.4884 60.3088 25.7422C60.7645 26.0307 61.2733 26.1748 61.8352 26.1748C62.3669 26.1748 62.7698 26.0381 63.0432 25.7646C63.3316 25.4761 63.4758 25.1268 63.4758 24.7168C63.4758 24.3371 63.339 24.0335 63.0657 23.8057C62.8074 23.5778 62.4656 23.38 62.0403 23.2129C61.6149 23.0458 61.1592 22.8708 60.6731 22.6885C60.2024 22.4911 59.7545 22.2557 59.3293 21.9824C58.9041 21.709 58.5539 21.3598 58.2805 20.9346C58.0223 20.494 57.8938 19.9316 57.8938 19.248C57.8938 18.6101 58.0606 18.033 58.3948 17.5166C58.729 16.9849 59.192 16.5665 59.7844 16.2627C60.3919 15.959 61.0831 15.8077 61.8577 15.8076Z" fill="#2845C1"/>
<path d="M71.3215 15.8076C72.1569 15.8077 72.9319 15.99 73.6457 16.3545C74.3101 16.6867 74.8424 17.1009 75.2419 17.5971C75.3075 17.6785 75.2933 17.7966 75.2137 17.8644L73.8098 19.0622C73.7213 19.1377 73.5875 19.1217 73.5141 19.0313C73.2548 18.7122 72.9568 18.4427 72.6204 18.2227C72.2255 17.9645 71.7923 17.835 71.3215 17.835C70.9873 17.835 70.6984 17.8961 70.4553 18.0176C70.2275 18.1391 70.0524 18.2986 69.9309 18.4961C69.8246 18.6935 69.7717 18.9142 69.7717 19.1572C69.7718 19.5217 69.9013 19.8254 70.1594 20.0684C70.4328 20.2962 70.782 20.4941 71.2073 20.6611C71.6326 20.8282 72.0811 21.0105 72.552 21.208C73.0379 21.3903 73.494 21.6182 73.9192 21.8916C74.3443 22.1498 74.6864 22.4991 74.9446 22.9395C75.2178 23.3646 75.3547 23.9038 75.3547 24.5566C75.3547 25.3313 75.1641 25.9922 74.7844 26.5391C74.4199 27.0707 73.934 27.4809 73.3264 27.7695C72.7189 28.0581 72.0506 28.2031 71.3215 28.2031C70.3341 28.2031 69.4755 28.0208 68.7463 27.6562C68.0745 27.3063 67.48 26.8462 66.9629 26.2769C66.8893 26.1959 66.8983 26.0708 66.9805 25.9985L68.3839 24.7635C68.4696 24.6881 68.6006 24.6998 68.6752 24.7863C69.0054 25.1694 69.371 25.4884 69.7717 25.7422C70.2273 26.0307 70.7362 26.1747 71.2981 26.1748C71.8298 26.1748 72.2327 26.0381 72.5061 25.7646C72.7946 25.4761 72.9387 25.1268 72.9387 24.7168C72.9387 24.337 72.802 24.0335 72.5286 23.8057C72.2703 23.5778 71.9285 23.38 71.5032 23.2129C71.0779 23.0458 70.622 22.8707 70.136 22.6885C69.6653 22.4911 69.2174 22.2557 68.7922 21.9824C68.3671 21.7091 68.0178 21.3597 67.7444 20.9346C67.4861 20.494 67.3567 19.9316 67.3567 19.248C67.3567 18.6102 67.5236 18.033 67.8577 17.5166C68.1918 16.985 68.655 16.5665 69.2473 16.2627C69.8549 15.9589 70.5468 15.8076 71.3215 15.8076Z" fill="#2845C1"/>
<path d="M86.3313 15.8076C87.1668 15.8076 87.9416 15.9899 88.6555 16.3545C89.32 16.6868 89.8521 17.1008 90.2517 17.5971C90.3173 17.6785 90.303 17.7966 90.2235 17.8644L88.8195 19.0622C88.731 19.1377 88.5973 19.1217 88.5239 19.0314C88.2646 18.7122 87.9666 18.4426 87.6301 18.2227C87.2352 17.9644 86.8022 17.835 86.3313 17.835C85.9971 17.835 85.7081 17.8961 85.4651 18.0176C85.2374 18.1391 85.0631 18.2988 84.9416 18.4961C84.8353 18.6936 84.7815 18.9142 84.7815 19.1572C84.7816 19.5216 84.911 19.8254 85.1692 20.0684C85.4426 20.2962 85.7918 20.4941 86.217 20.6611C86.6424 20.8282 87.0909 21.0105 87.5618 21.208C88.0478 21.3903 88.5036 21.6182 88.9289 21.8916C89.3542 22.1498 89.6961 22.499 89.9543 22.9395C90.2276 23.3647 90.3644 23.9037 90.3645 24.5566C90.3645 25.3311 90.1747 25.9923 89.7952 26.5391C89.4306 27.0707 88.9438 27.4809 88.3362 27.7695C87.7286 28.0581 87.0604 28.2031 86.3313 28.2031C85.3439 28.2031 84.4852 28.0208 83.7561 27.6562C83.0843 27.3063 82.4905 26.8461 81.9736 26.2769C81.9 26.1959 81.909 26.0708 81.9911 25.9985L83.3937 24.7636C83.4793 24.6881 83.6104 24.6998 83.685 24.7863C84.0152 25.1694 84.3808 25.4884 84.7815 25.7422C85.2372 26.0308 85.7468 26.1748 86.3088 26.1748C86.8403 26.1748 87.2425 26.038 87.5159 25.7646C87.8044 25.4761 87.9494 25.1269 87.9495 24.7168C87.9495 24.3372 87.8125 24.0335 87.5393 23.8057C87.2811 23.5779 86.9391 23.38 86.5139 23.2129C86.0886 23.0458 85.6328 22.8707 85.1467 22.6885C84.6758 22.491 84.2273 22.2559 83.802 21.9824C83.3768 21.7091 83.0275 21.3597 82.7541 20.9346C82.4959 20.494 82.3664 19.9316 82.3664 19.248C82.3664 18.61 82.5342 18.0331 82.8684 17.5166C83.2026 16.9849 83.6656 16.5665 84.258 16.2627C84.8656 15.959 85.5567 15.8076 86.3313 15.8076Z" fill="#2845C1"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M106.191 16.0361C107.239 16.0361 108.128 16.2257 108.857 16.6055C109.586 16.9852 110.133 17.5094 110.498 18.1777C110.877 18.8461 111.067 19.5981 111.067 20.4336V27.7756C111.067 27.886 110.977 27.9756 110.867 27.9756H108.76C108.65 27.9756 108.56 27.886 108.56 27.7756V27.0162C108.56 26.9218 108.438 26.8771 108.372 26.9448C108.27 27.0497 108.159 27.1502 108.037 27.2461C107.626 27.5803 107.148 27.8392 106.601 28.0215C106.054 28.2189 105.5 28.3173 104.938 28.3174C103.814 28.3174 102.894 28.0139 102.18 27.4062C101.466 26.7986 101.11 25.9855 101.11 24.9678C101.11 24.102 101.436 23.3654 102.089 22.7578C102.743 22.1502 103.684 21.7473 104.915 21.5498L108.394 20.9589C108.49 20.9426 108.56 20.8593 108.56 20.7618V20.4336C108.56 19.7502 108.333 19.2186 107.877 18.8389C107.422 18.444 106.829 18.2462 106.1 18.2461C105.508 18.2461 104.968 18.3756 104.482 18.6338C104.071 18.8594 103.717 19.1602 103.422 19.5365C103.351 19.626 103.222 19.6465 103.132 19.5773L101.606 18.4098C101.52 18.3442 101.501 18.2219 101.568 18.1365C102.052 17.5117 102.666 17.0165 103.411 16.6514C104.231 16.2412 105.158 16.0361 106.191 16.0361ZM105.553 23.2139C104.87 23.3202 104.376 23.5171 104.072 23.8057C103.768 24.079 103.616 24.4284 103.616 24.8535C103.616 25.2788 103.775 25.6289 104.094 25.9023C104.428 26.1758 104.877 26.3125 105.439 26.3125C106.138 26.3125 106.753 26.153 107.285 25.834C107.721 25.5722 108.09 25.2382 108.393 24.833C108.508 24.6795 108.56 24.4899 108.56 24.2983V22.9508C108.56 22.8272 108.449 22.7332 108.327 22.7535L105.553 23.2139Z" fill="#2845C1"/>
<path d="M95.9868 16.2912C95.9868 16.4017 96.0764 16.4912 96.1868 16.4912H99.5017C99.6121 16.4912 99.7017 16.5808 99.7017 16.6912V18.5471C99.7017 18.6575 99.6121 18.7471 99.5017 18.7471H96.1868C96.0764 18.7471 95.9868 18.8366 95.9868 18.9471V23.9199C95.9868 24.421 96.0708 24.8159 96.2378 25.1045C96.4048 25.3929 96.6403 25.6055 96.9439 25.7422C97.2477 25.8637 97.605 25.9248 98.0151 25.9248C98.4101 25.9248 98.7446 25.902 99.0181 25.8564C99.1739 25.8218 99.3174 25.7847 99.4485 25.7451C99.5829 25.7045 99.7241 25.8022 99.7241 25.9426V27.7314C99.7241 27.8104 99.6778 27.8826 99.6042 27.9113C99.3605 28.0061 99.0888 28.0733 98.7896 28.1123C98.4554 28.173 98.0605 28.2031 97.605 28.2031C96.6936 28.2031 95.926 28.0436 95.3032 27.7246C94.6959 27.3904 94.2325 26.9117 93.9136 26.2891C93.6099 25.6512 93.4585 24.8615 93.4585 23.9199V18.9471C93.4585 18.8366 93.369 18.7471 93.2585 18.7471H91.6087C91.4982 18.7471 91.4087 18.6575 91.4087 18.5471V16.6912C91.4087 16.5808 91.4982 16.4912 91.6087 16.4912H93.2585C93.369 16.4912 93.4585 16.4017 93.4585 16.2912V13.8211C93.4585 13.7106 93.548 13.6211 93.6585 13.6211H95.7868C95.8973 13.6211 95.9868 13.7106 95.9868 13.8211V16.2912Z" fill="#2845C1"/>
<path d="M130.563 16.2912C130.563 16.4017 130.653 16.4912 130.763 16.4912H134.077C134.187 16.4912 134.277 16.5808 134.277 16.6912V18.5471C134.277 18.6575 134.187 18.7471 134.077 18.7471H130.763C130.653 18.7471 130.563 18.8366 130.563 18.9471V23.9199C130.563 24.421 130.646 24.8159 130.813 25.1045C130.98 25.3931 131.216 25.6055 131.52 25.7422C131.824 25.8637 132.18 25.9248 132.59 25.9248C132.985 25.9248 133.32 25.902 133.593 25.8564C133.749 25.8218 133.893 25.7847 134.024 25.7451C134.158 25.7045 134.299 25.8022 134.299 25.9425V27.7313C134.299 27.8104 134.253 27.8826 134.179 27.9113C133.936 28.0061 133.665 28.0733 133.366 28.1123C133.032 28.1731 132.636 28.2031 132.18 28.2031C131.269 28.2031 130.502 28.0436 129.879 27.7246C129.272 27.3904 128.809 26.9119 128.49 26.2891C128.186 25.6511 128.034 24.8616 128.034 23.9199V18.9471C128.034 18.8366 127.944 18.7471 127.834 18.7471H126.092C125.982 18.7471 125.892 18.6575 125.892 18.5471V16.6912C125.892 16.5808 125.982 16.4912 126.092 16.4912H127.834C127.944 16.4912 128.034 16.4017 128.034 16.2912V13.8211C128.034 13.7106 128.123 13.6211 128.234 13.6211H130.363C130.473 13.6211 130.563 13.7106 130.563 13.8211V16.2912Z" fill="#2845C1"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M56.8209 27.7034C56.8716 27.8344 56.7749 27.9756 56.6344 27.9756H54.0801C53.9969 27.9756 53.9224 27.9241 53.893 27.8464L52.3355 23.7298C52.3061 23.652 52.2316 23.6006 52.1485 23.6006H45.6707C45.5876 23.6006 45.5132 23.6519 45.4837 23.7296L43.9196 27.8466C43.8901 27.9243 43.8157 27.9756 43.7326 27.9756H41.2002C41.0598 27.9756 40.9631 27.8346 41.0136 27.7036L47.0107 12.1534C47.0404 12.0763 47.1146 12.0254 47.1973 12.0254H50.6144C50.697 12.0254 50.7711 12.0762 50.8009 12.1532L56.8209 27.7034ZM46.5361 20.9604C46.4864 21.0913 46.5831 21.2314 46.7231 21.2314H51.1006C51.2405 21.2314 51.3372 21.0915 51.2876 20.9606L49.1032 15.1895C49.038 15.0173 48.7946 15.0172 48.7292 15.1892L46.5361 20.9604Z" fill="#2845C1"/>
<path d="M80.1372 27.7756C80.1372 27.886 80.0477 27.9756 79.9372 27.9756H77.8314C77.7209 27.9756 77.6314 27.886 77.6314 27.7756V16.6912C77.6314 16.5808 77.7209 16.4912 77.8314 16.4912H79.9372C80.0477 16.4912 80.1372 16.5808 80.1372 16.6912V27.7756Z" fill="#2845C1"/>
<path d="M120.009 16.0361C120.86 16.0361 121.619 16.2258 122.288 16.6055C122.971 16.9852 123.511 17.5477 123.906 18.292C124.301 19.0363 124.499 19.9708 124.499 21.0947V27.7756C124.499 27.886 124.409 27.9756 124.299 27.9756H122.169C122.059 27.9756 121.969 27.886 121.969 27.7756V21.1855C121.969 20.3046 121.741 19.6209 121.286 19.1348C120.83 18.6335 120.206 18.3828 119.417 18.3828C118.915 18.3829 118.475 18.4813 118.095 18.6787C117.715 18.861 117.389 19.0971 117.116 19.3857C116.919 19.6051 116.745 19.8331 116.592 20.0699C116.498 20.2162 116.455 20.3884 116.455 20.5624V27.7756C116.455 27.886 116.365 27.9756 116.255 27.9756H114.149C114.038 27.9756 113.949 27.886 113.949 27.7756V16.6912C113.949 16.5808 114.038 16.4912 114.149 16.4912H116.255C116.365 16.4912 116.455 16.5808 116.455 16.6912V17.2892C116.455 17.3794 116.566 17.4247 116.631 17.3628C117.008 17.0071 117.443 16.7088 117.936 16.4688C118.528 16.1802 119.219 16.0361 120.009 16.0361Z" fill="#2845C1"/>
<path d="M78.9067 10.75C79.2105 10.75 79.484 10.8257 79.7271 10.9775C79.9853 11.1294 80.1831 11.3345 80.3198 11.5928C80.4717 11.8358 80.5474 12.1093 80.5474 12.4131C80.5474 12.7168 80.4716 12.9977 80.3198 13.2559C80.1831 13.5141 79.9853 13.7192 79.7271 13.8711C79.484 14.0078 79.2106 14.0762 78.9067 14.0762C78.603 14.0762 78.3222 14.0078 78.064 13.8711C77.8057 13.7192 77.6006 13.5141 77.4487 13.2559C77.2969 12.9977 77.2212 12.7168 77.2212 12.4131C77.2212 11.9574 77.3807 11.57 77.6997 11.251C78.0338 10.917 78.436 10.75 78.9067 10.75Z" fill="#2845C1"/>
<path d="M12.3425 33.7362C12.2926 33.7362 12.2676 33.7362 12.2486 33.7265C12.2318 33.718 12.2182 33.7044 12.2097 33.6876C12.2 33.6686 12.2 33.6436 12.2 33.5937V29.2425C12.2 29.1926 12.2 29.1677 12.2097 29.1486C12.2182 29.1318 12.2318 29.1182 12.2486 29.1097C12.2676 29.1 12.2926 29.1 12.3425 29.1H13.5643C14.5313 29.1 15.1208 29.5768 15.1208 30.3584C15.1208 30.648 15.0195 30.9064 14.8257 31.1068C14.7572 31.1777 14.7229 31.2131 14.7171 31.2364C14.7109 31.2615 14.7123 31.274 14.7237 31.2972C14.7343 31.3187 14.7739 31.3451 14.8532 31.3978C15.1981 31.6275 15.3857 31.9699 15.3857 32.3719C15.3857 33.2196 14.73 33.7362 13.6637 33.7362H12.3425ZM13.5842 29.8815H13.283C13.2331 29.8815 13.2081 29.8815 13.1891 29.8912C13.1723 29.8998 13.1587 29.9134 13.1502 29.9301C13.1405 29.9492 13.1405 29.9741 13.1405 30.024V30.7855C13.1405 30.8354 13.1405 30.8603 13.1502 30.8794C13.1587 30.8961 13.1723 30.9097 13.1891 30.9183C13.2081 30.928 13.2331 30.928 13.283 30.928H13.5842C13.9419 30.928 14.1538 30.7359 14.1538 30.3981C14.1538 30.0736 13.9419 29.8815 13.5842 29.8815ZM13.7167 31.7095H13.283C13.2331 31.7095 13.2081 31.7095 13.1891 31.7192C13.1723 31.7278 13.1587 31.7414 13.1502 31.7582C13.1405 31.7772 13.1405 31.8022 13.1405 31.852V32.8122C13.1405 32.8621 13.1405 32.887 13.1502 32.9061C13.1587 32.9228 13.1723 32.9365 13.1891 32.945C13.2081 32.9547 13.2331 32.9547 13.283 32.9547H13.7167C14.1538 32.9547 14.4187 32.7229 14.4187 32.3321C14.4187 31.9347 14.1538 31.7095 13.7167 31.7095Z" fill="var(--c--globals--colors--logo-1)"/>
<path d="M16.4271 33.7362C16.3772 33.7362 16.3523 33.7362 16.3333 33.7265C16.3165 33.718 16.3029 33.7044 16.2943 33.6876C16.2846 33.6686 16.2846 33.6436 16.2846 33.5937V29.2425C16.2846 29.1926 16.2846 29.1677 16.2943 29.1486C16.3029 29.1318 16.3165 29.1182 16.3333 29.1097C16.3523 29.1 16.3772 29.1 16.4271 29.1H18.8444C18.8943 29.1 18.9192 29.1 18.9383 29.1097C18.955 29.1182 18.9687 29.1318 18.9772 29.1486C18.9869 29.1677 18.9869 29.1926 18.9869 29.2425V29.739C18.9869 29.7889 18.9869 29.8138 18.9772 29.8329C18.9687 29.8496 18.955 29.8633 18.9383 29.8718C18.9192 29.8815 18.8943 29.8815 18.8444 29.8815H17.3676C17.3177 29.8815 17.2928 29.8815 17.2738 29.8912C17.257 29.8998 17.2434 29.9134 17.2348 29.9301C17.2251 29.9492 17.2251 29.9741 17.2251 30.024V30.8385C17.2251 30.8884 17.2251 30.9133 17.2348 30.9323C17.2434 30.9491 17.257 30.9627 17.2738 30.9713C17.2928 30.981 17.3177 30.981 17.3676 30.981H18.5795C18.6294 30.981 18.6543 30.981 18.6733 30.9907C18.6901 30.9992 18.7037 31.0128 18.7123 31.0296C18.722 31.0487 18.722 31.0736 18.722 31.1235V31.62C18.722 31.6699 18.722 31.6948 18.7123 31.7139C18.7037 31.7306 18.6901 31.7443 18.6733 31.7528C18.6543 31.7625 18.6294 31.7625 18.5795 31.7625H17.3676C17.3177 31.7625 17.2928 31.7625 17.2738 31.7722C17.257 31.7808 17.2434 31.7944 17.2348 31.8111C17.2251 31.8302 17.2251 31.8551 17.2251 31.905V32.8122C17.2251 32.8621 17.2251 32.887 17.2348 32.9061C17.2434 32.9228 17.257 32.9365 17.2738 32.945C17.2928 32.9547 17.3177 32.9547 17.3676 32.9547H18.8444C18.8943 32.9547 18.9192 32.9547 18.9383 32.9644C18.955 32.9729 18.9687 32.9866 18.9772 33.0033C18.9869 33.0224 18.9869 33.0473 18.9869 33.0972V33.5937C18.9869 33.6436 18.9869 33.6686 18.9772 33.6876C18.9687 33.7044 18.955 33.718 18.9383 33.7265C18.9192 33.7362 18.8943 33.7362 18.8444 33.7362H16.4271Z" fill="var(--c--globals--colors--logo-1)"/>
<path d="M19.7762 29.9544C19.7263 29.9544 19.7014 29.9544 19.6823 29.9447C19.6656 29.9361 19.652 29.9225 19.6434 29.9057C19.6337 29.8867 19.6337 29.8618 19.6337 29.8119V29.2425C19.6337 29.1926 19.6337 29.1677 19.6434 29.1486C19.652 29.1318 19.6656 29.1182 19.6823 29.1097C19.7014 29.1 19.7263 29.1 19.7762 29.1H23.2929C23.3428 29.1 23.3678 29.1 23.3868 29.1097C23.4036 29.1182 23.4172 29.1318 23.4257 29.1486C23.4354 29.1677 23.4354 29.1926 23.4354 29.2425V29.8119C23.4354 29.8618 23.4354 29.8867 23.4257 29.9057C23.4172 29.9225 23.4036 29.9361 23.3868 29.9447C23.3678 29.9544 23.3428 29.9544 23.2929 29.9544H22.1473C22.0974 29.9544 22.0725 29.9544 22.0535 29.9641C22.0367 29.9726 22.0231 29.9862 22.0145 30.003C22.0048 30.0221 22.0048 30.047 22.0048 30.0969V33.5937C22.0048 33.6436 22.0048 33.6686 21.9951 33.6876C21.9866 33.7044 21.973 33.718 21.9562 33.7265C21.9371 33.7362 21.9122 33.7362 21.8623 33.7362H21.2068C21.1569 33.7362 21.132 33.7362 21.113 33.7265C21.0962 33.718 21.0826 33.7044 21.074 33.6876C21.0643 33.6686 21.0643 33.6436 21.0643 33.5937V30.0969C21.0643 30.047 21.0643 30.0221 21.0546 30.003C21.0461 29.9862 21.0325 29.9726 21.0157 29.9641C20.9966 29.9544 20.9717 29.9544 20.9218 29.9544H19.7762Z" fill="var(--c--globals--colors--logo-1)"/>
<path d="M23.3724 33.7362C23.3031 33.7362 23.2685 33.7362 23.2463 33.7217C23.2268 33.709 23.2131 33.6891 23.2081 33.6664C23.2024 33.6405 23.2146 33.6081 23.2392 33.5433L24.8864 29.192C24.899 29.1588 24.9053 29.1422 24.9161 29.13C24.9257 29.1192 24.9377 29.1109 24.9512 29.1058C24.9665 29.1 24.9842 29.1 25.0197 29.1H26.0548C26.0903 29.1 26.108 29.1 26.1233 29.1058C26.1368 29.1109 26.1488 29.1192 26.1584 29.13C26.1692 29.1422 26.1755 29.1588 26.188 29.192L27.8353 33.5433C27.8598 33.6081 27.8721 33.6405 27.8664 33.6664C27.8613 33.6891 27.8476 33.709 27.8282 33.7217C27.806 33.7362 27.7713 33.7362 27.702 33.7362H27.0076C26.9718 33.7362 26.9539 33.7362 26.9385 33.7304C26.925 33.7252 26.9129 33.7167 26.9033 33.7058C26.8925 33.6934 26.8863 33.6766 26.8739 33.643L26.4988 32.6241C26.4865 32.5905 26.4803 32.5737 26.4694 32.5613C26.4599 32.5503 26.4478 32.5419 26.4342 32.5367C26.4188 32.5308 26.4009 32.5308 26.3651 32.5308H24.7094C24.6736 32.5308 24.6556 32.5308 24.6403 32.5367C24.6267 32.5419 24.6146 32.5503 24.605 32.5613C24.5942 32.5737 24.588 32.5905 24.5757 32.6241L24.2006 33.643C24.1882 33.6766 24.182 33.6934 24.1712 33.7058C24.1616 33.7167 24.1495 33.7252 24.1359 33.7304C24.1206 33.7362 24.1027 33.7362 24.0668 33.7362H23.3724ZM24.9713 31.538C24.9478 31.6024 24.936 31.6346 24.9419 31.6603C24.9471 31.6828 24.9608 31.7024 24.9802 31.715C25.0023 31.7294 25.0366 31.7294 25.1051 31.7294H25.9694C26.0379 31.7294 26.0721 31.7294 26.0943 31.715C26.1136 31.7024 26.1274 31.6828 26.1325 31.6603C26.1385 31.6346 26.1267 31.6024 26.1032 31.538L25.6711 30.3542C25.6298 30.2411 25.6092 30.1846 25.579 30.1686C25.5529 30.1547 25.5216 30.1547 25.4955 30.1686C25.4653 30.1846 25.4447 30.2411 25.4034 30.3542L24.9713 31.538Z" fill="var(--c--globals--colors--logo-1)"/>
<path d="M61.8577 15.8076C62.6931 15.8076 63.468 15.99 64.1819 16.3545C64.8463 16.6867 65.3785 17.1009 65.7781 17.5971C65.8437 17.6785 65.8294 17.7966 65.7498 17.8645L64.3459 19.0622C64.2574 19.1377 64.1236 19.1217 64.0503 19.0314C63.7909 18.7122 63.4929 18.4426 63.1565 18.2227C62.7616 17.9645 62.3285 17.835 61.8577 17.835C61.5237 17.835 61.2353 17.8962 60.9924 18.0176C60.7646 18.1391 60.5895 18.2986 60.468 18.4961C60.3617 18.6936 60.3088 18.9142 60.3088 19.1572C60.3089 19.5216 60.4375 19.8254 60.6955 20.0684C60.969 20.2962 61.319 20.494 61.7444 20.6611C62.1696 20.8282 62.6174 21.0106 63.0881 21.208C63.5742 21.3903 64.03 21.6182 64.4553 21.8916C64.8806 22.1498 65.2225 22.4989 65.4807 22.9395C65.7541 23.3647 65.8908 23.9037 65.8909 24.5566C65.8909 25.3312 65.7012 25.9923 65.3215 26.5391C64.957 27.0706 64.471 27.4809 63.8635 27.7695C63.2559 28.0582 62.5868 28.2031 61.8577 28.2031C60.8706 28.2031 60.0124 28.0207 59.2834 27.6562C58.6116 27.3063 58.0171 26.8462 57.5 26.2769C57.4264 26.1959 57.4354 26.0708 57.5176 25.9985L58.921 24.7635C59.0067 24.6881 59.1377 24.6998 59.2123 24.7863C59.5425 25.1694 59.9081 25.4884 60.3088 25.7422C60.7645 26.0307 61.2733 26.1748 61.8352 26.1748C62.3669 26.1748 62.7698 26.0381 63.0432 25.7646C63.3316 25.4761 63.4758 25.1268 63.4758 24.7168C63.4758 24.3371 63.339 24.0335 63.0657 23.8057C62.8074 23.5778 62.4656 23.38 62.0403 23.2129C61.6149 23.0458 61.1592 22.8708 60.6731 22.6885C60.2024 22.4911 59.7545 22.2557 59.3293 21.9824C58.9041 21.709 58.5539 21.3598 58.2805 20.9346C58.0223 20.494 57.8938 19.9316 57.8938 19.248C57.8938 18.6101 58.0606 18.033 58.3948 17.5166C58.729 16.9849 59.192 16.5665 59.7844 16.2627C60.3919 15.959 61.0831 15.8077 61.8577 15.8076Z" fill="var(--c--globals--colors--logo-1)"/>
<path d="M71.3215 15.8076C72.1569 15.8077 72.9319 15.99 73.6457 16.3545C74.3101 16.6867 74.8424 17.1009 75.2419 17.5971C75.3075 17.6785 75.2933 17.7966 75.2137 17.8644L73.8098 19.0622C73.7213 19.1377 73.5875 19.1217 73.5141 19.0313C73.2548 18.7122 72.9568 18.4427 72.6204 18.2227C72.2255 17.9645 71.7923 17.835 71.3215 17.835C70.9873 17.835 70.6984 17.8961 70.4553 18.0176C70.2275 18.1391 70.0524 18.2986 69.9309 18.4961C69.8246 18.6935 69.7717 18.9142 69.7717 19.1572C69.7718 19.5217 69.9013 19.8254 70.1594 20.0684C70.4328 20.2962 70.782 20.4941 71.2073 20.6611C71.6326 20.8282 72.0811 21.0105 72.552 21.208C73.0379 21.3903 73.494 21.6182 73.9192 21.8916C74.3443 22.1498 74.6864 22.4991 74.9446 22.9395C75.2178 23.3646 75.3547 23.9038 75.3547 24.5566C75.3547 25.3313 75.1641 25.9922 74.7844 26.5391C74.4199 27.0707 73.934 27.4809 73.3264 27.7695C72.7189 28.0581 72.0506 28.2031 71.3215 28.2031C70.3341 28.2031 69.4755 28.0208 68.7463 27.6562C68.0745 27.3063 67.48 26.8462 66.9629 26.2769C66.8893 26.1959 66.8983 26.0708 66.9805 25.9985L68.3839 24.7635C68.4696 24.6881 68.6006 24.6998 68.6752 24.7863C69.0054 25.1694 69.371 25.4884 69.7717 25.7422C70.2273 26.0307 70.7362 26.1747 71.2981 26.1748C71.8298 26.1748 72.2327 26.0381 72.5061 25.7646C72.7946 25.4761 72.9387 25.1268 72.9387 24.7168C72.9387 24.337 72.802 24.0335 72.5286 23.8057C72.2703 23.5778 71.9285 23.38 71.5032 23.2129C71.0779 23.0458 70.622 22.8707 70.136 22.6885C69.6653 22.4911 69.2174 22.2557 68.7922 21.9824C68.3671 21.7091 68.0178 21.3597 67.7444 20.9346C67.4861 20.494 67.3567 19.9316 67.3567 19.248C67.3567 18.6102 67.5236 18.033 67.8577 17.5166C68.1918 16.985 68.655 16.5665 69.2473 16.2627C69.8549 15.9589 70.5468 15.8076 71.3215 15.8076Z" fill="var(--c--globals--colors--logo-1)"/>
<path d="M86.3313 15.8076C87.1668 15.8076 87.9416 15.9899 88.6555 16.3545C89.32 16.6868 89.8521 17.1008 90.2517 17.5971C90.3173 17.6785 90.303 17.7966 90.2235 17.8644L88.8195 19.0622C88.731 19.1377 88.5973 19.1217 88.5239 19.0314C88.2646 18.7122 87.9666 18.4426 87.6301 18.2227C87.2352 17.9644 86.8022 17.835 86.3313 17.835C85.9971 17.835 85.7081 17.8961 85.4651 18.0176C85.2374 18.1391 85.0631 18.2988 84.9416 18.4961C84.8353 18.6936 84.7815 18.9142 84.7815 19.1572C84.7816 19.5216 84.911 19.8254 85.1692 20.0684C85.4426 20.2962 85.7918 20.4941 86.217 20.6611C86.6424 20.8282 87.0909 21.0105 87.5618 21.208C88.0478 21.3903 88.5036 21.6182 88.9289 21.8916C89.3542 22.1498 89.6961 22.499 89.9543 22.9395C90.2276 23.3647 90.3644 23.9037 90.3645 24.5566C90.3645 25.3311 90.1747 25.9923 89.7952 26.5391C89.4306 27.0707 88.9438 27.4809 88.3362 27.7695C87.7286 28.0581 87.0604 28.2031 86.3313 28.2031C85.3439 28.2031 84.4852 28.0208 83.7561 27.6562C83.0843 27.3063 82.4905 26.8461 81.9736 26.2769C81.9 26.1959 81.909 26.0708 81.9911 25.9985L83.3937 24.7636C83.4793 24.6881 83.6104 24.6998 83.685 24.7863C84.0152 25.1694 84.3808 25.4884 84.7815 25.7422C85.2372 26.0308 85.7468 26.1748 86.3088 26.1748C86.8403 26.1748 87.2425 26.038 87.5159 25.7646C87.8044 25.4761 87.9494 25.1269 87.9495 24.7168C87.9495 24.3372 87.8125 24.0335 87.5393 23.8057C87.2811 23.5779 86.9391 23.38 86.5139 23.2129C86.0886 23.0458 85.6328 22.8707 85.1467 22.6885C84.6758 22.491 84.2273 22.2559 83.802 21.9824C83.3768 21.7091 83.0275 21.3597 82.7541 20.9346C82.4959 20.494 82.3664 19.9316 82.3664 19.248C82.3664 18.61 82.5342 18.0331 82.8684 17.5166C83.2026 16.9849 83.6656 16.5665 84.258 16.2627C84.8656 15.959 85.5567 15.8076 86.3313 15.8076Z" fill="var(--c--globals--colors--logo-1)"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M106.191 16.0361C107.239 16.0361 108.128 16.2257 108.857 16.6055C109.586 16.9852 110.133 17.5094 110.498 18.1777C110.877 18.8461 111.067 19.5981 111.067 20.4336V27.7756C111.067 27.886 110.977 27.9756 110.867 27.9756H108.76C108.65 27.9756 108.56 27.886 108.56 27.7756V27.0162C108.56 26.9218 108.438 26.8771 108.372 26.9448C108.27 27.0497 108.159 27.1502 108.037 27.2461C107.626 27.5803 107.148 27.8392 106.601 28.0215C106.054 28.2189 105.5 28.3173 104.938 28.3174C103.814 28.3174 102.894 28.0139 102.18 27.4062C101.466 26.7986 101.11 25.9855 101.11 24.9678C101.11 24.102 101.436 23.3654 102.089 22.7578C102.743 22.1502 103.684 21.7473 104.915 21.5498L108.394 20.9589C108.49 20.9426 108.56 20.8593 108.56 20.7618V20.4336C108.56 19.7502 108.333 19.2186 107.877 18.8389C107.422 18.444 106.829 18.2462 106.1 18.2461C105.508 18.2461 104.968 18.3756 104.482 18.6338C104.071 18.8594 103.717 19.1602 103.422 19.5365C103.351 19.626 103.222 19.6465 103.132 19.5773L101.606 18.4098C101.52 18.3442 101.501 18.2219 101.568 18.1365C102.052 17.5117 102.666 17.0165 103.411 16.6514C104.231 16.2412 105.158 16.0361 106.191 16.0361ZM105.553 23.2139C104.87 23.3202 104.376 23.5171 104.072 23.8057C103.768 24.079 103.616 24.4284 103.616 24.8535C103.616 25.2788 103.775 25.6289 104.094 25.9023C104.428 26.1758 104.877 26.3125 105.439 26.3125C106.138 26.3125 106.753 26.153 107.285 25.834C107.721 25.5722 108.09 25.2382 108.393 24.833C108.508 24.6795 108.56 24.4899 108.56 24.2983V22.9508C108.56 22.8272 108.449 22.7332 108.327 22.7535L105.553 23.2139Z" fill="var(--c--globals--colors--logo-1)"/>
<path d="M95.9868 16.2912C95.9868 16.4017 96.0764 16.4912 96.1868 16.4912H99.5017C99.6121 16.4912 99.7017 16.5808 99.7017 16.6912V18.5471C99.7017 18.6575 99.6121 18.7471 99.5017 18.7471H96.1868C96.0764 18.7471 95.9868 18.8366 95.9868 18.9471V23.9199C95.9868 24.421 96.0708 24.8159 96.2378 25.1045C96.4048 25.3929 96.6403 25.6055 96.9439 25.7422C97.2477 25.8637 97.605 25.9248 98.0151 25.9248C98.4101 25.9248 98.7446 25.902 99.0181 25.8564C99.1739 25.8218 99.3174 25.7847 99.4485 25.7451C99.5829 25.7045 99.7241 25.8022 99.7241 25.9426V27.7314C99.7241 27.8104 99.6778 27.8826 99.6042 27.9113C99.3605 28.0061 99.0888 28.0733 98.7896 28.1123C98.4554 28.173 98.0605 28.2031 97.605 28.2031C96.6936 28.2031 95.926 28.0436 95.3032 27.7246C94.6959 27.3904 94.2325 26.9117 93.9136 26.2891C93.6099 25.6512 93.4585 24.8615 93.4585 23.9199V18.9471C93.4585 18.8366 93.369 18.7471 93.2585 18.7471H91.6087C91.4982 18.7471 91.4087 18.6575 91.4087 18.5471V16.6912C91.4087 16.5808 91.4982 16.4912 91.6087 16.4912H93.2585C93.369 16.4912 93.4585 16.4017 93.4585 16.2912V13.8211C93.4585 13.7106 93.548 13.6211 93.6585 13.6211H95.7868C95.8973 13.6211 95.9868 13.7106 95.9868 13.8211V16.2912Z" fill="var(--c--globals--colors--logo-1)"/>
<path d="M130.563 16.2912C130.563 16.4017 130.653 16.4912 130.763 16.4912H134.077C134.187 16.4912 134.277 16.5808 134.277 16.6912V18.5471C134.277 18.6575 134.187 18.7471 134.077 18.7471H130.763C130.653 18.7471 130.563 18.8366 130.563 18.9471V23.9199C130.563 24.421 130.646 24.8159 130.813 25.1045C130.98 25.3931 131.216 25.6055 131.52 25.7422C131.824 25.8637 132.18 25.9248 132.59 25.9248C132.985 25.9248 133.32 25.902 133.593 25.8564C133.749 25.8218 133.893 25.7847 134.024 25.7451C134.158 25.7045 134.299 25.8022 134.299 25.9425V27.7313C134.299 27.8104 134.253 27.8826 134.179 27.9113C133.936 28.0061 133.665 28.0733 133.366 28.1123C133.032 28.1731 132.636 28.2031 132.18 28.2031C131.269 28.2031 130.502 28.0436 129.879 27.7246C129.272 27.3904 128.809 26.9119 128.49 26.2891C128.186 25.6511 128.034 24.8616 128.034 23.9199V18.9471C128.034 18.8366 127.944 18.7471 127.834 18.7471H126.092C125.982 18.7471 125.892 18.6575 125.892 18.5471V16.6912C125.892 16.5808 125.982 16.4912 126.092 16.4912H127.834C127.944 16.4912 128.034 16.4017 128.034 16.2912V13.8211C128.034 13.7106 128.123 13.6211 128.234 13.6211H130.363C130.473 13.6211 130.563 13.7106 130.563 13.8211V16.2912Z" fill="var(--c--globals--colors--logo-1)"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M56.8209 27.7034C56.8716 27.8344 56.7749 27.9756 56.6344 27.9756H54.0801C53.9969 27.9756 53.9224 27.9241 53.893 27.8464L52.3355 23.7298C52.3061 23.652 52.2316 23.6006 52.1485 23.6006H45.6707C45.5876 23.6006 45.5132 23.6519 45.4837 23.7296L43.9196 27.8466C43.8901 27.9243 43.8157 27.9756 43.7326 27.9756H41.2002C41.0598 27.9756 40.9631 27.8346 41.0136 27.7036L47.0107 12.1534C47.0404 12.0763 47.1146 12.0254 47.1973 12.0254H50.6144C50.697 12.0254 50.7711 12.0762 50.8009 12.1532L56.8209 27.7034ZM46.5361 20.9604C46.4864 21.0913 46.5831 21.2314 46.7231 21.2314H51.1006C51.2405 21.2314 51.3372 21.0915 51.2876 20.9606L49.1032 15.1895C49.038 15.0173 48.7946 15.0172 48.7292 15.1892L46.5361 20.9604Z" fill="var(--c--globals--colors--logo-1)"/>
<path d="M80.1372 27.7756C80.1372 27.886 80.0477 27.9756 79.9372 27.9756H77.8314C77.7209 27.9756 77.6314 27.886 77.6314 27.7756V16.6912C77.6314 16.5808 77.7209 16.4912 77.8314 16.4912H79.9372C80.0477 16.4912 80.1372 16.5808 80.1372 16.6912V27.7756Z" fill="var(--c--globals--colors--logo-1)"/>
<path d="M120.009 16.0361C120.86 16.0361 121.619 16.2258 122.288 16.6055C122.971 16.9852 123.511 17.5477 123.906 18.292C124.301 19.0363 124.499 19.9708 124.499 21.0947V27.7756C124.499 27.886 124.409 27.9756 124.299 27.9756H122.169C122.059 27.9756 121.969 27.886 121.969 27.7756V21.1855C121.969 20.3046 121.741 19.6209 121.286 19.1348C120.83 18.6335 120.206 18.3828 119.417 18.3828C118.915 18.3829 118.475 18.4813 118.095 18.6787C117.715 18.861 117.389 19.0971 117.116 19.3857C116.919 19.6051 116.745 19.8331 116.592 20.0699C116.498 20.2162 116.455 20.3884 116.455 20.5624V27.7756C116.455 27.886 116.365 27.9756 116.255 27.9756H114.149C114.038 27.9756 113.949 27.886 113.949 27.7756V16.6912C113.949 16.5808 114.038 16.4912 114.149 16.4912H116.255C116.365 16.4912 116.455 16.5808 116.455 16.6912V17.2892C116.455 17.3794 116.566 17.4247 116.631 17.3628C117.008 17.0071 117.443 16.7088 117.936 16.4688C118.528 16.1802 119.219 16.0361 120.009 16.0361Z" fill="var(--c--globals--colors--logo-1)"/>
<path d="M78.9067 10.75C79.2105 10.75 79.484 10.8257 79.7271 10.9775C79.9853 11.1294 80.1831 11.3345 80.3198 11.5928C80.4717 11.8358 80.5474 12.1093 80.5474 12.4131C80.5474 12.7168 80.4716 12.9977 80.3198 13.2559C80.1831 13.5141 79.9853 13.7192 79.7271 13.8711C79.484 14.0078 79.2106 14.0762 78.9067 14.0762C78.603 14.0762 78.3222 14.0078 78.064 13.8711C77.8057 13.7192 77.6006 13.5141 77.4487 13.2559C77.2969 12.9977 77.2212 12.7168 77.2212 12.4131C77.2212 11.9574 77.3807 11.57 77.6997 11.251C78.0338 10.917 78.436 10.75 78.9067 10.75Z" fill="var(--c--globals--colors--logo-1)"/>
<defs>
<linearGradient id="paint0_linear_6479_3681" x1="20" y1="27" x2="20" y2="36" gradientUnits="userSpaceOnUse">
<stop stop-color="#2845C1"/>
<stop offset="1" stop-color="#EEF1F4"/>
<stop stop-color="var(--c--globals--colors--logo-1)"/>
<stop offset="1" stop-color="var(--c--contextuals--background--semantic--neutral--tertiary)"/>
</linearGradient>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

@@ -1,20 +1,20 @@
<svg width="65" height="64" viewBox="0 0 65 64" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M44.7737 12.1078C44.7737 10.7446 44.7737 10.063 44.6322 9.44065C44.3707 8.29032 43.7579 7.24979 42.8788 6.46318C42.4032 6.03759 41.8071 5.707 40.615 5.04582L37.5686 3.35621C36.4423 2.73158 35.8792 2.41926 35.2982 2.24352C34.2246 1.91883 33.079 1.91883 32.0054 2.24352C31.4244 2.41926 30.8613 2.73158 29.7351 3.35621L28.4787 4.05301C26.9538 4.89875 26.1914 5.32161 25.8421 5.81385C25.1888 6.73448 25.1894 7.96764 25.8436 8.88764C26.1933 9.37954 26.9562 9.80168 28.4819 10.646L29.4989 11.2088C30.6929 11.8695 31.2899 12.1998 31.7663 12.6255C32.6468 13.4121 33.2607 14.4534 33.5226 15.6047C33.6644 16.2276 33.6644 16.9099 33.6644 18.2745V19.4391C33.6644 21.0927 33.6644 21.9195 33.9128 22.4525C34.3771 23.4487 35.4075 24.0556 36.5038 23.9787C37.0905 23.9376 37.8137 23.5368 39.2599 22.7351L40.6133 21.9849C41.8059 21.3238 42.4022 20.9933 42.878 20.5677C43.7575 19.7811 44.3705 18.7404 44.6321 17.5898C44.7737 16.9673 44.7737 16.2855 44.7737 14.922V12.1078Z" fill="#2845C1"/>
<path d="M10.8637 32.3143C9.68318 32.9959 9.0929 33.3367 8.62465 33.7704C7.75919 34.572 7.16444 35.623 6.92278 36.7776C6.79203 37.4023 6.80376 38.0838 6.82723 39.4468L6.88719 42.9299C6.90935 44.2175 6.92044 44.8614 7.05877 45.4524C7.31435 46.5445 7.88715 47.5366 8.70513 48.304C9.14785 48.7194 9.69988 49.0509 10.8039 49.7139L12.0356 50.4535C13.5304 51.3512 14.2779 51.8001 14.8788 51.8565C16.0027 51.962 17.0704 51.3449 17.54 50.3183C17.7912 49.7695 17.7753 48.8978 17.7436 47.1543L17.7225 45.9921C17.6977 44.6277 17.6853 43.9456 17.8157 43.3202C18.0567 42.1643 18.6515 41.1121 19.5176 40.3095C19.9862 39.8753 20.5771 39.5342 21.7589 38.8519L22.7674 38.2696C24.1995 37.4428 24.9155 37.0294 25.2529 36.5477C25.8835 35.6475 25.8939 34.4518 25.2792 33.5407C24.9502 33.0532 24.2415 32.6273 22.8241 31.7757L21.4978 30.9787C20.329 30.2765 19.7446 29.9253 19.1381 29.726C18.0171 29.3577 16.8093 29.3471 15.6821 29.6959C15.0722 29.8845 14.4818 30.2254 13.3009 30.9072L10.8637 32.3143Z" fill="#2845C1"/>
<path d="M22.53 51.8922C22.53 53.2554 22.53 53.937 22.6714 54.5594C22.9329 55.7097 23.5457 56.7502 24.4248 57.5368C24.9005 57.9624 25.4965 58.293 26.6887 58.9542L29.7351 60.6438C30.8613 61.2684 31.4244 61.5807 32.0054 61.7565C33.079 62.0812 34.2246 62.0812 35.2982 61.7565C35.8792 61.5807 36.4423 61.2684 37.5686 60.6438L38.8249 59.947C40.3498 59.1012 41.1122 58.6784 41.4615 58.1862C42.1148 57.2655 42.1142 56.0324 41.4601 55.1124C41.1103 54.6205 40.3474 54.1983 38.8218 53.354L37.8047 52.7912C36.6107 52.1305 36.0137 51.8002 35.5373 51.3745C34.6568 50.5879 34.043 49.5466 33.781 48.3953C33.6393 47.7724 33.6393 47.0901 33.6393 45.7255V44.5609C33.6393 42.9073 33.6393 42.0805 33.3908 41.5475C32.9265 40.5513 31.8962 39.9444 30.7998 40.0213C30.2131 40.0624 29.49 40.4632 28.0437 41.2649L26.6903 42.0151C25.4977 42.6761 24.9015 43.0067 24.4256 43.4323C23.5461 44.2189 22.9331 45.2596 22.6715 46.4102C22.53 47.0327 22.53 47.7145 22.53 49.078V51.8922Z" fill="#2845C1"/>
<path d="M45.318 51.5779C46.4986 52.2595 47.0889 52.6003 47.6986 52.789C48.8256 53.1376 50.0331 53.1273 51.1539 52.7592C51.7603 52.5601 52.3446 52.2092 53.5132 51.5074L56.4997 49.7139C57.6037 49.0509 58.1558 48.7194 58.5985 48.304C59.4165 47.5366 59.9893 46.5445 60.2449 45.4524C60.3832 44.8614 60.3943 44.2175 60.4164 42.9299L60.4412 41.4935C60.4712 39.75 60.4862 38.8783 60.2345 38.3297C59.7639 37.3036 58.6957 36.6875 57.5718 36.794C56.9709 36.851 56.2239 37.3006 54.7299 38.1997L53.734 38.7991C52.5648 39.5028 51.9802 39.8546 51.3734 40.0544C50.2519 40.4236 49.0432 40.4346 47.9152 40.0858C47.3049 39.8971 46.714 39.5559 45.5322 38.8736L44.5236 38.2913C43.0916 37.4645 42.3755 37.0511 41.7897 36.9998C40.6948 36.9038 39.654 37.4926 39.1724 38.4806C38.9147 39.0092 38.9003 39.8359 38.8714 41.4892L38.8444 43.0364C38.8206 44.3997 38.8087 45.0814 38.9394 45.7062C39.1809 46.8612 39.7756 47.9125 40.6412 48.7143C41.1096 49.1481 41.7 49.489 42.8809 50.1708L45.318 51.5779Z" fill="#2845C1"/>
<path d="M56.4399 31.6857C57.6204 31.0041 58.2107 30.6633 58.679 30.2296C59.5444 29.428 60.1392 28.3771 60.3808 27.2224C60.5116 26.5977 60.4999 25.9162 60.4764 24.5532L60.4164 21.0701C60.3943 19.7825 60.3832 19.1386 60.2449 18.5476C59.9893 17.4555 59.4165 16.4634 58.5985 15.696C58.1558 15.2806 57.6037 14.9491 56.4997 14.2861L55.2681 13.5465C53.7732 12.6488 53.0258 12.1999 52.4248 12.1435C51.3009 12.038 50.2332 12.6551 49.7636 13.6817C49.5125 14.2305 49.5283 15.1022 49.56 16.8457L49.5811 18.0079C49.6059 19.3723 49.6183 20.0544 49.4879 20.6798C49.2469 21.8357 48.6521 22.8879 47.786 23.6905C47.3174 24.1247 46.7265 24.4658 45.5447 25.1481L44.5362 25.7304C43.1041 26.5572 42.3881 26.9706 42.0507 27.4523C41.4201 28.3525 41.4097 29.5482 42.0244 30.4593C42.3534 30.9468 43.0621 31.3727 44.4795 32.2243L45.8059 33.0213C46.9747 33.7235 47.5591 34.0747 48.1655 34.274C49.2865 34.6423 50.4943 34.6529 51.6215 34.3041C52.2314 34.1155 52.8219 33.7746 54.0027 33.0928L56.4399 31.6857Z" fill="#2845C1"/>
<path d="M21.9855 12.4222C20.805 11.7406 20.2147 11.3998 19.6049 11.2112C18.478 10.8625 17.2705 10.8728 16.1497 11.2409C15.5433 11.44 14.959 11.7909 13.7903 12.4927L10.8039 14.2862C9.6998 14.9492 9.14777 15.2807 8.70505 15.6961C7.88707 16.4635 7.31427 17.4556 7.05869 18.5477C6.92036 19.1387 6.90927 19.7826 6.88711 21.0702L6.86238 22.5066C6.83237 24.2501 6.81736 25.1218 7.069 25.6704C7.53963 26.6965 8.60788 27.3126 9.73171 27.2061C10.3326 27.1491 11.0796 26.6995 12.5736 25.8004L13.5695 25.201C14.7387 24.4973 15.3233 24.1455 15.9301 23.9457C17.0517 23.5765 18.2603 23.5655 19.3884 23.9143C19.9987 24.103 20.5896 24.4442 21.7714 25.1265L22.7799 25.7088C24.212 26.5356 24.928 26.949 25.5139 27.0003C26.6087 27.0963 27.6495 26.5075 28.1311 25.5195C28.3889 24.9909 28.4033 24.1642 28.4321 22.5109L28.4591 20.9638C28.4829 19.6004 28.4948 18.9187 28.3642 18.2939C28.1227 17.1389 27.5279 16.0876 26.6623 15.2858C26.194 14.852 25.6035 14.5111 24.4227 13.8293L21.9855 12.4222Z" fill="#C83F49"/>
<rect x="8.8125" y="46" width="48" height="18" rx="9" fill="#EEF1F4"/>
<path d="M44.7737 12.1078C44.7737 10.7446 44.7737 10.063 44.6322 9.44065C44.3707 8.29032 43.7579 7.24979 42.8788 6.46318C42.4032 6.03759 41.8071 5.707 40.615 5.04582L37.5686 3.35621C36.4423 2.73158 35.8792 2.41926 35.2982 2.24352C34.2246 1.91883 33.079 1.91883 32.0054 2.24352C31.4244 2.41926 30.8613 2.73158 29.7351 3.35621L28.4787 4.05301C26.9538 4.89875 26.1914 5.32161 25.8421 5.81385C25.1888 6.73448 25.1894 7.96764 25.8436 8.88764C26.1933 9.37954 26.9562 9.80168 28.4819 10.646L29.4989 11.2088C30.6929 11.8695 31.2899 12.1998 31.7663 12.6255C32.6468 13.4121 33.2607 14.4534 33.5226 15.6047C33.6644 16.2276 33.6644 16.9099 33.6644 18.2745V19.4391C33.6644 21.0927 33.6644 21.9195 33.9128 22.4525C34.3771 23.4487 35.4075 24.0556 36.5038 23.9787C37.0905 23.9376 37.8137 23.5368 39.2599 22.7351L40.6133 21.9849C41.8059 21.3238 42.4022 20.9933 42.878 20.5677C43.7575 19.7811 44.3705 18.7404 44.6321 17.5898C44.7737 16.9673 44.7737 16.2855 44.7737 14.922V12.1078Z" fill="var(--c--globals--colors--logo-1)"/>
<path d="M10.8637 32.3143C9.68318 32.9959 9.0929 33.3367 8.62465 33.7704C7.75919 34.572 7.16444 35.623 6.92278 36.7776C6.79203 37.4023 6.80376 38.0838 6.82723 39.4468L6.88719 42.9299C6.90935 44.2175 6.92044 44.8614 7.05877 45.4524C7.31435 46.5445 7.88715 47.5366 8.70513 48.304C9.14785 48.7194 9.69988 49.0509 10.8039 49.7139L12.0356 50.4535C13.5304 51.3512 14.2779 51.8001 14.8788 51.8565C16.0027 51.962 17.0704 51.3449 17.54 50.3183C17.7912 49.7695 17.7753 48.8978 17.7436 47.1543L17.7225 45.9921C17.6977 44.6277 17.6853 43.9456 17.8157 43.3202C18.0567 42.1643 18.6515 41.1121 19.5176 40.3095C19.9862 39.8753 20.5771 39.5342 21.7589 38.8519L22.7674 38.2696C24.1995 37.4428 24.9155 37.0294 25.2529 36.5477C25.8835 35.6475 25.8939 34.4518 25.2792 33.5407C24.9502 33.0532 24.2415 32.6273 22.8241 31.7757L21.4978 30.9787C20.329 30.2765 19.7446 29.9253 19.1381 29.726C18.0171 29.3577 16.8093 29.3471 15.6821 29.6959C15.0722 29.8845 14.4818 30.2254 13.3009 30.9072L10.8637 32.3143Z" fill="var(--c--globals--colors--logo-1)"/>
<path d="M22.53 51.8922C22.53 53.2554 22.53 53.937 22.6714 54.5594C22.9329 55.7097 23.5457 56.7502 24.4248 57.5368C24.9005 57.9624 25.4965 58.293 26.6887 58.9542L29.7351 60.6438C30.8613 61.2684 31.4244 61.5807 32.0054 61.7565C33.079 62.0812 34.2246 62.0812 35.2982 61.7565C35.8792 61.5807 36.4423 61.2684 37.5686 60.6438L38.8249 59.947C40.3498 59.1012 41.1122 58.6784 41.4615 58.1862C42.1148 57.2655 42.1142 56.0324 41.4601 55.1124C41.1103 54.6205 40.3474 54.1983 38.8218 53.354L37.8047 52.7912C36.6107 52.1305 36.0137 51.8002 35.5373 51.3745C34.6568 50.5879 34.043 49.5466 33.781 48.3953C33.6393 47.7724 33.6393 47.0901 33.6393 45.7255V44.5609C33.6393 42.9073 33.6393 42.0805 33.3908 41.5475C32.9265 40.5513 31.8962 39.9444 30.7998 40.0213C30.2131 40.0624 29.49 40.4632 28.0437 41.2649L26.6903 42.0151C25.4977 42.6761 24.9015 43.0067 24.4256 43.4323C23.5461 44.2189 22.9331 45.2596 22.6715 46.4102C22.53 47.0327 22.53 47.7145 22.53 49.078V51.8922Z" fill="var(--c--globals--colors--logo-1)"/>
<path d="M45.318 51.5779C46.4986 52.2595 47.0889 52.6003 47.6986 52.789C48.8256 53.1376 50.0331 53.1273 51.1539 52.7592C51.7603 52.5601 52.3446 52.2092 53.5132 51.5074L56.4997 49.7139C57.6037 49.0509 58.1558 48.7194 58.5985 48.304C59.4165 47.5366 59.9893 46.5445 60.2449 45.4524C60.3832 44.8614 60.3943 44.2175 60.4164 42.9299L60.4412 41.4935C60.4712 39.75 60.4862 38.8783 60.2345 38.3297C59.7639 37.3036 58.6957 36.6875 57.5718 36.794C56.9709 36.851 56.2239 37.3006 54.7299 38.1997L53.734 38.7991C52.5648 39.5028 51.9802 39.8546 51.3734 40.0544C50.2519 40.4236 49.0432 40.4346 47.9152 40.0858C47.3049 39.8971 46.714 39.5559 45.5322 38.8736L44.5236 38.2913C43.0916 37.4645 42.3755 37.0511 41.7897 36.9998C40.6948 36.9038 39.654 37.4926 39.1724 38.4806C38.9147 39.0092 38.9003 39.8359 38.8714 41.4892L38.8444 43.0364C38.8206 44.3997 38.8087 45.0814 38.9394 45.7062C39.1809 46.8612 39.7756 47.9125 40.6412 48.7143C41.1096 49.1481 41.7 49.489 42.8809 50.1708L45.318 51.5779Z" fill="var(--c--globals--colors--logo-1)"/>
<path d="M56.4399 31.6857C57.6204 31.0041 58.2107 30.6633 58.679 30.2296C59.5444 29.428 60.1392 28.3771 60.3808 27.2224C60.5116 26.5977 60.4999 25.9162 60.4764 24.5532L60.4164 21.0701C60.3943 19.7825 60.3832 19.1386 60.2449 18.5476C59.9893 17.4555 59.4165 16.4634 58.5985 15.696C58.1558 15.2806 57.6037 14.9491 56.4997 14.2861L55.2681 13.5465C53.7732 12.6488 53.0258 12.1999 52.4248 12.1435C51.3009 12.038 50.2332 12.6551 49.7636 13.6817C49.5125 14.2305 49.5283 15.1022 49.56 16.8457L49.5811 18.0079C49.6059 19.3723 49.6183 20.0544 49.4879 20.6798C49.2469 21.8357 48.6521 22.8879 47.786 23.6905C47.3174 24.1247 46.7265 24.4658 45.5447 25.1481L44.5362 25.7304C43.1041 26.5572 42.3881 26.9706 42.0507 27.4523C41.4201 28.3525 41.4097 29.5482 42.0244 30.4593C42.3534 30.9468 43.0621 31.3727 44.4795 32.2243L45.8059 33.0213C46.9747 33.7235 47.5591 34.0747 48.1655 34.274C49.2865 34.6423 50.4943 34.6529 51.6215 34.3041C52.2314 34.1155 52.8219 33.7746 54.0027 33.0928L56.4399 31.6857Z" fill="var(--c--globals--colors--logo-1)"/>
<path d="M21.9855 12.4222C20.805 11.7406 20.2147 11.3998 19.6049 11.2112C18.478 10.8625 17.2705 10.8728 16.1497 11.2409C15.5433 11.44 14.959 11.7909 13.7903 12.4927L10.8039 14.2862C9.6998 14.9492 9.14777 15.2807 8.70505 15.6961C7.88707 16.4635 7.31427 17.4556 7.05869 18.5477C6.92036 19.1387 6.90927 19.7826 6.88711 21.0702L6.86238 22.5066C6.83237 24.2501 6.81736 25.1218 7.069 25.6704C7.53963 26.6965 8.60788 27.3126 9.73171 27.2061C10.3326 27.1491 11.0796 26.6995 12.5736 25.8004L13.5695 25.201C14.7387 24.4973 15.3233 24.1455 15.9301 23.9457C17.0517 23.5765 18.2603 23.5655 19.3884 23.9143C19.9987 24.103 20.5896 24.4442 21.7714 25.1265L22.7799 25.7088C24.212 26.5356 24.928 26.949 25.5139 27.0003C26.6087 27.0963 27.6495 26.5075 28.1311 25.5195C28.3889 24.9909 28.4033 24.1642 28.4321 22.5109L28.4591 20.9638C28.4829 19.6004 28.4948 18.9187 28.3642 18.2939C28.1227 17.1389 27.5279 16.0876 26.6623 15.2858C26.194 14.852 25.6035 14.5111 24.4227 13.8293L21.9855 12.4222Z" fill="var(--c--globals--colors--logo-2)"/>
<rect x="8.8125" y="46" width="48" height="18" rx="9" fill="var(--c--contextuals--background--semantic--neutral--tertiary)"/>
<rect x="8.8125" y="46" width="48" height="18" rx="9" fill="url(#paint0_linear_13360_26870)" fill-opacity="0.1"/>
<path d="M17.4974 59.4725C17.3976 59.4725 17.3478 59.4725 17.3097 59.4531C17.2761 59.436 17.2489 59.4087 17.2318 59.3752C17.2124 59.3371 17.2124 59.2872 17.2124 59.1875V50.485C17.2124 50.3852 17.2124 50.3353 17.2318 50.2972C17.2489 50.2637 17.2761 50.2364 17.3097 50.2194C17.3478 50.2 17.3976 50.2 17.4974 50.2H19.9412C21.8752 50.2 23.0541 51.1537 23.0541 52.7168C23.0541 53.296 22.8514 53.8129 22.464 54.2136C22.3269 54.3553 22.2583 54.4262 22.2468 54.4727C22.2343 54.5229 22.237 54.5479 22.2599 54.5943C22.2811 54.6373 22.3603 54.6901 22.5189 54.7957C23.2086 55.255 23.584 55.9399 23.584 56.7437C23.584 58.4393 22.2726 59.4725 20.1399 59.4725H17.4974ZM19.9809 51.763H19.3784C19.2786 51.763 19.2288 51.763 19.1907 51.7825C19.1571 51.7995 19.1299 51.8268 19.1128 51.8603C19.0934 51.8984 19.0934 51.9483 19.0934 52.048V53.571C19.0934 53.6707 19.0934 53.7206 19.1128 53.7587C19.1299 53.7922 19.1571 53.8195 19.1907 53.8366C19.2288 53.856 19.2786 53.856 19.3784 53.856H19.9809C20.6962 53.856 21.1201 53.4718 21.1201 52.7963C21.1201 52.1472 20.6962 51.763 19.9809 51.763ZM20.2458 55.4191H19.3784C19.2786 55.4191 19.2288 55.4191 19.1907 55.4385C19.1571 55.4556 19.1299 55.4828 19.1128 55.5163C19.0934 55.5544 19.0934 55.6043 19.0934 55.7041V57.6244C19.0934 57.7242 19.0934 57.774 19.1128 57.8121C19.1299 57.8457 19.1571 57.8729 19.1907 57.89C19.2288 57.9094 19.2786 57.9094 19.3784 57.9094H20.2458C21.1201 57.9094 21.65 57.4458 21.65 56.6642C21.65 55.8694 21.1201 55.4191 20.2458 55.4191Z" fill="#2845C1"/>
<path d="M25.6668 59.4725C25.567 59.4725 25.5171 59.4725 25.479 59.4531C25.4455 59.436 25.4182 59.4087 25.4012 59.3752C25.3818 59.3371 25.3818 59.2872 25.3818 59.1875V50.485C25.3818 50.3852 25.3818 50.3353 25.4012 50.2972C25.4182 50.2637 25.4455 50.2364 25.479 50.2194C25.5171 50.2 25.567 50.2 25.6668 50.2H30.5013C30.6011 50.2 30.651 50.2 30.6891 50.2194C30.7226 50.2364 30.7498 50.2637 30.7669 50.2972C30.7863 50.3353 30.7863 50.3852 30.7863 50.485V51.478C30.7863 51.5778 30.7863 51.6277 30.7669 51.6658C30.7498 51.6993 30.7226 51.7265 30.6891 51.7436C30.651 51.763 30.6011 51.763 30.5013 51.763H27.5478C27.448 51.763 27.3981 51.763 27.36 51.7825C27.3265 51.7995 27.2992 51.8268 27.2822 51.8603C27.2628 51.8984 27.2628 51.9483 27.2628 52.048V53.677C27.2628 53.7767 27.2628 53.8266 27.2822 53.8647C27.2992 53.8982 27.3265 53.9255 27.36 53.9425C27.3981 53.962 27.448 53.962 27.5478 53.962H29.9715C30.0712 53.962 30.1211 53.962 30.1592 53.9814C30.1927 53.9984 30.22 54.0257 30.237 54.0592C30.2565 54.0973 30.2565 54.1472 30.2565 54.247V55.24C30.2565 55.3398 30.2565 55.3897 30.237 55.4278C30.22 55.4613 30.1927 55.4885 30.1592 55.5056C30.1211 55.525 30.0712 55.525 29.9715 55.525H27.5478C27.448 55.525 27.3981 55.525 27.36 55.5445C27.3265 55.5615 27.2992 55.5888 27.2822 55.6223C27.2628 55.6604 27.2628 55.7103 27.2628 55.81V57.6244C27.2628 57.7242 27.2628 57.774 27.2822 57.8121C27.2992 57.8457 27.3265 57.8729 27.36 57.89C27.3981 57.9094 27.448 57.9094 27.5478 57.9094H30.5013C30.6011 57.9094 30.651 57.9094 30.6891 57.9288C30.7226 57.9459 30.7498 57.9731 30.7669 58.0067C30.7863 58.0448 30.7863 58.0946 30.7863 58.1944V59.1875C30.7863 59.2872 30.7863 59.3371 30.7669 59.3752C30.7498 59.4087 30.7226 59.436 30.6891 59.4531C30.651 59.4725 30.6011 59.4725 30.5013 59.4725H25.6668Z" fill="#2845C1"/>
<path d="M32.3649 51.9087C32.2652 51.9087 32.2153 51.9087 32.1772 51.8893C32.1437 51.8723 32.1164 51.845 32.0993 51.8115C32.0799 51.7734 32.0799 51.7235 32.0799 51.6237V50.485C32.0799 50.3852 32.0799 50.3353 32.0993 50.2972C32.1164 50.2637 32.1437 50.2364 32.1772 50.2194C32.2153 50.2 32.2652 50.2 32.3649 50.2H39.3984C39.4982 50.2 39.548 50.2 39.5861 50.2194C39.6197 50.2364 39.6469 50.2637 39.664 50.2972C39.6834 50.3353 39.6834 50.3852 39.6834 50.485V51.6237C39.6834 51.7235 39.6834 51.7734 39.664 51.8115C39.6469 51.845 39.6197 51.8723 39.5861 51.8893C39.548 51.9087 39.4982 51.9087 39.3984 51.9087H37.1072C37.0074 51.9087 36.9575 51.9087 36.9194 51.9282C36.8859 51.9452 36.8587 51.9725 36.8416 52.006C36.8222 52.0441 36.8222 52.094 36.8222 52.1937V59.1875C36.8222 59.2872 36.8222 59.3371 36.8027 59.3752C36.7857 59.4087 36.7584 59.436 36.7249 59.4531C36.6868 59.4725 36.6369 59.4725 36.5372 59.4725H35.2262C35.1264 59.4725 35.0765 59.4725 35.0384 59.4531C35.0049 59.436 34.9776 59.4087 34.9606 59.3752C34.9412 59.3371 34.9412 59.2872 34.9412 59.1875V52.1937C34.9412 52.094 34.9412 52.0441 34.9217 52.006C34.9047 51.9725 34.8774 51.9452 34.8439 51.9282C34.8058 51.9087 34.7559 51.9087 34.6562 51.9087H32.3649Z" fill="#2845C1"/>
<path d="M39.5574 59.4725C39.4188 59.4725 39.3495 59.4725 39.305 59.4434C39.2662 59.4179 39.2388 59.3783 39.2287 59.3329C39.2172 59.281 39.2418 59.2162 39.2908 59.0866L42.5854 50.384C42.6105 50.3177 42.6231 50.2845 42.6447 50.26C42.6638 50.2384 42.6879 50.2218 42.7149 50.2115C42.7454 50.2 42.7809 50.2 42.8519 50.2H44.9221C44.993 50.2 45.0285 50.2 45.0591 50.2115C45.086 50.2218 45.1101 50.2384 45.1293 50.26C45.1509 50.2845 45.1635 50.3177 45.1886 50.384L48.4831 59.0866C48.5322 59.2162 48.5567 59.281 48.5452 59.3329C48.5352 59.3783 48.5078 59.4179 48.4689 59.4434C48.4245 59.4725 48.3552 59.4725 48.2166 59.4725H46.8278C46.7561 59.4725 46.7203 59.4725 46.6896 59.4607C46.6624 59.4503 46.6382 59.4335 46.6191 59.4116C46.5975 59.3868 46.5851 59.3532 46.5603 59.2859L45.8102 57.2482C45.7854 57.1809 45.773 57.1473 45.7514 57.1225C45.7323 57.1007 45.7081 57.0838 45.6809 57.0734C45.6502 57.0616 45.6143 57.0616 45.5427 57.0616H42.2313C42.1596 57.0616 42.1238 57.0616 42.0931 57.0734C42.0659 57.0838 42.0417 57.1007 42.0226 57.1225C42.0009 57.1473 41.9885 57.1809 41.9638 57.2482L41.2136 59.2859C41.1889 59.3532 41.1765 59.3868 41.1549 59.4116C41.1357 59.4335 41.1115 59.4503 41.0844 59.4607C41.0536 59.4725 41.0178 59.4725 40.9462 59.4725H39.5574ZM42.755 55.0761C42.708 55.2048 42.6845 55.2692 42.6964 55.3206C42.7067 55.3656 42.7342 55.4049 42.7729 55.43C42.8172 55.4588 42.8857 55.4588 43.0227 55.4588H44.7512C44.8883 55.4588 44.9568 55.4588 45.0011 55.43C45.0398 55.4049 45.0672 55.3656 45.0776 55.3206C45.0894 55.2692 45.0659 55.2048 45.0189 55.0761L44.1547 52.7084C44.0722 52.4823 44.0309 52.3692 43.9705 52.3372C43.9183 52.3094 43.8557 52.3094 43.8034 52.3372C43.7431 52.3692 43.7018 52.4823 43.6193 52.7084L42.755 55.0761Z" fill="#2845C1"/>
<path d="M17.4974 59.4725C17.3976 59.4725 17.3478 59.4725 17.3097 59.4531C17.2761 59.436 17.2489 59.4087 17.2318 59.3752C17.2124 59.3371 17.2124 59.2872 17.2124 59.1875V50.485C17.2124 50.3852 17.2124 50.3353 17.2318 50.2972C17.2489 50.2637 17.2761 50.2364 17.3097 50.2194C17.3478 50.2 17.3976 50.2 17.4974 50.2H19.9412C21.8752 50.2 23.0541 51.1537 23.0541 52.7168C23.0541 53.296 22.8514 53.8129 22.464 54.2136C22.3269 54.3553 22.2583 54.4262 22.2468 54.4727C22.2343 54.5229 22.237 54.5479 22.2599 54.5943C22.2811 54.6373 22.3603 54.6901 22.5189 54.7957C23.2086 55.255 23.584 55.9399 23.584 56.7437C23.584 58.4393 22.2726 59.4725 20.1399 59.4725H17.4974ZM19.9809 51.763H19.3784C19.2786 51.763 19.2288 51.763 19.1907 51.7825C19.1571 51.7995 19.1299 51.8268 19.1128 51.8603C19.0934 51.8984 19.0934 51.9483 19.0934 52.048V53.571C19.0934 53.6707 19.0934 53.7206 19.1128 53.7587C19.1299 53.7922 19.1571 53.8195 19.1907 53.8366C19.2288 53.856 19.2786 53.856 19.3784 53.856H19.9809C20.6962 53.856 21.1201 53.4718 21.1201 52.7963C21.1201 52.1472 20.6962 51.763 19.9809 51.763ZM20.2458 55.4191H19.3784C19.2786 55.4191 19.2288 55.4191 19.1907 55.4385C19.1571 55.4556 19.1299 55.4828 19.1128 55.5163C19.0934 55.5544 19.0934 55.6043 19.0934 55.7041V57.6244C19.0934 57.7242 19.0934 57.774 19.1128 57.8121C19.1299 57.8457 19.1571 57.8729 19.1907 57.89C19.2288 57.9094 19.2786 57.9094 19.3784 57.9094H20.2458C21.1201 57.9094 21.65 57.4458 21.65 56.6642C21.65 55.8694 21.1201 55.4191 20.2458 55.4191Z" fill="var(--c--globals--colors--logo-1)"/>
<path d="M25.6668 59.4725C25.567 59.4725 25.5171 59.4725 25.479 59.4531C25.4455 59.436 25.4182 59.4087 25.4012 59.3752C25.3818 59.3371 25.3818 59.2872 25.3818 59.1875V50.485C25.3818 50.3852 25.3818 50.3353 25.4012 50.2972C25.4182 50.2637 25.4455 50.2364 25.479 50.2194C25.5171 50.2 25.567 50.2 25.6668 50.2H30.5013C30.6011 50.2 30.651 50.2 30.6891 50.2194C30.7226 50.2364 30.7498 50.2637 30.7669 50.2972C30.7863 50.3353 30.7863 50.3852 30.7863 50.485V51.478C30.7863 51.5778 30.7863 51.6277 30.7669 51.6658C30.7498 51.6993 30.7226 51.7265 30.6891 51.7436C30.651 51.763 30.6011 51.763 30.5013 51.763H27.5478C27.448 51.763 27.3981 51.763 27.36 51.7825C27.3265 51.7995 27.2992 51.8268 27.2822 51.8603C27.2628 51.8984 27.2628 51.9483 27.2628 52.048V53.677C27.2628 53.7767 27.2628 53.8266 27.2822 53.8647C27.2992 53.8982 27.3265 53.9255 27.36 53.9425C27.3981 53.962 27.448 53.962 27.5478 53.962H29.9715C30.0712 53.962 30.1211 53.962 30.1592 53.9814C30.1927 53.9984 30.22 54.0257 30.237 54.0592C30.2565 54.0973 30.2565 54.1472 30.2565 54.247V55.24C30.2565 55.3398 30.2565 55.3897 30.237 55.4278C30.22 55.4613 30.1927 55.4885 30.1592 55.5056C30.1211 55.525 30.0712 55.525 29.9715 55.525H27.5478C27.448 55.525 27.3981 55.525 27.36 55.5445C27.3265 55.5615 27.2992 55.5888 27.2822 55.6223C27.2628 55.6604 27.2628 55.7103 27.2628 55.81V57.6244C27.2628 57.7242 27.2628 57.774 27.2822 57.8121C27.2992 57.8457 27.3265 57.8729 27.36 57.89C27.3981 57.9094 27.448 57.9094 27.5478 57.9094H30.5013C30.6011 57.9094 30.651 57.9094 30.6891 57.9288C30.7226 57.9459 30.7498 57.9731 30.7669 58.0067C30.7863 58.0448 30.7863 58.0946 30.7863 58.1944V59.1875C30.7863 59.2872 30.7863 59.3371 30.7669 59.3752C30.7498 59.4087 30.7226 59.436 30.6891 59.4531C30.651 59.4725 30.6011 59.4725 30.5013 59.4725H25.6668Z" fill="var(--c--globals--colors--logo-1)"/>
<path d="M32.3649 51.9087C32.2652 51.9087 32.2153 51.9087 32.1772 51.8893C32.1437 51.8723 32.1164 51.845 32.0993 51.8115C32.0799 51.7734 32.0799 51.7235 32.0799 51.6237V50.485C32.0799 50.3852 32.0799 50.3353 32.0993 50.2972C32.1164 50.2637 32.1437 50.2364 32.1772 50.2194C32.2153 50.2 32.2652 50.2 32.3649 50.2H39.3984C39.4982 50.2 39.548 50.2 39.5861 50.2194C39.6197 50.2364 39.6469 50.2637 39.664 50.2972C39.6834 50.3353 39.6834 50.3852 39.6834 50.485V51.6237C39.6834 51.7235 39.6834 51.7734 39.664 51.8115C39.6469 51.845 39.6197 51.8723 39.5861 51.8893C39.548 51.9087 39.4982 51.9087 39.3984 51.9087H37.1072C37.0074 51.9087 36.9575 51.9087 36.9194 51.9282C36.8859 51.9452 36.8587 51.9725 36.8416 52.006C36.8222 52.0441 36.8222 52.094 36.8222 52.1937V59.1875C36.8222 59.2872 36.8222 59.3371 36.8027 59.3752C36.7857 59.4087 36.7584 59.436 36.7249 59.4531C36.6868 59.4725 36.6369 59.4725 36.5372 59.4725H35.2262C35.1264 59.4725 35.0765 59.4725 35.0384 59.4531C35.0049 59.436 34.9776 59.4087 34.9606 59.3752C34.9412 59.3371 34.9412 59.2872 34.9412 59.1875V52.1937C34.9412 52.094 34.9412 52.0441 34.9217 52.006C34.9047 51.9725 34.8774 51.9452 34.8439 51.9282C34.8058 51.9087 34.7559 51.9087 34.6562 51.9087H32.3649Z" fill="var(--c--globals--colors--logo-1)"/>
<path d="M39.5574 59.4725C39.4188 59.4725 39.3495 59.4725 39.305 59.4434C39.2662 59.4179 39.2388 59.3783 39.2287 59.3329C39.2172 59.281 39.2418 59.2162 39.2908 59.0866L42.5854 50.384C42.6105 50.3177 42.6231 50.2845 42.6447 50.26C42.6638 50.2384 42.6879 50.2218 42.7149 50.2115C42.7454 50.2 42.7809 50.2 42.8519 50.2H44.9221C44.993 50.2 45.0285 50.2 45.0591 50.2115C45.086 50.2218 45.1101 50.2384 45.1293 50.26C45.1509 50.2845 45.1635 50.3177 45.1886 50.384L48.4831 59.0866C48.5322 59.2162 48.5567 59.281 48.5452 59.3329C48.5352 59.3783 48.5078 59.4179 48.4689 59.4434C48.4245 59.4725 48.3552 59.4725 48.2166 59.4725H46.8278C46.7561 59.4725 46.7203 59.4725 46.6896 59.4607C46.6624 59.4503 46.6382 59.4335 46.6191 59.4116C46.5975 59.3868 46.5851 59.3532 46.5603 59.2859L45.8102 57.2482C45.7854 57.1809 45.773 57.1473 45.7514 57.1225C45.7323 57.1007 45.7081 57.0838 45.6809 57.0734C45.6502 57.0616 45.6143 57.0616 45.5427 57.0616H42.2313C42.1596 57.0616 42.1238 57.0616 42.0931 57.0734C42.0659 57.0838 42.0417 57.1007 42.0226 57.1225C42.0009 57.1473 41.9885 57.1809 41.9638 57.2482L41.2136 59.2859C41.1889 59.3532 41.1765 59.3868 41.1549 59.4116C41.1357 59.4335 41.1115 59.4503 41.0844 59.4607C41.0536 59.4725 41.0178 59.4725 40.9462 59.4725H39.5574ZM42.755 55.0761C42.708 55.2048 42.6845 55.2692 42.6964 55.3206C42.7067 55.3656 42.7342 55.4049 42.7729 55.43C42.8172 55.4588 42.8857 55.4588 43.0227 55.4588H44.7512C44.8883 55.4588 44.9568 55.4588 45.0011 55.43C45.0398 55.4049 45.0672 55.3656 45.0776 55.3206C45.0894 55.2692 45.0659 55.2048 45.0189 55.0761L44.1547 52.7084C44.0722 52.4823 44.0309 52.3692 43.9705 52.3372C43.9183 52.3094 43.8557 52.3094 43.8034 52.3372C43.7431 52.3692 43.7018 52.4823 43.6193 52.7084L42.755 55.0761Z" fill="var(--c--globals--colors--logo-1)"/>
<defs>
<linearGradient id="paint0_linear_13360_26870" x1="32.8125" y1="46" x2="32.8125" y2="64" gradientUnits="userSpaceOnUse">
<stop stop-color="#2845C1"/>
<stop offset="1" stop-color="#EEF1F4"/>
<stop stop-color="var(--c--globals--colors--logo-1)"/>
<stop offset="1" stop-color="var(--c--contextuals--background--semantic--neutral--tertiary)"/>
</linearGradient>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 13 KiB

@@ -14,8 +14,10 @@ export interface BoxProps {
as?: HTMLElementType;
$align?: CSSProperties['alignItems'];
$background?: CSSProperties['background'];
$border?: CSSProperties['border'];
$color?: CSSProperties['color'];
$css?: string | RuleSet<object>;
$cursor?: CSSProperties['cursor'];
$direction?: CSSProperties['flexDirection'];
$display?: CSSProperties['display'];
$effect?: 'show' | 'hide';
@@ -37,28 +39,44 @@ export interface BoxProps {
$shrink?: CSSProperties['flexShrink'];
$transition?: CSSProperties['transition'];
$width?: CSSProperties['width'];
$wrap?: CSSProperties['flexWrap'];
$zIndex?: CSSProperties['zIndex'];
$wrap?: CSSProperties['flexWrap'];
// Theming props
$layer?: 'background' | 'content' | 'border';
$theme?:
| 'brand'
| 'error'
| 'gray'
| 'info'
| 'success'
| 'warning'
| 'neutral'
| 'contextual'
| 'disabled'
| (string & {});
$scope?: 'surface' | 'semantic' | 'palette' | (string & {});
$variation?: 'primary' | 'secondary' | 'tertiary' | (string & {});
$withThemeBG?: boolean;
$withThemeBorder?: boolean;
$withThemeInherited?: boolean;
}
export type BoxType = ComponentPropsWithRef<typeof Box>;
export const Box = styled('div')<BoxProps>`
display: flex;
flex-direction: column;
${({ $align }) => $align && `align-items: ${$align};`}
${({ $background }) => $background && `background: ${$background};`}
${({ $color }) => $color && `color: ${$color};`}
${({ $direction }) => $direction && `flex-direction: ${$direction};`}
${({ $display }) => $display && `display: ${$display};`}
${({ $cursor }) => $cursor && `cursor: ${$cursor};`}
${({ $direction }) => `flex-direction: ${$direction || 'column'};`}
${({ $display, as }) =>
`display: ${$display || (as?.match('span|input') ? 'inline-flex' : 'flex')};`}
${({ $flex }) => $flex && `flex: ${$flex};`}
${({ $gap }) => $gap && `gap: ${$gap};`}
${({ $height }) => $height && `height: ${$height};`}
${({ $hasTransition }) =>
$hasTransition && $hasTransition === 'slow'
? `transition: all 0.5s ease-in-out;`
? `transition: all 0.5s var(--c--globals--transitions--ease-out);`
: $hasTransition
? `transition: all 0.3s ease-in-out;`
? `transition: all var(--c--globals--transitions--duration) var(--c--globals--transitions--ease-out);`
: ''}
${({ $justify }) => $justify && `justify-content: ${$justify};`}
${({ $margin }) => $margin && stylesMargin($margin)}
@@ -72,6 +90,86 @@ export const Box = styled('div')<BoxProps>`
${({ $position }) => $position && `position: ${$position};`}
${({ $radius }) => $radius && `border-radius: ${$radius};`}
${({ $shrink }) => $shrink && `flex-shrink: ${$shrink};`}
${({
$layer = 'border',
$theme = 'brand',
$variation = 'primary',
$scope = 'semantic',
$border,
$withThemeBorder,
$withThemeInherited,
}) => {
if ($border) {
return `border: ${$border};`;
}
if (!$layer || !$scope || !$theme || !$withThemeBorder) {
return '';
}
if ($withThemeInherited) {
return `border: inherit;`;
}
return `border: 1px solid var(--c--contextuals--${$layer}--${$scope}${$theme ? `--${$theme}` : ''}${$variation ? `--${$variation}` : ''});`;
}}
${({
$layer = 'background',
$theme = 'brand',
$variation = 'primary',
$scope = 'semantic',
$background,
$withThemeBG,
$withThemeInherited,
}) => {
if ($background) {
return `background: ${$background};`;
}
if (!$layer || !$scope || !$theme || !$withThemeBG) {
return '';
}
if ($withThemeInherited) {
return `background: inherit;`;
}
return `background: var(--c--contextuals--${$layer}--${$scope}${$theme ? `--${$theme}` : ''}${$variation ? `--${$variation}` : ''});`;
}}
${({
$layer = 'content',
$theme = 'neutral',
$variation = 'primary',
$scope = 'semantic',
$color,
$withThemeBG,
$withThemeInherited,
}) => {
if ($color) {
return `color: ${$color};`;
}
if (!$layer || !$scope) {
return '';
}
// There is a special case when primary with background
if (
$withThemeBG &&
$layer === 'content' &&
$scope === 'semantic' &&
$variation === 'primary' &&
$theme
) {
$variation = `on-${$theme}`;
}
if ($withThemeInherited) {
return `color: inherit;`;
}
return `color: var(--c--contextuals--${$layer}--${$scope}${$theme ? `--${$theme}` : ''}${$variation ? `--${$variation}` : ''});`;
}}
${({ $transition }) => $transition && `transition: ${$transition};`}
${({ $width }) => $width && `width: ${$width};`}
${({ $wrap }) => $wrap && `flex-wrap: ${$wrap};`}
@@ -91,7 +189,7 @@ export const Box = styled('div')<BoxProps>`
return (
effect &&
`
transition: all 0.3s ease-in-out;
transition: all var(--c--globals--transitions--duration) var(--c--globals--transitions--ease-out);
${effect}
`
);
@@ -39,12 +39,12 @@ const BoxButton = forwardRef<HTMLDivElement, BoxButtonType>(
font-family: inherit;
color: ${props.disabled
? 'var(--c--theme--colors--greyscale-400) !important'
? 'var(--c--theme--colors--gray-400) !important'
: 'inherit'};
${$css || ''}
`}
{...props}
className={`--docs--box-button ${props.className || ''}`}
className={`--conversations--box-button ${props.className || ''}`}
onClick={(event: React.MouseEvent<HTMLDivElement>) => {
if (props.disabled) {
return;
@@ -1,26 +1,13 @@
import { PropsWithChildren } from 'react';
import { css } from 'styled-components';
import { useCunninghamTheme } from '@/cunningham';
import { Box, BoxType } from '.';
export const Card = ({
children,
$css,
...props
}: PropsWithChildren<BoxType>) => {
const { colorsTokens } = useCunninghamTheme();
export const Card = ({ children, ...props }: PropsWithChildren<BoxType>) => {
return (
<Box
className={`--docs--card ${props.className || ''}`}
$background="white"
$radius="4px"
$css={css`
border: 1px solid ${colorsTokens['greyscale-200']};
${$css}
`}
{...props}
>
{children}
@@ -13,11 +13,13 @@ import { useCunninghamTheme } from '@/cunningham';
import { BoxProps } from './Box';
const StyledPopover = styled(Popover)`
background-color: white;
border-radius: 4px;
box-shadow: 1px 1px 5px rgba(0, 0, 0, 0.1);
border: 1px solid #dddddd;
transition: opacity 0.2s ease-in-out;
background-color: var(--c--contextuals--background--surface--primary);
border-radius: var(--c--globals--spacings--st);
box-shadow: 0 0 6px 0 rgba(0, 0, 145, 0.1);
border: 1px solid var(--c--contextuals--border--surface--primary);
transition: opacity var(--c--globals--transitions--duration)
var(--c--globals--transitions--ease-out);
color: var(--c--contextuals--content--semantic--brand--tertiary);
`;
interface StyledButtonProps {
@@ -28,16 +30,23 @@ const StyledButton = styled(Button)<StyledButtonProps>`
border: none;
background: none;
outline: none;
transition: background 0.2s ease-in-out;
font-weight: 500;
font-size: 0.938rem;
padding: 0;
&:focus-visible {
outline: 2px solid #3e5de7;
outline-offset: 2px;
font-weight: var(--c--components--button--font-weight);
font-size: var(--c--components--button--medium-font-size);
padding: var(--c--globals--spacings--0);
border-radius: var(--c--globals--spacings--st);
color: var(--c--contextuals--content--semantic--brand--tertiary);
&:hover {
background-color: var(
--c--contextuals--background--semantic--contextual--primary
);
}
&:focus-visible {
box-shadow: 0 0 0 2px var(--c--globals--colors--brand-400);
background-color: var(
--c--contextuals--background--semantic--brand--tertiary-hover
);
border-radius: var(--c--globals--spacings--st);
}
${({ $css }) => $css};
`;
@@ -47,6 +56,7 @@ export interface DropButtonProps {
isOpen?: boolean;
onOpenChange?: (isOpen: boolean) => void;
label?: string;
testId?: string;
}
export const DropButton = ({
@@ -56,6 +66,7 @@ export const DropButton = ({
onOpenChange,
children,
label,
testId,
}: PropsWithChildren<DropButtonProps>) => {
const { themeTokens } = useCunninghamTheme();
const font = themeTokens['font']?.['families']['base'];
@@ -78,6 +89,7 @@ export const DropButton = ({
ref={triggerRef}
onPress={() => onOpenChangeHandler(true)}
aria-label={label}
data-testid={testId}
$css={css`
font-family: ${font};
${buttonCss};
@@ -33,7 +33,7 @@ export const DropdownMenu = ({
label,
topMessage,
}: PropsWithChildren<DropdownMenuProps>) => {
const { spacingsTokens, colorsTokens } = useCunninghamTheme();
const { spacingsTokens } = useCunninghamTheme();
const [isOpen, setIsOpen] = useState(false);
const [buttonWidth, setButtonWidth] = useState<number | undefined>(undefined);
const blockButtonRef = useRef<HTMLDivElement>(null);
@@ -64,14 +64,20 @@ export const DropdownMenu = ({
<Box
ref={blockButtonRef}
$direction="row"
$theme="brand"
$scope="semantic"
$variation="neutral"
$align="center"
$position="relative"
aria-controls="menu"
>
<Box>{children}</Box>
<Icon
$variation="text"
$theme="primary"
$css={css`
color: var(
--c--contextuals--content--semantic--brand--tertiary
);
`}
iconName={isOpen ? 'arrow_drop_up' : 'arrow_drop_down'}
/>
</Box>
@@ -89,7 +95,8 @@ export const DropdownMenu = ({
>
{topMessage && (
<Text
$variation="700"
$theme="brand"
$variation="tertiary"
$wrap="wrap"
$size="xs"
$weight="bold"
@@ -120,8 +127,6 @@ export const DropdownMenu = ({
key={option.label}
$align="center"
$justify="space-between"
$background={colorsTokens['greyscale-000']}
$color={colorsTokens['primary-600']}
$padding={{ vertical: 'xs', horizontal: 'base' }}
$width="100%"
$gap={spacingsTokens['base']}
@@ -134,23 +139,29 @@ export const DropdownMenu = ({
`}
${index === options.length - 1 &&
css`
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
border-bottom-left-radius: var(--c--globals--spacings--st);
border-bottom-right-radius: var(--c--globals--spacings--st);
`}
font-size: var(--c--theme--font--sizes--sm);
color: var(--c--theme--colors--greyscale-1000);
font-weight: 500;
font-size: var(--c--globals--font--sizes--sm);
color: var(
--c--contextuals--content--semantic--brand--tertiary
);
font-weight: var(--c--globals--font--weights--medium);
cursor: ${isDisabled ? 'not-allowed' : 'pointer'};
user-select: none;
&:hover,
&:focus {
background-color: var(--c--theme--colors--greyscale-050);
&:hover {
background-color: var(
--c--contextuals--background--semantic--contextual--primary
);
}
&:focus-visible {
outline: 2px solid #3e5de7;
outline: 2px solid var(--c--globals--colors--brand-400);
outline-offset: -2px;
background-color: var(
--c--contextuals--background--semantic--contextual--primary
);
}
`}
>
@@ -162,9 +173,10 @@ export const DropdownMenu = ({
{option.icon && (
<Icon
$size="20px"
$theme="greyscale"
$variation={isDisabled ? '400' : '1000'}
$theme="neutral"
$variation={isDisabled ? 'tertiary' : 'primary'}
iconName={option.icon}
aria-hidden="true"
/>
)}
<Text $variation={isDisabled ? '400' : '1000'}>
@@ -172,7 +184,7 @@ export const DropdownMenu = ({
</Text>
</Box>
{option.isSelected && (
<Icon iconName="check" $size="20px" $theme="greyscale" />
<Icon iconName="check" $size="20px" aria-hidden="true" />
)}
</BoxButton>
);
@@ -10,12 +10,14 @@ type IconProps = TextType & {
export const Icon = ({
iconName,
variant = 'outlined',
$theme = 'neutral',
...textProps
}: IconProps) => {
return (
<Text
{...textProps}
aria-hidden="true"
$theme={$theme}
className={clsx('--docs--icon-bg', textProps.className, {
'material-symbols': variant === 'filled',
'material-symbols-outlined': variant === 'outlined',
@@ -39,7 +39,7 @@ export const InfiniteScroll = ({
{!isLoading && hasMore && (
<Button
onClick={() => void next()}
color="primary-text"
color="neutral"
icon={<Icon iconName="arrow_downward" />}
>
{buttonLabel ?? t('Load more')}
@@ -5,7 +5,7 @@ import { tokens } from '@/cunningham';
import { Box, BoxProps } from './Box';
const { sizes } = tokens.themes.default.theme.font;
const { sizes } = tokens.themes.default.globals.font;
type TextSizes = keyof typeof sizes;
export interface TextProps extends BoxProps {
@@ -13,65 +13,28 @@ export interface TextProps extends BoxProps {
$ellipsis?: boolean;
$weight?: CSSProperties['fontWeight'];
$textAlign?: CSSProperties['textAlign'];
$textTransform?: CSSProperties['textTransform'];
$size?: TextSizes | (string & {});
$theme?:
| 'primary'
| 'primary-text'
| 'secondary'
| 'secondary-text'
| 'info'
| 'success'
| 'warning'
| 'danger'
| 'greyscale';
$variation?:
| 'text'
| '000'
| '050'
| '100'
| '200'
| '300'
| '400'
| '500'
| '550'
| '600'
| '650'
| '700'
| '750'
| '800'
| '850'
| '900'
| '1000';
}
export type TextType = ComponentPropsWithRef<typeof Text>;
export const TextStyled = styled(Box)<TextProps>`
${({ $textAlign }) => $textAlign && `text-align: ${$textAlign};`}
${({ $textTransform }) =>
$textTransform && `text-transform: ${$textTransform};`}
${({ $weight }) => $weight && `font-weight: ${$weight};`}
${({ $size }) =>
$size &&
`font-size: ${$size in sizes ? sizes[$size as TextSizes] : $size};`}
${({ $theme, $variation }) =>
`color: var(--c--theme--colors--${$theme}-${$variation});`}
${({ $color }) => $color && `color: ${$color};`}
${({ $ellipsis }) =>
$ellipsis &&
`white-space: nowrap; overflow: hidden; text-overflow: ellipsis;`}
`;
const Text = forwardRef<HTMLElement, ComponentPropsWithRef<typeof TextStyled>>(
({ className, ...props }, ref) => {
return (
<TextStyled
ref={ref}
as="span"
$theme="greyscale"
$variation="text"
className={className}
{...props}
/>
);
(props, ref) => {
return <TextStyled ref={ref} as="span" {...props} />;
},
);
@@ -7,7 +7,7 @@ import { Box, Text, TextType } from '@/components';
const AlertStyled = styled(Alert)`
& .c__button--tertiary:hover {
background-color: var(--c--theme--colors--greyscale-200);
background-color: var(--c--theme--colors--gray-200);
}
`;
@@ -0,0 +1,71 @@
import { Box } from './Box';
import { Icon } from './Icon';
interface ToggleSwitchProps {
checked: boolean;
onChange: () => void;
disabled?: boolean;
'aria-label'?: string;
}
export const ToggleSwitch = ({
checked,
onChange,
disabled = false,
'aria-label': ariaLabel,
}: ToggleSwitchProps) => {
return (
<Box $css="padding-top: 2px;">
<Box
$css={`
position: relative;
width: 44px;
height: 24px;
background-color: ${checked ? 'var(--c--contextuals--content--semantic--brand--tertiary)' : 'var(--c--contextuals--content--semantic--neutral--tertiary)'};
border-radius: 12px;
cursor: ${disabled ? 'not-allowed' : 'pointer'};
transition: all 0.2s ease;
opacity: ${disabled ? 0.6 : 1};
`}
onClick={disabled ? undefined : onChange}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
if (!disabled) {
onChange();
}
}
}}
role="switch"
aria-checked={checked}
aria-label={ariaLabel}
aria-disabled={disabled}
tabIndex={disabled ? -1 : 0}
>
<Box
$css={`
position: absolute;
top: 2px;
left: ${checked ? '22px' : '2px'};
width: 20px;
height: 20px;
background-color: white;
border-radius: 50%;
transition: left 0.2s ease;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
display: flex;
align-items: center;
justify-content: center;
`}
>
<Icon
iconName={checked ? 'check' : ''}
$size="12px"
$theme="brand"
$variation="tertiary"
/>
</Box>
</Box>
</Box>
);
};
@@ -12,5 +12,6 @@ export * from './SideModal';
export * from './separators';
export * from './Text';
export * from './TextErrors';
export * from './ToggleSwitch';
export * from './CustomToast';
export * from './ToastProvider';
@@ -64,9 +64,8 @@ export const QuickSearch = ({
bottom: -20px;
background: linear-gradient(
to bottom,
#F7F8FA 0%,
rgba(247, 248, 250, 0.7) 40%,
rgba(255, 255, 255, 0) 100%
rgba(var(--c--contextuals--background--surface--tertiary), 1),
rgba(var(--c--contextuals--background--surface--tertiary), 0)
);
}
${
@@ -74,17 +73,16 @@ export const QuickSearch = ({
? `
&:before {
content: "";
background-color: #F7F8FA;
background-color: var(--c--contextuals--background--surface--tertiary);
position: absolute;
width: 100%;
height: 20px;
top: -8px;
}
background-color: #F7F8FA;
`
: 'background-color: #FFF;'
: ''
}
}`}
`}
>
<QuickSearchInput
withSeparator={hasChildrens(children)}
@@ -77,34 +77,38 @@ export const QuickSearchInput = ({
justify-content: space-between;
border-radius: 4px;
margin: 12px 12px 0 12px;
border: 1px solid #CFD5DE;
background-color: #eff1f5;
border: 1px solid var(--c--contextuals--border--semantic--neutral--tertiary);
background-color: var(--c--contextuals--background--semantic--neutral--tertiary);
padding: 6px 8px;
cursor: pointer !important;
transition: border-color 0.2s ease;
&:hover {
border: 2px solid #CFD5DE;
border: 2px solid var(--c--contextuals--border--semantic--neutral--tertiary);
padding: 5px 7px;
}
&:focus-within {
border: 2px solid #3E5DE7;
border: 2px solid var(--c--contextuals--border--semantic--brand--primary);
padding: 5px 7px;
}
& input {
font-family: 'Marianne' !important;
border: none;
font-size: 0.9rem;
color: var(--c--contextuals--content--semantic--neutral--primary);
font-weight: 400;
background-color: transparent;
&:focus {
border: none;
outline: none;
}
&::placeholder {
color: var(--c--contextuals--content--semantic--neutral--tertiary);
}
}
`}
>
<Box $direction="row" $gap="6px">
<Icon iconName="search" $variation="600" />
<Icon iconName="search" $theme="neutral" $variation="tertiary" />
<Command.Input
aria-label={t('Quick search input')}
value={localValue}
@@ -132,10 +136,10 @@ export const QuickSearchInput = ({
padding: 7px;
z-index: 1000;
&:hover {
background-color: #e4e6ea;
background-color: var(--c--contextuals--background--semantic--neutral--secondary);
}
&:focus-visible {
outline: 2px solid #3E5DE7;
outline: 2px solid var(--c--contextuals--border--semantic--brand--primary);
outline-offset: 2px;
}`}
>
@@ -15,10 +15,9 @@ export const QuickSearchStyle = createGlobalStyle`
[cmdk-input] {
border: none;
&::placeholder {
input::placeholder {
font-size: 14px;
color: #626A80;
color: var(--c--contextuals--content--semantic--neutral--tertiary) !important;
}
}
@@ -41,14 +40,14 @@ export const QuickSearchStyle = createGlobalStyle`
&:hover,
&[data-selected='true'] {
background: var(--c--theme--colors--greyscale-100);
background: var(--c--theme--colors--gray-100);
.show-right-on-focus {
opacity: 1;
}
}
&[data-disabled='true'] {
color: var(--c--theme--colors--greyscale-500);
color: var(--c--theme--colors--gray-500);
cursor: not-allowed;
}
@@ -79,7 +78,7 @@ export const QuickSearchStyle = createGlobalStyle`
height: 20px;
border-radius: 4px;
color: white;
background: var(--c--theme--colors--greyscale-500);
background: var(--c--contextuals--content--semantic--neutral--tertiary);
display: inline-flex;
align-items: center;
justify-content: center;
@@ -102,7 +101,7 @@ export const QuickSearchStyle = createGlobalStyle`
padding: 0 var(--c--theme--spacings--xs);
user-select: none;
font-size: 12px;
color: var(--c--theme--colors--greyscale-700);
color: var(--c--theme--colors--gray-700);
font-weight: 700;;
margin-bottom: var(--c--theme--spacings--xs);
}
@@ -26,7 +26,7 @@ export const HorizontalSeparator = ({
$background={
variant === SeparatorVariant.DARK
? '#e5e5e533'
: colorsTokens['greyscale-100']
: colorsTokens['gray-100']
}
className="--docs--horizontal-separator"
/>
@@ -13,7 +13,7 @@ export const SeparatedSection = ({
showSeparator = true,
children,
}: PropsWithChildren<Props>) => {
const { colorsTokens, spacingsTokens } = useCunninghamTheme();
const { spacingsTokens } = useCunninghamTheme();
return (
<Box
$css={css`
@@ -21,7 +21,8 @@ export const SeparatedSection = ({
padding: ${spacingsTokens['sm']} 0;
${showSeparator &&
css`
border-bottom: 1px solid ${colorsTokens['greyscale-100']};
border-bottom: 1px solid
var(--c--contextuals--border--surface--primary);
`}
`}
>
@@ -1,16 +1,20 @@
import { useCunninghamTheme } from '../useCunninghamTheme';
describe('<useCunninghamTheme />', () => {
it('has the logo correctly set', () => {
expect(useCunninghamTheme.getState().themeTokens.logo?.src).toBe('');
it('has the favicon correctly set', () => {
const favicon1 = (
useCunninghamTheme.getState().componentTokens as Record<string, unknown>
).favicon as { 'png-light': string; 'png-dark': string } | undefined;
expect(favicon1?.['png-light']).toBe('/assets/favicon-light.png');
// Change theme
useCunninghamTheme.getState().setTheme('dsfr');
const { themeTokens } = useCunninghamTheme.getState();
const logo = themeTokens.logo;
expect(logo?.src).toBe('/assets/logo-gouv.svg');
expect(logo?.widthHeader).toBe('110px');
expect(logo?.widthFooter).toBe('220px');
const { componentTokens } = useCunninghamTheme.getState();
const favicon = (componentTokens as Record<string, unknown>).favicon as
| { 'png-light': string; 'png-dark': string }
| undefined;
expect(favicon?.['png-light']).toBe('/assets/favicon-light.png');
expect(favicon?.['png-dark']).toBe('/assets/favicon-dark.png');
});
});
@@ -1,4 +1,5 @@
@import url('@gouvfr-lasuite/ui-kit/style');
@import url('@gouvfr-lasuite/ui-kit/fonts/Marianne');
@import url('./cunningham-tokens.css');
:root {
@@ -26,7 +27,7 @@
* Select
**/
--c--components--forms-select--value-color--disabled: var(
--c--theme--colors--greyscale-400
--c--globals--colors--gray-400
);
/**
@@ -41,7 +42,7 @@
* Tooltip
*/
.c__tooltip {
padding: 4px 6px;
padding: var(--c--globals--font--sizes--sm) var(--c--globals--spacings--xxs);
}
/**
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -1,61 +1,163 @@
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 &
Partial<(typeof tokens.themes)[keyof typeof tokens.themes]>;
type ColorsTokens = Tokens['theme']['colors'];
type FontSizesTokens = Tokens['theme']['font']['sizes'];
type SpacingsTokens = Tokens['theme']['spacings'];
type ComponentTokens = Tokens['components'] & {
alpha?: boolean;
beta?: boolean;
};
type ColorsTokens = Tokens['globals']['colors'];
type FontSizesTokens = Tokens['globals']['font']['sizes'];
type SpacingsTokens = Tokens['globals']['spacings'];
type ComponentTokens = Partial<
| (Tokens['components'] & Tokens['globals']['components'])
| Record<string, unknown>
> &
Record<string, unknown>;
type ContextualTokens = Tokens['contextuals'];
export type Theme = keyof typeof tokens.themes;
interface ThemeStore {
colorsTokens: Partial<ColorsTokens>;
componentTokens: ComponentTokens;
contextualTokens: ContextualTokens;
currentTokens: Partial<Tokens>;
fontSizesTokens: Partial<FontSizesTokens>;
setTheme: (theme: Theme) => void;
spacingsTokens: Partial<SpacingsTokens>;
theme: Theme;
themeTokens: Partial<Tokens['theme']>;
baseTheme: Theme; // 'default' or 'dsfr' (not persisted)
themeTokens: Partial<Tokens['globals']>;
isDarkMode: boolean;
toggleDarkMode: () => void;
}
const getMergedTokens = (theme: Theme) => {
return merge({}, tokens.themes['default'], tokens.themes[theme]);
};
const DEFAULT_THEME: Theme = 'generic';
const defaultTokens = getMergedTokens(DEFAULT_THEME);
const initialState: ThemeStore = {
colorsTokens: defaultTokens.theme.colors,
componentTokens: defaultTokens.components,
currentTokens: tokens.themes[DEFAULT_THEME] as Partial<Tokens>,
fontSizesTokens: defaultTokens.theme.font.sizes,
setTheme: () => {},
spacingsTokens: defaultTokens.theme.spacings,
theme: DEFAULT_THEME,
themeTokens: defaultTokens.theme,
const getComponentTokens = (
mergedTokens: ReturnType<typeof getMergedTokens>,
) => {
// Merge components from root level (favicon, etc.) and globals.components (logo, etc.)
return merge(
{},
mergedTokens.components || {},
mergedTokens.globals?.components || {},
);
};
export const useCunninghamTheme = create<ThemeStore>((set) => ({
...initialState,
setTheme: (theme: Theme) => {
const newTokens = getMergedTokens(theme);
const DEFAULT_THEME: Theme = 'default';
const defaultTokens = getMergedTokens(DEFAULT_THEME);
set({
colorsTokens: newTokens.theme.colors,
componentTokens: newTokens.components,
currentTokens: tokens.themes[theme] as Partial<Tokens>,
fontSizesTokens: newTokens.theme.font.sizes,
spacingsTokens: newTokens.theme.spacings,
theme,
themeTokens: newTokens.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),
contextualTokens: defaultTokens.contextuals,
currentTokens: tokens.themes[DEFAULT_THEME] as Partial<Tokens>,
fontSizesTokens: defaultTokens.globals.font.sizes,
setTheme: () => {},
spacingsTokens: defaultTokens.globals.spacings,
theme: DEFAULT_THEME,
baseTheme: DEFAULT_THEME,
themeTokens: defaultTokens.globals,
isDarkMode: getIsDarkModeFromPreferences(),
toggleDarkMode: () => {},
};
export const useCunninghamTheme = create<ThemeStore>()(
persist(
(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);
set({
colorsTokens: newTokens.globals.colors,
componentTokens: getComponentTokens(newTokens),
contextualTokens: newTokens.contextuals,
currentTokens: tokens.themes[finalTheme] as Partial<Tokens>,
fontSizesTokens: newTokens.globals.font.sizes,
spacingsTokens: newTokens.globals.spacings,
theme: finalTheme,
baseTheme,
themeTokens: newTokens.globals,
isDarkMode,
});
},
toggleDarkMode: () => {
useChatPreferencesStore.getState().toggleDarkModePreferences();
set((state) => {
const newIsDarkMode = getIsDarkModeFromPreferences();
const newTheme: Theme = newIsDarkMode
? state.baseTheme === 'dsfr'
? 'dsfr-dark'
: 'dark'
: state.baseTheme;
const newTokens = getMergedTokens(newTheme);
return {
colorsTokens: newTokens.globals.colors,
componentTokens: getComponentTokens(newTokens),
contextualTokens: newTokens.contextuals,
currentTokens: tokens.themes[newTheme] as Partial<Tokens>,
fontSizesTokens: newTokens.globals.font.sizes,
spacingsTokens: newTokens.globals.spacings,
theme: newTheme,
baseTheme: state.baseTheme,
themeTokens: newTokens.globals,
isDarkMode: newIsDarkMode,
};
});
},
}),
{
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();
}
},
},
),
);
@@ -232,13 +232,21 @@ export const ActivationPage = () => {
width: 100%;
height: 40px;
padding: 6px 8px;
border: 1px solid ${error ? 'var(--c--theme--colors--danger-600)' : 'var(--c--theme--colors--greyscale-150)'};
border: 1px solid ${
error
? 'var(--c--contextuals--border--semantic--error--primary)'
: 'var(--c--contextuals--border--semantic--neutral--tertiary)'
};
border-radius: 4px;
fontSize: 14px;
font-size: 14px;
outline: none;
&:focus {
border: 1px solid ${error ? 'var(--c--theme--colors--danger-600)' : 'var(--c--theme--colors--greyscale-150)'};
border: 1px solid ${
error
? 'var(--c--contextuals--border--semantic--error--primary)'
: 'var(--c--contextuals--border--semantic--neutral--tertiary)'
};
box-shadow: none;
}
}
@@ -258,8 +266,8 @@ export const ActivationPage = () => {
{error && (
<Text
$size="xs"
$theme="danger"
$variation="600"
$theme="error"
$variation="tertiary"
$margin={{ top: '4px' }}
>
{error}
@@ -270,7 +278,7 @@ export const ActivationPage = () => {
<Button
type="submit"
fullWidth={isDesktop ? false : true}
color="primary"
color="brand"
icon={<IconKey />}
disabled={isSubmitting || !code.trim()}
style={{
@@ -360,14 +368,15 @@ export const ActivationPage = () => {
<Box $direction="column" $gap="1rem" $justify="flex-end">
<Button
fullWidth
color="primary"
color="brand"
onClick={handleNotificationRegister}
>
{t('Notify me')}
</Button>
<Button
fullWidth
color="tertiary"
color="neutral"
variant="bordered"
onClick={() => {
setIsNotifyModalOpen(false);
}}
@@ -442,8 +451,8 @@ export const ActivationPage = () => {
<Text
$size="xs"
$weight="700"
$theme="primary"
$variation="650"
$theme="brand"
$variation="primary"
$align="center"
>
{app.name}
@@ -18,7 +18,8 @@ export const ButtonLogin = () => {
return (
<Button
onClick={() => gotoLogin()}
color="primary-text"
color="brand"
variant="tertiary"
aria-label={t('Login')}
className="--docs--button-login"
>
@@ -30,7 +31,8 @@ export const ButtonLogin = () => {
return (
<Button
onClick={gotoLogout}
color="primary-text"
color="brand"
variant="tertiary"
icon={!isDesktop ? <Icon iconName="logout" $theme="primary" /> : ''}
aria-label={t('Logout')}
className="--docs--button-logout"
@@ -1,5 +1,5 @@
import { Button } from '@openfun/cunningham-react';
import { useTranslation } from 'react-i18next';
import { css } from 'styled-components';
import { Box, Icon, Text } from '@/components';
@@ -54,7 +54,7 @@ export const AttachmentList = ({
$align={isReadOnly ? 'left' : 'center'}
>
<Box
$background="var(--c--theme--colors--greyscale-050)"
$background="var(--c--contextuals--background--semantic--neutral--tertiary)"
$width="200px"
$direction="row"
$gap="8px"
@@ -66,7 +66,7 @@ export const AttachmentList = ({
>
{/* Extension du fichier */}
<Box
$background="var(--c--theme--colors--greyscale-200)"
$background="var(--c--contextuals--background--palette--gray--primary)"
$width="22px"
$height="22px"
$direction="row"
@@ -74,11 +74,11 @@ export const AttachmentList = ({
$justify="center"
$css={`
flex-shrink: 0;
border-radius: 4px;
border-radius: 8px;
`}
>
<Text
$color="var(--c--theme--colors--greyscale-700)"
$color="var(--c--contextuals--content--semantic--overlay--primary)"
$weight="500"
$css={`
font-size: 7px;
@@ -90,7 +90,7 @@ export const AttachmentList = ({
<Text
$size="xs"
$variation="500"
$color="var(--c--theme--colors--greyscale-850)"
$color="var(--c--contextuals--content--semantic--neutral--primary)"
$css={`
overflow: hidden;
text-overflow: ellipsis;
@@ -105,8 +105,11 @@ export const AttachmentList = ({
{name}
</Text>
{!isReadOnly && onRemove && (
<Box
role="button"
<Button
color="neutral"
variant="tertiary"
size="small"
className="c__button--without-padding"
tabIndex={0}
aria-label={t('Remove attachment')}
onClick={removeAttachment}
@@ -116,22 +119,9 @@ export const AttachmentList = ({
removeAttachment();
}
}}
$css={css`
display: block;
padding: 4px;
border-radius: 4px;
cursor: pointer;
&:hover {
background-color: #e1e3e7 !important;
}
&:focus-visible {
outline: 2px solid #3e5de7;
outline-offset: 2px;
}
`}
>
<Icon iconName="close" $theme="greyscale" $size="18px" />
</Box>
<Icon iconName="close" $size="18px" />
</Button>
)}
</Box>
</Box>
@@ -1,18 +1,13 @@
import { Message, SourceUIPart, ToolInvocationUIPart } from '@ai-sdk/ui-utils';
import { Message, SourceUIPart } 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, useRef, useState } from 'react';
import { useCallback, useEffect, useMemo, 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, Icon, Loader, Text } from '@/components';
import { Box, 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';
@@ -21,13 +16,9 @@ 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 { SourceItemList } from '@/features/chat/components/SourceItemList';
import { ToolInvocationItem } from '@/features/chat/components/ToolInvocationItem';
import { MessageItem } from '@/features/chat/components/MessageItem';
import { useClipboard } from '@/hook';
import { useResponsiveStore } from '@/stores';
@@ -289,21 +280,21 @@ export const Chat = ({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [messages]);
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);
}
}
};
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]);
// Calculer la hauteur pour le message de streaming
const calculateStreamingHeight = useCallback(() => {
@@ -620,6 +611,7 @@ export const Chat = ({
flex-basis: auto;
height: 100%;
flex-grow: 1;
z-index: 1;
`}
>
<Box
@@ -641,320 +633,26 @@ export const Chat = ({
>
{messages.length > 0 && (
<Box>
{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
$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>
) : (
<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="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={() => copyToClipboard(message.content)}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
copyToClipboard(message.content);
}
}}
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>
{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--neutral action-chat-button ${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>
);
})}
{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}
/>
))}
</Box>
)}
{(status !== 'ready' && status !== 'streaming' && status !== 'error') ||
@@ -989,7 +687,7 @@ export const Chat = ({
position: relative;
bottom: ${isMobile ? '8px' : '20px'};
margin: auto;
background-color: white;
background-color: var(--c--contextuals--background--surface--secondary);
z-index: 1000;
`}
$gap="6px"
@@ -33,14 +33,10 @@ export const ChatError = ({ hasLastSubmission, onRetry }: ChatErrorProps) => {
>
{hasLastSubmission ? (
<Button
size="small"
color="tertiary"
size="nano"
color="neutral"
variant="bordered"
onClick={onRetry}
className="retry-button"
style={{
color: 'var(--c--theme--colors--greyscale-550)',
borderColor: 'var(--c--theme--colors--greyscale-300)',
}}
icon={
<svg
xmlns="http://www.w3.org/2000/svg"
@@ -60,12 +56,9 @@ export const ChatError = ({ hasLastSubmission, onRetry }: ChatErrorProps) => {
</Button>
) : (
<Button
size="small"
color="tertiary"
style={{
color: 'var(--c--theme--colors--greyscale-550)',
borderColor: 'var(--c--theme--colors--greyscale-300)',
}}
size="nano"
color="brand"
variant="bordered"
onClick={() => {
void router.push('/');
}}
@@ -71,10 +71,12 @@ export const CodeBlock = ({ children, ...props }: CodeBlockProps) => {
return (
<>
<CopyCodeButton onCopy={handleCopy} />
<Box ref={preRef} $position="relative" as="pre" {...props}>
{children}
</Box>
<figure data-rehype-pretty-code-figure="">
<CopyCodeButton onCopy={handleCopy} />
<Box ref={preRef} $position="relative" as="pre" {...props}>
{children}
</Box>
</figure>
</>
);
};
@@ -22,7 +22,7 @@ const ThumbUp = () => (
fill-rule="evenodd"
clip-rule="evenodd"
d="M10.48 3.31533C11.4515 1.69679 13.739 1.54257 14.9186 3.01635C15.4744 3.71116 15.654 4.63525 15.3984 5.4876L14.4273 8.723H18.8941C20.9405 8.72323 22.427 10.6688 21.8891 12.6431L19.8454 20.1363C19.4771 21.4869 18.2502 22.4245 16.8505 22.4246H6.73028C6.72367 22.4247 6.71705 22.4256 6.71041 22.4256C6.70377 22.4256 6.69716 22.4247 6.69055 22.4246H3.60568C2.71921 22.4244 2.00016 21.7055 2 20.8189V10.3287C2.00022 9.44222 2.71921 8.72323 3.60568 8.723H6.56824C6.9817 8.723 7.36522 8.5056 7.57807 8.15119L10.48 3.31533ZM13.4143 4.22061C13.0737 3.79509 12.4124 3.83894 12.1317 4.30633L9.23079 9.14219C8.86738 9.74788 8.31458 10.2004 7.67424 10.4447V20.498H16.8505C17.3813 20.4979 17.846 20.1415 17.9857 19.6293L20.0294 12.1361C20.2331 11.3875 19.6701 10.6498 18.8941 10.6496H13.9966C12.9211 10.6496 12.1498 9.61217 12.4589 8.58188L13.5523 4.9346C13.6259 4.68866 13.5747 4.42117 13.4143 4.22061ZM3.92765 10.6496V20.498H5.74659V10.6496H3.92765Z"
fill="#1C1B1F"
fill="var(--c--contextuals--content--semantic--neutral--secondary)"
/>
</svg>
);
@@ -39,7 +39,7 @@ const ThumbDown = () => (
fill-rule="evenodd"
clip-rule="evenodd"
d="M16.8504 3C18.2501 3.00011 19.477 3.8724 19.8453 5.12866L21.889 12.0991C22.4272 13.9358 20.9405 15.7465 18.894 15.7467H14.4283L15.3984 18.7554C15.6541 19.5485 15.4745 20.4078 14.9185 21.0543C13.7389 22.4257 11.4514 22.2821 10.4799 20.7761L7.57803 16.2777C7.36518 15.948 6.98164 15.7467 6.56822 15.7467H3.60567C2.71925 15.7465 2.00023 15.0777 2 14.253V4.49366C2.00023 3.66904 2.71922 3.00021 3.60567 3H16.8504ZM7.6742 14.1432C8.31475 14.3704 8.86725 14.7922 9.23074 15.3558L12.1316 19.8543C12.4123 20.2894 13.0736 20.3302 13.4143 19.934L13.4697 19.8621C13.5862 19.6864 13.6168 19.4713 13.5523 19.2708L12.4588 15.877C12.1497 14.9185 12.9211 13.9535 13.9965 13.9535H18.894C19.6701 13.9533 20.2334 13.2672 20.0293 12.5707L17.9856 5.6003C17.8459 5.12383 17.3812 4.79329 16.8504 4.79317H7.6742V14.1432ZM3.92764 13.9535H5.74656V4.79317H3.92764V13.9535Z"
fill="#1C1B1F"
fill="var(--c--contextuals--content--semantic--neutral--secondary)"
/>
</svg>
);
@@ -56,7 +56,7 @@ const ThumbDownFilled = () => (
fill-rule="evenodd"
clip-rule="evenodd"
d="M16.8504 3C18.2501 3.00011 19.477 3.8724 19.8453 5.12866L21.889 12.0991C22.4272 13.9358 20.9405 15.7465 18.894 15.7467H14.4283L15.3984 18.7554C15.6541 19.5485 15.4745 20.4078 14.9185 21.0543C13.7389 22.4257 11.4514 22.2821 10.4799 20.7761L7.57803 16.2777C7.36518 15.948 6.98163 15.7467 6.56822 15.7467H3.60567C2.71925 15.7465 2.00023 15.0777 2 14.253V4.49366C2.00023 3.66904 2.71922 3.00021 3.60567 3H16.8504ZM5.92764 13.4535C5.92764 13.7297 6.1515 13.9535 6.42764 13.9535H7.07803C7.35417 13.9535 7.57803 13.7297 7.57803 13.4535V5.29317C7.57803 5.01703 7.35417 4.79317 7.07803 4.79317H6.42764C6.1515 4.79317 5.92764 5.01703 5.92764 5.29317V13.4535Z"
fill="#3E5DE7"
fill="var(--c--contextuals--content--semantic--brand--tertiary)"
/>
</svg>
);
@@ -73,7 +73,7 @@ const ThumbUpFilled = () => (
fill-rule="evenodd"
clip-rule="evenodd"
d="M10.48 3.31533C11.4515 1.69679 13.739 1.54257 14.9186 3.01635C15.4744 3.71116 15.654 4.63525 15.3984 5.4876L14.4273 8.723H18.8941C20.9405 8.72323 22.427 10.6688 21.8891 12.6431L19.8454 20.1363C19.4771 21.4869 18.2502 22.4245 16.8505 22.4246H6.73028C6.72367 22.4247 6.71705 22.4256 6.71041 22.4256C6.70378 22.4256 6.69716 22.4247 6.69055 22.4246H3.60568C2.71921 22.4244 2.00016 21.7055 2 20.8189V10.3287C2.00022 9.44222 2.71921 8.72323 3.60568 8.723H6.56824C6.9817 8.723 7.36522 8.5056 7.57807 8.15119L10.48 3.31533ZM6.42765 10.6496C6.15151 10.6496 5.92765 10.8735 5.92765 11.1496V19.998C5.92765 20.2741 6.15151 20.498 6.42765 20.498H7C7.27614 20.498 7.5 20.2741 7.5 19.998V11.1496C7.5 10.8735 7.27614 10.6496 7 10.6496H6.42765Z"
fill="#3E5DE7"
fill="var(--c--contextuals--content--semantic--brand--tertiary)"
/>
</svg>
);
@@ -62,9 +62,9 @@ const CONTAINER_CSS = `
const INPUT_BOX_CSS = `
box-shadow: 0 4px 16px 0 rgba(0, 0, 0, 0.08);
border-radius: 12px;
border: 1px solid var(--c--theme--colors--greyscale-200);
border: 1px solid var(--c--contextuals--border--surface--primary);
position: relative;
background: white;
background: var(--c--contextuals--background--surface--primary);
transition: all 0.2s ease;
`;
@@ -72,10 +72,10 @@ const FILE_DROP_CSS = `
top: -1px; left: -1px;
border-radius: 12px;
z-index: 1001;
background-color: #EDF0FF;
background-color: var(--c--contextuals--background--semantic--brand--tertiary);
width: 100%;
height: 100%;
outline: 2px solid #90A7FF;
outline: 2px solid var(--c--contextuals--border--semantic--brand--secondary);
box-shadow: 0 0 64px 0 rgba(62, 93, 231, 0.25);
`;
const DRAG_FADE_CSS = `
@@ -90,7 +90,7 @@ const DRAG_FADE_CSS = `
`;
const TEXTAREA_STYLE: React.CSSProperties = {
padding: '1rem 1.5rem',
padding: '1rem 1.5rem 0.5rem 1.5rem',
background: 'transparent',
outline: 'none',
fontSize: '1rem',
@@ -213,39 +213,56 @@ export const InputChat = ({
}
}, [status, input]);
const handleFilesAccepted = useCallback(
(acceptedFiles: File[]) => {
setFiles((prev) => {
const dt = new DataTransfer();
const validateAndAddFiles = useCallback(
(filesToAdd: File[]) => {
const acceptedFiles: File[] = [];
const rejectedFiles: File[] = [];
// Keep existing files
if (prev) {
Array.from(prev).forEach((f) => dt.items.add(f));
filesToAdd.forEach((file) => {
if (isFileAccepted(file)) {
acceptedFiles.push(file);
} else {
rejectedFiles.push(file);
}
// 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;
});
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);
}
});
return dt.files;
});
}
},
[setFiles],
[isFileAccepted, showToastError, setFiles],
);
const { isDragActive } = useFileDragDrop({
enabled: fileUploadEnabled,
isFileAccepted,
onFilesAccepted: handleFilesAccepted,
onFilesAccepted: validateAndAddFiles,
onFilesRejected: () => showToastError(),
});
@@ -293,6 +310,41 @@ 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();
}, []);
@@ -309,46 +361,10 @@ export const InputChat = ({
return;
}
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;
});
}
validateAndAddFiles(Array.from(fileList));
e.target.value = '';
},
[isFileAccepted, showToastError, setFiles],
[validateAndAddFiles],
);
const handleAttachmentRemove = useCallback(
@@ -421,10 +437,16 @@ export const InputChat = ({
>
<FilesIcon />
<Box>
<Text $weight="700" $color="#223E9E">
<Text
$weight="700"
$color="var(--c--contextuals--border--semantic--brand--primary)"
>
{t('Add file')}
</Text>
<Text $weight="400" $color="#223E9E">
<Text
$weight="400"
$color="var(--c--contextuals--border--semantic--brand--primary)"
>
{t('To add a file to the conversation, drop it here.')}
</Text>
</Box>
@@ -437,6 +459,7 @@ export const InputChat = ({
name="inputchat-textarea"
onChange={handleTextareaChange}
onKeyDown={handleTextareaKeyDown}
onPaste={handlePaste}
disabled={isInputDisabled}
rows={1}
style={textareaStyle}
@@ -49,7 +49,8 @@ const MOBILE_WEB_BUTTON_CSS = `
const ACTIVE_WEB_BUTTON_CSS = `
.research-web-button {
background-color: var(--c--theme--colors--primary-100) !important;
background-color: var(--c--contextuals--background--semantic--brand--secondary) !important;
color: var(--c--contextuals--content--semantic--brand--secondary) !important;
}
`;
@@ -132,51 +133,36 @@ export const InputChatActions = memo(
<Box
$flex="1"
$direction="row"
$align="end"
$padding={STYLES.horizontalPadding}
$gap="xs"
>
{/* Attach file button */}
<Button
size="small"
size="nano"
type="button"
color="neutral"
className="c__button--neutral"
variant="tertiary"
disabled={!fileUploadEnabled || isUploadingFiles}
onClick={onAttachClick}
aria-label={t('Add attach file')}
className="c__button--neutral attach-file-button"
icon={
<Icon
iconName="attach_file"
$theme="greyscale"
$variation="550"
$size={attachIconSize}
/>
}
icon={<Icon iconName="attach_file" $size={attachIconSize} />}
>
{!isMobile && (
<Text $theme="greyscale" $variation="550" $weight="500">
{t('Attach file')}
</Text>
)}
{!isMobile && <Text $weight="500">{t('Attach file')}</Text>}
</Button>
{/* Web search toggle button */}
{onWebSearchToggle && (
<Box $margin={STYLES.webSearchMargin} $css={webSearchWrapperCss}>
<Button
size="small"
size="nano"
type="button"
disabled={!webSearchEnabled || isUploadingFiles}
onClick={onWebSearchToggle}
aria-label={t('Research on the web')}
className="c__button--neutral research-web-button"
icon={
<Icon
iconName="language"
$theme="greyscale"
$variation="550"
$css={webSearchIconCss}
/>
}
icon={<Icon iconName="language" $css={webSearchIconCss} />}
>
{!isMobile && (
<Text
@@ -193,13 +179,18 @@ export const InputChatActions = memo(
$gap="xs"
$css={MOBILE_TEXT_WRAPPER_CSS}
>
<Text $theme="primary" $weight="500" $css={MOBILE_TEXT_CSS}>
<Text
$theme="brand"
$variation="secondary"
$weight="500"
$css={MOBILE_TEXT_CSS}
>
{t('Web')}
</Text>
<Icon
iconName="close"
$variation="text"
$theme="primary"
$theme="brand"
$variation="secondary"
$size="md"
$css={CLOSE_ICON_CSS}
/>
@@ -211,7 +202,12 @@ export const InputChatActions = memo(
</Box>
{/* Right side: Model selector + Send */}
<Box $direction="row" $padding={STYLES.horizontalPadding} $gap="xs">
<Box
$direction="row"
$align="center"
$padding={STYLES.horizontalPadding}
$gap="xs"
>
{onModelSelect && (
<Box $padding={STYLES.horizontalPaddingXs}>
<ModelSelector
@@ -0,0 +1,89 @@
// 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>
);
@@ -0,0 +1,554 @@
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);
@@ -152,8 +152,8 @@ export const ModelSelector = ({
bottom: 100%;
right: -30px;
width: 300px;
background: white;
border: 1px solid var(--c--theme--colors--greyscale-100);
background: var(--c--contextuals--background--surface--tertiary);
border: 1px solid var(--c--contextuals--background--surface--secondary);
border-radius: 4px;
box-shadow: 0 0 6px 0 rgba(0, 0, 145, 0.10);
z-index: 1000;
@@ -162,7 +162,7 @@ export const ModelSelector = ({
overflow-y: auto;
overflow-x: hidden;
justify-content: space-between;
/* Custom scrollbar */
&::-webkit-scrollbar {
width: 6px;
@@ -171,11 +171,11 @@ export const ModelSelector = ({
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: var(--c--theme--colors--greyscale-300);
background: var(--c--contextuals--background--surface--secondary);
border-radius: 3px;
}
&::-webkit-scrollbar-thumb:hover {
background: var(--c--theme--colors--greyscale-400);
background: var(--c--contextuals--background--surface--secondary);
}
`}
>
@@ -192,13 +192,13 @@ export const ModelSelector = ({
position: relative;
&:hover {
background-color: #f2f5f4;
background-color: var(--c--contextuals--background--surface--secondary);
}
${
currentModel?.hrid === model.hrid
? `
background-color: #f2f5f4;
background-color: var(--c--contextuals--background--surface--secondary);
`
: ''
}
@@ -227,8 +227,8 @@ export const ModelSelector = ({
$css="display: flex; align-items: center; justify-content: space-between; gap: 16px;"
>
<Text
$theme="greyscale"
$variation="850"
$theme="neutral"
$variation="primary"
$weight="500"
$size="s"
>
@@ -237,8 +237,8 @@ export const ModelSelector = ({
{model.is_default && (
<Box>
<Text
$theme="greyscale"
$variation="550"
$theme="neutral"
$variation="tertiary"
$size="xs"
$weight="400"
>
@@ -46,12 +46,12 @@ export const ScrollDown: React.FC<ScrollDownProps> = ({
`}
>
<Button
className="c__button--bordered"
className="c__button--background"
aria-label="See more"
color="brand"
variant="bordered"
onClick={onClick}
icon={
<Icon $variation="text" $theme="primary" iconName="arrow_downward" />
}
icon={<Icon $theme="inherit" iconName="arrow_downward" />}
/>
</Box>
);
@@ -39,28 +39,23 @@ export const SendButton = ({
<Button
size="small"
type="submit"
className="c__button--send"
aria-label={t('Send')}
disabled={disabled || status === 'error'}
color="primary"
icon={
<Icon
$variation="800"
$theme="primary-text"
iconName="arrow_upward"
/>
}
variant="primary"
icon={<Icon $theme="primary-text" iconName="arrow_upward" />}
/>
) : (
<Button
size="small"
size="nano"
type="button"
aria-label={t('Stop')}
disabled={false}
onClick={onClick}
className="c__button--stop"
className="c__button--stop bg-semantic-neutral-primary"
icon={<StopIcon />}
>
<Text $theme="greyscale" $variation="000">
<Text $theme="neutral" $variation="on-neutral">
{t('Stop')}
</Text>
</Button>
@@ -5,7 +5,7 @@ import { Box, StyledLink } from '@/components';
const styles: Record<string, React.CSSProperties> = {
title: {
color: '#222631',
color: 'var(--c--contextuals--content--semantic--neutral--primary)',
fontWeight: '500',
whiteSpace: 'nowrap',
overflow: 'hidden',
@@ -173,10 +173,10 @@ export const SourceItem: React.FC<SourceItemProps> = ({ url, metadata }) => {
text-overflow: ellipsis;
background-color: transparent;
transition: background-color 0.3s;
color: var(--c--theme--colors--greyscale-500);
color: var(--c--contextuals--content--semantic--neutral--tertiary);
&:hover {
color: var(--c--theme--colors--greyscale-700);
background-color: #EEF1F4;
color: var(--c--contextuals--content--semantic--neutral--tertiary);
background-color: var(--c--contextuals--background--semantic--neutral--tertiary);
}
`}
>
@@ -23,14 +23,13 @@ const SourceItemListComponent: React.FC<SourceItemListProps> = ({
if (parts.length === 0) {
return null;
}
return (
<Box
$direction="column"
$padding={{ all: 'sm' }}
$gap="4px"
$css={`
border: 1px solid var(--c--theme--colors--greyscale-100);
border: 1px solid var(--c--contextuals--border--surface--primary);
border-radius: 8px;
margin-top: 0.5rem;
overflow: hidden;
@@ -3,13 +3,7 @@ import { useTranslation } from 'react-i18next';
import { Box } from '@/components';
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 SUGGESTIONS_COUNT = 4;
const WRAPPER_CSS = `position: absolute;
top: 1rem;
@@ -42,7 +36,15 @@ export const SuggestionCarousel = ({
const [currentSuggestionIndex, setCurrentSuggestionIndex] = useState(0);
const [isResetting, setIsResetting] = useState(false);
const suggestions = useMemo(() => SUGGESTION_KEYS.map((key) => t(key)), [t]);
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 carouselSuggestions = useMemo(
() => [...suggestions, suggestions[0]],
[suggestions],
@@ -29,13 +29,15 @@ 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"
@@ -48,9 +50,7 @@ export const ToolInvocationItem: React.FC<ToolInvocationItemProps> = ({
>
<Loader />
<Text $variation="600" $size="md">
{t('Extracting documents: {{documents}} ...', {
documents: documentIdentifiers.join(', '),
})}
{t(label, { documents: documentIdentifiers.join(', ') })}
</Text>
</Box>
);
@@ -0,0 +1,171 @@
/* 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();
});
});
@@ -0,0 +1,536 @@
/* 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,4 +1,5 @@
import { render, screen } from '@testing-library/react';
import i18next from 'i18next';
import '@/i18n/initI18n';
@@ -9,8 +10,26 @@ describe('SuggestionCarousel', () => {
jest.useFakeTimers();
});
afterEach(() => {
afterEach(async () => {
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,8 +4,10 @@ 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;
@@ -16,8 +18,11 @@ 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 }),
@@ -0,0 +1,32 @@
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')),
});
@@ -19,8 +19,9 @@ export const Feedback = (_props: { buttonProps?: Partial<ButtonProps> }) => {
<Button
icon={<InfoIcon />}
fullWidth
color="info"
variant="secondary"
onClick={() => modal.open()}
className="c__button--info"
>
{t('Give feedback')}
</Button>
@@ -101,25 +102,25 @@ const FeedbackButton = ({
align-items: center;
transition: all 0.2s ease;
cursor: pointer;
border: 1px solid var(--c--theme--colors--greyscale-100);
color: var(--c--theme--colors--greyscale-600) !important;
border: 1px solid var(--c--contextuals--border--semantic--neutral--tertiary);
color: var(--c--contextuals--content--semantic--neutral--secondary) !important;
&:hover {
border-color: #b6d7fb;
background-color: #e7f2fd;
color: var(--c--theme--colors--info-600) !important;
background-color: var(--c--contextuals--background--semantic--info--tertiary) !important;
color: var(--c--contextuals--content--semantic--info--secondary) !important;
span {
color: var(--c--theme--colors--info-600) !important;
color: var(--c--contextuals--content--semantic--info--secondary) !important;
}
}
`}
>
<div style={{ flexShrink: 0 }}>{icon}</div>
<Box $direction="column" $gap="xs">
<Text $weight="700" $size="sm" $color="greyscale-900">
<Text $weight="700" $size="sm">
{title}
</Text>
<Text $size="sm" $color="greyscale-600" style={{ lineHeight: '1.4' }}>
<Text $size="sm" style={{ lineHeight: '1.4' }}>
{description}
</Text>
</Box>
@@ -172,25 +173,8 @@ 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="#265eaa"
fill="currentColor"
/>
</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>
// );
// };
@@ -5,6 +5,7 @@ import styled, { css } from 'styled-components';
import { Box, StyledLink, Text } from '@/components/';
import { useConfig } from '@/core/config';
import { useCunninghamTheme } from '@/cunningham';
import { Title } from '../header';
@@ -15,7 +16,6 @@ const BlueStripe = styled.div`
position: absolute;
height: 2px;
width: 100%;
background: var(--c--theme--colors--primary-600);
top: 0;
`;
@@ -24,8 +24,13 @@ export const Footer = () => {
const footerJson = config?.theme_customization?.footer;
const { i18n, t } = useTranslation();
const resolvedLanguage = i18n.resolvedLanguage;
const { componentTokens } = useCunninghamTheme();
const [content, setContent] = useState<ContentType>();
const themeLogo = (componentTokens as Record<string, unknown>).logo as
| { src: string; alt: string; widthHeader: string; widthFooter: string }
| undefined;
useEffect(() => {
if (!footerJson) {
return;
@@ -34,7 +39,15 @@ export const Footer = () => {
const langData = footerJson[resolvedLanguage as keyof typeof footerJson];
const innerContent: ContentType = {};
innerContent.logo = langData?.logo || footerJson?.default?.logo;
// Use theme logo if available (for DSFR theme), otherwise use config logo
innerContent.logo = themeLogo?.src
? {
src: themeLogo.src,
alt: themeLogo.alt,
width: themeLogo.widthFooter,
withTitle: false,
}
: langData?.logo || footerJson?.default?.logo;
innerContent.legalLinks =
langData?.legalLinks || footerJson?.default?.legalLinks;
innerContent.externalLinks =
@@ -47,7 +60,7 @@ export const Footer = () => {
: footerJson?.default?.bottomInformation;
setContent(innerContent);
}, [footerJson, resolvedLanguage]);
}, [footerJson, resolvedLanguage, themeLogo]);
const { logo, legalLinks, externalLinks, bottomInformation } = content || {};
@@ -115,12 +128,15 @@ export const Footer = () => {
gap:0.2rem;
transition: box-shadow 0.3s;
&:hover {
box-shadow: 0px 2px 0 0 var(--c--theme--colors--greyscale-text);
box-shadow: 0px 2px 0 0 var(--c--contextuals--content--semantic--neutral--tertiary);
}
`}
>
<Text $weight="bold">{label}</Text>
<IconLink width={18} />
<IconLink
width={18}
color="var(--c--contextuals--content--semantic--neutral--primary)"
/>
</StyledLink>
))}
</Box>
@@ -151,13 +167,16 @@ export const Footer = () => {
`}
>
<Text
$variation="600"
$variation="secondary"
$size="m"
$theme="neutral"
$transition="box-shadow 0.3s"
$css={css`
&:hover {
box-shadow: 0px 2px 0 0
var(--c--theme--colors--greyscale-text);
var(
--c--contextuals--content--semantic--neutral--tertiary
);
}
`}
>
@@ -171,7 +190,8 @@ export const Footer = () => {
as="p"
$size="m"
$margin={{ top: 'big' }}
$variation="600"
$variation="secondary"
$theme="neutral"
$display="inline"
className="--docs--footer-licence"
>
@@ -183,12 +203,17 @@ export const Footer = () => {
$css={css`
display: inline-flex;
box-shadow: 0px 1px 0 0
var(--c--theme--colors--greyscale-text);
var(--c--contextuals--content--semantic--neutral--tertiary);
gap: 0.2rem;
`}
>
<Text $variation="600">{bottomInformation.link.label}</Text>
<IconLink width={14} />
<Text $variation="tertiary" $theme="neutral">
{bottomInformation.link.label}
</Text>
<IconLink
width={14}
color="var(--c--contextuals--content--semantic--neutral--primary)"
/>
</StyledLink>
)}
</Text>

Some files were not shown because too many files have changed in this diff Show More