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.
This commit is contained in:
parent
84d3b57ddc
commit
d3180b7d78
18 changed files with 717 additions and 4 deletions
249
frontend/src/routes/memory/index.svelte
Normal file
249
frontend/src/routes/memory/index.svelte
Normal file
|
|
@ -0,0 +1,249 @@
|
|||
<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>
|
||||
Loading…
Add table
Add a link
Reference in a new issue