J.A.R.V.I.S-rust/frontend/src/routes/memory/index.svelte
Bossiara13 d3180b7d78 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

249 lines
6.7 KiB
Svelte
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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>