mirror of
https://github.com/mudler/LocalAI.git
synced 2026-11-05 20:13:23 -05:00
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 <mudler@localai.io>
This commit is contained in:
20 files changed
+1551
-14
No files matched your search
@@ -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."
|
||||
}
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
@@ -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}}개를 휴지통으로 옮겼습니다."
|
||||
}
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
@@ -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}} 个文件移到回收站。"
|
||||
}
|
||||
}
|
||||
@@ -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 }) {
|
||||
</Callout>
|
||||
)}
|
||||
<div className="nx-body">
|
||||
{waiting.count > 0 && (
|
||||
<button
|
||||
type="button" className="nx-waiting" data-testid="inbox-proposals"
|
||||
onClick={() => { setOpen(false); navigate('/app/chats/proposals') }}
|
||||
>
|
||||
<span className="nx-tile nx-tile--accent" aria-hidden="true"><Icon name="edit" /></span>
|
||||
<span className="nx-waiting__text">
|
||||
<span className="nx-item__title">{t('inbox.proposalsWaiting', { count: waiting.count })}</span>
|
||||
<span className="nx-item__body">{t('inbox.proposalsBody')}</span>
|
||||
</span>
|
||||
<Icon name="arrow-right" className="nx-item__go" />
|
||||
</button>
|
||||
)}
|
||||
{done && <p className="nx-note nx-note--ok" role="status" data-testid="inbox-done"><Icon name="check-circle" />{done}</p>}
|
||||
{error && <p className="nx-note nx-note--error" role="alert"><Icon name="alert-circle" />{error}</p>}
|
||||
{failedLoad && (
|
||||
|
||||
@@ -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
|
||||
? <TriggerChangeCard key={`tc-${id}`} id={id} t={tc} tick={proposalTick} />
|
||||
: <ProposalCard key={`p-${id}`} id={id} t={tc} tick={proposalTick} />))
|
||||
return [
|
||||
...triggerCards(items).map(({ kind, id }) => (kind === CHANGE_CARD
|
||||
? <TriggerChangeCard key={`tc-${id}`} id={id} t={tc} tick={proposalTick} />
|
||||
: <ProposalCard key={`p-${id}`} id={id} t={tc} tick={proposalTick} />)),
|
||||
// A change a model proposed to a memory note, a profile or another kind.
|
||||
...changeProposalIds(items).map((id) => <ChangeProposalCard key={`cp-${id}`} id={id} t={tc} tick={proposalTick} />),
|
||||
]
|
||||
}
|
||||
|
||||
// A tool call that has an MCP App: the tool line, then the app in its frame.
|
||||
|
||||
@@ -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 (
|
||||
<div className="cp-diff" role="group" aria-label={t('changes.diff.label', { target })} data-testid="change-diff">
|
||||
<p className="cp-diff__stat" data-testid="change-diff-stat">
|
||||
{before === '' ? <span>{t('changes.diff.newFile')} · </span> : null}
|
||||
<span className="cp-diff__n cp-diff__n--add">{t('changes.diff.added', { count: added })}</span>
|
||||
{' '}
|
||||
<span className="cp-diff__n cp-diff__n--del">{t('changes.diff.removed', { count: removed })}</span>
|
||||
</p>
|
||||
{added + removed === 0 ? <p className="cx-help">{t('changes.diff.same')}</p> : (
|
||||
<ul className="cp-diff__lines">
|
||||
{shown.map((op, i) => (op.type === 'gap' ? (
|
||||
<li key={i} className="cp-diff__gap" data-op="gap">{t('changes.diff.gap', { count: op.count })}</li>
|
||||
) : (
|
||||
<li key={i} className="cp-diff__line" data-op={op.type}>
|
||||
<span className="cp-diff__mark" aria-hidden="true">{op.type === 'add' ? '+' : op.type === 'del' ? '−' : ''}</span>
|
||||
{op.type !== 'same' && <span className="sr-only">{t(op.type === 'add' ? 'changes.diff.srAdded' : 'changes.diff.srRemoved')}</span>}
|
||||
<code className="cp-diff__text">{op.text || ' '}</code>
|
||||
</li>
|
||||
)))}
|
||||
{rows.length > shown.length && <li className="cp-diff__gap" data-op="gap">{t('changes.diff.more', { count: rows.length - shown.length })}</li>}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// 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 = <span className="sr-only" role="status" aria-live="polite">{said}</span>
|
||||
const wrap = (node) => <>{node}{status}</>
|
||||
const alert = error && <p className="cx-help cx-help--err" role="alert" data-testid="change-error">{error}</p>
|
||||
const tid = 'change-proposal'
|
||||
|
||||
if (view.kind === 'loading') return wrap(<div className="cx-change" data-testid={tid} data-state="loading">{t('changes.loading')}</div>)
|
||||
if (view.kind === 'error') {
|
||||
return wrap(
|
||||
<div className="cx-change" data-testid={tid} data-state="error">
|
||||
{t('changes.unread', { message: view.message })}{' '}
|
||||
<Button size="sm" variant="ghost" onClick={load}>{t('proposal.retry')}</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
if (view.kind === 'removed') return wrap(<div className="cx-change" data-testid={tid} data-state="removed">{t('changes.removed')}</div>)
|
||||
|
||||
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'), <code key="c">{target}</code>],
|
||||
['source', t('changes.row.source'), sourceName(p, t)],
|
||||
]
|
||||
|
||||
if (view.kind === 'applied') {
|
||||
return wrap(
|
||||
<div className="cx-receipt" data-testid={tid} data-state="applied" data-kind={p.kind}>
|
||||
<Icon name="check-circle" className="cx-receipt__icon" />
|
||||
<div className="cx-receipt__main">
|
||||
<p className="cx-receipt__line"><b>{t('changes.receipt.applied')}</b> <span data-testid="change-summary">{p.summary}</span></p>
|
||||
<p className="cx-receipt__sub"><span>{kindName(p, t)} · {scopeName(p, t)}</span><code>{target}</code></p>
|
||||
{alert}
|
||||
</div>
|
||||
<span className="cx-receipt__acts">
|
||||
{view.canUndo && (
|
||||
<Button size="sm" variant="ghost" icon="undo" busy={busy === 'undo'} disabled={!!busy}
|
||||
onClick={() => act('undo', () => chatChangesApi.undo(p.id), t('changes.announce.undone'))}
|
||||
>{t('proposal.receipt.undo')}</Button>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
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(
|
||||
<div className="cx-receipt cx-receipt--quiet" data-testid={tid} data-state={view.kind} data-kind={p.kind}>
|
||||
<div className="cx-receipt__main">
|
||||
<p className="cx-receipt__line"><b>{t(`changes.receipt.${view.kind}`)}</b> <span data-testid="change-summary">{p.summary}</span></p>
|
||||
{(view.kind === 'expired' || view.kind === 'stale') && <p className="cx-help" data-testid="change-why">{t(`changes.why.${view.kind}`)}</p>}
|
||||
{alert}
|
||||
</div>
|
||||
{again && (
|
||||
<span className="cx-receipt__acts">
|
||||
<Button size="sm" variant="secondary" busy={busy === 'again'} disabled={!!busy}
|
||||
onClick={() => act('again', async () => { await sendToChat(t('changes.askAgainText', { summary: p.summary, target })) }, t('changes.announce.asked'))}
|
||||
>{t('changes.askAgain')}</Button>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Waiting for an answer.
|
||||
const expiry = expiryName(p, t)
|
||||
return wrap(
|
||||
<div className="cx-proposal cp-card" data-testid={tid} data-state="pending" data-kind={p.kind} aria-labelledby={`${uid}-h`}>
|
||||
<div className="cx-proposal__head">
|
||||
<b id={`${uid}-h`} data-testid="change-summary">{p.summary}</b>
|
||||
</div>
|
||||
<p className="cx-help">{t('changes.help')}</p>
|
||||
<dl className="cx-plan" data-testid="change-rows">
|
||||
{meta.map(([key, label, value]) => (
|
||||
<div className="cx-plan__row" key={key} data-row={key}><dt>{label}</dt><dd>{value}</dd></div>
|
||||
))}
|
||||
{expiry && <div className="cx-plan__row" data-row="expires"><dt>{t('changes.row.expires')}</dt><dd>{expiry}</dd></div>}
|
||||
</dl>
|
||||
{current === undefined ? null : (edit === null
|
||||
? <Diff before={current ?? ''} after={p.content || ''} target={target} t={t} />
|
||||
: null)}
|
||||
{current === null && <p className="cx-help">{t('changes.unreadFile')}</p>}
|
||||
{edit !== null ? (
|
||||
<form
|
||||
className="cx-proposal__edit cp-edit"
|
||||
onSubmit={(e) => { e.preventDefault(); act('confirm', () => chatChangesApi.confirm(p.id, edit), t('changes.announce.applied')) }}
|
||||
onKeyDown={(e) => { if (e.key === 'Escape') { e.stopPropagation(); setEdit(null) } }}
|
||||
>
|
||||
<label className="cx-field__label" htmlFor={`${uid}-text`}>{t('changes.editLabel', { target })}</label>
|
||||
<textarea
|
||||
id={`${uid}-text`} className="cx-in cp-edit__text" rows={Math.min(14, Math.max(5, edit.split('\n').length + 1))}
|
||||
value={edit} spellCheck={false} autoFocus data-testid="change-edit-text"
|
||||
aria-describedby={`${uid}-size`}
|
||||
onChange={(e) => setEdit(e.target.value)}
|
||||
/>
|
||||
<p className="cx-help" id={`${uid}-size`}>{byteText(new TextEncoder().encode(edit).length)}</p>
|
||||
<span className="cx-pending__acts">
|
||||
<button type="submit" className="cx-btn cx-btn--primary" disabled={!!busy}>{t('changes.confirmEdited')}</button>
|
||||
<button type="button" className="cx-btn" onClick={() => setEdit(null)}>{t('proposal.cancelEdit')}</button>
|
||||
</span>
|
||||
</form>
|
||||
) : (
|
||||
<span className="cx-pending__acts">
|
||||
<button type="button" className="cx-btn cx-btn--primary" disabled={!!busy}
|
||||
onClick={() => act('confirm', () => chatChangesApi.confirm(p.id), t('changes.announce.applied'))}
|
||||
>{t('changes.confirm')}</button>
|
||||
<button type="button" className="cx-btn" disabled={!!busy} onClick={() => setEdit(p.content || '')}>{t('changes.edit')}</button>
|
||||
<button type="button" className="cx-btn" disabled={!!busy}
|
||||
onClick={() => act('dismiss', () => chatChangesApi.dismiss(p.id), t('changes.announce.dismissed'))}
|
||||
>{t('proposal.dismiss')}</button>
|
||||
</span>
|
||||
)}
|
||||
{alert}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/* The card of a change a model proposed: a line diff of what would be written.
|
||||
It sits in the frame of the proposal card (.cx-proposal in pages/chats.css)
|
||||
and uses only design tokens. */
|
||||
.cp-diff { margin: 0 0 12px; }
|
||||
.cp-diff__stat { margin: 0 0 6px; font-size: var(--text-xs); color: var(--dk-muted); }
|
||||
.cp-diff__n--add { color: var(--dk-ok); font-weight: 600; }
|
||||
.cp-diff__n--del { color: var(--dk-error); font-weight: 600; }
|
||||
.cp-diff__lines {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
max-height: 280px;
|
||||
overflow: auto;
|
||||
border-radius: var(--dk-radius-2);
|
||||
box-shadow: inset 0 0 0 1px var(--dk-edge);
|
||||
font-family: var(--font-mono);
|
||||
font-size: var(--text-xs);
|
||||
line-height: 1.5;
|
||||
}
|
||||
.cp-diff__line { display: flex; gap: 8px; padding: 1px 10px 1px 6px; }
|
||||
.cp-diff__line[data-op="add"] { background: var(--dk-ok-wash); }
|
||||
.cp-diff__line[data-op="del"] { background: var(--dk-error-wash); }
|
||||
.cp-diff__mark { flex: none; width: 1ch; text-align: center; color: var(--dk-muted); user-select: none; }
|
||||
.cp-diff__line[data-op="add"] .cp-diff__mark { color: var(--dk-ok); }
|
||||
.cp-diff__line[data-op="del"] .cp-diff__mark { color: var(--dk-error); }
|
||||
.cp-diff__text { min-width: 0; white-space: pre-wrap; overflow-wrap: anywhere; color: var(--dk-text); font: inherit; }
|
||||
.cp-diff__line[data-op="same"] .cp-diff__text { color: var(--dk-muted); }
|
||||
.cp-diff__gap { padding: 2px 10px; color: var(--dk-muted); font-style: italic; border-top: 1px dashed var(--dk-edge); border-bottom: 1px dashed var(--dk-edge); }
|
||||
.cp-edit__text { width: 100%; min-height: 96px; font-family: var(--font-mono); font-size: var(--text-xs); resize: vertical; }
|
||||
|
||||
/* The Proposals page: one card for each change that waits. */
|
||||
.pp-list { margin: 0; padding: 0; list-style: none; display: grid; gap: 12px; max-width: 720px; }
|
||||
.pp-item .cx-proposal,
|
||||
.pp-item .cx-receipt { margin-top: 0; }
|
||||
@@ -0,0 +1,5 @@
|
||||
import { createContext } from 'react'
|
||||
|
||||
// How a card in the thread sends a message to the open chat, for the cards
|
||||
// that offer to ask the assistant again. null outside the chat page.
|
||||
export const ChatSendContext = createContext(null)
|
||||
@@ -152,6 +152,27 @@
|
||||
.nx-item__main:focus { outline: none; }
|
||||
.nx-item__main:focus-visible { box-shadow: inset 0 0 0 2px var(--focus-ring-color); }
|
||||
|
||||
.nx-waiting {
|
||||
display: grid;
|
||||
grid-template-columns: 32px minmax(0, 1fr) 16px;
|
||||
align-items: center;
|
||||
column-gap: var(--sp-3);
|
||||
width: 100%;
|
||||
padding: var(--sp-3) var(--sp-4);
|
||||
border: 0;
|
||||
border-bottom: 1px solid var(--color-border-subtle);
|
||||
background: var(--dk-accent-wash);
|
||||
color: inherit;
|
||||
font: inherit;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
.nx-waiting:focus { outline: none; }
|
||||
.nx-waiting:focus-visible { box-shadow: inset 0 0 0 2px var(--focus-ring-color); }
|
||||
.nx-waiting__text { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
|
||||
.nx-waiting .nx-item__title { color: var(--color-text-primary); font-weight: var(--font-weight-medium, 600); }
|
||||
.nx-waiting .nx-item__go { opacity: 1; }
|
||||
|
||||
.nx-tile {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { chatChangesApi } from '../utils/api'
|
||||
import { CHANGES_EVENT } from '../utils/changeProposal'
|
||||
|
||||
const POLL_MS = 60_000
|
||||
|
||||
// The change proposals that wait for the person: how many, for the badge of the
|
||||
// inbox and the Proposals page. It reads again when the tab is shown, every
|
||||
// minute, when any card answers one (CHANGES_EVENT) and on refresh(). A server
|
||||
// that keeps no proposals (files off, chats off) answers 404 or 409: the count
|
||||
// then stays zero and the hook stops asking.
|
||||
export function usePendingChanges() {
|
||||
const [items, setItems] = useState([])
|
||||
const [status, setStatus] = useState('loading')
|
||||
const [error, setError] = useState(null)
|
||||
const offRef = useRef(false)
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
if (offRef.current) return
|
||||
try {
|
||||
const list = await chatChangesApi.list('pending')
|
||||
setItems(Array.isArray(list) ? list : [])
|
||||
setStatus('ready')
|
||||
setError(null)
|
||||
} catch (e) {
|
||||
if (e?.status === 404 || e?.status === 409) { offRef.current = true; setItems([]); setStatus('ready'); return }
|
||||
setError(e)
|
||||
setStatus('error')
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
refresh()
|
||||
const timer = setInterval(refresh, POLL_MS)
|
||||
const onShow = () => { if (document.visibilityState === 'visible') refresh() }
|
||||
window.addEventListener(CHANGES_EVENT, refresh)
|
||||
document.addEventListener('visibilitychange', onShow)
|
||||
return () => {
|
||||
clearInterval(timer)
|
||||
window.removeEventListener(CHANGES_EVENT, refresh)
|
||||
document.removeEventListener('visibilitychange', onShow)
|
||||
}
|
||||
}, [refresh])
|
||||
|
||||
return { items, count: items.length, status, error, refresh }
|
||||
}
|
||||
@@ -72,6 +72,7 @@ import ConfirmDialog from '../components/ConfirmDialog'
|
||||
import ChatsMenu from '../components/ChatsMenu'
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import ChatInbox from '../components/ChatInbox'
|
||||
import { ChatSendContext } from '../components/chat/chatSendContext'
|
||||
import { useAuth } from '../context/AuthContext'
|
||||
import { useOperations } from '../hooks/useOperations'
|
||||
import { useLoadedModels } from '../hooks/useLoadedModels'
|
||||
@@ -1060,6 +1061,7 @@ function ChatPage() {
|
||||
{isEmpty && <EmptyHead name={activeId ? title : (pickedAgent?.name || '')} manage={manage} />}
|
||||
<div className="cx-thread" data-testid="chat-thread" role="region" aria-label={tc('transcript')} hidden={isEmpty}>
|
||||
<EgressCardsContext.Provider value={egressCards}>
|
||||
<ChatSendContext.Provider value={sendLive}>
|
||||
{rows.map((row, i) => {
|
||||
if (row.kind === 'activity') return <ActivityRow key={row.key} id={`cx-act-${row.key}`} items={row.items} proposalTick={chat.ticks.proposals} />
|
||||
if (row.kind === 'app') return <AppRow key={row.key} chatId={activeId} row={row} onSend={sendLive} />
|
||||
@@ -1083,6 +1085,7 @@ function ChatPage() {
|
||||
fileChipsFor(row),
|
||||
]
|
||||
})}
|
||||
</ChatSendContext.Provider>
|
||||
</EgressCardsContext.Provider>
|
||||
|
||||
{(chat.running || streaming.text || streaming.reasoning || streaming.tools.length > 0) && (
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
/* eslint-disable no-unused-vars -- components used only inside JSX look unused to this config, which has no eslint-plugin-react */
|
||||
import { useRef } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ChangeProposalCard from '../components/chat/ChangeProposalCard'
|
||||
import { EmptyState, LoadError, Page, PageHeader, SkeletonRows } from '../components/ui'
|
||||
import { usePendingChanges } from '../hooks/usePendingChanges'
|
||||
import './chats.css'
|
||||
|
||||
// The changes that models proposed and that wait for an answer, from every
|
||||
// chat and every connector, each as the card the chat draws. A card that was
|
||||
// answered stays until the list is read again, so the receipt can be read and
|
||||
// Undo used.
|
||||
export default function Proposals() {
|
||||
const { t } = useTranslation('chats')
|
||||
const pending = usePendingChanges()
|
||||
const { status, error, refresh } = pending
|
||||
// A card stays on the page after it is answered, with its receipt, until the
|
||||
// page is left; the list the server gives only holds the ones that wait.
|
||||
const seen = useRef(new Map())
|
||||
for (const p of pending.items) if (!seen.current.has(p.id)) seen.current.set(p.id, p)
|
||||
const items = Array.from(seen.current.values())
|
||||
return (
|
||||
<Page className="pp-page" data-testid="proposals-page">
|
||||
<PageHeader title={t('proposalsPage.title')} supporting={t('proposalsPage.supporting')} />
|
||||
{status === 'loading' && <SkeletonRows count={3} />}
|
||||
{status === 'error' && <LoadError title={t('proposalsPage.loadError')} error={error} onRetry={refresh} testId="proposals-load-error" />}
|
||||
{status === 'ready' && items.length === 0 && (
|
||||
<EmptyState variant="no-results" icon="check-circle" title={t('proposalsPage.emptyTitle')} body={t('proposalsPage.emptyBody')} testId="proposals-empty" />
|
||||
)}
|
||||
{items.length > 0 && (
|
||||
<ul className="pp-list" aria-label={t('proposalsPage.title')}>
|
||||
{items.map((p) => <li key={p.id} className="pp-item"><ChangeProposalCard id={p.id} initial={p} t={t} /></li>)}
|
||||
</ul>
|
||||
)}
|
||||
</Page>
|
||||
)
|
||||
}
|
||||
@@ -102,6 +102,7 @@ const AgentTasks = page('agent-tasks', () => import('./pages/AgentTasks'))
|
||||
const ChatsAdmin = page('chats-admin', () => import('./pages/ChatsAdmin'))
|
||||
const ChatConnectors = page('chats', () => import('./pages/ChatConnectors'))
|
||||
const ChatConversion = page('chats', () => import('./pages/ChatConversion'))
|
||||
const Proposals = page('chats', () => import('./pages/Proposals'))
|
||||
const Models = page('models', () => import('./pages/Models'))
|
||||
const ManageRedirect = page('manage', () => import('./pages/ManageRedirect'))
|
||||
// Referenced only from JSX below — same blind spot as Activity further down.
|
||||
@@ -255,6 +256,7 @@ const appChildren = [
|
||||
{ path: 'chats/triggers', element: <ChatsRedirect /> },
|
||||
{ path: 'chats/connectors', element: <Feature feature="connectors"><ChatConnectors /></Feature> },
|
||||
{ path: 'chats/conversion', element: <Feature feature="chats"><ChatConversion /></Feature> },
|
||||
{ path: 'chats/proposals', element: <Feature feature="chats"><Proposals /></Feature> },
|
||||
// One route for a new chat and an open one, so the page stays mounted when a
|
||||
// chat starts or is left (a delete waiting for its Undo survives the move).
|
||||
{ path: 'chats/all', element: <Feature feature="chats"><AllChats /></Feature> },
|
||||
|
||||
Vendored
+12
@@ -533,6 +533,18 @@ export const chatTriggersApi = {
|
||||
dismissChange: (id) => fetchJSON(`/api/chats/triggers/changes/${enc2(id)}`, { method: 'DELETE' }),
|
||||
}
|
||||
|
||||
// The changes a model proposed to your memory, profile and other files, and
|
||||
// the notes the host puts in the prompt. confirm may carry the person's own
|
||||
// text, which is written in place of the proposed one.
|
||||
export const chatChangesApi = {
|
||||
list: (status) => fetchJSON(`/api/chats/changes${q({ status })}`),
|
||||
get: (id) => fetchJSON(`/api/chats/changes/${enc2(id)}`),
|
||||
confirm: (id, content) => postJSON(`/api/chats/changes/${enc2(id)}/confirm`, content === undefined ? {} : { content }),
|
||||
dismiss: (id) => postJSON(`/api/chats/changes/${enc2(id)}/dismiss`, {}),
|
||||
undo: (id) => postJSON(`/api/chats/changes/${enc2(id)}/undo`, {}),
|
||||
contextFiles: () => fetchJSON('/api/chats/context-files'),
|
||||
}
|
||||
|
||||
// The inbox: what ended while the user was elsewhere.
|
||||
export const chatNotificationsApi = {
|
||||
list: (unread, limit = 50) => fetchJSON(`/api/chats/notifications${q({ unread: unread ? 1 : undefined, limit })}`),
|
||||
|
||||
Reference in new issue
Block a user