From c5cda3fa888a0f017f654971f17256f3312d67ef Mon Sep 17 00:00:00 2001 From: prPMDev <129408157+prPMDev@users.noreply.github.com> Date: Tue, 16 Jun 2026 22:56:51 -0700 Subject: [PATCH] =?UTF-8?q?UI:=20post-MSI=20fixes=20=E2=80=94=20Detection?= =?UTF-8?q?=E2=86=92Models,=20tab=20reorder,=20real=20save-to-folder?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Rename the "Detection" tab + screen to "Models" (matches the internal name) - Reorder sidebar to Redact · Models · History · Advanced · About - De-duplicate "Clear history": keep it on the History tab, drop the Advanced copy - Make "Save redacted files to" real: folder picker (Tauri dialog) + persisted path (default Downloads/Frisket), write via Rust write_text_file/ensure_dir, quiet "Saved ✓" with an on-demand "Show in folder" - Fix About acknowledgements: drop PyMuPDF (unused here); credit pdf.js, Transformers.js, ONNX Runtime - Enable logging in release builds so the log directory exists and About → Open works - Tone down + unify the privacy copy; flex the path-field layout so rows don't overflow Co-Authored-By: Claude Opus 4.8 --- package-lock.json | 33 +++--- package.json | 1 + src-tauri/Cargo.lock | 170 +++++++++++++++++++++++++--- src-tauri/Cargo.toml | 1 + src-tauri/capabilities/default.json | 3 +- src-tauri/src/lib.rs | 34 ++++-- ui/index.html | 15 ++- ui/src/main.ts | 90 ++++++++++++++- ui/src/strings.ts | 10 +- ui/styles/app.css | 5 +- 10 files changed, 306 insertions(+), 56 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2ca70cf..59ae3bb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,15 +1,16 @@ { "name": "frisket", - "version": "0.1.0", + "version": "0.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "frisket", - "version": "0.1.0", + "version": "0.2.0", "dependencies": { "@huggingface/transformers": "^4.2.0", "@tauri-apps/api": "^2.11.0", + "@tauri-apps/plugin-dialog": "^2.7.1", "@tauri-apps/plugin-opener": "^2.5.4", "onnxruntime-web": "^1.26.0-dev.20260416-b7804b056c", "pdfjs-dist": "^6.0.227" @@ -895,12 +896,6 @@ "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", "license": "BSD-3-Clause" }, - "node_modules/@protobufjs/inquire": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.2.tgz", - "integrity": "sha512-pa0vFRuws4wkvaXKK1uXZMAwAX4/t8ANaJo45iw/oQHNQ9q5xUzwgFmVJGXiga2BeN+zpX7Vf9vmsiIa2J+MUw==", - "license": "BSD-3-Clause" - }, "node_modules/@protobufjs/path": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", @@ -1417,6 +1412,15 @@ "node": ">= 10" } }, + "node_modules/@tauri-apps/plugin-dialog": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-dialog/-/plugin-dialog-2.7.1.tgz", + "integrity": "sha512-OK1UBXYt+ojcmxMktzzuyonYIFta8CmAASpX+CA+DTGK24KlHjhYI6x2iOJ/TjZF4N7/ACK1oFmEOjIY9IhzOQ==", + "license": "MIT OR Apache-2.0", + "dependencies": { + "@tauri-apps/api": "^2.11.0" + } + }, "node_modules/@tauri-apps/plugin-opener": { "version": "2.5.4", "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-opener/-/plugin-opener-2.5.4.tgz", @@ -2270,9 +2274,9 @@ } }, "node_modules/protobufjs": { - "version": "7.6.2", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.2.tgz", - "integrity": "sha512-N9EiLovGEQOJSPF26Ij7qUGvahfEnq0eeYZ02aigIedkmz1qZSwjnP9SBITHJuF/6MYbIW4HDN8zdYjsjqJKXQ==", + "version": "7.6.4", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.4.tgz", + "integrity": "sha512-RJJPTTpvFfHcWLkIa2JFWK4XvtSzS0yEWDmunqHXli1h3JlkbcQZXDZdcWxv+JK3Xsl5/UFDPZ0iGm7DAengYw==", "hasInstallScript": true, "license": "BSD-3-Clause", "dependencies": { @@ -2282,7 +2286,6 @@ "@protobufjs/eventemitter": "^1.1.1", "@protobufjs/fetch": "^1.1.1", "@protobufjs/float": "^1.0.2", - "@protobufjs/inquire": "^1.1.2", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.1", @@ -2542,9 +2545,9 @@ "license": "MIT" }, "node_modules/vite": { - "version": "8.0.15", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.15.tgz", - "integrity": "sha512-qpgllRxrLqwsMAGRdLhsEr9bepaOQk1rxH1xT2coBXLaEB/bfkqQj1j7RMxwMfnYrvO1ZnFMiwX+wBVgnsyn0g==", + "version": "8.0.16", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.16.tgz", + "integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index b114a13..6fb7a9e 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "dependencies": { "@huggingface/transformers": "^4.2.0", "@tauri-apps/api": "^2.11.0", + "@tauri-apps/plugin-dialog": "^2.7.1", "@tauri-apps/plugin-opener": "^2.5.4", "onnxruntime-web": "^1.26.0-dev.20260416-b7804b056c", "pdfjs-dist": "^6.0.227" diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index c69d58e..4ed71ad 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -1154,6 +1154,20 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "frisket" +version = "0.2.0" +dependencies = [ + "log", + "serde", + "serde_json", + "tauri", + "tauri-build", + "tauri-plugin-dialog", + "tauri-plugin-log", + "tauri-plugin-opener", +] + [[package]] name = "funty" version = "2.0.0" @@ -2368,6 +2382,7 @@ checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" dependencies = [ "bitflags 2.11.1", "block2", + "libc", "objc2", "objc2-core-foundation", ] @@ -2863,19 +2878,6 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" -[[package]] -name = "frisket" -version = "0.2.0" -dependencies = [ - "log", - "serde", - "serde_json", - "tauri", - "tauri-build", - "tauri-plugin-log", - "tauri-plugin-opener", -] - [[package]] name = "redox_syscall" version = "0.5.18" @@ -2988,6 +2990,30 @@ dependencies = [ "web-sys", ] +[[package]] +name = "rfd" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a15ad77d9e70a92437d8f74c35d99b4e4691128df018833e99f90bcd36152672" +dependencies = [ + "block2", + "dispatch2", + "glib-sys", + "gobject-sys", + "gtk-sys", + "js-sys", + "log", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "raw-window-handle", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows-sys 0.60.2", +] + [[package]] name = "rkyv" version = "0.7.46" @@ -3736,6 +3762,48 @@ dependencies = [ "walkdir", ] +[[package]] +name = "tauri-plugin-dialog" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65981abb771e74e571a38196c3baa11c459379164791eba0e67abc1a5fac9884" +dependencies = [ + "log", + "raw-window-handle", + "rfd", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "tauri-plugin-fs", + "thiserror 2.0.18", + "url", +] + +[[package]] +name = "tauri-plugin-fs" +version = "2.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7ecc274121aca0c036a2b42d1cbe83d368d348f54e0bb8a735c2b1548e8f371" +dependencies = [ + "anyhow", + "dunce", + "glob", + "log", + "objc2-foundation", + "percent-encoding", + "schemars 0.8.22", + "serde", + "serde_json", + "serde_repr", + "tauri", + "tauri-plugin", + "tauri-utils", + "thiserror 2.0.18", + "toml 1.1.2+spec-1.1.0", + "url", +] + [[package]] name = "tauri-plugin-log" version = "2.8.0" @@ -4877,6 +4945,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + [[package]] name = "windows-sys" version = "0.61.2" @@ -4910,13 +4987,30 @@ dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm", + "windows_i686_gnullvm 0.52.6", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link 0.2.1", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + [[package]] name = "windows-threading" version = "0.1.0" @@ -4947,6 +5041,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + [[package]] name = "windows_aarch64_msvc" version = "0.42.2" @@ -4959,6 +5059,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + [[package]] name = "windows_i686_gnu" version = "0.42.2" @@ -4971,12 +5077,24 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + [[package]] name = "windows_i686_msvc" version = "0.42.2" @@ -4989,6 +5107,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + [[package]] name = "windows_x86_64_gnu" version = "0.42.2" @@ -5001,6 +5125,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + [[package]] name = "windows_x86_64_gnullvm" version = "0.42.2" @@ -5013,6 +5143,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + [[package]] name = "windows_x86_64_msvc" version = "0.42.2" @@ -5025,6 +5161,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + [[package]] name = "winnow" version = "0.5.40" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index c00b98c..19d5d46 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -24,3 +24,4 @@ log = "0.4" tauri = { version = "2.11.2", features = [] } tauri-plugin-log = "2" tauri-plugin-opener = "2" +tauri-plugin-dialog = "2" diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index 639f455..4d95ac6 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -7,6 +7,7 @@ ], "permissions": [ "core:default", - "opener:default" + "opener:default", + "dialog:default" ] } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 7bc26e3..f7f36f6 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,15 +1,35 @@ +// Write text to an arbitrary file path (creating parent folders). A small app command instead +// of the fs plugin so a user-chosen "Save redacted files to" folder needs no scope wrangling. +#[tauri::command] +fn write_text_file(path: String, contents: String) -> Result<(), String> { + let p = std::path::Path::new(&path); + if let Some(dir) = p.parent() { + std::fs::create_dir_all(dir).map_err(|e| e.to_string())?; + } + std::fs::write(p, contents).map_err(|e| e.to_string()) +} + +// Make sure a folder exists before we reveal it (the OS file explorer can't open a missing dir). +#[tauri::command] +fn ensure_dir(path: String) -> Result<(), String> { + std::fs::create_dir_all(&path).map_err(|e| e.to_string()) +} + #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { tauri::Builder::default() .plugin(tauri_plugin_opener::init()) + .plugin(tauri_plugin_dialog::init()) + .invoke_handler(tauri::generate_handler![write_text_file, ensure_dir]) .setup(|app| { - if cfg!(debug_assertions) { - app.handle().plugin( - tauri_plugin_log::Builder::default() - .level(log::LevelFilter::Info) - .build(), - )?; - } + // Logging runs in every build (not only debug) so the log directory exists and the + // "Open log directory" button in About actually reveals something on the installed app. + app.handle().plugin( + tauri_plugin_log::Builder::default() + .level(log::LevelFilter::Info) + .build(), + )?; + log::info!("Frisket {} started", env!("CARGO_PKG_VERSION")); Ok(()) }) .run(tauri::generate_context!()) diff --git a/ui/index.html b/ui/index.html index ccf2ac9..257e2e8 100644 --- a/ui/index.html +++ b/ui/index.html @@ -16,9 +16,9 @@
Frisket
@@ -59,7 +59,7 @@

Redact a document

@@ -105,7 +104,7 @@

Redact a document

Diagnostics i
Acknowledgments
-
GLiNER · Tauri · PyMuPDF
Built on GLiNER, Tauri, and PyMuPDF. Thanks to those communities.
+
GLiNER · Tauri · pdf.js · Transformers.js · ONNX Runtime
Built on GLiNER, Tauri, pdf.js, Transformers.js, and ONNX Runtime. Thanks to those communities.
diff --git a/ui/src/main.ts b/ui/src/main.ts index 644b00b..beaf2e9 100644 --- a/ui/src/main.ts +++ b/ui/src/main.ts @@ -35,6 +35,81 @@ async function openAppDir(which: "data" | "log") { } } +// ───────── save redacted files to a real folder (Tauri) ───────── +// In the packaged app, "Save" writes the redacted text to a user-chosen folder via a tiny Rust +// command and reveals it; the folder defaults to /Frisket and is remembered. In the +// plain browser build (npm run dev) there's no Tauri backend, so each step throws and callers +// fall back to the blob download — same airplane-mode-safe, dynamic-import pattern as the opens. +const SAVE_DIR_KEY = "frisket.saveDir.v1"; +async function defaultSaveDir(): Promise { + const { downloadDir, join } = await import("@tauri-apps/api/path"); + return await join(await downloadDir(), "Frisket"); +} +async function currentSaveDir(): Promise { + return localStorage.getItem(SAVE_DIR_KEY) || (await defaultSaveDir()); +} +// Write _redacted.txt into the save folder. Returns the full path, or null when there's +// no Tauri backend (browser dev) so the caller can fall back to a download. Quiet by design: +// it never pops the file explorer — revealing is on demand via revealSaveTarget(). +let lastSavedPath: string | null = null; +async function saveToFolder(name: string, text: string): Promise { + try { + const { invoke } = await import("@tauri-apps/api/core"); + const { join } = await import("@tauri-apps/api/path"); + const full = await join(await currentSaveDir(), name.replace(/\.[^.]+$/, "") + "_redacted.txt"); + await invoke("write_text_file", { path: full, contents: text }); + lastSavedPath = full; + diag.info("file_saved", {}); + return full; + } catch (e) { + diag.error("save_failed", e); + return null; + } +} +// On-demand reveal: open the file explorer at the last saved file, or the save folder if +// nothing has been saved yet. Only ever called from a "Show in folder" click. +async function revealSaveTarget() { + try { + const { invoke } = await import("@tauri-apps/api/core"); + const { revealItemInDir } = await import("@tauri-apps/plugin-opener"); + if (lastSavedPath) { await revealItemInDir(lastSavedPath); return; } + const dir = await currentSaveDir(); + try { await invoke("ensure_dir", { path: dir }); } catch { /* may exist / browser dev */ } + await revealItemInDir(dir); + } catch (e) { diag.error("reveal_failed", e); } +} +// Shared wiring for the result/history "Save" button: write to the folder, else download. +function wireSaveButton(btn: HTMLButtonElement, name: string, text: string) { + btn.addEventListener("click", async () => { + const full = await saveToFolder(name, text); + if (full) flash(btn, COPY.saved); + else downloadText(name, text); + }); +} + +// Advanced › "Save redacted files to": show the folder, let the user change it, reveal it. +const saveDirInput = document.querySelector("#saveDirInput"); +async function refreshSaveDirInput() { + if (!saveDirInput) return; + try { saveDirInput.value = await currentSaveDir(); } catch { saveDirInput.value = "Downloads/Frisket"; } +} +void refreshSaveDirInput(); +document.querySelector("#pickSaveDir")?.addEventListener("click", async () => { + try { + const { open } = await import("@tauri-apps/plugin-dialog"); + const sel = await open({ directory: true, multiple: false }); + if (typeof sel === "string") { localStorage.setItem(SAVE_DIR_KEY, sel); void refreshSaveDirInput(); } + } catch (e) { diag.error("pick_dir_failed", e); } +}); +document.querySelector("#openSaveDir")?.addEventListener("click", async () => { + try { + const { invoke } = await import("@tauri-apps/api/core"); + const { revealItemInDir } = await import("@tauri-apps/plugin-opener"); + const dir = await currentSaveDir(); + try { await invoke("ensure_dir", { path: dir }); } catch { /* may already exist / browser dev */ } + await revealItemInDir(dir); + } catch (e) { diag.error("open_savedir_failed", e); } +}); // Copy the privacy-safe diagnostics log to the clipboard (blob downloads don't fire in WebView2). document.querySelector("#exportDiag")?.addEventListener("click", async (e) => { @@ -582,7 +657,8 @@ function renderResult(original: string, redacted: string, changes: Change[]) {
- + +
${chips}
@@ -645,7 +721,8 @@ function renderResult(original: string, redacted: string, changes: Change[]) { try { await navigator.clipboard.writeText(redacted); flash(btn, COPY.copied); } catch { flash(btn, COPY.copyFailed); } }); - resultEl.querySelector("#dlBtn")!.addEventListener("click", () => downloadText(currentName, redacted)); + wireSaveButton(resultEl.querySelector("#saveBtn")!, currentName, redacted); + resultEl.querySelector("#showFolderBtn")!.addEventListener("click", () => void revealSaveTarget()); } // ───────── run log (privacy-safe: metadata + counts ONLY, never the PII) ───────── @@ -690,7 +767,8 @@ function showHistoryItem(rec: RunRecord) {
${COPY.reopened}
- + +
@@ -708,7 +786,8 @@ function showHistoryItem(rec: RunRecord) { const b = e.currentTarget as HTMLButtonElement; try { await navigator.clipboard.writeText(txt); flash(b, COPY.copied); } catch { flash(b, COPY.copyFailed); } }); - resultEl.querySelector("#dlBtn")!.addEventListener("click", () => downloadText(rec.file, txt)); + wireSaveButton(resultEl.querySelector("#saveBtn")!, rec.file, txt); + resultEl.querySelector("#showFolderBtn")!.addEventListener("click", () => void revealSaveTarget()); } function renderHistory() { @@ -755,5 +834,4 @@ function wireArmedClear(sel: string, doneMsg?: string) { if (doneMsg) setTimeout(() => { btn.textContent = COPY.clearHistory; }, 1500); }); } -wireArmedClear("#clearHist"); -wireArmedClear("#clearAll", COPY.cleared); // Advanced › "Clear saved history": same effect +wireArmedClear("#clearHist", COPY.cleared); // History tab is the single home for clearing diff --git a/ui/src/strings.ts b/ui/src/strings.ts index e1bff35..88bac10 100644 --- a/ui/src/strings.ts +++ b/ui/src/strings.ts @@ -57,6 +57,9 @@ export const COPY = { hide: "Hide originals", copyRedacted: "Copy redacted", download: "Download", + save: "Save", + saved: "Saved ✓", + showInFolder: "Show in folder", editSettings: "Edit settings", newFile: "New file", @@ -74,11 +77,10 @@ export const COPY = { // Static info-tooltip texts, keyed by the icon's data-tip attribute. Level and mode tips // are NOT here — they compose from the engine registries at runtime (single source). export const TIPS: Record = { - detection: "The detection method in use. Click to switch it, or open the Detection tab to add models.", + detection: "The detection method in use. Click to switch it, or open the Models tab to add more.", customTerms: "Your own words to always redact: an employer, a nickname, a project name. Separate with commas.", - seed: "Picks which fake values you get. Same set, same fakes, every run. It's not a password and can't un-redact anything.", - saveTo: "Where your redacted files are saved.", - clearHistory: "Clears the local history log. Your original files were never saved.", + seed: "Controls which fake values are used as replacements. Keep it the same to get identical results each run, or regenerate for a different set.", + saveTo: "The folder where redacted files are saved when you click Save. Change it to any folder you like.", source: "Frisket is open source (MIT).", support: "No donations. A star or a share helps.", diagnostics: "A privacy-safe event log: never your document, the matches, or file names. Copy it to report an issue.", diff --git a/ui/styles/app.css b/ui/styles/app.css index abc655c..ac2d1c2 100644 --- a/ui/styles/app.css +++ b/ui/styles/app.css @@ -85,7 +85,10 @@ button, input { font-family: inherit; } border-radius: var(--r-md); font-size: var(--fs-sm); transition: border-color var(--dur) var(--ease), box-shadow var(--dur) var(--ease); } .input::placeholder { color: var(--text-muted); } .input:focus { outline: none; border-color: var(--primary); box-shadow: 0 0 0 3px var(--primary-soft); } -.input--path { font-family: var(--font-mono); font-size: var(--fs-xs); width: 300px; color: var(--text-2); } +.input--path { font-family: var(--font-mono); font-size: var(--fs-xs); flex: 1 1 auto; width: auto; min-width: 0; color: var(--text-2); } +/* Path rows let the field grow/shrink so the buttons stay right-aligned and the row never + overflows the panel — needed once "Save redacted files to" gained a second button. */ +.setrow .v:has(.input--path) { flex: 1; min-width: 0; } /* ===== Buttons ===== */ .btn { display: inline-flex; align-items: center; justify-content: center; gap: var(--s-2);