From 747e288f4881075e517338ff78fe50ebaa499267 Mon Sep 17 00:00:00 2001 From: blackpanther26 Date: Sat, 9 Aug 2025 22:14:49 +0530 Subject: [PATCH 01/25] add dashboard to tui --- Cargo.lock | 790 ++++++++++++++++++++++++++++++++- Cargo.toml | 5 +- crates/tui/Cargo.toml | 16 + crates/tui/src/app.rs | 34 ++ crates/tui/src/main.rs | 64 +++ crates/tui/src/ui/dashboard.rs | 138 ++++++ crates/tui/src/ui/mod.rs | 1 + 7 files changed, 1035 insertions(+), 13 deletions(-) create mode 100644 crates/tui/Cargo.toml create mode 100644 crates/tui/src/app.rs create mode 100644 crates/tui/src/main.rs create mode 100644 crates/tui/src/ui/dashboard.rs create mode 100644 crates/tui/src/ui/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 6326b4e..11a1038 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,21 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + [[package]] name = "aho-corasick" version = "1.1.3" @@ -11,6 +26,18 @@ dependencies = [ "memchr", ] +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "anyhow" +version = "1.0.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" + [[package]] name = "api" version = "0.1.0" @@ -20,6 +47,27 @@ dependencies = [ "storage", ] +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "backtrace" +version = "0.3.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets 0.52.6", +] + [[package]] name = "bincode" version = "1.3.3" @@ -59,7 +107,7 @@ dependencies = [ "bitflags 2.9.1", "cexpr", "clang-sys", - "itertools", + "itertools 0.13.0", "proc-macro2", "quote", "regex", @@ -80,6 +128,12 @@ version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" +[[package]] +name = "bytes" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" + [[package]] name = "bzip2-sys" version = "0.1.13+1.0.8" @@ -90,6 +144,21 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "cassowary" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" + +[[package]] +name = "castaway" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a" +dependencies = [ + "rustversion", +] + [[package]] name = "cc" version = "1.2.27" @@ -127,6 +196,46 @@ dependencies = [ "libloading", ] +[[package]] +name = "color-eyre" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5920befb47832a6d61ee3a3a846565cfa39b331331e68a3b1d1116630f2f26d" +dependencies = [ + "backtrace", + "color-spantrace", + "eyre", + "indenter", + "once_cell", + "owo-colors", + "tracing-error", +] + +[[package]] +name = "color-spantrace" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8b88ea9df13354b55bc7234ebcce36e6ef896aca2e42a15de9e10edce01b427" +dependencies = [ + "once_cell", + "owo-colors", + "tracing-core", + "tracing-error", +] + +[[package]] +name = "compact_str" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f86b9c4c00838774a6d902ef931eff7470720c51d90c2e32cfe15dc304737b3f" +dependencies = [ + "castaway", + "cfg-if", + "itoa", + "ryu", + "static_assertions", +] + [[package]] name = "core" version = "0.1.0" @@ -135,12 +244,59 @@ dependencies = [ "serde", ] +[[package]] +name = "crossterm" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df" +dependencies = [ + "bitflags 2.9.1", + "crossterm_winapi", + "libc", + "mio 0.8.11", + "parking_lot", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + [[package]] name = "either" version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "eyre" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec" +dependencies = [ + "indenter", + "once_cell", +] + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + [[package]] name = "getrandom" version = "0.3.3" @@ -150,15 +306,44 @@ dependencies = [ "cfg-if", "libc", "r-efi", - "wasi", + "wasi 0.14.2+wasi-0.2.4", ] +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + [[package]] name = "glob" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "indenter" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "964de6e86d545b246d84badc0fef527924ace5134f30641c203ef52ba83f58d5" + [[package]] name = "index" version = "0.1.0" @@ -166,6 +351,26 @@ dependencies = [ "core", ] +[[package]] +name = "io-uring" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d93587f37623a1a17d94ef2bc9ada592f5465fe7732084ab7beefabe5c77c0c4" +dependencies = [ + "bitflags 2.9.1", + "cfg-if", + "libc", +] + +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + [[package]] name = "itertools" version = "0.13.0" @@ -175,6 +380,12 @@ dependencies = [ "either", ] +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + [[package]] name = "jobserver" version = "0.1.33" @@ -210,7 +421,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" dependencies = [ "cfg-if", - "windows-targets", + "windows-targets 0.53.2", ] [[package]] @@ -240,6 +451,31 @@ dependencies = [ "vcpkg", ] +[[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 = "log" +version = "0.4.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" + +[[package]] +name = "lru" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" +dependencies = [ + "hashbrown", +] + [[package]] name = "lz4-sys" version = "1.11.1+lz4-1.10.0" @@ -262,6 +498,38 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +dependencies = [ + "libc", + "log", + "wasi 0.11.1+wasi-snapshot-preview1", + "windows-sys 0.48.0", +] + +[[package]] +name = "mio" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" +dependencies = [ + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", + "windows-sys 0.59.0", +] + [[package]] name = "nom" version = "7.1.3" @@ -272,12 +540,68 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "object" +version = "0.36.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "owo-colors" +version = "4.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48dd4f4a2c8405440fd0462561f0e5806bd0f77e86f51c761481bdd4018b545e" + +[[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 0.52.6", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + [[package]] name = "peeking_take_while" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + [[package]] name = "pkg-config" version = "0.3.32" @@ -318,6 +642,35 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "ratatui" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f44c9e68fd46eda15c646fbb85e1040b657a58cdc8c98db1d97a55930d991eef" +dependencies = [ + "bitflags 2.9.1", + "cassowary", + "compact_str", + "crossterm", + "itertools 0.12.1", + "lru", + "paste", + "stability", + "strum", + "unicode-segmentation", + "unicode-truncate", + "unicode-width", +] + +[[package]] +name = "redox_syscall" +version = "0.5.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" +dependencies = [ + "bitflags 2.9.1", +] + [[package]] name = "regex" version = "1.11.1" @@ -357,6 +710,12 @@ dependencies = [ "librocksdb-sys", ] +[[package]] +name = "rustc-demangle" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" + [[package]] name = "rustc-hash" version = "1.1.0" @@ -369,6 +728,24 @@ version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[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" @@ -398,12 +775,89 @@ dependencies = [ "index", ] +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + [[package]] name = "shlex" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "signal-hook" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" +dependencies = [ + "libc", + "mio 0.8.11", + "signal-hook", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" +dependencies = [ + "libc", +] + +[[package]] +name = "slab" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "stability" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d904e7009df136af5297832a3ace3370cd14ff1546a232f4f185036c2736fcac" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "storage" version = "0.1.0" @@ -414,6 +868,28 @@ dependencies = [ "rocksdb", ] +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn", +] + [[package]] name = "syn" version = "2.0.103" @@ -425,18 +901,149 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "tokio" +version = "1.47.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" +dependencies = [ + "backtrace", + "bytes", + "io-uring", + "libc", + "mio 1.0.4", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "slab", + "socket2", + "tokio-macros", + "windows-sys 0.59.0", +] + +[[package]] +name = "tokio-macros" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-error" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b1581020d7a273442f5b45074a6a57d5757ad0a47dac0e9f0bd57b81936f3db" +dependencies = [ + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" +dependencies = [ + "sharded-slab", + "thread_local", + "tracing-core", +] + +[[package]] +name = "tui" +version = "0.1.0" +dependencies = [ + "anyhow", + "color-eyre", + "core", + "crossterm", + "index", + "ratatui", + "serde", + "storage", + "tokio", +] + [[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 = "unicode-truncate" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf" +dependencies = [ + "itertools 0.13.0", + "unicode-segmentation", + "unicode-width", +] + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + [[package]] name = "vcpkg" version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + [[package]] name = "wasi" version = "0.14.2+wasi-0.2.4" @@ -446,64 +1053,225 @@ dependencies = [ "wit-bindgen-rt", ] +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + [[package]] name = "windows-targets" version = "0.53.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c66f69fcc9ce11da9966ddb31a40968cad001c5bedeb5c2b82ede4253ab48aef" 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", + "windows_aarch64_gnullvm 0.53.0", + "windows_aarch64_msvc 0.53.0", + "windows_i686_gnu 0.53.0", + "windows_i686_gnullvm 0.53.0", + "windows_i686_msvc 0.53.0", + "windows_x86_64_gnu 0.53.0", + "windows_x86_64_gnullvm 0.53.0", + "windows_x86_64_msvc 0.53.0", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[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_gnullvm" version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + [[package]] name = "windows_aarch64_msvc" version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[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_gnu" version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" +[[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_gnullvm" version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + [[package]] name = "windows_i686_msvc" version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[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_gnu" version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[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_gnullvm" version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + [[package]] name = "windows_x86_64_msvc" version = "0.53.0" diff --git a/Cargo.toml b/Cargo.toml index 9e0a204..c2276b3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,15 +5,16 @@ members = [ "crates/storage", "crates/index", "crates/server", + "crates/tui", ] # You can define shared dependencies for all crates here [workspace.dependencies] -# tokio = { version = "1.37.0", features = ["full"] } +tokio = { version = "1.37.0", features = ["full"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" # thiserror = "1.0" -# anyhow = "1.0" +anyhow = "1.0" # tracing = "0.1.41" # Add any other dependencies that multiple crates will use # For example, if you introduce Axum/Tonic later, they might go here. diff --git a/crates/tui/Cargo.toml b/crates/tui/Cargo.toml new file mode 100644 index 0000000..2e8d3b9 --- /dev/null +++ b/crates/tui/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "tui" +version = "0.1.0" +edition = "2021" + +[dependencies] +color-eyre = "0.6.5" +ratatui = "0.26" +crossterm = "0.27" +tokio.workspace = true +serde.workspace = true +anyhow.workspace = true + +core = { path = "../core" } +storage = { path = "../storage" } +index = { path = "../index" } diff --git a/crates/tui/src/app.rs b/crates/tui/src/app.rs new file mode 100644 index 0000000..7cc68cd --- /dev/null +++ b/crates/tui/src/app.rs @@ -0,0 +1,34 @@ +use crossterm::event::{Event, KeyCode, KeyEvent}; +use std::io; + +#[derive(Debug, Default)] +pub struct App { + pub should_quit: bool, +} + +impl App { + pub fn new() -> Self { + Self::default() + } + + // pub fn run(&mut self) -> io::Result<()> { + // Ok(()) + // } + + pub fn handle_event(&mut self, event: Event) -> io::Result<()> { + if let Event::Key(key) = event { + self.handle_key_event(key)?; + } + Ok(()) + } + + fn handle_key_event(&mut self, key: KeyEvent) -> io::Result<()> { + match key.code { + KeyCode::Char('q') | KeyCode::Esc => { + self.should_quit = true; + } + _ => {} + } + Ok(()) + } +} diff --git a/crates/tui/src/main.rs b/crates/tui/src/main.rs new file mode 100644 index 0000000..75578f4 --- /dev/null +++ b/crates/tui/src/main.rs @@ -0,0 +1,64 @@ +mod app; +mod ui; + +use app::App; +use color_eyre::Result; +use crossterm::{ + event::{self, DisableMouseCapture, EnableMouseCapture}, + execute, + terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, +}; +use ratatui::{backend::CrosstermBackend, Terminal}; +use std::io; +use ui::dashboard::render_dashboard; + +fn main() -> Result<()> { + color_eyre::install()?; + + // Setup terminal + enable_raw_mode()?; + let mut stdout = io::stdout(); + execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?; + let backend = CrosstermBackend::new(stdout); + let mut terminal = Terminal::new(backend)?; + + // Create app + let mut app = App::new(); + + // Run the app + let res = run_app(&mut terminal, &mut app); + + // Restore terminal + disable_raw_mode()?; + execute!( + terminal.backend_mut(), + LeaveAlternateScreen, + DisableMouseCapture + )?; + terminal.show_cursor()?; + + if let Err(err) = res { + eprintln!("{err:?}"); + } + + Ok(()) +} + +fn run_app( + terminal: &mut Terminal, + app: &mut App, +) -> io::Result<()> { + loop { + terminal.draw(|f| render_dashboard(f, app))?; + + if event::poll(std::time::Duration::from_millis(50))? { + let event = event::read()?; + app.handle_event(event)?; + } + + if app.should_quit { + break; + } + } + Ok(()) +} diff --git a/crates/tui/src/ui/dashboard.rs b/crates/tui/src/ui/dashboard.rs new file mode 100644 index 0000000..48089c9 --- /dev/null +++ b/crates/tui/src/ui/dashboard.rs @@ -0,0 +1,138 @@ +use ratatui::{ + layout::{Alignment, Constraint, Direction, Layout}, + style::{Color, Style}, + text::{Line, Span}, + widgets::{Block, Borders, Paragraph}, + Frame, +}; + +use crate::app::App; + +#[allow(unused_variables)] +pub fn render_dashboard(f: &mut Frame, app: &App) { + let size = f.size(); + + // Create main layout + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Percentage(20), + Constraint::Percentage(50), + Constraint::Percentage(30), + ]) + .split(size); + + // Create beautiful ASCII-style title + let title_lines = vec![ + Line::from(""), + Line::from(""), + Line::from(vec![Span::styled( + "██╗ ██╗███████╗ ██████╗████████╗ ██████╗ ██████╗ ", + Style::default() + .fg(Color::Cyan) + .add_modifier(ratatui::style::Modifier::BOLD), + )]), + Line::from(vec![Span::styled( + "██║ ██║██╔════╝██╔════╝╚══██╔══╝██╔═══██╗██╔══██╗", + Style::default() + .fg(Color::Cyan) + .add_modifier(ratatui::style::Modifier::BOLD), + )]), + Line::from(vec![Span::styled( + "██║ ██║█████╗ ██║ ██║ ██║ ██║██████╔╝", + Style::default() + .fg(Color::Cyan) + .add_modifier(ratatui::style::Modifier::BOLD), + )]), + Line::from(vec![Span::styled( + "╚██╗ ██╔╝██╔══╝ ██║ ██║ ██║ ██║██╔══██╗", + Style::default() + .fg(Color::Cyan) + .add_modifier(ratatui::style::Modifier::BOLD), + )]), + Line::from(vec![Span::styled( + " ╚████╔╝ ███████╗╚██████╗ ██║ ╚██████╔╝██║ ██║", + Style::default() + .fg(Color::Cyan) + .add_modifier(ratatui::style::Modifier::BOLD), + )]), + Line::from(vec![Span::styled( + " ╚═══╝ ╚══════╝ ╚═════╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝", + Style::default() + .fg(Color::Cyan) + .add_modifier(ratatui::style::Modifier::BOLD), + )]), + Line::from(""), + Line::from(vec![Span::styled( + "██████╗ ██████╗ ", + Style::default() + .fg(Color::Cyan) + .add_modifier(ratatui::style::Modifier::BOLD), + )]), + Line::from(vec![Span::styled( + "██╔══██╗██╔══██╗", + Style::default() + .fg(Color::Cyan) + .add_modifier(ratatui::style::Modifier::BOLD), + )]), + Line::from(vec![Span::styled( + "██║ ██║██████╔╝", + Style::default() + .fg(Color::Cyan) + .add_modifier(ratatui::style::Modifier::BOLD), + )]), + Line::from(vec![Span::styled( + "██║ ██║██╔══██╗", + Style::default() + .fg(Color::Cyan) + .add_modifier(ratatui::style::Modifier::BOLD), + )]), + Line::from(vec![Span::styled( + "██████╔╝██████╔╝", + Style::default() + .fg(Color::Cyan) + .add_modifier(ratatui::style::Modifier::BOLD), + )]), + Line::from(vec![Span::styled( + "╚═════╝ ╚═════╝ ", + Style::default() + .fg(Color::Cyan) + .add_modifier(ratatui::style::Modifier::BOLD), + )]), + Line::from(""), + Line::from(""), + ]; + + let title = Paragraph::new(title_lines) + .block( + Block::default() + .borders(Borders::ALL) + .title("Welcome") + .title_alignment(Alignment::Center) + .border_style(Style::default().fg(Color::Cyan)), + ) + .alignment(Alignment::Center); + + // Enhanced instructions with better styling + let instructions = Paragraph::new(vec![ + Line::from(vec![Span::styled( + "Press 'q' or 'Esc' to quit", + Style::default() + .fg(Color::Gray) + .add_modifier(ratatui::style::Modifier::ITALIC), + )]), + Line::from(vec![Span::styled( + "Navigate with arrow keys", + Style::default().fg(Color::DarkGray), + )]), + ]) + .alignment(Alignment::Center) + .block( + Block::default() + .borders(Borders::TOP) + .border_style(Style::default().fg(Color::DarkGray)), + ); + + f.render_widget(title, chunks[1]); + f.render_widget(instructions, chunks[2]); +} diff --git a/crates/tui/src/ui/mod.rs b/crates/tui/src/ui/mod.rs new file mode 100644 index 0000000..d87ba85 --- /dev/null +++ b/crates/tui/src/ui/mod.rs @@ -0,0 +1 @@ +pub mod dashboard; From c0bb2c991479ff40ce21e5f27dffbf9ba84de303 Mon Sep 17 00:00:00 2001 From: blackpanther26 Date: Sun, 24 Aug 2025 16:55:49 +0530 Subject: [PATCH 02/25] add basic page layouts --- crates/tui/src/app.rs | 35 +++++++++++-- crates/tui/src/main.rs | 12 +++-- crates/tui/src/ui/dashboard.rs | 22 ++++---- crates/tui/src/ui/db.rs | 68 +++++++++++++++++++++++++ crates/tui/src/ui/mod.rs | 2 + crates/tui/src/ui/vector_operations.rs | 70 ++++++++++++++++++++++++++ 6 files changed, 193 insertions(+), 16 deletions(-) create mode 100644 crates/tui/src/ui/db.rs create mode 100644 crates/tui/src/ui/vector_operations.rs diff --git a/crates/tui/src/app.rs b/crates/tui/src/app.rs index 7cc68cd..17e2bd6 100644 --- a/crates/tui/src/app.rs +++ b/crates/tui/src/app.rs @@ -1,9 +1,18 @@ use crossterm::event::{Event, KeyCode, KeyEvent}; use std::io; +#[derive(Debug, Default, Clone, PartialEq)] +pub enum AppState { + #[default] + Dashboard, + Database, + VectorOperations, +} + #[derive(Debug, Default)] pub struct App { pub should_quit: bool, + pub state: AppState, } impl App { @@ -11,10 +20,6 @@ impl App { Self::default() } - // pub fn run(&mut self) -> io::Result<()> { - // Ok(()) - // } - pub fn handle_event(&mut self, event: Event) -> io::Result<()> { if let Event::Key(key) = event { self.handle_key_event(key)?; @@ -27,8 +32,30 @@ impl App { KeyCode::Char('q') | KeyCode::Esc => { self.should_quit = true; } + KeyCode::Right | KeyCode::Enter => { + self.next_page(); + } + KeyCode::Left => { + self.previous_page(); + } _ => {} } Ok(()) } + + fn next_page(&mut self) { + self.state = match self.state { + AppState::Dashboard => AppState::Database, + AppState::Database => AppState::VectorOperations, + AppState::VectorOperations => AppState::Dashboard, + }; + } + + fn previous_page(&mut self) { + self.state = match self.state { + AppState::Dashboard => AppState::VectorOperations, + AppState::Database => AppState::Dashboard, + AppState::VectorOperations => AppState::Database, + }; + } } diff --git a/crates/tui/src/main.rs b/crates/tui/src/main.rs index 75578f4..f1dd47c 100644 --- a/crates/tui/src/main.rs +++ b/crates/tui/src/main.rs @@ -1,7 +1,7 @@ mod app; mod ui; -use app::App; +use app::{App, AppState}; use color_eyre::Result; use crossterm::{ event::{self, DisableMouseCapture, EnableMouseCapture}, @@ -10,7 +10,9 @@ use crossterm::{ }; use ratatui::{backend::CrosstermBackend, Terminal}; use std::io; -use ui::dashboard::render_dashboard; +use ui::{ + dashboard::render_dashboard, db::render_database, vector_operations::render_vector_operations, +}; fn main() -> Result<()> { color_eyre::install()?; @@ -49,7 +51,11 @@ fn run_app( app: &mut App, ) -> io::Result<()> { loop { - terminal.draw(|f| render_dashboard(f, app))?; + terminal.draw(|f| match app.state { + AppState::Dashboard => render_dashboard(f, app), + AppState::Database => render_database(f, app), + AppState::VectorOperations => render_vector_operations(f, app), + })?; if event::poll(std::time::Duration::from_millis(50))? { let event = event::read()?; diff --git a/crates/tui/src/ui/dashboard.rs b/crates/tui/src/ui/dashboard.rs index 48089c9..e07445e 100644 --- a/crates/tui/src/ui/dashboard.rs +++ b/crates/tui/src/ui/dashboard.rs @@ -12,7 +12,6 @@ use crate::app::App; pub fn render_dashboard(f: &mut Frame, app: &App) { let size = f.size(); - // Create main layout let chunks = Layout::default() .direction(Direction::Vertical) .constraints([ @@ -22,7 +21,6 @@ pub fn render_dashboard(f: &mut Frame, app: &App) { ]) .split(size); - // Create beautiful ASCII-style title let title_lines = vec![ Line::from(""), Line::from(""), @@ -113,23 +111,29 @@ pub fn render_dashboard(f: &mut Frame, app: &App) { ) .alignment(Alignment::Center); - // Enhanced instructions with better styling let instructions = Paragraph::new(vec![ + Line::from(vec![ + Span::styled("→ Enter/Right", Style::default().fg(Color::Green)), + Span::raw(" to navigate to Database Management"), + ]), + Line::from(vec![ + Span::styled("← Left", Style::default().fg(Color::Gray)), + Span::raw(" | "), + Span::styled("→ Right", Style::default().fg(Color::Gray)), + Span::raw(" to navigate between pages"), + ]), Line::from(vec![Span::styled( "Press 'q' or 'Esc' to quit", Style::default() - .fg(Color::Gray) + .fg(Color::Red) .add_modifier(ratatui::style::Modifier::ITALIC), )]), - Line::from(vec![Span::styled( - "Navigate with arrow keys", - Style::default().fg(Color::DarkGray), - )]), ]) .alignment(Alignment::Center) .block( Block::default() - .borders(Borders::TOP) + .borders(Borders::ALL) + .title("Navigation") .border_style(Style::default().fg(Color::DarkGray)), ); diff --git a/crates/tui/src/ui/db.rs b/crates/tui/src/ui/db.rs new file mode 100644 index 0000000..6802440 --- /dev/null +++ b/crates/tui/src/ui/db.rs @@ -0,0 +1,68 @@ +use ratatui::{ + layout::{Alignment, Constraint, Direction, Layout}, + style::{Color, Style}, + text::{Line, Span}, + widgets::{Block, Borders, List, ListItem, Paragraph}, + Frame, +}; + +use crate::app::App; + +#[allow(unused_variables)] +pub fn render_database(f: &mut Frame, app: &App) { + let size = f.size(); + + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Length(3), + Constraint::Min(0), + Constraint::Length(3), + ]) + .split(size); + + let title = Paragraph::new("Database Management") + .block( + Block::default() + .borders(Borders::ALL) + .title("Vector DB") + .title_alignment(Alignment::Center) + .border_style(Style::default().fg(Color::Green)), + ) + .alignment(Alignment::Center) + .style(Style::default().fg(Color::Green)); + + let db_items = vec![ + ListItem::new("Create New Database"), + ListItem::new("Open Existing Database"), + ListItem::new("Delete Database"), + ]; + + let db_list = List::new(db_items) + .block( + Block::default() + .borders(Borders::ALL) + .title("Database Operations") + .border_style(Style::default().fg(Color::Green)), + ) + .style(Style::default().fg(Color::White)) + .highlight_style(Style::default().fg(Color::Yellow)); + + let instructions = Paragraph::new(vec![Line::from(vec![ + Span::styled("← Previous", Style::default().fg(Color::Gray)), + Span::raw(" | "), + Span::styled("→ Next", Style::default().fg(Color::Gray)), + Span::raw(" | "), + Span::styled("q/Esc Quit", Style::default().fg(Color::Red)), + ])]) + .alignment(Alignment::Center) + .block( + Block::default() + .borders(Borders::ALL) + .border_style(Style::default().fg(Color::DarkGray)), + ); + + f.render_widget(title, chunks[0]); + f.render_widget(db_list, chunks[1]); + f.render_widget(instructions, chunks[2]); +} diff --git a/crates/tui/src/ui/mod.rs b/crates/tui/src/ui/mod.rs index d87ba85..5cf8fd3 100644 --- a/crates/tui/src/ui/mod.rs +++ b/crates/tui/src/ui/mod.rs @@ -1 +1,3 @@ pub mod dashboard; +pub mod db; +pub mod vector_operations; diff --git a/crates/tui/src/ui/vector_operations.rs b/crates/tui/src/ui/vector_operations.rs new file mode 100644 index 0000000..b3c61a7 --- /dev/null +++ b/crates/tui/src/ui/vector_operations.rs @@ -0,0 +1,70 @@ +use ratatui::{ + layout::{Alignment, Constraint, Direction, Layout}, + style::{Color, Style}, + text::{Line, Span}, + widgets::{Block, Borders, List, ListItem, Paragraph}, + Frame, +}; + +use crate::app::App; + +#[allow(unused_variables)] +pub fn render_vector_operations(f: &mut Frame, app: &App) { + let size = f.size(); + + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Length(3), + Constraint::Min(0), + Constraint::Length(3), + ]) + .split(size); + + let title = Paragraph::new("Vector Operations") + .block( + Block::default() + .borders(Borders::ALL) + .title("Vector DB") + .title_alignment(Alignment::Center) + .border_style(Style::default().fg(Color::Magenta)), + ) + .alignment(Alignment::Center) + .style(Style::default().fg(Color::Magenta)); + + let vector_items = vec![ + ListItem::new("List All Vectors"), + ListItem::new("Get Vector"), + ListItem::new("Insert Vector"), + ListItem::new("Delete Vector"), + ListItem::new("Search Similar Vectors"), + ]; + + let vector_list = List::new(vector_items) + .block( + Block::default() + .borders(Borders::ALL) + .title("Available Operations") + .border_style(Style::default().fg(Color::Magenta)), + ) + .style(Style::default().fg(Color::White)) + .highlight_style(Style::default().fg(Color::Yellow)); + + let instructions = Paragraph::new(vec![Line::from(vec![ + Span::styled("← Previous", Style::default().fg(Color::Gray)), + Span::raw(" | "), + Span::styled("→ Next", Style::default().fg(Color::Gray)), + Span::raw(" | "), + Span::styled("q/Esc Quit", Style::default().fg(Color::Red)), + ])]) + .alignment(Alignment::Center) + .block( + Block::default() + .borders(Borders::ALL) + .border_style(Style::default().fg(Color::DarkGray)), + ); + + f.render_widget(title, chunks[0]); + f.render_widget(vector_list, chunks[1]); + f.render_widget(instructions, chunks[2]); +} From 827b3d4e6a117b3f8223e73c3e2d7a2b67ee208a Mon Sep 17 00:00:00 2001 From: blackpanther26 Date: Sun, 24 Aug 2025 17:11:51 +0530 Subject: [PATCH 03/25] fix page navigation --- crates/tui/src/app.rs | 4 ++-- crates/tui/src/ui/dashboard.rs | 6 ------ crates/tui/src/ui/vector_operations.rs | 2 -- 3 files changed, 2 insertions(+), 10 deletions(-) diff --git a/crates/tui/src/app.rs b/crates/tui/src/app.rs index 17e2bd6..2317853 100644 --- a/crates/tui/src/app.rs +++ b/crates/tui/src/app.rs @@ -47,13 +47,13 @@ impl App { self.state = match self.state { AppState::Dashboard => AppState::Database, AppState::Database => AppState::VectorOperations, - AppState::VectorOperations => AppState::Dashboard, + AppState::VectorOperations => AppState::VectorOperations, }; } fn previous_page(&mut self) { self.state = match self.state { - AppState::Dashboard => AppState::VectorOperations, + AppState::Dashboard => AppState::Dashboard, AppState::Database => AppState::Dashboard, AppState::VectorOperations => AppState::Database, }; diff --git a/crates/tui/src/ui/dashboard.rs b/crates/tui/src/ui/dashboard.rs index e07445e..72c3c7c 100644 --- a/crates/tui/src/ui/dashboard.rs +++ b/crates/tui/src/ui/dashboard.rs @@ -116,12 +116,6 @@ pub fn render_dashboard(f: &mut Frame, app: &App) { Span::styled("→ Enter/Right", Style::default().fg(Color::Green)), Span::raw(" to navigate to Database Management"), ]), - Line::from(vec![ - Span::styled("← Left", Style::default().fg(Color::Gray)), - Span::raw(" | "), - Span::styled("→ Right", Style::default().fg(Color::Gray)), - Span::raw(" to navigate between pages"), - ]), Line::from(vec![Span::styled( "Press 'q' or 'Esc' to quit", Style::default() diff --git a/crates/tui/src/ui/vector_operations.rs b/crates/tui/src/ui/vector_operations.rs index b3c61a7..56e3ee5 100644 --- a/crates/tui/src/ui/vector_operations.rs +++ b/crates/tui/src/ui/vector_operations.rs @@ -53,8 +53,6 @@ pub fn render_vector_operations(f: &mut Frame, app: &App) { let instructions = Paragraph::new(vec![Line::from(vec![ Span::styled("← Previous", Style::default().fg(Color::Gray)), Span::raw(" | "), - Span::styled("→ Next", Style::default().fg(Color::Gray)), - Span::raw(" | "), Span::styled("q/Esc Quit", Style::default().fg(Color::Red)), ])]) .alignment(Alignment::Center) From 9b0c19b872f64a38c38ff48e8fef01d25e365c95 Mon Sep 17 00:00:00 2001 From: blackpanther26 Date: Sun, 24 Aug 2025 17:35:33 +0530 Subject: [PATCH 04/25] add selection --- crates/tui/src/app.rs | 42 ++++++++++++++++++++++++++ crates/tui/src/ui/dashboard.rs | 10 ++---- crates/tui/src/ui/db.rs | 15 +++++++-- crates/tui/src/ui/vector_operations.rs | 15 +++++++-- 4 files changed, 69 insertions(+), 13 deletions(-) diff --git a/crates/tui/src/app.rs b/crates/tui/src/app.rs index 2317853..5137658 100644 --- a/crates/tui/src/app.rs +++ b/crates/tui/src/app.rs @@ -13,6 +13,8 @@ pub enum AppState { pub struct App { pub should_quit: bool, pub state: AppState, + pub db_selected: usize, + pub vector_selected: usize, } impl App { @@ -38,6 +40,12 @@ impl App { KeyCode::Left => { self.previous_page(); } + KeyCode::Up => { + self.select_previous(); + } + KeyCode::Down => { + self.select_next(); + } _ => {} } Ok(()) @@ -58,4 +66,38 @@ impl App { AppState::VectorOperations => AppState::Database, }; } + + fn select_previous(&mut self) { + match self.state { + AppState::Dashboard => {} + AppState::Database => { + if self.db_selected > 0 { + self.db_selected -= 1; + } + } + AppState::VectorOperations => { + if self.vector_selected > 0 { + self.vector_selected -= 1; + } + } + } + } + + fn select_next(&mut self) { + match self.state { + AppState::Dashboard => {} + AppState::Database => { + let max_items = 3; // Number of database operations + if self.db_selected < max_items - 1 { + self.db_selected += 1; + } + } + AppState::VectorOperations => { + let max_items = 5; // Number of vector operations + if self.vector_selected < max_items - 1 { + self.vector_selected += 1; + } + } + } + } } diff --git a/crates/tui/src/ui/dashboard.rs b/crates/tui/src/ui/dashboard.rs index 72c3c7c..62bbf33 100644 --- a/crates/tui/src/ui/dashboard.rs +++ b/crates/tui/src/ui/dashboard.rs @@ -14,11 +14,7 @@ pub fn render_dashboard(f: &mut Frame, app: &App) { let chunks = Layout::default() .direction(Direction::Vertical) - .constraints([ - Constraint::Percentage(20), - Constraint::Percentage(50), - Constraint::Percentage(30), - ]) + .constraints([Constraint::Percentage(70), Constraint::Percentage(30)]) .split(size); let title_lines = vec![ @@ -131,6 +127,6 @@ pub fn render_dashboard(f: &mut Frame, app: &App) { .border_style(Style::default().fg(Color::DarkGray)), ); - f.render_widget(title, chunks[1]); - f.render_widget(instructions, chunks[2]); + f.render_widget(title, chunks[0]); + f.render_widget(instructions, chunks[1]); } diff --git a/crates/tui/src/ui/db.rs b/crates/tui/src/ui/db.rs index 6802440..967c429 100644 --- a/crates/tui/src/ui/db.rs +++ b/crates/tui/src/ui/db.rs @@ -2,7 +2,7 @@ use ratatui::{ layout::{Alignment, Constraint, Direction, Layout}, style::{Color, Style}, text::{Line, Span}, - widgets::{Block, Borders, List, ListItem, Paragraph}, + widgets::{Block, Borders, List, ListItem, ListState, Paragraph}, Frame, }; @@ -46,9 +46,18 @@ pub fn render_database(f: &mut Frame, app: &App) { .border_style(Style::default().fg(Color::Green)), ) .style(Style::default().fg(Color::White)) - .highlight_style(Style::default().fg(Color::Yellow)); + .highlight_style(Style::default().fg(Color::Green).bg(Color::Gray)); + + let mut list_state = ListState::default(); + list_state.select(Some(app.db_selected)); let instructions = Paragraph::new(vec![Line::from(vec![ + Span::styled("↑ Up", Style::default().fg(Color::Gray)), + Span::raw(" | "), + Span::styled("↓ Down", Style::default().fg(Color::Gray)), + Span::raw(" | "), + Span::styled("Enter Select", Style::default().fg(Color::Green)), + Span::raw(" | "), Span::styled("← Previous", Style::default().fg(Color::Gray)), Span::raw(" | "), Span::styled("→ Next", Style::default().fg(Color::Gray)), @@ -63,6 +72,6 @@ pub fn render_database(f: &mut Frame, app: &App) { ); f.render_widget(title, chunks[0]); - f.render_widget(db_list, chunks[1]); + f.render_stateful_widget(db_list, chunks[1], &mut list_state); f.render_widget(instructions, chunks[2]); } diff --git a/crates/tui/src/ui/vector_operations.rs b/crates/tui/src/ui/vector_operations.rs index 56e3ee5..2e42180 100644 --- a/crates/tui/src/ui/vector_operations.rs +++ b/crates/tui/src/ui/vector_operations.rs @@ -2,7 +2,7 @@ use ratatui::{ layout::{Alignment, Constraint, Direction, Layout}, style::{Color, Style}, text::{Line, Span}, - widgets::{Block, Borders, List, ListItem, Paragraph}, + widgets::{Block, Borders, List, ListItem, ListState, Paragraph}, Frame, }; @@ -48,9 +48,18 @@ pub fn render_vector_operations(f: &mut Frame, app: &App) { .border_style(Style::default().fg(Color::Magenta)), ) .style(Style::default().fg(Color::White)) - .highlight_style(Style::default().fg(Color::Yellow)); + .highlight_style(Style::default().fg(Color::Magenta).bg(Color::Gray)); + + let mut list_state = ListState::default(); + list_state.select(Some(app.vector_selected)); let instructions = Paragraph::new(vec![Line::from(vec![ + Span::styled("↑ Up", Style::default().fg(Color::Gray)), + Span::raw(" | "), + Span::styled("↓ Down", Style::default().fg(Color::Gray)), + Span::raw(" | "), + Span::styled("Enter Select", Style::default().fg(Color::Green)), + Span::raw(" | "), Span::styled("← Previous", Style::default().fg(Color::Gray)), Span::raw(" | "), Span::styled("q/Esc Quit", Style::default().fg(Color::Red)), @@ -63,6 +72,6 @@ pub fn render_vector_operations(f: &mut Frame, app: &App) { ); f.render_widget(title, chunks[0]); - f.render_widget(vector_list, chunks[1]); + f.render_stateful_widget(vector_list, chunks[1], &mut list_state); f.render_widget(instructions, chunks[2]); } From 94e9f43d2d478739f4122600ddf25564f30fd34f Mon Sep 17 00:00:00 2001 From: blackpanther26 Date: Sun, 24 Aug 2025 17:53:37 +0530 Subject: [PATCH 05/25] refactor --- crates/tui/src/app.rs | 54 +++++++------ crates/tui/src/main.rs | 46 ++++++------ crates/tui/src/ui/components.rs | 82 ++++++++++++++++++++ crates/tui/src/ui/db.rs | 98 +++++++++--------------- crates/tui/src/ui/mod.rs | 1 + crates/tui/src/ui/vector_operations.rs | 100 ++++++++++--------------- 6 files changed, 208 insertions(+), 173 deletions(-) create mode 100644 crates/tui/src/ui/components.rs diff --git a/crates/tui/src/app.rs b/crates/tui/src/app.rs index 5137658..db4c97a 100644 --- a/crates/tui/src/app.rs +++ b/crates/tui/src/app.rs @@ -1,6 +1,8 @@ use crossterm::event::{Event, KeyCode, KeyEvent}; use std::io; +use crate::ui::{db, vector_operations}; + #[derive(Debug, Default, Clone, PartialEq)] pub enum AppState { #[default] @@ -31,26 +33,20 @@ impl App { fn handle_key_event(&mut self, key: KeyEvent) -> io::Result<()> { match key.code { - KeyCode::Char('q') | KeyCode::Esc => { - self.should_quit = true; - } - KeyCode::Right | KeyCode::Enter => { - self.next_page(); - } - KeyCode::Left => { - self.previous_page(); - } - KeyCode::Up => { - self.select_previous(); - } - KeyCode::Down => { - self.select_next(); - } + KeyCode::Char('q') | KeyCode::Esc => self.quit(), + KeyCode::Right | KeyCode::Enter => self.next_page(), + KeyCode::Left => self.previous_page(), + KeyCode::Up => self.select_previous(), + KeyCode::Down => self.select_next(), _ => {} } Ok(()) } + fn quit(&mut self) { + self.should_quit = true; + } + fn next_page(&mut self) { self.state = match self.state { AppState::Dashboard => AppState::Database, @@ -71,14 +67,10 @@ impl App { match self.state { AppState::Dashboard => {} AppState::Database => { - if self.db_selected > 0 { - self.db_selected -= 1; - } + self.db_selected = self.db_selected.saturating_sub(1); } AppState::VectorOperations => { - if self.vector_selected > 0 { - self.vector_selected -= 1; - } + self.vector_selected = self.vector_selected.saturating_sub(1); } } } @@ -87,17 +79,21 @@ impl App { match self.state { AppState::Dashboard => {} AppState::Database => { - let max_items = 3; // Number of database operations - if self.db_selected < max_items - 1 { - self.db_selected += 1; - } + let max_items = db::get_db_operations_count(); + self.db_selected = (self.db_selected + 1).min(max_items - 1); } AppState::VectorOperations => { - let max_items = 5; // Number of vector operations - if self.vector_selected < max_items - 1 { - self.vector_selected += 1; - } + let max_items = vector_operations::get_vector_operations_count(); + self.vector_selected = (self.vector_selected + 1).min(max_items - 1); } } } + + pub fn get_selected_operation(&self) -> Option { + match self.state { + AppState::Dashboard => None, + AppState::Database => Some(self.db_selected), + AppState::VectorOperations => Some(self.vector_selected), + } + } } diff --git a/crates/tui/src/main.rs b/crates/tui/src/main.rs index f1dd47c..26f21ca 100644 --- a/crates/tui/src/main.rs +++ b/crates/tui/src/main.rs @@ -1,6 +1,5 @@ mod app; mod ui; - use app::{App, AppState}; use color_eyre::Result; use crossterm::{ @@ -14,23 +13,35 @@ use ui::{ dashboard::render_dashboard, db::render_database, vector_operations::render_vector_operations, }; +const POLL_DURATION: std::time::Duration = std::time::Duration::from_millis(50); + fn main() -> Result<()> { color_eyre::install()?; - // Setup terminal + let mut terminal = setup_terminal()?; + let mut app = App::new(); + + let result = run_app(&mut terminal, &mut app); + + restore_terminal(&mut terminal)?; + + if let Err(err) = result { + eprintln!("Application error: {err:?}"); + } + + Ok(()) +} + +fn setup_terminal() -> Result>> { enable_raw_mode()?; let mut stdout = io::stdout(); execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?; let backend = CrosstermBackend::new(stdout); - let mut terminal = Terminal::new(backend)?; - - // Create app - let mut app = App::new(); - - // Run the app - let res = run_app(&mut terminal, &mut app); + let terminal = Terminal::new(backend)?; + Ok(terminal) +} - // Restore terminal +fn restore_terminal(terminal: &mut Terminal>) -> Result<()> { disable_raw_mode()?; execute!( terminal.backend_mut(), @@ -38,18 +49,10 @@ fn main() -> Result<()> { DisableMouseCapture )?; terminal.show_cursor()?; - - if let Err(err) = res { - eprintln!("{err:?}"); - } - Ok(()) } -fn run_app( - terminal: &mut Terminal, - app: &mut App, -) -> io::Result<()> { +fn run_app(terminal: &mut Terminal>, app: &mut App) -> io::Result<()> { loop { terminal.draw(|f| match app.state { AppState::Dashboard => render_dashboard(f, app), @@ -57,9 +60,8 @@ fn run_app( AppState::VectorOperations => render_vector_operations(f, app), })?; - if event::poll(std::time::Duration::from_millis(50))? { - let event = event::read()?; - app.handle_event(event)?; + if event::poll(POLL_DURATION)? { + app.handle_event(event::read()?)?; } if app.should_quit { diff --git a/crates/tui/src/ui/components.rs b/crates/tui/src/ui/components.rs new file mode 100644 index 0000000..0ff67ef --- /dev/null +++ b/crates/tui/src/ui/components.rs @@ -0,0 +1,82 @@ +use ratatui::{ + layout::Alignment, + style::{Color, Style}, + text::{Line, Span}, + widgets::{Block, Borders, List, ListItem, ListState, Paragraph}, + Frame, +}; + +pub struct PageTitle { + pub text: &'static str, + pub color: Color, +} + +impl PageTitle { + pub fn render(&self) -> Paragraph<'static> { + Paragraph::new(self.text) + .block( + Block::default() + .borders(Borders::ALL) + .title("Vector DB") + .title_alignment(Alignment::Center) + .border_style(Style::default().fg(self.color)), + ) + .alignment(Alignment::Center) + .style(Style::default().fg(self.color)) + } +} + +pub struct OperationsList { + pub items: Vec>, + pub title: &'static str, + pub color: Color, + pub selected: usize, +} + +impl OperationsList { + pub fn render(&self, f: &mut Frame, area: ratatui::layout::Rect) { + let list = List::new(self.items.clone()) + .block( + Block::default() + .borders(Borders::ALL) + .title(self.title) + .border_style(Style::default().fg(self.color)), + ) + .style(Style::default().fg(Color::White)) + .highlight_style(Style::default().fg(self.color).bg(Color::Gray)); + + let mut list_state = ListState::default(); + list_state.select(Some(self.selected)); + + f.render_stateful_widget(list, area, &mut list_state); + } +} + +pub fn create_instructions(spans: Vec<(String, Color)>) -> Paragraph<'static> { + let mut line_spans = Vec::new(); + + for (i, (text, color)) in spans.iter().enumerate() { + if i > 0 { + line_spans.push(Span::raw(" | ")); + } + line_spans.push(Span::styled(text.clone(), Style::default().fg(*color))); + } + + Paragraph::new(vec![Line::from(line_spans)]) + .alignment(Alignment::Center) + .block( + Block::default() + .borders(Borders::ALL) + .border_style(Style::default().fg(Color::DarkGray)), + ) +} + +pub fn common_instructions() -> Vec<(String, Color)> { + vec![ + ("↑ Up".to_string(), Color::Gray), + ("↓ Down".to_string(), Color::Gray), + ("Enter Select".to_string(), Color::Green), + ("← Previous".to_string(), Color::Gray), + ("q/Esc Quit".to_string(), Color::Red), + ] +} diff --git a/crates/tui/src/ui/db.rs b/crates/tui/src/ui/db.rs index 967c429..36b8cdb 100644 --- a/crates/tui/src/ui/db.rs +++ b/crates/tui/src/ui/db.rs @@ -1,17 +1,24 @@ use ratatui::{ - layout::{Alignment, Constraint, Direction, Layout}, - style::{Color, Style}, - text::{Line, Span}, - widgets::{Block, Borders, List, ListItem, ListState, Paragraph}, + layout::{Constraint, Direction, Layout}, + style::Color, + widgets::ListItem, Frame, }; +use super::components::{common_instructions, create_instructions, OperationsList, PageTitle}; use crate::app::App; -#[allow(unused_variables)] -pub fn render_database(f: &mut Frame, app: &App) { - let size = f.size(); +const DB_OPERATIONS: &[&str] = &[ + "Create New Database", + "Open Existing Database", + "Delete Database", +]; + +fn get_db_items() -> Vec> { + DB_OPERATIONS.iter().map(|&op| ListItem::new(op)).collect() +} +pub fn render_database(f: &mut Frame, app: &App) { let chunks = Layout::default() .direction(Direction::Vertical) .constraints([ @@ -19,59 +26,28 @@ pub fn render_database(f: &mut Frame, app: &App) { Constraint::Min(0), Constraint::Length(3), ]) - .split(size); - - let title = Paragraph::new("Database Management") - .block( - Block::default() - .borders(Borders::ALL) - .title("Vector DB") - .title_alignment(Alignment::Center) - .border_style(Style::default().fg(Color::Green)), - ) - .alignment(Alignment::Center) - .style(Style::default().fg(Color::Green)); - - let db_items = vec![ - ListItem::new("Create New Database"), - ListItem::new("Open Existing Database"), - ListItem::new("Delete Database"), - ]; - - let db_list = List::new(db_items) - .block( - Block::default() - .borders(Borders::ALL) - .title("Database Operations") - .border_style(Style::default().fg(Color::Green)), - ) - .style(Style::default().fg(Color::White)) - .highlight_style(Style::default().fg(Color::Green).bg(Color::Gray)); - - let mut list_state = ListState::default(); - list_state.select(Some(app.db_selected)); - - let instructions = Paragraph::new(vec![Line::from(vec![ - Span::styled("↑ Up", Style::default().fg(Color::Gray)), - Span::raw(" | "), - Span::styled("↓ Down", Style::default().fg(Color::Gray)), - Span::raw(" | "), - Span::styled("Enter Select", Style::default().fg(Color::Green)), - Span::raw(" | "), - Span::styled("← Previous", Style::default().fg(Color::Gray)), - Span::raw(" | "), - Span::styled("→ Next", Style::default().fg(Color::Gray)), - Span::raw(" | "), - Span::styled("q/Esc Quit", Style::default().fg(Color::Red)), - ])]) - .alignment(Alignment::Center) - .block( - Block::default() - .borders(Borders::ALL) - .border_style(Style::default().fg(Color::DarkGray)), - ); + .split(f.size()); + + let title = PageTitle { + text: "Database Management", + color: Color::Green, + }; + + let operations_list = OperationsList { + items: get_db_items(), + title: "Database Operations", + color: Color::Green, + selected: app.db_selected, + }; + + let mut instructions = common_instructions(); + instructions.insert(4, ("→ Next".to_string(), Color::Gray)); + + f.render_widget(title.render(), chunks[0]); + operations_list.render(f, chunks[1]); + f.render_widget(create_instructions(instructions), chunks[2]); +} - f.render_widget(title, chunks[0]); - f.render_stateful_widget(db_list, chunks[1], &mut list_state); - f.render_widget(instructions, chunks[2]); +pub fn get_db_operations_count() -> usize { + DB_OPERATIONS.len() } diff --git a/crates/tui/src/ui/mod.rs b/crates/tui/src/ui/mod.rs index 5cf8fd3..be4731f 100644 --- a/crates/tui/src/ui/mod.rs +++ b/crates/tui/src/ui/mod.rs @@ -1,3 +1,4 @@ +pub mod components; pub mod dashboard; pub mod db; pub mod vector_operations; diff --git a/crates/tui/src/ui/vector_operations.rs b/crates/tui/src/ui/vector_operations.rs index 2e42180..1c6a976 100644 --- a/crates/tui/src/ui/vector_operations.rs +++ b/crates/tui/src/ui/vector_operations.rs @@ -1,17 +1,29 @@ use ratatui::{ - layout::{Alignment, Constraint, Direction, Layout}, - style::{Color, Style}, - text::{Line, Span}, - widgets::{Block, Borders, List, ListItem, ListState, Paragraph}, + layout::{Constraint, Direction, Layout}, + style::Color, + widgets::ListItem, Frame, }; +use super::components::{common_instructions, create_instructions, OperationsList, PageTitle}; use crate::app::App; -#[allow(unused_variables)] -pub fn render_vector_operations(f: &mut Frame, app: &App) { - let size = f.size(); +const VECTOR_OPERATIONS: &[&str] = &[ + "List All Vectors", + "Get Vector", + "Insert Vector", + "Delete Vector", + "Search Similar Vectors", +]; + +fn get_vector_items() -> Vec> { + VECTOR_OPERATIONS + .iter() + .map(|&op| ListItem::new(op)) + .collect() +} +pub fn render_vector_operations(f: &mut Frame, app: &App) { let chunks = Layout::default() .direction(Direction::Vertical) .constraints([ @@ -19,59 +31,25 @@ pub fn render_vector_operations(f: &mut Frame, app: &App) { Constraint::Min(0), Constraint::Length(3), ]) - .split(size); - - let title = Paragraph::new("Vector Operations") - .block( - Block::default() - .borders(Borders::ALL) - .title("Vector DB") - .title_alignment(Alignment::Center) - .border_style(Style::default().fg(Color::Magenta)), - ) - .alignment(Alignment::Center) - .style(Style::default().fg(Color::Magenta)); - - let vector_items = vec![ - ListItem::new("List All Vectors"), - ListItem::new("Get Vector"), - ListItem::new("Insert Vector"), - ListItem::new("Delete Vector"), - ListItem::new("Search Similar Vectors"), - ]; - - let vector_list = List::new(vector_items) - .block( - Block::default() - .borders(Borders::ALL) - .title("Available Operations") - .border_style(Style::default().fg(Color::Magenta)), - ) - .style(Style::default().fg(Color::White)) - .highlight_style(Style::default().fg(Color::Magenta).bg(Color::Gray)); - - let mut list_state = ListState::default(); - list_state.select(Some(app.vector_selected)); - - let instructions = Paragraph::new(vec![Line::from(vec![ - Span::styled("↑ Up", Style::default().fg(Color::Gray)), - Span::raw(" | "), - Span::styled("↓ Down", Style::default().fg(Color::Gray)), - Span::raw(" | "), - Span::styled("Enter Select", Style::default().fg(Color::Green)), - Span::raw(" | "), - Span::styled("← Previous", Style::default().fg(Color::Gray)), - Span::raw(" | "), - Span::styled("q/Esc Quit", Style::default().fg(Color::Red)), - ])]) - .alignment(Alignment::Center) - .block( - Block::default() - .borders(Borders::ALL) - .border_style(Style::default().fg(Color::DarkGray)), - ); + .split(f.size()); + + let title = PageTitle { + text: "Vector Operations", + color: Color::Magenta, + }; + + let operations_list = OperationsList { + items: get_vector_items(), + title: "Available Operations", + color: Color::Magenta, + selected: app.vector_selected, + }; + + f.render_widget(title.render(), chunks[0]); + operations_list.render(f, chunks[1]); + f.render_widget(create_instructions(common_instructions()), chunks[2]); +} - f.render_widget(title, chunks[0]); - f.render_stateful_widget(vector_list, chunks[1], &mut list_state); - f.render_widget(instructions, chunks[2]); +pub fn get_vector_operations_count() -> usize { + VECTOR_OPERATIONS.len() } From bc3197481e87efb87b9554c0380dee41f219e675 Mon Sep 17 00:00:00 2001 From: blackpanther26 Date: Tue, 23 Sep 2025 17:11:17 +0530 Subject: [PATCH 06/25] feat: implement modular database management --- Cargo.lock | 197 +++++++++++++++++++++++++ crates/tui/Cargo.toml | 2 +- crates/tui/src/app.rs | 99 ------------- crates/tui/src/app/database.rs | 103 +++++++++++++ crates/tui/src/app/events.rs | 177 ++++++++++++++++++++++ crates/tui/src/app/mod.rs | 121 +++++++++++++++ crates/tui/src/app/modal.rs | 97 ++++++++++++ crates/tui/src/app/state.rs | 14 ++ crates/tui/src/main.rs | 20 ++- crates/tui/src/ui/db.rs | 6 +- crates/tui/src/ui/mod.rs | 1 + crates/tui/src/ui/modal.rs | 112 ++++++++++++++ crates/tui/src/ui/vector_operations.rs | 40 ++++- 13 files changed, 873 insertions(+), 116 deletions(-) delete mode 100644 crates/tui/src/app.rs create mode 100644 crates/tui/src/app/database.rs create mode 100644 crates/tui/src/app/events.rs create mode 100644 crates/tui/src/app/mod.rs create mode 100644 crates/tui/src/app/modal.rs create mode 100644 crates/tui/src/app/state.rs create mode 100644 crates/tui/src/ui/modal.rs diff --git a/Cargo.lock b/Cargo.lock index 11a1038..447db65 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -32,6 +32,15 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anyhow" version = "1.0.98" @@ -128,6 +137,12 @@ version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" +[[package]] +name = "bumpalo" +version = "3.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" + [[package]] name = "bytes" version = "1.10.1" @@ -185,6 +200,20 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" +[[package]] +name = "chrono" +version = "0.4.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link", +] + [[package]] name = "clang-sys" version = "1.8.1" @@ -244,6 +273,12 @@ dependencies = [ "serde", ] +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + [[package]] name = "crossterm" version = "0.27.0" @@ -338,6 +373,30 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "iana-time-zone" +version = "0.1.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "indenter" version = "0.3.4" @@ -396,6 +455,16 @@ dependencies = [ "libc", ] +[[package]] +name = "js-sys" +version = "0.3.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "852f13bec5eba4ba9afbeb93fd7c13fe56147f055939ae21c43a29a0ecb2702e" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -540,6 +609,15 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + [[package]] name = "object" version = "0.36.7" @@ -987,6 +1065,7 @@ name = "tui" version = "0.1.0" dependencies = [ "anyhow", + "chrono", "color-eyre", "core", "crossterm", @@ -1053,6 +1132,65 @@ dependencies = [ "wit-bindgen-rt", ] +[[package]] +name = "wasm-bindgen" +version = "0.2.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab10a69fbd0a177f5f649ad4d8d3305499c42bab9aef2f7ff592d0ec8f833819" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb702423545a6007bbc368fde243ba47ca275e549c8a28617f56f6ba53b1d1c" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc65f4f411d91494355917b605e1480033152658d71f722a90647f56a70c88a0" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffc003a991398a8ee604a401e194b6b3a39677b3173d6e74495eb51b82e99a32" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "293c37f4efa430ca14db3721dfbe48d8c33308096bd44d80ebaa775ab71ba1cf" +dependencies = [ + "unicode-ident", +] + [[package]] name = "winapi" version = "0.3.9" @@ -1075,6 +1213,65 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-core" +version = "0.62.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57fe7168f7de578d2d8a05b07fd61870d2e73b4020e9f49aa00da8471723497c" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65" + +[[package]] +name = "windows-result" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7084dcc306f89883455a206237404d3eaf961e5bd7e0f312f7c91f57eb44167f" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7218c655a553b0bed4426cf54b20d7ba363ef543b52d515b3e48d7fd55318dda" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-sys" version = "0.48.0" diff --git a/crates/tui/Cargo.toml b/crates/tui/Cargo.toml index 2e8d3b9..8c319f4 100644 --- a/crates/tui/Cargo.toml +++ b/crates/tui/Cargo.toml @@ -10,7 +10,7 @@ crossterm = "0.27" tokio.workspace = true serde.workspace = true anyhow.workspace = true - +chrono = { version = "0.4", features = ["serde"] } core = { path = "../core" } storage = { path = "../storage" } index = { path = "../index" } diff --git a/crates/tui/src/app.rs b/crates/tui/src/app.rs deleted file mode 100644 index db4c97a..0000000 --- a/crates/tui/src/app.rs +++ /dev/null @@ -1,99 +0,0 @@ -use crossterm::event::{Event, KeyCode, KeyEvent}; -use std::io; - -use crate::ui::{db, vector_operations}; - -#[derive(Debug, Default, Clone, PartialEq)] -pub enum AppState { - #[default] - Dashboard, - Database, - VectorOperations, -} - -#[derive(Debug, Default)] -pub struct App { - pub should_quit: bool, - pub state: AppState, - pub db_selected: usize, - pub vector_selected: usize, -} - -impl App { - pub fn new() -> Self { - Self::default() - } - - pub fn handle_event(&mut self, event: Event) -> io::Result<()> { - if let Event::Key(key) = event { - self.handle_key_event(key)?; - } - Ok(()) - } - - fn handle_key_event(&mut self, key: KeyEvent) -> io::Result<()> { - match key.code { - KeyCode::Char('q') | KeyCode::Esc => self.quit(), - KeyCode::Right | KeyCode::Enter => self.next_page(), - KeyCode::Left => self.previous_page(), - KeyCode::Up => self.select_previous(), - KeyCode::Down => self.select_next(), - _ => {} - } - Ok(()) - } - - fn quit(&mut self) { - self.should_quit = true; - } - - fn next_page(&mut self) { - self.state = match self.state { - AppState::Dashboard => AppState::Database, - AppState::Database => AppState::VectorOperations, - AppState::VectorOperations => AppState::VectorOperations, - }; - } - - fn previous_page(&mut self) { - self.state = match self.state { - AppState::Dashboard => AppState::Dashboard, - AppState::Database => AppState::Dashboard, - AppState::VectorOperations => AppState::Database, - }; - } - - fn select_previous(&mut self) { - match self.state { - AppState::Dashboard => {} - AppState::Database => { - self.db_selected = self.db_selected.saturating_sub(1); - } - AppState::VectorOperations => { - self.vector_selected = self.vector_selected.saturating_sub(1); - } - } - } - - fn select_next(&mut self) { - match self.state { - AppState::Dashboard => {} - AppState::Database => { - let max_items = db::get_db_operations_count(); - self.db_selected = (self.db_selected + 1).min(max_items - 1); - } - AppState::VectorOperations => { - let max_items = vector_operations::get_vector_operations_count(); - self.vector_selected = (self.vector_selected + 1).min(max_items - 1); - } - } - } - - pub fn get_selected_operation(&self) -> Option { - match self.state { - AppState::Dashboard => None, - AppState::Database => Some(self.db_selected), - AppState::VectorOperations => Some(self.vector_selected), - } - } -} diff --git a/crates/tui/src/app/database.rs b/crates/tui/src/app/database.rs new file mode 100644 index 0000000..0efc7e9 --- /dev/null +++ b/crates/tui/src/app/database.rs @@ -0,0 +1,103 @@ +use std::io; +use std::path::PathBuf; +use std::sync::Arc; +use storage::{create_storage_engine, StorageEngine, StorageType}; + +pub struct DatabaseManager { + pub current_db_path: Option, + pub storage_engine: Option>, + pub available_databases: Vec<(String, PathBuf)>, + pub selected_database: Option<(String, PathBuf)>, +} + +impl DatabaseManager { + pub fn new() -> Self { + Self { + current_db_path: None, + storage_engine: None, + available_databases: Vec::new(), + selected_database: None, + } + } + + pub fn create_new_database(&mut self, name: String, path: PathBuf) -> io::Result<()> { + match create_storage_engine(StorageType::RocksDb, &path) { + Ok(storage) => { + self.storage_engine = Some(storage); + self.current_db_path = Some(path.clone()); + self.available_databases.push((name.clone(), path.clone())); + self.selected_database = Some((name, path)); + Ok(()) + } + Err(e) => Err(io::Error::new( + io::ErrorKind::Other, + format!("Failed to create database: {:?}", e), + )), + } + } + + pub fn select_database(&mut self, name: String, path: PathBuf) -> io::Result<()> { + if !path.exists() { + return Err(io::Error::new( + io::ErrorKind::NotFound, + "Database path does not exist", + )); + } + + self.selected_database = Some((name, path)); + Ok(()) + } + + pub fn delete_database(&mut self, path: &PathBuf) -> io::Result<()> { + // Close current connection if it's the same database + if let Some(current_path) = &self.current_db_path { + if current_path == path { + self.storage_engine = None; + self.current_db_path = None; + } + } + + // Remove from available databases list + self.available_databases + .retain(|(_, db_path)| db_path != path); + + // Delete the database directory + if path.exists() { + std::fs::remove_dir_all(path)?; + } + + Ok(()) + } + + pub fn load_available_databases(&mut self) -> io::Result<()> { + let db_dir = PathBuf::from("./databases"); + if !db_dir.exists() { + std::fs::create_dir_all(&db_dir)?; + } + + self.available_databases.clear(); + + for entry in std::fs::read_dir(&db_dir)? { + let entry = entry?; + let path = entry.path(); + if path.is_dir() { + if let Some(name) = path.file_name() { + self.available_databases + .push((name.to_string_lossy().to_string(), path)); + } + } + } + + Ok(()) + } + + pub fn get_selected_database_name(&self) -> Option<&str> { + self.selected_database + .as_ref() + .map(|(name, _)| name.as_str()) + } + + pub fn is_database_selected(&self) -> bool { + self.selected_database.is_some() + } +} diff --git a/crates/tui/src/app/events.rs b/crates/tui/src/app/events.rs new file mode 100644 index 0000000..4e50682 --- /dev/null +++ b/crates/tui/src/app/events.rs @@ -0,0 +1,177 @@ +use super::{App, AppState, ModalType}; +use crossterm::event::{Event, KeyCode, KeyEvent}; +use std::io; +use std::path::PathBuf; + +pub fn handle_event(app: &mut App, event: Event) -> io::Result<()> { + if let Event::Key(key) = event { + handle_key_event(app, key)?; + } + Ok(()) +} + +fn handle_key_event(app: &mut App, key: KeyEvent) -> io::Result<()> { + // Handle modal input first + if app.modal.is_showing() && app.modal.input_mode() { + return handle_modal_input(app, key); + } + + // Handle modal navigation + if app.modal.is_showing() { + return handle_modal_navigation(app, key); + } + + // Regular navigation + match app.state { + AppState::Database => handle_database_keys(app, key)?, + _ => handle_general_keys(app, key), + } + + Ok(()) +} + +fn handle_modal_input(app: &mut App, key: KeyEvent) -> io::Result<()> { + match key.code { + KeyCode::Enter => { + execute_modal_action(app)?; + app.modal.close(); + } + KeyCode::Esc => { + app.modal.close(); + } + KeyCode::Char(c) => { + app.modal.add_char(c); + } + KeyCode::Backspace => { + app.modal.remove_char(); + } + _ => {} + } + Ok(()) +} + +fn handle_modal_navigation(app: &mut App, key: KeyEvent) -> io::Result<()> { + match key.code { + KeyCode::Enter => { + if matches!( + app.modal.modal_type(), + Some(ModalType::DatabaseList) | Some(ModalType::DeleteDatabase) + ) { + execute_selected_db_operation(app)?; + } else { + app.modal.enable_input_mode(); + } + } + KeyCode::Esc => { + app.modal.close(); + } + KeyCode::Up => { + if matches!( + app.modal.modal_type(), + Some(ModalType::DatabaseList) | Some(ModalType::DeleteDatabase) + ) { + app.select_previous(); + } + } + KeyCode::Down => { + if matches!( + app.modal.modal_type(), + Some(ModalType::DatabaseList) | Some(ModalType::DeleteDatabase) + ) { + app.select_next(); + } + } + _ => {} + } + Ok(()) +} + +fn handle_database_keys(app: &mut App, key: KeyEvent) -> io::Result<()> { + match key.code { + KeyCode::Enter => open_database_modal(app)?, + KeyCode::Up => app.select_previous(), + KeyCode::Down => app.select_next(), + KeyCode::Left => app.previous_page(), + KeyCode::Right => app.next_page(), + KeyCode::Char('q') | KeyCode::Esc => app.quit(), + _ => {} + } + Ok(()) +} + +fn handle_general_keys(app: &mut App, key: KeyEvent) { + match key.code { + KeyCode::Char('q') | KeyCode::Esc => app.quit(), + KeyCode::Right | KeyCode::Enter => app.next_page(), + KeyCode::Left => app.previous_page(), + KeyCode::Up => app.select_previous(), + KeyCode::Down => app.select_next(), + _ => {} + } +} + +fn open_database_modal(app: &mut App) -> io::Result<()> { + match app.db_selected { + 0 => app.modal.show_create_database(), + 1 => { + app.database.load_available_databases()?; + app.modal.show_database_list(); + } + 2 => { + app.database.load_available_databases()?; + app.modal.show_delete_database(); + } + _ => {} + } + Ok(()) +} + +fn execute_modal_action(app: &mut App) -> io::Result<()> { + match app.modal.modal_type() { + Some(ModalType::CreateDatabase) => { + let input = app.modal.get_input_value(); + if !input.is_empty() { + let name = input; + let path = PathBuf::from(format!("./databases/{}", name)); + app.database.create_new_database(name, path)?; + } + } + Some(ModalType::DatabaseList) => { + let selected_index = app.modal.selected_index(); + if let Some((name, path)) = app.database.available_databases.get(selected_index) { + app.database.select_database(name.clone(), path.clone())?; + } + } + Some(ModalType::DeleteDatabase) => { + let selected_index = app.modal.selected_index(); + if let Some((_, path)) = app.database.available_databases.get(selected_index) { + let path = path.clone(); + app.database.delete_database(&path)?; + } + } + _ => {} + } + Ok(()) +} + +fn execute_selected_db_operation(app: &mut App) -> io::Result<()> { + match app.modal.modal_type() { + Some(ModalType::DatabaseList) => { + let selected_index = app.modal.selected_index(); + if let Some((name, path)) = app.database.available_databases.get(selected_index) { + app.database.select_database(name.clone(), path.clone())?; + app.modal.close(); + } + } + Some(ModalType::DeleteDatabase) => { + let selected_index = app.modal.selected_index(); + if let Some((_, path)) = app.database.available_databases.get(selected_index) { + let path = path.clone(); + app.database.delete_database(&path)?; + app.modal.close(); + } + } + _ => {} + } + Ok(()) +} diff --git a/crates/tui/src/app/mod.rs b/crates/tui/src/app/mod.rs new file mode 100644 index 0000000..b189529 --- /dev/null +++ b/crates/tui/src/app/mod.rs @@ -0,0 +1,121 @@ +mod database; +mod events; +mod modal; +mod state; + +pub use state::*; + +use crate::ui::{db, vector_operations}; +use crossterm::event::Event; +use std::io; +use std::path::PathBuf; + +pub struct App { + pub should_quit: bool, + pub state: AppState, + pub db_selected: usize, + pub vector_selected: usize, + pub database: database::DatabaseManager, + pub modal: modal::ModalManager, +} + +impl Default for App { + fn default() -> Self { + Self::new() + } +} + +impl App { + pub fn new() -> Self { + Self { + should_quit: false, + state: AppState::default(), + db_selected: 0, + vector_selected: 0, + database: database::DatabaseManager::new(), + modal: modal::ModalManager::new(), + } + } + + pub fn handle_event(&mut self, event: Event) -> io::Result<()> { + events::handle_event(self, event) + } + + pub fn quit(&mut self) { + self.should_quit = true; + } + + pub fn next_page(&mut self) { + self.state = match self.state { + AppState::Dashboard => { + let _ = self.database.load_available_databases(); + AppState::Database + } + AppState::Database => AppState::VectorOperations, + AppState::VectorOperations => AppState::VectorOperations, + }; + } + + pub fn previous_page(&mut self) { + self.state = match self.state { + AppState::Dashboard => AppState::Dashboard, + AppState::Database => AppState::Dashboard, + AppState::VectorOperations => AppState::Database, + }; + } + + pub fn select_previous(&mut self) { + match self.state { + AppState::Dashboard => {} + AppState::Database => { + if self.modal.is_showing() { + self.modal.select_previous(); + } else { + self.db_selected = self.db_selected.saturating_sub(1); + } + } + AppState::VectorOperations => { + self.vector_selected = self.vector_selected.saturating_sub(1); + } + } + } + + pub fn select_next(&mut self) { + match self.state { + AppState::Dashboard => {} + AppState::Database => { + if self.modal.is_showing() { + let max_items = self.database.available_databases.len(); + self.modal.select_next(max_items); + } else { + let max_items = db::get_db_operations_count(); + self.db_selected = (self.db_selected + 1).min(max_items - 1); + } + } + AppState::VectorOperations => { + let max_items = vector_operations::get_vector_operations_count(); + self.vector_selected = (self.vector_selected + 1).min(max_items - 1); + } + } + } + + pub fn show_modal(&self) -> bool { + self.modal.is_showing() + } + + pub fn modal_type(&self) -> Option<&ModalType> { + self.modal.modal_type() + } + + pub fn input_buffer(&self) -> &str { + self.modal.input_buffer() + } + + pub fn input_mode(&self) -> bool { + self.modal.input_mode() + } + + pub fn available_databases(&self) -> &[(String, PathBuf)] { + &self.database.available_databases + } +} diff --git a/crates/tui/src/app/modal.rs b/crates/tui/src/app/modal.rs new file mode 100644 index 0000000..9807c0d --- /dev/null +++ b/crates/tui/src/app/modal.rs @@ -0,0 +1,97 @@ +use super::state::ModalType; + +pub struct ModalManager { + show_modal: bool, + modal_type: Option, + input_buffer: String, + input_mode: bool, + selected_index: usize, +} + +impl ModalManager { + pub fn new() -> Self { + Self { + show_modal: false, + modal_type: None, + input_buffer: String::new(), + input_mode: false, + selected_index: 0, + } + } + + pub fn is_showing(&self) -> bool { + self.show_modal + } + + pub fn modal_type(&self) -> Option<&ModalType> { + self.modal_type.as_ref() + } + + pub fn input_buffer(&self) -> &str { + &self.input_buffer + } + + pub fn input_mode(&self) -> bool { + self.input_mode + } + + pub fn selected_index(&self) -> usize { + self.selected_index + } + + pub fn show_create_database(&mut self) { + self.show_modal = true; + self.modal_type = Some(ModalType::CreateDatabase); + self.input_buffer.clear(); + self.input_mode = true; + self.selected_index = 0; + } + + pub fn show_database_list(&mut self) { + self.show_modal = true; + self.modal_type = Some(ModalType::DatabaseList); + self.input_mode = false; + self.selected_index = 0; + } + + pub fn show_delete_database(&mut self) { + self.show_modal = true; + self.modal_type = Some(ModalType::DeleteDatabase); + self.input_mode = false; + self.selected_index = 0; + } + + pub fn close(&mut self) { + self.show_modal = false; + self.modal_type = None; + self.input_buffer.clear(); + self.input_mode = false; + self.selected_index = 0; + } + + pub fn enable_input_mode(&mut self) { + self.input_mode = true; + } + + pub fn add_char(&mut self, c: char) { + self.input_buffer.push(c); + } + + pub fn remove_char(&mut self) { + self.input_buffer.pop(); + } + + pub fn select_previous(&mut self) { + self.selected_index = self.selected_index.saturating_sub(1); + } + + pub fn select_next(&mut self, max_items: usize) { + if max_items > 0 { + self.selected_index = (self.selected_index + 1).min(max_items - 1); + } + } + + pub fn get_input_value(&self) -> String { + self.input_buffer.clone() + } +} diff --git a/crates/tui/src/app/state.rs b/crates/tui/src/app/state.rs new file mode 100644 index 0000000..f7711fb --- /dev/null +++ b/crates/tui/src/app/state.rs @@ -0,0 +1,14 @@ +#[derive(Debug, Default, Clone, PartialEq)] +pub enum AppState { + #[default] + Dashboard, + Database, + VectorOperations, +} + +#[derive(Debug, Clone, PartialEq)] +pub enum ModalType { + CreateDatabase, + DeleteDatabase, + DatabaseList, +} diff --git a/crates/tui/src/main.rs b/crates/tui/src/main.rs index 26f21ca..3617be4 100644 --- a/crates/tui/src/main.rs +++ b/crates/tui/src/main.rs @@ -1,6 +1,7 @@ mod app; mod ui; -use app::{App, AppState}; + +use app::App; use color_eyre::Result; use crossterm::{ event::{self, DisableMouseCapture, EnableMouseCapture}, @@ -10,7 +11,8 @@ use crossterm::{ use ratatui::{backend::CrosstermBackend, Terminal}; use std::io; use ui::{ - dashboard::render_dashboard, db::render_database, vector_operations::render_vector_operations, + dashboard::render_dashboard, db::render_database, modal::render_modal, + vector_operations::render_vector_operations, }; const POLL_DURATION: std::time::Duration = std::time::Duration::from_millis(50); @@ -54,10 +56,16 @@ fn restore_terminal(terminal: &mut Terminal>) -> Re fn run_app(terminal: &mut Terminal>, app: &mut App) -> io::Result<()> { loop { - terminal.draw(|f| match app.state { - AppState::Dashboard => render_dashboard(f, app), - AppState::Database => render_database(f, app), - AppState::VectorOperations => render_vector_operations(f, app), + terminal.draw(|f| { + match app.state { + app::AppState::Dashboard => render_dashboard(f, app), + app::AppState::Database => render_database(f, app), + app::AppState::VectorOperations => render_vector_operations(f, app), + } + + if app.show_modal() { + render_modal(f, app); + } })?; if event::poll(POLL_DURATION)? { diff --git a/crates/tui/src/ui/db.rs b/crates/tui/src/ui/db.rs index 36b8cdb..616e7a0 100644 --- a/crates/tui/src/ui/db.rs +++ b/crates/tui/src/ui/db.rs @@ -8,11 +8,7 @@ use ratatui::{ use super::components::{common_instructions, create_instructions, OperationsList, PageTitle}; use crate::app::App; -const DB_OPERATIONS: &[&str] = &[ - "Create New Database", - "Open Existing Database", - "Delete Database", -]; +const DB_OPERATIONS: &[&str] = &["Create New Database", "Select Database", "Delete Database"]; fn get_db_items() -> Vec> { DB_OPERATIONS.iter().map(|&op| ListItem::new(op)).collect() diff --git a/crates/tui/src/ui/mod.rs b/crates/tui/src/ui/mod.rs index be4731f..c29510e 100644 --- a/crates/tui/src/ui/mod.rs +++ b/crates/tui/src/ui/mod.rs @@ -1,4 +1,5 @@ pub mod components; pub mod dashboard; pub mod db; +pub mod modal; pub mod vector_operations; diff --git a/crates/tui/src/ui/modal.rs b/crates/tui/src/ui/modal.rs new file mode 100644 index 0000000..ba51a85 --- /dev/null +++ b/crates/tui/src/ui/modal.rs @@ -0,0 +1,112 @@ +use crate::app::{App, ModalType}; +use ratatui::{ + prelude::*, + widgets::{Block, Borders, Clear, List, ListItem, Paragraph}, +}; + +pub fn render_modal(f: &mut Frame, app: &App) { + if !app.show_modal() { + return; + } + + let size = f.size(); + let popup_area = centered_rect(60, 60, size); + + // Clear the area + f.render_widget(Clear, popup_area); + + match app.modal_type() { + Some(ModalType::CreateDatabase) => render_create_database_modal(f, app, popup_area), + Some(ModalType::DatabaseList) => render_database_list_modal(f, app, popup_area), + Some(ModalType::DeleteDatabase) => render_delete_database_modal(f, app, popup_area), + _ => {} + } +} + +fn render_create_database_modal(f: &mut Frame, app: &App, area: Rect) { + let block = Block::default() + .title("Create New Database") + .borders(Borders::ALL); + + let input = Paragraph::new(app.input_buffer()) + .block(block) + .wrap(ratatui::widgets::Wrap { trim: true }); + + f.render_widget(input, area); + + if app.input_mode() { + f.set_cursor(area.x + app.input_buffer().len() as u16 + 1, area.y + 1); + } +} + +fn render_database_list_modal(f: &mut Frame, app: &App, area: Rect) { + let items: Vec = app + .available_databases() + .iter() + .enumerate() + .map(|(i, (name, _))| { + let style = if i == app.modal.selected_index() { + Style::default().bg(Color::Yellow).fg(Color::Black) + } else { + Style::default() + }; + ListItem::new(name.as_str()).style(style) + }) + .collect(); + + let list = List::new(items) + .block( + Block::default() + .title("Select Database") + .borders(Borders::ALL), + ) + .highlight_style(Style::default().bg(Color::Yellow).fg(Color::Black)); + + f.render_widget(list, area); +} + +fn render_delete_database_modal(f: &mut Frame, app: &App, area: Rect) { + let items: Vec = app + .available_databases() + .iter() + .enumerate() + .map(|(i, (name, _))| { + let style = if i == app.modal.selected_index() { + Style::default().bg(Color::Yellow).fg(Color::Black) + } else { + Style::default() + }; + ListItem::new(name.as_str()).style(style) + }) + .collect(); + + let list = List::new(items) + .block( + Block::default() + .title("Delete Database") + .borders(Borders::ALL), + ) + .highlight_style(Style::default().bg(Color::Yellow).fg(Color::Black)); + + f.render_widget(list, area); +} + +fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect { + let popup_layout = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Percentage((100 - percent_y) / 2), + Constraint::Percentage(percent_y), + Constraint::Percentage((100 - percent_y) / 2), + ]) + .split(r); + + Layout::default() + .direction(Direction::Horizontal) + .constraints([ + Constraint::Percentage((100 - percent_x) / 2), + Constraint::Percentage(percent_x), + Constraint::Percentage((100 - percent_x) / 2), + ]) + .split(popup_layout[1])[1] +} diff --git a/crates/tui/src/ui/vector_operations.rs b/crates/tui/src/ui/vector_operations.rs index 1c6a976..28dc052 100644 --- a/crates/tui/src/ui/vector_operations.rs +++ b/crates/tui/src/ui/vector_operations.rs @@ -1,7 +1,8 @@ use ratatui::{ layout::{Constraint, Direction, Layout}, - style::Color, - widgets::ListItem, + style::{Color, Style}, + text::{Line, Span}, + widgets::{Block, Borders, ListItem, Paragraph}, Frame, }; @@ -28,6 +29,7 @@ pub fn render_vector_operations(f: &mut Frame, app: &App) { .direction(Direction::Vertical) .constraints([ Constraint::Length(3), + Constraint::Length(3), // Add space for database info Constraint::Min(0), Constraint::Length(3), ]) @@ -38,16 +40,44 @@ pub fn render_vector_operations(f: &mut Frame, app: &App) { color: Color::Magenta, }; + // Database info section + let db_info = if let Some(db_name) = app.database.get_selected_database_name() { + Paragraph::new(vec![Line::from(vec![ + Span::styled("Selected Database: ", Style::default().fg(Color::Gray)), + Span::styled(db_name, Style::default().fg(Color::Green)), + ])]) + .block( + Block::default() + .borders(Borders::ALL) + .title("Database Info") + .border_style(Style::default().fg(Color::Gray)), + ) + } else { + Paragraph::new("No database selected. Please select a database first.") + .style(Style::default().fg(Color::Red)) + .block( + Block::default() + .borders(Borders::ALL) + .title("Database Info") + .border_style(Style::default().fg(Color::Red)), + ) + }; + let operations_list = OperationsList { items: get_vector_items(), title: "Available Operations", - color: Color::Magenta, + color: if app.database.is_database_selected() { + Color::Magenta + } else { + Color::DarkGray + }, selected: app.vector_selected, }; f.render_widget(title.render(), chunks[0]); - operations_list.render(f, chunks[1]); - f.render_widget(create_instructions(common_instructions()), chunks[2]); + f.render_widget(db_info, chunks[1]); + operations_list.render(f, chunks[2]); + f.render_widget(create_instructions(common_instructions()), chunks[3]); } pub fn get_vector_operations_count() -> usize { From 76a1b22a379627e9d3662db9ff10e424fe6c8bb9 Mon Sep 17 00:00:00 2001 From: Entity069 Date: Wed, 8 Oct 2025 02:40:20 +0530 Subject: [PATCH 07/25] fix: add checks for database name before creating --- crates/tui/src/app/database.rs | 8 ++++++++ crates/tui/src/app/events.rs | 33 ++++++++++++++++++--------------- crates/tui/src/app/mod.rs | 4 ++++ crates/tui/src/app/modal.rs | 19 +++++++++++++++++++ crates/tui/src/app/state.rs | 1 + crates/tui/src/ui/modal.rs | 26 ++++++++++++++++++++++++++ 6 files changed, 76 insertions(+), 15 deletions(-) diff --git a/crates/tui/src/app/database.rs b/crates/tui/src/app/database.rs index 0efc7e9..b5214f9 100644 --- a/crates/tui/src/app/database.rs +++ b/crates/tui/src/app/database.rs @@ -21,6 +21,14 @@ impl DatabaseManager { } pub fn create_new_database(&mut self, name: String, path: PathBuf) -> io::Result<()> { + // Check if database already exists to avoid name collisions + if path.exists() { + return Err(io::Error::new( + io::ErrorKind::AlreadyExists, + format!("Database '{name}' already exists!"), + )); + } + match create_storage_engine(StorageType::RocksDb, &path) { Ok(storage) => { self.storage_engine = Some(storage); diff --git a/crates/tui/src/app/events.rs b/crates/tui/src/app/events.rs index 4e50682..46f8de4 100644 --- a/crates/tui/src/app/events.rs +++ b/crates/tui/src/app/events.rs @@ -32,10 +32,10 @@ fn handle_key_event(app: &mut App, key: KeyEvent) -> io::Result<()> { fn handle_modal_input(app: &mut App, key: KeyEvent) -> io::Result<()> { match key.code { - KeyCode::Enter => { - execute_modal_action(app)?; - app.modal.close(); - } + KeyCode::Enter => match execute_modal_action(app) { + Ok(()) => app.modal.close(), + Err(err) => app.modal.show_error(err.to_string()), + }, KeyCode::Esc => { app.modal.close(); } @@ -52,16 +52,15 @@ fn handle_modal_input(app: &mut App, key: KeyEvent) -> io::Result<()> { fn handle_modal_navigation(app: &mut App, key: KeyEvent) -> io::Result<()> { match key.code { - KeyCode::Enter => { - if matches!( - app.modal.modal_type(), - Some(ModalType::DatabaseList) | Some(ModalType::DeleteDatabase) - ) { - execute_selected_db_operation(app)?; - } else { - app.modal.enable_input_mode(); + KeyCode::Enter => match app.modal.modal_type() { + Some(ModalType::DatabaseList) | Some(ModalType::DeleteDatabase) => { + if let Err(err) = execute_selected_db_operation(app) { + app.modal.show_error(err.to_string()); + } } - } + Some(ModalType::Error) => app.modal.close(), + _ => app.modal.enable_input_mode(), + }, KeyCode::Esc => { app.modal.close(); } @@ -88,7 +87,11 @@ fn handle_modal_navigation(app: &mut App, key: KeyEvent) -> io::Result<()> { fn handle_database_keys(app: &mut App, key: KeyEvent) -> io::Result<()> { match key.code { - KeyCode::Enter => open_database_modal(app)?, + KeyCode::Enter => { + if let Err(err) = open_database_modal(app) { + app.modal.show_error(err.to_string()); + } + } KeyCode::Up => app.select_previous(), KeyCode::Down => app.select_next(), KeyCode::Left => app.previous_page(), @@ -132,7 +135,7 @@ fn execute_modal_action(app: &mut App) -> io::Result<()> { let input = app.modal.get_input_value(); if !input.is_empty() { let name = input; - let path = PathBuf::from(format!("./databases/{}", name)); + let path = PathBuf::from(format!("./databases/{name}")); app.database.create_new_database(name, path)?; } } diff --git a/crates/tui/src/app/mod.rs b/crates/tui/src/app/mod.rs index b189529..030284d 100644 --- a/crates/tui/src/app/mod.rs +++ b/crates/tui/src/app/mod.rs @@ -118,4 +118,8 @@ impl App { pub fn available_databases(&self) -> &[(String, PathBuf)] { &self.database.available_databases } + + pub fn error_message(&self) -> Option<&str> { + self.modal.error_message() + } } diff --git a/crates/tui/src/app/modal.rs b/crates/tui/src/app/modal.rs index 9807c0d..09b15c3 100644 --- a/crates/tui/src/app/modal.rs +++ b/crates/tui/src/app/modal.rs @@ -6,6 +6,7 @@ pub struct ModalManager { input_buffer: String, input_mode: bool, selected_index: usize, + error_message: Option, } impl ModalManager { @@ -16,6 +17,7 @@ impl ModalManager { input_buffer: String::new(), input_mode: false, selected_index: 0, + error_message: None, } } @@ -45,6 +47,7 @@ impl ModalManager { self.input_buffer.clear(); self.input_mode = true; self.selected_index = 0; + self.error_message = None; } pub fn show_database_list(&mut self) { @@ -52,6 +55,7 @@ impl ModalManager { self.modal_type = Some(ModalType::DatabaseList); self.input_mode = false; self.selected_index = 0; + self.error_message = None; } pub fn show_delete_database(&mut self) { @@ -59,6 +63,16 @@ impl ModalManager { self.modal_type = Some(ModalType::DeleteDatabase); self.input_mode = false; self.selected_index = 0; + self.error_message = None; + } + + pub fn show_error>(&mut self, message: S) { + self.show_modal = true; + self.modal_type = Some(ModalType::Error); + self.input_mode = false; + self.input_buffer.clear(); + self.selected_index = 0; + self.error_message = Some(message.into()); } pub fn close(&mut self) { @@ -67,6 +81,7 @@ impl ModalManager { self.input_buffer.clear(); self.input_mode = false; self.selected_index = 0; + self.error_message = None; } pub fn enable_input_mode(&mut self) { @@ -94,4 +109,8 @@ impl ModalManager { pub fn get_input_value(&self) -> String { self.input_buffer.clone() } + + pub fn error_message(&self) -> Option<&str> { + self.error_message.as_deref() + } } diff --git a/crates/tui/src/app/state.rs b/crates/tui/src/app/state.rs index f7711fb..9ea18c3 100644 --- a/crates/tui/src/app/state.rs +++ b/crates/tui/src/app/state.rs @@ -11,4 +11,5 @@ pub enum ModalType { CreateDatabase, DeleteDatabase, DatabaseList, + Error, } diff --git a/crates/tui/src/ui/modal.rs b/crates/tui/src/ui/modal.rs index ba51a85..50c6145 100644 --- a/crates/tui/src/ui/modal.rs +++ b/crates/tui/src/ui/modal.rs @@ -19,6 +19,7 @@ pub fn render_modal(f: &mut Frame, app: &App) { Some(ModalType::CreateDatabase) => render_create_database_modal(f, app, popup_area), Some(ModalType::DatabaseList) => render_database_list_modal(f, app, popup_area), Some(ModalType::DeleteDatabase) => render_delete_database_modal(f, app, popup_area), + Some(ModalType::Error) => render_error_modal(f, app, popup_area), _ => {} } } @@ -91,6 +92,31 @@ fn render_delete_database_modal(f: &mut Frame, app: &App, area: Rect) { f.render_widget(list, area); } +fn render_error_modal(f: &mut Frame, app: &App, area: Rect) { + let message = app.error_message().unwrap_or("An unknown error occurred!"); + + let content = Paragraph::new(vec![ + Line::from(message.to_string()), + Line::from(""), + Line::from(Span::styled( + "Press Enter or Esc to dismiss", + Style::default().fg(Color::Gray), + )), + ]) + .block( + Block::default() + .title(Span::styled( + "Error", + Style::default().fg(Color::Red).add_modifier(Modifier::BOLD), + )) + .borders(Borders::ALL) + .border_style(Style::default().fg(Color::Red)), + ) + .wrap(ratatui::widgets::Wrap { trim: true }); + + f.render_widget(content, area); +} + fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect { let popup_layout = Layout::default() .direction(Direction::Vertical) From 46d2b31dbb93393a35a47962ca06eff0a86cb672 Mon Sep 17 00:00:00 2001 From: Entity069 Date: Wed, 8 Oct 2025 02:45:56 +0530 Subject: [PATCH 08/25] chore: add get,insert,delete vectors functionality in TUI --- .gitignore | 4 +- crates/tui/src/app/database.rs | 16 ++- crates/tui/src/app/events.rs | 179 ++++++++++++++++++++++++++++++++- crates/tui/src/app/mod.rs | 16 +++ crates/tui/src/app/modal.rs | 100 +++++++++++++++++- crates/tui/src/app/state.rs | 5 + crates/tui/src/ui/modal.rs | 152 +++++++++++++++++++++++++++- 7 files changed, 462 insertions(+), 10 deletions(-) diff --git a/.gitignore b/.gitignore index eede8e6..192444e 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,6 @@ /db .DS_Store /.idea -/testdb \ No newline at end of file +/testdb +.TODO +/databases \ No newline at end of file diff --git a/crates/tui/src/app/database.rs b/crates/tui/src/app/database.rs index b5214f9..309addc 100644 --- a/crates/tui/src/app/database.rs +++ b/crates/tui/src/app/database.rs @@ -51,9 +51,19 @@ impl DatabaseManager { "Database path does not exist", )); } - - self.selected_database = Some((name, path)); - Ok(()) + // Open the selected database + match create_storage_engine(StorageType::RocksDb, &path) { + Ok(storage) => { + self.storage_engine = Some(storage); + self.current_db_path = Some(path.clone()); + self.selected_database = Some((name, path)); + Ok(()) + } + Err(e) => Err(io::Error::new( + io::ErrorKind::Other, + format!("Failed to open database: {:?}", e), + )), + } } pub fn delete_database(&mut self, path: &PathBuf) -> io::Result<()> { diff --git a/crates/tui/src/app/events.rs b/crates/tui/src/app/events.rs index 46f8de4..82b3287 100644 --- a/crates/tui/src/app/events.rs +++ b/crates/tui/src/app/events.rs @@ -1,4 +1,5 @@ use super::{App, AppState, ModalType}; +use core::Payload; use crossterm::event::{Event, KeyCode, KeyEvent}; use std::io; use std::path::PathBuf; @@ -33,12 +34,25 @@ fn handle_key_event(app: &mut App, key: KeyEvent) -> io::Result<()> { fn handle_modal_input(app: &mut App, key: KeyEvent) -> io::Result<()> { match key.code { KeyCode::Enter => match execute_modal_action(app) { - Ok(()) => app.modal.close(), + Ok(()) => { + // keep result modals open until user dismisses them; close others + match app.modal.modal_type() { + Some(ModalType::Success) + | Some(ModalType::Failure) + | Some(ModalType::Error) => { + // leave open + } + _ => app.modal.close(), + } + } Err(err) => app.modal.show_error(err.to_string()), }, KeyCode::Esc => { app.modal.close(); } + KeyCode::Tab => { + app.switch_field(); + } KeyCode::Char(c) => { app.modal.add_char(c); } @@ -105,7 +119,21 @@ fn handle_database_keys(app: &mut App, key: KeyEvent) -> io::Result<()> { fn handle_general_keys(app: &mut App, key: KeyEvent) { match key.code { KeyCode::Char('q') | KeyCode::Esc => app.quit(), - KeyCode::Right | KeyCode::Enter => app.next_page(), + KeyCode::Right => app.next_page(), + KeyCode::Enter => { + // Map options with vector operations page + // TODO: Add listing and semantic search + if matches!(app.state, AppState::VectorOperations) { + match app.vector_selected { + 1 => app.modal.show_get_vector(), + 2 => app.modal.show_insert_vector(), + 3 => app.modal.show_delete_vector(), + _ => {} + } + } else { + app.next_page() + } + } KeyCode::Left => app.previous_page(), KeyCode::Up => app.select_previous(), KeyCode::Down => app.select_next(), @@ -152,11 +180,158 @@ fn execute_modal_action(app: &mut App) -> io::Result<()> { app.database.delete_database(&path)?; } } + Some(ModalType::GetVector) => { + let input = app.modal.get_input_value(); + let vector_id_field = &input.parse::().ok(); + if vector_id_field.is_none() { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "IDs should be an integer!", + )); + } + let id = vector_id_field.unwrap(); + if let Some(storage) = &app.database.storage_engine { + // Invoke get_vector and get_payload function from storage crate to get vector + match storage.get_vector(id).map_err(to_io) { + Ok(vec_opt) => match storage.get_payload(id).map_err(to_io) { + Ok(pay_opt) => { + if vec_opt.is_none() && pay_opt.is_none() { + app.modal + .show_failure(format!("Vector with id={id} not found!")); + } else { + let vec_display = if let Some(v) = vec_opt.as_ref() { + let total = v.len(); + let take = total.min(9); + let elems = v + .iter() + .take(take) + .map(|x| format!("{x:.2}")) + .collect::>() + .join(", "); + if total > take { + format!("[{elems}, ...] ({total} dims)") + } else { + format!("[{elems}] ({total} dims)") + } + } else { + "None".to_string() + }; + + let payload_display = match pay_opt.as_ref() { + Some(p) => format!("{p:?}"), + None => "None".to_string(), + }; + + app.modal.show_success(format!( + "ID: {id}, Payload: {payload_display}, Vector: {vec_display}" + )); + } + } + Err(e) => { + app.modal.show_failure(format!("Storage error: {e}")); + } + }, + Err(e) => { + app.modal.show_failure(format!("Storage error: {e}")); + } + } + } else { + app.modal.show_error("No database selected!"); + } + } + Some(ModalType::InsertVector) => { + // Form fields for insertion: id, vector, payload + let id_text = app.modal.get_input_value(); + let vec_text = app.modal.secondary_input(); + let payload_text = app.modal.tertiary_input(); + + let vector_id_field = id_text.parse::().ok(); + if vector_id_field.is_none() { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "IDs should be an integer!", + )); + } + let id = vector_id_field.unwrap(); + + match parse_vector(vec_text) { + Some(vec) => { + if let Some(storage) = &app.database.storage_engine { + let payload_opt = if payload_text.trim().is_empty() { + None + } else { + Some(Payload {}) + }; + match storage + .insert_point(id, Some(vec), payload_opt) + .map_err(to_io) + { + Ok(()) => app + .modal + .show_success(format!("Inserted vector with id={id}!")), + Err(e) => app.modal.show_failure(format!("Storage error: {e}")), + } + } else { + app.modal.show_error("No database selected!"); + } + } + None => { + app.modal + .show_failure("Vectors should be a list of floats (e.g. [0.1,0.2,...])!"); + } + } + } + Some(ModalType::DeleteVector) => { + let input = app.modal.get_input_value(); + let vector_id_field = &input.parse::().ok(); + if vector_id_field.is_none() { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "IDs should be an integer!", + )); + } + let id = vector_id_field.unwrap(); + if let Some(storage) = &app.database.storage_engine { + match storage.contains_point(id).map_err(to_io) { + Ok(exists) => { + if !exists { + app.modal + .show_failure(format!("Vector with id={id} not found")); + } else { + match storage.delete_point(id).map_err(to_io) { + Ok(()) => app.modal.show_success(format!("Deleted vector id={id}")), + Err(e) => app.modal.show_failure(format!("Storage error: {e}")), + } + } + } + Err(e) => app.modal.show_failure(format!("Storage error: {e}")), + } + } else { + app.modal.show_error("No database selected!"); + } + } _ => {} } Ok(()) } +fn to_io(e: E) -> io::Error { + io::Error::new(io::ErrorKind::Other, format!("{e:?}")) +} + +fn parse_vector(input: &str) -> Option> { + input + .trim() + .strip_prefix('[')? + .strip_suffix(']')? + .split(',') + .map(str::trim) + .filter(|s| !s.is_empty()) + .map(|s| s.parse::()) + .collect::, _>>() + .ok() +} + fn execute_selected_db_operation(app: &mut App) -> io::Result<()> { match app.modal.modal_type() { Some(ModalType::DatabaseList) => { diff --git a/crates/tui/src/app/mod.rs b/crates/tui/src/app/mod.rs index 030284d..ffe286c 100644 --- a/crates/tui/src/app/mod.rs +++ b/crates/tui/src/app/mod.rs @@ -115,6 +115,22 @@ impl App { self.modal.input_mode() } + pub fn secondary_input(&self) -> &str { + self.modal.secondary_input() + } + + pub fn tertiary_input(&self) -> &str { + self.modal.tertiary_input() + } + + pub fn active_field(&self) -> usize { + self.modal.active_field() + } + + pub fn switch_field(&mut self) { + self.modal.switch_field(); + } + pub fn available_databases(&self) -> &[(String, PathBuf)] { &self.database.available_databases } diff --git a/crates/tui/src/app/modal.rs b/crates/tui/src/app/modal.rs index 09b15c3..9f3f58a 100644 --- a/crates/tui/src/app/modal.rs +++ b/crates/tui/src/app/modal.rs @@ -4,6 +4,9 @@ pub struct ModalManager { show_modal: bool, modal_type: Option, input_buffer: String, + secondary_input: String, + tertiary_input: String, + active_field: usize, input_mode: bool, selected_index: usize, error_message: Option, @@ -15,6 +18,9 @@ impl ModalManager { show_modal: false, modal_type: None, input_buffer: String::new(), + secondary_input: String::new(), + tertiary_input: String::new(), + active_field: 0, input_mode: false, selected_index: 0, error_message: None, @@ -33,6 +39,18 @@ impl ModalManager { &self.input_buffer } + pub fn secondary_input(&self) -> &str { + &self.secondary_input + } + + pub fn tertiary_input(&self) -> &str { + &self.tertiary_input + } + + pub fn active_field(&self) -> usize { + self.active_field + } + pub fn input_mode(&self) -> bool { self.input_mode } @@ -50,6 +68,60 @@ impl ModalManager { self.error_message = None; } + pub fn show_get_vector(&mut self) { + self.show_modal = true; + self.modal_type = Some(ModalType::GetVector); + self.input_buffer.clear(); + self.secondary_input.clear(); + self.tertiary_input.clear(); + self.active_field = 0; + self.input_mode = true; + self.selected_index = 0; + self.error_message = None; + } + + pub fn show_insert_vector(&mut self) { + self.show_modal = true; + self.modal_type = Some(ModalType::InsertVector); + self.input_buffer.clear(); + self.secondary_input.clear(); + self.tertiary_input.clear(); + self.input_mode = true; + self.selected_index = 0; + self.error_message = None; + } + + pub fn show_delete_vector(&mut self) { + self.show_modal = true; + self.modal_type = Some(ModalType::DeleteVector); + self.input_buffer.clear(); + self.secondary_input.clear(); + self.tertiary_input.clear(); + self.active_field = 0; + self.input_mode = true; + self.selected_index = 0; + self.error_message = None; + } + pub fn show_success>(&mut self, message: S) { + self.show_modal = true; + self.modal_type = Some(ModalType::Success); + self.input_mode = false; + self.input_buffer.clear(); + self.secondary_input.clear(); + self.tertiary_input.clear(); + self.error_message = Some(message.into()); + } + + pub fn show_failure>(&mut self, message: S) { + self.show_modal = true; + self.modal_type = Some(ModalType::Failure); + self.input_mode = false; + self.input_buffer.clear(); + self.secondary_input.clear(); + self.tertiary_input.clear(); + self.error_message = Some(message.into()); + } + pub fn show_database_list(&mut self) { self.show_modal = true; self.modal_type = Some(ModalType::DatabaseList); @@ -71,6 +143,9 @@ impl ModalManager { self.modal_type = Some(ModalType::Error); self.input_mode = false; self.input_buffer.clear(); + self.secondary_input.clear(); + self.tertiary_input.clear(); + self.active_field = 0; self.selected_index = 0; self.error_message = Some(message.into()); } @@ -79,6 +154,9 @@ impl ModalManager { self.show_modal = false; self.modal_type = None; self.input_buffer.clear(); + self.secondary_input.clear(); + self.tertiary_input.clear(); + self.active_field = 0; self.input_mode = false; self.selected_index = 0; self.error_message = None; @@ -89,11 +167,29 @@ impl ModalManager { } pub fn add_char(&mut self, c: char) { - self.input_buffer.push(c); + match self.active_field { + 0 => self.input_buffer.push(c), + 1 => self.secondary_input.push(c), + _ => self.tertiary_input.push(c), + } } pub fn remove_char(&mut self) { - self.input_buffer.pop(); + match self.active_field { + 0 => { + self.input_buffer.pop(); + } + 1 => { + self.secondary_input.pop(); + } + _ => { + self.tertiary_input.pop(); + } + } + } + + pub fn switch_field(&mut self) { + self.active_field = (self.active_field + 1) % 3; } pub fn select_previous(&mut self) { diff --git a/crates/tui/src/app/state.rs b/crates/tui/src/app/state.rs index 9ea18c3..8f399a4 100644 --- a/crates/tui/src/app/state.rs +++ b/crates/tui/src/app/state.rs @@ -12,4 +12,9 @@ pub enum ModalType { DeleteDatabase, DatabaseList, Error, + Success, + Failure, + GetVector, + InsertVector, + DeleteVector, } diff --git a/crates/tui/src/ui/modal.rs b/crates/tui/src/ui/modal.rs index 50c6145..737fe35 100644 --- a/crates/tui/src/ui/modal.rs +++ b/crates/tui/src/ui/modal.rs @@ -20,6 +20,11 @@ pub fn render_modal(f: &mut Frame, app: &App) { Some(ModalType::DatabaseList) => render_database_list_modal(f, app, popup_area), Some(ModalType::DeleteDatabase) => render_delete_database_modal(f, app, popup_area), Some(ModalType::Error) => render_error_modal(f, app, popup_area), + Some(ModalType::GetVector) => render_get_vector_modal(f, app, popup_area), + Some(ModalType::InsertVector) => render_insert_vector_modal(f, app, popup_area), + Some(ModalType::DeleteVector) => render_delete_vector_modal(f, app, popup_area), + Some(ModalType::Success) => render_success_modal(f, app, popup_area), + Some(ModalType::Failure) => render_failure_modal(f, app, popup_area), _ => {} } } @@ -40,6 +45,100 @@ fn render_create_database_modal(f: &mut Frame, app: &App, area: Rect) { } } +fn render_get_vector_modal(f: &mut Frame, app: &App, area: Rect) { + let block = Block::default() + .title("Get Vector by ID") + .borders(Borders::ALL); + + let lines = vec![ + Line::from(Span::raw("ID:")), + Line::from(Span::raw(app.input_buffer().to_string())), + Line::from(""), + Line::from(Span::styled( + "Press Tab to switch field (if any). Press Enter to submit.", + Style::default().fg(Color::Gray), + )), + ]; + + let input = Paragraph::new(lines) + .block(block) + .wrap(ratatui::widgets::Wrap { trim: true }); + f.render_widget(input, area); + + if app.input_mode() { + // content line is the second line of the paragraph => area.y + 2 + f.set_cursor(area.x + 1 + app.input_buffer().len() as u16, area.y + 2); + } +} + +fn render_insert_vector_modal(f: &mut Frame, app: &App, area: Rect) { + let block = Block::default() + .title("Insert Vector") + .borders(Borders::ALL); + + let lines = vec![ + Line::from(Span::raw("ID (int):")), + Line::from(Span::raw(app.input_buffer().to_string())), + Line::from(Span::raw("")), + Line::from(Span::raw("Vector (e.g. [0.1,0.2,...]):")), + Line::from(Span::raw(app.secondary_input().to_string())), + Line::from(Span::raw("")), + Line::from(Span::raw("Payload (optional):")), + Line::from(Span::raw(app.tertiary_input().to_string())), + Line::from(""), + Line::from(Span::styled( + "Use Tab to switch fields. Enter to submit.", + Style::default().fg(Color::Gray), + )), + ]; + + let input = Paragraph::new(lines) + .block(block) + .wrap(ratatui::widgets::Wrap { trim: true }); + f.render_widget(input, area); + + if app.input_mode() { + // Here active_field: 0 -> ID, 1 -> Vector, 2 -> Payload + // Paragraph layout: label (y+1), content (y+2), blank (y+3), next label (y+4), next content (y+5), ... + if app.active_field() == 0 { + // ID content is at line index 1 -> y + 2 + f.set_cursor(area.x + 1 + app.input_buffer().len() as u16, area.y + 2); + } else if app.active_field() == 1 { + // Vector content is at line index 4 -> y + 5 + f.set_cursor(area.x + 1 + app.secondary_input().len() as u16, area.y + 5); + } else { + // Payload content is at line index 7 -> y + 8 + f.set_cursor(area.x + 1 + app.tertiary_input().len() as u16, area.y + 8); + } + } +} + +fn render_delete_vector_modal(f: &mut Frame, app: &App, area: Rect) { + let block = Block::default() + .title("Delete Vector by ID") + .borders(Borders::ALL); + + let lines = vec![ + Line::from(Span::raw("ID:")), + Line::from(Span::raw(app.input_buffer().to_string())), + Line::from(""), + Line::from(Span::styled( + "Press Enter to delete", + Style::default().fg(Color::Gray), + )), + ]; + + let input = Paragraph::new(lines) + .block(block) + .wrap(ratatui::widgets::Wrap { trim: true }); + f.render_widget(input, area); + + if app.input_mode() { + // content line is the second line of the paragraph => area.y + 2 + f.set_cursor(area.x + 1 + app.input_buffer().len() as u16, area.y + 2); + } +} + fn render_database_list_modal(f: &mut Frame, app: &App, area: Rect) { let items: Vec = app .available_databases() @@ -94,12 +193,11 @@ fn render_delete_database_modal(f: &mut Frame, app: &App, area: Rect) { fn render_error_modal(f: &mut Frame, app: &App, area: Rect) { let message = app.error_message().unwrap_or("An unknown error occurred!"); - let content = Paragraph::new(vec![ Line::from(message.to_string()), Line::from(""), Line::from(Span::styled( - "Press Enter or Esc to dismiss", + "Press Esc to dismiss", Style::default().fg(Color::Gray), )), ]) @@ -117,6 +215,56 @@ fn render_error_modal(f: &mut Frame, app: &App, area: Rect) { f.render_widget(content, area); } +fn render_success_modal(f: &mut Frame, app: &App, area: Rect) { + let message = app.error_message().unwrap_or("Success"); + let content = Paragraph::new(vec![ + Line::from(message.to_string()), + Line::from(""), + Line::from(Span::styled( + "Press Esc to dismiss", + Style::default().fg(Color::Gray), + )), + ]) + .block( + Block::default() + .title(Span::styled( + "Success", + Style::default() + .fg(Color::Green) + .add_modifier(Modifier::BOLD), + )) + .borders(Borders::ALL) + .border_style(Style::default().fg(Color::Green)), + ) + .wrap(ratatui::widgets::Wrap { trim: true }); + + f.render_widget(content, area); +} + +fn render_failure_modal(f: &mut Frame, app: &App, area: Rect) { + let message = app.error_message().unwrap_or("Failure"); + let content = Paragraph::new(vec![ + Line::from(message.to_string()), + Line::from(""), + Line::from(Span::styled( + "Press Esc to dismiss", + Style::default().fg(Color::Gray), + )), + ]) + .block( + Block::default() + .title(Span::styled( + "Failure", + Style::default().fg(Color::Red).add_modifier(Modifier::BOLD), + )) + .borders(Borders::ALL) + .border_style(Style::default().fg(Color::Red)), + ) + .wrap(ratatui::widgets::Wrap { trim: true }); + + f.render_widget(content, area); +} + fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect { let popup_layout = Layout::default() .direction(Direction::Vertical) From 84b2426c7c9876ac33a8243123d9f0aaf93bca4f Mon Sep 17 00:00:00 2001 From: Entity069 Date: Wed, 8 Oct 2025 22:19:45 +0530 Subject: [PATCH 09/25] chore: add listing vector in storage --- crates/storage/src/in_memory.rs | 7 +++++++ crates/storage/src/lib.rs | 5 +++++ crates/storage/src/rocks_db.rs | 35 +++++++++++++++++++++++++++++++++ 3 files changed, 47 insertions(+) diff --git a/crates/storage/src/in_memory.rs b/crates/storage/src/in_memory.rs index ea078ab..738f459 100644 --- a/crates/storage/src/in_memory.rs +++ b/crates/storage/src/in_memory.rs @@ -38,4 +38,11 @@ impl StorageEngine for MemoryStorage { fn get_vector(&self, _id: PointId) -> Result, DbError> { Ok(None) } + fn list_vectors( + &self, + _offset: PointId, + _limit: usize, + ) -> Result, PointId)>, DbError> { + Ok(None) + } } diff --git a/crates/storage/src/lib.rs b/crates/storage/src/lib.rs index 07c1107..c781115 100644 --- a/crates/storage/src/lib.rs +++ b/crates/storage/src/lib.rs @@ -15,6 +15,11 @@ pub trait StorageEngine { fn get_payload(&self, id: PointId) -> Result, DbError>; fn delete_point(&self, id: PointId) -> Result<(), DbError>; fn contains_point(&self, id: PointId) -> Result; + fn list_vectors( + &self, + offset: PointId, + limit: usize, + ) -> Result, PointId)>, DbError>; } pub mod in_memory; diff --git a/crates/storage/src/rocks_db.rs b/crates/storage/src/rocks_db.rs index 73905c1..765b6f6 100644 --- a/crates/storage/src/rocks_db.rs +++ b/crates/storage/src/rocks_db.rs @@ -121,6 +121,41 @@ impl StorageEngine for RocksDbStorage { Ok(value.vector) } + + fn list_vectors( + &self, + offset: PointId, + limit: usize, + ) -> Result, PointId)>, DbError> { + if limit < 1 { + return Ok(None); + } + + let mut result = Vec::with_capacity(limit); + let iter = self.db.iterator(rocksdb::IteratorMode::From( + offset.to_string().as_bytes(), + rocksdb::Direction::Forward, + )); + let mut last_id = offset; + + for item in iter { + let (_, v) = item.map_err(|e| DbError::StorageError(e.into_string()))?; + let point: Point = deserialize(&v).map_err(|_| DbError::DeserializationError)?; + + if point.id < offset { + continue; + } + + if let Some(vec) = point.vector { + last_id = point.id; + result.push((point.id, vec)); + if result.len() == limit { + break; + } + } + } + Ok(Some((result, last_id + 1))) + } } #[cfg(test)] From f422ed343a498ac4e244b84d74876fa655510a2b Mon Sep 17 00:00:00 2001 From: Entity069 Date: Thu, 9 Oct 2025 12:05:23 +0530 Subject: [PATCH 10/25] chore: add listing vector in TUI --- crates/tui/src/app/events.rs | 335 ++++++++++++++++++++----- crates/tui/src/app/mod.rs | 20 ++ crates/tui/src/app/modal.rs | 85 +++++++ crates/tui/src/app/state.rs | 46 ++++ crates/tui/src/ui/db.rs | 9 +- crates/tui/src/ui/modal.rs | 271 ++++++++++++++------ crates/tui/src/ui/vector_operations.rs | 9 +- 7 files changed, 629 insertions(+), 146 deletions(-) diff --git a/crates/tui/src/app/events.rs b/crates/tui/src/app/events.rs index 82b3287..64d47e2 100644 --- a/crates/tui/src/app/events.rs +++ b/crates/tui/src/app/events.rs @@ -1,9 +1,12 @@ -use super::{App, AppState, ModalType}; +use super::{App, AppState, ModalType, VectorListItem}; use core::Payload; use crossterm::event::{Event, KeyCode, KeyEvent}; use std::io; use std::path::PathBuf; +// Set how many vectors to fetch per function call in list_vectors +const VECTOR_LIST_LIMIT: usize = 50; + pub fn handle_event(app: &mut App, event: Event) -> io::Result<()> { if let Event::Key(key) = event { handle_key_event(app, key)?; @@ -39,7 +42,8 @@ fn handle_modal_input(app: &mut App, key: KeyEvent) -> io::Result<()> { match app.modal.modal_type() { Some(ModalType::Success) | Some(ModalType::Failure) - | Some(ModalType::Error) => { + | Some(ModalType::Error) + | Some(ModalType::VectorDetails) => { // leave open } _ => app.modal.close(), @@ -72,26 +76,97 @@ fn handle_modal_navigation(app: &mut App, key: KeyEvent) -> io::Result<()> { app.modal.show_error(err.to_string()); } } + Some(ModalType::ConfirmDeleteDatabase) => { + if let Err(err) = execute_selected_db_operation(app) { + app.modal.show_error(err.to_string()); + } + } + Some(ModalType::ListVectors) => { + open_selected_vector_from_list(app)?; + } Some(ModalType::Error) => app.modal.close(), + Some(ModalType::VectorDetails) => { + close_vector_detail_modal(app); + } _ => app.modal.enable_input_mode(), }, - KeyCode::Esc => { - app.modal.close(); - } - KeyCode::Up => { + KeyCode::Esc => match app.modal.modal_type() { + Some(ModalType::Success) + | Some(ModalType::Failure) + | Some(ModalType::VectorDetails) + if app.vector_list_post_restore && !app.vector_list_items.is_empty() => + { + reopen_vector_list_modal(app); + } + Some(ModalType::ConfirmDeleteDatabase) => { + app.vector_list_post_restore = false; + restore_delete_database_modal(app); + } + Some(ModalType::ListVectors) => { + app.vector_list_post_restore = false; + app.vector_list_next_offset = None; + app.modal.close(); + } + Some(ModalType::VectorDetails) => { + close_vector_detail_modal(app); + } + _ => { + app.vector_list_post_restore = false; + app.modal.close(); + } + }, + KeyCode::Up => match app.modal.modal_type() { + Some(ModalType::DatabaseList) | Some(ModalType::DeleteDatabase) => { + app.select_previous(); + } + Some(ModalType::ListVectors) => { + if app.modal.selected_index() > 0 { + app.modal.select_previous(); + app.vector_list_selected_index = app.modal.selected_index(); + } + } + Some(ModalType::ConfirmDeleteDatabase) => { + app.modal.set_selected_index(0, 2); + } + _ => {} + }, + KeyCode::Down => match app.modal.modal_type() { + Some(ModalType::DatabaseList) | Some(ModalType::DeleteDatabase) => { + app.select_next(); + } + Some(ModalType::ListVectors) => { + let current_len = app.vector_list_items.len(); + if current_len == 0 { + return Ok(()); + } + + let mut max_items = current_len; + if app.modal.selected_index() + 1 >= current_len && fetch_next_vector_page(app)? { + max_items = app.vector_list_items.len(); + } + + app.modal.select_next(max_items); + app.vector_list_selected_index = app.modal.selected_index(); + } + Some(ModalType::ConfirmDeleteDatabase) => { + app.modal.set_selected_index(1, 2); + } + _ => {} + }, + KeyCode::Left => { if matches!( app.modal.modal_type(), - Some(ModalType::DatabaseList) | Some(ModalType::DeleteDatabase) + Some(ModalType::ConfirmDeleteDatabase) ) { - app.select_previous(); + app.modal.set_selected_index(0, 2); } } - KeyCode::Down => { + KeyCode::Right => { if matches!( app.modal.modal_type(), - Some(ModalType::DatabaseList) | Some(ModalType::DeleteDatabase) + Some(ModalType::ConfirmDeleteDatabase) ) { - app.select_next(); + app.modal.set_selected_index(1, 2); } } _ => {} @@ -122,9 +197,14 @@ fn handle_general_keys(app: &mut App, key: KeyEvent) { KeyCode::Right => app.next_page(), KeyCode::Enter => { // Map options with vector operations page - // TODO: Add listing and semantic search + // TODO: Add semantic search if matches!(app.state, AppState::VectorOperations) { match app.vector_selected { + 0 => { + if let Err(err) = initialize_vector_listing(app) { + app.modal.show_error(err.to_string()); + } + } 1 => app.modal.show_get_vector(), 2 => app.modal.show_insert_vector(), 3 => app.modal.show_delete_vector(), @@ -164,7 +244,8 @@ fn execute_modal_action(app: &mut App) -> io::Result<()> { if !input.is_empty() { let name = input; let path = PathBuf::from(format!("./databases/{name}")); - app.database.create_new_database(name, path)?; + app.database.create_new_database(name.clone(), path)?; + app.modal.show_success(format!("Created database '{name}'")); } } Some(ModalType::DatabaseList) => { @@ -175,9 +256,10 @@ fn execute_modal_action(app: &mut App) -> io::Result<()> { } Some(ModalType::DeleteDatabase) => { let selected_index = app.modal.selected_index(); - if let Some((_, path)) = app.database.available_databases.get(selected_index) { - let path = path.clone(); - app.database.delete_database(&path)?; + if let Some((name, path)) = app.database.available_databases.get(selected_index) { + app.pending_delete_database = Some((name.clone(), path.clone())); + app.pending_delete_database_index = Some(selected_index); + app.modal.show_confirm_delete_database(name.clone()); } } Some(ModalType::GetVector) => { @@ -190,54 +272,7 @@ fn execute_modal_action(app: &mut App) -> io::Result<()> { )); } let id = vector_id_field.unwrap(); - if let Some(storage) = &app.database.storage_engine { - // Invoke get_vector and get_payload function from storage crate to get vector - match storage.get_vector(id).map_err(to_io) { - Ok(vec_opt) => match storage.get_payload(id).map_err(to_io) { - Ok(pay_opt) => { - if vec_opt.is_none() && pay_opt.is_none() { - app.modal - .show_failure(format!("Vector with id={id} not found!")); - } else { - let vec_display = if let Some(v) = vec_opt.as_ref() { - let total = v.len(); - let take = total.min(9); - let elems = v - .iter() - .take(take) - .map(|x| format!("{x:.2}")) - .collect::>() - .join(", "); - if total > take { - format!("[{elems}, ...] ({total} dims)") - } else { - format!("[{elems}] ({total} dims)") - } - } else { - "None".to_string() - }; - - let payload_display = match pay_opt.as_ref() { - Some(p) => format!("{p:?}"), - None => "None".to_string(), - }; - - app.modal.show_success(format!( - "ID: {id}, Payload: {payload_display}, Vector: {vec_display}" - )); - } - } - Err(e) => { - app.modal.show_failure(format!("Storage error: {e}")); - } - }, - Err(e) => { - app.modal.show_failure(format!("Storage error: {e}")); - } - } - } else { - app.modal.show_error("No database selected!"); - } + show_vector_info(app, id)?; } Some(ModalType::InsertVector) => { // Form fields for insertion: id, vector, payload @@ -315,8 +350,21 @@ fn execute_modal_action(app: &mut App) -> io::Result<()> { Ok(()) } +fn restore_delete_database_modal(app: &mut App) { + let index = app + .pending_delete_database_index + .unwrap_or_else(|| app.modal.selected_index()); + app.modal.show_delete_database(); + let max_items = app.database.available_databases.len(); + app.modal + .set_selected_index(index.min(max_items.saturating_sub(1)), max_items); + app.pending_delete_database = None; + app.pending_delete_database_index = None; + app.vector_list_post_restore = false; +} + fn to_io(e: E) -> io::Error { - io::Error::new(io::ErrorKind::Other, format!("{e:?}")) + io::Error::other(format!("{e:?}")) } fn parse_vector(input: &str) -> Option> { @@ -343,13 +391,162 @@ fn execute_selected_db_operation(app: &mut App) -> io::Result<()> { } Some(ModalType::DeleteDatabase) => { let selected_index = app.modal.selected_index(); - if let Some((_, path)) = app.database.available_databases.get(selected_index) { - let path = path.clone(); - app.database.delete_database(&path)?; - app.modal.close(); + if let Some((name, path)) = app.database.available_databases.get(selected_index) { + app.pending_delete_database = Some((name.clone(), path.clone())); + app.pending_delete_database_index = Some(selected_index); + app.modal.show_confirm_delete_database(name.clone()); + } + } + Some(ModalType::ConfirmDeleteDatabase) => { + let confirm = app.modal.selected_index() == 0; + if confirm { + if let Some((name, path)) = app.pending_delete_database.take() { + app.database.delete_database(&path)?; + app.pending_delete_database_index = None; + app.modal.show_success(format!("Deleted database '{name}'")); + } + } else { + restore_delete_database_modal(app); } } _ => {} } Ok(()) } + +fn initialize_vector_listing(app: &mut App) -> io::Result<()> { + if app.database.storage_engine.is_none() { + app.modal.show_error("No database selected!"); + return Ok(()); + } + + app.vector_list_items.clear(); + app.vector_list_next_offset = Some(0); + app.vector_list_post_restore = false; + app.vector_list_selected_index = 0; + app.vector_detail = None; + + if fetch_next_vector_page(app)? { + app.modal.show_vector_list(); + let len = app.vector_list_items.len(); + app.modal.set_selected_index(0, len); + } else { + app.modal + .show_failure("No vectors found for the current query."); + } + + Ok(()) +} + +fn fetch_next_vector_page(app: &mut App) -> io::Result { + let Some(storage) = &app.database.storage_engine else { + app.modal.show_error("No database selected!"); + return Ok(false); + }; + + let Some(offset) = app.vector_list_next_offset else { + return Ok(false); + }; + + let response = storage + .list_vectors(offset, VECTOR_LIST_LIMIT) + .map_err(to_io)?; + + let Some((vectors, next_offset)) = response else { + app.vector_list_next_offset = None; + return Ok(false); + }; + + if vectors.is_empty() { + app.vector_list_next_offset = None; + return Ok(false); + } + + let vector_count = vectors.len(); + + for (id, vector) in vectors.into_iter() { + let payload = storage.get_payload(id).map_err(to_io)?; + app.vector_list_items.push(VectorListItem { + id, + vector, + payload, + }); + } + + if vector_count == VECTOR_LIST_LIMIT { + app.vector_list_next_offset = Some(next_offset); + } else { + app.vector_list_next_offset = None; + } + + Ok(true) +} + +fn open_selected_vector_from_list(app: &mut App) -> io::Result<()> { + let selected_index = app.modal.selected_index(); + if let Some(item) = app.vector_list_items.get(selected_index) { + show_vector_info(app, item.id)?; + app.vector_list_selected_index = selected_index; + app.vector_list_post_restore = + matches!(app.modal.modal_type(), Some(ModalType::VectorDetails)); + } + Ok(()) +} + +fn reopen_vector_list_modal(app: &mut App) { + let len = app.vector_list_items.len(); + if len == 0 { + app.vector_list_post_restore = false; + app.vector_detail = None; + app.modal.close(); + return; + } + + let desired_index = app.vector_list_selected_index.min(len.saturating_sub(1)); + + app.modal.show_vector_list(); + app.modal.set_selected_index(desired_index, len); + app.vector_list_selected_index = desired_index; + app.vector_list_post_restore = false; + app.vector_detail = None; +} + +fn close_vector_detail_modal(app: &mut App) { + if app.vector_list_post_restore && !app.vector_list_items.is_empty() { + reopen_vector_list_modal(app); + } else { + app.vector_detail = None; + app.vector_list_post_restore = false; + app.modal.close(); + } +} + +fn show_vector_info(app: &mut App, id: u64) -> io::Result<()> { + let Some(storage) = &app.database.storage_engine else { + app.modal.show_error("No database selected!"); + return Ok(()); + }; + + app.vector_list_post_restore = false; + + let vector_opt = storage.get_vector(id).map_err(to_io)?; + let payload_opt = storage.get_payload(id).map_err(to_io)?; + + if vector_opt.is_none() && payload_opt.is_none() { + app.vector_detail = None; + app.modal + .show_failure(format!("Vector with id={id} not found!")); + return Ok(()); + } + + let vector = vector_opt.unwrap_or_default(); + let payload = payload_opt; + + app.vector_detail = Some(VectorListItem { + id, + vector, + payload, + }); + app.modal.show_vector_details(); + Ok(()) +} diff --git a/crates/tui/src/app/mod.rs b/crates/tui/src/app/mod.rs index ffe286c..5eb6622 100644 --- a/crates/tui/src/app/mod.rs +++ b/crates/tui/src/app/mod.rs @@ -17,6 +17,14 @@ pub struct App { pub vector_selected: usize, pub database: database::DatabaseManager, pub modal: modal::ModalManager, + + pub vector_list_items: Vec, + pub vector_list_next_offset: Option, + pub vector_list_post_restore: bool, + pub vector_list_selected_index: usize, + pub vector_detail: Option, + pub pending_delete_database: Option<(String, PathBuf)>, + pub pending_delete_database_index: Option, } impl Default for App { @@ -34,6 +42,14 @@ impl App { vector_selected: 0, database: database::DatabaseManager::new(), modal: modal::ModalManager::new(), + + vector_list_items: Vec::new(), + vector_list_next_offset: None, + vector_list_post_restore: false, + vector_list_selected_index: 0, + vector_detail: None, + pending_delete_database: None, + pending_delete_database_index: None, } } @@ -138,4 +154,8 @@ impl App { pub fn error_message(&self) -> Option<&str> { self.modal.error_message() } + + pub fn modal_footer_items(&self) -> Vec<(String, ratatui::style::Color)> { + self.modal.footer_items() + } } diff --git a/crates/tui/src/app/modal.rs b/crates/tui/src/app/modal.rs index 9f3f58a..09c0b34 100644 --- a/crates/tui/src/app/modal.rs +++ b/crates/tui/src/app/modal.rs @@ -1,4 +1,5 @@ use super::state::ModalType; +use ratatui::style::Color; pub struct ModalManager { show_modal: bool, @@ -102,6 +103,21 @@ impl ModalManager { self.selected_index = 0; self.error_message = None; } + + pub fn show_vector_list(&mut self) { + self.show_modal = true; + self.modal_type = Some(ModalType::ListVectors); + self.input_mode = false; + self.error_message = None; + self.selected_index = 0; + } + pub fn show_vector_details(&mut self) { + self.show_modal = true; + self.modal_type = Some(ModalType::VectorDetails); + self.input_mode = false; + self.selected_index = 0; + self.error_message = None; + } pub fn show_success>(&mut self, message: S) { self.show_modal = true; self.modal_type = Some(ModalType::Success); @@ -138,6 +154,14 @@ impl ModalManager { self.error_message = None; } + pub fn show_confirm_delete_database(&mut self, name: String) { + self.show_modal = true; + self.modal_type = Some(ModalType::ConfirmDeleteDatabase); + self.input_mode = false; + self.selected_index = 0; + self.error_message = Some(format!("Delete database '{name}'?")); + } + pub fn show_error>(&mut self, message: S) { self.show_modal = true; self.modal_type = Some(ModalType::Error); @@ -202,6 +226,14 @@ impl ModalManager { } } + pub fn set_selected_index(&mut self, index: usize, max_items: usize) { + if max_items == 0 { + self.selected_index = 0; + } else { + self.selected_index = index.min(max_items - 1); + } + } + pub fn get_input_value(&self) -> String { self.input_buffer.clone() } @@ -209,4 +241,57 @@ impl ModalManager { pub fn error_message(&self) -> Option<&str> { self.error_message.as_deref() } + + pub fn footer_items(&self) -> Vec<(String, Color)> { + use ModalType::*; + + match self.modal_type { + Some(CreateDatabase) => vec![ + ("Enter Create".into(), Color::Green), + ("Esc Cancel".into(), Color::Red), + ], + Some(DatabaseList) => vec![ + ("↑ Navigate".into(), Color::Gray), + ("↓ Navigate".into(), Color::Gray), + ("Enter Select".into(), Color::Green), + ("Esc Cancel".into(), Color::Red), + ], + Some(DeleteDatabase) => vec![ + ("↑ Navigate".into(), Color::Gray), + ("↓ Navigate".into(), Color::Gray), + ("Enter Continue".into(), Color::Green), + ("Esc Cancel".into(), Color::Red), + ], + Some(ConfirmDeleteDatabase) => vec![ + ("←/→ Toggle".into(), Color::Gray), + ("Enter Confirm".into(), Color::Green), + ("Esc Back".into(), Color::Red), + ], + Some(GetVector) => vec![ + ("Enter Fetch".into(), Color::Green), + ("Esc Close".into(), Color::Red), + ], + Some(InsertVector) => vec![ + ("Tab Next".into(), Color::Gray), + ("Enter Insert".into(), Color::Green), + ("Esc Cancel".into(), Color::Red), + ], + Some(DeleteVector) => vec![ + ("Enter Delete".into(), Color::Green), + ("Esc Cancel".into(), Color::Red), + ], + Some(ListVectors) => vec![ + ("↑ Scroll".into(), Color::Gray), + ("↓ Scroll".into(), Color::Gray), + ("Enter Details".into(), Color::Green), + ("Esc Close".into(), Color::Red), + ], + Some(VectorDetails) => vec![ + ("Enter Close".into(), Color::Green), + ("Esc Back".into(), Color::Red), + ], + Some(Success) | Some(Failure) | Some(Error) => vec![("Esc Dismiss".into(), Color::Red)], + _ => vec![("Esc Cancel".into(), Color::Red)], + } + } } diff --git a/crates/tui/src/app/state.rs b/crates/tui/src/app/state.rs index 8f399a4..98d17b8 100644 --- a/crates/tui/src/app/state.rs +++ b/crates/tui/src/app/state.rs @@ -1,3 +1,5 @@ +use core::{DenseVector, Payload}; + #[derive(Debug, Default, Clone, PartialEq)] pub enum AppState { #[default] @@ -10,7 +12,10 @@ pub enum AppState { pub enum ModalType { CreateDatabase, DeleteDatabase, + ConfirmDeleteDatabase, DatabaseList, + ListVectors, + VectorDetails, Error, Success, Failure, @@ -18,3 +23,44 @@ pub enum ModalType { InsertVector, DeleteVector, } + +#[derive(Debug, Clone)] +pub struct VectorListItem { + pub id: u64, + pub vector: DenseVector, + pub payload: Option, +} + +impl VectorListItem { + pub fn dims(&self) -> usize { + self.vector.len() + } + + pub fn snippet(&self, max_dims: usize) -> String { + if self.vector.is_empty() { + return "[]".to_string(); + } + + let take = self.vector.len().min(max_dims); + let snippet = self + .vector + .iter() + .take(take) + .map(|v| format!("{v:.2}")) + .collect::>() + .join(", "); + + if self.vector.len() > take { + format!("[{snippet}, ...]") + } else { + format!("[{snippet}]") + } + } + + pub fn payload_summary(&self) -> String { + self.payload + .as_ref() + .map(|payload| format!("{payload:?}")) + .unwrap_or_else(|| "None".to_string()) + } +} diff --git a/crates/tui/src/ui/db.rs b/crates/tui/src/ui/db.rs index 616e7a0..77d0198 100644 --- a/crates/tui/src/ui/db.rs +++ b/crates/tui/src/ui/db.rs @@ -36,8 +36,13 @@ pub fn render_database(f: &mut Frame, app: &App) { selected: app.db_selected, }; - let mut instructions = common_instructions(); - instructions.insert(4, ("→ Next".to_string(), Color::Gray)); + let instructions = if app.show_modal() { + app.modal_footer_items() + } else { + let mut base = common_instructions(); + base.insert(4, ("→ Next".to_string(), Color::Gray)); + base + }; f.render_widget(title.render(), chunks[0]); operations_list.render(f, chunks[1]); diff --git a/crates/tui/src/ui/modal.rs b/crates/tui/src/ui/modal.rs index 737fe35..ad2f82f 100644 --- a/crates/tui/src/ui/modal.rs +++ b/crates/tui/src/ui/modal.rs @@ -1,7 +1,7 @@ use crate::app::{App, ModalType}; use ratatui::{ prelude::*, - widgets::{Block, Borders, Clear, List, ListItem, Paragraph}, + widgets::{Block, Borders, Cell, Clear, List, ListItem, Paragraph, Row, Table, TableState}, }; pub fn render_modal(f: &mut Frame, app: &App) { @@ -10,7 +10,12 @@ pub fn render_modal(f: &mut Frame, app: &App) { } let size = f.size(); - let popup_area = centered_rect(60, 60, size); + let (modal_width, modal_height) = match app.modal_type() { + Some(ModalType::ListVectors) => (85, 70), + Some(ModalType::VectorDetails) => (80, 60), + _ => (60, 60), + }; + let popup_area = centered_rect(modal_width, modal_height, size); // Clear the area f.render_widget(Clear, popup_area); @@ -19,10 +24,15 @@ pub fn render_modal(f: &mut Frame, app: &App) { Some(ModalType::CreateDatabase) => render_create_database_modal(f, app, popup_area), Some(ModalType::DatabaseList) => render_database_list_modal(f, app, popup_area), Some(ModalType::DeleteDatabase) => render_delete_database_modal(f, app, popup_area), + Some(ModalType::ConfirmDeleteDatabase) => { + render_confirm_delete_database_modal(f, app, popup_area) + } Some(ModalType::Error) => render_error_modal(f, app, popup_area), Some(ModalType::GetVector) => render_get_vector_modal(f, app, popup_area), Some(ModalType::InsertVector) => render_insert_vector_modal(f, app, popup_area), Some(ModalType::DeleteVector) => render_delete_vector_modal(f, app, popup_area), + Some(ModalType::ListVectors) => render_vector_list_modal(f, app, popup_area), + Some(ModalType::VectorDetails) => render_vector_details_modal(f, app, popup_area), Some(ModalType::Success) => render_success_modal(f, app, popup_area), Some(ModalType::Failure) => render_failure_modal(f, app, popup_area), _ => {} @@ -54,10 +64,6 @@ fn render_get_vector_modal(f: &mut Frame, app: &App, area: Rect) { Line::from(Span::raw("ID:")), Line::from(Span::raw(app.input_buffer().to_string())), Line::from(""), - Line::from(Span::styled( - "Press Tab to switch field (if any). Press Enter to submit.", - Style::default().fg(Color::Gray), - )), ]; let input = Paragraph::new(lines) @@ -66,7 +72,6 @@ fn render_get_vector_modal(f: &mut Frame, app: &App, area: Rect) { f.render_widget(input, area); if app.input_mode() { - // content line is the second line of the paragraph => area.y + 2 f.set_cursor(area.x + 1 + app.input_buffer().len() as u16, area.y + 2); } } @@ -85,11 +90,6 @@ fn render_insert_vector_modal(f: &mut Frame, app: &App, area: Rect) { Line::from(Span::raw("")), Line::from(Span::raw("Payload (optional):")), Line::from(Span::raw(app.tertiary_input().to_string())), - Line::from(""), - Line::from(Span::styled( - "Use Tab to switch fields. Enter to submit.", - Style::default().fg(Color::Gray), - )), ]; let input = Paragraph::new(lines) @@ -98,16 +98,11 @@ fn render_insert_vector_modal(f: &mut Frame, app: &App, area: Rect) { f.render_widget(input, area); if app.input_mode() { - // Here active_field: 0 -> ID, 1 -> Vector, 2 -> Payload - // Paragraph layout: label (y+1), content (y+2), blank (y+3), next label (y+4), next content (y+5), ... if app.active_field() == 0 { - // ID content is at line index 1 -> y + 2 f.set_cursor(area.x + 1 + app.input_buffer().len() as u16, area.y + 2); } else if app.active_field() == 1 { - // Vector content is at line index 4 -> y + 5 f.set_cursor(area.x + 1 + app.secondary_input().len() as u16, area.y + 5); } else { - // Payload content is at line index 7 -> y + 8 f.set_cursor(area.x + 1 + app.tertiary_input().len() as u16, area.y + 8); } } @@ -122,10 +117,6 @@ fn render_delete_vector_modal(f: &mut Frame, app: &App, area: Rect) { Line::from(Span::raw("ID:")), Line::from(Span::raw(app.input_buffer().to_string())), Line::from(""), - Line::from(Span::styled( - "Press Enter to delete", - Style::default().fg(Color::Gray), - )), ]; let input = Paragraph::new(lines) @@ -134,7 +125,6 @@ fn render_delete_vector_modal(f: &mut Frame, app: &App, area: Rect) { f.render_widget(input, area); if app.input_mode() { - // content line is the second line of the paragraph => area.y + 2 f.set_cursor(area.x + 1 + app.input_buffer().len() as u16, area.y + 2); } } @@ -191,75 +181,208 @@ fn render_delete_database_modal(f: &mut Frame, app: &App, area: Rect) { f.render_widget(list, area); } +fn render_vector_list_modal(f: &mut Frame, app: &App, area: Rect) { + let has_more = app.vector_list_next_offset.is_some(); + let title = if has_more { + "Vectors (scroll for more)" + } else { + "Vectors" + }; + + let header_cells = ["Dims", "ID", "Vector", "Payload"].into_iter().map(|h| { + Cell::from(h).style( + Style::default() + .fg(Color::Magenta) + .add_modifier(Modifier::BOLD), + ) + }); + let header = Row::new(header_cells).height(1); + + let rows: Vec = app + .vector_list_items + .iter() + .map(|item| { + Row::new(vec![ + Cell::from(item.dims().to_string()), + Cell::from(item.id.to_string()), + Cell::from(item.snippet(8)), + Cell::from(item.payload_summary()), + ]) + }) + .collect(); + + let mut state = TableState::default(); + if !app.vector_list_items.is_empty() { + let selected = app + .modal + .selected_index() + .min(app.vector_list_items.len().saturating_sub(1)); + state.select(Some(selected)); + } + + let widths = [ + Constraint::Length(8), + Constraint::Length(14), + Constraint::Percentage(45), + Constraint::Percentage(33), + ]; + + let table = Table::new(rows, widths) + .header(header) + .block( + Block::default() + .title(title) + .borders(Borders::ALL) + .border_style(Style::default().fg(Color::Magenta)), + ) + .column_spacing(2) + .highlight_symbol("▶ ") + .highlight_style(Style::default().bg(Color::Yellow).fg(Color::Black)); + + f.render_stateful_widget(table, area, &mut state); +} + +fn render_vector_details_modal(f: &mut Frame, app: &App, area: Rect) { + let block = Block::default() + .title("Vector Details") + .borders(Borders::ALL) + .border_style(Style::default().fg(Color::Magenta)); + + if let Some(item) = &app.vector_detail { + let header_cells = ["Dims", "ID", "Vector", "Payload"].into_iter().map(|h| { + Cell::from(h).style( + Style::default() + .fg(Color::Magenta) + .add_modifier(Modifier::BOLD), + ) + }); + let header = Row::new(header_cells).height(1); + + let row = Row::new(vec![ + Cell::from(item.dims().to_string()), + Cell::from(item.id.to_string()), + Cell::from(item.snippet(16)), + Cell::from(item.payload_summary()), + ]) + .height(2); + let rows = vec![row]; + + let widths = [ + Constraint::Length(8), + Constraint::Length(14), + Constraint::Percentage(45), + Constraint::Percentage(33), + ]; + + let table = Table::new(rows, widths) + .header(header) + .block(block) + .column_spacing(2); + + f.render_widget(table, area); + } else { + let placeholder = Paragraph::new("No vector data available.") + .block(block) + .alignment(Alignment::Center) + .wrap(ratatui::widgets::Wrap { trim: true }); + f.render_widget(placeholder, area); + } +} + fn render_error_modal(f: &mut Frame, app: &App, area: Rect) { let message = app.error_message().unwrap_or("An unknown error occurred!"); - let content = Paragraph::new(vec![ - Line::from(message.to_string()), - Line::from(""), - Line::from(Span::styled( - "Press Esc to dismiss", - Style::default().fg(Color::Gray), - )), - ]) - .block( - Block::default() - .title(Span::styled( - "Error", - Style::default().fg(Color::Red).add_modifier(Modifier::BOLD), - )) - .borders(Borders::ALL) - .border_style(Style::default().fg(Color::Red)), - ) - .wrap(ratatui::widgets::Wrap { trim: true }); + let content = Paragraph::new(vec![Line::from(message.to_string())]) + .block( + Block::default() + .title(Span::styled( + "Error", + Style::default().fg(Color::Red).add_modifier(Modifier::BOLD), + )) + .borders(Borders::ALL) + .border_style(Style::default().fg(Color::Red)), + ) + .wrap(ratatui::widgets::Wrap { trim: true }); f.render_widget(content, area); } fn render_success_modal(f: &mut Frame, app: &App, area: Rect) { let message = app.error_message().unwrap_or("Success"); - let content = Paragraph::new(vec![ - Line::from(message.to_string()), - Line::from(""), - Line::from(Span::styled( - "Press Esc to dismiss", - Style::default().fg(Color::Gray), - )), - ]) - .block( - Block::default() - .title(Span::styled( - "Success", - Style::default() - .fg(Color::Green) - .add_modifier(Modifier::BOLD), - )) - .borders(Borders::ALL) - .border_style(Style::default().fg(Color::Green)), - ) - .wrap(ratatui::widgets::Wrap { trim: true }); + let content = Paragraph::new(vec![Line::from(message.to_string())]) + .block( + Block::default() + .title(Span::styled( + "Success", + Style::default() + .fg(Color::Green) + .add_modifier(Modifier::BOLD), + )) + .borders(Borders::ALL) + .border_style(Style::default().fg(Color::Green)), + ) + .wrap(ratatui::widgets::Wrap { trim: true }); f.render_widget(content, area); } fn render_failure_modal(f: &mut Frame, app: &App, area: Rect) { let message = app.error_message().unwrap_or("Failure"); + let content = Paragraph::new(vec![Line::from(message.to_string())]) + .block( + Block::default() + .title(Span::styled( + "Failure", + Style::default().fg(Color::Red).add_modifier(Modifier::BOLD), + )) + .borders(Borders::ALL) + .border_style(Style::default().fg(Color::Red)), + ) + .wrap(ratatui::widgets::Wrap { trim: true }); + + f.render_widget(content, area); +} + +fn render_confirm_delete_database_modal(f: &mut Frame, app: &App, area: Rect) { + let prompt = app + .error_message() + .unwrap_or("Are you sure you want to delete this database?"); + + let block = Block::default() + .title("Confirm Deletion") + .borders(Borders::ALL) + .border_style(Style::default().fg(Color::Red)); + + let options = ["Yes", "No"]; + let selected = app + .modal + .selected_index() + .min(options.len().saturating_sub(1)); + + let option_line = options + .iter() + .enumerate() + .map(|(i, option)| { + if i == selected { + Span::styled( + format!("[ {option} ]"), + Style::default() + .fg(Color::Black) + .bg(Color::Yellow) + .add_modifier(Modifier::BOLD), + ) + } else { + Span::styled(format!(" {option} "), Style::default().fg(Color::Gray)) + } + }) + .collect::>(); + let content = Paragraph::new(vec![ - Line::from(message.to_string()), + Line::from(prompt.to_string()), Line::from(""), - Line::from(Span::styled( - "Press Esc to dismiss", - Style::default().fg(Color::Gray), - )), + Line::from(option_line), ]) - .block( - Block::default() - .title(Span::styled( - "Failure", - Style::default().fg(Color::Red).add_modifier(Modifier::BOLD), - )) - .borders(Borders::ALL) - .border_style(Style::default().fg(Color::Red)), - ) + .block(block) + .alignment(Alignment::Center) .wrap(ratatui::widgets::Wrap { trim: true }); f.render_widget(content, area); diff --git a/crates/tui/src/ui/vector_operations.rs b/crates/tui/src/ui/vector_operations.rs index 28dc052..153bbbe 100644 --- a/crates/tui/src/ui/vector_operations.rs +++ b/crates/tui/src/ui/vector_operations.rs @@ -77,7 +77,14 @@ pub fn render_vector_operations(f: &mut Frame, app: &App) { f.render_widget(title.render(), chunks[0]); f.render_widget(db_info, chunks[1]); operations_list.render(f, chunks[2]); - f.render_widget(create_instructions(common_instructions()), chunks[3]); + + let instructions = if app.show_modal() { + app.modal_footer_items() + } else { + common_instructions() + }; + + f.render_widget(create_instructions(instructions), chunks[3]); } pub fn get_vector_operations_count() -> usize { From 159b2710438b1f6c2ba89b87e431fbaf4e194e84 Mon Sep 17 00:00:00 2001 From: Entity069 Date: Thu, 9 Oct 2025 20:01:58 +0530 Subject: [PATCH 11/25] chore: add searching similar vectors in TUI --- crates/tui/src/app/events.rs | 113 ++++++++++++++++++++++++++++++++++- crates/tui/src/app/modal.rs | 18 ++++++ crates/tui/src/app/state.rs | 1 + crates/tui/src/ui/modal.rs | 30 ++++++++++ 4 files changed, 159 insertions(+), 3 deletions(-) diff --git a/crates/tui/src/app/events.rs b/crates/tui/src/app/events.rs index 64d47e2..0ec7f0c 100644 --- a/crates/tui/src/app/events.rs +++ b/crates/tui/src/app/events.rs @@ -1,8 +1,9 @@ use super::{App, AppState, ModalType, VectorListItem}; -use core::Payload; +use core::{DenseVector, Payload, Similarity}; use crossterm::event::{Event, KeyCode, KeyEvent}; -use std::io; +use index::distance; use std::path::PathBuf; +use std::{cmp::Ordering, io}; // Set how many vectors to fetch per function call in list_vectors const VECTOR_LIST_LIMIT: usize = 50; @@ -43,7 +44,8 @@ fn handle_modal_input(app: &mut App, key: KeyEvent) -> io::Result<()> { Some(ModalType::Success) | Some(ModalType::Failure) | Some(ModalType::Error) - | Some(ModalType::VectorDetails) => { + | Some(ModalType::VectorDetails) + | Some(ModalType::ListVectors) => { // leave open } _ => app.modal.close(), @@ -208,6 +210,7 @@ fn handle_general_keys(app: &mut App, key: KeyEvent) { 1 => app.modal.show_get_vector(), 2 => app.modal.show_insert_vector(), 3 => app.modal.show_delete_vector(), + 4 => app.modal.show_search_similar_vectors(), _ => {} } } else { @@ -345,6 +348,110 @@ fn execute_modal_action(app: &mut App) -> io::Result<()> { app.modal.show_error("No database selected!"); } } + Some(ModalType::SearchSimilarVectors) => { + let k_text = app.modal.get_input_value(); + let vector_text = app.modal.secondary_input().to_string(); + + let k = k_text.parse::().ok().filter(|value| *value > 0); + if k.is_none() { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "k should be a positive integer!", + )); + } + let k = k.unwrap(); + + let Some(query) = parse_vector(&vector_text) else { + app.modal + .show_failure("Query vector should be a list of floats (e.g. [0.1,0.2,...])!"); + return Ok(()); + }; + + let Some(storage) = &app.database.storage_engine else { + app.modal.show_error("No database selected!"); + return Ok(()); + }; + + let mut all_vectors: Vec<(u64, DenseVector)> = Vec::new(); + let mut next_offset = Some(0); + while let Some(offset) = next_offset { + let response = storage + .list_vectors(offset, VECTOR_LIST_LIMIT) + .map_err(to_io)?; + let Some((vectors, following_offset)) = response else { + break; + }; + + if vectors.is_empty() { + break; + } + + let batch_len = vectors.len(); + for (id, vector) in vectors { + all_vectors.push((id, vector)); + } + + if batch_len == VECTOR_LIST_LIMIT { + next_offset = Some(following_offset); + } else { + next_offset = None; + } + } + + if all_vectors.is_empty() { + app.modal + .show_failure("No vectors available in the selected database!"); + return Ok(()); + } + + let query_len = query.len(); + let mut scored: Vec<(f32, u64, DenseVector)> = Vec::new(); + // currently to avoid panic on dimension mismatch, we skip vectors with different dims + // TODO: enforce consistent dimensions on insert/search functions + for (id, vector) in all_vectors.into_iter() { + if vector.len() != query_len { + continue; + } + // TODO: replace with search function after enforcing dimension consistency + let score = distance(vector.clone(), query.clone(), Similarity::Cosine); + scored.push((score, id, vector)); + } + + if scored.is_empty() { + app.modal + .show_failure("No vectors with matching dimensions found for the query!"); + return Ok(()); + } + + scored.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(Ordering::Equal)); + + let take = k.min(scored.len()); + + app.vector_list_items.clear(); + app.vector_detail = None; + app.vector_list_next_offset = None; + app.vector_list_post_restore = false; + app.vector_list_selected_index = 0; + + for (_, id, vector) in scored.into_iter().take(take) { + let payload = storage.get_payload(id).map_err(to_io)?; + app.vector_list_items.push(VectorListItem { + id, + vector, + payload, + }); + } + + if app.vector_list_items.is_empty() { + app.modal + .show_failure("Unable to display results. Try a different query vector."); + return Ok(()); + } + + app.modal.show_vector_list(); + let len = app.vector_list_items.len(); + app.modal.set_selected_index(0, len); + } _ => {} } Ok(()) diff --git a/crates/tui/src/app/modal.rs b/crates/tui/src/app/modal.rs index 09c0b34..31bbc90 100644 --- a/crates/tui/src/app/modal.rs +++ b/crates/tui/src/app/modal.rs @@ -87,6 +87,19 @@ impl ModalManager { self.input_buffer.clear(); self.secondary_input.clear(); self.tertiary_input.clear(); + self.active_field = 0; + self.input_mode = true; + self.selected_index = 0; + self.error_message = None; + } + + pub fn show_search_similar_vectors(&mut self) { + self.show_modal = true; + self.modal_type = Some(ModalType::SearchSimilarVectors); + self.input_buffer.clear(); + self.secondary_input.clear(); + self.tertiary_input.clear(); + self.active_field = 0; self.input_mode = true; self.selected_index = 0; self.error_message = None; @@ -276,6 +289,11 @@ impl ModalManager { ("Enter Insert".into(), Color::Green), ("Esc Cancel".into(), Color::Red), ], + Some(SearchSimilarVectors) => vec![ + ("Tab Next".into(), Color::Gray), + ("Enter Search".into(), Color::Green), + ("Esc Cancel".into(), Color::Red), + ], Some(DeleteVector) => vec![ ("Enter Delete".into(), Color::Green), ("Esc Cancel".into(), Color::Red), diff --git a/crates/tui/src/app/state.rs b/crates/tui/src/app/state.rs index 98d17b8..01a0e6b 100644 --- a/crates/tui/src/app/state.rs +++ b/crates/tui/src/app/state.rs @@ -22,6 +22,7 @@ pub enum ModalType { GetVector, InsertVector, DeleteVector, + SearchSimilarVectors, } #[derive(Debug, Clone)] diff --git a/crates/tui/src/ui/modal.rs b/crates/tui/src/ui/modal.rs index ad2f82f..59a7ea6 100644 --- a/crates/tui/src/ui/modal.rs +++ b/crates/tui/src/ui/modal.rs @@ -30,6 +30,9 @@ pub fn render_modal(f: &mut Frame, app: &App) { Some(ModalType::Error) => render_error_modal(f, app, popup_area), Some(ModalType::GetVector) => render_get_vector_modal(f, app, popup_area), Some(ModalType::InsertVector) => render_insert_vector_modal(f, app, popup_area), + Some(ModalType::SearchSimilarVectors) => { + render_search_similar_vectors_modal(f, app, popup_area) + } Some(ModalType::DeleteVector) => render_delete_vector_modal(f, app, popup_area), Some(ModalType::ListVectors) => render_vector_list_modal(f, app, popup_area), Some(ModalType::VectorDetails) => render_vector_details_modal(f, app, popup_area), @@ -108,6 +111,33 @@ fn render_insert_vector_modal(f: &mut Frame, app: &App, area: Rect) { } } +fn render_search_similar_vectors_modal(f: &mut Frame, app: &App, area: Rect) { + let block = Block::default() + .title("Search Similar Vectors") + .borders(Borders::ALL); + + let lines = vec![ + Line::from(Span::raw("Top-k (int):")), + Line::from(Span::raw(app.input_buffer().to_string())), + Line::from(Span::raw("")), + Line::from(Span::raw("Query Vector (e.g. [0.1,0.2,...]):")), + Line::from(Span::raw(app.secondary_input().to_string())), + ]; + + let input = Paragraph::new(lines) + .block(block) + .wrap(ratatui::widgets::Wrap { trim: true }); + f.render_widget(input, area); + + if app.input_mode() { + if app.active_field() == 0 { + f.set_cursor(area.x + 1 + app.input_buffer().len() as u16, area.y + 2); + } else { + f.set_cursor(area.x + 1 + app.secondary_input().len() as u16, area.y + 5); + } + } +} + fn render_delete_vector_modal(f: &mut Frame, app: &App, area: Rect) { let block = Block::default() .title("Delete Vector by ID") From 050f0bfdd59b9e0d3ef5c3c643c9c8c312c55a0b Mon Sep 17 00:00:00 2001 From: Entity069 Date: Fri, 10 Oct 2025 15:11:32 +0530 Subject: [PATCH 12/25] feat: generate embeddings in TUI --- .env | 4 + Cargo.lock | 1399 ++++++++++++++++++++---- crates/tui/Cargo.toml | 2 + crates/tui/src/app/embeddings.rs | 149 +++ crates/tui/src/app/events.rs | 193 +++- crates/tui/src/app/mod.rs | 11 +- crates/tui/src/app/modal.rs | 51 + crates/tui/src/app/state.rs | 3 + crates/tui/src/ui/modal.rs | 93 ++ crates/tui/src/ui/vector_operations.rs | 3 + 10 files changed, 1674 insertions(+), 234 deletions(-) create mode 100644 crates/tui/src/app/embeddings.rs diff --git a/.env b/.env index af3955f..1c2019d 100644 --- a/.env +++ b/.env @@ -5,3 +5,7 @@ DATABASE_PATH2=Enter-path-2-here DATABASE_NAME2=Enter-name-2-here DATABASE_PATH3=Enter-path-3-here DATABASE_NAME3=Enter-name-3-here + +TEXT_EMBEDDING_URL=http://localhost:8080/vectors +SENTENCE_EMBEDDING_URL=http://localhost:8000/vectorize +IMAGE_EMBEDDING_URL=http://localhost:8080/vectors_img \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 4bca95c..ab84ba4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,9 +4,9 @@ version = 4 [[package]] name = "addr2line" -version = "0.24.2" +version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" dependencies = [ "gimli", ] @@ -43,9 +43,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.98" +version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" [[package]] name = "api" @@ -57,6 +57,12 @@ dependencies = [ "tempfile", ] +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "autocfg" version = "1.5.0" @@ -65,9 +71,9 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "backtrace" -version = "0.3.75" +version = "0.3.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" +checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" dependencies = [ "addr2line", "cfg-if", @@ -75,9 +81,15 @@ dependencies = [ "miniz_oxide", "object", "rustc-demangle", - "windows-targets 0.52.6", + "windows-link 0.2.1", ] +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "bincode" version = "1.3.3" @@ -110,11 +122,11 @@ dependencies = [ [[package]] name = "bindgen" -version = "0.71.1" +version = "0.72.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f58bf3d7db68cfbac37cfc485a8d711e87e064c3d0fe0435b92f7a407f9d6b3" +checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.4", "cexpr", "clang-sys", "itertools 0.13.0", @@ -134,9 +146,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.9.1" +version = "2.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" +checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" [[package]] name = "bumpalo" @@ -177,10 +189,11 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.27" +version = "1.2.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d487aa071b5f64da6f19a3e848e3578944b726ee5a4854b82172f02aa876bfdc" +checksum = "e1d05d92f4b1fd76aad469d46cdd858ca761576082cd37df81416691e50199fb" dependencies = [ + "find-msvc-tools", "jobserver", "libc", "shlex", @@ -197,9 +210,9 @@ dependencies = [ [[package]] name = "cfg-if" -version = "1.0.1" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" +checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" [[package]] name = "chrono" @@ -212,7 +225,7 @@ dependencies = [ "num-traits", "serde", "wasm-bindgen", - "windows-link", + "windows-link 0.2.1", ] [[package]] @@ -274,6 +287,16 @@ dependencies = [ "serde", ] +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -286,7 +309,7 @@ version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.4", "crossterm_winapi", "libc", "mio 0.8.11", @@ -305,18 +328,48 @@ dependencies = [ "winapi", ] +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "either" version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + [[package]] name = "equivalent" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + [[package]] name = "eyre" version = "0.6.12" @@ -327,25 +380,114 @@ dependencies = [ "once_cell", ] +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "find-msvc-tools" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0399f9d26e5191ce32c498bebd31e7a3ceabc2745f0ac54af3f335126c3f24b3" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + [[package]] name = "foldhash" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" -name = "errno" -version = "0.3.13" + +[[package]] +name = "foreign-types" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" dependencies = [ - "libc", - "windows-sys 0.60.2", + "foreign-types-shared", ] [[package]] -name = "fastrand" -version = "2.3.0" +name = "foreign-types-shared" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", +] [[package]] name = "getrandom" @@ -356,20 +498,39 @@ dependencies = [ "cfg-if", "libc", "r-efi", - "wasi 0.14.2+wasi-0.2.4", + "wasi 0.14.7+wasi-0.2.4", ] [[package]] name = "gimli" -version = "0.31.1" +version = "0.32.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" +checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" [[package]] name = "glob" -version = "0.3.2" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "h2" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" +checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] [[package]] name = "hashbrown" @@ -388,6 +549,126 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "http" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hyper" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "system-configuration", + "tokio", + "tower-service", + "tracing", + "windows-registry", +] + [[package]] name = "iana-time-zone" version = "0.1.64" @@ -412,6 +693,113 @@ dependencies = [ "cc", ] +[[package]] +name = "icu_collections" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" + +[[package]] +name = "icu_properties" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "potential_utf", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" + +[[package]] +name = "icu_provider" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" +dependencies = [ + "displaydoc", + "icu_locale_core", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + [[package]] name = "indenter" version = "0.3.4" @@ -425,17 +813,43 @@ dependencies = [ "core", ] +[[package]] +name = "indexmap" +version = "2.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5" +dependencies = [ + "equivalent", + "hashbrown", +] + [[package]] name = "io-uring" -version = "0.7.9" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d93587f37623a1a17d94ef2bc9ada592f5465fe7732084ab7beefabe5c77c0c4" +checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.4", "cfg-if", "libc", ] +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "iri-string" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "itertools" version = "0.12.1" @@ -462,19 +876,19 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "jobserver" -version = "0.1.33" +version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" dependencies = [ - "getrandom", + "getrandom 0.3.3", "libc", ] [[package]] name = "js-sys" -version = "0.3.80" +version = "0.3.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "852f13bec5eba4ba9afbeb93fd7c13fe56147f055939ae21c43a29a0ecb2702e" +checksum = "ec48937a97411dcb524a265206ccd4c90bb711fca92b2792c407f268825b9305" dependencies = [ "once_cell", "wasm-bindgen", @@ -494,18 +908,18 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "libc" -version = "0.2.174" +version = "0.2.176" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" +checksum = "58f929b4d672ea937a23a1ab494143d968337a5f47e56d0815df1e0890ddf174" [[package]] name = "libloading" -version = "0.8.8" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" dependencies = [ "cfg-if", - "windows-targets 0.53.2", + "windows-link 0.2.1", ] [[package]] @@ -535,21 +949,32 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "linux-raw-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" + +[[package]] +name = "litemap" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" + [[package]] name = "lock_api" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" dependencies = [ - "autocfg", "scopeguard", ] [[package]] name = "log" -version = "0.4.27" +version = "0.4.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" [[package]] name = "lru" @@ -559,10 +984,6 @@ checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" dependencies = [ "hashbrown", ] -name = "linux-raw-sys" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" [[package]] name = "lz4-sys" @@ -576,9 +997,25 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.5" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] [[package]] name = "minimal-lexical" @@ -619,8 +1056,25 @@ dependencies = [ ] [[package]] -name = "nom" -version = "7.1.3" +name = "native-tls" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "nom" +version = "7.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" dependencies = [ @@ -639,9 +1093,9 @@ dependencies = [ [[package]] name = "object" -version = "0.36.7" +version = "0.37.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" dependencies = [ "memchr", ] @@ -652,17 +1106,61 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "openssl" +version = "0.10.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" +dependencies = [ + "bitflags 2.9.4", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + +[[package]] +name = "openssl-sys" +version = "0.9.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "owo-colors" -version = "4.2.2" +version = "4.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48dd4f4a2c8405440fd0462561f0e5806bd0f77e86f51c761481bdd4018b545e" +checksum = "9c6901729fa79e91a0913333229e9ca5dc725089d1c363b2f4b4760709dc4a52" [[package]] name = "parking_lot" -version = "0.12.4" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" dependencies = [ "lock_api", "parking_lot_core", @@ -670,15 +1168,15 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.11" +version = "0.9.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", - "windows-targets 0.52.6", + "windows-link 0.2.1", ] [[package]] @@ -693,23 +1191,44 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + [[package]] name = "pin-project-lite" version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + [[package]] name = "pkg-config" version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "potential_utf" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a" +dependencies = [ + "zerovec", +] + [[package]] name = "prettyplease" -version = "0.2.34" +version = "0.2.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6837b9e10d61f45f987d50808f83d1ee3d206c66acf650c3e4ae2e1f6ddedf55" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", "syn", @@ -717,18 +1236,18 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.95" +version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.40" +version = "1.0.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" dependencies = [ "proc-macro2", ] @@ -745,7 +1264,7 @@ version = "0.26.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f44c9e68fd46eda15c646fbb85e1040b657a58cdc8c98db1d97a55930d991eef" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.4", "cassowary", "compact_str", "crossterm", @@ -761,18 +1280,18 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.17" +version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.4", ] [[package]] name = "regex" -version = "1.11.1" +version = "1.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +checksum = "8b5288124840bee7b386bc413c487869b360b2b4ec421ea56425128692f2a82c" dependencies = [ "aho-corasick", "memchr", @@ -782,9 +1301,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.9" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +checksum = "833eb9ce86d40ef33cb1306d8accf7bc8ec2bfea4355cbdebb3df68b40925cad" dependencies = [ "aho-corasick", "memchr", @@ -793,9 +1312,66 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.5" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" + +[[package]] +name = "reqwest" +version = "0.12.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d429f34c8092b2d42c7c93cec323bb4adeb7c67698f70839adec842ec10c7ceb" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-tls", + "hyper-util", + "js-sys", + "log", + "mime", + "mime_guess", + "native-tls", + "percent-encoding", + "pin-project-lite", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-native-tls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "ring" +version = "0.17.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.16", + "libc", + "untrusted", + "windows-sys 0.52.0", +] [[package]] name = "rocksdb" @@ -825,6 +1401,52 @@ version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" +[[package]] +name = "rustix" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" +dependencies = [ + "bitflags 2.9.4", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd3c25631629d034ce7cd9940adc9d45762d46de2b0f57193c4443b92c6d4d40" +dependencies = [ + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10b3f4191e8a80e6b43eebabfac91e5dcecebb27a71f04e820c47ec41d314bf" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + [[package]] name = "rustversion" version = "1.0.22" @@ -837,43 +1459,99 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +[[package]] +name = "schannel" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" -name = "rustix" -version = "1.0.7" + +[[package]] +name = "security-framework" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags 2.9.1", - "errno", + "bitflags 2.9.4", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" +dependencies = [ + "core-foundation-sys", "libc", - "linux-raw-sys", - "windows-sys 0.59.0", ] [[package]] name = "serde" -version = "1.0.219" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.219" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", "syn", ] +[[package]] +name = "serde_json" +version = "1.0.145" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + [[package]] name = "server" version = "0.1.0" @@ -960,6 +1638,12 @@ dependencies = [ "syn", ] +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + [[package]] name = "static_assertions" version = "1.1.0" @@ -999,17 +1683,77 @@ dependencies = [ "syn", ] +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "syn" -version = "2.0.103" +version = "2.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4307e30089d6fd6aff212f2da3a1f9e32f3223b1f010fb09b7c95f90f3ca1e8" +checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags 2.9.4", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "tempfile" +version = "3.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" +dependencies = [ + "fastrand", + "getrandom 0.3.3", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + [[package]] name = "thread_local" version = "1.1.9" @@ -1019,6 +1763,16 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "tinystr" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" +dependencies = [ + "displaydoc", + "zerovec", +] + [[package]] name = "tokio" version = "1.47.1" @@ -1050,6 +1804,84 @@ dependencies = [ "syn", ] +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tower" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" +dependencies = [ + "bitflags 2.9.4", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + [[package]] name = "tracing" version = "0.1.41" @@ -1082,15 +1914,21 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.19" +version = "0.3.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" +checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5" dependencies = [ "sharded-slab", "thread_local", "tracing-core", ] +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + [[package]] name = "tui" version = "0.1.0" @@ -1102,26 +1940,24 @@ dependencies = [ "crossterm", "index", "ratatui", + "reqwest", "serde", + "serde_json", "storage", "tokio", -name = "tempfile" -version = "3.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" -dependencies = [ - "fastrand", - "getrandom", - "once_cell", - "rustix", - "windows-sys 0.60.2", ] +[[package]] +name = "unicase" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" + [[package]] name = "unicode-ident" -version = "1.0.18" +version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" [[package]] name = "unicode-segmentation" @@ -1146,6 +1982,30 @@ version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + [[package]] name = "valuable" version = "0.1.1" @@ -1158,6 +2018,15 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" @@ -1166,18 +2035,27 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasi" -version = "0.14.2+wasi-0.2.4" +version = "0.14.7+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "883478de20367e224c0090af9cf5f9fa85bed63a95c1abf3afc5c083ebc06e8c" +dependencies = [ + "wasip2", +] + +[[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" dependencies = [ - "wit-bindgen-rt", + "wit-bindgen", ] [[package]] name = "wasm-bindgen" -version = "0.2.103" +version = "0.2.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab10a69fbd0a177f5f649ad4d8d3305499c42bab9aef2f7ff592d0ec8f833819" +checksum = "c1da10c01ae9f1ae40cbfac0bac3b1e724b320abfcf52229f80b547c0d250e2d" dependencies = [ "cfg-if", "once_cell", @@ -1188,9 +2066,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.103" +version = "0.2.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bb702423545a6007bbc368fde243ba47ca275e549c8a28617f56f6ba53b1d1c" +checksum = "671c9a5a66f49d8a47345ab942e2cb93c7d1d0339065d4f8139c486121b43b19" dependencies = [ "bumpalo", "log", @@ -1200,11 +2078,24 @@ dependencies = [ "wasm-bindgen-shared", ] +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e038d41e478cc73bae0ff9b36c60cff1c98b8f38f8d7e8061e79ee63608ac5c" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "wasm-bindgen-macro" -version = "0.2.103" +version = "0.2.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc65f4f411d91494355917b605e1480033152658d71f722a90647f56a70c88a0" +checksum = "7ca60477e4c59f5f2986c50191cd972e3a50d8a95603bc9434501cf156a9a119" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1212,9 +2103,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.103" +version = "0.2.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffc003a991398a8ee604a401e194b6b3a39677b3173d6e74495eb51b82e99a32" +checksum = "9f07d2f20d4da7b26400c9f4a0511e6e0345b040694e8a75bd41d578fa4421d7" dependencies = [ "proc-macro2", "quote", @@ -1225,13 +2116,23 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.103" +version = "0.2.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "293c37f4efa430ca14db3721dfbe48d8c33308096bd44d80ebaa775ab71ba1cf" +checksum = "bad67dc8b2a1a6e5448428adec4c3e84c43e561d8c9ee8a9e5aabeb193ec41d1" dependencies = [ "unicode-ident", ] +[[package]] +name = "web-sys" +version = "0.3.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9367c417a924a74cae129e6a2ae3b47fabb1f8995595ab474029da749a8be120" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "winapi" version = "0.3.9" @@ -1256,22 +2157,22 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows-core" -version = "0.62.0" +version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57fe7168f7de578d2d8a05b07fd61870d2e73b4020e9f49aa00da8471723497c" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ "windows-implement", "windows-interface", - "windows-link", - "windows-result", - "windows-strings", + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", ] [[package]] name = "windows-implement" -version = "0.60.0" +version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", @@ -1280,9 +2181,9 @@ dependencies = [ [[package]] name = "windows-interface" -version = "0.59.1" +version = "0.59.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", @@ -1291,26 +2192,61 @@ dependencies = [ [[package]] name = "windows-link" -version = "0.2.0" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-registry" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" +dependencies = [ + "windows-link 0.1.3", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] [[package]] name = "windows-result" -version = "0.4.0" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7084dcc306f89883455a206237404d3eaf961e5bd7e0f312f7c91f57eb44167f" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" dependencies = [ - "windows-link", + "windows-link 0.2.1", ] [[package]] name = "windows-strings" -version = "0.5.0" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7218c655a553b0bed4426cf54b20d7ba363ef543b52d515b3e48d7fd55318dda" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" dependencies = [ - "windows-link", + "windows-link 0.2.1", ] [[package]] @@ -1322,6 +2258,15 @@ dependencies = [ "windows-targets 0.48.5", ] +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.59.0" @@ -1331,6 +2276,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link 0.2.1", +] + [[package]] name = "windows-targets" version = "0.48.5" @@ -1344,12 +2298,6 @@ dependencies = [ "windows_x86_64_gnu 0.48.5", "windows_x86_64_gnullvm 0.48.5", "windows_x86_64_msvc 0.48.5", -name = "windows-sys" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" -dependencies = [ - "windows-targets 0.53.2", ] [[package]] @@ -1361,29 +2309,13 @@ dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm 0.52.6", + "windows_i686_gnullvm", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] -[[package]] -name = "windows-targets" -version = "0.53.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c66f69fcc9ce11da9966ddb31a40968cad001c5bedeb5c2b82ede4253ab48aef" -dependencies = [ - "windows_aarch64_gnullvm 0.53.0", - "windows_aarch64_msvc 0.53.0", - "windows_i686_gnu 0.53.0", - "windows_i686_gnullvm 0.53.0", - "windows_i686_msvc 0.53.0", - "windows_x86_64_gnu 0.53.0", - "windows_x86_64_gnullvm 0.53.0", - "windows_x86_64_msvc 0.53.0", -] - [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" @@ -1396,12 +2328,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" - [[package]] name = "windows_aarch64_msvc" version = "0.48.5" @@ -1414,12 +2340,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" -[[package]] -name = "windows_aarch64_msvc" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" - [[package]] name = "windows_i686_gnu" version = "0.48.5" @@ -1432,24 +2352,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" -[[package]] -name = "windows_i686_gnu" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" - [[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_gnullvm" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" - [[package]] name = "windows_i686_msvc" version = "0.48.5" @@ -1462,12 +2370,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" -[[package]] -name = "windows_i686_msvc" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" - [[package]] name = "windows_x86_64_gnu" version = "0.48.5" @@ -1480,12 +2382,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" -[[package]] -name = "windows_x86_64_gnu" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" - [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" @@ -1498,12 +2394,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" - [[package]] name = "windows_x86_64_msvc" version = "0.48.5" @@ -1517,27 +2407,108 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] -name = "windows_x86_64_msvc" -version = "0.53.0" +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + +[[package]] +name = "writeable" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" + +[[package]] +name = "yoke" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" +checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] [[package]] -name = "wit-bindgen-rt" -version = "0.39.0" +name = "zerofrom" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" dependencies = [ - "bitflags 2.9.1", + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zerotrie" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" +dependencies = [ + "proc-macro2", + "quote", + "syn", ] [[package]] name = "zstd-sys" -version = "2.0.15+zstd.1.5.7" +version = "2.0.16+zstd.1.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb81183ddd97d0c74cedf1d50d85c8d08c1b8b68ee863bdee9e706eedba1a237" +checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" dependencies = [ - "bindgen 0.71.1", + "bindgen 0.72.1", "cc", "pkg-config", ] diff --git a/crates/tui/Cargo.toml b/crates/tui/Cargo.toml index 8c319f4..39ed787 100644 --- a/crates/tui/Cargo.toml +++ b/crates/tui/Cargo.toml @@ -14,3 +14,5 @@ chrono = { version = "0.4", features = ["serde"] } core = { path = "../core" } storage = { path = "../storage" } index = { path = "../index" } +serde_json.workspace = true +reqwest = { version = "0.12", features = ["json", "blocking", "multipart"] } diff --git a/crates/tui/src/app/embeddings.rs b/crates/tui/src/app/embeddings.rs new file mode 100644 index 0000000..161eda4 --- /dev/null +++ b/crates/tui/src/app/embeddings.rs @@ -0,0 +1,149 @@ +use reqwest::blocking::{multipart, Client, Response}; +use reqwest::StatusCode; +use serde::de::DeserializeOwned; +use serde::Deserialize; +use std::env; +use std::path::Path; +use std::time::Duration; + +#[derive(Debug)] +pub struct EmbeddingClient { + client: Client, + text_url: String, + sentence_url: String, + image_url: String, +} + +#[derive(Debug)] +pub enum EmbeddingError { + Io(std::io::Error), + Http(reqwest::Error), + UnexpectedStatus(StatusCode, String), +} + +impl std::fmt::Display for EmbeddingError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + EmbeddingError::Io(err) => write!(f, "I/O error: {err}"), + EmbeddingError::Http(err) => write!(f, "HTTP error: {err}"), + EmbeddingError::UnexpectedStatus(status, body) => { + write!(f, "Server returned {status}: {body}") + } + } + } +} + +impl std::error::Error for EmbeddingError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + EmbeddingError::Io(err) => Some(err), + EmbeddingError::Http(err) => Some(err), + EmbeddingError::UnexpectedStatus(_, _) => None, + } + } +} + +impl From for EmbeddingError { + fn from(value: std::io::Error) -> Self { + Self::Io(value) + } +} + +impl From for EmbeddingError { + fn from(value: reqwest::Error) -> Self { + Self::Http(value) + } +} + +impl EmbeddingClient { + pub fn new() -> Self { + let client = Client::builder() + .timeout(Duration::from_secs(15)) + .build() + .unwrap_or_else(|_| Client::new()); + let text_url = env::var("TEXT_EMBEDDING_URL").expect("TEXT_EMBEDDING_URL must be set"); + let sentence_url = + env::var("SENTENCE_EMBEDDING_URL").expect("SENTENCE_EMBEDDING_URL must be set"); + let image_url = env::var("IMAGE_EMBEDDING_URL").expect("IMAGE_EMBEDDING_URL must be set"); + + Self { + client, + text_url, + sentence_url, + image_url, + } + } + + pub fn text_embeddings(&self, text: &str) -> Result, EmbeddingError> { + let payload = serde_json::json!({ "text": text }); + let response = self.client.post(&self.text_url).json(&payload).send()?; + + Self::parse_response::(response).map(|body| body.result) + } + + pub fn sentence_embeddings(&self, sentence: &str) -> Result, EmbeddingError> { + let payload = serde_json::json!({ + "text": sentence, + "config": { + "pooling_strategy": "mean" + } + }); + + let response = self.client.post(&self.sentence_url).json(&payload).send()?; + + Self::parse_response::(response).map(|body| body.vector) + } + + pub fn image_embeddings(&self, path: &Path) -> Result, EmbeddingError> { + let file = std::fs::File::open(path)?; + let file_name = path + .file_name() + .and_then(|name| name.to_str()) + .unwrap_or("image"); + + let part = multipart::Part::reader(file) + .file_name(file_name.to_string()) + .mime_str("application/octet-stream")?; + + let form = multipart::Form::new().part("file", part); + + let response = self.client.post(&self.image_url).multipart(form).send()?; + + Self::parse_response::(response).map(|body| body.result) + } + + fn parse_response(response: Response) -> Result + where + T: DeserializeOwned, + { + if response.status().is_success() { + let parsed = response.json::()?; + Ok(parsed) + } else { + let status = response.status(); + let body = response.text().unwrap_or_else(|_| "".to_string()); + Err(EmbeddingError::UnexpectedStatus(status, body)) + } + } +} + +impl Default for EmbeddingClient { + fn default() -> Self { + Self::new() + } +} + +#[derive(Debug, Deserialize)] +struct TextEmbeddingResponse { + result: Vec, +} + +#[derive(Debug, Deserialize)] +struct SentenceEmbeddingResponse { + vector: Vec, +} + +#[derive(Debug, Deserialize)] +struct ImageEmbeddingResponse { + result: Vec, +} diff --git a/crates/tui/src/app/events.rs b/crates/tui/src/app/events.rs index 0ec7f0c..dde3b94 100644 --- a/crates/tui/src/app/events.rs +++ b/crates/tui/src/app/events.rs @@ -197,26 +197,24 @@ fn handle_general_keys(app: &mut App, key: KeyEvent) { match key.code { KeyCode::Char('q') | KeyCode::Esc => app.quit(), KeyCode::Right => app.next_page(), - KeyCode::Enter => { - // Map options with vector operations page - // TODO: Add semantic search - if matches!(app.state, AppState::VectorOperations) { - match app.vector_selected { - 0 => { - if let Err(err) = initialize_vector_listing(app) { - app.modal.show_error(err.to_string()); - } + KeyCode::Enter => match app.state { + AppState::VectorOperations => match app.vector_selected { + 0 => { + if let Err(err) = initialize_vector_listing(app) { + app.modal.show_error(err.to_string()); } - 1 => app.modal.show_get_vector(), - 2 => app.modal.show_insert_vector(), - 3 => app.modal.show_delete_vector(), - 4 => app.modal.show_search_similar_vectors(), - _ => {} } - } else { - app.next_page() - } - } + 1 => app.modal.show_get_vector(), + 2 => app.modal.show_insert_vector(), + 3 => app.modal.show_delete_vector(), + 4 => app.modal.show_search_similar_vectors(), + 5 => app.modal.show_text_embedding(), + 6 => app.modal.show_sentence_embedding(), + 7 => app.modal.show_image_embedding(), + _ => {} + }, + _ => app.next_page(), + }, KeyCode::Left => app.previous_page(), KeyCode::Up => app.select_previous(), KeyCode::Down => app.select_next(), @@ -452,6 +450,165 @@ fn execute_modal_action(app: &mut App) -> io::Result<()> { let len = app.vector_list_items.len(); app.modal.set_selected_index(0, len); } + Some(ModalType::TextEmbedding) => { + let id_text = app.modal.get_input_value(); + let text_raw = app.modal.secondary_input().to_string(); + let payload_text = app.modal.tertiary_input().to_string(); + + let trimmed_text = text_raw.trim(); + + let id = id_text.parse::().map_err(|_| { + io::Error::new(io::ErrorKind::InvalidInput, "IDs should be an integer!") + })?; + + if trimmed_text.is_empty() { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "Text cannot be empty!", + )); + } + + let Some(storage) = &app.database.storage_engine else { + app.modal.show_error("No database selected!"); + return Ok(()); + }; + + match app.embeddings.text_embeddings(trimmed_text) { + Ok(vector) => { + let dims = vector.len(); + let payload_opt = if payload_text.trim().is_empty() { + None + } else { + Some(Payload {}) + }; + + match storage + .insert_point(id, Some(vector), payload_opt) + .map_err(to_io) + { + Ok(()) => app.modal.show_success(format!( + "Text embedding inserted (id={id}, {dims} dims)." + )), + Err(err) => app + .modal + .show_failure(format!("Failed to insert embedding: {err}")), + } + } + Err(err) => app + .modal + .show_failure(format!("Failed to generate embedding: {err}")), + } + } + Some(ModalType::SentenceEmbedding) => { + let id_text = app.modal.get_input_value(); + let sentence_raw = app.modal.secondary_input().to_string(); + let payload_text = app.modal.tertiary_input().to_string(); + + let trimmed_sentence = sentence_raw.trim(); + + let id = id_text.parse::().map_err(|_| { + io::Error::new(io::ErrorKind::InvalidInput, "IDs should be an integer!") + })?; + + if trimmed_sentence.is_empty() { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "Sentence cannot be empty!", + )); + } + + let Some(storage) = &app.database.storage_engine else { + app.modal.show_error("No database selected!"); + return Ok(()); + }; + + match app.embeddings.sentence_embeddings(trimmed_sentence) { + Ok(vector) => { + let dims = vector.len(); + let payload_opt = if payload_text.trim().is_empty() { + None + } else { + Some(Payload {}) + }; + + match storage + .insert_point(id, Some(vector), payload_opt) + .map_err(to_io) + { + Ok(()) => app.modal.show_success(format!( + "Sentence embedding inserted (id={id}, {dims} dims)." + )), + Err(err) => app + .modal + .show_failure(format!("Failed to insert embedding: {err}")), + } + } + Err(err) => app + .modal + .show_failure(format!("Failed to generate embedding: {err}")), + } + } + Some(ModalType::ImageEmbedding) => { + let id_text = app.modal.get_input_value(); + let path_raw = app.modal.secondary_input().to_string(); + let payload_text = app.modal.tertiary_input().to_string(); + + let trimmed_path = path_raw.trim(); + + let id = id_text.parse::().map_err(|_| { + io::Error::new(io::ErrorKind::InvalidInput, "IDs should be an integer!") + })?; + + if trimmed_path.is_empty() { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "Image path cannot be empty!", + )); + } + + let Some(storage) = &app.database.storage_engine else { + app.modal.show_error("No database selected!"); + return Ok(()); + }; + + let path = PathBuf::from(trimmed_path); + if !path.exists() { + app.modal + .show_failure(format!("Image not found at path '{trimmed_path}'")); + return Ok(()); + } + if !path.is_file() { + app.modal + .show_failure(format!("Path '{trimmed_path}' is not a file")); + return Ok(()); + } + + match app.embeddings.image_embeddings(&path) { + Ok(vector) => { + let dims = vector.len(); + let payload_opt = if payload_text.trim().is_empty() { + None + } else { + Some(Payload {}) + }; + + match storage + .insert_point(id, Some(vector), payload_opt) + .map_err(to_io) + { + Ok(()) => app.modal.show_success(format!( + "Image embedding inserted (id={id}, {dims} dims)." + )), + Err(err) => app + .modal + .show_failure(format!("Failed to insert embedding: {err}")), + } + } + Err(err) => app + .modal + .show_failure(format!("Failed to generate embedding: {err}")), + } + } _ => {} } Ok(()) diff --git a/crates/tui/src/app/mod.rs b/crates/tui/src/app/mod.rs index 5eb6622..d552b7b 100644 --- a/crates/tui/src/app/mod.rs +++ b/crates/tui/src/app/mod.rs @@ -1,4 +1,5 @@ mod database; +mod embeddings; mod events; mod modal; mod state; @@ -17,6 +18,7 @@ pub struct App { pub vector_selected: usize, pub database: database::DatabaseManager, pub modal: modal::ModalManager, + pub embeddings: embeddings::EmbeddingClient, pub vector_list_items: Vec, pub vector_list_next_offset: Option, @@ -42,6 +44,7 @@ impl App { vector_selected: 0, database: database::DatabaseManager::new(), modal: modal::ModalManager::new(), + embeddings: embeddings::EmbeddingClient::default(), vector_list_items: Vec::new(), vector_list_next_offset: None, @@ -105,12 +108,16 @@ impl App { self.modal.select_next(max_items); } else { let max_items = db::get_db_operations_count(); - self.db_selected = (self.db_selected + 1).min(max_items - 1); + if max_items > 0 { + self.db_selected = (self.db_selected + 1).min(max_items - 1); + } } } AppState::VectorOperations => { let max_items = vector_operations::get_vector_operations_count(); - self.vector_selected = (self.vector_selected + 1).min(max_items - 1); + if max_items > 0 { + self.vector_selected = (self.vector_selected + 1).min(max_items - 1); + } } } } diff --git a/crates/tui/src/app/modal.rs b/crates/tui/src/app/modal.rs index 31bbc90..bc0a539 100644 --- a/crates/tui/src/app/modal.rs +++ b/crates/tui/src/app/modal.rs @@ -117,6 +117,42 @@ impl ModalManager { self.error_message = None; } + pub fn show_text_embedding(&mut self) { + self.show_modal = true; + self.modal_type = Some(ModalType::TextEmbedding); + self.input_buffer.clear(); + self.secondary_input.clear(); + self.tertiary_input.clear(); + self.active_field = 0; + self.input_mode = true; + self.selected_index = 0; + self.error_message = None; + } + + pub fn show_sentence_embedding(&mut self) { + self.show_modal = true; + self.modal_type = Some(ModalType::SentenceEmbedding); + self.input_buffer.clear(); + self.secondary_input.clear(); + self.tertiary_input.clear(); + self.active_field = 0; + self.input_mode = true; + self.selected_index = 0; + self.error_message = None; + } + + pub fn show_image_embedding(&mut self) { + self.show_modal = true; + self.modal_type = Some(ModalType::ImageEmbedding); + self.input_buffer.clear(); + self.secondary_input.clear(); + self.tertiary_input.clear(); + self.active_field = 0; + self.input_mode = true; + self.selected_index = 0; + self.error_message = None; + } + pub fn show_vector_list(&mut self) { self.show_modal = true; self.modal_type = Some(ModalType::ListVectors); @@ -298,6 +334,21 @@ impl ModalManager { ("Enter Delete".into(), Color::Green), ("Esc Cancel".into(), Color::Red), ], + Some(TextEmbedding) => vec![ + ("Tab Next".into(), Color::Gray), + ("Enter Insert".into(), Color::Green), + ("Esc Cancel".into(), Color::Red), + ], + Some(SentenceEmbedding) => vec![ + ("Tab Next".into(), Color::Gray), + ("Enter Insert".into(), Color::Green), + ("Esc Cancel".into(), Color::Red), + ], + Some(ImageEmbedding) => vec![ + ("Tab Next".into(), Color::Gray), + ("Enter Insert".into(), Color::Green), + ("Esc Cancel".into(), Color::Red), + ], Some(ListVectors) => vec![ ("↑ Scroll".into(), Color::Gray), ("↓ Scroll".into(), Color::Gray), diff --git a/crates/tui/src/app/state.rs b/crates/tui/src/app/state.rs index 01a0e6b..ef49f36 100644 --- a/crates/tui/src/app/state.rs +++ b/crates/tui/src/app/state.rs @@ -23,6 +23,9 @@ pub enum ModalType { InsertVector, DeleteVector, SearchSimilarVectors, + TextEmbedding, + SentenceEmbedding, + ImageEmbedding, } #[derive(Debug, Clone)] diff --git a/crates/tui/src/ui/modal.rs b/crates/tui/src/ui/modal.rs index 59a7ea6..a623512 100644 --- a/crates/tui/src/ui/modal.rs +++ b/crates/tui/src/ui/modal.rs @@ -34,6 +34,9 @@ pub fn render_modal(f: &mut Frame, app: &App) { render_search_similar_vectors_modal(f, app, popup_area) } Some(ModalType::DeleteVector) => render_delete_vector_modal(f, app, popup_area), + Some(ModalType::TextEmbedding) => render_text_embedding_modal(f, app, popup_area), + Some(ModalType::SentenceEmbedding) => render_sentence_embedding_modal(f, app, popup_area), + Some(ModalType::ImageEmbedding) => render_image_embedding_modal(f, app, popup_area), Some(ModalType::ListVectors) => render_vector_list_modal(f, app, popup_area), Some(ModalType::VectorDetails) => render_vector_details_modal(f, app, popup_area), Some(ModalType::Success) => render_success_modal(f, app, popup_area), @@ -159,6 +162,96 @@ fn render_delete_vector_modal(f: &mut Frame, app: &App, area: Rect) { } } +fn render_text_embedding_modal(f: &mut Frame, app: &App, area: Rect) { + let block = Block::default() + .title("Generate Text Embedding") + .borders(Borders::ALL); + + let lines = vec![ + Line::from(Span::raw("ID (int):")), + Line::from(Span::raw(app.input_buffer().to_string())), + Line::from(Span::raw("")), + Line::from(Span::raw("Text:")), + Line::from(Span::raw(app.secondary_input().to_string())), + Line::from(Span::raw("")), + Line::from(Span::raw("Payload (optional):")), + Line::from(Span::raw(app.tertiary_input().to_string())), + ]; + + let input = Paragraph::new(lines) + .block(block) + .wrap(ratatui::widgets::Wrap { trim: true }); + f.render_widget(input, area); + + if app.input_mode() { + match app.active_field() { + 0 => f.set_cursor(area.x + 1 + app.input_buffer().len() as u16, area.y + 2), + 1 => f.set_cursor(area.x + 1 + app.secondary_input().len() as u16, area.y + 5), + _ => f.set_cursor(area.x + 1 + app.tertiary_input().len() as u16, area.y + 8), + } + } +} + +fn render_sentence_embedding_modal(f: &mut Frame, app: &App, area: Rect) { + let block = Block::default() + .title("Generate Sentence Embedding") + .borders(Borders::ALL); + + let lines = vec![ + Line::from(Span::raw("ID (int):")), + Line::from(Span::raw(app.input_buffer().to_string())), + Line::from(Span::raw("")), + Line::from(Span::raw("Sentence:")), + Line::from(Span::raw(app.secondary_input().to_string())), + Line::from(Span::raw("")), + Line::from(Span::raw("Payload (optional):")), + Line::from(Span::raw(app.tertiary_input().to_string())), + ]; + + let input = Paragraph::new(lines) + .block(block) + .wrap(ratatui::widgets::Wrap { trim: true }); + f.render_widget(input, area); + + if app.input_mode() { + match app.active_field() { + 0 => f.set_cursor(area.x + 1 + app.input_buffer().len() as u16, area.y + 2), + 1 => f.set_cursor(area.x + 1 + app.secondary_input().len() as u16, area.y + 5), + _ => f.set_cursor(area.x + 1 + app.tertiary_input().len() as u16, area.y + 8), + } + } +} + +fn render_image_embedding_modal(f: &mut Frame, app: &App, area: Rect) { + let block = Block::default() + .title("Generate Image Embedding") + .borders(Borders::ALL); + + let lines = vec![ + Line::from(Span::raw("ID (int):")), + Line::from(Span::raw(app.input_buffer().to_string())), + Line::from(Span::raw("")), + Line::from(Span::raw("Image Path:")), + Line::from(Span::raw(app.secondary_input().to_string())), + Line::from(Span::raw("")), + Line::from(Span::raw("Payload (optional):")), + Line::from(Span::raw(app.tertiary_input().to_string())), + ]; + + let input = Paragraph::new(lines) + .block(block) + .wrap(ratatui::widgets::Wrap { trim: true }); + f.render_widget(input, area); + + if app.input_mode() { + match app.active_field() { + 0 => f.set_cursor(area.x + 1 + app.input_buffer().len() as u16, area.y + 2), + 1 => f.set_cursor(area.x + 1 + app.secondary_input().len() as u16, area.y + 5), + _ => f.set_cursor(area.x + 1 + app.tertiary_input().len() as u16, area.y + 8), + } + } +} + fn render_database_list_modal(f: &mut Frame, app: &App, area: Rect) { let items: Vec = app .available_databases() diff --git a/crates/tui/src/ui/vector_operations.rs b/crates/tui/src/ui/vector_operations.rs index 153bbbe..a09f629 100644 --- a/crates/tui/src/ui/vector_operations.rs +++ b/crates/tui/src/ui/vector_operations.rs @@ -15,6 +15,9 @@ const VECTOR_OPERATIONS: &[&str] = &[ "Insert Vector", "Delete Vector", "Search Similar Vectors", + "Insert Text Embedding", + "Insert Sentence Embedding", + "Insert Image Embedding", ]; fn get_vector_items() -> Vec> { From d0ae0b19e1b058b18c0f4aad2bb666f09763f304 Mon Sep 17 00:00:00 2001 From: Entity069 Date: Fri, 10 Oct 2025 17:22:57 +0530 Subject: [PATCH 13/25] fix: add dotenv --- crates/tui/Cargo.toml | 1 + crates/tui/src/app/embeddings.rs | 1 + 2 files changed, 2 insertions(+) diff --git a/crates/tui/Cargo.toml b/crates/tui/Cargo.toml index 39ed787..539e82b 100644 --- a/crates/tui/Cargo.toml +++ b/crates/tui/Cargo.toml @@ -16,3 +16,4 @@ storage = { path = "../storage" } index = { path = "../index" } serde_json.workspace = true reqwest = { version = "0.12", features = ["json", "blocking", "multipart"] } +dotenv = "0.15" \ No newline at end of file diff --git a/crates/tui/src/app/embeddings.rs b/crates/tui/src/app/embeddings.rs index 161eda4..a186906 100644 --- a/crates/tui/src/app/embeddings.rs +++ b/crates/tui/src/app/embeddings.rs @@ -57,6 +57,7 @@ impl From for EmbeddingError { impl EmbeddingClient { pub fn new() -> Self { + dotenv::dotenv().ok(); let client = Client::builder() .timeout(Duration::from_secs(15)) .build() From c9dc6241f9b28091ff45ad4b6fe66d3c3794c61f Mon Sep 17 00:00:00 2001 From: Entity069 Date: Fri, 17 Oct 2025 21:35:13 +0530 Subject: [PATCH 14/25] fix: close current storage engine before attempting to open another --- crates/tui/src/app/database.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/crates/tui/src/app/database.rs b/crates/tui/src/app/database.rs index 309addc..796c772 100644 --- a/crates/tui/src/app/database.rs +++ b/crates/tui/src/app/database.rs @@ -20,7 +20,15 @@ impl DatabaseManager { } } + fn close_current_database(&mut self) { + self.storage_engine = None; + self.current_db_path = None; + self.selected_database = None; + } + pub fn create_new_database(&mut self, name: String, path: PathBuf) -> io::Result<()> { + // Drop any database currently open before creating a new one + self.close_current_database(); // Check if database already exists to avoid name collisions if path.exists() { return Err(io::Error::new( @@ -45,6 +53,8 @@ impl DatabaseManager { } pub fn select_database(&mut self, name: String, path: PathBuf) -> io::Result<()> { + self.close_current_database(); + if !path.exists() { return Err(io::Error::new( io::ErrorKind::NotFound, From 96bca3ecf65b1ede3065dd5bbfa6f45dde1e0c97 Mon Sep 17 00:00:00 2001 From: Entity069 Date: Sat, 25 Oct 2025 08:22:10 +0530 Subject: [PATCH 15/25] fix: change id from u64 to uuidv4;complete Payload struct --- crates/api/Cargo.toml | 1 + crates/api/src/lib.rs | 52 ++++++++++++++++++++++++++-------- crates/core/Cargo.toml | 3 +- crates/core/src/types.rs | 12 ++++++-- crates/index/Cargo.toml | 2 +- crates/index/src/flat.rs | 60 +++++++++++++++++++++++++--------------- 6 files changed, 92 insertions(+), 38 deletions(-) diff --git a/crates/api/Cargo.toml b/crates/api/Cargo.toml index c21bcb7..af62411 100644 --- a/crates/api/Cargo.toml +++ b/crates/api/Cargo.toml @@ -9,3 +9,4 @@ core = { path = "../core" } index = { path = "../index" } storage = { path = "../storage" } tempfile = "3.20.0" +uuid.workspace = true \ No newline at end of file diff --git a/crates/api/src/lib.rs b/crates/api/src/lib.rs index 4f53fe9..897d0d1 100644 --- a/crates/api/src/lib.rs +++ b/crates/api/src/lib.rs @@ -2,7 +2,7 @@ use core::{DbError, IndexedVector, Similarity}; use core::{DenseVector, Payload, Point, PointId}; use std::path::PathBuf; -use std::sync::atomic::{AtomicU64, Ordering}; +// use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::{Arc, RwLock}; use index::flat::FlatIndex; @@ -10,10 +10,16 @@ use index::{IndexType, VectorIndex}; use storage::rocks_db::RocksDbStorage; use storage::{StorageEngine, StorageType}; -static NEXT_ID: AtomicU64 = AtomicU64::new(1); +use uuid::Uuid; -fn generate_point_id() -> u64 { - NEXT_ID.fetch_add(1, Ordering::Relaxed) +// static NEXT_ID: AtomicU64 = AtomicU64::new(1); + +// fn generate_point_id() -> u64 { +// NEXT_ID.fetch_add(1, Ordering::Relaxed) +// } + +fn generate_point_id() -> PointId { + Uuid::new_v4() } pub struct VectorDb { @@ -116,6 +122,7 @@ mod tests { // TODO: Add more exhaustive tests use super::*; + use core::ContentType; use tempfile::tempdir; // Helper function to create a test database @@ -134,24 +141,35 @@ mod tests { fn test_insert_and_get() { let db = create_test_db(); let vector = vec![1.0, 2.0, 3.0]; - let payload = Payload {}; + let payload = Payload { + content_type: ContentType::Text, + content: "Test content".to_string(), + }; // Test insert - let id = db.insert(vector.clone(), payload).unwrap(); - assert!(id > 0); + let id = db.insert(vector.clone(), payload.clone()).unwrap(); + assert!(id != Uuid::nil()); // Test get let point = db.get(id).unwrap().unwrap(); assert_eq!(point.id, id); assert_eq!(point.vector.as_ref().unwrap(), &vector); assert_eq!(point.payload.as_ref().unwrap(), &payload); + assert_eq!( + point.payload.as_ref().unwrap().content_type, + ContentType::Text + ); + assert_eq!(point.payload.as_ref().unwrap().content, "Test content"); } #[test] fn test_delete() { let db = create_test_db(); let vector = vec![1.0, 2.0, 3.0]; - let payload = Payload {}; + let payload = Payload { + content_type: ContentType::Text, + content: "Test content".to_string(), + }; // Insert a point let id = db.insert(vector, payload).unwrap(); @@ -174,7 +192,11 @@ mod tests { let mut ids = Vec::new(); for vector in vectors { - let id = db.insert(vector, Payload {}).unwrap(); + let payload = Payload { + content_type: ContentType::Text, + content: format!("Test content {vector:?}"), + }; + let id = db.insert(vector, payload).unwrap(); ids.push(id); } @@ -194,7 +216,15 @@ mod tests { let mut ids = Vec::new(); for i in 0..5 { let vector = vec![i as f32, 0.0, 0.0]; - let id = db.insert(vector, Payload {}).unwrap(); + let id = db + .insert( + vector, + Payload { + content_type: ContentType::Text, + content: format!("Test content {i}"), + }, + ) + .unwrap(); ids.push(id); } @@ -210,7 +240,7 @@ mod tests { let db = create_test_db(); // Get non-existent point - assert!(db.get(999).unwrap().is_none()); + assert!(db.get(Uuid::new_v4()).unwrap().is_none()); let query = vec![1.0, 2.0, 3.0]; let results = db.search(query, Similarity::Cosine, 10).unwrap(); diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index c293076..19e3048 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -8,4 +8,5 @@ license = "MIT" [dependencies] bincode = "1.3.3" -serde = { version = "1.0", features = ["derive"] } \ No newline at end of file +serde = { version = "1.0", features = ["derive"] } +uuid.workspace = true \ No newline at end of file diff --git a/crates/core/src/types.rs b/crates/core/src/types.rs index 7230b58..8ca63fa 100644 --- a/crates/core/src/types.rs +++ b/crates/core/src/types.rs @@ -1,6 +1,7 @@ use serde::{Deserialize, Serialize}; +use uuid::Uuid; -pub type PointId = u64; +pub type PointId = Uuid; /// Type of vector element. pub type Element = f32; @@ -16,8 +17,15 @@ pub enum StoredVector { } #[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq)] +pub enum ContentType { + Text, + Image, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] pub struct Payload { - // Define here how payload is managed + pub content_type: ContentType, + pub content: String, } #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] diff --git a/crates/index/Cargo.toml b/crates/index/Cargo.toml index 501a5f8..a63f465 100644 --- a/crates/index/Cargo.toml +++ b/crates/index/Cargo.toml @@ -8,4 +8,4 @@ edition = "2021" [dependencies] core = { path = "../core" } - +uuid.workspace = true \ No newline at end of file diff --git a/crates/index/src/flat.rs b/crates/index/src/flat.rs index c78ead1..1e8a064 100644 --- a/crates/index/src/flat.rs +++ b/crates/index/src/flat.rs @@ -67,6 +67,7 @@ impl VectorIndex for FlatIndex { #[cfg(test)] mod tests { use super::*; + use uuid::Uuid; #[test] fn test_flat_index_new() { @@ -78,11 +79,11 @@ mod tests { fn test_flat_index_build() { let vectors = vec![ IndexedVector { - id: 1, + id: Uuid::new_v4(), vector: vec![1.0, 2.0, 3.0], }, IndexedVector { - id: 2, + id: Uuid::new_v4(), vector: vec![4.0, 5.0, 6.0], }, ]; @@ -94,7 +95,7 @@ mod tests { fn test_insert() { let mut index = FlatIndex::new(); let vector = IndexedVector { - id: 1, + id: Uuid::new_v4(), vector: vec![1.0, 2.0, 3.0], }; @@ -106,13 +107,14 @@ mod tests { #[test] fn test_delete_existing() { let mut index = FlatIndex::new(); + let existing_id = Uuid::new_v4(); let vector = IndexedVector { - id: 1, + id: existing_id, vector: vec![1.0, 2.0, 3.0], }; index.insert(vector).unwrap(); - let result = index.delete(1).unwrap(); + let result = index.delete(existing_id).unwrap(); assert!(result); assert_eq!(index.index.len(), 0); } @@ -121,12 +123,12 @@ mod tests { fn test_delete_non_existing() { let mut index = FlatIndex::new(); let vector = IndexedVector { - id: 1, + id: Uuid::new_v4(), vector: vec![1.0, 2.0, 3.0], }; index.insert(vector).unwrap(); - let result = index.delete(999).unwrap(); + let result = index.delete(Uuid::new_v4()).unwrap(); assert!(!result); assert_eq!(index.index.len(), 1); } @@ -134,21 +136,24 @@ mod tests { #[test] fn test_search_euclidean() { let mut index = FlatIndex::new(); + let id1 = Uuid::new_v4(); + let id2 = Uuid::new_v4(); + let id3 = Uuid::new_v4(); index .insert(IndexedVector { - id: 1, + id: id1, vector: vec![1.0, 1.0], }) .unwrap(); index .insert(IndexedVector { - id: 2, + id: id2, vector: vec![2.0, 2.0], }) .unwrap(); index .insert(IndexedVector { - id: 3, + id: id3, vector: vec![10.0, 10.0], }) .unwrap(); @@ -156,53 +161,59 @@ mod tests { let results = index .search(vec![0.0, 0.0], Similarity::Euclidean, 2) .unwrap(); - assert_eq!(results, vec![1, 2]); + assert_eq!(results, vec![id1, id2]); } #[test] fn test_search_cosine() { let mut index = FlatIndex::new(); + let id1 = Uuid::new_v4(); + let id2 = Uuid::new_v4(); + let id3 = Uuid::new_v4(); index .insert(IndexedVector { - id: 1, + id: id1, vector: vec![1.0, 0.0], }) .unwrap(); index .insert(IndexedVector { - id: 2, + id: id2, vector: vec![0.5, 0.5], }) .unwrap(); index .insert(IndexedVector { - id: 3, + id: id3, vector: vec![0.0, 1.0], }) .unwrap(); let results = index.search(vec![1.0, 1.0], Similarity::Cosine, 2).unwrap(); - assert_eq!(results, vec![2, 1]); + assert_eq!(results, vec![id2, id1]); } #[test] fn test_search_manhattan() { let mut index = FlatIndex::new(); + let id1 = Uuid::new_v4(); + let id2 = Uuid::new_v4(); + let id3 = Uuid::new_v4(); index .insert(IndexedVector { - id: 1, + id: id1, vector: vec![1.0, 1.0], }) .unwrap(); index .insert(IndexedVector { - id: 2, + id: id2, vector: vec![2.0, 2.0], }) .unwrap(); index .insert(IndexedVector { - id: 3, + id: id3, vector: vec![5.0, 5.0], }) .unwrap(); @@ -210,27 +221,30 @@ mod tests { let results = index .search(vec![0.0, 0.0], Similarity::Manhattan, 2) .unwrap(); - assert_eq!(results, vec![1, 2]); + assert_eq!(results, vec![id1, id2]); } #[test] fn test_search_hamming() { let mut index = FlatIndex::new(); + let id1 = Uuid::new_v4(); + let id2 = Uuid::new_v4(); + let id3 = Uuid::new_v4(); index .insert(IndexedVector { - id: 1, + id: id1, vector: vec![1.0, 0.0, 1.0, 0.0], }) .unwrap(); index .insert(IndexedVector { - id: 2, + id: id2, vector: vec![1.0, 0.0, 0.0, 0.0], }) .unwrap(); index .insert(IndexedVector { - id: 3, + id: id3, vector: vec![0.0, 0.0, 0.0, 0.0], }) .unwrap(); @@ -238,7 +252,7 @@ mod tests { let results = index .search(vec![1.0, 0.0, 0.0, 0.0], Similarity::Hamming, 2) .unwrap(); - assert_eq!(results, vec![2, 3]); + assert_eq!(results, vec![id2, id3]); } #[test] From 3a8478bf91cf9ae7770458c6c4381083034749c4 Mon Sep 17 00:00:00 2001 From: Entity069 Date: Mon, 27 Oct 2025 15:39:11 +0530 Subject: [PATCH 16/25] fix: update test for updated id and payload --- crates/storage/Cargo.toml | 1 + crates/storage/src/rocks_db.rs | 50 ++++++++++++++++++++++++---------- 2 files changed, 36 insertions(+), 15 deletions(-) diff --git a/crates/storage/Cargo.toml b/crates/storage/Cargo.toml index af121de..cd02fe2 100644 --- a/crates/storage/Cargo.toml +++ b/crates/storage/Cargo.toml @@ -6,6 +6,7 @@ edition = "2021" [dependencies] bincode = "1.3.3" rocksdb = "0.21.0" +uuid.workspace = true core = { path = "../core" } index = { path = "../index" } diff --git a/crates/storage/src/rocks_db.rs b/crates/storage/src/rocks_db.rs index 765b6f6..3902425 100644 --- a/crates/storage/src/rocks_db.rs +++ b/crates/storage/src/rocks_db.rs @@ -60,7 +60,7 @@ impl StorageEngine for RocksDbStorage { payload, }; let value = serialize(&point).map_err(|e| DbError::SerializationError(e.to_string()))?; - match self.db.put(key, value.as_ref() as &[u8]) { + match self.db.put(key.as_bytes(), value.as_slice()) { Ok(_) => Ok(()), Err(e) => Err(DbError::StorageError(e.into_string())), } @@ -142,7 +142,7 @@ impl StorageEngine for RocksDbStorage { let (_, v) = item.map_err(|e| DbError::StorageError(e.into_string()))?; let point: Point = deserialize(&v).map_err(|_| DbError::DeserializationError)?; - if point.id < offset { + if point.id <= offset { continue; } @@ -154,13 +154,15 @@ impl StorageEngine for RocksDbStorage { } } } - Ok(Some((result, last_id + 1))) + Ok(Some((result, last_id))) } } #[cfg(test)] mod tests { use super::*; + use core::ContentType; + use uuid::Uuid; use tempfile::tempdir; @@ -182,9 +184,12 @@ mod tests { #[test] fn test_insert_and_get_vector() { let (db, path) = create_test_db(); - let id = 1; + let id = Uuid::new_v4(); let vector = Some(vec![0.1, 0.2, 0.3]); - let payload = None; + let payload = Some(Payload { + content_type: ContentType::Text, + content: "Test".to_string(), + }); assert!(db.insert_point(id, vector.clone(), payload).is_ok()); let result = db.get_vector(id).unwrap(); @@ -196,13 +201,21 @@ mod tests { #[test] fn test_insert_and_get_payload() { let (db, path) = create_test_db(); - let id = 2; - let payload = Some(Payload {}); + let id = Uuid::new_v4(); + let payload = Some(Payload { + content_type: ContentType::Text, + content: "Test".to_string(), + }); let vector = None; + // Move payload into insert_point and recreate expected for comparison assert!(db.insert_point(id, vector, payload).is_ok()); let result = db.get_payload(id).unwrap(); - assert_eq!(result, payload); + let expected = Some(Payload { + content_type: ContentType::Text, + content: "Test".to_string(), + }); + assert_eq!(result, expected); std::fs::remove_dir_all(path).unwrap_or_default(); } @@ -210,12 +223,16 @@ mod tests { #[test] fn test_contains_point() { let (db, path) = create_test_db(); - let id = 3; + let id = Uuid::new_v4(); + let payload = Some(Payload { + content_type: ContentType::Text, + content: "Test".to_string(), + }); assert!(!db.contains_point(id).unwrap()); let vector = Some(vec![0.4, 0.5, 0.6]); - db.insert_point(id, vector, None).unwrap(); + db.insert_point(id, vector, payload).unwrap(); assert!(db.contains_point(id).unwrap()); @@ -225,12 +242,15 @@ mod tests { #[test] fn test_delete_point() { let (db, path) = create_test_db(); - let id = 4; + let id = Uuid::new_v4(); + let payload = Some(Payload { + content_type: ContentType::Text, + content: "Test".to_string(), + }); let vector = vec![0.7, 0.8, 0.9]; - let payload = Payload {}; - db.insert_point(id, Some(vector), Some(payload)).unwrap(); + db.insert_point(id, Some(vector), payload).unwrap(); assert!(db.contains_point(id).unwrap()); @@ -246,7 +266,7 @@ mod tests { #[test] fn test_get_nonexistent_vector() { let (db, path) = create_test_db(); - let id = 999; + let id = Uuid::new_v4(); assert_eq!(db.get_vector(id).unwrap(), None); @@ -256,7 +276,7 @@ mod tests { #[test] fn test_get_nonexistent_payload() { let (db, path) = create_test_db(); - let id = 999; + let id = Uuid::new_v4(); assert_eq!(db.get_payload(id).unwrap(), None); From 3536083ff89af4a933b28d1c03d3a082f6f5f076 Mon Sep 17 00:00:00 2001 From: Entity069 Date: Tue, 28 Oct 2025 22:11:12 +0530 Subject: [PATCH 17/25] feat: add listing vectors in api --- crates/api/src/lib.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/crates/api/src/lib.rs b/crates/api/src/lib.rs index 897d0d1..f762678 100644 --- a/crates/api/src/lib.rs +++ b/crates/api/src/lib.rs @@ -88,6 +88,14 @@ impl VectorDb { Ok(vectors) } + + pub fn list( + &self, + offset: PointId, + limit: usize, + ) -> Result, PointId)>, DbError> { + self.storage.list_vectors(offset, limit) + } } pub struct DbConfig { From c207c024546c0abe0c1655d305750603d4761d93 Mon Sep 17 00:00:00 2001 From: Entity069 Date: Thu, 30 Oct 2025 11:19:03 +0530 Subject: [PATCH 18/25] fix: update tui with updated id and payload --- crates/tui/Cargo.toml | 2 + crates/tui/src/app/embeddings.rs | 22 -- crates/tui/src/app/events.rs | 202 ++++-------------- crates/tui/src/app/mod.rs | 7 +- crates/tui/src/app/modal.rs | 83 +------- crates/tui/src/app/state.rs | 41 +--- crates/tui/src/ui/modal.rs | 278 ++++++++----------------- crates/tui/src/ui/vector_operations.rs | 3 - 8 files changed, 147 insertions(+), 491 deletions(-) diff --git a/crates/tui/Cargo.toml b/crates/tui/Cargo.toml index 539e82b..8baedb1 100644 --- a/crates/tui/Cargo.toml +++ b/crates/tui/Cargo.toml @@ -10,10 +10,12 @@ crossterm = "0.27" tokio.workspace = true serde.workspace = true anyhow.workspace = true +uuid.workspace = true chrono = { version = "0.4", features = ["serde"] } core = { path = "../core" } storage = { path = "../storage" } index = { path = "../index" } +api = { path = "../api" } serde_json.workspace = true reqwest = { version = "0.12", features = ["json", "blocking", "multipart"] } dotenv = "0.15" \ No newline at end of file diff --git a/crates/tui/src/app/embeddings.rs b/crates/tui/src/app/embeddings.rs index a186906..9e9e59d 100644 --- a/crates/tui/src/app/embeddings.rs +++ b/crates/tui/src/app/embeddings.rs @@ -10,7 +10,6 @@ use std::time::Duration; pub struct EmbeddingClient { client: Client, text_url: String, - sentence_url: String, image_url: String, } @@ -63,14 +62,11 @@ impl EmbeddingClient { .build() .unwrap_or_else(|_| Client::new()); let text_url = env::var("TEXT_EMBEDDING_URL").expect("TEXT_EMBEDDING_URL must be set"); - let sentence_url = - env::var("SENTENCE_EMBEDDING_URL").expect("SENTENCE_EMBEDDING_URL must be set"); let image_url = env::var("IMAGE_EMBEDDING_URL").expect("IMAGE_EMBEDDING_URL must be set"); Self { client, text_url, - sentence_url, image_url, } } @@ -82,19 +78,6 @@ impl EmbeddingClient { Self::parse_response::(response).map(|body| body.result) } - pub fn sentence_embeddings(&self, sentence: &str) -> Result, EmbeddingError> { - let payload = serde_json::json!({ - "text": sentence, - "config": { - "pooling_strategy": "mean" - } - }); - - let response = self.client.post(&self.sentence_url).json(&payload).send()?; - - Self::parse_response::(response).map(|body| body.vector) - } - pub fn image_embeddings(&self, path: &Path) -> Result, EmbeddingError> { let file = std::fs::File::open(path)?; let file_name = path @@ -139,11 +122,6 @@ struct TextEmbeddingResponse { result: Vec, } -#[derive(Debug, Deserialize)] -struct SentenceEmbeddingResponse { - vector: Vec, -} - #[derive(Debug, Deserialize)] struct ImageEmbeddingResponse { result: Vec, diff --git a/crates/tui/src/app/events.rs b/crates/tui/src/app/events.rs index dde3b94..47d86be 100644 --- a/crates/tui/src/app/events.rs +++ b/crates/tui/src/app/events.rs @@ -1,9 +1,10 @@ use super::{App, AppState, ModalType, VectorListItem}; -use core::{DenseVector, Payload, Similarity}; +use core::{ContentType, DenseVector, Payload, Similarity}; use crossterm::event::{Event, KeyCode, KeyEvent}; use index::distance; use std::path::PathBuf; use std::{cmp::Ordering, io}; +use uuid::Uuid; // Set how many vectors to fetch per function call in list_vectors const VECTOR_LIST_LIMIT: usize = 50; @@ -204,13 +205,10 @@ fn handle_general_keys(app: &mut App, key: KeyEvent) { app.modal.show_error(err.to_string()); } } - 1 => app.modal.show_get_vector(), - 2 => app.modal.show_insert_vector(), - 3 => app.modal.show_delete_vector(), - 4 => app.modal.show_search_similar_vectors(), - 5 => app.modal.show_text_embedding(), - 6 => app.modal.show_sentence_embedding(), - 7 => app.modal.show_image_embedding(), + 1 => app.modal.show_delete_vector(), + 2 => app.modal.show_search_similar_vectors(), + 3 => app.modal.show_text_embedding(), + 4 => app.modal.show_image_embedding(), _ => {} }, _ => app.next_page(), @@ -263,70 +261,12 @@ fn execute_modal_action(app: &mut App) -> io::Result<()> { app.modal.show_confirm_delete_database(name.clone()); } } - Some(ModalType::GetVector) => { - let input = app.modal.get_input_value(); - let vector_id_field = &input.parse::().ok(); - if vector_id_field.is_none() { - return Err(io::Error::new( - io::ErrorKind::InvalidInput, - "IDs should be an integer!", - )); - } - let id = vector_id_field.unwrap(); - show_vector_info(app, id)?; - } - Some(ModalType::InsertVector) => { - // Form fields for insertion: id, vector, payload - let id_text = app.modal.get_input_value(); - let vec_text = app.modal.secondary_input(); - let payload_text = app.modal.tertiary_input(); - - let vector_id_field = id_text.parse::().ok(); - if vector_id_field.is_none() { - return Err(io::Error::new( - io::ErrorKind::InvalidInput, - "IDs should be an integer!", - )); - } - let id = vector_id_field.unwrap(); - match parse_vector(vec_text) { - Some(vec) => { - if let Some(storage) = &app.database.storage_engine { - let payload_opt = if payload_text.trim().is_empty() { - None - } else { - Some(Payload {}) - }; - match storage - .insert_point(id, Some(vec), payload_opt) - .map_err(to_io) - { - Ok(()) => app - .modal - .show_success(format!("Inserted vector with id={id}!")), - Err(e) => app.modal.show_failure(format!("Storage error: {e}")), - } - } else { - app.modal.show_error("No database selected!"); - } - } - None => { - app.modal - .show_failure("Vectors should be a list of floats (e.g. [0.1,0.2,...])!"); - } - } - } Some(ModalType::DeleteVector) => { let input = app.modal.get_input_value(); - let vector_id_field = &input.parse::().ok(); - if vector_id_field.is_none() { - return Err(io::Error::new( - io::ErrorKind::InvalidInput, - "IDs should be an integer!", - )); - } - let id = vector_id_field.unwrap(); + let id = Uuid::parse_str(input.trim()).map_err(|_| { + io::Error::new(io::ErrorKind::InvalidInput, "ID should be a valid UUID v4!") + })?; if let Some(storage) = &app.database.storage_engine { match storage.contains_point(id).map_err(to_io) { Ok(exists) => { @@ -348,7 +288,7 @@ fn execute_modal_action(app: &mut App) -> io::Result<()> { } Some(ModalType::SearchSimilarVectors) => { let k_text = app.modal.get_input_value(); - let vector_text = app.modal.secondary_input().to_string(); + let text_raw = app.modal.secondary_input().to_string(); let k = k_text.parse::().ok().filter(|value| *value > 0); if k.is_none() { @@ -359,19 +299,29 @@ fn execute_modal_action(app: &mut App) -> io::Result<()> { } let k = k.unwrap(); - let Some(query) = parse_vector(&vector_text) else { - app.modal - .show_failure("Query vector should be a list of floats (e.g. [0.1,0.2,...])!"); - return Ok(()); - }; + let trimmed_text = text_raw.trim(); + if trimmed_text.is_empty() { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "Text cannot be empty!", + )); + } let Some(storage) = &app.database.storage_engine else { app.modal.show_error("No database selected!"); return Ok(()); }; - let mut all_vectors: Vec<(u64, DenseVector)> = Vec::new(); - let mut next_offset = Some(0); + let query = match app.embeddings.text_embeddings(trimmed_text) { + Ok(v) => v, + Err(err) => { + app.modal + .show_failure(format!("Failed to generate embedding: {err}")); + return Ok(()); + } + }; + let mut all_vectors: Vec<(Uuid, DenseVector)> = Vec::new(); + let mut next_offset = Some(Uuid::nil()); while let Some(offset) = next_offset { let response = storage .list_vectors(offset, VECTOR_LIST_LIMIT) @@ -403,7 +353,7 @@ fn execute_modal_action(app: &mut App) -> io::Result<()> { } let query_len = query.len(); - let mut scored: Vec<(f32, u64, DenseVector)> = Vec::new(); + let mut scored: Vec<(f32, Uuid, DenseVector)> = Vec::new(); // currently to avoid panic on dimension mismatch, we skip vectors with different dims // TODO: enforce consistent dimensions on insert/search functions for (id, vector) in all_vectors.into_iter() { @@ -451,15 +401,11 @@ fn execute_modal_action(app: &mut App) -> io::Result<()> { app.modal.set_selected_index(0, len); } Some(ModalType::TextEmbedding) => { - let id_text = app.modal.get_input_value(); let text_raw = app.modal.secondary_input().to_string(); - let payload_text = app.modal.tertiary_input().to_string(); let trimmed_text = text_raw.trim(); - let id = id_text.parse::().map_err(|_| { - io::Error::new(io::ErrorKind::InvalidInput, "IDs should be an integer!") - })?; + let id = Uuid::new_v4(); if trimmed_text.is_empty() { return Err(io::Error::new( @@ -476,11 +422,10 @@ fn execute_modal_action(app: &mut App) -> io::Result<()> { match app.embeddings.text_embeddings(trimmed_text) { Ok(vector) => { let dims = vector.len(); - let payload_opt = if payload_text.trim().is_empty() { - None - } else { - Some(Payload {}) - }; + let payload_opt = Some(Payload { + content_type: ContentType::Text, + content: trimmed_text.to_string(), + }); match storage .insert_point(id, Some(vector), payload_opt) @@ -499,65 +444,12 @@ fn execute_modal_action(app: &mut App) -> io::Result<()> { .show_failure(format!("Failed to generate embedding: {err}")), } } - Some(ModalType::SentenceEmbedding) => { - let id_text = app.modal.get_input_value(); - let sentence_raw = app.modal.secondary_input().to_string(); - let payload_text = app.modal.tertiary_input().to_string(); - - let trimmed_sentence = sentence_raw.trim(); - - let id = id_text.parse::().map_err(|_| { - io::Error::new(io::ErrorKind::InvalidInput, "IDs should be an integer!") - })?; - - if trimmed_sentence.is_empty() { - return Err(io::Error::new( - io::ErrorKind::InvalidInput, - "Sentence cannot be empty!", - )); - } - - let Some(storage) = &app.database.storage_engine else { - app.modal.show_error("No database selected!"); - return Ok(()); - }; - - match app.embeddings.sentence_embeddings(trimmed_sentence) { - Ok(vector) => { - let dims = vector.len(); - let payload_opt = if payload_text.trim().is_empty() { - None - } else { - Some(Payload {}) - }; - - match storage - .insert_point(id, Some(vector), payload_opt) - .map_err(to_io) - { - Ok(()) => app.modal.show_success(format!( - "Sentence embedding inserted (id={id}, {dims} dims)." - )), - Err(err) => app - .modal - .show_failure(format!("Failed to insert embedding: {err}")), - } - } - Err(err) => app - .modal - .show_failure(format!("Failed to generate embedding: {err}")), - } - } Some(ModalType::ImageEmbedding) => { - let id_text = app.modal.get_input_value(); let path_raw = app.modal.secondary_input().to_string(); - let payload_text = app.modal.tertiary_input().to_string(); let trimmed_path = path_raw.trim(); - let id = id_text.parse::().map_err(|_| { - io::Error::new(io::ErrorKind::InvalidInput, "IDs should be an integer!") - })?; + let id = Uuid::new_v4(); if trimmed_path.is_empty() { return Err(io::Error::new( @@ -586,11 +478,10 @@ fn execute_modal_action(app: &mut App) -> io::Result<()> { match app.embeddings.image_embeddings(&path) { Ok(vector) => { let dims = vector.len(); - let payload_opt = if payload_text.trim().is_empty() { - None - } else { - Some(Payload {}) - }; + let payload_opt = Some(Payload { + content_type: ContentType::Image, + content: trimmed_path.to_string(), + }); match storage .insert_point(id, Some(vector), payload_opt) @@ -631,19 +522,6 @@ fn to_io(e: E) -> io::Error { io::Error::other(format!("{e:?}")) } -fn parse_vector(input: &str) -> Option> { - input - .trim() - .strip_prefix('[')? - .strip_suffix(']')? - .split(',') - .map(str::trim) - .filter(|s| !s.is_empty()) - .map(|s| s.parse::()) - .collect::, _>>() - .ok() -} - fn execute_selected_db_operation(app: &mut App) -> io::Result<()> { match app.modal.modal_type() { Some(ModalType::DatabaseList) => { @@ -685,7 +563,7 @@ fn initialize_vector_listing(app: &mut App) -> io::Result<()> { } app.vector_list_items.clear(); - app.vector_list_next_offset = Some(0); + app.vector_list_next_offset = Some(Uuid::nil()); app.vector_list_post_restore = false; app.vector_list_selected_index = 0; app.vector_detail = None; @@ -785,7 +663,7 @@ fn close_vector_detail_modal(app: &mut App) { } } -fn show_vector_info(app: &mut App, id: u64) -> io::Result<()> { +fn show_vector_info(app: &mut App, id: Uuid) -> io::Result<()> { let Some(storage) = &app.database.storage_engine else { app.modal.show_error("No database selected!"); return Ok(()); diff --git a/crates/tui/src/app/mod.rs b/crates/tui/src/app/mod.rs index d552b7b..de62c78 100644 --- a/crates/tui/src/app/mod.rs +++ b/crates/tui/src/app/mod.rs @@ -10,6 +10,7 @@ use crate::ui::{db, vector_operations}; use crossterm::event::Event; use std::io; use std::path::PathBuf; +use uuid::Uuid; pub struct App { pub should_quit: bool, @@ -21,7 +22,7 @@ pub struct App { pub embeddings: embeddings::EmbeddingClient, pub vector_list_items: Vec, - pub vector_list_next_offset: Option, + pub vector_list_next_offset: Option, pub vector_list_post_restore: bool, pub vector_list_selected_index: usize, pub vector_detail: Option, @@ -142,10 +143,6 @@ impl App { self.modal.secondary_input() } - pub fn tertiary_input(&self) -> &str { - self.modal.tertiary_input() - } - pub fn active_field(&self) -> usize { self.modal.active_field() } diff --git a/crates/tui/src/app/modal.rs b/crates/tui/src/app/modal.rs index bc0a539..6ce0bc5 100644 --- a/crates/tui/src/app/modal.rs +++ b/crates/tui/src/app/modal.rs @@ -6,7 +6,6 @@ pub struct ModalManager { modal_type: Option, input_buffer: String, secondary_input: String, - tertiary_input: String, active_field: usize, input_mode: bool, selected_index: usize, @@ -20,7 +19,6 @@ impl ModalManager { modal_type: None, input_buffer: String::new(), secondary_input: String::new(), - tertiary_input: String::new(), active_field: 0, input_mode: false, selected_index: 0, @@ -44,10 +42,6 @@ impl ModalManager { &self.secondary_input } - pub fn tertiary_input(&self) -> &str { - &self.tertiary_input - } - pub fn active_field(&self) -> usize { self.active_field } @@ -69,36 +63,11 @@ impl ModalManager { self.error_message = None; } - pub fn show_get_vector(&mut self) { - self.show_modal = true; - self.modal_type = Some(ModalType::GetVector); - self.input_buffer.clear(); - self.secondary_input.clear(); - self.tertiary_input.clear(); - self.active_field = 0; - self.input_mode = true; - self.selected_index = 0; - self.error_message = None; - } - - pub fn show_insert_vector(&mut self) { - self.show_modal = true; - self.modal_type = Some(ModalType::InsertVector); - self.input_buffer.clear(); - self.secondary_input.clear(); - self.tertiary_input.clear(); - self.active_field = 0; - self.input_mode = true; - self.selected_index = 0; - self.error_message = None; - } - pub fn show_search_similar_vectors(&mut self) { self.show_modal = true; self.modal_type = Some(ModalType::SearchSimilarVectors); self.input_buffer.clear(); self.secondary_input.clear(); - self.tertiary_input.clear(); self.active_field = 0; self.input_mode = true; self.selected_index = 0; @@ -110,7 +79,6 @@ impl ModalManager { self.modal_type = Some(ModalType::DeleteVector); self.input_buffer.clear(); self.secondary_input.clear(); - self.tertiary_input.clear(); self.active_field = 0; self.input_mode = true; self.selected_index = 0; @@ -122,20 +90,7 @@ impl ModalManager { self.modal_type = Some(ModalType::TextEmbedding); self.input_buffer.clear(); self.secondary_input.clear(); - self.tertiary_input.clear(); - self.active_field = 0; - self.input_mode = true; - self.selected_index = 0; - self.error_message = None; - } - - pub fn show_sentence_embedding(&mut self) { - self.show_modal = true; - self.modal_type = Some(ModalType::SentenceEmbedding); - self.input_buffer.clear(); - self.secondary_input.clear(); - self.tertiary_input.clear(); - self.active_field = 0; + self.active_field = 1; self.input_mode = true; self.selected_index = 0; self.error_message = None; @@ -146,8 +101,7 @@ impl ModalManager { self.modal_type = Some(ModalType::ImageEmbedding); self.input_buffer.clear(); self.secondary_input.clear(); - self.tertiary_input.clear(); - self.active_field = 0; + self.active_field = 1; self.input_mode = true; self.selected_index = 0; self.error_message = None; @@ -173,7 +127,6 @@ impl ModalManager { self.input_mode = false; self.input_buffer.clear(); self.secondary_input.clear(); - self.tertiary_input.clear(); self.error_message = Some(message.into()); } @@ -183,7 +136,6 @@ impl ModalManager { self.input_mode = false; self.input_buffer.clear(); self.secondary_input.clear(); - self.tertiary_input.clear(); self.error_message = Some(message.into()); } @@ -217,7 +169,6 @@ impl ModalManager { self.input_mode = false; self.input_buffer.clear(); self.secondary_input.clear(); - self.tertiary_input.clear(); self.active_field = 0; self.selected_index = 0; self.error_message = Some(message.into()); @@ -228,7 +179,6 @@ impl ModalManager { self.modal_type = None; self.input_buffer.clear(); self.secondary_input.clear(); - self.tertiary_input.clear(); self.active_field = 0; self.input_mode = false; self.selected_index = 0; @@ -243,7 +193,7 @@ impl ModalManager { match self.active_field { 0 => self.input_buffer.push(c), 1 => self.secondary_input.push(c), - _ => self.tertiary_input.push(c), + _ => {} } } @@ -255,14 +205,14 @@ impl ModalManager { 1 => { self.secondary_input.pop(); } - _ => { - self.tertiary_input.pop(); - } + _ => {} } } pub fn switch_field(&mut self) { - self.active_field = (self.active_field + 1) % 3; + if matches!(self.modal_type, Some(ModalType::SearchSimilarVectors)) { + self.active_field = (self.active_field + 1) % 2; + } } pub fn select_previous(&mut self) { @@ -294,7 +244,7 @@ impl ModalManager { pub fn footer_items(&self) -> Vec<(String, Color)> { use ModalType::*; - match self.modal_type { + match self.modal_type.as_ref() { Some(CreateDatabase) => vec![ ("Enter Create".into(), Color::Green), ("Esc Cancel".into(), Color::Red), @@ -316,15 +266,7 @@ impl ModalManager { ("Enter Confirm".into(), Color::Green), ("Esc Back".into(), Color::Red), ], - Some(GetVector) => vec![ - ("Enter Fetch".into(), Color::Green), - ("Esc Close".into(), Color::Red), - ], - Some(InsertVector) => vec![ - ("Tab Next".into(), Color::Gray), - ("Enter Insert".into(), Color::Green), - ("Esc Cancel".into(), Color::Red), - ], + Some(SearchSimilarVectors) => vec![ ("Tab Next".into(), Color::Gray), ("Enter Search".into(), Color::Green), @@ -335,17 +277,10 @@ impl ModalManager { ("Esc Cancel".into(), Color::Red), ], Some(TextEmbedding) => vec![ - ("Tab Next".into(), Color::Gray), - ("Enter Insert".into(), Color::Green), - ("Esc Cancel".into(), Color::Red), - ], - Some(SentenceEmbedding) => vec![ - ("Tab Next".into(), Color::Gray), ("Enter Insert".into(), Color::Green), ("Esc Cancel".into(), Color::Red), ], Some(ImageEmbedding) => vec![ - ("Tab Next".into(), Color::Gray), ("Enter Insert".into(), Color::Green), ("Esc Cancel".into(), Color::Red), ], diff --git a/crates/tui/src/app/state.rs b/crates/tui/src/app/state.rs index ef49f36..dcdda01 100644 --- a/crates/tui/src/app/state.rs +++ b/crates/tui/src/app/state.rs @@ -1,4 +1,5 @@ use core::{DenseVector, Payload}; +use uuid::Uuid; #[derive(Debug, Default, Clone, PartialEq)] pub enum AppState { @@ -19,52 +20,16 @@ pub enum ModalType { Error, Success, Failure, - GetVector, - InsertVector, + DeleteVector, SearchSimilarVectors, TextEmbedding, - SentenceEmbedding, ImageEmbedding, } #[derive(Debug, Clone)] pub struct VectorListItem { - pub id: u64, + pub id: Uuid, pub vector: DenseVector, pub payload: Option, } - -impl VectorListItem { - pub fn dims(&self) -> usize { - self.vector.len() - } - - pub fn snippet(&self, max_dims: usize) -> String { - if self.vector.is_empty() { - return "[]".to_string(); - } - - let take = self.vector.len().min(max_dims); - let snippet = self - .vector - .iter() - .take(take) - .map(|v| format!("{v:.2}")) - .collect::>() - .join(", "); - - if self.vector.len() > take { - format!("[{snippet}, ...]") - } else { - format!("[{snippet}]") - } - } - - pub fn payload_summary(&self) -> String { - self.payload - .as_ref() - .map(|payload| format!("{payload:?}")) - .unwrap_or_else(|| "None".to_string()) - } -} diff --git a/crates/tui/src/ui/modal.rs b/crates/tui/src/ui/modal.rs index a623512..fb697cc 100644 --- a/crates/tui/src/ui/modal.rs +++ b/crates/tui/src/ui/modal.rs @@ -28,14 +28,11 @@ pub fn render_modal(f: &mut Frame, app: &App) { render_confirm_delete_database_modal(f, app, popup_area) } Some(ModalType::Error) => render_error_modal(f, app, popup_area), - Some(ModalType::GetVector) => render_get_vector_modal(f, app, popup_area), - Some(ModalType::InsertVector) => render_insert_vector_modal(f, app, popup_area), Some(ModalType::SearchSimilarVectors) => { render_search_similar_vectors_modal(f, app, popup_area) } Some(ModalType::DeleteVector) => render_delete_vector_modal(f, app, popup_area), Some(ModalType::TextEmbedding) => render_text_embedding_modal(f, app, popup_area), - Some(ModalType::SentenceEmbedding) => render_sentence_embedding_modal(f, app, popup_area), Some(ModalType::ImageEmbedding) => render_image_embedding_modal(f, app, popup_area), Some(ModalType::ListVectors) => render_vector_list_modal(f, app, popup_area), Some(ModalType::VectorDetails) => render_vector_details_modal(f, app, popup_area), @@ -61,59 +58,6 @@ fn render_create_database_modal(f: &mut Frame, app: &App, area: Rect) { } } -fn render_get_vector_modal(f: &mut Frame, app: &App, area: Rect) { - let block = Block::default() - .title("Get Vector by ID") - .borders(Borders::ALL); - - let lines = vec![ - Line::from(Span::raw("ID:")), - Line::from(Span::raw(app.input_buffer().to_string())), - Line::from(""), - ]; - - let input = Paragraph::new(lines) - .block(block) - .wrap(ratatui::widgets::Wrap { trim: true }); - f.render_widget(input, area); - - if app.input_mode() { - f.set_cursor(area.x + 1 + app.input_buffer().len() as u16, area.y + 2); - } -} - -fn render_insert_vector_modal(f: &mut Frame, app: &App, area: Rect) { - let block = Block::default() - .title("Insert Vector") - .borders(Borders::ALL); - - let lines = vec![ - Line::from(Span::raw("ID (int):")), - Line::from(Span::raw(app.input_buffer().to_string())), - Line::from(Span::raw("")), - Line::from(Span::raw("Vector (e.g. [0.1,0.2,...]):")), - Line::from(Span::raw(app.secondary_input().to_string())), - Line::from(Span::raw("")), - Line::from(Span::raw("Payload (optional):")), - Line::from(Span::raw(app.tertiary_input().to_string())), - ]; - - let input = Paragraph::new(lines) - .block(block) - .wrap(ratatui::widgets::Wrap { trim: true }); - f.render_widget(input, area); - - if app.input_mode() { - if app.active_field() == 0 { - f.set_cursor(area.x + 1 + app.input_buffer().len() as u16, area.y + 2); - } else if app.active_field() == 1 { - f.set_cursor(area.x + 1 + app.secondary_input().len() as u16, area.y + 5); - } else { - f.set_cursor(area.x + 1 + app.tertiary_input().len() as u16, area.y + 8); - } - } -} - fn render_search_similar_vectors_modal(f: &mut Frame, app: &App, area: Rect) { let block = Block::default() .title("Search Similar Vectors") @@ -123,7 +67,7 @@ fn render_search_similar_vectors_modal(f: &mut Frame, app: &App, area: Rect) { Line::from(Span::raw("Top-k (int):")), Line::from(Span::raw(app.input_buffer().to_string())), Line::from(Span::raw("")), - Line::from(Span::raw("Query Vector (e.g. [0.1,0.2,...]):")), + Line::from(Span::raw("Text:")), Line::from(Span::raw(app.secondary_input().to_string())), ]; @@ -147,7 +91,7 @@ fn render_delete_vector_modal(f: &mut Frame, app: &App, area: Rect) { .borders(Borders::ALL); let lines = vec![ - Line::from(Span::raw("ID:")), + Line::from(Span::raw("ID (uuid):")), Line::from(Span::raw(app.input_buffer().to_string())), Line::from(""), ]; @@ -164,48 +108,12 @@ fn render_delete_vector_modal(f: &mut Frame, app: &App, area: Rect) { fn render_text_embedding_modal(f: &mut Frame, app: &App, area: Rect) { let block = Block::default() - .title("Generate Text Embedding") + .title("Insert Text Embedding") .borders(Borders::ALL); let lines = vec![ - Line::from(Span::raw("ID (int):")), - Line::from(Span::raw(app.input_buffer().to_string())), - Line::from(Span::raw("")), Line::from(Span::raw("Text:")), Line::from(Span::raw(app.secondary_input().to_string())), - Line::from(Span::raw("")), - Line::from(Span::raw("Payload (optional):")), - Line::from(Span::raw(app.tertiary_input().to_string())), - ]; - - let input = Paragraph::new(lines) - .block(block) - .wrap(ratatui::widgets::Wrap { trim: true }); - f.render_widget(input, area); - - if app.input_mode() { - match app.active_field() { - 0 => f.set_cursor(area.x + 1 + app.input_buffer().len() as u16, area.y + 2), - 1 => f.set_cursor(area.x + 1 + app.secondary_input().len() as u16, area.y + 5), - _ => f.set_cursor(area.x + 1 + app.tertiary_input().len() as u16, area.y + 8), - } - } -} - -fn render_sentence_embedding_modal(f: &mut Frame, app: &App, area: Rect) { - let block = Block::default() - .title("Generate Sentence Embedding") - .borders(Borders::ALL); - - let lines = vec![ - Line::from(Span::raw("ID (int):")), - Line::from(Span::raw(app.input_buffer().to_string())), - Line::from(Span::raw("")), - Line::from(Span::raw("Sentence:")), - Line::from(Span::raw(app.secondary_input().to_string())), - Line::from(Span::raw("")), - Line::from(Span::raw("Payload (optional):")), - Line::from(Span::raw(app.tertiary_input().to_string())), ]; let input = Paragraph::new(lines) @@ -214,28 +122,18 @@ fn render_sentence_embedding_modal(f: &mut Frame, app: &App, area: Rect) { f.render_widget(input, area); if app.input_mode() { - match app.active_field() { - 0 => f.set_cursor(area.x + 1 + app.input_buffer().len() as u16, area.y + 2), - 1 => f.set_cursor(area.x + 1 + app.secondary_input().len() as u16, area.y + 5), - _ => f.set_cursor(area.x + 1 + app.tertiary_input().len() as u16, area.y + 8), - } + f.set_cursor(area.x + 1 + app.secondary_input().len() as u16, area.y + 2); } } fn render_image_embedding_modal(f: &mut Frame, app: &App, area: Rect) { let block = Block::default() - .title("Generate Image Embedding") + .title("Insert Image Embedding") .borders(Borders::ALL); let lines = vec![ - Line::from(Span::raw("ID (int):")), - Line::from(Span::raw(app.input_buffer().to_string())), - Line::from(Span::raw("")), Line::from(Span::raw("Image Path:")), Line::from(Span::raw(app.secondary_input().to_string())), - Line::from(Span::raw("")), - Line::from(Span::raw("Payload (optional):")), - Line::from(Span::raw(app.tertiary_input().to_string())), ]; let input = Paragraph::new(lines) @@ -244,11 +142,7 @@ fn render_image_embedding_modal(f: &mut Frame, app: &App, area: Rect) { f.render_widget(input, area); if app.input_mode() { - match app.active_field() { - 0 => f.set_cursor(area.x + 1 + app.input_buffer().len() as u16, area.y + 2), - 1 => f.set_cursor(area.x + 1 + app.secondary_input().len() as u16, area.y + 5), - _ => f.set_cursor(area.x + 1 + app.tertiary_input().len() as u16, area.y + 8), - } + f.set_cursor(area.x + 1 + app.secondary_input().len() as u16, area.y + 2); } } @@ -304,6 +198,49 @@ fn render_delete_database_modal(f: &mut Frame, app: &App, area: Rect) { f.render_widget(list, area); } +fn render_confirm_delete_database_modal(f: &mut Frame, app: &App, area: Rect) { + let prompt = app.error_message().unwrap_or("Are you sure?"); + let options = ["Yes", "No"]; + let selected = app + .modal + .selected_index() + .min(options.len().saturating_sub(1)); + + let option_line = options + .iter() + .enumerate() + .map(|(i, option)| { + if i == selected { + Span::styled( + format!("[ {option} ]"), + Style::default() + .fg(Color::Black) + .bg(Color::Yellow) + .add_modifier(Modifier::BOLD), + ) + } else { + Span::styled(format!(" {option} "), Style::default().fg(Color::Gray)) + } + }) + .collect::>(); + + let block = Block::default() + .title("Confirm Deletion") + .borders(Borders::ALL) + .border_style(Style::default().fg(Color::Red)); + + let content = Paragraph::new(vec![ + Line::from(prompt.to_string()), + Line::from(""), + Line::from(option_line), + ]) + .block(block) + .alignment(Alignment::Center) + .wrap(ratatui::widgets::Wrap { trim: true }); + + f.render_widget(content, area); +} + fn render_vector_list_modal(f: &mut Frame, app: &App, area: Rect) { let has_more = app.vector_list_next_offset.is_some(); let title = if has_more { @@ -312,24 +249,30 @@ fn render_vector_list_modal(f: &mut Frame, app: &App, area: Rect) { "Vectors" }; - let header_cells = ["Dims", "ID", "Vector", "Payload"].into_iter().map(|h| { - Cell::from(h).style( - Style::default() - .fg(Color::Magenta) - .add_modifier(Modifier::BOLD), - ) - }); + let header_cells = ["ID", "Payload Type", "Payload Content"] + .into_iter() + .map(|h| { + Cell::from(h).style( + Style::default() + .fg(Color::Magenta) + .add_modifier(Modifier::BOLD), + ) + }); let header = Row::new(header_cells).height(1); let rows: Vec = app .vector_list_items .iter() .map(|item| { + let id_str = item.id.to_string(); + let (ptype, pcontent) = match &item.payload { + Some(p) => (format!("{:?}", p.content_type), p.content.clone()), + None => ("None".to_string(), "".to_string()), + }; Row::new(vec![ - Cell::from(item.dims().to_string()), - Cell::from(item.id.to_string()), - Cell::from(item.snippet(8)), - Cell::from(item.payload_summary()), + Cell::from(id_str), + Cell::from(ptype), + Cell::from(pcontent), ]) }) .collect(); @@ -344,10 +287,9 @@ fn render_vector_list_modal(f: &mut Frame, app: &App, area: Rect) { } let widths = [ - Constraint::Length(8), + Constraint::Length(38), Constraint::Length(14), - Constraint::Percentage(45), - Constraint::Percentage(33), + Constraint::Percentage(60), ]; let table = Table::new(rows, widths) @@ -372,29 +314,35 @@ fn render_vector_details_modal(f: &mut Frame, app: &App, area: Rect) { .border_style(Style::default().fg(Color::Magenta)); if let Some(item) = &app.vector_detail { - let header_cells = ["Dims", "ID", "Vector", "Payload"].into_iter().map(|h| { - Cell::from(h).style( - Style::default() - .fg(Color::Magenta) - .add_modifier(Modifier::BOLD), - ) - }); + let header_cells = ["ID", "Payload Type", "Payload Content"] + .into_iter() + .map(|h| { + Cell::from(h).style( + Style::default() + .fg(Color::Magenta) + .add_modifier(Modifier::BOLD), + ) + }); let header = Row::new(header_cells).height(1); + let id_str = item.id.to_string(); + let (ptype, pcontent) = match &item.payload { + Some(p) => (format!("{:?}", p.content_type), p.content.clone()), + None => ("None".to_string(), "".to_string()), + }; + let row = Row::new(vec![ - Cell::from(item.dims().to_string()), - Cell::from(item.id.to_string()), - Cell::from(item.snippet(16)), - Cell::from(item.payload_summary()), + Cell::from(id_str), + Cell::from(ptype), + Cell::from(pcontent), ]) .height(2); let rows = vec![row]; let widths = [ - Constraint::Length(8), + Constraint::Length(38), Constraint::Length(14), - Constraint::Percentage(45), - Constraint::Percentage(33), + Constraint::Percentage(60), ]; let table = Table::new(rows, widths) @@ -449,7 +397,9 @@ fn render_success_modal(f: &mut Frame, app: &App, area: Rect) { } fn render_failure_modal(f: &mut Frame, app: &App, area: Rect) { - let message = app.error_message().unwrap_or("Failure"); + let message = app + .error_message() + .unwrap_or("Operation failed. Please try again."); let content = Paragraph::new(vec![Line::from(message.to_string())]) .block( Block::default() @@ -465,52 +415,6 @@ fn render_failure_modal(f: &mut Frame, app: &App, area: Rect) { f.render_widget(content, area); } -fn render_confirm_delete_database_modal(f: &mut Frame, app: &App, area: Rect) { - let prompt = app - .error_message() - .unwrap_or("Are you sure you want to delete this database?"); - - let block = Block::default() - .title("Confirm Deletion") - .borders(Borders::ALL) - .border_style(Style::default().fg(Color::Red)); - - let options = ["Yes", "No"]; - let selected = app - .modal - .selected_index() - .min(options.len().saturating_sub(1)); - - let option_line = options - .iter() - .enumerate() - .map(|(i, option)| { - if i == selected { - Span::styled( - format!("[ {option} ]"), - Style::default() - .fg(Color::Black) - .bg(Color::Yellow) - .add_modifier(Modifier::BOLD), - ) - } else { - Span::styled(format!(" {option} "), Style::default().fg(Color::Gray)) - } - }) - .collect::>(); - - let content = Paragraph::new(vec![ - Line::from(prompt.to_string()), - Line::from(""), - Line::from(option_line), - ]) - .block(block) - .alignment(Alignment::Center) - .wrap(ratatui::widgets::Wrap { trim: true }); - - f.render_widget(content, area); -} - fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect { let popup_layout = Layout::default() .direction(Direction::Vertical) diff --git a/crates/tui/src/ui/vector_operations.rs b/crates/tui/src/ui/vector_operations.rs index a09f629..ad9ab08 100644 --- a/crates/tui/src/ui/vector_operations.rs +++ b/crates/tui/src/ui/vector_operations.rs @@ -11,12 +11,9 @@ use crate::app::App; const VECTOR_OPERATIONS: &[&str] = &[ "List All Vectors", - "Get Vector", - "Insert Vector", "Delete Vector", "Search Similar Vectors", "Insert Text Embedding", - "Insert Sentence Embedding", "Insert Image Embedding", ]; From dc4f1fb0698c2dd0b182929b4422edf0aec8ee31 Mon Sep 17 00:00:00 2001 From: Entity069 Date: Thu, 30 Oct 2025 15:22:31 +0530 Subject: [PATCH 19/25] fix[api]: populate index after initiating db instance in init_api() function --- crates/api/src/lib.rs | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/crates/api/src/lib.rs b/crates/api/src/lib.rs index f762678..c51aa01 100644 --- a/crates/api/src/lib.rs +++ b/crates/api/src/lib.rs @@ -96,6 +96,31 @@ impl VectorDb { ) -> Result, PointId)>, DbError> { self.storage.list_vectors(offset, limit) } + + // populates the current index with vectors from the storage + pub fn build_index(&self) -> Result { + // start from the minimal UUID and fetch in bounded batches and insert + let mut offset = Uuid::nil(); + let page_size: usize = 1000; + let mut inserted: usize = 0; + + let mut index = self.index.write().map_err(|_| DbError::LockError)?; + + while let Some((batch, next_offset)) = self.storage.list_vectors(offset, page_size)? { + if batch.is_empty() || next_offset == offset { + break; + } + + for (id, vector) in batch { + index.insert(IndexedVector { id, vector })?; + inserted += 1; + } + + offset = next_offset; + } + + Ok(inserted) + } } pub struct DbConfig { @@ -121,6 +146,9 @@ pub fn init_api(config: DbConfig) -> Result { // Init the db let db = VectorDb::_new(storage, index); + // populate the current index with vectors from the storage + db.build_index()?; + Ok(db) } From 7ee3a8b93ebe23d09b72ca1ccddf1d650472356c Mon Sep 17 00:00:00 2001 From: Entity069 Date: Thu, 30 Oct 2025 20:08:11 +0530 Subject: [PATCH 20/25] refactor[tui]: update TUI to use api --- crates/tui/src/app/database.rs | 36 ++++++-- crates/tui/src/app/events.rs | 162 ++++++++++----------------------- 2 files changed, 75 insertions(+), 123 deletions(-) diff --git a/crates/tui/src/app/database.rs b/crates/tui/src/app/database.rs index 796c772..849bca7 100644 --- a/crates/tui/src/app/database.rs +++ b/crates/tui/src/app/database.rs @@ -1,11 +1,14 @@ +use api::{init_api, DbConfig, VectorDb}; +use index::IndexType; use std::io; use std::path::PathBuf; use std::sync::Arc; -use storage::{create_storage_engine, StorageEngine, StorageType}; +use storage::{StorageEngine, StorageType}; pub struct DatabaseManager { pub current_db_path: Option, pub storage_engine: Option>, + pub api_db: Option, pub available_databases: Vec<(String, PathBuf)>, pub selected_database: Option<(String, PathBuf)>, } @@ -15,6 +18,7 @@ impl DatabaseManager { Self { current_db_path: None, storage_engine: None, + api_db: None, available_databases: Vec::new(), selected_database: None, } @@ -24,6 +28,7 @@ impl DatabaseManager { self.storage_engine = None; self.current_db_path = None; self.selected_database = None; + self.api_db = None; } pub fn create_new_database(&mut self, name: String, path: PathBuf) -> io::Result<()> { @@ -37,9 +42,16 @@ impl DatabaseManager { )); } - match create_storage_engine(StorageType::RocksDb, &path) { - Ok(storage) => { - self.storage_engine = Some(storage); + let cfg = DbConfig { + storage_type: StorageType::RocksDb, + index_type: IndexType::Flat, + data_path: path.clone(), + dimension: 512, + }; + + match init_api(cfg) { + Ok(db) => { + self.api_db = Some(db); self.current_db_path = Some(path.clone()); self.available_databases.push((name.clone(), path.clone())); self.selected_database = Some((name, path)); @@ -61,10 +73,17 @@ impl DatabaseManager { "Database path does not exist", )); } - // Open the selected database - match create_storage_engine(StorageType::RocksDb, &path) { - Ok(storage) => { - self.storage_engine = Some(storage); + + let cfg = DbConfig { + storage_type: StorageType::RocksDb, + index_type: IndexType::Flat, + data_path: path.clone(), + dimension: 512, + }; + + match init_api(cfg) { + Ok(db) => { + self.api_db = Some(db); self.current_db_path = Some(path.clone()); self.selected_database = Some((name, path)); Ok(()) @@ -82,6 +101,7 @@ impl DatabaseManager { if current_path == path { self.storage_engine = None; self.current_db_path = None; + self.api_db = None; } } diff --git a/crates/tui/src/app/events.rs b/crates/tui/src/app/events.rs index 47d86be..acc58a1 100644 --- a/crates/tui/src/app/events.rs +++ b/crates/tui/src/app/events.rs @@ -1,9 +1,8 @@ use super::{App, AppState, ModalType, VectorListItem}; -use core::{ContentType, DenseVector, Payload, Similarity}; +use core::{ContentType, Payload, Similarity}; use crossterm::event::{Event, KeyCode, KeyEvent}; -use index::distance; +use std::io; use std::path::PathBuf; -use std::{cmp::Ordering, io}; use uuid::Uuid; // Set how many vectors to fetch per function call in list_vectors @@ -267,20 +266,15 @@ fn execute_modal_action(app: &mut App) -> io::Result<()> { let id = Uuid::parse_str(input.trim()).map_err(|_| { io::Error::new(io::ErrorKind::InvalidInput, "ID should be a valid UUID v4!") })?; - if let Some(storage) = &app.database.storage_engine { - match storage.contains_point(id).map_err(to_io) { - Ok(exists) => { - if !exists { - app.modal - .show_failure(format!("Vector with id={id} not found")); - } else { - match storage.delete_point(id).map_err(to_io) { - Ok(()) => app.modal.show_success(format!("Deleted vector id={id}")), - Err(e) => app.modal.show_failure(format!("Storage error: {e}")), - } - } - } - Err(e) => app.modal.show_failure(format!("Storage error: {e}")), + if let Some(db) = &app.database.api_db { + match db.get(id).map_err(to_io)? { + Some(_) => match db.delete(id).map_err(to_io) { + Ok(()) => app.modal.show_success(format!("Deleted vector id={id}")), + Err(e) => app.modal.show_failure(format!("DB error: {e}")), + }, + None => app + .modal + .show_failure(format!("Vector with id={id} not found")), } } else { app.modal.show_error("No database selected!"); @@ -307,7 +301,7 @@ fn execute_modal_action(app: &mut App) -> io::Result<()> { )); } - let Some(storage) = &app.database.storage_engine else { + let Some(db) = &app.database.api_db else { app.modal.show_error("No database selected!"); return Ok(()); }; @@ -320,60 +314,8 @@ fn execute_modal_action(app: &mut App) -> io::Result<()> { return Ok(()); } }; - let mut all_vectors: Vec<(Uuid, DenseVector)> = Vec::new(); - let mut next_offset = Some(Uuid::nil()); - while let Some(offset) = next_offset { - let response = storage - .list_vectors(offset, VECTOR_LIST_LIMIT) - .map_err(to_io)?; - let Some((vectors, following_offset)) = response else { - break; - }; - - if vectors.is_empty() { - break; - } - - let batch_len = vectors.len(); - for (id, vector) in vectors { - all_vectors.push((id, vector)); - } - - if batch_len == VECTOR_LIST_LIMIT { - next_offset = Some(following_offset); - } else { - next_offset = None; - } - } - - if all_vectors.is_empty() { - app.modal - .show_failure("No vectors available in the selected database!"); - return Ok(()); - } - - let query_len = query.len(); - let mut scored: Vec<(f32, Uuid, DenseVector)> = Vec::new(); - // currently to avoid panic on dimension mismatch, we skip vectors with different dims - // TODO: enforce consistent dimensions on insert/search functions - for (id, vector) in all_vectors.into_iter() { - if vector.len() != query_len { - continue; - } - // TODO: replace with search function after enforcing dimension consistency - let score = distance(vector.clone(), query.clone(), Similarity::Cosine); - scored.push((score, id, vector)); - } - if scored.is_empty() { - app.modal - .show_failure("No vectors with matching dimensions found for the query!"); - return Ok(()); - } - - scored.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(Ordering::Equal)); - - let take = k.min(scored.len()); + let ids = db.search(query, Similarity::Cosine, k).map_err(to_io)?; app.vector_list_items.clear(); app.vector_detail = None; @@ -381,13 +323,16 @@ fn execute_modal_action(app: &mut App) -> io::Result<()> { app.vector_list_post_restore = false; app.vector_list_selected_index = 0; - for (_, id, vector) in scored.into_iter().take(take) { - let payload = storage.get_payload(id).map_err(to_io)?; - app.vector_list_items.push(VectorListItem { - id, - vector, - payload, - }); + for id in ids.into_iter() { + if let Some(point) = db.get(id).map_err(to_io)? { + if let Some(vector) = point.vector { + app.vector_list_items.push(VectorListItem { + id: point.id, + vector, + payload: point.payload, + }); + } + } } if app.vector_list_items.is_empty() { @@ -405,8 +350,6 @@ fn execute_modal_action(app: &mut App) -> io::Result<()> { let trimmed_text = text_raw.trim(); - let id = Uuid::new_v4(); - if trimmed_text.is_empty() { return Err(io::Error::new( io::ErrorKind::InvalidInput, @@ -414,7 +357,7 @@ fn execute_modal_action(app: &mut App) -> io::Result<()> { )); } - let Some(storage) = &app.database.storage_engine else { + let Some(db) = &app.database.api_db else { app.modal.show_error("No database selected!"); return Ok(()); }; @@ -422,17 +365,14 @@ fn execute_modal_action(app: &mut App) -> io::Result<()> { match app.embeddings.text_embeddings(trimmed_text) { Ok(vector) => { let dims = vector.len(); - let payload_opt = Some(Payload { + let payload = Payload { content_type: ContentType::Text, content: trimmed_text.to_string(), - }); - - match storage - .insert_point(id, Some(vector), payload_opt) - .map_err(to_io) - { - Ok(()) => app.modal.show_success(format!( - "Text embedding inserted (id={id}, {dims} dims)." + }; + + match db.insert(vector, payload).map_err(to_io) { + Ok(new_id) => app.modal.show_success(format!( + "Text embedding inserted (id={new_id}, {dims} dims)." )), Err(err) => app .modal @@ -449,8 +389,6 @@ fn execute_modal_action(app: &mut App) -> io::Result<()> { let trimmed_path = path_raw.trim(); - let id = Uuid::new_v4(); - if trimmed_path.is_empty() { return Err(io::Error::new( io::ErrorKind::InvalidInput, @@ -458,7 +396,7 @@ fn execute_modal_action(app: &mut App) -> io::Result<()> { )); } - let Some(storage) = &app.database.storage_engine else { + let Some(db) = &app.database.api_db else { app.modal.show_error("No database selected!"); return Ok(()); }; @@ -478,17 +416,14 @@ fn execute_modal_action(app: &mut App) -> io::Result<()> { match app.embeddings.image_embeddings(&path) { Ok(vector) => { let dims = vector.len(); - let payload_opt = Some(Payload { + let payload = Payload { content_type: ContentType::Image, content: trimmed_path.to_string(), - }); - - match storage - .insert_point(id, Some(vector), payload_opt) - .map_err(to_io) - { - Ok(()) => app.modal.show_success(format!( - "Image embedding inserted (id={id}, {dims} dims)." + }; + + match db.insert(vector, payload).map_err(to_io) { + Ok(new_id) => app.modal.show_success(format!( + "Image embedding inserted (id={new_id}, {dims} dims)." )), Err(err) => app .modal @@ -557,7 +492,7 @@ fn execute_selected_db_operation(app: &mut App) -> io::Result<()> { } fn initialize_vector_listing(app: &mut App) -> io::Result<()> { - if app.database.storage_engine.is_none() { + if app.database.api_db.is_none() { app.modal.show_error("No database selected!"); return Ok(()); } @@ -581,7 +516,7 @@ fn initialize_vector_listing(app: &mut App) -> io::Result<()> { } fn fetch_next_vector_page(app: &mut App) -> io::Result { - let Some(storage) = &app.database.storage_engine else { + let Some(db) = &app.database.api_db else { app.modal.show_error("No database selected!"); return Ok(false); }; @@ -590,9 +525,7 @@ fn fetch_next_vector_page(app: &mut App) -> io::Result { return Ok(false); }; - let response = storage - .list_vectors(offset, VECTOR_LIST_LIMIT) - .map_err(to_io)?; + let response = db.list(offset, VECTOR_LIST_LIMIT).map_err(to_io)?; let Some((vectors, next_offset)) = response else { app.vector_list_next_offset = None; @@ -607,7 +540,7 @@ fn fetch_next_vector_page(app: &mut App) -> io::Result { let vector_count = vectors.len(); for (id, vector) in vectors.into_iter() { - let payload = storage.get_payload(id).map_err(to_io)?; + let payload = db.get(id).map_err(to_io)?.and_then(|p| p.payload); app.vector_list_items.push(VectorListItem { id, vector, @@ -664,25 +597,24 @@ fn close_vector_detail_modal(app: &mut App) { } fn show_vector_info(app: &mut App, id: Uuid) -> io::Result<()> { - let Some(storage) = &app.database.storage_engine else { + let Some(db) = &app.database.api_db else { app.modal.show_error("No database selected!"); return Ok(()); }; app.vector_list_post_restore = false; - let vector_opt = storage.get_vector(id).map_err(to_io)?; - let payload_opt = storage.get_payload(id).map_err(to_io)?; - - if vector_opt.is_none() && payload_opt.is_none() { + let point_opt = db.get(id).map_err(to_io)?; + if point_opt.is_none() { app.vector_detail = None; app.modal .show_failure(format!("Vector with id={id} not found!")); return Ok(()); } - let vector = vector_opt.unwrap_or_default(); - let payload = payload_opt; + let point = point_opt.unwrap(); + let vector = point.vector.unwrap_or_default(); + let payload = point.payload; app.vector_detail = Some(VectorListItem { id, From ed4592fe0fc96121cfacb7f76106a20d2af465ba Mon Sep 17 00:00:00 2001 From: Entity069 Date: Thu, 30 Oct 2025 23:42:32 +0530 Subject: [PATCH 21/25] chore[api]: add test for listing vectors and building index --- crates/api/src/lib.rs | 54 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/crates/api/src/lib.rs b/crates/api/src/lib.rs index c51aa01..5f5a452 100644 --- a/crates/api/src/lib.rs +++ b/crates/api/src/lib.rs @@ -282,4 +282,58 @@ mod tests { let results = db.search(query, Similarity::Cosine, 10).unwrap(); assert_eq!(results.len(), 0); } + + #[test] + fn test_list_vectors() { + let db = create_test_db(); + // insert some points + let mut ids = Vec::new(); + for i in 0..10 { + let i = i as f32; + let vector = vec![i, i + 1.0, i + 2.0]; + let id = db + .insert( + vector, + Payload { + content_type: ContentType::Text, + content: format!("Test content {i}"), + }, + ) + .unwrap(); + ids.push(id); + } + + // list vectors with limit 5 + // list the values as well as their length + let (vectors, next_offset) = db.list(Uuid::nil(), 5).unwrap().unwrap(); + assert_eq!(vectors.len(), 5); + + // list next set of vectors + // list the values as well as their length + let (next_vectors, _) = db.list(next_offset, 5).unwrap().unwrap(); + assert_eq!(next_vectors.len(), 5); + } + + #[test] + fn test_build_index() { + let db = create_test_db(); + + // insert some points + for i in 0..10 { + let i = i as f32; + let vector = vec![i, i + 1.0, i + 2.0]; + db.insert( + vector, + Payload { + content_type: ContentType::Text, + content: format!("Test content {i}"), + }, + ) + .unwrap(); + } + + // rebuild the index + let inserted = db.build_index().unwrap(); + assert_eq!(inserted, 10); + } } From e359536ab4bac8e07e6c2384dabf301d6521f603 Mon Sep 17 00:00:00 2001 From: Entity069 Date: Fri, 31 Oct 2025 00:01:54 +0530 Subject: [PATCH 22/25] refactor: add prompt text for db creation --- crates/tui/src/ui/modal.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/crates/tui/src/ui/modal.rs b/crates/tui/src/ui/modal.rs index fb697cc..7343975 100644 --- a/crates/tui/src/ui/modal.rs +++ b/crates/tui/src/ui/modal.rs @@ -47,14 +47,20 @@ fn render_create_database_modal(f: &mut Frame, app: &App, area: Rect) { .title("Create New Database") .borders(Borders::ALL); - let input = Paragraph::new(app.input_buffer()) + let lines = vec![ + Line::from(Span::raw("Enter name of new database:")), + Line::from(Span::raw(app.input_buffer().to_string())), + ]; + + let input = Paragraph::new(lines) .block(block) .wrap(ratatui::widgets::Wrap { trim: true }); f.render_widget(input, area); if app.input_mode() { - f.set_cursor(area.x + app.input_buffer().len() as u16 + 1, area.y + 1); + // place cursor on the second line where the input buffer is shown + f.set_cursor(area.x + 1 + app.input_buffer().len() as u16, area.y + 2); } } From 0951311aed2362f96d96227ccb505f04f0ed966a Mon Sep 17 00:00:00 2001 From: Entity069 Date: Sat, 8 Nov 2025 02:29:20 +0530 Subject: [PATCH 23/25] chore: add uuid crate --- Cargo.lock | 25 +++++++++++++++++++++++++ Cargo.toml | 1 + 2 files changed, 26 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index ab84ba4..054faaa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -55,6 +55,7 @@ dependencies = [ "index", "storage", "tempfile", + "uuid", ] [[package]] @@ -285,6 +286,7 @@ version = "0.1.0" dependencies = [ "bincode", "serde", + "uuid", ] [[package]] @@ -339,6 +341,12 @@ dependencies = [ "syn", ] +[[package]] +name = "dotenv" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" + [[package]] name = "either" version = "1.15.0" @@ -811,6 +819,7 @@ name = "index" version = "0.1.0" dependencies = [ "core", + "uuid", ] [[package]] @@ -1659,6 +1668,7 @@ dependencies = [ "index", "rocksdb", "tempfile", + "uuid", ] [[package]] @@ -1934,10 +1944,12 @@ name = "tui" version = "0.1.0" dependencies = [ "anyhow", + "api", "chrono", "color-eyre", "core", "crossterm", + "dotenv", "index", "ratatui", "reqwest", @@ -1945,6 +1957,7 @@ dependencies = [ "serde_json", "storage", "tokio", + "uuid", ] [[package]] @@ -2006,6 +2019,18 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "uuid" +version = "1.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" +dependencies = [ + "getrandom 0.3.3", + "js-sys", + "serde", + "wasm-bindgen", +] + [[package]] name = "valuable" version = "0.1.1" diff --git a/Cargo.toml b/Cargo.toml index c2276b3..ebfe9cd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,6 +21,7 @@ anyhow = "1.0" # axum = { version = "0.7", features = ["macros"] } # tonic = "0.11" # prost = "0.12" +uuid = { version = "1.18.1", features = ["v4", "serde"] } # # crates/some-crate/Cargo.toml # [dependencies] From 36f7dca47fd061bc36fbc0c8b0e91fc7db42bf87 Mon Sep 17 00:00:00 2001 From: Entity069 Date: Sat, 8 Nov 2025 10:14:05 +0530 Subject: [PATCH 24/25] chore: remove sentence embeddings and use text only --- .env | 1 - 1 file changed, 1 deletion(-) diff --git a/.env b/.env index 1c2019d..252fd54 100644 --- a/.env +++ b/.env @@ -7,5 +7,4 @@ DATABASE_PATH3=Enter-path-3-here DATABASE_NAME3=Enter-name-3-here TEXT_EMBEDDING_URL=http://localhost:8080/vectors -SENTENCE_EMBEDDING_URL=http://localhost:8000/vectorize IMAGE_EMBEDDING_URL=http://localhost:8080/vectors_img \ No newline at end of file From c1853ffa680d4b1a3fd9777f58f0622d18b58361 Mon Sep 17 00:00:00 2001 From: Entity069 Date: Sat, 8 Nov 2025 10:14:51 +0530 Subject: [PATCH 25/25] chore: rename core to defs --- Cargo.lock | 20 ++++++++++---------- crates/api/src/lib.rs | 2 +- crates/index/Cargo.toml | 2 +- crates/storage/src/rocks_db.rs | 2 +- crates/tui/Cargo.toml | 2 +- crates/tui/src/app/events.rs | 2 +- crates/tui/src/app/state.rs | 2 +- 7 files changed, 16 insertions(+), 16 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 135a9b0..e0dc22f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -280,15 +280,6 @@ dependencies = [ "static_assertions", ] -[[package]] -name = "core" -version = "0.1.0" -dependencies = [ - "bincode", - "serde", - "uuid", -] - [[package]] name = "core-foundation" version = "0.9.4" @@ -330,6 +321,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "defs" +version = "0.1.0" +dependencies = [ + "bincode", + "serde", + "uuid", +] + [[package]] name = "displaydoc" version = "0.2.5" @@ -1947,8 +1947,8 @@ dependencies = [ "api", "chrono", "color-eyre", - "core", "crossterm", + "defs", "dotenv", "index", "ratatui", diff --git a/crates/api/src/lib.rs b/crates/api/src/lib.rs index b139305..3657f58 100644 --- a/crates/api/src/lib.rs +++ b/crates/api/src/lib.rs @@ -158,7 +158,7 @@ mod tests { // TODO: Add more exhaustive tests use super::*; - use core::ContentType; + use defs::ContentType; use tempfile::tempdir; // Helper function to create a test database diff --git a/crates/index/Cargo.toml b/crates/index/Cargo.toml index 4191633..7643ba3 100644 --- a/crates/index/Cargo.toml +++ b/crates/index/Cargo.toml @@ -7,5 +7,5 @@ edition = "2021" [dependencies] -core = { path = "../defs" } +defs = { path = "../defs" } uuid.workspace = true \ No newline at end of file diff --git a/crates/storage/src/rocks_db.rs b/crates/storage/src/rocks_db.rs index 5faeaad..a86ad79 100644 --- a/crates/storage/src/rocks_db.rs +++ b/crates/storage/src/rocks_db.rs @@ -161,7 +161,7 @@ impl StorageEngine for RocksDbStorage { #[cfg(test)] mod tests { use super::*; - use core::ContentType; + use defs::ContentType; use uuid::Uuid; use tempfile::tempdir; diff --git a/crates/tui/Cargo.toml b/crates/tui/Cargo.toml index 506e0ec..b745781 100644 --- a/crates/tui/Cargo.toml +++ b/crates/tui/Cargo.toml @@ -12,7 +12,7 @@ serde.workspace = true anyhow.workspace = true uuid.workspace = true chrono = { version = "0.4", features = ["serde"] } -core = { path = "../defs" } +defs = { path = "../defs" } storage = { path = "../storage" } index = { path = "../index" } api = { path = "../api" } diff --git a/crates/tui/src/app/events.rs b/crates/tui/src/app/events.rs index acc58a1..97b1122 100644 --- a/crates/tui/src/app/events.rs +++ b/crates/tui/src/app/events.rs @@ -1,6 +1,6 @@ use super::{App, AppState, ModalType, VectorListItem}; -use core::{ContentType, Payload, Similarity}; use crossterm::event::{Event, KeyCode, KeyEvent}; +use defs::{ContentType, Payload, Similarity}; use std::io; use std::path::PathBuf; use uuid::Uuid; diff --git a/crates/tui/src/app/state.rs b/crates/tui/src/app/state.rs index dcdda01..12668f7 100644 --- a/crates/tui/src/app/state.rs +++ b/crates/tui/src/app/state.rs @@ -1,4 +1,4 @@ -use core::{DenseVector, Payload}; +use defs::{DenseVector, Payload}; use uuid::Uuid; #[derive(Debug, Default, Clone, PartialEq)]