Implement semantic embeddings and duplicate review
This commit is contained in:
630
Cargo.lock
generated
630
Cargo.lock
generated
@@ -68,7 +68,7 @@ dependencies = [
|
|||||||
"actix-rt",
|
"actix-rt",
|
||||||
"actix-service",
|
"actix-service",
|
||||||
"actix-utils",
|
"actix-utils",
|
||||||
"base64",
|
"base64 0.22.1",
|
||||||
"bitflags 2.11.0",
|
"bitflags 2.11.0",
|
||||||
"brotli",
|
"brotli",
|
||||||
"bytes",
|
"bytes",
|
||||||
@@ -250,6 +250,7 @@ dependencies = [
|
|||||||
"cfg-if",
|
"cfg-if",
|
||||||
"getrandom 0.3.4",
|
"getrandom 0.3.4",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
|
"serde",
|
||||||
"version_check",
|
"version_check",
|
||||||
"zerocopy",
|
"zerocopy",
|
||||||
]
|
]
|
||||||
@@ -777,6 +778,12 @@ dependencies = [
|
|||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "base64"
|
||||||
|
version = "0.13.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "base64"
|
name = "base64"
|
||||||
version = "0.22.1"
|
version = "0.22.1"
|
||||||
@@ -801,12 +808,13 @@ name = "bds-core"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"axum",
|
"axum",
|
||||||
"base64",
|
"base64 0.22.1",
|
||||||
"chrono",
|
"chrono",
|
||||||
"deunicode",
|
"deunicode",
|
||||||
"diesel",
|
"diesel",
|
||||||
"diesel_migrations",
|
"diesel_migrations",
|
||||||
"dirs 5.0.1",
|
"dirs 5.0.1",
|
||||||
|
"fastembed",
|
||||||
"fluent-bundle",
|
"fluent-bundle",
|
||||||
"fluent-syntax",
|
"fluent-syntax",
|
||||||
"htmd",
|
"htmd",
|
||||||
@@ -816,6 +824,7 @@ dependencies = [
|
|||||||
"liquid",
|
"liquid",
|
||||||
"liquid-core",
|
"liquid-core",
|
||||||
"mlua",
|
"mlua",
|
||||||
|
"ort",
|
||||||
"pagefind",
|
"pagefind",
|
||||||
"pulldown-cmark 0.13.3",
|
"pulldown-cmark 0.13.3",
|
||||||
"quick-xml 0.41.0",
|
"quick-xml 0.41.0",
|
||||||
@@ -832,6 +841,7 @@ dependencies = [
|
|||||||
"thiserror 2.0.18",
|
"thiserror 2.0.18",
|
||||||
"tokio",
|
"tokio",
|
||||||
"url",
|
"url",
|
||||||
|
"usearch",
|
||||||
"uuid",
|
"uuid",
|
||||||
"walkdir",
|
"walkdir",
|
||||||
]
|
]
|
||||||
@@ -1188,6 +1198,15 @@ dependencies = [
|
|||||||
"wayland-client",
|
"wayland-client",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "castaway"
|
||||||
|
version = "0.2.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a"
|
||||||
|
dependencies = [
|
||||||
|
"rustversion",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cc"
|
name = "cc"
|
||||||
version = "1.2.58"
|
version = "1.2.58"
|
||||||
@@ -1337,6 +1356,17 @@ dependencies = [
|
|||||||
"unicode-width 0.1.14",
|
"unicode-width 0.1.14",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "codespan-reporting"
|
||||||
|
version = "0.13.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "af491d569909a7e4dee0ad7db7f5341fef5c614d5b8ec8cf765732aba3cff681"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
"termcolor",
|
||||||
|
"unicode-width 0.2.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "color_quant"
|
name = "color_quant"
|
||||||
version = "1.1.0"
|
version = "1.1.0"
|
||||||
@@ -1390,6 +1420,21 @@ dependencies = [
|
|||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "compact_str"
|
||||||
|
version = "0.9.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9dfdd1c2274d9aa354115b09dc9a901d6c5576818cdf70d14cae2bdb47df00ab"
|
||||||
|
dependencies = [
|
||||||
|
"castaway",
|
||||||
|
"cfg-if",
|
||||||
|
"itoa",
|
||||||
|
"rustversion",
|
||||||
|
"ryu",
|
||||||
|
"serde",
|
||||||
|
"static_assertions",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "compression-codecs"
|
name = "compression-codecs"
|
||||||
version = "0.4.37"
|
version = "0.4.37"
|
||||||
@@ -1495,10 +1540,29 @@ version = "0.18.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747"
|
checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"percent-encoding",
|
||||||
"time",
|
"time",
|
||||||
"version_check",
|
"version_check",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cookie_store"
|
||||||
|
version = "0.22.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "15b2c103cf610ec6cae3da84a766285b42fd16aad564758459e6ecf128c75206"
|
||||||
|
dependencies = [
|
||||||
|
"cookie 0.18.1",
|
||||||
|
"document-features",
|
||||||
|
"idna",
|
||||||
|
"indexmap 2.13.0",
|
||||||
|
"log",
|
||||||
|
"serde",
|
||||||
|
"serde_derive",
|
||||||
|
"serde_json",
|
||||||
|
"time",
|
||||||
|
"url",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "core-foundation"
|
name = "core-foundation"
|
||||||
version = "0.9.4"
|
version = "0.9.4"
|
||||||
@@ -1684,6 +1748,68 @@ version = "1.2.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f"
|
checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cxx"
|
||||||
|
version = "1.0.198"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6fe442a792c7c736eea18b32a7f8a3b63cf8aafabda6760042dc2fdeda456291"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"cxx-build",
|
||||||
|
"cxxbridge-cmd",
|
||||||
|
"cxxbridge-flags",
|
||||||
|
"cxxbridge-macro",
|
||||||
|
"foldhash 0.2.0",
|
||||||
|
"link-cplusplus",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cxx-build"
|
||||||
|
version = "1.0.198"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e3184a94384c663718698311a78a51ac00c484c10b4eeac06fb0a068c5f64fa2"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"codespan-reporting 0.13.1",
|
||||||
|
"indexmap 2.13.0",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"scratch",
|
||||||
|
"syn 3.0.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cxxbridge-cmd"
|
||||||
|
version = "1.0.198"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0148d8fd1199329ddf1d157a5e134e51ceff37c6a7ddd38615c399d81cb05d8d"
|
||||||
|
dependencies = [
|
||||||
|
"clap",
|
||||||
|
"codespan-reporting 0.13.1",
|
||||||
|
"indexmap 2.13.0",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 3.0.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cxxbridge-flags"
|
||||||
|
version = "1.0.198"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "52850339faed2eaadd24e286dc1d8268cc6f8a7bd9524d713adc9099566b4c89"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cxxbridge-macro"
|
||||||
|
version = "1.0.198"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2c77c856545d886c9bd5215409ebb63b925e262135248b50c79e5a5f194ee47c"
|
||||||
|
dependencies = [
|
||||||
|
"indexmap 2.13.0",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 3.0.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "d3d12"
|
name = "d3d12"
|
||||||
version = "0.19.0"
|
version = "0.19.0"
|
||||||
@@ -1711,14 +1837,38 @@ dependencies = [
|
|||||||
"zbus 4.4.0",
|
"zbus 4.4.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "darling"
|
||||||
|
version = "0.20.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee"
|
||||||
|
dependencies = [
|
||||||
|
"darling_core 0.20.11",
|
||||||
|
"darling_macro 0.20.11",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "darling"
|
name = "darling"
|
||||||
version = "0.21.3"
|
version = "0.21.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0"
|
checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"darling_core",
|
"darling_core 0.21.3",
|
||||||
"darling_macro",
|
"darling_macro 0.21.3",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "darling_core"
|
||||||
|
version = "0.20.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e"
|
||||||
|
dependencies = [
|
||||||
|
"fnv",
|
||||||
|
"ident_case",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"strsim",
|
||||||
|
"syn 2.0.117",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1735,17 +1885,37 @@ dependencies = [
|
|||||||
"syn 2.0.117",
|
"syn 2.0.117",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "darling_macro"
|
||||||
|
version = "0.20.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead"
|
||||||
|
dependencies = [
|
||||||
|
"darling_core 0.20.11",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.117",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "darling_macro"
|
name = "darling_macro"
|
||||||
version = "0.21.3"
|
version = "0.21.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81"
|
checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"darling_core",
|
"darling_core 0.21.3",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.117",
|
"syn 2.0.117",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dary_heap"
|
||||||
|
version = "0.3.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8b1e3a325bc115f096c8b77bbf027a7c2592230e70be2d985be950d3d5e60ebe"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "data-url"
|
name = "data-url"
|
||||||
version = "0.3.2"
|
version = "0.3.2"
|
||||||
@@ -1788,6 +1958,37 @@ dependencies = [
|
|||||||
"powerfmt",
|
"powerfmt",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "derive_builder"
|
||||||
|
version = "0.20.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947"
|
||||||
|
dependencies = [
|
||||||
|
"derive_builder_macro",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "derive_builder_core"
|
||||||
|
version = "0.20.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8"
|
||||||
|
dependencies = [
|
||||||
|
"darling 0.20.11",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.117",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "derive_builder_macro"
|
||||||
|
version = "0.20.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c"
|
||||||
|
dependencies = [
|
||||||
|
"derive_builder_core",
|
||||||
|
"syn 2.0.117",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "derive_more"
|
name = "derive_more"
|
||||||
version = "2.1.1"
|
version = "2.1.1"
|
||||||
@@ -1985,6 +2186,15 @@ version = "0.3.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0688c2a7f92e427f44895cd63841bff7b29f8d7a1648b9e7e07a4a365b2e1257"
|
checksum = "0688c2a7f92e427f44895cd63841bff7b29f8d7a1648b9e7e07a4a365b2e1257"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "document-features"
|
||||||
|
version = "0.2.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61"
|
||||||
|
dependencies = [
|
||||||
|
"litrs",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dom_query"
|
name = "dom_query"
|
||||||
version = "0.27.0"
|
version = "0.27.0"
|
||||||
@@ -2064,7 +2274,7 @@ version = "0.2.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "dd122633e4bef06db27737f21d3738fb89c8f6d5360d6d9d7635dda142a7757e"
|
checksum = "dd122633e4bef06db27737f21d3738fb89c8f6d5360d6d9d7635dda142a7757e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"darling",
|
"darling 0.21.3",
|
||||||
"either",
|
"either",
|
||||||
"heck 0.5.0",
|
"heck 0.5.0",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
@@ -2212,6 +2422,12 @@ version = "3.3.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59"
|
checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "esaxx-rs"
|
||||||
|
version = "0.1.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d817e038c30374a4bcb22f94d0a8a0e216958d4c3dcde369b1439fec4bdda6e6"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "etagere"
|
name = "etagere"
|
||||||
version = "0.2.15"
|
version = "0.2.15"
|
||||||
@@ -2273,6 +2489,22 @@ version = "1.0.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "dd2e7510819d6fbf51a5545c8f922716ecfb14df168a3242f7d33e0239efe6a1"
|
checksum = "dd2e7510819d6fbf51a5545c8f922716ecfb14df168a3242f7d33e0239efe6a1"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fastembed"
|
||||||
|
version = "5.17.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f3c8600c9ec79b51d60c19911fe14eac04fe9c2895e87d2a3e80e2213d645a32"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"hf-hub",
|
||||||
|
"ndarray",
|
||||||
|
"ort",
|
||||||
|
"safetensors",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"tokenizers",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fastrand"
|
name = "fastrand"
|
||||||
version = "2.3.0"
|
version = "2.3.0"
|
||||||
@@ -3114,6 +3346,32 @@ version = "0.2.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df"
|
checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hf-hub"
|
||||||
|
version = "0.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "aef3982638978efa195ff11b305f51f1f22f4f0a6cabee7af79b383ebee6a213"
|
||||||
|
dependencies = [
|
||||||
|
"dirs 6.0.0",
|
||||||
|
"http 1.4.0",
|
||||||
|
"indicatif",
|
||||||
|
"libc",
|
||||||
|
"log",
|
||||||
|
"rand 0.9.2",
|
||||||
|
"reqwest",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"thiserror 2.0.18",
|
||||||
|
"ureq",
|
||||||
|
"windows-sys 0.61.2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hmac-sha256"
|
||||||
|
version = "1.1.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ec9d92d097f4749b64e8cc33d924d9f40a2d4eb91402b458014b781f5733d60f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "htmd"
|
name = "htmd"
|
||||||
version = "0.5.4"
|
version = "0.5.4"
|
||||||
@@ -3250,7 +3508,7 @@ version = "0.1.20"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"
|
checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64",
|
"base64 0.22.1",
|
||||||
"bytes",
|
"bytes",
|
||||||
"futures-channel",
|
"futures-channel",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
@@ -3705,6 +3963,19 @@ dependencies = [
|
|||||||
"serde_core",
|
"serde_core",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "indicatif"
|
||||||
|
version = "0.18.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "993f007684f2e9727160da8b960ec161264703bfd1af084fd2e34d040c9a0dd4"
|
||||||
|
dependencies = [
|
||||||
|
"console",
|
||||||
|
"portable-atomic",
|
||||||
|
"unicode-width 0.2.2",
|
||||||
|
"unit-prefix",
|
||||||
|
"web-time",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "instant"
|
name = "instant"
|
||||||
version = "0.1.13"
|
version = "0.1.13"
|
||||||
@@ -4185,6 +4456,15 @@ dependencies = [
|
|||||||
"x11",
|
"x11",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "link-cplusplus"
|
||||||
|
version = "1.0.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7f78c730aaa7d0b9336a299029ea49f9ee53b0ed06e9202e8cb7db9bae7b8c82"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "linked-hash-map"
|
name = "linked-hash-map"
|
||||||
version = "0.5.6"
|
version = "0.5.6"
|
||||||
@@ -4269,6 +4549,12 @@ version = "0.8.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0"
|
checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "litrs"
|
||||||
|
version = "1.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "local-channel"
|
name = "local-channel"
|
||||||
version = "0.1.5"
|
version = "0.1.5"
|
||||||
@@ -4412,6 +4698,28 @@ dependencies = [
|
|||||||
"num-traits",
|
"num-traits",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lzma-rust2"
|
||||||
|
version = "0.15.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e20f57f9918e5bd7bc58c22cdd70a6afc7375d4dd9683af5f2b34bd3d2bba619"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "macro_rules_attribute"
|
||||||
|
version = "0.2.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "65049d7923698040cd0b1ddcced9b0eb14dd22c5f86ae59c3740eab64a676520"
|
||||||
|
dependencies = [
|
||||||
|
"macro_rules_attribute-proc_macro",
|
||||||
|
"paste",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "macro_rules_attribute-proc_macro"
|
||||||
|
version = "0.2.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "670fdfda89751bc4a84ac13eaa63e205cf0fd22b4c9a5fbfa085b63c1f1d3a30"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "malloc_buf"
|
name = "malloc_buf"
|
||||||
version = "0.0.6"
|
version = "0.0.6"
|
||||||
@@ -4450,6 +4758,16 @@ version = "0.8.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"
|
checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "matrixmultiply"
|
||||||
|
version = "0.3.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3f607c237553f086e7043417a51df26b2eb899d3caff94e6a67592ff992fedc7"
|
||||||
|
dependencies = [
|
||||||
|
"autocfg",
|
||||||
|
"rawpointer",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "maybe-rayon"
|
name = "maybe-rayon"
|
||||||
version = "0.1.1"
|
version = "0.1.1"
|
||||||
@@ -4625,6 +4943,28 @@ dependencies = [
|
|||||||
"pkg-config",
|
"pkg-config",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "monostate"
|
||||||
|
version = "0.1.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3341a273f6c9d5bef1908f17b7267bbab0e95c9bf69a0d4dcf8e9e1b2c76ef67"
|
||||||
|
dependencies = [
|
||||||
|
"monostate-impl",
|
||||||
|
"serde",
|
||||||
|
"serde_core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "monostate-impl"
|
||||||
|
version = "0.1.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e4db6d5580af57bf992f59068d4ea26fd518574ff48d7639b255a36f9de6e7e9"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.117",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "moxcms"
|
name = "moxcms"
|
||||||
version = "0.8.1"
|
version = "0.8.1"
|
||||||
@@ -4669,7 +5009,7 @@ checksum = "50e3524642f53d9af419ab5e8dd29d3ba155708267667c2f3f06c88c9e130843"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"bit-set 0.5.3",
|
"bit-set 0.5.3",
|
||||||
"bitflags 2.11.0",
|
"bitflags 2.11.0",
|
||||||
"codespan-reporting",
|
"codespan-reporting 0.11.1",
|
||||||
"hexf-parse",
|
"hexf-parse",
|
||||||
"indexmap 2.13.0",
|
"indexmap 2.13.0",
|
||||||
"log",
|
"log",
|
||||||
@@ -4681,6 +5021,21 @@ dependencies = [
|
|||||||
"unicode-xid",
|
"unicode-xid",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ndarray"
|
||||||
|
version = "0.17.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "520080814a7a6b4a6e9070823bb24b4531daac8c4627e08ba5de8c5ef2f2752d"
|
||||||
|
dependencies = [
|
||||||
|
"matrixmultiply",
|
||||||
|
"num-complex",
|
||||||
|
"num-integer",
|
||||||
|
"num-traits",
|
||||||
|
"portable-atomic",
|
||||||
|
"portable-atomic-util",
|
||||||
|
"rawpointer",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ndk"
|
name = "ndk"
|
||||||
version = "0.9.0"
|
version = "0.9.0"
|
||||||
@@ -4774,6 +5129,15 @@ dependencies = [
|
|||||||
"num-traits",
|
"num-traits",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num-complex"
|
||||||
|
version = "0.4.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495"
|
||||||
|
dependencies = [
|
||||||
|
"num-traits",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-conv"
|
name = "num-conv"
|
||||||
version = "0.2.1"
|
version = "0.2.1"
|
||||||
@@ -4843,6 +5207,15 @@ dependencies = [
|
|||||||
"syn 2.0.117",
|
"syn 2.0.117",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "numkong"
|
||||||
|
version = "7.7.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b58d4bb97df102ebdde66a352a20c0bc65c7c407ee9bef12ebe317edc2458a55"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "objc"
|
name = "objc"
|
||||||
version = "0.2.7"
|
version = "0.2.7"
|
||||||
@@ -5338,6 +5711,30 @@ dependencies = [
|
|||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ort"
|
||||||
|
version = "2.0.0-rc.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d7de3af33d24a745ffb8fab904b13478438d1cd52868e6f17735ef6e1f8bf133"
|
||||||
|
dependencies = [
|
||||||
|
"ndarray",
|
||||||
|
"ort-sys",
|
||||||
|
"smallvec",
|
||||||
|
"tracing",
|
||||||
|
"ureq",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ort-sys"
|
||||||
|
version = "2.0.0-rc.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d7b497d21a8b6fbb4b5a544f8fadb77e801a09ae0add9e411d31c6f89e3c1e90"
|
||||||
|
dependencies = [
|
||||||
|
"hmac-sha256",
|
||||||
|
"lzma-rust2",
|
||||||
|
"ureq",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "owned_ttf_parser"
|
name = "owned_ttf_parser"
|
||||||
version = "0.25.1"
|
version = "0.25.1"
|
||||||
@@ -5357,7 +5754,7 @@ dependencies = [
|
|||||||
"actix-web",
|
"actix-web",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-compression",
|
"async-compression",
|
||||||
"base64",
|
"base64 0.22.1",
|
||||||
"bit-set 0.10.0",
|
"bit-set 0.10.0",
|
||||||
"clap",
|
"clap",
|
||||||
"console",
|
"console",
|
||||||
@@ -5736,7 +6133,7 @@ version = "1.8.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07"
|
checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64",
|
"base64 0.22.1",
|
||||||
"indexmap 2.13.0",
|
"indexmap 2.13.0",
|
||||||
"quick-xml 0.38.4",
|
"quick-xml 0.38.4",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -5798,6 +6195,21 @@ dependencies = [
|
|||||||
"nom 7.1.3",
|
"nom 7.1.3",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "portable-atomic"
|
||||||
|
version = "1.14.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3d20d5497ef88037a52ff98267d066e7f11fcc5e99bbfbd58a42336193aacec3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "portable-atomic-util"
|
||||||
|
version = "0.2.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618"
|
||||||
|
dependencies = [
|
||||||
|
"portable-atomic",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "portpicker"
|
name = "portpicker"
|
||||||
version = "0.1.1"
|
version = "0.1.1"
|
||||||
@@ -6217,6 +6629,12 @@ version = "0.6.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539"
|
checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rawpointer"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rayon"
|
name = "rayon"
|
||||||
version = "1.11.0"
|
version = "1.11.0"
|
||||||
@@ -6227,6 +6645,17 @@ dependencies = [
|
|||||||
"rayon-core",
|
"rayon-core",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rayon-cond"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2964d0cf57a3e7a06e8183d14a8b527195c706b7983549cd5462d5aa3747438f"
|
||||||
|
dependencies = [
|
||||||
|
"either",
|
||||||
|
"itertools",
|
||||||
|
"rayon",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rayon-core"
|
name = "rayon-core"
|
||||||
version = "1.13.0"
|
version = "1.13.0"
|
||||||
@@ -6352,7 +6781,7 @@ version = "0.12.28"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147"
|
checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64",
|
"base64 0.22.1",
|
||||||
"bytes",
|
"bytes",
|
||||||
"futures-channel",
|
"futures-channel",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
@@ -6376,12 +6805,14 @@ dependencies = [
|
|||||||
"sync_wrapper",
|
"sync_wrapper",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-rustls",
|
"tokio-rustls",
|
||||||
|
"tokio-util",
|
||||||
"tower",
|
"tower",
|
||||||
"tower-http",
|
"tower-http",
|
||||||
"tower-service",
|
"tower-service",
|
||||||
"url",
|
"url",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
"wasm-bindgen-futures",
|
"wasm-bindgen-futures",
|
||||||
|
"wasm-streams",
|
||||||
"web-sys",
|
"web-sys",
|
||||||
"webpki-roots",
|
"webpki-roots",
|
||||||
]
|
]
|
||||||
@@ -6569,6 +7000,7 @@ version = "0.23.37"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4"
|
checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"log",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"ring",
|
"ring",
|
||||||
"rustls-pki-types",
|
"rustls-pki-types",
|
||||||
@@ -6627,6 +7059,19 @@ version = "1.0.23"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
|
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "safetensors"
|
||||||
|
version = "0.8.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "79b079b829cb27a1c3c374341345ed2e8b2c0c839034522cee576c140bd7f846"
|
||||||
|
dependencies = [
|
||||||
|
"hashbrown 0.16.1",
|
||||||
|
"libc",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"tempfile",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "same-file"
|
name = "same-file"
|
||||||
version = "1.0.6"
|
version = "1.0.6"
|
||||||
@@ -6648,6 +7093,12 @@ version = "1.2.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "scratch"
|
||||||
|
version = "1.0.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d68f2ec51b097e4c1a75b681a8bec621909b5e91f15bb7b840c4f2f7b01148b2"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sctk-adwaita"
|
name = "sctk-adwaita"
|
||||||
version = "0.10.1"
|
version = "0.10.1"
|
||||||
@@ -7104,6 +7555,17 @@ dependencies = [
|
|||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "socks"
|
||||||
|
version = "0.3.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f0c3dbbd9ae980613c6dd8e28a9407b50509d3803b57624d5dfe8315218cd58b"
|
||||||
|
dependencies = [
|
||||||
|
"byteorder",
|
||||||
|
"libc",
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "softbuffer"
|
name = "softbuffer"
|
||||||
version = "0.4.8"
|
version = "0.4.8"
|
||||||
@@ -7171,6 +7633,18 @@ dependencies = [
|
|||||||
"bitflags 2.11.0",
|
"bitflags 2.11.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "spm_precompiled"
|
||||||
|
version = "0.1.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5851699c4033c63636f7ea4cf7b7c1f1bf06d0cc03cfb42e711de5a5c46cf326"
|
||||||
|
dependencies = [
|
||||||
|
"base64 0.13.1",
|
||||||
|
"nom 7.1.3",
|
||||||
|
"serde",
|
||||||
|
"unicode-segmentation",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sqlite-wasm-rs"
|
name = "sqlite-wasm-rs"
|
||||||
version = "0.5.5"
|
version = "0.5.5"
|
||||||
@@ -7296,6 +7770,17 @@ dependencies = [
|
|||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "syn"
|
||||||
|
version = "3.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f2fac314a64dc9a36e61a9eb4261a5e9bbfbc922b27e518af97bc32b926cf967"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sync_wrapper"
|
name = "sync_wrapper"
|
||||||
version = "1.0.2"
|
version = "1.0.2"
|
||||||
@@ -7589,6 +8074,39 @@ version = "0.1.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tokenizers"
|
||||||
|
version = "0.22.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b238e22d44a15349529690fb07bd645cf58149a1b1e44d6cb5bd1641ff1a6223"
|
||||||
|
dependencies = [
|
||||||
|
"ahash 0.8.12",
|
||||||
|
"aho-corasick",
|
||||||
|
"compact_str",
|
||||||
|
"dary_heap",
|
||||||
|
"derive_builder",
|
||||||
|
"esaxx-rs",
|
||||||
|
"getrandom 0.3.4",
|
||||||
|
"itertools",
|
||||||
|
"log",
|
||||||
|
"macro_rules_attribute",
|
||||||
|
"monostate",
|
||||||
|
"onig",
|
||||||
|
"paste",
|
||||||
|
"rand 0.9.2",
|
||||||
|
"rayon",
|
||||||
|
"rayon-cond",
|
||||||
|
"regex",
|
||||||
|
"regex-syntax",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"spm_precompiled",
|
||||||
|
"thiserror 2.0.18",
|
||||||
|
"unicode-normalization-alignments",
|
||||||
|
"unicode-segmentation",
|
||||||
|
"unicode_categories",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio"
|
name = "tokio"
|
||||||
version = "1.50.0"
|
version = "1.50.0"
|
||||||
@@ -8007,6 +8525,15 @@ dependencies = [
|
|||||||
"tinyvec",
|
"tinyvec",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-normalization-alignments"
|
||||||
|
version = "0.1.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "43f613e4fa046e69818dd287fdc4bc78175ff20331479dab6e1b0f98d57062de"
|
||||||
|
dependencies = [
|
||||||
|
"smallvec",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-properties"
|
name = "unicode-properties"
|
||||||
version = "0.1.4"
|
version = "0.1.4"
|
||||||
@@ -8049,6 +8576,18 @@ version = "0.2.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
|
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode_categories"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unit-prefix"
|
||||||
|
version = "0.5.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "81e544489bf3d8ef66c953931f56617f423cd4b5494be343d9b9d3dda037b9a3"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unsafe-libyaml"
|
name = "unsafe-libyaml"
|
||||||
version = "0.2.11"
|
version = "0.2.11"
|
||||||
@@ -8061,6 +8600,39 @@ version = "0.9.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
|
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ureq"
|
||||||
|
version = "3.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "dea7109cdcd5864d4eeb1b58a1648dc9bf520360d7af16ec26d0a9354bafcfc0"
|
||||||
|
dependencies = [
|
||||||
|
"base64 0.22.1",
|
||||||
|
"cookie_store",
|
||||||
|
"flate2",
|
||||||
|
"log",
|
||||||
|
"percent-encoding",
|
||||||
|
"rustls",
|
||||||
|
"rustls-pki-types",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"socks",
|
||||||
|
"ureq-proto",
|
||||||
|
"utf8-zero",
|
||||||
|
"webpki-roots",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ureq-proto"
|
||||||
|
version = "0.6.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e994ba84b0bd1b1b0cf92878b7ef898a5c1760108fe7b6010327e274917a808c"
|
||||||
|
dependencies = [
|
||||||
|
"base64 0.22.1",
|
||||||
|
"http 1.4.0",
|
||||||
|
"httparse",
|
||||||
|
"log",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "url"
|
name = "url"
|
||||||
version = "2.5.8"
|
version = "2.5.8"
|
||||||
@@ -8080,13 +8652,24 @@ version = "2.1.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
|
checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "usearch"
|
||||||
|
version = "2.26.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1cd7f672d20412962c457b11c858c6c5aecb949808a5345a95e1d671112bcf72"
|
||||||
|
dependencies = [
|
||||||
|
"cxx",
|
||||||
|
"cxx-build",
|
||||||
|
"numkong",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "usvg"
|
name = "usvg"
|
||||||
version = "0.42.0"
|
version = "0.42.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b84ea542ae85c715f07b082438a4231c3760539d902e11d093847a0b22963032"
|
checksum = "b84ea542ae85c715f07b082438a4231c3760539d902e11d093847a0b22963032"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64",
|
"base64 0.22.1",
|
||||||
"data-url",
|
"data-url",
|
||||||
"flate2",
|
"flate2",
|
||||||
"fontdb 0.18.0",
|
"fontdb 0.18.0",
|
||||||
@@ -8119,6 +8702,12 @@ version = "0.1.8"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1292c0d970b54115d14f2492fe0170adf21d68a1de108eebc51c1df4f346a091"
|
checksum = "1292c0d970b54115d14f2492fe0170adf21d68a1de108eebc51c1df4f346a091"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "utf8-zero"
|
||||||
|
version = "0.8.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b8c0a043c9540bae7c578c88f91dda8bd82e59ae27c21baca69c8b191aaf5a6e"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "utf8_iter"
|
name = "utf8_iter"
|
||||||
version = "1.0.4"
|
version = "1.0.4"
|
||||||
@@ -8298,6 +8887,19 @@ dependencies = [
|
|||||||
"wasmparser",
|
"wasmparser",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-streams"
|
||||||
|
version = "0.4.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65"
|
||||||
|
dependencies = [
|
||||||
|
"futures-util",
|
||||||
|
"js-sys",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"wasm-bindgen-futures",
|
||||||
|
"web-sys",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-timer"
|
name = "wasm-timer"
|
||||||
version = "0.2.5"
|
version = "0.2.5"
|
||||||
@@ -8637,7 +9239,7 @@ dependencies = [
|
|||||||
"bit-vec 0.6.3",
|
"bit-vec 0.6.3",
|
||||||
"bitflags 2.11.0",
|
"bitflags 2.11.0",
|
||||||
"cfg_aliases 0.1.1",
|
"cfg_aliases 0.1.1",
|
||||||
"codespan-reporting",
|
"codespan-reporting 0.11.1",
|
||||||
"indexmap 2.13.0",
|
"indexmap 2.13.0",
|
||||||
"log",
|
"log",
|
||||||
"naga",
|
"naga",
|
||||||
@@ -9440,7 +10042,7 @@ version = "0.55.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3013fd6116aac351dd2e18f349b28b2cfef3a5ff3253a9d0ce2d7193bb1b4429"
|
checksum = "3013fd6116aac351dd2e18f349b28b2cfef3a5ff3253a9d0ce2d7193bb1b4429"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64",
|
"base64 0.22.1",
|
||||||
"block2 0.6.2",
|
"block2 0.6.2",
|
||||||
"cookie 0.18.1",
|
"cookie 0.18.1",
|
||||||
"crossbeam-channel",
|
"crossbeam-channel",
|
||||||
|
|||||||
@@ -52,6 +52,9 @@ diesel_migrations = "2.3.2"
|
|||||||
libsqlite3-sys = { version = "0.37.0", features = ["bundled"] }
|
libsqlite3-sys = { version = "0.37.0", features = ["bundled"] }
|
||||||
mlua = { version = "0.12", features = ["lua54", "vendored", "serialize"] }
|
mlua = { version = "0.12", features = ["lua54", "vendored", "serialize"] }
|
||||||
url = "2.5"
|
url = "2.5"
|
||||||
|
fastembed = { version = "5.17.3", default-features = false, features = ["hf-hub-rustls-tls", "ort-download-binaries-rustls-tls"] }
|
||||||
|
usearch = "2.26"
|
||||||
|
ort = { version = "=2.0.0-rc.12", default-features = false, features = ["std", "api-24", "coreml", "directml"] }
|
||||||
|
|
||||||
# UI framework
|
# UI framework
|
||||||
iced = { version = "0.13", features = ["wgpu", "advanced", "canvas", "image", "svg", "tokio", "markdown"] }
|
iced = { version = "0.13", features = ["wgpu", "advanced", "canvas", "image", "svg", "tokio", "markdown"] }
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ The project is under active development. Core blogging workflows are broadly ava
|
|||||||
- WordPress WXR migration with saved analyses, HTML-to-Markdown and shortcode conversion, conflict/taxonomy review, recoverable 500-item execution batches, media-parent linking, progress reporting, and optional AI-assisted taxonomy mapping.
|
- WordPress WXR migration with saved analyses, HTML-to-Markdown and shortcode conversion, conflict/taxonomy review, recoverable 500-item execution batches, media-parent linking, progress reporting, and optional AI-assisted taxonomy mapping.
|
||||||
- Template and Lua script management with explicit syntax-check feedback, using a custom Ropey/Syntect/Cosmic Text editor and the documented, bDS2-signature-compatible project-scoped [`bds` host API](docs/scripting/API_REFERENCE.md) across utilities, rendered macros, and Blogmark transforms.
|
- Template and Lua script management with explicit syntax-check feedback, using a custom Ropey/Syntect/Cosmic Text editor and the documented, bDS2-signature-compatible project-scoped [`bds` host API](docs/scripting/API_REFERENCE.md) across utilities, rendered macros, and Blogmark transforms.
|
||||||
- SQLite and filesystem persistence with frontmatter, sidecars, rebuild, metadata diff/repair, and FTS5 search.
|
- SQLite and filesystem persistence with frontmatter, sidecars, rebuild, metadata diff/repair, and FTS5 search.
|
||||||
|
- Optional on-device multilingual semantic search and tag suggestions backed by a persistent USearch index, with duplicate-post review and dismissal in the desktop workspace.
|
||||||
- Project-scoped typed domain events synchronize desktop views with shared-engine and future CLI mutations; persisted CLI notifications are consumed once, and the selected UI language is shared through settings.
|
- Project-scoped typed domain events synchronize desktop views with shared-engine and future CLI mutations; persisted CLI notifications are consumed once, and the selected UI language is shared through settings.
|
||||||
- Headless `bds-cli` automation for rebuild/repair/render, publishing and Git sync, post/media/gallery creation, shared settings/projects, utility Lua tasks, JSON I/O, airplane-mode AI routing, and guarded launcher installation from Settings → Data or `bds-cli install`.
|
- Headless `bds-cli` automation for rebuild/repair/render, publishing and Git sync, post/media/gallery creation, shared settings/projects, utility Lua tasks, JSON I/O, airplane-mode AI routing, and guarded launcher installation from Settings → Data or `bds-cli install`.
|
||||||
- Local MCP automation over stdio or a localhost-only stateless HTTP endpoint, with project resources, read/search/count tools, inert write proposals, explicit desktop approval, and opt-in Claude Code/Copilot configuration.
|
- Local MCP automation over stdio or a localhost-only stateless HTTP endpoint, with project resources, read/search/count tools, inert write proposals, explicit desktop approval, and opt-in Claude Code/Copilot configuration.
|
||||||
|
|||||||
@@ -132,7 +132,7 @@ Available:
|
|||||||
- Fixed `.lua` script file contract.
|
- Fixed `.lua` script file contract.
|
||||||
- Generated and bundled API/type references plus executable macro, transform, and utility examples under `docs/scripting/`.
|
- Generated and bundled API/type references plus executable macro, transform, and utility examples under `docs/scripting/`.
|
||||||
- Manifest-driven runtime, documentation, and completion data with drift checks.
|
- Manifest-driven runtime, documentation, and completion data with drift checks.
|
||||||
- `bds.sync` and `bds.embeddings` remain extension APIs and are not exposed by core.
|
- The embedding extension adds the bDS2-compatible `bds.embeddings` namespace; `bds.sync` remains outside the core API.
|
||||||
|
|
||||||
## Remaining Core Blocks
|
## Remaining Core Blocks
|
||||||
|
|
||||||
|
|||||||
@@ -42,17 +42,13 @@ Done:
|
|||||||
- Fixed native A2UI cards, charts, forms, lists, metrics, mind maps, tables, and tabs with safe nested fallbacks, persistent stable-ID interaction state, debounced form values, and strictly allowlisted application actions.
|
- Fixed native A2UI cards, charts, forms, lists, metrics, mind maps, tables, and tabs with safe nested fallbacks, persistent stable-ID interaction state, debounced form values, and strictly allowlisted application actions.
|
||||||
- Online/airplane endpoint routing uses the shared secure endpoint and model infrastructure; unavailable modes direct the user to the existing localized AI settings.
|
- Online/airplane endpoint routing uses the shared secure endpoint and model infrastructure; unavailable modes direct the user to the existing localized AI settings.
|
||||||
|
|
||||||
### Embeddings, semantic search, and duplicates — Open
|
### Embeddings, semantic search, and duplicates — Done
|
||||||
|
|
||||||
Existing source contains schema placeholders only.
|
- Lazy, locally cached `multilingual-e5-small` inference with 384-dimensional mean-pooled, normalized vectors and native Core ML acceleration on macOS or DirectML on Windows.
|
||||||
|
- Project-isolated USearch HNSW indexes with SQLite BLOB vectors as the recovery source, debounced persistence, lifecycle updates, rebuild/backfill, and CLI repair.
|
||||||
Open:
|
- Semantic sidebar search, link ranking, editor tag suggestions, and the bDS2-compatible `bds.embeddings` Lua API.
|
||||||
|
- Localized duplicate review with exact-match detection, 500-pair pagination, canonical single/batch dismissal, and post navigation.
|
||||||
- Embedding generation and persistence.
|
- Embedding-aware filesystem rebuild and metadata diff/repair, settings gating, project-switch/shutdown flushes, and native menu commands.
|
||||||
- Vector index and semantic search.
|
|
||||||
- Expose `bds.embeddings` after those engines exist.
|
|
||||||
- Duplicate detection, dismissed-pair handling, metadata integrity, and UI.
|
|
||||||
- Replace the Find Duplicates placeholder.
|
|
||||||
|
|
||||||
### Translation QA and documentation UX — Partly done
|
### Translation QA and documentation UX — Partly done
|
||||||
|
|
||||||
|
|||||||
@@ -317,12 +317,6 @@ fn active_project(db: &Database) -> Result<(Project, PathBuf)> {
|
|||||||
|
|
||||||
fn rebuild(db: &Database, incremental: bool) -> Result<CommandOutput> {
|
fn rebuild(db: &Database, incremental: bool) -> Result<CommandOutput> {
|
||||||
let (project, data_dir) = active_project(db)?;
|
let (project, data_dir) = active_project(db)?;
|
||||||
let metadata = engine::meta::read_project_json(&data_dir)?;
|
|
||||||
if metadata.semantic_similarity_enabled {
|
|
||||||
bail!(
|
|
||||||
"semantic similarity is enabled, but rebuild cannot continue without the real on-device embedding engine"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if incremental {
|
if incremental {
|
||||||
let (applied, imported, failed) = cli_sync::run_cli_mutation(db.conn(), || {
|
let (applied, imported, failed) = cli_sync::run_cli_mutation(db.conn(), || {
|
||||||
let report =
|
let report =
|
||||||
@@ -490,9 +484,13 @@ fn repair(db: &Database, part: RepairPart) -> Result<CommandOutput> {
|
|||||||
RepairPart::Embeddings => {
|
RepairPart::Embeddings => {
|
||||||
let metadata = engine::meta::read_project_json(&data_dir)?;
|
let metadata = engine::meta::read_project_json(&data_dir)?;
|
||||||
if metadata.semantic_similarity_enabled {
|
if metadata.semantic_similarity_enabled {
|
||||||
bail!(
|
let service = engine::embedding::EmbeddingService::production(db.conn(), &data_dir);
|
||||||
"semantic similarity is enabled, but no real on-device embedding engine is registered"
|
let indexed = service.reindex_all(&project.id)?;
|
||||||
);
|
service.flush_project(&project.id)?;
|
||||||
|
return Ok(output(
|
||||||
|
"Embedding index rebuilt",
|
||||||
|
json!({"rebuilt": indexed.len(), "disabled": false}),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
Ok(output(
|
Ok(output(
|
||||||
"Embedding repair skipped because semantic similarity is disabled",
|
"Embedding repair skipped because semantic similarity is disabled",
|
||||||
@@ -1431,8 +1429,9 @@ mod tests {
|
|||||||
let mut metadata = engine::meta::read_project_json(&fixture.project_dir).unwrap();
|
let mut metadata = engine::meta::read_project_json(&fixture.project_dir).unwrap();
|
||||||
metadata.semantic_similarity_enabled = true;
|
metadata.semantic_similarity_enabled = true;
|
||||||
engine::meta::write_project_json(&fixture.project_dir, &metadata).unwrap();
|
engine::meta::write_project_json(&fixture.project_dir, &metadata).unwrap();
|
||||||
assert!(fixture.run(&["repair", "embeddings"], "").is_err());
|
let repaired = fixture.run(&["repair", "embeddings"], "").unwrap();
|
||||||
assert!(fixture.run(&["rebuild"], "").is_err());
|
assert_eq!(repaired.data["rebuilt"], 0);
|
||||||
|
fixture.run(&["rebuild"], "").unwrap();
|
||||||
metadata.semantic_similarity_enabled = false;
|
metadata.semantic_similarity_enabled = false;
|
||||||
engine::meta::write_project_json(&fixture.project_dir, &metadata).unwrap();
|
engine::meta::write_project_json(&fixture.project_dir, &metadata).unwrap();
|
||||||
fixture.run(&["render"], "").unwrap();
|
fixture.run(&["render"], "").unwrap();
|
||||||
|
|||||||
@@ -38,6 +38,9 @@ mlua = { workspace = true }
|
|||||||
url = { workspace = true }
|
url = { workspace = true }
|
||||||
base64 = { workspace = true }
|
base64 = { workspace = true }
|
||||||
dirs = { workspace = true }
|
dirs = { workspace = true }
|
||||||
|
fastembed = { workspace = true }
|
||||||
|
usearch = { workspace = true }
|
||||||
|
ort = { workspace = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tempfile = "3"
|
tempfile = "3"
|
||||||
|
|||||||
@@ -258,9 +258,12 @@ CREATE TABLE IF NOT EXISTS embedding_keys (
|
|||||||
post_id TEXT NOT NULL,
|
post_id TEXT NOT NULL,
|
||||||
project_id TEXT NOT NULL,
|
project_id TEXT NOT NULL,
|
||||||
content_hash TEXT NOT NULL,
|
content_hash TEXT NOT NULL,
|
||||||
vector TEXT NOT NULL
|
vector BLOB NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS embedding_keys_project_post_idx
|
||||||
|
ON embedding_keys(project_id, post_id);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS dismissed_duplicate_pairs (
|
CREATE TABLE IF NOT EXISTS dismissed_duplicate_pairs (
|
||||||
id TEXT NOT NULL PRIMARY KEY,
|
id TEXT NOT NULL PRIMARY KEY,
|
||||||
project_id TEXT NOT NULL REFERENCES projects(id),
|
project_id TEXT NOT NULL REFERENCES projects(id),
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
-- This file should undo anything in `up.sql`
|
||||||
|
CREATE TABLE embedding_keys_old (
|
||||||
|
label INTEGER NOT NULL PRIMARY KEY,
|
||||||
|
post_id TEXT NOT NULL,
|
||||||
|
project_id TEXT NOT NULL,
|
||||||
|
content_hash TEXT NOT NULL,
|
||||||
|
vector TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO embedding_keys_old (label, post_id, project_id, content_hash, vector)
|
||||||
|
SELECT label, post_id, project_id, content_hash, CAST(vector AS TEXT)
|
||||||
|
FROM embedding_keys;
|
||||||
|
|
||||||
|
DROP TABLE embedding_keys;
|
||||||
|
ALTER TABLE embedding_keys_old RENAME TO embedding_keys;
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
-- Your SQL goes here
|
||||||
|
CREATE TABLE embedding_keys_new (
|
||||||
|
label INTEGER NOT NULL PRIMARY KEY,
|
||||||
|
post_id TEXT NOT NULL,
|
||||||
|
project_id TEXT NOT NULL,
|
||||||
|
content_hash TEXT NOT NULL,
|
||||||
|
vector BLOB NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO embedding_keys_new (label, post_id, project_id, content_hash, vector)
|
||||||
|
SELECT label, post_id, project_id, content_hash, CAST(vector AS BLOB)
|
||||||
|
FROM embedding_keys;
|
||||||
|
|
||||||
|
DROP TABLE embedding_keys;
|
||||||
|
ALTER TABLE embedding_keys_new RENAME TO embedding_keys;
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX embedding_keys_project_post_idx
|
||||||
|
ON embedding_keys(project_id, post_id);
|
||||||
@@ -36,7 +36,7 @@ mod tests {
|
|||||||
let applied = db
|
let applied = db
|
||||||
.conn()
|
.conn()
|
||||||
.with_migrations(|conn| conn.applied_migrations().unwrap().len());
|
.with_migrations(|conn| conn.applied_migrations().unwrap().len());
|
||||||
assert_eq!(applied, 6);
|
assert_eq!(applied, 7);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
283
crates/bds-core/src/db/queries/embedding.rs
Normal file
283
crates/bds-core/src/db/queries/embedding.rs
Normal file
@@ -0,0 +1,283 @@
|
|||||||
|
use diesel::prelude::*;
|
||||||
|
|
||||||
|
use crate::db::DbConnection;
|
||||||
|
use crate::db::schema::{dismissed_duplicate_pairs, embedding_keys};
|
||||||
|
use crate::model::{DismissedDuplicatePair, EmbeddingKey};
|
||||||
|
|
||||||
|
pub fn get_key_for_post(
|
||||||
|
conn: &DbConnection,
|
||||||
|
project_id: &str,
|
||||||
|
post_id: &str,
|
||||||
|
) -> QueryResult<Option<EmbeddingKey>> {
|
||||||
|
conn.with(|c| {
|
||||||
|
embedding_keys::table
|
||||||
|
.filter(embedding_keys::project_id.eq(project_id))
|
||||||
|
.filter(embedding_keys::post_id.eq(post_id))
|
||||||
|
.select(EmbeddingKey::as_select())
|
||||||
|
.first(c)
|
||||||
|
.optional()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn list_keys(conn: &DbConnection, project_id: &str) -> QueryResult<Vec<EmbeddingKey>> {
|
||||||
|
conn.with(|c| {
|
||||||
|
embedding_keys::table
|
||||||
|
.filter(embedding_keys::project_id.eq(project_id))
|
||||||
|
.order(embedding_keys::label.asc())
|
||||||
|
.select(EmbeddingKey::as_select())
|
||||||
|
.load(c)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn max_label(conn: &DbConnection) -> QueryResult<i64> {
|
||||||
|
conn.with(|c| {
|
||||||
|
embedding_keys::table
|
||||||
|
.select(embedding_keys::label)
|
||||||
|
.order(embedding_keys::label.desc())
|
||||||
|
.first(c)
|
||||||
|
.optional()
|
||||||
|
.map(|label| label.unwrap_or(0))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn upsert_key(conn: &DbConnection, key: &EmbeddingKey) -> QueryResult<()> {
|
||||||
|
conn.with(|c| {
|
||||||
|
diesel::insert_into(embedding_keys::table)
|
||||||
|
.values(key)
|
||||||
|
.on_conflict((embedding_keys::project_id, embedding_keys::post_id))
|
||||||
|
.do_update()
|
||||||
|
.set((
|
||||||
|
embedding_keys::content_hash.eq(&key.content_hash),
|
||||||
|
embedding_keys::vector.eq(&key.vector),
|
||||||
|
))
|
||||||
|
.execute(c)
|
||||||
|
.map(|_| ())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn delete_key_for_post(
|
||||||
|
conn: &DbConnection,
|
||||||
|
project_id: &str,
|
||||||
|
post_id: &str,
|
||||||
|
) -> QueryResult<()> {
|
||||||
|
conn.with(|c| {
|
||||||
|
diesel::delete(
|
||||||
|
embedding_keys::table
|
||||||
|
.filter(embedding_keys::project_id.eq(project_id))
|
||||||
|
.filter(embedding_keys::post_id.eq(post_id)),
|
||||||
|
)
|
||||||
|
.execute(c)
|
||||||
|
.map(|_| ())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn delete_stale_keys(
|
||||||
|
conn: &DbConnection,
|
||||||
|
project_id: &str,
|
||||||
|
live_post_ids: &[String],
|
||||||
|
) -> QueryResult<usize> {
|
||||||
|
conn.with(|c| {
|
||||||
|
let query = embedding_keys::table.filter(embedding_keys::project_id.eq(project_id));
|
||||||
|
if live_post_ids.is_empty() {
|
||||||
|
diesel::delete(query).execute(c)
|
||||||
|
} else {
|
||||||
|
diesel::delete(query.filter(embedding_keys::post_id.ne_all(live_post_ids))).execute(c)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn insert_dismissed_pair(
|
||||||
|
conn: &DbConnection,
|
||||||
|
pair: &DismissedDuplicatePair,
|
||||||
|
) -> QueryResult<()> {
|
||||||
|
conn.with(|c| {
|
||||||
|
diesel::insert_into(dismissed_duplicate_pairs::table)
|
||||||
|
.values(pair)
|
||||||
|
.on_conflict((
|
||||||
|
dismissed_duplicate_pairs::project_id,
|
||||||
|
dismissed_duplicate_pairs::post_id_a,
|
||||||
|
dismissed_duplicate_pairs::post_id_b,
|
||||||
|
))
|
||||||
|
.do_nothing()
|
||||||
|
.execute(c)
|
||||||
|
.map(|_| ())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn insert_dismissed_pairs(
|
||||||
|
conn: &DbConnection,
|
||||||
|
pairs: &[DismissedDuplicatePair],
|
||||||
|
) -> QueryResult<usize> {
|
||||||
|
if pairs.is_empty() {
|
||||||
|
return Ok(0);
|
||||||
|
}
|
||||||
|
conn.with(|c| {
|
||||||
|
diesel::insert_into(dismissed_duplicate_pairs::table)
|
||||||
|
.values(pairs)
|
||||||
|
.on_conflict((
|
||||||
|
dismissed_duplicate_pairs::project_id,
|
||||||
|
dismissed_duplicate_pairs::post_id_a,
|
||||||
|
dismissed_duplicate_pairs::post_id_b,
|
||||||
|
))
|
||||||
|
.do_nothing()
|
||||||
|
.execute(c)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn list_dismissed_pairs(
|
||||||
|
conn: &DbConnection,
|
||||||
|
project_id: &str,
|
||||||
|
) -> QueryResult<Vec<DismissedDuplicatePair>> {
|
||||||
|
conn.with(|c| {
|
||||||
|
dismissed_duplicate_pairs::table
|
||||||
|
.filter(dismissed_duplicate_pairs::project_id.eq(project_id))
|
||||||
|
.select(DismissedDuplicatePair::as_select())
|
||||||
|
.load(c)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn delete_orphan_dismissals(
|
||||||
|
conn: &DbConnection,
|
||||||
|
project_id: &str,
|
||||||
|
live_post_ids: &[String],
|
||||||
|
) -> QueryResult<usize> {
|
||||||
|
conn.with(|c| {
|
||||||
|
let query = dismissed_duplicate_pairs::table
|
||||||
|
.filter(dismissed_duplicate_pairs::project_id.eq(project_id));
|
||||||
|
if live_post_ids.is_empty() {
|
||||||
|
diesel::delete(query).execute(c)
|
||||||
|
} else {
|
||||||
|
diesel::delete(
|
||||||
|
query.filter(
|
||||||
|
dismissed_duplicate_pairs::post_id_a
|
||||||
|
.ne_all(live_post_ids)
|
||||||
|
.or(dismissed_duplicate_pairs::post_id_b.ne_all(live_post_ids)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.execute(c)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn delete_dismissals_for_post(
|
||||||
|
conn: &DbConnection,
|
||||||
|
project_id: &str,
|
||||||
|
post_id: &str,
|
||||||
|
) -> QueryResult<usize> {
|
||||||
|
conn.with(|c| {
|
||||||
|
diesel::delete(
|
||||||
|
dismissed_duplicate_pairs::table
|
||||||
|
.filter(dismissed_duplicate_pairs::project_id.eq(project_id))
|
||||||
|
.filter(
|
||||||
|
dismissed_duplicate_pairs::post_id_a
|
||||||
|
.eq(post_id)
|
||||||
|
.or(dismissed_duplicate_pairs::post_id_b.eq(post_id)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.execute(c)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::db::Database;
|
||||||
|
use crate::model::{Post, PostStatus, Project};
|
||||||
|
|
||||||
|
fn seeded() -> (Database, String, String) {
|
||||||
|
let db = Database::open_in_memory().unwrap();
|
||||||
|
db.migrate().unwrap();
|
||||||
|
let project_id = "embedding-project".to_string();
|
||||||
|
crate::db::queries::project::insert_project(
|
||||||
|
db.conn(),
|
||||||
|
&Project {
|
||||||
|
id: project_id.clone(),
|
||||||
|
name: "Embedding".into(),
|
||||||
|
slug: "embedding".into(),
|
||||||
|
description: None,
|
||||||
|
data_path: Some("/tmp/embedding".into()),
|
||||||
|
is_active: true,
|
||||||
|
created_at: 1,
|
||||||
|
updated_at: 1,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let post_id = "embedding-post".to_string();
|
||||||
|
crate::db::queries::post::insert_post(
|
||||||
|
db.conn(),
|
||||||
|
&Post {
|
||||||
|
id: post_id.clone(),
|
||||||
|
project_id: project_id.clone(),
|
||||||
|
title: "Post".into(),
|
||||||
|
slug: "post".into(),
|
||||||
|
excerpt: None,
|
||||||
|
content: Some("Body".into()),
|
||||||
|
status: PostStatus::Draft,
|
||||||
|
author: None,
|
||||||
|
language: Some("en".into()),
|
||||||
|
do_not_translate: false,
|
||||||
|
template_slug: None,
|
||||||
|
file_path: "posts/post.md".into(),
|
||||||
|
checksum: None,
|
||||||
|
tags: vec![],
|
||||||
|
categories: vec![],
|
||||||
|
published_title: None,
|
||||||
|
published_content: None,
|
||||||
|
published_tags: None,
|
||||||
|
published_categories: None,
|
||||||
|
published_excerpt: None,
|
||||||
|
created_at: 1,
|
||||||
|
updated_at: 1,
|
||||||
|
published_at: None,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
(db, project_id, post_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn embedding_vector_round_trips_as_blob_and_dismissals_are_canonical() {
|
||||||
|
let (db, project_id, post_id) = seeded();
|
||||||
|
let key = EmbeddingKey {
|
||||||
|
label: 1,
|
||||||
|
post_id: post_id.clone(),
|
||||||
|
project_id: project_id.clone(),
|
||||||
|
content_hash: "hash".into(),
|
||||||
|
vector: vec![0, 1, 255],
|
||||||
|
};
|
||||||
|
upsert_key(db.conn(), &key).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
get_key_for_post(db.conn(), &project_id, &post_id).unwrap(),
|
||||||
|
Some(key)
|
||||||
|
);
|
||||||
|
|
||||||
|
let replacement = EmbeddingKey {
|
||||||
|
label: 2,
|
||||||
|
post_id: post_id.clone(),
|
||||||
|
project_id: project_id.clone(),
|
||||||
|
content_hash: "new-hash".into(),
|
||||||
|
vector: vec![3, 2, 1],
|
||||||
|
};
|
||||||
|
upsert_key(db.conn(), &replacement).unwrap();
|
||||||
|
let updated = get_key_for_post(db.conn(), &project_id, &post_id)
|
||||||
|
.unwrap()
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(updated.label, 1, "a post keeps its stable HNSW label");
|
||||||
|
assert_eq!(updated.content_hash, "new-hash");
|
||||||
|
assert_eq!(updated.vector, vec![3, 2, 1]);
|
||||||
|
|
||||||
|
let pair = DismissedDuplicatePair {
|
||||||
|
id: "dismissal".into(),
|
||||||
|
project_id: project_id.clone(),
|
||||||
|
post_id_a: "a".into(),
|
||||||
|
post_id_b: "b".into(),
|
||||||
|
dismissed_at: 1,
|
||||||
|
};
|
||||||
|
insert_dismissed_pair(db.conn(), &pair).unwrap();
|
||||||
|
insert_dismissed_pair(db.conn(), &pair).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
list_dismissed_pairs(db.conn(), &project_id).unwrap(),
|
||||||
|
vec![pair]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
pub mod chat;
|
pub mod chat;
|
||||||
pub mod db_notification;
|
pub mod db_notification;
|
||||||
|
pub mod embedding;
|
||||||
pub mod generated_file_hash;
|
pub mod generated_file_hash;
|
||||||
pub mod import_definition;
|
pub mod import_definition;
|
||||||
pub mod mcp_proposal;
|
pub mod mcp_proposal;
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ diesel::table! {
|
|||||||
post_id -> Text,
|
post_id -> Text,
|
||||||
project_id -> Text,
|
project_id -> Text,
|
||||||
content_hash -> Text,
|
content_hash -> Text,
|
||||||
vector -> Text,
|
vector -> Binary,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
1382
crates/bds-core/src/engine/embedding.rs
Normal file
1382
crates/bds-core/src/engine/embedding.rs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -5,6 +5,7 @@ use std::path::Path;
|
|||||||
use crate::db::DbConnection as Connection;
|
use crate::db::DbConnection as Connection;
|
||||||
use walkdir::WalkDir;
|
use walkdir::WalkDir;
|
||||||
|
|
||||||
|
use crate::db::queries::embedding as qe;
|
||||||
use crate::db::queries::media as qm;
|
use crate::db::queries::media as qm;
|
||||||
use crate::db::queries::media_translation as qmt;
|
use crate::db::queries::media_translation as qmt;
|
||||||
use crate::db::queries::post as qp;
|
use crate::db::queries::post as qp;
|
||||||
@@ -159,6 +160,49 @@ pub fn compute_metadata_diff(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 6. Detect orphans
|
// 6. Detect orphans
|
||||||
|
if crate::engine::meta::read_project_json(data_dir)
|
||||||
|
.is_ok_and(|metadata| metadata.semantic_similarity_enabled)
|
||||||
|
{
|
||||||
|
let service = crate::engine::embedding::EmbeddingService::production(conn, data_dir);
|
||||||
|
let keys = qe::list_keys(conn, project_id)?
|
||||||
|
.into_iter()
|
||||||
|
.map(|key| (key.post_id.clone(), key))
|
||||||
|
.collect::<std::collections::HashMap<_, _>>();
|
||||||
|
for post in &posts {
|
||||||
|
let expected = service.content_hash_for_post(post)?;
|
||||||
|
let key = keys.get(&post.id);
|
||||||
|
let current_hash = key.map(|key| key.content_hash.as_str()).unwrap_or("");
|
||||||
|
let vector_status = key
|
||||||
|
.filter(|key| crate::engine::embedding::decode_vector(&key.vector).is_ok())
|
||||||
|
.map(|_| "ready")
|
||||||
|
.unwrap_or("missing");
|
||||||
|
if current_hash != expected || vector_status != "ready" {
|
||||||
|
let mut fields = Vec::new();
|
||||||
|
if current_hash != expected {
|
||||||
|
fields.push(DiffField {
|
||||||
|
field_name: "content_hash".into(),
|
||||||
|
db_value: current_hash.into(),
|
||||||
|
file_value: expected,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if vector_status != "ready" {
|
||||||
|
fields.push(DiffField {
|
||||||
|
field_name: "embedding".into(),
|
||||||
|
db_value: vector_status.into(),
|
||||||
|
file_value: "ready".into(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
report.diffs.push(EntityDiff {
|
||||||
|
entity_type: "embedding".into(),
|
||||||
|
entity_id: post.id.clone(),
|
||||||
|
file_path: format!("projects/{project_id}/embeddings.usearch"),
|
||||||
|
fields,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. Detect orphans
|
||||||
let orphans = detect_orphan_files(conn, data_dir, project_id)?;
|
let orphans = detect_orphan_files(conn, data_dir, project_id)?;
|
||||||
report.orphans = orphans;
|
report.orphans = orphans;
|
||||||
|
|
||||||
@@ -204,6 +248,11 @@ pub fn repair_metadata_diff_item(
|
|||||||
conn, data_dir, project_id, &path,
|
conn, data_dir, project_id, &path,
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
|
"embedding" => {
|
||||||
|
let post = qp::get_post_by_id(conn, &item.entity_id)?;
|
||||||
|
crate::engine::embedding::EmbeddingService::production(conn, data_dir)
|
||||||
|
.sync_post(&post)?;
|
||||||
|
}
|
||||||
other => return unsupported_repair(other),
|
other => return unsupported_repair(other),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -222,6 +271,8 @@ pub fn repair_metadata_diff_item(
|
|||||||
)?,
|
)?,
|
||||||
"script" => rewrite_script_from_database(conn, data_dir, &item.entity_id)?,
|
"script" => rewrite_script_from_database(conn, data_dir, &item.entity_id)?,
|
||||||
"template" => rewrite_template_from_database(conn, data_dir, &item.entity_id)?,
|
"template" => rewrite_template_from_database(conn, data_dir, &item.entity_id)?,
|
||||||
|
"embedding" => crate::engine::embedding::EmbeddingService::production(conn, data_dir)
|
||||||
|
.flush_project(project_id)?,
|
||||||
other => return unsupported_repair(other),
|
other => return unsupported_repair(other),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ mod chat_tools;
|
|||||||
pub mod cli_launcher;
|
pub mod cli_launcher;
|
||||||
pub mod cli_sync;
|
pub mod cli_sync;
|
||||||
pub mod domain_events;
|
pub mod domain_events;
|
||||||
|
pub mod embedding;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
pub mod gallery_import;
|
pub mod gallery_import;
|
||||||
pub mod generation;
|
pub mod generation;
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ pub struct RebuildReport {
|
|||||||
)]
|
)]
|
||||||
pub fn create_post(
|
pub fn create_post(
|
||||||
conn: &Connection,
|
conn: &Connection,
|
||||||
_data_dir: &Path,
|
data_dir: &Path,
|
||||||
project_id: &str,
|
project_id: &str,
|
||||||
title: &str,
|
title: &str,
|
||||||
content: Option<&str>,
|
content: Option<&str>,
|
||||||
@@ -91,6 +91,7 @@ pub fn create_post(
|
|||||||
fts_index_post(conn, &post)?;
|
fts_index_post(conn, &post)?;
|
||||||
|
|
||||||
emit_post(&post, NotificationAction::Created);
|
emit_post(&post, NotificationAction::Created);
|
||||||
|
crate::engine::embedding::sync_post_best_effort(conn, data_dir, &post);
|
||||||
|
|
||||||
Ok(post)
|
Ok(post)
|
||||||
}
|
}
|
||||||
@@ -102,7 +103,7 @@ pub fn create_post(
|
|||||||
)]
|
)]
|
||||||
pub fn update_post(
|
pub fn update_post(
|
||||||
conn: &Connection,
|
conn: &Connection,
|
||||||
_data_dir: &Path,
|
data_dir: &Path,
|
||||||
post_id: &str,
|
post_id: &str,
|
||||||
title: Option<&str>,
|
title: Option<&str>,
|
||||||
slug: Option<&str>,
|
slug: Option<&str>,
|
||||||
@@ -169,7 +170,7 @@ pub fn update_post(
|
|||||||
if post.status == PostStatus::Published || post.status == PostStatus::Archived {
|
if post.status == PostStatus::Published || post.status == PostStatus::Archived {
|
||||||
// Reload content from filesystem if content field is NULL (published state)
|
// Reload content from filesystem if content field is NULL (published state)
|
||||||
if post.content.is_none() && !post.file_path.is_empty() {
|
if post.content.is_none() && !post.file_path.is_empty() {
|
||||||
let abs_path = _data_dir.join(&post.file_path);
|
let abs_path = data_dir.join(&post.file_path);
|
||||||
if abs_path.exists()
|
if abs_path.exists()
|
||||||
&& let Ok(file_content) = fs::read_to_string(&abs_path)
|
&& let Ok(file_content) = fs::read_to_string(&abs_path)
|
||||||
&& let Ok((_fm, body)) = read_post_file(&file_content)
|
&& let Ok((_fm, body)) = read_post_file(&file_content)
|
||||||
@@ -187,6 +188,7 @@ pub fn update_post(
|
|||||||
fts_index_post(conn, &post)?;
|
fts_index_post(conn, &post)?;
|
||||||
|
|
||||||
emit_post(&post, NotificationAction::Updated);
|
emit_post(&post, NotificationAction::Updated);
|
||||||
|
crate::engine::embedding::sync_post_best_effort(conn, data_dir, &post);
|
||||||
|
|
||||||
Ok(post)
|
Ok(post)
|
||||||
}
|
}
|
||||||
@@ -210,6 +212,7 @@ pub fn publish_post(conn: &Connection, data_dir: &Path, post_id: &str) -> Engine
|
|||||||
Ok(post) => {
|
Ok(post) => {
|
||||||
conn.release_savepoint()?;
|
conn.release_savepoint()?;
|
||||||
emit_post(&post, NotificationAction::Updated);
|
emit_post(&post, NotificationAction::Updated);
|
||||||
|
crate::engine::embedding::sync_post_best_effort(conn, data_dir, &post);
|
||||||
Ok(post)
|
Ok(post)
|
||||||
}
|
}
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
@@ -343,7 +346,9 @@ pub fn archive_post(conn: &Connection, data_dir: &Path, post_id: &str) -> Engine
|
|||||||
}
|
}
|
||||||
let now = now_unix_ms();
|
let now = now_unix_ms();
|
||||||
qp::update_post_status(conn, post_id, &PostStatus::Archived, now)?;
|
qp::update_post_status(conn, post_id, &PostStatus::Archived, now)?;
|
||||||
|
post.status = PostStatus::Archived;
|
||||||
emit_post(&post, NotificationAction::Updated);
|
emit_post(&post, NotificationAction::Updated);
|
||||||
|
crate::engine::embedding::sync_post_best_effort(conn, data_dir, &post);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -455,6 +460,7 @@ pub fn discard_post_draft(conn: &Connection, data_dir: &Path, post_id: &str) ->
|
|||||||
Ok(post) => {
|
Ok(post) => {
|
||||||
conn.release_savepoint()?;
|
conn.release_savepoint()?;
|
||||||
emit_post(&post, NotificationAction::Updated);
|
emit_post(&post, NotificationAction::Updated);
|
||||||
|
crate::engine::embedding::sync_post_best_effort(conn, data_dir, &post);
|
||||||
Ok(post)
|
Ok(post)
|
||||||
}
|
}
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
@@ -503,6 +509,8 @@ pub fn delete_post(conn: &Connection, data_dir: &Path, post_id: &str) -> EngineR
|
|||||||
// Delete post from DB
|
// Delete post from DB
|
||||||
qp::delete_post(conn, post_id)?;
|
qp::delete_post(conn, post_id)?;
|
||||||
|
|
||||||
|
crate::engine::embedding::remove_post_best_effort(conn, data_dir, &post.project_id, post_id);
|
||||||
|
|
||||||
emit_post(&post, NotificationAction::Deleted);
|
emit_post(&post, NotificationAction::Deleted);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -238,6 +238,7 @@ pub fn delete_project(
|
|||||||
let is_custom_path = project.data_path.is_some();
|
let is_custom_path = project.data_path.is_some();
|
||||||
|
|
||||||
q::delete_project(conn, project_id)?;
|
q::delete_project(conn, project_id)?;
|
||||||
|
crate::engine::embedding::EmbeddingService::forget_project(project_id);
|
||||||
|
|
||||||
// Clean up internal filesystem only (not custom external paths per spec)
|
// Clean up internal filesystem only (not custom external paths per spec)
|
||||||
if !is_custom_path
|
if !is_custom_path
|
||||||
|
|||||||
@@ -170,15 +170,18 @@ fn rebuild_from_filesystem_inner(
|
|||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
progress(0.98, "Refreshing semantic index...");
|
||||||
|
crate::engine::embedding::EmbeddingService::production(conn, data_dir)
|
||||||
|
.index_unindexed(project_id)?;
|
||||||
|
|
||||||
progress(1.0, "Rebuild complete");
|
progress(1.0, "Rebuild complete");
|
||||||
Ok(report)
|
Ok(report)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn clear_project_rows(conn: &Connection, project_id: &str) -> EngineResult<()> {
|
fn clear_project_rows(conn: &Connection, project_id: &str) -> EngineResult<()> {
|
||||||
use crate::db::schema::{
|
use crate::db::schema::{
|
||||||
dismissed_duplicate_pairs, embedding_keys, generated_file_hashes, import_definitions,
|
generated_file_hashes, import_definitions, media, media_translations, post_links,
|
||||||
media, media_translations, post_links, post_media, post_translations, posts, scripts, tags,
|
post_media, post_translations, posts, scripts, tags, templates,
|
||||||
templates,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let post_ids = crate::db::queries::post::list_posts_by_project(conn, project_id)?
|
let post_ids = crate::db::queries::post::list_posts_by_project(conn, project_id)?
|
||||||
@@ -219,13 +222,6 @@ fn clear_project_rows(conn: &Connection, project_id: &str) -> EngineResult<()> {
|
|||||||
generated_file_hashes::table.filter(generated_file_hashes::project_id.eq(project_id)),
|
generated_file_hashes::table.filter(generated_file_hashes::project_id.eq(project_id)),
|
||||||
)
|
)
|
||||||
.execute(connection)?;
|
.execute(connection)?;
|
||||||
diesel::delete(embedding_keys::table.filter(embedding_keys::project_id.eq(project_id)))
|
|
||||||
.execute(connection)?;
|
|
||||||
diesel::delete(
|
|
||||||
dismissed_duplicate_pairs::table
|
|
||||||
.filter(dismissed_duplicate_pairs::project_id.eq(project_id)),
|
|
||||||
)
|
|
||||||
.execute(connection)?;
|
|
||||||
diesel::delete(
|
diesel::delete(
|
||||||
import_definitions::table.filter(import_definitions::project_id.eq(project_id)),
|
import_definitions::table.filter(import_definitions::project_id.eq(project_id)),
|
||||||
)
|
)
|
||||||
|
|||||||
50
crates/bds-core/src/model/embedding.rs
Normal file
50
crates/bds-core/src/model/embedding.rs
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(
|
||||||
|
Debug,
|
||||||
|
Clone,
|
||||||
|
PartialEq,
|
||||||
|
Serialize,
|
||||||
|
Deserialize,
|
||||||
|
diesel::Queryable,
|
||||||
|
diesel::Selectable,
|
||||||
|
diesel::Insertable,
|
||||||
|
diesel::AsChangeset,
|
||||||
|
)]
|
||||||
|
#[diesel(
|
||||||
|
table_name = crate::db::schema::embedding_keys,
|
||||||
|
check_for_backend(diesel::sqlite::Sqlite),
|
||||||
|
treat_none_as_default_value = false
|
||||||
|
)]
|
||||||
|
pub struct EmbeddingKey {
|
||||||
|
pub label: i64,
|
||||||
|
pub post_id: String,
|
||||||
|
pub project_id: String,
|
||||||
|
pub content_hash: String,
|
||||||
|
pub vector: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(
|
||||||
|
Debug,
|
||||||
|
Clone,
|
||||||
|
PartialEq,
|
||||||
|
Eq,
|
||||||
|
Serialize,
|
||||||
|
Deserialize,
|
||||||
|
diesel::Queryable,
|
||||||
|
diesel::Selectable,
|
||||||
|
diesel::Insertable,
|
||||||
|
diesel::AsChangeset,
|
||||||
|
)]
|
||||||
|
#[diesel(
|
||||||
|
table_name = crate::db::schema::dismissed_duplicate_pairs,
|
||||||
|
check_for_backend(diesel::sqlite::Sqlite),
|
||||||
|
treat_none_as_default_value = false
|
||||||
|
)]
|
||||||
|
pub struct DismissedDuplicatePair {
|
||||||
|
pub id: String,
|
||||||
|
pub project_id: String,
|
||||||
|
pub post_id_a: String,
|
||||||
|
pub post_id_b: String,
|
||||||
|
pub dismissed_at: i64,
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
mod chat;
|
mod chat;
|
||||||
|
mod embedding;
|
||||||
mod event;
|
mod event;
|
||||||
mod generation;
|
mod generation;
|
||||||
mod import;
|
mod import;
|
||||||
@@ -12,6 +13,7 @@ mod tag;
|
|||||||
mod template;
|
mod template;
|
||||||
|
|
||||||
pub use chat::{ChatConversation, ChatMessage, ChatRole, NewChatConversation, NewChatMessage};
|
pub use chat::{ChatConversation, ChatMessage, ChatRole, NewChatConversation, NewChatMessage};
|
||||||
|
pub use embedding::{DismissedDuplicatePair, EmbeddingKey};
|
||||||
pub use event::DomainEvent;
|
pub use event::DomainEvent;
|
||||||
pub use generation::{
|
pub use generation::{
|
||||||
DbNotification, DomainEntity, GeneratedFileHash, NotificationAction, NotificationEntity,
|
DbNotification, DomainEntity, GeneratedFileHash, NotificationAction, NotificationEntity,
|
||||||
|
|||||||
@@ -1382,6 +1382,95 @@ impl CoreHost {
|
|||||||
(_, response) => one_shot_json(response),
|
(_, response) => one_shot_json(response),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn embeddings(&self, method: &str, args: &[Value]) -> HostResult<Value> {
|
||||||
|
let db = self.database()?;
|
||||||
|
let service = engine::embedding::EmbeddingService::production(db.conn(), &self.data_dir);
|
||||||
|
match method {
|
||||||
|
"get_progress" => {
|
||||||
|
let (indexed, total) = service.indexing_progress(&self.project_id)?;
|
||||||
|
Ok(json!({"indexed": indexed, "total": total}))
|
||||||
|
}
|
||||||
|
"find_similar" => {
|
||||||
|
let post_id = string_arg(args, 0)?;
|
||||||
|
self.scoped(
|
||||||
|
|conn| crate::db::queries::post::get_post_by_id(conn, post_id),
|
||||||
|
|post| post.project_id.as_str(),
|
||||||
|
)?;
|
||||||
|
let limit = args.get(1).and_then(Value::as_u64).unwrap_or(5) as usize;
|
||||||
|
Ok(Value::Array(
|
||||||
|
service
|
||||||
|
.find_similar(post_id, limit)?
|
||||||
|
.into_iter()
|
||||||
|
.map(|post| {
|
||||||
|
json!({
|
||||||
|
"post_id": post.post_id,
|
||||||
|
"title": post.title,
|
||||||
|
"score": post.similarity,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
"compute_similarities" => {
|
||||||
|
let post_id = string_arg(args, 0)?;
|
||||||
|
self.scoped(
|
||||||
|
|conn| crate::db::queries::post::get_post_by_id(conn, post_id),
|
||||||
|
|post| post.project_id.as_str(),
|
||||||
|
)?;
|
||||||
|
let target_ids = string_array_arg(args, 1)?;
|
||||||
|
json_value(service.compute_similarities(post_id, &target_ids))
|
||||||
|
}
|
||||||
|
"suggest_tags" => {
|
||||||
|
let post_id = string_arg(args, 0)?;
|
||||||
|
self.scoped(
|
||||||
|
|conn| crate::db::queries::post::get_post_by_id(conn, post_id),
|
||||||
|
|post| post.project_id.as_str(),
|
||||||
|
)?;
|
||||||
|
json_value(service.suggest_tags(post_id))
|
||||||
|
}
|
||||||
|
"find_duplicates" => {
|
||||||
|
let mut page = 0;
|
||||||
|
let pairs = loop {
|
||||||
|
let result = service.find_duplicates(&self.project_id, page)?;
|
||||||
|
if !result.has_more {
|
||||||
|
break result.pairs;
|
||||||
|
}
|
||||||
|
page += 1;
|
||||||
|
};
|
||||||
|
Ok(Value::Array(
|
||||||
|
pairs
|
||||||
|
.into_iter()
|
||||||
|
.map(|pair| {
|
||||||
|
json!({
|
||||||
|
"post_id_a": pair.post_id_a,
|
||||||
|
"title_a": pair.title_a,
|
||||||
|
"post_id_b": pair.post_id_b,
|
||||||
|
"title_b": pair.title_b,
|
||||||
|
"score": pair.similarity,
|
||||||
|
"similarity": pair.similarity,
|
||||||
|
"exact_match": pair.exact_match,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
"dismiss_pair" => {
|
||||||
|
let post_id_a = string_arg(args, 0)?;
|
||||||
|
let post_id_b = string_arg(args, 1)?;
|
||||||
|
for post_id in [post_id_a, post_id_b] {
|
||||||
|
self.scoped(
|
||||||
|
|conn| crate::db::queries::post::get_post_by_id(conn, post_id),
|
||||||
|
|post| post.project_id.as_str(),
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
service.dismiss_duplicate_pair(post_id_a, post_id_b)?;
|
||||||
|
Ok(Value::Bool(true))
|
||||||
|
}
|
||||||
|
"index_unindexed_posts" => json_value(service.index_unindexed(&self.project_id)),
|
||||||
|
_ => Err(format!("unknown embeddings capability: {method}").into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl HostApi for CoreHost {
|
impl HostApi for CoreHost {
|
||||||
@@ -1398,6 +1487,7 @@ impl HostApi for CoreHost {
|
|||||||
"tasks" => self.tasks(method, &arguments),
|
"tasks" => self.tasks(method, &arguments),
|
||||||
"publish" => self.publish(method, &arguments),
|
"publish" => self.publish(method, &arguments),
|
||||||
"chat" => self.chat(method, &arguments),
|
"chat" => self.chat(method, &arguments),
|
||||||
|
"embeddings" => self.embeddings(method, &arguments),
|
||||||
"bds" if method == "report_progress" => self.report_progress(&arguments),
|
"bds" if method == "report_progress" => self.report_progress(&arguments),
|
||||||
_ => Err(format!("unknown host capability: {namespace}.{method}").into()),
|
_ => Err(format!("unknown host capability: {namespace}.{method}").into()),
|
||||||
};
|
};
|
||||||
@@ -1537,6 +1627,19 @@ fn string_arg(args: &[Value], index: usize) -> HostResult<&str> {
|
|||||||
.ok_or_else(|| format!("argument {} must be a string", index + 1).into())
|
.ok_or_else(|| format!("argument {} must be a string", index + 1).into())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn string_array_arg(args: &[Value], index: usize) -> HostResult<Vec<String>> {
|
||||||
|
args.get(index)
|
||||||
|
.and_then(Value::as_array)
|
||||||
|
.map(|items| {
|
||||||
|
items
|
||||||
|
.iter()
|
||||||
|
.filter_map(Value::as_str)
|
||||||
|
.map(str::to_owned)
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
|
.ok_or_else(|| format!("argument {} must be a table", index + 1).into())
|
||||||
|
}
|
||||||
|
|
||||||
fn string_field<'a>(value: &'a Map<String, Value>, field: &str) -> HostResult<&'a str> {
|
fn string_field<'a>(value: &'a Map<String, Value>, field: &str) -> HostResult<&'a str> {
|
||||||
value
|
value
|
||||||
.get(field)
|
.get(field)
|
||||||
@@ -1821,6 +1924,9 @@ mod tests {
|
|||||||
upload = upload,
|
upload = upload,
|
||||||
timestamp = post.created_at,
|
timestamp = post.created_at,
|
||||||
project_path = bds.app.get_default_project_path(),
|
project_path = bds.app.get_default_project_path(),
|
||||||
|
embedding_progress = bds.embeddings.get_progress(),
|
||||||
|
embedding_backfill = bds.embeddings.index_unindexed_posts(),
|
||||||
|
foreign_embedding = bds.embeddings.find_similar(input.foreign_post, 5),
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
"#,
|
"#,
|
||||||
@@ -1842,5 +1948,11 @@ mod tests {
|
|||||||
assert!(result.value["upload"].is_null());
|
assert!(result.value["upload"].is_null());
|
||||||
assert!(result.value["timestamp"].as_str().unwrap().contains('T'));
|
assert!(result.value["timestamp"].as_str().unwrap().contains('T'));
|
||||||
assert_eq!(manager.progress(task_id), Some(0.5));
|
assert_eq!(manager.progress(task_id), Some(0.5));
|
||||||
|
assert_eq!(
|
||||||
|
result.value["embedding_progress"],
|
||||||
|
json!({"indexed": 0, "total": 1})
|
||||||
|
);
|
||||||
|
assert_eq!(result.value["embedding_backfill"], json!([]));
|
||||||
|
assert!(result.value["foreign_embedding"].is_null());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -456,6 +456,7 @@ mod tests {
|
|||||||
[
|
[
|
||||||
"app",
|
"app",
|
||||||
"chat",
|
"chat",
|
||||||
|
"embeddings",
|
||||||
"media",
|
"media",
|
||||||
"meta",
|
"meta",
|
||||||
"posts",
|
"posts",
|
||||||
@@ -473,7 +474,7 @@ mod tests {
|
|||||||
function main()
|
function main()
|
||||||
return {
|
return {
|
||||||
sync = bds.sync,
|
sync = bds.sync,
|
||||||
embeddings = bds.embeddings,
|
embeddings = type(bds.embeddings),
|
||||||
report_progress = type(bds.report_progress),
|
report_progress = type(bds.report_progress),
|
||||||
post_search = type(bds.posts.search),
|
post_search = type(bds.posts.search),
|
||||||
app_toast = type(bds.app.toast),
|
app_toast = type(bds.app.toast),
|
||||||
@@ -492,6 +493,7 @@ mod tests {
|
|||||||
"report_progress": "function",
|
"report_progress": "function",
|
||||||
"post_search": "function",
|
"post_search": "function",
|
||||||
"app_toast": "function",
|
"app_toast": "function",
|
||||||
|
"embeddings": "table",
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -514,6 +516,7 @@ mod tests {
|
|||||||
bds.tasks.status_snapshot(),
|
bds.tasks.status_snapshot(),
|
||||||
bds.publish.upload_site({}),
|
bds.publish.upload_site({}),
|
||||||
bds.chat.detect_post_language("title", "body"),
|
bds.chat.detect_post_language("title", "body"),
|
||||||
|
bds.embeddings.get_progress(),
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
"#,
|
"#,
|
||||||
@@ -525,8 +528,8 @@ mod tests {
|
|||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert_eq!(execution.value.as_array().unwrap().len(), 11);
|
assert_eq!(execution.value.as_array().unwrap().len(), 12);
|
||||||
assert_eq!(host.0.lock().unwrap().len(), 11);
|
assert_eq!(host.0.lock().unwrap().len(), 12);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ use crate::views::{
|
|||||||
DashboardCategory, DashboardRecentPost, DashboardState, DashboardStats, DashboardTag,
|
DashboardCategory, DashboardRecentPost, DashboardState, DashboardStats, DashboardTag,
|
||||||
DashboardTimelineMonth,
|
DashboardTimelineMonth,
|
||||||
},
|
},
|
||||||
|
duplicates::DuplicatesState,
|
||||||
git::{GitDiffLoad, GitDiffState, GitNetworkCompletion, GitSnapshot, GitUiState},
|
git::{GitDiffLoad, GitDiffState, GitNetworkCompletion, GitSnapshot, GitUiState},
|
||||||
import_editor::{
|
import_editor::{
|
||||||
ImportAnalysisEvent, ImportEditorMsg, ImportEditorState, ImportExecutionEvent,
|
ImportAnalysisEvent, ImportEditorMsg, ImportEditorState, ImportExecutionEvent,
|
||||||
@@ -241,6 +242,23 @@ pub enum Message {
|
|||||||
result: Result<engine::generation::GenerationReport, String>,
|
result: Result<engine::generation::GenerationReport, String>,
|
||||||
},
|
},
|
||||||
SiteValidationLoaded(Result<engine::validate_site::SiteValidationReport, String>),
|
SiteValidationLoaded(Result<engine::validate_site::SiteValidationReport, String>),
|
||||||
|
DuplicatesRefresh,
|
||||||
|
DuplicatesLoaded(Result<engine::embedding::DuplicateSearchResult, String>),
|
||||||
|
DuplicatesToggle(String, String),
|
||||||
|
DuplicatesCheckAll,
|
||||||
|
DuplicatesUncheckAll,
|
||||||
|
DuplicatesDismiss(String, String),
|
||||||
|
DuplicatesDismissSelected,
|
||||||
|
DuplicatesDismissed(Result<(), String>),
|
||||||
|
DuplicatesShowMore,
|
||||||
|
DuplicatesOpenPost(String),
|
||||||
|
EmbeddingReindex,
|
||||||
|
EmbeddingBackfill,
|
||||||
|
LoadSemanticTagSuggestions(String),
|
||||||
|
SemanticTagSuggestionsLoaded {
|
||||||
|
post_id: String,
|
||||||
|
result: Result<Vec<String>, String>,
|
||||||
|
},
|
||||||
|
|
||||||
// Git
|
// Git
|
||||||
GitRefresh,
|
GitRefresh,
|
||||||
@@ -927,6 +945,7 @@ pub struct BdsApp {
|
|||||||
settings_state: Option<SettingsViewState>,
|
settings_state: Option<SettingsViewState>,
|
||||||
dashboard_state: Option<DashboardState>,
|
dashboard_state: Option<DashboardState>,
|
||||||
site_validation_state: SiteValidationState,
|
site_validation_state: SiteValidationState,
|
||||||
|
duplicates_state: DuplicatesState,
|
||||||
metadata_diff_state: MetadataDiffState,
|
metadata_diff_state: MetadataDiffState,
|
||||||
translation_validation_state: crate::views::translation_validation::TranslationValidationState,
|
translation_validation_state: crate::views::translation_validation::TranslationValidationState,
|
||||||
git_state: GitUiState,
|
git_state: GitUiState,
|
||||||
@@ -1091,6 +1110,7 @@ impl BdsApp {
|
|||||||
settings_state: None,
|
settings_state: None,
|
||||||
dashboard_state: None,
|
dashboard_state: None,
|
||||||
site_validation_state: SiteValidationState::default(),
|
site_validation_state: SiteValidationState::default(),
|
||||||
|
duplicates_state: DuplicatesState::default(),
|
||||||
metadata_diff_state: MetadataDiffState::default(),
|
metadata_diff_state: MetadataDiffState::default(),
|
||||||
translation_validation_state: Default::default(),
|
translation_validation_state: Default::default(),
|
||||||
git_state: GitUiState::default(),
|
git_state: GitUiState::default(),
|
||||||
@@ -1169,6 +1189,7 @@ impl BdsApp {
|
|||||||
settings_state: None,
|
settings_state: None,
|
||||||
dashboard_state: None,
|
dashboard_state: None,
|
||||||
site_validation_state: SiteValidationState::default(),
|
site_validation_state: SiteValidationState::default(),
|
||||||
|
duplicates_state: DuplicatesState::default(),
|
||||||
metadata_diff_state: MetadataDiffState::default(),
|
metadata_diff_state: MetadataDiffState::default(),
|
||||||
translation_validation_state: Default::default(),
|
translation_validation_state: Default::default(),
|
||||||
git_state: GitUiState::default(),
|
git_state: GitUiState::default(),
|
||||||
@@ -1495,14 +1516,22 @@ impl BdsApp {
|
|||||||
Message::OpenTab(tab) => {
|
Message::OpenTab(tab) => {
|
||||||
self.flush_active_post_editor();
|
self.flush_active_post_editor();
|
||||||
let idx = tabs::open_tab(&mut self.tabs, tab);
|
let idx = tabs::open_tab(&mut self.tabs, tab);
|
||||||
|
let mut semantic_post_id = None;
|
||||||
if let Some(t) = self.tabs.get(idx) {
|
if let Some(t) = self.tabs.get(idx) {
|
||||||
self.active_tab = Some(t.id.clone());
|
self.active_tab = Some(t.id.clone());
|
||||||
let tab_clone = t.clone();
|
let tab_clone = t.clone();
|
||||||
|
if tab_clone.tab_type == TabType::Post {
|
||||||
|
semantic_post_id = Some(tab_clone.id.clone());
|
||||||
|
}
|
||||||
self.load_editor_for_tab(&tab_clone);
|
self.load_editor_for_tab(&tab_clone);
|
||||||
}
|
}
|
||||||
self.enforce_panel_tab_fallback();
|
self.enforce_panel_tab_fallback();
|
||||||
self.sync_menu_state();
|
self.sync_menu_state();
|
||||||
self.sync_embedded_preview_for_active_post()
|
let mut tasks = vec![self.sync_embedded_preview_for_active_post()];
|
||||||
|
if let Some(post_id) = semantic_post_id {
|
||||||
|
tasks.push(Task::done(Message::LoadSemanticTagSuggestions(post_id)));
|
||||||
|
}
|
||||||
|
Task::batch(tasks)
|
||||||
}
|
}
|
||||||
Message::CloseTab(id) => {
|
Message::CloseTab(id) => {
|
||||||
if self.active_tab.as_deref() == Some(id.as_str()) {
|
if self.active_tab.as_deref() == Some(id.as_str()) {
|
||||||
@@ -1573,17 +1602,24 @@ impl BdsApp {
|
|||||||
}
|
}
|
||||||
let sidebar_task = self.refresh_counts();
|
let sidebar_task = self.refresh_counts();
|
||||||
self.sync_menu_state();
|
self.sync_menu_state();
|
||||||
sidebar_task
|
Task::batch([sidebar_task, Task::done(Message::EmbeddingBackfill)])
|
||||||
}
|
}
|
||||||
Message::SwitchProject(project_id) => {
|
Message::SwitchProject(project_id) => {
|
||||||
self.project_dropdown_open = false;
|
self.project_dropdown_open = false;
|
||||||
if let Some(ref db) = self.db {
|
if let Some(ref db) = self.db {
|
||||||
|
if let (Some(outgoing), Some(data_dir)) = (&self.active_project, &self.data_dir)
|
||||||
|
{
|
||||||
|
let _ =
|
||||||
|
engine::embedding::EmbeddingService::production(db.conn(), data_dir)
|
||||||
|
.flush_project(&outgoing.id);
|
||||||
|
}
|
||||||
match engine::project::set_active_project(db.conn(), &project_id) {
|
match engine::project::set_active_project(db.conn(), &project_id) {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
self.reset_git_for_project_change();
|
self.reset_git_for_project_change();
|
||||||
self.active_project =
|
self.active_project =
|
||||||
self.projects.iter().find(|p| p.id == project_id).cloned();
|
self.projects.iter().find(|p| p.id == project_id).cloned();
|
||||||
self.preview_session = None;
|
self.preview_session = None;
|
||||||
|
self.duplicates_state = DuplicatesState::default();
|
||||||
self.hide_embedded_preview();
|
self.hide_embedded_preview();
|
||||||
self.data_dir = self
|
self.data_dir = self
|
||||||
.active_project
|
.active_project
|
||||||
@@ -1626,7 +1662,11 @@ impl BdsApp {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.sync_menu_state();
|
self.sync_menu_state();
|
||||||
Task::batch([self.refresh_counts(), self.refresh_git_if_visible()])
|
Task::batch([
|
||||||
|
self.refresh_counts(),
|
||||||
|
self.refresh_git_if_visible(),
|
||||||
|
Task::done(Message::EmbeddingBackfill),
|
||||||
|
])
|
||||||
}
|
}
|
||||||
Message::ProjectSwitched(result) => {
|
Message::ProjectSwitched(result) => {
|
||||||
match result {
|
match result {
|
||||||
@@ -2119,6 +2159,118 @@ impl BdsApp {
|
|||||||
}
|
}
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
|
Message::DuplicatesRefresh => self.start_duplicate_search(0),
|
||||||
|
Message::DuplicatesShowMore => {
|
||||||
|
let next = self.duplicates_state.page.saturating_add(1);
|
||||||
|
self.start_duplicate_search(next)
|
||||||
|
}
|
||||||
|
Message::DuplicatesLoaded(result) => {
|
||||||
|
self.duplicates_state.is_loading = false;
|
||||||
|
self.duplicates_state.has_run = true;
|
||||||
|
match result {
|
||||||
|
Ok(result) => {
|
||||||
|
self.duplicates_state.result = result;
|
||||||
|
self.duplicates_state.error = None;
|
||||||
|
self.duplicates_state.selected.retain(|pair| {
|
||||||
|
self.duplicates_state.result.pairs.iter().any(|candidate| {
|
||||||
|
candidate.post_id_a == pair.0 && candidate.post_id_b == pair.1
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Err(error) => self.duplicates_state.error = Some(error),
|
||||||
|
}
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
Message::DuplicatesToggle(a, b) => {
|
||||||
|
if !self
|
||||||
|
.duplicates_state
|
||||||
|
.selected
|
||||||
|
.remove(&(a.clone(), b.clone()))
|
||||||
|
{
|
||||||
|
self.duplicates_state.selected.insert((a, b));
|
||||||
|
}
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
Message::DuplicatesCheckAll => {
|
||||||
|
self.duplicates_state.selected = self
|
||||||
|
.duplicates_state
|
||||||
|
.result
|
||||||
|
.pairs
|
||||||
|
.iter()
|
||||||
|
.map(|pair| (pair.post_id_a.clone(), pair.post_id_b.clone()))
|
||||||
|
.collect();
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
Message::DuplicatesUncheckAll => {
|
||||||
|
self.duplicates_state.selected.clear();
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
Message::DuplicatesDismiss(a, b) => self.dismiss_duplicate_pairs(vec![(a, b)]),
|
||||||
|
Message::DuplicatesDismissSelected => self
|
||||||
|
.dismiss_duplicate_pairs(self.duplicates_state.selected.iter().cloned().collect()),
|
||||||
|
Message::DuplicatesDismissed(result) => {
|
||||||
|
match result {
|
||||||
|
Ok(()) => {
|
||||||
|
self.duplicates_state.selected.clear();
|
||||||
|
self.notify(
|
||||||
|
ToastLevel::Success,
|
||||||
|
&t(self.ui_locale, "duplicates.dismissed"),
|
||||||
|
);
|
||||||
|
return self.start_duplicate_search(self.duplicates_state.page);
|
||||||
|
}
|
||||||
|
Err(error) => self.notify(ToastLevel::Error, &error),
|
||||||
|
}
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
Message::DuplicatesOpenPost(post_id) => {
|
||||||
|
let Some(db) = &self.db else {
|
||||||
|
return Task::none();
|
||||||
|
};
|
||||||
|
let Ok(post) = bds_core::db::queries::post::get_post_by_id(db.conn(), &post_id)
|
||||||
|
else {
|
||||||
|
return Task::none();
|
||||||
|
};
|
||||||
|
Task::done(Message::OpenTab(Tab {
|
||||||
|
id: post.id,
|
||||||
|
tab_type: TabType::Post,
|
||||||
|
title: post.title,
|
||||||
|
is_transient: false,
|
||||||
|
is_dirty: false,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
Message::EmbeddingReindex => self.start_embedding_reindex(),
|
||||||
|
Message::EmbeddingBackfill => self.start_embedding_backfill(),
|
||||||
|
Message::LoadSemanticTagSuggestions(post_id) => {
|
||||||
|
let Some(data_dir) = self.data_dir.clone() else {
|
||||||
|
return Task::none();
|
||||||
|
};
|
||||||
|
let db_path = self.db_path.clone();
|
||||||
|
let returned_post_id = post_id.clone();
|
||||||
|
Task::perform(
|
||||||
|
async move {
|
||||||
|
tokio::task::spawn_blocking(move || {
|
||||||
|
let db = Database::open(&db_path).map_err(|error| error.to_string())?;
|
||||||
|
engine::embedding::EmbeddingService::production(db.conn(), &data_dir)
|
||||||
|
.suggest_tags(&post_id)
|
||||||
|
.map_err(|error| error.to_string())
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap_or_else(|error| Err(format!("task panicked: {error}")))
|
||||||
|
},
|
||||||
|
move |result| Message::SemanticTagSuggestionsLoaded {
|
||||||
|
post_id: returned_post_id.clone(),
|
||||||
|
result,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Message::SemanticTagSuggestionsLoaded { post_id, result } => {
|
||||||
|
if let (Some(state), Ok(suggestions)) =
|
||||||
|
(self.post_editors.get_mut(&post_id), result)
|
||||||
|
{
|
||||||
|
state.semantic_tag_suggestions = suggestions;
|
||||||
|
}
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
message @ (Message::MainWindowLoaded(_) | Message::EmbeddedPreviewReady(_)) => {
|
message @ (Message::MainWindowLoaded(_) | Message::EmbeddedPreviewReady(_)) => {
|
||||||
self.handle_preview_message(message)
|
self.handle_preview_message(message)
|
||||||
}
|
}
|
||||||
@@ -2129,6 +2281,7 @@ impl BdsApp {
|
|||||||
self.refresh_task_snapshots();
|
self.refresh_task_snapshots();
|
||||||
self.process_chat_events();
|
self.process_chat_events();
|
||||||
self.persist_due_chat_surface_state();
|
self.persist_due_chat_surface_state();
|
||||||
|
let _ = engine::embedding::EmbeddingService::flush_due();
|
||||||
if !self.search_index_rebuild_running {
|
if !self.search_index_rebuild_running {
|
||||||
self.auto_save_due_post_editors();
|
self.auto_save_due_post_editors();
|
||||||
}
|
}
|
||||||
@@ -2646,6 +2799,151 @@ impl BdsApp {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn start_duplicate_search(&mut self, page: usize) -> Task<Message> {
|
||||||
|
let (Some(project), Some(data_dir)) = (&self.active_project, &self.data_dir) else {
|
||||||
|
return Task::none();
|
||||||
|
};
|
||||||
|
self.duplicates_state.enabled = engine::meta::read_project_json(data_dir)
|
||||||
|
.is_ok_and(|metadata| metadata.semantic_similarity_enabled);
|
||||||
|
self.duplicates_state.page = page;
|
||||||
|
self.duplicates_state.error = None;
|
||||||
|
if !self.duplicates_state.enabled {
|
||||||
|
self.duplicates_state.is_loading = false;
|
||||||
|
self.duplicates_state.has_run = false;
|
||||||
|
self.duplicates_state.result = Default::default();
|
||||||
|
return Task::none();
|
||||||
|
}
|
||||||
|
self.duplicates_state.is_loading = true;
|
||||||
|
let db_path = self.db_path.clone();
|
||||||
|
let data_dir = data_dir.clone();
|
||||||
|
let project_id = project.id.clone();
|
||||||
|
Task::perform(
|
||||||
|
async move {
|
||||||
|
tokio::task::spawn_blocking(move || {
|
||||||
|
let db = Database::open(&db_path).map_err(|error| error.to_string())?;
|
||||||
|
engine::embedding::EmbeddingService::production(db.conn(), &data_dir)
|
||||||
|
.find_duplicates(&project_id, page)
|
||||||
|
.map_err(|error| error.to_string())
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap_or_else(|error| Err(format!("task panicked: {error}")))
|
||||||
|
},
|
||||||
|
Message::DuplicatesLoaded,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dismiss_duplicate_pairs(&mut self, pairs: Vec<(String, String)>) -> Task<Message> {
|
||||||
|
if pairs.is_empty() {
|
||||||
|
return Task::none();
|
||||||
|
}
|
||||||
|
let (Some(_), Some(data_dir)) = (&self.db, &self.data_dir) else {
|
||||||
|
return Task::none();
|
||||||
|
};
|
||||||
|
let db_path = self.db_path.clone();
|
||||||
|
let data_dir = data_dir.clone();
|
||||||
|
Task::perform(
|
||||||
|
async move {
|
||||||
|
tokio::task::spawn_blocking(move || {
|
||||||
|
let db = Database::open(&db_path).map_err(|error| error.to_string())?;
|
||||||
|
engine::embedding::EmbeddingService::production(db.conn(), &data_dir)
|
||||||
|
.dismiss_duplicate_pairs(&pairs)
|
||||||
|
.map_err(|error| error.to_string())
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap_or_else(|error| Err(format!("task panicked: {error}")))
|
||||||
|
},
|
||||||
|
Message::DuplicatesDismissed,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn start_embedding_reindex(&mut self) -> Task<Message> {
|
||||||
|
let Some(data_dir) = &self.data_dir else {
|
||||||
|
return Task::none();
|
||||||
|
};
|
||||||
|
if !engine::meta::read_project_json(data_dir)
|
||||||
|
.is_ok_and(|metadata| metadata.semantic_similarity_enabled)
|
||||||
|
{
|
||||||
|
self.notify(
|
||||||
|
ToastLevel::Warning,
|
||||||
|
&t(self.ui_locale, "duplicates.disabled"),
|
||||||
|
);
|
||||||
|
return Task::none();
|
||||||
|
}
|
||||||
|
let locale = self.ui_locale;
|
||||||
|
self.spawn_engine_task(
|
||||||
|
"menu.item.rebuildEmbeddingIndex",
|
||||||
|
move |db_path, project_id, data_dir, tm, tid| {
|
||||||
|
let db = Database::open(&db_path).map_err(|error| error.to_string())?;
|
||||||
|
let service = engine::embedding::EmbeddingService::production(db.conn(), &data_dir);
|
||||||
|
let indexed = service
|
||||||
|
.reindex_all_with_progress(&project_id, |current, total| {
|
||||||
|
tm.report_progress(
|
||||||
|
tid,
|
||||||
|
Some(current as f32 / total.max(1) as f32),
|
||||||
|
Some(tw(
|
||||||
|
locale,
|
||||||
|
"embeddings.indexingProgress",
|
||||||
|
&[
|
||||||
|
("current", ¤t.to_string()),
|
||||||
|
("total", &total.to_string()),
|
||||||
|
],
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
!tm.is_cancelled(tid)
|
||||||
|
})
|
||||||
|
.map_err(|error| error.to_string())?;
|
||||||
|
service
|
||||||
|
.flush_project(&project_id)
|
||||||
|
.map_err(|error| error.to_string())?;
|
||||||
|
Ok(tw(
|
||||||
|
locale,
|
||||||
|
"embeddings.reindexed",
|
||||||
|
&[("count", &indexed.len().to_string())],
|
||||||
|
))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn start_embedding_backfill(&mut self) -> Task<Message> {
|
||||||
|
let Some(data_dir) = &self.data_dir else {
|
||||||
|
return Task::none();
|
||||||
|
};
|
||||||
|
if !engine::meta::read_project_json(data_dir)
|
||||||
|
.is_ok_and(|metadata| metadata.semantic_similarity_enabled)
|
||||||
|
{
|
||||||
|
return Task::none();
|
||||||
|
}
|
||||||
|
let locale = self.ui_locale;
|
||||||
|
self.spawn_engine_task(
|
||||||
|
"embeddings.indexing",
|
||||||
|
move |db_path, project_id, data_dir, tm, tid| {
|
||||||
|
let db = Database::open(&db_path).map_err(|error| error.to_string())?;
|
||||||
|
let indexed = engine::embedding::EmbeddingService::production(db.conn(), &data_dir)
|
||||||
|
.index_unindexed_with_progress(&project_id, |current, total| {
|
||||||
|
tm.report_progress(
|
||||||
|
tid,
|
||||||
|
Some(current as f32 / total.max(1) as f32),
|
||||||
|
Some(tw(
|
||||||
|
locale,
|
||||||
|
"embeddings.indexingProgress",
|
||||||
|
&[
|
||||||
|
("current", ¤t.to_string()),
|
||||||
|
("total", &total.to_string()),
|
||||||
|
],
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
!tm.is_cancelled(tid)
|
||||||
|
})
|
||||||
|
.map_err(|error| error.to_string())?;
|
||||||
|
Ok(tw(
|
||||||
|
locale,
|
||||||
|
"embeddings.indexed",
|
||||||
|
&[("count", &indexed.len().to_string())],
|
||||||
|
))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn view(&self) -> Element<'_, Message> {
|
pub fn view(&self) -> Element<'_, Message> {
|
||||||
let active_name = self.active_project.as_ref().map(|p| p.name.as_str());
|
let active_name = self.active_project.as_ref().map(|p| p.name.as_str());
|
||||||
let active_post_filter = match self.sidebar_view {
|
let active_post_filter = match self.sidebar_view {
|
||||||
@@ -2706,6 +3004,7 @@ impl BdsApp {
|
|||||||
self.settings_state.as_ref(),
|
self.settings_state.as_ref(),
|
||||||
self.dashboard_state.as_ref(),
|
self.dashboard_state.as_ref(),
|
||||||
&self.site_validation_state,
|
&self.site_validation_state,
|
||||||
|
&self.duplicates_state,
|
||||||
&self.metadata_diff_state,
|
&self.metadata_diff_state,
|
||||||
&self.translation_validation_state,
|
&self.translation_validation_state,
|
||||||
&self.git_state,
|
&self.git_state,
|
||||||
@@ -3397,6 +3696,11 @@ impl BdsApp {
|
|||||||
}
|
}
|
||||||
MenuAction::RebuildDatabase => Task::done(Message::RebuildDatabase),
|
MenuAction::RebuildDatabase => Task::done(Message::RebuildDatabase),
|
||||||
MenuAction::ReindexText => Task::done(Message::ReindexText),
|
MenuAction::ReindexText => Task::done(Message::ReindexText),
|
||||||
|
MenuAction::RebuildEmbeddingIndex => Task::done(Message::EmbeddingReindex),
|
||||||
|
MenuAction::FindDuplicates => {
|
||||||
|
self.open_singleton_tab(TabType::FindDuplicates, "tabBar.findDuplicates");
|
||||||
|
Task::done(Message::DuplicatesRefresh)
|
||||||
|
}
|
||||||
MenuAction::MetadataDiff => Task::done(Message::RunMetadataDiff),
|
MenuAction::MetadataDiff => Task::done(Message::RunMetadataDiff),
|
||||||
MenuAction::RegenerateCalendar => Task::done(Message::RegenerateCalendar),
|
MenuAction::RegenerateCalendar => Task::done(Message::RegenerateCalendar),
|
||||||
MenuAction::ValidateTranslations => Task::done(Message::ValidateTranslations),
|
MenuAction::ValidateTranslations => Task::done(Message::ValidateTranslations),
|
||||||
@@ -4777,7 +5081,7 @@ impl BdsApp {
|
|||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
let ids = bds_core::db::fts::search_posts_filtered(
|
let mut ids = bds_core::db::fts::search_posts_filtered(
|
||||||
db.conn(),
|
db.conn(),
|
||||||
query,
|
query,
|
||||||
&self.content_language,
|
&self.content_language,
|
||||||
@@ -4786,6 +5090,19 @@ impl BdsApp {
|
|||||||
.map(|results| results.post_ids)
|
.map(|results| results.post_ids)
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
if let Some(data_dir) = &self.data_dir
|
||||||
|
&& let Ok(scores) = engine::embedding::EmbeddingService::production(db.conn(), data_dir)
|
||||||
|
.compute_similarities(current_post_id, &ids)
|
||||||
|
{
|
||||||
|
ids.sort_by(|a, b| {
|
||||||
|
scores
|
||||||
|
.get(b)
|
||||||
|
.copied()
|
||||||
|
.unwrap_or_default()
|
||||||
|
.total_cmp(&scores.get(a).copied().unwrap_or_default())
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
ids.into_iter()
|
ids.into_iter()
|
||||||
.filter_map(|post_id| {
|
.filter_map(|post_id| {
|
||||||
bds_core::db::queries::post::get_post_by_id(db.conn(), &post_id).ok()
|
bds_core::db::queries::post::get_post_by_id(db.conn(), &post_id).ok()
|
||||||
@@ -6199,6 +6516,7 @@ impl BdsApp {
|
|||||||
blog_languages: Vec::new(),
|
blog_languages: Vec::new(),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
let semantic_was_enabled = meta.semantic_similarity_enabled;
|
||||||
meta.name = state.project_name.clone();
|
meta.name = state.project_name.clone();
|
||||||
meta.description = {
|
meta.description = {
|
||||||
let value = state.project_description.text();
|
let value = state.project_description.text();
|
||||||
@@ -6249,6 +6567,8 @@ impl BdsApp {
|
|||||||
let file_result = engine::meta::write_project_json(data_dir, &meta);
|
let file_result = engine::meta::write_project_json(data_dir, &meta);
|
||||||
match (db_result, file_result) {
|
match (db_result, file_result) {
|
||||||
(Ok(()), Ok(())) => {
|
(Ok(()), Ok(())) => {
|
||||||
|
let semantic_should_backfill =
|
||||||
|
state.semantic_similarity_enabled && !semantic_was_enabled;
|
||||||
if let Some(listing) =
|
if let Some(listing) =
|
||||||
self.projects.iter_mut().find(|p| p.id == project.id)
|
self.projects.iter_mut().find(|p| p.id == project.id)
|
||||||
{
|
{
|
||||||
@@ -6258,6 +6578,9 @@ impl BdsApp {
|
|||||||
self.blog_languages = state.blog_languages.clone();
|
self.blog_languages = state.blog_languages.clone();
|
||||||
self.dashboard_state = Some(self.hydrate_dashboard_state());
|
self.dashboard_state = Some(self.hydrate_dashboard_state());
|
||||||
self.notify(ToastLevel::Success, &t(self.ui_locale, "editor.saved"));
|
self.notify(ToastLevel::Success, &t(self.ui_locale, "editor.saved"));
|
||||||
|
if semantic_should_backfill {
|
||||||
|
return Task::done(Message::EmbeddingBackfill);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
(Err(e), _) => self.notify_operation_failed("common.save", e),
|
(Err(e), _) => self.notify_operation_failed("common.save", e),
|
||||||
(_, Err(e)) => self.notify_operation_failed("common.save", e),
|
(_, Err(e)) => self.notify_operation_failed("common.save", e),
|
||||||
@@ -7927,6 +8250,12 @@ impl BdsApp {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Drop for BdsApp {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
let _ = engine::embedding::EmbeddingService::flush_all();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn content_sample(content: &str, max_len: usize) -> String {
|
fn content_sample(content: &str, max_len: usize) -> String {
|
||||||
content.chars().take(max_len).collect()
|
content.chars().take(max_len).collect()
|
||||||
}
|
}
|
||||||
@@ -9184,7 +9513,10 @@ mod tests {
|
|||||||
let mut app = make_app(db, project, &tmp);
|
let mut app = make_app(db, project, &tmp);
|
||||||
let _ = app.refresh_counts();
|
let _ = app.refresh_counts();
|
||||||
|
|
||||||
let dash = app.dashboard_state.expect("dashboard state should be set");
|
let dash = app
|
||||||
|
.dashboard_state
|
||||||
|
.clone()
|
||||||
|
.expect("dashboard state should be set");
|
||||||
let now = chrono::Utc::now();
|
let now = chrono::Utc::now();
|
||||||
assert_eq!(dash.stats.total_posts, 2);
|
assert_eq!(dash.stats.total_posts, 2);
|
||||||
assert_eq!(dash.stats.published_count, 1);
|
assert_eq!(dash.stats.published_count, 1);
|
||||||
@@ -9552,10 +9884,10 @@ mod tests {
|
|||||||
db_path.as_path(),
|
db_path.as_path(),
|
||||||
&project.id,
|
&project.id,
|
||||||
"en",
|
"en",
|
||||||
|
Some(tmp.path()),
|
||||||
&filter,
|
&filter,
|
||||||
false,
|
false,
|
||||||
50,
|
(50, 0),
|
||||||
0,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(posts.len(), 1);
|
assert_eq!(posts.len(), 1);
|
||||||
|
|||||||
@@ -148,6 +148,19 @@ impl BdsApp {
|
|||||||
}
|
}
|
||||||
state.tags_input.clear();
|
state.tags_input.clear();
|
||||||
}
|
}
|
||||||
|
PostEditorMsg::AddSuggestedTag(tag) => {
|
||||||
|
if !state
|
||||||
|
.tags
|
||||||
|
.iter()
|
||||||
|
.any(|current| current.eq_ignore_ascii_case(&tag))
|
||||||
|
{
|
||||||
|
state.tags.push(tag.clone());
|
||||||
|
state.mark_dirty();
|
||||||
|
}
|
||||||
|
state
|
||||||
|
.semantic_tag_suggestions
|
||||||
|
.retain(|candidate| !candidate.eq_ignore_ascii_case(&tag));
|
||||||
|
}
|
||||||
PostEditorMsg::RemoveTag(tag) => {
|
PostEditorMsg::RemoveTag(tag) => {
|
||||||
state.tags.retain(|t| t != &tag);
|
state.tags.retain(|t| t != &tag);
|
||||||
state.mark_dirty();
|
state.mark_dirty();
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
use super::*;
|
use super::*;
|
||||||
|
use chrono::Datelike;
|
||||||
|
|
||||||
impl BdsApp {
|
impl BdsApp {
|
||||||
/// Number of items to load per sidebar page.
|
/// Number of items to load per sidebar page.
|
||||||
@@ -14,6 +15,7 @@ impl BdsApp {
|
|||||||
let db_path = self.db_path.clone();
|
let db_path = self.db_path.clone();
|
||||||
let project_id = project.id.clone();
|
let project_id = project.id.clone();
|
||||||
let content_language = self.content_language.clone();
|
let content_language = self.content_language.clone();
|
||||||
|
let data_dir = self.data_dir.clone();
|
||||||
let filter = match self.sidebar_view {
|
let filter = match self.sidebar_view {
|
||||||
SidebarView::Pages => self.page_filter.clone(),
|
SidebarView::Pages => self.page_filter.clone(),
|
||||||
_ => self.post_filter.clone(),
|
_ => self.post_filter.clone(),
|
||||||
@@ -27,10 +29,10 @@ impl BdsApp {
|
|||||||
&db_path,
|
&db_path,
|
||||||
&project_id,
|
&project_id,
|
||||||
&content_language,
|
&content_language,
|
||||||
|
data_dir.as_deref(),
|
||||||
&filter,
|
&filter,
|
||||||
is_pages,
|
is_pages,
|
||||||
Self::SIDEBAR_PAGE_SIZE + 1,
|
(Self::SIDEBAR_PAGE_SIZE + 1, 0),
|
||||||
0,
|
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
@@ -99,6 +101,7 @@ impl BdsApp {
|
|||||||
let db_path = self.db_path.clone();
|
let db_path = self.db_path.clone();
|
||||||
let project_id = project.id.clone();
|
let project_id = project.id.clone();
|
||||||
let content_language = self.content_language.clone();
|
let content_language = self.content_language.clone();
|
||||||
|
let data_dir = self.data_dir.clone();
|
||||||
let offset = self.sidebar_posts.len() as i64;
|
let offset = self.sidebar_posts.len() as i64;
|
||||||
let filter = match self.sidebar_view {
|
let filter = match self.sidebar_view {
|
||||||
SidebarView::Pages => self.page_filter.clone(),
|
SidebarView::Pages => self.page_filter.clone(),
|
||||||
@@ -113,10 +116,10 @@ impl BdsApp {
|
|||||||
&db_path,
|
&db_path,
|
||||||
&project_id,
|
&project_id,
|
||||||
&content_language,
|
&content_language,
|
||||||
|
data_dir.as_deref(),
|
||||||
&filter,
|
&filter,
|
||||||
is_pages,
|
is_pages,
|
||||||
Self::SIDEBAR_PAGE_SIZE + 1,
|
(Self::SIDEBAR_PAGE_SIZE + 1, offset),
|
||||||
offset,
|
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
@@ -163,15 +166,16 @@ impl BdsApp {
|
|||||||
db_path: &Path,
|
db_path: &Path,
|
||||||
project_id: &str,
|
project_id: &str,
|
||||||
content_language: &str,
|
content_language: &str,
|
||||||
|
data_dir: Option<&Path>,
|
||||||
filter: &PostFilter,
|
filter: &PostFilter,
|
||||||
is_pages: bool,
|
is_pages: bool,
|
||||||
limit: i64,
|
pagination: (i64, i64),
|
||||||
offset: i64,
|
|
||||||
) -> Vec<Post> {
|
) -> Vec<Post> {
|
||||||
let Ok(db) = Database::open(db_path) else {
|
let Ok(db) = Database::open(db_path) else {
|
||||||
return Vec::new();
|
return Vec::new();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let (limit, offset) = pagination;
|
||||||
let params = Self::build_post_filter_params(filter, is_pages);
|
let params = Self::build_post_filter_params(filter, is_pages);
|
||||||
if filter.search_query.trim().is_empty() {
|
if filter.search_query.trim().is_empty() {
|
||||||
return bds_core::db::queries::post::list_posts_filtered(
|
return bds_core::db::queries::post::list_posts_filtered(
|
||||||
@@ -184,6 +188,11 @@ impl BdsApp {
|
|||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let semantic_enabled = data_dir.is_some_and(|dir| {
|
||||||
|
engine::meta::read_project_json(dir)
|
||||||
|
.is_ok_and(|metadata| metadata.semantic_similarity_enabled)
|
||||||
|
});
|
||||||
|
let requested = limit.saturating_add(offset).max(0) as usize;
|
||||||
let fts_filters = bds_core::db::fts::PostSearchFilters {
|
let fts_filters = bds_core::db::fts::PostSearchFilters {
|
||||||
status: params.status.as_deref(),
|
status: params.status.as_deref(),
|
||||||
tags: (!params.tags.is_empty()).then_some(params.tags.as_slice()),
|
tags: (!params.tags.is_empty()).then_some(params.tags.as_slice()),
|
||||||
@@ -193,12 +202,16 @@ impl BdsApp {
|
|||||||
month: params.month,
|
month: params.month,
|
||||||
from: params.from,
|
from: params.from,
|
||||||
to: params.to,
|
to: params.to,
|
||||||
limit: Some(limit as usize),
|
limit: Some(if semantic_enabled {
|
||||||
offset: Some(offset as usize),
|
requested
|
||||||
|
} else {
|
||||||
|
limit as usize
|
||||||
|
}),
|
||||||
|
offset: Some(if semantic_enabled { 0 } else { offset as usize }),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
let ids = bds_core::db::fts::search_posts_filtered(
|
let fts_ids = bds_core::db::fts::search_posts_filtered(
|
||||||
db.conn(),
|
db.conn(),
|
||||||
¶ms.search_query,
|
¶ms.search_query,
|
||||||
content_language,
|
content_language,
|
||||||
@@ -207,11 +220,60 @@ impl BdsApp {
|
|||||||
.map(|results| results.post_ids)
|
.map(|results| results.post_ids)
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
ids.into_iter()
|
let mut ids = Vec::new();
|
||||||
|
if semantic_enabled
|
||||||
|
&& let Some(data_dir) = data_dir
|
||||||
|
&& let Ok(similar) =
|
||||||
|
engine::embedding::EmbeddingService::production(db.conn(), data_dir)
|
||||||
|
.semantic_search(project_id, ¶ms.search_query, requested)
|
||||||
|
{
|
||||||
|
ids.extend(similar.into_iter().map(|item| item.post_id));
|
||||||
|
}
|
||||||
|
ids.extend(fts_ids);
|
||||||
|
let mut seen = HashSet::new();
|
||||||
|
let posts = ids
|
||||||
|
.into_iter()
|
||||||
|
.filter(|post_id| seen.insert(post_id.clone()))
|
||||||
.filter_map(|post_id| {
|
.filter_map(|post_id| {
|
||||||
bds_core::db::queries::post::get_post_by_id(db.conn(), &post_id).ok()
|
bds_core::db::queries::post::get_post_by_id(db.conn(), &post_id).ok()
|
||||||
})
|
})
|
||||||
.filter(|post| post.project_id == project_id)
|
.filter(|post| post.project_id == project_id)
|
||||||
|
.filter(|post| {
|
||||||
|
params
|
||||||
|
.status
|
||||||
|
.as_ref()
|
||||||
|
.is_none_or(|status| post.status.as_str() == status)
|
||||||
|
})
|
||||||
|
.filter(|post| {
|
||||||
|
params
|
||||||
|
.language
|
||||||
|
.as_ref()
|
||||||
|
.is_none_or(|language| post.language.as_deref() == Some(language))
|
||||||
|
})
|
||||||
|
.filter(|post| {
|
||||||
|
params
|
||||||
|
.tags
|
||||||
|
.iter()
|
||||||
|
.all(|wanted| post.tags.iter().any(|tag| tag.eq_ignore_ascii_case(wanted)))
|
||||||
|
})
|
||||||
|
.filter(|post| {
|
||||||
|
params.categories.iter().all(|wanted| {
|
||||||
|
post.categories
|
||||||
|
.iter()
|
||||||
|
.any(|category| category.eq_ignore_ascii_case(wanted))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.filter(|post| params.from.is_none_or(|from| post.created_at >= from))
|
||||||
|
.filter(|post| params.to.is_none_or(|to| post.created_at <= to))
|
||||||
|
.filter(|post| {
|
||||||
|
if params.year.is_none() && params.month.is_none() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
chrono::DateTime::from_timestamp_millis(post.created_at).is_some_and(|date| {
|
||||||
|
params.year.is_none_or(|year| date.year() == year)
|
||||||
|
&& params.month.is_none_or(|month| date.month() == month)
|
||||||
|
})
|
||||||
|
})
|
||||||
.filter(|post| {
|
.filter(|post| {
|
||||||
let is_page_post = post
|
let is_page_post = post
|
||||||
.categories
|
.categories
|
||||||
@@ -222,8 +284,12 @@ impl BdsApp {
|
|||||||
} else {
|
} else {
|
||||||
!is_page_post
|
!is_page_post
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
.collect()
|
if semantic_enabled {
|
||||||
|
posts.skip(offset as usize).take(limit as usize).collect()
|
||||||
|
} else {
|
||||||
|
posts.collect()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn query_sidebar_media_blocking(
|
pub(super) fn query_sidebar_media_blocking(
|
||||||
|
|||||||
@@ -35,6 +35,8 @@ pub enum MenuAction {
|
|||||||
EditMenu,
|
EditMenu,
|
||||||
RebuildDatabase,
|
RebuildDatabase,
|
||||||
ReindexText,
|
ReindexText,
|
||||||
|
RebuildEmbeddingIndex,
|
||||||
|
FindDuplicates,
|
||||||
MetadataDiff,
|
MetadataDiff,
|
||||||
RegenerateCalendar,
|
RegenerateCalendar,
|
||||||
ValidateTranslations,
|
ValidateTranslations,
|
||||||
@@ -69,6 +71,8 @@ impl MenuAction {
|
|||||||
MenuAction::EditMenu,
|
MenuAction::EditMenu,
|
||||||
MenuAction::RebuildDatabase,
|
MenuAction::RebuildDatabase,
|
||||||
MenuAction::ReindexText,
|
MenuAction::ReindexText,
|
||||||
|
MenuAction::RebuildEmbeddingIndex,
|
||||||
|
MenuAction::FindDuplicates,
|
||||||
MenuAction::MetadataDiff,
|
MenuAction::MetadataDiff,
|
||||||
MenuAction::RegenerateCalendar,
|
MenuAction::RegenerateCalendar,
|
||||||
MenuAction::ValidateTranslations,
|
MenuAction::ValidateTranslations,
|
||||||
@@ -101,6 +105,8 @@ impl MenuAction {
|
|||||||
"edit_menu" => Self::EditMenu,
|
"edit_menu" => Self::EditMenu,
|
||||||
"rebuild_database" => Self::RebuildDatabase,
|
"rebuild_database" => Self::RebuildDatabase,
|
||||||
"reindex_text" => Self::ReindexText,
|
"reindex_text" => Self::ReindexText,
|
||||||
|
"rebuild_embedding_index" => Self::RebuildEmbeddingIndex,
|
||||||
|
"find_duplicates" => Self::FindDuplicates,
|
||||||
"metadata_diff" => Self::MetadataDiff,
|
"metadata_diff" => Self::MetadataDiff,
|
||||||
"regenerate_calendar" => Self::RegenerateCalendar,
|
"regenerate_calendar" => Self::RegenerateCalendar,
|
||||||
"validate_translations" => Self::ValidateTranslations,
|
"validate_translations" => Self::ValidateTranslations,
|
||||||
@@ -136,6 +142,8 @@ impl MenuAction {
|
|||||||
Self::EditMenu => "menu.item.editMenu",
|
Self::EditMenu => "menu.item.editMenu",
|
||||||
Self::RebuildDatabase => "menu.item.rebuildDatabase",
|
Self::RebuildDatabase => "menu.item.rebuildDatabase",
|
||||||
Self::ReindexText => "menu.item.reindexText",
|
Self::ReindexText => "menu.item.reindexText",
|
||||||
|
Self::RebuildEmbeddingIndex => "menu.item.rebuildEmbeddingIndex",
|
||||||
|
Self::FindDuplicates => "menu.item.findDuplicates",
|
||||||
Self::MetadataDiff => "menu.item.metadataDiff",
|
Self::MetadataDiff => "menu.item.metadataDiff",
|
||||||
Self::RegenerateCalendar => "menu.item.regenerateCalendar",
|
Self::RegenerateCalendar => "menu.item.regenerateCalendar",
|
||||||
Self::ValidateTranslations => "menu.item.validateTranslations",
|
Self::ValidateTranslations => "menu.item.validateTranslations",
|
||||||
@@ -173,6 +181,8 @@ pub(crate) fn action_enabled(
|
|||||||
| MenuAction::EditMenu
|
| MenuAction::EditMenu
|
||||||
| MenuAction::RebuildDatabase
|
| MenuAction::RebuildDatabase
|
||||||
| MenuAction::ReindexText
|
| MenuAction::ReindexText
|
||||||
|
| MenuAction::RebuildEmbeddingIndex
|
||||||
|
| MenuAction::FindDuplicates
|
||||||
| MenuAction::MetadataDiff
|
| MenuAction::MetadataDiff
|
||||||
| MenuAction::RegenerateCalendar
|
| MenuAction::RegenerateCalendar
|
||||||
| MenuAction::ValidateTranslations
|
| MenuAction::ValidateTranslations
|
||||||
@@ -205,6 +215,8 @@ pub(crate) fn action_enabled(
|
|||||||
| MenuAction::EditMenu
|
| MenuAction::EditMenu
|
||||||
| MenuAction::RebuildDatabase
|
| MenuAction::RebuildDatabase
|
||||||
| MenuAction::ReindexText
|
| MenuAction::ReindexText
|
||||||
|
| MenuAction::RebuildEmbeddingIndex
|
||||||
|
| MenuAction::FindDuplicates
|
||||||
| MenuAction::MetadataDiff
|
| MenuAction::MetadataDiff
|
||||||
| MenuAction::RegenerateCalendar
|
| MenuAction::RegenerateCalendar
|
||||||
| MenuAction::ValidateTranslations
|
| MenuAction::ValidateTranslations
|
||||||
@@ -413,6 +425,13 @@ pub fn build_menu_bar(locale: UiLocale) -> (Menu, MenuRegistry) {
|
|||||||
let _ = blog_menu.append(&PredefinedMenuItem::separator());
|
let _ = blog_menu.append(&PredefinedMenuItem::separator());
|
||||||
let _ = blog_menu.append(&item(&mut reg, MenuAction::RebuildDatabase, locale, None));
|
let _ = blog_menu.append(&item(&mut reg, MenuAction::RebuildDatabase, locale, None));
|
||||||
let _ = blog_menu.append(&item(&mut reg, MenuAction::ReindexText, locale, None));
|
let _ = blog_menu.append(&item(&mut reg, MenuAction::ReindexText, locale, None));
|
||||||
|
let _ = blog_menu.append(&item(
|
||||||
|
&mut reg,
|
||||||
|
MenuAction::RebuildEmbeddingIndex,
|
||||||
|
locale,
|
||||||
|
None,
|
||||||
|
));
|
||||||
|
let _ = blog_menu.append(&item(&mut reg, MenuAction::FindDuplicates, locale, None));
|
||||||
let _ = blog_menu.append(&item(&mut reg, MenuAction::MetadataDiff, locale, None));
|
let _ = blog_menu.append(&item(&mut reg, MenuAction::MetadataDiff, locale, None));
|
||||||
let _ = blog_menu.append(&PredefinedMenuItem::separator());
|
let _ = blog_menu.append(&PredefinedMenuItem::separator());
|
||||||
let _ = blog_menu.append(&item(
|
let _ = blog_menu.append(&item(
|
||||||
|
|||||||
182
crates/bds-ui/src/views/duplicates.rs
Normal file
182
crates/bds-ui/src/views/duplicates.rs
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
|
use bds_core::engine::embedding::DuplicateSearchResult;
|
||||||
|
use bds_core::i18n::UiLocale;
|
||||||
|
use iced::widget::text::Shaping;
|
||||||
|
use iced::widget::{Space, button, checkbox, column, container, row, scrollable, text};
|
||||||
|
use iced::{Color, Element, Length};
|
||||||
|
|
||||||
|
use crate::app::Message;
|
||||||
|
use crate::components::inputs;
|
||||||
|
use crate::i18n::{t, tw};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct DuplicatesState {
|
||||||
|
pub enabled: bool,
|
||||||
|
pub is_loading: bool,
|
||||||
|
pub has_run: bool,
|
||||||
|
pub page: usize,
|
||||||
|
pub result: DuplicateSearchResult,
|
||||||
|
pub selected: HashSet<(String, String)>,
|
||||||
|
pub error: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn view(state: &DuplicatesState, locale: UiLocale) -> Element<'_, Message> {
|
||||||
|
let refresh = if state.is_loading {
|
||||||
|
button(text(t(locale, "duplicates.searching")).size(13)).style(inputs::secondary_button)
|
||||||
|
} else {
|
||||||
|
button(text(t(locale, "common.refresh")).size(13))
|
||||||
|
.on_press(Message::DuplicatesRefresh)
|
||||||
|
.style(inputs::secondary_button)
|
||||||
|
}
|
||||||
|
.padding([6, 16]);
|
||||||
|
|
||||||
|
let dismiss_checked = if state.selected.is_empty() || state.is_loading {
|
||||||
|
button(text(t(locale, "duplicates.dismissChecked")).size(13))
|
||||||
|
.style(inputs::secondary_button)
|
||||||
|
} else {
|
||||||
|
button(
|
||||||
|
text(tw(
|
||||||
|
locale,
|
||||||
|
"duplicates.dismissCheckedCount",
|
||||||
|
&[("count", &state.selected.len().to_string())],
|
||||||
|
))
|
||||||
|
.size(13),
|
||||||
|
)
|
||||||
|
.on_press(Message::DuplicatesDismissSelected)
|
||||||
|
.style(inputs::primary_button)
|
||||||
|
}
|
||||||
|
.padding([6, 16]);
|
||||||
|
|
||||||
|
let toolbar = inputs::toolbar(
|
||||||
|
vec![
|
||||||
|
text(t(locale, "duplicates.title"))
|
||||||
|
.size(20)
|
||||||
|
.shaping(Shaping::Advanced)
|
||||||
|
.into(),
|
||||||
|
text(tw(
|
||||||
|
locale,
|
||||||
|
"duplicates.count",
|
||||||
|
&[("count", &state.result.pairs.len().to_string())],
|
||||||
|
))
|
||||||
|
.size(12)
|
||||||
|
.color(inputs::LABEL_COLOR)
|
||||||
|
.into(),
|
||||||
|
],
|
||||||
|
vec![
|
||||||
|
button(text(t(locale, "duplicates.checkAll")).size(13))
|
||||||
|
.on_press(Message::DuplicatesCheckAll)
|
||||||
|
.padding([6, 12])
|
||||||
|
.style(inputs::secondary_button)
|
||||||
|
.into(),
|
||||||
|
button(text(t(locale, "duplicates.uncheckAll")).size(13))
|
||||||
|
.on_press(Message::DuplicatesUncheckAll)
|
||||||
|
.padding([6, 12])
|
||||||
|
.style(inputs::secondary_button)
|
||||||
|
.into(),
|
||||||
|
dismiss_checked.into(),
|
||||||
|
refresh.into(),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
let body: Element<'_, Message> = if !state.enabled {
|
||||||
|
inputs::card(
|
||||||
|
text(t(locale, "duplicates.disabled"))
|
||||||
|
.size(14)
|
||||||
|
.color(inputs::LABEL_COLOR),
|
||||||
|
)
|
||||||
|
.into()
|
||||||
|
} else if state.is_loading && !state.has_run {
|
||||||
|
inputs::card(
|
||||||
|
text(t(locale, "duplicates.searching"))
|
||||||
|
.size(14)
|
||||||
|
.color(inputs::LABEL_COLOR),
|
||||||
|
)
|
||||||
|
.into()
|
||||||
|
} else if let Some(error) = &state.error {
|
||||||
|
inputs::card(
|
||||||
|
text(error.clone())
|
||||||
|
.size(14)
|
||||||
|
.color(Color::from_rgb(0.90, 0.38, 0.38)),
|
||||||
|
)
|
||||||
|
.into()
|
||||||
|
} else if state.has_run && state.result.pairs.is_empty() {
|
||||||
|
inputs::card(
|
||||||
|
text(t(locale, "duplicates.empty"))
|
||||||
|
.size(14)
|
||||||
|
.color(inputs::LABEL_COLOR),
|
||||||
|
)
|
||||||
|
.into()
|
||||||
|
} else {
|
||||||
|
let mut pairs = column!().spacing(8);
|
||||||
|
for pair in &state.result.pairs {
|
||||||
|
let key = (pair.post_id_a.clone(), pair.post_id_b.clone());
|
||||||
|
let checked = state.selected.contains(&key);
|
||||||
|
let badge = if pair.exact_match {
|
||||||
|
t(locale, "duplicates.exactMatch")
|
||||||
|
} else {
|
||||||
|
format!("{:.1}%", pair.similarity * 100.0)
|
||||||
|
};
|
||||||
|
pairs = pairs.push(inputs::card(
|
||||||
|
row![
|
||||||
|
checkbox("", checked)
|
||||||
|
.on_toggle({
|
||||||
|
let a = pair.post_id_a.clone();
|
||||||
|
let b = pair.post_id_b.clone();
|
||||||
|
move |_| Message::DuplicatesToggle(a.clone(), b.clone())
|
||||||
|
})
|
||||||
|
.size(16),
|
||||||
|
button(
|
||||||
|
text(pair.title_a.clone())
|
||||||
|
.size(13)
|
||||||
|
.shaping(Shaping::Advanced)
|
||||||
|
)
|
||||||
|
.on_press(Message::DuplicatesOpenPost(pair.post_id_a.clone()))
|
||||||
|
.padding([5, 8])
|
||||||
|
.style(inputs::disclosure_button),
|
||||||
|
text("→").size(14).color(inputs::LABEL_COLOR),
|
||||||
|
button(
|
||||||
|
text(pair.title_b.clone())
|
||||||
|
.size(13)
|
||||||
|
.shaping(Shaping::Advanced)
|
||||||
|
)
|
||||||
|
.on_press(Message::DuplicatesOpenPost(pair.post_id_b.clone()))
|
||||||
|
.padding([5, 8])
|
||||||
|
.style(inputs::disclosure_button),
|
||||||
|
Space::with_width(Length::Fill),
|
||||||
|
text(badge).size(12).color(if pair.exact_match {
|
||||||
|
Color::from_rgb(0.96, 0.68, 0.28)
|
||||||
|
} else {
|
||||||
|
Color::from_rgb(0.55, 0.76, 0.92)
|
||||||
|
}),
|
||||||
|
button(text(t(locale, "duplicates.dismiss")).size(12))
|
||||||
|
.on_press(Message::DuplicatesDismiss(
|
||||||
|
pair.post_id_a.clone(),
|
||||||
|
pair.post_id_b.clone()
|
||||||
|
))
|
||||||
|
.padding([5, 12])
|
||||||
|
.style(inputs::secondary_button),
|
||||||
|
]
|
||||||
|
.spacing(8)
|
||||||
|
.align_y(iced::Alignment::Center),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if state.result.has_more {
|
||||||
|
pairs = pairs.push(
|
||||||
|
button(text(t(locale, "duplicates.showMore")).size(13))
|
||||||
|
.on_press(Message::DuplicatesShowMore)
|
||||||
|
.padding([7, 16])
|
||||||
|
.style(inputs::secondary_button),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
scrollable(container(pairs).padding(2))
|
||||||
|
.height(Length::Fill)
|
||||||
|
.into()
|
||||||
|
};
|
||||||
|
|
||||||
|
container(column![toolbar, body].spacing(12))
|
||||||
|
.padding(16)
|
||||||
|
.width(Length::Fill)
|
||||||
|
.height(Length::Fill)
|
||||||
|
.into()
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ pub mod activity_bar;
|
|||||||
pub mod chat_surfaces;
|
pub mod chat_surfaces;
|
||||||
pub mod chat_view;
|
pub mod chat_view;
|
||||||
pub mod dashboard;
|
pub mod dashboard;
|
||||||
|
pub mod duplicates;
|
||||||
pub mod git;
|
pub mod git;
|
||||||
pub mod import_editor;
|
pub mod import_editor;
|
||||||
pub mod media_editor;
|
pub mod media_editor;
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ pub struct PostEditorState {
|
|||||||
pub quick_actions_open: bool,
|
pub quick_actions_open: bool,
|
||||||
pub tags_input: String,
|
pub tags_input: String,
|
||||||
pub categories_input: String,
|
pub categories_input: String,
|
||||||
|
pub semantic_tag_suggestions: Vec<String>,
|
||||||
pub active_language: String,
|
pub active_language: String,
|
||||||
pub canonical_language: String,
|
pub canonical_language: String,
|
||||||
pub blog_languages: Vec<String>,
|
pub blog_languages: Vec<String>,
|
||||||
@@ -123,6 +124,7 @@ impl Clone for PostEditorState {
|
|||||||
quick_actions_open: self.quick_actions_open,
|
quick_actions_open: self.quick_actions_open,
|
||||||
tags_input: self.tags_input.clone(),
|
tags_input: self.tags_input.clone(),
|
||||||
categories_input: self.categories_input.clone(),
|
categories_input: self.categories_input.clone(),
|
||||||
|
semantic_tag_suggestions: self.semantic_tag_suggestions.clone(),
|
||||||
active_language: self.active_language.clone(),
|
active_language: self.active_language.clone(),
|
||||||
canonical_language: self.canonical_language.clone(),
|
canonical_language: self.canonical_language.clone(),
|
||||||
blog_languages: self.blog_languages.clone(),
|
blog_languages: self.blog_languages.clone(),
|
||||||
@@ -188,6 +190,7 @@ impl PostEditorState {
|
|||||||
quick_actions_open: false,
|
quick_actions_open: false,
|
||||||
tags_input: String::new(),
|
tags_input: String::new(),
|
||||||
categories_input: String::new(),
|
categories_input: String::new(),
|
||||||
|
semantic_tag_suggestions: Vec::new(),
|
||||||
active_language: canonical_lang.clone(),
|
active_language: canonical_lang.clone(),
|
||||||
canonical_language: canonical_lang,
|
canonical_language: canonical_lang,
|
||||||
blog_languages: blog_languages.to_vec(),
|
blog_languages: blog_languages.to_vec(),
|
||||||
@@ -342,6 +345,7 @@ pub enum PostEditorMsg {
|
|||||||
SwitchLanguage(String),
|
SwitchLanguage(String),
|
||||||
TagsInputChanged(String),
|
TagsInputChanged(String),
|
||||||
TagsInputSubmit,
|
TagsInputSubmit,
|
||||||
|
AddSuggestedTag(String),
|
||||||
RemoveTag(String),
|
RemoveTag(String),
|
||||||
CategoriesInputChanged(String),
|
CategoriesInputChanged(String),
|
||||||
CategoriesInputSubmit,
|
CategoriesInputSubmit,
|
||||||
@@ -619,6 +623,28 @@ pub fn view<'a>(
|
|||||||
Message::PostEditor(PostEditorMsg::TagsInputSubmit),
|
Message::PostEditor(PostEditorMsg::TagsInputSubmit),
|
||||||
|tag| Message::PostEditor(PostEditorMsg::RemoveTag(tag)),
|
|tag| Message::PostEditor(PostEditorMsg::RemoveTag(tag)),
|
||||||
);
|
);
|
||||||
|
let semantic_tags: Element<'a, Message> = if state.semantic_tag_suggestions.is_empty() {
|
||||||
|
Space::new(0, 0).into()
|
||||||
|
} else {
|
||||||
|
let mut chips = row![
|
||||||
|
text(t(locale, "editor.semanticTagSuggestions"))
|
||||||
|
.size(11)
|
||||||
|
.color(inputs::LABEL_COLOR)
|
||||||
|
]
|
||||||
|
.spacing(6)
|
||||||
|
.align_y(iced::Alignment::Center);
|
||||||
|
for tag in &state.semantic_tag_suggestions {
|
||||||
|
chips = chips.push(
|
||||||
|
button(text(format!("+ {tag}")).size(11))
|
||||||
|
.on_press(Message::PostEditor(PostEditorMsg::AddSuggestedTag(
|
||||||
|
tag.clone(),
|
||||||
|
)))
|
||||||
|
.padding([4, 8])
|
||||||
|
.style(inputs::secondary_button),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
chips.into()
|
||||||
|
};
|
||||||
|
|
||||||
// Categories chip input
|
// Categories chip input
|
||||||
let categories_section = chip_input_field(
|
let categories_section = chip_input_field(
|
||||||
@@ -776,6 +802,7 @@ pub fn view<'a>(
|
|||||||
meta_row1,
|
meta_row1,
|
||||||
meta_row2,
|
meta_row2,
|
||||||
tags_section,
|
tags_section,
|
||||||
|
semantic_tags,
|
||||||
categories_section,
|
categories_section,
|
||||||
outlinks_section,
|
outlinks_section,
|
||||||
backlinks_section,
|
backlinks_section,
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ use crate::views::{
|
|||||||
activity_bar,
|
activity_bar,
|
||||||
chat_view::{self, ChatEditorState},
|
chat_view::{self, ChatEditorState},
|
||||||
dashboard::DashboardState,
|
dashboard::DashboardState,
|
||||||
|
duplicates::{self, DuplicatesState},
|
||||||
git::{self, GitDiffState, GitUiState},
|
git::{self, GitDiffState, GitUiState},
|
||||||
media_editor::{self, MediaEditorState},
|
media_editor::{self, MediaEditorState},
|
||||||
metadata_diff::{self, MetadataDiffState},
|
metadata_diff::{self, MetadataDiffState},
|
||||||
@@ -128,6 +129,7 @@ pub fn view<'a>(
|
|||||||
settings_state: Option<&'a SettingsViewState>,
|
settings_state: Option<&'a SettingsViewState>,
|
||||||
dashboard_state: Option<&'a DashboardState>,
|
dashboard_state: Option<&'a DashboardState>,
|
||||||
site_validation_state: &'a SiteValidationState,
|
site_validation_state: &'a SiteValidationState,
|
||||||
|
duplicates_state: &'a DuplicatesState,
|
||||||
metadata_diff_state: &'a MetadataDiffState,
|
metadata_diff_state: &'a MetadataDiffState,
|
||||||
translation_validation_state: &'a TranslationValidationState,
|
translation_validation_state: &'a TranslationValidationState,
|
||||||
git_state: &'a GitUiState,
|
git_state: &'a GitUiState,
|
||||||
@@ -158,6 +160,7 @@ pub fn view<'a>(
|
|||||||
settings_state,
|
settings_state,
|
||||||
dashboard_state,
|
dashboard_state,
|
||||||
site_validation_state,
|
site_validation_state,
|
||||||
|
duplicates_state,
|
||||||
metadata_diff_state,
|
metadata_diff_state,
|
||||||
translation_validation_state,
|
translation_validation_state,
|
||||||
git_diffs,
|
git_diffs,
|
||||||
@@ -400,6 +403,7 @@ fn route_content_area<'a>(
|
|||||||
settings_state: Option<&'a SettingsViewState>,
|
settings_state: Option<&'a SettingsViewState>,
|
||||||
dashboard_state: Option<&'a DashboardState>,
|
dashboard_state: Option<&'a DashboardState>,
|
||||||
site_validation_state: &'a SiteValidationState,
|
site_validation_state: &'a SiteValidationState,
|
||||||
|
duplicates_state: &'a DuplicatesState,
|
||||||
metadata_diff_state: &'a MetadataDiffState,
|
metadata_diff_state: &'a MetadataDiffState,
|
||||||
translation_validation_state: &'a TranslationValidationState,
|
translation_validation_state: &'a TranslationValidationState,
|
||||||
git_diffs: &'a HashMap<String, GitDiffState>,
|
git_diffs: &'a HashMap<String, GitDiffState>,
|
||||||
@@ -489,6 +493,7 @@ fn route_content_area<'a>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
ContentRoute::SiteValidation => site_validation::view(site_validation_state, locale),
|
ContentRoute::SiteValidation => site_validation::view(site_validation_state, locale),
|
||||||
|
ContentRoute::FindDuplicates => duplicates::view(duplicates_state, locale),
|
||||||
ContentRoute::MetadataDiff => metadata_diff::view(metadata_diff_state, locale),
|
ContentRoute::MetadataDiff => metadata_diff::view(metadata_diff_state, locale),
|
||||||
ContentRoute::TranslationValidation => {
|
ContentRoute::TranslationValidation => {
|
||||||
translation_validation::view(translation_validation_state, locale)
|
translation_validation::view(translation_validation_state, locale)
|
||||||
@@ -524,6 +529,7 @@ enum ContentRoute<'a> {
|
|||||||
Tags,
|
Tags,
|
||||||
Settings,
|
Settings,
|
||||||
SiteValidation,
|
SiteValidation,
|
||||||
|
FindDuplicates,
|
||||||
MetadataDiff,
|
MetadataDiff,
|
||||||
TranslationValidation,
|
TranslationValidation,
|
||||||
GitDiff(&'a str),
|
GitDiff(&'a str),
|
||||||
@@ -612,11 +618,11 @@ fn route_kind<'a>(
|
|||||||
TabType::SiteValidation => ContentRoute::SiteValidation,
|
TabType::SiteValidation => ContentRoute::SiteValidation,
|
||||||
TabType::MetadataDiff => ContentRoute::MetadataDiff,
|
TabType::MetadataDiff => ContentRoute::MetadataDiff,
|
||||||
TabType::GitDiff => ContentRoute::GitDiff(tab_id),
|
TabType::GitDiff => ContentRoute::GitDiff(tab_id),
|
||||||
|
TabType::FindDuplicates => ContentRoute::FindDuplicates,
|
||||||
TabType::Style
|
TabType::Style
|
||||||
| TabType::MenuEditor
|
| TabType::MenuEditor
|
||||||
| TabType::Documentation
|
| TabType::Documentation
|
||||||
| TabType::ApiDocumentation
|
| TabType::ApiDocumentation => ContentRoute::Placeholder(&tab.title),
|
||||||
| TabType::FindDuplicates => ContentRoute::Placeholder(&tab.title),
|
|
||||||
TabType::TranslationValidation => ContentRoute::TranslationValidation,
|
TabType::TranslationValidation => ContentRoute::TranslationValidation,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -678,7 +684,6 @@ mod tests {
|
|||||||
TabType::MenuEditor,
|
TabType::MenuEditor,
|
||||||
TabType::Documentation,
|
TabType::Documentation,
|
||||||
TabType::ApiDocumentation,
|
TabType::ApiDocumentation,
|
||||||
TabType::FindDuplicates,
|
|
||||||
];
|
];
|
||||||
|
|
||||||
for tab_type in unsupported {
|
for tab_type in unsupported {
|
||||||
@@ -703,6 +708,35 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn duplicate_tab_routes_to_real_review_surface() {
|
||||||
|
let tabs = vec![tab(
|
||||||
|
"find_duplicates",
|
||||||
|
TabType::FindDuplicates,
|
||||||
|
"Duplicates",
|
||||||
|
)];
|
||||||
|
let empty_posts = HashMap::new();
|
||||||
|
let empty_media = HashMap::new();
|
||||||
|
let empty_templates = HashMap::new();
|
||||||
|
let empty_scripts = HashMap::new();
|
||||||
|
let empty_imports = HashMap::new();
|
||||||
|
let site_validation = SiteValidationState::default();
|
||||||
|
let route = route_kind(
|
||||||
|
&tabs,
|
||||||
|
Some("find_duplicates"),
|
||||||
|
&empty_posts,
|
||||||
|
&empty_media,
|
||||||
|
&empty_templates,
|
||||||
|
&empty_scripts,
|
||||||
|
&empty_imports,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
&site_validation,
|
||||||
|
);
|
||||||
|
assert!(matches!(route, ContentRoute::FindDuplicates));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn git_diff_tab_routes_to_real_view() {
|
fn git_diff_tab_routes_to_real_view() {
|
||||||
let empty_posts = HashMap::new();
|
let empty_posts = HashMap::new();
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ Macro scripts expose `render(input, context)` and transform scripts expose `main
|
|||||||
- [Root helpers](#root-helpers)
|
- [Root helpers](#root-helpers)
|
||||||
- [`bds.app`](#bdsapp)
|
- [`bds.app`](#bdsapp)
|
||||||
- [`bds.chat`](#bdschat)
|
- [`bds.chat`](#bdschat)
|
||||||
|
- [`bds.embeddings`](#bdsembeddings)
|
||||||
- [`bds.media`](#bdsmedia)
|
- [`bds.media`](#bdsmedia)
|
||||||
- [`bds.meta`](#bdsmeta)
|
- [`bds.meta`](#bdsmeta)
|
||||||
- [`bds.posts`](#bdsposts)
|
- [`bds.posts`](#bdsposts)
|
||||||
@@ -795,6 +796,230 @@ local result = bds.chat.translate_media_metadata("example-id", "en")
|
|||||||
{ key = "value" }
|
{ key = "value" }
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## `bds.embeddings`
|
||||||
|
|
||||||
|
### `bds.embeddings.compute_similarities`
|
||||||
|
|
||||||
|
Compute similarity scores from one source post to target posts.
|
||||||
|
|
||||||
|
**Signature**
|
||||||
|
|
||||||
|
```text
|
||||||
|
bds.embeddings.compute_similarities(post_id: string, target_ids: table) -> table | nil
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Required | Example |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `post_id` | `string` | Yes | `"example-id"` |
|
||||||
|
| `target_ids` | `table` | Yes | `{ title = "Example" }` |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`table | nil`. `nil` means no value was available or the host operation failed.
|
||||||
|
|
||||||
|
**Example call**
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local result = bds.embeddings.compute_similarities("example-id", { title = "Example" })
|
||||||
|
```
|
||||||
|
|
||||||
|
**Example response**
|
||||||
|
|
||||||
|
```lua
|
||||||
|
{ key = "value" }
|
||||||
|
```
|
||||||
|
|
||||||
|
### `bds.embeddings.dismiss_pair`
|
||||||
|
|
||||||
|
Dismiss a duplicate candidate pair.
|
||||||
|
|
||||||
|
**Signature**
|
||||||
|
|
||||||
|
```text
|
||||||
|
bds.embeddings.dismiss_pair(post_id_a: string, post_id_b: string) -> boolean
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Required | Example |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `post_id_a` | `string` | Yes | `"example"` |
|
||||||
|
| `post_id_b` | `string` | Yes | `"example"` |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`boolean`. `false` means the operation was rejected or failed.
|
||||||
|
|
||||||
|
**Example call**
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local result = bds.embeddings.dismiss_pair("example", "example")
|
||||||
|
```
|
||||||
|
|
||||||
|
**Example response**
|
||||||
|
|
||||||
|
```lua
|
||||||
|
true
|
||||||
|
```
|
||||||
|
|
||||||
|
### `bds.embeddings.find_duplicates`
|
||||||
|
|
||||||
|
Find duplicate post candidates for the current project.
|
||||||
|
|
||||||
|
**Signature**
|
||||||
|
|
||||||
|
```text
|
||||||
|
bds.embeddings.find_duplicates() -> table | nil
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
None.
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`table | nil`. `nil` means no value was available or the host operation failed.
|
||||||
|
|
||||||
|
**Example call**
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local result = bds.embeddings.find_duplicates()
|
||||||
|
```
|
||||||
|
|
||||||
|
**Example response**
|
||||||
|
|
||||||
|
```lua
|
||||||
|
{ key = "value" }
|
||||||
|
```
|
||||||
|
|
||||||
|
### `bds.embeddings.find_similar`
|
||||||
|
|
||||||
|
Find posts similar to the given post id.
|
||||||
|
|
||||||
|
**Signature**
|
||||||
|
|
||||||
|
```text
|
||||||
|
bds.embeddings.find_similar(post_id: string, limit?: integer) -> table | nil
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Required | Example |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `post_id` | `string` | Yes | `"example-id"` |
|
||||||
|
| `limit` | `integer` | No | `nil` |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`table | nil`. `nil` means no value was available or the host operation failed.
|
||||||
|
|
||||||
|
**Example call**
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local result = bds.embeddings.find_similar("example-id", nil)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Example response**
|
||||||
|
|
||||||
|
```lua
|
||||||
|
{ key = "value" }
|
||||||
|
```
|
||||||
|
|
||||||
|
### `bds.embeddings.get_progress`
|
||||||
|
|
||||||
|
Get embedding index progress for the current project.
|
||||||
|
|
||||||
|
**Signature**
|
||||||
|
|
||||||
|
```text
|
||||||
|
bds.embeddings.get_progress() -> table | nil
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
None.
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`table | nil`. `nil` means no value was available or the host operation failed.
|
||||||
|
|
||||||
|
**Example call**
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local result = bds.embeddings.get_progress()
|
||||||
|
```
|
||||||
|
|
||||||
|
**Example response**
|
||||||
|
|
||||||
|
```lua
|
||||||
|
{ key = "value" }
|
||||||
|
```
|
||||||
|
|
||||||
|
### `bds.embeddings.index_unindexed_posts`
|
||||||
|
|
||||||
|
Index posts missing embeddings for the current project.
|
||||||
|
|
||||||
|
**Signature**
|
||||||
|
|
||||||
|
```text
|
||||||
|
bds.embeddings.index_unindexed_posts() -> table | nil
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
None.
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`table | nil`. `nil` means no value was available or the host operation failed.
|
||||||
|
|
||||||
|
**Example call**
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local result = bds.embeddings.index_unindexed_posts()
|
||||||
|
```
|
||||||
|
|
||||||
|
**Example response**
|
||||||
|
|
||||||
|
```lua
|
||||||
|
{ key = "value" }
|
||||||
|
```
|
||||||
|
|
||||||
|
### `bds.embeddings.suggest_tags`
|
||||||
|
|
||||||
|
Suggest tags for a post from semantic similarity.
|
||||||
|
|
||||||
|
**Signature**
|
||||||
|
|
||||||
|
```text
|
||||||
|
bds.embeddings.suggest_tags(post_id: string, exclude_tags?: table) -> table | nil
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Required | Example |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `post_id` | `string` | Yes | `"example-id"` |
|
||||||
|
| `exclude_tags` | `table` | No | `{ title = "Example" }` |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`table | nil`. `nil` means no value was available or the host operation failed.
|
||||||
|
|
||||||
|
**Example call**
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local result = bds.embeddings.suggest_tags("example-id", { title = "Example" })
|
||||||
|
```
|
||||||
|
|
||||||
|
**Example response**
|
||||||
|
|
||||||
|
```lua
|
||||||
|
{ key = "value" }
|
||||||
|
```
|
||||||
|
|
||||||
## `bds.media`
|
## `bds.media`
|
||||||
|
|
||||||
### `bds.media.delete_translation`
|
### `bds.media.delete_translation`
|
||||||
|
|||||||
@@ -1543,6 +1543,99 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"returns": "table | nil"
|
"returns": "table | nil"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Compute similarity scores from one source post to target posts.",
|
||||||
|
"name": "compute_similarities",
|
||||||
|
"namespace": "embeddings",
|
||||||
|
"params": [
|
||||||
|
{
|
||||||
|
"name": "post_id",
|
||||||
|
"required": true,
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "target_ids",
|
||||||
|
"required": true,
|
||||||
|
"type": "table"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"returns": "table | nil"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Dismiss a duplicate candidate pair.",
|
||||||
|
"name": "dismiss_pair",
|
||||||
|
"namespace": "embeddings",
|
||||||
|
"params": [
|
||||||
|
{
|
||||||
|
"name": "post_id_a",
|
||||||
|
"required": true,
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "post_id_b",
|
||||||
|
"required": true,
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"returns": "boolean"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Find duplicate post candidates for the current project.",
|
||||||
|
"name": "find_duplicates",
|
||||||
|
"namespace": "embeddings",
|
||||||
|
"params": [],
|
||||||
|
"returns": "table | nil"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Find posts similar to the given post id.",
|
||||||
|
"name": "find_similar",
|
||||||
|
"namespace": "embeddings",
|
||||||
|
"params": [
|
||||||
|
{
|
||||||
|
"name": "post_id",
|
||||||
|
"required": true,
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "limit",
|
||||||
|
"required": false,
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"returns": "table | nil"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Get embedding index progress for the current project.",
|
||||||
|
"name": "get_progress",
|
||||||
|
"namespace": "embeddings",
|
||||||
|
"params": [],
|
||||||
|
"returns": "table | nil"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Index posts missing embeddings for the current project.",
|
||||||
|
"name": "index_unindexed_posts",
|
||||||
|
"namespace": "embeddings",
|
||||||
|
"params": [],
|
||||||
|
"returns": "table | nil"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Suggest tags for a post from semantic similarity.",
|
||||||
|
"name": "suggest_tags",
|
||||||
|
"namespace": "embeddings",
|
||||||
|
"params": [
|
||||||
|
{
|
||||||
|
"name": "post_id",
|
||||||
|
"required": true,
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "exclude_tags",
|
||||||
|
"required": false,
|
||||||
|
"type": "table"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"returns": "table | nil"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"root_methods": [
|
"root_methods": [
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -1669,6 +1669,106 @@
|
|||||||
],
|
],
|
||||||
"returns": "table | nil"
|
"returns": "table | nil"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"description": "Compute similarity scores from one source post to target posts.",
|
||||||
|
"detail": "bds.embeddings.compute_similarities(post_id: string, target_ids: table) -> table | nil",
|
||||||
|
"insert_text": "bds.embeddings.compute_similarities(\"example-id\", { title = \"Example\" })",
|
||||||
|
"label": "bds.embeddings.compute_similarities",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "post_id",
|
||||||
|
"required": true,
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "target_ids",
|
||||||
|
"required": true,
|
||||||
|
"type": "table"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"returns": "table | nil"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Dismiss a duplicate candidate pair.",
|
||||||
|
"detail": "bds.embeddings.dismiss_pair(post_id_a: string, post_id_b: string) -> boolean",
|
||||||
|
"insert_text": "bds.embeddings.dismiss_pair(\"example\", \"example\")",
|
||||||
|
"label": "bds.embeddings.dismiss_pair",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "post_id_a",
|
||||||
|
"required": true,
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "post_id_b",
|
||||||
|
"required": true,
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"returns": "boolean"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Find duplicate post candidates for the current project.",
|
||||||
|
"detail": "bds.embeddings.find_duplicates() -> table | nil",
|
||||||
|
"insert_text": "bds.embeddings.find_duplicates()",
|
||||||
|
"label": "bds.embeddings.find_duplicates",
|
||||||
|
"parameters": [],
|
||||||
|
"returns": "table | nil"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Find posts similar to the given post id.",
|
||||||
|
"detail": "bds.embeddings.find_similar(post_id: string, limit?: integer) -> table | nil",
|
||||||
|
"insert_text": "bds.embeddings.find_similar(\"example-id\", nil)",
|
||||||
|
"label": "bds.embeddings.find_similar",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "post_id",
|
||||||
|
"required": true,
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "limit",
|
||||||
|
"required": false,
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"returns": "table | nil"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Get embedding index progress for the current project.",
|
||||||
|
"detail": "bds.embeddings.get_progress() -> table | nil",
|
||||||
|
"insert_text": "bds.embeddings.get_progress()",
|
||||||
|
"label": "bds.embeddings.get_progress",
|
||||||
|
"parameters": [],
|
||||||
|
"returns": "table | nil"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Index posts missing embeddings for the current project.",
|
||||||
|
"detail": "bds.embeddings.index_unindexed_posts() -> table | nil",
|
||||||
|
"insert_text": "bds.embeddings.index_unindexed_posts()",
|
||||||
|
"label": "bds.embeddings.index_unindexed_posts",
|
||||||
|
"parameters": [],
|
||||||
|
"returns": "table | nil"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Suggest tags for a post from semantic similarity.",
|
||||||
|
"detail": "bds.embeddings.suggest_tags(post_id: string, exclude_tags?: table) -> table | nil",
|
||||||
|
"insert_text": "bds.embeddings.suggest_tags(\"example-id\", { title = \"Example\" })",
|
||||||
|
"label": "bds.embeddings.suggest_tags",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "post_id",
|
||||||
|
"required": true,
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "exclude_tags",
|
||||||
|
"required": false,
|
||||||
|
"type": "table"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"returns": "table | nil"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"description": "Report progress for the current managed job.",
|
"description": "Report progress for the current managed job.",
|
||||||
"detail": "bds.report_progress(payload: table) -> boolean",
|
"detail": "bds.report_progress(payload: table) -> boolean",
|
||||||
|
|||||||
@@ -19,6 +19,27 @@ menu-item-publishSelected = Ausgewählte veröffentlichen
|
|||||||
menu-item-previewPost = Beitragsvorschau
|
menu-item-previewPost = Beitragsvorschau
|
||||||
menu-item-rebuildDatabase = Datenbank aus Dateien neu aufbauen
|
menu-item-rebuildDatabase = Datenbank aus Dateien neu aufbauen
|
||||||
menu-item-reindexText = Suchtext neu indizieren
|
menu-item-reindexText = Suchtext neu indizieren
|
||||||
|
menu-item-rebuildEmbeddingIndex = Embedding-Index neu erstellen
|
||||||
|
menu-item-findDuplicates = Doppelte Beiträge finden
|
||||||
|
common-refresh = Aktualisieren
|
||||||
|
editor-semanticTagSuggestions = Vorschläge aus ähnlichen Beiträgen
|
||||||
|
duplicates-title = Doppelte Beiträge finden
|
||||||
|
duplicates-searching = Duplikate werden gesucht…
|
||||||
|
duplicates-count = { $count } mögliche Paare
|
||||||
|
duplicates-disabled = Semantische Ähnlichkeit ist für dieses Projekt nicht aktiviert.
|
||||||
|
duplicates-empty = Keine möglichen doppelten Beiträge gefunden.
|
||||||
|
duplicates-checkAll = Alle auswählen
|
||||||
|
duplicates-uncheckAll = Auswahl aufheben
|
||||||
|
duplicates-dismissChecked = Ausgewählte verwerfen
|
||||||
|
duplicates-dismissCheckedCount = Ausgewählte verwerfen ({ $count })
|
||||||
|
duplicates-exactMatch = Exakte Übereinstimmung
|
||||||
|
duplicates-dismiss = Verwerfen
|
||||||
|
duplicates-dismissed = Duplikatpaar wurde verworfen.
|
||||||
|
duplicates-showMore = Mehr anzeigen
|
||||||
|
embeddings-reindexed = Embeddings für { $count } Beiträge neu erstellt.
|
||||||
|
embeddings-indexing = Semantische Inhalte werden indexiert
|
||||||
|
embeddings-indexingProgress = Beitrag { $current } von { $total } wird indexiert
|
||||||
|
embeddings-indexed = Der semantische Index ist für { $count } Beiträge bereit.
|
||||||
menu-item-metadataDiff = Metadaten-Diff-Werkzeug
|
menu-item-metadataDiff = Metadaten-Diff-Werkzeug
|
||||||
menu-item-editMenu = Blog-Menü bearbeiten
|
menu-item-editMenu = Blog-Menü bearbeiten
|
||||||
menu-item-generateSitemap = Site rendern
|
menu-item-generateSitemap = Site rendern
|
||||||
|
|||||||
@@ -19,6 +19,27 @@ menu-item-publishSelected = Publish Selected
|
|||||||
menu-item-previewPost = Preview Post
|
menu-item-previewPost = Preview Post
|
||||||
menu-item-rebuildDatabase = Rebuild Database from Files
|
menu-item-rebuildDatabase = Rebuild Database from Files
|
||||||
menu-item-reindexText = Reindex Search Text
|
menu-item-reindexText = Reindex Search Text
|
||||||
|
menu-item-rebuildEmbeddingIndex = Rebuild Embedding Index
|
||||||
|
menu-item-findDuplicates = Find Duplicate Posts
|
||||||
|
common-refresh = Refresh
|
||||||
|
editor-semanticTagSuggestions = Suggested from similar posts
|
||||||
|
duplicates-title = Find Duplicate Posts
|
||||||
|
duplicates-searching = Searching for duplicates…
|
||||||
|
duplicates-count = { $count } potential pairs
|
||||||
|
duplicates-disabled = Semantic similarity is not enabled for this project.
|
||||||
|
duplicates-empty = No potential duplicate posts found.
|
||||||
|
duplicates-checkAll = Check All
|
||||||
|
duplicates-uncheckAll = Uncheck All
|
||||||
|
duplicates-dismissChecked = Dismiss Checked
|
||||||
|
duplicates-dismissCheckedCount = Dismiss Checked ({ $count })
|
||||||
|
duplicates-exactMatch = Exact Match
|
||||||
|
duplicates-dismiss = Dismiss
|
||||||
|
duplicates-dismissed = Duplicate pair dismissal saved.
|
||||||
|
duplicates-showMore = Show More
|
||||||
|
embeddings-reindexed = Rebuilt embeddings for { $count } posts.
|
||||||
|
embeddings-indexing = Indexing semantic content
|
||||||
|
embeddings-indexingProgress = Indexing post { $current } of { $total }
|
||||||
|
embeddings-indexed = Semantic index is ready for { $count } posts.
|
||||||
menu-item-metadataDiff = Metadata Diff Tool
|
menu-item-metadataDiff = Metadata Diff Tool
|
||||||
menu-item-editMenu = Edit Blog Menu
|
menu-item-editMenu = Edit Blog Menu
|
||||||
menu-item-generateSitemap = Render Site
|
menu-item-generateSitemap = Render Site
|
||||||
|
|||||||
@@ -19,6 +19,27 @@ menu-item-publishSelected = Publicar selección
|
|||||||
menu-item-previewPost = Vista previa de entrada
|
menu-item-previewPost = Vista previa de entrada
|
||||||
menu-item-rebuildDatabase = Reconstruir base de datos desde archivos
|
menu-item-rebuildDatabase = Reconstruir base de datos desde archivos
|
||||||
menu-item-reindexText = Reindexar texto de búsqueda
|
menu-item-reindexText = Reindexar texto de búsqueda
|
||||||
|
menu-item-rebuildEmbeddingIndex = Reconstruir índice semántico
|
||||||
|
menu-item-findDuplicates = Buscar publicaciones duplicadas
|
||||||
|
common-refresh = Actualizar
|
||||||
|
editor-semanticTagSuggestions = Sugerencias de publicaciones similares
|
||||||
|
duplicates-title = Buscar publicaciones duplicadas
|
||||||
|
duplicates-searching = Buscando duplicados…
|
||||||
|
duplicates-count = { $count } pares potenciales
|
||||||
|
duplicates-disabled = La similitud semántica no está activada para este proyecto.
|
||||||
|
duplicates-empty = No se encontraron publicaciones posiblemente duplicadas.
|
||||||
|
duplicates-checkAll = Marcar todo
|
||||||
|
duplicates-uncheckAll = Desmarcar todo
|
||||||
|
duplicates-dismissChecked = Descartar marcados
|
||||||
|
duplicates-dismissCheckedCount = Descartar marcados ({ $count })
|
||||||
|
duplicates-exactMatch = Coincidencia exacta
|
||||||
|
duplicates-dismiss = Descartar
|
||||||
|
duplicates-dismissed = Se descartó el par duplicado.
|
||||||
|
duplicates-showMore = Mostrar más
|
||||||
|
embeddings-reindexed = Se reconstruyeron los embeddings de { $count } publicaciones.
|
||||||
|
embeddings-indexing = Indexando contenido semántico
|
||||||
|
embeddings-indexingProgress = Indexando publicación { $current } de { $total }
|
||||||
|
embeddings-indexed = El índice semántico está listo para { $count } publicaciones.
|
||||||
menu-item-metadataDiff = Herramienta diff de metadatos
|
menu-item-metadataDiff = Herramienta diff de metadatos
|
||||||
menu-item-editMenu = Editar menú del blog
|
menu-item-editMenu = Editar menú del blog
|
||||||
menu-item-generateSitemap = Renderizar sitio
|
menu-item-generateSitemap = Renderizar sitio
|
||||||
|
|||||||
@@ -19,6 +19,27 @@ menu-item-publishSelected = Publier la sélection
|
|||||||
menu-item-previewPost = Aperçu de l'article
|
menu-item-previewPost = Aperçu de l'article
|
||||||
menu-item-rebuildDatabase = Reconstruire la base de données
|
menu-item-rebuildDatabase = Reconstruire la base de données
|
||||||
menu-item-reindexText = Réindexer le texte de recherche
|
menu-item-reindexText = Réindexer le texte de recherche
|
||||||
|
menu-item-rebuildEmbeddingIndex = Reconstruire l’index sémantique
|
||||||
|
menu-item-findDuplicates = Rechercher les articles en double
|
||||||
|
common-refresh = Actualiser
|
||||||
|
editor-semanticTagSuggestions = Suggestions d’articles similaires
|
||||||
|
duplicates-title = Rechercher les articles en double
|
||||||
|
duplicates-searching = Recherche des doublons…
|
||||||
|
duplicates-count = { $count } paires potentielles
|
||||||
|
duplicates-disabled = La similarité sémantique n’est pas activée pour ce projet.
|
||||||
|
duplicates-empty = Aucun doublon potentiel trouvé.
|
||||||
|
duplicates-checkAll = Tout cocher
|
||||||
|
duplicates-uncheckAll = Tout décocher
|
||||||
|
duplicates-dismissChecked = Ignorer la sélection
|
||||||
|
duplicates-dismissCheckedCount = Ignorer la sélection ({ $count })
|
||||||
|
duplicates-exactMatch = Correspondance exacte
|
||||||
|
duplicates-dismiss = Ignorer
|
||||||
|
duplicates-dismissed = La paire de doublons a été ignorée.
|
||||||
|
duplicates-showMore = Afficher plus
|
||||||
|
embeddings-reindexed = Embeddings reconstruits pour { $count } articles.
|
||||||
|
embeddings-indexing = Indexation du contenu sémantique
|
||||||
|
embeddings-indexingProgress = Indexation de l’article { $current } sur { $total }
|
||||||
|
embeddings-indexed = L’index sémantique est prêt pour { $count } articles.
|
||||||
menu-item-metadataDiff = Outil de diff des métadonnées
|
menu-item-metadataDiff = Outil de diff des métadonnées
|
||||||
menu-item-editMenu = Modifier le menu du blog
|
menu-item-editMenu = Modifier le menu du blog
|
||||||
menu-item-generateSitemap = Rendre le site
|
menu-item-generateSitemap = Rendre le site
|
||||||
|
|||||||
@@ -19,6 +19,27 @@ menu-item-publishSelected = Pubblica selezionato
|
|||||||
menu-item-previewPost = Anteprima post
|
menu-item-previewPost = Anteprima post
|
||||||
menu-item-rebuildDatabase = Ricostruisci database dai file
|
menu-item-rebuildDatabase = Ricostruisci database dai file
|
||||||
menu-item-reindexText = Reindicizza testo di ricerca
|
menu-item-reindexText = Reindicizza testo di ricerca
|
||||||
|
menu-item-rebuildEmbeddingIndex = Ricostruisci indice semantico
|
||||||
|
menu-item-findDuplicates = Trova post duplicati
|
||||||
|
common-refresh = Aggiorna
|
||||||
|
editor-semanticTagSuggestions = Suggerimenti da post simili
|
||||||
|
duplicates-title = Trova post duplicati
|
||||||
|
duplicates-searching = Ricerca duplicati…
|
||||||
|
duplicates-count = { $count } coppie potenziali
|
||||||
|
duplicates-disabled = La somiglianza semantica non è abilitata per questo progetto.
|
||||||
|
duplicates-empty = Nessun possibile post duplicato trovato.
|
||||||
|
duplicates-checkAll = Seleziona tutto
|
||||||
|
duplicates-uncheckAll = Deseleziona tutto
|
||||||
|
duplicates-dismissChecked = Ignora selezionati
|
||||||
|
duplicates-dismissCheckedCount = Ignora selezionati ({ $count })
|
||||||
|
duplicates-exactMatch = Corrispondenza esatta
|
||||||
|
duplicates-dismiss = Ignora
|
||||||
|
duplicates-dismissed = Coppia duplicata ignorata.
|
||||||
|
duplicates-showMore = Mostra altro
|
||||||
|
embeddings-reindexed = Embedding ricostruiti per { $count } post.
|
||||||
|
embeddings-indexing = Indicizzazione dei contenuti semantici
|
||||||
|
embeddings-indexingProgress = Indicizzazione del post { $current } di { $total }
|
||||||
|
embeddings-indexed = L’indice semantico è pronto per { $count } post.
|
||||||
menu-item-metadataDiff = Strumento diff metadati
|
menu-item-metadataDiff = Strumento diff metadati
|
||||||
menu-item-editMenu = Modifica menu blog
|
menu-item-editMenu = Modifica menu blog
|
||||||
menu-item-generateSitemap = Renderizza sito
|
menu-item-generateSitemap = Renderizza sito
|
||||||
|
|||||||
@@ -142,7 +142,9 @@ surface ScriptRuntimeSurface {
|
|||||||
-- the core one-shot AI bridge; conversational chat remains extension.
|
-- the core one-shot AI bridge; conversational chat remains extension.
|
||||||
-- bds.report_progress(payload) reports managed-job progress. The Rust
|
-- bds.report_progress(payload) reports managed-job progress. The Rust
|
||||||
-- host may retain bds.app.progress and bds.app.toast as additive APIs.
|
-- host may retain bds.app.progress and bds.app.toast as additive APIs.
|
||||||
-- bds.sync and bds.embeddings belong to the extension contract.
|
-- bds.embeddings: compute_similarities, dismiss_pair, find_duplicates,
|
||||||
|
-- find_similar, get_progress, index_unindexed_posts, suggest_tags.
|
||||||
|
-- bds.sync remains a future extension contract.
|
||||||
|
|
||||||
@guarantee HostApiFailureValues
|
@guarantee HostApiFailureValues
|
||||||
-- Host failures do not expose implementation exceptions across the Lua
|
-- Host failures do not expose implementation exceptions across the Lua
|
||||||
|
|||||||
Reference in New Issue
Block a user