Compare commits

..

58 Commits

Author SHA1 Message Date
627d98c199 Update README with new AI features and enhancements 2026-03-14 00:35:21 +01:00
5db45375f3 Add sanitizeAiResponse function call in App.tsx 2026-03-14 00:34:24 +01:00
cb814d94fa Add function to sanitize AI response 2026-03-14 00:33:21 +01:00
4393a40ad2 Add AI action dispatch and scope handling in App.tsx 2026-03-14 00:29:20 +01:00
7e854bd5d1 Remove unused AI action handling functions 2026-03-14 00:29:16 +01:00
1c664523a6 Add styles for modal selection preview 2026-03-14 00:15:03 +01:00
ebaa428f05 Update AI edit scope modal and custom prompt handling 2026-03-14 00:14:52 +01:00
97e9b26251 Update keydown event handling in src/App.tsx 2026-03-14 00:14:43 +01:00
73beaea077 Remove unused function openCustomPromptLightbox in src/App.tsx 2026-03-14 00:14:43 +01:00
9a42468cfd Update custom prompt handling in App.tsx 2026-03-14 00:14:29 +01:00
2b0023d3e1 Add AI action dispatch and scope handling in App.tsx 2026-03-14 00:14:22 +01:00
790848342c Enhance AI action handling with scope and selection support 2026-03-14 00:14:12 +01:00
f4e79276f0 Refactor state management in App.tsx 2026-03-14 00:13:53 +01:00
ef1201f753 Add types for AI action request and related components 2026-03-14 00:13:47 +01:00
d128ac92dc Add custom prompt feature and enhance keyboard handling 2026-03-14 00:06:44 +01:00
4432251158 Add custom prompt handling and update AI tools menu 2026-03-14 00:06:34 +01:00
79cd72f24f Add openCustomPromptLightbox function 2026-03-14 00:06:34 +01:00
727ef480c5 Add custom prompt state and handlers 2026-03-14 00:06:34 +01:00
2c20ab748e Add persistCurrentTextVersion function and update runAiAction to use it 2026-03-13 23:32:55 +01:00
cd2c4d3782 Remove unused variable in App.tsx 2026-03-13 23:14:20 +01:00
507a6f9f9a Update CSS for settings panel and prompt accordion 2026-03-13 23:09:31 +01:00
ee36277c21 Simplify and optimize AI prompt template deletion logic 2026-03-13 23:09:24 +01:00
f7a5c21d52 Ensure expandedPromptId is valid when aiPromptTemplates change 2026-03-13 23:09:19 +01:00
e5e24eb7f3 Reset expandedPromptId default value in App.tsx 2026-03-13 23:09:13 +01:00
07cfb06ee6 Update button disabled condition in App.tsx 2026-03-13 23:09:13 +01:00
6d414d1592 Refactor prompt management in App.tsx 2026-03-13 23:09:02 +01:00
f255f9a5a1 Simplify runAiAction and update AI tool handling 2026-03-13 23:08:47 +01:00
7df435d18d Refactor AI prompt handling in App.tsx 2026-03-13 23:08:19 +01:00
ffb83f9c71 Add function to get AI prompt template label and update state management 2026-03-13 23:08:12 +01:00
a4e863751b Refactor AI prompt handling and storage 2026-03-13 23:08:05 +01:00
e2b739ec6b Update settings panel width and add new styles 2026-03-13 22:58:31 +01:00
18f8ccc62a Refactor prompt section in App.tsx 2026-03-13 22:58:23 +01:00
625be7503d Update button click handler and label in App.tsx 2026-03-13 22:58:23 +01:00
4da563df70 Refactor AI action handling and add AI tools menu 2026-03-13 22:58:02 +01:00
f0765c1f42 Add functions to update AI prompt and add style preset 2026-03-13 22:57:01 +01:00
43e1fb1c79 Update App.tsx with new state variables and localStorage handling 2026-03-13 22:56:50 +01:00
b211342e8a Add AI prompt definitions and related functionality 2026-03-13 22:56:45 +01:00
f5f1b76bfb Enhance database export functionality and update CSS styles 2026-03-13 21:57:48 +01:00
d5bfefefd1 Refactor settings panel layout and add export database functionality 2026-03-13 21:57:42 +01:00
a7d5b58dbf Implement database export functionality 2026-03-13 21:57:08 +01:00
6fa837ab61 Reset database export status when settings are closed 2026-03-13 21:57:08 +01:00
a3947147d6 Add database export state variables 2026-03-13 21:57:08 +01:00
f9bea7ecaa Update App.tsx to include database export functionality 2026-03-13 21:56:44 +01:00
b40c8eae06 Add function to export database snapshot 2026-03-13 21:56:44 +01:00
14e1f40ecf Add logic to focus editor after creating new text 2026-03-13 21:54:19 +01:00
b2b0afdaf6 Add --md-surface-alt variable to markdown-render.css 2026-03-12 16:17:00 +01:00
f1c96d66e6 Add light theme styles for editor textarea wrap and markdown preview 2026-03-12 16:15:54 +01:00
e92af02673 Update markdown root surface alt color for dark theme 2026-03-12 16:14:42 +01:00
db20dd5107 Add background styling for dark theme in editor and markdown preview 2026-03-12 16:14:42 +01:00
56ab395eb2 Update CSS for dark theme in editor components 2026-03-12 16:12:33 +01:00
7671d6e39a Update editor theme extension in App.tsx 2026-03-12 16:00:54 +01:00
c8c7814db4 Update editor theme handling and caret color in CSS 2026-03-12 15:59:31 +01:00
7bbdab340c Add editor theme extension based on theme in App.tsx 2026-03-12 15:59:25 +01:00
41393e59bb Add themeCompartmentRef to App.tsx 2026-03-12 15:59:25 +01:00
fbd573d9e5 auto-git:
[change] src/index.css
2026-03-12 15:56:00 +01:00
30727b4bee auto-git:
[change] src/App.tsx
2026-03-11 11:08:51 +01:00
c7b7c40137 Update dependencies and add search functionality 2026-03-11 11:08:51 +01:00
e0e86b9199 auto-git:
[change] .gitignore
2026-03-11 10:29:11 +01:00
11 changed files with 1634 additions and 366 deletions

2
.gitignore vendored
View File

@@ -11,5 +11,5 @@ npm-debug.log*
yarn-debug.log*
yarn-error.log*
src-tauri/target/
src-tauri/gen/Cargo.lock
src-tauri/gen/
*.lock

View File

@@ -1,16 +1,19 @@
# TextDB
TextDB is a **non-destructive**, completely offline, local-first text editor with built-in Markdown preview **and local AI conversion**. Its core promise is simple: your edits never overwrite your history. Every manual save is immutable and append-only, and autosaves are kept as a separate draft layer. It can also convert plain text to Markdown using a local Ollama model (recommended: `ministral:3b`). No accounts, no telemetry, no remote fonts, and no outside network calls.
TextDB is a **non-destructive**, completely offline, local-first text editor with built-in Markdown preview and local AI writing tools. Its core promise is simple: your edits never overwrite your history. Every manual save is immutable and append-only, and autosaves are kept as a separate draft layer. With a local Ollama model (recommended: `ministral:3b`), it can convert plain text to Markdown, proofread, summarize, translate, rewrite, change style, or run a custom instruction against a whole text or a selected passage. No accounts, no telemetry, no remote fonts, and no outside network calls.
## Features
- Non-destructive editing: immutable manual saves + safe autosaved drafts.
- Version history panel with quick restore.
- Markdown preview mode with split view.
- Convert plain text to Markdown manually or via local Ollama (recommended model: `ministral:3b`).
- Markdown preview mode with split view, printable output, clickable links, and automatic raw URL detection.
- Local AI tools via Ollama for Markdown conversion, proofreading, summarizing, translation, rewriting, style changes, and custom prompts.
- AI edits can run on the whole text or just the selected part, and results are written back as drafts.
- In-document search with Cmd/Ctrl+F, result highlighting, and next / previous navigation.
- Live document stats in the footer: characters, words, sentences, and estimated tokens.
- Sort your texts in nested folders with expandable tree; move items around via right-click.
- Open/import `.txt` and `.md` files; export to `.txt` or print / `.pdf`.
- Settings for theme, text size, and optional display of line numbers in the textarea.
- Open/import text files from the file picker or system “Open With”; export to `.txt`, export the database, or print / `.pdf`.
- Settings for theme, text size, line numbers, split view, Ollama connection, and editable AI prompt templates.
## Use cases
@@ -19,6 +22,7 @@ TextDB is a **non-destructive**, completely offline, local-first text editor wit
- **Research notes**: maintain evolving notes with an audit trail of changes.
- **Sensitive drafts**: keep local-only writing with immutable history and zero cloud sync.
- **Prompt or snippet library**: store and iterate on reusable text safely.
- **Offline writing assistant**: run local AI edits without sending text to external services.
## Tech stack
@@ -40,12 +44,13 @@ npm install
npm run tauri dev
```
For AI based text-to-markdown conversion, install Ollama: `https://ollama.com`
For local AI tools, install Ollama: `https://ollama.com`
## Autosave + versioning behavior
- Typing triggers a debounced autosave (~600ms) that writes to the draft record.
- Manual saves (Cmd/Ctrl+S or the button) append a new version and clear any draft.
- AI edits preserve the current text state first when needed, then write the AI result back as a new draft.
- Autosave only overwrites the single draft row; manual versions are never overwritten.
- History shows all manual versions plus the current draft (if any).
@@ -54,4 +59,4 @@ For AI based text-to-markdown conversion, install Ollama: `https://ollama.com`
- SQLite is loaded via `Database.load("sqlite:text.db")`.
- The database file is stored in the Tauri app data directory.
- The app is fully offline by design: no telemetry, no external fonts, no CDNs.
- UI settings (theme, text size, line numbers, sidebar + folder collapse state, last selected text) are stored in localStorage.
- UI settings (theme, text size, line numbers, split view, sidebar + folder collapse state, last selected text, and AI prompt templates) are stored in localStorage.

View File

@@ -11,6 +11,7 @@
},
"dependencies": {
"@codemirror/commands": "^6.10.1",
"@codemirror/search": "^6.6.0",
"@codemirror/state": "^6.5.4",
"@codemirror/view": "^6.39.12",
"@tauri-apps/api": "^2.0.0",

142
src-tauri/Cargo.lock generated
View File

@@ -1574,9 +1574,9 @@ dependencies = [
[[package]]
name = "ico"
version = "0.4.0"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc50b891e4acf8fe0e71ef88ec43ad82ee07b3810ad09de10f1d01f072ed4b98"
checksum = "3e795dff5605e0f04bff85ca41b51a96b83e80b281e96231bcaaf1ac35103371"
dependencies = [
"byteorder",
"png 0.17.16",
@@ -2244,38 +2244,9 @@ checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c"
dependencies = [
"bitflags 2.10.0",
"block2",
"libc",
"objc2",
"objc2-cloud-kit",
"objc2-core-data",
"objc2-core-foundation",
"objc2-core-graphics",
"objc2-core-image",
"objc2-core-text",
"objc2-core-video",
"objc2-foundation",
"objc2-quartz-core",
]
[[package]]
name = "objc2-cloud-kit"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c"
dependencies = [
"bitflags 2.10.0",
"objc2",
"objc2-foundation",
]
[[package]]
name = "objc2-core-data"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa"
dependencies = [
"bitflags 2.10.0",
"objc2",
"objc2-foundation",
]
@@ -2303,41 +2274,6 @@ dependencies = [
"objc2-io-surface",
]
[[package]]
name = "objc2-core-image"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006"
dependencies = [
"objc2",
"objc2-foundation",
]
[[package]]
name = "objc2-core-text"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d"
dependencies = [
"bitflags 2.10.0",
"objc2",
"objc2-core-foundation",
"objc2-core-graphics",
]
[[package]]
name = "objc2-core-video"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d425caf1df73233f29fd8a5c3e5edbc30d2d4307870f802d18f00d83dc5141a6"
dependencies = [
"bitflags 2.10.0",
"objc2",
"objc2-core-foundation",
"objc2-core-graphics",
"objc2-io-surface",
]
[[package]]
name = "objc2-encode"
version = "4.1.0"
@@ -2361,7 +2297,6 @@ checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272"
dependencies = [
"bitflags 2.10.0",
"block2",
"libc",
"objc2",
"objc2-core-foundation",
]
@@ -2377,16 +2312,6 @@ dependencies = [
"objc2-core-foundation",
]
[[package]]
name = "objc2-javascript-core"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a1e6550c4caed348956ce3370c9ffeca70bb1dbed4fa96112e7c6170e074586"
dependencies = [
"objc2",
"objc2-core-foundation",
]
[[package]]
name = "objc2-quartz-core"
version = "0.3.2"
@@ -2399,17 +2324,6 @@ dependencies = [
"objc2-foundation",
]
[[package]]
name = "objc2-security"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "709fe137109bd1e8b5a99390f77a7d8b2961dafc1a1c5db8f2e60329ad6d895a"
dependencies = [
"bitflags 2.10.0",
"objc2",
"objc2-core-foundation",
]
[[package]]
name = "objc2-ui-kit"
version = "0.3.2"
@@ -2434,8 +2348,6 @@ dependencies = [
"objc2-app-kit",
"objc2-core-foundation",
"objc2-foundation",
"objc2-javascript-core",
"objc2-security",
]
[[package]]
@@ -3074,9 +2986,9 @@ checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58"
[[package]]
name = "reqwest"
version = "0.12.28"
version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147"
checksum = "ab3f43e3283ab1488b624b44b0e988d0acea0b3214e694730a055cb6b2efa801"
dependencies = [
"base64 0.22.1",
"bytes",
@@ -3093,7 +3005,6 @@ dependencies = [
"pin-project-lite",
"serde",
"serde_json",
"serde_urlencoded",
"sync_wrapper",
"tokio",
"tokio-util",
@@ -4010,9 +3921,9 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
[[package]]
name = "tauri"
version = "2.9.5"
version = "2.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a3868da5508446a7cd08956d523ac3edf0a8bc20bf7e4038f9a95c2800d2033"
checksum = "da77cc00fb9028caf5b5d4650f75e31f1ef3693459dfca7f7e506d1ecef0ba2d"
dependencies = [
"anyhow",
"bytes",
@@ -4061,9 +3972,9 @@ dependencies = [
[[package]]
name = "tauri-build"
version = "2.5.3"
version = "2.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17fcb8819fd16463512a12f531d44826ce566f486d7ccd211c9c8cebdaec4e08"
checksum = "4bbc990d1dbf57a8e1c7fa2327f2a614d8b757805603c1b9ba5c81bade09fd4d"
dependencies = [
"anyhow",
"cargo_toml",
@@ -4083,9 +3994,9 @@ dependencies = [
[[package]]
name = "tauri-codegen"
version = "2.5.2"
version = "2.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fa9844cefcf99554a16e0a278156ae73b0d8680bbc0e2ad1e4287aadd8489cf"
checksum = "d4a24476afd977c5d5d169f72425868613d82747916dd29e0a357c84c4bd6d29"
dependencies = [
"base64 0.22.1",
"brotli",
@@ -4110,9 +4021,9 @@ dependencies = [
[[package]]
name = "tauri-macros"
version = "2.5.2"
version = "2.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3764a12f886d8245e66b7ee9b43ccc47883399be2019a61d80cf0f4117446fde"
checksum = "d39b349a98dadaffebb73f0a40dcd1f23c999211e5a2e744403db384d0c33de7"
dependencies = [
"heck 0.5.0",
"proc-macro2",
@@ -4236,9 +4147,9 @@ dependencies = [
[[package]]
name = "tauri-runtime"
version = "2.9.2"
version = "2.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87f766fe9f3d1efc4b59b17e7a891ad5ed195fa8d23582abb02e6c9a01137892"
checksum = "2826d79a3297ed08cd6ea7f412644ef58e32969504bc4fbd8d7dbeabc4445ea2"
dependencies = [
"cookie",
"dpi",
@@ -4261,9 +4172,9 @@ dependencies = [
[[package]]
name = "tauri-runtime-wry"
version = "2.9.3"
version = "2.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "187a3f26f681bdf028f796ccf57cf478c1ee422c50128e5a0a6ebeb3f5910065"
checksum = "e11ea2e6f801d275fdd890d6c9603736012742a1c33b96d0db788c9cdebf7f9e"
dependencies = [
"gtk",
"http",
@@ -4271,7 +4182,6 @@ dependencies = [
"log",
"objc2",
"objc2-app-kit",
"objc2-foundation",
"once_cell",
"percent-encoding",
"raw-window-handle",
@@ -4288,9 +4198,9 @@ dependencies = [
[[package]]
name = "tauri-utils"
version = "2.8.1"
version = "2.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76a423c51176eb3616ee9b516a9fa67fed5f0e78baaba680e44eb5dd2cc37490"
checksum = "219a1f983a2af3653f75b5747f76733b0da7ff03069c7a41901a5eb3ace4557d"
dependencies = [
"anyhow",
"brotli",
@@ -5001,9 +4911,9 @@ dependencies = [
[[package]]
name = "wasm-streams"
version = "0.4.2"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65"
checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb"
dependencies = [
"futures-util",
"js-sys",
@@ -5094,9 +5004,9 @@ dependencies = [
[[package]]
name = "webkit2gtk"
version = "2.0.1"
version = "2.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76b1bc1e54c581da1e9f179d0b38512ba358fb1af2d634a1affe42e37172361a"
checksum = "a1027150013530fb2eaf806408df88461ae4815a45c541c8975e61d6f2fc4793"
dependencies = [
"bitflags 1.3.2",
"cairo-rs",
@@ -5118,9 +5028,9 @@ dependencies = [
[[package]]
name = "webkit2gtk-sys"
version = "2.0.1"
version = "2.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62daa38afc514d1f8f12b8693d30d5993ff77ced33ce30cd04deebc267a6d57c"
checksum = "916a5f65c2ef0dfe12fff695960a2ec3d4565359fdbb2e9943c974e06c734ea5"
dependencies = [
"bitflags 1.3.2",
"cairo-sys-rs",
@@ -5739,9 +5649,9 @@ checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9"
[[package]]
name = "wry"
version = "0.53.5"
version = "0.54.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "728b7d4c8ec8d81cab295e0b5b8a4c263c0d41a785fb8f8c4df284e5411140a2"
checksum = "bb26159b420aa77684589a744ae9a9461a95395b848764ad12290a14d960a11a"
dependencies = [
"base64 0.22.1",
"block2",

View File

@@ -5,12 +5,12 @@ description = "TextDB"
edition = "2021"
[build-dependencies]
tauri-build = { version = "2", features = [] }
tauri-build = { version = "2.5", features = [] }
[dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tauri = { version = "2", features = [] }
tauri = { version = "2.10", features = [] }
tauri-plugin-clipboard-manager = { version = "2" }
tauri-plugin-dialog = { version = "2" }
tauri-plugin-fs = { version = "2" }

View File

@@ -63,7 +63,8 @@
},
{
"ext": [
"md"
"md",
"markdown"
],
"name": "Markdown Document",
"description": "Markdown document",
@@ -71,8 +72,7 @@
"role": "Editor",
"rank": "Default",
"contentTypes": [
"net.daringfireball.markdown",
"public.plain-text"
"net.daringfireball.markdown"
]
}
]

File diff suppressed because it is too large Load Diff

View File

@@ -135,6 +135,7 @@ body {
border: 1px solid var(--border);
background: var(--bg-elevated);
color: var(--ink);
caret-color: var(--ink);
font-size: 0.95rem;
}
@@ -237,6 +238,7 @@ body {
font: inherit;
color: var(--ink);
background: var(--bg-input);
caret-color: var(--ink);
border: 1px solid var(--border);
border-radius: 10px;
padding: 6px 10px;
@@ -360,6 +362,7 @@ body:not([data-theme="light"]) .folder-item {
font: inherit;
color: var(--ink);
background: var(--bg-input);
caret-color: var(--ink);
border: 1px solid var(--border);
border-radius: 10px;
padding: 6px 10px;
@@ -489,6 +492,7 @@ body:not([data-theme="light"]) .folder-item {
padding: 6px 2px 8px;
background: transparent;
color: var(--ink);
caret-color: var(--ink);
}
.title-input:focus {
@@ -504,6 +508,11 @@ body:not([data-theme="light"]) .folder-item {
color: var(--muted);
}
.status-line--secondary {
margin-top: 4px;
font-size: 0.82rem;
}
.icon-button {
display: inline-flex;
align-items: center;
@@ -591,10 +600,26 @@ body:not([data-theme="light"]) .folder-item {
border-color: transparent;
}
body[data-theme="light"] .editor__textarea-wrap--preview {
background: var(--bg);
}
body:not([data-theme="light"]) .editor__textarea-wrap--preview {
background: var(--bg);
}
.editor__textarea-wrap--split {
flex-direction: row;
}
body[data-theme="light"] .editor__textarea-wrap--split {
background: var(--bg);
}
body:not([data-theme="light"]) .editor__textarea-wrap--split {
background: var(--bg);
}
.editor__textarea-wrap--split .editor__codemirror {
flex: 1;
border-right: 1px solid var(--border);
@@ -604,6 +629,14 @@ body:not([data-theme="light"]) .folder-item {
flex: 1;
}
body[data-theme="light"] .editor__textarea-wrap--split .markdown-preview {
background: var(--bg);
}
body:not([data-theme="light"]) .editor__textarea-wrap--split .markdown-preview {
background: var(--bg);
}
.editor__textarea-wrap:focus-within {
border-color: rgba(255, 255, 255, 0.18);
}
@@ -627,6 +660,16 @@ body:not([data-theme="light"]) .folder-item {
line-height: 1.5;
}
.editor__codemirror .cm-editor.cm-focused > .cm-scroller > .cm-cursorLayer .cm-cursor,
.editor__codemirror .cm-dropCursor {
border-left-color: #f5f5f5 !important;
border-left-width: 1.6px !important;
}
.editor__codemirror .cm-fatCursor {
background: rgba(245, 245, 245, 0.45);
}
.editor__codemirror .cm-scroller {
overflow: auto;
}
@@ -649,6 +692,76 @@ body:not([data-theme="light"]) .folder-item {
background: transparent;
}
.editor__codemirror .cm-panels {
background: var(--bg-elevated);
color: var(--ink);
border-bottom: 1px solid var(--border);
}
.editor__codemirror .cm-panels-top {
border-bottom: 1px solid var(--border);
}
.editor__codemirror .cm-search {
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: center;
padding: 10px 12px;
}
.editor__codemirror .cm-search label {
display: inline-flex;
align-items: center;
gap: 6px;
color: var(--muted);
}
.editor__codemirror .cm-search input,
.editor__codemirror .cm-search button {
font: inherit;
}
.editor__codemirror .cm-search input[type="text"] {
min-width: 220px;
padding: 8px 10px;
border-radius: 10px;
border: 1px solid var(--border);
background: var(--bg-input);
color: var(--ink);
}
.editor__codemirror .cm-search button {
border-radius: 10px;
border: 1px solid var(--border);
padding: 8px 10px;
background: var(--bg-input);
color: var(--ink);
cursor: pointer;
}
.editor__codemirror .cm-search button:hover {
border-color: var(--accent-strong);
}
.editor__codemirror .cm-search [name="close"] {
padding-left: 12px;
padding-right: 12px;
}
.editor__codemirror .cm-search input[type="checkbox"] {
accent-color: var(--ink);
}
.editor__codemirror .cm-searchMatch {
background: rgba(214, 214, 96, 0.28);
outline: 1px solid rgba(214, 214, 96, 0.38);
}
.editor__codemirror .cm-searchMatch.cm-searchMatch-selected {
background: rgba(233, 216, 112, 0.42);
}
.markdown-preview {
flex: 1;
width: 100%;
@@ -657,6 +770,14 @@ body:not([data-theme="light"]) .folder-item {
color: var(--ink);
}
body[data-theme="light"] .markdown-preview {
background: var(--bg);
}
body:not([data-theme="light"]) .markdown-preview {
background: var(--bg);
}
.markdown-preview--hidden {
display: none;
}
@@ -670,6 +791,19 @@ body:not([data-theme="light"]) .folder-item {
.editor__footer-status {
margin-left: auto;
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 4px;
}
.editor__stats {
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
gap: 10px;
font-size: 0.82rem;
color: var(--muted);
}
.banner {
@@ -829,6 +963,10 @@ body:not([data-theme="light"]) .folder-item {
gap: 12px;
}
.modal__card--wide {
width: min(560px, 92vw);
}
.modal__title {
font-size: 1.2rem;
}
@@ -837,6 +975,37 @@ body:not([data-theme="light"]) .folder-item {
color: var(--muted);
}
.modal__textarea {
width: 100%;
min-height: 170px;
resize: vertical;
border-radius: 12px;
border: 1px solid var(--border);
background: var(--bg-input);
color: var(--ink);
caret-color: var(--ink);
padding: 12px 14px;
font: inherit;
}
.modal__hint {
font-size: 0.8rem;
color: var(--muted);
}
.modal__selection-preview {
max-height: 180px;
overflow: auto;
white-space: pre-wrap;
word-break: break-word;
border-radius: 12px;
border: 1px solid var(--border);
background: var(--bg-input);
color: var(--ink);
padding: 12px 14px;
font-size: 0.9rem;
}
.modal__actions {
display: flex;
justify-content: flex-end;
@@ -927,6 +1096,8 @@ body:not([data-theme="light"]) .folder-item {
inset: 0;
display: grid;
place-items: center;
padding: 16px;
overflow: auto;
z-index: 30;
}
@@ -939,7 +1110,8 @@ body:not([data-theme="light"]) .folder-item {
.settings-panel {
position: relative;
z-index: 1;
width: min(420px, 92vw);
width: min(560px, calc(100vw - 32px));
max-height: calc(100vh - 32px);
padding: 20px;
border-radius: 16px;
background: var(--bg-elevated);
@@ -948,6 +1120,7 @@ body:not([data-theme="light"]) .folder-item {
display: flex;
flex-direction: column;
gap: 16px;
overflow: hidden;
}
.settings-panel__header {
@@ -960,12 +1133,26 @@ body:not([data-theme="light"]) .folder-item {
font-size: 1.1rem;
}
.settings-panel__body {
display: flex;
flex-direction: column;
gap: 16px;
min-height: 0;
overflow-y: auto;
padding-right: 4px;
margin-right: -4px;
}
.settings-panel__section {
display: flex;
flex-direction: column;
gap: 8px;
}
.settings-panel__section--flush {
gap: 10px;
}
.settings-panel__section-title {
font-size: 0.75rem;
letter-spacing: 0.08em;
@@ -985,6 +1172,7 @@ body:not([data-theme="light"]) .folder-item {
border: 1px solid var(--border);
background: var(--bg-input);
color: var(--ink);
caret-color: var(--ink);
padding: 8px 10px;
font-size: 0.95rem;
}
@@ -1025,10 +1213,82 @@ body:not([data-theme="light"]) .folder-item {
color: var(--muted);
}
.settings-panel__status {
font-size: 0.8rem;
word-break: break-word;
}
.settings-panel__status--success {
color: var(--ink);
}
.settings-panel__status--error {
color: #d36b6b;
}
.prompt-accordion {
border: 1px solid var(--border);
border-radius: 12px;
background: var(--bg-input);
overflow: hidden;
}
.prompt-accordion__toggle {
width: 100%;
border: none;
background: transparent;
color: var(--ink);
padding: 12px 14px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
font: inherit;
text-align: left;
cursor: pointer;
}
.prompt-accordion__indicator {
color: var(--muted);
font-size: 1.1rem;
line-height: 1;
}
.prompt-accordion__body {
display: flex;
flex-direction: column;
gap: 12px;
padding: 0 14px 14px;
}
.prompt-accordion__actions {
display: flex;
justify-content: flex-end;
}
body[data-theme="light"] .editor__textarea-wrap:focus-within {
border-color: var(--accent-strong);
}
body[data-theme="light"] .editor__codemirror .cm-editor.cm-focused > .cm-scroller > .cm-cursorLayer .cm-cursor,
body[data-theme="light"] .editor__codemirror .cm-dropCursor {
border-left-color: #1f1f1f !important;
border-left-width: 1.6px !important;
}
body[data-theme="light"] .editor__codemirror .cm-fatCursor {
background: rgba(31, 31, 31, 0.28);
}
body[data-theme="light"] .editor__codemirror .cm-searchMatch {
background: rgba(219, 199, 81, 0.3);
outline-color: rgba(170, 141, 33, 0.28);
}
body[data-theme="light"] .editor__codemirror .cm-searchMatch.cm-searchMatch-selected {
background: rgba(219, 199, 81, 0.44);
}
.settings-panel__section--row {
flex-direction: row;
align-items: center;

View File

@@ -434,3 +434,9 @@ export async function deleteTextVersion(promptId: string, versionId: string) {
[nextId, promptId]
);
}
export async function exportDatabaseSnapshot(destinationPath: string) {
const db = await getDb();
const escapedPath = destinationPath.replace(/'/g, "''");
await db.execute(`VACUUM INTO '${escapedPath}'`);
}

View File

@@ -17,9 +17,14 @@
"Liberation Mono", "Courier New", monospace;
}
body:not([data-theme="light"]) .md-root {
--md-surface-alt: var(--bg);
}
body[data-theme="light"] .md-root {
--md-link-color: #0a66c2;
--md-link-hover-color: #004182;
--md-surface-alt: var(--bg);
--md-code-inline-bg: rgba(0, 0, 0, 0.06);
}

View File

@@ -179,15 +179,55 @@ export function markdownToHTML(text) {
return '';
};
html = html.replace(/\[([^\]]+?)\]\(([^)]+?)\)/g, (_, label, href) => {
const url = safeLink(href);
const tooltip = escapeHtml(href || '');
const renderExternalLink = (label, hrefRaw) => {
const url = safeLink(hrefRaw);
const tooltip = escapeHtml(hrefRaw || '');
if (!url) return label;
return `<a class="md-link md-link--external" href="${escapeAttr(
url
)}" target="_blank" rel="noreferrer noopener"><span class="md-link__label">${label}</span> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="md-icon md-icon-external"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path><polyline points="15 3 21 3 21 9"></polyline><line x1="10" y1="14" x2="21" y2="3"></line></svg><span class="md-link__tooltip">${tooltip}</span></a>`;
};
html = html.replace(/\[([^\]]+?)\]\(([^)]+?)\)/g, (_, label, href) => {
return renderExternalLink(label, href);
});
const autoLinkPlainUrls = (source) => {
const protectedChunkRe = /(<a\b[\s\S]*?<\/a>|<code>[\s\S]*?<\/code>|@@CODEBLOCK\d+@@)/g;
const isProtectedChunk = /^(<a\b[\s\S]*<\/a>|<code>[\s\S]*<\/code>|@@CODEBLOCK\d+@@)$/;
const trimTrailingPunctuation = (value) => {
let linked = value;
let trailing = '';
while (linked.length > 0) {
const last = linked[linked.length - 1];
if (!/[)\].,!?;:]/.test(last)) break;
if (last === ')') {
const opens = (linked.match(/\(/g) || []).length;
const closes = (linked.match(/\)/g) || []).length;
if (closes <= opens) break;
}
linked = linked.slice(0, -1);
trailing = last + trailing;
}
return { linked, trailing };
};
return source
.split(protectedChunkRe)
.map((part) => {
if (!part || isProtectedChunk.test(part)) return part;
return part.replace(/(^|[\s(>])((?:https?:\/\/|www\.)[^\s<]+)/g, (_, lead, rawUrl) => {
const { linked, trailing } = trimTrailingPunctuation(rawUrl);
if (!linked) return `${lead}${rawUrl}`;
const href = /^www\./i.test(linked) ? `https://${linked}` : linked;
return `${lead}${renderExternalLink(linked, href)}${trailing}`;
});
})
.join('');
};
html = autoLinkPlainUrls(html);
// 6) Convert line-breaks to HTML paragraphs and <br /> inside paragraphs
const linesWithHtml = html.split("\n");
const htmlLines = [];