feat: /history page (recognition log) + multi wake-word loading
# Recognition history page
New /history route in the Tauri GUI. Every voice/text phrase that
reaches the dispatcher gets a row with: timestamp, the phrase, what
happened, command id + confidence (if matched), which matcher fired
(intent / fuzzy / LLM-router / LLM-fallback).
Color coding:
- Green: command matched AND executed successfully
- Red: command matched but failed, OR dispatcher error
- Blue: LLM fallback handled it (Jarvis spoke a free-form reply)
- Orange: no match at all ("Не понял")
Live updates: GUI polls every 2s from the on-disk log, so the page
shows daemon writes in near real time even though they're separate
processes. Filter box for searching by phrase or command id.
Architecture:
- New core module `recognition_log` with ring buffer (cap 500) +
atomic JSON write-through to `<APP_CONFIG_DIR>/recognition_log.json`.
- `record(phrase, source, outcome)` is the single call-site from the
daemon's `execute_command()` — hooked into the 4 outcome paths
(matched-ok, matched-fail, not-found, llm-handled, error).
- `recent(limit)` reads the in-memory buffer (daemon's view).
- `recent_from_disk(limit)` re-reads the JSON file — GUI uses this
since the GUI process has its own buffer that doesn't see the
daemon's writes.
- 5 new unit tests covering ring buffer trimming, outcome serde
roundtrip, missing/corrupt/oversized file recovery.
GUI:
- `crates/jarvis-gui/src/tauri_commands/history.rs`: history_recent,
history_clear. Flattens the Outcome enum into a single struct that's
easier for the Svelte template to render.
- `frontend/src/routes/history/index.svelte`: ~270 lines. Stats badges
(✓ N matched / ✗ N misses / total), filter input, virtual list of
entry cards with color-coded left border. Polls every 2s.
- Header gets a new "История" / "History" button (between Plugins and
Settings). Russian + English locale entries added.
# Multi wake-word loading
Was: `init()` loaded the bundled `jarvis-default.rpw` + at most ONE
custom (from `settings.custom_wake_word`). User had to pick a single
trained model.
Now: loads the bundled default PLUS every .rpw in
`APP_CONFIG_DIR/wake_words/` simultaneously. Rustpotter natively
supports multiple wake-word triggers — each adds robustness for
different voice profiles. The legacy `custom_wake_word` field is
checked for back-compat but is a no-op if it points inside the
already-loaded directory.
User-facing impact: train the wake-word once via /wake-trainer →
restart daemon → detection improves automatically without picking a
single "active" model.
# Settings → "Обучить wake-word" button
Added a purple button on the settings page that links to
/wake-trainer. The trainer existed but had no in-GUI link, so users
couldn't find it without typing the URL. Now sits next to the
"Конструктор команд (Python)" button.
Tests: 140 → 145 rust core tests (+5 recognition_log). Frontend
rebuilds in 6.2s. Release builds of jarvis-app + jarvis-gui green.
Practical test: GUI launches (MainWindowTitle confirmed), seed log
file written + visible to the page.
This commit is contained in:
parent
965441d4db
commit
73fc404ec7
13 changed files with 867 additions and 63 deletions
|
|
@ -22,6 +22,12 @@ fn main() {
|
|||
// Register our toast AUMID — see crates/jarvis-core/src/toast.rs.
|
||||
jarvis_core::toast::register_aumid();
|
||||
|
||||
// Share the recognition log with the daemon — both processes read the
|
||||
// same JSON file under APP_CONFIG_DIR. The GUI re-reads it on each
|
||||
// /history poll so logs from the daemon show up there too. Failure is
|
||||
// non-fatal: the page just stays empty.
|
||||
let _ = jarvis_core::recognition_log::init();
|
||||
|
||||
// basic logging setup (simpler for GUI)
|
||||
simple_log::quick!("info");
|
||||
|
||||
|
|
@ -144,6 +150,10 @@ fn main() {
|
|||
// Python command builder launcher
|
||||
tauri_commands::open_command_builder,
|
||||
|
||||
// Recognition history
|
||||
tauri_commands::history_recent,
|
||||
tauri_commands::history_clear,
|
||||
|
||||
// Wake-word trainer wizard
|
||||
tauri_commands::wake_trainer_status,
|
||||
tauri_commands::wake_trainer_defaults,
|
||||
|
|
|
|||
|
|
@ -67,6 +67,10 @@ pub use plugins::*;
|
|||
mod builder;
|
||||
pub use builder::*;
|
||||
|
||||
// Recognition history page
|
||||
mod history;
|
||||
pub use history::*;
|
||||
|
||||
// Wake-word training wizard
|
||||
mod wake_trainer;
|
||||
pub use wake_trainer::*;
|
||||
91
crates/jarvis-gui/src/tauri_commands/history.rs
Normal file
91
crates/jarvis-gui/src/tauri_commands/history.rs
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
//! Tauri commands for the /history page — recognition log with
|
||||
//! green/red status badges. The data lives in
|
||||
//! `<APP_CONFIG_DIR>/recognition_log.json`, which the daemon writes to
|
||||
//! whenever it dispatches a phrase. The GUI reads from disk every poll
|
||||
//! so it sees daemon writes even though they're separate processes.
|
||||
|
||||
use serde::Serialize;
|
||||
|
||||
/// Flat view of `recognition_log::RecognitionEntry` optimised for the
|
||||
/// Svelte renderer — outcomes flattened to a single `kind` discriminator
|
||||
/// and explicit `command_id` / `confidence_pct` so the template doesn't
|
||||
/// have to walk a tagged union.
|
||||
#[derive(Serialize)]
|
||||
pub struct HistoryEntry {
|
||||
pub ts: i64,
|
||||
pub phrase: String,
|
||||
pub source: String,
|
||||
/// "matched" | "not_found" | "llm_handled" | "error"
|
||||
pub kind: &'static str,
|
||||
pub command_id: Option<String>,
|
||||
pub confidence_pct: Option<u8>,
|
||||
pub via: Option<String>,
|
||||
pub success: Option<bool>,
|
||||
pub error_message: Option<String>,
|
||||
}
|
||||
|
||||
fn flatten(e: jarvis_core::recognition_log::RecognitionEntry) -> HistoryEntry {
|
||||
use jarvis_core::recognition_log::Outcome;
|
||||
match e.outcome {
|
||||
Outcome::Matched { command_id, confidence_pct, via, success } => HistoryEntry {
|
||||
ts: e.ts,
|
||||
phrase: e.phrase,
|
||||
source: e.source,
|
||||
kind: "matched",
|
||||
command_id: Some(command_id),
|
||||
confidence_pct,
|
||||
via: Some(via),
|
||||
success: Some(success),
|
||||
error_message: None,
|
||||
},
|
||||
Outcome::NotFound => HistoryEntry {
|
||||
ts: e.ts,
|
||||
phrase: e.phrase,
|
||||
source: e.source,
|
||||
kind: "not_found",
|
||||
command_id: None,
|
||||
confidence_pct: None,
|
||||
via: None,
|
||||
success: Some(false),
|
||||
error_message: None,
|
||||
},
|
||||
Outcome::LlmHandled => HistoryEntry {
|
||||
ts: e.ts,
|
||||
phrase: e.phrase,
|
||||
source: e.source,
|
||||
kind: "llm_handled",
|
||||
command_id: None,
|
||||
confidence_pct: None,
|
||||
via: Some("llm".into()),
|
||||
success: Some(true),
|
||||
error_message: None,
|
||||
},
|
||||
Outcome::Error { message } => HistoryEntry {
|
||||
ts: e.ts,
|
||||
phrase: e.phrase,
|
||||
source: e.source,
|
||||
kind: "error",
|
||||
command_id: None,
|
||||
confidence_pct: None,
|
||||
via: None,
|
||||
success: Some(false),
|
||||
error_message: Some(message),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Return up to `limit` newest recognition entries. Re-reads from disk
|
||||
/// so the GUI always sees the daemon's latest writes. `limit = 0` → all.
|
||||
#[tauri::command]
|
||||
pub fn history_recent(limit: usize) -> Vec<HistoryEntry> {
|
||||
jarvis_core::recognition_log::recent_from_disk(limit)
|
||||
.into_iter()
|
||||
.map(flatten)
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Wipe the recognition log. Returns the count of removed entries.
|
||||
#[tauri::command]
|
||||
pub fn history_clear() -> usize {
|
||||
jarvis_core::recognition_log::clear()
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue