Compare commits

...

21 Commits

Author SHA1 Message Date
providenz 0746d23a1e 🌐(i18n) update translated strings
Update translated files with new translations

Signed-off-by: Laurent Paoletti <lp@providenz.fr>
2026-03-12 09:17:35 +01:00
Laurent Paoletti c699d7503d 🔖(patch) bump release to 0.0.14
Signed-off-by: Laurent Paoletti <lp@providenz.fr>
2026-03-11 16:20:40 +01:00
Laurent Paoletti bc7568ccf1 ⬆️(back) update django and pypdf
Signed-off-by: Laurent Paoletti <lp@providenz.fr>
2026-03-11 15:12:22 +01:00
eliott07 c17a8ae9b9 🐛(frontend) fix inverted toast message for settings toggles
fixes inverted toast introduced in smart search PR

Remove fixed inverted toast entry from changelog
2026-03-05 13:58:38 +01:00
eliott07 4545083210 (user) allow disabling automatic internet search
Added user preference to disable automatic tool calls to internet.
Preference is stored in user model, can be changed via frontend setting and
prevents the tool from showing up unless
'Search on the web' is explicitly enabled

Signed-off-by: eliott07 <eliott07@gmail.com>

🐛(settings) fix smart web search default and improve settings layout

Updated default value of smart search, adjusted settings modal layout & spacing
2026-02-26 16:27:56 +01:00
natoromano 1088d88aba 🚑️(back) fix mime type for pptx
Fix wrong mime type in config (for pptx)
2026-02-25 17:33:16 +01:00
Laurent Paoletti 99353357ff ♻️(front) migrate from ESLint 8 to ESLint 9 flat config
- Replace .eslintrc.js files with eslint.config.mjs (flat config)
  - Upgrade eslint from 8.57.0 to ^9.0.0
  - Replace eslint-plugin-import with eslint-plugin-import-x
  - Replace eslint-config-next with eslint-plugin-react +
  eslint-plugin-react-hooks
  - Convert shared config modules from .js (CJS) to .mjs (ESM)
  - Update lint scripts to use `eslint src/` instead of `next lint`

Signed-off-by: Laurent Paoletti <lp@providenz.fr>
2026-02-25 10:32:49 +01:00
Laurent Paoletti 8feed1e3ac ⬆️(back) update django-pydantic-field and pypdf
Signed-off-by: Laurent Paoletti <lp@providenz.fr>
2026-02-24 21:27:08 +01:00
Laurent Paoletti 61f86e18df ⬆️(back) update pillow
Signed-off-by: Laurent Paoletti <lp@providenz.fr>
2026-02-24 20:15:55 +01:00
Laurent Paoletti 2b8123462a 🐛(front) fix dark mode styling on chat messages
Signed-off-by: Laurent Paoletti <lp@providenz.fr>
2026-02-24 17:58:48 +01:00
Quentin BEY 3c3eaf3eeb 🚸(oidc) ignore case when fallback on email
Some identity providers might change the case, but in our
products we don't consider case variation to be consider as
different email addresses.

Next step would be to normalize the DB value of email to
be lower-case.
2026-02-24 16:07:21 +01:00
Quentin BEY e925bff1a2 👷(crowdin) use uv instead of pip
This garanties consistency across actions
2026-02-24 16:05:01 +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
77 changed files with 6262 additions and 2285 deletions
+3
View File
@@ -1,6 +1,7 @@
[codespell]
skip =
./git/,
**/*.pdf,
**/*.po,
**/*.pot,
**/*.json,
@@ -9,9 +10,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
+8 -9
View File
@@ -22,15 +22,14 @@ jobs:
- name: Checkout
uses: actions/checkout@v4
# Backend i18n
- name: Install Python
uses: actions/setup-python@v5
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: "3.13.3"
cache: "pip"
- name: Upgrade pip and setuptools
run: pip install --upgrade pip setuptools
- name: Install development dependencies
run: pip install --user .
python-version-file: "src/backend/pyproject.toml"
- name: Install uv
uses: astral-sh/setup-uv@v6
- name: Install the project
run: uv sync --locked --all-extras
working-directory: src/backend
- name: Restore the mail templates
uses: actions/cache@v4
@@ -46,7 +45,7 @@ jobs:
- name: generate pot files
working-directory: src/backend
run: |
DJANGO_CONFIGURATION=Build python manage.py makemessages -a --keep-pot
DJANGO_CONFIGURATION=Build uv run python manage.py makemessages -a --keep-pot
# frontend i18n
- name: Setup Node.js
uses: actions/setup-node@v4
+34 -5
View File
@@ -8,6 +8,34 @@ and this project adheres to
## [Unreleased]
## [0.0.14] - 2026-03-11
### Added
- ✨(user) allow disabling automatic internet search
- ✨(waffle) hide the waffle if not fr theme
- ✨(front) allow pasting an attachment from clipboard
- ✨(array) temporarily adjust array
### Changed
- ⚡️(front) optimize streaming markdown rendering performance
- ⬆️(back) update pydantic-ai
- ♻️(chat) refactor AIAgentService for readability and maintainability
- 🚸(oidc) ignore case when fallback on email #281
- ⬆️(back) update pillow, django-pydantic-field, pypdf
- ♻️(front) migrate from ESLint 8 to ESLint 9 flat config
- ⬆️(back) update django and pypdf
### Fixed
- 💚(docker) vendor mime.types file instead of fetching from Apache SVN
- 🚑️(back) fix mime type for pptx
- 🐛(front) fix math formulas and carousel translations
- 🐛(helm) reverse liveness and readiness for backend deployment
- 🐛(front) fix dark mode styling on chat messages
- 🐛(front) fixed inverted toast for setting changes
## [0.0.13] - 2026-02-09
### Added
@@ -18,17 +46,17 @@ and this project adheres to
- 🧱(files) allow to use S3 storage without external access #849
- ✨(backend) add FindRagBackend #209
- ⬆️(back) update dependencies
- ✨(back) Use adaptive parsing for pdf documents
- ✨(back) use adaptive parsing for pdf documents
### Changed
- 💄(darkmode) change color feedback butto
- 💄(darkmode) change color feedback button
- 🏗️(back) migrate to uv
- ♻️(front) optimize syntax highlighting bundle size
### Fixed
- 🐛(back) Cast collection Ids to API expected types
- 🐛(back) cast collection Ids to API expected types
## [0.0.12] - 2026-01-27
@@ -46,7 +74,7 @@ and this project adheres to
### Changed
- 📦️(front) update react
- ✨(chat) Generate and edit conversation title
- ✨(chat) generate and edit conversation title
### Fixed
@@ -215,7 +243,8 @@ and this project adheres to
- 💄(chat) add code highlighting for LLM responses #67
[unreleased]: https://github.com/suitenumerique/conversations/compare/v0.0.13...main
[unreleased]: https://github.com/suitenumerique/conversations/compare/v0.0.14...main
[0.0.14]: https://github.com/suitenumerique/conversations/releases/v0.0.14
[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
+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
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,95 @@
"""Unit tests for add_document_rag_search_tool_from_setting integration with AIAgentService."""
# pylint: disable=redefined-outer-name, protected-access
import pytest
from pydantic_ai.models.test import TestModel
from chat.clients.pydantic_ai import AIAgentService
from chat.factories import ChatConversationFactory, UserFactory
from chat.llm_configuration import LLModel, LLMProvider
pytestmark = pytest.mark.django_db()
@pytest.fixture()
def _llm_config_with_websearch(settings):
"""Configure a single active model that includes the web search tool."""
settings.LLM_CONFIGURATIONS = {
"default-model": LLModel(
hrid="default-model",
model_name="amazing-llm",
human_readable_name="Amazing LLM",
is_active=True,
icon=None,
system_prompt="You are an amazing assistant.",
tools=["web_search_brave_with_document_backend"],
provider=LLMProvider(
hrid="unused",
base_url="https://example.com",
api_key="key",
),
),
}
def test_smart_search_disabled_suppresses_tool_at_runtime(_llm_config_with_websearch):
"""
When smart search is off, the tool must be suppressed at runtime.
"""
user = UserFactory(allow_smart_web_search=False)
conversation = ChatConversationFactory(owner=user)
service = AIAgentService(conversation, user=user)
assert service._is_smart_search_enabled is False
assert service._is_web_search_enabled is True
# Replicate what _run_agent does before calling the model
if not service._is_smart_search_enabled and service._is_web_search_enabled:
service._context_deps.web_search_enabled = False
with service.conversation_agent.override(model=TestModel(), deps=service._context_deps):
response = service.conversation_agent.run_sync("Search the web for something.")
assert response.output == "success (no tool calls)"
def test_smart_search_enabled_tool_is_called(_llm_config_with_websearch):
"""
When smart search is on, the tool must be invoked.
"""
user = UserFactory(allow_smart_web_search=True)
conversation = ChatConversationFactory(owner=user)
service = AIAgentService(conversation, user=user)
assert service._is_smart_search_enabled is True
assert service._context_deps.web_search_enabled is True
with service.conversation_agent.override(model=TestModel(), deps=service._context_deps):
response = service.conversation_agent.run_sync("Search the web for something.")
assert "web_search_brave_with_document_backend" in response.output
def test_force_websearch_overrides_smart_search_disabled(_llm_config_with_websearch):
"""
When smart search is off, the tool must be enabled via force_web_search.
"""
user = UserFactory(allow_smart_web_search=False)
conversation = ChatConversationFactory(owner=user)
service = AIAgentService(conversation, user=user)
assert service._is_smart_search_enabled is False
assert service._context_deps.web_search_enabled is False
service._setup_web_search(force_web_search=True)
web_search_tool_name = service.conversation_agent.get_web_search_tool_name()
assert service._context_deps.web_search_enabled is True
assert any(
callable(instr) and web_search_tool_name in instr()
for instr in service.conversation_agent._instructions
)
with service.conversation_agent.override(model=TestModel(), deps=service._context_deps):
response = service.conversation_agent.run_sync("Search the web for something.")
assert "web_search_brave_with_document_backend" in response.output
+1 -1
View File
@@ -20,7 +20,7 @@ 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)
@@ -190,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"],
@@ -198,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,
@@ -317,6 +332,7 @@ def test_post_conversation_data_protocol_triggers_keepalives(
"Answer in english."
),
"kind": "request",
"metadata": None,
"parts": [
{
"content": ["Hello"],
@@ -325,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,
@@ -438,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"],
@@ -446,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,
@@ -624,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": [
@@ -644,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,
@@ -790,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?"],
@@ -798,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,
@@ -834,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"},
@@ -845,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,
@@ -992,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?"],
@@ -1000,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,
@@ -1036,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.",
@@ -1046,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,
@@ -1302,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?"],
@@ -1310,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": [
{
@@ -1321,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,
@@ -1442,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"],
@@ -1450,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,
@@ -1582,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"],
@@ -1590,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,
@@ -273,7 +273,7 @@ def test_post_conversation_with_document_upload(
api_client,
mock_document_api, # pylint: disable=unused-argument
sample_pdf_content,
today_promt_date,
today_prompt_date,
mock_ai_agent_service,
):
"""
@@ -409,7 +409,7 @@ def test_post_conversation_with_document_upload(
assert chat_conversation.pydantic_messages[0] == {
"instructions": "You are a helpful test assistant :)\n\n"
f"{today_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, "
@@ -424,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?"],
@@ -432,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": [
{
@@ -444,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,
@@ -465,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, "
@@ -481,6 +487,7 @@ def test_post_conversation_with_document_upload(
"available via the internal store."
),
"kind": "request",
"metadata": None,
"parts": [
{
"content": [
@@ -498,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,
@@ -602,7 +614,7 @@ def test_post_conversation_with_document_upload_summarize( # pylint: disable=to
api_client,
mock_document_api, # pylint: disable=unused-argument
sample_pdf_content,
today_promt_date,
today_prompt_date,
mock_ai_agent_service,
mock_summarization_agent, # pylint: disable=unused-argument
):
@@ -739,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, "
@@ -755,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."],
@@ -763,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": [
{
@@ -775,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,
@@ -796,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, "
@@ -812,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.",
@@ -823,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,
@@ -73,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"},
@@ -140,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 "
@@ -156,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."
@@ -229,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 "
@@ -249,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": [
@@ -259,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,
@@ -545,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(
@@ -558,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"
@@ -570,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(
@@ -579,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.",
@@ -738,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": [
{
@@ -747,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,
@@ -791,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,
@@ -853,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=[
@@ -861,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, "
@@ -957,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, "
@@ -974,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": [
@@ -984,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,
+6 -1
View File
@@ -672,6 +672,11 @@ class Base(BraveSettings, Configuration):
environ_name="DEFAULT_ALLOW_CONVERSATION_ANALYTICS",
environ_prefix=None,
)
DEFAULT_ALLOW_SMART_WEB_SEARCH = values.BooleanValue(
default=True,
environ_name="DEFAULT_ALLOW_SMART_WEB_SEARCH",
environ_prefix=None,
)
# These settings are default values used in the default LLM_CONFIGURATIONS
# They allow a deployment with only one model without a specific configuration file
@@ -702,7 +707,7 @@ class Base(BraveSettings, Configuration):
# docx files
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
# pptx files
"application/vnd.openxmlformats-officedocument.presentationml",
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
# xlsx and xls files
"application/vnd.ms-excel",
"application/excel",
+1
View File
@@ -13,6 +13,7 @@ class UserSerializer(serializers.ModelSerializer):
fields = [
"id",
"allow_conversation_analytics",
"allow_smart_web_search",
"email",
"full_name",
"short_name",
+5 -1
View File
@@ -55,7 +55,11 @@ class OIDCAuthenticationBackend(LaSuiteOIDCAuthenticationBackend):
more information about the usage of the application.
"""
return super().create_user(
claims | {"allow_conversation_analytics": settings.DEFAULT_ALLOW_CONVERSATION_ANALYTICS}
claims
| {
"allow_conversation_analytics": settings.DEFAULT_ALLOW_CONVERSATION_ANALYTICS,
"allow_smart_web_search": settings.DEFAULT_ALLOW_SMART_WEB_SEARCH,
}
)
def authenticate(self, request, **kwargs):
+1
View File
@@ -22,6 +22,7 @@ class UserFactory(factory.django.DjangoModelFactory):
skip_postgeneration_save = True
allow_conversation_analytics = factory.Faker("boolean", chance_of_getting_true=50)
allow_smart_web_search = factory.Faker("boolean", chance_of_getting_true=50)
sub = factory.Sequence(lambda n: f"user{n!s}")
email = factory.Faker("email")
full_name = factory.Faker("name")
@@ -0,0 +1,21 @@
# Generated by Django 5.2.9 on 2025-12-10 15:19
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("core", "0003_alter_user_short_name"),
]
operations = [
migrations.AddField(
model_name="user",
name="allow_smart_web_search",
field=models.BooleanField(
default=True,
help_text="Whether the user allows to use smart web search features.",
verbose_name="allow smart web search",
),
),
]
+11 -2
View File
@@ -77,10 +77,13 @@ class UserManager(auth_models.UserManager):
if settings.OIDC_FALLBACK_TO_EMAIL_FOR_IDENTIFICATION:
try:
return self.get(email=email)
return self.get(email__iexact=email)
except self.model.DoesNotExist:
pass
elif self.filter(email=email).exists() and not settings.OIDC_ALLOW_DUPLICATE_EMAILS:
elif (
self.filter(email__iexact=email).exists()
and not settings.OIDC_ALLOW_DUPLICATE_EMAILS
):
raise DuplicateEmailError(
_(
"We couldn't find a user with this sub but the email is already "
@@ -163,6 +166,12 @@ class User(AbstractBaseUser, BaseModel, auth_models.PermissionsMixin):
help_text=_("Whether the user allows to use their conversations for analytics."),
)
allow_smart_web_search = models.BooleanField(
_("allow smart web search"),
default=True,
help_text=_("Whether the user allows to use smart web search features."),
)
objects = UserManager()
USERNAME_FIELD = "admin_email"
@@ -64,6 +64,28 @@ def test_authentication_getter_existing_user_via_email(django_assert_num_queries
assert user == db_user
def test_authentication_getter_existing_user_via_email_case_insensitive(
django_assert_num_queries, monkeypatch
):
"""
If an existing user doesn't match the sub but matches the email with different case,
the user should be returned (case-insensitive email matching).
"""
klass = OIDCAuthenticationBackend()
db_user = UserFactory(email="john.doe@example.com")
def get_userinfo_mocked(*args):
return {"sub": "123", "email": "JOHN.DOE@EXAMPLE.COM"}
monkeypatch.setattr(OIDCAuthenticationBackend, "get_userinfo", get_userinfo_mocked)
with django_assert_num_queries(4): # user by sub, user by mail, update sub
user = klass.get_or_create_user(access_token="test-token", id_token=None, payload=None)
assert user == db_user
def test_authentication_getter_email_none(monkeypatch):
"""
If no user is found with the sub and no email is provided, a new user should be created.
@@ -149,6 +171,39 @@ def test_authentication_getter_existing_user_no_fallback_to_email_no_duplicate(
assert models.User.objects.count() == 1
def test_authentication_getter_existing_user_no_fallback_to_email_no_duplicate_case_insensitive(
settings, monkeypatch
):
"""
When the "OIDC_FALLBACK_TO_EMAIL_FOR_IDENTIFICATION" setting is set to False,
the system should detect duplicate emails even with different case.
"""
klass = OIDCAuthenticationBackend()
_db_user = UserFactory(email="john.doe@example.com")
# Set the setting to False
settings.OIDC_FALLBACK_TO_EMAIL_FOR_IDENTIFICATION = False
settings.OIDC_ALLOW_DUPLICATE_EMAILS = False
def get_userinfo_mocked(*args):
return {"sub": "123", "email": "JOHN.DOE@EXAMPLE.COM"}
monkeypatch.setattr(OIDCAuthenticationBackend, "get_userinfo", get_userinfo_mocked)
with pytest.raises(
SuspiciousOperation,
match=(
"We couldn't find a user with this sub but the email is already associated "
"with a registered user."
),
):
klass.get_or_create_user(access_token="test-token", id_token=None, payload=None)
# Since the sub doesn't match, it should not create a new user
assert models.User.objects.count() == 1
def test_authentication_getter_existing_user_with_email(django_assert_num_queries, monkeypatch):
"""
When the user's info contains an email and targets an existing user,
+17 -4
View File
@@ -225,6 +225,7 @@ def test_api_users_retrieve_me_authenticated():
assert response.json() == {
"id": str(user.id),
"allow_conversation_analytics": user.allow_conversation_analytics,
"allow_smart_web_search": user.allow_smart_web_search,
"email": user.email,
"full_name": user.full_name,
"language": user.language,
@@ -338,7 +339,7 @@ def test_api_users_update_anonymous():
def test_api_users_update_authenticated_self():
"""
Authenticated users should be able to update their own user but only "language",
"allow_conversation_analytics" and "timezone" fields.
"allow_conversation_analytics", "allow_smart_web_search" and "timezone" fields.
"""
user = factories.UserFactory()
@@ -350,6 +351,7 @@ def test_api_users_update_authenticated_self():
serializers.UserSerializer(
instance=factories.UserFactory(
allow_conversation_analytics=not user.allow_conversation_analytics,
allow_smart_web_search=not user.allow_smart_web_search,
)
).data
)
@@ -364,7 +366,12 @@ def test_api_users_update_authenticated_self():
user.refresh_from_db()
user_values = dict(serializers.UserSerializer(instance=user).data)
for key, value in user_values.items():
if key in ["allow_conversation_analytics", "language", "timezone"]:
if key in [
"allow_conversation_analytics",
"allow_smart_web_search",
"language",
"timezone",
]:
assert value == new_user_values[key]
else:
assert value == old_user_values[key]
@@ -419,7 +426,7 @@ def test_api_users_patch_anonymous():
def test_api_users_patch_authenticated_self():
"""
Authenticated users should be able to patch their own user but only "language",
"allow_conversation_analytics" and "timezone" fields.
"allow_conversation_analytics", "allow_smart_web_search" and "timezone" fields.
"""
user = factories.UserFactory()
@@ -431,6 +438,7 @@ def test_api_users_patch_authenticated_self():
serializers.UserSerializer(
instance=factories.UserFactory(
allow_conversation_analytics=not user.allow_conversation_analytics,
allow_smart_web_search=not user.allow_smart_web_search,
)
).data
)
@@ -446,7 +454,12 @@ def test_api_users_patch_authenticated_self():
user.refresh_from_db()
user_values = dict(serializers.UserSerializer(instance=user).data)
for key, value in user_values.items():
if key in ["allow_conversation_analytics", "language", "timezone"]:
if key in [
"allow_conversation_analytics",
"allow_smart_web_search",
"language",
"timezone",
]:
assert value == new_user_values[key]
else:
assert value == old_user_values[key]
+80 -88
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: la-suite-conversations\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-10-20 21:48+0000\n"
"PO-Revision-Date: 2026-02-09 13:05\n"
"POT-Creation-Date: 2026-02-26 21:42+0000\n"
"PO-Revision-Date: 2026-03-11 15:23\n"
"Last-Translator: \n"
"Language-Team: German\n"
"Language: de_DE\n"
@@ -17,328 +17,320 @@ msgstr ""
"X-Crowdin-File: backend-conversations.pot\n"
"X-Crowdin-File-ID: 26\n"
#: activation_codes/admin.py:55 build/lib/activation_codes/admin.py:55
#: activation_codes/admin.py:55
msgid "Configuration"
msgstr ""
#: activation_codes/admin.py:66 build/lib/activation_codes/admin.py:66
#: activation_codes/admin.py:66
msgid "Usage details"
msgstr ""
#: activation_codes/admin.py:70 activation_codes/admin.py:226
#: build/lib/activation_codes/admin.py:70
#: build/lib/activation_codes/admin.py:226
msgid "Timestamps"
msgstr ""
#: activation_codes/admin.py:109 build/lib/activation_codes/admin.py:109
#: activation_codes/admin.py:109
msgid "Usage"
msgstr ""
#: activation_codes/admin.py:117 build/lib/activation_codes/admin.py:117
#: activation_codes/admin.py:117
msgid "Description"
msgstr ""
#: activation_codes/admin.py:124 build/lib/activation_codes/admin.py:124
#: activation_codes/admin.py:124
msgid "No users have used this code yet"
msgstr ""
#: activation_codes/admin.py:135 build/lib/activation_codes/admin.py:135
#: activation_codes/admin.py:135
msgid "Name"
msgstr ""
#: activation_codes/admin.py:136 activation_codes/admin.py:246
#: build/lib/activation_codes/admin.py:136
#: build/lib/activation_codes/admin.py:246
msgid "Email"
msgstr ""
#: activation_codes/admin.py:137 build/lib/activation_codes/admin.py:137
#: activation_codes/admin.py:137
msgid "Date"
msgstr ""
#: activation_codes/admin.py:161 build/lib/activation_codes/admin.py:161
#: activation_codes/admin.py:161
msgid "Users who used this code"
msgstr ""
#: activation_codes/admin.py:163 build/lib/activation_codes/admin.py:163
#: activation_codes/admin.py:163
msgid "Recompute current uses from related activations"
msgstr ""
#: activation_codes/admin.py:177 build/lib/activation_codes/admin.py:177
#: activation_codes/admin.py:177
msgid "All selected activation codes already have correct usage counts."
msgstr ""
#: activation_codes/admin.py:182 build/lib/activation_codes/admin.py:182
#: activation_codes/admin.py:182
#, python-format
msgid "Successfully recomputed usage counts for %(count)d activation code(s)."
msgstr ""
#: activation_codes/admin.py:240 activation_codes/admin.py:284
#: build/lib/activation_codes/admin.py:240
#: build/lib/activation_codes/admin.py:284
msgid "User"
msgstr ""
#: activation_codes/admin.py:291 build/lib/activation_codes/admin.py:291
#: activation_codes/admin.py:291
msgid "Has used activation code"
msgstr ""
#: activation_codes/admin.py:293 build/lib/activation_codes/admin.py:293
#: activation_codes/admin.py:293
msgid "Add selected users to Brevo waiting list"
msgstr ""
#: activation_codes/admin.py:314 build/lib/activation_codes/admin.py:314
#: activation_codes/admin.py:314
#, python-format
msgid "Added %(count)d user(s) to Brevo waiting list."
msgstr ""
#: activation_codes/admin.py:319 activation_codes/admin.py:347
#: build/lib/activation_codes/admin.py:319
#: build/lib/activation_codes/admin.py:347
msgid "No valid email address found in selected registrations."
msgstr ""
#: activation_codes/admin.py:323 build/lib/activation_codes/admin.py:323
#: activation_codes/admin.py:323
msgid "Remove selected users from Brevo waiting list"
msgstr ""
#: activation_codes/admin.py:342 build/lib/activation_codes/admin.py:342
#: activation_codes/admin.py:342
#, python-format
msgid "Removed %(count)d user(s) from Brevo waiting list."
msgstr ""
#: activation_codes/models.py:38 activation_codes/models.py:85
#: activation_codes/models.py:178 build/lib/activation_codes/models.py:38
#: build/lib/activation_codes/models.py:85
#: build/lib/activation_codes/models.py:178
#: activation_codes/models.py:178
msgid "activation code"
msgstr ""
#: activation_codes/models.py:39 build/lib/activation_codes/models.py:39
#: activation_codes/models.py:39
msgid "The activation code that users will enter"
msgstr ""
#: activation_codes/models.py:46 build/lib/activation_codes/models.py:46
#: activation_codes/models.py:46
msgid "Code must be alphanumeric and contain no spaces or special characters"
msgstr ""
#: activation_codes/models.py:52 build/lib/activation_codes/models.py:52
#: activation_codes/models.py:52
msgid "maximum uses"
msgstr ""
#: activation_codes/models.py:53 build/lib/activation_codes/models.py:53
#: activation_codes/models.py:53
msgid "Maximum number of times this code can be used. 0 means unlimited."
msgstr ""
#: activation_codes/models.py:58 build/lib/activation_codes/models.py:58
#: activation_codes/models.py:58
msgid "current uses"
msgstr ""
#: activation_codes/models.py:59 build/lib/activation_codes/models.py:59
#: activation_codes/models.py:59
msgid "Number of times this code has been used"
msgstr ""
#: activation_codes/models.py:65 build/lib/activation_codes/models.py:65
#: build/lib/core/models.py:151 core/models.py:151
#: activation_codes/models.py:65 core/models.py:154
msgid "active"
msgstr ""
#: activation_codes/models.py:66 build/lib/activation_codes/models.py:66
#: activation_codes/models.py:66
msgid "Whether this code can still be used"
msgstr ""
#: activation_codes/models.py:71 build/lib/activation_codes/models.py:71
#: activation_codes/models.py:71
msgid "expires at"
msgstr ""
#: activation_codes/models.py:72 build/lib/activation_codes/models.py:72
#: activation_codes/models.py:72
msgid "Date and time when this code expires"
msgstr ""
#: activation_codes/models.py:78 build/lib/activation_codes/models.py:78
#: activation_codes/models.py:78
msgid "description"
msgstr ""
#: activation_codes/models.py:79 build/lib/activation_codes/models.py:79
#: activation_codes/models.py:79
msgid "Internal description or notes about this code"
msgstr ""
#: activation_codes/models.py:86 build/lib/activation_codes/models.py:86
#: activation_codes/models.py:86
msgid "activation codes"
msgstr ""
#: activation_codes/models.py:128 build/lib/activation_codes/models.py:128
#: activation_codes/models.py:128
msgid "This activation code is no longer valid"
msgstr ""
#: activation_codes/models.py:136 build/lib/activation_codes/models.py:136
#: activation_codes/models.py:136
msgid "You have already activated your account"
msgstr ""
#: activation_codes/models.py:170 activation_codes/models.py:202
#: build/lib/activation_codes/models.py:170
#: build/lib/activation_codes/models.py:202 build/lib/core/models.py:173
#: core/models.py:173
#: core/models.py:182
msgid "user"
msgstr ""
#: activation_codes/models.py:171 build/lib/activation_codes/models.py:171
#: activation_codes/models.py:171
msgid "The user who used the activation code"
msgstr ""
#: activation_codes/models.py:179 build/lib/activation_codes/models.py:179
#: activation_codes/models.py:179
msgid "The activation code that was used"
msgstr ""
#: activation_codes/models.py:186 activation_codes/models.py:210
#: build/lib/activation_codes/models.py:186
#: build/lib/activation_codes/models.py:210
msgid "user activation"
msgstr ""
#: activation_codes/models.py:187 build/lib/activation_codes/models.py:187
#: activation_codes/models.py:187
msgid "user activations"
msgstr ""
#: activation_codes/models.py:203 build/lib/activation_codes/models.py:203
#: activation_codes/models.py:203
msgid "The user who made the registration request"
msgstr ""
#: activation_codes/models.py:211 build/lib/activation_codes/models.py:211
#: activation_codes/models.py:211
msgid "Store if the user received an activation code and used it"
msgstr ""
#: activation_codes/models.py:220 build/lib/activation_codes/models.py:220
#: activation_codes/models.py:220
msgid "user registration request"
msgstr ""
#: activation_codes/models.py:221 build/lib/activation_codes/models.py:221
#: activation_codes/models.py:221
msgid "user registration requests"
msgstr ""
#: activation_codes/serializers.py:14
#: build/lib/activation_codes/serializers.py:14
msgid "The activation code to validate"
msgstr ""
#: activation_codes/viewsets.py:107 build/lib/activation_codes/viewsets.py:107
#: activation_codes/viewsets.py:107
msgid "Your account has been successfully activated"
msgstr ""
#: build/lib/chat/apps.py:12 chat/apps.py:12
#: chat/apps.py:12
msgid "chat application"
msgstr ""
#: build/lib/core/admin.py:26 core/admin.py:26
#: core/admin.py:26
msgid "Personal info"
msgstr ""
#: build/lib/core/admin.py:40 core/admin.py:40
#: core/admin.py:40
msgid "Permissions"
msgstr ""
#: build/lib/core/admin.py:52 core/admin.py:52
#: core/admin.py:52
msgid "Important dates"
msgstr ""
#: build/lib/core/models.py:39 core/models.py:39
#: core/models.py:39
msgid "id"
msgstr "id"
#: build/lib/core/models.py:40 core/models.py:40
#: core/models.py:40
msgid "primary key for the record as UUID"
msgstr ""
#: build/lib/core/models.py:46 core/models.py:46
#: core/models.py:46
msgid "created on"
msgstr ""
#: build/lib/core/models.py:47 core/models.py:47
#: core/models.py:47
msgid "date and time at which a record was created"
msgstr ""
#: build/lib/core/models.py:52 core/models.py:52
#: core/models.py:52
msgid "updated on"
msgstr ""
#: build/lib/core/models.py:53 core/models.py:53
#: core/models.py:53
msgid "date and time at which a record was last updated"
msgstr ""
#: build/lib/core/models.py:86 core/models.py:86
#: core/models.py:89
msgid "We couldn't find a user with this sub but the email is already associated with a registered user."
msgstr ""
#: build/lib/core/models.py:99 core/models.py:99
#: core/models.py:102
msgid "Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/_/: characters."
msgstr ""
#: build/lib/core/models.py:105 core/models.py:105
#: core/models.py:108
msgid "sub"
msgstr ""
#: build/lib/core/models.py:107 core/models.py:107
#: core/models.py:110
msgid "Required. 255 characters or fewer. Letters, numbers, and @/./+/-/_/: characters only."
msgstr ""
#: build/lib/core/models.py:116 core/models.py:116
#: core/models.py:119
msgid "full name"
msgstr ""
#: build/lib/core/models.py:117 core/models.py:117
#: core/models.py:120
msgid "short name"
msgstr ""
#: build/lib/core/models.py:119 core/models.py:119
#: core/models.py:122
msgid "identity email address"
msgstr ""
#: build/lib/core/models.py:123 core/models.py:123
#: core/models.py:126
msgid "admin email address"
msgstr ""
#: build/lib/core/models.py:129 core/models.py:129
#: core/models.py:132
msgid "language"
msgstr ""
#: build/lib/core/models.py:130 core/models.py:130
#: core/models.py:133
msgid "The language in which the user wants to see the interface."
msgstr ""
#: build/lib/core/models.py:138 core/models.py:138
#: core/models.py:141
msgid "The timezone in which the user wants to see times."
msgstr ""
#: build/lib/core/models.py:141 core/models.py:141
#: core/models.py:144
msgid "device"
msgstr ""
#: build/lib/core/models.py:143 core/models.py:143
#: core/models.py:146
msgid "Whether the user is a device or a real user."
msgstr ""
#: build/lib/core/models.py:146 core/models.py:146
#: core/models.py:149
msgid "staff status"
msgstr ""
#: build/lib/core/models.py:148 core/models.py:148
#: core/models.py:151
msgid "Whether the user can log into this admin site."
msgstr ""
#: build/lib/core/models.py:154 core/models.py:154
#: core/models.py:157
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
msgstr ""
#: build/lib/core/models.py:161 core/models.py:161
#: core/models.py:164
msgid "allow conversation analytics"
msgstr ""
#: build/lib/core/models.py:163 core/models.py:163
#: core/models.py:166
msgid "Whether the user allows to use their conversations for analytics."
msgstr ""
#: build/lib/core/models.py:174 core/models.py:174
#: core/models.py:170
msgid "allow smart web search"
msgstr ""
#: core/models.py:172
msgid "Whether the user allows to use smart web search features."
msgstr ""
#: core/models.py:183
msgid "users"
msgstr ""
+80 -88
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: la-suite-conversations\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-10-20 21:48+0000\n"
"PO-Revision-Date: 2026-02-09 13:05\n"
"POT-Creation-Date: 2026-02-26 21:42+0000\n"
"PO-Revision-Date: 2026-03-11 15:23\n"
"Last-Translator: \n"
"Language-Team: English\n"
"Language: en_US\n"
@@ -17,328 +17,320 @@ msgstr ""
"X-Crowdin-File: backend-conversations.pot\n"
"X-Crowdin-File-ID: 26\n"
#: activation_codes/admin.py:55 build/lib/activation_codes/admin.py:55
#: activation_codes/admin.py:55
msgid "Configuration"
msgstr ""
#: activation_codes/admin.py:66 build/lib/activation_codes/admin.py:66
#: activation_codes/admin.py:66
msgid "Usage details"
msgstr ""
#: activation_codes/admin.py:70 activation_codes/admin.py:226
#: build/lib/activation_codes/admin.py:70
#: build/lib/activation_codes/admin.py:226
msgid "Timestamps"
msgstr ""
#: activation_codes/admin.py:109 build/lib/activation_codes/admin.py:109
#: activation_codes/admin.py:109
msgid "Usage"
msgstr ""
#: activation_codes/admin.py:117 build/lib/activation_codes/admin.py:117
#: activation_codes/admin.py:117
msgid "Description"
msgstr ""
#: activation_codes/admin.py:124 build/lib/activation_codes/admin.py:124
#: activation_codes/admin.py:124
msgid "No users have used this code yet"
msgstr ""
#: activation_codes/admin.py:135 build/lib/activation_codes/admin.py:135
#: activation_codes/admin.py:135
msgid "Name"
msgstr ""
#: activation_codes/admin.py:136 activation_codes/admin.py:246
#: build/lib/activation_codes/admin.py:136
#: build/lib/activation_codes/admin.py:246
msgid "Email"
msgstr ""
#: activation_codes/admin.py:137 build/lib/activation_codes/admin.py:137
#: activation_codes/admin.py:137
msgid "Date"
msgstr ""
#: activation_codes/admin.py:161 build/lib/activation_codes/admin.py:161
#: activation_codes/admin.py:161
msgid "Users who used this code"
msgstr ""
#: activation_codes/admin.py:163 build/lib/activation_codes/admin.py:163
#: activation_codes/admin.py:163
msgid "Recompute current uses from related activations"
msgstr ""
#: activation_codes/admin.py:177 build/lib/activation_codes/admin.py:177
#: activation_codes/admin.py:177
msgid "All selected activation codes already have correct usage counts."
msgstr ""
#: activation_codes/admin.py:182 build/lib/activation_codes/admin.py:182
#: activation_codes/admin.py:182
#, python-format
msgid "Successfully recomputed usage counts for %(count)d activation code(s)."
msgstr ""
#: activation_codes/admin.py:240 activation_codes/admin.py:284
#: build/lib/activation_codes/admin.py:240
#: build/lib/activation_codes/admin.py:284
msgid "User"
msgstr ""
#: activation_codes/admin.py:291 build/lib/activation_codes/admin.py:291
#: activation_codes/admin.py:291
msgid "Has used activation code"
msgstr ""
#: activation_codes/admin.py:293 build/lib/activation_codes/admin.py:293
#: activation_codes/admin.py:293
msgid "Add selected users to Brevo waiting list"
msgstr ""
#: activation_codes/admin.py:314 build/lib/activation_codes/admin.py:314
#: activation_codes/admin.py:314
#, python-format
msgid "Added %(count)d user(s) to Brevo waiting list."
msgstr ""
#: activation_codes/admin.py:319 activation_codes/admin.py:347
#: build/lib/activation_codes/admin.py:319
#: build/lib/activation_codes/admin.py:347
msgid "No valid email address found in selected registrations."
msgstr ""
#: activation_codes/admin.py:323 build/lib/activation_codes/admin.py:323
#: activation_codes/admin.py:323
msgid "Remove selected users from Brevo waiting list"
msgstr ""
#: activation_codes/admin.py:342 build/lib/activation_codes/admin.py:342
#: activation_codes/admin.py:342
#, python-format
msgid "Removed %(count)d user(s) from Brevo waiting list."
msgstr ""
#: activation_codes/models.py:38 activation_codes/models.py:85
#: activation_codes/models.py:178 build/lib/activation_codes/models.py:38
#: build/lib/activation_codes/models.py:85
#: build/lib/activation_codes/models.py:178
#: activation_codes/models.py:178
msgid "activation code"
msgstr ""
#: activation_codes/models.py:39 build/lib/activation_codes/models.py:39
#: activation_codes/models.py:39
msgid "The activation code that users will enter"
msgstr ""
#: activation_codes/models.py:46 build/lib/activation_codes/models.py:46
#: activation_codes/models.py:46
msgid "Code must be alphanumeric and contain no spaces or special characters"
msgstr ""
#: activation_codes/models.py:52 build/lib/activation_codes/models.py:52
#: activation_codes/models.py:52
msgid "maximum uses"
msgstr ""
#: activation_codes/models.py:53 build/lib/activation_codes/models.py:53
#: activation_codes/models.py:53
msgid "Maximum number of times this code can be used. 0 means unlimited."
msgstr ""
#: activation_codes/models.py:58 build/lib/activation_codes/models.py:58
#: activation_codes/models.py:58
msgid "current uses"
msgstr ""
#: activation_codes/models.py:59 build/lib/activation_codes/models.py:59
#: activation_codes/models.py:59
msgid "Number of times this code has been used"
msgstr ""
#: activation_codes/models.py:65 build/lib/activation_codes/models.py:65
#: build/lib/core/models.py:151 core/models.py:151
#: activation_codes/models.py:65 core/models.py:154
msgid "active"
msgstr ""
#: activation_codes/models.py:66 build/lib/activation_codes/models.py:66
#: activation_codes/models.py:66
msgid "Whether this code can still be used"
msgstr ""
#: activation_codes/models.py:71 build/lib/activation_codes/models.py:71
#: activation_codes/models.py:71
msgid "expires at"
msgstr ""
#: activation_codes/models.py:72 build/lib/activation_codes/models.py:72
#: activation_codes/models.py:72
msgid "Date and time when this code expires"
msgstr ""
#: activation_codes/models.py:78 build/lib/activation_codes/models.py:78
#: activation_codes/models.py:78
msgid "description"
msgstr ""
#: activation_codes/models.py:79 build/lib/activation_codes/models.py:79
#: activation_codes/models.py:79
msgid "Internal description or notes about this code"
msgstr ""
#: activation_codes/models.py:86 build/lib/activation_codes/models.py:86
#: activation_codes/models.py:86
msgid "activation codes"
msgstr ""
#: activation_codes/models.py:128 build/lib/activation_codes/models.py:128
#: activation_codes/models.py:128
msgid "This activation code is no longer valid"
msgstr ""
#: activation_codes/models.py:136 build/lib/activation_codes/models.py:136
#: activation_codes/models.py:136
msgid "You have already activated your account"
msgstr ""
#: activation_codes/models.py:170 activation_codes/models.py:202
#: build/lib/activation_codes/models.py:170
#: build/lib/activation_codes/models.py:202 build/lib/core/models.py:173
#: core/models.py:173
#: core/models.py:182
msgid "user"
msgstr ""
#: activation_codes/models.py:171 build/lib/activation_codes/models.py:171
#: activation_codes/models.py:171
msgid "The user who used the activation code"
msgstr ""
#: activation_codes/models.py:179 build/lib/activation_codes/models.py:179
#: activation_codes/models.py:179
msgid "The activation code that was used"
msgstr ""
#: activation_codes/models.py:186 activation_codes/models.py:210
#: build/lib/activation_codes/models.py:186
#: build/lib/activation_codes/models.py:210
msgid "user activation"
msgstr ""
#: activation_codes/models.py:187 build/lib/activation_codes/models.py:187
#: activation_codes/models.py:187
msgid "user activations"
msgstr ""
#: activation_codes/models.py:203 build/lib/activation_codes/models.py:203
#: activation_codes/models.py:203
msgid "The user who made the registration request"
msgstr ""
#: activation_codes/models.py:211 build/lib/activation_codes/models.py:211
#: activation_codes/models.py:211
msgid "Store if the user received an activation code and used it"
msgstr ""
#: activation_codes/models.py:220 build/lib/activation_codes/models.py:220
#: activation_codes/models.py:220
msgid "user registration request"
msgstr ""
#: activation_codes/models.py:221 build/lib/activation_codes/models.py:221
#: activation_codes/models.py:221
msgid "user registration requests"
msgstr ""
#: activation_codes/serializers.py:14
#: build/lib/activation_codes/serializers.py:14
msgid "The activation code to validate"
msgstr ""
#: activation_codes/viewsets.py:107 build/lib/activation_codes/viewsets.py:107
#: activation_codes/viewsets.py:107
msgid "Your account has been successfully activated"
msgstr ""
#: build/lib/chat/apps.py:12 chat/apps.py:12
#: chat/apps.py:12
msgid "chat application"
msgstr ""
#: build/lib/core/admin.py:26 core/admin.py:26
#: core/admin.py:26
msgid "Personal info"
msgstr ""
#: build/lib/core/admin.py:40 core/admin.py:40
#: core/admin.py:40
msgid "Permissions"
msgstr ""
#: build/lib/core/admin.py:52 core/admin.py:52
#: core/admin.py:52
msgid "Important dates"
msgstr ""
#: build/lib/core/models.py:39 core/models.py:39
#: core/models.py:39
msgid "id"
msgstr ""
#: build/lib/core/models.py:40 core/models.py:40
#: core/models.py:40
msgid "primary key for the record as UUID"
msgstr ""
#: build/lib/core/models.py:46 core/models.py:46
#: core/models.py:46
msgid "created on"
msgstr ""
#: build/lib/core/models.py:47 core/models.py:47
#: core/models.py:47
msgid "date and time at which a record was created"
msgstr ""
#: build/lib/core/models.py:52 core/models.py:52
#: core/models.py:52
msgid "updated on"
msgstr ""
#: build/lib/core/models.py:53 core/models.py:53
#: core/models.py:53
msgid "date and time at which a record was last updated"
msgstr ""
#: build/lib/core/models.py:86 core/models.py:86
#: core/models.py:89
msgid "We couldn't find a user with this sub but the email is already associated with a registered user."
msgstr ""
#: build/lib/core/models.py:99 core/models.py:99
#: core/models.py:102
msgid "Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/_/: characters."
msgstr ""
#: build/lib/core/models.py:105 core/models.py:105
#: core/models.py:108
msgid "sub"
msgstr ""
#: build/lib/core/models.py:107 core/models.py:107
#: core/models.py:110
msgid "Required. 255 characters or fewer. Letters, numbers, and @/./+/-/_/: characters only."
msgstr ""
#: build/lib/core/models.py:116 core/models.py:116
#: core/models.py:119
msgid "full name"
msgstr ""
#: build/lib/core/models.py:117 core/models.py:117
#: core/models.py:120
msgid "short name"
msgstr ""
#: build/lib/core/models.py:119 core/models.py:119
#: core/models.py:122
msgid "identity email address"
msgstr ""
#: build/lib/core/models.py:123 core/models.py:123
#: core/models.py:126
msgid "admin email address"
msgstr ""
#: build/lib/core/models.py:129 core/models.py:129
#: core/models.py:132
msgid "language"
msgstr ""
#: build/lib/core/models.py:130 core/models.py:130
#: core/models.py:133
msgid "The language in which the user wants to see the interface."
msgstr ""
#: build/lib/core/models.py:138 core/models.py:138
#: core/models.py:141
msgid "The timezone in which the user wants to see times."
msgstr ""
#: build/lib/core/models.py:141 core/models.py:141
#: core/models.py:144
msgid "device"
msgstr ""
#: build/lib/core/models.py:143 core/models.py:143
#: core/models.py:146
msgid "Whether the user is a device or a real user."
msgstr ""
#: build/lib/core/models.py:146 core/models.py:146
#: core/models.py:149
msgid "staff status"
msgstr ""
#: build/lib/core/models.py:148 core/models.py:148
#: core/models.py:151
msgid "Whether the user can log into this admin site."
msgstr ""
#: build/lib/core/models.py:154 core/models.py:154
#: core/models.py:157
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
msgstr ""
#: build/lib/core/models.py:161 core/models.py:161
#: core/models.py:164
msgid "allow conversation analytics"
msgstr ""
#: build/lib/core/models.py:163 core/models.py:163
#: core/models.py:166
msgid "Whether the user allows to use their conversations for analytics."
msgstr ""
#: build/lib/core/models.py:174 core/models.py:174
#: core/models.py:170
msgid "allow smart web search"
msgstr ""
#: core/models.py:172
msgid "Whether the user allows to use smart web search features."
msgstr ""
#: core/models.py:183
msgid "users"
msgstr ""
+80 -88
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: la-suite-conversations\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-10-20 21:48+0000\n"
"PO-Revision-Date: 2026-02-09 13:05\n"
"POT-Creation-Date: 2026-02-26 21:42+0000\n"
"PO-Revision-Date: 2026-03-11 15:23\n"
"Last-Translator: \n"
"Language-Team: French\n"
"Language: fr_FR\n"
@@ -17,328 +17,320 @@ msgstr ""
"X-Crowdin-File: backend-conversations.pot\n"
"X-Crowdin-File-ID: 26\n"
#: activation_codes/admin.py:55 build/lib/activation_codes/admin.py:55
#: activation_codes/admin.py:55
msgid "Configuration"
msgstr "Configuration"
#: activation_codes/admin.py:66 build/lib/activation_codes/admin.py:66
#: activation_codes/admin.py:66
msgid "Usage details"
msgstr "Détails d'utilisation"
#: activation_codes/admin.py:70 activation_codes/admin.py:226
#: build/lib/activation_codes/admin.py:70
#: build/lib/activation_codes/admin.py:226
msgid "Timestamps"
msgstr "Horodatages"
#: activation_codes/admin.py:109 build/lib/activation_codes/admin.py:109
#: activation_codes/admin.py:109
msgid "Usage"
msgstr "Utilisation"
#: activation_codes/admin.py:117 build/lib/activation_codes/admin.py:117
#: activation_codes/admin.py:117
msgid "Description"
msgstr "Description"
#: activation_codes/admin.py:124 build/lib/activation_codes/admin.py:124
#: activation_codes/admin.py:124
msgid "No users have used this code yet"
msgstr "Aucun utilisateur n'a encore utilisé ce code"
#: activation_codes/admin.py:135 build/lib/activation_codes/admin.py:135
#: activation_codes/admin.py:135
msgid "Name"
msgstr "Nom"
#: activation_codes/admin.py:136 activation_codes/admin.py:246
#: build/lib/activation_codes/admin.py:136
#: build/lib/activation_codes/admin.py:246
msgid "Email"
msgstr "E-mail"
#: activation_codes/admin.py:137 build/lib/activation_codes/admin.py:137
#: activation_codes/admin.py:137
msgid "Date"
msgstr "Date"
#: activation_codes/admin.py:161 build/lib/activation_codes/admin.py:161
#: activation_codes/admin.py:161
msgid "Users who used this code"
msgstr "Utilisateurs qui ont utilisé ce code"
#: activation_codes/admin.py:163 build/lib/activation_codes/admin.py:163
#: activation_codes/admin.py:163
msgid "Recompute current uses from related activations"
msgstr "Recalculer les utilisations actuelles à partir des activations liées"
#: activation_codes/admin.py:177 build/lib/activation_codes/admin.py:177
#: activation_codes/admin.py:177
msgid "All selected activation codes already have correct usage counts."
msgstr "Tous les codes d'activation sélectionnés ont déjà un nombre correct d'utilisations."
#: activation_codes/admin.py:182 build/lib/activation_codes/admin.py:182
#: activation_codes/admin.py:182
#, python-format
msgid "Successfully recomputed usage counts for %(count)d activation code(s)."
msgstr "Utilisation recalculée avec succès pour %(count)d code(s) d'activation."
#: activation_codes/admin.py:240 activation_codes/admin.py:284
#: build/lib/activation_codes/admin.py:240
#: build/lib/activation_codes/admin.py:284
msgid "User"
msgstr "Utilisateur"
#: activation_codes/admin.py:291 build/lib/activation_codes/admin.py:291
#: activation_codes/admin.py:291
msgid "Has used activation code"
msgstr "A utilisé le code d'activation"
#: activation_codes/admin.py:293 build/lib/activation_codes/admin.py:293
#: activation_codes/admin.py:293
msgid "Add selected users to Brevo waiting list"
msgstr "Ajouter les utilisateurs sélectionnés à la liste d'attente Brevo"
#: activation_codes/admin.py:314 build/lib/activation_codes/admin.py:314
#: activation_codes/admin.py:314
#, python-format
msgid "Added %(count)d user(s) to Brevo waiting list."
msgstr "%(count)d utilisateur(s) ajoutés à la liste d'attente Brevo."
#: activation_codes/admin.py:319 activation_codes/admin.py:347
#: build/lib/activation_codes/admin.py:319
#: build/lib/activation_codes/admin.py:347
msgid "No valid email address found in selected registrations."
msgstr "Aucune adresse e-mail valide trouvée dans les inscriptions sélectionnées."
#: activation_codes/admin.py:323 build/lib/activation_codes/admin.py:323
#: activation_codes/admin.py:323
msgid "Remove selected users from Brevo waiting list"
msgstr "Supprimer les utilisateurs sélectionnés de la liste d'attente Brevo"
#: activation_codes/admin.py:342 build/lib/activation_codes/admin.py:342
#: activation_codes/admin.py:342
#, python-format
msgid "Removed %(count)d user(s) from Brevo waiting list."
msgstr "Suppression de %(count)d utilisateur(s) de la liste d'attente Brevo."
#: activation_codes/models.py:38 activation_codes/models.py:85
#: activation_codes/models.py:178 build/lib/activation_codes/models.py:38
#: build/lib/activation_codes/models.py:85
#: build/lib/activation_codes/models.py:178
#: activation_codes/models.py:178
msgid "activation code"
msgstr "code d'activation"
#: activation_codes/models.py:39 build/lib/activation_codes/models.py:39
#: activation_codes/models.py:39
msgid "The activation code that users will enter"
msgstr "Le code d'activation que les utilisateurs entreront"
#: activation_codes/models.py:46 build/lib/activation_codes/models.py:46
#: activation_codes/models.py:46
msgid "Code must be alphanumeric and contain no spaces or special characters"
msgstr "Le code doit être alphanumérique et ne contenir ni espaces ni caractères spéciaux"
#: activation_codes/models.py:52 build/lib/activation_codes/models.py:52
#: activation_codes/models.py:52
msgid "maximum uses"
msgstr "utilisations maximales"
#: activation_codes/models.py:53 build/lib/activation_codes/models.py:53
#: activation_codes/models.py:53
msgid "Maximum number of times this code can be used. 0 means unlimited."
msgstr "Nombre maximum d'utilisation de ce code. 0 signifie illimité."
#: activation_codes/models.py:58 build/lib/activation_codes/models.py:58
#: activation_codes/models.py:58
msgid "current uses"
msgstr "utilisations actuelles"
#: activation_codes/models.py:59 build/lib/activation_codes/models.py:59
#: activation_codes/models.py:59
msgid "Number of times this code has been used"
msgstr "Nombre de fois où ce code a été utilisé"
#: activation_codes/models.py:65 build/lib/activation_codes/models.py:65
#: build/lib/core/models.py:151 core/models.py:151
#: activation_codes/models.py:65 core/models.py:154
msgid "active"
msgstr "actif"
#: activation_codes/models.py:66 build/lib/activation_codes/models.py:66
#: activation_codes/models.py:66
msgid "Whether this code can still be used"
msgstr "Si ce code peut encore être utilisé"
#: activation_codes/models.py:71 build/lib/activation_codes/models.py:71
#: activation_codes/models.py:71
msgid "expires at"
msgstr "expiration"
#: activation_codes/models.py:72 build/lib/activation_codes/models.py:72
#: activation_codes/models.py:72
msgid "Date and time when this code expires"
msgstr "Date et heure d'expiration de ce code"
#: activation_codes/models.py:78 build/lib/activation_codes/models.py:78
#: activation_codes/models.py:78
msgid "description"
msgstr "description"
#: activation_codes/models.py:79 build/lib/activation_codes/models.py:79
#: activation_codes/models.py:79
msgid "Internal description or notes about this code"
msgstr "Description interne ou notes à propos de ce code"
#: activation_codes/models.py:86 build/lib/activation_codes/models.py:86
#: activation_codes/models.py:86
msgid "activation codes"
msgstr "codes d'activation"
#: activation_codes/models.py:128 build/lib/activation_codes/models.py:128
#: activation_codes/models.py:128
msgid "This activation code is no longer valid"
msgstr "Ce code d'activation n'est plus valide"
#: activation_codes/models.py:136 build/lib/activation_codes/models.py:136
#: activation_codes/models.py:136
msgid "You have already activated your account"
msgstr "Vous avez déjà activé votre compte"
#: activation_codes/models.py:170 activation_codes/models.py:202
#: build/lib/activation_codes/models.py:170
#: build/lib/activation_codes/models.py:202 build/lib/core/models.py:173
#: core/models.py:173
#: core/models.py:182
msgid "user"
msgstr "utilisateur"
#: activation_codes/models.py:171 build/lib/activation_codes/models.py:171
#: activation_codes/models.py:171
msgid "The user who used the activation code"
msgstr "L'utilisateur qui a utilisé le code d'activation"
#: activation_codes/models.py:179 build/lib/activation_codes/models.py:179
#: activation_codes/models.py:179
msgid "The activation code that was used"
msgstr "Le code d'activation qui a été utilisé"
#: activation_codes/models.py:186 activation_codes/models.py:210
#: build/lib/activation_codes/models.py:186
#: build/lib/activation_codes/models.py:210
msgid "user activation"
msgstr "activation d'utilisateur"
#: activation_codes/models.py:187 build/lib/activation_codes/models.py:187
#: activation_codes/models.py:187
msgid "user activations"
msgstr "activations d'utilisateurs"
#: activation_codes/models.py:203 build/lib/activation_codes/models.py:203
#: activation_codes/models.py:203
msgid "The user who made the registration request"
msgstr "L'utilisateur qui a fait la demande d'enregistrement"
#: activation_codes/models.py:211 build/lib/activation_codes/models.py:211
#: activation_codes/models.py:211
msgid "Store if the user received an activation code and used it"
msgstr "Enregistrer si l'utilisateur a reçu un code d'activation et l'a utilisé"
#: activation_codes/models.py:220 build/lib/activation_codes/models.py:220
#: activation_codes/models.py:220
msgid "user registration request"
msgstr "demande d'inscription d'utilisateur"
#: activation_codes/models.py:221 build/lib/activation_codes/models.py:221
#: activation_codes/models.py:221
msgid "user registration requests"
msgstr "demandes d'inscription d'utilisateur"
#: activation_codes/serializers.py:14
#: build/lib/activation_codes/serializers.py:14
msgid "The activation code to validate"
msgstr "Le code d'activation à valider"
#: activation_codes/viewsets.py:107 build/lib/activation_codes/viewsets.py:107
#: activation_codes/viewsets.py:107
msgid "Your account has been successfully activated"
msgstr "Votre compte a été activé avec succès"
#: build/lib/chat/apps.py:12 chat/apps.py:12
#: chat/apps.py:12
msgid "chat application"
msgstr "application de chat"
#: build/lib/core/admin.py:26 core/admin.py:26
#: core/admin.py:26
msgid "Personal info"
msgstr "Informations personnelles"
#: build/lib/core/admin.py:40 core/admin.py:40
#: core/admin.py:40
msgid "Permissions"
msgstr "Permissions"
#: build/lib/core/admin.py:52 core/admin.py:52
#: core/admin.py:52
msgid "Important dates"
msgstr "Dates importantes"
#: build/lib/core/models.py:39 core/models.py:39
#: core/models.py:39
msgid "id"
msgstr "id"
#: build/lib/core/models.py:40 core/models.py:40
#: core/models.py:40
msgid "primary key for the record as UUID"
msgstr "clé primaire pour l'enregistrement en tant que UUID"
#: build/lib/core/models.py:46 core/models.py:46
#: core/models.py:46
msgid "created on"
msgstr "créé le"
#: build/lib/core/models.py:47 core/models.py:47
#: core/models.py:47
msgid "date and time at which a record was created"
msgstr "date et heure de création de l'enregistrement"
#: build/lib/core/models.py:52 core/models.py:52
#: core/models.py:52
msgid "updated on"
msgstr "mis à jour le"
#: build/lib/core/models.py:53 core/models.py:53
#: core/models.py:53
msgid "date and time at which a record was last updated"
msgstr "date et heure de la dernière mise à jour de l'enregistrement"
#: build/lib/core/models.py:86 core/models.py:86
#: core/models.py:89
msgid "We couldn't find a user with this sub but the email is already associated with a registered user."
msgstr "Nous n'avons pas pu trouver un utilisateur avec ce sous-groupe mais l'e-mail est déjà associé à un utilisateur enregistré."
#: build/lib/core/models.py:99 core/models.py:99
#: core/models.py:102
msgid "Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/_/: characters."
msgstr "Saisissez un 'sub' valide. Cette valeur ne peut contenir que des lettres, des chiffres et les caractères @/./+/-/_/: uniquement."
#: build/lib/core/models.py:105 core/models.py:105
#: core/models.py:108
msgid "sub"
msgstr "sub"
#: build/lib/core/models.py:107 core/models.py:107
#: core/models.py:110
msgid "Required. 255 characters or fewer. Letters, numbers, and @/./+/-/_/: characters only."
msgstr "Obligatoire. 255 caractères ou moins. Lettres, chiffres et caractères @/./+/-/_/: uniquement."
#: build/lib/core/models.py:116 core/models.py:116
#: core/models.py:119
msgid "full name"
msgstr "nom complet"
#: build/lib/core/models.py:117 core/models.py:117
#: core/models.py:120
msgid "short name"
msgstr "nom court"
#: build/lib/core/models.py:119 core/models.py:119
#: core/models.py:122
msgid "identity email address"
msgstr "adresse e-mail d'identité"
#: build/lib/core/models.py:123 core/models.py:123
#: core/models.py:126
msgid "admin email address"
msgstr "adresse e-mail de l'administrateur"
#: build/lib/core/models.py:129 core/models.py:129
#: core/models.py:132
msgid "language"
msgstr "langue"
#: build/lib/core/models.py:130 core/models.py:130
#: core/models.py:133
msgid "The language in which the user wants to see the interface."
msgstr "La langue dans laquelle l'utilisateur veut voir l'interface."
#: build/lib/core/models.py:138 core/models.py:138
#: core/models.py:141
msgid "The timezone in which the user wants to see times."
msgstr "Le fuseau horaire dans lequel l'utilisateur souhaite voir les heures."
#: build/lib/core/models.py:141 core/models.py:141
#: core/models.py:144
msgid "device"
msgstr "appareil"
#: build/lib/core/models.py:143 core/models.py:143
#: core/models.py:146
msgid "Whether the user is a device or a real user."
msgstr "Si l'utilisateur est un appareil ou un utilisateur réel."
#: build/lib/core/models.py:146 core/models.py:146
#: core/models.py:149
msgid "staff status"
msgstr "statut d'équipe"
#: build/lib/core/models.py:148 core/models.py:148
#: core/models.py:151
msgid "Whether the user can log into this admin site."
msgstr "Si l'utilisateur peut se connecter à ce site d'administration."
#: build/lib/core/models.py:154 core/models.py:154
#: core/models.py:157
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
msgstr "Si cet utilisateur doit être traité comme actif. Désélectionnez ceci au lieu de supprimer des comptes."
#: build/lib/core/models.py:161 core/models.py:161
#: core/models.py:164
msgid "allow conversation analytics"
msgstr "autoriser les analyses de conversation"
#: build/lib/core/models.py:163 core/models.py:163
#: core/models.py:166
msgid "Whether the user allows to use their conversations for analytics."
msgstr "Si l'utilisateur autorise l'utilisation de ses conversations pour des analyses."
#: build/lib/core/models.py:174 core/models.py:174
#: core/models.py:170
msgid "allow smart web search"
msgstr "autoriser la recherche intelligente sur le web"
#: core/models.py:172
msgid "Whether the user allows to use smart web search features."
msgstr "Si l'utilisateur autorise l'utilisation de fonctions de recherche intelligente sur le Web."
#: core/models.py:183
msgid "users"
msgstr "utilisateurs"
+80 -88
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: la-suite-conversations\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-10-20 21:48+0000\n"
"PO-Revision-Date: 2026-02-09 13:05\n"
"POT-Creation-Date: 2026-02-26 21:42+0000\n"
"PO-Revision-Date: 2026-03-11 15:23\n"
"Last-Translator: \n"
"Language-Team: Dutch\n"
"Language: nl_NL\n"
@@ -17,328 +17,320 @@ msgstr ""
"X-Crowdin-File: backend-conversations.pot\n"
"X-Crowdin-File-ID: 26\n"
#: activation_codes/admin.py:55 build/lib/activation_codes/admin.py:55
#: activation_codes/admin.py:55
msgid "Configuration"
msgstr "Configuratie"
#: activation_codes/admin.py:66 build/lib/activation_codes/admin.py:66
#: activation_codes/admin.py:66
msgid "Usage details"
msgstr "Gebruiksdetails"
#: activation_codes/admin.py:70 activation_codes/admin.py:226
#: build/lib/activation_codes/admin.py:70
#: build/lib/activation_codes/admin.py:226
msgid "Timestamps"
msgstr "Tijdstempels"
#: activation_codes/admin.py:109 build/lib/activation_codes/admin.py:109
#: activation_codes/admin.py:109
msgid "Usage"
msgstr "Gebruik"
#: activation_codes/admin.py:117 build/lib/activation_codes/admin.py:117
#: activation_codes/admin.py:117
msgid "Description"
msgstr "Beschrijving"
#: activation_codes/admin.py:124 build/lib/activation_codes/admin.py:124
#: activation_codes/admin.py:124
msgid "No users have used this code yet"
msgstr "Er zijn nog geen gebruikers die deze code hebben gebruikt"
#: activation_codes/admin.py:135 build/lib/activation_codes/admin.py:135
#: activation_codes/admin.py:135
msgid "Name"
msgstr "Naam"
#: activation_codes/admin.py:136 activation_codes/admin.py:246
#: build/lib/activation_codes/admin.py:136
#: build/lib/activation_codes/admin.py:246
msgid "Email"
msgstr "E-mail"
#: activation_codes/admin.py:137 build/lib/activation_codes/admin.py:137
#: activation_codes/admin.py:137
msgid "Date"
msgstr "Datum"
#: activation_codes/admin.py:161 build/lib/activation_codes/admin.py:161
#: activation_codes/admin.py:161
msgid "Users who used this code"
msgstr "Gebruikers die deze code hebben gebruikt"
#: activation_codes/admin.py:163 build/lib/activation_codes/admin.py:163
#: activation_codes/admin.py:163
msgid "Recompute current uses from related activations"
msgstr "Herbereken het huidige gebruik van gerelateerde activeringen"
#: activation_codes/admin.py:177 build/lib/activation_codes/admin.py:177
#: activation_codes/admin.py:177
msgid "All selected activation codes already have correct usage counts."
msgstr "Alle geselecteerde activeringscodes hebben al het juiste gebruiksaantal."
#: activation_codes/admin.py:182 build/lib/activation_codes/admin.py:182
#: activation_codes/admin.py:182
#, python-format
msgid "Successfully recomputed usage counts for %(count)d activation code(s)."
msgstr "Het gebruik van %(count)d activeringscode(s) is opnieuw berekend."
#: activation_codes/admin.py:240 activation_codes/admin.py:284
#: build/lib/activation_codes/admin.py:240
#: build/lib/activation_codes/admin.py:284
msgid "User"
msgstr "Gebruiker"
#: activation_codes/admin.py:291 build/lib/activation_codes/admin.py:291
#: activation_codes/admin.py:291
msgid "Has used activation code"
msgstr "Heeft activeringscode gebruikt"
#: activation_codes/admin.py:293 build/lib/activation_codes/admin.py:293
#: activation_codes/admin.py:293
msgid "Add selected users to Brevo waiting list"
msgstr "Voeg geselecteerde gebruikers toe aan de Brevo-wachtlijst"
#: activation_codes/admin.py:314 build/lib/activation_codes/admin.py:314
#: activation_codes/admin.py:314
#, python-format
msgid "Added %(count)d user(s) to Brevo waiting list."
msgstr "%(count)d gebruiker(s) toegevoegd aan de Brevo-wachtlijst."
#: activation_codes/admin.py:319 activation_codes/admin.py:347
#: build/lib/activation_codes/admin.py:319
#: build/lib/activation_codes/admin.py:347
msgid "No valid email address found in selected registrations."
msgstr "Er is geen geldig e-mailadres gevonden in de geselecteerde registraties."
#: activation_codes/admin.py:323 build/lib/activation_codes/admin.py:323
#: activation_codes/admin.py:323
msgid "Remove selected users from Brevo waiting list"
msgstr "Geselecteerde gebruikers van de Brevo-wachtlijst verwijderen"
#: activation_codes/admin.py:342 build/lib/activation_codes/admin.py:342
#: activation_codes/admin.py:342
#, python-format
msgid "Removed %(count)d user(s) from Brevo waiting list."
msgstr "%(count)d gebruiker(s) verwijderd van de Brevo-wachtlijst."
#: activation_codes/models.py:38 activation_codes/models.py:85
#: activation_codes/models.py:178 build/lib/activation_codes/models.py:38
#: build/lib/activation_codes/models.py:85
#: build/lib/activation_codes/models.py:178
#: activation_codes/models.py:178
msgid "activation code"
msgstr "activeringscode"
#: activation_codes/models.py:39 build/lib/activation_codes/models.py:39
#: activation_codes/models.py:39
msgid "The activation code that users will enter"
msgstr "De activeringscode die gebruikers invoeren"
#: activation_codes/models.py:46 build/lib/activation_codes/models.py:46
#: activation_codes/models.py:46
msgid "Code must be alphanumeric and contain no spaces or special characters"
msgstr "De code moet alfanumeriek zijn en mag geen spaties of speciale tekens bevatten"
#: activation_codes/models.py:52 build/lib/activation_codes/models.py:52
#: activation_codes/models.py:52
msgid "maximum uses"
msgstr "maximaal gebruik"
#: activation_codes/models.py:53 build/lib/activation_codes/models.py:53
#: activation_codes/models.py:53
msgid "Maximum number of times this code can be used. 0 means unlimited."
msgstr "Maximaal aantal keren dat deze code kan worden gebruikt. 0 betekent onbeperkt."
#: activation_codes/models.py:58 build/lib/activation_codes/models.py:58
#: activation_codes/models.py:58
msgid "current uses"
msgstr "huidig gebruik"
#: activation_codes/models.py:59 build/lib/activation_codes/models.py:59
#: activation_codes/models.py:59
msgid "Number of times this code has been used"
msgstr "Aantal keren dat deze code is gebruikt"
#: activation_codes/models.py:65 build/lib/activation_codes/models.py:65
#: build/lib/core/models.py:151 core/models.py:151
#: activation_codes/models.py:65 core/models.py:154
msgid "active"
msgstr "actief"
#: activation_codes/models.py:66 build/lib/activation_codes/models.py:66
#: activation_codes/models.py:66
msgid "Whether this code can still be used"
msgstr "Of deze code nog gebruikt kan worden"
#: activation_codes/models.py:71 build/lib/activation_codes/models.py:71
#: activation_codes/models.py:71
msgid "expires at"
msgstr "vervalt op"
#: activation_codes/models.py:72 build/lib/activation_codes/models.py:72
#: activation_codes/models.py:72
msgid "Date and time when this code expires"
msgstr "Datum en tijd waarop deze code verloopt"
#: activation_codes/models.py:78 build/lib/activation_codes/models.py:78
#: activation_codes/models.py:78
msgid "description"
msgstr "beschrijving"
#: activation_codes/models.py:79 build/lib/activation_codes/models.py:79
#: activation_codes/models.py:79
msgid "Internal description or notes about this code"
msgstr "Interne beschrijving of notities over deze code"
#: activation_codes/models.py:86 build/lib/activation_codes/models.py:86
#: activation_codes/models.py:86
msgid "activation codes"
msgstr "activeringscodes"
#: activation_codes/models.py:128 build/lib/activation_codes/models.py:128
#: activation_codes/models.py:128
msgid "This activation code is no longer valid"
msgstr "Deze activeringscode is niet meer geldig"
#: activation_codes/models.py:136 build/lib/activation_codes/models.py:136
#: activation_codes/models.py:136
msgid "You have already activated your account"
msgstr "Je hebt je account al geactiveerd"
#: activation_codes/models.py:170 activation_codes/models.py:202
#: build/lib/activation_codes/models.py:170
#: build/lib/activation_codes/models.py:202 build/lib/core/models.py:173
#: core/models.py:173
#: core/models.py:182
msgid "user"
msgstr "gebruiker"
#: activation_codes/models.py:171 build/lib/activation_codes/models.py:171
#: activation_codes/models.py:171
msgid "The user who used the activation code"
msgstr "De gebruiker die de activeringscode heeft gebruikt"
#: activation_codes/models.py:179 build/lib/activation_codes/models.py:179
#: activation_codes/models.py:179
msgid "The activation code that was used"
msgstr "De activeringscode die is gebruikt"
#: activation_codes/models.py:186 activation_codes/models.py:210
#: build/lib/activation_codes/models.py:186
#: build/lib/activation_codes/models.py:210
msgid "user activation"
msgstr "gebruikers activering"
#: activation_codes/models.py:187 build/lib/activation_codes/models.py:187
#: activation_codes/models.py:187
msgid "user activations"
msgstr "gebruikersactivaties"
#: activation_codes/models.py:203 build/lib/activation_codes/models.py:203
#: activation_codes/models.py:203
msgid "The user who made the registration request"
msgstr "De gebruiker die het registratieverzoek heeft gedaan"
#: activation_codes/models.py:211 build/lib/activation_codes/models.py:211
#: activation_codes/models.py:211
msgid "Store if the user received an activation code and used it"
msgstr "Opslaan of de gebruiker een activeringscode heeft ontvangen en deze heeft gebruikt"
#: activation_codes/models.py:220 build/lib/activation_codes/models.py:220
#: activation_codes/models.py:220
msgid "user registration request"
msgstr "gebruikersregistratieverzoek"
#: activation_codes/models.py:221 build/lib/activation_codes/models.py:221
#: activation_codes/models.py:221
msgid "user registration requests"
msgstr "gebruikersregistratieverzoeken"
#: activation_codes/serializers.py:14
#: build/lib/activation_codes/serializers.py:14
msgid "The activation code to validate"
msgstr "De activeringscode om te valideren"
#: activation_codes/viewsets.py:107 build/lib/activation_codes/viewsets.py:107
#: activation_codes/viewsets.py:107
msgid "Your account has been successfully activated"
msgstr "Uw account is succesvol geactiveerd"
#: build/lib/chat/apps.py:12 chat/apps.py:12
#: chat/apps.py:12
msgid "chat application"
msgstr "chatapplicatie"
#: build/lib/core/admin.py:26 core/admin.py:26
#: core/admin.py:26
msgid "Personal info"
msgstr "Persoonlijke gegevens"
#: build/lib/core/admin.py:40 core/admin.py:40
#: core/admin.py:40
msgid "Permissions"
msgstr "Machtigingen"
#: build/lib/core/admin.py:52 core/admin.py:52
#: core/admin.py:52
msgid "Important dates"
msgstr "Belangrijke data"
#: build/lib/core/models.py:39 core/models.py:39
#: core/models.py:39
msgid "id"
msgstr "id"
#: build/lib/core/models.py:40 core/models.py:40
#: core/models.py:40
msgid "primary key for the record as UUID"
msgstr "primaire sleutel voor het record als UUID"
#: build/lib/core/models.py:46 core/models.py:46
#: core/models.py:46
msgid "created on"
msgstr "gemaakt op"
#: build/lib/core/models.py:47 core/models.py:47
#: core/models.py:47
msgid "date and time at which a record was created"
msgstr "datum en tijd waarop een record is aangemaakt"
#: build/lib/core/models.py:52 core/models.py:52
#: core/models.py:52
msgid "updated on"
msgstr "bijgewerkt op"
#: build/lib/core/models.py:53 core/models.py:53
#: core/models.py:53
msgid "date and time at which a record was last updated"
msgstr "datum en tijd waarop een record voor het laatst is bijgewerkt"
#: build/lib/core/models.py:86 core/models.py:86
#: core/models.py:89
msgid "We couldn't find a user with this sub but the email is already associated with a registered user."
msgstr "We konden geen gebruiker met dit e-mailadres vinden, maar het e-mailadres is al gekoppeld aan een geregistreerde gebruiker."
#: build/lib/core/models.py:99 core/models.py:99
#: core/models.py:102
msgid "Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/_/: characters."
msgstr "Voer een geldig subsubtype in. Deze waarde mag alleen letters, cijfers en @/./+/-/_/:-tekens bevatten."
#: build/lib/core/models.py:105 core/models.py:105
#: core/models.py:108
msgid "sub"
msgstr "id"
#: build/lib/core/models.py:107 core/models.py:107
#: core/models.py:110
msgid "Required. 255 characters or fewer. Letters, numbers, and @/./+/-/_/: characters only."
msgstr "Verplicht. Maximaal 255 tekens. Alleen letters, cijfers en @/./+/-/_/: tekens."
#: build/lib/core/models.py:116 core/models.py:116
#: core/models.py:119
msgid "full name"
msgstr "volledige naam"
#: build/lib/core/models.py:117 core/models.py:117
#: core/models.py:120
msgid "short name"
msgstr "korte naam"
#: build/lib/core/models.py:119 core/models.py:119
#: core/models.py:122
msgid "identity email address"
msgstr "identiteits e-mailadres"
#: build/lib/core/models.py:123 core/models.py:123
#: core/models.py:126
msgid "admin email address"
msgstr "beheerders e-mailadres"
#: build/lib/core/models.py:129 core/models.py:129
#: core/models.py:132
msgid "language"
msgstr "taal"
#: build/lib/core/models.py:130 core/models.py:130
#: core/models.py:133
msgid "The language in which the user wants to see the interface."
msgstr "De taal waarin de gebruiker de interface wil zien."
#: build/lib/core/models.py:138 core/models.py:138
#: core/models.py:141
msgid "The timezone in which the user wants to see times."
msgstr "De tijdzone waarin de gebruiker de tijden wil zien."
#: build/lib/core/models.py:141 core/models.py:141
#: core/models.py:144
msgid "device"
msgstr "apparaat"
#: build/lib/core/models.py:143 core/models.py:143
#: core/models.py:146
msgid "Whether the user is a device or a real user."
msgstr "Of de gebruiker een apparaat of een echte gebruiker is."
#: build/lib/core/models.py:146 core/models.py:146
#: core/models.py:149
msgid "staff status"
msgstr "personeelsstatus"
#: build/lib/core/models.py:148 core/models.py:148
#: core/models.py:151
msgid "Whether the user can log into this admin site."
msgstr "Of de gebruiker kan inloggen op deze beheersite."
#: build/lib/core/models.py:154 core/models.py:154
#: core/models.py:157
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
msgstr "Of deze gebruiker als actief moet worden beschouwd. Deselecteer dit in plaats van accounts te verwijderen."
#: build/lib/core/models.py:161 core/models.py:161
#: core/models.py:164
msgid "allow conversation analytics"
msgstr "conversatieanalyse toestaan"
#: build/lib/core/models.py:163 core/models.py:163
#: core/models.py:166
msgid "Whether the user allows to use their conversations for analytics."
msgstr "Of de gebruiker toestaat dat zijn/haar gesprekken voor analyses worden gebruikt."
#: build/lib/core/models.py:174 core/models.py:174
#: core/models.py:170
msgid "allow smart web search"
msgstr "smart web zoeken toestaan"
#: core/models.py:172
msgid "Whether the user allows to use smart web search features."
msgstr "Of de gebruiker toestemming geeft om smart web zoekfuncties te gebruiken."
#: core/models.py:183
msgid "users"
msgstr "gebruikers"
+80 -88
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: la-suite-conversations\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-10-20 21:48+0000\n"
"PO-Revision-Date: 2026-02-09 13:05\n"
"POT-Creation-Date: 2026-02-26 21:42+0000\n"
"PO-Revision-Date: 2026-03-11 15:23\n"
"Last-Translator: \n"
"Language-Team: Russian\n"
"Language: ru_RU\n"
@@ -17,328 +17,320 @@ msgstr ""
"X-Crowdin-File: backend-conversations.pot\n"
"X-Crowdin-File-ID: 26\n"
#: activation_codes/admin.py:55 build/lib/activation_codes/admin.py:55
#: activation_codes/admin.py:55
msgid "Configuration"
msgstr "Настройки"
#: activation_codes/admin.py:66 build/lib/activation_codes/admin.py:66
#: activation_codes/admin.py:66
msgid "Usage details"
msgstr "Сведения об использовании"
#: activation_codes/admin.py:70 activation_codes/admin.py:226
#: build/lib/activation_codes/admin.py:70
#: build/lib/activation_codes/admin.py:226
msgid "Timestamps"
msgstr "Временные метки"
#: activation_codes/admin.py:109 build/lib/activation_codes/admin.py:109
#: activation_codes/admin.py:109
msgid "Usage"
msgstr "Использование"
#: activation_codes/admin.py:117 build/lib/activation_codes/admin.py:117
#: activation_codes/admin.py:117
msgid "Description"
msgstr "Описание"
#: activation_codes/admin.py:124 build/lib/activation_codes/admin.py:124
#: activation_codes/admin.py:124
msgid "No users have used this code yet"
msgstr "Пока нет пользователей, использовавших этот код"
#: activation_codes/admin.py:135 build/lib/activation_codes/admin.py:135
#: activation_codes/admin.py:135
msgid "Name"
msgstr "Имя"
#: activation_codes/admin.py:136 activation_codes/admin.py:246
#: build/lib/activation_codes/admin.py:136
#: build/lib/activation_codes/admin.py:246
msgid "Email"
msgstr "Эл. почта"
#: activation_codes/admin.py:137 build/lib/activation_codes/admin.py:137
#: activation_codes/admin.py:137
msgid "Date"
msgstr "Дата"
#: activation_codes/admin.py:161 build/lib/activation_codes/admin.py:161
#: activation_codes/admin.py:161
msgid "Users who used this code"
msgstr "Пользователи, использующие этот код"
#: activation_codes/admin.py:163 build/lib/activation_codes/admin.py:163
#: activation_codes/admin.py:163
msgid "Recompute current uses from related activations"
msgstr "Обновить данные использования связанных активаций"
#: activation_codes/admin.py:177 build/lib/activation_codes/admin.py:177
#: activation_codes/admin.py:177
msgid "All selected activation codes already have correct usage counts."
msgstr "Все выбранные коды активации уже имеют правильное количество использований."
#: activation_codes/admin.py:182 build/lib/activation_codes/admin.py:182
#: activation_codes/admin.py:182
#, python-format
msgid "Successfully recomputed usage counts for %(count)d activation code(s)."
msgstr "Количество использованных кодов активации (%(count)d) успешно пересчитано."
#: activation_codes/admin.py:240 activation_codes/admin.py:284
#: build/lib/activation_codes/admin.py:240
#: build/lib/activation_codes/admin.py:284
msgid "User"
msgstr "Пользователь"
#: activation_codes/admin.py:291 build/lib/activation_codes/admin.py:291
#: activation_codes/admin.py:291
msgid "Has used activation code"
msgstr "Использует код активации"
#: activation_codes/admin.py:293 build/lib/activation_codes/admin.py:293
#: activation_codes/admin.py:293
msgid "Add selected users to Brevo waiting list"
msgstr "Добавить выбранных пользователей в список ожидания Brevo"
#: activation_codes/admin.py:314 build/lib/activation_codes/admin.py:314
#: activation_codes/admin.py:314
#, python-format
msgid "Added %(count)d user(s) to Brevo waiting list."
msgstr "В список ожидания Brevo добавлено пользователей: %(count)d."
#: activation_codes/admin.py:319 activation_codes/admin.py:347
#: build/lib/activation_codes/admin.py:319
#: build/lib/activation_codes/admin.py:347
msgid "No valid email address found in selected registrations."
msgstr "В выбранных регистрациях не найден действительный адрес электронной почты."
#: activation_codes/admin.py:323 build/lib/activation_codes/admin.py:323
#: activation_codes/admin.py:323
msgid "Remove selected users from Brevo waiting list"
msgstr "Удалить выбранных пользователей из списка ожидания Brevo"
#: activation_codes/admin.py:342 build/lib/activation_codes/admin.py:342
#: activation_codes/admin.py:342
#, python-format
msgid "Removed %(count)d user(s) from Brevo waiting list."
msgstr "Из списка ожидания Brevo удалено пользователей: %(count)d."
#: activation_codes/models.py:38 activation_codes/models.py:85
#: activation_codes/models.py:178 build/lib/activation_codes/models.py:38
#: build/lib/activation_codes/models.py:85
#: build/lib/activation_codes/models.py:178
#: activation_codes/models.py:178
msgid "activation code"
msgstr "код активации"
#: activation_codes/models.py:39 build/lib/activation_codes/models.py:39
#: activation_codes/models.py:39
msgid "The activation code that users will enter"
msgstr "Код активации, который будут вводить пользователи"
#: activation_codes/models.py:46 build/lib/activation_codes/models.py:46
#: activation_codes/models.py:46
msgid "Code must be alphanumeric and contain no spaces or special characters"
msgstr "Код должен быть буквенно-цифровым и не содержать пробелов или специальных символов"
#: activation_codes/models.py:52 build/lib/activation_codes/models.py:52
#: activation_codes/models.py:52
msgid "maximum uses"
msgstr "максимум использований"
#: activation_codes/models.py:53 build/lib/activation_codes/models.py:53
#: activation_codes/models.py:53
msgid "Maximum number of times this code can be used. 0 means unlimited."
msgstr "Сколько раз можно использовать этот код. 0 означает неограниченно."
#: activation_codes/models.py:58 build/lib/activation_codes/models.py:58
#: activation_codes/models.py:58
msgid "current uses"
msgstr "использовано"
#: activation_codes/models.py:59 build/lib/activation_codes/models.py:59
#: activation_codes/models.py:59
msgid "Number of times this code has been used"
msgstr "Сколько раз этот код был использован"
#: activation_codes/models.py:65 build/lib/activation_codes/models.py:65
#: build/lib/core/models.py:151 core/models.py:151
#: activation_codes/models.py:65 core/models.py:154
msgid "active"
msgstr "активный"
#: activation_codes/models.py:66 build/lib/activation_codes/models.py:66
#: activation_codes/models.py:66
msgid "Whether this code can still be used"
msgstr "Можно ли ещё использовать этот код"
#: activation_codes/models.py:71 build/lib/activation_codes/models.py:71
#: activation_codes/models.py:71
msgid "expires at"
msgstr "действителен до"
#: activation_codes/models.py:72 build/lib/activation_codes/models.py:72
#: activation_codes/models.py:72
msgid "Date and time when this code expires"
msgstr "Дата и время окончания действия этого кода"
#: activation_codes/models.py:78 build/lib/activation_codes/models.py:78
#: activation_codes/models.py:78
msgid "description"
msgstr "описание"
#: activation_codes/models.py:79 build/lib/activation_codes/models.py:79
#: activation_codes/models.py:79
msgid "Internal description or notes about this code"
msgstr "Внутреннее описание или примечания об этом коде"
#: activation_codes/models.py:86 build/lib/activation_codes/models.py:86
#: activation_codes/models.py:86
msgid "activation codes"
msgstr "коды активации"
#: activation_codes/models.py:128 build/lib/activation_codes/models.py:128
#: activation_codes/models.py:128
msgid "This activation code is no longer valid"
msgstr "Этот код активации больше не действителен"
#: activation_codes/models.py:136 build/lib/activation_codes/models.py:136
#: activation_codes/models.py:136
msgid "You have already activated your account"
msgstr "Вы уже активировали свою учётную запись"
#: activation_codes/models.py:170 activation_codes/models.py:202
#: build/lib/activation_codes/models.py:170
#: build/lib/activation_codes/models.py:202 build/lib/core/models.py:173
#: core/models.py:173
#: core/models.py:182
msgid "user"
msgstr "пользователь"
#: activation_codes/models.py:171 build/lib/activation_codes/models.py:171
#: activation_codes/models.py:171
msgid "The user who used the activation code"
msgstr "Пользователь, использовавший код активации"
#: activation_codes/models.py:179 build/lib/activation_codes/models.py:179
#: activation_codes/models.py:179
msgid "The activation code that was used"
msgstr "Использованный код активации"
#: activation_codes/models.py:186 activation_codes/models.py:210
#: build/lib/activation_codes/models.py:186
#: build/lib/activation_codes/models.py:210
msgid "user activation"
msgstr "активация пользователя"
#: activation_codes/models.py:187 build/lib/activation_codes/models.py:187
#: activation_codes/models.py:187
msgid "user activations"
msgstr "активации пользователя"
#: activation_codes/models.py:203 build/lib/activation_codes/models.py:203
#: activation_codes/models.py:203
msgid "The user who made the registration request"
msgstr "Пользователь, который сделал запрос на регистрацию"
#: activation_codes/models.py:211 build/lib/activation_codes/models.py:211
#: activation_codes/models.py:211
msgid "Store if the user received an activation code and used it"
msgstr "Сохранить, если пользователь получил код активации и использовал его"
#: activation_codes/models.py:220 build/lib/activation_codes/models.py:220
#: activation_codes/models.py:220
msgid "user registration request"
msgstr "запрос на регистрацию пользователя"
#: activation_codes/models.py:221 build/lib/activation_codes/models.py:221
#: activation_codes/models.py:221
msgid "user registration requests"
msgstr "запросы на регистрацию пользователя"
#: activation_codes/serializers.py:14
#: build/lib/activation_codes/serializers.py:14
msgid "The activation code to validate"
msgstr "Код активации для проверки"
#: activation_codes/viewsets.py:107 build/lib/activation_codes/viewsets.py:107
#: activation_codes/viewsets.py:107
msgid "Your account has been successfully activated"
msgstr "Ваша учётная запись успешно активирована"
#: build/lib/chat/apps.py:12 chat/apps.py:12
#: chat/apps.py:12
msgid "chat application"
msgstr "приложение чата"
#: build/lib/core/admin.py:26 core/admin.py:26
#: core/admin.py:26
msgid "Personal info"
msgstr "Личные данные"
#: build/lib/core/admin.py:40 core/admin.py:40
#: core/admin.py:40
msgid "Permissions"
msgstr "Разрешения"
#: build/lib/core/admin.py:52 core/admin.py:52
#: core/admin.py:52
msgid "Important dates"
msgstr "Важные даты"
#: build/lib/core/models.py:39 core/models.py:39
#: core/models.py:39
msgid "id"
msgstr "id"
#: build/lib/core/models.py:40 core/models.py:40
#: core/models.py:40
msgid "primary key for the record as UUID"
msgstr "первичный ключ для записи как UUID"
#: build/lib/core/models.py:46 core/models.py:46
#: core/models.py:46
msgid "created on"
msgstr "создано"
#: build/lib/core/models.py:47 core/models.py:47
#: core/models.py:47
msgid "date and time at which a record was created"
msgstr "дата и время создания записи"
#: build/lib/core/models.py:52 core/models.py:52
#: core/models.py:52
msgid "updated on"
msgstr "обновлено"
#: build/lib/core/models.py:53 core/models.py:53
#: core/models.py:53
msgid "date and time at which a record was last updated"
msgstr "дата и время последнего обновления записи"
#: build/lib/core/models.py:86 core/models.py:86
#: core/models.py:89
msgid "We couldn't find a user with this sub but the email is already associated with a registered user."
msgstr "Мы не смогли найти пользователя с этими данными, но этот адрес уже связан с зарегистрированным пользователем."
#: build/lib/core/models.py:99 core/models.py:99
#: core/models.py:102
msgid "Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/_/: characters."
msgstr "Введите правильный префикс. Он может содержать только буквы, цифры и символы @/./+/-/_/."
#: build/lib/core/models.py:105 core/models.py:105
#: core/models.py:108
msgid "sub"
msgstr "префикс"
#: build/lib/core/models.py:107 core/models.py:107
#: core/models.py:110
msgid "Required. 255 characters or fewer. Letters, numbers, and @/./+/-/_/: characters only."
msgstr "Обязательно. 255 символов или меньше. Только буквы, цифры и @/./+/-/_/: /."
#: build/lib/core/models.py:116 core/models.py:116
#: core/models.py:119
msgid "full name"
msgstr "полное имя"
#: build/lib/core/models.py:117 core/models.py:117
#: core/models.py:120
msgid "short name"
msgstr "короткое имя"
#: build/lib/core/models.py:119 core/models.py:119
#: core/models.py:122
msgid "identity email address"
msgstr "личный адрес электронной почты"
#: build/lib/core/models.py:123 core/models.py:123
#: core/models.py:126
msgid "admin email address"
msgstr "e-mail администратора"
#: build/lib/core/models.py:129 core/models.py:129
#: core/models.py:132
msgid "language"
msgstr "язык"
#: build/lib/core/models.py:130 core/models.py:130
#: core/models.py:133
msgid "The language in which the user wants to see the interface."
msgstr "Язык, на котором пользователь хочет видеть интерфейс."
#: build/lib/core/models.py:138 core/models.py:138
#: core/models.py:141
msgid "The timezone in which the user wants to see times."
msgstr "Часовой пояс, в котором пользователь хочет видеть время."
#: build/lib/core/models.py:141 core/models.py:141
#: core/models.py:144
msgid "device"
msgstr "устройство"
#: build/lib/core/models.py:143 core/models.py:143
#: core/models.py:146
msgid "Whether the user is a device or a real user."
msgstr "Пользователь является устройством или человеком."
#: build/lib/core/models.py:146 core/models.py:146
#: core/models.py:149
msgid "staff status"
msgstr "статус сотрудника"
#: build/lib/core/models.py:148 core/models.py:148
#: core/models.py:151
msgid "Whether the user can log into this admin site."
msgstr "Может ли пользователь войти на этот административный сайт."
#: build/lib/core/models.py:154 core/models.py:154
#: core/models.py:157
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
msgstr "Должен ли пользователь рассматриваться как активный. Альтернатива удалению учётных записей."
#: build/lib/core/models.py:161 core/models.py:161
#: core/models.py:164
msgid "allow conversation analytics"
msgstr "разрешить аналитику для беседы"
#: build/lib/core/models.py:163 core/models.py:163
#: core/models.py:166
msgid "Whether the user allows to use their conversations for analytics."
msgstr "Разрешает ли пользователь использовать свои беседы для аналитики."
#: build/lib/core/models.py:174 core/models.py:174
#: core/models.py:170
msgid "allow smart web search"
msgstr "разрешить умный поиск в Интернете"
#: core/models.py:172
msgid "Whether the user allows to use smart web search features."
msgstr "Разрешает ли пользователь использовать умный поиск в Интернете."
#: core/models.py:183
msgid "users"
msgstr "пользователи"
+80 -88
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: la-suite-conversations\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-10-20 21:48+0000\n"
"PO-Revision-Date: 2026-02-09 13:05\n"
"POT-Creation-Date: 2026-02-26 21:42+0000\n"
"PO-Revision-Date: 2026-03-11 15:23\n"
"Last-Translator: \n"
"Language-Team: Ukrainian\n"
"Language: uk_UA\n"
@@ -17,328 +17,320 @@ msgstr ""
"X-Crowdin-File: backend-conversations.pot\n"
"X-Crowdin-File-ID: 26\n"
#: activation_codes/admin.py:55 build/lib/activation_codes/admin.py:55
#: activation_codes/admin.py:55
msgid "Configuration"
msgstr "Налаштування"
#: activation_codes/admin.py:66 build/lib/activation_codes/admin.py:66
#: activation_codes/admin.py:66
msgid "Usage details"
msgstr "Відомості про використання"
#: activation_codes/admin.py:70 activation_codes/admin.py:226
#: build/lib/activation_codes/admin.py:70
#: build/lib/activation_codes/admin.py:226
msgid "Timestamps"
msgstr "Відмітки часу"
#: activation_codes/admin.py:109 build/lib/activation_codes/admin.py:109
#: activation_codes/admin.py:109
msgid "Usage"
msgstr "Використання"
#: activation_codes/admin.py:117 build/lib/activation_codes/admin.py:117
#: activation_codes/admin.py:117
msgid "Description"
msgstr "Опис"
#: activation_codes/admin.py:124 build/lib/activation_codes/admin.py:124
#: activation_codes/admin.py:124
msgid "No users have used this code yet"
msgstr "Користувачі ще не використовували цей код"
#: activation_codes/admin.py:135 build/lib/activation_codes/admin.py:135
#: activation_codes/admin.py:135
msgid "Name"
msgstr "Ім’я"
#: activation_codes/admin.py:136 activation_codes/admin.py:246
#: build/lib/activation_codes/admin.py:136
#: build/lib/activation_codes/admin.py:246
msgid "Email"
msgstr "Ел. пошта"
#: activation_codes/admin.py:137 build/lib/activation_codes/admin.py:137
#: activation_codes/admin.py:137
msgid "Date"
msgstr "Дата"
#: activation_codes/admin.py:161 build/lib/activation_codes/admin.py:161
#: activation_codes/admin.py:161
msgid "Users who used this code"
msgstr "Користувачі, що використовували цей код"
#: activation_codes/admin.py:163 build/lib/activation_codes/admin.py:163
#: activation_codes/admin.py:163
msgid "Recompute current uses from related activations"
msgstr "Перерахувати поточні використання пов'язаних ресурсів"
#: activation_codes/admin.py:177 build/lib/activation_codes/admin.py:177
#: activation_codes/admin.py:177
msgid "All selected activation codes already have correct usage counts."
msgstr "Усі обрані коди активації вже мають коректні лічильники використання."
#: activation_codes/admin.py:182 build/lib/activation_codes/admin.py:182
#: activation_codes/admin.py:182
#, python-format
msgid "Successfully recomputed usage counts for %(count)d activation code(s)."
msgstr "Успішно переобчислено використання коду активації %(count)d."
#: activation_codes/admin.py:240 activation_codes/admin.py:284
#: build/lib/activation_codes/admin.py:240
#: build/lib/activation_codes/admin.py:284
msgid "User"
msgstr "Користувач"
#: activation_codes/admin.py:291 build/lib/activation_codes/admin.py:291
#: activation_codes/admin.py:291
msgid "Has used activation code"
msgstr "Використано код активації"
#: activation_codes/admin.py:293 build/lib/activation_codes/admin.py:293
#: activation_codes/admin.py:293
msgid "Add selected users to Brevo waiting list"
msgstr "Додати обраних користувачів до списку очікування Brevo"
#: activation_codes/admin.py:314 build/lib/activation_codes/admin.py:314
#: activation_codes/admin.py:314
#, python-format
msgid "Added %(count)d user(s) to Brevo waiting list."
msgstr "До списку очікування Brevo додано користувачів: %(count)d."
#: activation_codes/admin.py:319 activation_codes/admin.py:347
#: build/lib/activation_codes/admin.py:319
#: build/lib/activation_codes/admin.py:347
msgid "No valid email address found in selected registrations."
msgstr "Серед обраних реєстрацій не знайдено дійсної адреси електронної пошти."
#: activation_codes/admin.py:323 build/lib/activation_codes/admin.py:323
#: activation_codes/admin.py:323
msgid "Remove selected users from Brevo waiting list"
msgstr "Видалити обраних користувачів зі списку очікування Brevo"
#: activation_codes/admin.py:342 build/lib/activation_codes/admin.py:342
#: activation_codes/admin.py:342
#, python-format
msgid "Removed %(count)d user(s) from Brevo waiting list."
msgstr "Зі списку очікування Brevo видалено користувачів: %(count)d"
#: activation_codes/models.py:38 activation_codes/models.py:85
#: activation_codes/models.py:178 build/lib/activation_codes/models.py:38
#: build/lib/activation_codes/models.py:85
#: build/lib/activation_codes/models.py:178
#: activation_codes/models.py:178
msgid "activation code"
msgstr "код активації"
#: activation_codes/models.py:39 build/lib/activation_codes/models.py:39
#: activation_codes/models.py:39
msgid "The activation code that users will enter"
msgstr "Код активації, що буде введений користувачами"
#: activation_codes/models.py:46 build/lib/activation_codes/models.py:46
#: activation_codes/models.py:46
msgid "Code must be alphanumeric and contain no spaces or special characters"
msgstr "Код має бути буквено-цифровим, без пробілів або спеціальних символів"
#: activation_codes/models.py:52 build/lib/activation_codes/models.py:52
#: activation_codes/models.py:52
msgid "maximum uses"
msgstr "максимум використань"
#: activation_codes/models.py:53 build/lib/activation_codes/models.py:53
#: activation_codes/models.py:53
msgid "Maximum number of times this code can be used. 0 means unlimited."
msgstr "Максимальна кількість разів використання для цього коду. 0 - необмежена."
#: activation_codes/models.py:58 build/lib/activation_codes/models.py:58
#: activation_codes/models.py:58
msgid "current uses"
msgstr "використано"
#: activation_codes/models.py:59 build/lib/activation_codes/models.py:59
#: activation_codes/models.py:59
msgid "Number of times this code has been used"
msgstr "Кількість разів використання цього коду"
#: activation_codes/models.py:65 build/lib/activation_codes/models.py:65
#: build/lib/core/models.py:151 core/models.py:151
#: activation_codes/models.py:65 core/models.py:154
msgid "active"
msgstr "активний"
#: activation_codes/models.py:66 build/lib/activation_codes/models.py:66
#: activation_codes/models.py:66
msgid "Whether this code can still be used"
msgstr "Чи цей код все ще може бути використаний"
#: activation_codes/models.py:71 build/lib/activation_codes/models.py:71
#: activation_codes/models.py:71
msgid "expires at"
msgstr "дійсний до"
#: activation_codes/models.py:72 build/lib/activation_codes/models.py:72
#: activation_codes/models.py:72
msgid "Date and time when this code expires"
msgstr "Дата та час, коли закінчується дія цього коду"
#: activation_codes/models.py:78 build/lib/activation_codes/models.py:78
#: activation_codes/models.py:78
msgid "description"
msgstr "опис"
#: activation_codes/models.py:79 build/lib/activation_codes/models.py:79
#: activation_codes/models.py:79
msgid "Internal description or notes about this code"
msgstr "Внутрішній опис або нотатки про цей код"
#: activation_codes/models.py:86 build/lib/activation_codes/models.py:86
#: activation_codes/models.py:86
msgid "activation codes"
msgstr "коди активації"
#: activation_codes/models.py:128 build/lib/activation_codes/models.py:128
#: activation_codes/models.py:128
msgid "This activation code is no longer valid"
msgstr "Цей код активації вже не дійсний"
#: activation_codes/models.py:136 build/lib/activation_codes/models.py:136
#: activation_codes/models.py:136
msgid "You have already activated your account"
msgstr "Ви вже активували свій обліковий запис"
#: activation_codes/models.py:170 activation_codes/models.py:202
#: build/lib/activation_codes/models.py:170
#: build/lib/activation_codes/models.py:202 build/lib/core/models.py:173
#: core/models.py:173
#: core/models.py:182
msgid "user"
msgstr "користувач"
#: activation_codes/models.py:171 build/lib/activation_codes/models.py:171
#: activation_codes/models.py:171
msgid "The user who used the activation code"
msgstr "Користувач, який користувався кодом активації"
#: activation_codes/models.py:179 build/lib/activation_codes/models.py:179
#: activation_codes/models.py:179
msgid "The activation code that was used"
msgstr "Використаний код активації"
#: activation_codes/models.py:186 activation_codes/models.py:210
#: build/lib/activation_codes/models.py:186
#: build/lib/activation_codes/models.py:210
msgid "user activation"
msgstr "активація користувача"
#: activation_codes/models.py:187 build/lib/activation_codes/models.py:187
#: activation_codes/models.py:187
msgid "user activations"
msgstr "активації користувача"
#: activation_codes/models.py:203 build/lib/activation_codes/models.py:203
#: activation_codes/models.py:203
msgid "The user who made the registration request"
msgstr "Користувач, що зробив запит на реєстрацію"
#: activation_codes/models.py:211 build/lib/activation_codes/models.py:211
#: activation_codes/models.py:211
msgid "Store if the user received an activation code and used it"
msgstr "Зберегти, якщо користувач отримав код активації та використав його"
#: activation_codes/models.py:220 build/lib/activation_codes/models.py:220
#: activation_codes/models.py:220
msgid "user registration request"
msgstr "запит на реєстрацію користувача"
#: activation_codes/models.py:221 build/lib/activation_codes/models.py:221
#: activation_codes/models.py:221
msgid "user registration requests"
msgstr "запити на реєстрацію користувачів"
#: activation_codes/serializers.py:14
#: build/lib/activation_codes/serializers.py:14
msgid "The activation code to validate"
msgstr "Код активації для перевірки"
#: activation_codes/viewsets.py:107 build/lib/activation_codes/viewsets.py:107
#: activation_codes/viewsets.py:107
msgid "Your account has been successfully activated"
msgstr "Ваш обліковий запис успішно активовано"
#: build/lib/chat/apps.py:12 chat/apps.py:12
#: chat/apps.py:12
msgid "chat application"
msgstr "чат-застосунок"
#: build/lib/core/admin.py:26 core/admin.py:26
#: core/admin.py:26
msgid "Personal info"
msgstr "Особисті дані"
#: build/lib/core/admin.py:40 core/admin.py:40
#: core/admin.py:40
msgid "Permissions"
msgstr "Дозволи"
#: build/lib/core/admin.py:52 core/admin.py:52
#: core/admin.py:52
msgid "Important dates"
msgstr "Важливі дати"
#: build/lib/core/models.py:39 core/models.py:39
#: core/models.py:39
msgid "id"
msgstr "id"
#: build/lib/core/models.py:40 core/models.py:40
#: core/models.py:40
msgid "primary key for the record as UUID"
msgstr "первинний ключ для запису як UUID"
#: build/lib/core/models.py:46 core/models.py:46
#: core/models.py:46
msgid "created on"
msgstr "створено"
#: build/lib/core/models.py:47 core/models.py:47
#: core/models.py:47
msgid "date and time at which a record was created"
msgstr "дата і час, коли запис було створено"
#: build/lib/core/models.py:52 core/models.py:52
#: core/models.py:52
msgid "updated on"
msgstr "оновлено"
#: build/lib/core/models.py:53 core/models.py:53
#: core/models.py:53
msgid "date and time at which a record was last updated"
msgstr "дата і час, коли запис був востаннє оновлений"
#: build/lib/core/models.py:86 core/models.py:86
#: core/models.py:89
msgid "We couldn't find a user with this sub but the email is already associated with a registered user."
msgstr "Ми не змогли знайти користувача з цими даними, але адреса вже пов'язана з зареєстрованим користувачем."
#: build/lib/core/models.py:99 core/models.py:99
#: core/models.py:102
msgid "Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/_/: characters."
msgstr "Введіть правильний префікс. Це значення може містити лише літери, цифри та символи @/./+/-/_/."
#: build/lib/core/models.py:105 core/models.py:105
#: core/models.py:108
msgid "sub"
msgstr "префікс"
#: build/lib/core/models.py:107 core/models.py:107
#: core/models.py:110
msgid "Required. 255 characters or fewer. Letters, numbers, and @/./+/-/_/: characters only."
msgstr "Обов'язково. 255 символів або менше. Лише літери, цифри та символи @/./+/-/_/."
#: build/lib/core/models.py:116 core/models.py:116
#: core/models.py:119
msgid "full name"
msgstr "повне ім'я"
#: build/lib/core/models.py:117 core/models.py:117
#: core/models.py:120
msgid "short name"
msgstr "коротке ім'я"
#: build/lib/core/models.py:119 core/models.py:119
#: core/models.py:122
msgid "identity email address"
msgstr "адреса електронної пошти особи"
#: build/lib/core/models.py:123 core/models.py:123
#: core/models.py:126
msgid "admin email address"
msgstr "електронна адреса адміністратора"
#: build/lib/core/models.py:129 core/models.py:129
#: core/models.py:132
msgid "language"
msgstr "мова"
#: build/lib/core/models.py:130 core/models.py:130
#: core/models.py:133
msgid "The language in which the user wants to see the interface."
msgstr "Мова, якою користувач хоче бачити інтерфейс."
#: build/lib/core/models.py:138 core/models.py:138
#: core/models.py:141
msgid "The timezone in which the user wants to see times."
msgstr "Часовий пояс, в якому користувач хоче бачити час."
#: build/lib/core/models.py:141 core/models.py:141
#: core/models.py:144
msgid "device"
msgstr "пристрій"
#: build/lib/core/models.py:143 core/models.py:143
#: core/models.py:146
msgid "Whether the user is a device or a real user."
msgstr "Чи є користувач пристроєм чи реальним користувачем."
#: build/lib/core/models.py:146 core/models.py:146
#: core/models.py:149
msgid "staff status"
msgstr "статус співробітника"
#: build/lib/core/models.py:148 core/models.py:148
#: core/models.py:151
msgid "Whether the user can log into this admin site."
msgstr "Чи може користувач увійти на цей сайт адміністратора."
#: build/lib/core/models.py:154 core/models.py:154
#: core/models.py:157
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
msgstr "Чи слід ставитися до цього користувача як до активного. Зніміть вибір замість видалення облікового запису."
#: build/lib/core/models.py:161 core/models.py:161
#: core/models.py:164
msgid "allow conversation analytics"
msgstr "дозволити аналітику бесіди"
#: build/lib/core/models.py:163 core/models.py:163
#: core/models.py:166
msgid "Whether the user allows to use their conversations for analytics."
msgstr "Чи дозволяє користувач використовувати свої розмови для аналітики."
#: build/lib/core/models.py:174 core/models.py:174
#: core/models.py:170
msgid "allow smart web search"
msgstr "дозволити розумний пошук в Інтернеті"
#: core/models.py:172
msgid "Whether the user allows to use smart web search features."
msgstr "Чи дозволяє користувач використовувати розумні пошукові функції, пов'язані з Інтернетом."
#: core/models.py:183
msgid "users"
msgstr "користувачі"
+17 -7
View File
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "conversations"
version = "0.0.13"
version = "0.0.14"
authors = [{ "name" = "DINUM", "email" = "dev@mail.numerique.gouv.fr" }]
classifiers = [
"Development Status :: 5 - Production/Stable",
@@ -35,11 +35,11 @@ dependencies = [
"django-filter==25.2",
"django-lasuite[all]==0.0.18",
"django-parler==2.3",
"django-pydantic-field==0.4.0",
"django-pydantic-field==0.5.4",
"django-redis==6.0.0",
"django-storages[s3]==1.14.6",
"django-timezone-field>=5.1",
"django==5.2.11",
"django==5.2.12",
"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.62.0",
"psycopg[binary]==3.2.12",
"PyJWT==2.10.1",
"python-magic==0.4.27",
@@ -67,7 +67,7 @@ dependencies = [
"trafilatura==2.0.0",
"uvicorn==0.38.0",
"whitenoise==6.11.0",
"pypdf>=6.6.2",
"pypdf==6.7.5",
]
[project.urls]
@@ -108,6 +108,16 @@ zip-safe = true
[tool.distutils.bdist_wheel]
universal = true
[tool.uv]
required-environments = [
"sys_platform == 'linux' and platform_machine == 'x86_64'",
"sys_platform == 'darwin'",
]
override-dependencies = [
"cryptography>=46.0.5", # CVE-2026-26007
"pillow>=12.1.1", #CVE-2026-25990
]
[tool.uv.build-backend]
module-root = ""
source-exclude = [
@@ -147,8 +157,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]
+132 -102
View File
@@ -3,8 +3,17 @@ revision = 3
requires-python = "==3.13.*"
resolution-markers = [
"sys_platform == 'win32'",
"sys_platform == 'emscripten'",
"sys_platform != 'emscripten' and sys_platform != 'win32'",
"sys_platform != 'win32'",
]
required-markers = [
"platform_machine == 'x86_64' and sys_platform == 'linux'",
"sys_platform == 'darwin'",
]
[manifest]
overrides = [
{ name = "cryptography", specifier = ">=46.0.5" },
{ name = "pillow", specifier = ">=12.1.1" },
]
[[package]]
@@ -132,20 +141,20 @@ wheels = [
[[package]]
name = "azure-core"
version = "1.38.0"
version = "1.38.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "requests" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/dc/1b/e503e08e755ea94e7d3419c9242315f888fc664211c90d032e40479022bf/azure_core-1.38.0.tar.gz", hash = "sha256:8194d2682245a3e4e3151a667c686464c3786fed7918b394d035bdcd61bb5993", size = 363033, upload-time = "2026-01-12T17:03:05.535Z" }
sdist = { url = "https://files.pythonhosted.org/packages/53/9b/23893febea484ad8183112c9419b5eb904773adb871492b5fa8ff7b21e09/azure_core-1.38.1.tar.gz", hash = "sha256:9317db1d838e39877eb94a2240ce92fa607db68adf821817b723f0d679facbf6", size = 363323, upload-time = "2026-02-11T02:03:06.051Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/fc/d8/b8fcba9464f02b121f39de2db2bf57f0b216fe11d014513d666e8634380d/azure_core-1.38.0-py3-none-any.whl", hash = "sha256:ab0c9b2cd71fecb1842d52c965c95285d3cfb38902f6766e4a471f1cd8905335", size = 217825, upload-time = "2026-01-12T17:03:07.291Z" },
{ url = "https://files.pythonhosted.org/packages/db/88/aaea2ad269ce70b446660371286272c1f6ba66541a7f6f635baf8b0db726/azure_core-1.38.1-py3-none-any.whl", hash = "sha256:69f08ee3d55136071b7100de5b198994fc1c5f89d2b91f2f43156d20fcf200a4", size = 217930, upload-time = "2026-02-11T02:03:07.548Z" },
]
[[package]]
name = "azure-identity"
version = "1.25.1"
version = "1.25.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "azure-core" },
@@ -154,9 +163,9 @@ dependencies = [
{ name = "msal-extensions" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/06/8d/1a6c41c28a37eab26dc85ab6c86992c700cd3f4a597d9ed174b0e9c69489/azure_identity-1.25.1.tar.gz", hash = "sha256:87ca8328883de6036443e1c37b40e8dc8fb74898240f61071e09d2e369361456", size = 279826, upload-time = "2025-10-06T20:30:02.194Z" }
sdist = { url = "https://files.pythonhosted.org/packages/c2/3a/439a32a5e23e45f6a91f0405949dc66cfe6834aba15a430aebfc063a81e7/azure_identity-1.25.2.tar.gz", hash = "sha256:030dbaa720266c796221c6cdbd1999b408c079032c919fef725fcc348a540fe9", size = 284709, upload-time = "2026-02-11T01:55:42.323Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/83/7b/5652771e24fff12da9dde4c20ecf4682e606b104f26419d139758cc935a6/azure_identity-1.25.1-py3-none-any.whl", hash = "sha256:e9edd720af03dff020223cd269fa3a61e8f345ea75443858273bcb44844ab651", size = 191317, upload-time = "2025-10-06T20:30:04.251Z" },
{ url = "https://files.pythonhosted.org/packages/9b/77/f658c76f9e9a52c784bd836aaca6fd5b9aae176f1f53273e758a2bcda695/azure_identity-1.25.2-py3-none-any.whl", hash = "sha256:1b40060553d01a72ba0d708b9a46d0f61f56312e215d8896d836653ffdc6753d", size = 191423, upload-time = "2026-02-11T01:55:44.245Z" },
]
[[package]]
@@ -391,7 +400,7 @@ wheels = [
[[package]]
name = "conversations"
version = "0.0.13"
version = "0.0.14"
source = { editable = "." }
dependencies = [
{ name = "beautifulsoup4" },
@@ -471,7 +480,7 @@ requires-dist = [
{ name = "brotli", specifier = "==1.2.0" },
{ name = "deprecated" },
{ name = "dirty-equals", marker = "extra == 'dev'", specifier = "==0.10.0" },
{ name = "django", specifier = "==5.2.11" },
{ name = "django", specifier = "==5.2.12" },
{ name = "django-configurations", specifier = "==2.5.1" },
{ name = "django-cors-headers", specifier = "==4.9.0" },
{ name = "django-countries", specifier = "==8.1.0" },
@@ -479,7 +488,7 @@ requires-dist = [
{ name = "django-filter", specifier = "==25.2" },
{ name = "django-lasuite", extras = ["all"], specifier = "==0.0.18" },
{ name = "django-parler", specifier = "==2.3" },
{ name = "django-pydantic-field", specifier = "==0.4.0" },
{ name = "django-pydantic-field", specifier = "==0.5.4" },
{ name = "django-redis", specifier = "==6.0.0" },
{ name = "django-storages", extras = ["s3"], specifier = "==1.14.6" },
{ name = "django-test-migrations", marker = "extra == 'dev'", specifier = "==1.5.0" },
@@ -505,13 +514,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.62.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 = "pypdf", specifier = "==6.7.5" },
{ 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" },
@@ -584,43 +593,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]]
@@ -697,16 +704,16 @@ wheels = [
[[package]]
name = "django"
version = "5.2.11"
version = "5.2.12"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "asgiref" },
{ name = "sqlparse" },
{ name = "tzdata", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/17/f2/3e57ef696b95067e05ae206171e47a8e53b9c84eec56198671ef9eaa51a6/django-5.2.11.tar.gz", hash = "sha256:7f2d292ad8b9ee35e405d965fbbad293758b858c34bbf7f3df551aeeac6f02d3", size = 10885017, upload-time = "2026-02-03T13:52:50.554Z" }
sdist = { url = "https://files.pythonhosted.org/packages/bd/55/b9445fc0695b03746f355c05b2eecc54c34e05198c686f4fc4406b722b52/django-5.2.12.tar.gz", hash = "sha256:6b809af7165c73eff5ce1c87fdae75d4da6520d6667f86401ecf55b681eb1eeb", size = 10860574, upload-time = "2026-03-03T13:56:05.509Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/91/a7/2b112ab430575bf3135b8304ac372248500d99c352f777485f53fdb9537e/django-5.2.11-py3-none-any.whl", hash = "sha256:e7130df33ada9ab5e5e929bc19346a20fe383f5454acb2cc004508f242ee92c0", size = 8291375, upload-time = "2026-02-03T13:52:42.47Z" },
{ url = "https://files.pythonhosted.org/packages/4e/32/4b144e125678efccf5d5b61581de1c4088d6b0286e46096e3b8de0d556c8/django-5.2.12-py3-none-any.whl", hash = "sha256:4853482f395c3a151937f6991272540fcbf531464f254a347bf7c89f53c8cff7", size = 8310245, upload-time = "2026-03-03T13:56:01.174Z" },
]
[[package]]
@@ -808,16 +815,16 @@ wheels = [
[[package]]
name = "django-pydantic-field"
version = "0.4.0"
version = "0.5.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "django" },
{ name = "pydantic" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/af/d6/dca6645f87345a7962ed0cd19e358368b02e83d5d172e30cd583a04d9f5e/django_pydantic_field-0.4.0.tar.gz", hash = "sha256:4fc35a45d9e511b9e9320e68569ec22fe11d8cbf40a88609d7bc1a3294e4ca9d", size = 37344, upload-time = "2025-11-06T10:31:05.84Z" }
sdist = { url = "https://files.pythonhosted.org/packages/42/3d/d2390f1383eb60b25c7b68b3493af566831b0db540e47ed85a395727bae2/django_pydantic_field-0.5.4.tar.gz", hash = "sha256:3c884299ea777e8221e4c0ff222e078960ba3576a3b6970636629524e5d6cf39", size = 22013, upload-time = "2026-02-22T13:47:30.633Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/97/50/03465856af47160cbca1d6b0eb5bba827db7276cb12ed01b6acb28a6c956/django_pydantic_field-0.4.0-py3-none-any.whl", hash = "sha256:081bf08ef18d1b9118988e36cad8faef915b935bec371a094828f6981fb2863d", size = 41726, upload-time = "2025-11-06T10:31:04.128Z" },
{ url = "https://files.pythonhosted.org/packages/ab/88/1acbef013f172f9f03836d3c03639ae5604a46e9f1a921c6e623e60f8594/django_pydantic_field-0.5.4-py3-none-any.whl", hash = "sha256:5c080b5f0ea17ae11b5305d649778a87437c61a403894933337fe3e295f627a9", size = 36963, upload-time = "2026-02-22T13:47:29.613Z" },
]
[[package]]
@@ -1035,15 +1042,11 @@ wheels = [
]
[[package]]
name = "griffe"
version = "1.15.0"
name = "griffelib"
version = "2.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama" },
]
sdist = { url = "https://files.pythonhosted.org/packages/0d/0c/3a471b6e31951dce2360477420d0a8d1e00dea6cf33b70f3e8c3ab6e28e1/griffe-1.15.0.tar.gz", hash = "sha256:7726e3afd6f298fbc3696e67958803e7ac843c1cfe59734b6251a40cdbfb5eea", size = 424112, upload-time = "2025-11-10T15:03:15.52Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/9c/83/3b1d03d36f224edded98e9affd0467630fc09d766c0e56fb1498cbb04a9b/griffe-1.15.0-py3-none-any.whl", hash = "sha256:6f6762661949411031f5fcda9593f586e6ce8340f0ba88921a0f2ef7a81eb9a3", size = 150705, upload-time = "2025-11-10T15:03:13.549Z" },
{ url = "https://files.pythonhosted.org/packages/4d/51/c936033e16d12b627ea334aaaaf42229c37620d0f15593456ab69ab48161/griffelib-2.0.0-py3-none-any.whl", hash = "sha256:01284878c966508b6d6f1dbff9b6fa607bc062d8261c5c7253cb285b06422a7f", size = 142004, upload-time = "2026-02-09T19:09:40.561Z" },
]
[[package]]
@@ -1966,7 +1969,7 @@ name = "pexpect"
version = "4.9.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "ptyprocess", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" },
{ name = "ptyprocess", marker = "sys_platform != 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450, upload-time = "2023-11-25T09:07:26.339Z" }
wheels = [
@@ -1975,35 +1978,35 @@ wheels = [
[[package]]
name = "pillow"
version = "12.1.0"
version = "12.1.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d0/02/d52c733a2452ef1ffcc123b68e6606d07276b0e358db70eabad7e40042b7/pillow-12.1.0.tar.gz", hash = "sha256:5c5ae0a06e9ea030ab786b0251b32c7e4ce10e58d983c0d5c56029455180b5b9", size = 46977283, upload-time = "2026-01-02T09:13:29.892Z" }
sdist = { url = "https://files.pythonhosted.org/packages/1f/42/5c74462b4fd957fcd7b13b04fb3205ff8349236ea74c7c375766d6c82288/pillow-12.1.1.tar.gz", hash = "sha256:9ad8fa5937ab05218e2b6a4cff30295ad35afd2f83ac592e68c0d871bb0fdbc4", size = 46980264, upload-time = "2026-02-11T04:23:07.146Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/dd/c7/2530a4aa28248623e9d7f27316b42e27c32ec410f695929696f2e0e4a778/pillow-12.1.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:7b5dd7cbae20285cdb597b10eb5a2c13aa9de6cde9bb64a3c1317427b1db1ae1", size = 4062543, upload-time = "2026-01-02T09:11:31.566Z" },
{ url = "https://files.pythonhosted.org/packages/8f/1f/40b8eae823dc1519b87d53c30ed9ef085506b05281d313031755c1705f73/pillow-12.1.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:29a4cef9cb672363926f0470afc516dbf7305a14d8c54f7abbb5c199cd8f8179", size = 4138373, upload-time = "2026-01-02T09:11:33.367Z" },
{ url = "https://files.pythonhosted.org/packages/d4/77/6fa60634cf06e52139fd0e89e5bbf055e8166c691c42fb162818b7fda31d/pillow-12.1.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:681088909d7e8fa9e31b9799aaa59ba5234c58e5e4f1951b4c4d1082a2e980e0", size = 3601241, upload-time = "2026-01-02T09:11:35.011Z" },
{ url = "https://files.pythonhosted.org/packages/4f/bf/28ab865de622e14b747f0cd7877510848252d950e43002e224fb1c9ababf/pillow-12.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:983976c2ab753166dc66d36af6e8ec15bb511e4a25856e2227e5f7e00a160587", size = 5262410, upload-time = "2026-01-02T09:11:36.682Z" },
{ url = "https://files.pythonhosted.org/packages/1c/34/583420a1b55e715937a85bd48c5c0991598247a1fd2eb5423188e765ea02/pillow-12.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:db44d5c160a90df2d24a24760bbd37607d53da0b34fb546c4c232af7192298ac", size = 4657312, upload-time = "2026-01-02T09:11:38.535Z" },
{ url = "https://files.pythonhosted.org/packages/1d/fd/f5a0896839762885b3376ff04878f86ab2b097c2f9a9cdccf4eda8ba8dc0/pillow-12.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6b7a9d1db5dad90e2991645874f708e87d9a3c370c243c2d7684d28f7e133e6b", size = 6232605, upload-time = "2026-01-02T09:11:40.602Z" },
{ url = "https://files.pythonhosted.org/packages/98/aa/938a09d127ac1e70e6ed467bd03834350b33ef646b31edb7452d5de43792/pillow-12.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6258f3260986990ba2fa8a874f8b6e808cf5abb51a94015ca3dc3c68aa4f30ea", size = 8041617, upload-time = "2026-01-02T09:11:42.721Z" },
{ url = "https://files.pythonhosted.org/packages/17/e8/538b24cb426ac0186e03f80f78bc8dc7246c667f58b540bdd57c71c9f79d/pillow-12.1.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e115c15e3bc727b1ca3e641a909f77f8ca72a64fff150f666fcc85e57701c26c", size = 6346509, upload-time = "2026-01-02T09:11:44.955Z" },
{ url = "https://files.pythonhosted.org/packages/01/9a/632e58ec89a32738cabfd9ec418f0e9898a2b4719afc581f07c04a05e3c9/pillow-12.1.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6741e6f3074a35e47c77b23a4e4f2d90db3ed905cb1c5e6e0d49bff2045632bc", size = 7038117, upload-time = "2026-01-02T09:11:46.736Z" },
{ url = "https://files.pythonhosted.org/packages/c7/a2/d40308cf86eada842ca1f3ffa45d0ca0df7e4ab33c83f81e73f5eaed136d/pillow-12.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:935b9d1aed48fcfb3f838caac506f38e29621b44ccc4f8a64d575cb1b2a88644", size = 6460151, upload-time = "2026-01-02T09:11:48.625Z" },
{ url = "https://files.pythonhosted.org/packages/f1/88/f5b058ad6453a085c5266660a1417bdad590199da1b32fb4efcff9d33b05/pillow-12.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5fee4c04aad8932da9f8f710af2c1a15a83582cfb884152a9caa79d4efcdbf9c", size = 7164534, upload-time = "2026-01-02T09:11:50.445Z" },
{ url = "https://files.pythonhosted.org/packages/19/ce/c17334caea1db789163b5d855a5735e47995b0b5dc8745e9a3605d5f24c0/pillow-12.1.0-cp313-cp313-win32.whl", hash = "sha256:a786bf667724d84aa29b5db1c61b7bfdde380202aaca12c3461afd6b71743171", size = 6332551, upload-time = "2026-01-02T09:11:52.234Z" },
{ url = "https://files.pythonhosted.org/packages/e5/07/74a9d941fa45c90a0d9465098fe1ec85de3e2afbdc15cc4766622d516056/pillow-12.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:461f9dfdafa394c59cd6d818bdfdbab4028b83b02caadaff0ffd433faf4c9a7a", size = 7040087, upload-time = "2026-01-02T09:11:54.822Z" },
{ url = "https://files.pythonhosted.org/packages/88/09/c99950c075a0e9053d8e880595926302575bc742b1b47fe1bbcc8d388d50/pillow-12.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:9212d6b86917a2300669511ed094a9406888362e085f2431a7da985a6b124f45", size = 2452470, upload-time = "2026-01-02T09:11:56.522Z" },
{ url = "https://files.pythonhosted.org/packages/b5/ba/970b7d85ba01f348dee4d65412476321d40ee04dcb51cd3735b9dc94eb58/pillow-12.1.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:00162e9ca6d22b7c3ee8e61faa3c3253cd19b6a37f126cad04f2f88b306f557d", size = 5264816, upload-time = "2026-01-02T09:11:58.227Z" },
{ url = "https://files.pythonhosted.org/packages/10/60/650f2fb55fdba7a510d836202aa52f0baac633e50ab1cf18415d332188fb/pillow-12.1.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:7d6daa89a00b58c37cb1747ec9fb7ac3bc5ffd5949f5888657dfddde6d1312e0", size = 4660472, upload-time = "2026-01-02T09:12:00.798Z" },
{ url = "https://files.pythonhosted.org/packages/2b/c0/5273a99478956a099d533c4f46cbaa19fd69d606624f4334b85e50987a08/pillow-12.1.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e2479c7f02f9d505682dc47df8c0ea1fc5e264c4d1629a5d63fe3e2334b89554", size = 6268974, upload-time = "2026-01-02T09:12:02.572Z" },
{ url = "https://files.pythonhosted.org/packages/b4/26/0bf714bc2e73d5267887d47931d53c4ceeceea6978148ed2ab2a4e6463c4/pillow-12.1.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f188d580bd870cda1e15183790d1cc2fa78f666e76077d103edf048eed9c356e", size = 8073070, upload-time = "2026-01-02T09:12:04.75Z" },
{ url = "https://files.pythonhosted.org/packages/43/cf/1ea826200de111a9d65724c54f927f3111dc5ae297f294b370a670c17786/pillow-12.1.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0fde7ec5538ab5095cc02df38ee99b0443ff0e1c847a045554cf5f9af1f4aa82", size = 6380176, upload-time = "2026-01-02T09:12:06.626Z" },
{ url = "https://files.pythonhosted.org/packages/03/e0/7938dd2b2013373fd85d96e0f38d62b7a5a262af21ac274250c7ca7847c9/pillow-12.1.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0ed07dca4a8464bada6139ab38f5382f83e5f111698caf3191cb8dbf27d908b4", size = 7067061, upload-time = "2026-01-02T09:12:08.624Z" },
{ url = "https://files.pythonhosted.org/packages/86/ad/a2aa97d37272a929a98437a8c0ac37b3cf012f4f8721e1bd5154699b2518/pillow-12.1.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:f45bd71d1fa5e5749587613037b172e0b3b23159d1c00ef2fc920da6f470e6f0", size = 6491824, upload-time = "2026-01-02T09:12:10.488Z" },
{ url = "https://files.pythonhosted.org/packages/a4/44/80e46611b288d51b115826f136fb3465653c28f491068a72d3da49b54cd4/pillow-12.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:277518bf4fe74aa91489e1b20577473b19ee70fb97c374aa50830b279f25841b", size = 7190911, upload-time = "2026-01-02T09:12:12.772Z" },
{ url = "https://files.pythonhosted.org/packages/86/77/eacc62356b4cf81abe99ff9dbc7402750044aed02cfd6a503f7c6fc11f3e/pillow-12.1.0-cp313-cp313t-win32.whl", hash = "sha256:7315f9137087c4e0ee73a761b163fc9aa3b19f5f606a7fc08d83fd3e4379af65", size = 6336445, upload-time = "2026-01-02T09:12:14.775Z" },
{ url = "https://files.pythonhosted.org/packages/e7/3c/57d81d0b74d218706dafccb87a87ea44262c43eef98eb3b164fd000e0491/pillow-12.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:0ddedfaa8b5f0b4ffbc2fa87b556dc59f6bb4ecb14a53b33f9189713ae8053c0", size = 7045354, upload-time = "2026-01-02T09:12:16.599Z" },
{ url = "https://files.pythonhosted.org/packages/ac/82/8b9b97bba2e3576a340f93b044a3a3a09841170ab4c1eb0d5c93469fd32f/pillow-12.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:80941e6d573197a0c28f394753de529bb436b1ca990ed6e765cf42426abc39f8", size = 2454547, upload-time = "2026-01-02T09:12:18.704Z" },
{ url = "https://files.pythonhosted.org/packages/d5/11/6db24d4bd7685583caeae54b7009584e38da3c3d4488ed4cd25b439de486/pillow-12.1.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:d242e8ac078781f1de88bf823d70c1a9b3c7950a44cdf4b7c012e22ccbcd8e4e", size = 4062689, upload-time = "2026-02-11T04:21:06.804Z" },
{ url = "https://files.pythonhosted.org/packages/33/c0/ce6d3b1fe190f0021203e0d9b5b99e57843e345f15f9ef22fcd43842fd21/pillow-12.1.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:02f84dfad02693676692746df05b89cf25597560db2857363a208e393429f5e9", size = 4138535, upload-time = "2026-02-11T04:21:08.452Z" },
{ url = "https://files.pythonhosted.org/packages/a0/c6/d5eb6a4fb32a3f9c21a8c7613ec706534ea1cf9f4b3663e99f0d83f6fca8/pillow-12.1.1-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:e65498daf4b583091ccbb2556c7000abf0f3349fcd57ef7adc9a84a394ed29f6", size = 3601364, upload-time = "2026-02-11T04:21:10.194Z" },
{ url = "https://files.pythonhosted.org/packages/14/a1/16c4b823838ba4c9c52c0e6bbda903a3fe5a1bdbf1b8eb4fff7156f3e318/pillow-12.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6c6db3b84c87d48d0088943bf33440e0c42370b99b1c2a7989216f7b42eede60", size = 5262561, upload-time = "2026-02-11T04:21:11.742Z" },
{ url = "https://files.pythonhosted.org/packages/bb/ad/ad9dc98ff24f485008aa5cdedaf1a219876f6f6c42a4626c08bc4e80b120/pillow-12.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8b7e5304e34942bf62e15184219a7b5ad4ff7f3bb5cca4d984f37df1a0e1aee2", size = 4657460, upload-time = "2026-02-11T04:21:13.786Z" },
{ url = "https://files.pythonhosted.org/packages/9e/1b/f1a4ea9a895b5732152789326202a82464d5254759fbacae4deea3069334/pillow-12.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:18e5bddd742a44b7e6b1e773ab5db102bd7a94c32555ba656e76d319d19c3850", size = 6232698, upload-time = "2026-02-11T04:21:15.949Z" },
{ url = "https://files.pythonhosted.org/packages/95/f4/86f51b8745070daf21fd2e5b1fe0eb35d4db9ca26e6d58366562fb56a743/pillow-12.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc44ef1f3de4f45b50ccf9136999d71abb99dca7706bc75d222ed350b9fd2289", size = 8041706, upload-time = "2026-02-11T04:21:17.723Z" },
{ url = "https://files.pythonhosted.org/packages/29/9b/d6ecd956bb1266dd1045e995cce9b8d77759e740953a1c9aad9502a0461e/pillow-12.1.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5a8eb7ed8d4198bccbd07058416eeec51686b498e784eda166395a23eb99138e", size = 6346621, upload-time = "2026-02-11T04:21:19.547Z" },
{ url = "https://files.pythonhosted.org/packages/71/24/538bff45bde96535d7d998c6fed1a751c75ac7c53c37c90dc2601b243893/pillow-12.1.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47b94983da0c642de92ced1702c5b6c292a84bd3a8e1d1702ff923f183594717", size = 7038069, upload-time = "2026-02-11T04:21:21.378Z" },
{ url = "https://files.pythonhosted.org/packages/94/0e/58cb1a6bc48f746bc4cb3adb8cabff73e2742c92b3bf7a220b7cf69b9177/pillow-12.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:518a48c2aab7ce596d3bf79d0e275661b846e86e4d0e7dec34712c30fe07f02a", size = 6460040, upload-time = "2026-02-11T04:21:23.148Z" },
{ url = "https://files.pythonhosted.org/packages/6c/57/9045cb3ff11eeb6c1adce3b2d60d7d299d7b273a2e6c8381a524abfdc474/pillow-12.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a550ae29b95c6dc13cf69e2c9dc5747f814c54eeb2e32d683e5e93af56caa029", size = 7164523, upload-time = "2026-02-11T04:21:25.01Z" },
{ url = "https://files.pythonhosted.org/packages/73/f2/9be9cb99f2175f0d4dbadd6616ce1bf068ee54a28277ea1bf1fbf729c250/pillow-12.1.1-cp313-cp313-win32.whl", hash = "sha256:a003d7422449f6d1e3a34e3dd4110c22148336918ddbfc6a32581cd54b2e0b2b", size = 6332552, upload-time = "2026-02-11T04:21:27.238Z" },
{ url = "https://files.pythonhosted.org/packages/3f/eb/b0834ad8b583d7d9d42b80becff092082a1c3c156bb582590fcc973f1c7c/pillow-12.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:344cf1e3dab3be4b1fa08e449323d98a2a3f819ad20f4b22e77a0ede31f0faa1", size = 7040108, upload-time = "2026-02-11T04:21:29.462Z" },
{ url = "https://files.pythonhosted.org/packages/d5/7d/fc09634e2aabdd0feabaff4a32f4a7d97789223e7c2042fd805ea4b4d2c2/pillow-12.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:5c0dd1636633e7e6a0afe7bf6a51a14992b7f8e60de5789018ebbdfae55b040a", size = 2453712, upload-time = "2026-02-11T04:21:31.072Z" },
{ url = "https://files.pythonhosted.org/packages/19/2a/b9d62794fc8a0dd14c1943df68347badbd5511103e0d04c035ffe5cf2255/pillow-12.1.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0330d233c1a0ead844fc097a7d16c0abff4c12e856c0b325f231820fee1f39da", size = 5264880, upload-time = "2026-02-11T04:21:32.865Z" },
{ url = "https://files.pythonhosted.org/packages/26/9d/e03d857d1347fa5ed9247e123fcd2a97b6220e15e9cb73ca0a8d91702c6e/pillow-12.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5dae5f21afb91322f2ff791895ddd8889e5e947ff59f71b46041c8ce6db790bc", size = 4660616, upload-time = "2026-02-11T04:21:34.97Z" },
{ url = "https://files.pythonhosted.org/packages/f7/ec/8a6d22afd02570d30954e043f09c32772bfe143ba9285e2fdb11284952cd/pillow-12.1.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2e0c664be47252947d870ac0d327fea7e63985a08794758aa8af5b6cb6ec0c9c", size = 6269008, upload-time = "2026-02-11T04:21:36.623Z" },
{ url = "https://files.pythonhosted.org/packages/3d/1d/6d875422c9f28a4a361f495a5f68d9de4a66941dc2c619103ca335fa6446/pillow-12.1.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:691ab2ac363b8217f7d31b3497108fb1f50faab2f75dfb03284ec2f217e87bf8", size = 8073226, upload-time = "2026-02-11T04:21:38.585Z" },
{ url = "https://files.pythonhosted.org/packages/a1/cd/134b0b6ee5eda6dc09e25e24b40fdafe11a520bc725c1d0bbaa5e00bf95b/pillow-12.1.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9e8064fb1cc019296958595f6db671fba95209e3ceb0c4734c9baf97de04b20", size = 6380136, upload-time = "2026-02-11T04:21:40.562Z" },
{ url = "https://files.pythonhosted.org/packages/7a/a9/7628f013f18f001c1b98d8fffe3452f306a70dc6aba7d931019e0492f45e/pillow-12.1.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:472a8d7ded663e6162dafdf20015c486a7009483ca671cece7a9279b512fcb13", size = 7067129, upload-time = "2026-02-11T04:21:42.521Z" },
{ url = "https://files.pythonhosted.org/packages/1e/f8/66ab30a2193b277785601e82ee2d49f68ea575d9637e5e234faaa98efa4c/pillow-12.1.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:89b54027a766529136a06cfebeecb3a04900397a3590fd252160b888479517bf", size = 6491807, upload-time = "2026-02-11T04:21:44.22Z" },
{ url = "https://files.pythonhosted.org/packages/da/0b/a877a6627dc8318fdb84e357c5e1a758c0941ab1ddffdafd231983788579/pillow-12.1.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:86172b0831b82ce4f7877f280055892b31179e1576aa00d0df3bb1bbf8c3e524", size = 7190954, upload-time = "2026-02-11T04:21:46.114Z" },
{ url = "https://files.pythonhosted.org/packages/83/43/6f732ff85743cf746b1361b91665d9f5155e1483817f693f8d57ea93147f/pillow-12.1.1-cp313-cp313t-win32.whl", hash = "sha256:44ce27545b6efcf0fdbdceb31c9a5bdea9333e664cda58a7e674bb74608b3986", size = 6336441, upload-time = "2026-02-11T04:21:48.22Z" },
{ url = "https://files.pythonhosted.org/packages/3b/44/e865ef3986611bb75bfabdf94a590016ea327833f434558801122979cd0e/pillow-12.1.1-cp313-cp313t-win_amd64.whl", hash = "sha256:a285e3eb7a5a45a2ff504e31f4a8d1b12ef62e84e5411c6804a42197c1cf586c", size = 7045383, upload-time = "2026-02-11T04:21:50.015Z" },
{ url = "https://files.pythonhosted.org/packages/a8/c6/f4fb24268d0c6908b9f04143697ea18b0379490cb74ba9e8d41b898bd005/pillow-12.1.1-cp313-cp313t-win_arm64.whl", hash = "sha256:cc7d296b5ea4d29e6570dabeaed58d31c3fea35a633a69679fb03d7664f43fb3", size = 2456104, upload-time = "2026-02-11T04:21:51.633Z" },
]
[[package]]
@@ -2163,20 +2166,20 @@ wheels = [
[[package]]
name = "pydantic-ai-slim"
version = "1.17.0"
version = "1.62.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "genai-prices" },
{ name = "griffe" },
{ name = "griffelib" },
{ name = "httpx" },
{ name = "opentelemetry-api" },
{ name = "pydantic" },
{ 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/cc/8d/6350a49f2e4b636efbcfc233221420ab576e4ba4edba38254cb84ae4a1e6/pydantic_ai_slim-1.62.0.tar.gz", hash = "sha256:00d84f659107bbbd88823a3d3dbe7348385935a9870b9d7d4ba799256f6b6983", size = 422452, upload-time = "2026-02-19T05:07:10.292Z" }
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/3d/67/21e9b3b0944568662e3790c936226bd48a9f27c6b5f27b5916f5857bc4d8/pydantic_ai_slim-1.62.0-py3-none-any.whl", hash = "sha256:5210073fadd46f65859a67da67845093c487f025fa430ed027151f22ec684ab2", size = 549296, upload-time = "2026-02-19T05:07:01.624Z" },
]
[package.optional-dependencies]
@@ -2194,6 +2197,7 @@ mistral = [
]
openai = [
{ name = "openai" },
{ name = "tiktoken" },
]
[[package]]
@@ -2223,7 +2227,7 @@ wheels = [
[[package]]
name = "pydantic-evals"
version = "1.17.0"
version = "1.62.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
@@ -2233,14 +2237,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/23/90/080f6722412263395d1d6d066ee90fa8bc2722ce097844220c2d9c946877/pydantic_evals-1.62.0.tar.gz", hash = "sha256:198c4bee936718a4acf6f504056b113e60b34eb49021df8889a394e14c803693", size = 56434, upload-time = "2026-02-19T05:07:11.793Z" }
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/d8/b9/dc8dba744ec02b16c6fd1abe3fd8ef1b00fd05c72feef5069851b811952f/pydantic_evals-1.62.0-py3-none-any.whl", hash = "sha256:0ca7e10037ed90393c54b6cff41370d6d4bac63f8c878715599c58863c303db1", size = 67341, upload-time = "2026-02-19T05:07:03.83Z" },
]
[[package]]
name = "pydantic-graph"
version = "1.17.0"
version = "1.62.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "httpx" },
@@ -2248,9 +2252,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/3b/b6/0b084c847ecd99624f4fbc5c8ecd3f67a2388a282a32612b2a68c3b3595f/pydantic_graph-1.62.0.tar.gz", hash = "sha256:efe56bee3a8ca35b11a3be6a5f7352419fe182ef1e1323a3267ee12dec95f3c7", size = 58529, upload-time = "2026-02-19T05:07:12.947Z" }
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/f0/12/1a9cbcd59fd070ba72b0fe544caa6ca97758518643523ec2bf1162084e0d/pydantic_graph-1.62.0-py3-none-any.whl", hash = "sha256:abe0e7b356b4d3202b069ec020d8dd1f647f55e9a0e85cd272dab48250bde87d", size = 72350, upload-time = "2026-02-19T05:07:05.305Z" },
]
[[package]]
@@ -2366,11 +2370,11 @@ wheels = [
[[package]]
name = "pypdf"
version = "6.6.2"
version = "6.7.5"
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" }
sdist = { url = "https://files.pythonhosted.org/packages/f6/52/37cc0aa9e9d1bf7729a737a0d83f8b3f851c8eb137373d9f71eafb0a3405/pypdf-6.7.5.tar.gz", hash = "sha256:40bb2e2e872078655f12b9b89e2f900888bb505e88a82150b64f9f34fa25651d", size = 5304278, upload-time = "2026-03-02T09:05:21.464Z" }
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" },
{ url = "https://files.pythonhosted.org/packages/05/89/336673efd0a88956562658aba4f0bbef7cb92a6fbcbcaf94926dbc82b408/pypdf-6.7.5-py3-none-any.whl", hash = "sha256:07ba7f1d6e6d9aa2a17f5452e320a84718d4ce863367f7ede2fd72280349ab13", size = 331421, upload-time = "2026-03-02T09:05:19.722Z" },
]
[[package]]
@@ -2888,6 +2892,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"
@@ -3096,15 +3126,15 @@ wheels = [
[[package]]
name = "youtube-transcript-api"
version = "1.2.3"
version = "1.0.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "defusedxml" },
{ name = "requests" },
]
sdist = { url = "https://files.pythonhosted.org/packages/87/03/68c69b2d3e282d45cb3c07e5836a9146ff9574cde720570ffc7eb124e56b/youtube_transcript_api-1.2.3.tar.gz", hash = "sha256:76016b71b410b124892c74df24b07b052702cf3c53afb300d0a2c547c0b71b68", size = 469757, upload-time = "2025-10-13T15:57:17.532Z" }
sdist = { url = "https://files.pythonhosted.org/packages/b0/32/f60d87a99c05a53604c58f20f670c7ea6262b55e0bbeb836ffe4550b248b/youtube_transcript_api-1.0.3.tar.gz", hash = "sha256:902baf90e7840a42e1e148335e09fe5575dbff64c81414957aea7038e8a4db46", size = 2153252, upload-time = "2025-03-25T18:14:21.119Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ef/75/a861661b73d862e323c12af96ecfb237fb4d1433e551183d4172d39d5275/youtube_transcript_api-1.2.3-py3-none-any.whl", hash = "sha256:0c1b32ea5e739f9efde8c42e3d43e67df475185af6f820109607577b83768375", size = 485140, upload-time = "2025-10-13T15:57:16.034Z" },
{ url = "https://files.pythonhosted.org/packages/f0/44/40c03bb0f8bddfb9d2beff2ed31641f52d96c287ba881d20e0c074784ac2/youtube_transcript_api-1.0.3-py3-none-any.whl", hash = "sha256:d1874e57de65cf14c9d7d09b2b37c814d6287fa0e770d4922c4cd32a5b3f6c47", size = 2169911, upload-time = "2025-03-25T18:14:19.416Z" },
]
[[package]]
@@ -1,14 +0,0 @@
module.exports = {
root: true,
extends: ['conversations/next'],
parserOptions: {
tsconfigRootDir: __dirname,
project: ['./tsconfig.json'],
},
settings: {
next: {
rootDir: __dirname,
},
},
ignorePatterns: ['node_modules', '.eslintrc.js', 'service-worker.js'],
};
@@ -145,10 +145,8 @@ export const commonGlobals = {
export const whiteLabelGlobals = {
colors: {
'logo-1-light': '#4844AD',
'logo-2-light': '#4844AD',
'logo-1-dark': '#BEC5F0',
'logo-2-dark': '#BEC5F0',
'logo-1': '#4844AD',
'logo-2': '#4844AD',
'brand-050': '#EEF1FA',
'brand-100': '#DDE2F5',
'brand-150': '#CED3F1',
@@ -0,0 +1,6 @@
import { nextConfig } from 'eslint-config-conversations/next.mjs';
export default nextConfig({
tsconfigRootDir: import.meta.dirname,
nextRootDir: import.meta.dirname,
});
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "app-conversations",
"version": "0.0.13",
"version": "0.0.14",
"private": true,
"scripts": {
"dev": "next dev",
@@ -8,8 +8,8 @@
"build:ci": "cp .env.development .env.local && yarn build",
"build-theme": "cunningham -g css,ts -o src/cunningham --utility-classes && yarn prettier && yarn stylelint --fix",
"start": "npx -y serve@latest out",
"lint": "tsc --noEmit && next lint",
"lint:fix": "tsc --noEmit && next lint --fix",
"lint": "tsc --noEmit && eslint src/",
"lint:fix": "tsc --noEmit && eslint src/ --fix",
"prettier": "prettier --write .",
"stylelint": "stylelint \"**/*.css\"",
"test": "jest",
@@ -1,8 +1,8 @@
:root {
--c--globals--colors--logo-1-light: #4844ad;
--c--globals--colors--logo-2-light: #4844ad;
--c--globals--colors--logo-1-dark: #bec5f0;
--c--globals--colors--logo-2-dark: #bec5f0;
--c--globals--colors--logo-1-light: #377fdb;
--c--globals--colors--logo-2-light: #377fdb;
--c--globals--colors--logo-1-dark: #c1d6f2;
--c--globals--colors--logo-2-dark: #c1d6f2;
--c--globals--colors--brand-050: #eef1fa;
--c--globals--colors--brand-100: #dde2f5;
--c--globals--colors--brand-150: #ced3f1;
@@ -332,6 +332,8 @@
--c--globals--colors--white-900: #f8f8f9e5;
--c--globals--colors--white-950: #f8f8f9f2;
--c--globals--colors--white-975: #f8f8f9f9;
--c--globals--colors--logo-1: #4844ad;
--c--globals--colors--logo-2: #4844ad;
--c--globals--transitions--ease-in: cubic-bezier(0.32, 0, 0.67, 0);
--c--globals--transitions--ease-out: cubic-bezier(0.33, 1, 0.68, 1);
--c--globals--transitions--ease-in-out: cubic-bezier(0.65, 0, 0.35, 1);
@@ -893,10 +895,10 @@
}
.cunningham-theme--dark {
--c--globals--colors--logo-1-light: #4844ad;
--c--globals--colors--logo-2-light: #4844ad;
--c--globals--colors--logo-1-dark: #bec5f0;
--c--globals--colors--logo-2-dark: #bec5f0;
--c--globals--colors--logo-1-light: #377fdb;
--c--globals--colors--logo-2-light: #377fdb;
--c--globals--colors--logo-1-dark: #c1d6f2;
--c--globals--colors--logo-2-dark: #c1d6f2;
--c--globals--colors--brand-050: #eef1fa;
--c--globals--colors--brand-100: #dde2f5;
--c--globals--colors--brand-150: #ced3f1;
@@ -1226,6 +1228,8 @@
--c--globals--colors--white-900: #f8f8f9e5;
--c--globals--colors--white-950: #f8f8f9f2;
--c--globals--colors--white-975: #f8f8f9f9;
--c--globals--colors--logo-1: #4844ad;
--c--globals--colors--logo-2: #4844ad;
--c--globals--transitions--ease-in: cubic-bezier(0.32, 0, 0.67, 0);
--c--globals--transitions--ease-out: cubic-bezier(0.33, 1, 0.68, 1);
--c--globals--transitions--ease-in-out: cubic-bezier(0.65, 0, 0.35, 1);
@@ -4854,6 +4858,14 @@
color: var(--c--globals--colors--white-975);
}
.clr-logo-1 {
color: var(--c--globals--colors--logo-1);
}
.clr-logo-2 {
color: var(--c--globals--colors--logo-2);
}
.bg-logo-1-light {
background-color: var(--c--globals--colors--logo-1-light);
}
@@ -6186,6 +6198,14 @@
background-color: var(--c--globals--colors--white-975);
}
.bg-logo-1 {
background-color: var(--c--globals--colors--logo-1);
}
.bg-logo-2 {
background-color: var(--c--globals--colors--logo-2);
}
.bg-surface-primary {
background-color: var(--c--contextuals--background--surface--primary);
}
@@ -3,10 +3,10 @@ export const tokens = {
default: {
globals: {
colors: {
'logo-1-light': '#4844AD',
'logo-2-light': '#4844AD',
'logo-1-dark': '#BEC5F0',
'logo-2-dark': '#BEC5F0',
'logo-1-light': '#377FDB',
'logo-2-light': '#377FDB',
'logo-1-dark': '#C1D6F2',
'logo-2-dark': '#C1D6F2',
'brand-050': '#EEF1FA',
'brand-100': '#DDE2F5',
'brand-150': '#CED3F1',
@@ -336,6 +336,8 @@ export const tokens = {
'white-900': '#F8F8F9E5',
'white-950': '#F8F8F9F2',
'white-975': '#F8F8F9F9',
'logo-1': '#4844AD',
'logo-2': '#4844AD',
},
transitions: {
'ease-in': 'cubic-bezier(0.32, 0, 0.67, 0)',
@@ -547,8 +549,8 @@ export const tokens = {
},
},
content: {
logo1: '#4844AD',
logo2: '#4844AD',
logo1: '#377FDB',
logo2: '#377FDB',
semantic: {
contextual: { primary: '#F8F8F9F2' },
overlay: { primary: '#F8F8F9F2' },
@@ -681,10 +683,10 @@ export const tokens = {
dark: {
globals: {
colors: {
'logo-1-light': '#4844AD',
'logo-2-light': '#4844AD',
'logo-1-dark': '#BEC5F0',
'logo-2-dark': '#BEC5F0',
'logo-1-light': '#377FDB',
'logo-2-light': '#377FDB',
'logo-1-dark': '#C1D6F2',
'logo-2-dark': '#C1D6F2',
'brand-050': '#EEF1FA',
'brand-100': '#DDE2F5',
'brand-150': '#CED3F1',
@@ -1014,6 +1016,8 @@ export const tokens = {
'white-900': '#F8F8F9E5',
'white-950': '#F8F8F9F2',
'white-975': '#F8F8F9F9',
'logo-1': '#4844AD',
'logo-2': '#4844AD',
},
transitions: {
'ease-in': 'cubic-bezier(0.32, 0, 0.67, 0)',
@@ -1225,8 +1229,8 @@ export const tokens = {
},
},
content: {
logo1: '#BEC5F0',
logo2: '#BEC5F0',
logo1: '#C1D6F2',
logo2: '#C1D6F2',
semantic: {
contextual: { primary: '#1B1B23D9' },
overlay: { primary: '#1B1B23D9' },
@@ -1876,8 +1880,8 @@ export const tokens = {
},
},
content: {
logo1: '#4844AD',
logo2: '#4844AD',
logo1: '#377FDB',
logo2: '#377FDB',
semantic: {
contextual: { primary: '#F8F8F9F2' },
overlay: { primary: '#F8F8F9F2' },
@@ -2527,8 +2531,8 @@ export const tokens = {
},
},
content: {
logo1: '#BEC5F0',
logo2: '#BEC5F0',
logo1: '#C1D6F2',
logo2: '#C1D6F2',
semantic: {
contextual: { primary: '#1B1B23D9' },
overlay: { primary: '#1B1B23D9' },
-2
View File
@@ -1,5 +1,3 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
declare module '*.svg' {
import * as React from 'react';
@@ -13,6 +13,7 @@ export interface User {
full_name: string;
short_name: string;
language?: string;
allow_smart_web_search: boolean;
allow_conversation_analytics: boolean;
sub?: string;
}
@@ -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 rehypeShikiFromHighlighter from '@shikijs/rehype/core';
import 'katex/dist/katex.min.css'; // `rehype-katex` does not import the CSS for you
import { useRouter } from 'next/router';
import { use, 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 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';
@@ -35,9 +26,6 @@ import { useSourceMetadataCache } from '../hooks';
import { useChatPreferencesStore } from '../stores/useChatPreferencesStore';
import { usePendingChatStore } from '../stores/usePendingChatStore';
import { useScrollStore } from '../stores/useScrollStore';
import { getHighlighter } from '../utils/shiki';
const highlighterPromise = getHighlighter();
// Define Attachment type locally (mirroring backend structure)
export interface Attachment {
@@ -54,7 +42,7 @@ export const Chat = ({
const { t } = useTranslation();
const copyToClipboard = useClipboard();
const { isMobile } = useResponsiveStore();
const highlighter = use(highlighterPromise);
const streamProtocol = 'data'; // or 'text'
const {
@@ -292,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(() => {
@@ -645,319 +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
className={`chatMessage ${message.role === 'user' ? 'chatMessage--user' : 'chatMessage--assistant'}`}
style={
shouldApplyStreamingHeight
? { minHeight: `${streamingMessageHeight}px` }
: undefined
}
>
{/* Message content */}
{message.content && (
<Box
className="mainContent-chat"
data-testid={
message.role === 'assistant'
? 'assistant-message-content'
: undefined
}
$padding={{ all: 'xxs' }}
>
<p className="sr-only">
{message.role === 'user'
? t('You said: ')
: t('Assistant IA replied: ')}
</p>
{message.role === 'user' ? (
<Text
as="p"
$css="white-space: pre-wrap; display: block;"
$theme="greyscale"
$variation="850"
>
{message.content}
</Text>
) : (
<MarkdownHooks
remarkPlugins={[remarkGfm, remarkMath]}
rehypePlugins={[
[
rehypeShikiFromHighlighter,
highlighter,
{
theme: 'github-dark-dimmed',
fallbackLanguage: 'plaintext',
},
],
rehypeKatex,
]}
components={{
// Custom components for Markdown rendering
// eslint-disable-next-line @typescript-eslint/no-unused-vars
p: ({ node, ...props }) => (
<Text
as="p"
$css="display: block"
$theme="greyscale"
$variation="850"
{...props}
/>
),
a: ({ children, ...props }) => (
<a target="_blank" {...props}>
{children}
</a>
),
// eslint-disable-next-line @typescript-eslint/no-unused-vars
pre: ({ node, children, ...props }) => (
<CodeBlock {...props}>{children}</CodeBlock>
),
}}
>
{message.content}
</MarkdownHooks>
)}
</Box>
)}
<Box $direction="column" $gap="2">
{isCurrentlyStreaming &&
isLastAssistantMessageInConversation &&
status === 'streaming' &&
message.parts?.some(
(part) =>
part.type === 'tool-invocation' &&
part.toolInvocation.toolName !==
'document_parsing',
) && (
<Box
$direction="row"
$align="center"
$gap="6px"
$width="100%"
$maxWidth="750px"
$margin={{
all: 'auto',
top: 'base',
bottom: 'md',
}}
>
<Loader />
<Text $variation="600" $size="md">
{(() => {
const toolInvocation = message.parts?.find(
(part) =>
part.type === 'tool-invocation' &&
part.toolInvocation.toolName !==
'document_parsing',
);
if (
toolInvocation?.type ===
'tool-invocation' &&
toolInvocation.toolInvocation.toolName ===
'summarize'
) {
return t('Summarizing...');
}
return t('Search...');
})()}
</Text>
</Box>
)}
{message.parts
?.filter((part) => part.type === 'tool-invocation')
.map(
(part: ToolInvocationUIPart, partIndex: number) =>
part.type === 'tool-invocation' &&
isCurrentlyStreaming &&
isLastAssistantMessageInConversation ? (
<ToolInvocationItem
key={`tool-invocation-${partIndex}`}
toolInvocation={part.toolInvocation}
status={status}
hideSearchLoader={true}
/>
) : null,
)}
</Box>
{message.role === 'assistant' &&
!(
isLastAssistantMessageInConversation &&
status === 'streaming'
) && (
<Box
$css="font-size: 12px;"
$direction="row"
$align="center"
className="clr-content-semantic-neutral-secondary"
$justify="space-between"
$gap="6px"
$margin={{ top: 'base' }}
>
<Box $direction="row" $gap="4px">
<Box
$theme="neutral"
$variation="secondary"
$direction="row"
$align="center"
$gap="4px"
className="c__button c__button--brand c__button--brand--tertiary c__button--nano clr-content-semantic-neutral-secondary"
onClick={() => copyToClipboard(message.content)}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
copyToClipboard(message.content);
}
}}
role="button"
tabIndex={0}
>
<Icon
iconName="content_copy"
$variation="550"
$size="16px"
/>
{!isMobile && (
<Text $theme="neutral" $variation="secondary">
{t('Copy')}
</Text>
)}
</Box>
{message.parts?.some(
(part) => part.type === 'source',
) &&
(() => {
const sourceCount =
message.parts?.filter(
(part) => part.type === 'source',
).length || 0;
return (
<Box
$direction="row"
$align="center"
$gap="4px"
className={`c__button c__button--brand c__button--brand--tertiary c__button--nano ${isSourceOpen === message.id ? 'action-chat-button--open' : ''}`}
onClick={() => openSources(message.id)}
onKeyDown={(e) => {
if (
e.key === 'Enter' ||
e.key === ' '
) {
e.preventDefault();
openSources(message.id);
}
}}
role="button"
tabIndex={0}
>
<Icon
iconName="book"
$theme="greyscale"
$variation="550"
$size="16px"
className="action-chat-button-icon"
/>
<Text
$theme="greyscale"
$variation="550"
$weight="500"
$size="12px"
>
{t('Show')} {sourceCount}{' '}
{sourceCount !== 1
? t('sources')
: t('source')}
</Text>
</Box>
);
})()}
</Box>
<Box $direction="row" $gap="4px">
{/* We should display the button, but disabled if no trace linked */}
{conversationId &&
message.id &&
message.id.startsWith('trace-') && (
<FeedbackButtons
conversationId={conversationId}
messageId={message.id}
/>
)}
</Box>
</Box>
)}
{message.parts &&
isSourceOpen === message.id &&
(() => {
const sourceParts = message.parts.filter(
(part): part is SourceUIPart =>
part.type === 'source',
);
return (
<Box
$css={`
animation: fade-in 0.2s ease-out;
`}
>
<SourceItemList
parts={sourceParts}
getMetadata={getMetadata}
/>
</Box>
);
})()}
</Box>
</Box>
</Box>
);
})}
{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') ||
@@ -1,5 +1,5 @@
import { Button } from '@openfun/cunningham-react';
import React, { useState } from 'react';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Box, useToast } from '@/components';
@@ -19,8 +19,8 @@ const ThumbUp = () => (
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
fillRule="evenodd"
clipRule="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="var(--c--contextuals--content--semantic--neutral--secondary)"
/>
@@ -36,8 +36,8 @@ const ThumbDown = () => (
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
fillRule="evenodd"
clipRule="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="var(--c--contextuals--content--semantic--neutral--secondary)"
/>
@@ -53,8 +53,8 @@ const ThumbDownFilled = () => (
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
fillRule="evenodd"
clipRule="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="var(--c--contextuals--content--semantic--brand--tertiary)"
/>
@@ -70,8 +70,8 @@ const ThumbUpFilled = () => (
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
fillRule="evenodd"
clipRule="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="var(--c--contextuals--content--semantic--brand--tertiary)"
/>
@@ -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(
@@ -443,6 +459,7 @@ export const InputChat = ({
name="inputchat-textarea"
onChange={handleTextareaChange}
onKeyDown={handleTextareaKeyDown}
onPaste={handlePaste}
disabled={isInputDisabled}
rows={1}
style={textareaStyle}
@@ -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,537 @@
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 }) => (
<>
{/* 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} />}
</>
),
(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]);
// 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
className={`chatMessage ${message.role === 'user' ? 'chatMessage--user' : 'chatMessage--assistant'}`}
style={
shouldApplyStreamingHeight
? { minHeight: `${streamingMessageHeight}px` }
: undefined
}
>
{/* Message content */}
{message.content && (
<Box
className="mainContent-chat"
data-testid={
message.role === 'assistant'
? 'assistant-message-content'
: undefined
}
$padding={{ all: 'xxs' }}
>
<p className="sr-only">
{message.role === 'user'
? t('You said: ')
: t('Assistant IA replied: ')}
</p>
{message.role === 'user' ? (
<Text
as="p"
$css="white-space: pre-wrap; display: block;"
$theme="greyscale"
$variation="850"
>
{message.content}
</Text>
) : (
// 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">
{activeToolInvocation?.toolName === 'summarize'
? t('Summarizing...')
: t('Search...')}
</Text>
</Box>
)}
{toolInvocationParts.map((part, partIndex) =>
isCurrentlyStreaming && isLastAssistantMessage ? (
<ToolInvocationItem
key={`tool-invocation-${partIndex}`}
toolInvocation={part.toolInvocation}
status={status}
hideSearchLoader={true}
/>
) : null,
)}
</Box>
{message.role === 'assistant' &&
!(isLastAssistantMessage && status === 'streaming') && (
<Box
$css="color: #222631; font-size: 12px;"
$direction="row"
$align="center"
$justify="space-between"
$gap="6px"
$margin={{ top: 'base' }}
>
<Box $direction="row" $gap="4px">
<Box
$direction="row"
$align="center"
$gap="4px"
className="c__button--neutral action-chat-button"
onClick={handleCopy}
onKeyDown={handleCopyKeyDown}
role="button"
tabIndex={0}
>
<Icon
iconName="content_copy"
$theme="greyscale"
$variation="550"
$size="16px"
className="action-chat-button-icon"
/>
{!isMobile && (
<Text $theme="greyscale" $variation="550">
{t('Copy')}
</Text>
)}
</Box>
{sourceParts.length > 0 && (
<Box
$direction="row"
$align="center"
$gap="4px"
className={`c__button--neutral action-chat-button ${isSourceOpen === message.id ? 'action-chat-button--open' : ''}`}
onClick={handleOpenSources}
onKeyDown={handleOpenSourcesKeyDown}
role="button"
tabIndex={0}
>
<Icon
iconName="book"
$theme="greyscale"
$variation="550"
$size="16px"
className="action-chat-button-icon"
/>
<Text
$theme="greyscale"
$variation="550"
$weight="500"
$size="12px"
>
{t('Show')} {sourceParts.length}{' '}
{sourceParts.length !== 1 ? t('sources') : t('source')}
</Text>
</Box>
)}
</Box>
<Box $direction="row" $gap="4px">
{conversationId &&
message.id &&
message.id.startsWith('trace-') && (
<FeedbackButtons
conversationId={conversationId}
messageId={message.id}
/>
)}
</Box>
</Box>
)}
{isSourceOpen === message.id && sourceParts.length > 0 && (
<Box
$css={`
animation: fade-in 0.2s ease-out;
`}
>
<SourceItemList parts={sourceParts} getMetadata={getMetadata} />
</Box>
)}
</Box>
</Box>
</Box>
);
};
MessageItemComponent.displayName = 'MessageItem';
// Custom comparison function for React.memo
// Only re-render when props that affect rendering change
const arePropsEqual = (
prevProps: MessageItemProps,
nextProps: MessageItemProps,
): boolean => {
// Always re-render if message content changed
if (prevProps.message.id !== nextProps.message.id) {
return false;
}
if (prevProps.message.content !== nextProps.message.content) {
return false;
}
if (prevProps.message.role !== nextProps.message.role) {
return false;
}
// Check parts changes (for streaming tool invocations and sources)
const prevPartsLength = prevProps.message.parts?.length ?? 0;
const nextPartsLength = nextProps.message.parts?.length ?? 0;
if (prevPartsLength !== nextPartsLength) {
return false;
}
// Check attachments
const prevAttachmentsLength =
prevProps.message.experimental_attachments?.length ?? 0;
const nextAttachmentsLength =
nextProps.message.experimental_attachments?.length ?? 0;
if (prevAttachmentsLength !== nextAttachmentsLength) {
return false;
}
// Check rendering flags
if (prevProps.isLastMessage !== nextProps.isLastMessage) {
return false;
}
if (prevProps.isLastAssistantMessage !== nextProps.isLastAssistantMessage) {
return false;
}
if (
prevProps.isFirstConversationMessage !==
nextProps.isFirstConversationMessage
) {
return false;
}
if (prevProps.streamingMessageHeight !== nextProps.streamingMessageHeight) {
return false;
}
if (prevProps.status !== nextProps.status) {
return false;
}
if (prevProps.isSourceOpen !== nextProps.isSourceOpen) {
return false;
}
if (prevProps.isMobile !== nextProps.isMobile) {
return false;
}
if (prevProps.conversationId !== nextProps.conversationId) {
return false;
}
return true;
};
export const MessageItem = React.memo(MessageItemComponent, arePropsEqual);
@@ -23,7 +23,6 @@ const SourceItemListComponent: React.FC<SourceItemListProps> = ({
if (parts.length === 0) {
return null;
}
return (
<Box
$direction="column"
@@ -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],
@@ -45,11 +45,6 @@ export const ToolInvocationItem: React.FC<ToolInvocationItemProps> = ({
$width="100%"
$maxWidth="750px"
$margin={{ all: 'auto', top: 'base', bottom: 'md' }}
$background="var(--c--contextuals--background--surface--tertiary)"
$color="var(--c--contextuals--content--semantic--neutral--secondary)"
$padding={{ all: 'sm' }}
$radius="8px"
$css="font-size: 0.9em; width: 100%; white-space: pre-wrap; word-wrap: break-word;"
>
<Loader />
<Text $variation="600" $size="md">
@@ -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', () => {
@@ -24,7 +24,10 @@ import { LaGaufre } from './LaGaufre';
export const Header = () => {
const { t } = useTranslation();
const { spacingsTokens, colorsTokens } = useCunninghamTheme();
const { spacingsTokens, colorsTokens, componentTokens } =
useCunninghamTheme();
const showLaGaufre =
(componentTokens as Record<string, unknown>)['la-gaufre'] === true;
const { isDesktop } = useResponsiveStore();
const { setPanelOpen } = useChatPreferencesStore();
const { isAtTop } = useChatScroll();
@@ -95,7 +98,7 @@ export const Header = () => {
>
<ButtonLogin />
<LanguagePicker />
<LaGaufre />
{showLaGaufre && <LaGaufre />}
</Box>
)}
</Box>
@@ -26,6 +26,8 @@ export const HomeHeader = () => {
const logo = (componentTokens as Record<string, unknown>).logo as
| { src: string; alt: string; widthHeader: string; widthFooter: string }
| undefined;
const showLaGaufre =
(componentTokens as Record<string, unknown>)['la-gaufre'] === true;
return (
<Box
@@ -79,7 +81,7 @@ export const HomeHeader = () => {
<Box $direction="row" $gap="1rem" $align="center">
<ButtonLogin />
<LanguagePicker />
<LaGaufre />
{showLaGaufre && <LaGaufre />}
</Box>
)}
</Box>
@@ -1,7 +1,7 @@
import { Modal, ModalSize } from '@openfun/cunningham-react';
import { useTranslation } from 'react-i18next';
import { Box, StyledLink, Text, ToggleSwitch, useToast } from '@/components';
import { Box, Text, ToggleSwitch, useToast } from '@/components';
import { useUserUpdate } from '@/core/api/useUserUpdate';
import { useCunninghamTheme } from '@/cunningham';
import { useAuthQuery } from '@/features/auth/api';
@@ -11,6 +11,20 @@ interface SettingsModalProps {
isOpen: boolean;
}
const STATUS_I18N_KEYS: Record<
'allow_conversation_analytics' | 'allow_smart_web_search',
{ enabled: string; disabled: string }
> = {
allow_conversation_analytics: {
enabled: 'Conversation analysis enabled',
disabled: 'Conversation analysis disabled',
},
allow_smart_web_search: {
enabled: 'Automatic web search enabled',
disabled: 'Automatic web search disabled',
},
};
export const SettingsModal = ({ onClose, isOpen }: SettingsModalProps) => {
const { t } = useTranslation();
const { data: user } = useAuthQuery();
@@ -18,30 +32,26 @@ export const SettingsModal = ({ onClose, isOpen }: SettingsModalProps) => {
const { showToast } = useToast();
const { isDarkMode, toggleDarkMode } = useCunninghamTheme();
const handleToggleChange = async () => {
const handleToggleChange = async (
field: 'allow_conversation_analytics' | 'allow_smart_web_search',
) => {
if (!user) {
return;
}
try {
const updatedValue = !user[field];
await updateUser({
id: user.id,
allow_conversation_analytics: !user.allow_conversation_analytics,
[field]: updatedValue,
});
// Toast de succès
showToast(
'success',
user.allow_conversation_analytics
? t('Conversation analysis disabled')
: t('Conversation analysis enabled'),
'check_circle',
3000,
);
const toastMessage = updatedValue
? t(STATUS_I18N_KEYS[field].enabled)
: t(STATUS_I18N_KEYS[field].disabled);
showToast('success', toastMessage, 'check_circle', 3000);
} catch (error) {
console.error('Error updating user settings:', error);
// Toast d'erreur
console.error(`Error updating user settings for ${field}:`, error);
showToast('error', t('Failed to update settings'), 'error', 3000);
}
};
@@ -56,6 +66,7 @@ export const SettingsModal = ({ onClose, isOpen }: SettingsModalProps) => {
>
<Box aria-label={t('Assistant settings')}>
<Box $justify="space-between">
{/* Intro */}
<Box $gap="2xs">
<Text
$size="xs"
@@ -68,59 +79,100 @@ export const SettingsModal = ({ onClose, isOpen }: SettingsModalProps) => {
'The Assistant is a sovereign AI for public servants. It helps with daily tasks (rephrasing, summarising, translating, information search). Your data stays in France on secure, state-compliant infrastructure and is never used for commercial purposes.',
)}
</Text>
</Box>
{/* Conversation Analytics */}
<Box $gap="2xs">
<Text
$size="md"
$weight="500"
$theme="greyscale"
$variation="850"
$padding={{ top: 'xs' }}
$padding={{ top: 'xs', bottom: 'xs' }}
>
{t('Allow conversation analysis')}
</Text>
</Box>
<Box $direction="row" $justify="space-between" $align="flex-start">
<Box $css="max-width: 70%;">
<Text
$css={`
display: inline-block;
`}
$size="xs"
$theme="greyscale"
$variation="600"
$weight="400"
>
{t(
'If enabled, this allows us to analyse your exchanges to improve the Assistant. If disabled, all conversations remain confidential and are not used in any way. ',
)}
<StyledLink
<Box $direction="row" $justify="space-between" $align="flex-start">
<Box $css="max-width: 70%;">
<Text
$css={`
display: inline-block;
`}
target="_blank"
href="https://docs.numerique.gouv.fr/docs/53d1dfb9-481d-4a68-b75c-7208c03d4dec/"
$size="xs"
$theme="greyscale"
$variation="600"
$weight="400"
$padding={{ top: 'sm', bottom: 'sm' }}
>
<Text
$css={`
text-decoration: underline !important;
`}
$size="xs"
$theme="greyscale"
$variation="600"
$weight="400"
{t(
'If enabled, this allows us to analyse your exchanges to improve the Assistant. If disabled, all conversations remain confidential and are not used in any way. ',
)}
<a
style={{
display: 'inline-block',
color:
'var(--c--contextuals--content--semantic--neutral--primary)',
}}
target="_blank"
rel="noopener noreferrer"
href="https://docs.numerique.gouv.fr/docs/53d1dfb9-481d-4a68-b75c-7208c03d4dec/"
>
{t('Learn more about data usage.')}
</Text>
</StyledLink>
</Text>
</a>
</Text>
</Box>
<ToggleSwitch
checked={user?.allow_conversation_analytics ?? false}
onChange={() =>
void handleToggleChange('allow_conversation_analytics')
}
disabled={isPending}
aria-label={t('Allow conversation analysis')}
/>
</Box>
<ToggleSwitch
checked={user?.allow_conversation_analytics ?? false}
onChange={() => void handleToggleChange()}
disabled={isPending}
aria-label={t('Allow conversation analysis')}
/>
</Box>
{/* Smart Web Search (title + description + Dark Mode spacing) */}
<Box $gap="2xs">
<Text
$size="md"
$weight="500"
$theme="greyscale"
$variation="850"
$padding={{ top: 'xs', bottom: 'xs' }}
>
{t('Automatic web search')}
</Text>
<Box $direction="row" $justify="space-between" $align="flex-start">
<Box $css="max-width: 70%;">
<Text
$css={`
display: inline-block;
`}
$size="xs"
$theme="greyscale"
$variation="600"
$weight="400"
$padding={{ top: 'sm', bottom: 'sm' }}
>
{t(
"The Assistant automatically decides when to perform a web search to enrich responses. If disabled, the Assistant will not use the web unless you click the 'Search the web' button.",
)}
</Text>
</Box>
<ToggleSwitch
checked={user?.allow_smart_web_search ?? false}
onChange={() =>
void handleToggleChange('allow_smart_web_search')
}
disabled={isPending}
aria-label={t('Automatic web search')}
/>
</Box>
</Box>
{/* Dark Mode (exact copy of your box) */}
<Box
$display="block"
$justify="space-between"
@@ -16,12 +16,14 @@
"An error occurred while renaming the conversation": "Une erreur est survenue lors du renommage de la conversation",
"An error occurred. Please try again.": "Une erreur s'est produite. Veuillez réessayer.",
"Are you sure you want to delete this conversation ?": "Êtes-vous sûr de vouloir supprimer cette conversation ?",
"Ask a question": "Poser une question",
"Assistant IA replied: ": "Assistant IA a répondu : ",
"Assistant is already available, log in to use it now.": "L'Assistant est déjà disponible, connectez-vous pour l'utiliser maintenant.",
"Assistant is in development: your feedback matters! Choose how to share your ideas:": "L'assistant est en cours de développement : vos commentaires sont importants ! Choisissez comment partager vos avis :",
"Assistant settings": "Paramètres de l'Assistant",
"Attach file": "Joindre un fichier",
"Attachment summary not supported": "Résumé des pièces jointes non pris en charge",
"Automatic web search": "Recherche web automatique",
"Cancel": "Annuler",
"Clear search": "Effacer la recherche",
"Close model selector": "Fermer le sélecteur de modèle",
@@ -31,8 +33,6 @@
"Confirm deletion": "Confirmer la suppression",
"Content modal to delete conversation": "Modale pour supprimer la conversation",
"Content modal to rename a conversation": "Modale pour renommer la conversation",
"Conversation analysis disabled": "Analyse de la conversation désactivée",
"Conversation analysis enabled": "Analyse de la conversation activée",
"Copied": "Copié",
"Copy": "Copier",
"Copy code": "Copier le code",
@@ -55,6 +55,7 @@
"Feedback Négatif": "Retour négatif",
"Feedback positif": "Retour positif",
"File type not supported": "Type de fichier non pris en charge",
"Find recent news about...": "Trouver les dernières actualités concernant...",
"Get notified about the Public Beta.": "Soyez informé de la Bêta publique.",
"Get notified for the public beta": "Être notifié pour la bêta publique",
"Give a quick opinion": "Donner un avis rapide",
@@ -108,6 +109,7 @@
"Start conversation": "Entamer la conversation",
"Stop": "Stop",
"Summarizing...": "Résumé en cours...",
"The Assistant automatically decides when to perform a web search to enrich responses. If disabled, the Assistant will not use the web unless you click the 'Search the web' button.": "L'Assistant décide automatiquement quand effectuer une recherche web pour enrichir les réponses. Si désactivé, l'Assistant n'utilisera pas le Web à moins que vous ne cliquiez sur le bouton « Rechercher sur le Web ».",
"The Assistant is a sovereign AI for public servants. It helps with daily tasks (rephrasing, summarising, translating, information search). Your data stays in France on secure, state-compliant infrastructure and is never used for commercial purposes.": "L'Assistant est une IA souveraine destinée aux fonctionnaires. Il les aide dans leurs tâches quotidiennes (reformulation, résumé, traduction, recherche d'informations). Vos données restent en France, sur une infrastructure sécurisée et conforme aux normes nationales, et ne sont jamais utilisées à des fins commerciales.",
"The Assistant is in Beta": "L'Assistant est en Bêta",
"The conversation has been deleted.": "La conversation a été supprimée.",
@@ -115,6 +117,7 @@
"The summary feature is not supported yet.": "La fonctionnalité de résumé n'est pas encore prise en charge.",
"Thinking...": "Réflexion...",
"To add a file to the conversation, drop it here.": "Pour ajouter un fichier à la conversation, déposez-le ici.",
"Turn this list into bullet points": "Transformer cette liste en points à puces",
"Unlock access": "Déverrouiller l'accès",
"Unlocking...": "Déverrouillage en cours...",
"Untitled conversation": "Conversation sans titre",
@@ -124,6 +127,7 @@
"We'll email you when the public beta opens.": "Nous vous enverrons un email quand la bêta publique sera ouverte.",
"Web": "Web",
"What is on your mind?": "Quavez-vous en tête ?",
"Write a short product description": "Écrire une description courte du produit",
"Write on Tchap": "Écrire sur Tchap",
"You are on the list": "Vous êtes dans la liste",
"You do not have permission to view this page.": "Vous navez pas la permission de voir cette page.",
@@ -151,12 +155,14 @@
"An error occurred while renaming the conversation": "Er is een fout opgetreden bij het hernoemen van het gesprek",
"An error occurred. Please try again.": "Er is een fout opgetreden. Probeer het opnieuw.",
"Are you sure you want to delete this conversation ?": "Weet u zeker dat u dit gesprek wilt verwijderen?",
"Ask a question": "Stel een vraag",
"Assistant IA replied: ": "AI Assistent antwoordde: ",
"Assistant is already available, log in to use it now.": "Assistent is al beschikbaar, log in om het nu te gebruiken.",
"Assistant is in development: your feedback matters! Choose how to share your ideas:": "Assistent is in ontwikkeling: uw feedback telt! Kies hoe u uw ideeën wilt delen:",
"Assistant settings": "Assistentinstellingen",
"Attach file": "Bestand bijvoegen",
"Attachment summary not supported": "Bijlageoverzicht niet ondersteund",
"Automatic web search": "Automatisch webzoeken",
"Cancel": "Annuleren",
"Clear search": "Zoekopdracht wissen",
"Close model selector": "Sluit modelselector",
@@ -166,8 +172,6 @@
"Confirm deletion": "Verwijdering bevestigen",
"Content modal to delete conversation": "Inhoudsvenster om conversatie te verwijderen",
"Content modal to rename a conversation": "Content modal om een gesprek te hernoemen",
"Conversation analysis disabled": "Gespreksanalyse uitgeschakeld",
"Conversation analysis enabled": "Gespreksanalyse ingeschakeld",
"Copied": "Gekopieerd",
"Copy": "Kopiëren",
"Copy code": "Kopieer code",
@@ -190,6 +194,7 @@
"Feedback Négatif": "Negatieve feedback",
"Feedback positif": "Positieve feedback",
"File type not supported": "Bestandstype niet ondersteund",
"Find recent news about...": "Vind recent nieuws over...",
"Get notified about the Public Beta.": "Ontvang een melding over de openbare bèta.",
"Get notified for the public beta": "Ontvang een melding voor de openbare bètaversie",
"Give a quick opinion": "Geef snel een mening",
@@ -243,6 +248,7 @@
"Start conversation": "Begin een gesprek",
"Stop": "Stop",
"Summarizing...": "Samenvatten...",
"The Assistant automatically decides when to perform a web search to enrich responses. If disabled, the Assistant will not use the web unless you click the 'Search the web' button.": "De assistent beslist automatisch wanneer er op het web gezocht moet worden om de reacties te verrijken. Indien uitgeschakeld zal de assistent het web niet gebruiken, tenzij u op de knop 'Zoeken naar het web' klikt.",
"The Assistant is a sovereign AI for public servants. It helps with daily tasks (rephrasing, summarising, translating, information search). Your data stays in France on secure, state-compliant infrastructure and is never used for commercial purposes.": "De assistent is een soevereine AI voor overheidsambtenaren. Het helpt met dagelijkse taken (herformuleren, samenvatten, vertalen, informatiezoeken). Je gegevens blijven in Nederland op een veilige, door de staat compatibele infrastructuur en worden nooit voor commerciële doeleinden gebruikt.",
"The Assistant is in Beta": "De Assistent is in bèta",
"The conversation has been deleted.": "Het gesprek is verwijderd.",
@@ -250,6 +256,7 @@
"The summary feature is not supported yet.": "De samenvattingsfunctie wordt nog niet ondersteund.",
"Thinking...": "Denken...",
"To add a file to the conversation, drop it here.": "Als u een bestand aan het gesprek wilt toevoegen, sleept u het hierheen.",
"Turn this list into bullet points": "Verander deze lijst in opsommingspunten",
"Unlock access": "Toegang ontgrendelen",
"Unlocking...": "Ontgrendelen...",
"Untitled conversation": "Ongetiteld gesprek",
@@ -259,6 +266,7 @@
"We'll email you when the public beta opens.": "We sturen u een e-mail zodra de openbare bètaversie beschikbaar is.",
"Web": "Internet",
"What is on your mind?": "Waar denk je aan?",
"Write a short product description": "Schrijf een korte productbeschrijving",
"Write on Tchap": "Schrijf op Tchap",
"You are on the list": "Je staat op de lijst",
"You do not have permission to view this page.": "U heeft geen toestemming om deze pagina te bekijken.",
@@ -286,12 +294,14 @@
"An error occurred while renaming the conversation": "Ошибка при переименовании беседы",
"An error occurred. Please try again.": "Произошла ошибка. Пожалуйста, повторите попытку.",
"Are you sure you want to delete this conversation ?": "Вы действительно хотите удалить эту беседу?",
"Ask a question": "Задать вопрос",
"Assistant IA replied: ": "Помощник ИИ ответил: ",
"Assistant is already available, log in to use it now.": "Помощник уже доступен, просто войдите в систему.",
"Assistant is in development: your feedback matters! Choose how to share your ideas:": "Помощник находится в разработке: ваши отзывы важны! Выберите, как поделиться своими идеями:",
"Assistant settings": "Настройки помощника",
"Attach file": "Прикрепить файл",
"Attachment summary not supported": "Сводка о вложениях не поддерживается",
"Automatic web search": "Автопоиск в Интернете",
"Cancel": "Отмена",
"Clear search": "Очистить поиск",
"Close model selector": "Закрыть выбор модели",
@@ -301,8 +311,6 @@
"Confirm deletion": "Подтвердите удаление",
"Content modal to delete conversation": "Подтверждение удаления беседы",
"Content modal to rename a conversation": "Подтверждение переименования беседы",
"Conversation analysis disabled": "Анализ бесед отключён",
"Conversation analysis enabled": "Анализ бесед включён",
"Copied": "Скопировано",
"Copy": "Копировать",
"Copy code": "Скопировать код",
@@ -325,6 +333,7 @@
"Feedback Négatif": "Отрицательный отзыв",
"Feedback positif": "Положительный отзыв",
"File type not supported": "Тип файла не поддерживается",
"Find recent news about...": "Найти последние новости о...",
"Get notified about the Public Beta.": "Получать уведомления о публичной бета-версии.",
"Get notified for the public beta": "Получать уведомления о публичной бета-версии",
"Give a quick opinion": "Оставить быстрый отзыв",
@@ -378,6 +387,7 @@
"Start conversation": "Начать беседу",
"Stop": "Остановить",
"Summarizing...": "Обобщение...",
"The Assistant automatically decides when to perform a web search to enrich responses. If disabled, the Assistant will not use the web unless you click the 'Search the web' button.": "Помощник автоматически принимает решение о том, когда осуществлять поиск в Интернете для обогащения ответов. Если эта функция отключена, то помощник не будет использовать Интернет, пока вы не нажмёте на кнопку «Поиск в Интернете».",
"The Assistant is a sovereign AI for public servants. It helps with daily tasks (rephrasing, summarising, translating, information search). Your data stays in France on secure, state-compliant infrastructure and is never used for commercial purposes.": "Помощник — независимый ИИ для государственных служащих. Он помогает в выполнении повседневных задач (перефразирование, резюмирование, перевод, поиск информации). Ваши данные хранятся во Франции на безопасной инфраструктуре, соответствующей государственным требованиям, и никогда не используются в коммерческих целях.",
"The Assistant is in Beta": "Помощник находится на этапе Бета-версии",
"The conversation has been deleted.": "Беседа была удалена.",
@@ -385,6 +395,7 @@
"The summary feature is not supported yet.": "Функция сводки пока не поддерживается.",
"Thinking...": "Размышление...",
"To add a file to the conversation, drop it here.": "Чтобы добавить файл в беседу, поместите его сюда.",
"Turn this list into bullet points": "Преобразовать этот список в маркированный",
"Unlock access": "Разблокировать",
"Unlocking...": "Разблокировка...",
"Untitled conversation": "Беседа без названия",
@@ -394,6 +405,7 @@
"We'll email you when the public beta opens.": "Мы отправим вам письмо, когда станет доступна публичная бета-версия.",
"Web": "Интернет",
"What is on your mind?": "О чём вы думаете?",
"Write a short product description": "Введите краткое описание продукта",
"Write on Tchap": "Написать в Tchap",
"You are on the list": "Вы в списке",
"You do not have permission to view this page.": "У вас недостаточно прав для просмотра этой страницы.",
@@ -421,12 +433,14 @@
"An error occurred while renaming the conversation": "Сталася помилка під час перейменування розмови",
"An error occurred. Please try again.": "Сталась помилка. Спробуйте ще раз.",
"Are you sure you want to delete this conversation ?": "Ви дійсно бажаєте видалити цю розмову?",
"Ask a question": "Задати питання",
"Assistant IA replied: ": "Відповідь помічника ШІ: ",
"Assistant is already available, log in to use it now.": "Помічник вже доступний, увійдіть щоб почати використання.",
"Assistant is in development: your feedback matters! Choose how to share your ideas:": "Помічник в розробці: ваші відгуки мають значення! Оберіть, як поділитися своїми ідеями:",
"Assistant settings": "Налаштування помічника",
"Attach file": "Додати файл",
"Attachment summary not supported": "Узагальнення вкладень не підтримується",
"Automatic web search": "Автоматичний пошук в Інтернеті",
"Cancel": "Скасувати",
"Clear search": "Очистити вікно пошуку",
"Close model selector": "Закрити вікно вибору моделі",
@@ -436,8 +450,6 @@
"Confirm deletion": "Підтвердження видалення",
"Content modal to delete conversation": "Підтвердження видалення розмови",
"Content modal to rename a conversation": "Підтвердження перейменування розмови",
"Conversation analysis disabled": "Аналіз розмови вимкнено",
"Conversation analysis enabled": "Аналіз розмов увімкнено",
"Copied": "Скопійовано",
"Copy": "Копіювати",
"Copy code": "Скопіювати код",
@@ -460,6 +472,7 @@
"Feedback Négatif": "Негативний відгук",
"Feedback positif": "Позитивний відгук",
"File type not supported": "Тип файлу не підтримується",
"Find recent news about...": "Знайти останні новини про...",
"Get notified about the Public Beta.": "Отримувати повідомлення про публічну бета-версію.",
"Get notified for the public beta": "Отримувати повідомлення про публічну бета-версію",
"Give a quick opinion": "Дати швидкий відгук",
@@ -513,6 +526,7 @@
"Start conversation": "Почати розмову",
"Stop": "Зупинити",
"Summarizing...": "Узагальнення...",
"The Assistant automatically decides when to perform a web search to enrich responses. If disabled, the Assistant will not use the web unless you click the 'Search the web' button.": "Помічник автоматично вирішує, коли виконувати веб-пошук для збагачення відповідей. Якщо вимкнено, помічник не буде використовувати Інтернет, якщо ви не натиснете кнопку «Шукати в Інтернеті».",
"The Assistant is a sovereign AI for public servants. It helps with daily tasks (rephrasing, summarising, translating, information search). Your data stays in France on secure, state-compliant infrastructure and is never used for commercial purposes.": "Помічник — це незалежний штучний інтелект для державних службовців. Він допомагає у виконанні щоденних завдань (перефразування, узагальнення, переклад, пошук інформації). Ваші дані зберігаються у Франції на безпечній інфраструктурі, що відповідає державним вимогам, і ніколи не використовуються в комерційних цілях.",
"The Assistant is in Beta": "Помічник у бета-версії",
"The conversation has been deleted.": "Розмова була видалена.",
@@ -520,6 +534,7 @@
"The summary feature is not supported yet.": "Функція узагальнення ще не підтримується.",
"Thinking...": "Мислення...",
"To add a file to the conversation, drop it here.": "Щоб додати файл до розмови, покладіть його сюди.",
"Turn this list into bullet points": "Перетворити цей список на маркований",
"Unlock access": "Розблокувати доступ",
"Unlocking...": "Розблоковування...",
"Untitled conversation": "Розмова без назви",
@@ -529,6 +544,7 @@
"We'll email you when the public beta opens.": "Коли стане доступна публічна бета-версія, ми надішлемо вам листа.",
"Web": "Інтернет",
"What is on your mind?": "Що у вас на думці?",
"Write a short product description": "Напишіть короткий опис продукту",
"Write on Tchap": "Написати у Tchap",
"You are on the list": "Ви є в списку",
"You do not have permission to view this page.": "У вас немає прав для перегляду цієї сторінки.",
@@ -301,6 +301,40 @@ figure[data-rehype-pretty-code-figure] > pre > code {
display: block;
}
.mainContent-chat table {
display: block;
width: 100%;
margin-top: 32px;
overflow-x: scroll;
font-size: 0.875rem;
}
.mainContent-chat table th {
color: var(--c--contextuals--content--semantic--neutral--secondary);
font-weight: 500;
font-size: 0.75rem;
}
.mainContent-chat table tr,
.mainContent-chat table th {
display: flex;
border-bottom: 1px solid var(--c--contextuals--border--surface--primary);
}
.mainContent-chat tbody tr,
.mainContent-chat thead th {
padding: 8px;
}
.mainContent-chat table tr:last-child {
border-bottom: none;
}
.mainContent-chat table td,
.mainContent-chat table th {
min-width: 200px;
}
.sr-only {
position: absolute;
width: 1px;
-9
View File
@@ -1,9 +0,0 @@
module.exports = {
root: true,
extends: ['conversations/playwright'],
parserOptions: {
tsconfigRootDir: __dirname,
project: ['./tsconfig.json'],
},
ignorePatterns: ['node_modules'],
};
@@ -1,5 +1,7 @@
import { expect, test } from '@playwright/test';
import { overrideConfig } from './common';
test.beforeEach(async ({ page }) => {
await page.goto('/home/');
});
@@ -69,4 +71,73 @@ test.describe('Chat page', () => {
await chatHistoryLink.click();
await expect(messageContent).toBeVisible();
});
test('the user can paste a document into the chat input', async ({
page,
}) => {
await overrideConfig(page, {
FEATURE_FLAGS: {
'document-upload': 'enabled',
'web-search': 'enabled',
},
});
await page.goto('/');
const chatInput = page.getByRole('textbox', {
name: 'Enter your message or a',
});
await expect(chatInput).toBeVisible();
await chatInput.click();
// Create a test file content
const fileContent = 'Test document content for paste';
const fileName = 'test-document.txt';
const fileType = 'text/plain';
// Simulate paste event with file
await page.evaluate(
({ content, name, type }) => {
const textarea = document.querySelector(
'textarea[name="inputchat-textarea"]',
) as HTMLTextAreaElement;
if (!textarea) return;
// Create a File object
const file = new File([content], name, { type });
// Create a DataTransfer object to simulate clipboard
const dataTransfer = new DataTransfer();
dataTransfer.items.add(file);
// Create a paste event - ClipboardEvent constructor doesn't accept clipboardData
// so we create a regular Event and add clipboardData property
const pasteEvent = new Event('paste', {
bubbles: true,
cancelable: true,
}) as unknown as ClipboardEvent;
// Define clipboardData property to make it accessible
Object.defineProperty(pasteEvent, 'clipboardData', {
value: {
files: dataTransfer.files,
items: dataTransfer.items,
types: Array.from(dataTransfer.types),
getData: () => '',
setData: () => {},
},
writable: false,
configurable: true,
});
textarea.dispatchEvent(pasteEvent);
},
{ content: fileContent, name: fileName, type: fileType },
);
// Wait for the file to be processed and appear in the attachment list
// The attachment should be visible with the file name
const attachment = page.getByText(fileName, { exact: false }).first();
await expect(attachment).toBeVisible({ timeout: 5000 });
});
});
@@ -26,7 +26,7 @@ export const CONFIG = {
theme_customization: {},
chat_upload_accept:
'application/vnd.openxmlformats-officedocument.wordprocessingml.document,' +
'application/vnd.openxmlformats-officedocument.presentationml,' +
'application/vnd.openxmlformats-officedocument.presentationml.presentation,' +
'application/vnd.ms-excel,' +
'application/excel,' +
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,' +
+5
View File
@@ -0,0 +1,5 @@
import { playwrightConfig } from 'eslint-config-conversations/playwright.mjs';
export default playwrightConfig({
tsconfigRootDir: import.meta.dirname,
});
+2 -2
View File
@@ -1,9 +1,9 @@
{
"name": "app-e2e",
"version": "0.0.13",
"version": "0.0.14",
"private": true,
"scripts": {
"lint": "eslint . --ext .ts",
"lint": "eslint .",
"install-playwright": "playwright install --with-deps",
"test": "playwright test",
"test:ui": "yarn test --ui",
+4 -3
View File
@@ -1,6 +1,6 @@
{
"name": "conversations",
"version": "0.0.13",
"version": "0.0.14",
"private": true,
"workspaces": {
"packages": [
@@ -31,9 +31,10 @@
"@types/react-dom": "19.1.6",
"@typescript-eslint/eslint-plugin": "8.33.1",
"@typescript-eslint/parser": "8.33.1",
"eslint": "8.57.0",
"eslint": "^9.0.0",
"react": "19.2.1",
"react-dom": "19.2.1",
"typescript": "5.8.3"
"typescript": "5.8.3",
"minimatch": ">=10.2.1"
}
}
@@ -1,21 +0,0 @@
const common = require('./common');
module.exports = {
root: true,
settings: {
react: {
version: 'detect',
},
},
extends: [
'plugin:prettier/recommended',
'plugin:import/recommended',
'plugin:react/recommended',
],
rules: common.globalRules,
parserOptions: {
sourceType: 'module',
ecmaVersion: 'latest',
},
ignorePatterns: ['node_modules'],
};
@@ -1,76 +0,0 @@
const eslintTS = [
{
files: ['*.ts', '*.tsx'],
extends: [
'plugin:@typescript-eslint/recommended',
'plugin:@typescript-eslint/recommended-requiring-type-checking',
],
parser: '@typescript-eslint/parser', // Specifies the ESLint parser
parserOptions: {
tsconfigRootDir: __dirname,
project: ['./tsconfig.json'],
},
rules: {
'@typescript-eslint/no-explicit-any': 'error',
'@typescript-eslint/no-non-null-assertion': 'error',
'@typescript-eslint/no-unused-vars': [
'error',
{ varsIgnorePattern: '^_', argsIgnorePattern: '^_' },
],
'sort-imports': [
'error',
{
ignoreDeclarationSort: true,
},
],
},
},
{
files: ['*.d.ts'],
rules: {
'no-unused-vars': 'off',
},
},
];
const globalRules = {
'block-scoped-var': 'error',
curly: ['error', 'all'],
'import/no-duplicates': ['error', { considerQueryString: false }],
'import/order': [
'error',
{
alphabetize: {
order: 'asc',
},
groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index'],
pathGroups: [
{
pattern: '@/**',
group: 'internal',
},
],
pathGroupsExcludedImportTypes: ['builtin'],
'newlines-between': 'always',
warnOnUnassignedImports: true,
},
],
'no-alert': 'error',
'no-unused-vars': [
'error',
{ varsIgnorePattern: '^_', argsIgnorePattern: '^_' },
],
'no-var': 'error',
'react/jsx-curly-brace-presence': [
'error',
{ props: 'never', children: 'never', propElementValues: 'always' },
],
'sort-imports': [
'error',
{
ignoreDeclarationSort: true,
},
],
};
module.exports = { eslintTS, globalRules };
@@ -0,0 +1,78 @@
import tsPlugin from '@typescript-eslint/eslint-plugin';
import tsParser from '@typescript-eslint/parser';
import importXPlugin from 'eslint-plugin-import-x';
// Base import-x plugin config (included in all profiles)
export const importXConfig = {
plugins: {
'import-x': importXPlugin,
},
};
// Rules shared across all profiles
export const globalRules = {
'block-scoped-var': 'error',
curly: ['error', 'all'],
'import-x/no-duplicates': ['error', { considerQueryString: false }],
'import-x/order': [
'error',
{
alphabetize: { order: 'asc' },
groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index'],
pathGroups: [{ pattern: '@/**', group: 'internal' }],
pathGroupsExcludedImportTypes: ['builtin'],
'newlines-between': 'always',
warnOnUnassignedImports: true,
},
],
'no-alert': 'error',
'no-unused-vars': [
'error',
{ varsIgnorePattern: '^_', argsIgnorePattern: '^_' },
],
'no-var': 'error',
'sort-imports': ['error', { ignoreDeclarationSort: true }],
};
// React-specific global rules (for profiles using React)
export const reactGlobalRules = {
'react/jsx-curly-brace-presence': [
'error',
{ props: 'never', children: 'never', propElementValues: 'always' },
],
};
// TypeScript configs factory - returns flat config array
export function typescriptConfigs(tsconfigRootDir) {
return [
...tsPlugin.configs['flat/recommended-type-checked'],
{
files: ['**/*.{ts,tsx,mts,cts}'],
languageOptions: {
parser: tsParser,
parserOptions: {
projectService: true,
tsconfigRootDir,
},
},
rules: {
'@typescript-eslint/no-explicit-any': 'error',
'@typescript-eslint/no-non-null-assertion': 'error',
'@typescript-eslint/no-unused-vars': [
'error',
{ varsIgnorePattern: '^_', argsIgnorePattern: '^_' },
],
'sort-imports': ['error', { ignoreDeclarationSort: true }],
},
},
{
files: ['**/*.d.ts'],
rules: { 'no-unused-vars': 'off' },
},
// Disable type-checked rules for JS/MJS/CJS files (e.g. eslint.config.mjs)
{
files: ['**/*.{js,mjs,cjs}'],
...tsPlugin.configs['flat/disable-type-checked'],
},
];
}
@@ -0,0 +1,22 @@
import prettierRecommended from 'eslint-plugin-prettier/recommended';
import { globalRules, importXConfig } from './common.mjs';
export default [
{ ignores: ['node_modules/**'] },
// Import-x
importXConfig,
// Base rules for JS/MJS config files
{
languageOptions: {
sourceType: 'module',
ecmaVersion: 'latest',
},
rules: globalRules,
},
// Prettier (last)
prettierRecommended,
];
@@ -1,51 +0,0 @@
const common = require('./common');
module.exports = {
extends: ['plugin:prettier/recommended', 'plugin:react/recommended'],
rules: common.globalRules,
settings: {
react: {
version: 'detect',
},
},
overrides: [
...common.eslintTS,
{
files: ['*.spec.*', '*.test.*', '**/__mock__/**/*'],
plugins: ['jest'],
extends: ['plugin:jest/recommended', 'plugin:testing-library/react'],
rules: {
'@typescript-eslint/ban-types': 'off',
'@typescript-eslint/no-empty-function': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/no-unsafe-argument': 'off',
'@typescript-eslint/no-unsafe-assignment': 'off',
'@typescript-eslint/no-unsafe-call': 'off',
'@typescript-eslint/no-unsafe-member-access': 'off',
'@typescript-eslint/no-unsafe-return': 'off',
'testing-library/no-await-sync-events': [
'error',
{ eventModules: ['fire-event'] },
],
'testing-library/await-async-events': [
'error',
{
eventModule: 'userEvent',
},
],
'testing-library/no-manual-cleanup': 'off',
'@typescript-eslint/no-unused-vars': [
'error',
{ varsIgnorePattern: '^_', argsIgnorePattern: '^_' },
],
'react/display-name': 0,
'jest/expect-expect': 'error',
'@typescript-eslint/unbound-method': 'off',
'jest/unbound-method': 'error',
'react/react-in-jsx-scope': 'off',
},
},
],
ignorePatterns: ['node_modules'],
};
@@ -0,0 +1,90 @@
import jestPlugin from 'eslint-plugin-jest';
import prettierRecommended from 'eslint-plugin-prettier/recommended';
import reactPlugin from 'eslint-plugin-react';
import testingLibraryPlugin from 'eslint-plugin-testing-library';
import {
globalRules,
importXConfig,
reactGlobalRules,
typescriptConfigs,
} from './common.mjs';
const testFilePatterns = ['**/*.spec.*', '**/*.test.*', '**/__mocks__/**/*'];
// Test-specific configs (reused by next.mjs for test file overrides)
export function jestTestConfigs() {
return [
{
files: testFilePatterns,
...jestPlugin.configs['flat/recommended'],
rules: {
...jestPlugin.configs['flat/recommended'].rules,
'@typescript-eslint/no-empty-function': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/no-unsafe-argument': 'off',
'@typescript-eslint/no-unsafe-assignment': 'off',
'@typescript-eslint/no-unsafe-call': 'off',
'@typescript-eslint/no-unsafe-member-access': 'off',
'@typescript-eslint/no-unsafe-return': 'off',
'@typescript-eslint/no-unused-vars': [
'error',
{ varsIgnorePattern: '^_', argsIgnorePattern: '^_' },
],
'@typescript-eslint/unbound-method': 'off',
'jest/expect-expect': 'error',
'jest/unbound-method': 'error',
'react/display-name': 0,
'react/react-in-jsx-scope': 'off',
},
},
{
files: testFilePatterns,
...testingLibraryPlugin.configs['flat/react'],
rules: {
...testingLibraryPlugin.configs['flat/react'].rules,
'testing-library/no-await-sync-events': [
'error',
{ eventModules: ['fire-event'] },
],
'testing-library/await-async-events': [
'error',
{ eventModule: 'userEvent' },
],
'testing-library/no-manual-cleanup': 'off',
},
},
];
}
// Full jest profile (standalone use for packages like i18n)
export function jestConfig(tsconfigRootDir) {
return [
{ ignores: ['node_modules/**'] },
// React
reactPlugin.configs.flat.recommended,
{ settings: { react: { version: 'detect' } } },
// Import-x
importXConfig,
// Base rules
{
rules: {
...globalRules,
...reactGlobalRules,
},
},
// TypeScript
...typescriptConfigs(tsconfigRootDir),
// Test overrides
...jestTestConfigs(),
// Prettier (last)
prettierRecommended,
];
}
@@ -1,40 +0,0 @@
const common = require('./common');
module.exports = {
extends: [
'next',
'plugin:prettier/recommended',
'plugin:@tanstack/eslint-plugin-query/recommended',
'plugin:jsx-a11y/recommended',
],
parserOptions: {
babelOptions: {
presets: [require.resolve('next/babel')],
},
},
settings: {
'jsx-a11y': {
polymorphicPropName: 'as',
components: {
Input: 'input',
Button: 'button',
Box: 'div',
Text: 'span',
Select: 'select',
},
},
},
rules: {
...common.globalRules,
'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': 'error',
},
overrides: [
...common.eslintTS,
{
files: ['*.spec.*', '*.test.*', '**/__mock__/**/*'],
extends: ['conversations/jest'],
},
],
ignorePatterns: ['node_modules'],
};
@@ -0,0 +1,91 @@
import nextPlugin from '@next/eslint-plugin-next';
import queryPlugin from '@tanstack/eslint-plugin-query';
import jsxA11yPlugin from 'eslint-plugin-jsx-a11y';
import prettierRecommended from 'eslint-plugin-prettier/recommended';
import reactPlugin from 'eslint-plugin-react';
import reactHooksPlugin from 'eslint-plugin-react-hooks';
import globals from 'globals';
import {
globalRules,
importXConfig,
reactGlobalRules,
typescriptConfigs,
} from './common.mjs';
import { jestTestConfigs } from './jest.mjs';
export function nextConfig({ tsconfigRootDir, nextRootDir }) {
return [
// Ignores
{
ignores: ['.next/', '.swc/', 'out/', 'coverage/', 'service-worker.js'],
},
// Next.js plugin
nextPlugin.flatConfig.recommended,
// React
reactPlugin.configs.flat.recommended,
reactPlugin.configs.flat['jsx-runtime'],
// React hooks (only classic rules; v7 Compiler rules can be opted-in later)
{
plugins: { 'react-hooks': reactHooksPlugin },
rules: {
'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': 'error',
},
},
// JSX A11y
{
...jsxA11yPlugin.flatConfigs.recommended,
settings: {
...jsxA11yPlugin.flatConfigs.recommended.settings,
'jsx-a11y': {
polymorphicPropName: 'as',
components: {
Input: 'input',
Button: 'button',
Box: 'div',
Text: 'span',
Select: 'select',
},
},
},
},
// Import-x
importXConfig,
// Browser + Node globals + base rules
{
languageOptions: {
globals: {
...globals.browser,
...globals.node,
},
},
settings: {
react: { version: 'detect' },
next: { rootDir: nextRootDir },
},
rules: {
...globalRules,
...reactGlobalRules,
},
},
// TypeScript
...typescriptConfigs(tsconfigRootDir),
// Test files (Jest)
...jestTestConfigs(),
// TanStack Query
...queryPlugin.configs['flat/recommended'],
// Prettier (must be last)
prettierRecommended,
];
}
@@ -1,9 +1,9 @@
{
"name": "eslint-config-conversations",
"version": "0.0.13",
"version": "0.0.14",
"license": "MIT",
"scripts": {
"lint": "eslint --ext .js ."
"lint": "eslint ."
},
"dependencies": {
"@next/eslint-plugin-next": "15.3.3",
@@ -11,14 +11,16 @@
"@typescript-eslint/eslint-plugin": "*",
"@typescript-eslint/parser": "*",
"eslint": "*",
"eslint-config-next": "15.3.3",
"eslint-config-prettier": "10.1.5",
"eslint-plugin-import": "2.31.0",
"eslint-plugin-import-x": "4.16.1",
"eslint-plugin-jest": "28.12.0",
"eslint-plugin-jsx-a11y": "6.10.2",
"eslint-plugin-playwright": "2.2.0",
"eslint-plugin-prettier": "5.4.1",
"eslint-plugin-react": "7.37.5",
"eslint-plugin-react-hooks": "7.0.1",
"eslint-plugin-testing-library": "7.4.0",
"globals": "15.15.0",
"prettier": "3.5.3"
}
}
@@ -1,36 +0,0 @@
const common = require('./common');
module.exports = {
extends: ['next', 'plugin:prettier/recommended'],
settings: {
react: {
version: 'detect',
},
},
parserOptions: {
babelOptions: {
presets: [require.resolve('next/babel')],
},
},
rules: { ...common.globalRules, '@next/next/no-html-link-for-pages': 'off' },
overrides: [
...common.eslintTS,
{
files: ['**/*.ts'],
rules: {
'@typescript-eslint/no-unsafe-member-access': 'off',
},
},
{
files: ['*.spec.*', '*.test.*', '**/__mock__/**/*'],
extends: ['plugin:playwright/recommended'],
plugins: ['playwright'],
rules: {
'@typescript-eslint/no-unsafe-member-access': 'off',
'@typescript-eslint/no-unsafe-assignment': 'off',
'@typescript-eslint/no-non-null-assertion': 'off',
},
},
],
ignorePatterns: ['node_modules'],
};
@@ -0,0 +1,57 @@
import playwrightPlugin from 'eslint-plugin-playwright';
import prettierRecommended from 'eslint-plugin-prettier/recommended';
import reactPlugin from 'eslint-plugin-react';
import globals from 'globals';
import { globalRules, importXConfig, typescriptConfigs } from './common.mjs';
export function playwrightConfig({ tsconfigRootDir }) {
return [
// Ignores
{ ignores: ['coverage/', 'report/', 'screenshots/', 'test-results/'] },
// React (for JSX parsing support)
reactPlugin.configs.flat.recommended,
{ settings: { react: { version: 'detect' } } },
// Import-x
importXConfig,
// Browser + Node globals + base rules
{
languageOptions: {
globals: {
...globals.browser,
...globals.node,
},
},
rules: globalRules,
},
// TypeScript
...typescriptConfigs(tsconfigRootDir),
// Relax TS rules for all TS files in e2e
{
files: ['**/*.ts'],
rules: {
'@typescript-eslint/no-unsafe-member-access': 'off',
},
},
// Playwright test files
{
files: ['**/*.spec.*', '**/*.test.*', '**/__mock__/**/*'],
...playwrightPlugin.configs['flat/recommended'],
rules: {
...playwrightPlugin.configs['flat/recommended'].rules,
'@typescript-eslint/no-unsafe-member-access': 'off',
'@typescript-eslint/no-unsafe-assignment': 'off',
'@typescript-eslint/no-non-null-assertion': 'off',
},
},
// Prettier (last)
prettierRecommended,
];
}
-11
View File
@@ -1,11 +0,0 @@
module.exports = {
root: true,
extends: ['conversations/jest', 'plugin:import/recommended'],
parserOptions: {
sourceType: 'module',
ecmaVersion: 'latest',
tsconfigRootDir: __dirname,
project: ['./tsconfig.json'],
},
ignorePatterns: ['node_modules'],
};
@@ -0,0 +1,3 @@
import { jestConfig } from 'eslint-config-conversations/jest.mjs';
export default jestConfig(import.meta.dirname);
+2 -3
View File
@@ -1,6 +1,6 @@
{
"name": "packages-i18n",
"version": "0.0.13",
"version": "0.0.14",
"private": true,
"scripts": {
"extract-translation": "yarn extract-translation:conversations",
@@ -8,14 +8,13 @@
"format-deploy": "yarn format-deploy:conversations",
"format-deploy:conversations": "node ./format-deploy.mjs --app=conversations --output=../../apps/conversations/src/i18n/translations.json",
"format-rebuild-fr:conversations": "node ./rebuild-translations.mjs --language=fr --app=conversations --output=../../apps/conversations/src/i18n/translations.json",
"lint": "eslint --ext .js,.ts,.mjs .",
"lint": "eslint .",
"test": "jest"
},
"dependencies": {
"@types/jest": "29.5.14",
"@types/node": "*",
"eslint-config-conversations": "*",
"eslint-plugin-import": "2.31.0",
"i18next-parser": "9.3.0",
"jest": "29.7.0",
"ts-jest": "29.3.4",
+238 -339
View File
@@ -255,6 +255,13 @@
dependencies:
"@babel/types" "^7.28.5"
"@babel/parser@^7.24.4":
version "7.29.0"
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.29.0.tgz#669ef345add7d057e92b7ed15f0bac07611831b6"
integrity sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==
dependencies:
"@babel/types" "^7.29.0"
"@babel/plugin-bugfix-firefox-class-in-computed-class-key@^7.28.5":
version "7.28.5"
resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.28.5.tgz#fbde57974707bbfa0376d34d425ff4fa6c732421"
@@ -1024,6 +1031,14 @@
"@babel/helper-string-parser" "^7.27.1"
"@babel/helper-validator-identifier" "^7.28.5"
"@babel/types@^7.29.0":
version "7.29.0"
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.29.0.tgz#9f5b1e838c446e72cf3cd4b918152b8c605e37c7"
integrity sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==
dependencies:
"@babel/helper-string-parser" "^7.27.1"
"@babel/helper-validator-identifier" "^7.28.5"
"@bcoe/v8-coverage@^0.2.3":
version "0.2.3"
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
@@ -1297,37 +1312,73 @@
resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz#9bdad8176be7811ad148d1f8772359041f46c6c5"
integrity sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==
"@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.7.0", "@eslint-community/eslint-utils@^4.9.1":
"@eslint-community/eslint-utils@^4.7.0", "@eslint-community/eslint-utils@^4.8.0", "@eslint-community/eslint-utils@^4.9.1":
version "4.9.1"
resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz#4e90af67bc51ddee6cdef5284edf572ec376b595"
integrity sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==
dependencies:
eslint-visitor-keys "^3.4.3"
"@eslint-community/regexpp@^4.10.0", "@eslint-community/regexpp@^4.6.1":
"@eslint-community/regexpp@^4.10.0", "@eslint-community/regexpp@^4.12.1":
version "4.12.2"
resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.12.2.tgz#bccdf615bcf7b6e8db830ec0b8d21c9a25de597b"
integrity sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==
"@eslint/eslintrc@^2.1.4":
version "2.1.4"
resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.1.4.tgz#388a269f0f25c1b6adc317b5a2c55714894c70ad"
integrity sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==
"@eslint/config-array@^0.21.1":
version "0.21.1"
resolved "https://registry.yarnpkg.com/@eslint/config-array/-/config-array-0.21.1.tgz#7d1b0060fea407f8301e932492ba8c18aff29713"
integrity sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==
dependencies:
"@eslint/object-schema" "^2.1.7"
debug "^4.3.1"
minimatch "^3.1.2"
"@eslint/config-helpers@^0.4.2":
version "0.4.2"
resolved "https://registry.yarnpkg.com/@eslint/config-helpers/-/config-helpers-0.4.2.tgz#1bd006ceeb7e2e55b2b773ab318d300e1a66aeda"
integrity sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==
dependencies:
"@eslint/core" "^0.17.0"
"@eslint/core@^0.17.0":
version "0.17.0"
resolved "https://registry.yarnpkg.com/@eslint/core/-/core-0.17.0.tgz#77225820413d9617509da9342190a2019e78761c"
integrity sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==
dependencies:
"@types/json-schema" "^7.0.15"
"@eslint/eslintrc@^3.3.1":
version "3.3.3"
resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-3.3.3.tgz#26393a0806501b5e2b6a43aa588a4d8df67880ac"
integrity sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==
dependencies:
ajv "^6.12.4"
debug "^4.3.2"
espree "^9.6.0"
globals "^13.19.0"
espree "^10.0.1"
globals "^14.0.0"
ignore "^5.2.0"
import-fresh "^3.2.1"
js-yaml "^4.1.0"
js-yaml "^4.1.1"
minimatch "^3.1.2"
strip-json-comments "^3.1.1"
"@eslint/js@8.57.0":
version "8.57.0"
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.57.0.tgz#a5417ae8427873f1dd08b70b3574b453e67b5f7f"
integrity sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==
"@eslint/js@9.39.2":
version "9.39.2"
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.39.2.tgz#2d4b8ec4c3ea13c1b3748e0c97ecd766bdd80599"
integrity sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==
"@eslint/object-schema@^2.1.7":
version "2.1.7"
resolved "https://registry.yarnpkg.com/@eslint/object-schema/-/object-schema-2.1.7.tgz#6e2126a1347e86a4dedf8706ec67ff8e107ebbad"
integrity sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==
"@eslint/plugin-kit@^0.4.1":
version "0.4.1"
resolved "https://registry.yarnpkg.com/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz#9779e3fd9b7ee33571a57435cf4335a1794a6cb2"
integrity sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==
dependencies:
"@eslint/core" "^0.17.0"
levn "^0.4.1"
"@fontsource-variable/roboto-flex@5.2.5":
version "5.2.5"
@@ -1457,24 +1508,28 @@
dependencies:
is-negated-glob "^1.0.0"
"@humanwhocodes/config-array@^0.11.14":
version "0.11.14"
resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.14.tgz#d78e481a039f7566ecc9660b4ea7fe6b1fec442b"
integrity sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==
"@humanfs/core@^0.19.1":
version "0.19.1"
resolved "https://registry.yarnpkg.com/@humanfs/core/-/core-0.19.1.tgz#17c55ca7d426733fe3c561906b8173c336b40a77"
integrity sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==
"@humanfs/node@^0.16.6":
version "0.16.7"
resolved "https://registry.yarnpkg.com/@humanfs/node/-/node-0.16.7.tgz#822cb7b3a12c5a240a24f621b5a2413e27a45f26"
integrity sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==
dependencies:
"@humanwhocodes/object-schema" "^2.0.2"
debug "^4.3.1"
minimatch "^3.0.5"
"@humanfs/core" "^0.19.1"
"@humanwhocodes/retry" "^0.4.0"
"@humanwhocodes/module-importer@^1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz#af5b2691a22b44be847b0ca81641c5fb6ad0172c"
integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==
"@humanwhocodes/object-schema@^2.0.2":
version "2.0.3"
resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz#4a2868d75d6d6963e423bcf90b7fd1be343409d3"
integrity sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==
"@humanwhocodes/retry@^0.4.0", "@humanwhocodes/retry@^0.4.2":
version "0.4.3"
resolved "https://registry.yarnpkg.com/@humanwhocodes/retry/-/retry-0.4.3.tgz#c2b9d2e374ee62c586d3adbea87199b1d7a7a6ba"
integrity sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==
"@img/colour@^1.0.0":
version "1.0.0"
@@ -2016,7 +2071,7 @@
resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b"
integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==
"@nodelib/fs.walk@^1.2.3", "@nodelib/fs.walk@^1.2.8":
"@nodelib/fs.walk@^1.2.3":
version "1.2.8"
resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a"
integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==
@@ -2024,11 +2079,6 @@
"@nodelib/fs.scandir" "2.1.5"
fastq "^1.6.0"
"@nolyfill/is-core-module@1.0.39":
version "1.0.39"
resolved "https://registry.yarnpkg.com/@nolyfill/is-core-module/-/is-core-module-1.0.39.tgz#3dc35ba0f1e66b403c00b39344f870298ebb1c8e"
integrity sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==
"@openfun/cunningham-react@4.0.0":
version "4.0.0"
resolved "https://registry.yarnpkg.com/@openfun/cunningham-react/-/cunningham-react-4.0.0.tgz#5d273c72a2ced76cf060931b546238b7ee75c6ec"
@@ -4025,16 +4075,6 @@
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.35.0.tgz#05f25dbc9981bee1ae6e713daab10397044a46ca"
integrity sha512-PIQeY5XDkrOysbQblSW7v3l1MDZzkTEzAfTPkj5VAu3FW8fS4ynyLg2sINp0fp3SjZ8xkRYpLqoKcYqAkhU1dw==
"@rtsao/scc@^1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@rtsao/scc/-/scc-1.1.0.tgz#927dd2fae9bc3361403ac2c7a00c32ddce9ad7e8"
integrity sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==
"@rushstack/eslint-patch@^1.10.3":
version "1.15.0"
resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.15.0.tgz#8184bcb37791e6d3c3c13a9bfbe4af263f66665f"
integrity sha512-ojSshQPKwVvSMR8yT2L/QtUkV5SXi/IfDiJ4/8d6UbTPjiHVmxZzUAzGD8Tzks1b9+qQkZa0isUOvYObedITaw==
"@sentry-internal/browser-utils@9.26.0":
version "9.26.0"
resolved "https://registry.yarnpkg.com/@sentry-internal/browser-utils/-/browser-utils-9.26.0.tgz#b6ea902e9bed1954451c5738c0374d49f59271ef"
@@ -4731,11 +4771,6 @@
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841"
integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==
"@types/json5@^0.0.29":
version "0.0.29"
resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==
"@types/katex@^0.16.0":
version "0.16.7"
resolved "https://registry.yarnpkg.com/@types/katex/-/katex-0.16.7.tgz#03ab680ab4fa4fbc6cb46ecf987ecad5d8019868"
@@ -4902,7 +4937,7 @@
dependencies:
"@types/yargs-parser" "*"
"@typescript-eslint/eslint-plugin@*", "@typescript-eslint/eslint-plugin@8.33.1", "@typescript-eslint/eslint-plugin@^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0":
"@typescript-eslint/eslint-plugin@*", "@typescript-eslint/eslint-plugin@8.33.1":
version "8.33.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.33.1.tgz#532641b416ed2afd5be893cddb2a58e9cd1f7a3e"
integrity sha512-TDCXj+YxLgtvxvFlAvpoRv9MAncDLBV2oT9Bd7YBGC/b/sEURoOYuIwLI99rjWOfY3QtDzO+mk0n4AmdFExW8A==
@@ -4917,7 +4952,7 @@
natural-compare "^1.4.0"
ts-api-utils "^2.1.0"
"@typescript-eslint/parser@*", "@typescript-eslint/parser@8.33.1", "@typescript-eslint/parser@^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0":
"@typescript-eslint/parser@*", "@typescript-eslint/parser@8.33.1":
version "8.33.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.33.1.tgz#ef9a5ee6aa37a6b4f46cc36d08a14f828238afe2"
integrity sha512-qwxv6dq682yVvgKKp2qWwLgRbscDAYktPptK4JPojCwwi3R9cwrvIxS4lvBpzmcqzR4bdn54Z0IG1uHFskW4dA==
@@ -5002,6 +5037,11 @@
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.53.1.tgz#101f203f0807a63216cceceedb815fabe21d5793"
integrity sha512-jr/swrr2aRmUAUjW5/zQHbMaui//vQlsZcJKijZf3M26bnmLj8LyZUpj8/Rd6uzaek06OWsqdofN/Thenm5O8A==
"@typescript-eslint/types@^8.35.0":
version "8.56.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.56.0.tgz#a2444011b9a98ca13d70411d2cbfed5443b3526a"
integrity sha512-DBsLPs3GsWhX5HylbP9HNG15U0bnwut55Lx12bHB9MpXxQ+R5GC8MwQe+N1UFXxAeQDvEsEDY6ZYwX03K7Z6HQ==
"@typescript-eslint/typescript-estree@8.33.1":
version "8.33.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.33.1.tgz#d271beed470bc915b8764e22365d4925c2ea265d"
@@ -5069,7 +5109,7 @@
"@typescript-eslint/types" "8.52.0"
eslint-visitor-keys "^4.2.1"
"@ungap/structured-clone@^1.0.0", "@ungap/structured-clone@^1.2.0":
"@ungap/structured-clone@^1.0.0":
version "1.3.0"
resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.3.0.tgz#d06bbb384ebcf6c505fde1c3d0ed4ddffe0aaff8"
integrity sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==
@@ -5332,7 +5372,7 @@ acorn-walk@^8.0.2, acorn-walk@^8.1.1:
dependencies:
acorn "^8.11.0"
acorn@^8.1.0, acorn@^8.11.0, acorn@^8.14.0, acorn@^8.15.0, acorn@^8.4.1, acorn@^8.8.1, acorn@^8.9.0:
acorn@^8.1.0, acorn@^8.11.0, acorn@^8.14.0, acorn@^8.15.0, acorn@^8.4.1, acorn@^8.8.1:
version "8.15.0"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.15.0.tgz#a360898bc415edaac46c8241f6383975b930b816"
integrity sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==
@@ -5464,7 +5504,7 @@ array-buffer-byte-length@^1.0.1, array-buffer-byte-length@^1.0.2:
call-bound "^1.0.3"
is-array-buffer "^3.0.5"
array-includes@^3.1.6, array-includes@^3.1.8, array-includes@^3.1.9:
array-includes@^3.1.6, array-includes@^3.1.8:
version "3.1.9"
resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.9.tgz#1f0ccaa08e90cdbc3eb433210f903ad0f17c3f3a"
integrity sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==
@@ -5495,20 +5535,7 @@ array.prototype.findlast@^1.2.5:
es-object-atoms "^1.0.0"
es-shim-unscopables "^1.0.2"
array.prototype.findlastindex@^1.2.5, array.prototype.findlastindex@^1.2.6:
version "1.2.6"
resolved "https://registry.yarnpkg.com/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz#cfa1065c81dcb64e34557c9b81d012f6a421c564"
integrity sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==
dependencies:
call-bind "^1.0.8"
call-bound "^1.0.4"
define-properties "^1.2.1"
es-abstract "^1.23.9"
es-errors "^1.3.0"
es-object-atoms "^1.1.1"
es-shim-unscopables "^1.1.0"
array.prototype.flat@^1.3.1, array.prototype.flat@^1.3.2, array.prototype.flat@^1.3.3:
array.prototype.flat@^1.3.1:
version "1.3.3"
resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz#534aaf9e6e8dd79fb6b9a9917f839ef1ec63afe5"
integrity sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==
@@ -5696,16 +5723,16 @@ bail@^2.0.0:
resolved "https://registry.yarnpkg.com/bail/-/bail-2.0.2.tgz#d26f5cd8fe5d6f832a31517b9f7c356040ba6d5d"
integrity sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==
balanced-match@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
balanced-match@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-2.0.0.tgz#dc70f920d78db8b858535795867bf48f820633d9"
integrity sha512-1ugUSr8BHXRnK23KfuYS+gVMC3LB8QGH9W1iGtDPsNWoQbgtXSExkBu2aDR4epiGWZOjZsj6lDl/N/AqqTC3UA==
balanced-match@^4.0.2:
version "4.0.3"
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-4.0.3.tgz#6337a2f23e0604a30481423432f99eac603599f9"
integrity sha512-1pHv8LX9CpKut1Zp4EXey7Z8OfH11ONNH6Dhi2WDUt31VVZFXZzKwXcysBgqSumFCmR+0dqjMK5v5JiFHzi0+g==
bare-events@^2.7.0:
version "2.8.2"
resolved "https://registry.yarnpkg.com/bare-events/-/bare-events-2.8.2.tgz#7b3e10bd8e1fc80daf38bb516921678f566ab89f"
@@ -5745,20 +5772,12 @@ boolbase@^1.0.0:
resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e"
integrity sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==
brace-expansion@^1.1.7:
version "1.1.12"
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.12.tgz#ab9b454466e5a8cc3a187beaad580412a9c5b843"
integrity sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==
brace-expansion@^5.0.2:
version "5.0.2"
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-5.0.2.tgz#b6c16d0791087af6c2bc463f52a8142046c06b6f"
integrity sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw==
dependencies:
balanced-match "^1.0.0"
concat-map "0.0.1"
brace-expansion@^2.0.1:
version "2.0.2"
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.2.tgz#54fc53237a613d854c7bd37463aad17df87214e7"
integrity sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==
dependencies:
balanced-match "^1.0.0"
balanced-match "^4.0.2"
braces@^3.0.3, braces@~3.0.2:
version "3.0.3"
@@ -6154,6 +6173,11 @@ commander@^8.3.0:
resolved "https://registry.yarnpkg.com/commander/-/commander-8.3.0.tgz#4837ea1b2da67b9c616a67afbb0fafee567bca66"
integrity sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==
comment-parser@^1.4.1:
version "1.4.5"
resolved "https://registry.yarnpkg.com/comment-parser/-/comment-parser-1.4.5.tgz#6c595cd090737a1010fe5ff40d86e1d21b7bd6ce"
integrity sha512-aRDkn3uyIlCFfk5NUA+VdwMmMsh8JGhc4hapfV4yxymHGQ3BVskMQfoXGpCo5IoBuQ9tS5iiVKhCpTcB4pW4qw==
common-tags@^1.8.0:
version "1.8.2"
resolved "https://registry.yarnpkg.com/common-tags/-/common-tags-1.8.2.tgz#94ebb3c076d26032745fd54face7f688ef5ac9c6"
@@ -6169,11 +6193,6 @@ compute-scroll-into-view@^3.1.0:
resolved "https://registry.yarnpkg.com/compute-scroll-into-view/-/compute-scroll-into-view-3.1.1.tgz#02c3386ec531fb6a9881967388e53e8564f3e9aa"
integrity sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw==
concat-map@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==
convert-source-map@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a"
@@ -6253,7 +6272,7 @@ cross-env@7.0.3:
dependencies:
cross-spawn "^7.0.1"
cross-spawn@^7.0.1, cross-spawn@^7.0.2, cross-spawn@^7.0.3, cross-spawn@^7.0.6:
cross-spawn@^7.0.1, cross-spawn@^7.0.3, cross-spawn@^7.0.6:
version "7.0.6"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f"
integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==
@@ -6411,7 +6430,7 @@ data-view-byte-offset@^1.0.1:
es-errors "^1.3.0"
is-data-view "^1.0.1"
debug@4, debug@^4.0.0, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@^4.3.5, debug@^4.4.0, debug@^4.4.1, debug@^4.4.3:
debug@4, debug@^4.0.0, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@^4.3.5, debug@^4.4.1, debug@^4.4.3:
version "4.4.3"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.3.tgz#c6ae432d9bd9662582fce08709b038c58e9e3d6a"
integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==
@@ -6425,7 +6444,7 @@ debug@^2.2.0:
dependencies:
ms "2.0.0"
debug@^3.1.0, debug@^3.2.7:
debug@^3.1.0:
version "3.2.7"
resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a"
integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==
@@ -6542,13 +6561,6 @@ doctrine@^2.1.0:
dependencies:
esutils "^2.0.2"
doctrine@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961"
integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==
dependencies:
esutils "^2.0.2"
dom-accessibility-api@^0.5.9:
version "0.5.16"
resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz#5a7429e6066eb3664d911e33fb0e45de8eb08453"
@@ -6838,7 +6850,7 @@ es-set-tostringtag@^2.1.0:
has-tostringtag "^1.0.2"
hasown "^2.0.2"
es-shim-unscopables@^1.0.2, es-shim-unscopables@^1.1.0:
es-shim-unscopables@^1.0.2:
version "1.1.0"
resolved "https://registry.yarnpkg.com/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz#438df35520dac5d105f3943d927549ea3b00f4b5"
integrity sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==
@@ -6917,105 +6929,33 @@ escodegen@^2.0.0:
optionalDependencies:
source-map "~0.6.1"
eslint-config-next@15.3.3:
version "15.3.3"
resolved "https://registry.yarnpkg.com/eslint-config-next/-/eslint-config-next-15.3.3.tgz#356ca4b8647a6024c332260addcd261d3b4b6c6d"
integrity sha512-QJLv/Ouk2vZnxL4b67njJwTLjTf7uZRltI0LL4GERYR4qMF5z08+gxkfODAeaK7TiC6o+cER91bDaEnwrTWV6Q==
dependencies:
"@next/eslint-plugin-next" "15.3.3"
"@rushstack/eslint-patch" "^1.10.3"
"@typescript-eslint/eslint-plugin" "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0"
"@typescript-eslint/parser" "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0"
eslint-import-resolver-node "^0.3.6"
eslint-import-resolver-typescript "^3.5.2"
eslint-plugin-import "^2.31.0"
eslint-plugin-jsx-a11y "^6.10.0"
eslint-plugin-react "^7.37.0"
eslint-plugin-react-hooks "^5.0.0"
eslint-config-prettier@10.1.5:
version "10.1.5"
resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-10.1.5.tgz#00c18d7225043b6fbce6a665697377998d453782"
integrity sha512-zc1UmCpNltmVY34vuLRV61r1K27sWuX39E+uyUnY8xS2Bex88VV9cugG+UZbRSRGtGyFboj+D8JODyme1plMpw==
eslint-import-resolver-node@^0.3.6, eslint-import-resolver-node@^0.3.9:
version "0.3.9"
resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz#d4eaac52b8a2e7c3cd1903eb00f7e053356118ac"
integrity sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==
eslint-import-context@^0.1.9:
version "0.1.9"
resolved "https://registry.yarnpkg.com/eslint-import-context/-/eslint-import-context-0.1.9.tgz#967b0b2f0a90ef4b689125e088f790f0b7756dbe"
integrity sha512-K9Hb+yRaGAGUbwjhFNHvSmmkZs9+zbuoe3kFQ4V1wYjrepUFYM2dZAfNtjbbj3qsPfUfsA68Bx/ICWQMi+C8Eg==
dependencies:
debug "^3.2.7"
is-core-module "^2.13.0"
resolve "^1.22.4"
get-tsconfig "^4.10.1"
stable-hash-x "^0.2.0"
eslint-import-resolver-typescript@^3.5.2:
version "3.10.1"
resolved "https://registry.yarnpkg.com/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.10.1.tgz#23dac32efa86a88e2b8232eb244ac499ad636db2"
integrity sha512-A1rHYb06zjMGAxdLSkN2fXPBwuSaQ0iO5M/hdyS0Ajj1VBaRp0sPD3dn1FhME3c/JluGFbwSxyCfqdSbtQLAHQ==
eslint-plugin-import-x@4.16.1:
version "4.16.1"
resolved "https://registry.yarnpkg.com/eslint-plugin-import-x/-/eslint-plugin-import-x-4.16.1.tgz#a96ee1ad5ba6816f9a5573a9617935011a24c4df"
integrity sha512-vPZZsiOKaBAIATpFE2uMI4w5IRwdv/FpQ+qZZMR4E+PeOcM4OeoEbqxRMnywdxP19TyB/3h6QBB0EWon7letSQ==
dependencies:
"@nolyfill/is-core-module" "1.0.39"
debug "^4.4.0"
get-tsconfig "^4.10.0"
is-bun-module "^2.0.0"
stable-hash "^0.0.5"
tinyglobby "^0.2.13"
unrs-resolver "^1.6.2"
eslint-module-utils@^2.12.0, eslint-module-utils@^2.12.1:
version "2.12.1"
resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz#f76d3220bfb83c057651359295ab5854eaad75ff"
integrity sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==
dependencies:
debug "^3.2.7"
eslint-plugin-import@2.31.0:
version "2.31.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.31.0.tgz#310ce7e720ca1d9c0bb3f69adfd1c6bdd7d9e0e7"
integrity sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==
dependencies:
"@rtsao/scc" "^1.1.0"
array-includes "^3.1.8"
array.prototype.findlastindex "^1.2.5"
array.prototype.flat "^1.3.2"
array.prototype.flatmap "^1.3.2"
debug "^3.2.7"
doctrine "^2.1.0"
eslint-import-resolver-node "^0.3.9"
eslint-module-utils "^2.12.0"
hasown "^2.0.2"
is-core-module "^2.15.1"
"@typescript-eslint/types" "^8.35.0"
comment-parser "^1.4.1"
debug "^4.4.1"
eslint-import-context "^0.1.9"
is-glob "^4.0.3"
minimatch "^3.1.2"
object.fromentries "^2.0.8"
object.groupby "^1.0.3"
object.values "^1.2.0"
semver "^6.3.1"
string.prototype.trimend "^1.0.8"
tsconfig-paths "^3.15.0"
eslint-plugin-import@^2.31.0:
version "2.32.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz#602b55faa6e4caeaa5e970c198b5c00a37708980"
integrity sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==
dependencies:
"@rtsao/scc" "^1.1.0"
array-includes "^3.1.9"
array.prototype.findlastindex "^1.2.6"
array.prototype.flat "^1.3.3"
array.prototype.flatmap "^1.3.3"
debug "^3.2.7"
doctrine "^2.1.0"
eslint-import-resolver-node "^0.3.9"
eslint-module-utils "^2.12.1"
hasown "^2.0.2"
is-core-module "^2.16.1"
is-glob "^4.0.3"
minimatch "^3.1.2"
object.fromentries "^2.0.8"
object.groupby "^1.0.3"
object.values "^1.2.1"
semver "^6.3.1"
string.prototype.trimend "^1.0.9"
tsconfig-paths "^3.15.0"
minimatch "^9.0.3 || ^10.0.1"
semver "^7.7.2"
stable-hash-x "^0.2.0"
unrs-resolver "^1.9.2"
eslint-plugin-jest@28.12.0:
version "28.12.0"
@@ -7024,7 +6964,7 @@ eslint-plugin-jest@28.12.0:
dependencies:
"@typescript-eslint/utils" "^6.0.0 || ^7.0.0 || ^8.0.0"
eslint-plugin-jsx-a11y@6.10.2, eslint-plugin-jsx-a11y@^6.10.0:
eslint-plugin-jsx-a11y@6.10.2:
version "6.10.2"
resolved "https://registry.yarnpkg.com/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.2.tgz#d2812bb23bf1ab4665f1718ea442e8372e638483"
integrity sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==
@@ -7060,12 +7000,18 @@ eslint-plugin-prettier@5.4.1:
prettier-linter-helpers "^1.0.0"
synckit "^0.11.7"
eslint-plugin-react-hooks@^5.0.0:
version "5.2.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz#1be0080901e6ac31ce7971beed3d3ec0a423d9e3"
integrity sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==
eslint-plugin-react-hooks@7.0.1:
version "7.0.1"
resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz#66e258db58ece50723ef20cc159f8aa908219169"
integrity sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==
dependencies:
"@babel/core" "^7.24.4"
"@babel/parser" "^7.24.4"
hermes-parser "^0.25.1"
zod "^3.25.0 || ^4.0.0"
zod-validation-error "^3.5.0 || ^4.0.0"
eslint-plugin-react@^7.37.0:
eslint-plugin-react@7.37.5:
version "7.37.5"
resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz#2975511472bdda1b272b34d779335c9b0e877065"
integrity sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==
@@ -7105,15 +7051,15 @@ eslint-scope@5.1.1:
esrecurse "^4.3.0"
estraverse "^4.1.1"
eslint-scope@^7.2.2:
version "7.2.2"
resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.2.2.tgz#deb4f92563390f32006894af62a22dba1c46423f"
integrity sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==
eslint-scope@^8.4.0:
version "8.4.0"
resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-8.4.0.tgz#88e646a207fad61436ffa39eb505147200655c82"
integrity sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==
dependencies:
esrecurse "^4.3.0"
estraverse "^5.2.0"
eslint-visitor-keys@^3.4.1, eslint-visitor-keys@^3.4.3:
eslint-visitor-keys@^3.4.3:
version "3.4.3"
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800"
integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==
@@ -7123,65 +7069,61 @@ eslint-visitor-keys@^4.2.0, eslint-visitor-keys@^4.2.1:
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz#4cfea60fe7dd0ad8e816e1ed026c1d5251b512c1"
integrity sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==
eslint@*, eslint@8.57.0:
version "8.57.0"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.57.0.tgz#c786a6fd0e0b68941aaf624596fb987089195668"
integrity sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==
eslint@*, eslint@^9.0.0:
version "9.39.2"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-9.39.2.tgz#cb60e6d16ab234c0f8369a3fe7cc87967faf4b6c"
integrity sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==
dependencies:
"@eslint-community/eslint-utils" "^4.2.0"
"@eslint-community/regexpp" "^4.6.1"
"@eslint/eslintrc" "^2.1.4"
"@eslint/js" "8.57.0"
"@humanwhocodes/config-array" "^0.11.14"
"@eslint-community/eslint-utils" "^4.8.0"
"@eslint-community/regexpp" "^4.12.1"
"@eslint/config-array" "^0.21.1"
"@eslint/config-helpers" "^0.4.2"
"@eslint/core" "^0.17.0"
"@eslint/eslintrc" "^3.3.1"
"@eslint/js" "9.39.2"
"@eslint/plugin-kit" "^0.4.1"
"@humanfs/node" "^0.16.6"
"@humanwhocodes/module-importer" "^1.0.1"
"@nodelib/fs.walk" "^1.2.8"
"@ungap/structured-clone" "^1.2.0"
"@humanwhocodes/retry" "^0.4.2"
"@types/estree" "^1.0.6"
ajv "^6.12.4"
chalk "^4.0.0"
cross-spawn "^7.0.2"
cross-spawn "^7.0.6"
debug "^4.3.2"
doctrine "^3.0.0"
escape-string-regexp "^4.0.0"
eslint-scope "^7.2.2"
eslint-visitor-keys "^3.4.3"
espree "^9.6.1"
esquery "^1.4.2"
eslint-scope "^8.4.0"
eslint-visitor-keys "^4.2.1"
espree "^10.4.0"
esquery "^1.5.0"
esutils "^2.0.2"
fast-deep-equal "^3.1.3"
file-entry-cache "^6.0.1"
file-entry-cache "^8.0.0"
find-up "^5.0.0"
glob-parent "^6.0.2"
globals "^13.19.0"
graphemer "^1.4.0"
ignore "^5.2.0"
imurmurhash "^0.1.4"
is-glob "^4.0.0"
is-path-inside "^3.0.3"
js-yaml "^4.1.0"
json-stable-stringify-without-jsonify "^1.0.1"
levn "^0.4.1"
lodash.merge "^4.6.2"
minimatch "^3.1.2"
natural-compare "^1.4.0"
optionator "^0.9.3"
strip-ansi "^6.0.1"
text-table "^0.2.0"
espree@^9.6.0, espree@^9.6.1:
version "9.6.1"
resolved "https://registry.yarnpkg.com/espree/-/espree-9.6.1.tgz#a2a17b8e434690a5432f2f8018ce71d331a48c6f"
integrity sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==
espree@^10.0.1, espree@^10.4.0:
version "10.4.0"
resolved "https://registry.yarnpkg.com/espree/-/espree-10.4.0.tgz#d54f4949d4629005a1fa168d937c3ff1f7e2a837"
integrity sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==
dependencies:
acorn "^8.9.0"
acorn "^8.15.0"
acorn-jsx "^5.3.2"
eslint-visitor-keys "^3.4.1"
eslint-visitor-keys "^4.2.1"
esprima@^4.0.0, esprima@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71"
integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==
esquery@^1.4.2:
esquery@^1.5.0:
version "1.7.0"
resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.7.0.tgz#08d048f261f0ddedb5bae95f46809463d9c9496d"
integrity sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==
@@ -7387,12 +7329,12 @@ file-entry-cache@^10.1.0:
dependencies:
flat-cache "^6.1.13"
file-entry-cache@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027"
integrity sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==
file-entry-cache@^8.0.0:
version "8.0.0"
resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-8.0.0.tgz#7787bddcf1131bffb92636c69457bbc0edd6d81f"
integrity sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==
dependencies:
flat-cache "^3.0.4"
flat-cache "^4.0.0"
filelist@^1.0.4:
version "1.0.4"
@@ -7424,14 +7366,13 @@ find-up@^5.0.0:
locate-path "^6.0.0"
path-exists "^4.0.0"
flat-cache@^3.0.4:
version "3.2.0"
resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.2.0.tgz#2c0c2d5040c99b1632771a9d105725c0115363ee"
integrity sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==
flat-cache@^4.0.0:
version "4.0.1"
resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-4.0.1.tgz#0ece39fcb14ee012f4b0410bd33dd9c1f011127c"
integrity sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==
dependencies:
flatted "^3.2.9"
keyv "^4.5.3"
rimraf "^3.0.2"
keyv "^4.5.4"
flat-cache@^6.1.13:
version "6.1.19"
@@ -7646,10 +7587,10 @@ get-symbol-description@^1.1.0:
es-errors "^1.3.0"
get-intrinsic "^1.2.6"
get-tsconfig@^4.10.0:
version "4.13.0"
resolved "https://registry.yarnpkg.com/get-tsconfig/-/get-tsconfig-4.13.0.tgz#fcdd991e6d22ab9a600f00e91c318707a5d9a0d7"
integrity sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==
get-tsconfig@^4.10.1:
version "4.13.6"
resolved "https://registry.yarnpkg.com/get-tsconfig/-/get-tsconfig-4.13.6.tgz#2fbfda558a98a691a798f123afd95915badce876"
integrity sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==
dependencies:
resolve-pkg-maps "^1.0.0"
@@ -7736,13 +7677,23 @@ global-prefix@^3.0.0:
kind-of "^6.0.2"
which "^1.3.1"
globals@^13.19.0, globals@^13.23.0:
globals@15.15.0:
version "15.15.0"
resolved "https://registry.yarnpkg.com/globals/-/globals-15.15.0.tgz#7c4761299d41c32b075715a4ce1ede7897ff72a8"
integrity sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==
globals@^13.23.0:
version "13.24.0"
resolved "https://registry.yarnpkg.com/globals/-/globals-13.24.0.tgz#8432a19d78ce0c1e833949c36adb345400bb1171"
integrity sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==
dependencies:
type-fest "^0.20.2"
globals@^14.0.0:
version "14.0.0"
resolved "https://registry.yarnpkg.com/globals/-/globals-14.0.0.tgz#898d7413c29babcf6bafe56fcadded858ada724e"
integrity sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==
globalthis@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/globalthis/-/globalthis-1.0.4.tgz#7430ed3a975d97bfb59bcce41f5cabbafa651236"
@@ -7987,6 +7938,18 @@ heimdalljs@^0.2.6:
dependencies:
rsvp "~3.2.1"
hermes-estree@0.25.1:
version "0.25.1"
resolved "https://registry.yarnpkg.com/hermes-estree/-/hermes-estree-0.25.1.tgz#6aeec17d1983b4eabf69721f3aa3eb705b17f480"
integrity sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==
hermes-parser@^0.25.1:
version "0.25.1"
resolved "https://registry.yarnpkg.com/hermes-parser/-/hermes-parser-0.25.1.tgz#5be0e487b2090886c62bd8a11724cd766d5f54d1"
integrity sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==
dependencies:
hermes-estree "0.25.1"
hoist-non-react-statics@^3.3.2:
version "3.3.2"
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
@@ -8279,19 +8242,12 @@ is-boolean-object@^1.2.1:
call-bound "^1.0.3"
has-tostringtag "^1.0.2"
is-bun-module@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/is-bun-module/-/is-bun-module-2.0.0.tgz#4d7859a87c0fcac950c95e666730e745eae8bddd"
integrity sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==
dependencies:
semver "^7.7.1"
is-callable@^1.2.7:
version "1.2.7"
resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055"
integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==
is-core-module@^2.13.0, is-core-module@^2.15.1, is-core-module@^2.16.1:
is-core-module@^2.13.0, is-core-module@^2.16.1:
version "2.16.1"
resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.16.1.tgz#2a98801a849f43e2add644fbb6bc6229b19a4ef4"
integrity sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==
@@ -8403,11 +8359,6 @@ is-obj@^1.0.1:
resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f"
integrity sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==
is-path-inside@^3.0.3:
version "3.0.3"
resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283"
integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==
is-plain-obj@^4.0.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-4.1.0.tgz#d65025edec3657ce032fd7db63c97883eaed71f0"
@@ -9008,7 +8959,7 @@ js-yaml@^3.13.1:
argparse "^1.0.7"
esprima "^4.0.0"
js-yaml@^4.1.0:
js-yaml@^4.1.0, js-yaml@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.1.tgz#854c292467705b699476e1a2decc0c8a3458806b"
integrity sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==
@@ -9082,13 +9033,6 @@ json-stable-stringify-without-jsonify@^1.0.1:
resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651"
integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==
json5@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.2.tgz#63d98d60f21b313b77c4d6da18bfa69d80e1d593"
integrity sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==
dependencies:
minimist "^1.2.0"
json5@^2.2.0, json5@^2.2.3:
version "2.2.3"
resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283"
@@ -9132,7 +9076,7 @@ katex@^0.16.0:
dependencies:
commander "^8.3.0"
keyv@^4.5.3:
keyv@^4.5.4:
version "4.5.4"
resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93"
integrity sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==
@@ -9933,38 +9877,12 @@ min-indent@^1.0.0:
resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869"
integrity sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==
minimatch@^3.0.2, minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2:
version "3.1.2"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b"
integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==
minimatch@>=10.2.1, minimatch@^3.0.2, minimatch@^3.0.4, minimatch@^3.1.1, minimatch@^3.1.2, minimatch@^5.0.1, minimatch@^8.0.2, minimatch@^9.0.0, "minimatch@^9.0.3 || ^10.0.1", minimatch@^9.0.4, minimatch@^9.0.5:
version "10.2.2"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-10.2.2.tgz#361603ee323cfb83496fea2ae17cc44ea4e1f99f"
integrity sha512-+G4CpNBxa5MprY+04MbgOw1v7So6n5JY166pFi9KfYwT78fxScCeSNQSNzp6dpPSW2rONOps6Ocam1wFhCgoVw==
dependencies:
brace-expansion "^1.1.7"
minimatch@^5.0.1:
version "5.1.6"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.6.tgz#1cfcb8cf5522ea69952cd2af95ae09477f122a96"
integrity sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==
dependencies:
brace-expansion "^2.0.1"
minimatch@^8.0.2:
version "8.0.4"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-8.0.4.tgz#847c1b25c014d4e9a7f68aaf63dedd668a626229"
integrity sha512-W0Wvr9HyFXZRGIDgCicunpQ299OKXs9RgZfaukz4qAW/pJhcpUfupc9c+OObPOFueNy8VSrZgEmDtk6Kh4WzDA==
dependencies:
brace-expansion "^2.0.1"
minimatch@^9.0.0, minimatch@^9.0.4, minimatch@^9.0.5:
version "9.0.5"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.5.tgz#d74f9dd6b57d83d8e98cfb82133b03978bc929e5"
integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==
dependencies:
brace-expansion "^2.0.1"
minimist@^1.2.0, minimist@^1.2.6:
version "1.2.8"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c"
integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==
brace-expansion "^5.0.2"
minipass@^4.2.4:
version "4.2.8"
@@ -10147,16 +10065,7 @@ object.fromentries@^2.0.8:
es-abstract "^1.23.2"
es-object-atoms "^1.0.0"
object.groupby@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/object.groupby/-/object.groupby-1.0.3.tgz#9b125c36238129f6f7b61954a1e7176148d5002e"
integrity sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==
dependencies:
call-bind "^1.0.7"
define-properties "^1.2.1"
es-abstract "^1.23.2"
object.values@^1.1.6, object.values@^1.2.0, object.values@^1.2.1:
object.values@^1.1.6, object.values@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.2.1.tgz#deed520a50809ff7f75a7cfd4bc64c7a038c6216"
integrity sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==
@@ -11305,7 +11214,7 @@ resolve@1.22.8:
path-parse "^1.0.7"
supports-preserve-symlinks-flag "^1.0.0"
resolve@^1.20.0, resolve@^1.22.1, resolve@^1.22.10, resolve@^1.22.4, resolve@^1.22.8:
resolve@^1.20.0, resolve@^1.22.1, resolve@^1.22.10, resolve@^1.22.8:
version "1.22.11"
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.11.tgz#aad857ce1ffb8bfa9b0b1ac29f1156383f68c262"
integrity sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==
@@ -11474,7 +11383,7 @@ semver@^6.3.0, semver@^6.3.1:
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4"
integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==
semver@^7.5.2, semver@^7.5.3, semver@^7.5.4, semver@^7.6.0, semver@^7.7.1, semver@^7.7.2, semver@^7.7.3:
semver@^7.5.2, semver@^7.5.3, semver@^7.5.4, semver@^7.6.0, semver@^7.7.2, semver@^7.7.3:
version "7.7.3"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.3.tgz#4b5f4143d007633a8dc671cd0a6ef9147b8bb946"
integrity sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==
@@ -11734,10 +11643,10 @@ sprintf-js@~1.0.2:
resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
integrity sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==
stable-hash@^0.0.5:
version "0.0.5"
resolved "https://registry.yarnpkg.com/stable-hash/-/stable-hash-0.0.5.tgz#94e8837aaeac5b4d0f631d2972adef2924b40269"
integrity sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==
stable-hash-x@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/stable-hash-x/-/stable-hash-x-0.2.0.tgz#dfd76bfa5d839a7470125c6a6b3c8b22061793e9"
integrity sha512-o3yWv49B/o4QZk5ZcsALc6t0+eCelPc44zZsLtCQnZPDwFpDYSWcDnrv2TtMmMbQ7uKo3J0HTURCqckw23czNQ==
stack-utils@^2.0.3:
version "2.0.6"
@@ -11880,7 +11789,7 @@ string.prototype.trim@^1.2.10:
es-object-atoms "^1.0.0"
has-property-descriptors "^1.0.2"
string.prototype.trimend@^1.0.8, string.prototype.trimend@^1.0.9:
string.prototype.trimend@^1.0.9:
version "1.0.9"
resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz#62e2731272cd285041b36596054e9f66569b6942"
integrity sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==
@@ -11951,11 +11860,6 @@ strip-ansi@^7.0.1, strip-ansi@^7.1.0:
dependencies:
ansi-regex "^6.0.1"
strip-bom@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3"
integrity sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==
strip-bom@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-4.0.0.tgz#9c3505c1db45bcedca3d9cf7a16f5c5aa3901878"
@@ -12242,11 +12146,6 @@ text-decoder@^1.1.0:
dependencies:
b4a "^1.6.4"
text-table@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==
throttleit@2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/throttleit/-/throttleit-2.1.0.tgz#a7e4aa0bf4845a5bd10daa39ea0c783f631a07b4"
@@ -12260,7 +12159,7 @@ through2@^2.0.1:
readable-stream "~2.3.6"
xtend "~4.0.1"
tinyglobby@^0.2.13, tinyglobby@^0.2.15:
tinyglobby@^0.2.15:
version "0.2.15"
resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.15.tgz#e228dd1e638cea993d2fdb4fcd2d4602a79951c2"
integrity sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==
@@ -12366,16 +12265,6 @@ ts-node@10.9.2:
v8-compile-cache-lib "^3.0.1"
yn "3.1.1"
tsconfig-paths@^3.15.0:
version "3.15.0"
resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz#5299ec605e55b1abb23ec939ef15edaf483070d4"
integrity sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==
dependencies:
"@types/json5" "^0.0.29"
json5 "^1.0.2"
minimist "^1.2.6"
strip-bom "^3.0.0"
tslib@2.6.2:
version "2.6.2"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae"
@@ -12632,7 +12521,7 @@ unplugin@1.0.1:
webpack-sources "^3.2.3"
webpack-virtual-modules "^0.5.0"
unrs-resolver@^1.6.2:
unrs-resolver@^1.9.2:
version "1.11.1"
resolved "https://registry.yarnpkg.com/unrs-resolver/-/unrs-resolver-1.11.1.tgz#be9cd8686c99ef53ecb96df2a473c64d304048a9"
integrity sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==
@@ -13368,6 +13257,16 @@ zod-to-json-schema@^3.24.1:
resolved "https://registry.yarnpkg.com/zod-to-json-schema/-/zod-to-json-schema-3.25.1.tgz#7f24962101a439ddade2bf1aeab3c3bfec7d84ba"
integrity sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==
"zod-validation-error@^3.5.0 || ^4.0.0":
version "4.0.2"
resolved "https://registry.yarnpkg.com/zod-validation-error/-/zod-validation-error-4.0.2.tgz#bc605eba49ce0fcd598c127fee1c236be3f22918"
integrity sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==
"zod@^3.25.0 || ^4.0.0":
version "4.3.6"
resolved "https://registry.yarnpkg.com/zod/-/zod-4.3.6.tgz#89c56e0aa7d2b05107d894412227087885ab112a"
integrity sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==
zod@^3.25.67:
version "3.25.76"
resolved "https://registry.yarnpkg.com/zod/-/zod-3.25.76.tgz#26841c3f6fd22a6a2760e7ccb719179768471e34"
+2 -2
View File
@@ -234,10 +234,10 @@ backend:
## @param backend.probes.readiness.initialDelaySeconds [nullable] Configure timeout for backend readiness probe
probes:
liveness:
path: /__heartbeat__
path: /__lbheartbeat__
initialDelaySeconds: 10
readiness:
path: /__lbheartbeat__
path: /__heartbeat__
initialDelaySeconds: 10
## @param backend.resources Resource requirements for the backend container
+2 -2
View File
@@ -1,6 +1,5 @@
{
"name": "mail_mjml",
"version": "0.0.13",
"description": "An util to generate html and text django's templates from mjml templates",
"type": "module",
"dependencies": {
@@ -18,5 +17,6 @@
},
"repository": "https://github.com/numerique-gouv/conversations",
"author": "DINUM",
"license": "MIT"
"license": "MIT",
"version": "0.0.14"
}