From c6f004436c8455f4da28b103f556122cbcd26e97 Mon Sep 17 00:00:00 2001 From: Ettore Di Giacinto Date: Tue, 3 Nov 2026 10:15:03 +0000 Subject: [PATCH] feat(ui): show change proposals as a card and on a Proposals page A propose_change tool line now draws a card in the thread, for any kind and any source: the kind, file and scope, the source, the time left and a line diff against the file as it is now. The owner can Confirm, edit the text then confirm, or Dismiss. A confirmed change shows a receipt with Undo. Expired and stale proposals say nothing was saved and offer to ask the assistant again; a failed write leaves the proposal waiting. The inbox button counts the waiting changes and its panel links to a new Proposals page that lists them with the same card. Assisted-by: Claude Code:claude-sonnet-5-5 Signed-off-by: Ettore Di Giacinto --- .../react-ui/public/locales/de/chats.json | 123 +++++++- .../react-ui/public/locales/en/chats.json | 123 +++++++- .../react-ui/public/locales/es/chats.json | 123 +++++++- .../react-ui/public/locales/fr/chats.json | 123 +++++++- .../react-ui/public/locales/id/chats.json | 123 +++++++- .../react-ui/public/locales/it/chats.json | 123 +++++++- .../react-ui/public/locales/ko/chats.json | 123 +++++++- .../react-ui/public/locales/pt-BR/chats.json | 123 +++++++- .../react-ui/public/locales/zh-CN/chats.json | 123 +++++++- .../react-ui/src/components/ChatInbox.jsx | 23 +- .../src/components/chat/ActivityFold.jsx | 12 +- .../components/chat/ChangeProposalCard.jsx | 263 ++++++++++++++++++ .../src/components/chat/changeproposal.css | 34 +++ .../src/components/chat/chatSendContext.js | 5 + core/http/react-ui/src/components/inbox.css | 21 ++ .../react-ui/src/hooks/usePendingChanges.js | 46 +++ core/http/react-ui/src/pages/Chat.jsx | 3 + core/http/react-ui/src/pages/Proposals.jsx | 37 +++ core/http/react-ui/src/router.jsx | 2 + core/http/react-ui/src/utils/api.js | 12 + 20 files changed, 1551 insertions(+), 14 deletions(-) create mode 100644 core/http/react-ui/src/components/chat/ChangeProposalCard.jsx create mode 100644 core/http/react-ui/src/components/chat/changeproposal.css create mode 100644 core/http/react-ui/src/components/chat/chatSendContext.js create mode 100644 core/http/react-ui/src/hooks/usePendingChanges.js create mode 100644 core/http/react-ui/src/pages/Proposals.jsx diff --git a/core/http/react-ui/public/locales/de/chats.json b/core/http/react-ui/public/locales/de/chats.json index c3d37c0250..1e450375ff 100644 --- a/core/http/react-ui/public/locales/de/chats.json +++ b/core/http/react-ui/public/locales/de/chats.json @@ -454,7 +454,10 @@ "itemUnread": "{{title}}, ungelesen", "arrived": "Neue Benachrichtigung: {{title}}", "arrivedMany_one": "{{count}} neue Benachrichtigung", - "arrivedMany_other": "{{count}} neue Benachrichtigungen" + "arrivedMany_other": "{{count}} neue Benachrichtigungen", + "proposalsWaiting_one": "{{count}} Änderung wartet auf dich", + "proposalsWaiting_other": "{{count}} Änderungen warten auf dich", + "proposalsBody": "Ein Modell hat eine Änderung vorgeschlagen. Nichts wird gespeichert, bevor du antwortest." }, "change": { "loading": "Die Trigger-Änderung wird gelesen...", @@ -583,5 +586,123 @@ "stopped_one": "{{count}} Schleife gestoppt.", "stopped_other": "{{count}} Schleifen gestoppt." } + }, + "changes": { + "loading": "Der vorgeschlagene Änderung wird gelesen...", + "unread": "Die vorgeschlagene Änderung konnte nicht gelesen werden: {{message}}", + "removed": "Dieser Vorschlag existiert nicht mehr.", + "kind": { + "memory": "Gedächtnis", + "profile": "Profil", + "skill": "Skill", + "page": "Seite", + "dashboard": "Dashboard" + }, + "scope": { + "user": "Alle deine Chats", + "agent": "Agent {{agent}}", + "agentAny": "Ein Agent", + "channel": "Diese Unterhaltung" + }, + "help": "Nichts wird gespeichert, bevor du bestätigst.", + "row": { + "kind": "Art", + "target": "Datei", + "source": "Von", + "expires": "Läuft ab in" + }, + "source": { + "chat": "Ein Chat", + "connector": "Connector {{name}}", + "connectorFrom": "Connector {{name}}, von {{sender}}" + }, + "expiresHours": "{{hours}} Std. {{minutes}} Min.", + "expiresMinutes": "{{minutes}} Min.", + "diff": { + "label": "Änderungen an {{target}}", + "newFile": "Neue Datei", + "added_one": "{{count}} Zeile hinzugefügt", + "added_other": "{{count}} Zeilen hinzugefügt", + "removed_one": "{{count}} Zeile entfernt", + "removed_other": "{{count}} Zeilen entfernt", + "same": "Kein Unterschied zum aktuellen Text.", + "gap_one": "{{count}} unveränderte Zeile", + "gap_other": "{{count}} unveränderte Zeilen", + "more_one": "{{count}} weitere Zeile nicht angezeigt", + "more_other": "{{count}} weitere Zeilen nicht angezeigt", + "srAdded": "Hinzugefügt:", + "srRemoved": "Entfernt:" + }, + "unreadFile": "Die aktuelle Datei konnte nicht gelesen werden, daher gilt der ganze Text als neu.", + "confirm": "Bestätigen", + "edit": "Bearbeiten und bestätigen", + "confirmEdited": "Mit meinem Text bestätigen", + "editLabel": "Text zum Speichern in {{target}}", + "failed": "Die Änderung konnte nicht gespeichert werden: {{message}}. Sie wartet weiter auf dich.", + "receipt": { + "applied": "Angewendet:", + "reverted": "Rückgängig gemacht:", + "dismissed": "Abgelehnt:", + "expired": "Abgelaufen:", + "stale": "Nicht angewendet:" + }, + "why": { + "expired": "Sie hat mehr als einen Tag gewartet. Nichts wurde gespeichert.", + "stale": "Die Datei hat sich seit dem Vorschlag geändert, daher wurde nichts gespeichert." + }, + "askAgain": "Erneut anfragen", + "askAgainText": "Bitte schlage diese Änderung noch einmal vor, die Datei hat sich inzwischen geändert: {{summary}} ({{target}})", + "announce": { + "applied": "Änderung angewendet.", + "dismissed": "Änderung abgelehnt.", + "undone": "Änderung rückgängig gemacht.", + "asked": "Der Assistent wurde gebeten, es erneut vorzuschlagen." + } + }, + "proposalsPage": { + "title": "Vorschläge", + "supporting": "Änderungen, die Modelle vorgeschlagen haben und die auf deine Antwort warten.", + "emptyTitle": "Nichts wartet", + "emptyBody": "Wenn ein Modell eine Änderung an deinem Gedächtnis oder einem Agentenprofil vorschlägt, erscheint sie hier, bis du antwortest.", + "loadError": "Die Vorschläge konnten nicht geladen werden." + }, + "memoryPage": { + "lede": "Die Notizen, die LocalAI in den Prompt deiner Chats legt. Ein Modell ändert sie erst, nachdem du einen Vorschlag bestätigt hast. Hier kannst du sie lesen, bearbeiten und löschen.", + "limits": "Grenzen: Ein Index hat höchstens {{lines}} Zeilen und {{index}}, ein Profil {{profile}}, und ein Zug enthält höchstens {{turn}} Notizen.", + "proposalsLink": "Wartende Änderungen ansehen", + "emptyTitle": "Noch keine Notizen", + "emptyBody": "Wenn du einen Assistenten bittest, sich etwas zu merken, und das bestätigst, erscheinen die Notizen hier.", + "loadError": "Deine Notizen konnten nicht geladen werden.", + "use": "{{size}} von {{max}} · {{lines}} von {{maxLines}} Zeilen", + "useBytes": "{{size}} von {{max}}", + "useLabel": "Größe des Index im Verhältnis zur Grenze", + "over": "Über der Grenze: das Ende wird nicht an das Modell gesendet.", + "index": "Index", + "group": { + "user": "Deine Notizen", + "agent": "Notizen von Agent {{key}}", + "channel": "Notizen der Unterhaltung {{key}}", + "profile": "Profil von Agent {{key}}" + }, + "edit": "Bearbeiten", + "save": "Speichern", + "editLabel": "Text von {{name}}", + "editSizeIndex": "{{size}} von {{max}} · {{lines}} von {{maxLines}} Zeilen", + "editSize": "{{size}} von {{max}}", + "tooBig": "Zu lang: kürze den Text, um zu speichern.", + "saveFailed": "Speichern fehlgeschlagen: {{message}}", + "saved": "{{name}} gespeichert.", + "emptyFile": "(leer)", + "delete": "{{name}} löschen", + "deleteYes": "Löschen", + "confirmDelete": "{{name}} löschen?", + "deleted": "{{name}} in den Papierkorb verschoben.", + "clear": "Alle Notizen löschen", + "clearAsk_one": "{{count}} Datei löschen? Sie kommt in den Papierkorb und lässt sich unter Dateien wiederherstellen.", + "clearAsk_other": "{{count}} Dateien löschen? Sie kommen in den Papierkorb und lassen sich unter Dateien wiederherstellen.", + "clearYes_one": "{{count}} Datei löschen", + "clearYes_other": "{{count}} Dateien löschen", + "cleared_one": "{{count}} Datei in den Papierkorb verschoben.", + "cleared_other": "{{count}} Dateien in den Papierkorb verschoben." } } diff --git a/core/http/react-ui/public/locales/en/chats.json b/core/http/react-ui/public/locales/en/chats.json index a1cf1570e0..1e284dc579 100644 --- a/core/http/react-ui/public/locales/en/chats.json +++ b/core/http/react-ui/public/locales/en/chats.json @@ -305,7 +305,10 @@ "itemUnread": "{{title}}, unread", "arrived": "New notification: {{title}}", "arrivedMany_one": "{{count}} new notification", - "arrivedMany_other": "{{count}} new notifications" + "arrivedMany_other": "{{count}} new notifications", + "proposalsWaiting_one": "{{count}} change waits for you", + "proposalsWaiting_other": "{{count}} changes wait for you", + "proposalsBody": "A model proposed a change. Nothing is saved until you answer." }, "connectorsPage": { "title": "Connectors", @@ -918,5 +921,123 @@ "stopped_one": "Stopped {{count}} loop.", "stopped_other": "Stopped {{count}} loops." } + }, + "changes": { + "loading": "Reading the proposed change...", + "unread": "The proposed change could not be read: {{message}}", + "removed": "This proposal is gone.", + "kind": { + "memory": "Memory", + "profile": "Profile", + "skill": "Skill", + "page": "Page", + "dashboard": "Dashboard" + }, + "scope": { + "user": "All your chats", + "agent": "Agent {{agent}}", + "agentAny": "One agent", + "channel": "This conversation" + }, + "help": "Nothing is saved until you confirm.", + "row": { + "kind": "Kind", + "target": "File", + "source": "From", + "expires": "Expires in" + }, + "source": { + "chat": "A chat", + "connector": "Connector {{name}}", + "connectorFrom": "Connector {{name}}, from {{sender}}" + }, + "expiresHours": "{{hours}} h {{minutes}} min", + "expiresMinutes": "{{minutes}} min", + "diff": { + "label": "Changes to {{target}}", + "newFile": "New file", + "added_one": "{{count}} line added", + "added_other": "{{count}} lines added", + "removed_one": "{{count}} line removed", + "removed_other": "{{count}} lines removed", + "same": "No difference from the current text.", + "gap_one": "{{count}} unchanged line", + "gap_other": "{{count}} unchanged lines", + "more_one": "{{count}} more line not shown", + "more_other": "{{count}} more lines not shown", + "srAdded": "Added:", + "srRemoved": "Removed:" + }, + "unreadFile": "The current file could not be read, so all of the text counts as new.", + "confirm": "Confirm", + "edit": "Edit then confirm", + "confirmEdited": "Confirm with my text", + "editLabel": "Text to save in {{target}}", + "failed": "Could not save the change: {{message}}. It is still waiting for you.", + "receipt": { + "applied": "Applied:", + "reverted": "Undone:", + "dismissed": "Dismissed:", + "expired": "Expired:", + "stale": "Not applied:" + }, + "why": { + "expired": "It waited more than a day. Nothing was saved.", + "stale": "The file changed after this was proposed, so nothing was saved." + }, + "askAgain": "Ask again", + "askAgainText": "Please propose this change again, the file has changed since: {{summary}} ({{target}})", + "announce": { + "applied": "Change applied.", + "dismissed": "Change dismissed.", + "undone": "Change undone.", + "asked": "Asked the assistant to propose it again." + } + }, + "proposalsPage": { + "title": "Proposals", + "supporting": "Changes that models asked for and that wait for your answer.", + "emptyTitle": "Nothing is waiting", + "emptyBody": "When a model proposes a change to your memory or an agent profile, it shows here until you answer.", + "loadError": "Could not load the proposals." + }, + "memoryPage": { + "lede": "The notes that LocalAI puts in the prompt of your chats. A model changes them only after you confirm a proposal. Here you can read, edit and delete them.", + "limits": "Caps: an index has at most {{lines}} lines and {{index}}, a profile {{profile}}, and one turn carries at most {{turn}} of notes.", + "proposalsLink": "See the changes waiting for you", + "emptyTitle": "No notes yet", + "emptyBody": "When you ask an assistant to remember something and you confirm it, the notes appear here.", + "loadError": "Could not load your notes.", + "use": "{{size}} of {{max}} · {{lines}} of {{maxLines}} lines", + "useBytes": "{{size}} of {{max}}", + "useLabel": "Size of the index against its cap", + "over": "Over the cap: the end is not sent to the model.", + "index": "Index", + "group": { + "user": "Your notes", + "agent": "Notes of agent {{key}}", + "channel": "Notes of conversation {{key}}", + "profile": "Profile of agent {{key}}" + }, + "edit": "Edit", + "save": "Save", + "editLabel": "Text of {{name}}", + "editSizeIndex": "{{size}} of {{max}} · {{lines}} of {{maxLines}} lines", + "editSize": "{{size}} of {{max}}", + "tooBig": "Too long: shorten it to save.", + "saveFailed": "Could not save: {{message}}", + "saved": "Saved {{name}}.", + "emptyFile": "(empty)", + "delete": "Delete {{name}}", + "deleteYes": "Delete", + "confirmDelete": "Delete {{name}}?", + "deleted": "Moved {{name}} to the Trash.", + "clear": "Clear all notes", + "clearAsk_one": "Delete {{count}} file? It moves to the Trash and you can restore it from Files.", + "clearAsk_other": "Delete {{count}} files? They move to the Trash and you can restore them from Files.", + "clearYes_one": "Delete {{count}} file", + "clearYes_other": "Delete {{count}} files", + "cleared_one": "Moved {{count}} file to the Trash.", + "cleared_other": "Moved {{count}} files to the Trash." } } diff --git a/core/http/react-ui/public/locales/es/chats.json b/core/http/react-ui/public/locales/es/chats.json index 263e8a5495..ac67746342 100644 --- a/core/http/react-ui/public/locales/es/chats.json +++ b/core/http/react-ui/public/locales/es/chats.json @@ -454,7 +454,10 @@ "itemUnread": "{{title}}, sin leer", "arrived": "Nueva notificación: {{title}}", "arrivedMany_one": "{{count}} notificación nueva", - "arrivedMany_other": "{{count}} notificaciones nuevas" + "arrivedMany_other": "{{count}} notificaciones nuevas", + "proposalsWaiting_one": "{{count}} cambio te espera", + "proposalsWaiting_other": "{{count}} cambios te esperan", + "proposalsBody": "Un modelo propuso un cambio. No se guarda nada hasta que respondas." }, "change": { "loading": "Leyendo el cambio del disparador...", @@ -583,5 +586,123 @@ "stopped_one": "Se detuvo {{count}} bucle.", "stopped_other": "Se detuvieron {{count}} bucles." } + }, + "changes": { + "loading": "Leyendo el cambio propuesto...", + "unread": "No se pudo leer el cambio propuesto: {{message}}", + "removed": "Esta propuesta ya no existe.", + "kind": { + "memory": "Memoria", + "profile": "Perfil", + "skill": "Habilidad", + "page": "Página", + "dashboard": "Panel" + }, + "scope": { + "user": "Todos tus chats", + "agent": "Agente {{agent}}", + "agentAny": "Un agente", + "channel": "Esta conversación" + }, + "help": "No se guarda nada hasta que confirmes.", + "row": { + "kind": "Tipo", + "target": "Archivo", + "source": "De", + "expires": "Caduca en" + }, + "source": { + "chat": "Un chat", + "connector": "Conector {{name}}", + "connectorFrom": "Conector {{name}}, de {{sender}}" + }, + "expiresHours": "{{hours}} h {{minutes}} min", + "expiresMinutes": "{{minutes}} min", + "diff": { + "label": "Cambios en {{target}}", + "newFile": "Archivo nuevo", + "added_one": "{{count}} línea añadida", + "added_other": "{{count}} líneas añadidas", + "removed_one": "{{count}} línea eliminada", + "removed_other": "{{count}} líneas eliminadas", + "same": "Sin diferencias con el texto actual.", + "gap_one": "{{count}} línea sin cambios", + "gap_other": "{{count}} líneas sin cambios", + "more_one": "{{count}} línea más sin mostrar", + "more_other": "{{count}} líneas más sin mostrar", + "srAdded": "Añadida:", + "srRemoved": "Eliminada:" + }, + "unreadFile": "No se pudo leer el archivo actual, así que todo el texto cuenta como nuevo.", + "confirm": "Confirmar", + "edit": "Editar y confirmar", + "confirmEdited": "Confirmar con mi texto", + "editLabel": "Texto que se guardará en {{target}}", + "failed": "No se pudo guardar el cambio: {{message}}. Sigue esperando tu respuesta.", + "receipt": { + "applied": "Aplicado:", + "reverted": "Deshecho:", + "dismissed": "Descartado:", + "expired": "Caducado:", + "stale": "No aplicado:" + }, + "why": { + "expired": "Esperó más de un día. No se guardó nada.", + "stale": "El archivo cambió después de la propuesta, así que no se guardó nada." + }, + "askAgain": "Pedir de nuevo", + "askAgainText": "Por favor, propón este cambio de nuevo; el archivo ha cambiado desde entonces: {{summary}} ({{target}})", + "announce": { + "applied": "Cambio aplicado.", + "dismissed": "Cambio descartado.", + "undone": "Cambio deshecho.", + "asked": "Se pidió al asistente que lo proponga de nuevo." + } + }, + "proposalsPage": { + "title": "Propuestas", + "supporting": "Cambios que pidieron los modelos y que esperan tu respuesta.", + "emptyTitle": "No hay nada pendiente", + "emptyBody": "Cuando un modelo propone un cambio en tu memoria o en el perfil de un agente, aparece aquí hasta que respondas.", + "loadError": "No se pudieron cargar las propuestas." + }, + "memoryPage": { + "lede": "Las notas que LocalAI añade al prompt de tus chats. Un modelo solo las cambia después de que confirmes una propuesta. Aquí puedes leerlas, editarlas y eliminarlas.", + "limits": "Límites: un índice tiene como máximo {{lines}} líneas y {{index}}, un perfil {{profile}}, y un turno lleva como máximo {{turn}} de notas.", + "proposalsLink": "Ver los cambios pendientes", + "emptyTitle": "Aún no hay notas", + "emptyBody": "Cuando pides a un asistente que recuerde algo y lo confirmas, las notas aparecen aquí.", + "loadError": "No se pudieron cargar tus notas.", + "use": "{{size}} de {{max}} · {{lines}} de {{maxLines}} líneas", + "useBytes": "{{size}} de {{max}}", + "useLabel": "Tamaño del índice frente a su límite", + "over": "Supera el límite: el final no se envía al modelo.", + "index": "Índice", + "group": { + "user": "Tus notas", + "agent": "Notas del agente {{key}}", + "channel": "Notas de la conversación {{key}}", + "profile": "Perfil del agente {{key}}" + }, + "edit": "Editar", + "save": "Guardar", + "editLabel": "Texto de {{name}}", + "editSizeIndex": "{{size}} de {{max}} · {{lines}} de {{maxLines}} líneas", + "editSize": "{{size}} de {{max}}", + "tooBig": "Demasiado largo: acórtalo para guardar.", + "saveFailed": "No se pudo guardar: {{message}}", + "saved": "{{name}} guardado.", + "emptyFile": "(vacío)", + "delete": "Eliminar {{name}}", + "deleteYes": "Eliminar", + "confirmDelete": "¿Eliminar {{name}}?", + "deleted": "{{name}} movido a la Papelera.", + "clear": "Borrar todas las notas", + "clearAsk_one": "¿Eliminar {{count}} archivo? Va a la Papelera y puedes restaurarlo desde Archivos.", + "clearAsk_other": "¿Eliminar {{count}} archivos? Van a la Papelera y puedes restaurarlos desde Archivos.", + "clearYes_one": "Eliminar {{count}} archivo", + "clearYes_other": "Eliminar {{count}} archivos", + "cleared_one": "{{count}} archivo movido a la Papelera.", + "cleared_other": "{{count}} archivos movidos a la Papelera." } } diff --git a/core/http/react-ui/public/locales/fr/chats.json b/core/http/react-ui/public/locales/fr/chats.json index 05b6c4799f..8d294f4c0b 100644 --- a/core/http/react-ui/public/locales/fr/chats.json +++ b/core/http/react-ui/public/locales/fr/chats.json @@ -454,7 +454,10 @@ "itemUnread": "{{title}}, non lue", "arrived": "Nouvelle notification : {{title}}", "arrivedMany_one": "{{count}} nouvelle notification", - "arrivedMany_other": "{{count}} nouvelles notifications" + "arrivedMany_other": "{{count}} nouvelles notifications", + "proposalsWaiting_one": "{{count}} modification vous attend", + "proposalsWaiting_other": "{{count}} modifications vous attendent", + "proposalsBody": "Un modèle a proposé une modification. Rien n'est enregistré avant votre réponse." }, "change": { "loading": "Lecture de la modification du déclencheur...", @@ -583,5 +586,123 @@ "stopped_one": "{{count}} boucle arrêtée.", "stopped_other": "{{count}} boucles arrêtées." } + }, + "changes": { + "loading": "Lecture de la modification proposée...", + "unread": "Impossible de lire la modification proposée : {{message}}", + "removed": "Cette proposition n'existe plus.", + "kind": { + "memory": "Mémoire", + "profile": "Profil", + "skill": "Skill", + "page": "Page", + "dashboard": "Tableau de bord" + }, + "scope": { + "user": "Toutes vos conversations", + "agent": "Agent {{agent}}", + "agentAny": "Un agent", + "channel": "Cette conversation" + }, + "help": "Rien n'est enregistré avant votre confirmation.", + "row": { + "kind": "Type", + "target": "Fichier", + "source": "De", + "expires": "Expire dans" + }, + "source": { + "chat": "Une conversation", + "connector": "Connecteur {{name}}", + "connectorFrom": "Connecteur {{name}}, de {{sender}}" + }, + "expiresHours": "{{hours}} h {{minutes}} min", + "expiresMinutes": "{{minutes}} min", + "diff": { + "label": "Modifications de {{target}}", + "newFile": "Nouveau fichier", + "added_one": "{{count}} ligne ajoutée", + "added_other": "{{count}} lignes ajoutées", + "removed_one": "{{count}} ligne supprimée", + "removed_other": "{{count}} lignes supprimées", + "same": "Aucune différence avec le texte actuel.", + "gap_one": "{{count}} ligne inchangée", + "gap_other": "{{count}} lignes inchangées", + "more_one": "{{count}} ligne de plus non affichée", + "more_other": "{{count}} lignes de plus non affichées", + "srAdded": "Ajoutée :", + "srRemoved": "Supprimée :" + }, + "unreadFile": "Le fichier actuel n'a pas pu être lu : tout le texte compte comme nouveau.", + "confirm": "Confirmer", + "edit": "Modifier puis confirmer", + "confirmEdited": "Confirmer avec mon texte", + "editLabel": "Texte à enregistrer dans {{target}}", + "failed": "Impossible d'enregistrer la modification : {{message}}. Elle attend toujours votre réponse.", + "receipt": { + "applied": "Appliquée :", + "reverted": "Annulée :", + "dismissed": "Refusée :", + "expired": "Expirée :", + "stale": "Non appliquée :" + }, + "why": { + "expired": "Elle a attendu plus d'un jour. Rien n'a été enregistré.", + "stale": "Le fichier a changé après la proposition : rien n'a été enregistré." + }, + "askAgain": "Redemander", + "askAgainText": "Merci de proposer à nouveau cette modification, le fichier a changé entre-temps : {{summary}} ({{target}})", + "announce": { + "applied": "Modification appliquée.", + "dismissed": "Modification refusée.", + "undone": "Modification annulée.", + "asked": "L'assistant a été prié de la proposer à nouveau." + } + }, + "proposalsPage": { + "title": "Propositions", + "supporting": "Modifications demandées par les modèles et en attente de votre réponse.", + "emptyTitle": "Rien en attente", + "emptyBody": "Quand un modèle propose une modification de votre mémoire ou du profil d'un agent, elle apparaît ici jusqu'à votre réponse.", + "loadError": "Impossible de charger les propositions." + }, + "memoryPage": { + "lede": "Les notes que LocalAI ajoute au prompt de vos conversations. Un modèle ne les modifie qu'après votre confirmation d'une proposition. Ici, vous pouvez les lire, les modifier et les supprimer.", + "limits": "Limites : un index compte au plus {{lines}} lignes et {{index}}, un profil {{profile}}, et un tour transporte au plus {{turn}} de notes.", + "proposalsLink": "Voir les modifications en attente", + "emptyTitle": "Pas encore de notes", + "emptyBody": "Quand vous demandez à un assistant de retenir quelque chose et que vous confirmez, les notes apparaissent ici.", + "loadError": "Impossible de charger vos notes.", + "use": "{{size}} sur {{max}} · {{lines}} sur {{maxLines}} lignes", + "useBytes": "{{size}} sur {{max}}", + "useLabel": "Taille de l'index par rapport à sa limite", + "over": "Au-delà de la limite : la fin n'est pas envoyée au modèle.", + "index": "Index", + "group": { + "user": "Vos notes", + "agent": "Notes de l'agent {{key}}", + "channel": "Notes de la conversation {{key}}", + "profile": "Profil de l'agent {{key}}" + }, + "edit": "Modifier", + "save": "Enregistrer", + "editLabel": "Texte de {{name}}", + "editSizeIndex": "{{size}} sur {{max}} · {{lines}} sur {{maxLines}} lignes", + "editSize": "{{size}} sur {{max}}", + "tooBig": "Trop long : raccourcissez pour enregistrer.", + "saveFailed": "Impossible d'enregistrer : {{message}}", + "saved": "{{name}} enregistré.", + "emptyFile": "(vide)", + "delete": "Supprimer {{name}}", + "deleteYes": "Supprimer", + "confirmDelete": "Supprimer {{name}} ?", + "deleted": "{{name}} déplacé dans la corbeille.", + "clear": "Effacer toutes les notes", + "clearAsk_one": "Supprimer {{count}} fichier ? Il va dans la corbeille et se restaure depuis Fichiers.", + "clearAsk_other": "Supprimer {{count}} fichiers ? Ils vont dans la corbeille et se restaurent depuis Fichiers.", + "clearYes_one": "Supprimer {{count}} fichier", + "clearYes_other": "Supprimer {{count}} fichiers", + "cleared_one": "{{count}} fichier déplacé dans la corbeille.", + "cleared_other": "{{count}} fichiers déplacés dans la corbeille." } } diff --git a/core/http/react-ui/public/locales/id/chats.json b/core/http/react-ui/public/locales/id/chats.json index 0ca09fe668..360259704e 100644 --- a/core/http/react-ui/public/locales/id/chats.json +++ b/core/http/react-ui/public/locales/id/chats.json @@ -454,7 +454,10 @@ "itemUnread": "{{title}}, belum dibaca", "arrived": "Notifikasi baru: {{title}}", "arrivedMany_one": "{{count}} notifikasi baru", - "arrivedMany_other": "{{count}} notifikasi baru" + "arrivedMany_other": "{{count}} notifikasi baru", + "proposalsWaiting_one": "{{count}} perubahan menunggu Anda", + "proposalsWaiting_other": "{{count}} perubahan menunggu Anda", + "proposalsBody": "Model mengusulkan perubahan. Tidak ada yang disimpan sampai Anda menjawab." }, "change": { "loading": "Membaca perubahan pemicu...", @@ -583,5 +586,123 @@ "stopped_one": "{{count}} perulangan dihentikan.", "stopped_other": "{{count}} perulangan dihentikan." } + }, + "changes": { + "loading": "Membaca perubahan yang diusulkan...", + "unread": "Perubahan yang diusulkan tidak dapat dibaca: {{message}}", + "removed": "Usulan ini sudah tidak ada.", + "kind": { + "memory": "Memori", + "profile": "Profil", + "skill": "Skill", + "page": "Halaman", + "dashboard": "Dasbor" + }, + "scope": { + "user": "Semua obrolan Anda", + "agent": "Agen {{agent}}", + "agentAny": "Satu agen", + "channel": "Percakapan ini" + }, + "help": "Tidak ada yang disimpan sampai Anda mengonfirmasi.", + "row": { + "kind": "Jenis", + "target": "Berkas", + "source": "Dari", + "expires": "Kedaluwarsa dalam" + }, + "source": { + "chat": "Obrolan", + "connector": "Konektor {{name}}", + "connectorFrom": "Konektor {{name}}, dari {{sender}}" + }, + "expiresHours": "{{hours}} j {{minutes}} mnt", + "expiresMinutes": "{{minutes}} mnt", + "diff": { + "label": "Perubahan pada {{target}}", + "newFile": "Berkas baru", + "added_one": "{{count}} baris ditambahkan", + "added_other": "{{count}} baris ditambahkan", + "removed_one": "{{count}} baris dihapus", + "removed_other": "{{count}} baris dihapus", + "same": "Tidak ada perbedaan dari teks saat ini.", + "gap_one": "{{count}} baris tidak berubah", + "gap_other": "{{count}} baris tidak berubah", + "more_one": "{{count}} baris lagi tidak ditampilkan", + "more_other": "{{count}} baris lagi tidak ditampilkan", + "srAdded": "Ditambahkan:", + "srRemoved": "Dihapus:" + }, + "unreadFile": "Berkas saat ini tidak dapat dibaca, jadi seluruh teks dianggap baru.", + "confirm": "Konfirmasi", + "edit": "Edit lalu konfirmasi", + "confirmEdited": "Konfirmasi dengan teks saya", + "editLabel": "Teks yang disimpan di {{target}}", + "failed": "Perubahan tidak dapat disimpan: {{message}}. Masih menunggu Anda.", + "receipt": { + "applied": "Diterapkan:", + "reverted": "Dibatalkan:", + "dismissed": "Ditolak:", + "expired": "Kedaluwarsa:", + "stale": "Tidak diterapkan:" + }, + "why": { + "expired": "Menunggu lebih dari sehari. Tidak ada yang disimpan.", + "stale": "Berkas berubah setelah usulan dibuat, jadi tidak ada yang disimpan." + }, + "askAgain": "Minta lagi", + "askAgainText": "Tolong usulkan perubahan ini lagi, berkas sudah berubah sejak itu: {{summary}} ({{target}})", + "announce": { + "applied": "Perubahan diterapkan.", + "dismissed": "Perubahan ditolak.", + "undone": "Perubahan dibatalkan.", + "asked": "Asisten diminta mengusulkannya lagi." + } + }, + "proposalsPage": { + "title": "Usulan", + "supporting": "Perubahan yang diminta model dan menunggu jawaban Anda.", + "emptyTitle": "Tidak ada yang menunggu", + "emptyBody": "Saat model mengusulkan perubahan pada memori Anda atau profil agen, usulan itu tampil di sini sampai Anda menjawab.", + "loadError": "Usulan tidak dapat dimuat." + }, + "memoryPage": { + "lede": "Catatan yang dimasukkan LocalAI ke prompt obrolan Anda. Model hanya mengubahnya setelah Anda mengonfirmasi usulan. Di sini Anda dapat membaca, mengedit, dan menghapusnya.", + "limits": "Batas: indeks maksimal {{lines}} baris dan {{index}}, profil {{profile}}, dan satu giliran membawa maksimal {{turn}} catatan.", + "proposalsLink": "Lihat perubahan yang menunggu", + "emptyTitle": "Belum ada catatan", + "emptyBody": "Saat Anda meminta asisten mengingat sesuatu dan mengonfirmasinya, catatan muncul di sini.", + "loadError": "Catatan Anda tidak dapat dimuat.", + "use": "{{size}} dari {{max}} · {{lines}} dari {{maxLines}} baris", + "useBytes": "{{size}} dari {{max}}", + "useLabel": "Ukuran indeks terhadap batasnya", + "over": "Melebihi batas: bagian akhir tidak dikirim ke model.", + "index": "Indeks", + "group": { + "user": "Catatan Anda", + "agent": "Catatan agen {{key}}", + "channel": "Catatan percakapan {{key}}", + "profile": "Profil agen {{key}}" + }, + "edit": "Edit", + "save": "Simpan", + "editLabel": "Teks {{name}}", + "editSizeIndex": "{{size}} dari {{max}} · {{lines}} dari {{maxLines}} baris", + "editSize": "{{size}} dari {{max}}", + "tooBig": "Terlalu panjang: persingkat untuk menyimpan.", + "saveFailed": "Tidak dapat menyimpan: {{message}}", + "saved": "{{name}} disimpan.", + "emptyFile": "(kosong)", + "delete": "Hapus {{name}}", + "deleteYes": "Hapus", + "confirmDelete": "Hapus {{name}}?", + "deleted": "{{name}} dipindahkan ke Tempat Sampah.", + "clear": "Hapus semua catatan", + "clearAsk_one": "Hapus {{count}} berkas? Berkas dipindahkan ke Tempat Sampah dan dapat dipulihkan dari Berkas.", + "clearAsk_other": "Hapus {{count}} berkas? Berkas dipindahkan ke Tempat Sampah dan dapat dipulihkan dari Berkas.", + "clearYes_one": "Hapus {{count}} berkas", + "clearYes_other": "Hapus {{count}} berkas", + "cleared_one": "{{count}} berkas dipindahkan ke Tempat Sampah.", + "cleared_other": "{{count}} berkas dipindahkan ke Tempat Sampah." } } diff --git a/core/http/react-ui/public/locales/it/chats.json b/core/http/react-ui/public/locales/it/chats.json index 1fb1b4d77c..82a84631f6 100644 --- a/core/http/react-ui/public/locales/it/chats.json +++ b/core/http/react-ui/public/locales/it/chats.json @@ -454,7 +454,10 @@ "itemUnread": "{{title}}, da leggere", "arrived": "Nuova notifica: {{title}}", "arrivedMany_one": "{{count}} nuova notifica", - "arrivedMany_other": "{{count}} nuove notifiche" + "arrivedMany_other": "{{count}} nuove notifiche", + "proposalsWaiting_one": "{{count}} modifica attende te", + "proposalsWaiting_other": "{{count}} modifiche attendono te", + "proposalsBody": "Un modello ha proposto una modifica. Non viene salvato nulla finché non rispondi." }, "change": { "loading": "Lettura della modifica del trigger...", @@ -583,5 +586,123 @@ "stopped_one": "Fermato {{count}} ciclo.", "stopped_other": "Fermati {{count}} cicli." } + }, + "changes": { + "loading": "Lettura della modifica proposta...", + "unread": "Impossibile leggere la modifica proposta: {{message}}", + "removed": "Questa proposta non esiste più.", + "kind": { + "memory": "Memoria", + "profile": "Profilo", + "skill": "Skill", + "page": "Pagina", + "dashboard": "Dashboard" + }, + "scope": { + "user": "Tutte le tue chat", + "agent": "Agente {{agent}}", + "agentAny": "Un agente", + "channel": "Questa conversazione" + }, + "help": "Non viene salvato nulla finché non confermi.", + "row": { + "kind": "Tipo", + "target": "File", + "source": "Da", + "expires": "Scade tra" + }, + "source": { + "chat": "Una chat", + "connector": "Connettore {{name}}", + "connectorFrom": "Connettore {{name}}, da {{sender}}" + }, + "expiresHours": "{{hours}} h {{minutes}} min", + "expiresMinutes": "{{minutes}} min", + "diff": { + "label": "Modifiche a {{target}}", + "newFile": "Nuovo file", + "added_one": "{{count}} riga aggiunta", + "added_other": "{{count}} righe aggiunte", + "removed_one": "{{count}} riga rimossa", + "removed_other": "{{count}} righe rimosse", + "same": "Nessuna differenza dal testo attuale.", + "gap_one": "{{count}} riga invariata", + "gap_other": "{{count}} righe invariate", + "more_one": "Altra {{count}} riga non mostrata", + "more_other": "Altre {{count}} righe non mostrate", + "srAdded": "Aggiunta:", + "srRemoved": "Rimossa:" + }, + "unreadFile": "Non è stato possibile leggere il file attuale, quindi tutto il testo conta come nuovo.", + "confirm": "Conferma", + "edit": "Modifica e conferma", + "confirmEdited": "Conferma con il mio testo", + "editLabel": "Testo da salvare in {{target}}", + "failed": "Impossibile salvare la modifica: {{message}}. È ancora in attesa di te.", + "receipt": { + "applied": "Applicata:", + "reverted": "Annullata:", + "dismissed": "Rifiutata:", + "expired": "Scaduta:", + "stale": "Non applicata:" + }, + "why": { + "expired": "Ha atteso più di un giorno. Non è stato salvato nulla.", + "stale": "Il file è cambiato dopo la proposta, quindi non è stato salvato nulla." + }, + "askAgain": "Chiedi di nuovo", + "askAgainText": "Per favore proponi di nuovo questa modifica, il file è cambiato nel frattempo: {{summary}} ({{target}})", + "announce": { + "applied": "Modifica applicata.", + "dismissed": "Modifica rifiutata.", + "undone": "Modifica annullata.", + "asked": "Ho chiesto all'assistente di riproporla." + } + }, + "proposalsPage": { + "title": "Proposte", + "supporting": "Modifiche richieste dai modelli che attendono la tua risposta.", + "emptyTitle": "Niente in attesa", + "emptyBody": "Quando un modello propone una modifica alla tua memoria o al profilo di un agente, appare qui finché non rispondi.", + "loadError": "Impossibile caricare le proposte." + }, + "memoryPage": { + "lede": "Le note che LocalAI inserisce nel prompt delle tue chat. Un modello le modifica solo dopo che confermi una proposta. Qui puoi leggerle, modificarle ed eliminarle.", + "limits": "Limiti: un indice ha al massimo {{lines}} righe e {{index}}, un profilo {{profile}}, e un turno porta al massimo {{turn}} di note.", + "proposalsLink": "Vedi le modifiche in attesa", + "emptyTitle": "Ancora nessuna nota", + "emptyBody": "Quando chiedi a un assistente di ricordare qualcosa e confermi, le note appaiono qui.", + "loadError": "Impossibile caricare le tue note.", + "use": "{{size}} di {{max}} · {{lines}} di {{maxLines}} righe", + "useBytes": "{{size}} di {{max}}", + "useLabel": "Dimensione dell'indice rispetto al limite", + "over": "Oltre il limite: la parte finale non viene inviata al modello.", + "index": "Indice", + "group": { + "user": "Le tue note", + "agent": "Note dell'agente {{key}}", + "channel": "Note della conversazione {{key}}", + "profile": "Profilo dell'agente {{key}}" + }, + "edit": "Modifica", + "save": "Salva", + "editLabel": "Testo di {{name}}", + "editSizeIndex": "{{size}} di {{max}} · {{lines}} di {{maxLines}} righe", + "editSize": "{{size}} di {{max}}", + "tooBig": "Troppo lungo: accorcialo per salvare.", + "saveFailed": "Impossibile salvare: {{message}}", + "saved": "{{name}} salvato.", + "emptyFile": "(vuoto)", + "delete": "Elimina {{name}}", + "deleteYes": "Elimina", + "confirmDelete": "Eliminare {{name}}?", + "deleted": "{{name}} spostato nel Cestino.", + "clear": "Cancella tutte le note", + "clearAsk_one": "Eliminare {{count}} file? Va nel Cestino e puoi ripristinarlo da File.", + "clearAsk_other": "Eliminare {{count}} file? Vanno nel Cestino e puoi ripristinarli da File.", + "clearYes_one": "Elimina {{count}} file", + "clearYes_other": "Elimina {{count}} file", + "cleared_one": "{{count}} file spostato nel Cestino.", + "cleared_other": "{{count}} file spostati nel Cestino." } } diff --git a/core/http/react-ui/public/locales/ko/chats.json b/core/http/react-ui/public/locales/ko/chats.json index 1686a73e31..f5d6e83ccc 100644 --- a/core/http/react-ui/public/locales/ko/chats.json +++ b/core/http/react-ui/public/locales/ko/chats.json @@ -454,7 +454,10 @@ "itemUnread": "{{title}}, 읽지 않음", "arrived": "새 알림: {{title}}", "arrivedMany_one": "새 알림 {{count}}개", - "arrivedMany_other": "새 알림 {{count}}개" + "arrivedMany_other": "새 알림 {{count}}개", + "proposalsWaiting_one": "변경 {{count}}개가 기다리고 있습니다", + "proposalsWaiting_other": "변경 {{count}}개가 기다리고 있습니다", + "proposalsBody": "모델이 변경을 제안했습니다. 답하기 전에는 아무것도 저장되지 않습니다." }, "change": { "loading": "트리거 변경 사항을 읽는 중...", @@ -583,5 +586,123 @@ "stopped_one": "반복 {{count}}개를 멈췄습니다.", "stopped_other": "반복 {{count}}개를 멈췄습니다." } + }, + "changes": { + "loading": "제안된 변경을 읽는 중...", + "unread": "제안된 변경을 읽을 수 없습니다: {{message}}", + "removed": "이 제안은 더 이상 없습니다.", + "kind": { + "memory": "메모리", + "profile": "프로필", + "skill": "스킬", + "page": "페이지", + "dashboard": "대시보드" + }, + "scope": { + "user": "내 모든 채팅", + "agent": "에이전트 {{agent}}", + "agentAny": "에이전트 하나", + "channel": "이 대화" + }, + "help": "확인하기 전에는 아무것도 저장되지 않습니다.", + "row": { + "kind": "종류", + "target": "파일", + "source": "출처", + "expires": "만료까지" + }, + "source": { + "chat": "채팅", + "connector": "커넥터 {{name}}", + "connectorFrom": "커넥터 {{name}}, 보낸 사람 {{sender}}" + }, + "expiresHours": "{{hours}}시간 {{minutes}}분", + "expiresMinutes": "{{minutes}}분", + "diff": { + "label": "{{target}}의 변경 사항", + "newFile": "새 파일", + "added_one": "{{count}}줄 추가", + "added_other": "{{count}}줄 추가", + "removed_one": "{{count}}줄 삭제", + "removed_other": "{{count}}줄 삭제", + "same": "현재 텍스트와 다른 점이 없습니다.", + "gap_one": "변경 없는 {{count}}줄", + "gap_other": "변경 없는 {{count}}줄", + "more_one": "표시되지 않은 {{count}}줄 더", + "more_other": "표시되지 않은 {{count}}줄 더", + "srAdded": "추가됨:", + "srRemoved": "삭제됨:" + }, + "unreadFile": "현재 파일을 읽을 수 없어 전체 텍스트를 새 내용으로 봅니다.", + "confirm": "확인", + "edit": "편집 후 확인", + "confirmEdited": "내 텍스트로 확인", + "editLabel": "{{target}}에 저장할 텍스트", + "failed": "변경을 저장하지 못했습니다: {{message}}. 아직 답을 기다리고 있습니다.", + "receipt": { + "applied": "적용됨:", + "reverted": "되돌림:", + "dismissed": "거절됨:", + "expired": "만료됨:", + "stale": "적용되지 않음:" + }, + "why": { + "expired": "하루 넘게 기다렸습니다. 아무것도 저장되지 않았습니다.", + "stale": "제안 이후 파일이 바뀌어 아무것도 저장되지 않았습니다." + }, + "askAgain": "다시 요청", + "askAgainText": "파일이 그 사이 바뀌었으니 이 변경을 다시 제안해 주세요: {{summary}} ({{target}})", + "announce": { + "applied": "변경을 적용했습니다.", + "dismissed": "변경을 거절했습니다.", + "undone": "변경을 되돌렸습니다.", + "asked": "어시스턴트에게 다시 제안하도록 요청했습니다." + } + }, + "proposalsPage": { + "title": "제안", + "supporting": "모델이 요청했고 당신의 답을 기다리는 변경입니다.", + "emptyTitle": "대기 중인 항목이 없습니다", + "emptyBody": "모델이 메모리나 에이전트 프로필 변경을 제안하면 답할 때까지 여기에 표시됩니다.", + "loadError": "제안을 불러오지 못했습니다." + }, + "memoryPage": { + "lede": "LocalAI가 채팅 프롬프트에 넣는 메모입니다. 모델은 당신이 제안을 확인한 뒤에만 이를 바꿉니다. 여기서 읽고, 편집하고, 삭제할 수 있습니다.", + "limits": "한도: 색인은 최대 {{lines}}줄과 {{index}}, 프로필은 {{profile}}, 한 턴에는 최대 {{turn}}의 메모가 들어갑니다.", + "proposalsLink": "대기 중인 변경 보기", + "emptyTitle": "아직 메모가 없습니다", + "emptyBody": "어시스턴트에게 무언가를 기억하도록 요청하고 확인하면 메모가 여기에 나타납니다.", + "loadError": "메모를 불러오지 못했습니다.", + "use": "{{size}} / {{max}} · {{lines}} / {{maxLines}}줄", + "useBytes": "{{size}} / {{max}}", + "useLabel": "색인 크기와 한도", + "over": "한도 초과: 끝부분은 모델에 전달되지 않습니다.", + "index": "색인", + "group": { + "user": "내 메모", + "agent": "에이전트 {{key}}의 메모", + "channel": "대화 {{key}}의 메모", + "profile": "에이전트 {{key}}의 프로필" + }, + "edit": "편집", + "save": "저장", + "editLabel": "{{name}}의 텍스트", + "editSizeIndex": "{{size}} / {{max}} · {{lines}} / {{maxLines}}줄", + "editSize": "{{size}} / {{max}}", + "tooBig": "너무 깁니다. 저장하려면 줄여 주세요.", + "saveFailed": "저장하지 못했습니다: {{message}}", + "saved": "{{name}}을(를) 저장했습니다.", + "emptyFile": "(비어 있음)", + "delete": "{{name}} 삭제", + "deleteYes": "삭제", + "confirmDelete": "{{name}}을(를) 삭제할까요?", + "deleted": "{{name}}을(를) 휴지통으로 옮겼습니다.", + "clear": "모든 메모 지우기", + "clearAsk_one": "파일 {{count}}개를 삭제할까요? 휴지통으로 이동하며 파일에서 복원할 수 있습니다.", + "clearAsk_other": "파일 {{count}}개를 삭제할까요? 휴지통으로 이동하며 파일에서 복원할 수 있습니다.", + "clearYes_one": "파일 {{count}}개 삭제", + "clearYes_other": "파일 {{count}}개 삭제", + "cleared_one": "파일 {{count}}개를 휴지통으로 옮겼습니다.", + "cleared_other": "파일 {{count}}개를 휴지통으로 옮겼습니다." } } diff --git a/core/http/react-ui/public/locales/pt-BR/chats.json b/core/http/react-ui/public/locales/pt-BR/chats.json index 455155e0e9..a7b518cbd4 100644 --- a/core/http/react-ui/public/locales/pt-BR/chats.json +++ b/core/http/react-ui/public/locales/pt-BR/chats.json @@ -454,7 +454,10 @@ "itemUnread": "{{title}}, não lida", "arrived": "Nova notificação: {{title}}", "arrivedMany_one": "{{count}} nova notificação", - "arrivedMany_other": "{{count}} novas notificações" + "arrivedMany_other": "{{count}} novas notificações", + "proposalsWaiting_one": "{{count}} alteração aguarda você", + "proposalsWaiting_other": "{{count}} alterações aguardam você", + "proposalsBody": "Um modelo propôs uma alteração. Nada é salvo até você responder." }, "change": { "loading": "Lendo a alteração do gatilho...", @@ -583,5 +586,123 @@ "stopped_one": "{{count}} loop parado.", "stopped_other": "{{count}} loops parados." } + }, + "changes": { + "loading": "Lendo a alteração proposta...", + "unread": "Não foi possível ler a alteração proposta: {{message}}", + "removed": "Esta proposta não existe mais.", + "kind": { + "memory": "Memória", + "profile": "Perfil", + "skill": "Skill", + "page": "Página", + "dashboard": "Painel" + }, + "scope": { + "user": "Todos os seus chats", + "agent": "Agente {{agent}}", + "agentAny": "Um agente", + "channel": "Esta conversa" + }, + "help": "Nada é salvo até você confirmar.", + "row": { + "kind": "Tipo", + "target": "Arquivo", + "source": "De", + "expires": "Expira em" + }, + "source": { + "chat": "Um chat", + "connector": "Conector {{name}}", + "connectorFrom": "Conector {{name}}, de {{sender}}" + }, + "expiresHours": "{{hours}} h {{minutes}} min", + "expiresMinutes": "{{minutes}} min", + "diff": { + "label": "Alterações em {{target}}", + "newFile": "Arquivo novo", + "added_one": "{{count}} linha adicionada", + "added_other": "{{count}} linhas adicionadas", + "removed_one": "{{count}} linha removida", + "removed_other": "{{count}} linhas removidas", + "same": "Nenhuma diferença do texto atual.", + "gap_one": "{{count}} linha sem alteração", + "gap_other": "{{count}} linhas sem alteração", + "more_one": "Mais {{count}} linha não exibida", + "more_other": "Mais {{count}} linhas não exibidas", + "srAdded": "Adicionada:", + "srRemoved": "Removida:" + }, + "unreadFile": "Não foi possível ler o arquivo atual, então todo o texto conta como novo.", + "confirm": "Confirmar", + "edit": "Editar e confirmar", + "confirmEdited": "Confirmar com meu texto", + "editLabel": "Texto a salvar em {{target}}", + "failed": "Não foi possível salvar a alteração: {{message}}. Ela continua aguardando você.", + "receipt": { + "applied": "Aplicada:", + "reverted": "Desfeita:", + "dismissed": "Dispensada:", + "expired": "Expirada:", + "stale": "Não aplicada:" + }, + "why": { + "expired": "Aguardou mais de um dia. Nada foi salvo.", + "stale": "O arquivo mudou depois da proposta, então nada foi salvo." + }, + "askAgain": "Pedir de novo", + "askAgainText": "Por favor, proponha esta alteração de novo; o arquivo mudou desde então: {{summary}} ({{target}})", + "announce": { + "applied": "Alteração aplicada.", + "dismissed": "Alteração dispensada.", + "undone": "Alteração desfeita.", + "asked": "O assistente foi solicitado a propor de novo." + } + }, + "proposalsPage": { + "title": "Propostas", + "supporting": "Alterações que os modelos pediram e que aguardam sua resposta.", + "emptyTitle": "Nada aguardando", + "emptyBody": "Quando um modelo propõe uma alteração na sua memória ou no perfil de um agente, ela aparece aqui até você responder.", + "loadError": "Não foi possível carregar as propostas." + }, + "memoryPage": { + "lede": "As notas que o LocalAI coloca no prompt dos seus chats. Um modelo só as altera depois que você confirma uma proposta. Aqui você pode lê-las, editá-las e excluí-las.", + "limits": "Limites: um índice tem no máximo {{lines}} linhas e {{index}}, um perfil {{profile}}, e um turno leva no máximo {{turn}} de notas.", + "proposalsLink": "Ver as alterações pendentes", + "emptyTitle": "Ainda não há notas", + "emptyBody": "Quando você pede a um assistente que lembre de algo e confirma, as notas aparecem aqui.", + "loadError": "Não foi possível carregar suas notas.", + "use": "{{size}} de {{max}} · {{lines}} de {{maxLines}} linhas", + "useBytes": "{{size}} de {{max}}", + "useLabel": "Tamanho do índice em relação ao limite", + "over": "Acima do limite: o final não é enviado ao modelo.", + "index": "Índice", + "group": { + "user": "Suas notas", + "agent": "Notas do agente {{key}}", + "channel": "Notas da conversa {{key}}", + "profile": "Perfil do agente {{key}}" + }, + "edit": "Editar", + "save": "Salvar", + "editLabel": "Texto de {{name}}", + "editSizeIndex": "{{size}} de {{max}} · {{lines}} de {{maxLines}} linhas", + "editSize": "{{size}} de {{max}}", + "tooBig": "Longo demais: encurte para salvar.", + "saveFailed": "Não foi possível salvar: {{message}}", + "saved": "{{name}} salvo.", + "emptyFile": "(vazio)", + "delete": "Excluir {{name}}", + "deleteYes": "Excluir", + "confirmDelete": "Excluir {{name}}?", + "deleted": "{{name}} movido para a Lixeira.", + "clear": "Limpar todas as notas", + "clearAsk_one": "Excluir {{count}} arquivo? Ele vai para a Lixeira e você pode restaurá-lo em Arquivos.", + "clearAsk_other": "Excluir {{count}} arquivos? Eles vão para a Lixeira e você pode restaurá-los em Arquivos.", + "clearYes_one": "Excluir {{count}} arquivo", + "clearYes_other": "Excluir {{count}} arquivos", + "cleared_one": "{{count}} arquivo movido para a Lixeira.", + "cleared_other": "{{count}} arquivos movidos para a Lixeira." } } diff --git a/core/http/react-ui/public/locales/zh-CN/chats.json b/core/http/react-ui/public/locales/zh-CN/chats.json index e9362528f6..bf842c7d23 100644 --- a/core/http/react-ui/public/locales/zh-CN/chats.json +++ b/core/http/react-ui/public/locales/zh-CN/chats.json @@ -454,7 +454,10 @@ "itemUnread": "{{title}},未读", "arrived": "新通知:{{title}}", "arrivedMany_one": "{{count}} 条新通知", - "arrivedMany_other": "{{count}} 条新通知" + "arrivedMany_other": "{{count}} 条新通知", + "proposalsWaiting_one": "{{count}} 项更改等待你处理", + "proposalsWaiting_other": "{{count}} 项更改等待你处理", + "proposalsBody": "模型提议了一项更改。在你回应之前不会保存任何内容。" }, "change": { "loading": "正在读取触发器更改...", @@ -583,5 +586,123 @@ "stopped_one": "已停止 {{count}} 个循环。", "stopped_other": "已停止 {{count}} 个循环。" } + }, + "changes": { + "loading": "正在读取提议的更改...", + "unread": "无法读取提议的更改:{{message}}", + "removed": "此提议已不存在。", + "kind": { + "memory": "记忆", + "profile": "配置文件", + "skill": "技能", + "page": "页面", + "dashboard": "仪表板" + }, + "scope": { + "user": "你的所有聊天", + "agent": "智能体 {{agent}}", + "agentAny": "一个智能体", + "channel": "此对话" + }, + "help": "确认之前不会保存任何内容。", + "row": { + "kind": "类型", + "target": "文件", + "source": "来源", + "expires": "剩余时间" + }, + "source": { + "chat": "聊天", + "connector": "连接器 {{name}}", + "connectorFrom": "连接器 {{name}},来自 {{sender}}" + }, + "expiresHours": "{{hours}} 小时 {{minutes}} 分钟", + "expiresMinutes": "{{minutes}} 分钟", + "diff": { + "label": "对 {{target}} 的更改", + "newFile": "新文件", + "added_one": "新增 {{count}} 行", + "added_other": "新增 {{count}} 行", + "removed_one": "删除 {{count}} 行", + "removed_other": "删除 {{count}} 行", + "same": "与当前文本没有差异。", + "gap_one": "{{count}} 行未变", + "gap_other": "{{count}} 行未变", + "more_one": "另有 {{count}} 行未显示", + "more_other": "另有 {{count}} 行未显示", + "srAdded": "新增:", + "srRemoved": "删除:" + }, + "unreadFile": "无法读取当前文件,因此全部文本都视为新内容。", + "confirm": "确认", + "edit": "编辑后确认", + "confirmEdited": "用我的文本确认", + "editLabel": "保存到 {{target}} 的文本", + "failed": "无法保存更改:{{message}}。它仍在等待你处理。", + "receipt": { + "applied": "已应用:", + "reverted": "已撤销:", + "dismissed": "已拒绝:", + "expired": "已过期:", + "stale": "未应用:" + }, + "why": { + "expired": "已等待超过一天,未保存任何内容。", + "stale": "文件在提议之后发生了变化,因此未保存任何内容。" + }, + "askAgain": "重新请求", + "askAgainText": "请重新提议这项更改,文件此后已变化:{{summary}}({{target}})", + "announce": { + "applied": "更改已应用。", + "dismissed": "更改已拒绝。", + "undone": "更改已撤销。", + "asked": "已请助手重新提议。" + } + }, + "proposalsPage": { + "title": "提议", + "supporting": "模型提出、等待你回应的更改。", + "emptyTitle": "没有待处理的内容", + "emptyBody": "当模型提议更改你的记忆或智能体配置文件时,会显示在这里,直到你作出回应。", + "loadError": "无法加载提议。" + }, + "memoryPage": { + "lede": "LocalAI 放入你聊天提示词中的笔记。只有在你确认提议后,模型才会修改它们。你可以在这里查看、编辑和删除。", + "limits": "上限:索引最多 {{lines}} 行和 {{index}},配置文件 {{profile}},每轮对话最多携带 {{turn}} 的笔记。", + "proposalsLink": "查看等待处理的更改", + "emptyTitle": "还没有笔记", + "emptyBody": "当你让助手记住某事并确认后,笔记会显示在这里。", + "loadError": "无法加载你的笔记。", + "use": "{{size}} / {{max}} · {{lines}} / {{maxLines}} 行", + "useBytes": "{{size}} / {{max}}", + "useLabel": "索引大小与上限", + "over": "超出上限:末尾部分不会发送给模型。", + "index": "索引", + "group": { + "user": "你的笔记", + "agent": "智能体 {{key}} 的笔记", + "channel": "对话 {{key}} 的笔记", + "profile": "智能体 {{key}} 的配置文件" + }, + "edit": "编辑", + "save": "保存", + "editLabel": "{{name}} 的文本", + "editSizeIndex": "{{size}} / {{max}} · {{lines}} / {{maxLines}} 行", + "editSize": "{{size}} / {{max}}", + "tooBig": "太长:请缩短后再保存。", + "saveFailed": "无法保存:{{message}}", + "saved": "已保存 {{name}}。", + "emptyFile": "(空)", + "delete": "删除 {{name}}", + "deleteYes": "删除", + "confirmDelete": "删除 {{name}}?", + "deleted": "已将 {{name}} 移到回收站。", + "clear": "清除所有笔记", + "clearAsk_one": "删除 {{count}} 个文件?文件会移到回收站,可在“文件”中恢复。", + "clearAsk_other": "删除 {{count}} 个文件?文件会移到回收站,可在“文件”中恢复。", + "clearYes_one": "删除 {{count}} 个文件", + "clearYes_other": "删除 {{count}} 个文件", + "cleared_one": "已将 {{count}} 个文件移到回收站。", + "cleared_other": "已将 {{count}} 个文件移到回收站。" } } diff --git a/core/http/react-ui/src/components/ChatInbox.jsx b/core/http/react-ui/src/components/ChatInbox.jsx index 99a22eae89..3fc65dde9f 100644 --- a/core/http/react-ui/src/components/ChatInbox.jsx +++ b/core/http/react-ui/src/components/ChatInbox.jsx @@ -2,6 +2,7 @@ import { useCallback, useEffect, useId, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { useNavigate } from 'react-router-dom' +import { usePendingChanges } from '../hooks/usePendingChanges' import { disablePush, enablePush, pushState } from '../utils/chatPush' import { relativeDate } from '../utils/fileBrowser' import { @@ -130,6 +131,7 @@ export default function ChatInbox({ inbox }) { const [busy, setBusy] = useState(false) const [fresh, setFresh] = useState(() => new Set()) const [announce, setAnnounce] = useState('') + const waiting = usePendingChanges() const ref = useRef(null) const btnRef = useRef(null) const panelRef = useRef(null) @@ -169,6 +171,7 @@ export default function ChatInbox({ inbox }) { useEffect(() => { if (!open) { setConfirm(''); setDone(''); setError(''); return } inbox.refresh() + waiting.refresh() panelRef.current?.focus({ preventScroll: true }) // eslint-disable-next-line react-hooks/exhaustive-deps -- once per opening }, [open]) @@ -239,8 +242,11 @@ export default function ChatInbox({ inbox }) { } } - const badge = badgeText(inbox.unread) - const buttonName = inbox.unread > 0 ? t('inbox.buttonUnread', { count: inbox.unread }) : t('inbox.title') + // The badge counts what needs a look: unread notifications and the changes + // that wait for an answer. + const attention = inbox.unread + waiting.count + const badge = badgeText(attention) + const buttonName = attention > 0 ? t('inbox.buttonUnread', { count: attention }) : t('inbox.title') const filters = [ { id: 'all', label: t('inbox.filterAll') }, { id: 'unread', label: t('inbox.filterUnread') }, @@ -340,6 +346,19 @@ export default function ChatInbox({ inbox }) { )}
+ {waiting.count > 0 && ( + + )} {done &&

{done}

} {error &&

{error}

} {failedLoad && ( diff --git a/core/http/react-ui/src/components/chat/ActivityFold.jsx b/core/http/react-ui/src/components/chat/ActivityFold.jsx index d2a8347f89..8bf0ce55fd 100644 --- a/core/http/react-ui/src/components/chat/ActivityFold.jsx +++ b/core/http/react-ui/src/components/chat/ActivityFold.jsx @@ -4,6 +4,8 @@ import { renderMarkdown, highlightAll } from '../../utils/markdown' import { toolLine, toolOutput } from '../../utils/chatThread' import { addonChange, toolRefusal } from '../../utils/addonChange' import { CHANGE_CARD, triggerCards } from '../../utils/chatProposal' +import { changeProposalIds } from '../../utils/changeProposal' +import ChangeProposalCard from './ChangeProposalCard' import { toolLabel, formatDuration, runTimes } from '../../utils/toolLabels' import Icon from '../Icon' import { openChatFile } from './chatActions' @@ -357,9 +359,13 @@ function NetworkCards({ items }) { // ends; a run with no message after it (ActivityRow) draws them with the fold. export function ActivityProposals({ items, proposalTick = 0 }) { const { t: tc } = useTranslation('chats') - return triggerCards(items).map(({ kind, id }) => (kind === CHANGE_CARD - ? - : )) + return [ + ...triggerCards(items).map(({ kind, id }) => (kind === CHANGE_CARD + ? + : )), + // A change a model proposed to a memory note, a profile or another kind. + ...changeProposalIds(items).map((id) => ), + ] } // A tool call that has an MCP App: the tool line, then the app in its frame. diff --git a/core/http/react-ui/src/components/chat/ChangeProposalCard.jsx b/core/http/react-ui/src/components/chat/ChangeProposalCard.jsx new file mode 100644 index 0000000000..840c906cfe --- /dev/null +++ b/core/http/react-ui/src/components/chat/ChangeProposalCard.jsx @@ -0,0 +1,263 @@ +/* eslint-disable no-unused-vars -- components used only inside JSX look unused to this config, which has no eslint-plugin-react */ +import { useCallback, useContext, useEffect, useId, useMemo, useState } from 'react' +import { Link } from 'react-router-dom' +import { chatChangesApi, filesApi } from '../../utils/api' +import { + byteText, CHANGES_EVENT, changeProposalView, KNOWN_KINDS, KNOWN_SCOPES, scopeOfProposal, sourceOfProposal, targetPath, timeLeft, +} from '../../utils/changeProposal' +import { diffLines, diffStats, withContext } from '../../utils/lineDiff' +import Icon from '../Icon' +import { Button } from '../ui' +import { ChatSendContext } from './chatSendContext' +import './changeproposal.css' + + +// Past this many shown lines a diff stops and says how many more there are. +const MAX_SHOWN = 120 +// The most of the current file the card reads to draw the diff. +const MAX_READ = 1 << 20 + +const announceChanged = () => { + if (typeof window !== 'undefined') window.dispatchEvent(new CustomEvent(CHANGES_EVENT)) +} + +function kindName(p, t) { + return KNOWN_KINDS.includes(p.kind) ? t(`changes.kind.${p.kind}`) : String(p.kind || '') +} + +function scopeName(p, t) { + const { scope, agent } = scopeOfProposal(p) + if (!KNOWN_SCOPES.includes(scope)) return String(scope) + if (scope === 'agent' && !agent) return t('changes.scope.agentAny') + return t(`changes.scope.${scope}`, { agent }) +} + +function sourceName(p, t) { + const s = sourceOfProposal(p) + if (s.kind === 'chat') return t('changes.source.chat') + if (s.sender) return t('changes.source.connectorFrom', { name: s.id || s.kind, sender: s.sender }) + return t('changes.source.connector', { name: s.id || s.kind }) +} + +function expiryName(p, t) { + const left = timeLeft(p) + if (!left) return '' + return left.hours > 0 ? t('changes.expiresHours', { hours: left.hours, minutes: left.minutes }) : t('changes.expiresMinutes', { minutes: left.minutes }) +} + +// The lines a proposal adds and removes, with a few unchanged lines around +// them. Every row says what it is in words for a screen reader; the sign in +// the margin is for the eye. A file the card could not read is shown as all +// new text. +function Diff({ before, after, target, t }) { + const ops = useMemo(() => diffLines(before ?? '', after ?? ''), [before, after]) + const { added, removed } = diffStats(ops) + const rows = useMemo(() => withContext(ops, 2), [ops]) + const shown = rows.slice(0, MAX_SHOWN) + return ( +
+

+ {before === '' ? {t('changes.diff.newFile')} · : null} + {t('changes.diff.added', { count: added })} + {' '} + {t('changes.diff.removed', { count: removed })} +

+ {added + removed === 0 ?

{t('changes.diff.same')}

: ( +
    + {shown.map((op, i) => (op.type === 'gap' ? ( +
  • {t('changes.diff.gap', { count: op.count })}
  • + ) : ( +
  • + + {op.type !== 'same' && {t(op.type === 'add' ? 'changes.diff.srAdded' : 'changes.diff.srRemoved')}} + {op.text || ' '} +
  • + )))} + {rows.length > shown.length &&
  • {t('changes.diff.more', { count: rows.length - shown.length })}
  • } +
+ )} +
+ ) +} + +// A change that a model proposed to something that lasts (a memory note, an +// agent profile, and the kinds that come later), drawn for any kind and any +// source: a tool line whose card is of kind change_proposal (card.id is the +// proposal's id), or a row of the Proposals page. It reads the proposal from +// the server, so it says the same after a reload and on every frontend: +// waiting (Confirm, Edit then confirm, Dismiss), then a receipt: applied (with +// Undo), undone, dismissed, expired or stale. Nothing is written until the +// person confirms. +export default function ChangeProposalCard({ id, initial, t, tick = 0 }) { + const uid = useId() + const sendToChat = useContext(ChatSendContext) + const [view, setView] = useState(() => (initial ? changeProposalView(initial) : { kind: 'loading' })) + // The current text of the target: '' for a file that is not there, null when + // it could not be read. + const [current, setCurrent] = useState(undefined) + const [edit, setEdit] = useState(null) + const [busy, setBusy] = useState('') + const [error, setError] = useState('') + const [said, setSaid] = useState('') + + const load = useCallback(async () => { + let v + try { + v = changeProposalView(await chatChangesApi.get(id)) + } catch (e) { + v = changeProposalView(null, e) + } + setView(v) + if (v.kind === 'pending') { + try { + setCurrent(await filesApi.readText(targetPath(v.proposal), MAX_READ)) + } catch (e) { + setCurrent(e?.status === 404 ? '' : null) + } + } + return v + }, [id]) + useEffect(() => { load() }, [load, tick]) + + const act = async (what, call, say) => { + setBusy(what) + setError('') + let failure = null + try { + await call() + setEdit(null) + if (say) setSaid(say) + } catch (e) { + failure = e + setSaid('') + } + const v = await load() + // A 409 means the proposal moved on (stale, expired, answered elsewhere): + // the card then says so in its own words. Any other failure leaves the + // proposal waiting, with the reason; a write that failed is one of them. + if (failure && !(failure.status === 409 && v.kind !== 'pending' && v.kind !== 'applied')) { + const reason = failure.message.replace(/^studio: /, '').replace(/[.\s]+$/, '') + setError(failure.status === 409 ? reason : t('changes.failed', { message: reason })) + } + setBusy('') + announceChanged() + } + + const status = {said} + const wrap = (node) => <>{node}{status} + const alert = error &&

{error}

+ const tid = 'change-proposal' + + if (view.kind === 'loading') return wrap(
{t('changes.loading')}
) + if (view.kind === 'error') { + return wrap( +
+ {t('changes.unread', { message: view.message })}{' '} + +
+ ) + } + if (view.kind === 'removed') return wrap(
{t('changes.removed')}
) + + const p = view.proposal + const target = p.target || '' + const meta = [ + ['kind', t('changes.row.kind'), `${kindName(p, t)} · ${scopeName(p, t)}`], + ['target', t('changes.row.target'), {target}], + ['source', t('changes.row.source'), sourceName(p, t)], + ] + + if (view.kind === 'applied') { + return wrap( +
+ +
+

{t('changes.receipt.applied')} {p.summary}

+

{kindName(p, t)} · {scopeName(p, t)}{target}

+ {alert} +
+ + {view.canUndo && ( + + )} + +
+ ) + } + + if (view.kind !== 'pending') { + // Quiet receipts: reverted, dismissed, expired and stale. The last two + // offer to ask the assistant again, where there is a chat to ask. + const again = (view.kind === 'expired' || view.kind === 'stale') && sendToChat + return wrap( +
+
+

{t(`changes.receipt.${view.kind}`)} {p.summary}

+ {(view.kind === 'expired' || view.kind === 'stale') &&

{t(`changes.why.${view.kind}`)}

} + {alert} +
+ {again && ( + + + + )} +
+ ) + } + + // Waiting for an answer. + const expiry = expiryName(p, t) + return wrap( +
+
+ {p.summary} +
+

{t('changes.help')}

+
+ {meta.map(([key, label, value]) => ( +
{label}
{value}
+ ))} + {expiry &&
{t('changes.row.expires')}
{expiry}
} +
+ {current === undefined ? null : (edit === null + ? + : null)} + {current === null &&

{t('changes.unreadFile')}

} + {edit !== null ? ( +
{ e.preventDefault(); act('confirm', () => chatChangesApi.confirm(p.id, edit), t('changes.announce.applied')) }} + onKeyDown={(e) => { if (e.key === 'Escape') { e.stopPropagation(); setEdit(null) } }} + > + +