diff --git a/core/http/react-ui/public/locales/de/auth.json b/core/http/react-ui/public/locales/de/auth.json index f0232dda09..d1b1121932 100644 --- a/core/http/react-ui/public/locales/de/auth.json +++ b/core/http/react-ui/public/locales/de/auth.json @@ -55,7 +55,8 @@ "security": "Sicherheit", "apiKeys": "API-Schlüssel", "about": "Über dich und den Assistenten", - "appearance": "Darstellung" + "appearance": "Darstellung", + "memory": "Gedächtnis" }, "profile": { "displayName": "Anzeigename", diff --git a/core/http/react-ui/public/locales/en/auth.json b/core/http/react-ui/public/locales/en/auth.json index 68594b46f2..705cbe7235 100644 --- a/core/http/react-ui/public/locales/en/auth.json +++ b/core/http/react-ui/public/locales/en/auth.json @@ -80,7 +80,8 @@ "apiKeys": "API keys", "usage": "Usage", "about": "About you and the assistant", - "appearance": "Appearance" + "appearance": "Appearance", + "memory": "Memory" }, "profile": { "displayName": "Display name", diff --git a/core/http/react-ui/public/locales/es/auth.json b/core/http/react-ui/public/locales/es/auth.json index 0c799e9388..6449180f06 100644 --- a/core/http/react-ui/public/locales/es/auth.json +++ b/core/http/react-ui/public/locales/es/auth.json @@ -55,7 +55,8 @@ "security": "Seguridad", "apiKeys": "Claves API", "about": "Sobre ti y el asistente", - "appearance": "Apariencia" + "appearance": "Apariencia", + "memory": "Memoria" }, "profile": { "displayName": "Nombre para mostrar", diff --git a/core/http/react-ui/public/locales/fr/auth.json b/core/http/react-ui/public/locales/fr/auth.json index 596540ebf1..0ffffdf8df 100644 --- a/core/http/react-ui/public/locales/fr/auth.json +++ b/core/http/react-ui/public/locales/fr/auth.json @@ -55,7 +55,8 @@ "security": "Sécurité", "apiKeys": "Clés d'API", "about": "À propos de vous et de l'assistant", - "appearance": "Apparence" + "appearance": "Apparence", + "memory": "Mémoire" }, "profile": { "displayName": "Nom affiché", diff --git a/core/http/react-ui/public/locales/id/auth.json b/core/http/react-ui/public/locales/id/auth.json index 9d1c5752e1..e936a88c91 100644 --- a/core/http/react-ui/public/locales/id/auth.json +++ b/core/http/react-ui/public/locales/id/auth.json @@ -55,7 +55,8 @@ "security": "Keamanan", "apiKeys": "API Key", "about": "Tentang Anda dan asisten", - "appearance": "Tampilan" + "appearance": "Tampilan", + "memory": "Memori" }, "profile": { "displayName": "Nama tampilan", diff --git a/core/http/react-ui/public/locales/it/auth.json b/core/http/react-ui/public/locales/it/auth.json index fa24696154..2ad1a4f074 100644 --- a/core/http/react-ui/public/locales/it/auth.json +++ b/core/http/react-ui/public/locales/it/auth.json @@ -55,7 +55,8 @@ "security": "Sicurezza", "apiKeys": "Chiavi API", "about": "Su di te e sull'assistente", - "appearance": "Aspetto" + "appearance": "Aspetto", + "memory": "Memoria" }, "profile": { "displayName": "Nome visualizzato", diff --git a/core/http/react-ui/public/locales/ko/auth.json b/core/http/react-ui/public/locales/ko/auth.json index 37bac414f4..8d7d7f97fd 100644 --- a/core/http/react-ui/public/locales/ko/auth.json +++ b/core/http/react-ui/public/locales/ko/auth.json @@ -55,7 +55,8 @@ "security": "보안", "apiKeys": "API 키", "about": "나와 어시스턴트에 대하여", - "appearance": "모양" + "appearance": "모양", + "memory": "메모리" }, "profile": { "displayName": "표시 이름", diff --git a/core/http/react-ui/public/locales/pt-BR/auth.json b/core/http/react-ui/public/locales/pt-BR/auth.json index 6988bf7536..be78eb1af3 100644 --- a/core/http/react-ui/public/locales/pt-BR/auth.json +++ b/core/http/react-ui/public/locales/pt-BR/auth.json @@ -55,7 +55,8 @@ "security": "Segurança", "apiKeys": "Chaves de API", "about": "Sobre você e o assistente", - "appearance": "Aparência" + "appearance": "Aparência", + "memory": "Memória" }, "profile": { "displayName": "Nome de exibição", diff --git a/core/http/react-ui/public/locales/zh-CN/auth.json b/core/http/react-ui/public/locales/zh-CN/auth.json index 21a60bf8dc..6c6a4e10a7 100644 --- a/core/http/react-ui/public/locales/zh-CN/auth.json +++ b/core/http/react-ui/public/locales/zh-CN/auth.json @@ -55,7 +55,8 @@ "security": "安全", "apiKeys": "API 密钥", "about": "关于您和助手", - "appearance": "外观" + "appearance": "外观", + "memory": "记忆" }, "profile": { "displayName": "显示名称", diff --git a/core/http/react-ui/src/components/MemoryTab.jsx b/core/http/react-ui/src/components/MemoryTab.jsx new file mode 100644 index 0000000000..c4f1b9e54e --- /dev/null +++ b/core/http/react-ui/src/components/MemoryTab.jsx @@ -0,0 +1,212 @@ +/* eslint-disable no-unused-vars -- components used only inside JSX look unused to this config, which has no eslint-plugin-react */ +import { useCallback, useEffect, useId, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { Link } from 'react-router-dom' +import { chatChangesApi, filesApi } from '../utils/api' +import { byteText } from '../utils/changeProposal' +import { allFiles, groupId, groupKind, indexFits, indexUse } from '../utils/memoryFiles' +import Icon from './Icon' +import { Button, Callout, EmptyState, LoadError, SkeletonRows } from './ui' +import './memorytab.css' + +const READ_LIMIT = 64 * 1024 +const path = (p) => (p.startsWith('/') ? p : `/${p}`) + +// The size of an index against its caps, as a bar and words. The bar is a +// progress element, so it is read as "x of y" without a style. +function Use({ group, t }) { + const u = indexUse(group) + if (!u.index || !u.maxBytes) return null + const words = t(u.maxLines ? 'memoryPage.use' : 'memoryPage.useBytes', { size: byteText(u.bytes), max: byteText(u.maxBytes), lines: u.lines, maxLines: u.maxLines }) + return ( +

+ + {words} + {u.over && {' '}{t('memoryPage.over')}} +

+ ) +} + +// One file of a group: its name and size, and a panel that shows the text and +// switches to an editor. The owner writes straight into their own files here; +// the host keeps every write as a change set, and a delete goes to the Trash. +function FileRow({ file, group, t, busy, onDelete, onSaved, setError }) { + const uid = useId() + const [open, setOpen] = useState(false) + const [text, setText] = useState(null) + const [edit, setEdit] = useState(null) + const [saving, setSaving] = useState(false) + const [ask, setAsk] = useState(false) + + const toggle = async () => { + if (open) { setOpen(false); setEdit(null); return } + setOpen(true) + if (text === null) { + try { setText(await filesApi.readText(path(file.path), READ_LIMIT)) } catch (e) { setText(''); setError(e.message) } + } + } + const fit = file.index && edit !== null ? indexFits(group, edit) : null + const save = async () => { + setSaving(true) + setError('') + try { + await filesApi.uploadWithProgress(path(file.path), new Blob([edit], { type: 'text/markdown' }), { replace: true }) + setText(edit) + setEdit(null) + onSaved(file) + } catch (e) { + setError(t('memoryPage.saveFailed', { message: e.message })) + } finally { + setSaving(false) + } + } + + return ( +
  • +
    + + {byteText(file.size)} + {ask ? ( + + + + + ) : ( + +
    + {open && ( +
    + {text === null ? : edit === null ? ( + <> +
    {text || t('memoryPage.emptyFile')}
    + + + ) : ( +
    { e.preventDefault(); if (!fit || fit.fits) save() }} onKeyDown={(e) => { if (e.key === 'Escape') { e.stopPropagation(); setEdit(null) } }}> + +