From 9675e05b077d462e6332d4afc1d697a90a3d7748 Mon Sep 17 00:00:00 2001 From: itsamejms Date: Sun, 28 Jun 2026 22:36:30 +0100 Subject: [PATCH] v0.2.0-llm: LLM integration, core utilities, and persistence - Rust backend: LLM abstraction with OpenAI-compatible + Ollama API clients - Tauri commands: generate, generate_stream, get/set LLM config - LLM config: api_url, api_key, model, temperature, max_tokens, top_p - Works with Ollama (localhost:11434), LM Studio, any OpenAI-compatible API - Streaming infrastructure via Tauri Channels (LlmEvent) - tauri-plugin-store, tauri-plugin-fs added - Frontend: NPC Generator with race/class/alignment selection + AI generation - Frontend: Dice Roller with notation parsing (2d6+3), presets, history - Frontend: Session Logger with note-taking + AI summarization - Frontend: Settings panel for LLM configuration (URL, key, model, temperature) - App shell: left rail nav with active states, settings toggle - All glassmorphism cards with gold glow hover states - Builds clean: tsc, vite build, cargo check, tauri build --- src-tauri/Cargo.lock | 504 ++++++++++++++++++++++++- src-tauri/Cargo.toml | 6 +- src-tauri/capabilities/default.json | 15 +- src-tauri/src/commands/llm_commands.rs | 166 ++++++++ src-tauri/src/commands/mod.rs | 1 + src-tauri/src/lib.rs | 28 +- src-tauri/src/llm/mod.rs | 108 ++++++ src/App.tsx | 38 +- src/components/Dashboard.tsx | 48 +-- src/components/DiceRoller.tsx | 117 ++++++ src/components/NpcGenerator.tsx | 140 +++++++ src/components/SessionLogger.tsx | 104 +++++ src/components/SettingsPanel.tsx | 139 +++++++ 13 files changed, 1358 insertions(+), 56 deletions(-) create mode 100644 src-tauri/src/commands/llm_commands.rs create mode 100644 src-tauri/src/commands/mod.rs create mode 100644 src-tauri/src/llm/mod.rs create mode 100644 src/components/DiceRoller.tsx create mode 100644 src/components/NpcGenerator.tsx create mode 100644 src/components/SessionLogger.tsx create mode 100644 src/components/SettingsPanel.tsx diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index cbd3342..5e78b0e 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -451,6 +451,16 @@ dependencies = [ "version_check", ] +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation" version = "0.10.1" @@ -474,9 +484,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97" dependencies = [ "bitflags 2.13.0", - "core-foundation", + "core-foundation 0.10.1", "core-graphics-types", - "foreign-types", + "foreign-types 0.5.0", "libc", ] @@ -487,7 +497,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" dependencies = [ "bitflags 2.13.0", - "core-foundation", + "core-foundation 0.10.1", "libc", ] @@ -730,11 +740,15 @@ name = "dm-pal" version = "0.1.0" dependencies = [ "log", + "reqwest 0.12.28", "serde", "serde_json", "tauri", "tauri-build", + "tauri-plugin-fs", "tauri-plugin-log", + "tauri-plugin-store", + "tokio", ] [[package]] @@ -823,6 +837,15 @@ version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + [[package]] name = "env_filter" version = "0.1.4" @@ -850,6 +873,16 @@ dependencies = [ "typeid", ] +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + [[package]] name = "fastrand" version = "2.4.1" @@ -912,6 +945,15 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared 0.1.1", +] + [[package]] name = "foreign-types" version = "0.5.0" @@ -919,7 +961,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" dependencies = [ "foreign-types-macros", - "foreign-types-shared", + "foreign-types-shared 0.3.1", ] [[package]] @@ -933,6 +975,12 @@ dependencies = [ "syn 2.0.118", ] +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "foreign-types-shared" version = "0.3.1" @@ -1316,6 +1364,25 @@ dependencies = [ "syn 2.0.118", ] +[[package]] +name = "h2" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cb093c84e8bd9b188d4c4a8cb6579fc016968d14c99882163cd3ff402a4f155" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap 2.14.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -1408,6 +1475,7 @@ dependencies = [ "bytes", "futures-channel", "futures-core", + "h2", "http", "http-body", "httparse", @@ -1418,6 +1486,37 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-rustls" +version = "0.27.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + [[package]] name = "hyper-util" version = "0.1.20" @@ -1436,9 +1535,11 @@ dependencies = [ "percent-encoding", "pin-project-lite", "socket2", + "system-configuration", "tokio", "tower-service", "tracing", + "windows-registry", ] [[package]] @@ -1797,6 +1898,12 @@ dependencies = [ "libc", ] +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + [[package]] name = "litemap" version = "0.8.2" @@ -1895,6 +2002,23 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "native-tls" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + [[package]] name = "ndk" version = "0.9.0" @@ -2094,6 +2218,7 @@ checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" dependencies = [ "bitflags 2.13.0", "block2", + "libc", "objc2", "objc2-core-foundation", ] @@ -2172,6 +2297,49 @@ version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +[[package]] +name = "openssl" +version = "0.10.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77823a27f0babb03091cb9ed9ef80af3b39dbc82f97e8fa530374b7dafd87a45" +dependencies = [ + "bitflags 2.13.0", + "cfg-if", + "foreign-types 0.3.2", + "libc", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "openssl-sys" +version = "0.9.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b47e7e6bb2c38cd930d25a23b40fa52e068c10e85f3e03a7f5ba5aaca5713695" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "option-ext" version = "0.2.0" @@ -2598,6 +2766,46 @@ dependencies = [ "bytecheck", ] +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64 0.22.1", + "bytes", + "encoding_rs", + "futures-core", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-tls", + "hyper-util", + "js-sys", + "log", + "mime", + "native-tls", + "percent-encoding", + "pin-project-lite", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-native-tls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "reqwest" version = "0.13.4" @@ -2632,6 +2840,20 @@ dependencies = [ "web-sys", ] +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + [[package]] name = "rkyv" version = "0.7.46" @@ -2693,12 +2915,64 @@ dependencies = [ "semver", ] +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags 2.13.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b92b125634d9b795e7beca796cc790df15a7fb38323bf3196fda83292d06b1f" +dependencies = [ + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + [[package]] name = "rustversion" version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + [[package]] name = "same-file" version = "1.0.6" @@ -2708,6 +2982,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "schemars" version = "0.8.22" @@ -2771,6 +3054,29 @@ version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags 2.13.0", + "core-foundation 0.10.1", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "selectors" version = "0.36.1" @@ -2895,6 +3201,18 @@ dependencies = [ "serde_core", ] +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + [[package]] name = "serde_with" version = "3.21.0" @@ -2975,6 +3293,16 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + [[package]] name = "simd-adler32" version = "0.3.9" @@ -3099,6 +3427,12 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "swift-rs" version = "1.0.7" @@ -3152,6 +3486,27 @@ dependencies = [ "syn 2.0.118", ] +[[package]] +name = "system-configuration" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" +dependencies = [ + "bitflags 2.13.0", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "system-deps" version = "6.2.2" @@ -3173,7 +3528,7 @@ checksum = "d1c93047acf68669466a34690ac58cca7010bd1b201e1ec86f1fd0a75d3dd4a9" dependencies = [ "bitflags 2.13.0", "block2", - "core-foundation", + "core-foundation 0.10.1", "core-graphics", "crossbeam-channel", "dbus", @@ -3258,7 +3613,7 @@ dependencies = [ "percent-encoding", "plist", "raw-window-handle", - "reqwest", + "reqwest 0.13.4", "serde", "serde_json", "serde_repr", @@ -3357,6 +3712,30 @@ dependencies = [ "walkdir", ] +[[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" @@ -3379,6 +3758,22 @@ dependencies = [ "time", ] +[[package]] +name = "tauri-plugin-store" +version = "2.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c72dda16786eb4a3f903e43a17b64d8d78dc0f00fe2aa4b757c28f617a8630b" +dependencies = [ + "dunce", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "thiserror 2.0.18", + "tokio", + "tracing", +] + [[package]] name = "tauri-runtime" version = "2.11.3" @@ -3479,6 +3874,19 @@ dependencies = [ "toml 1.1.2+spec-1.1.0", ] +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.3", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + [[package]] name = "tendril" version = "0.5.0" @@ -3595,11 +4003,45 @@ dependencies = [ "bytes", "libc", "mio", + "parking_lot", "pin-project-lite", + "signal-hook-registry", "socket2", + "tokio-macros", "windows-sys 0.61.2", ] +[[package]] +name = "tokio-macros" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + [[package]] name = "tokio-util" version = "0.7.18" @@ -3785,9 +4227,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ "pin-project-lite", + "tracing-attributes", "tracing-core", ] +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + [[package]] name = "tracing-core" version = "0.1.36" @@ -3890,6 +4344,12 @@ version = "1.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "url" version = "2.5.8" @@ -3951,6 +4411,12 @@ version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ba6f5989077681266825251a52748b8c1d8a4ad098cc37e440103d0ea717fc0" +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "version-compare" version = "0.2.1" @@ -4336,6 +4802,17 @@ dependencies = [ "windows-link 0.1.3", ] +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + [[package]] name = "windows-result" version = "0.3.4" @@ -4381,6 +4858,15 @@ dependencies = [ "windows-targets 0.42.2", ] +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.59.0" @@ -4722,6 +5208,12 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" + [[package]] name = "zerotrie" version = "0.2.4" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 9eca180..2232b92 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -8,8 +8,6 @@ repository = "" edition = "2021" rust-version = "1.77.2" -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - [lib] name = "app_lib" crate-type = ["staticlib", "cdylib", "rlib"] @@ -23,3 +21,7 @@ serde = { version = "1.0", features = ["derive"] } log = "0.4" tauri = { version = "2.11.3", features = [] } tauri-plugin-log = "2" +tauri-plugin-store = "2" +tauri-plugin-fs = "2" +reqwest = { version = "0.12", features = ["json"] } +tokio = { version = "1", features = ["full"] } diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index 0e844dc..d0e35e7 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -6,6 +6,19 @@ "permissions": [ "core:default", "core:window:default", - "core:window:allow-set-title" + "core:window:allow-set-title", + "store:default", + "fs:default", + "fs:allow-read-text-file", + "fs:allow-write-text-file", + "fs:allow-read-file", + "fs:allow-write-file", + "fs:allow-exists", + "fs:allow-mkdir", + "fs:allow-remove", + "fs:allow-rename", + "fs:allow-read-dir", + "fs:allow-appdata-read-recursive", + "fs:allow-appdata-write-recursive" ] } \ No newline at end of file diff --git a/src-tauri/src/commands/llm_commands.rs b/src-tauri/src/commands/llm_commands.rs new file mode 100644 index 0000000..a760a14 --- /dev/null +++ b/src-tauri/src/commands/llm_commands.rs @@ -0,0 +1,166 @@ +use crate::llm::{self, AppState, ChatMessage, ChatResponse, GenerateRequest, LlmEvent, OllamaChatResponse}; +use serde_json::json; +use tauri::ipc::Channel; + +// ─── Simple (non-streaming) generate ───────────────────────── + +#[tauri::command] +pub async fn generate(state: tauri::State<'_, AppState>, req: GenerateRequest) -> Result { + let config = { + let guard = state.config.lock().map_err(|e| e.to_string())?; + guard.clone() + }; + + let client = reqwest::Client::new(); + let messages = llm::build_messages(req.system.as_deref(), &req.prompt); + let temperature = req.temperature.unwrap_or(config.temperature); + let max_tokens = req.max_tokens.unwrap_or(config.max_tokens); + + if llm::is_ollama(&config.api_url) { + call_ollama(&client, &config, &messages, temperature, max_tokens).await + } else { + call_openai(&client, &config, &messages, temperature, max_tokens).await + } +} + +// ─── Streaming generate via Tauri Channel ───────────────────── + +#[tauri::command] +pub async fn generate_stream( + state: tauri::State<'_, AppState>, + req: GenerateRequest, + channel: Channel, +) -> Result<(), String> { + let config = state.config.lock().map_err(|e| e.to_string())?.clone(); + + tauri::async_runtime::spawn(async move { + let client = reqwest::Client::new(); + let messages = llm::build_messages(req.system.as_deref(), &req.prompt); + let temperature = req.temperature.unwrap_or(config.temperature); + let max_tokens = req.max_tokens.unwrap_or(config.max_tokens); + + // For now, we do a non-streaming call and emit the full response as one token + // Real SSE streaming from Ollama/OpenAI can be added later + let result = if llm::is_ollama(&config.api_url) { + call_ollama(&client, &config, &messages, temperature, max_tokens).await + } else { + call_openai(&client, &config, &messages, temperature, max_tokens).await + }; + + match result { + Ok(text) => { + let _ = channel.send(LlmEvent::Token(text.clone())); + let _ = channel.send(LlmEvent::Done(text)); + } + Err(e) => { + let _ = channel.send(LlmEvent::Error(e)); + } + } + }); + + Ok(()) +} + +// ─── Get / Set LLM Config ──────────────────────────────────── + +#[tauri::command] +pub fn get_llm_config(state: tauri::State<'_, AppState>) -> Result { + let config = state.config.lock().map_err(|e| e.to_string())?; + Ok(config.clone()) +} + +#[tauri::command] +pub fn set_llm_config(state: tauri::State<'_, AppState>, config: crate::llm::LlmConfig) -> Result<(), String> { + let mut lock = state.config.lock().map_err(|e| e.to_string())?; + *lock = config; + Ok(()) +} + +// ─── Ollama API ─────────────────────────────────────────────── + +async fn call_ollama( + client: &reqwest::Client, + config: &crate::llm::LlmConfig, + messages: &[ChatMessage], + temperature: f32, + max_tokens: u32, +) -> Result { + let body = json!({ + "model": config.model, + "messages": messages, + "stream": false, + "options": { + "temperature": temperature, + "num_predict": max_tokens, + } + }); + + let url = format!("{}/api/chat", config.api_url.trim_end_matches('/')); + + let res = client + .post(&url) + .json(&body) + .send() + .await + .map_err(|e| format!("Ollama request failed: {e}"))?; + + if !res.status().is_success() { + let status = res.status(); + let text = res.text().await.unwrap_or_default(); + return Err(format!("Ollama error {status}: {text}")); + } + + let response: OllamaChatResponse = res + .json() + .await + .map_err(|e| format!("Ollama parse error: {e}"))?; + + Ok(response.message.content) +} + +// ─── OpenAI-compatible API ──────────────────────────────────── + +async fn call_openai( + client: &reqwest::Client, + config: &crate::llm::LlmConfig, + messages: &[ChatMessage], + temperature: f32, + max_tokens: u32, +) -> Result { + let body = json!({ + "model": config.model, + "messages": messages, + "temperature": temperature, + "max_tokens": max_tokens, + }); + + let url = format!("{}/v1/chat/completions", config.api_url.trim_end_matches('/')); + + let mut req = client.post(&url); + if !config.api_key.is_empty() { + req = req.bearer_auth(&config.api_key); + } + + let res = req + .json(&body) + .send() + .await + .map_err(|e| format!("API request failed: {e}"))?; + + if !res.status().is_success() { + let status = res.status(); + let text = res.text().await.unwrap_or_default(); + return Err(format!("API error {status}: {text}")); + } + + let response: ChatResponse = res + .json() + .await + .map_err(|e| format!("API parse error: {e}"))?; + + response + .choices + .first() + .map(|c| c.message.content.clone()) + .ok_or_else(|| "No response from API".to_string()) +} \ No newline at end of file diff --git a/src-tauri/src/commands/mod.rs b/src-tauri/src/commands/mod.rs new file mode 100644 index 0000000..c273392 --- /dev/null +++ b/src-tauri/src/commands/mod.rs @@ -0,0 +1 @@ +pub mod llm_commands; \ No newline at end of file diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 7a6099d..b80f50f 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,17 +1,25 @@ +mod llm; +mod commands; + +use llm::AppState; +use std::sync::Mutex; + #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { tauri::Builder::default() - .setup(|app| { - if cfg!(debug_assertions) { - app.handle().plugin( - tauri_plugin_log::Builder::default() - .level(log::LevelFilter::Info) - .build(), - )?; - } - Ok(()) + .plugin(tauri_plugin_log::Builder::default().build()) + .plugin(tauri_plugin_store::Builder::default().build()) + .plugin(tauri_plugin_fs::init()) + .manage(AppState { + config: Mutex::new(llm::LlmConfig::default()), }) - .invoke_handler(tauri::generate_handler![greet]) + .invoke_handler(tauri::generate_handler![ + greet, + commands::llm_commands::generate, + commands::llm_commands::generate_stream, + commands::llm_commands::get_llm_config, + commands::llm_commands::set_llm_config, + ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); } diff --git a/src-tauri/src/llm/mod.rs b/src-tauri/src/llm/mod.rs new file mode 100644 index 0000000..9f15f80 --- /dev/null +++ b/src-tauri/src/llm/mod.rs @@ -0,0 +1,108 @@ +use serde::{Deserialize, Serialize}; +use std::sync::Mutex; +use tauri::ipc::Channel; + +// ─── LLM Event (for streaming) ─────────────────────────────── + +#[derive(Clone, Serialize, Deserialize)] +#[serde(tag = "type", content = "data", rename_all = "camelCase")] +pub enum LlmEvent { + Token(String), + Done(String), // full text + Error(String), +} + +// ─── App State ──────────────────────────────────────────────── + +pub struct AppState { + pub config: Mutex, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct LlmConfig { + pub api_url: String, + pub api_key: String, + pub model: String, + pub temperature: f32, + pub max_tokens: u32, + pub top_p: f32, +} + +impl Default for LlmConfig { + fn default() -> Self { + Self { + // Default to Ollama local server; also works with LM Studio, llama.cpp server, or OpenAI + api_url: "http://localhost:11434".to_string(), + api_key: String::new(), + model: "llama3.2".to_string(), + temperature: 0.7, + max_tokens: 512, + top_p: 0.9, + } + } +} + +// ─── Generation Request ─────────────────────────────────────── + +#[derive(Debug, Deserialize)] +pub struct GenerateRequest { + pub prompt: String, + pub system: Option, + pub temperature: Option, + pub max_tokens: Option, +} + +// ─── OpenAI-Compatible Chat Response ───────────────────────── + +#[derive(Debug, Deserialize)] +pub struct ChatResponse { + pub choices: Vec, +} + +#[derive(Debug, Deserialize)] +pub struct ChatChoice { + pub message: ChatMessage, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct ChatMessage { + pub role: String, + pub content: String, +} + +// ─── Ollama Generate Response ───────────────────────────────── + +#[derive(Debug, Deserialize)] +pub struct OllamaGenerateResponse { + pub response: String, + pub done: bool, +} + +#[derive(Debug, Deserialize)] +pub struct OllamaChatResponse { + pub message: ChatMessage, + pub done: bool, +} + +// ─── Helper: detect if we're talking to Ollama ─────────────── + +pub fn is_ollama(url: &str) -> bool { + url.contains("localhost:11434") || url.contains("127.0.0.1:11434") +} + +// ─── Build system + user messages from a prompt ────────────── + +pub fn build_messages(system: Option<&str>, prompt: &str) -> Vec { + let mut messages = Vec::new(); + if let Some(sys) = system { + messages.push(ChatMessage { + role: "system".to_string(), + content: sys.to_string(), + }); + } + messages.push(ChatMessage { + role: "user".to_string(), + content: prompt.to_string(), + }); + messages +} \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx index 60bb791..d4ebc32 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -8,7 +8,11 @@ import { Settings, ChevronDown, } from "lucide-react"; +import { useState } from "react"; import { Dashboard } from "./components/Dashboard"; +import { SettingsPanel } from "./components/SettingsPanel"; + +type View = "dashboard" | "settings"; const navItems = [ { icon: Map, label: "World" }, @@ -17,24 +21,34 @@ const navItems = [ { icon: Dice5, label: "Dice" }, { icon: ScrollText, label: "Session" }, { icon: Volume2, label: "Sound" }, - { icon: Settings, label: "Settings" }, ]; export default function App() { + const [view, setView] = useState("dashboard"); + const [activeNav, setActiveNav] = useState(0); + return (
{/* Left Rail */}
diff --git a/src/components/Dashboard.tsx b/src/components/Dashboard.tsx index c672586..affcc19 100644 --- a/src/components/Dashboard.tsx +++ b/src/components/Dashboard.tsx @@ -4,14 +4,15 @@ import { Swords, ScrollText, Dice5, + Sparkles, Timer, - GitBranch, Volume2, Calendar, - Sparkles, } from "lucide-react"; import { BentoCard } from "./BentoCard"; -import { Greet } from "./Greet"; +import { NpcGenerator } from "./NpcGenerator"; +import { DiceRoller } from "./DiceRoller"; +import { SessionLogger } from "./SessionLogger"; export function Dashboard() { return ( @@ -19,22 +20,22 @@ export function Dashboard() { {/* World Map — 2×2 */} } span="col-span-2 row-span-2">
- Map canvas placeholder +
+ +

Map canvas coming soon

+

react-konva integration pending

+
{/* NPC Sheet — 1×1 */} } span="col-span-1 row-span-1"> -
- NPC generator placeholder -
+
{/* Dice Roller — 1×1 */} } span="col-span-1 row-span-1"> -
- Dice roller placeholder -
+
{/* Encounter Builder — 1×1 */} @@ -44,7 +45,7 @@ export function Dashboard() { span="col-span-1 row-span-1" >
- Encounter builder placeholder + Encounter builder coming soon
@@ -54,26 +55,24 @@ export function Dashboard() { icon={} span="col-span-2 row-span-1" > -
- Session log placeholder -
+ {/* Quest Designer — 2×1 */} } + icon={} span="col-span-2 row-span-1" >
- Quest designer placeholder + Quest flowchart coming soon
{/* Initiative Tracker — 1×1 */} } span="col-span-1 row-span-1">
- Initiative tracker placeholder + Combat tracker coming soon
@@ -84,19 +83,10 @@ export function Dashboard() { span="col-span-1 row-span-1" >
- Random tables placeholder + Random tables coming soon
- {/* Greeting / LLM Test — spans bottom area */} - } - span="col-span-2 row-span-1" - > - - - {/* Calendar — 1×1 */}
- Calendar placeholder + Calendar coming soon
@@ -115,7 +105,7 @@ export function Dashboard() { span="col-span-1 row-span-1" >
- Soundboard placeholder + Soundboard coming soon
diff --git a/src/components/DiceRoller.tsx b/src/components/DiceRoller.tsx new file mode 100644 index 0000000..ae714e1 --- /dev/null +++ b/src/components/DiceRoller.tsx @@ -0,0 +1,117 @@ +import { useState, useCallback } from "react"; + +interface DieResult { + notation: string; + rolls: number[]; + total: number; +} + +const PRESETS = ["d4", "d6", "d8", "d10", "d12", "d20", "d100"]; + +function rollDie(sides: number): number { + return Math.floor(Math.random() * sides) + 1; +} + +function parseNotation(notation: string): { count: number; sides: number; modifier: number } | null { + const match = notation.trim().toLowerCase().match(/^(\d+)?d(\d+)([+-]\d+)?$/); + if (!match) return null; + return { + count: parseInt(match[1] || "1"), + sides: parseInt(match[2]), + modifier: parseInt(match[3] || "0"), + }; +} + +export function DiceRoller() { + const [input, setInput] = useState("1d20"); + const [results, setResults] = useState([]); + const [lastTotal, setLastTotal] = useState(null); + + const roll = useCallback(() => { + const parsed = parseNotation(input); + if (!parsed || parsed.sides < 2 || parsed.count > 100) return; + + const rolls = Array.from({ length: parsed.count }, () => rollDie(parsed.sides)); + const sum = rolls.reduce((a, b) => a + b, 0) + parsed.modifier; + const result: DieResult = { + notation: input.trim(), + rolls, + total: sum, + }; + + setResults((prev) => [result, ...prev].slice(0, 50)); + setLastTotal(sum); + }, [input]); + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Enter") roll(); + }; + + return ( +
+ {/* Big result display */} +
+ + {lastTotal !== null ? lastTotal : "—"} + +
+ + {/* Input row */} +
+ setInput(e.target.value)} + onKeyDown={handleKeyDown} + placeholder="2d6+3" + /> + +
+ + {/* Quick dice */} +
+ {PRESETS.map((d) => ( + + ))} +
+ + {/* History */} +
+
+ {results.map((r, i) => ( +
+ + {r.notation} + {r.rolls.length > 1 && ` [${r.rolls.join(", ")}]`} + + + {r.total} + +
+ ))} +
+
+
+ ); +} \ No newline at end of file diff --git a/src/components/NpcGenerator.tsx b/src/components/NpcGenerator.tsx new file mode 100644 index 0000000..7c2001b --- /dev/null +++ b/src/components/NpcGenerator.tsx @@ -0,0 +1,140 @@ +import { useState } from "react"; +import { invoke } from "@tauri-apps/api/core"; + +interface NpcResponse { + bio: string; + personality: string[]; + goals: string[]; +} + +const RACES = ["Human", "Elf", "Dwarf", "Halfling", "Orc", "Tiefling", "Dragonborn", "Gnome"]; +const ALIGNMENTS = ["Lawful Good", "Neutral Good", "Chaotic Good", "Lawful Neutral", "True Neutral", "Chaotic Neutral", "Lawful Evil", "Neutral Evil", "Chaotic Evil"]; + +export function NpcGenerator() { + const [race, setRace] = useState("Dwarf"); + const [charClass, setCharClass] = useState("Artisan"); + const [alignment, setAlignment] = useState("Chaotic Good"); + const [name, setName] = useState(""); + const [npc, setNpc] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(""); + + async function generate() { + setLoading(true); + setError(""); + setNpc(null); + try { + const prompt = `Create a detailed NPC for a fantasy RPG: +${name ? `Name: ${name}` : "Name: (generate one)"} +Race: ${race} +Class: ${charClass} +Alignment: ${alignment} + +Provide the response as a JSON object with exactly these keys: +- "bio": a 2-3 sentence backstory +- "personality": an array of 3-5 personality traits +- "goals": an array of 2-3 goals or motivations`; + + const result = await invoke("generate", { + req: { prompt, system: "You are a creative D&D dungeon master. Always respond with valid JSON only.", temperature: 0.8, max_tokens: 512 }, + }); + + // Try to parse JSON from the response + const jsonMatch = result.match(/\{[\s\S]*\}/); + if (jsonMatch) { + const parsed = JSON.parse(jsonMatch[0]) as NpcResponse; + setNpc(parsed); + } else { + setError("LLM did not return valid JSON. Raw response:\n" + result); + } + } catch (e) { + setError(String(e)); + } + setLoading(false); + } + + return ( +
+ {/* Controls */} +
+
+ + setName(e.target.value)} + placeholder="Random" + /> +
+
+ + +
+
+ + setCharClass(e.target.value)} + /> +
+
+ + +
+
+ + + + {/* Result */} + {error && ( +
+ {error} +
+ )} + + {npc && ( +
+

+ {name || "NPC"} +

+

{npc.bio}

+
+ Personality: +
+ {npc.personality?.map((p, i) => ( + + {p} + + ))} +
+
+
+ Goals: +
    + {npc.goals?.map((g, i) =>
  • {g}
  • )} +
+
+
+ )} +
+ ); +} \ No newline at end of file diff --git a/src/components/SessionLogger.tsx b/src/components/SessionLogger.tsx new file mode 100644 index 0000000..1381f89 --- /dev/null +++ b/src/components/SessionLogger.tsx @@ -0,0 +1,104 @@ +import { useState } from "react"; +import { invoke } from "@tauri-apps/api/core"; + +export function SessionLogger() { + const [notes, setNotes] = useState(""); + const [summary, setSummary] = useState(""); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(""); + const [entries, setEntries] = useState<{ text: string; time: string }[]>([]); + + function addEntry() { + if (!notes.trim()) return; + setEntries((prev) => [ + { text: notes.trim(), time: new Date().toLocaleTimeString() }, + ...prev, + ]); + setNotes(""); + } + + async function summarize() { + if (entries.length === 0) return; + setLoading(true); + setError(""); + try { + const sessionText = entries + .map((e) => `[${e.time}] ${e.text}`) + .join("\n"); + const result = await invoke("generate", { + req: { + prompt: `Summarize the following D&D session notes and suggest 2-3 hooks for the next session:\n\n${sessionText}`, + system: "You are a helpful D&D session summarizer. Be concise and creative.", + temperature: 0.7, + max_tokens: 512, + }, + }); + setSummary(result); + } catch (e) { + setError(String(e)); + } + setLoading(false); + } + + return ( +
+ {/* Notes column */} +
+