Compare commits
23 Commits
6a25c6c05c
..
0.1.4
| Author | SHA1 | Date | |
|---|---|---|---|
| 7272d19207 | |||
| 9614d2884b | |||
| c891a8be58 | |||
| b6d0e10a7d | |||
| 8cf54f3346 | |||
| 874d7ff377 | |||
| 3c47ae1305 | |||
| 67d8902eaf | |||
| 2a7ec348c5 | |||
| 0653427557 | |||
| 6b6f65713d | |||
| c21819e786 | |||
| cc7eb3e7fb | |||
| 745e03a74f | |||
| e5a485d3a7 | |||
| 6c40f34122 | |||
| 5294feb5ff | |||
| 6832f1c5bc | |||
| 65213d3a9c | |||
| 73d5654e25 | |||
| 71f8f76f99 | |||
| 4d0e0c90a7 | |||
| bc71a30bfa |
@@ -9,17 +9,17 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: actions-rust-lang/setup-rust-toolchain@v1
|
- uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||||
with:
|
# with:
|
||||||
target: x86_64-pc-windows-gnu
|
# target: x86_64-pc-windows-gnu
|
||||||
|
|
||||||
- run: cargo build --release
|
- run: cargo build --release
|
||||||
- run: cargo build --release --target x86_64-pc-windows-gnu
|
# - run: cargo build --release --target x86_64-pc-windows-gnu
|
||||||
- uses: actions/upload-artifact@v4
|
- uses: christopherhx/gitea-upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: linux-release
|
name: linux-release
|
||||||
path: target/release/doc_writing_tool
|
path: target/release/worldcoder
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v4
|
# - uses: actions/upload-artifact@v4
|
||||||
with:
|
# with:
|
||||||
name: windows-release
|
# name: windows-release
|
||||||
path: target/x86_64-pc-windows-gnu/release/doc_writing_tool.exe
|
# path: target/x86_64-pc-windows-gnu/release/doc_writing_tool.exe
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
/target
|
/target
|
||||||
*/target
|
*/target
|
||||||
/project
|
/project
|
||||||
|
Cargo.lock
|
||||||
|
*.pkg.tar.zst
|
||||||
|
/pkg
|
||||||
|
/.config
|
||||||
|
|||||||
Vendored
+7
-2
@@ -1,9 +1,14 @@
|
|||||||
{
|
{
|
||||||
"rust-analyzer.check.command": "clippy",
|
"rust-analyzer.check.command": "clippy",
|
||||||
"editor.formatOnSave": true,
|
"editor.formatOnSave": true,
|
||||||
"rust-analyzer.cargo.features": "all",
|
|
||||||
"files.eol": "\n",
|
"files.eol": "\n",
|
||||||
"files.insertFinalNewline": true,
|
"files.insertFinalNewline": true,
|
||||||
"files.trimFinalNewlines": true,
|
"files.trimFinalNewlines": true,
|
||||||
"files.trimTrailingWhitespace": true
|
"files.trimTrailingWhitespace": true,
|
||||||
|
"rust-analyzer.cargo.features": [
|
||||||
|
"native",
|
||||||
|
"llm"
|
||||||
|
],
|
||||||
|
"rust-analyzer.cargo.noDefaultFeatures": true,
|
||||||
|
"rust-analyzer.cargo.allFeatures": false
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+188
-199
File diff suppressed because it is too large
Load Diff
+11
-10
@@ -1,12 +1,11 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "doc_writing_tool"
|
name = "worldcoder"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
eframe = "0.32.0"
|
eframe = "0.32.0"
|
||||||
egui = { version = "0.32.0", features = ["serde"] }
|
egui = { version = "0.32.0", features = ["serde"] }
|
||||||
editor = { path = "./editor" }
|
|
||||||
egui_extras = { version = "0.32.0", features = [
|
egui_extras = { version = "0.32.0", features = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"datepicker",
|
"datepicker",
|
||||||
@@ -16,15 +15,17 @@ egui_extras = { version = "0.32.0", features = [
|
|||||||
egui_file = "0.23.0"
|
egui_file = "0.23.0"
|
||||||
image = { version = "0.25.6", features = ["jpeg", "png"] }
|
image = { version = "0.25.6", features = ["jpeg", "png"] }
|
||||||
serde = { version = "1.0.219", features = ["derive"] }
|
serde = { version = "1.0.219", features = ["derive"] }
|
||||||
serde_json = "1.0.140"
|
serde_json = "1.0.141"
|
||||||
chrono = { version = "0.4.41", features = ["serde"] }
|
chrono = { version = "0.4.41", features = ["serde"] }
|
||||||
thiserror = "2.0.12"
|
thiserror = "2.0.14"
|
||||||
egui_commonmark = { version = "0.21.1", features = ["embedded_image"] }
|
egui_commonmark = { version = "0.21.1", features = ["embedded_image"] }
|
||||||
walkdir = "2.5.0"
|
|
||||||
uuid = { version = "1.17.0", features = ["v4"] }
|
uuid = { version = "1.17.0", features = ["v4"] }
|
||||||
reqwest = { version = "0.12.22", features = ["blocking", "json"] }
|
reqwest = { version = "0.12.23", features = ["blocking", "json"] }
|
||||||
|
tempfile = "3.20.0"
|
||||||
|
itertools = "0.14.0"
|
||||||
|
|
||||||
|
[features]
|
||||||
[target.x86_64-pc-windows-gnu]
|
default = ["native", "llm"]
|
||||||
linker = "x86_64-w64-mingw32-gcc"
|
web = []
|
||||||
ar = "x86_64-w64-mingw32-gcc-ar"
|
native = []
|
||||||
|
llm = []
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
pkgname=worldcoder
|
||||||
|
pkgver=0.1.1
|
||||||
|
pkgrel=3
|
||||||
|
makedepends=('rust' 'cargo')
|
||||||
|
arch=('i686' 'x86_64' 'armv6h' 'armv7h')
|
||||||
|
|
||||||
|
prepare() {
|
||||||
|
cargo fetch --locked --target "$CARCH-unknown-linux-gnu"
|
||||||
|
}
|
||||||
|
|
||||||
|
build() {
|
||||||
|
export RUSTUP_TOOLCHAIN=stable
|
||||||
|
export CARGO_TARGET_DIR=target
|
||||||
|
cargo build --frozen --release --all-features
|
||||||
|
}
|
||||||
|
|
||||||
|
check() {
|
||||||
|
export RUSTUP_TOOLCHAIN=stable
|
||||||
|
cargo test --frozen --all-features
|
||||||
|
}
|
||||||
|
|
||||||
|
package() {
|
||||||
|
install -Dm0755 -t "$pkgdir/usr/bin/" "target/release/$pkgname"
|
||||||
|
}
|
||||||
Generated
-354
@@ -1,354 +0,0 @@
|
|||||||
# This file is automatically @generated by Cargo.
|
|
||||||
# It is not intended for manual editing.
|
|
||||||
version = 4
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "ab_glyph"
|
|
||||||
version = "0.2.30"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "1e0f4f6fbdc5ee39f2ede9f5f3ec79477271a6d6a2baff22310d51736bda6cea"
|
|
||||||
dependencies = [
|
|
||||||
"ab_glyph_rasterizer",
|
|
||||||
"owned_ttf_parser",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "ab_glyph_rasterizer"
|
|
||||||
version = "0.1.9"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "b2187590a23ab1e3df8681afdf0987c48504d80291f002fcdb651f0ef5e25169"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "ahash"
|
|
||||||
version = "0.8.12"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75"
|
|
||||||
dependencies = [
|
|
||||||
"cfg-if",
|
|
||||||
"once_cell",
|
|
||||||
"version_check",
|
|
||||||
"zerocopy",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "autocfg"
|
|
||||||
version = "1.5.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "bitflags"
|
|
||||||
version = "2.9.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "cfg-if"
|
|
||||||
version = "1.0.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "ecolor"
|
|
||||||
version = "0.32.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "4a631732d995184114016fab22fc7e3faf73d6841c2d7650395fe251fbcd9285"
|
|
||||||
dependencies = [
|
|
||||||
"emath",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "editor"
|
|
||||||
version = "0.1.0"
|
|
||||||
dependencies = [
|
|
||||||
"egui",
|
|
||||||
"serde",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "egui"
|
|
||||||
version = "0.32.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "8470210c95a42cc985d9ffebfd5067eea55bdb1c3f7611484907db9639675e28"
|
|
||||||
dependencies = [
|
|
||||||
"ahash",
|
|
||||||
"bitflags",
|
|
||||||
"emath",
|
|
||||||
"epaint",
|
|
||||||
"nohash-hasher",
|
|
||||||
"profiling",
|
|
||||||
"smallvec",
|
|
||||||
"unicode-segmentation",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "emath"
|
|
||||||
version = "0.32.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "45f057b141e7e46340c321400be74b793543b1b213036f0f989c35d35957c32e"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "epaint"
|
|
||||||
version = "0.32.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "94cca02195f0552c17cabdc02f39aa9ab6fbd815dac60ab1cd3d5b0aa6f9551c"
|
|
||||||
dependencies = [
|
|
||||||
"ab_glyph",
|
|
||||||
"ahash",
|
|
||||||
"ecolor",
|
|
||||||
"emath",
|
|
||||||
"epaint_default_fonts",
|
|
||||||
"nohash-hasher",
|
|
||||||
"parking_lot",
|
|
||||||
"profiling",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "epaint_default_fonts"
|
|
||||||
version = "0.32.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "e8495e11ed527dff39663b8c36b6c2b2799d7e4287fb90556e455d72eca0b4d3"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "libc"
|
|
||||||
version = "0.2.174"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "lock_api"
|
|
||||||
version = "0.4.13"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765"
|
|
||||||
dependencies = [
|
|
||||||
"autocfg",
|
|
||||||
"scopeguard",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "nohash-hasher"
|
|
||||||
version = "0.2.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "once_cell"
|
|
||||||
version = "1.21.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "owned_ttf_parser"
|
|
||||||
version = "0.25.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "22ec719bbf3b2a81c109a4e20b1f129b5566b7dce654bc3872f6a05abf82b2c4"
|
|
||||||
dependencies = [
|
|
||||||
"ttf-parser",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "parking_lot"
|
|
||||||
version = "0.12.4"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13"
|
|
||||||
dependencies = [
|
|
||||||
"lock_api",
|
|
||||||
"parking_lot_core",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "parking_lot_core"
|
|
||||||
version = "0.9.11"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5"
|
|
||||||
dependencies = [
|
|
||||||
"cfg-if",
|
|
||||||
"libc",
|
|
||||||
"redox_syscall",
|
|
||||||
"smallvec",
|
|
||||||
"windows-targets",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "proc-macro2"
|
|
||||||
version = "1.0.95"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
|
|
||||||
dependencies = [
|
|
||||||
"unicode-ident",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "profiling"
|
|
||||||
version = "1.0.17"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "3eb8486b569e12e2c32ad3e204dbaba5e4b5b216e9367044f25f1dba42341773"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "quote"
|
|
||||||
version = "1.0.40"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "redox_syscall"
|
|
||||||
version = "0.5.13"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "0d04b7d0ee6b4a0207a0a7adb104d23ecb0b47d6beae7152d0fa34b692b29fd6"
|
|
||||||
dependencies = [
|
|
||||||
"bitflags",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "scopeguard"
|
|
||||||
version = "1.2.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "serde"
|
|
||||||
version = "1.0.219"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
|
|
||||||
dependencies = [
|
|
||||||
"serde_derive",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "serde_derive"
|
|
||||||
version = "1.0.219"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "smallvec"
|
|
||||||
version = "1.15.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "syn"
|
|
||||||
version = "2.0.104"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"unicode-ident",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "ttf-parser"
|
|
||||||
version = "0.25.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "unicode-ident"
|
|
||||||
version = "1.0.18"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "unicode-segmentation"
|
|
||||||
version = "1.12.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "version_check"
|
|
||||||
version = "0.9.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows-targets"
|
|
||||||
version = "0.52.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
|
|
||||||
dependencies = [
|
|
||||||
"windows_aarch64_gnullvm",
|
|
||||||
"windows_aarch64_msvc",
|
|
||||||
"windows_i686_gnu",
|
|
||||||
"windows_i686_gnullvm",
|
|
||||||
"windows_i686_msvc",
|
|
||||||
"windows_x86_64_gnu",
|
|
||||||
"windows_x86_64_gnullvm",
|
|
||||||
"windows_x86_64_msvc",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows_aarch64_gnullvm"
|
|
||||||
version = "0.52.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows_aarch64_msvc"
|
|
||||||
version = "0.52.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows_i686_gnu"
|
|
||||||
version = "0.52.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows_i686_gnullvm"
|
|
||||||
version = "0.52.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows_i686_msvc"
|
|
||||||
version = "0.52.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows_x86_64_gnu"
|
|
||||||
version = "0.52.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows_x86_64_gnullvm"
|
|
||||||
version = "0.52.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows_x86_64_msvc"
|
|
||||||
version = "0.52.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "zerocopy"
|
|
||||||
version = "0.8.26"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f"
|
|
||||||
dependencies = [
|
|
||||||
"zerocopy-derive",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "zerocopy-derive"
|
|
||||||
version = "0.8.26"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn",
|
|
||||||
]
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "editor"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2024"
|
|
||||||
description = "a basic text editor widget with line numbers"
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
egui = "0.32.0"
|
|
||||||
serde = "1"
|
|
||||||
|
|
||||||
[lib]
|
|
||||||
name = "editor"
|
|
||||||
path = "src/lib.rs"
|
|
||||||
@@ -1,214 +0,0 @@
|
|||||||
use egui::Color32;
|
|
||||||
use egui::TextBuffer;
|
|
||||||
use egui::widgets::text_edit::TextEditOutput;
|
|
||||||
|
|
||||||
use std::hash::{Hash, Hasher};
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
|
||||||
/// CodeEditor struct which stores settings for highlighting.
|
|
||||||
pub struct CodeEditor {
|
|
||||||
id: String,
|
|
||||||
numlines: bool,
|
|
||||||
numlines_shift: isize,
|
|
||||||
numlines_only_natural: bool,
|
|
||||||
fontsize: f32,
|
|
||||||
stick_to_bottom: bool,
|
|
||||||
desired_width: f32,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Hash for CodeEditor {
|
|
||||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
|
||||||
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
|
|
||||||
(self.fontsize as u32).hash(state);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for CodeEditor {
|
|
||||||
fn default() -> CodeEditor {
|
|
||||||
CodeEditor {
|
|
||||||
id: String::from("Code Editor"),
|
|
||||||
numlines: true,
|
|
||||||
numlines_shift: 0,
|
|
||||||
numlines_only_natural: false,
|
|
||||||
fontsize: 10.0,
|
|
||||||
stick_to_bottom: false,
|
|
||||||
desired_width: f32::INFINITY,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CodeEditor {
|
|
||||||
pub fn id_source(self, id_source: impl Into<String>) -> Self {
|
|
||||||
CodeEditor {
|
|
||||||
id: id_source.into(),
|
|
||||||
..self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Use custom font size
|
|
||||||
///
|
|
||||||
/// **Default: 10.0**
|
|
||||||
pub fn with_fontsize(self, fontsize: f32) -> Self {
|
|
||||||
CodeEditor { fontsize, ..self }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Use UI font size
|
|
||||||
pub fn with_ui_fontsize(self, ui: &mut egui::Ui) -> Self {
|
|
||||||
CodeEditor {
|
|
||||||
fontsize: egui::TextStyle::Monospace.resolve(ui.style()).size,
|
|
||||||
..self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Show or hide lines numbering
|
|
||||||
///
|
|
||||||
/// **Default: true**
|
|
||||||
pub fn with_numlines(self, numlines: bool) -> Self {
|
|
||||||
CodeEditor { numlines, ..self }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Shift lines numbering by this value
|
|
||||||
///
|
|
||||||
/// **Default: 0**
|
|
||||||
pub fn with_numlines_shift(self, numlines_shift: isize) -> Self {
|
|
||||||
CodeEditor {
|
|
||||||
numlines_shift,
|
|
||||||
..self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Show lines numbering only above zero, useful for enabling numbering since nth row
|
|
||||||
///
|
|
||||||
/// **Default: false**
|
|
||||||
pub fn with_numlines_only_natural(self, numlines_only_natural: bool) -> Self {
|
|
||||||
CodeEditor {
|
|
||||||
numlines_only_natural,
|
|
||||||
..self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Should the containing area shrink if the content is small?
|
|
||||||
///
|
|
||||||
/// **Default: false**
|
|
||||||
pub fn auto_shrink(self, shrink: bool) -> Self {
|
|
||||||
CodeEditor {
|
|
||||||
desired_width: if shrink { 0.0 } else { self.desired_width },
|
|
||||||
..self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets the desired width of the code editor
|
|
||||||
///
|
|
||||||
/// **Default: `f32::INFINITY`**
|
|
||||||
pub fn desired_width(self, width: f32) -> Self {
|
|
||||||
CodeEditor {
|
|
||||||
desired_width: width,
|
|
||||||
..self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Stick to bottom
|
|
||||||
/// The scroll handle will stick to the bottom position even while the content size
|
|
||||||
/// changes dynamically. This can be useful to simulate terminal UIs or log/info scrollers.
|
|
||||||
/// The scroll handle remains stuck until user manually changes position. Once "unstuck"
|
|
||||||
/// it will remain focused on whatever content viewport the user left it on. If the scroll
|
|
||||||
/// handle is dragged to the bottom it will again become stuck and remain there until manually
|
|
||||||
/// pulled from the end position.
|
|
||||||
///
|
|
||||||
/// **Default: false**
|
|
||||||
pub fn stick_to_bottom(self, stick_to_bottom: bool) -> Self {
|
|
||||||
CodeEditor {
|
|
||||||
stick_to_bottom,
|
|
||||||
..self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn format(&self, _string: &str) -> egui::text::TextFormat {
|
|
||||||
let font_id = egui::FontId::monospace(self.fontsize);
|
|
||||||
let color = Color32::WHITE;
|
|
||||||
egui::text::TextFormat::simple(font_id, color)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn numlines_show(&self, ui: &mut egui::Ui, text: &str) {
|
|
||||||
let total = if text.ends_with('\n') || text.is_empty() {
|
|
||||||
text.lines().count() + 1
|
|
||||||
} else {
|
|
||||||
text.lines().count()
|
|
||||||
} as isize;
|
|
||||||
|
|
||||||
let max_indent = total
|
|
||||||
.to_string()
|
|
||||||
.len()
|
|
||||||
.max(!self.numlines_only_natural as usize * self.numlines_shift.to_string().len());
|
|
||||||
let mut counter = (1..=total)
|
|
||||||
.map(|i| {
|
|
||||||
let num = i + self.numlines_shift;
|
|
||||||
if num <= 0 && self.numlines_only_natural {
|
|
||||||
String::new()
|
|
||||||
} else {
|
|
||||||
let label = num.to_string();
|
|
||||||
format!(
|
|
||||||
"{}{label}",
|
|
||||||
" ".repeat(max_indent.saturating_sub(label.len()))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect::<Vec<String>>()
|
|
||||||
.join("\n");
|
|
||||||
|
|
||||||
#[allow(clippy::cast_precision_loss)]
|
|
||||||
let width = max_indent as f32
|
|
||||||
* self.fontsize
|
|
||||||
* 0.5
|
|
||||||
* !(total + self.numlines_shift <= 0 && self.numlines_only_natural) as u8 as f32;
|
|
||||||
|
|
||||||
let mut layouter = |ui: &egui::Ui, string: &dyn TextBuffer, _wrap_width: f32| {
|
|
||||||
let layout_job = egui::text::LayoutJob::single_section(
|
|
||||||
string.as_str().to_string(), // Convert TextBuffer to String
|
|
||||||
egui::TextFormat::simple(egui::FontId::monospace(self.fontsize), Color32::WHITE),
|
|
||||||
);
|
|
||||||
ui.fonts(|f| f.layout_job(layout_job))
|
|
||||||
};
|
|
||||||
|
|
||||||
ui.add(
|
|
||||||
egui::TextEdit::multiline(&mut counter)
|
|
||||||
.id_source(format!("{}_numlines", self.id))
|
|
||||||
.font(egui::TextStyle::Monospace)
|
|
||||||
.interactive(false)
|
|
||||||
.frame(false)
|
|
||||||
.desired_width(width)
|
|
||||||
.layouter(&mut layouter),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Show Code Editor
|
|
||||||
pub fn show(&mut self, ui: &mut egui::Ui, text: &mut dyn egui::TextBuffer) -> TextEditOutput {
|
|
||||||
let mut text_edit_output: Option<TextEditOutput> = None;
|
|
||||||
let code_editor = |ui: &mut egui::Ui| {
|
|
||||||
ui.horizontal_top(|h| {
|
|
||||||
if self.numlines {
|
|
||||||
self.numlines_show(h, text.as_str());
|
|
||||||
}
|
|
||||||
egui::ScrollArea::horizontal()
|
|
||||||
.hscroll(true)
|
|
||||||
.id_salt(format!("{}_inner_scroll", self.id))
|
|
||||||
.show(h, |ui| {
|
|
||||||
let output = egui::TextEdit::multiline(text)
|
|
||||||
.id_source(&self.id)
|
|
||||||
.lock_focus(true)
|
|
||||||
.frame(false)
|
|
||||||
.desired_width(self.desired_width)
|
|
||||||
.show(ui);
|
|
||||||
text_edit_output = Some(output);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
egui::ScrollArea::vertical()
|
|
||||||
.id_salt(format!("{}_outer_scroll", self.id))
|
|
||||||
.stick_to_bottom(self.stick_to_bottom)
|
|
||||||
.show(ui, code_editor);
|
|
||||||
|
|
||||||
text_edit_output.expect("TextEditOutput should exist at this point")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 1.5 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 20 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 254 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 145 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 157 KiB |
@@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"date": "2025-07-17",
|
|
||||||
"project_name": "New Project",
|
|
||||||
"project_author": "Your Name",
|
|
||||||
"project_description": "Description of your project"
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"title": "test",
|
|
||||||
"id": "83592caa-f97d-427e-9d6a-50a586c30e6e",
|
|
||||||
"description": "ee",
|
|
||||||
"tags": [],
|
|
||||||
"content": "# Test project\n\n- this project is a test to ensure that this tool can be integrated with AI models correctly\n- I’m testing various prompts and parameters to evaluate its capabilities. The initial focus is on simple tasks like list generation, text summarization, and question answering. More complex scenarios involving code generation and creative writing will follow in subsequent phases. A key aspect of this test project involves documenting all interactions – both the prompts used and the AI’s responses – for later analysis. This allows us to identify patterns, biases, and areas where the tool can be improved. ",
|
|
||||||
"parent": null
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
{
|
|
||||||
"id": "161227ef-ba29-41a7-b40a-ed4ac550a8ea",
|
|
||||||
"template_id": "69cf7e1d-96a1-4d2a-9f08-bd3386d4bc69",
|
|
||||||
"name": "nucleus",
|
|
||||||
"fields": {
|
|
||||||
"age": {
|
|
||||||
"Number": 0.0
|
|
||||||
},
|
|
||||||
"parent": {
|
|
||||||
"Link": ""
|
|
||||||
},
|
|
||||||
"dob": {
|
|
||||||
"Date": "1970-01-01"
|
|
||||||
},
|
|
||||||
"pfp": {
|
|
||||||
"Image": "characters/nucleus.png"
|
|
||||||
},
|
|
||||||
"description": {
|
|
||||||
"MultiLine": "an AI"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"tags": []
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
{
|
|
||||||
"id": "3ce0e977-9f65-4f4c-a036-67f3d5c25fdc",
|
|
||||||
"template_id": "69cf7e1d-96a1-4d2a-9f08-bd3386d4bc69",
|
|
||||||
"name": "ZXQ5",
|
|
||||||
"fields": {
|
|
||||||
"dob": {
|
|
||||||
"Date": "1970-01-01"
|
|
||||||
},
|
|
||||||
"description": {
|
|
||||||
"MultiLine": "yes"
|
|
||||||
},
|
|
||||||
"age": {
|
|
||||||
"Number": 19.1
|
|
||||||
},
|
|
||||||
"parent": {
|
|
||||||
"Link": ""
|
|
||||||
},
|
|
||||||
"pfp": {
|
|
||||||
"Image": "characters/zxq5.png"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"tags": []
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
{
|
|
||||||
"id": "57429207-5fc1-4bab-a524-c550773c3d45",
|
|
||||||
"template_id": "69cf7e1d-96a1-4d2a-9f08-bd3386d4bc69",
|
|
||||||
"name": "Tayles",
|
|
||||||
"fields": {
|
|
||||||
"pfp": {
|
|
||||||
"Image": "characters/tayles.png"
|
|
||||||
},
|
|
||||||
"parent": {
|
|
||||||
"Link": ""
|
|
||||||
},
|
|
||||||
"description": {
|
|
||||||
"MultiLine": "trainspotter"
|
|
||||||
},
|
|
||||||
"age": {
|
|
||||||
"Number": 17.5
|
|
||||||
},
|
|
||||||
"dob": {
|
|
||||||
"Date": "1970-01-01"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"tags": []
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
{
|
|
||||||
"id": "be24e58f-3f79-4c5a-9224-9037eea5f51f",
|
|
||||||
"template_id": "69cf7e1d-96a1-4d2a-9f08-bd3386d4bc69",
|
|
||||||
"name": "The Order",
|
|
||||||
"fields": {
|
|
||||||
"pfp": {
|
|
||||||
"Image": "characters/the order.png"
|
|
||||||
},
|
|
||||||
"description": {
|
|
||||||
"MultiLine": "yes"
|
|
||||||
},
|
|
||||||
"dob": {
|
|
||||||
"Date": "1970-01-29"
|
|
||||||
},
|
|
||||||
"parent": {
|
|
||||||
"Link": ""
|
|
||||||
},
|
|
||||||
"age": {
|
|
||||||
"Number": 20.6
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"tags": [
|
|
||||||
"bbeddabd-914c-4648-8262-bf14bfcf8fff"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
{
|
|
||||||
"id": "deeaee92-bdec-4eb3-bb3b-ee760fc83d45",
|
|
||||||
"template_id": "69cf7e1d-96a1-4d2a-9f08-bd3386d4bc69",
|
|
||||||
"name": "The Chancellor",
|
|
||||||
"fields": {
|
|
||||||
"age": {
|
|
||||||
"Number": 37.0
|
|
||||||
},
|
|
||||||
"parent": {
|
|
||||||
"Link": ""
|
|
||||||
},
|
|
||||||
"dob": {
|
|
||||||
"Date": "1970-01-01"
|
|
||||||
},
|
|
||||||
"description": {
|
|
||||||
"MultiLine": "a tall ahh american"
|
|
||||||
},
|
|
||||||
"pfp": {
|
|
||||||
"Image": "characters/the chancellor.jpg"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"tags": [
|
|
||||||
"bbeddabd-914c-4648-8262-bf14bfcf8fff"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
{
|
|
||||||
"id": "bbeddabd-914c-4648-8262-bf14bfcf8fff",
|
|
||||||
"name": "American",
|
|
||||||
"description": "an american smh",
|
|
||||||
"color": [
|
|
||||||
0,
|
|
||||||
32,
|
|
||||||
207,
|
|
||||||
255
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "Species",
|
|
||||||
"id": "353649f9-e1f3-46d9-b723-8e56b510b2cc",
|
|
||||||
"description": "A classification system for living or digital entities.",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"name": "Diverged from",
|
|
||||||
"field_type": {
|
|
||||||
"Link": {
|
|
||||||
"template_id": null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": false,
|
|
||||||
"on_preview": false,
|
|
||||||
"description": "did this diverge from another documented species?"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Appearance / Features",
|
|
||||||
"field_type": "MultiLine",
|
|
||||||
"required": true,
|
|
||||||
"on_preview": true,
|
|
||||||
"description": "anatomy etc."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "behaviour",
|
|
||||||
"field_type": "MultiLine",
|
|
||||||
"required": true,
|
|
||||||
"on_preview": true,
|
|
||||||
"description": "aggressive, collaborative, etc.."
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "Character",
|
|
||||||
"id": "69cf7e1d-96a1-4d2a-9f08-bd3386d4bc69",
|
|
||||||
"description": "a character",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"name": "description",
|
|
||||||
"field_type": "MultiLine",
|
|
||||||
"required": true,
|
|
||||||
"on_preview": true,
|
|
||||||
"description": "yes"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "age",
|
|
||||||
"field_type": "Number",
|
|
||||||
"required": true,
|
|
||||||
"on_preview": false,
|
|
||||||
"description": "yes"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "dob",
|
|
||||||
"field_type": "Date",
|
|
||||||
"required": true,
|
|
||||||
"on_preview": false,
|
|
||||||
"description": "yes"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "parent",
|
|
||||||
"field_type": {
|
|
||||||
"Link": {
|
|
||||||
"template_id": null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": true,
|
|
||||||
"on_preview": false,
|
|
||||||
"description": "yes"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "pfp",
|
|
||||||
"field_type": "Image",
|
|
||||||
"required": true,
|
|
||||||
"on_preview": true,
|
|
||||||
"description": "yes"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
+6
-6
@@ -24,9 +24,9 @@
|
|||||||
|
|
||||||
### v0.1.1 - Editor
|
### v0.1.1 - Editor
|
||||||
|
|
||||||
- [ ] Basic editor (markdown formatting)
|
- [x] Basic editor (markdown formatting)
|
||||||
- [x] Basic text editing
|
- [x] Basic text editing
|
||||||
- [ ] Load & Save text file
|
- [x] Load & Save text file
|
||||||
- [x] Editor preview
|
- [x] Editor preview
|
||||||
- [x] Preview text in markdown
|
- [x] Preview text in markdown
|
||||||
|
|
||||||
@@ -34,13 +34,13 @@
|
|||||||
|
|
||||||
### v0.2.0 - links & context building
|
### v0.2.0 - links & context building
|
||||||
|
|
||||||
- [ ] Links between objects
|
- [x] Links between objects
|
||||||
- [ ] Links in templates
|
- [ ] Links in templates
|
||||||
|
|
||||||
### v0.2.1 - writing projects & organisation
|
### v0.2.1 - writing projects & organisation
|
||||||
|
|
||||||
- [ ] Project management
|
- [x] Project management
|
||||||
- [ ] Chapters/organisation
|
- [x] Chapters/organisation
|
||||||
|
|
||||||
## v0.3 - Workflows & AI integration
|
## v0.3 - Workflows & AI integration
|
||||||
|
|
||||||
@@ -70,7 +70,7 @@
|
|||||||
|
|
||||||
- [ ] Content generation AI
|
- [ ] Content generation AI
|
||||||
- [ ] Collect context from across a story or project
|
- [ ] Collect context from across a story or project
|
||||||
- [ ] Generate content for stories
|
- [x] Generate content for stories (Done to a very basic level)
|
||||||
- [ ] Create original content in the form of objects using templates
|
- [ ] Create original content in the form of objects using templates
|
||||||
- [ ] Create a new template
|
- [ ] Create a new template
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
use egui::{TextEdit, vec2};
|
use egui::{TextEdit, vec2};
|
||||||
|
|
||||||
use crate::{PROJECT_FOLDER, util};
|
use crate::{
|
||||||
|
PROJECT_FOLDER,
|
||||||
|
filesystem::{FILESYSTEM, FsError, LegacyFileSystem},
|
||||||
|
util,
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Asset {
|
pub struct Asset {
|
||||||
@@ -25,15 +29,14 @@ impl Asset {
|
|||||||
println!("old_path: {old_path:?}");
|
println!("old_path: {old_path:?}");
|
||||||
println!("new_path: {new_path:?}");
|
println!("new_path: {new_path:?}");
|
||||||
|
|
||||||
// move from src dir to name path
|
if let Err(FsError::Io(err)) = FILESYSTEM.rename(&old_path, &new_path) {
|
||||||
if let Err(err) = std::fs::rename(&old_path, &new_path) {
|
|
||||||
match err.kind() {
|
match err.kind() {
|
||||||
std::io::ErrorKind::NotFound => {
|
std::io::ErrorKind::NotFound => {
|
||||||
let dir = new_path.parent().unwrap();
|
let dir = new_path.parent().unwrap();
|
||||||
if !dir.exists() {
|
if !dir.exists() {
|
||||||
std::fs::create_dir_all(dir).unwrap();
|
FILESYSTEM.mkdir(dir).unwrap();
|
||||||
}
|
}
|
||||||
std::fs::rename(&old_path, &new_path).unwrap();
|
FILESYSTEM.rename(&old_path, &new_path).unwrap();
|
||||||
}
|
}
|
||||||
_ => panic!("Failed to rename file: {err}"),
|
_ => panic!("Failed to rename file: {err}"),
|
||||||
}
|
}
|
||||||
@@ -73,7 +76,7 @@ impl Asset {
|
|||||||
|
|
||||||
ui.separator();
|
ui.separator();
|
||||||
|
|
||||||
if let Ok(bytes) = std::fs::read(Self::path(&self.name)) {
|
if let Ok(bytes) = FILESYSTEM.read_bytes(&Self::path(&self.name)) {
|
||||||
let image_source = egui::ImageSource::Bytes {
|
let image_source = egui::ImageSource::Bytes {
|
||||||
uri: std::borrow::Cow::Owned(self.name.clone()),
|
uri: std::borrow::Cow::Owned(self.name.clone()),
|
||||||
bytes: bytes.into(),
|
bytes: bytes.into(),
|
||||||
|
|||||||
@@ -1,34 +1,51 @@
|
|||||||
|
use std::path::Path;
|
||||||
|
|
||||||
use egui::TextEdit;
|
use egui::TextEdit;
|
||||||
use egui_commonmark::{CommonMarkCache, CommonMarkViewer};
|
use egui_commonmark::{CommonMarkCache, CommonMarkViewer};
|
||||||
use serde::{self, Deserialize, Serialize};
|
use serde::{self, Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::{PROJECT_FOLDER, editors::tags::Tag, llm_integration::content_llm::ai_enabled, util};
|
use crate::{
|
||||||
|
editors::{settings_editor::ProjectSettings, tags::Tag},
|
||||||
|
filesystem::{FILESYSTEM, LegacyFileSystem},
|
||||||
|
util,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[cfg(feature = "llm")]
|
||||||
|
use crate::llm_integration::content_llm::{ContentAI, ReadyState};
|
||||||
|
|
||||||
pub struct MainEditor {
|
pub struct MainEditor {
|
||||||
pub content: ContentSection,
|
pub content: ContentSection,
|
||||||
pub show_editor: bool,
|
pub show_editor: bool,
|
||||||
|
pub editor_separate_window: bool,
|
||||||
pub show_preview: bool,
|
pub show_preview: bool,
|
||||||
preview_cache: CommonMarkCache,
|
preview_cache: CommonMarkCache,
|
||||||
|
|
||||||
|
#[cfg(feature = "llm")]
|
||||||
|
dialog: ContentAI,
|
||||||
|
#[cfg(feature = "llm")]
|
||||||
|
pub show_ai: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Clone for MainEditor {
|
impl Clone for MainEditor {
|
||||||
fn clone(&self) -> Self {
|
fn clone(&self) -> Self {
|
||||||
Self {
|
Self {
|
||||||
content: self.content.clone(),
|
content: self.content.clone(),
|
||||||
|
|
||||||
show_editor: self.show_editor,
|
show_editor: self.show_editor,
|
||||||
|
editor_separate_window: self.editor_separate_window,
|
||||||
show_preview: self.show_preview,
|
show_preview: self.show_preview,
|
||||||
preview_cache: CommonMarkCache::default(),
|
preview_cache: CommonMarkCache::default(),
|
||||||
|
|
||||||
|
#[cfg(feature = "llm")]
|
||||||
|
dialog: self.dialog.clone(),
|
||||||
|
#[cfg(feature = "llm")]
|
||||||
|
show_ai: self.show_ai,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
pub struct ContentSection {
|
pub struct ContentSection {
|
||||||
#[serde(default)]
|
|
||||||
pub title: String,
|
pub title: String,
|
||||||
|
|
||||||
#[serde(default)]
|
|
||||||
pub id: String,
|
pub id: String,
|
||||||
|
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
@@ -62,21 +79,16 @@ impl ContentSection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn save(&mut self) -> Result<(), Box<dyn std::error::Error>> {
|
pub fn save(&mut self) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let path = PROJECT_FOLDER
|
FILESYSTEM.write(
|
||||||
.join("documents")
|
Path::new(&format!("documents/{id}.json", id = &self.id)),
|
||||||
.join(format!("{}.json", &self.id));
|
self.clone(),
|
||||||
|
)?;
|
||||||
let content = serde_json::to_string_pretty(self)?;
|
|
||||||
std::fs::write(path, content)?;
|
|
||||||
self.saved = true;
|
self.saved = true;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn load(id: &str) -> Result<Self, Box<dyn std::error::Error>> {
|
pub fn load(id: &str) -> Result<Self, Box<dyn std::error::Error>> {
|
||||||
let path = PROJECT_FOLDER.join("documents").join(format!("{id}.json"));
|
let mut section: Self = FILESYSTEM.read(Path::new(&format!("documents/{id}.json")))?;
|
||||||
|
|
||||||
let content = std::fs::read_to_string(&path)?;
|
|
||||||
let mut section: Self = serde_json::from_str(&content)?;
|
|
||||||
section.saved = true;
|
section.saved = true;
|
||||||
section.id = id.to_string();
|
section.id = id.to_string();
|
||||||
Ok(section)
|
Ok(section)
|
||||||
@@ -96,7 +108,13 @@ impl MainEditor {
|
|||||||
content: ContentSection::new(),
|
content: ContentSection::new(),
|
||||||
show_editor: false, // Start with editor hidden
|
show_editor: false, // Start with editor hidden
|
||||||
show_preview: false,
|
show_preview: false,
|
||||||
|
editor_separate_window: false,
|
||||||
preview_cache: CommonMarkCache::default(),
|
preview_cache: CommonMarkCache::default(),
|
||||||
|
|
||||||
|
#[cfg(feature = "llm")]
|
||||||
|
show_ai: false,
|
||||||
|
#[cfg(feature = "llm")]
|
||||||
|
dialog: ContentAI::new(String::new()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,20 +123,17 @@ impl MainEditor {
|
|||||||
content,
|
content,
|
||||||
show_editor: true,
|
show_editor: true,
|
||||||
show_preview: false,
|
show_preview: false,
|
||||||
|
editor_separate_window: false,
|
||||||
preview_cache: CommonMarkCache::default(),
|
preview_cache: CommonMarkCache::default(),
|
||||||
|
|
||||||
|
#[cfg(feature = "llm")]
|
||||||
|
show_ai: false,
|
||||||
|
#[cfg(feature = "llm")]
|
||||||
|
dialog: ContentAI::new(String::new()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn ui(&mut self, ctx: &egui::Context) {
|
pub fn render_ui(&mut self, project: &mut ProjectSettings, ui: &mut egui::Ui) {
|
||||||
// Show the editor window if enabled
|
|
||||||
let mut show = self.show_editor;
|
|
||||||
if show {
|
|
||||||
egui::Window::new("Markdown Editor")
|
|
||||||
.resizable(true)
|
|
||||||
.default_width(1000.0)
|
|
||||||
.default_height(800.0)
|
|
||||||
.open(&mut show)
|
|
||||||
.show(ctx, |ui| {
|
|
||||||
ui.vertical(|ui| {
|
ui.vertical(|ui| {
|
||||||
// check for Ctrl+S to save
|
// check for Ctrl+S to save
|
||||||
if ui.input(|i| i.key_pressed(egui::Key::S) && i.modifiers.ctrl) {
|
if ui.input(|i| i.key_pressed(egui::Key::S) && i.modifiers.ctrl) {
|
||||||
@@ -154,11 +169,11 @@ impl MainEditor {
|
|||||||
|
|
||||||
// delete button
|
// delete button
|
||||||
if ui.button("Delete").clicked() {
|
if ui.button("Delete").clicked() {
|
||||||
std::fs::remove_file(
|
FILESYSTEM
|
||||||
PROJECT_FOLDER
|
.delete(Path::new(&format!(
|
||||||
.join("documents")
|
"documents/{id}.json",
|
||||||
.join(format!("{}.json", self.content.id)),
|
id = &self.content.id
|
||||||
)
|
)))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
*self = Self::new();
|
*self = Self::new();
|
||||||
@@ -171,6 +186,15 @@ impl MainEditor {
|
|||||||
|
|
||||||
// preview toggle
|
// preview toggle
|
||||||
ui.checkbox(&mut self.show_preview, "Preview");
|
ui.checkbox(&mut self.show_preview, "Preview");
|
||||||
|
|
||||||
|
// assistant toggle
|
||||||
|
#[cfg(feature = "llm")]
|
||||||
|
if project.ai_enabled() {
|
||||||
|
ui.checkbox(&mut self.show_ai, "AI Assistant");
|
||||||
|
}
|
||||||
|
|
||||||
|
// editor toggle
|
||||||
|
ui.checkbox(&mut self.editor_separate_window, "Pop out editor");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -208,22 +232,55 @@ impl MainEditor {
|
|||||||
ui.end_row();
|
ui.end_row();
|
||||||
|
|
||||||
ui.strong("Tags");
|
ui.strong("Tags");
|
||||||
Tag::selector_ui(
|
Tag::selector_ui(&mut self.content.tags, ui, Some(&mut self.content.saved));
|
||||||
&mut self.content.tags,
|
|
||||||
ui,
|
|
||||||
Some(&mut self.content.saved),
|
|
||||||
);
|
|
||||||
ui.end_row();
|
ui.end_row();
|
||||||
});
|
});
|
||||||
|
|
||||||
ui.separator();
|
ui.separator();
|
||||||
|
|
||||||
|
#[cfg(feature = "llm")]
|
||||||
|
if self.show_ai && project.ai_enabled() {
|
||||||
|
let dialog = &mut self.dialog;
|
||||||
|
|
||||||
|
dialog.content = self.content.content.clone();
|
||||||
|
dialog.ui(ui, project);
|
||||||
|
|
||||||
|
if *dialog.ready.lock().unwrap() == ReadyState::Ready {
|
||||||
|
self.content
|
||||||
|
.content
|
||||||
|
.push_str(&dialog.result.lock().unwrap());
|
||||||
|
self.content.saved = false;
|
||||||
|
*dialog.ready.lock().unwrap() = ReadyState::Idle;
|
||||||
|
} else if *dialog.ready.lock().unwrap() == ReadyState::Halted {
|
||||||
|
*dialog.ready.lock().unwrap() = ReadyState::Idle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if self.show_preview {
|
if self.show_preview {
|
||||||
self.preview_ui(ui);
|
self.preview_ui(ui);
|
||||||
}
|
}
|
||||||
|
|
||||||
self.editor_ui(ui);
|
self.editor_ui(ui, project);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ui(&mut self, ctx: &egui::Context, project: &mut ProjectSettings) {
|
||||||
|
// Show the editor window if enabled
|
||||||
|
let mut show = self.show_editor;
|
||||||
|
if show {
|
||||||
|
if self.editor_separate_window {
|
||||||
|
egui::Window::new("Editor")
|
||||||
|
.resizable(true)
|
||||||
|
.default_width(1000.0)
|
||||||
|
.default_height(800.0)
|
||||||
|
.open(&mut show)
|
||||||
|
.show(ctx, |ui| {
|
||||||
|
self.render_ui(project, ui);
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
egui::CentralPanel::default().show(ctx, |ui| {
|
||||||
|
self.render_ui(project, ui);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.show_editor = show;
|
self.show_editor = show;
|
||||||
@@ -263,7 +320,7 @@ impl MainEditor {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn editor_ui(&mut self, ui: &mut egui::Ui) {
|
fn editor_ui(&mut self, ui: &mut egui::Ui, _project: &mut ProjectSettings) {
|
||||||
let _response = egui::ScrollArea::both()
|
let _response = egui::ScrollArea::both()
|
||||||
.auto_shrink([false, false])
|
.auto_shrink([false, false])
|
||||||
.id_salt("editor_scroll")
|
.id_salt("editor_scroll")
|
||||||
@@ -281,49 +338,19 @@ impl MainEditor {
|
|||||||
|
|
||||||
ui.set_min_width(max_width as f32);
|
ui.set_min_width(max_width as f32);
|
||||||
|
|
||||||
let text_edit = TextEdit::multiline(&mut self.content.content)
|
let response = ui.add(
|
||||||
|
TextEdit::multiline(&mut self.content.content)
|
||||||
.id_source("MainEditor_editor")
|
.id_source("MainEditor_editor")
|
||||||
.font(egui::TextStyle::Monospace)
|
.font(egui::TextStyle::Monospace)
|
||||||
.interactive(true)
|
.interactive(true)
|
||||||
.frame(false)
|
.frame(false)
|
||||||
.lock_focus(true)
|
.lock_focus(true)
|
||||||
.hint_text("Type here...")
|
.hint_text("Type here...")
|
||||||
.desired_width(max_width as f32);
|
.desired_width(max_width as f32),
|
||||||
|
);
|
||||||
let mut ctx_menu = false;
|
if response.changed() {
|
||||||
let response = ui
|
|
||||||
.add_sized(
|
|
||||||
egui::vec2(max_width as f32 - 30.0, ui.available_height()),
|
|
||||||
text_edit,
|
|
||||||
)
|
|
||||||
.on_hover_text("Right click to open context menu")
|
|
||||||
.context_menu(|ui| {
|
|
||||||
ctx_menu = true;
|
|
||||||
|
|
||||||
ui.menu_button("AI Actions", |ui| {
|
|
||||||
ui.add_enabled_ui(ai_enabled(), |ui| {
|
|
||||||
if ui.button("Summarise").clicked() {
|
|
||||||
println!("Summarise");
|
|
||||||
}
|
|
||||||
|
|
||||||
if ui.button("Continue").clicked() {
|
|
||||||
let content = self.content.content.clone();
|
|
||||||
let response =
|
|
||||||
crate::llm_integration::content_llm::continue_content(
|
|
||||||
&content, "", 1024,
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
self.content.content.push_str(&response);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
if let Some(response) = response {
|
|
||||||
if response.response.changed() || ctx_menu {
|
|
||||||
self.content.saved = false;
|
self.content.saved = false;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,80 +0,0 @@
|
|||||||
use std::io::Read;
|
|
||||||
|
|
||||||
use chrono::NaiveDate;
|
|
||||||
use egui_extras::DatePickerButton;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
use crate::PROJECT_FOLDER;
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
|
||||||
pub struct ProjectContext {
|
|
||||||
date: NaiveDate,
|
|
||||||
project_name: String,
|
|
||||||
project_author: String,
|
|
||||||
project_description: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ProjectContext {
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self::default()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn load() -> Self {
|
|
||||||
let path = PROJECT_FOLDER.join("context.json");
|
|
||||||
if let Ok(mut file) = std::fs::File::open(path) {
|
|
||||||
let mut contents = String::new();
|
|
||||||
file.read_to_string(&mut contents).unwrap();
|
|
||||||
if let Ok(proj) = serde_json::from_str(&contents) {
|
|
||||||
return proj;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Self::default()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn save(&self) {
|
|
||||||
let path = PROJECT_FOLDER.join("context.json");
|
|
||||||
let content = serde_json::to_string_pretty(self).unwrap();
|
|
||||||
std::fs::write(path, content).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub fn ui(&mut self, ui: &mut egui::Ui) {
|
|
||||||
// table
|
|
||||||
egui::Grid::new("context_editor")
|
|
||||||
.striped(true)
|
|
||||||
.num_columns(2)
|
|
||||||
.show(ui, |ui| {
|
|
||||||
ui.label("Project Name");
|
|
||||||
ui.text_edit_singleline(&mut self.project_name);
|
|
||||||
|
|
||||||
ui.end_row();
|
|
||||||
|
|
||||||
ui.label("Project Author");
|
|
||||||
ui.text_edit_singleline(&mut self.project_author);
|
|
||||||
|
|
||||||
ui.end_row();
|
|
||||||
|
|
||||||
ui.label("Project Description");
|
|
||||||
ui.text_edit_singleline(&mut self.project_description);
|
|
||||||
|
|
||||||
ui.end_row();
|
|
||||||
|
|
||||||
ui.label("Date");
|
|
||||||
ui.add(DatePickerButton::new(&mut self.date));
|
|
||||||
|
|
||||||
ui.end_row();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for ProjectContext {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
date: chrono::Local::now().naive_local().into(),
|
|
||||||
project_name: "New Project".to_string(),
|
|
||||||
project_author: "Your Name".to_string(),
|
|
||||||
project_description: "Description of your project".to_string(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
pub mod asset_editor;
|
pub mod asset_editor;
|
||||||
pub mod content_editor;
|
pub mod content_editor;
|
||||||
pub mod context_editor;
|
|
||||||
pub mod note_editor;
|
pub mod note_editor;
|
||||||
pub mod object_editor;
|
pub mod object_editor;
|
||||||
|
pub mod settings_editor;
|
||||||
pub mod tags;
|
pub mod tags;
|
||||||
pub mod template_editor;
|
pub mod template_editor;
|
||||||
|
|||||||
+17
-12
@@ -1,13 +1,19 @@
|
|||||||
use std::fs;
|
use std::path::Path;
|
||||||
|
|
||||||
use egui::TextEdit;
|
use egui::TextEdit;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::{PROJECT_FOLDER, editors::tags::Tag, util};
|
use crate::{
|
||||||
|
editors::tags::Tag,
|
||||||
|
filesystem::{FILESYSTEM, LegacyFileSystem},
|
||||||
|
util,
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
pub struct Note {
|
pub struct Note {
|
||||||
|
pub id: String,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub content: String,
|
pub content: String,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
@@ -17,12 +23,14 @@ pub struct Note {
|
|||||||
pub tags: Vec<String>,
|
pub tags: Vec<String>,
|
||||||
|
|
||||||
#[serde(skip)]
|
#[serde(skip)]
|
||||||
pub id: String,
|
#[serde(default = "default_saved")]
|
||||||
|
|
||||||
#[serde(skip)]
|
|
||||||
pub saved: bool,
|
pub saved: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn default_saved() -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
impl Default for Note {
|
impl Default for Note {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
@@ -48,18 +56,15 @@ impl Note {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn save(&mut self) -> std::io::Result<()> {
|
pub fn save(&mut self) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let id = &self.id;
|
let id = &self.id;
|
||||||
let path = PROJECT_FOLDER.join("notes").join(format!("{id}.json"));
|
FILESYSTEM.write(Path::new(&format!("notes/{id}.json")), self.clone())?;
|
||||||
fs::write(path, serde_json::to_string(&self)?)?;
|
|
||||||
self.saved = true;
|
self.saved = true;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn load(id: &str) -> std::io::Result<Self> {
|
pub fn load(id: &str) -> Result<Self, Box<dyn std::error::Error>> {
|
||||||
let path = PROJECT_FOLDER.join("notes").join(format!("{id}.json"));
|
let mut note: Self = FILESYSTEM.read(Path::new(&format!("notes/{id}.json")))?;
|
||||||
let content = fs::read_to_string(path)?;
|
|
||||||
let mut note: Note = serde_json::from_str(&content)?;
|
|
||||||
note.id = id.to_string();
|
note.id = id.to_string();
|
||||||
note.saved = true;
|
note.saved = true;
|
||||||
Ok(note)
|
Ok(note)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
use core::f32;
|
use core::f32;
|
||||||
use egui::{CollapsingHeader, RichText, Sense, TextEdit, Ui, UiBuilder, vec2};
|
use egui::{CollapsingHeader, RichText, Sense, TextEdit, Ui, UiBuilder, vec2};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
PROJECT_FOLDER, RightPanelContent,
|
PROJECT_FOLDER, RightPanelContent,
|
||||||
@@ -8,6 +9,7 @@ use crate::{
|
|||||||
tags::Tag,
|
tags::Tag,
|
||||||
template_editor::{FieldValue, Template},
|
template_editor::{FieldValue, Template},
|
||||||
},
|
},
|
||||||
|
filesystem::{FILESYSTEM, LegacyFileSystem},
|
||||||
util,
|
util,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -81,21 +83,14 @@ impl ObjectInstance {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn save(&mut self) -> Result<(), Box<dyn std::error::Error>> {
|
pub fn save(&mut self) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let path = PROJECT_FOLDER
|
let id = &self.id;
|
||||||
.join("objects")
|
FILESYSTEM.write(Path::new(&format!("objects/{id}.json")), self.clone())?;
|
||||||
.join(format!("{}.json", &self.id));
|
|
||||||
|
|
||||||
let content = serde_json::to_string_pretty(self)?;
|
|
||||||
std::fs::write(&path, content)?;
|
|
||||||
self.saved = true;
|
self.saved = true;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn load(id: &str) -> Result<Self, Box<dyn std::error::Error>> {
|
pub fn load(id: &str) -> Result<Self, Box<dyn std::error::Error>> {
|
||||||
let path = PROJECT_FOLDER.join("objects").join(format!("{id}.json"));
|
let mut instance: Self = FILESYSTEM.read(Path::new(&format!("objects/{id}.json")))?;
|
||||||
|
|
||||||
let content = std::fs::read_to_string(&path)?;
|
|
||||||
let mut instance: ObjectInstance = serde_json::from_str(&content)?;
|
|
||||||
instance.saved = true;
|
instance.saved = true;
|
||||||
Ok(instance)
|
Ok(instance)
|
||||||
}
|
}
|
||||||
@@ -137,12 +132,10 @@ impl ObjectInstance {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ui.button("Delete").clicked() {
|
if ui.button("Delete").clicked() {
|
||||||
std::fs::remove_file(
|
let id = &self.id;
|
||||||
PROJECT_FOLDER
|
FILESYSTEM
|
||||||
.join("objects")
|
.delete(Path::new(&format!("objects/{id}.json")))
|
||||||
.join(format!("{}.json", self.id)),
|
.expect("Failed to delete object");
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
*right_panel = Some(RightPanelContent::None);
|
*right_panel = Some(RightPanelContent::None);
|
||||||
}
|
}
|
||||||
@@ -274,7 +267,7 @@ impl ObjectInstance {
|
|||||||
if !value.is_empty() {
|
if !value.is_empty() {
|
||||||
let path = PROJECT_FOLDER.join("assets").join(&value);
|
let path = PROJECT_FOLDER.join("assets").join(&value);
|
||||||
|
|
||||||
if let Ok(bytes) = std::fs::read(&path) {
|
if let Ok(bytes) = FILESYSTEM.read_bytes(&path) {
|
||||||
let image_source = egui::ImageSource::Bytes {
|
let image_source = egui::ImageSource::Bytes {
|
||||||
uri: std::borrow::Cow::Owned(path.to_str().unwrap().to_string()),
|
uri: std::borrow::Cow::Owned(path.to_str().unwrap().to_string()),
|
||||||
bytes: bytes.into(),
|
bytes: bytes.into(),
|
||||||
|
|||||||
@@ -0,0 +1,396 @@
|
|||||||
|
use chrono::NaiveDate;
|
||||||
|
use egui::TextEdit;
|
||||||
|
use egui_extras::DatePickerButton;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
filesystem::{FILESYSTEM, LegacyFileSystem},
|
||||||
|
util::saved_status,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone)]
|
||||||
|
pub struct ProjectSettings {
|
||||||
|
date: NaiveDate,
|
||||||
|
project_name: String,
|
||||||
|
project_author: String,
|
||||||
|
project_description: String,
|
||||||
|
|
||||||
|
// AI settings
|
||||||
|
#[cfg(feature = "llm")]
|
||||||
|
pub ai_context: String,
|
||||||
|
|
||||||
|
// settings
|
||||||
|
#[serde(skip)]
|
||||||
|
pub global_settings: EditorSettings,
|
||||||
|
#[serde(skip)]
|
||||||
|
pub local_overrides: EditorSettings,
|
||||||
|
|
||||||
|
#[serde(skip)]
|
||||||
|
pub open: bool,
|
||||||
|
|
||||||
|
#[serde(skip)]
|
||||||
|
pub saved: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ProjectSettings {
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load() -> Self {
|
||||||
|
if let Ok(mut proj) = FILESYSTEM.read::<Self>(Path::new("project.json")) {
|
||||||
|
proj.saved = true;
|
||||||
|
proj.global_settings = EditorSettings::load_global();
|
||||||
|
proj.local_overrides = EditorSettings::load();
|
||||||
|
proj
|
||||||
|
} else {
|
||||||
|
Self::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn save(&mut self) {
|
||||||
|
FILESYSTEM
|
||||||
|
.write(Path::new("project.json"), self.clone())
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
self.global_settings.save();
|
||||||
|
self.local_overrides.save();
|
||||||
|
|
||||||
|
self.saved = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(unused)]
|
||||||
|
fn config_str_override(
|
||||||
|
label: &str,
|
||||||
|
field: &mut Option<String>,
|
||||||
|
default: &str,
|
||||||
|
ui: &mut egui::Ui,
|
||||||
|
) -> bool {
|
||||||
|
let mut changed = false;
|
||||||
|
|
||||||
|
ui.label(label);
|
||||||
|
|
||||||
|
if let Some(value) = field {
|
||||||
|
if ui.text_edit_singleline(value).changed() {
|
||||||
|
changed = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
if ui.button("Remove Override").clicked() {
|
||||||
|
*field = None;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
} else if ui.button("Override").clicked() {
|
||||||
|
*field = Some(default.to_string());
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.end_row();
|
||||||
|
changed
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(unused)]
|
||||||
|
fn config_bool_override(
|
||||||
|
label: &str,
|
||||||
|
field: &mut Option<bool>,
|
||||||
|
default: bool,
|
||||||
|
ui: &mut egui::Ui,
|
||||||
|
) -> bool {
|
||||||
|
let mut changed = false;
|
||||||
|
|
||||||
|
ui.label(label);
|
||||||
|
|
||||||
|
if let Some(value) = field {
|
||||||
|
if ui.checkbox(value, "Enable AI").changed() {
|
||||||
|
changed = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
if ui.button("Remove Override").clicked() {
|
||||||
|
*field = None;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
} else if ui.button("Override").clicked() {
|
||||||
|
*field = Some(default);
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.end_row();
|
||||||
|
changed
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(unused)]
|
||||||
|
fn config_str(field: &mut String, label: &str, ui: &mut egui::Ui) -> bool {
|
||||||
|
let mut changed = false;
|
||||||
|
|
||||||
|
ui.label(label);
|
||||||
|
if ui.text_edit_singleline(field).changed() {
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.end_row();
|
||||||
|
|
||||||
|
changed
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(unused)]
|
||||||
|
fn config_bool(label: &str, field: &mut bool, ui: &mut egui::Ui) -> bool {
|
||||||
|
let mut changed = false;
|
||||||
|
|
||||||
|
ui.label(label);
|
||||||
|
if ui.checkbox(field, "Enable AI").changed() {
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.end_row();
|
||||||
|
|
||||||
|
changed
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(unused)]
|
||||||
|
pub fn ui(&mut self, ui: &mut egui::Ui) {
|
||||||
|
// save state
|
||||||
|
saved_status(ui, self.saved, "N/A", "Project Settings");
|
||||||
|
if ui.button("Save").clicked() {
|
||||||
|
self.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ui.input(|i| i.key_pressed(egui::Key::S) && i.modifiers.ctrl) {
|
||||||
|
self.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.separator();
|
||||||
|
|
||||||
|
// project settings
|
||||||
|
ui.heading("Project Settings");
|
||||||
|
egui::Grid::new("project settings")
|
||||||
|
.striped(true)
|
||||||
|
.num_columns(2)
|
||||||
|
.show(ui, |ui| {
|
||||||
|
if Self::config_str(&mut self.project_name, "Project Name", ui) { self.saved = false };
|
||||||
|
if Self::config_str(&mut self.project_author, "Project Author", ui) { self.saved = false };
|
||||||
|
if Self::config_str(&mut self.project_description, "Project Description", ui) { self.saved = false };
|
||||||
|
|
||||||
|
ui.label("Date");
|
||||||
|
if ui.add(DatePickerButton::new(&mut self.date)).changed() { self.saved = false };
|
||||||
|
ui.end_row();
|
||||||
|
|
||||||
|
#[cfg(feature = "llm")]
|
||||||
|
{
|
||||||
|
ui.label("AI Context Prompt");
|
||||||
|
if ui.add(TextEdit::multiline(&mut self.ai_context)
|
||||||
|
.font(egui::TextStyle::Monospace)
|
||||||
|
.interactive(true)
|
||||||
|
.frame(false)
|
||||||
|
.lock_focus(true)
|
||||||
|
.hint_text("What is this project about? what should the LLM know when generating content for this project?")).changed() { self.saved = false };
|
||||||
|
ui.end_row();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ui.separator();
|
||||||
|
|
||||||
|
// local settings overrides for editor
|
||||||
|
|
||||||
|
ui.heading("Local Overrides");
|
||||||
|
egui::Grid::new("local overrides")
|
||||||
|
.striped(true)
|
||||||
|
.num_columns(2)
|
||||||
|
.show(ui, |ui| {
|
||||||
|
#[cfg(feature = "llm")]
|
||||||
|
if ProjectSettings::config_str_override(
|
||||||
|
"LLM API URI",
|
||||||
|
&mut self.local_overrides.llm_api_uri,
|
||||||
|
"http://localhost:1234",
|
||||||
|
ui,
|
||||||
|
) {
|
||||||
|
self.saved = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "llm")]
|
||||||
|
if ProjectSettings::config_str_override(
|
||||||
|
"LLM API Key",
|
||||||
|
&mut self.local_overrides.llm_api_key,
|
||||||
|
"1234",
|
||||||
|
ui,
|
||||||
|
) {
|
||||||
|
self.saved = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "llm")]
|
||||||
|
if ProjectSettings::config_bool_override(
|
||||||
|
"Enable AI",
|
||||||
|
&mut self.local_overrides.ai_enabled,
|
||||||
|
true,
|
||||||
|
ui,
|
||||||
|
) {
|
||||||
|
self.saved = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ui.separator();
|
||||||
|
|
||||||
|
// global editor settings
|
||||||
|
ui.heading("Global Editor Settings");
|
||||||
|
egui::Grid::new("global settings")
|
||||||
|
.striped(true)
|
||||||
|
.num_columns(2)
|
||||||
|
.show(ui, |ui| {
|
||||||
|
#[cfg(feature = "llm")]
|
||||||
|
if Self::config_bool(
|
||||||
|
"Enable AI",
|
||||||
|
self.global_settings.ai_enabled.as_mut().unwrap(),
|
||||||
|
ui,
|
||||||
|
) {
|
||||||
|
self.saved = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "llm")]
|
||||||
|
if Self::config_str(
|
||||||
|
self.global_settings.llm_api_uri.as_mut().unwrap(),
|
||||||
|
"LLM API URI",
|
||||||
|
ui,
|
||||||
|
) {
|
||||||
|
self.saved = false
|
||||||
|
};
|
||||||
|
|
||||||
|
#[cfg(feature = "llm")]
|
||||||
|
if Self::config_str(
|
||||||
|
self.global_settings.llm_api_key.as_mut().unwrap(),
|
||||||
|
"LLM API Key",
|
||||||
|
ui,
|
||||||
|
) {
|
||||||
|
self.saved = false
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "llm")]
|
||||||
|
#[allow(unused)]
|
||||||
|
pub fn ai_enabled(&mut self) -> bool {
|
||||||
|
let client = reqwest::blocking::Client::new();
|
||||||
|
|
||||||
|
if self.global_settings.ai_enabled.unwrap() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if client
|
||||||
|
.get(self.global_settings.llm_api_uri.clone().unwrap() + "/v1/models")
|
||||||
|
.send()
|
||||||
|
.is_ok()
|
||||||
|
{
|
||||||
|
self.global_settings.ai_enabled = Some(true);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn open(&mut self) {
|
||||||
|
self.open = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn _close(&mut self) {
|
||||||
|
self.open = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ProjectSettings {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
date: chrono::Local::now().naive_local().into(),
|
||||||
|
project_name: "New Project".to_string(),
|
||||||
|
project_author: "Your Name".to_string(),
|
||||||
|
project_description: "Description of your project".to_string(),
|
||||||
|
|
||||||
|
#[cfg(feature = "llm")]
|
||||||
|
ai_context: "".to_string(),
|
||||||
|
global_settings: EditorSettings::default(),
|
||||||
|
local_overrides: EditorSettings::new(),
|
||||||
|
|
||||||
|
// window state
|
||||||
|
open: false,
|
||||||
|
saved: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone)]
|
||||||
|
pub struct EditorSettings {
|
||||||
|
pub dark_theme: Option<bool>,
|
||||||
|
|
||||||
|
#[cfg(feature = "llm")]
|
||||||
|
pub llm_api_uri: Option<String>,
|
||||||
|
#[cfg(feature = "llm")]
|
||||||
|
pub llm_api_key: Option<String>,
|
||||||
|
#[cfg(feature = "llm")]
|
||||||
|
pub ai_enabled: Option<bool>,
|
||||||
|
|
||||||
|
#[serde(skip)]
|
||||||
|
is_global: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for EditorSettings {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
#[cfg(feature = "llm")]
|
||||||
|
llm_api_uri: Some("http://localhost:1234".to_string()),
|
||||||
|
#[cfg(feature = "llm")]
|
||||||
|
llm_api_key: Some("".to_string()),
|
||||||
|
#[cfg(feature = "llm")]
|
||||||
|
ai_enabled: Some(true),
|
||||||
|
dark_theme: Some(true),
|
||||||
|
|
||||||
|
// window state
|
||||||
|
is_global: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EditorSettings {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
#[cfg(feature = "llm")]
|
||||||
|
llm_api_uri: None,
|
||||||
|
#[cfg(feature = "llm")]
|
||||||
|
llm_api_key: None,
|
||||||
|
#[cfg(feature = "llm")]
|
||||||
|
ai_enabled: None,
|
||||||
|
dark_theme: None,
|
||||||
|
|
||||||
|
is_global: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load() -> Self {
|
||||||
|
if let Ok(mut settings) = FILESYSTEM.read::<Self>(Path::new("settings.json")) {
|
||||||
|
settings.is_global = false;
|
||||||
|
return settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn save(&self) {
|
||||||
|
let path = if self.is_global {
|
||||||
|
FILESYSTEM.config_path()
|
||||||
|
} else {
|
||||||
|
PathBuf::from("settings.json")
|
||||||
|
};
|
||||||
|
|
||||||
|
FILESYSTEM.write(path.as_path(), self.clone()).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load_global() -> Self {
|
||||||
|
let path = FILESYSTEM.config_path();
|
||||||
|
|
||||||
|
if !path.exists() {
|
||||||
|
FILESYSTEM.mkdir(path.parent().unwrap()).unwrap();
|
||||||
|
FILESYSTEM.write(path.as_path(), Self::default()).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut settings = FILESYSTEM.read::<Self>(path.as_path()).unwrap();
|
||||||
|
settings.is_global = true;
|
||||||
|
settings
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,12 +1,13 @@
|
|||||||
use core::fmt;
|
|
||||||
|
|
||||||
use chrono::NaiveDate;
|
use chrono::NaiveDate;
|
||||||
|
use core::fmt;
|
||||||
use egui::ScrollArea;
|
use egui::ScrollArea;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
PROJECT_FOLDER, RightPanelContent,
|
RightPanelContent,
|
||||||
editors::object_editor::ObjectInstance,
|
editors::object_editor::ObjectInstance,
|
||||||
|
filesystem::{FILESYSTEM, LegacyFileSystem},
|
||||||
util::{self, Error},
|
util::{self, Error},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -172,21 +173,14 @@ impl Default for Template {
|
|||||||
|
|
||||||
impl Template {
|
impl Template {
|
||||||
pub fn load(id: &str) -> Result<Self, Box<dyn std::error::Error>> {
|
pub fn load(id: &str) -> Result<Self, Box<dyn std::error::Error>> {
|
||||||
let path = PROJECT_FOLDER.join("templates").join(format!("{id}.json"));
|
let mut template = FILESYSTEM.read::<Self>(Path::new(&format!("templates/{id}.json")))?;
|
||||||
|
|
||||||
let content = std::fs::read_to_string(&path)?;
|
|
||||||
let mut template: Self = serde_json::from_str(&content)?;
|
|
||||||
template.saved = true;
|
template.saved = true;
|
||||||
Ok(template)
|
Ok(template)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn save(&mut self) -> Result<(), Box<dyn std::error::Error>> {
|
pub fn save(&mut self) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let path = PROJECT_FOLDER
|
let id = &self.id;
|
||||||
.join("templates")
|
FILESYSTEM.write(Path::new(&format!("templates/{id}.json")), self.clone())?;
|
||||||
.join(format!("{}.json", &self.id));
|
|
||||||
|
|
||||||
let content = serde_json::to_string_pretty(self)?;
|
|
||||||
std::fs::write(path, content)?;
|
|
||||||
self.saved = true;
|
self.saved = true;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -204,16 +198,6 @@ impl Template {
|
|||||||
|
|
||||||
ScrollArea::vertical().show(ui, |ui| {
|
ScrollArea::vertical().show(ui, |ui| {
|
||||||
ui.vertical(|ui| {
|
ui.vertical(|ui| {
|
||||||
// ui.group(|ui| {
|
|
||||||
// ui.horizontal(|ui| {
|
|
||||||
// if self.saved {
|
|
||||||
// ui.label(RichText::new("✓ Saved").color(egui::Color32::GREEN));
|
|
||||||
// } else {
|
|
||||||
// ui.label(RichText::new("* Unsaved").color(egui::Color32::YELLOW));
|
|
||||||
// }
|
|
||||||
// ui.label(format!("id: {}", self.id));
|
|
||||||
// });
|
|
||||||
// });
|
|
||||||
util::saved_status(ui, self.saved, &self.id, &self.name);
|
util::saved_status(ui, self.saved, &self.id, &self.name);
|
||||||
|
|
||||||
// Save/Cancel buttons
|
// Save/Cancel buttons
|
||||||
@@ -232,13 +216,10 @@ impl Template {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ui.button("Delete").clicked() {
|
if ui.button("Delete").clicked() {
|
||||||
std::fs::remove_file(
|
let id = &self.id;
|
||||||
PROJECT_FOLDER
|
FILESYSTEM
|
||||||
.join("templates")
|
.delete(Path::new(&format!("templates/{id}.json")))
|
||||||
.join(format!("{}.json", self.id)),
|
|
||||||
)
|
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
*new_instance = Some(RightPanelContent::None);
|
*new_instance = Some(RightPanelContent::None);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+17
-19
@@ -1,4 +1,7 @@
|
|||||||
use walkdir::{DirEntry, WalkDir};
|
use itertools::Itertools;
|
||||||
|
use std::fs::{self, DirEntry};
|
||||||
|
|
||||||
|
// use walkdir::{DirEntry, WalkDir};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
PROJECT_FOLDER, RightPanelContent,
|
PROJECT_FOLDER, RightPanelContent,
|
||||||
@@ -249,24 +252,22 @@ impl Explorer {
|
|||||||
});
|
});
|
||||||
})
|
})
|
||||||
.body(|ui| {
|
.body(|ui| {
|
||||||
let entries: Vec<_> = WalkDir::new(PROJECT_FOLDER.join("assets"))
|
let entries = fs::read_dir(PROJECT_FOLDER.join("assets"))
|
||||||
.min_depth(1)
|
.unwrap()
|
||||||
.max_depth(1) // Only immediate children
|
.filter_map(Result::ok)
|
||||||
.sort_by(|a, b| {
|
.sorted_by(|a, b| {
|
||||||
// Directories first, then files
|
// Directories first, then files
|
||||||
let a_is_dir = a.file_type().is_dir();
|
let a_is_dir = a.file_type().unwrap().is_dir();
|
||||||
let b_is_dir = b.file_type().is_dir();
|
let b_is_dir = b.file_type().unwrap().is_dir();
|
||||||
if a_is_dir == b_is_dir {
|
if a_is_dir == b_is_dir {
|
||||||
a.file_name().cmp(b.file_name())
|
a.file_name().cmp(&b.file_name())
|
||||||
} else if a_is_dir {
|
} else if a_is_dir {
|
||||||
std::cmp::Ordering::Less
|
std::cmp::Ordering::Less
|
||||||
} else {
|
} else {
|
||||||
std::cmp::Ordering::Greater
|
std::cmp::Ordering::Greater
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.into_iter()
|
.collect::<Vec<_>>();
|
||||||
.filter_map(Result::ok)
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
for entry in entries {
|
for entry in entries {
|
||||||
Self::render_entry(ui, to_load, &entry);
|
Self::render_entry(ui, to_load, &entry);
|
||||||
@@ -275,19 +276,16 @@ impl Explorer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn render_entry(ui: &mut egui::Ui, to_load: &mut Option<RightPanelContent>, entry: &DirEntry) {
|
fn render_entry(ui: &mut egui::Ui, to_load: &mut Option<RightPanelContent>, entry: &DirEntry) {
|
||||||
let file_type = entry.file_type();
|
let file_type = entry.file_type().unwrap();
|
||||||
let is_dir = file_type.is_dir();
|
let is_dir = file_type.is_dir();
|
||||||
let file_name = entry.file_name().to_string_lossy();
|
let file_name = entry.file_name().to_str().unwrap().to_string();
|
||||||
let path = entry.path();
|
let path = entry.path();
|
||||||
|
|
||||||
if is_dir {
|
if is_dir {
|
||||||
let entries: Vec<_> = WalkDir::new(path)
|
let entries = fs::read_dir(path)
|
||||||
.min_depth(1)
|
.unwrap()
|
||||||
.max_depth(1)
|
|
||||||
.sort_by(|a, b| a.file_name().cmp(b.file_name()))
|
|
||||||
.into_iter()
|
|
||||||
.filter_map(Result::ok)
|
.filter_map(Result::ok)
|
||||||
.collect();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
egui::collapsing_header::CollapsingState::load_with_default_open(
|
egui::collapsing_header::CollapsingState::load_with_default_open(
|
||||||
ui.ctx(),
|
ui.ctx(),
|
||||||
|
|||||||
@@ -0,0 +1,103 @@
|
|||||||
|
use std::{
|
||||||
|
collections::HashMap,
|
||||||
|
io,
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
sync::LazyLock,
|
||||||
|
};
|
||||||
|
|
||||||
|
use serde::{Serialize, de::DeserializeOwned};
|
||||||
|
|
||||||
|
#[cfg(feature = "native")]
|
||||||
|
use crate::PROJECT_FOLDER;
|
||||||
|
use crate::filesystem::native::NativeFileSystem;
|
||||||
|
|
||||||
|
#[cfg(feature = "native")]
|
||||||
|
pub mod native;
|
||||||
|
|
||||||
|
#[cfg(feature = "web")]
|
||||||
|
pub mod web;
|
||||||
|
|
||||||
|
pub static FILESYSTEM: LazyLock<NativeFileSystem> = LazyLock::new(|| {
|
||||||
|
#[cfg(feature = "native")]
|
||||||
|
return NativeFileSystem::new(PROJECT_FOLDER.clone());
|
||||||
|
#[cfg(feature = "web")]
|
||||||
|
return Box::new(web::WebFileSystem::new());
|
||||||
|
});
|
||||||
|
|
||||||
|
pub trait LegacyFileSystem {
|
||||||
|
fn read<T: DeserializeOwned>(&self, path: &Path) -> Result<T, FsError>;
|
||||||
|
fn read_bytes(&self, path: &Path) -> Result<Vec<u8>, FsError>;
|
||||||
|
fn write<T: Serialize>(&self, path: &Path, data: T) -> Result<(), FsError>;
|
||||||
|
fn delete(&self, path: &Path) -> Result<(), FsError>;
|
||||||
|
fn mkdir(&self, path: &Path) -> Result<(), FsError>;
|
||||||
|
fn rename(&self, path: &Path, new_path: &Path) -> Result<(), FsError>;
|
||||||
|
|
||||||
|
#[allow(unused)]
|
||||||
|
fn exists(&self, path: &Path) -> bool;
|
||||||
|
|
||||||
|
fn config_path(&self) -> PathBuf;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ────────────────────────────────────────────────────────────────
|
||||||
|
// Custom error type
|
||||||
|
// ────────────────────────────────────────────────────────────────
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum FsError {
|
||||||
|
Io(io::Error),
|
||||||
|
Serde(serde_json::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for FsError {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
FsError::Io(e) => write!(f, "IO error: {e}"),
|
||||||
|
FsError::Serde(e) => write!(f, "Serialization error: {e}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for FsError {
|
||||||
|
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||||
|
match self {
|
||||||
|
FsError::Io(e) => Some(e),
|
||||||
|
FsError::Serde(e) => Some(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert the two underlying error types into our own
|
||||||
|
impl From<io::Error> for FsError {
|
||||||
|
fn from(err: io::Error) -> Self {
|
||||||
|
FsError::Io(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl From<serde_json::Error> for FsError {
|
||||||
|
fn from(err: serde_json::Error) -> Self {
|
||||||
|
FsError::Serde(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub struct Id(String);
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub trait FileSystem {
|
||||||
|
fn load<T: DeserializeOwned>(&self, id: Id) -> Result<T, FsError>;
|
||||||
|
fn save<T: Serialize>(&self, id: Id, data: T) -> Result<(), FsError>;
|
||||||
|
fn mkdir(&self, path: Path) -> Result<(), FsError>;
|
||||||
|
fn exists(&self, path: Path) -> bool;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub struct Index {
|
||||||
|
file_cache: HashMap<Id, PathBuf>,
|
||||||
|
project_root: Directory,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub struct Directory {
|
||||||
|
name: String,
|
||||||
|
id: Id,
|
||||||
|
children: HashMap<Id, Directory>,
|
||||||
|
files: Vec<Id>,
|
||||||
|
}
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
// ────────────────────────────────────────────────────────────────
|
||||||
|
// Imports
|
||||||
|
// ────────────────────────────────────────────────────────────────
|
||||||
|
use std::fs;
|
||||||
|
use std::io::{ErrorKind, Read};
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
use serde::Serialize;
|
||||||
|
use serde::de::DeserializeOwned;
|
||||||
|
|
||||||
|
use crate::filesystem::{FsError, LegacyFileSystem};
|
||||||
|
|
||||||
|
// ────────────────────────────────────────────────────────────────
|
||||||
|
// Concrete implementation
|
||||||
|
// ────────────────────────────────────────────────────────────────
|
||||||
|
/// The concrete file‑system. All paths are interpreted relative to
|
||||||
|
/// `project_root`.
|
||||||
|
pub struct NativeFileSystem {
|
||||||
|
project_root: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NativeFileSystem {
|
||||||
|
/// Create a new instance.
|
||||||
|
pub fn new(root: impl Into<PathBuf>) -> Self {
|
||||||
|
Self {
|
||||||
|
project_root: root.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolve the user supplied *relative* path against the project root.
|
||||||
|
#[inline]
|
||||||
|
fn full_path(&self, path: &Path) -> PathBuf {
|
||||||
|
self.project_root.join(path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ────────────────────────────────────────────────────────────────
|
||||||
|
// Implementation of the trait
|
||||||
|
// ────────────────────────────────────────────────────────────────
|
||||||
|
impl LegacyFileSystem for NativeFileSystem {
|
||||||
|
fn read<T: DeserializeOwned>(&self, path: &Path) -> Result<T, FsError> {
|
||||||
|
let full_path = self.full_path(path);
|
||||||
|
let file = fs::File::open(full_path).map_err(FsError::Io)?;
|
||||||
|
serde_json::from_reader(file).map_err(FsError::Serde)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_bytes(&self, path: &Path) -> Result<Vec<u8>, FsError> {
|
||||||
|
let full_path = self.full_path(path);
|
||||||
|
let mut contents = Vec::new();
|
||||||
|
fs::File::open(full_path)?.read_to_end(&mut contents)?;
|
||||||
|
Ok(contents)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write<T: Serialize>(&self, path: &Path, data: T) -> Result<(), FsError> {
|
||||||
|
let full_path = self.full_path(path);
|
||||||
|
|
||||||
|
// Ensure the parent directory exists.
|
||||||
|
if let Some(parent) = full_path.parent() {
|
||||||
|
fs::create_dir_all(parent)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let file = fs::File::create(full_path)?;
|
||||||
|
serde_json::to_writer(file, &data).map_err(FsError::Serde)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn delete(&self, path: &Path) -> Result<(), FsError> {
|
||||||
|
let full_path = self.full_path(path);
|
||||||
|
match fs::remove_file(&full_path) {
|
||||||
|
Ok(()) => Ok(()),
|
||||||
|
Err(e) if e.kind() == ErrorKind::IsADirectory => {
|
||||||
|
// Remove a directory tree.
|
||||||
|
fs::remove_dir_all(full_path).map_err(FsError::Io)
|
||||||
|
}
|
||||||
|
Err(e) => Err(FsError::Io(e)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mkdir(&self, path: &Path) -> Result<(), FsError> {
|
||||||
|
let full_path = self.full_path(path);
|
||||||
|
fs::create_dir_all(full_path).map_err(FsError::Io)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn exists(&self, path: &Path) -> bool {
|
||||||
|
let full_path = self.full_path(path);
|
||||||
|
full_path.exists()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rename(&self, path: &Path, other: &Path) -> Result<(), FsError> {
|
||||||
|
let full_path = self.full_path(path);
|
||||||
|
let full_other = self.full_path(other);
|
||||||
|
fs::rename(full_path, full_other).map_err(FsError::Io)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn config_path(&self) -> PathBuf {
|
||||||
|
match std::env::var("HOME") {
|
||||||
|
Ok(path) => PathBuf::from(path + "/.config/worldcoder/settings.json"),
|
||||||
|
Err(_) => {
|
||||||
|
eprintln!(
|
||||||
|
"XDG_CONFIG_HOME not set, using default path of ~/.config/worldcoder/settings.json"
|
||||||
|
);
|
||||||
|
"~/.config/worldcoder/settings.json".into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,58 +1,480 @@
|
|||||||
|
use std::{
|
||||||
|
io::{BufRead, BufReader},
|
||||||
|
sync::{Arc, Mutex},
|
||||||
|
};
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::editors::settings_editor::ProjectSettings;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct ContentAI {
|
||||||
|
pub open: bool,
|
||||||
|
|
||||||
|
// model input
|
||||||
|
pub content: String,
|
||||||
|
pub instruction: String,
|
||||||
|
pub context_override: String,
|
||||||
|
pub system_prompt: String,
|
||||||
|
|
||||||
|
// model settings
|
||||||
|
pub max_tokens: usize,
|
||||||
|
pub temperature: f32,
|
||||||
|
pub reasoning_effort: ReasoningEffort,
|
||||||
|
pub model_override: String,
|
||||||
|
|
||||||
|
// model output
|
||||||
|
pub reasoning: Arc<Mutex<String>>,
|
||||||
|
pub result: Arc<Mutex<String>>,
|
||||||
|
pub ready: Arc<Mutex<ReadyState>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ContentAI {
|
||||||
|
pub fn new(content: String) -> Self {
|
||||||
|
Self {
|
||||||
|
// model input
|
||||||
|
content,
|
||||||
|
instruction: String::new(),
|
||||||
|
context_override: String::new(),
|
||||||
|
system_prompt: String::new(),
|
||||||
|
|
||||||
|
// model settings
|
||||||
|
max_tokens: 2048,
|
||||||
|
reasoning_effort: ReasoningEffort::default(),
|
||||||
|
temperature: 0.7,
|
||||||
|
model_override: String::new(),
|
||||||
|
reasoning: Arc::new(Mutex::new(String::new())),
|
||||||
|
|
||||||
|
// output
|
||||||
|
result: Arc::new(Mutex::new(String::new())),
|
||||||
|
ready: Arc::new(Mutex::new(ReadyState::Idle)),
|
||||||
|
|
||||||
|
// ui
|
||||||
|
open: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ui(&mut self, ui: &mut egui::Ui, project: &mut ProjectSettings) {
|
||||||
|
let is_open = self.open;
|
||||||
|
|
||||||
|
if is_open {
|
||||||
|
egui::SidePanel::right("ai_assistant").show_inside(ui, |ui| {
|
||||||
|
Self::ui_main(self, ui, project);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
self.open = is_open;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ui_output_box(&mut self, ui: &mut egui::Ui, project: &mut ProjectSettings) {
|
||||||
|
let mut ready_lock = self.ready.lock().unwrap();
|
||||||
|
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
if *ready_lock == ReadyState::Generating {
|
||||||
|
if ui.button("Cancel").clicked() {
|
||||||
|
*ready_lock = ReadyState::Halted;
|
||||||
|
}
|
||||||
|
if ui.button("Stop").clicked() {
|
||||||
|
*ready_lock = ReadyState::Idle;
|
||||||
|
}
|
||||||
|
ui.spinner();
|
||||||
|
ui.label("Generating...");
|
||||||
|
}
|
||||||
|
|
||||||
|
if *ready_lock == ReadyState::Idle {
|
||||||
|
let continue_content = || {
|
||||||
|
let content = self.content.clone();
|
||||||
|
let project = project.clone();
|
||||||
|
let result = self.result.clone();
|
||||||
|
let reasoning = self.reasoning.clone();
|
||||||
|
let ready = self.ready.clone();
|
||||||
|
|
||||||
|
let options = AIOptions {
|
||||||
|
max_completion_tokens: self.max_tokens,
|
||||||
|
reasoning_effort: self.reasoning_effort,
|
||||||
|
temperature: self.temperature,
|
||||||
|
model_override: if !self.model_override.is_empty() {
|
||||||
|
Some(self.model_override.clone())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let ai_input = AIInput {
|
||||||
|
system_prompt: self.system_prompt.clone(),
|
||||||
|
user_prompt: format!(
|
||||||
|
"{}\n\n{} {}",
|
||||||
|
self.instruction.clone(),
|
||||||
|
project.ai_context.clone(),
|
||||||
|
self.context_override.clone()
|
||||||
|
),
|
||||||
|
previous_content: content.clone(),
|
||||||
|
structure: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
result.lock().unwrap().clear();
|
||||||
|
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
let result = crate::llm_integration::content_llm::continue_content(
|
||||||
|
ai_input,
|
||||||
|
options,
|
||||||
|
project,
|
||||||
|
result,
|
||||||
|
reasoning,
|
||||||
|
ready.clone(),
|
||||||
|
);
|
||||||
|
if let Err(e) = result {
|
||||||
|
eprintln!("Error in content generation: {e}");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
if ui.button("Generate ").clicked() {
|
||||||
|
continue_content();
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.label("Idle");
|
||||||
|
}
|
||||||
|
|
||||||
|
// show regardless of state
|
||||||
|
if ui.button("Insert").clicked() {
|
||||||
|
*ready_lock = ReadyState::Ready;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ui.button("Clear").clicked() {
|
||||||
|
self.result.lock().unwrap().clear();
|
||||||
|
self.reasoning.lock().unwrap().clear();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ui.spacing();
|
||||||
|
|
||||||
|
ui.vertical(|ui| {
|
||||||
|
egui::TopBottomPanel::top("reasoning_output")
|
||||||
|
.resizable(true)
|
||||||
|
.show_inside(ui, |ui| {
|
||||||
|
egui::ScrollArea::both()
|
||||||
|
.auto_shrink([false, true])
|
||||||
|
.id_salt("reasoning_output")
|
||||||
|
.max_width(ui.available_width())
|
||||||
|
// .max_height(ui.available_height() / 3.0)
|
||||||
|
.show(ui, |ui| {
|
||||||
|
ui.add(
|
||||||
|
egui::TextEdit::multiline(&mut *self.reasoning.lock().unwrap())
|
||||||
|
.font(egui::TextStyle::Monospace)
|
||||||
|
.interactive(false)
|
||||||
|
.desired_rows(5)
|
||||||
|
.frame(false)
|
||||||
|
.desired_width(ui.available_width())
|
||||||
|
.lock_focus(true)
|
||||||
|
.hint_text("Reasoning will appear here..."),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
egui::ScrollArea::both()
|
||||||
|
.auto_shrink([false, false])
|
||||||
|
.id_salt("llm_output")
|
||||||
|
.max_width(ui.available_width())
|
||||||
|
.show(ui, |ui| {
|
||||||
|
ui.add(
|
||||||
|
egui::TextEdit::multiline(&mut *self.result.lock().unwrap())
|
||||||
|
.font(egui::TextStyle::Monospace)
|
||||||
|
.interactive(false)
|
||||||
|
.desired_rows(0)
|
||||||
|
.frame(false)
|
||||||
|
.desired_width(ui.available_width())
|
||||||
|
.lock_focus(true)
|
||||||
|
.hint_text("Content will appear here..."),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ui_main(&mut self, ui: &mut egui::Ui, project: &mut ProjectSettings) {
|
||||||
|
{
|
||||||
|
ui.weak("(The model will see current file content)");
|
||||||
|
|
||||||
|
egui::CollapsingHeader::new("Settings")
|
||||||
|
.default_open(true)
|
||||||
|
.show(ui, |ui| {
|
||||||
|
egui::Grid::new("continue_grid")
|
||||||
|
.num_columns(2)
|
||||||
|
.striped(true)
|
||||||
|
.show(ui, |ui| {
|
||||||
|
ui.label("Max Tokens");
|
||||||
|
ui.add(
|
||||||
|
egui::DragValue::new(&mut self.max_tokens)
|
||||||
|
.range(128..=u32::MAX)
|
||||||
|
.speed(128),
|
||||||
|
);
|
||||||
|
ui.end_row();
|
||||||
|
|
||||||
|
ui.label("Temperature");
|
||||||
|
ui.add(
|
||||||
|
egui::DragValue::new(&mut self.temperature)
|
||||||
|
.range(0.0..=2.0)
|
||||||
|
.speed(0.1),
|
||||||
|
);
|
||||||
|
|
||||||
|
ui.label("Reasoning effort");
|
||||||
|
|
||||||
|
egui::ComboBox::from_id_salt("reasoning_effort")
|
||||||
|
.selected_text(self.reasoning_effort.to_string())
|
||||||
|
.show_ui(ui, |ui| {
|
||||||
|
ui.selectable_value(
|
||||||
|
&mut self.reasoning_effort,
|
||||||
|
ReasoningEffort::Minimal,
|
||||||
|
"Minimal",
|
||||||
|
);
|
||||||
|
ui.selectable_value(
|
||||||
|
&mut self.reasoning_effort,
|
||||||
|
ReasoningEffort::Low,
|
||||||
|
"Low",
|
||||||
|
);
|
||||||
|
ui.selectable_value(
|
||||||
|
&mut self.reasoning_effort,
|
||||||
|
ReasoningEffort::Medium,
|
||||||
|
"Medium",
|
||||||
|
);
|
||||||
|
ui.selectable_value(
|
||||||
|
&mut self.reasoning_effort,
|
||||||
|
ReasoningEffort::High,
|
||||||
|
"High",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
ui.end_row();
|
||||||
|
|
||||||
|
ui.label("Model override");
|
||||||
|
ui.add(egui::TextEdit::singleline(&mut self.model_override));
|
||||||
|
ui.end_row();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
egui::TopBottomPanel::top("continue_instruction")
|
||||||
|
.resizable(true)
|
||||||
|
.show_inside(ui, |ui| {
|
||||||
|
egui::CollapsingHeader::new("Instructions")
|
||||||
|
.default_open(true)
|
||||||
|
.show(ui, |ui| {
|
||||||
|
egui::ScrollArea::vertical()
|
||||||
|
.auto_shrink([false, false])
|
||||||
|
.max_height(ui.available_height())
|
||||||
|
.show(ui, |ui| {
|
||||||
|
ui.add(
|
||||||
|
egui::TextEdit::multiline(&mut self.instruction)
|
||||||
|
.frame(false)
|
||||||
|
.desired_width(ui.available_width())
|
||||||
|
.hint_text("Writing Instructions"),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
egui::TopBottomPanel::top("continue_context")
|
||||||
|
.resizable(true)
|
||||||
|
.show_inside(ui, |ui| {
|
||||||
|
egui::CollapsingHeader::new("Context")
|
||||||
|
.default_open(true)
|
||||||
|
.show(ui, |ui| {
|
||||||
|
egui::ScrollArea::vertical()
|
||||||
|
.auto_shrink([false, false])
|
||||||
|
.max_height(ui.available_height())
|
||||||
|
.show(ui, |ui| {
|
||||||
|
ui.add(
|
||||||
|
egui::TextEdit::multiline(&mut self.context_override)
|
||||||
|
.frame(false)
|
||||||
|
.desired_width(ui.available_width())
|
||||||
|
.hint_text("Any additional context?"),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
egui::TopBottomPanel::top("continue_system_prompt")
|
||||||
|
.resizable(true)
|
||||||
|
.show_inside(ui, |ui| {
|
||||||
|
egui::CollapsingHeader::new("System prompt")
|
||||||
|
.default_open(true)
|
||||||
|
.show(ui, |ui| {
|
||||||
|
egui::ScrollArea::vertical()
|
||||||
|
.auto_shrink([false, false])
|
||||||
|
.max_height(ui.available_height())
|
||||||
|
.show(ui, |ui| {
|
||||||
|
ui.add(
|
||||||
|
egui::TextEdit::multiline(&mut self.system_prompt)
|
||||||
|
.frame(false)
|
||||||
|
.desired_width(ui.available_width())
|
||||||
|
.hint_text("System prompt"),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
self.ui_output_box(ui, project);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub fn continue_content(
|
pub fn continue_content(
|
||||||
context: &str,
|
ai_input: AIInput,
|
||||||
instruction: &str,
|
// context: String,
|
||||||
_max_tokens: usize,
|
// previous_content: String,
|
||||||
) -> Result<String, Box<dyn std::error::Error>> {
|
// instruction: String,
|
||||||
|
options: AIOptions,
|
||||||
|
project: ProjectSettings,
|
||||||
|
result: Arc<Mutex<String>>,
|
||||||
|
reasoning: Arc<Mutex<String>>,
|
||||||
|
ready: Arc<Mutex<ReadyState>>,
|
||||||
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
*ready.lock().unwrap() = ReadyState::Generating;
|
||||||
|
|
||||||
let client = reqwest::blocking::Client::new();
|
let client = reqwest::blocking::Client::new();
|
||||||
|
|
||||||
let messages = vec![
|
let messages = vec![
|
||||||
Message {
|
Message {
|
||||||
role: "system".to_string(),
|
role: "system".to_string(),
|
||||||
content: "
|
content: ai_input.system_prompt,
|
||||||
Please generate content that is a direct continuation of the given text.
|
|
||||||
Your response should be a logical next step in the content and should not repeat any of the text from the instruction or the content.
|
|
||||||
Do not generate any text that is not a direct continuation of the content.
|
|
||||||
if extra instructions are provided, follow them exactly, otherwise continue the text in a logical way.
|
|
||||||
".to_string(),
|
|
||||||
},
|
},
|
||||||
Message {
|
Message {
|
||||||
role: "user".to_string(),
|
role: "user".to_string(),
|
||||||
content: context.to_string(),
|
content: format!(
|
||||||
},
|
"<Instructions> {}\n\n<Previous content> {}\n\n",
|
||||||
Message {
|
ai_input.user_prompt, ai_input.previous_content
|
||||||
role: "user".to_string(),
|
),
|
||||||
content: format!("Instructions: {instruction}"),
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
let request = ChatRequest {
|
let request = ChatRequest {
|
||||||
messages,
|
messages,
|
||||||
temperature: 0.7,
|
temperature: options.temperature,
|
||||||
|
max_tokens: options.max_completion_tokens,
|
||||||
|
model: options.model_override,
|
||||||
|
reasoning_effort: options.reasoning_effort,
|
||||||
|
stream: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
let response = client
|
let llm_api_uri = if let Some(uri) = project.local_overrides.llm_api_uri {
|
||||||
.post("http://localhost:1234/v1/chat/completions")
|
uri
|
||||||
.json(&request)
|
|
||||||
.send()?;
|
|
||||||
|
|
||||||
if !response.status().is_success() {
|
|
||||||
return Err(format!("Request failed: {}", response.text()?).into());
|
|
||||||
}
|
|
||||||
|
|
||||||
let response: ChatResponse = response.json()?;
|
|
||||||
|
|
||||||
if let Some(choice) = response.choices.into_iter().next() {
|
|
||||||
Ok(choice.message.content)
|
|
||||||
} else {
|
} else {
|
||||||
Err("No response from model".into())
|
project.global_settings.llm_api_uri.unwrap()
|
||||||
|
};
|
||||||
|
|
||||||
|
let api_key = if let Some(key) = project.local_overrides.llm_api_key {
|
||||||
|
if key.is_empty() { None } else { Some(key) }
|
||||||
|
} else if let Some(key) = project.global_settings.llm_api_key {
|
||||||
|
if key.is_empty() { None } else { Some(key) }
|
||||||
|
} else {
|
||||||
|
return Err("No API key found".into());
|
||||||
|
};
|
||||||
|
|
||||||
|
let response = if let Some(k) = api_key {
|
||||||
|
client
|
||||||
|
.post(llm_api_uri + "/api/v0/chat/completions")
|
||||||
|
.json(&request)
|
||||||
|
.bearer_auth(k)
|
||||||
|
.send()?
|
||||||
|
} else {
|
||||||
|
client
|
||||||
|
.post(llm_api_uri + "/api/v0/chat/completions")
|
||||||
|
.json(&request)
|
||||||
|
.send()?
|
||||||
|
};
|
||||||
|
|
||||||
|
println!("success!");
|
||||||
|
|
||||||
|
// println!("response: {}", response.text().unwrap());
|
||||||
|
let reader = BufReader::new(response);
|
||||||
|
for line in reader.lines() {
|
||||||
|
// initial loop to check if the user has terminated the generation
|
||||||
|
{
|
||||||
|
let mut ready = ready.lock().unwrap();
|
||||||
|
|
||||||
|
if *ready == ReadyState::Halted {
|
||||||
|
result.lock().unwrap().clear();
|
||||||
|
reasoning.lock().unwrap().clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if *ready != ReadyState::Generating {
|
||||||
|
*ready = ReadyState::Idle;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let line = line?;
|
||||||
|
if line == "data: [DONE]" {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(json) = line.strip_prefix("data: ") {
|
||||||
|
if let Ok(chunk) = serde_json::from_str::<StreamingChatResponse>(json) {
|
||||||
|
println!("chunk: {chunk:?}");
|
||||||
|
|
||||||
|
if let Some(content) = chunk.choices[0].delta.content.as_ref() {
|
||||||
|
println!("content: {content}");
|
||||||
|
result.lock().unwrap().push_str(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(reasoning_content) = chunk.choices[0].delta.reasoning_content.as_ref() {
|
||||||
|
println!("reasoning_content: {reasoning_content}");
|
||||||
|
reasoning.lock().unwrap().push_str(reasoning_content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
*ready.lock().unwrap() = ReadyState::Idle;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn ai_enabled() -> bool {
|
pub struct AIOptions {
|
||||||
let client = reqwest::blocking::Client::new();
|
pub max_completion_tokens: usize,
|
||||||
client.get("http://localhost:1234/v1/models").send().is_ok()
|
pub temperature: f32,
|
||||||
|
pub reasoning_effort: ReasoningEffort,
|
||||||
|
pub model_override: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct AIInput {
|
||||||
|
pub system_prompt: String,
|
||||||
|
pub user_prompt: String,
|
||||||
|
pub previous_content: String,
|
||||||
|
#[allow(unused)]
|
||||||
|
pub structure: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Clone, Copy)]
|
||||||
|
pub enum ReadyState {
|
||||||
|
Idle,
|
||||||
|
Generating,
|
||||||
|
Ready,
|
||||||
|
Halted,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Copy, Clone, PartialEq, Default)]
|
||||||
|
pub enum ReasoningEffort {
|
||||||
|
#[serde(rename = "minimal")]
|
||||||
|
Minimal,
|
||||||
|
|
||||||
|
#[default]
|
||||||
|
#[serde(rename = "low")]
|
||||||
|
Low,
|
||||||
|
#[serde(rename = "medium")]
|
||||||
|
Medium,
|
||||||
|
#[serde(rename = "high")]
|
||||||
|
High,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for ReasoningEffort {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
ReasoningEffort::Minimal => write!(f, "Minimal"),
|
||||||
|
ReasoningEffort::Low => write!(f, "Low"),
|
||||||
|
ReasoningEffort::Medium => write!(f, "Medium"),
|
||||||
|
ReasoningEffort::High => write!(f, "High"),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Simple request structure
|
// Simple request structure
|
||||||
@@ -60,6 +482,39 @@ pub fn ai_enabled() -> bool {
|
|||||||
struct ChatRequest {
|
struct ChatRequest {
|
||||||
messages: Vec<Message>,
|
messages: Vec<Message>,
|
||||||
temperature: f32,
|
temperature: f32,
|
||||||
|
max_tokens: usize,
|
||||||
|
stream: bool,
|
||||||
|
reasoning_effort: ReasoningEffort,
|
||||||
|
|
||||||
|
// if we give the API model:null it returns 500
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
model: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Streaming response structures
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
struct StreamingChatResponse {
|
||||||
|
choices: Vec<StreamingChoice>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
struct StreamingChoice {
|
||||||
|
delta: Delta,
|
||||||
|
#[serde(default)]
|
||||||
|
#[allow(unused)]
|
||||||
|
finish_reason: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
struct Delta {
|
||||||
|
#[serde(default)]
|
||||||
|
#[allow(unused)]
|
||||||
|
role: Option<String>,
|
||||||
|
|
||||||
|
#[serde(default)]
|
||||||
|
content: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
reasoning_content: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
@@ -70,10 +525,12 @@ struct Message {
|
|||||||
|
|
||||||
#[derive(Deserialize, Debug)]
|
#[derive(Deserialize, Debug)]
|
||||||
struct ChatResponse {
|
struct ChatResponse {
|
||||||
|
#[allow(unused)]
|
||||||
choices: Vec<Choice>,
|
choices: Vec<Choice>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Debug)]
|
#[derive(Deserialize, Debug)]
|
||||||
struct Choice {
|
struct Choice {
|
||||||
|
#[allow(unused)]
|
||||||
message: Message,
|
message: Message,
|
||||||
}
|
}
|
||||||
|
|||||||
+35
-18
@@ -1,18 +1,23 @@
|
|||||||
|
#![windows_subsystem = "windows"]
|
||||||
|
|
||||||
use std::{path::PathBuf, sync::LazyLock};
|
use std::{path::PathBuf, sync::LazyLock};
|
||||||
|
|
||||||
use egui::ScrollArea;
|
use egui::ScrollArea;
|
||||||
|
|
||||||
mod editors;
|
mod editors;
|
||||||
mod explorer;
|
mod explorer;
|
||||||
|
|
||||||
|
#[cfg(feature = "llm")]
|
||||||
mod llm_integration;
|
mod llm_integration;
|
||||||
mod scene;
|
|
||||||
|
|
||||||
mod util;
|
mod util;
|
||||||
|
|
||||||
|
mod filesystem;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
editors::{
|
editors::{
|
||||||
asset_editor::Asset, content_editor, context_editor::ProjectContext, note_editor,
|
asset_editor::Asset, content_editor, note_editor, object_editor::ObjectInstance,
|
||||||
object_editor::ObjectInstance, tags::Tag, template_editor::Template,
|
settings_editor::ProjectSettings, tags::Tag, template_editor::Template,
|
||||||
},
|
},
|
||||||
explorer::Explorer,
|
explorer::Explorer,
|
||||||
};
|
};
|
||||||
@@ -31,16 +36,18 @@ fn main() {
|
|||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
let _ = eframe::run_native("Code Editor", options, Box::new(|_cc| Ok(Box::new(app))));
|
if let Err(e) = eframe::run_native("World Coder", options, Box::new(|_cc| Ok(Box::new(app)))) {
|
||||||
|
eprintln!("Failed to run app: {e}");
|
||||||
|
}
|
||||||
|
|
||||||
|
eprintln!("App closed");
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Interface {
|
pub struct Interface {
|
||||||
// dialog: Option<egui_file::FileDialog>,
|
|
||||||
right_panel_content: RightPanelContent,
|
right_panel_content: RightPanelContent,
|
||||||
editor: content_editor::MainEditor,
|
editor: content_editor::MainEditor,
|
||||||
scene: scene::EditorScene,
|
|
||||||
explorer: Explorer,
|
explorer: Explorer,
|
||||||
project: ProjectContext,
|
project: ProjectSettings,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl eframe::App for Interface {
|
impl eframe::App for Interface {
|
||||||
@@ -84,13 +91,12 @@ impl Interface {
|
|||||||
Self {
|
Self {
|
||||||
right_panel_content: RightPanelContent::None,
|
right_panel_content: RightPanelContent::None,
|
||||||
editor: content_editor::MainEditor::new(),
|
editor: content_editor::MainEditor::new(),
|
||||||
scene: scene::EditorScene::new(),
|
|
||||||
explorer: Explorer::new(),
|
explorer: Explorer::new(),
|
||||||
project: ProjectContext::load(),
|
project: ProjectSettings::load(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_top_panel(&self, ctx: &egui::Context) {
|
fn render_top_panel(&mut self, ctx: &egui::Context) {
|
||||||
// Top bar with actions
|
// Top bar with actions
|
||||||
egui::TopBottomPanel::top("top").show(ctx, |ui| {
|
egui::TopBottomPanel::top("top").show(ctx, |ui| {
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
@@ -99,9 +105,24 @@ impl Interface {
|
|||||||
ui.separator();
|
ui.separator();
|
||||||
|
|
||||||
// version
|
// version
|
||||||
ui.label(VERSION)
|
ui.label(VERSION);
|
||||||
|
|
||||||
|
// Settings
|
||||||
|
if ui.button("Settings").clicked() {
|
||||||
|
self.project.open();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if self.project.open {
|
||||||
|
let mut open = self.project.open;
|
||||||
|
egui::Window::new("Settings")
|
||||||
|
.open(&mut open)
|
||||||
|
.show(ctx, |ui| {
|
||||||
|
self.project.ui(ui);
|
||||||
|
});
|
||||||
|
self.project.open = open;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_left_panel(&mut self, ctx: &egui::Context) {
|
fn render_left_panel(&mut self, ctx: &egui::Context) {
|
||||||
@@ -110,8 +131,7 @@ impl Interface {
|
|||||||
.resizable(true)
|
.resizable(true)
|
||||||
.default_width(250.0)
|
.default_width(250.0)
|
||||||
.show(ctx, |ui| {
|
.show(ctx, |ui| {
|
||||||
ui.heading("Project Files");
|
ui.heading("Explorer");
|
||||||
ui.separator();
|
|
||||||
|
|
||||||
let mut to_load: Option<RightPanelContent> = None;
|
let mut to_load: Option<RightPanelContent> = None;
|
||||||
let mut load_doc: Option<content_editor::MainEditor> = None;
|
let mut load_doc: Option<content_editor::MainEditor> = None;
|
||||||
@@ -126,8 +146,6 @@ impl Interface {
|
|||||||
|
|
||||||
if let Some(load_doc) = load_doc {
|
if let Some(load_doc) = load_doc {
|
||||||
self.editor = load_doc;
|
self.editor = load_doc;
|
||||||
self.editor.show_editor = true;
|
|
||||||
self.editor.show_preview = true;
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -188,8 +206,7 @@ impl Interface {
|
|||||||
|
|
||||||
// render main content area
|
// render main content area
|
||||||
fn render_main_content(&mut self, ctx: &egui::Context) {
|
fn render_main_content(&mut self, ctx: &egui::Context) {
|
||||||
self.editor.ui(ctx);
|
self.editor.ui(ctx, &mut self.project);
|
||||||
self.scene.ui(ctx, &mut self.explorer.objects());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// configure appearance of UI elements
|
// configure appearance of UI elements
|
||||||
@@ -214,7 +231,7 @@ impl Interface {
|
|||||||
fonts.font_data.insert(
|
fonts.font_data.insert(
|
||||||
"JetBrains Mono Nerd Font".to_string(),
|
"JetBrains Mono Nerd Font".to_string(),
|
||||||
std::sync::Arc::new(egui::FontData::from_static(include_bytes!(
|
std::sync::Arc::new(egui::FontData::from_static(include_bytes!(
|
||||||
"/usr/local/share/fonts/j/JetBrainsMonoNerdFont_Regular.ttf",
|
"../font/JetBrainsMonoNerdFontMono_Regular.ttf",
|
||||||
))),
|
))),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
-147
@@ -1,147 +0,0 @@
|
|||||||
use egui::{RichText, vec2};
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
PROJECT_FOLDER,
|
|
||||||
editors::{
|
|
||||||
object_editor::ObjectInstance,
|
|
||||||
template_editor::{FieldType, FieldValue, Template},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
pub struct EditorScene {
|
|
||||||
rect: egui::Rect,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl EditorScene {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
rect: egui::Rect::ZERO,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn ui(&mut self, ctx: &egui::Context, objects: &mut [ObjectInstance]) {
|
|
||||||
egui::CentralPanel::default()
|
|
||||||
.frame(egui::Frame::NONE)
|
|
||||||
.show(ctx, |ui| {
|
|
||||||
egui::Scene::default()
|
|
||||||
.zoom_range(0.1..=10.0)
|
|
||||||
.show(ui, &mut self.rect, |ui| {
|
|
||||||
ui.horizontal_wrapped(|ui| {
|
|
||||||
ui.set_max_width(5000.0);
|
|
||||||
// Group objects by their template_id
|
|
||||||
use std::collections::HashMap;
|
|
||||||
let mut objects_by_template: HashMap<String, Vec<&ObjectInstance>> =
|
|
||||||
HashMap::new();
|
|
||||||
|
|
||||||
for obj in objects {
|
|
||||||
objects_by_template
|
|
||||||
.entry(obj.template_id.clone())
|
|
||||||
.or_default()
|
|
||||||
.push(obj);
|
|
||||||
}
|
|
||||||
|
|
||||||
// For each template with objects, create cards
|
|
||||||
for (template_id, template_objects) in objects_by_template {
|
|
||||||
// Try to load the template to get field definitions
|
|
||||||
if let Ok(mut template) = Template::load(&template_id) {
|
|
||||||
for obj in template_objects {
|
|
||||||
// Create a card for each object
|
|
||||||
egui::Frame::group(ui.style())
|
|
||||||
.fill(egui::Color32::from_rgba_premultiplied(
|
|
||||||
30, 30, 30, 200,
|
|
||||||
))
|
|
||||||
.corner_radius(4.0)
|
|
||||||
.show(ui, |ui| {
|
|
||||||
|
|
||||||
ui.vertical(|ui| {
|
|
||||||
ui.set_max_width(512.0);
|
|
||||||
ui.set_min_width(512.0);
|
|
||||||
|
|
||||||
// Object name as header
|
|
||||||
ui.heading(RichText::new(&obj.name).strong());
|
|
||||||
|
|
||||||
// Show fields with on_preview = true
|
|
||||||
template.fields.sort_by_key(|field| field.field_type != FieldType::Image);
|
|
||||||
for field_def in &template.fields {
|
|
||||||
if field_def.on_preview {
|
|
||||||
if let Some(field_value) =
|
|
||||||
obj.fields.get(&field_def.name)
|
|
||||||
{
|
|
||||||
ui.separator();
|
|
||||||
|
|
||||||
match field_value {
|
|
||||||
FieldValue::SingleLine(
|
|
||||||
text,
|
|
||||||
) => {
|
|
||||||
ui.strong(&field_def.name);
|
|
||||||
ui.label(text);
|
|
||||||
}
|
|
||||||
FieldValue::MultiLine(
|
|
||||||
text,
|
|
||||||
) => {
|
|
||||||
ui.strong(&field_def.name);
|
|
||||||
ui.label(text);
|
|
||||||
}
|
|
||||||
FieldValue::Number(n) => {
|
|
||||||
ui.strong(&field_def.name);
|
|
||||||
ui.label(n.to_string());
|
|
||||||
}
|
|
||||||
FieldValue::Date(date) => {
|
|
||||||
ui.strong(&field_def.name);
|
|
||||||
ui.label(
|
|
||||||
date.format(
|
|
||||||
"%Y-%m-%d",
|
|
||||||
)
|
|
||||||
.to_string(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
FieldValue::Image(value) => {
|
|
||||||
if !value.is_empty() {
|
|
||||||
let path = PROJECT_FOLDER.join("assets").join(value);
|
|
||||||
|
|
||||||
if let Ok(bytes) = std::fs::read(&path) {
|
|
||||||
let image_source = egui::ImageSource::Bytes {
|
|
||||||
uri: std::borrow::Cow::Owned(path.to_str().unwrap().to_string()),
|
|
||||||
bytes: bytes.into(),
|
|
||||||
};
|
|
||||||
ui.add(
|
|
||||||
egui::Image::new(image_source).fit_to_exact_size(vec2(512.0, 512.0)),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
FieldValue::Link(
|
|
||||||
target_id,
|
|
||||||
) => {
|
|
||||||
ui.strong(&field_def.name);
|
|
||||||
ui.label(format!(
|
|
||||||
"→ {target_id}"
|
|
||||||
));
|
|
||||||
}
|
|
||||||
FieldValue::Links(
|
|
||||||
links,
|
|
||||||
) => {
|
|
||||||
ui.strong(&field_def.name);
|
|
||||||
ui.label(format!(
|
|
||||||
"{} links",
|
|
||||||
links.len()
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add some spacing between cards
|
|
||||||
ui.add_space(8.0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user