GUI: Memory management completes the management trio (/macros + /scheduler + /memory).
Tauri commands (crates/jarvis-gui/src/tauri_commands/memory.rs)
- memory_list → Vec<MemoryFact{key, value, created_at, last_used_at, use_count}>
- memory_remember(k, v) → persist new fact (or overwrite)
- memory_forget(key) → delete by exact key
- memory_search(q, limit) → substring search
GUI /memory page (frontend/src/routes/memory/index.svelte)
- Top: add-fact form (key + value inputs + "+Добавить" button)
- Substring filter input for the list
- Per-card: key + use_count badge + value (highlighted box) + timestamps +
"Забыть" button with confirm() guard
- Auto-sorted by recency (last_used_at desc)
- Empty state shows voice-hint: 'скажи Jarvis-у "запомни что я люблю чай улун"'
Header (frontend/src/components/Header.svelte)
- New "Память" button between /scheduler and /settings.
- i18n: header-memory in ru/en/ua FTL files.
New voice packs
resources/commands/disk/ (2 cmds)
- disk.free "сколько свободно на диске" / "сколько места на диске C"
PowerShell Get-PSDrive → speaks "Свободно X ГБ из Y, это N%"
- disk.list "какие у меня диски" → "C 120 ГБ, D 300 ГБ, E 50 ГБ"
resources/commands/date_math/ (2 cmds)
- date.days_until "сколько дней до нового года" / "сколько до 8 марта" /
"сколько до 15 марта" — recognises Russian months,
holidays (Новый год, Рождество, 8 марта, 9 мая).
Auto-rolls to next year if target already passed.
Russian-grammar pluralisation (день/дня/дней).
- date.day_of_week "какой сегодня день недели" — Zeller's congruence,
maps to ru day name.
resources/commands/sleep_timer/ (3 cmds)
- sleep_timer.pause_in "выключи музыку через 30 минут"
→ scheduler one-shot lua action that fires media
VK_MEDIA_PLAY_PAUSE via the existing media_keys helper.
(Auto-generates _fire_pause.lua wrapper if missing.)
- sleep_timer.shutdown_in "выключи компьютер через 1 час"
→ shutdown.exe /s /t <secs>. Caps at 24 hours.
Speaks "Скажите 'отмени таймер' чтобы передумать."
- sleep_timer.cancel "отмени таймер выключения" / "не выключай компьютер"
→ shutdown /a + scheduler.remove("sleep_timer_pause"),
idempotent.
Pack count: 64 → 67. Tests: 112/112 pass.
Build: cargo build --release -p jarvis-gui green.
249 lines
6.7 KiB
Svelte
249 lines
6.7 KiB
Svelte
<script lang="ts">
|
||
import { onMount } from "svelte"
|
||
import { invoke } from "@tauri-apps/api/core"
|
||
import { goto } from "@roxi/routify"
|
||
|
||
import HDivider from "@/components/elements/HDivider.svelte"
|
||
import Footer from "@/components/Footer.svelte"
|
||
import {
|
||
Button, Space, Text, Notification, Badge, TextInput, Loader, Textarea,
|
||
} from "@svelteuidev/core"
|
||
import { Plus, TrashIcon, CrossCircled, MagnifyingGlass } from "radix-icons-svelte"
|
||
|
||
interface MemoryFact {
|
||
key: string
|
||
value: string
|
||
created_at: number
|
||
last_used_at: number
|
||
use_count: number
|
||
}
|
||
|
||
let facts: MemoryFact[] = []
|
||
let loading = true
|
||
let error = ""
|
||
let filter = ""
|
||
|
||
// new-fact form
|
||
let newKey = ""
|
||
let newValue = ""
|
||
|
||
async function load() {
|
||
loading = true
|
||
error = ""
|
||
try {
|
||
facts = await invoke<MemoryFact[]>("memory_list")
|
||
facts = [...facts].sort((a, b) => b.last_used_at - a.last_used_at)
|
||
} catch (e) {
|
||
error = String(e)
|
||
} finally {
|
||
loading = false
|
||
}
|
||
}
|
||
|
||
async function addFact() {
|
||
const k = newKey.trim()
|
||
const v = newValue.trim()
|
||
if (!k) { error = "Ключ не может быть пустым"; return }
|
||
if (!v) { error = "Значение не может быть пустым"; return }
|
||
try {
|
||
await invoke("memory_remember", { key: k, value: v })
|
||
newKey = ""
|
||
newValue = ""
|
||
await load()
|
||
} catch (e) { error = String(e) }
|
||
}
|
||
|
||
async function deleteFact(key: string) {
|
||
if (!confirm(`Забыть факт "${key}"?`)) return
|
||
try {
|
||
await invoke("memory_forget", { key })
|
||
await load()
|
||
} catch (e) { error = String(e) }
|
||
}
|
||
|
||
onMount(load)
|
||
|
||
$: visible = filter
|
||
? facts.filter(f =>
|
||
f.key.toLowerCase().includes(filter.toLowerCase()) ||
|
||
f.value.toLowerCase().includes(filter.toLowerCase())
|
||
)
|
||
: facts
|
||
|
||
function fmtDate(ts: number): string {
|
||
if (!ts) return "—"
|
||
return new Date(ts * 1000).toLocaleString()
|
||
}
|
||
</script>
|
||
|
||
<Space h="xl" />
|
||
|
||
<h2 class="page-title">Память</h2>
|
||
<Text size="sm" color="gray">
|
||
Долговременные факты о пользователе. LLM получает их в системном промпте автоматически,
|
||
когда фразой пересекаются. Хранятся в <code>long_term_memory.json</code>.
|
||
</Text>
|
||
|
||
<Space h="md" />
|
||
|
||
<div class="new-fact">
|
||
<div class="fact-row">
|
||
<TextInput placeholder="Ключ (напр. 'любимый чай')" variant="filled" bind:value={newKey} />
|
||
<TextInput placeholder="Значение (напр. 'улун')" variant="filled" bind:value={newValue} />
|
||
<Button color="lime" radius="md" size="sm" on:click={addFact}>
|
||
<Plus size={14} /> Добавить
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
|
||
<Space h="md" />
|
||
|
||
<TextInput
|
||
placeholder="Фильтр по ключу или значению"
|
||
variant="filled"
|
||
icon={MagnifyingGlass}
|
||
bind:value={filter}
|
||
/>
|
||
|
||
<Space h="sm" />
|
||
|
||
{#if error}
|
||
<Notification
|
||
title="Ошибка"
|
||
icon={CrossCircled}
|
||
color="red"
|
||
on:close={() => { error = "" }}
|
||
>
|
||
{error}
|
||
</Notification>
|
||
<Space h="sm" />
|
||
{/if}
|
||
|
||
{#if loading}
|
||
<Loader />
|
||
{:else if facts.length === 0}
|
||
<Text size="sm" color="gray">
|
||
Память пустая. Добавь факт сверху или скажи Jarvis-у "запомни что я люблю чай улун".
|
||
</Text>
|
||
{:else if visible.length === 0}
|
||
<Text size="sm" color="gray">
|
||
По фильтру ничего не найдено. Всего фактов: {facts.length}.
|
||
</Text>
|
||
{:else}
|
||
<div class="fact-list">
|
||
{#each visible as fact}
|
||
<div class="fact-card">
|
||
<div class="fact-header">
|
||
<span class="fact-key">{fact.key}</span>
|
||
<div class="fact-meta-inline">
|
||
<Badge color="cyan" variant="outline" size="sm">
|
||
{fact.use_count}× использований
|
||
</Badge>
|
||
</div>
|
||
</div>
|
||
<div class="fact-value">
|
||
{fact.value}
|
||
</div>
|
||
<div class="fact-meta">
|
||
<small>Создан: {fmtDate(fact.created_at)}</small>
|
||
<small>Последний доступ: {fmtDate(fact.last_used_at)}</small>
|
||
</div>
|
||
<div class="fact-actions">
|
||
<Button
|
||
color="red" radius="md" size="xs" uppercase
|
||
on:click={() => deleteFact(fact.key)}
|
||
>
|
||
<TrashIcon size={14} /> Забыть
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
{/each}
|
||
</div>
|
||
{/if}
|
||
|
||
<Space h="xl" />
|
||
|
||
<Button color="gray" radius="md" size="sm" uppercase fullSize on:click={() => $goto("/")}>
|
||
Назад
|
||
</Button>
|
||
|
||
<HDivider />
|
||
<Footer />
|
||
|
||
<style lang="scss">
|
||
.page-title {
|
||
margin: 0 0 4px 0;
|
||
color: #fff;
|
||
font-size: 1.3rem;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.new-fact {
|
||
background: rgba(30, 40, 45, 0.55);
|
||
border: 1px solid rgba(255, 255, 255, 0.07);
|
||
border-radius: 10px;
|
||
padding: 0.75rem;
|
||
|
||
.fact-row {
|
||
display: grid;
|
||
grid-template-columns: 1fr 2fr auto;
|
||
gap: 0.5rem;
|
||
align-items: end;
|
||
}
|
||
}
|
||
|
||
.fact-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 0.75rem;
|
||
}
|
||
|
||
.fact-card {
|
||
background: rgba(30, 40, 45, 0.75);
|
||
border: 1px solid rgba(82, 254, 254, 0.15);
|
||
border-radius: 10px;
|
||
padding: 0.85rem 1rem;
|
||
}
|
||
|
||
.fact-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 0.4rem;
|
||
}
|
||
|
||
.fact-key {
|
||
font-size: 0.95rem;
|
||
font-weight: 600;
|
||
color: #fff;
|
||
}
|
||
|
||
.fact-meta-inline {
|
||
display: flex;
|
||
gap: 0.4rem;
|
||
}
|
||
|
||
.fact-value {
|
||
background: rgba(0, 0, 0, 0.2);
|
||
border-radius: 6px;
|
||
padding: 0.5rem 0.7rem;
|
||
color: rgba(255, 255, 255, 0.9);
|
||
font-size: 0.9rem;
|
||
line-height: 1.45em;
|
||
margin-bottom: 0.5rem;
|
||
}
|
||
|
||
.fact-meta {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 0.75rem;
|
||
color: rgba(255, 255, 255, 0.4);
|
||
font-size: 0.7rem;
|
||
margin-bottom: 0.5rem;
|
||
}
|
||
|
||
.fact-actions {
|
||
display: flex;
|
||
gap: 0.4rem;
|
||
}
|
||
</style>
|