- Move Windows storage to DPAPI-backed local app data - Keep non-Windows storage on the native credential store - Update docs and UI copy to describe the new behavior
256 lines
7.5 KiB
TypeScript
256 lines
7.5 KiB
TypeScript
import { invoke } from "@tauri-apps/api/core";
|
|
import { listen } from "@tauri-apps/api/event";
|
|
import * as diff from "diff";
|
|
|
|
type RewriteMetrics = {
|
|
duration_ms: number;
|
|
input_length: number;
|
|
model: string;
|
|
success: boolean;
|
|
error_category?: string | null;
|
|
};
|
|
|
|
type RuntimeStatus = {
|
|
hotkey_registered: boolean;
|
|
background_mode: boolean;
|
|
active_model: string;
|
|
api_key_configured: boolean;
|
|
last_error?: string | null;
|
|
rewrite_attempts: number;
|
|
rewrite_successes: number;
|
|
rewrite_failures: number;
|
|
last_metrics?: RewriteMetrics | null;
|
|
};
|
|
|
|
function requireElement<T extends HTMLElement>(id: string): T {
|
|
const element = document.getElementById(id);
|
|
if (!element) {
|
|
throw new Error(`Missing required element: ${id}`);
|
|
}
|
|
return element as T;
|
|
}
|
|
|
|
const inputEl = requireElement<HTMLTextAreaElement>("input-text");
|
|
const outputEl = requireElement<HTMLTextAreaElement>("output-text");
|
|
const modelSelect = requireElement<HTMLSelectElement>("model-select");
|
|
const modeSelect = requireElement<HTMLSelectElement>("mode-select");
|
|
const rewriteBtn = requireElement<HTMLButtonElement>("rewrite-btn");
|
|
const copyBtn = requireElement<HTMLButtonElement>("copy-btn");
|
|
const toggleDiffBtn = requireElement<HTMLButtonElement>("toggle-diff-btn");
|
|
const diffView = requireElement<HTMLDivElement>("diff-view");
|
|
const loadingMsg = requireElement<HTMLDivElement>("loading");
|
|
const settingsBtn = requireElement<HTMLButtonElement>("settings-btn");
|
|
const settingsModal = requireElement<HTMLDivElement>("settings-modal");
|
|
const closeSettingsBtn = requireElement<HTMLButtonElement>("close-settings-btn");
|
|
const saveSettingsBtn = requireElement<HTMLButtonElement>("save-settings-btn");
|
|
const apiKeyInput = requireElement<HTMLInputElement>("api-key-input");
|
|
const runtimeStatusEl = requireElement<HTMLDivElement>("runtime-status");
|
|
const helperBanner = requireElement<HTMLDivElement>("helper-banner");
|
|
const helperBannerText = requireElement<HTMLParagraphElement>("helper-banner-text");
|
|
const hideToTrayBtn = requireElement<HTMLButtonElement>("hide-to-tray-btn");
|
|
const dismissBannerBtn = requireElement<HTMLButtonElement>("dismiss-banner-btn");
|
|
|
|
let showDiff = false;
|
|
|
|
function renderRuntimeStatus(status: RuntimeStatus) {
|
|
const metricParts: string[] = [];
|
|
if (status.last_metrics) {
|
|
metricParts.push(`${status.last_metrics.duration_ms} ms`);
|
|
metricParts.push(`${status.last_metrics.input_length} chars`);
|
|
metricParts.push(status.last_metrics.success ? "last run ok" : "last run failed");
|
|
if (status.last_metrics.error_category) {
|
|
metricParts.push(status.last_metrics.error_category);
|
|
}
|
|
}
|
|
|
|
const lines = [
|
|
`Hotkey: ${status.hotkey_registered ? "Ctrl + Alt + R active" : "not registered"}`,
|
|
`Window: ${status.background_mode ? "hidden to tray" : "visible"}`,
|
|
`Model: ${status.active_model}`,
|
|
`API key: ${status.api_key_configured ? "configured" : "missing"}`,
|
|
`Rewrites: ${status.rewrite_successes}/${status.rewrite_attempts} successful`,
|
|
];
|
|
|
|
if (metricParts.length > 0) {
|
|
lines.push(`Last run: ${metricParts.join(" | ")}`);
|
|
}
|
|
|
|
if (status.last_error) {
|
|
lines.push(`Last error: ${status.last_error}`);
|
|
}
|
|
|
|
runtimeStatusEl.textContent = lines.join(" | ");
|
|
}
|
|
|
|
async function refreshRuntimeStatus() {
|
|
try {
|
|
const status = await invoke<RuntimeStatus>("get_runtime_status");
|
|
renderRuntimeStatus(status);
|
|
} catch (error) {
|
|
console.error("Failed to load runtime status", error);
|
|
}
|
|
}
|
|
|
|
function renderDiff(original: string, rewritten: string) {
|
|
const diffResult = diff.diffWords(original, rewritten);
|
|
diffView.innerHTML = "";
|
|
diffResult.forEach((part) => {
|
|
const span = document.createElement("span");
|
|
span.textContent = part.value;
|
|
if (part.added) {
|
|
span.className = "diff-add";
|
|
} else if (part.removed) {
|
|
span.className = "diff-del";
|
|
}
|
|
diffView.appendChild(span);
|
|
});
|
|
}
|
|
|
|
function showBanner(message: string) {
|
|
helperBannerText.textContent = message;
|
|
helperBanner.classList.remove("hidden");
|
|
}
|
|
|
|
function hideBanner() {
|
|
helperBanner.classList.add("hidden");
|
|
}
|
|
|
|
modelSelect.addEventListener("change", async () => {
|
|
try {
|
|
await invoke("set_active_model", { model: modelSelect.value });
|
|
await refreshRuntimeStatus();
|
|
} catch (error) {
|
|
console.error("Failed to sync active model", error);
|
|
}
|
|
});
|
|
|
|
void invoke("set_active_model", { model: modelSelect.value })
|
|
.then(refreshRuntimeStatus)
|
|
.catch((error) => {
|
|
console.error("Failed to initialize active model", error);
|
|
});
|
|
|
|
settingsBtn.addEventListener("click", () => {
|
|
settingsModal.classList.remove("hidden");
|
|
});
|
|
|
|
closeSettingsBtn.addEventListener("click", () => {
|
|
settingsModal.classList.add("hidden");
|
|
});
|
|
|
|
dismissBannerBtn.addEventListener("click", hideBanner);
|
|
|
|
hideToTrayBtn.addEventListener("click", async () => {
|
|
try {
|
|
await invoke("hide_main_window");
|
|
hideBanner();
|
|
await refreshRuntimeStatus();
|
|
} catch (error) {
|
|
console.error("Failed to hide main window", error);
|
|
}
|
|
});
|
|
|
|
saveSettingsBtn.addEventListener("click", async () => {
|
|
const key = apiKeyInput.value.trim();
|
|
if (!key) return;
|
|
|
|
try {
|
|
saveSettingsBtn.disabled = true;
|
|
saveSettingsBtn.textContent = "Saving...";
|
|
await invoke("save_api_key", { key });
|
|
settingsModal.classList.add("hidden");
|
|
apiKeyInput.value = "";
|
|
await refreshRuntimeStatus();
|
|
alert("API key saved using native platform protection.");
|
|
} catch (err) {
|
|
alert("Error saving API key: " + err);
|
|
} finally {
|
|
saveSettingsBtn.disabled = false;
|
|
saveSettingsBtn.textContent = "Save Settings";
|
|
}
|
|
});
|
|
|
|
async function checkStatus() {
|
|
try {
|
|
const isSet = await invoke<boolean>("get_api_key_status");
|
|
if (!isSet) {
|
|
settingsModal.classList.remove("hidden");
|
|
}
|
|
} catch (error) {
|
|
console.error("Status check failed", error);
|
|
}
|
|
}
|
|
|
|
rewriteBtn.addEventListener("click", async () => {
|
|
const text = inputEl.value;
|
|
if (!text.trim()) return;
|
|
|
|
loadingMsg.classList.remove("hidden");
|
|
outputEl.classList.add("hidden");
|
|
diffView.classList.add("hidden");
|
|
rewriteBtn.disabled = true;
|
|
|
|
try {
|
|
const mode = modeSelect.value;
|
|
const model = modelSelect.value;
|
|
const result = await invoke<string>("rewrite_text", {
|
|
text,
|
|
modeStr: mode,
|
|
modelStr: model,
|
|
});
|
|
|
|
outputEl.value = result;
|
|
renderDiff(text, result);
|
|
|
|
if (showDiff) {
|
|
diffView.classList.remove("hidden");
|
|
} else {
|
|
outputEl.classList.remove("hidden");
|
|
}
|
|
} catch (error) {
|
|
console.error("Rewrite failed:", error);
|
|
outputEl.value = `Error: ${error}`;
|
|
outputEl.classList.remove("hidden");
|
|
} finally {
|
|
loadingMsg.classList.add("hidden");
|
|
rewriteBtn.disabled = false;
|
|
await refreshRuntimeStatus();
|
|
}
|
|
});
|
|
|
|
toggleDiffBtn.addEventListener("click", () => {
|
|
showDiff = !showDiff;
|
|
if (showDiff) {
|
|
toggleDiffBtn.textContent = "Show Text";
|
|
outputEl.classList.add("hidden");
|
|
diffView.classList.remove("hidden");
|
|
} else {
|
|
toggleDiffBtn.textContent = "Show Diff";
|
|
diffView.classList.add("hidden");
|
|
outputEl.classList.remove("hidden");
|
|
}
|
|
});
|
|
|
|
copyBtn.addEventListener("click", async () => {
|
|
try {
|
|
await navigator.clipboard.writeText(outputEl.value);
|
|
const originalText = copyBtn.textContent;
|
|
copyBtn.textContent = "Copied!";
|
|
setTimeout(() => {
|
|
copyBtn.textContent = originalText;
|
|
}, 2000);
|
|
} catch (error) {
|
|
console.error("Failed to copy", error);
|
|
}
|
|
});
|
|
|
|
void listen<string>("tray-hint", (event) => {
|
|
showBanner(event.payload);
|
|
});
|
|
|
|
void listen<RuntimeStatus>("runtime-status", (event) => {
|
|
renderRuntimeStatus(event.payload);
|
|
});
|
|
|
|
void Promise.all([checkStatus(), refreshRuntimeStatus()]);
|