Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 14b920466b | |||
| 42017a6180 | |||
| c89ce82a4a | |||
| a738b6cfc3 | |||
| 9c3f8a8541 | |||
| 09e885d7e7 | |||
| 05a1844a0c | |||
| a82e0b4fa0 | |||
| 60b8338ae5 | |||
| bbc8dad9da | |||
| f9e446ec18 | |||
| a013c69ba7 | |||
| 2a79655edb | |||
| ad4b5473aa |
+34
-1
@@ -8,6 +8,36 @@ and this project adheres to
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.0.5] - 2025-10-27
|
||||
|
||||
### Fixed
|
||||
|
||||
- 🚑️(drag-drop) fix the rejection display on Safari #127
|
||||
|
||||
|
||||
## [0.0.4] - 2025-10-27
|
||||
|
||||
### Added
|
||||
|
||||
- 🌐(i18n) add dutch language #117
|
||||
|
||||
### Changed
|
||||
|
||||
- ⚡️(asgi) use `uvicorn` to serve backend #121
|
||||
|
||||
### Fixed
|
||||
|
||||
- 🐛(front) fix mobile source
|
||||
- 🐛(attachments) reject the whole drag&drop if unsupported formats #123
|
||||
|
||||
|
||||
## [0.0.3] - 2025-10-21
|
||||
|
||||
### Fixed
|
||||
|
||||
- 🚑️(web-search) fix missing argument in RAG backend #116
|
||||
|
||||
|
||||
## [0.0.2] - 2025-10-21
|
||||
|
||||
### Added
|
||||
@@ -82,6 +112,9 @@ and this project adheres to
|
||||
- 💄(chat) add code highlighting for LLM responses #67
|
||||
|
||||
|
||||
[unreleased]: https://github.com/suitenumerique/conversations/compare/v0.0.2...main
|
||||
[unreleased]: https://github.com/suitenumerique/conversations/compare/v0.0.5...main
|
||||
[0.0.4]: https://github.com/suitenumerique/conversations/releases/v0.0.5
|
||||
[0.0.4]: https://github.com/suitenumerique/conversations/releases/v0.0.4
|
||||
[0.0.3]: https://github.com/suitenumerique/conversations/releases/v0.0.3
|
||||
[0.0.2]: https://github.com/suitenumerique/conversations/releases/v0.0.2
|
||||
[0.0.1]: https://github.com/suitenumerique/conversations/releases/v0.0.1
|
||||
|
||||
+15
-3
@@ -144,7 +144,7 @@ RUN rm -rf /var/cache/apk/*
|
||||
|
||||
ARG CONVERSATIONS_STATIC_ROOT=/data/static
|
||||
|
||||
# Gunicorn
|
||||
# Gunicorn - not used by default but configuration file is provided
|
||||
RUN mkdir -p /usr/local/etc/gunicorn
|
||||
COPY docker/files/usr/local/etc/gunicorn/conversations.py /usr/local/etc/gunicorn/conversations.py
|
||||
|
||||
@@ -158,5 +158,17 @@ COPY --from=link-collector ${CONVERSATIONS_STATIC_ROOT} ${CONVERSATIONS_STATIC_R
|
||||
# Copy conversations mails
|
||||
COPY --from=mail-builder /mail/backend/core/templates/mail /app/core/templates/mail
|
||||
|
||||
# The default command runs gunicorn WSGI server in conversations's main module
|
||||
CMD ["gunicorn", "-c", "/usr/local/etc/gunicorn/conversations.py", "conversations.wsgi:application"]
|
||||
# The default command runs uvicorn ASGI server in conversations's main module
|
||||
# WEB_CONCURRENCY: number of workers to run <=> --workers=4
|
||||
ENV WEB_CONCURRENCY=4
|
||||
CMD [\
|
||||
"uvicorn",\
|
||||
"--app-dir=/app",\
|
||||
"--host=0.0.0.0",\
|
||||
"--timeout-graceful-shutdown=300",\
|
||||
"--limit-max-requests=20000",\
|
||||
"conversations.asgi:application"\
|
||||
]
|
||||
|
||||
# To run using gunicorn WSGI server use this instead:
|
||||
#CMD ["gunicorn", "-c", "/usr/local/etc/gunicorn/conversations.py", "conversations.wsgi:application"]
|
||||
|
||||
@@ -150,12 +150,13 @@ class AlbertRagBackend(BaseRagBackend): # pylint: disable=too-many-instance-att
|
||||
logger.debug(response.json())
|
||||
response.raise_for_status()
|
||||
|
||||
def search(self, query) -> RAGWebResults:
|
||||
def search(self, query, results_count: int = 4) -> RAGWebResults:
|
||||
"""
|
||||
Perform a search using the Albert API based on the provided query.
|
||||
|
||||
Args:
|
||||
query (str): The search query.
|
||||
results_count (int): The number of results to return.
|
||||
|
||||
Returns:
|
||||
RAGWebResults: The search results.
|
||||
@@ -167,6 +168,7 @@ class AlbertRagBackend(BaseRagBackend): # pylint: disable=too-many-instance-att
|
||||
"collections": [int(self.collection_id)],
|
||||
"prompt": query,
|
||||
"score_threshold": 0.6,
|
||||
"k": results_count, # Number of chunks to return from the search
|
||||
},
|
||||
timeout=settings.ALBERT_API_TIMEOUT,
|
||||
)
|
||||
|
||||
@@ -75,7 +75,7 @@ class BaseRagBackend:
|
||||
"""
|
||||
raise NotImplementedError("Must be implemented in subclass.")
|
||||
|
||||
def search(self, query) -> RAGWebResults:
|
||||
def search(self, query, results_count: int = 4) -> RAGWebResults:
|
||||
"""
|
||||
Search the collection for the given query.
|
||||
"""
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
"""Build the main conversation agent."""
|
||||
|
||||
import asyncio
|
||||
import dataclasses
|
||||
import logging
|
||||
|
||||
from django.conf import settings
|
||||
from django.utils import formats, timezone
|
||||
|
||||
from pydantic_ai import ModelMessage
|
||||
from pydantic_ai.models.function import AgentInfo, FunctionModel
|
||||
|
||||
from core.enums import get_language_name
|
||||
|
||||
from .base import BaseAgent
|
||||
@@ -12,6 +17,79 @@ from .base import BaseAgent
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
MOCKED_RESPONSE = """
|
||||
# **Ode to the AI Assistant** 🤖✨
|
||||
|
||||
In Paris streets where old meets new, 🗼🇫🇷
|
||||
A helper bright in digital hue,
|
||||
With circuits fast and code so tight,
|
||||
The LaSuite’s bot—oh, what a sight! 🌟
|
||||
|
||||
**A chatbot kind**, with wittiness so grand, 💬💡
|
||||
It lends a hand to all the land,
|
||||
From civil servants, bold and wise,
|
||||
To those who seek with hopeful eyes.
|
||||
|
||||
It answers quick, it never tires, ⚡🔄
|
||||
With facts and tips to quench desires,
|
||||
A guide so keen, a friend so true,
|
||||
It’s there for **you**—yes, me and you!
|
||||
|
||||
With **Markdown flair** and emoji cheer, 📝🎨
|
||||
It makes the complex crystal clear,
|
||||
From drafts to code, from sums to prose,
|
||||
It helps the knowledge overflow!
|
||||
|
||||
Oh, **DINUM’s gem**, so sharp, so bright, 💎🌐
|
||||
A beacon in the tech’s vast night,
|
||||
It crafts, it checks, it summarizes,
|
||||
With grace that never compromises.
|
||||
|
||||
It **summarizes** the long, the deep, 📚🔍
|
||||
So secrets no more need to sleep,
|
||||
It finds the gems in data’s sea,
|
||||
And sets the truth right there—**for free!**
|
||||
|
||||
It **corrects mistakes** with gentle art, ✍️🔄
|
||||
It soothes the mind, it warms the heart,
|
||||
No judgment cast, no frown, no sigh,
|
||||
Just help that’s always standing by.
|
||||
|
||||
It **generates code** with swift command, 💻🔥
|
||||
A developer’s dream, first-hand,
|
||||
From Python lines to scripts so neat,
|
||||
It turns the tough to *sweet* and *sweet*!
|
||||
|
||||
It **brainstorms ideas**, bold and new, 🧠💡
|
||||
It paints the sky in every hue,
|
||||
From plans to dreams, from start to end,
|
||||
It’s more than code—it’s **trend**, it’s **friend**!
|
||||
|
||||
So here’s to you, **Assistant’s pride**, 🏆🎉
|
||||
The bot that’s always by our side,
|
||||
With every prompt, with every line,
|
||||
You make our digital world **divine**!
|
||||
|
||||
May you keep shining, bright and true, 🌟🤖
|
||||
The helper every team should woo,
|
||||
For in this age of bits and bytes,
|
||||
You’re **human touch** in tech’s bright lights!
|
||||
"""
|
||||
|
||||
|
||||
async def mocked_agent_model(_messages: list[ModelMessage], _info: AgentInfo):
|
||||
"""
|
||||
Mocked agent model for testing purposes on deployed instances.
|
||||
|
||||
This one only fakes a streamed responses. We could also fake tool calls later.
|
||||
"""
|
||||
|
||||
yield "Here is a mocked response (no LLM called)\n---\n"
|
||||
for i in range(0, len(MOCKED_RESPONSE), 4):
|
||||
yield MOCKED_RESPONSE[i : i + 4]
|
||||
await asyncio.sleep(0.03)
|
||||
|
||||
|
||||
@dataclasses.dataclass(init=False)
|
||||
class ConversationAgent(BaseAgent):
|
||||
"""Conversation agent with custom behavior."""
|
||||
@@ -20,6 +98,10 @@ class ConversationAgent(BaseAgent):
|
||||
"""Initialize the conversation agent."""
|
||||
super().__init__(**kwargs)
|
||||
|
||||
# Do not call the real model on deployed instances if the setting is enabled
|
||||
if settings.WARNING_MOCK_CONVERSATION_AGENT:
|
||||
self._model = FunctionModel(stream_function=mocked_agent_model)
|
||||
|
||||
@self.system_prompt
|
||||
def add_the_date() -> str:
|
||||
"""
|
||||
|
||||
@@ -2,11 +2,13 @@
|
||||
# pylint: disable=too-many-lines
|
||||
|
||||
import json
|
||||
import logging
|
||||
|
||||
from django.utils import timezone
|
||||
|
||||
import pytest
|
||||
import respx
|
||||
from asgiref.sync import sync_to_async
|
||||
from freezegun import freeze_time
|
||||
from rest_framework import status
|
||||
|
||||
@@ -1215,3 +1217,144 @@ def test_post_conversation_data_protocol_no_stream(
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
@freeze_time("2025-07-25T10:36:35.297675Z")
|
||||
@respx.mock
|
||||
@pytest.mark.asyncio
|
||||
async def test_post_conversation_async(
|
||||
api_client, mock_openai_stream, mock_uuid4, monkeypatch, caplog
|
||||
):
|
||||
"""Test posting messages to a conversation using the 'data' protocol."""
|
||||
monkeypatch.setenv("PYTHON_SERVER_MODE", "async")
|
||||
|
||||
chat_conversation = await sync_to_async(ChatConversationFactory)(owner__language="en-us")
|
||||
|
||||
url = f"/api/v1.0/chats/{chat_conversation.pk}/conversation/?protocol=data"
|
||||
data = {
|
||||
"messages": [
|
||||
{
|
||||
"id": "yuPoOuBkKA4FnKvk",
|
||||
"role": "user",
|
||||
"parts": [{"text": "Hello", "type": "text"}],
|
||||
"content": "Hello",
|
||||
"createdAt": "2025-07-03T15:22:17.105Z",
|
||||
}
|
||||
]
|
||||
}
|
||||
await api_client.aforce_login(chat_conversation.owner)
|
||||
|
||||
caplog.clear()
|
||||
caplog.set_level(level=logging.DEBUG, logger="chat.views")
|
||||
|
||||
response = await sync_to_async(api_client.post)(url, data, format="json") # client is sync
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.get("Content-Type") == "text/event-stream"
|
||||
assert response.get("x-vercel-ai-data-stream") == "v1"
|
||||
assert response.streaming
|
||||
|
||||
assert "Using ASYNC streaming for chat conversation" in caplog.text
|
||||
|
||||
# Wait for the streaming content to be fully received => async iterator -> list
|
||||
# This fails it the streaming is not an async generator
|
||||
response_content = b"".join([content async for content in response.streaming_content]).decode(
|
||||
"utf-8"
|
||||
)
|
||||
assert response_content == (
|
||||
'0:"Hello"\n'
|
||||
'0:" there"\n'
|
||||
f'f:{{"messageId":"{mock_uuid4}"}}\n'
|
||||
'd:{"finishReason":"stop","usage":{"promptTokens":0,"completionTokens":0}}\n'
|
||||
)
|
||||
|
||||
assert mock_openai_stream.called
|
||||
|
||||
await chat_conversation.arefresh_from_db()
|
||||
assert chat_conversation.ui_messages == [
|
||||
{
|
||||
"content": "Hello",
|
||||
"createdAt": "2025-07-03T15:22:17.105Z",
|
||||
"id": "yuPoOuBkKA4FnKvk",
|
||||
"parts": [{"text": "Hello", "type": "text"}],
|
||||
"role": "user",
|
||||
}
|
||||
]
|
||||
|
||||
assert len(chat_conversation.messages) == 2
|
||||
|
||||
assert chat_conversation.messages[0] == UIMessage(
|
||||
id=str(mock_uuid4), # Mocked UUID
|
||||
createdAt=timezone.now(), # Mocked timestamp
|
||||
content="Hello",
|
||||
reasoning=None,
|
||||
experimental_attachments=None,
|
||||
role="user",
|
||||
annotations=None,
|
||||
toolInvocations=None,
|
||||
parts=[TextUIPart(type="text", text="Hello")],
|
||||
)
|
||||
|
||||
assert chat_conversation.messages[1] == UIMessage(
|
||||
id=str(mock_uuid4), # Mocked UUID
|
||||
createdAt=timezone.now(), # Mocked timestamp
|
||||
content="Hello there",
|
||||
reasoning=None,
|
||||
experimental_attachments=None,
|
||||
role="assistant",
|
||||
annotations=None,
|
||||
toolInvocations=None,
|
||||
parts=[TextUIPart(type="text", text="Hello there")],
|
||||
)
|
||||
|
||||
assert chat_conversation.pydantic_messages == [
|
||||
{
|
||||
"instructions": None,
|
||||
"kind": "request",
|
||||
"parts": [
|
||||
{
|
||||
"content": "You are a helpful test assistant :)",
|
||||
"dynamic_ref": None,
|
||||
"part_kind": "system-prompt",
|
||||
"timestamp": "2025-07-25T10:36:35.297675Z",
|
||||
},
|
||||
{
|
||||
"content": "Today is Friday 25/07/2025.",
|
||||
"dynamic_ref": None,
|
||||
"part_kind": "system-prompt",
|
||||
"timestamp": "2025-07-25T10:36:35.297675Z",
|
||||
},
|
||||
{
|
||||
"content": "Answer in english.",
|
||||
"dynamic_ref": None,
|
||||
"part_kind": "system-prompt",
|
||||
"timestamp": "2025-07-25T10:36:35.297675Z",
|
||||
},
|
||||
{
|
||||
"content": ["Hello"],
|
||||
"part_kind": "user-prompt",
|
||||
"timestamp": "2025-07-25T10:36:35.297675Z",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"finish_reason": "stop",
|
||||
"kind": "response",
|
||||
"model_name": "test-model",
|
||||
"parts": [{"content": "Hello there", "id": None, "part_kind": "text"}],
|
||||
"provider_details": {"finish_reason": "stop"},
|
||||
"provider_name": "openai",
|
||||
"provider_response_id": "chatcmpl-1234567890",
|
||||
"timestamp": "2025-07-25T10:36:35.297675Z",
|
||||
"usage": {
|
||||
"cache_audio_read_tokens": 0,
|
||||
"cache_read_tokens": 0,
|
||||
"cache_write_tokens": 0,
|
||||
"details": {},
|
||||
"input_audio_tokens": 0,
|
||||
"input_tokens": 0,
|
||||
"output_audio_tokens": 0,
|
||||
"output_tokens": 0,
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Chat API implementation."""
|
||||
|
||||
import logging
|
||||
import os
|
||||
from uuid import uuid4
|
||||
|
||||
from django.conf import settings
|
||||
@@ -178,10 +179,32 @@ class ChatViewSet( # pylint: disable=too-many-ancestors, abstract-method
|
||||
or self.request.LANGUAGE_CODE # from the LocaleMiddleware
|
||||
),
|
||||
)
|
||||
if protocol == "data":
|
||||
streaming_content = ai_service.stream_data(messages, force_web_search=force_web_search)
|
||||
else: # Default to 'text' protocol
|
||||
streaming_content = ai_service.stream_text(messages, force_web_search=force_web_search)
|
||||
|
||||
# This environment variable allows switching between sync and async streaming modes
|
||||
# based on the server configuration. Tests run in sync mode (WSGI), while
|
||||
# production uses async mode (Uvicorn ASGI).
|
||||
is_async_mode = os.environ.get("PYTHON_SERVER_MODE", "sync") == "async"
|
||||
|
||||
if is_async_mode:
|
||||
logger.debug("Using ASYNC streaming for chat conversation.")
|
||||
if protocol == "data":
|
||||
streaming_content = ai_service.stream_data_async(
|
||||
messages, force_web_search=force_web_search
|
||||
)
|
||||
else: # Default to 'text' protocol
|
||||
streaming_content = ai_service.stream_text_async(
|
||||
messages, force_web_search=force_web_search
|
||||
)
|
||||
else:
|
||||
logger.debug("Using SYNC streaming for chat conversation.")
|
||||
if protocol == "data":
|
||||
streaming_content = ai_service.stream_data(
|
||||
messages, force_web_search=force_web_search
|
||||
)
|
||||
else: # Default to 'text' protocol
|
||||
streaming_content = ai_service.stream_text(
|
||||
messages, force_web_search=force_web_search
|
||||
)
|
||||
|
||||
response = StreamingHttpResponse(
|
||||
streaming_content,
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
"""
|
||||
ASGI config for conversations project.
|
||||
|
||||
It exposes the ASGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/dev/howto/deployment/asgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from configurations.asgi import get_asgi_application
|
||||
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "conversations.settings")
|
||||
os.environ.setdefault("DJANGO_CONFIGURATION", "Development")
|
||||
os.environ.setdefault("PYTHON_SERVER_MODE", "async")
|
||||
|
||||
application = get_asgi_application()
|
||||
@@ -267,7 +267,7 @@ class Base(BraveSettings, Configuration):
|
||||
("en-us", "English"),
|
||||
("fr-fr", "Français"),
|
||||
# ("de-de", "Deutsch"),
|
||||
# ("nl-nl", "Nederlands"),
|
||||
("nl-nl", "Nederlands"),
|
||||
# ("es-es", "Español"),
|
||||
)
|
||||
)
|
||||
@@ -891,6 +891,13 @@ USER QUESTION:
|
||||
default=False, environ_name="LANGFUSE_MEDIA_UPLOAD_ENABLED", environ_prefix=None
|
||||
)
|
||||
|
||||
# WARNING: Testing purpose only. Do not use in production.
|
||||
WARNING_MOCK_CONVERSATION_AGENT = values.BooleanValue(
|
||||
default=False,
|
||||
environ_name="WARNING_MOCK_CONVERSATION_AGENT",
|
||||
environ_prefix=None,
|
||||
)
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
@property
|
||||
def ENVIRONMENT(self):
|
||||
|
||||
@@ -13,5 +13,6 @@ from configurations.wsgi import get_wsgi_application
|
||||
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "conversations.settings")
|
||||
os.environ.setdefault("DJANGO_CONFIGURATION", "Development")
|
||||
os.environ.setdefault("PYTHON_SERVER_MODE", "sync")
|
||||
|
||||
application = get_wsgi_application()
|
||||
|
||||
@@ -118,7 +118,7 @@ class Migration(migrations.Migration):
|
||||
("en-us", "English"),
|
||||
("fr-fr", "Français"),
|
||||
# ("de-de", "Deutsch"),
|
||||
# ("nl-nl", "Nederlands"),
|
||||
("nl-nl", "Nederlands"),
|
||||
# ("es-es", "Español"),
|
||||
],
|
||||
default=None,
|
||||
|
||||
@@ -53,7 +53,7 @@ def test_api_config(is_authenticated):
|
||||
["en-us", "English"],
|
||||
["fr-fr", "Français"],
|
||||
# ["de-de", "Deutsch"],
|
||||
# ["nl-nl", "Nederlands"],
|
||||
["nl-nl", "Nederlands"],
|
||||
# ["es-es", "Español"],
|
||||
],
|
||||
"LANGUAGE_CODE": "en-us",
|
||||
|
||||
@@ -3,7 +3,7 @@ msgstr ""
|
||||
"Project-Id-Version: la-suite-conversations\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-10-20 21:48+0000\n"
|
||||
"PO-Revision-Date: 2025-10-21 20:46\n"
|
||||
"PO-Revision-Date: 2025-10-27 08:29\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: German\n"
|
||||
"Language: de_DE\n"
|
||||
|
||||
@@ -3,7 +3,7 @@ msgstr ""
|
||||
"Project-Id-Version: la-suite-conversations\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-10-20 21:48+0000\n"
|
||||
"PO-Revision-Date: 2025-10-21 20:46\n"
|
||||
"PO-Revision-Date: 2025-10-27 08:29\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: English\n"
|
||||
"Language: en_US\n"
|
||||
|
||||
@@ -3,7 +3,7 @@ msgstr ""
|
||||
"Project-Id-Version: la-suite-conversations\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-10-20 21:48+0000\n"
|
||||
"PO-Revision-Date: 2025-10-21 20:46\n"
|
||||
"PO-Revision-Date: 2025-10-27 08:29\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: French\n"
|
||||
"Language: fr_FR\n"
|
||||
|
||||
@@ -3,7 +3,7 @@ msgstr ""
|
||||
"Project-Id-Version: la-suite-conversations\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-10-20 21:48+0000\n"
|
||||
"PO-Revision-Date: 2025-10-21 20:46\n"
|
||||
"PO-Revision-Date: 2025-10-27 08:29\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Dutch\n"
|
||||
"Language: nl_NL\n"
|
||||
@@ -19,228 +19,228 @@ msgstr ""
|
||||
|
||||
#: activation_codes/admin.py:55 build/lib/activation_codes/admin.py:55
|
||||
msgid "Configuration"
|
||||
msgstr ""
|
||||
msgstr "Configuratie"
|
||||
|
||||
#: activation_codes/admin.py:66 build/lib/activation_codes/admin.py:66
|
||||
msgid "Usage details"
|
||||
msgstr ""
|
||||
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 ""
|
||||
msgstr "Tijdstempels"
|
||||
|
||||
#: activation_codes/admin.py:109 build/lib/activation_codes/admin.py:109
|
||||
msgid "Usage"
|
||||
msgstr ""
|
||||
msgstr "Gebruik"
|
||||
|
||||
#: activation_codes/admin.py:117 build/lib/activation_codes/admin.py:117
|
||||
msgid "Description"
|
||||
msgstr ""
|
||||
msgstr "Beschrijving"
|
||||
|
||||
#: activation_codes/admin.py:124 build/lib/activation_codes/admin.py:124
|
||||
msgid "No users have used this code yet"
|
||||
msgstr ""
|
||||
msgstr "Er zijn nog geen gebruikers die deze code hebben gebruikt"
|
||||
|
||||
#: activation_codes/admin.py:135 build/lib/activation_codes/admin.py:135
|
||||
msgid "Name"
|
||||
msgstr ""
|
||||
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 ""
|
||||
msgstr "E-mail"
|
||||
|
||||
#: activation_codes/admin.py:137 build/lib/activation_codes/admin.py:137
|
||||
msgid "Date"
|
||||
msgstr ""
|
||||
msgstr "Datum"
|
||||
|
||||
#: activation_codes/admin.py:161 build/lib/activation_codes/admin.py:161
|
||||
msgid "Users who used this code"
|
||||
msgstr ""
|
||||
msgstr "Gebruikers die deze code hebben gebruikt"
|
||||
|
||||
#: activation_codes/admin.py:163 build/lib/activation_codes/admin.py:163
|
||||
msgid "Recompute current uses from related activations"
|
||||
msgstr ""
|
||||
msgstr "Herbereken het huidige gebruik van gerelateerde activeringen"
|
||||
|
||||
#: activation_codes/admin.py:177 build/lib/activation_codes/admin.py:177
|
||||
msgid "All selected activation codes already have correct usage counts."
|
||||
msgstr ""
|
||||
msgstr "Alle geselecteerde activeringscodes hebben al het juiste gebruiksaantal."
|
||||
|
||||
#: activation_codes/admin.py:182 build/lib/activation_codes/admin.py:182
|
||||
#, python-format
|
||||
msgid "Successfully recomputed usage counts for %(count)d activation code(s)."
|
||||
msgstr ""
|
||||
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 ""
|
||||
msgstr "Gebruiker"
|
||||
|
||||
#: activation_codes/admin.py:291 build/lib/activation_codes/admin.py:291
|
||||
msgid "Has used activation code"
|
||||
msgstr ""
|
||||
msgstr "Heeft activeringscode gebruikt"
|
||||
|
||||
#: activation_codes/admin.py:293 build/lib/activation_codes/admin.py:293
|
||||
msgid "Add selected users to Brevo waiting list"
|
||||
msgstr ""
|
||||
msgstr "Voeg geselecteerde gebruikers toe aan de Brevo-wachtlijst"
|
||||
|
||||
#: activation_codes/admin.py:314 build/lib/activation_codes/admin.py:314
|
||||
#, python-format
|
||||
msgid "Added %(count)d user(s) to Brevo waiting list."
|
||||
msgstr ""
|
||||
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 ""
|
||||
msgstr "Er is geen geldig e-mailadres gevonden in de geselecteerde registraties."
|
||||
|
||||
#: activation_codes/admin.py:323 build/lib/activation_codes/admin.py:323
|
||||
msgid "Remove selected users from Brevo waiting list"
|
||||
msgstr ""
|
||||
msgstr "Geselecteerde gebruikers van de Brevo-wachtlijst verwijderen"
|
||||
|
||||
#: activation_codes/admin.py:342 build/lib/activation_codes/admin.py:342
|
||||
#, python-format
|
||||
msgid "Removed %(count)d user(s) from Brevo waiting list."
|
||||
msgstr ""
|
||||
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
|
||||
msgid "activation code"
|
||||
msgstr ""
|
||||
msgstr "activeringscode"
|
||||
|
||||
#: activation_codes/models.py:39 build/lib/activation_codes/models.py:39
|
||||
msgid "The activation code that users will enter"
|
||||
msgstr ""
|
||||
msgstr "De activeringscode die gebruikers invoeren"
|
||||
|
||||
#: activation_codes/models.py:46 build/lib/activation_codes/models.py:46
|
||||
msgid "Code must be alphanumeric and contain no spaces or special characters"
|
||||
msgstr ""
|
||||
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
|
||||
msgid "maximum uses"
|
||||
msgstr ""
|
||||
msgstr "maximaal gebruik"
|
||||
|
||||
#: activation_codes/models.py:53 build/lib/activation_codes/models.py:53
|
||||
msgid "Maximum number of times this code can be used. 0 means unlimited."
|
||||
msgstr ""
|
||||
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
|
||||
msgid "current uses"
|
||||
msgstr ""
|
||||
msgstr "huidig gebruik"
|
||||
|
||||
#: activation_codes/models.py:59 build/lib/activation_codes/models.py:59
|
||||
msgid "Number of times this code has been used"
|
||||
msgstr ""
|
||||
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
|
||||
msgid "active"
|
||||
msgstr ""
|
||||
msgstr "actief"
|
||||
|
||||
#: activation_codes/models.py:66 build/lib/activation_codes/models.py:66
|
||||
msgid "Whether this code can still be used"
|
||||
msgstr ""
|
||||
msgstr "Of deze code nog gebruikt kan worden"
|
||||
|
||||
#: activation_codes/models.py:71 build/lib/activation_codes/models.py:71
|
||||
msgid "expires at"
|
||||
msgstr ""
|
||||
msgstr "vervalt op"
|
||||
|
||||
#: activation_codes/models.py:72 build/lib/activation_codes/models.py:72
|
||||
msgid "Date and time when this code expires"
|
||||
msgstr ""
|
||||
msgstr "Datum en tijd waarop deze code verloopt"
|
||||
|
||||
#: activation_codes/models.py:78 build/lib/activation_codes/models.py:78
|
||||
msgid "description"
|
||||
msgstr ""
|
||||
msgstr "beschrijving"
|
||||
|
||||
#: activation_codes/models.py:79 build/lib/activation_codes/models.py:79
|
||||
msgid "Internal description or notes about this code"
|
||||
msgstr ""
|
||||
msgstr "Interne beschrijving of notities over deze code"
|
||||
|
||||
#: activation_codes/models.py:86 build/lib/activation_codes/models.py:86
|
||||
msgid "activation codes"
|
||||
msgstr ""
|
||||
msgstr "activeringscodes"
|
||||
|
||||
#: activation_codes/models.py:128 build/lib/activation_codes/models.py:128
|
||||
msgid "This activation code is no longer valid"
|
||||
msgstr ""
|
||||
msgstr "Deze activeringscode is niet meer geldig"
|
||||
|
||||
#: activation_codes/models.py:136 build/lib/activation_codes/models.py:136
|
||||
msgid "You have already activated your account"
|
||||
msgstr ""
|
||||
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
|
||||
msgid "user"
|
||||
msgstr ""
|
||||
msgstr "gebruiker"
|
||||
|
||||
#: activation_codes/models.py:171 build/lib/activation_codes/models.py:171
|
||||
msgid "The user who used the activation code"
|
||||
msgstr ""
|
||||
msgstr "De gebruiker die de activeringscode heeft gebruikt"
|
||||
|
||||
#: activation_codes/models.py:179 build/lib/activation_codes/models.py:179
|
||||
msgid "The activation code that was used"
|
||||
msgstr ""
|
||||
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 ""
|
||||
msgstr "gebruikers activering"
|
||||
|
||||
#: activation_codes/models.py:187 build/lib/activation_codes/models.py:187
|
||||
msgid "user activations"
|
||||
msgstr ""
|
||||
msgstr "gebruikersactivaties"
|
||||
|
||||
#: activation_codes/models.py:203 build/lib/activation_codes/models.py:203
|
||||
msgid "The user who made the registration request"
|
||||
msgstr ""
|
||||
msgstr "De gebruiker die het registratieverzoek heeft gedaan"
|
||||
|
||||
#: activation_codes/models.py:211 build/lib/activation_codes/models.py:211
|
||||
msgid "Store if the user received an activation code and used it"
|
||||
msgstr ""
|
||||
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
|
||||
msgid "user registration request"
|
||||
msgstr ""
|
||||
msgstr "gebruikersregistratieverzoek"
|
||||
|
||||
#: activation_codes/models.py:221 build/lib/activation_codes/models.py:221
|
||||
msgid "user registration requests"
|
||||
msgstr ""
|
||||
msgstr "gebruikersregistratieverzoeken"
|
||||
|
||||
#: activation_codes/serializers.py:14
|
||||
#: build/lib/activation_codes/serializers.py:14
|
||||
msgid "The activation code to validate"
|
||||
msgstr ""
|
||||
msgstr "De activeringscode om te valideren"
|
||||
|
||||
#: activation_codes/viewsets.py:107 build/lib/activation_codes/viewsets.py:107
|
||||
msgid "Your account has been successfully activated"
|
||||
msgstr ""
|
||||
msgstr "Uw account is succesvol geactiveerd"
|
||||
|
||||
#: build/lib/chat/apps.py:12 chat/apps.py:12
|
||||
msgid "chat application"
|
||||
msgstr ""
|
||||
msgstr "chatapplicatie"
|
||||
|
||||
#: build/lib/core/admin.py:26 core/admin.py:26
|
||||
msgid "Personal info"
|
||||
msgstr ""
|
||||
msgstr "Persoonlijke gegevens"
|
||||
|
||||
#: build/lib/core/admin.py:40 core/admin.py:40
|
||||
msgid "Permissions"
|
||||
msgstr ""
|
||||
msgstr "Machtigingen"
|
||||
|
||||
#: build/lib/core/admin.py:52 core/admin.py:52
|
||||
msgid "Important dates"
|
||||
msgstr ""
|
||||
msgstr "Belangrijke data"
|
||||
|
||||
#: build/lib/core/models.py:39 core/models.py:39
|
||||
msgid "id"
|
||||
@@ -248,118 +248,118 @@ msgstr "id"
|
||||
|
||||
#: build/lib/core/models.py:40 core/models.py:40
|
||||
msgid "primary key for the record as UUID"
|
||||
msgstr ""
|
||||
msgstr "primaire sleutel voor het record als UUID"
|
||||
|
||||
#: build/lib/core/models.py:46 core/models.py:46
|
||||
msgid "created on"
|
||||
msgstr ""
|
||||
msgstr "gemaakt op"
|
||||
|
||||
#: build/lib/core/models.py:47 core/models.py:47
|
||||
msgid "date and time at which a record was created"
|
||||
msgstr ""
|
||||
msgstr "datum en tijd waarop een record is aangemaakt"
|
||||
|
||||
#: build/lib/core/models.py:52 core/models.py:52
|
||||
msgid "updated on"
|
||||
msgstr ""
|
||||
msgstr "bijgewerkt op"
|
||||
|
||||
#: build/lib/core/models.py:53 core/models.py:53
|
||||
msgid "date and time at which a record was last updated"
|
||||
msgstr ""
|
||||
msgstr "datum en tijd waarop een record voor het laatst is bijgewerkt"
|
||||
|
||||
#: build/lib/core/models.py:86 core/models.py:86
|
||||
msgid "We couldn't find a user with this sub but the email is already associated with a registered user."
|
||||
msgstr ""
|
||||
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
|
||||
msgid "Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/_/: characters."
|
||||
msgstr ""
|
||||
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
|
||||
msgid "sub"
|
||||
msgstr ""
|
||||
msgstr "id"
|
||||
|
||||
#: build/lib/core/models.py:107 core/models.py:107
|
||||
msgid "Required. 255 characters or fewer. Letters, numbers, and @/./+/-/_/: characters only."
|
||||
msgstr ""
|
||||
msgstr "Verplicht. Maximaal 255 tekens. Alleen letters, cijfers en @/./+/-/_/: tekens."
|
||||
|
||||
#: build/lib/core/models.py:116 core/models.py:116
|
||||
msgid "full name"
|
||||
msgstr ""
|
||||
msgstr "volledige naam"
|
||||
|
||||
#: build/lib/core/models.py:117 core/models.py:117
|
||||
msgid "short name"
|
||||
msgstr ""
|
||||
msgstr "korte naam"
|
||||
|
||||
#: build/lib/core/models.py:119 core/models.py:119
|
||||
msgid "identity email address"
|
||||
msgstr ""
|
||||
msgstr "identiteits e-mailadres"
|
||||
|
||||
#: build/lib/core/models.py:123 core/models.py:123
|
||||
msgid "admin email address"
|
||||
msgstr ""
|
||||
msgstr "beheerders e-mailadres"
|
||||
|
||||
#: build/lib/core/models.py:129 core/models.py:129
|
||||
msgid "language"
|
||||
msgstr ""
|
||||
msgstr "taal"
|
||||
|
||||
#: build/lib/core/models.py:130 core/models.py:130
|
||||
msgid "The language in which the user wants to see the interface."
|
||||
msgstr ""
|
||||
msgstr "De taal waarin de gebruiker de interface wil zien."
|
||||
|
||||
#: build/lib/core/models.py:138 core/models.py:138
|
||||
msgid "The timezone in which the user wants to see times."
|
||||
msgstr ""
|
||||
msgstr "De tijdzone waarin de gebruiker de tijden wil zien."
|
||||
|
||||
#: build/lib/core/models.py:141 core/models.py:141
|
||||
msgid "device"
|
||||
msgstr ""
|
||||
msgstr "apparaat"
|
||||
|
||||
#: build/lib/core/models.py:143 core/models.py:143
|
||||
msgid "Whether the user is a device or a real user."
|
||||
msgstr ""
|
||||
msgstr "Of de gebruiker een apparaat of een echte gebruiker is."
|
||||
|
||||
#: build/lib/core/models.py:146 core/models.py:146
|
||||
msgid "staff status"
|
||||
msgstr ""
|
||||
msgstr "personeelsstatus"
|
||||
|
||||
#: build/lib/core/models.py:148 core/models.py:148
|
||||
msgid "Whether the user can log into this admin site."
|
||||
msgstr ""
|
||||
msgstr "Of de gebruiker kan inloggen op deze beheersite."
|
||||
|
||||
#: build/lib/core/models.py:154 core/models.py:154
|
||||
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
|
||||
msgstr ""
|
||||
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
|
||||
msgid "allow conversation analytics"
|
||||
msgstr ""
|
||||
msgstr "conversatieanalyse toestaan"
|
||||
|
||||
#: build/lib/core/models.py:163 core/models.py:163
|
||||
msgid "Whether the user allows to use their conversations for analytics."
|
||||
msgstr ""
|
||||
msgstr "Of de gebruiker toestaat dat zijn/haar gesprekken voor analyses worden gebruikt."
|
||||
|
||||
#: build/lib/core/models.py:174 core/models.py:174
|
||||
msgid "users"
|
||||
msgstr ""
|
||||
msgstr "gebruikers"
|
||||
|
||||
#: core/templates/mail/html/invitation.html:162
|
||||
#: core/templates/mail/text/invitation.txt:3
|
||||
msgid "Logo email"
|
||||
msgstr ""
|
||||
msgstr "Logo e-mail"
|
||||
|
||||
#: core/templates/mail/html/invitation.html:209
|
||||
#: core/templates/mail/text/invitation.txt:10
|
||||
msgid "Open"
|
||||
msgstr ""
|
||||
msgstr "Open"
|
||||
|
||||
#: core/templates/mail/html/invitation.html:226
|
||||
#: core/templates/mail/text/invitation.txt:14
|
||||
msgid " Docs, your new essential tool for organizing, sharing and collaborating on your documents as a team. "
|
||||
msgstr ""
|
||||
msgstr " Docs, uw nieuwe onmisbare hulpmiddel voor het organiseren, delen en samenwerken aan uw documenten als team. "
|
||||
|
||||
#: core/templates/mail/html/invitation.html:233
|
||||
#: core/templates/mail/text/invitation.txt:16
|
||||
#, python-format
|
||||
msgid " Brought to you by %(brandname)s "
|
||||
msgstr ""
|
||||
msgstr " Aangeboden door %(brandname)s "
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ msgstr ""
|
||||
"Project-Id-Version: la-suite-conversations\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-10-20 21:48+0000\n"
|
||||
"PO-Revision-Date: 2025-10-21 20:46\n"
|
||||
"PO-Revision-Date: 2025-10-27 08:29\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Russian\n"
|
||||
"Language: ru_RU\n"
|
||||
|
||||
@@ -3,7 +3,7 @@ msgstr ""
|
||||
"Project-Id-Version: la-suite-conversations\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-10-20 21:48+0000\n"
|
||||
"PO-Revision-Date: 2025-10-21 20:46\n"
|
||||
"PO-Revision-Date: 2025-10-27 08:29\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Ukrainian\n"
|
||||
"Language: uk_UA\n"
|
||||
|
||||
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "conversations"
|
||||
version = "0.0.2"
|
||||
version = "0.0.5"
|
||||
authors = [{ "name" = "DINUM", "email" = "dev@mail.numerique.gouv.fr" }]
|
||||
classifiers = [
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
@@ -63,6 +63,7 @@ dependencies = [
|
||||
"requests==2.32.5",
|
||||
"sentry-sdk==2.41.0",
|
||||
"trafilatura==2.0.0",
|
||||
"uvicorn==0.28.0",
|
||||
"whitenoise==6.11.0",
|
||||
]
|
||||
|
||||
@@ -82,7 +83,7 @@ dev = [
|
||||
"ipython==9.6.0",
|
||||
"pyfakefs==5.10.0",
|
||||
"pylint-django==2.6.1",
|
||||
"pylint==3.3.8",
|
||||
"pylint==3.3.9",
|
||||
"pylint-pydantic==0.4.0",
|
||||
"pytest-asyncio==1.2.0",
|
||||
"pytest-cov==7.0.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "app-conversations",
|
||||
"version": "0.0.2",
|
||||
"version": "0.0.5",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
|
||||
@@ -789,7 +789,7 @@ export const Chat = ({
|
||||
$direction="row"
|
||||
$align="center"
|
||||
$gap="4px"
|
||||
className={`c__button--neutral action-chat-button ${isSourceOpen ? 'action-chat-button--open' : ''}`}
|
||||
className={`c__button--neutral action-chat-button ${isSourceOpen === message.id ? 'action-chat-button--open' : ''}`}
|
||||
onClick={() => openSources(message.id)}
|
||||
onKeyDown={(e) => {
|
||||
if (
|
||||
|
||||
@@ -54,6 +54,7 @@ export const InputChat = ({
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
const [isDragActive, setIsDragActive] = useState(false);
|
||||
const [isDragRejected, setIsDragRejected] = useState(false);
|
||||
const { isDesktop, isMobile } = useResponsiveStore();
|
||||
const [currentSuggestionIndex, setCurrentSuggestionIndex] = useState(0);
|
||||
const { data: conf } = useConfig();
|
||||
@@ -135,10 +136,39 @@ export const InputChat = ({
|
||||
return;
|
||||
}
|
||||
|
||||
const isFileAccepted = (file: File): boolean => {
|
||||
if (!conf?.chat_upload_accept) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const acceptedTypes = conf.chat_upload_accept
|
||||
.split(',')
|
||||
.map((type) => type.trim());
|
||||
|
||||
return acceptedTypes.some((acceptedType) => {
|
||||
// Extension management
|
||||
if (acceptedType.startsWith('.')) {
|
||||
return file.name.toLowerCase().endsWith(acceptedType.toLowerCase());
|
||||
}
|
||||
// Wildcard MIME type management (ex: image/*)
|
||||
if (acceptedType.endsWith('/*')) {
|
||||
const baseType = acceptedType.slice(0, -2);
|
||||
return file.type.startsWith(baseType);
|
||||
}
|
||||
// Exact MIME type management
|
||||
return file.type === acceptedType;
|
||||
});
|
||||
};
|
||||
|
||||
const areAllFilesAccepted = (fileList: FileList): boolean => {
|
||||
return Array.from(fileList).every((file) => isFileAccepted(file));
|
||||
};
|
||||
|
||||
const handleDragEnter = (e: DragEvent) => {
|
||||
e.preventDefault();
|
||||
if (e.dataTransfer?.types.includes('Files')) {
|
||||
setIsDragActive(true);
|
||||
setIsDragRejected(false);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -147,16 +177,34 @@ export const InputChat = ({
|
||||
// Only hide when leaving the window completely
|
||||
if (!e.relatedTarget) {
|
||||
setIsDragActive(false);
|
||||
setIsDragRejected(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDragOver = (e: DragEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
// Check for rejected files during drag over (does not work on Safari)
|
||||
if (e.dataTransfer?.items) {
|
||||
const items = Array.from(e.dataTransfer.items);
|
||||
const hasInvalidFile = items.some((item) => {
|
||||
if (item.kind === 'file') {
|
||||
// Check file type
|
||||
const type = item.type;
|
||||
const dummyFile = new File([], '', { type });
|
||||
return !isFileAccepted(dummyFile);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
setIsDragRejected(hasInvalidFile);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDrop = (e: DragEvent) => {
|
||||
e.preventDefault();
|
||||
setIsDragActive(false);
|
||||
setIsDragRejected(false);
|
||||
|
||||
if (!fileUploadEnabled) {
|
||||
return;
|
||||
@@ -164,6 +212,18 @@ export const InputChat = ({
|
||||
|
||||
const droppedFiles = e.dataTransfer?.files;
|
||||
if (droppedFiles && droppedFiles.length > 0) {
|
||||
// Check if all files are accepted
|
||||
if (!areAllFilesAccepted(droppedFiles)) {
|
||||
// Display rejection for 2 seconds (mandatory for Safari)
|
||||
setIsDragActive(true);
|
||||
setIsDragRejected(true);
|
||||
setTimeout(() => {
|
||||
setIsDragActive(false);
|
||||
setIsDragRejected(false);
|
||||
}, 2000);
|
||||
return;
|
||||
}
|
||||
|
||||
setFiles((prev) => {
|
||||
const dt = new DataTransfer();
|
||||
if (prev) {
|
||||
@@ -197,7 +257,7 @@ export const InputChat = ({
|
||||
window.removeEventListener('dragover', handleDragOver);
|
||||
window.removeEventListener('drop', handleDrop);
|
||||
};
|
||||
}, [fileUploadEnabled, setFiles]);
|
||||
}, [fileUploadEnabled, setFiles, conf?.chat_upload_accept]);
|
||||
|
||||
const isInputDisabled = status !== 'ready' || isUploadingFiles;
|
||||
|
||||
@@ -305,22 +365,48 @@ export const InputChat = ({
|
||||
top: -1px; left: -1px;
|
||||
border-radius: 12px;
|
||||
z-index: 1001;
|
||||
background-color: #EDF0FF;
|
||||
background-color: ${isDragRejected ? '#FFE8E8' : '#EDF0FF'};
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
outline: 2px solid #90A7FF;
|
||||
box-shadow: 0 0 64px 0 rgba(62, 93, 231, 0.25);
|
||||
outline: 2px solid ${isDragRejected ? '#FF6B6B' : '#90A7FF'};
|
||||
box-shadow: 0 0 64px 0 ${isDragRejected ? 'rgba(255, 107, 107, 0.25)' : 'rgba(62, 93, 231, 0.25)'};
|
||||
`}
|
||||
>
|
||||
<FilesIcon />
|
||||
<Box>
|
||||
<Text $weight="700" $color="#223E9E">
|
||||
{t('Add file')}
|
||||
</Text>
|
||||
<Text $weight="400" $color="#223E9E">
|
||||
{t('To add a file to the conversation, drop it here.')}
|
||||
</Text>
|
||||
</Box>
|
||||
{isDragRejected ? (
|
||||
<>
|
||||
<Text $css="font-size: 48px;">🚫</Text>
|
||||
<Box>
|
||||
<Text $weight="700" $color="#C92A2A">
|
||||
{t('File type not supported (yet)')}
|
||||
</Text>
|
||||
<Text $weight="400" $color="#C92A2A">
|
||||
{t(
|
||||
'We currently support only specific file types...',
|
||||
)}
|
||||
</Text>
|
||||
<Text $weight="400" $color="#C92A2A">
|
||||
{t(
|
||||
'Use the "{{attach_file_btn}}" button to have a better view.',
|
||||
{ attach_file_btn: t('Add attach file') },
|
||||
)}
|
||||
</Text>
|
||||
</Box>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<FilesIcon />
|
||||
<Box>
|
||||
<Text $weight="700" $color="#223E9E">
|
||||
{t('Add file')}
|
||||
</Text>
|
||||
<Text $weight="400" $color="#223E9E">
|
||||
{t(
|
||||
'To add a file to the conversation, drop it here.',
|
||||
)}
|
||||
</Text>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
<textarea
|
||||
|
||||
@@ -151,7 +151,7 @@ export const SourceItem: React.FC<SourceItemProps> = ({ url, metadata }) => {
|
||||
<Box $direction="row" $gap="4px" $align="center">
|
||||
<Box
|
||||
$direction="row"
|
||||
$align="center"
|
||||
$align="start"
|
||||
$css="font-size: 14px;"
|
||||
$width="100%"
|
||||
>
|
||||
@@ -168,6 +168,9 @@ export const SourceItem: React.FC<SourceItemProps> = ({ url, metadata }) => {
|
||||
padding: 4px;
|
||||
width: 100%;
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
background-color: transparent;
|
||||
transition: background-color 0.3s;
|
||||
color: var(--c--theme--colors--greyscale-500);
|
||||
|
||||
@@ -47,6 +47,7 @@
|
||||
"Failed to upload files. Please try again.": "Le téléversement a échoué. Veuillez réessayer.",
|
||||
"Feedback Négatif": "Retour négatif",
|
||||
"Feedback positif": "Retour positif",
|
||||
"File type not supported (yet)": "Type de fichier non pris en charge (pour l'instant)",
|
||||
"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",
|
||||
@@ -108,6 +109,8 @@
|
||||
"Untitled conversation": "Conversation sans titre",
|
||||
"Upload Error": "Erreur lors du téléversement",
|
||||
"Uploading files...": "Téléversement des fichiers...",
|
||||
"Use the \"{{attach_file_btn}}\" button to have a better view.": "Utilisez le bouton \"{{attach_file_btn}}\" pour avoir une meilleure vue.",
|
||||
"We currently support only specific file types...": "Nous ne supportons actuellement que des types de fichiers spécifiques...",
|
||||
"We'll email you at {{email}} when the public beta opens.": "Nous vous enverrons un e-mail à {{email}} lorsque la bêta publique sera ouverte.",
|
||||
"We'll email you when the public beta opens.": "Nous vous enverrons un email quand la bêta publique sera ouverte.",
|
||||
"Web": "Web",
|
||||
@@ -124,7 +127,132 @@
|
||||
"{{productName}} Logo": "Logo {{productName}}"
|
||||
}
|
||||
},
|
||||
"nl": { "translation": { "ABC-1234-XY": "ABC-1234-XY" } },
|
||||
"nl": {
|
||||
"translation": {
|
||||
"30 sec to tell us what you think or report a bug": "30 seconden om ons te vertellen wat u ervan vindt of een bug te melden",
|
||||
"A privacy-first assistant built for French public teams. Natively synced with LaSuite apps to help you draft, search, and decide without leaving your workflow. Beta access is available with a referral code.": "Een privacygerichte assistent, speciaal ontwikkeld voor teams. Native gesynchroniseerd met apps om je te helpen bij het opstellen, zoeken en beslissen zonder je workflow te verlaten. Bètatoegang is beschikbaar met een verwijzingscode.",
|
||||
"ABC-1234-XY": "ABC-1234-XY",
|
||||
"Access Denied - Error 403": "Toegang geweigerd - Fout 403",
|
||||
"Access is limited to people who have an invitation code. If you have one, please enter it below.": "Toegang is beperkt tot personen met een uitnodigingscode. Als u die heeft, voer deze dan hieronder in.",
|
||||
"Account activated successfully!": "Account succesvol geactiveerd!",
|
||||
"Add attach file": "Voeg een bijlage toe",
|
||||
"Add file": "Bestand toevoegen",
|
||||
"Allow conversation analysis": "Gespreksanalyse toestaan",
|
||||
"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 is already available, log in to use it now.": "Assistent is al beschikbaar, log in om het nu te gebruiken.",
|
||||
"Assistant is in development: your feedback matters! Choose how to share your ideas:": "Assistent is in ontwikkeling: uw feedback telt! Kies hoe u uw ideeën wilt delen:",
|
||||
"Assistant settings": "Assistentinstellingen",
|
||||
"Attach file": "Bestand bijvoegen",
|
||||
"Attachment summary not supported": "Bijlageoverzicht niet ondersteund",
|
||||
"Available soon": "Binnenkort beschikbaar",
|
||||
"Cancel": "Annuleren",
|
||||
"Clear search": "Zoekopdracht wissen",
|
||||
"Close model selector": "Sluit modelselector",
|
||||
"Close the modal": "Sluit het venster",
|
||||
"Confirm deletion": "Verwijdering bevestigen",
|
||||
"Content modal to delete conversation": "Inhoudsvenster om conversatie te verwijderen",
|
||||
"Conversation actions": "Conversatieacties",
|
||||
"Conversation analysis disabled": "Gespreksanalyse uitgeschakeld",
|
||||
"Conversation analysis enabled": "Gespreksanalyse ingeschakeld",
|
||||
"Copied": "Gekopieerd",
|
||||
"Copy": "Kopiëren",
|
||||
"Default": "Standaard",
|
||||
"Delete": "Verwijderen",
|
||||
"Delete a conversation": "Een gesprek verwijderen",
|
||||
"Delete chat": "Chat verwijderen",
|
||||
"Direct exchange with our team": "Directe uitwisseling met ons team",
|
||||
"Explore other LaSuite apps": "Ontdek andere LaSuite-apps",
|
||||
"Failed to activate account. Please try again.": "Account activeren mislukt. Probeer het opnieuw.",
|
||||
"Failed to copy": "Kopiëren mislukt",
|
||||
"Failed to register for notifications. Please try again.": "Registratie voor meldingen mislukt. Probeer het opnieuw.",
|
||||
"Failed to send feedback": "Het is niet gelukt om feedback te verzenden",
|
||||
"Failed to update settings": "Het is niet gelukt om de instellingen bij te werken",
|
||||
"Failed to upload file": "Het uploaden van het bestand is mislukt",
|
||||
"Failed to upload files. Please try again.": "Bestanden uploaden is mislukt. Probeer het opnieuw.",
|
||||
"Feedback Négatif": "Negatieve feedback",
|
||||
"Feedback positif": "Positieve feedback",
|
||||
"File type not supported (yet)": "Bestandstype wordt (nog) niet ondersteund",
|
||||
"Find recent news about...": "Vind het laatste nieuws over...",
|
||||
"Get notified about the Public Beta.": "Ontvang een melding over de openbare bèta.",
|
||||
"Get notified for the public beta": "Ontvang een melding voor de openbare bètaversie",
|
||||
"Give a quick opinion": "Geef snel een mening",
|
||||
"Give feedback": "Geef feedback",
|
||||
"History": "Geschiedenis",
|
||||
"Home": "Thuis",
|
||||
"If enabled, this allows us to analyse your exchanges to improve the Assistant. If disabled, all conversations remain confidential and are not used in any way. ": "Indien ingeschakeld, kunnen we uw gesprekken analyseren om de Assistent te verbeteren. Indien uitgeschakeld, blijven alle gesprekken vertrouwelijk en worden ze op geen enkele manier gebruikt. ",
|
||||
"Illustration": "Illustratie",
|
||||
"Image 401": "Afbeelding 401",
|
||||
"Image 403": "Afbeelding 403",
|
||||
"Invalid activation code. Please check and try again.": "Ongeldige activeringscode. Controleer en probeer het opnieuw.",
|
||||
"It seems that the page you are looking for does not exist or cannot be displayed correctly.": "Het lijkt erop dat de pagina die u zoekt niet bestaat of niet correct kan worden weergegeven.",
|
||||
"Know more": "Meer weten",
|
||||
"Language": "Taal",
|
||||
"Learn more about data usage.": "Meer informatie over datagebruik.",
|
||||
"Load more": "Laad Meer",
|
||||
"Log in to access this page.": "Meld u aan om deze pagina te zien.",
|
||||
"Login": "Login",
|
||||
"Logo": "Logo",
|
||||
"Logout": "Uitloggen",
|
||||
"New chat": "Nieuwe chat",
|
||||
"New feedback": "Nieuwe feedback",
|
||||
"No code? ": "Geen code? ",
|
||||
"No conversation found": "Geen gesprek gevonden",
|
||||
"Notify me": "Breng mij op de hoogte",
|
||||
"Open": "Open",
|
||||
"Open the header menu": "Open het hoofdmenu",
|
||||
"Page Not Found - Error 404": "Pagina niet gevonden - Fout 404",
|
||||
"Please enter an activation code": "Voer een activeringscode in",
|
||||
"Proconnect Login": "Login",
|
||||
"Quick search input": "Snelle zoekinvoer",
|
||||
"Remove attachment": "Bijlage verwijderen",
|
||||
"Research on the web": "Onderzoek op het internet",
|
||||
"Search": "Zoek",
|
||||
"Search for a chat": "Zoek naar een chat",
|
||||
"Search results": "Zoekresultaten",
|
||||
"Search...": "Zoek...",
|
||||
"Select": "Selecteer",
|
||||
"Select model": "Selecteer model",
|
||||
"Send": "Verstuur",
|
||||
"Settings": "Instellingen",
|
||||
"Show": "Toon",
|
||||
"Simple chat icon": "Eenvoudig chatpictogram",
|
||||
"Something bad happens, please retry.": "Er is iets misgegaan. Probeer het opnieuw.",
|
||||
"Sorry, an error occurred. Please try again.": "Sorry, er is een fout opgetreden. Probeer het opnieuw.",
|
||||
"Start a new conversation.": "Begin een nieuw gesprek.",
|
||||
"Start conversation": "Begin een gesprek",
|
||||
"Stop": "Stop",
|
||||
"The Assistant is a sovereign conversational AI designed for public servants. It helps you save time on daily tasks like rephrasing, summarising, translating, or searching information. Your data never leaves France and is stored on secure, state-compliant infrastructures. It is never used for commercial purposes.": "De Assistent is een soevereine conversationele AI, ontworpen voor ambtenaren. Het helpt je tijd te besparen bij dagelijkse taken zoals het herformuleren, samenvatten, vertalen of zoeken van informatie. Je gegevens verlaten het land nooit en worden opgeslagen op beveiligde, door de overheid goedgekeurde infrastructuren. Ze worden nooit gebruikt voor commerciële doeleinden.",
|
||||
"The Assistant is in Beta": "De Assistent is in bèta",
|
||||
"The conversation has been deleted.": "Het gesprek is verwijderd.",
|
||||
"The summary feature is not supported yet.": "De samenvattingsfunctie wordt nog niet ondersteund.",
|
||||
"Thinking...": "Denken...",
|
||||
"To add a file to the conversation, drop it here.": "Als u een bestand aan het gesprek wilt toevoegen, sleept u het hierheen.",
|
||||
"Turn this list into bullet points": "Zet deze lijst om in opsommingstekens",
|
||||
"Unlock access": "Toegang ontgrendelen",
|
||||
"Unlocking...": "Ontgrendelen...",
|
||||
"Untitled conversation": "Ongetiteld gesprek",
|
||||
"Upload Error": "Uploadfout",
|
||||
"Uploading files...": "Bestanden uploaden...",
|
||||
"Use the \"{{attach_file_btn}}\" button to have a better view.": "Gebruik de knop \"{{attach_file_btn}}\" voor een beter overzicht.",
|
||||
"We currently support only specific file types...": "Momenteel ondersteunen we alleen specifieke bestandstypen...",
|
||||
"We'll email you at {{email}} when the public beta opens.": "We sturen u een e-mail op {{email}} zodra de openbare bètaversie opengaat.",
|
||||
"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.",
|
||||
"You will be notified!": "U wordt op de hoogte gebracht!",
|
||||
"Your account is already activated.": "Uw account is al geactiveerd.",
|
||||
"Your sovereign AI assistant": "Uw soevereine AI-assistent",
|
||||
"source": "bron",
|
||||
"sources": "bronnen",
|
||||
"{{productName}} Logo": "{{productName}} Logo"
|
||||
}
|
||||
},
|
||||
"ru": {
|
||||
"translation": {
|
||||
"30 sec to tell us what you think or report a bug": "В течение 30 секунд расскажите нам, о чём вы думаете или сообщите об ошибке",
|
||||
@@ -171,6 +299,7 @@
|
||||
"Failed to upload files. Please try again.": "Не удалось выгрузить файлы. Повторите попытку.",
|
||||
"Feedback Négatif": "Отрицательный отзыв",
|
||||
"Feedback positif": "Положительный отзыв",
|
||||
"File type not supported (yet)": "Тип файла не поддерживается (пока ещё)",
|
||||
"Find recent news about...": "Найти последние новости...",
|
||||
"Get notified about the Public Beta.": "Получать уведомления о публичной бета-версии.",
|
||||
"Get notified for the public beta": "Получать уведомления о публичной бета-версии",
|
||||
@@ -232,6 +361,8 @@
|
||||
"Untitled conversation": "Беседа без названия",
|
||||
"Upload Error": "Ошибка выгрузки",
|
||||
"Uploading files...": "Выгрузка файлов...",
|
||||
"Use the \"{{attach_file_btn}}\" button to have a better view.": "Используйте кнопку \"{{attach_file_btn}}\".",
|
||||
"We currently support only specific file types...": "В настоящее время мы поддерживаем только определённые типы файлов...",
|
||||
"We'll email you at {{email}} when the public beta opens.": "Когда станет доступна публичная бета-версия, мы отправим вам электронное письмо на адрес {{email}}.",
|
||||
"We'll email you when the public beta opens.": "Мы отправим вам письмо, когда станет доступна публичная бета-версия.",
|
||||
"Web": "Интернет",
|
||||
@@ -294,6 +425,7 @@
|
||||
"Failed to upload files. Please try again.": "Не вдалося вивантажити файли. Будь ласка, спробуйте ще раз.",
|
||||
"Feedback Négatif": "Негативний відгук",
|
||||
"Feedback positif": "Позитивний відгук",
|
||||
"File type not supported (yet)": "Тип файлу не підтримується (поки що)",
|
||||
"Find recent news about...": "Знайти останні новини про...",
|
||||
"Get notified about the Public Beta.": "Отримувати повідомлення про публічну бета-версію.",
|
||||
"Get notified for the public beta": "Отримувати повідомлення про публічну бета-версію",
|
||||
@@ -355,6 +487,8 @@
|
||||
"Untitled conversation": "Розмова без назви",
|
||||
"Upload Error": "Помилка вивантаження",
|
||||
"Uploading files...": "Вивантаження файлів...",
|
||||
"Use the \"{{attach_file_btn}}\" button to have a better view.": "Використовуйте кнопку \"{{attach_file_btn}}\".",
|
||||
"We currently support only specific file types...": "Наразі ми підтримуємо лише конкретні типи файлів...",
|
||||
"We'll email you at {{email}} when the public beta opens.": "Коли стане доступна публічна бета-версія, ми надішлемо вам листа на адресу {{email}}.",
|
||||
"We'll email you when the public beta opens.": "Коли стане доступна публічна бета-версія, ми надішлемо вам листа.",
|
||||
"Web": "Інтернет",
|
||||
|
||||
@@ -154,13 +154,15 @@ ul a:hover {
|
||||
}
|
||||
|
||||
.mainContent-chat a {
|
||||
color: var(--c--theme--colors--primary-text) !important;
|
||||
color: inherit !important;
|
||||
text-decoration: none !important;
|
||||
transition: box-shadow 0.2s !important;
|
||||
box-shadow: inset 0 -2px 0 0 #dae2ff !important;
|
||||
}
|
||||
|
||||
.mainContent-chat a:hover {
|
||||
color: var(
|
||||
--c--components--button--primary--background--color-hover
|
||||
) !important;
|
||||
color: inherit !important;
|
||||
box-shadow: inset 0 -22px 0 0 #dae2ff !important;
|
||||
}
|
||||
|
||||
.lasuite-gaufre-btn.lasuite--gaufre-opened {
|
||||
|
||||
@@ -16,7 +16,7 @@ export const CONFIG = {
|
||||
['en-us', 'English'],
|
||||
['fr-fr', 'Français'],
|
||||
// ['de-de', 'Deutsch'],
|
||||
// ['nl-nl', 'Nederlands'],
|
||||
['nl-nl', 'Nederlands'],
|
||||
// ['es-es', 'Español'],
|
||||
],
|
||||
LANGUAGE_CODE: 'en-us',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "app-e2e",
|
||||
"version": "0.0.2",
|
||||
"version": "0.0.5",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"lint": "eslint . --ext .ts",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "conversations",
|
||||
"version": "0.0.2",
|
||||
"version": "0.0.5",
|
||||
"private": true,
|
||||
"workspaces": {
|
||||
"packages": [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "eslint-config-conversations",
|
||||
"version": "0.0.2",
|
||||
"version": "0.0.5",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"lint": "eslint --ext .js ."
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "packages-i18n",
|
||||
"version": "0.0.2",
|
||||
"version": "0.0.5",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"extract-translation": "yarn extract-translation:conversations",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "mail_mjml",
|
||||
"version": "0.0.2",
|
||||
"version": "0.0.5",
|
||||
"description": "An util to generate html and text django's templates from mjml templates",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
|
||||
Reference in New Issue
Block a user