Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,13 @@ and this project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.ht

## [Unreleased]

## [0.9.0] - 2026-09-02

### Changed

- Rebuilt Settings as a full-height categorized workspace with automatic
saving for ordinary preferences, dedicated Provider and AI-template
list/detail managers, and a non-blocking first-run checklist.
- Hotword lists now accept optional `:weight` suffixes with full-width-colon
compatibility, save-time normalization, structured generation snapshots,
DashScope weights `1–5` and super-hotword `50`, and explicit disclosure when
Expand Down Expand Up @@ -353,7 +358,8 @@ and this project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.ht
- Added GitHub Actions workflows for continuous integration and tagged Windows releases.
- Added English and Simplified Chinese README documentation.

[Unreleased]: https://github.com/kwp-lab/nota/compare/v0.8.0...HEAD
[Unreleased]: https://github.com/kwp-lab/nota/compare/v0.9.0...HEAD
[0.9.0]: https://github.com/kwp-lab/nota/compare/v0.8.0...v0.9.0
[0.8.0]: https://github.com/kwp-lab/nota/compare/v0.7.0...v0.8.0
[0.7.0]: https://github.com/kwp-lab/nota/compare/069d067d016282ae7a9ca24a360fcc64a7b4155b...v0.7.0
[0.6.0]: https://github.com/kwp-lab/nota/compare/9dca2a84e556bf813f98beb0bab82ab5c5eb6295...069d067d016282ae7a9ca24a360fcc64a7b4155b
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@

<p align="center">
<img alt="Platform: Windows 11 x64" src="https://img.shields.io/badge/platform-Windows%2011%20x64-0B6AA2?style=flat-square&logo=windows11&logoColor=white">
<img alt="Version: 0.8.0" src="https://img.shields.io/badge/version-0.8.0-56615D?style=flat-square">
<img alt="Version: 0.9.0" src="https://img.shields.io/badge/version-0.9.0-56615D?style=flat-square">
<img alt="Status: early preview" src="https://img.shields.io/badge/status-early%20preview-C18B48?style=flat-square">
<img alt="Privacy: local first" src="https://img.shields.io/badge/privacy-local%20first-2F7D71?style=flat-square">
<img alt="Telemetry: none" src="https://img.shields.io/badge/telemetry-none-2F7D71?style=flat-square">
Expand Down
2 changes: 1 addition & 1 deletion README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@

<p align="center">
<img alt="平台:Windows 11 x64" src="https://img.shields.io/badge/platform-Windows%2011%20x64-0B6AA2?style=flat-square&logo=windows11&logoColor=white">
<img alt="版本:0.8.0" src="https://img.shields.io/badge/version-0.8.0-56615D?style=flat-square">
<img alt="版本:0.9.0" src="https://img.shields.io/badge/version-0.9.0-56615D?style=flat-square">
<img alt="状态:早期预览" src="https://img.shields.io/badge/status-early%20preview-C18B48?style=flat-square">
<img alt="隐私:本地优先" src="https://img.shields.io/badge/privacy-local%20first-2F7D71?style=flat-square">
<img alt="遥测:无" src="https://img.shields.io/badge/telemetry-none-2F7D71?style=flat-square">
Expand Down
2 changes: 1 addition & 1 deletion THIRD_PARTY_NOTICES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4242,7 +4242,7 @@ developed by Chen Jiaju, licensed under the MIT License and the Apache License 2
-------------------------------------------------------------------------------
MIT License (MIT)
Used by:
- nota 0.8.0
- nota 0.9.0
- brotli-decompressor 5.0.3
- cargo_toml 0.22.3
- dpi 0.1.2
Expand Down
4 changes: 2 additions & 2 deletions bom.cyclonedx.json
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
{
"bomFormat": "CycloneDX",
"specVersion": "1.6",
"serialNumber": "urn:uuid:62a7911f-9473-4929-ac10-00652eef1252",
"serialNumber": "urn:uuid:1a4f4ce6-ac43-4dee-a45d-8d03c8231185",
"version": 1,
"metadata": {
"component": {
"type": "application",
"bom-ref": "pkg:github/kwp-lab/nota",
"name": "Nota",
"version": "0.8.0",
"version": "0.9.0",
"licenses": [
{
"license": {
Expand Down
48 changes: 47 additions & 1 deletion docs/architecture.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Nota Client Architecture

- Status: Accepted
- Last updated: 2026-08-22
- Last updated: 2026-08-27
- Owners: Nota desktop maintainers
- Related code: `src/`, `src-tauri/src/controller.rs`,
`src-tauri/src/audio/`, `src-tauri/src/storage.rs`, `src-tauri/src/asr.rs`,
Expand Down Expand Up @@ -76,6 +76,52 @@ flowchart LR
| Configured ASR service | Model inference and server-side processing | Local recording ownership |
| Configured LLM provider | Explicitly requested text generation | Local Markdown, SQLite, or recording ownership |

### Settings Ownership and Persistence

`App.tsx` owns the canonical settings and Provider summaries shared with the
rest of the application, the current settings route for this application
session, and the one global leave guard for complex editors. The settings
workspace owns category navigation and page composition. Individual pages
receive one aggregated model/actions interface rather than a collection of IPC
callbacks.

`useSettingsController` is the single frontend owner of settings IPC, directory
selection, Provider/template refresh, ordinary-preference autosave, local
feedback, and complex-editor dirty state. ASR Provider, LLM Provider, and AI
template managers use the controller but own their type-specific drafts and
validation. They save explicitly and must confirm before discarding a changed
draft. Ordinary preference pages never contribute to the global leave guard.

Ordinary settings use optimistic rendering and a serialized, latest-snapshot
queue. The controller sends only one settings write at a time, coalesces newer
intent behind the in-flight write, and rolls back to the last confirmed
snapshot if persistence fails. Default ASR/LLM changes remain specialized IPC
commands so backend validation and automatic-transcription rules stay
authoritative. Shortcut text is a local paired draft and is submitted on blur
or Enter. The Rust controller re-registers global shortcuts only when either
shortcut field changed and restores the previous registration if persistence
fails.

```mermaid
stateDiagram-v2
[*] --> Confirmed
Confirmed --> Queued: user changes an ordinary preference
Queued --> Saving: no settings write is in flight
Saving --> Queued: newer target snapshot replaces queued target
Saving --> Confirmed: backend accepts the latest saved snapshot
Saving --> RolledBack: backend rejects registration or persistence
RolledBack --> Confirmed: restore the last confirmed snapshot
Confirmed --> DraftEditor: open Provider or template editor
DraftEditor --> Confirmed: explicit save or discard confirmed
DraftEditor --> DraftEditor: navigation blocked until user decides
```

The first-run checklist is a route inside this same workspace. Only its
**Finish** and **Set up later** actions may set `firstRunComplete`; unrelated
autosaves must preserve that field. After first run, the last settings route is
remembered only for the current process and defaults to recording settings on
the next launch.

## Non-Negotiable Invariants

- Recording must work without network access.
Expand Down
33 changes: 32 additions & 1 deletion docs/design-system.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Nota Visual Design System

- Status: Accepted
- Last updated: 2026-08-11
- Last updated: 2026-08-27
- Owners: Nota maintainers

This specification is the source of truth for Nota's in-product visual
Expand Down Expand Up @@ -155,6 +155,37 @@ page-specific CSS.
and must appear above scroll containers without being clipped.
- Keyboard focus must be visible. Hover-only behavior is not sufficient.

### Settings Center

Settings is a full-height workspace inside the application shell. The global
application sidebar remains visible, while the normal page header and footer
are removed so the settings hierarchy owns the available height.

- The settings category navigation is a persistent `200 px` grid track. It
never collapses or hides, including on Provider and template management
routes. A selected management route highlights its parent category.
- Ordinary preference pages use one readable column of stacked
`SettingsCard` rows. A card contains one icon, a primary label, optional
supporting text, and one trailing control or navigation affordance. It must
not become a second nested navigation system.
- Provider and template management pages use three conceptual columns: the
persistent category navigation, a `260 px` resource list, and a scrollable
detail editor. ASR and LLM editors share this shell and its feedback/action
treatments, but keep type-specific fields and validation.
- Explicit-save editors keep their actions in a stable detail footer. Built-in
templates use the same detail region in a read-only state; custom templates
may be edited, saved, or archived.
- At the `980 px` minimum application width, the category and resource tracks
reduce to approximately `168 px` and `220 px`. Multi-column forms become one
column. The category navigation remains visible and no settings route may
introduce horizontal page overflow.
- The category list, resource list, and detail region own independent vertical
scrolling where needed. Chinese long text may wrap in descriptions, while
paths and compact resource metadata use tokenized truncation treatments.

Settings uses existing semantic surface, border, status, spacing, and focus
roles. The workspace geometry does not introduce a new visual token role.

## Change and Review Checklist

For every visual change:
Expand Down
39 changes: 36 additions & 3 deletions docs/testing.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Testing and Acceptance

- Status: Accepted
- Last updated: 2026-08-22
- Last updated: 2026-08-27
- Owners: Nota maintainers
- Related configuration: `package.json`, `src-tauri/Cargo.toml`,
`.github/workflows/ci.yml`, `.github/workflows/frontend.yml`,
Expand Down Expand Up @@ -80,6 +80,7 @@ required devices or environment variables in the test name and ignore message.
| Area | Primary tests |
|---|---|
| React behavior and typed rendering | `src/*.test.tsx`, `src/components/*.test.tsx` |
| Settings navigation, autosave, Provider/template editors, and first run | `src/components/settings/*.test.tsx` and `src/App.test.tsx` |
| IPC serialization | `src-tauri/src/models.rs` tests |
| SQLite schema and migrations | `src-tauri/src/storage.rs` tests |
| ASR parsing, capability checks, request construction, chunk merging | `src-tauri/src/asr.rs` tests |
Expand Down Expand Up @@ -192,15 +193,47 @@ Server-side automated coverage must include:
- duration, upload-size, disk-space, and retention limits;
- exact `verbose_json 1.0` result compatibility.

## Settings Center Regression Matrix

Automated coverage for Settings must include:

- category navigation, parent-category highlighting on management routes,
management-page back navigation, session-only route restoration, and
first-run routing precedence;
- full-height settings mode retaining the application sidebar while omitting
the normal global header/footer, including the active-recording notice;
- serialized ordinary-settings autosave, newest-snapshot coalescing, success
feedback, persistence rollback, directory-picker cancellation, specialized
default Provider commands, and no leave prompt for confirmed ordinary
settings;
- paired shortcut draft submission on blur or Enter, whitespace trimming,
conflict feedback without replacing the effective pair, disabled shortcuts,
and no shortcut re-registration for unrelated settings writes;
- ASR and LLM Provider API-key keep/replace/clear, unsaved-draft connection
testing, model discovery and manual fallback, explicit save/delete, default
cleanup and first-valid default selection;
- DashScope fixed fields, no model discovery, and temporary upload disclosure;
- built-in template read-only behavior and copying, custom-template create,
save, revision update, archive, load failures, and dirty-editor navigation
protection;
- the first-run checklist allowing non-blocking category and Provider
navigation, Provider round trips, and only **Finish** or **Set up later**
persisting completion for the next startup.

Manual visual acceptance covers `1280×800` and `980×640`, Windows `125%`
scaling, Chinese long text, keyboard navigation and focus, plus disabled,
loading, empty-list, and error states. The category navigation must remain
visible and the workspace must not scroll horizontally.

## Diagnostic Logging Regression Matrix

Automated coverage for local diagnostics must include:

- UTC RFC3339 timestamps, one-line records, JSON escaping, and bounded text;
- the typed field allowlist rejecting sensitive field names;
- one active log plus two archives after rotation;
- Settings rendering and the open-directory command path without changing
settings dirty state;
- Settings rendering and the open-directory command path without enqueueing an
unrelated settings save or changing complex-editor dirty state;
- lifecycle metadata for recording, import, ASR, and LLM work, including
correlation IDs, coarse phases, HTTP status, elapsed time, estimates, and
actual token usage where available;
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "nota",
"private": true,
"version": "0.8.0",
"version": "0.9.0",
"license": "MIT",
"repository": {
"type": "git",
Expand Down
2 changes: 1 addition & 1 deletion src-tauri/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "nota"
version = "0.8.0"
version = "0.9.0"
description = "A private, local-first Windows meeting recorder"
authors = ["Nota Contributors"]
license = "MIT"
Expand Down
2 changes: 1 addition & 1 deletion src-tauri/src/asr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2539,7 +2539,7 @@ impl OggPcm16Reader {
decoded.drain(..skip);
self.pre_skip -= skip;
}
for source in decoded.chunks_exact(3) {
for source in decoded.as_chunks::<3>().0 {
let value = ((source[0] + source[1] + source[2]) / 3.0).clamp(-1.0, 1.0);
self.pending
.push_back((value * if value < 0.0 { 32_768.0 } else { 32_767.0 }).round() as i16);
Expand Down
67 changes: 65 additions & 2 deletions src-tauri/src/controller.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1345,6 +1345,12 @@ fn get_settings(state: State<AppState>) -> std::result::Result<AppSettings, Stri
command_result(state.storage.settings())
}

fn shortcut_settings_changed(previous: &AppSettings, next: &AppSettings) -> bool {
previous.shortcuts_enabled != next.shortcuts_enabled
|| previous.toggle_shortcut != next.toggle_shortcut
|| previous.stop_shortcut != next.stop_shortcut
}

#[tauri::command]
fn save_settings(
app: AppHandle,
Expand All @@ -1353,11 +1359,17 @@ fn save_settings(
) -> std::result::Result<(), String> {
command_result((|| {
let previous = state.storage.settings()?;
if let Err(error) = register_shortcuts(&app, &settings) {
let shortcuts_changed = shortcut_settings_changed(&previous, &settings);
if shortcuts_changed && let Err(error) = register_shortcuts(&app, &settings) {
let _ = register_shortcuts(&app, &previous);
bail!("快捷键注册失败,可能与其他应用冲突:{error}");
}
state.storage.save_settings(&settings)?;
if let Err(error) = state.storage.save_settings(&settings) {
if shortcuts_changed {
let _ = register_shortcuts(&app, &previous);
}
return Err(error);
}
logging::info("settings", "saved", &[]);
Ok(())
})())
Expand Down Expand Up @@ -3152,6 +3164,57 @@ pub fn run_app() {
});
}

#[cfg(test)]
mod settings_tests {
use super::*;

fn settings() -> AppSettings {
AppSettings {
output_directory: "C:\\Recordings".into(),
ai_documents_directory: "C:\\Documents".into(),
aec_mode: AecMode::Auto,
microphone_enabled: true,
first_run_complete: true,
shortcuts_enabled: true,
toggle_shortcut: "Ctrl+Alt+F9".into(),
stop_shortcut: "Ctrl+Alt+F10".into(),
active_asr_provider_id: None,
voiceprint_provider_id: None,
auto_transcribe: false,
auto_transcribe_hotword_list_id: None,
active_llm_provider_id: None,
}
}

#[test]
fn unrelated_settings_do_not_require_shortcut_registration() {
let previous = settings();
let mut next = previous.clone();
next.output_directory = "D:\\Meetings".into();
next.aec_mode = AecMode::On;
next.auto_transcribe = true;

assert!(!shortcut_settings_changed(&previous, &next));
}

#[test]
fn every_shortcut_field_requires_registration() {
let previous = settings();

let mut disabled = previous.clone();
disabled.shortcuts_enabled = false;
assert!(shortcut_settings_changed(&previous, &disabled));

let mut toggle = previous.clone();
toggle.toggle_shortcut = "Ctrl+Alt+F7".into();
assert!(shortcut_settings_changed(&previous, &toggle));

let mut stop = previous.clone();
stop.stop_shortcut = "Ctrl+Alt+F8".into();
assert!(shortcut_settings_changed(&previous, &stop));
}
}

#[cfg(test)]
mod transcript_export_tests {
use super::*;
Expand Down
6 changes: 4 additions & 2 deletions src-tauri/src/storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2922,8 +2922,10 @@ fn decode_embedding(bytes: &[u8], dimension: usize) -> Option<Vec<f32>> {
return None;
}
let values = bytes
.chunks_exact(4)
.map(|chunk| f32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]))
.as_chunks::<4>()
.0
.iter()
.map(|chunk| f32::from_le_bytes(*chunk))
.collect::<Vec<_>>();
values
.iter()
Expand Down
Loading