@djangocfg/widget-assistant 0.0.0-stage → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +33 -2
  3. package/package.json +72 -4
  4. package/src/AssistantChat.tsx +115 -0
  5. package/src/Host.tsx +57 -0
  6. package/src/composer/AssistantComposer.tsx +19 -0
  7. package/src/composer/ComposerBehaviour.tsx +84 -0
  8. package/src/composer/ComposerHelp.tsx +46 -0
  9. package/src/composer/keys.ts +2 -0
  10. package/src/composer/useCaretFocus.ts +49 -0
  11. package/src/composer/useComposerNodes.ts +14 -0
  12. package/src/composer/useFocusArea.ts +29 -0
  13. package/src/composer/useGracefulStop.ts +63 -0
  14. package/src/composer/useHeightFloor.ts +27 -0
  15. package/src/composer/useRecording.ts +18 -0
  16. package/src/composer/useSentPrompts.ts +20 -0
  17. package/src/composer/wholeTranscript.ts +106 -0
  18. package/src/i18n/index.ts +36 -0
  19. package/src/i18n/locales/ar.ts +34 -0
  20. package/src/i18n/locales/da.ts +34 -0
  21. package/src/i18n/locales/de.ts +34 -0
  22. package/src/i18n/locales/en.ts +34 -0
  23. package/src/i18n/locales/es.ts +34 -0
  24. package/src/i18n/locales/fr.ts +34 -0
  25. package/src/i18n/locales/it.ts +34 -0
  26. package/src/i18n/locales/ja.ts +34 -0
  27. package/src/i18n/locales/ko.ts +34 -0
  28. package/src/i18n/locales/nl.ts +34 -0
  29. package/src/i18n/locales/no.ts +34 -0
  30. package/src/i18n/locales/pl.ts +34 -0
  31. package/src/i18n/locales/pt-BR.ts +34 -0
  32. package/src/i18n/locales/ru.ts +34 -0
  33. package/src/i18n/locales/sv.ts +34 -0
  34. package/src/i18n/locales/tr.ts +34 -0
  35. package/src/i18n/locales/zh.ts +34 -0
  36. package/src/i18n/sessionCopy.ts +30 -0
  37. package/src/i18n/types.ts +42 -0
  38. package/src/index.ts +17 -0
  39. package/src/messages/MessageFooter.tsx +25 -0
  40. package/src/messages/NoticeLine.tsx +32 -0
  41. package/src/shell/AssistantFab.tsx +112 -0
  42. package/src/shell/AssistantFrame.tsx +103 -0
  43. package/src/shell/AssistantLocked.tsx +44 -0
  44. package/src/shell/ChatDock.tsx +62 -0
  45. package/src/shell/panelState.ts +74 -0
  46. package/src/stream/adapter.ts +144 -0
  47. package/src/stream/fromStored.ts +23 -0
  48. package/src/stream/reconcile.ts +45 -0
  49. package/src/stream/scriptedTransport.ts +28 -0
  50. package/src/stream/useSession.ts +136 -0
  51. package/src/styles/composer.css +14 -0
  52. package/src/styles/fab.css +53 -0
  53. package/src/styles/index.css +6 -0
  54. package/src/styles/layout.css +15 -0
  55. package/src/styles/widget.css +7 -0
  56. package/src/wire/frames.ts +53 -0
  57. package/src/wire/parseSSE.ts +76 -0
  58. package/src/wire/stored.ts +24 -0
  59. package/src/wire/transport.ts +94 -0
@@ -0,0 +1,18 @@
1
+ import { useEffect, useState } from 'react';
2
+
3
+ import type { ComposerNodes } from './useComposerNodes';
4
+
5
+ /** Whether dictation is on. The widget does not expose it, so it is read from the microphone's `data-recording` attribute (the one its halo is styled from). */
6
+ export function useRecording({ mic }: ComposerNodes): boolean {
7
+ const [recording, setRecording] = useState(false);
8
+ useEffect(() => {
9
+ const button = mic();
10
+ if (!button) return;
11
+ const read = () => setRecording(button.hasAttribute('data-recording'));
12
+ read();
13
+ const watch = new MutationObserver(read);
14
+ watch.observe(button, { attributes: true, attributeFilter: ['data-recording'] });
15
+ return () => watch.disconnect();
16
+ }, [mic]);
17
+ return recording;
18
+ }
@@ -0,0 +1,20 @@
1
+ import { useMemo } from 'react';
2
+
3
+ import { isTextPart, useChatStore, useMessageIds } from '@djangocfg/widget-chat';
4
+
5
+ /**
6
+ * The prompts sent in this room, oldest first, for the arrow-key recall: read from the LIVE transcript, so a question sent a moment ago is the first one recalled.
7
+ * (A list taken from the history the room was seeded with only changes on a reload, and recall then walked to an old question instead of the latest.)
8
+ */
9
+ export function useSentPrompts(): string[] {
10
+ const store = useChatStore();
11
+ const ids = useMessageIds();
12
+ return useMemo(() => {
13
+ const { byId } = store.getState().transcript;
14
+ return ids
15
+ .map((id) => byId[id])
16
+ .filter((message) => message?.role === 'user')
17
+ .map((message) => message!.parts.filter(isTextPart).map((part) => part.text).join('').trim())
18
+ .filter(Boolean);
19
+ }, [store, ids]);
20
+ }
@@ -0,0 +1,106 @@
1
+ import type { VoiceCapability } from '@djangocfg/widget-chat';
2
+
3
+ /**
4
+ * Browser dictation that always reports EVERYTHING heard since the start, as one text.
5
+ *
6
+ * The widget's own engine reports each result of the recogniser separately and the session stitches them (final results appended, the interim replaced). Chrome reports
7
+ * several results at once, ends the recogniser by itself after a pause, and revises earlier results, and under those the field kept only the last words. Here the whole
8
+ * transcript is rebuilt from `event.results` at every event and handed over as one replacing text, so nothing can be dropped by the stitching; a recogniser that ends on
9
+ * its own is started again and its text is kept in front of the next one.
10
+ */
11
+
12
+ interface ResultLike {
13
+ readonly length: number;
14
+ readonly [index: number]: { readonly transcript: string } | undefined;
15
+ }
16
+ interface EventLike {
17
+ readonly results: { readonly length: number; readonly [index: number]: ResultLike | undefined };
18
+ }
19
+ interface RecognitionLike {
20
+ continuous: boolean;
21
+ interimResults: boolean;
22
+ lang: string;
23
+ start: () => void;
24
+ stop: () => void;
25
+ abort: () => void;
26
+ onresult: ((event: EventLike) => void) | null;
27
+ onerror: ((event: { readonly error?: string }) => void) | null;
28
+ onend: (() => void) | null;
29
+ }
30
+ type Constructor = new () => RecognitionLike;
31
+
32
+ function constructorFor(): Constructor | undefined {
33
+ if (typeof window === 'undefined') return undefined;
34
+ const scope = window as unknown as { SpeechRecognition?: Constructor; webkitSpeechRecognition?: Constructor };
35
+ return scope.SpeechRecognition ?? scope.webkitSpeechRecognition;
36
+ }
37
+
38
+ const join = (parts: string[]) => parts.map((part) => part.trim()).filter(Boolean).join(' ');
39
+
40
+ /** `language` is read at every (re)start, so a language picked in the menu applies to the next dictation. `undefined` when the browser cannot dictate. */
41
+ export function createWholeTranscriptSpeech(language: () => string): VoiceCapability | undefined {
42
+ const Recognition = constructorFor();
43
+ if (!Recognition) return undefined;
44
+ let active: RecognitionLike | null = null;
45
+
46
+ return {
47
+ start: (onText, signal) =>
48
+ new Promise<void>((resolve, reject) => {
49
+ if (signal.aborted) return resolve();
50
+ // What earlier recogniser runs of this dictation heard (a run that ended by itself is started again).
51
+ let before = '';
52
+ let latest = '';
53
+
54
+ const run = () => {
55
+ const recognition = new Recognition();
56
+ active = recognition;
57
+ recognition.continuous = true;
58
+ recognition.interimResults = true;
59
+ recognition.lang = language();
60
+
61
+ recognition.onresult = (event) => {
62
+ const heard: string[] = [];
63
+ for (let i = 0; i < event.results.length; i += 1) {
64
+ const text = event.results[i]?.[0]?.transcript;
65
+ if (text) heard.push(text);
66
+ }
67
+ latest = join([before, ...heard]);
68
+ // Always `final: false`: each report REPLACES the last, and the session's own stop commits the latest one.
69
+ onText(latest, false);
70
+ };
71
+ recognition.onerror = (event) => {
72
+ if (event.error === 'no-speech' || event.error === 'aborted') return;
73
+ signal.removeEventListener('abort', onAbort);
74
+ active = null;
75
+ reject(new Error(event.error ?? 'speech recognition failed'));
76
+ };
77
+ recognition.onend = () => {
78
+ if (active === recognition) active = null;
79
+ if (signal.aborted) return resolve();
80
+ // Ended by itself (a pause, a time limit): keep what was heard and listen again.
81
+ before = latest;
82
+ try {
83
+ run();
84
+ } catch {
85
+ resolve();
86
+ }
87
+ };
88
+ try {
89
+ recognition.start();
90
+ } catch {
91
+ resolve();
92
+ }
93
+ };
94
+
95
+ const onAbort = () => {
96
+ active?.abort();
97
+ active = null;
98
+ resolve();
99
+ };
100
+ signal.addEventListener('abort', onAbort, { once: true });
101
+ run();
102
+ }),
103
+ // Gracefully: the engine may still report what was said, and the session reads it until its own stop.
104
+ stop: () => active?.stop(),
105
+ };
106
+ }
@@ -0,0 +1,36 @@
1
+ import { createCopy, type Locale } from '@djangocfg/i18n/copy';
2
+
3
+ import { ar } from './locales/ar';
4
+ import { da } from './locales/da';
5
+ import { de } from './locales/de';
6
+ import { en } from './locales/en';
7
+ import { es } from './locales/es';
8
+ import { fr } from './locales/fr';
9
+ import { it } from './locales/it';
10
+ import { ja } from './locales/ja';
11
+ import { ko } from './locales/ko';
12
+ import { nl } from './locales/nl';
13
+ import { no } from './locales/no';
14
+ import { pl } from './locales/pl';
15
+ import { ptBR } from './locales/pt-BR';
16
+ import { ru } from './locales/ru';
17
+ import { sv } from './locales/sv';
18
+ import { tr } from './locales/tr';
19
+ import { zh } from './locales/zh';
20
+ import type { AssistantCopy } from './types';
21
+
22
+ export type { AssistantCopy } from './types';
23
+
24
+ /** Every bundled dictionary, typed complete over the shared `Locale` union. */
25
+ export const ASSISTANT_DICTIONARIES: Readonly<Record<Locale, AssistantCopy>> = {
26
+ en, ru, ko, ja, de, fr, zh, it, es, nl, ar, tr, pl, sv, no, da,
27
+ 'pt-BR': ptBR,
28
+ };
29
+
30
+ const kit = createCopy(ASSISTANT_DICTIONARIES);
31
+
32
+ /**
33
+ * The assistant's own words for the active language: the `locale` prop, else the
34
+ * `@djangocfg/i18n` provider, else English. The host passes no strings.
35
+ */
36
+ export const useAssistantCopy = kit.useCopy;
@@ -0,0 +1,34 @@
1
+ import type { AssistantCopy } from '../types';
2
+
3
+ export const ar: AssistantCopy = {
4
+ close: 'إغلاق',
5
+ resize: 'تغيير عرض المساعد',
6
+ lockedTitle: 'سجّل الدخول للدردشة',
7
+ lockedBody: 'يختار المساعد الأفلام لك ويتذكر المحادثة. سجّل الدخول للبدء.',
8
+ lockedComposer: 'سجّل الدخول لتسأل',
9
+ signIn: 'تسجيل الدخول',
10
+ hint: 'نتحدث عن الأفلام مع الذكاء الاصطناعي؟',
11
+ statusThinking: 'أفكر…',
12
+ statusSearching: 'أبحث في الكتالوج…',
13
+ statusReading: 'أقرأ عن الأفلام…',
14
+ statusChoosing: 'أختار الأفضل…',
15
+ errAuth: 'سجّل الدخول لتسأل المساعد.',
16
+ errBusy: 'أنهِ أولاً في التبويب الآخر.',
17
+ errInvalid: 'الرسالة طويلة جدًا. اختصرها وأعد المحاولة.',
18
+ errRate: 'رسائل كثيرة جدًا. أعد المحاولة بعد {{seconds}} ث.',
19
+ errUnavailable: 'المساعد في استراحة. أعد المحاولة لاحقًا.',
20
+ failed: 'فشل البحث. حاول مجددًا.',
21
+ reconnecting: 'إعادة الاتصال…',
22
+ stalled: 'ما زالت الإجابة قيد الإعداد.',
23
+ retry: 'إعادة المحاولة',
24
+ clear: 'مسح المحادثة',
25
+ clearConfirm: 'حذف هذه المحادثة؟ لا يمكن التراجع.',
26
+ clearYes: 'حذف',
27
+ loadError: 'تعذّر تحميل المحادثة.',
28
+ helpLabel: 'مساعدة',
29
+ helpDictate: 'بدء الإملاء أو إيقافه',
30
+ helpSend: 'إرسال (أثناء الإملاء: إيقاف ثم إرسال)',
31
+ helpStop: 'إيقاف الإملاء مع إبقاء النص',
32
+ helpRecall: 'الأسئلة السابقة',
33
+ helpNewline: 'سطر جديد',
34
+ };
@@ -0,0 +1,34 @@
1
+ import type { AssistantCopy } from '../types';
2
+
3
+ export const da: AssistantCopy = {
4
+ close: 'Luk',
5
+ resize: 'Skift assistentens bredde',
6
+ lockedTitle: 'Log ind for at chatte',
7
+ lockedBody: 'Assistenten vælger film til dig og husker samtalen. Log ind for at komme i gang.',
8
+ lockedComposer: 'Log ind for at spørge',
9
+ signIn: 'Log ind',
10
+ hint: 'Skal vi snakke film med AI?',
11
+ statusThinking: 'Tænker…',
12
+ statusSearching: 'Søger i kataloget…',
13
+ statusReading: 'Læser om filmene…',
14
+ statusChoosing: 'Vælger de bedste…',
15
+ errAuth: 'Log ind for at spørge assistenten.',
16
+ errBusy: 'Afslut først i den anden fane.',
17
+ errInvalid: 'Beskeden er for lang. Forkort den og prøv igen.',
18
+ errRate: 'For mange beskeder. Prøv igen om {{seconds}} s.',
19
+ errUnavailable: 'Assistenten hviler. Prøv igen senere.',
20
+ failed: 'Søgningen mislykkedes. Prøv igen.',
21
+ reconnecting: 'Forbinder igen…',
22
+ stalled: 'Svaret er stadig undervejs.',
23
+ retry: 'Prøv igen',
24
+ clear: 'Ryd chatten',
25
+ clearConfirm: 'Slet denne samtale? Det kan ikke fortrydes.',
26
+ clearYes: 'Slet',
27
+ loadError: 'Samtalen kunne ikke indlæses.',
28
+ helpLabel: 'Hjælp',
29
+ helpDictate: 'Start eller stop diktering',
30
+ helpSend: 'Send (under diktering: stop og send)',
31
+ helpStop: 'Stop diktering, behold teksten',
32
+ helpRecall: 'Tidligere spørgsmål',
33
+ helpNewline: 'Ny linje',
34
+ };
@@ -0,0 +1,34 @@
1
+ import type { AssistantCopy } from '../types';
2
+
3
+ export const de: AssistantCopy = {
4
+ close: 'Schließen',
5
+ resize: 'Breite des Assistenten ändern',
6
+ lockedTitle: 'Anmelden, um zu chatten',
7
+ lockedBody: 'Der Assistent wählt Filme für dich aus und merkt sich das Gespräch. Melde dich an, um zu starten.',
8
+ lockedComposer: 'Anmelden, um zu fragen',
9
+ signIn: 'Anmelden',
10
+ hint: 'Über Filme mit KI sprechen?',
11
+ statusThinking: 'Ich denke nach…',
12
+ statusSearching: 'Durchsuche den Katalog…',
13
+ statusReading: 'Lese über die Filme…',
14
+ statusChoosing: 'Wähle die besten aus…',
15
+ errAuth: 'Melde dich an, um den Assistenten zu fragen.',
16
+ errBusy: 'Beende zuerst die Antwort im anderen Tab.',
17
+ errInvalid: 'Die Nachricht ist zu lang. Kürze sie und versuche es erneut.',
18
+ errRate: 'Zu viele Nachrichten. Versuche es in {{seconds}} s erneut.',
19
+ errUnavailable: 'Der Assistent pausiert. Versuche es später erneut.',
20
+ failed: 'Die Suche ist fehlgeschlagen. Bitte erneut versuchen.',
21
+ reconnecting: 'Verbinde neu…',
22
+ stalled: 'Die Antwort wird noch erstellt.',
23
+ retry: 'Erneut versuchen',
24
+ clear: 'Chat leeren',
25
+ clearConfirm: 'Diese Unterhaltung löschen? Das lässt sich nicht rückgängig machen.',
26
+ clearYes: 'Löschen',
27
+ loadError: 'Die Unterhaltung konnte nicht geladen werden.',
28
+ helpLabel: 'Hilfe',
29
+ helpDictate: 'Diktat starten oder beenden',
30
+ helpSend: 'Senden (beim Diktieren: beenden und senden)',
31
+ helpStop: 'Diktat beenden, Text behalten',
32
+ helpRecall: 'Frühere Fragen',
33
+ helpNewline: 'Neue Zeile',
34
+ };
@@ -0,0 +1,34 @@
1
+ import type { AssistantCopy } from '../types';
2
+
3
+ export const en: AssistantCopy = {
4
+ close: 'Close',
5
+ resize: 'Resize the assistant',
6
+ lockedTitle: 'Sign in to chat',
7
+ lockedBody: 'The assistant picks films for you and remembers the conversation. Sign in to start.',
8
+ lockedComposer: 'Sign in to ask anything',
9
+ signIn: 'Sign in',
10
+ hint: 'Talk films with AI?',
11
+ statusThinking: 'Thinking…',
12
+ statusSearching: 'Searching the catalog…',
13
+ statusReading: 'Reading about the films…',
14
+ statusChoosing: 'Choosing the best ones…',
15
+ errAuth: 'Sign in to ask the assistant.',
16
+ errBusy: 'Finish in the other tab first.',
17
+ errInvalid: 'That message is too long. Shorten it and try again.',
18
+ errRate: 'Too many messages. Try again in {{seconds}} s.',
19
+ errUnavailable: 'The assistant is resting. Try again later.',
20
+ failed: 'Search failed. Try again.',
21
+ reconnecting: 'Reconnecting…',
22
+ stalled: 'Still working on the answer.',
23
+ retry: 'Retry',
24
+ clear: 'Clear chat',
25
+ clearConfirm: 'Delete this conversation? This cannot be undone.',
26
+ clearYes: 'Delete',
27
+ loadError: 'Could not load the conversation.',
28
+ helpLabel: 'Help',
29
+ helpDictate: 'Start or stop dictation',
30
+ helpSend: 'Send (while dictating: stop and send)',
31
+ helpStop: 'Stop dictation, keep the text',
32
+ helpRecall: 'Earlier questions',
33
+ helpNewline: 'New line',
34
+ };
@@ -0,0 +1,34 @@
1
+ import type { AssistantCopy } from '../types';
2
+
3
+ export const es: AssistantCopy = {
4
+ close: 'Cerrar',
5
+ resize: 'Cambiar el ancho del asistente',
6
+ lockedTitle: 'Inicia sesión para chatear',
7
+ lockedBody: 'El asistente elige películas para ti y recuerda la conversación. Inicia sesión para empezar.',
8
+ lockedComposer: 'Inicia sesión para preguntar',
9
+ signIn: 'Iniciar sesión',
10
+ hint: '¿Hablamos de películas con la IA?',
11
+ statusThinking: 'Pensando…',
12
+ statusSearching: 'Buscando en el catálogo…',
13
+ statusReading: 'Leyendo sobre las películas…',
14
+ statusChoosing: 'Eligiendo las mejores…',
15
+ errAuth: 'Inicia sesión para preguntar al asistente.',
16
+ errBusy: 'Termina primero en la otra pestaña.',
17
+ errInvalid: 'El mensaje es demasiado largo. Acórtalo e inténtalo de nuevo.',
18
+ errRate: 'Demasiados mensajes. Inténtalo de nuevo en {{seconds}} s.',
19
+ errUnavailable: 'El asistente está descansando. Inténtalo más tarde.',
20
+ failed: 'Falló la búsqueda. Inténtalo de nuevo.',
21
+ reconnecting: 'Reconectando…',
22
+ stalled: 'La respuesta sigue en curso.',
23
+ retry: 'Reintentar',
24
+ clear: 'Borrar chat',
25
+ clearConfirm: '¿Eliminar esta conversación? No se puede deshacer.',
26
+ clearYes: 'Eliminar',
27
+ loadError: 'No se pudo cargar la conversación.',
28
+ helpLabel: 'Ayuda',
29
+ helpDictate: 'Iniciar o detener el dictado',
30
+ helpSend: 'Enviar (al dictar: detener y enviar)',
31
+ helpStop: 'Detener el dictado y conservar el texto',
32
+ helpRecall: 'Preguntas anteriores',
33
+ helpNewline: 'Nueva línea',
34
+ };
@@ -0,0 +1,34 @@
1
+ import type { AssistantCopy } from '../types';
2
+
3
+ export const fr: AssistantCopy = {
4
+ close: 'Fermer',
5
+ resize: 'Redimensionner l’assistant',
6
+ lockedTitle: 'Connectez-vous pour discuter',
7
+ lockedBody: 'L’assistant choisit des films pour vous et se souvient de la conversation. Connectez-vous pour commencer.',
8
+ lockedComposer: 'Connectez-vous pour demander',
9
+ signIn: 'Se connecter',
10
+ hint: 'On parle films avec l’IA ?',
11
+ statusThinking: 'Je réfléchis…',
12
+ statusSearching: 'Recherche dans le catalogue…',
13
+ statusReading: 'Lecture sur les films…',
14
+ statusChoosing: 'Je choisis les meilleurs…',
15
+ errAuth: 'Connectez-vous pour interroger l\'assistant.',
16
+ errBusy: 'Terminez d\'abord dans l\'autre onglet.',
17
+ errInvalid: 'Ce message est trop long. Raccourcissez-le et réessayez.',
18
+ errRate: 'Trop de messages. Réessayez dans {{seconds}} s.',
19
+ errUnavailable: 'L\'assistant se repose. Réessayez plus tard.',
20
+ failed: 'La recherche a échoué. Réessayez.',
21
+ reconnecting: 'Reconnexion…',
22
+ stalled: 'La réponse est encore en cours.',
23
+ retry: 'Réessayer',
24
+ clear: 'Effacer le chat',
25
+ clearConfirm: 'Supprimer cette conversation ? Cette action est irréversible.',
26
+ clearYes: 'Supprimer',
27
+ loadError: 'Impossible de charger la conversation.',
28
+ helpLabel: 'Aide',
29
+ helpDictate: 'Démarrer ou arrêter la dictée',
30
+ helpSend: 'Envoyer (en dictée : arrêter et envoyer)',
31
+ helpStop: 'Arrêter la dictée, garder le texte',
32
+ helpRecall: 'Questions précédentes',
33
+ helpNewline: 'Nouvelle ligne',
34
+ };
@@ -0,0 +1,34 @@
1
+ import type { AssistantCopy } from '../types';
2
+
3
+ export const it: AssistantCopy = {
4
+ close: 'Chiudi',
5
+ resize: 'Ridimensiona l’assistente',
6
+ lockedTitle: 'Accedi per chattare',
7
+ lockedBody: 'L’assistente sceglie i film per te e ricorda la conversazione. Accedi per iniziare.',
8
+ lockedComposer: 'Accedi per chiedere',
9
+ signIn: 'Accedi',
10
+ hint: 'Parliamo di film con l’IA?',
11
+ statusThinking: 'Sto pensando…',
12
+ statusSearching: 'Cerco nel catalogo…',
13
+ statusReading: 'Leggo i film…',
14
+ statusChoosing: 'Scelgo i migliori…',
15
+ errAuth: 'Accedi per chiedere all\'assistente.',
16
+ errBusy: 'Prima termina nell\'altra scheda.',
17
+ errInvalid: 'Il messaggio è troppo lungo. Accorcialo e riprova.',
18
+ errRate: 'Troppi messaggi. Riprova tra {{seconds}} s.',
19
+ errUnavailable: 'L\'assistente si sta riposando. Riprova più tardi.',
20
+ failed: 'Ricerca non riuscita. Riprova.',
21
+ reconnecting: 'Riconnessione…',
22
+ stalled: 'La risposta è ancora in corso.',
23
+ retry: 'Riprova',
24
+ clear: 'Cancella chat',
25
+ clearConfirm: 'Eliminare questa conversazione? Non si può annullare.',
26
+ clearYes: 'Elimina',
27
+ loadError: 'Impossibile caricare la conversazione.',
28
+ helpLabel: 'Aiuto',
29
+ helpDictate: 'Avvia o ferma la dettatura',
30
+ helpSend: 'Invia (durante la dettatura: ferma e invia)',
31
+ helpStop: 'Ferma la dettatura, mantieni il testo',
32
+ helpRecall: 'Domande precedenti',
33
+ helpNewline: 'Nuova riga',
34
+ };
@@ -0,0 +1,34 @@
1
+ import type { AssistantCopy } from '../types';
2
+
3
+ export const ja: AssistantCopy = {
4
+ close: '閉じる',
5
+ resize: 'アシスタントの幅を変更',
6
+ lockedTitle: 'チャットするにはサインイン',
7
+ lockedBody: 'アシスタントがあなたに合う映画を選び、会話を覚えています。サインインして始めましょう。',
8
+ lockedComposer: '質問するにはサインイン',
9
+ signIn: 'サインイン',
10
+ hint: 'AIと映画の話をしませんか?',
11
+ statusThinking: '考え中…',
12
+ statusSearching: 'カタログを検索中…',
13
+ statusReading: '作品について調べ中…',
14
+ statusChoosing: 'おすすめを選んでいます…',
15
+ errAuth: 'アシスタントに聞くにはログインしてください。',
16
+ errBusy: '先に別のタブでの回答をお待ちください。',
17
+ errInvalid: 'メッセージが長すぎます。短くして再試行してください。',
18
+ errRate: 'メッセージが多すぎます。{{seconds}}秒後にお試しください。',
19
+ errUnavailable: 'アシスタントは休止中です。後でお試しください。',
20
+ failed: '検索に失敗しました。もう一度お試しください。',
21
+ reconnecting: '再接続中…',
22
+ stalled: 'まだ回答を作成中です。',
23
+ retry: '再試行',
24
+ clear: 'チャットを消去',
25
+ clearConfirm: 'この会話を削除しますか?元に戻せません。',
26
+ clearYes: '削除',
27
+ loadError: '会話を読み込めませんでした。',
28
+ helpLabel: 'ヘルプ',
29
+ helpDictate: '音声入力の開始/停止',
30
+ helpSend: '送信(音声入力中は停止して送信)',
31
+ helpStop: '音声入力を止めてテキストを残す',
32
+ helpRecall: '以前の質問',
33
+ helpNewline: '改行',
34
+ };
@@ -0,0 +1,34 @@
1
+ import type { AssistantCopy } from '../types';
2
+
3
+ export const ko: AssistantCopy = {
4
+ close: '닫기',
5
+ resize: '어시스턴트 너비 조절',
6
+ lockedTitle: '채팅하려면 로그인하세요',
7
+ lockedBody: '어시스턴트가 영화를 골라 주고 대화를 기억합니다. 시작하려면 로그인하세요.',
8
+ lockedComposer: '질문하려면 로그인하세요',
9
+ signIn: '로그인',
10
+ hint: 'AI와 영화 이야기 나눠볼까요?',
11
+ statusThinking: '생각 중…',
12
+ statusSearching: '카탈로그에서 찾는 중…',
13
+ statusReading: '영화 정보를 읽는 중…',
14
+ statusChoosing: '가장 좋은 작품을 고르는 중…',
15
+ errAuth: '어시스턴트에게 물으려면 로그인하세요.',
16
+ errBusy: '다른 탭에서 먼저 끝내 주세요.',
17
+ errInvalid: '메시지가 너무 깁니다. 줄여서 다시 시도하세요.',
18
+ errRate: '메시지가 너무 많습니다. {{seconds}}초 후 다시 시도하세요.',
19
+ errUnavailable: '어시스턴트가 쉬고 있습니다. 나중에 다시 시도하세요.',
20
+ failed: '검색에 실패했어요. 다시 시도해 주세요.',
21
+ reconnecting: '다시 연결하는 중…',
22
+ stalled: '아직 답변을 준비 중입니다.',
23
+ retry: '다시 시도',
24
+ clear: '대화 지우기',
25
+ clearConfirm: '이 대화를 삭제할까요? 되돌릴 수 없습니다.',
26
+ clearYes: '삭제',
27
+ loadError: '대화를 불러오지 못했습니다.',
28
+ helpLabel: '도움말',
29
+ helpDictate: '음성 입력 시작/중지',
30
+ helpSend: '보내기(음성 입력 중에는 중지 후 보내기)',
31
+ helpStop: '음성 입력 중지, 텍스트 유지',
32
+ helpRecall: '이전 질문',
33
+ helpNewline: '줄 바꿈',
34
+ };
@@ -0,0 +1,34 @@
1
+ import type { AssistantCopy } from '../types';
2
+
3
+ export const nl: AssistantCopy = {
4
+ close: 'Sluiten',
5
+ resize: 'Breedte van de assistent aanpassen',
6
+ lockedTitle: 'Log in om te chatten',
7
+ lockedBody: 'De assistent kiest films voor je en onthoudt het gesprek. Log in om te beginnen.',
8
+ lockedComposer: 'Log in om te vragen',
9
+ signIn: 'Inloggen',
10
+ hint: 'Praten over films met AI?',
11
+ statusThinking: 'Even nadenken…',
12
+ statusSearching: 'Zoeken in de catalogus…',
13
+ statusReading: 'Lezen over de films…',
14
+ statusChoosing: 'De beste kiezen…',
15
+ errAuth: 'Log in om de assistent te vragen.',
16
+ errBusy: 'Rond eerst af in het andere tabblad.',
17
+ errInvalid: 'Dat bericht is te lang. Maak het korter en probeer opnieuw.',
18
+ errRate: 'Te veel berichten. Probeer het over {{seconds}} s opnieuw.',
19
+ errUnavailable: 'De assistent rust even. Probeer het later opnieuw.',
20
+ failed: 'Zoeken mislukt. Probeer het opnieuw.',
21
+ reconnecting: 'Opnieuw verbinden…',
22
+ stalled: 'Het antwoord wordt nog gemaakt.',
23
+ retry: 'Opnieuw proberen',
24
+ clear: 'Chat wissen',
25
+ clearConfirm: 'Dit gesprek verwijderen? Dit kan niet ongedaan worden gemaakt.',
26
+ clearYes: 'Verwijderen',
27
+ loadError: 'Het gesprek kon niet worden geladen.',
28
+ helpLabel: 'Help',
29
+ helpDictate: 'Dicteren starten of stoppen',
30
+ helpSend: 'Verzenden (tijdens dicteren: stoppen en verzenden)',
31
+ helpStop: 'Dicteren stoppen, tekst behouden',
32
+ helpRecall: 'Eerdere vragen',
33
+ helpNewline: 'Nieuwe regel',
34
+ };
@@ -0,0 +1,34 @@
1
+ import type { AssistantCopy } from '../types';
2
+
3
+ export const no: AssistantCopy = {
4
+ close: 'Lukk',
5
+ resize: 'Endre bredden på assistenten',
6
+ lockedTitle: 'Logg inn for å chatte',
7
+ lockedBody: 'Assistenten velger filmer for deg og husker samtalen. Logg inn for å komme i gang.',
8
+ lockedComposer: 'Logg inn for å spørre',
9
+ signIn: 'Logg inn',
10
+ hint: 'Skal vi snakke film med AI?',
11
+ statusThinking: 'Tenker…',
12
+ statusSearching: 'Søker i katalogen…',
13
+ statusReading: 'Leser om filmene…',
14
+ statusChoosing: 'Velger de beste…',
15
+ errAuth: 'Logg inn for å spørre assistenten.',
16
+ errBusy: 'Fullfør først i den andre fanen.',
17
+ errInvalid: 'Meldingen er for lang. Kort den ned og prøv igjen.',
18
+ errRate: 'For mange meldinger. Prøv igjen om {{seconds}} s.',
19
+ errUnavailable: 'Assistenten hviler. Prøv igjen senere.',
20
+ failed: 'Søket mislyktes. Prøv igjen.',
21
+ reconnecting: 'Kobler til igjen…',
22
+ stalled: 'Svaret lages fortsatt.',
23
+ retry: 'Prøv igjen',
24
+ clear: 'Tøm chatten',
25
+ clearConfirm: 'Slette denne samtalen? Det kan ikke angres.',
26
+ clearYes: 'Slett',
27
+ loadError: 'Samtalen kunne ikke lastes.',
28
+ helpLabel: 'Hjelp',
29
+ helpDictate: 'Start eller stopp diktering',
30
+ helpSend: 'Send (under diktering: stopp og send)',
31
+ helpStop: 'Stopp diktering, behold teksten',
32
+ helpRecall: 'Tidligere spørsmål',
33
+ helpNewline: 'Ny linje',
34
+ };
@@ -0,0 +1,34 @@
1
+ import type { AssistantCopy } from '../types';
2
+
3
+ export const pl: AssistantCopy = {
4
+ close: 'Zamknij',
5
+ resize: 'Zmień szerokość asystenta',
6
+ lockedTitle: 'Zaloguj się, aby czatować',
7
+ lockedBody: 'Asystent wybiera filmy dla Ciebie i pamięta rozmowę. Zaloguj się, aby zacząć.',
8
+ lockedComposer: 'Zaloguj się, aby zapytać',
9
+ signIn: 'Zaloguj się',
10
+ hint: 'Pogadamy o filmach z AI?',
11
+ statusThinking: 'Myślę…',
12
+ statusSearching: 'Przeszukuję katalog…',
13
+ statusReading: 'Czytam o filmach…',
14
+ statusChoosing: 'Wybieram najlepsze…',
15
+ errAuth: 'Zaloguj się, aby zapytać asystenta.',
16
+ errBusy: 'Najpierw dokończ w drugiej karcie.',
17
+ errInvalid: 'Wiadomość jest za długa. Skróć ją i spróbuj ponownie.',
18
+ errRate: 'Za dużo wiadomości. Spróbuj za {{seconds}} s.',
19
+ errUnavailable: 'Asystent odpoczywa. Spróbuj później.',
20
+ failed: 'Wyszukiwanie nie powiodło się. Spróbuj ponownie.',
21
+ reconnecting: 'Łączę ponownie…',
22
+ stalled: 'Odpowiedź wciąż powstaje.',
23
+ retry: 'Spróbuj ponownie',
24
+ clear: 'Wyczyść czat',
25
+ clearConfirm: 'Usunąć tę rozmowę? Nie można tego cofnąć.',
26
+ clearYes: 'Usuń',
27
+ loadError: 'Nie udało się wczytać rozmowy.',
28
+ helpLabel: 'Pomoc',
29
+ helpDictate: 'Włącz lub zatrzymaj dyktowanie',
30
+ helpSend: 'Wyślij (podczas dyktowania: zatrzymaj i wyślij)',
31
+ helpStop: 'Zatrzymaj dyktowanie, zostaw tekst',
32
+ helpRecall: 'Poprzednie pytania',
33
+ helpNewline: 'Nowy wiersz',
34
+ };
@@ -0,0 +1,34 @@
1
+ import type { AssistantCopy } from '../types';
2
+
3
+ export const ptBR: AssistantCopy = {
4
+ close: 'Fechar',
5
+ resize: 'Redimensionar o assistente',
6
+ lockedTitle: 'Entre para conversar',
7
+ lockedBody: 'O assistente escolhe filmes para você e lembra da conversa. Entre para começar.',
8
+ lockedComposer: 'Entre para perguntar',
9
+ signIn: 'Entrar',
10
+ hint: 'Vamos falar de filmes com a IA?',
11
+ statusThinking: 'Pensando…',
12
+ statusSearching: 'Buscando no catálogo…',
13
+ statusReading: 'Lendo sobre os filmes…',
14
+ statusChoosing: 'Escolhendo os melhores…',
15
+ errAuth: 'Entre para perguntar ao assistente.',
16
+ errBusy: 'Termine primeiro na outra aba.',
17
+ errInvalid: 'A mensagem é longa demais. Encurte e tente de novo.',
18
+ errRate: 'Mensagens demais. Tente de novo em {{seconds}} s.',
19
+ errUnavailable: 'O assistente está descansando. Tente mais tarde.',
20
+ failed: 'A busca falhou. Tente de novo.',
21
+ reconnecting: 'Reconectando…',
22
+ stalled: 'A resposta ainda está sendo preparada.',
23
+ retry: 'Tentar de novo',
24
+ clear: 'Limpar conversa',
25
+ clearConfirm: 'Excluir esta conversa? Não dá para desfazer.',
26
+ clearYes: 'Excluir',
27
+ loadError: 'Não foi possível carregar a conversa.',
28
+ helpLabel: 'Ajuda',
29
+ helpDictate: 'Iniciar ou parar o ditado',
30
+ helpSend: 'Enviar (durante o ditado: parar e enviar)',
31
+ helpStop: 'Parar o ditado e manter o texto',
32
+ helpRecall: 'Perguntas anteriores',
33
+ helpNewline: 'Nova linha',
34
+ };