Voice (dictate, read aloud) and Draw a picture over /v1, with the app's models scope
ci / ui (push) Waiting to run
ci / check (push) Failing after 3s
ci / start (push) Failing after 3s

The server kept voice and images on /v1 (models scope, which the app's token
already asks for); /me's services and scopes decide what is offered. Rust
sends the recording to /v1/audio/transcriptions, reads /v1/audio/speech back
as a data: URL (CSP media-src data:), and for /v1/images/generations takes
only the id of each of the person's own files from the answer and fetches
/v1/files/<id>/content from the app's own server. The microphone is allowed
only within seconds of the page's own Dictate (voice_arm); WebKitGTK's
media stream is switched on through its settings getter.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
HomerandClaude Opus 5.5 committed 2026-11-10 14:02:56 +00:00
1 parent 67ce6076c5
commit 2b87fa4c29
16 files changed
+1024 -4

No files matched your search

+19 -2
View File
@@ -1168,6 +1168,18 @@ details[open] > summary .step__chev .icon { transform: rotate(90deg); }
box-shadow: var(--shadow-lg);
}
.dialog::backdrop { background: var(--scrim); }
.dialog--wide { width: min(var(--w-prose), calc(100vw - 2 * var(--sp-4))); }
/* Draw a picture (draw.js): the prompt, a shape and two buttons on one line, the pictures below. */
.draw .dialog__title { display: flex; gap: var(--sp-2); align-items: center; }
.draw__prompt { width: 100%; }
.draw__bar { display: grid; grid-template-columns: minmax(0, 1fr) auto auto; gap: var(--sp-2); align-items: center; }
.draw__size { justify-self: start; max-width: 100%; }
.draw__out { display: grid; gap: var(--sp-3); max-height: 60vh; overflow-y: auto; }
.draw__out:empty { display: none; }
.draw__pic { display: grid; gap: var(--sp-1); margin: 0; }
.draw__pic img { display: block; width: 100%; height: auto; border-radius: var(--radius-lg); background: var(--surface); }
.draw__pic figcaption { font-size: var(--text-xs); color: var(--ink-muted); overflow-wrap: anywhere; }
.dialog__body { display: grid; gap: var(--sp-3); padding: var(--sp-4); }
.dialog__title { font: 600 var(--text-md) / var(--leading-tight) var(--font-ui); }
/* A line to copy: as it is, on one line that scrolls on its own rather than the dialog. */
@@ -1276,11 +1288,16 @@ details[open] > summary .step__chev .icon { transform: rotate(90deg); }
.composer__files { justify-content: start; }
.composer__bar {
display: grid;
grid-template-columns: var(--chip-h) minmax(0, 1fr) auto auto;
grid-template-columns: var(--chip-h) minmax(0, 1fr) auto auto auto;
gap: var(--sp-2);
align-items: center;
}
.composer[data-attach="false"] .composer__bar { grid-template-columns: minmax(0, 1fr) auto auto; }
.composer[data-attach="false"] .composer__bar { grid-template-columns: minmax(0, 1fr) auto auto auto; }
/* Dictate: a round tool beside Send; while it records it says so in the accent. */
.composer__mic { width: var(--round); height: var(--round); border-radius: var(--radius-full); }
.composer .composer__mic.is-recording { color: var(--accent-ink); background: var(--accent); }
.composer__mic.is-busy, .msg__speak.is-busy { opacity: 0.6; cursor: progress; }
.msg__speak[aria-pressed="true"] { color: var(--accent); }
.composer__attach { width: var(--chip-h); height: var(--chip-h); border: var(--border-w) solid var(--border); border-radius: var(--radius-full); }
/* The chips stay on one line; where they do not fit, the line scrolls by itself. */
.composer__chips { display: flex; flex-wrap: nowrap; gap: var(--sp-1); min-width: 0; align-items: center; overflow-x: auto; scrollbar-width: none; }
+120
View File
@@ -116,8 +116,12 @@ const LEMBAS_I18N = {
"Delete this chat?": "Smazat tuto konverzaci?",
"Delete this scheduled task?": "Odstranit tuto naplánovanou úlohu?",
"Deny": "Zamítnout",
"Dictate a message": "Nadiktovat zprávu",
"Disconnected. Close and reopen to reconnect.": "Odpojeno. Pro opětovné připojení zavřete a znovu otevřete.",
"Done": "Hotovo",
"Draw": "Nakreslit",
"Draw a picture": "Nakreslit obrázek",
"Drawing… a picture can take a minute or two.": "Kreslím… obrázek může trvat minutu nebo dvě.",
"Drop the files to attach them": "Pusťte soubory a přiloží se",
"Due %(when)s": "Naplánováno na %(when)s",
"Each run sees the ones before it.": "Každé spuštění vidí ta předchozí.",
@@ -248,6 +252,7 @@ const LEMBAS_I18N = {
"Queued": "Ve frontě",
"Queued: {{ahead}} ahead": "Ve frontě: před ním {{ahead}}",
"Read again": "Načíst znovu",
"Read this aloud": "Přečíst nahlas",
"Reading the prompt": "Čte se výzva",
"Reading the prompt: {{done}} / {{total}} tokens ({{percent}}%)": "Čte se výzva: {{done}} / {{total}} tokenů ({{percent}} %)",
"Reasoning effort": "Náročnost uvažování",
@@ -290,6 +295,7 @@ const LEMBAS_I18N = {
"Server": "Server",
"Set the time yourself": "Nastavit čas sami",
"Settings": "Nastavení",
"Shape": "Tvar",
"Show %(n)s more": "Zobrazit další (%(n)s)",
"Shown by the agent": "Ukázal agent",
"Sidebar": "Postranní panel",
@@ -303,11 +309,13 @@ const LEMBAS_I18N = {
"Skipped": "Vynecháno",
"Something to remember…": "Něco k zapamatování…",
"Source": "Zdroj",
"Square": "Čtverec",
"Start the agent": "Spustit agenta",
"Stop": "Zastavit",
"Stop %(name)s": "Zastavit %(name)s",
"Stop %(name)s and the members under it? They leave the team.": "Zastavit %(name)s a členy pod ním? Opustí tým.",
"Stop %(name)s and the subagents under it?": "Zastavit %(name)s a podagenty pod ním?",
"Stop dictating": "Zastavit diktování",
"Stop the agent": "Zastavit agenta",
"Stopped": "Zastaveno",
"Stopped.": "Zastaveno.",
@@ -319,6 +327,7 @@ const LEMBAS_I18N = {
"Switched off here only. What is on to begin with is your default for this model, in Settings → Models.": "Vypnuto jen tady. Co je na začátku zapnuté, je vaše výchozí nastavení pro tento model v Nastavení → Modely.",
"System prompt": "Systémový pokyn",
"Taken in while it ran:": "Převzato během běhu:",
"Tall": "Vysoký",
"Tasks: %(n)s": "Úlohy: %(n)s",
"Team member": "Člen týmu",
"Team: %(size)s of at most %(max)s subagents": "Tým: %(size)s z nejvýše %(max)s podagentů",
@@ -346,6 +355,7 @@ const LEMBAS_I18N = {
"The instruction each run is given, on its own: say everything it needs.": "Instrukce, kterou dostane každé spuštění samostatně: uveďte vše, co potřebuje.",
"The interface only. Models answer in the language you write to them in, whatever this says.": "Jen rozhraní. Modely odpovídají v jazyce, ve kterém jim píšete, bez ohledu na toto nastavení.",
"The last %(n)s runs failed: %(why)s": "Posledních %(n)s spuštění selhalo: %(why)s",
"The microphone could not be opened: %(why)s": "Mikrofon se nepodařilo otevřít: %(why)s",
"The model's own": "Podle modelu",
"The most an agent chat may do here": "Nejvíc, co tu agentní konverzace smí",
"The next command": "Další příkaz",
@@ -357,6 +367,8 @@ const LEMBAS_I18N = {
"The reply is ready.": "Odpověď je hotová.",
"The result is no longer there.": "Výsledek už tu není.",
"The server refused this computer's link: %(why)s": "Server odmítl propojení tohoto počítače: %(why)s",
"The server's answer had no picture in it.": "V odpovědi serveru nebyl žádný obrázek.",
"The server's answer was not audio.": "Odpověď serveru nebyla zvuk.",
"The terminal could not be loaded.": "Terminál se nepodařilo načíst.",
"The terminal needs a newer LLeMbas server. Ask its administrator to update it.": "Terminál potřebuje novější server LLeMbas. Požádejte jeho správce o aktualizaci.",
"Theme": "Vzhled",
@@ -399,10 +411,13 @@ const LEMBAS_I18N = {
"Waiting for {{model}}: {{reason}}": "Čeká se na {{model}}: {{reason}}",
"What it does": "Co dělá",
"What should happen, and when": "Co se má stát a kdy",
"What to draw…": "Co nakreslit…",
"What your models remember about you, across chats. Change or remove anything here.": "Co si o vás vaše modely pamatují napříč konverzacemi. Cokoli tu můžete změnit nebo odstranit.",
"When": "Kdy",
"When it matters": "Když na tom záleží",
"Wide": "Široký",
"Writing": "Píše",
"Writing down…": "Zapisuji…",
"Writing: {{rate}} tok/s": "Píše: {{rate}} tok/s",
"Writing…": "Píše se…",
"Written to %(path)s": "Zapsáno do %(path)s",
@@ -566,8 +581,12 @@ const LEMBAS_I18N = {
"Delete this chat?": "Diesen Chat löschen?",
"Delete this scheduled task?": "Diese geplante Aufgabe löschen?",
"Deny": "Ablehnen",
"Dictate a message": "Nachricht diktieren",
"Disconnected. Close and reopen to reconnect.": "Getrennt. Schließen und erneut öffnen, um neu zu verbinden.",
"Done": "Fertig",
"Draw": "Zeichnen",
"Draw a picture": "Ein Bild zeichnen",
"Drawing… a picture can take a minute or two.": "Wird gezeichnet… ein Bild kann ein bis zwei Minuten dauern.",
"Drop the files to attach them": "Dateien ablegen, um sie anzuhängen",
"Due %(when)s": "Fällig %(when)s",
"Each run sees the ones before it.": "Jeder Lauf sieht die vorherigen.",
@@ -698,6 +717,7 @@ const LEMBAS_I18N = {
"Queued": "In der Warteschlange",
"Queued: {{ahead}} ahead": "In der Warteschlange: {{ahead}} davor",
"Read again": "Neu lesen",
"Read this aloud": "Vorlesen",
"Reading the prompt": "Lese den Prompt",
"Reading the prompt: {{done}} / {{total}} tokens ({{percent}}%)": "Lese den Prompt: {{done}} / {{total}} Tokens ({{percent}} %)",
"Reasoning effort": "Denkaufwand",
@@ -740,6 +760,7 @@ const LEMBAS_I18N = {
"Server": "Server",
"Set the time yourself": "Zeit selbst festlegen",
"Settings": "Einstellungen",
"Shape": "Form",
"Show %(n)s more": "%(n)s weitere anzeigen",
"Shown by the agent": "Vom Agenten gezeigt",
"Sidebar": "Seitenleiste",
@@ -753,11 +774,13 @@ const LEMBAS_I18N = {
"Skipped": "Ausgelassen",
"Something to remember…": "Etwas, das behalten werden soll…",
"Source": "Quelltext",
"Square": "Quadratisch",
"Start the agent": "Agent starten",
"Stop": "Stoppen",
"Stop %(name)s": "%(name)s stoppen",
"Stop %(name)s and the members under it? They leave the team.": "%(name)s und die Mitglieder darunter stoppen? Sie verlassen das Team.",
"Stop %(name)s and the subagents under it?": "%(name)s und die Subagenten darunter stoppen?",
"Stop dictating": "Diktieren beenden",
"Stop the agent": "Agent anhalten",
"Stopped": "Angehalten",
"Stopped.": "Angehalten.",
@@ -769,6 +792,7 @@ const LEMBAS_I18N = {
"Switched off here only. What is on to begin with is your default for this model, in Settings → Models.": "Nur hier ausgeschaltet. Was anfangs an ist, ist Ihr Standard für dieses Modell, unter Einstellungen → Modelle.",
"System prompt": "Systemprompt",
"Taken in while it ran:": "Während des Laufs übernommen:",
"Tall": "Hoch",
"Tasks: %(n)s": "Aufgaben: %(n)s",
"Team member": "Teammitglied",
"Team: %(size)s of at most %(max)s subagents": "Team: %(size)s von höchstens %(max)s Subagenten",
@@ -796,6 +820,7 @@ const LEMBAS_I18N = {
"The instruction each run is given, on its own: say everything it needs.": "Die Anweisung, die jeder Lauf für sich erhält: Sagen Sie alles, was er braucht.",
"The interface only. Models answer in the language you write to them in, whatever this says.": "Nur die Oberfläche. Modelle antworten in der Sprache, in der Sie ihnen schreiben, egal was hier steht.",
"The last %(n)s runs failed: %(why)s": "Die letzten %(n)s Läufe sind fehlgeschlagen: %(why)s",
"The microphone could not be opened: %(why)s": "Das Mikrofon konnte nicht geöffnet werden: %(why)s",
"The model's own": "Die des Modells",
"The most an agent chat may do here": "Das Meiste, was ein Agent-Chat hier darf",
"The next command": "Nächster Befehl",
@@ -807,6 +832,8 @@ const LEMBAS_I18N = {
"The reply is ready.": "Die Antwort ist fertig.",
"The result is no longer there.": "Das Ergebnis ist nicht mehr da.",
"The server refused this computer's link: %(why)s": "Der Server hat die Verknüpfung dieses Computers abgelehnt: %(why)s",
"The server's answer had no picture in it.": "Die Antwort des Servers enthielt kein Bild.",
"The server's answer was not audio.": "Die Antwort des Servers war kein Audio.",
"The terminal could not be loaded.": "Das Terminal konnte nicht geladen werden.",
"The terminal needs a newer LLeMbas server. Ask its administrator to update it.": "Das Terminal braucht einen neueren LLeMbas-Server. Bitten Sie seinen Administrator, ihn zu aktualisieren.",
"Theme": "Design",
@@ -849,10 +876,13 @@ const LEMBAS_I18N = {
"Waiting for {{model}}: {{reason}}": "Warte auf {{model}}: {{reason}}",
"What it does": "Was sie tut",
"What should happen, and when": "Was soll wann geschehen?",
"What to draw…": "Was gezeichnet werden soll…",
"What your models remember about you, across chats. Change or remove anything here.": "Was Ihre Modelle über Sie behalten, über alle Chats hinweg. Hier können Sie alles ändern oder entfernen.",
"When": "Wann",
"When it matters": "Wenn es zählt",
"Wide": "Breit",
"Writing": "Schreibe",
"Writing down…": "Wird aufgeschrieben…",
"Writing: {{rate}} tok/s": "Schreibe: {{rate}} Tok/s",
"Writing…": "Schreibt…",
"Written to %(path)s": "Geschrieben nach %(path)s",
@@ -1016,8 +1046,12 @@ const LEMBAS_I18N = {
"Delete this chat?": "¿Eliminar este chat?",
"Delete this scheduled task?": "¿Eliminar esta tarea programada?",
"Deny": "Denegar",
"Dictate a message": "Dictar un mensaje",
"Disconnected. Close and reopen to reconnect.": "Desconectado. Cierre y vuelva a abrir para reconectar.",
"Done": "Hecho",
"Draw": "Dibujar",
"Draw a picture": "Dibujar una imagen",
"Drawing… a picture can take a minute or two.": "Dibujando… una imagen puede tardar uno o dos minutos.",
"Drop the files to attach them": "Suelte los archivos para adjuntarlos",
"Due %(when)s": "Prevista %(when)s",
"Each run sees the ones before it.": "Cada ejecución ve las anteriores.",
@@ -1148,6 +1182,7 @@ const LEMBAS_I18N = {
"Queued": "En cola",
"Queued: {{ahead}} ahead": "En cola: {{ahead}} por delante",
"Read again": "Leer de nuevo",
"Read this aloud": "Leer en voz alta",
"Reading the prompt": "Leyendo el prompt",
"Reading the prompt: {{done}} / {{total}} tokens ({{percent}}%)": "Leyendo el prompt: {{done}} / {{total}} tokens ({{percent}} %)",
"Reasoning effort": "Esfuerzo de razonamiento",
@@ -1190,6 +1225,7 @@ const LEMBAS_I18N = {
"Server": "Servidor",
"Set the time yourself": "Fijar la hora usted mismo",
"Settings": "Ajustes",
"Shape": "Forma",
"Show %(n)s more": "Mostrar %(n)s más",
"Shown by the agent": "Mostrado por el agente",
"Sidebar": "Barra lateral",
@@ -1203,11 +1239,13 @@ const LEMBAS_I18N = {
"Skipped": "Omitida",
"Something to remember…": "Algo que recordar…",
"Source": "Código fuente",
"Square": "Cuadrada",
"Start the agent": "Iniciar el agente",
"Stop": "Detener",
"Stop %(name)s": "Detener %(name)s",
"Stop %(name)s and the members under it? They leave the team.": "¿Detener %(name)s y a los miembros que dependen de él? Dejan el equipo.",
"Stop %(name)s and the subagents under it?": "¿Detener a %(name)s y a los subagentes que tiene debajo?",
"Stop dictating": "Dejar de dictar",
"Stop the agent": "Detener el agente",
"Stopped": "Detenido",
"Stopped.": "Detenido.",
@@ -1219,6 +1257,7 @@ const LEMBAS_I18N = {
"Switched off here only. What is on to begin with is your default for this model, in Settings → Models.": "Desactivado solo aquí. Lo que está activado al principio es su valor predeterminado para este modelo, en Ajustes → Modelos.",
"System prompt": "Prompt de sistema",
"Taken in while it ran:": "Incorporado mientras se ejecutaba:",
"Tall": "Alta",
"Tasks: %(n)s": "Tareas: %(n)s",
"Team member": "Miembro del equipo",
"Team: %(size)s of at most %(max)s subagents": "Equipo: %(size)s de un máximo de %(max)s subagentes",
@@ -1246,6 +1285,7 @@ const LEMBAS_I18N = {
"The instruction each run is given, on its own: say everything it needs.": "La instrucción que recibe cada ejecución por sí sola: diga todo lo que necesita.",
"The interface only. Models answer in the language you write to them in, whatever this says.": "Solo la interfaz. Los modelos responden en el idioma en el que usted les escribe, diga lo que diga esto.",
"The last %(n)s runs failed: %(why)s": "Las últimas %(n)s ejecuciones fallaron: %(why)s",
"The microphone could not be opened: %(why)s": "No se pudo abrir el micrófono: %(why)s",
"The model's own": "La del modelo",
"The most an agent chat may do here": "Lo máximo que un chat de agente puede hacer aquí",
"The next command": "El comando siguiente",
@@ -1257,6 +1297,8 @@ const LEMBAS_I18N = {
"The reply is ready.": "La respuesta está lista.",
"The result is no longer there.": "El resultado ya no está.",
"The server refused this computer's link: %(why)s": "El servidor rechazó la vinculación de este equipo: %(why)s",
"The server's answer had no picture in it.": "La respuesta del servidor no traía ninguna imagen.",
"The server's answer was not audio.": "La respuesta del servidor no era audio.",
"The terminal could not be loaded.": "No se ha podido cargar el terminal.",
"The terminal needs a newer LLeMbas server. Ask its administrator to update it.": "El terminal necesita un servidor LLeMbas más reciente. Pida a su administrador que lo actualice.",
"Theme": "Tema",
@@ -1299,10 +1341,13 @@ const LEMBAS_I18N = {
"Waiting for {{model}}: {{reason}}": "Esperando a {{model}}: {{reason}}",
"What it does": "Qué hace",
"What should happen, and when": "Qué debe ocurrir, y cuándo",
"What to draw…": "Qué dibujar…",
"What your models remember about you, across chats. Change or remove anything here.": "Lo que sus modelos recuerdan de usted entre chats. Aquí puede cambiar o quitar cualquier cosa.",
"When": "Cuándo",
"When it matters": "Cuando importa",
"Wide": "Ancha",
"Writing": "Escribiendo",
"Writing down…": "Transcribiendo…",
"Writing: {{rate}} tok/s": "Escribiendo: {{rate}} tok/s",
"Writing…": "Escribiendo…",
"Written to %(path)s": "Escrito en %(path)s",
@@ -1466,8 +1511,12 @@ const LEMBAS_I18N = {
"Delete this chat?": "Supprimer cette conversation ?",
"Delete this scheduled task?": "Supprimer cette tâche planifiée ?",
"Deny": "Refuser",
"Dictate a message": "Dicter un message",
"Disconnected. Close and reopen to reconnect.": "Déconnecté. Fermez et rouvrez pour vous reconnecter.",
"Done": "Terminé",
"Draw": "Dessiner",
"Draw a picture": "Dessiner une image",
"Drawing… a picture can take a minute or two.": "Dessin en cours… une image peut prendre une minute ou deux.",
"Drop the files to attach them": "Déposez les fichiers pour les joindre",
"Due %(when)s": "Prévue %(when)s",
"Each run sees the ones before it.": "Chaque exécution voit les précédentes.",
@@ -1598,6 +1647,7 @@ const LEMBAS_I18N = {
"Queued": "En file d’attente",
"Queued: {{ahead}} ahead": "En file d’attente : {{ahead}} avant",
"Read again": "Relire",
"Read this aloud": "Lire à voix haute",
"Reading the prompt": "Lecture du prompt",
"Reading the prompt: {{done}} / {{total}} tokens ({{percent}}%)": "Lecture du prompt : {{done}} / {{total}} jetons ({{percent}} %)",
"Reasoning effort": "Effort de raisonnement",
@@ -1640,6 +1690,7 @@ const LEMBAS_I18N = {
"Server": "Serveur",
"Set the time yourself": "Régler l'heure vous-même",
"Settings": "Paramètres",
"Shape": "Forme",
"Show %(n)s more": "Afficher %(n)s de plus",
"Shown by the agent": "Montré par l'agent",
"Sidebar": "Barre latérale",
@@ -1653,11 +1704,13 @@ const LEMBAS_I18N = {
"Skipped": "Ignorée",
"Something to remember…": "Quelque chose à retenir…",
"Source": "Source",
"Square": "Carrée",
"Start the agent": "Démarrer l'agent",
"Stop": "Arrêter",
"Stop %(name)s": "Arrêter %(name)s",
"Stop %(name)s and the members under it? They leave the team.": "Arrêter %(name)s et les membres qui en dépendent ? Ils quittent l’équipe.",
"Stop %(name)s and the subagents under it?": "Arrêter %(name)s et les sous-agents en dessous ?",
"Stop dictating": "Arrêter la dictée",
"Stop the agent": "Arrêter l'agent",
"Stopped": "Arrêté",
"Stopped.": "Arrêté.",
@@ -1669,6 +1722,7 @@ const LEMBAS_I18N = {
"Switched off here only. What is on to begin with is your default for this model, in Settings → Models.": "Désactivé ici seulement. Ce qui est activé au départ est votre réglage par défaut pour ce modèle, dans Paramètres → Modèles.",
"System prompt": "Prompt système",
"Taken in while it ran:": "Pris en compte pendant l'exécution :",
"Tall": "Haute",
"Tasks: %(n)s": "Tâches : %(n)s",
"Team member": "Membre de l’équipe",
"Team: %(size)s of at most %(max)s subagents": "Équipe : %(size)s sur %(max)s sous-agents au plus",
@@ -1696,6 +1750,7 @@ const LEMBAS_I18N = {
"The instruction each run is given, on its own: say everything it needs.": "L'instruction donnée à chaque exécution, seule : dites tout ce dont elle a besoin.",
"The interface only. Models answer in the language you write to them in, whatever this says.": "L'interface uniquement. Les modèles répondent dans la langue dans laquelle vous leur écrivez, quoi qu'indique ce réglage.",
"The last %(n)s runs failed: %(why)s": "Les %(n)s dernières exécutions ont échoué : %(why)s",
"The microphone could not be opened: %(why)s": "Le micro n’a pas pu être ouvert : %(why)s",
"The model's own": "Celle du modèle",
"The most an agent chat may do here": "Le maximum qu'une conversation d'agent peut faire ici",
"The next command": "La commande suivante",
@@ -1707,6 +1762,8 @@ const LEMBAS_I18N = {
"The reply is ready.": "La réponse est prête.",
"The result is no longer there.": "Le résultat n'est plus là.",
"The server refused this computer's link: %(why)s": "Le serveur a refusé la liaison de cet ordinateur : %(why)s",
"The server's answer had no picture in it.": "La réponse du serveur ne contenait aucune image.",
"The server's answer was not audio.": "La réponse du serveur n’était pas de l’audio.",
"The terminal could not be loaded.": "Le terminal n'a pas pu être chargé.",
"The terminal needs a newer LLeMbas server. Ask its administrator to update it.": "Le terminal nécessite un serveur LLeMbas plus récent. Demandez à son administrateur de le mettre à jour.",
"Theme": "Thème",
@@ -1749,10 +1806,13 @@ const LEMBAS_I18N = {
"Waiting for {{model}}: {{reason}}": "En attente de {{model}} : {{reason}}",
"What it does": "Ce qu’elle fait",
"What should happen, and when": "Ce qui doit se passer, et quand",
"What to draw…": "Que dessiner…",
"What your models remember about you, across chats. Change or remove anything here.": "Ce que vos modèles retiennent de vous, d’une conversation à l’autre. Modifiez ou supprimez ce que vous voulez ici.",
"When": "Quand",
"When it matters": "Quand c'est important",
"Wide": "Large",
"Writing": "Écriture",
"Writing down…": "Transcription…",
"Writing: {{rate}} tok/s": "Écriture : {{rate}} jetons/s",
"Writing…": "Rédaction…",
"Written to %(path)s": "Écrit dans %(path)s",
@@ -1916,8 +1976,12 @@ const LEMBAS_I18N = {
"Delete this chat?": "Törli ezt a csevegést?",
"Delete this scheduled task?": "Törli ezt az ütemezett feladatot?",
"Deny": "Elutasítás",
"Dictate a message": "Üzenet diktálása",
"Disconnected. Close and reopen to reconnect.": "Kapcsolat bontva. Az újrakapcsolódáshoz zárja be és nyissa meg újra.",
"Done": "Kész",
"Draw": "Rajzolás",
"Draw a picture": "Kép rajzolása",
"Drawing… a picture can take a minute or two.": "Rajzolás… egy kép egy-két percig is eltarthat.",
"Drop the files to attach them": "Engedje el a fájlokat a csatoláshoz",
"Due %(when)s": "Esedékes: %(when)s",
"Each run sees the ones before it.": "Minden futás látja az előzőeket.",
@@ -2048,6 +2112,7 @@ const LEMBAS_I18N = {
"Queued": "Sorban áll",
"Queued: {{ahead}} ahead": "Sorban áll: {{ahead}} van előtte",
"Read again": "Újraolvasás",
"Read this aloud": "Felolvasás",
"Reading the prompt": "A prompt olvasása",
"Reading the prompt: {{done}} / {{total}} tokens ({{percent}}%)": "A prompt olvasása: {{done}} / {{total}} token ({{percent}}%)",
"Reasoning effort": "Gondolkodási erőfeszítés",
@@ -2090,6 +2155,7 @@ const LEMBAS_I18N = {
"Server": "Kiszolgáló",
"Set the time yourself": "Az időpont megadása kézzel",
"Settings": "Beállítások",
"Shape": "Forma",
"Show %(n)s more": "Még %(n)s megjelenítése",
"Shown by the agent": "Az ügynök mutatta",
"Sidebar": "Oldalsáv",
@@ -2103,11 +2169,13 @@ const LEMBAS_I18N = {
"Skipped": "Kihagyva",
"Something to remember…": "Valami, amit meg kell jegyezni…",
"Source": "Forrás",
"Square": "Négyzet",
"Start the agent": "Ügynök indítása",
"Stop": "Leállítás",
"Stop %(name)s": "%(name)s leállítása",
"Stop %(name)s and the members under it? They leave the team.": "Leállítja %(name)s tagot és az alatta lévőket? Kilépnek a csapatból.",
"Stop %(name)s and the subagents under it?": "Leállítod: %(name)s, és az alatta lévő alügynököket?",
"Stop dictating": "Diktálás leállítása",
"Stop the agent": "Ügynök leállítása",
"Stopped": "Leállítva",
"Stopped.": "Leállítva.",
@@ -2119,6 +2187,7 @@ const LEMBAS_I18N = {
"Switched off here only. What is on to begin with is your default for this model, in Settings → Models.": "Csak itt kikapcsolva. Hogy kezdetben mi van bekapcsolva, az ennél a modellnél az Ön alapértelmezése, a Beállítások → Modellek alatt.",
"System prompt": "Rendszerprompt",
"Taken in while it ran:": "Futás közben átvéve:",
"Tall": "Magas",
"Tasks: %(n)s": "Feladatok: %(n)s",
"Team member": "Csapattag",
"Team: %(size)s of at most %(max)s subagents": "Csapat: %(size)s a legfeljebb %(max)s alügynökből",
@@ -2146,6 +2215,7 @@ const LEMBAS_I18N = {
"The instruction each run is given, on its own: say everything it needs.": "Az utasítás, amelyet minden futás önmagában kap: mondjon el mindent, amire szüksége van.",
"The interface only. Models answer in the language you write to them in, whatever this says.": "Csak a felület. A modellek azon a nyelven válaszolnak, amelyen ír nekik, függetlenül ettől a beállítástól.",
"The last %(n)s runs failed: %(why)s": "Az utolsó %(n)s futás sikertelen: %(why)s",
"The microphone could not be opened: %(why)s": "A mikrofont nem sikerült megnyitni: %(why)s",
"The model's own": "A modell sajátja",
"The most an agent chat may do here": "A legtöbb, amit egy ügynök-csevegés itt tehet",
"The next command": "Következő parancs",
@@ -2157,6 +2227,8 @@ const LEMBAS_I18N = {
"The reply is ready.": "A válasz kész.",
"The result is no longer there.": "Az eredmény már nincs meg.",
"The server refused this computer's link: %(why)s": "A szerver elutasította ennek a számítógépnek a kapcsolatát: %(why)s",
"The server's answer had no picture in it.": "A szerver válaszában nem volt kép.",
"The server's answer was not audio.": "A szerver válasza nem hang volt.",
"The terminal could not be loaded.": "A terminált nem sikerült betölteni.",
"The terminal needs a newer LLeMbas server. Ask its administrator to update it.": "A terminálhoz újabb LLeMbas-szerver kell. Kérje meg a rendszergazdáját, hogy frissítse.",
"Theme": "Téma",
@@ -2199,10 +2271,13 @@ const LEMBAS_I18N = {
"Waiting for {{model}}: {{reason}}": "Várakozás a(z) {{model}} modellre: {{reason}}",
"What it does": "Mit csinál",
"What should happen, and when": "Mi történjen, és mikor",
"What to draw…": "Mit rajzoljon…",
"What your models remember about you, across chats. Change or remove anything here.": "Amit a modelljei megjegyeznek Önről a beszélgetéseken át. Itt bármit módosíthat vagy eltávolíthat.",
"When": "Mikor",
"When it matters": "Ha számít",
"Wide": "Széles",
"Writing": "Írás",
"Writing down…": "Leírás…",
"Writing: {{rate}} tok/s": "Írás: {{rate}} token/s",
"Writing…": "Írás…",
"Written to %(path)s": "Kiírva ide: %(path)s",
@@ -2366,8 +2441,12 @@ const LEMBAS_I18N = {
"Delete this chat?": "Eliminare questa chat?",
"Delete this scheduled task?": "Eliminare questa attività pianificata?",
"Deny": "Nega",
"Dictate a message": "Detta un messaggio",
"Disconnected. Close and reopen to reconnect.": "Disconnesso. Chiudi e riapri per riconnetterti.",
"Done": "Fatto",
"Draw": "Disegna",
"Draw a picture": "Disegna un'immagine",
"Drawing… a picture can take a minute or two.": "Disegno in corso… un’immagine può richiedere un minuto o due.",
"Drop the files to attach them": "Rilasci i file per allegarli",
"Due %(when)s": "Prevista %(when)s",
"Each run sees the ones before it.": "Ogni esecuzione vede quelle precedenti.",
@@ -2498,6 +2577,7 @@ const LEMBAS_I18N = {
"Queued": "In coda",
"Queued: {{ahead}} ahead": "In coda: {{ahead}} davanti",
"Read again": "Rileggi",
"Read this aloud": "Leggi ad alta voce",
"Reading the prompt": "Lettura del prompt",
"Reading the prompt: {{done}} / {{total}} tokens ({{percent}}%)": "Lettura del prompt: {{done}} / {{total}} token ({{percent}}%)",
"Reasoning effort": "Sforzo di ragionamento",
@@ -2540,6 +2620,7 @@ const LEMBAS_I18N = {
"Server": "Server",
"Set the time yourself": "Imposta tu l’orario",
"Settings": "Impostazioni",
"Shape": "Forma",
"Show %(n)s more": "Mostra altri %(n)s",
"Shown by the agent": "Mostrato dall'agente",
"Sidebar": "Barra laterale",
@@ -2553,11 +2634,13 @@ const LEMBAS_I18N = {
"Skipped": "Saltata",
"Something to remember…": "Qualcosa da ricordare…",
"Source": "Sorgente",
"Square": "Quadrata",
"Start the agent": "Avvia l'agente",
"Stop": "Ferma",
"Stop %(name)s": "Ferma %(name)s",
"Stop %(name)s and the members under it? They leave the team.": "Fermare %(name)s e i membri sotto di lui? Lasciano la squadra.",
"Stop %(name)s and the subagents under it?": "Fermare %(name)s e i subagenti sotto di esso?",
"Stop dictating": "Interrompi la dettatura",
"Stop the agent": "Ferma l'agente",
"Stopped": "Fermato",
"Stopped.": "Interrotto.",
@@ -2569,6 +2652,7 @@ const LEMBAS_I18N = {
"Switched off here only. What is on to begin with is your default for this model, in Settings → Models.": "Disattivato solo qui. Ciò che è attivo all'inizio è il tuo predefinito per questo modello, in Impostazioni → Modelli.",
"System prompt": "Prompt di sistema",
"Taken in while it ran:": "Accolto durante l'esecuzione:",
"Tall": "Alta",
"Tasks: %(n)s": "Attività: %(n)s",
"Team member": "Membro della squadra",
"Team: %(size)s of at most %(max)s subagents": "Squadra: %(size)s su un massimo di %(max)s sottoagenti",
@@ -2596,6 +2680,7 @@ const LEMBAS_I18N = {
"The instruction each run is given, on its own: say everything it needs.": "L’istruzione che ogni esecuzione riceve da sola: di’ tutto ciò che le serve.",
"The interface only. Models answer in the language you write to them in, whatever this says.": "Solo l'interfaccia. I modelli rispondono nella lingua in cui scrivi loro, qualunque cosa dica questa impostazione.",
"The last %(n)s runs failed: %(why)s": "Le ultime %(n)s esecuzioni non sono riuscite: %(why)s",
"The microphone could not be opened: %(why)s": "Impossibile aprire il microfono: %(why)s",
"The model's own": "Quello del modello",
"The most an agent chat may do here": "Il massimo che una chat agente può fare qui",
"The next command": "Il comando successivo",
@@ -2607,6 +2692,8 @@ const LEMBAS_I18N = {
"The reply is ready.": "La risposta è pronta.",
"The result is no longer there.": "Il risultato non c’è più.",
"The server refused this computer's link: %(why)s": "Il server ha rifiutato il collegamento di questo computer: %(why)s",
"The server's answer had no picture in it.": "La risposta del server non conteneva immagini.",
"The server's answer was not audio.": "La risposta del server non era audio.",
"The terminal could not be loaded.": "Impossibile caricare il terminale.",
"The terminal needs a newer LLeMbas server. Ask its administrator to update it.": "Il terminale richiede un server LLeMbas più recente. Chiedi al suo amministratore di aggiornarlo.",
"Theme": "Tema",
@@ -2649,10 +2736,13 @@ const LEMBAS_I18N = {
"Waiting for {{model}}: {{reason}}": "In attesa di {{model}}: {{reason}}",
"What it does": "Cosa fa",
"What should happen, and when": "Cosa deve succedere, e quando",
"What to draw…": "Cosa disegnare…",
"What your models remember about you, across chats. Change or remove anything here.": "Ciò che i tuoi modelli ricordano di te tra una chat e l’altra. Qui puoi cambiare o rimuovere qualsiasi cosa.",
"When": "Quando",
"When it matters": "Quando conta",
"Wide": "Larga",
"Writing": "Scrittura",
"Writing down…": "Trascrizione…",
"Writing: {{rate}} tok/s": "Scrittura: {{rate}} tok/s",
"Writing…": "Sta scrivendo…",
"Written to %(path)s": "Scritto in %(path)s",
@@ -2816,8 +2906,12 @@ const LEMBAS_I18N = {
"Delete this chat?": "Usunąć ten czat?",
"Delete this scheduled task?": "Usunąć to zaplanowane zadanie?",
"Deny": "Odrzuć",
"Dictate a message": "Podyktuj wiadomość",
"Disconnected. Close and reopen to reconnect.": "Rozłączono. Aby połączyć ponownie, zamknij i otwórz ponownie.",
"Done": "Gotowe",
"Draw": "Narysuj",
"Draw a picture": "Narysuj obraz",
"Drawing… a picture can take a minute or two.": "Rysowanie… obraz może zająć minutę lub dwie.",
"Drop the files to attach them": "Upuść pliki, aby je dołączyć",
"Due %(when)s": "Zaplanowane na %(when)s",
"Each run sees the ones before it.": "Każde uruchomienie widzi poprzednie.",
@@ -2948,6 +3042,7 @@ const LEMBAS_I18N = {
"Queued": "W kolejce",
"Queued: {{ahead}} ahead": "W kolejce: przed nim {{ahead}}",
"Read again": "Wczytaj ponownie",
"Read this aloud": "Przeczytaj na głos",
"Reading the prompt": "Czytanie promptu",
"Reading the prompt: {{done}} / {{total}} tokens ({{percent}}%)": "Czytanie promptu: {{done}} / {{total}} tokenów ({{percent}}%)",
"Reasoning effort": "Poziom rozumowania",
@@ -2990,6 +3085,7 @@ const LEMBAS_I18N = {
"Server": "Serwer",
"Set the time yourself": "Ustaw czas samodzielnie",
"Settings": "Ustawienia",
"Shape": "Kształt",
"Show %(n)s more": "Pokaż jeszcze %(n)s",
"Shown by the agent": "Pokazane przez agenta",
"Sidebar": "Pasek boczny",
@@ -3003,11 +3099,13 @@ const LEMBAS_I18N = {
"Skipped": "Pominięty",
"Something to remember…": "Coś do zapamiętania…",
"Source": "Źródło",
"Square": "Kwadrat",
"Start the agent": "Uruchom agenta",
"Stop": "Zatrzymać",
"Stop %(name)s": "Zatrzymaj %(name)s",
"Stop %(name)s and the members under it? They leave the team.": "Zatrzymać %(name)s i członków pod nim? Opuszczą zespół.",
"Stop %(name)s and the subagents under it?": "Zatrzymać %(name)s i podagentów pod nim?",
"Stop dictating": "Zatrzymaj dyktowanie",
"Stop the agent": "Zatrzymaj agenta",
"Stopped": "Zatrzymano",
"Stopped.": "Zatrzymano.",
@@ -3019,6 +3117,7 @@ const LEMBAS_I18N = {
"Switched off here only. What is on to begin with is your default for this model, in Settings → Models.": "Wyłączone tylko tutaj. To, co jest początkowo włączone, to ustawienie domyślne dla tego modelu w Ustawienia → Modele.",
"System prompt": "Prompt systemowy",
"Taken in while it ran:": "Przejęte w trakcie działania:",
"Tall": "Wysoki",
"Tasks: %(n)s": "Zadania: %(n)s",
"Team member": "Członek zespołu",
"Team: %(size)s of at most %(max)s subagents": "Zespół: %(size)s z najwyżej %(max)s podagentów",
@@ -3046,6 +3145,7 @@ const LEMBAS_I18N = {
"The instruction each run is given, on its own: say everything it needs.": "Instrukcja, którą każde uruchomienie dostaje samodzielnie: podaj wszystko, czego potrzebuje.",
"The interface only. Models answer in the language you write to them in, whatever this says.": "Tylko interfejs. Modele odpowiadają w języku, w którym się do nich pisze, bez względu na to ustawienie.",
"The last %(n)s runs failed: %(why)s": "Ostatnie uruchomienia nie powiodły się (%(n)s): %(why)s",
"The microphone could not be opened: %(why)s": "Nie udało się otworzyć mikrofonu: %(why)s",
"The model's own": "Domyślne modelu",
"The most an agent chat may do here": "Najwięcej, co czat agenta może tu zrobić",
"The next command": "Następne polecenie",
@@ -3057,6 +3157,8 @@ const LEMBAS_I18N = {
"The reply is ready.": "Odpowiedź jest gotowa.",
"The result is no longer there.": "Wyniku już nie ma.",
"The server refused this computer's link: %(why)s": "Serwer odrzucił połączenie tego komputera: %(why)s",
"The server's answer had no picture in it.": "W odpowiedzi serwera nie było obrazu.",
"The server's answer was not audio.": "Odpowiedź serwera nie była dźwiękiem.",
"The terminal could not be loaded.": "Nie udało się wczytać terminala.",
"The terminal needs a newer LLeMbas server. Ask its administrator to update it.": "Terminal wymaga nowszego serwera LLeMbas. Poproś jego administratora o aktualizację.",
"Theme": "Motyw",
@@ -3099,10 +3201,13 @@ const LEMBAS_I18N = {
"Waiting for {{model}}: {{reason}}": "Czekam na {{model}}: {{reason}}",
"What it does": "Co robi",
"What should happen, and when": "Co ma się stać i kiedy",
"What to draw…": "Co narysować…",
"What your models remember about you, across chats. Change or remove anything here.": "Co Twoje modele pamiętają o Tobie we wszystkich czatach. Tutaj możesz wszystko zmienić lub usunąć.",
"When": "Kiedy",
"When it matters": "Gdy to ważne",
"Wide": "Szeroki",
"Writing": "Pisanie",
"Writing down…": "Zapisywanie…",
"Writing: {{rate}} tok/s": "Pisanie: {{rate}} tok/s",
"Writing…": "Pisze…",
"Written to %(path)s": "Zapisano do %(path)s",
@@ -3266,8 +3371,12 @@ const LEMBAS_I18N = {
"Delete this chat?": "Zmazať túto konverzáciu?",
"Delete this scheduled task?": "Odstrániť túto naplánovanú úlohu?",
"Deny": "Odmietnuť",
"Dictate a message": "Nadiktovať správu",
"Disconnected. Close and reopen to reconnect.": "Odpojené. Zavrite a znova otvorte, aby sa pripojilo.",
"Done": "Hotovo",
"Draw": "Nakresliť",
"Draw a picture": "Nakresliť obrázok",
"Drawing… a picture can take a minute or two.": "Kreslím… obrázok môže trvať minútu alebo dve.",
"Drop the files to attach them": "Pustite súbory a priložia sa",
"Due %(when)s": "Naplánované na %(when)s",
"Each run sees the ones before it.": "Každé spustenie vidí tie predchádzajúce.",
@@ -3398,6 +3507,7 @@ const LEMBAS_I18N = {
"Queued": "Vo fronte",
"Queued: {{ahead}} ahead": "Vo fronte: pred ním {{ahead}}",
"Read again": "Načítať znova",
"Read this aloud": "Prečítať nahlas",
"Reading the prompt": "Číta sa výzva",
"Reading the prompt: {{done}} / {{total}} tokens ({{percent}}%)": "Číta sa výzva: {{done}} / {{total}} tokenov ({{percent}} %)",
"Reasoning effort": "Náročnosť uvažovania",
@@ -3440,6 +3550,7 @@ const LEMBAS_I18N = {
"Server": "Server",
"Set the time yourself": "Nastaviť čas sami",
"Settings": "Nastavenia",
"Shape": "Tvar",
"Show %(n)s more": "Zobraziť ďalšie (%(n)s)",
"Shown by the agent": "Ukázal agent",
"Sidebar": "Bočný panel",
@@ -3453,11 +3564,13 @@ const LEMBAS_I18N = {
"Skipped": "Vynechané",
"Something to remember…": "Niečo na zapamätanie…",
"Source": "Zdroj",
"Square": "Štvorec",
"Start the agent": "Spustiť agenta",
"Stop": "Zastaviť",
"Stop %(name)s": "Zastaviť %(name)s",
"Stop %(name)s and the members under it? They leave the team.": "Zastaviť %(name)s a členov pod ním? Opustia tím.",
"Stop %(name)s and the subagents under it?": "Zastaviť %(name)s a podagentov pod ním?",
"Stop dictating": "Zastaviť diktovanie",
"Stop the agent": "Zastaviť agenta",
"Stopped": "Zastavené",
"Stopped.": "Zastavené.",
@@ -3469,6 +3582,7 @@ const LEMBAS_I18N = {
"Switched off here only. What is on to begin with is your default for this model, in Settings → Models.": "Vypnuté len tu. Čo je na začiatku zapnuté, je vaša predvoľba pre tento model v Nastavenia → Modely.",
"System prompt": "Systémový pokyn",
"Taken in while it ran:": "Prevzaté počas behu:",
"Tall": "Vysoký",
"Tasks: %(n)s": "Úlohy: %(n)s",
"Team member": "Člen tímu",
"Team: %(size)s of at most %(max)s subagents": "Tím: %(size)s z najviac %(max)s podagentov",
@@ -3496,6 +3610,7 @@ const LEMBAS_I18N = {
"The instruction each run is given, on its own: say everything it needs.": "Inštrukcia, ktorú dostane každé spustenie samostatne: uveďte všetko, čo potrebuje.",
"The interface only. Models answer in the language you write to them in, whatever this says.": "Len rozhranie. Modely odpovedajú v jazyku, v ktorom im píšete, bez ohľadu na toto nastavenie.",
"The last %(n)s runs failed: %(why)s": "Posledné spustenia zlyhali (%(n)s): %(why)s",
"The microphone could not be opened: %(why)s": "Mikrofón sa nepodarilo otvoriť: %(why)s",
"The model's own": "Podľa modelu",
"The most an agent chat may do here": "Najviac, čo tu agentská konverzácia smie",
"The next command": "Nasledujúci príkaz",
@@ -3507,6 +3622,8 @@ const LEMBAS_I18N = {
"The reply is ready.": "Odpoveď je hotová.",
"The result is no longer there.": "Výsledok už tu nie je.",
"The server refused this computer's link: %(why)s": "Server odmietol prepojenie tohto počítača: %(why)s",
"The server's answer had no picture in it.": "V odpovedi servera nebol žiadny obrázok.",
"The server's answer was not audio.": "Odpoveď servera nebola zvuk.",
"The terminal could not be loaded.": "Terminál sa nepodarilo načítať.",
"The terminal needs a newer LLeMbas server. Ask its administrator to update it.": "Terminál potrebuje novší server LLeMbas. Požiadajte jeho správcu o aktualizáciu.",
"Theme": "Vzhľad",
@@ -3549,10 +3666,13 @@ const LEMBAS_I18N = {
"Waiting for {{model}}: {{reason}}": "Čaká sa na {{model}}: {{reason}}",
"What it does": "Čo robí",
"What should happen, and when": "Čo sa má stať a kedy",
"What to draw…": "Čo nakresliť…",
"What your models remember about you, across chats. Change or remove anything here.": "Čo si o vás vaše modely pamätajú naprieč konverzáciami. Čokoľvek tu môžete zmeniť alebo odstrániť.",
"When": "Kedy",
"When it matters": "Keď na tom záleží",
"Wide": "Široký",
"Writing": "Píše",
"Writing down…": "Zapisujem…",
"Writing: {{rate}} tok/s": "Píše: {{rate}} tok/s",
"Writing…": "Píše sa…",
"Written to %(path)s": "Zapísané do %(path)s",
+1
View File
@@ -84,6 +84,7 @@
<div class="composer__bar">
<button type="button" class="tool composer__attach" id="attach" aria-label="Attach files" title="Attach files" data-i18n-aria-label data-i18n-title></button>
<div class="composer__chips" id="composer-chips"></div>
<button type="button" class="tool composer__mic" id="dictate" hidden aria-pressed="false"></button>
<button type="button" class="round round--stop" id="stop" hidden aria-label="Stop" title="Stop" data-i18n-aria-label data-i18n-title></button>
<button type="submit" class="round" id="send" aria-label="Send" title="Send" data-i18n-aria-label data-i18n-title></button>
</div>
+2
View File
@@ -22,6 +22,7 @@ import { unarchive } from "./archived.js"
import { isProgress, progressLine, progressTicks } from "./progress.js"
import { aimTerminal, terminalButton, terminalOpen, toggleTerminal } from "./terminal.js"
import { scopePopover } from "./scope.js"
import { readAloudButton } from "./voice.js"
const MODES = ["manual", "edit", "auto", "plan"]
@@ -292,6 +293,7 @@ function assistantMessage(m, chat) {
"div",
{ class: "msg__actions" },
action(ICONS.copy, t("Copy"), () => copyText(replyText(m))),
readAloudButton(m.id, () => replyText(m)),
// Only the last reply: CSS shows it there and nowhere else (app.css, .msg__regen).
action(ICONS.refresh, t("Regenerate reply"), () => regenerate(m), "msg__regen"),
),
+3
View File
@@ -101,6 +101,9 @@ export const ICONS = {
bell: ["M8 1.5a4.5 4.5 0 014.5 4.5v3l1.5 2.5H2L3.5 9V6A4.5 4.5 0 018 1.5zM8 3a3 3 0 00-3 3v3.42l-.35.58h6.7L11 9.42V6a3 3 0 00-3-3z", "M6.25 12.5h3.5a1.75 1.75 0 01-3.5 0z"],
archive: ["M1.5 2.5h13v3.5h-13zM3 4v.5h10V4z", "M2.5 6.5H4V12h8V6.5h1.5v7h-11z", "M6 7.5h4V9H6z"],
contrast: ["M8 1.5a6.5 6.5 0 110 13 6.5 6.5 0 010-13zM8 3a5 5 0 100 10A5 5 0 008 3z", "M8 3a5 5 0 010 10z"],
mic: ["M8 1.5a2.5 2.5 0 012.5 2.5v4a2.5 2.5 0 01-5 0V4A2.5 2.5 0 018 1.5zM8 3a1 1 0 00-1 1v4a1 1 0 002 0V4a1 1 0 00-1-1z", "M3.5 7.5H5a3 3 0 006 0h1.5a4.5 4.5 0 01-3.75 4.44V14h-1.5v-2.06A4.5 4.5 0 013.5 7.5z"],
speaker: ["M2 6h2.5L8 3v10L4.5 10H2z", "M10.2 5.2a4 4 0 010 5.6l-1.06-1.06a2.5 2.5 0 000-3.48z", "M12.3 3.1a7 7 0 010 9.8l-1.06-1.06a5.5 5.5 0 000-7.68z"],
picture: ["M1.5 2.5h13v11h-13zM3 4v6.4l2.75-2.75 2.5 2.5 1.75-1.75L13 11.4V4z", "M10.5 5a1.25 1.25 0 110 2.5 1.25 1.25 0 010-2.5z"],
info: ["M8 1.5a6.5 6.5 0 110 13 6.5 6.5 0 010-13zM8 3a5 5 0 100 10A5 5 0 008 3z", "M7.25 7h1.5v4.5h-1.5zM7.25 4.5h1.5V6h-1.5z"],
}
+86
View File
@@ -0,0 +1,86 @@
// Draw a picture: the instance's own image generation (`POST /v1/images/generations`, its
// workflow and default checkpoint), from Ctrl+K. Offered only where `GET /me` says the account may
// (`services.images`) and the token holds `models`.
//
// The pictures are shown only as Rust read them: each one of the person's own files on this
// server (`/v1/files/<id>/content`, fetched by id from the app's own server, never from an
// address the answer named), handed over as a `data:` URL of an image type -- checked again here
// before it becomes an <img>.
import { h, icon, ICONS } from "./dom.js"
import { invoke, why } from "./bridge.js"
const PICTURE = /^data:image\/(png|jpeg|webp|gif);base64,[A-Za-z0-9+/=]+$/
/** Whether pictures can be drawn here, for the account `me` (`GET /me`). */
export function canDraw(me) {
const models = !Array.isArray(me?.scopes) || me.scopes.includes("models")
return !!me?.services?.images && models && me?.permissions?.["tools.image"] !== false
}
/** The dialog: a prompt, a shape, Draw, and the pictures under it. */
export function openDraw() {
const prompt = h("textarea", { class: "textarea draw__prompt", rows: "3", placeholder: t("What to draw…"), "aria-label": t("What to draw…") })
const size = h(
"select",
{ class: "draw__size", "aria-label": t("Shape") },
h("option", { value: "1024x1024", text: t("Square") }),
h("option", { value: "1344x768", text: t("Wide") }),
h("option", { value: "768x1344", text: t("Tall") }),
)
const status = h("p", { class: "status", role: "status", hidden: true })
const out = h("div", { class: "draw__out" })
const go = h("button", { type: "submit", class: "btn btn--primary", text: t("Draw") })
const form = h(
"form",
{ class: "dialog__body draw", method: "dialog" },
h("h2", { class: "dialog__title" }, icon(ICONS.picture), h("span", { text: t("Draw a picture") })),
prompt,
h("div", { class: "draw__bar" }, size, h("button", { type: "button", class: "btn", text: t("Close"), onclick: () => close() }), go),
status,
out,
)
const dlg = h("dialog", { class: "dialog dialog--wide" }, form)
let asked = 0
const close = () => {
asked++
dlg.close()
dlg.remove()
}
const said = (text, error = false) => {
status.hidden = !text
status.textContent = text
status.className = `status${error ? " is-error" : ""}`
status.setAttribute("role", error ? "alert" : "status")
}
form.addEventListener("submit", async (e) => {
e.preventDefault()
const words = prompt.value.trim()
if (!words) return prompt.focus()
const mine = ++asked
go.disabled = true
said(t("Drawing… a picture can take a minute or two."))
try {
const urls = await invoke("draw", { prompt: words, size: size.value })
if (mine !== asked) return
const pictures = (urls || []).filter((u) => PICTURE.test(u))
if (!pictures.length) throw { detail: t("The server's answer had no picture in it.") }
out.prepend(...pictures.map((src) => h("figure", { class: "draw__pic" }, h("img", { src, alt: words }), h("figcaption", { text: words }))))
said("")
} catch (err) {
if (mine === asked) said(why(err), true)
} finally {
if (mine === asked) go.disabled = false
}
})
prompt.addEventListener("keydown", (e) => {
if (e.key === "Enter" && (e.ctrlKey || e.metaKey)) form.requestSubmit()
})
dlg.addEventListener("cancel", (e) => {
e.preventDefault()
close()
})
document.body.append(dlg)
dlg.showModal()
prompt.focus()
}
+5
View File
@@ -22,6 +22,8 @@ import { initAgent, link } from "./agent.js"
import { initScheduled, showScheduled, scheduledCommands } from "./scheduled.js"
import { initArchived, showArchived } from "./archived.js"
import { showMemory } from "./memory.js"
import { initVoice, voiceFor } from "./voice.js"
import { canDraw, openDraw } from "./draw.js"
import { closePopover } from "./popup.js"
import { noteSubagentsCap } from "./models.js"
@@ -94,6 +96,7 @@ async function startApp() {
try {
me = await get("/me")
noteMe(me)
voiceFor(me)
} catch (err) {
$("thread-note").textContent = why(err)
$("thread-note").hidden = false
@@ -233,6 +236,7 @@ function commands() {
{ label: t("Memory"), run: () => showMemoryPage() },
{ label: t("Settings"), run: () => showSettings(true) },
]
if (canDraw(me)) out.push({ label: t("Draw a picture"), run: openDraw })
if (openChatId()) out.push({ label: panelOpen() ? t("Close the activity panel") : t("Open the activity panel"), run: togglePanel })
if (terminalAvailable()) out.push({ label: terminalOpen() ? t("Close terminal") : t("Terminal"), run: toggleTerminal })
if (status?.linked) out.push({ label: status.agent_wanted ? t("Stop the agent") : t("Start the agent"), run: () => invoke("set_agent", { on: !status.agent_wanted }) })
@@ -280,6 +284,7 @@ async function boot() {
$("thread-note").hidden = !text
},
})
initVoice()
initChat({
status: () => status,
thisDevices,
+180
View File
@@ -0,0 +1,180 @@
// Voice: a message dictated in the composer, and a reply read aloud. Both are the instance's own
// speech services on the OpenAI-compatible routes, which the app's token reaches through its
// `models` scope (src-tauri/src/server.rs):
//
// POST /v1/audio/transcriptions the recording -> { text } (Rust: `transcribe`)
// POST /v1/audio/speech { input } -> the audio (Rust: `speak`, a data: URL)
//
// Each is offered only where `GET /me` says the instance has it for this account (`services.stt`,
// `services.tts`) and the token holds `models` (`scopes`), so no button is there that can only fail.
// The microphone is asked for only when Dictate is pressed: Rust allows it for that moment alone.
import { $, h, icon, ICONS } from "./dom.js"
import { invoke, why } from "./bridge.js"
let me = null
const hasModels = () => !Array.isArray(me?.scopes) || me.scopes.includes("models")
/** Whether a message can be dictated here. */
export const canDictate = () => !!me?.services?.stt && hasModels() && me?.permissions?.["audio.transcribe"] !== false
/** Whether a reply can be read aloud here. */
export const canSpeak = () => !!me?.services?.tts && hasModels() && me?.permissions?.["audio.listen"] !== false
function say(text) {
const box = $("thread-note")
if (!box) return
box.textContent = text
box.hidden = !text
}
// --- Dictate --------------------------------------------------------------------------------------
let recorder = null // the recording in progress
let busy = false // a recording being turned into words
function drawMic() {
const b = $("dictate")
if (!b) return
b.hidden = !canDictate()
const on = !!recorder
b.classList.toggle("is-recording", on)
b.classList.toggle("is-busy", busy)
b.disabled = busy
b.setAttribute("aria-pressed", String(on))
const label = busy ? t("Writing down…") : on ? t("Stop dictating") : t("Dictate a message")
b.setAttribute("aria-label", label)
b.title = label
}
async function start() {
say("")
let stream
try {
await invoke("voice_arm")
stream = await navigator.mediaDevices.getUserMedia({ audio: true })
} catch (err) {
say(t("The microphone could not be opened: %(why)s", { why: err?.message || why(err) }))
return
}
const type = ["audio/webm;codecs=opus", "audio/ogg;codecs=opus", "audio/webm"].find((x) => window.MediaRecorder?.isTypeSupported?.(x))
let rec
try {
rec = new MediaRecorder(stream, type ? { mimeType: type } : undefined)
} catch (err) {
stream.getTracks().forEach((x) => x.stop())
say(t("The microphone could not be opened: %(why)s", { why: err?.message || why(err) }))
return
}
const chunks = []
rec.addEventListener("dataavailable", (e) => e.data.size && chunks.push(e.data))
rec.addEventListener("stop", async () => {
stream.getTracks().forEach((x) => x.stop())
recorder = null
const blob = new Blob(chunks, { type: rec.mimeType || type || "audio/webm" })
if (!blob.size) return drawMic()
busy = true
drawMic()
try {
const text = await invoke("transcribe", new Uint8Array(await blob.arrayBuffer()), { headers: { "x-type": blob.type } })
intoComposer(text)
} catch (err) {
say(why(err))
}
busy = false
drawMic()
})
recorder = rec
rec.start()
drawMic()
}
/** The words heard, where the cursor is in the message box, a space either side where needed. */
function intoComposer(text) {
text = String(text || "").trim()
if (!text) return
const box = $("composer-text")
const at = box.selectionStart ?? box.value.length
const end = box.selectionEnd ?? at
const before = box.value.slice(0, at)
const after = box.value.slice(end)
const words = `${before && !/\s$/.test(before) ? " " : ""}${text}${after && !/^\s/.test(after) ? " " : ""}`
box.value = before + words + after
box.selectionStart = box.selectionEnd = before.length + words.length
box.dispatchEvent(new Event("input"))
box.focus()
}
/** Stop a recording (a chat closed, the window hidden): what was said is still written down. */
export function stopDictating() {
if (recorder?.state === "recording") recorder.stop()
}
// --- Read aloud -----------------------------------------------------------------------------------
let playing = null // { id, audio, button }
/** A reply's words without the markdown around them, and without its code. */
export function plain(markdown) {
return String(markdown || "")
.replace(/```[\s\S]*?```/g, " ")
.replace(/!?\[([^\]]*)]\([^)]*\)/g, "$1")
.replace(/[*_`#>|~]/g, "")
.replace(/\s+/g, " ")
.trim()
}
function stopReading() {
if (!playing) return
playing.audio?.pause()
playing.button.setAttribute("aria-pressed", "false")
playing.button.classList.remove("is-busy")
playing = null
}
/** The Read aloud button of a reply (`id`), reading `text()` when pressed; null where it cannot. */
export function readAloudButton(id, text) {
if (!canSpeak()) return null
const label = t("Read this aloud")
const b = h("button", { type: "button", class: "msg__act msg__speak", "aria-label": label, title: label, "aria-pressed": "false" }, icon(ICONS.speaker))
b.addEventListener("click", async () => {
if (playing?.id === id) return stopReading()
stopReading()
const words = plain(text())
if (!words) return
const mine = { id, audio: null, button: b }
playing = mine
b.setAttribute("aria-pressed", "true")
b.classList.add("is-busy")
try {
const url = await invoke("speak", { text: words })
if (playing !== mine) return
// Only audio the app's own server sent, as Rust read it: a data: URL of an audio type.
if (!/^data:audio\/[a-z0-9.+-]+;base64,/i.test(url)) throw new Error(t("The server's answer was not audio."))
mine.audio = new Audio(url)
b.classList.remove("is-busy")
mine.audio.addEventListener("ended", () => playing === mine && stopReading())
await mine.audio.play()
} catch (err) {
if (playing === mine) stopReading()
say(why(err?.message ? { detail: err.message } : err))
}
})
return b
}
/** What the account may use, from `GET /me`: the composer's Dictate button is drawn by it. */
export function voiceFor(account) {
me = account
drawMic()
}
export function initVoice() {
const b = $("dictate")
b.replaceChildren(icon(ICONS.mic))
b.addEventListener("click", () => {
if (busy) return
if (recorder) recorder.stop()
else start()
})
drawMic()
}