J.A.R.V.I.S-rust/frontend/src/routes/memory/index.svelte

250 lines
6.7 KiB
Svelte
Raw Normal View History

feat: GUI /memory page + disk/date_math/sleep_timer packs (10 new commands) 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.
2026-05-15 23:35:23 +03:00
<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} /> &nbsp; Добавить
</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} /> &nbsp; Забыть
</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>