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
This commit is contained in:
Generated
+498
-6
@@ -451,6 +451,16 @@ dependencies = [
|
|||||||
"version_check",
|
"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]]
|
[[package]]
|
||||||
name = "core-foundation"
|
name = "core-foundation"
|
||||||
version = "0.10.1"
|
version = "0.10.1"
|
||||||
@@ -474,9 +484,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97"
|
checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.13.0",
|
"bitflags 2.13.0",
|
||||||
"core-foundation",
|
"core-foundation 0.10.1",
|
||||||
"core-graphics-types",
|
"core-graphics-types",
|
||||||
"foreign-types",
|
"foreign-types 0.5.0",
|
||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -487,7 +497,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb"
|
checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.13.0",
|
"bitflags 2.13.0",
|
||||||
"core-foundation",
|
"core-foundation 0.10.1",
|
||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -730,11 +740,15 @@ name = "dm-pal"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"log",
|
"log",
|
||||||
|
"reqwest 0.12.28",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"tauri",
|
"tauri",
|
||||||
"tauri-build",
|
"tauri-build",
|
||||||
|
"tauri-plugin-fs",
|
||||||
"tauri-plugin-log",
|
"tauri-plugin-log",
|
||||||
|
"tauri-plugin-store",
|
||||||
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -823,6 +837,15 @@ version = "1.2.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7"
|
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]]
|
[[package]]
|
||||||
name = "env_filter"
|
name = "env_filter"
|
||||||
version = "0.1.4"
|
version = "0.1.4"
|
||||||
@@ -850,6 +873,16 @@ dependencies = [
|
|||||||
"typeid",
|
"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]]
|
[[package]]
|
||||||
name = "fastrand"
|
name = "fastrand"
|
||||||
version = "2.4.1"
|
version = "2.4.1"
|
||||||
@@ -912,6 +945,15 @@ version = "0.2.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
|
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]]
|
[[package]]
|
||||||
name = "foreign-types"
|
name = "foreign-types"
|
||||||
version = "0.5.0"
|
version = "0.5.0"
|
||||||
@@ -919,7 +961,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965"
|
checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"foreign-types-macros",
|
"foreign-types-macros",
|
||||||
"foreign-types-shared",
|
"foreign-types-shared 0.3.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -933,6 +975,12 @@ dependencies = [
|
|||||||
"syn 2.0.118",
|
"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]]
|
[[package]]
|
||||||
name = "foreign-types-shared"
|
name = "foreign-types-shared"
|
||||||
version = "0.3.1"
|
version = "0.3.1"
|
||||||
@@ -1316,6 +1364,25 @@ dependencies = [
|
|||||||
"syn 2.0.118",
|
"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]]
|
[[package]]
|
||||||
name = "hashbrown"
|
name = "hashbrown"
|
||||||
version = "0.12.3"
|
version = "0.12.3"
|
||||||
@@ -1408,6 +1475,7 @@ dependencies = [
|
|||||||
"bytes",
|
"bytes",
|
||||||
"futures-channel",
|
"futures-channel",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
|
"h2",
|
||||||
"http",
|
"http",
|
||||||
"http-body",
|
"http-body",
|
||||||
"httparse",
|
"httparse",
|
||||||
@@ -1418,6 +1486,37 @@ dependencies = [
|
|||||||
"want",
|
"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]]
|
[[package]]
|
||||||
name = "hyper-util"
|
name = "hyper-util"
|
||||||
version = "0.1.20"
|
version = "0.1.20"
|
||||||
@@ -1436,9 +1535,11 @@ dependencies = [
|
|||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"socket2",
|
"socket2",
|
||||||
|
"system-configuration",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tower-service",
|
"tower-service",
|
||||||
"tracing",
|
"tracing",
|
||||||
|
"windows-registry",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1797,6 +1898,12 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "linux-raw-sys"
|
||||||
|
version = "0.12.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "litemap"
|
name = "litemap"
|
||||||
version = "0.8.2"
|
version = "0.8.2"
|
||||||
@@ -1895,6 +2002,23 @@ dependencies = [
|
|||||||
"windows-sys 0.61.2",
|
"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]]
|
[[package]]
|
||||||
name = "ndk"
|
name = "ndk"
|
||||||
version = "0.9.0"
|
version = "0.9.0"
|
||||||
@@ -2094,6 +2218,7 @@ checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.13.0",
|
"bitflags 2.13.0",
|
||||||
"block2",
|
"block2",
|
||||||
|
"libc",
|
||||||
"objc2",
|
"objc2",
|
||||||
"objc2-core-foundation",
|
"objc2-core-foundation",
|
||||||
]
|
]
|
||||||
@@ -2172,6 +2297,49 @@ version = "1.21.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
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]]
|
[[package]]
|
||||||
name = "option-ext"
|
name = "option-ext"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
@@ -2598,6 +2766,46 @@ dependencies = [
|
|||||||
"bytecheck",
|
"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]]
|
[[package]]
|
||||||
name = "reqwest"
|
name = "reqwest"
|
||||||
version = "0.13.4"
|
version = "0.13.4"
|
||||||
@@ -2632,6 +2840,20 @@ dependencies = [
|
|||||||
"web-sys",
|
"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]]
|
[[package]]
|
||||||
name = "rkyv"
|
name = "rkyv"
|
||||||
version = "0.7.46"
|
version = "0.7.46"
|
||||||
@@ -2693,12 +2915,64 @@ dependencies = [
|
|||||||
"semver",
|
"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]]
|
[[package]]
|
||||||
name = "rustversion"
|
name = "rustversion"
|
||||||
version = "1.0.22"
|
version = "1.0.22"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ryu"
|
||||||
|
version = "1.0.23"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "same-file"
|
name = "same-file"
|
||||||
version = "1.0.6"
|
version = "1.0.6"
|
||||||
@@ -2708,6 +2982,15 @@ dependencies = [
|
|||||||
"winapi-util",
|
"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]]
|
[[package]]
|
||||||
name = "schemars"
|
name = "schemars"
|
||||||
version = "0.8.22"
|
version = "0.8.22"
|
||||||
@@ -2771,6 +3054,29 @@ version = "4.1.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b"
|
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]]
|
[[package]]
|
||||||
name = "selectors"
|
name = "selectors"
|
||||||
version = "0.36.1"
|
version = "0.36.1"
|
||||||
@@ -2895,6 +3201,18 @@ dependencies = [
|
|||||||
"serde_core",
|
"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]]
|
[[package]]
|
||||||
name = "serde_with"
|
name = "serde_with"
|
||||||
version = "3.21.0"
|
version = "3.21.0"
|
||||||
@@ -2975,6 +3293,16 @@ version = "2.0.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
|
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]]
|
[[package]]
|
||||||
name = "simd-adler32"
|
name = "simd-adler32"
|
||||||
version = "0.3.9"
|
version = "0.3.9"
|
||||||
@@ -3099,6 +3427,12 @@ version = "0.11.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "subtle"
|
||||||
|
version = "2.6.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "swift-rs"
|
name = "swift-rs"
|
||||||
version = "1.0.7"
|
version = "1.0.7"
|
||||||
@@ -3152,6 +3486,27 @@ dependencies = [
|
|||||||
"syn 2.0.118",
|
"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]]
|
[[package]]
|
||||||
name = "system-deps"
|
name = "system-deps"
|
||||||
version = "6.2.2"
|
version = "6.2.2"
|
||||||
@@ -3173,7 +3528,7 @@ checksum = "d1c93047acf68669466a34690ac58cca7010bd1b201e1ec86f1fd0a75d3dd4a9"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.13.0",
|
"bitflags 2.13.0",
|
||||||
"block2",
|
"block2",
|
||||||
"core-foundation",
|
"core-foundation 0.10.1",
|
||||||
"core-graphics",
|
"core-graphics",
|
||||||
"crossbeam-channel",
|
"crossbeam-channel",
|
||||||
"dbus",
|
"dbus",
|
||||||
@@ -3258,7 +3613,7 @@ dependencies = [
|
|||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"plist",
|
"plist",
|
||||||
"raw-window-handle",
|
"raw-window-handle",
|
||||||
"reqwest",
|
"reqwest 0.13.4",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_repr",
|
"serde_repr",
|
||||||
@@ -3357,6 +3712,30 @@ dependencies = [
|
|||||||
"walkdir",
|
"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]]
|
[[package]]
|
||||||
name = "tauri-plugin-log"
|
name = "tauri-plugin-log"
|
||||||
version = "2.8.0"
|
version = "2.8.0"
|
||||||
@@ -3379,6 +3758,22 @@ dependencies = [
|
|||||||
"time",
|
"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]]
|
[[package]]
|
||||||
name = "tauri-runtime"
|
name = "tauri-runtime"
|
||||||
version = "2.11.3"
|
version = "2.11.3"
|
||||||
@@ -3479,6 +3874,19 @@ dependencies = [
|
|||||||
"toml 1.1.2+spec-1.1.0",
|
"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]]
|
[[package]]
|
||||||
name = "tendril"
|
name = "tendril"
|
||||||
version = "0.5.0"
|
version = "0.5.0"
|
||||||
@@ -3595,11 +4003,45 @@ dependencies = [
|
|||||||
"bytes",
|
"bytes",
|
||||||
"libc",
|
"libc",
|
||||||
"mio",
|
"mio",
|
||||||
|
"parking_lot",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
|
"signal-hook-registry",
|
||||||
"socket2",
|
"socket2",
|
||||||
|
"tokio-macros",
|
||||||
"windows-sys 0.61.2",
|
"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]]
|
[[package]]
|
||||||
name = "tokio-util"
|
name = "tokio-util"
|
||||||
version = "0.7.18"
|
version = "0.7.18"
|
||||||
@@ -3785,9 +4227,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
|
checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
|
"tracing-attributes",
|
||||||
"tracing-core",
|
"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]]
|
[[package]]
|
||||||
name = "tracing-core"
|
name = "tracing-core"
|
||||||
version = "0.1.36"
|
version = "0.1.36"
|
||||||
@@ -3890,6 +4344,12 @@ version = "1.13.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8"
|
checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "untrusted"
|
||||||
|
version = "0.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "url"
|
name = "url"
|
||||||
version = "2.5.8"
|
version = "2.5.8"
|
||||||
@@ -3951,6 +4411,12 @@ version = "1.12.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7ba6f5989077681266825251a52748b8c1d8a4ad098cc37e440103d0ea717fc0"
|
checksum = "7ba6f5989077681266825251a52748b8c1d8a4ad098cc37e440103d0ea717fc0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "vcpkg"
|
||||||
|
version = "0.2.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "version-compare"
|
name = "version-compare"
|
||||||
version = "0.2.1"
|
version = "0.2.1"
|
||||||
@@ -4336,6 +4802,17 @@ dependencies = [
|
|||||||
"windows-link 0.1.3",
|
"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]]
|
[[package]]
|
||||||
name = "windows-result"
|
name = "windows-result"
|
||||||
version = "0.3.4"
|
version = "0.3.4"
|
||||||
@@ -4381,6 +4858,15 @@ dependencies = [
|
|||||||
"windows-targets 0.42.2",
|
"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]]
|
[[package]]
|
||||||
name = "windows-sys"
|
name = "windows-sys"
|
||||||
version = "0.59.0"
|
version = "0.59.0"
|
||||||
@@ -4722,6 +5208,12 @@ dependencies = [
|
|||||||
"synstructure",
|
"synstructure",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zeroize"
|
||||||
|
version = "1.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zerotrie"
|
name = "zerotrie"
|
||||||
version = "0.2.4"
|
version = "0.2.4"
|
||||||
|
|||||||
@@ -8,8 +8,6 @@ repository = ""
|
|||||||
edition = "2021"
|
edition = "2021"
|
||||||
rust-version = "1.77.2"
|
rust-version = "1.77.2"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
name = "app_lib"
|
name = "app_lib"
|
||||||
crate-type = ["staticlib", "cdylib", "rlib"]
|
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||||
@@ -23,3 +21,7 @@ serde = { version = "1.0", features = ["derive"] }
|
|||||||
log = "0.4"
|
log = "0.4"
|
||||||
tauri = { version = "2.11.3", features = [] }
|
tauri = { version = "2.11.3", features = [] }
|
||||||
tauri-plugin-log = "2"
|
tauri-plugin-log = "2"
|
||||||
|
tauri-plugin-store = "2"
|
||||||
|
tauri-plugin-fs = "2"
|
||||||
|
reqwest = { version = "0.12", features = ["json"] }
|
||||||
|
tokio = { version = "1", features = ["full"] }
|
||||||
|
|||||||
@@ -6,6 +6,19 @@
|
|||||||
"permissions": [
|
"permissions": [
|
||||||
"core:default",
|
"core:default",
|
||||||
"core:window: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"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -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<String, String> {
|
||||||
|
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<LlmEvent>,
|
||||||
|
) -> 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<crate::llm::LlmConfig, String> {
|
||||||
|
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<String, String> {
|
||||||
|
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<String, String> {
|
||||||
|
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())
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
pub mod llm_commands;
|
||||||
+18
-10
@@ -1,17 +1,25 @@
|
|||||||
|
mod llm;
|
||||||
|
mod commands;
|
||||||
|
|
||||||
|
use llm::AppState;
|
||||||
|
use std::sync::Mutex;
|
||||||
|
|
||||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||||
pub fn run() {
|
pub fn run() {
|
||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
.setup(|app| {
|
.plugin(tauri_plugin_log::Builder::default().build())
|
||||||
if cfg!(debug_assertions) {
|
.plugin(tauri_plugin_store::Builder::default().build())
|
||||||
app.handle().plugin(
|
.plugin(tauri_plugin_fs::init())
|
||||||
tauri_plugin_log::Builder::default()
|
.manage(AppState {
|
||||||
.level(log::LevelFilter::Info)
|
config: Mutex::new(llm::LlmConfig::default()),
|
||||||
.build(),
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
})
|
})
|
||||||
.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!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running tauri application");
|
.expect("error while running tauri application");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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<LlmConfig>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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<String>,
|
||||||
|
pub temperature: Option<f32>,
|
||||||
|
pub max_tokens: Option<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── OpenAI-Compatible Chat Response ─────────────────────────
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct ChatResponse {
|
||||||
|
pub choices: Vec<ChatChoice>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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<ChatMessage> {
|
||||||
|
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
|
||||||
|
}
|
||||||
+30
-8
@@ -8,7 +8,11 @@ import {
|
|||||||
Settings,
|
Settings,
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
import { useState } from "react";
|
||||||
import { Dashboard } from "./components/Dashboard";
|
import { Dashboard } from "./components/Dashboard";
|
||||||
|
import { SettingsPanel } from "./components/SettingsPanel";
|
||||||
|
|
||||||
|
type View = "dashboard" | "settings";
|
||||||
|
|
||||||
const navItems = [
|
const navItems = [
|
||||||
{ icon: Map, label: "World" },
|
{ icon: Map, label: "World" },
|
||||||
@@ -17,24 +21,34 @@ const navItems = [
|
|||||||
{ icon: Dice5, label: "Dice" },
|
{ icon: Dice5, label: "Dice" },
|
||||||
{ icon: ScrollText, label: "Session" },
|
{ icon: ScrollText, label: "Session" },
|
||||||
{ icon: Volume2, label: "Sound" },
|
{ icon: Volume2, label: "Sound" },
|
||||||
{ icon: Settings, label: "Settings" },
|
|
||||||
];
|
];
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
|
const [view, setView] = useState<View>("dashboard");
|
||||||
|
const [activeNav, setActiveNav] = useState(0);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-screen w-screen overflow-hidden bg-[var(--color-bg-deep)]">
|
<div className="flex h-screen w-screen overflow-hidden bg-[var(--color-bg-deep)]">
|
||||||
{/* Left Rail */}
|
{/* Left Rail */}
|
||||||
<nav className="flex flex-col items-center w-14 bg-[var(--color-bg-surface)] border-r border-[var(--color-border-subtle)] py-3 gap-1 shrink-0">
|
<nav className="flex flex-col items-center w-14 bg-[var(--color-bg-surface)] border-r border-[var(--color-border-subtle)] py-3 gap-1 shrink-0">
|
||||||
{/* App Logo */}
|
{/* App Logo */}
|
||||||
<div className="w-8 h-8 rounded-lg bg-[var(--color-gold-bright)] flex items-center justify-center mb-4 text-[var(--color-bg-deep)] font-heading font-bold text-lg">
|
<button
|
||||||
|
onClick={() => setView("dashboard")}
|
||||||
|
className="w-8 h-8 rounded-lg bg-[var(--color-gold-bright)] flex items-center justify-center mb-4 text-[var(--color-bg-deep)] font-heading font-bold text-lg cursor-pointer"
|
||||||
|
title="DM-Pal"
|
||||||
|
>
|
||||||
⚔
|
⚔
|
||||||
</div>
|
</button>
|
||||||
|
|
||||||
{navItems.map((item, i) => (
|
{navItems.map((item, i) => (
|
||||||
<button
|
<button
|
||||||
key={item.label}
|
key={item.label}
|
||||||
|
onClick={() => {
|
||||||
|
setActiveNav(i);
|
||||||
|
setView("dashboard");
|
||||||
|
}}
|
||||||
className={`w-10 h-10 rounded-lg flex items-center justify-center transition-colors cursor-pointer ${
|
className={`w-10 h-10 rounded-lg flex items-center justify-center transition-colors cursor-pointer ${
|
||||||
i === 0
|
i === activeNav && view === "dashboard"
|
||||||
? "bg-[var(--color-bg-card)] text-[var(--color-gold-bright)]"
|
? "bg-[var(--color-bg-card)] text-[var(--color-gold-bright)]"
|
||||||
: "text-[var(--color-text-dim)] hover:text-[var(--color-text-secondary)] hover:bg-[var(--color-bg-card)]"
|
: "text-[var(--color-text-dim)] hover:text-[var(--color-text-secondary)] hover:bg-[var(--color-bg-card)]"
|
||||||
}`}
|
}`}
|
||||||
@@ -49,7 +63,12 @@ export default function App() {
|
|||||||
|
|
||||||
{/* Settings at bottom */}
|
{/* Settings at bottom */}
|
||||||
<button
|
<button
|
||||||
className="w-10 h-10 rounded-lg flex items-center justify-center text-[var(--color-text-dim)] hover:text-[var(--color-text-secondary)] hover:bg-[var(--color-bg-card)] transition-colors cursor-pointer"
|
onClick={() => setView(view === "settings" ? "dashboard" : "settings")}
|
||||||
|
className={`w-10 h-10 rounded-lg flex items-center justify-center transition-colors cursor-pointer ${
|
||||||
|
view === "settings"
|
||||||
|
? "bg-[var(--color-bg-card)] text-[var(--color-gold-bright)]"
|
||||||
|
: "text-[var(--color-text-dim)] hover:text-[var(--color-text-secondary)] hover:bg-[var(--color-bg-card)]"
|
||||||
|
}`}
|
||||||
title="Settings"
|
title="Settings"
|
||||||
>
|
>
|
||||||
<Settings size={18} />
|
<Settings size={18} />
|
||||||
@@ -59,7 +78,10 @@ export default function App() {
|
|||||||
{/* Main Content Area */}
|
{/* Main Content Area */}
|
||||||
<div className="flex flex-col flex-1 overflow-hidden">
|
<div className="flex flex-col flex-1 overflow-hidden">
|
||||||
{/* Title Bar */}
|
{/* Title Bar */}
|
||||||
<header className="flex items-center h-10 px-4 bg-[var(--color-bg-surface)] border-b border-[var(--color-border-subtle)] shrink-0" data-tauri-drag-region>
|
<header
|
||||||
|
className="flex items-center h-10 px-4 bg-[var(--color-bg-surface)] border-b border-[var(--color-border-subtle)] shrink-0"
|
||||||
|
data-tauri-drag-region
|
||||||
|
>
|
||||||
<span className="font-heading text-[var(--color-gold-bright)] text-base font-bold tracking-wider">
|
<span className="font-heading text-[var(--color-gold-bright)] text-base font-bold tracking-wider">
|
||||||
DM-Pal
|
DM-Pal
|
||||||
</span>
|
</span>
|
||||||
@@ -69,9 +91,9 @@ export default function App() {
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
{/* Dashboard */}
|
{/* Main Content */}
|
||||||
<main className="flex-1 overflow-auto">
|
<main className="flex-1 overflow-auto">
|
||||||
<Dashboard />
|
{view === "dashboard" ? <Dashboard /> : <SettingsPanel />}
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,14 +4,15 @@ import {
|
|||||||
Swords,
|
Swords,
|
||||||
ScrollText,
|
ScrollText,
|
||||||
Dice5,
|
Dice5,
|
||||||
|
Sparkles,
|
||||||
Timer,
|
Timer,
|
||||||
GitBranch,
|
|
||||||
Volume2,
|
Volume2,
|
||||||
Calendar,
|
Calendar,
|
||||||
Sparkles,
|
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { BentoCard } from "./BentoCard";
|
import { BentoCard } from "./BentoCard";
|
||||||
import { Greet } from "./Greet";
|
import { NpcGenerator } from "./NpcGenerator";
|
||||||
|
import { DiceRoller } from "./DiceRoller";
|
||||||
|
import { SessionLogger } from "./SessionLogger";
|
||||||
|
|
||||||
export function Dashboard() {
|
export function Dashboard() {
|
||||||
return (
|
return (
|
||||||
@@ -19,22 +20,22 @@ export function Dashboard() {
|
|||||||
{/* World Map — 2×2 */}
|
{/* World Map — 2×2 */}
|
||||||
<BentoCard title="World Map" icon={<Map size={16} />} span="col-span-2 row-span-2">
|
<BentoCard title="World Map" icon={<Map size={16} />} span="col-span-2 row-span-2">
|
||||||
<div className="flex items-center justify-center h-full text-[var(--color-text-dim)] text-sm">
|
<div className="flex items-center justify-center h-full text-[var(--color-text-dim)] text-sm">
|
||||||
Map canvas placeholder
|
<div className="text-center">
|
||||||
|
<Map size={48} className="mx-auto mb-2 opacity-30" />
|
||||||
|
<p>Map canvas coming soon</p>
|
||||||
|
<p className="text-xs mt-1">react-konva integration pending</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</BentoCard>
|
</BentoCard>
|
||||||
|
|
||||||
{/* NPC Sheet — 1×1 */}
|
{/* NPC Sheet — 1×1 */}
|
||||||
<BentoCard title="NPC Sheet" icon={<User size={16} />} span="col-span-1 row-span-1">
|
<BentoCard title="NPC Sheet" icon={<User size={16} />} span="col-span-1 row-span-1">
|
||||||
<div className="flex items-center justify-center h-full text-[var(--color-text-dim)] text-sm">
|
<NpcGenerator />
|
||||||
NPC generator placeholder
|
|
||||||
</div>
|
|
||||||
</BentoCard>
|
</BentoCard>
|
||||||
|
|
||||||
{/* Dice Roller — 1×1 */}
|
{/* Dice Roller — 1×1 */}
|
||||||
<BentoCard title="Dice Roller" icon={<Dice5 size={16} />} span="col-span-1 row-span-1">
|
<BentoCard title="Dice Roller" icon={<Dice5 size={16} />} span="col-span-1 row-span-1">
|
||||||
<div className="flex items-center justify-center h-full text-[var(--color-text-dim)] text-sm">
|
<DiceRoller />
|
||||||
Dice roller placeholder
|
|
||||||
</div>
|
|
||||||
</BentoCard>
|
</BentoCard>
|
||||||
|
|
||||||
{/* Encounter Builder — 1×1 */}
|
{/* Encounter Builder — 1×1 */}
|
||||||
@@ -44,7 +45,7 @@ export function Dashboard() {
|
|||||||
span="col-span-1 row-span-1"
|
span="col-span-1 row-span-1"
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-center h-full text-[var(--color-text-dim)] text-sm">
|
<div className="flex items-center justify-center h-full text-[var(--color-text-dim)] text-sm">
|
||||||
Encounter builder placeholder
|
Encounter builder coming soon
|
||||||
</div>
|
</div>
|
||||||
</BentoCard>
|
</BentoCard>
|
||||||
|
|
||||||
@@ -54,26 +55,24 @@ export function Dashboard() {
|
|||||||
icon={<ScrollText size={16} />}
|
icon={<ScrollText size={16} />}
|
||||||
span="col-span-2 row-span-1"
|
span="col-span-2 row-span-1"
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-center h-full text-[var(--color-text-dim)] text-sm">
|
<SessionLogger />
|
||||||
Session log placeholder
|
|
||||||
</div>
|
|
||||||
</BentoCard>
|
</BentoCard>
|
||||||
|
|
||||||
{/* Quest Designer — 2×1 */}
|
{/* Quest Designer — 2×1 */}
|
||||||
<BentoCard
|
<BentoCard
|
||||||
title="Quest Designer"
|
title="Quest Designer"
|
||||||
icon={<GitBranch size={16} />}
|
icon={<Sparkles size={16} />}
|
||||||
span="col-span-2 row-span-1"
|
span="col-span-2 row-span-1"
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-center h-full text-[var(--color-text-dim)] text-sm">
|
<div className="flex items-center justify-center h-full text-[var(--color-text-dim)] text-sm">
|
||||||
Quest designer placeholder
|
Quest flowchart coming soon
|
||||||
</div>
|
</div>
|
||||||
</BentoCard>
|
</BentoCard>
|
||||||
|
|
||||||
{/* Initiative Tracker — 1×1 */}
|
{/* Initiative Tracker — 1×1 */}
|
||||||
<BentoCard title="Initiative" icon={<Timer size={16} />} span="col-span-1 row-span-1">
|
<BentoCard title="Initiative" icon={<Timer size={16} />} span="col-span-1 row-span-1">
|
||||||
<div className="flex items-center justify-center h-full text-[var(--color-text-dim)] text-sm">
|
<div className="flex items-center justify-center h-full text-[var(--color-text-dim)] text-sm">
|
||||||
Initiative tracker placeholder
|
Combat tracker coming soon
|
||||||
</div>
|
</div>
|
||||||
</BentoCard>
|
</BentoCard>
|
||||||
|
|
||||||
@@ -84,19 +83,10 @@ export function Dashboard() {
|
|||||||
span="col-span-1 row-span-1"
|
span="col-span-1 row-span-1"
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-center h-full text-[var(--color-text-dim)] text-sm">
|
<div className="flex items-center justify-center h-full text-[var(--color-text-dim)] text-sm">
|
||||||
Random tables placeholder
|
Random tables coming soon
|
||||||
</div>
|
</div>
|
||||||
</BentoCard>
|
</BentoCard>
|
||||||
|
|
||||||
{/* Greeting / LLM Test — spans bottom area */}
|
|
||||||
<BentoCard
|
|
||||||
title="DM-Pal"
|
|
||||||
icon={<Volume2 size={16} />}
|
|
||||||
span="col-span-2 row-span-1"
|
|
||||||
>
|
|
||||||
<Greet />
|
|
||||||
</BentoCard>
|
|
||||||
|
|
||||||
{/* Calendar — 1×1 */}
|
{/* Calendar — 1×1 */}
|
||||||
<BentoCard
|
<BentoCard
|
||||||
title="Calendar"
|
title="Calendar"
|
||||||
@@ -104,7 +94,7 @@ export function Dashboard() {
|
|||||||
span="col-span-1 row-span-1"
|
span="col-span-1 row-span-1"
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-center h-full text-[var(--color-text-dim)] text-sm">
|
<div className="flex items-center justify-center h-full text-[var(--color-text-dim)] text-sm">
|
||||||
Calendar placeholder
|
Calendar coming soon
|
||||||
</div>
|
</div>
|
||||||
</BentoCard>
|
</BentoCard>
|
||||||
|
|
||||||
@@ -115,7 +105,7 @@ export function Dashboard() {
|
|||||||
span="col-span-1 row-span-1"
|
span="col-span-1 row-span-1"
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-center h-full text-[var(--color-text-dim)] text-sm">
|
<div className="flex items-center justify-center h-full text-[var(--color-text-dim)] text-sm">
|
||||||
Soundboard placeholder
|
Soundboard coming soon
|
||||||
</div>
|
</div>
|
||||||
</BentoCard>
|
</BentoCard>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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<DieResult[]>([]);
|
||||||
|
const [lastTotal, setLastTotal] = useState<number | null>(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 (
|
||||||
|
<div className="flex flex-col gap-3 h-full">
|
||||||
|
{/* Big result display */}
|
||||||
|
<div className="flex items-center justify-center py-3">
|
||||||
|
<span className="font-mono text-3xl font-bold text-[var(--color-gold-bright)]">
|
||||||
|
{lastTotal !== null ? lastTotal : "—"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Input row */}
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<input
|
||||||
|
className="flex-1 rounded-lg bg-[var(--color-bg-surface)] border border-[var(--color-border-glass)] px-3 py-2 text-[var(--color-text-primary)] placeholder-[var(--color-text-dim)] focus:outline-none focus:border-[var(--color-gold-bright)] text-sm font-mono"
|
||||||
|
value={input}
|
||||||
|
onChange={(e) => setInput(e.target.value)}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
placeholder="2d6+3"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={roll}
|
||||||
|
className="rounded-lg bg-[var(--color-gold-bright)] text-[var(--color-bg-deep)] px-4 py-2 text-sm font-semibold hover:bg-[var(--color-gold-muted)] transition-colors cursor-pointer"
|
||||||
|
>
|
||||||
|
Roll
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Quick dice */}
|
||||||
|
<div className="flex flex-wrap gap-1.5">
|
||||||
|
{PRESETS.map((d) => (
|
||||||
|
<button
|
||||||
|
key={d}
|
||||||
|
onClick={() => {
|
||||||
|
const parsed = parseNotation(`1${d}`);
|
||||||
|
if (!parsed) return;
|
||||||
|
const rolls = [rollDie(parsed.sides)];
|
||||||
|
const total = rolls[0] + parsed.modifier;
|
||||||
|
setResults((prev) => [{ notation: `1${d}`, rolls, total }, ...prev].slice(0, 50));
|
||||||
|
setLastTotal(total);
|
||||||
|
}}
|
||||||
|
className="rounded-lg bg-[var(--color-bg-surface)] border border-[var(--color-border-glass)] px-3 py-1 text-xs text-[var(--color-text-secondary)] hover:border-[var(--color-gold-bright)] hover:text-[var(--color-gold-bright)] transition-colors cursor-pointer font-mono"
|
||||||
|
>
|
||||||
|
{d}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* History */}
|
||||||
|
<div className="flex-1 overflow-y-auto">
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
{results.map((r, i) => (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
className="flex items-center justify-between rounded-lg bg-[var(--color-bg-surface)] border border-[var(--color-border-subtle)] px-3 py-1.5"
|
||||||
|
>
|
||||||
|
<span className="font-mono text-xs text-[var(--color-text-secondary)]">
|
||||||
|
{r.notation}
|
||||||
|
{r.rolls.length > 1 && ` [${r.rolls.join(", ")}]`}
|
||||||
|
</span>
|
||||||
|
<span className="font-mono text-sm font-bold text-[var(--color-gold-bright)]">
|
||||||
|
{r.total}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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<NpcResponse | null>(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<string>("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 (
|
||||||
|
<div className="flex flex-col gap-3 h-full overflow-y-auto">
|
||||||
|
{/* Controls */}
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<label className="text-[var(--color-text-secondary)] text-xs font-medium">Name</label>
|
||||||
|
<input
|
||||||
|
className="rounded-lg bg-[var(--color-bg-surface)] border border-[var(--color-border-glass)] px-3 py-1.5 text-[var(--color-text-primary)] placeholder-[var(--color-text-dim)] focus:outline-none focus:border-[var(--color-gold-bright)] text-sm"
|
||||||
|
value={name}
|
||||||
|
onChange={(e) => setName(e.target.value)}
|
||||||
|
placeholder="Random"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<label className="text-[var(--color-text-secondary)] text-xs font-medium">Race</label>
|
||||||
|
<select
|
||||||
|
className="rounded-lg bg-[var(--color-bg-surface)] border border-[var(--color-border-glass)] px-3 py-1.5 text-[var(--color-text-primary)] focus:outline-none focus:border-[var(--color-gold-bright)] text-sm cursor-pointer"
|
||||||
|
value={race}
|
||||||
|
onChange={(e) => setRace(e.target.value)}
|
||||||
|
>
|
||||||
|
{RACES.map((r) => <option key={r} value={r}>{r}</option>)}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<label className="text-[var(--color-text-secondary)] text-xs font-medium">Class</label>
|
||||||
|
<input
|
||||||
|
className="rounded-lg bg-[var(--color-bg-surface)] border border-[var(--color-border-glass)] px-3 py-1.5 text-[var(--color-text-primary)] placeholder-[var(--color-text-dim)] focus:outline-none focus:border-[var(--color-gold-bright)] text-sm"
|
||||||
|
value={charClass}
|
||||||
|
onChange={(e) => setCharClass(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<label className="text-[var(--color-text-secondary)] text-xs font-medium">Alignment</label>
|
||||||
|
<select
|
||||||
|
className="rounded-lg bg-[var(--color-bg-surface)] border border-[var(--color-border-glass)] px-3 py-1.5 text-[var(--color-text-primary)] focus:outline-none focus:border-[var(--color-gold-bright)] text-sm cursor-pointer"
|
||||||
|
value={alignment}
|
||||||
|
onChange={(e) => setAlignment(e.target.value)}
|
||||||
|
>
|
||||||
|
{ALIGNMENTS.map((a) => <option key={a} value={a}>{a}</option>)}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={generate}
|
||||||
|
disabled={loading}
|
||||||
|
className="rounded-lg bg-[var(--color-gold-bright)] text-[var(--color-bg-deep)] px-4 py-2 text-sm font-semibold hover:bg-[var(--color-gold-muted)] transition-colors cursor-pointer disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{loading ? "✨ Generating…" : "✨ Generate NPC"}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Result */}
|
||||||
|
{error && (
|
||||||
|
<div className="rounded-lg bg-[var(--color-danger)]/10 border border-[var(--color-danger)]/30 p-3 text-[var(--color-danger)] text-xs overflow-x-auto">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{npc && (
|
||||||
|
<div className="rounded-lg bg-[var(--color-bg-surface)] border border-[var(--color-border-glass)] p-3 flex flex-col gap-2">
|
||||||
|
<h3 className="font-heading text-[var(--color-gold-bright)] text-sm">
|
||||||
|
{name || "NPC"}
|
||||||
|
</h3>
|
||||||
|
<p className="text-[var(--color-text-primary)] text-sm leading-relaxed">{npc.bio}</p>
|
||||||
|
<div>
|
||||||
|
<span className="text-[var(--color-text-secondary)] text-xs font-medium">Personality:</span>
|
||||||
|
<div className="flex flex-wrap gap-1 mt-1">
|
||||||
|
{npc.personality?.map((p, i) => (
|
||||||
|
<span key={i} className="rounded-full bg-[var(--color-gold-glow)] text-[var(--color-gold-bright)] px-2 py-0.5 text-xs">
|
||||||
|
{p}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-[var(--color-text-secondary)] text-xs font-medium">Goals:</span>
|
||||||
|
<ul className="list-disc list-inside text-[var(--color-text-primary)] text-xs mt-1">
|
||||||
|
{npc.goals?.map((g, i) => <li key={i}>{g}</li>)}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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<string>("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 (
|
||||||
|
<div className="flex gap-3 h-full">
|
||||||
|
{/* Notes column */}
|
||||||
|
<div className="flex flex-col gap-2 flex-1">
|
||||||
|
<textarea
|
||||||
|
className="flex-1 rounded-lg bg-[var(--color-bg-surface)] border border-[var(--color-border-glass)] p-3 text-[var(--color-text-primary)] placeholder-[var(--color-text-dim)] focus:outline-none focus:border-[var(--color-gold-bright)] text-sm resize-none"
|
||||||
|
value={notes}
|
||||||
|
onChange={(e) => setNotes(e.target.value)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter" && !e.shiftKey) {
|
||||||
|
e.preventDefault();
|
||||||
|
addEntry();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
placeholder="Type a session note, press Enter to add…"
|
||||||
|
/>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<button
|
||||||
|
onClick={addEntry}
|
||||||
|
className="rounded-lg bg-[var(--color-bg-surface)] border border-[var(--color-border-glass)] text-[var(--color-text-secondary)] px-3 py-1.5 text-xs hover:border-[var(--color-gold-bright)] hover:text-[var(--color-gold-bright)] transition-colors cursor-pointer"
|
||||||
|
>
|
||||||
|
Add Note
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={summarize}
|
||||||
|
disabled={loading || entries.length === 0}
|
||||||
|
className="rounded-lg bg-[var(--color-gold-bright)] text-[var(--color-bg-deep)] px-3 py-1.5 text-xs font-semibold hover:bg-[var(--color-gold-muted)] transition-colors cursor-pointer disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{loading ? "Summarizing…" : "✨ AI Summary"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 overflow-y-auto">
|
||||||
|
{entries.map((e, i) => (
|
||||||
|
<div key={i} className="flex gap-2 py-1 border-b border-[var(--color-border-subtle)]">
|
||||||
|
<span className="text-[var(--color-text-dim)] text-xs font-mono shrink-0">
|
||||||
|
{e.time}
|
||||||
|
</span>
|
||||||
|
<span className="text-[var(--color-text-primary)] text-xs">{e.text}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* AI Summary column */}
|
||||||
|
<div className="flex-1 flex flex-col">
|
||||||
|
<h3 className="text-xs font-medium text-[var(--color-text-secondary)] mb-2">AI Summary</h3>
|
||||||
|
{error && (
|
||||||
|
<div className="rounded-lg bg-[var(--color-danger)]/10 border border-[var(--color-danger)]/30 p-2 text-[var(--color-danger)] text-xs mb-2">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="flex-1 rounded-lg bg-[var(--color-bg-surface)] border border-[var(--color-border-glass)] p-3 text-[var(--color-text-primary)] text-sm overflow-y-auto">
|
||||||
|
{summary || (
|
||||||
|
<span className="text-[var(--color-text-dim)] italic">
|
||||||
|
Add session notes and click "AI Summary" to generate a recap and next-session hooks.
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
|
|
||||||
|
interface LlmConfig {
|
||||||
|
api_url: string;
|
||||||
|
api_key: string;
|
||||||
|
model: string;
|
||||||
|
temperature: number;
|
||||||
|
max_tokens: number;
|
||||||
|
top_p: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SettingsPanel() {
|
||||||
|
const [config, setConfig] = useState<LlmConfig | null>(null);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [saved, setSaved] = useState(false);
|
||||||
|
|
||||||
|
async function loadConfig() {
|
||||||
|
try {
|
||||||
|
const c = await invoke<LlmConfig>("get_llm_config");
|
||||||
|
setConfig(c);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to load config:", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveConfig() {
|
||||||
|
if (!config) return;
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
await invoke("set_llm_config", { config });
|
||||||
|
setSaved(true);
|
||||||
|
setTimeout(() => setSaved(false), 2000);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to save config:", e);
|
||||||
|
}
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!config) {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center justify-center h-full gap-4">
|
||||||
|
<p className="text-[var(--color-text-secondary)] text-sm">
|
||||||
|
Configure your LLM connection
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
onClick={loadConfig}
|
||||||
|
className="rounded-lg bg-[var(--color-gold-bright)] text-[var(--color-bg-deep)] px-4 py-2 text-sm font-semibold hover:bg-[var(--color-gold-muted)] transition-colors cursor-pointer"
|
||||||
|
>
|
||||||
|
Load Settings
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-4 text-sm">
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<label className="text-[var(--color-text-secondary)] text-xs font-medium">
|
||||||
|
API URL
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
className="rounded-lg bg-[var(--color-bg-surface)] border border-[var(--color-border-glass)] px-3 py-2 text-[var(--color-text-primary)] placeholder-[var(--color-text-dim)] focus:outline-none focus:border-[var(--color-gold-bright)]"
|
||||||
|
value={config.api_url}
|
||||||
|
onChange={(e) => setConfig({ ...config, api_url: e.target.value })}
|
||||||
|
placeholder="http://localhost:11434"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<label className="text-[var(--color-text-secondary)] text-xs font-medium">
|
||||||
|
API Key (optional)
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
className="rounded-lg bg-[var(--color-bg-surface)] border border-[var(--color-border-glass)] px-3 py-2 text-[var(--color-text-primary)] placeholder-[var(--color-text-dim)] focus:outline-none focus:border-[var(--color-gold-bright)]"
|
||||||
|
type="password"
|
||||||
|
value={config.api_key}
|
||||||
|
onChange={(e) => setConfig({ ...config, api_key: e.target.value })}
|
||||||
|
placeholder="sk-... (leave blank for local)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<label className="text-[var(--color-text-secondary)] text-xs font-medium">
|
||||||
|
Model
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
className="rounded-lg bg-[var(--color-bg-surface)] border border-[var(--color-border-glass)] px-3 py-2 text-[var(--color-text-primary)] placeholder-[var(--color-text-dim)] focus:outline-none focus:border-[var(--color-gold-bright)]"
|
||||||
|
value={config.model}
|
||||||
|
onChange={(e) => setConfig({ ...config, model: e.target.value })}
|
||||||
|
placeholder="llama3.2"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-3">
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<label className="text-[var(--color-text-secondary)] text-xs font-medium">
|
||||||
|
Temperature: {config.temperature}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="0"
|
||||||
|
max="2"
|
||||||
|
step="0.1"
|
||||||
|
value={config.temperature}
|
||||||
|
onChange={(e) =>
|
||||||
|
setConfig({ ...config, temperature: parseFloat(e.target.value) })
|
||||||
|
}
|
||||||
|
className="accent-[var(--color-gold-bright)]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<label className="text-[var(--color-text-secondary)] text-xs font-medium">
|
||||||
|
Max Tokens: {config.max_tokens}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="64"
|
||||||
|
max="4096"
|
||||||
|
step="64"
|
||||||
|
value={config.max_tokens}
|
||||||
|
onChange={(e) =>
|
||||||
|
setConfig({ ...config, max_tokens: parseInt(e.target.value) })
|
||||||
|
}
|
||||||
|
className="accent-[var(--color-gold-bright)]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={saveConfig}
|
||||||
|
disabled={loading}
|
||||||
|
className="rounded-lg bg-[var(--color-gold-bright)] text-[var(--color-bg-deep)] px-4 py-2 text-sm font-semibold hover:bg-[var(--color-gold-muted)] transition-colors cursor-pointer disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{saved ? "✓ Saved" : loading ? "Saving…" : "Save Settings"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user