Mayo commited on
Commit ·
fe48401
1
Parent(s): f8d3d3d
refactor: OpenAPI codegen with utoipa and Orval
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- Cargo.lock +41 -57
- Cargo.toml +2 -1
- bun.lock +255 -12
- koharu-core/Cargo.toml +1 -1
- koharu-core/src/effect.rs +2 -3
- koharu-core/src/font.rs +4 -7
- koharu-core/src/lib.rs +4 -7
- koharu-core/src/protocol.rs +65 -91
- koharu-rpc/Cargo.toml +7 -2
- koharu-rpc/src/api.rs +722 -235
- koharu-rpc/src/bin/openapi.rs +16 -0
- koharu-rpc/src/lib.rs +1 -1
- koharu-rpc/src/server.rs +3 -3
- koharu-rpc/src/{events.rs → tracker.rs} +38 -171
- ui/app/(app)/about/page.tsx +6 -4
- ui/app/(app)/settings/page.tsx +9 -311
- ui/app/bootstrap/page.tsx +32 -37
- ui/app/providers.tsx +44 -279
- ui/app/splashscreen/page.tsx +31 -54
- ui/components/ActivityBubble.tsx +78 -72
- ui/components/AppErrorBoundary.tsx +3 -6
- ui/components/MenuBar.tsx +71 -27
- ui/components/Navigator.tsx +75 -108
- ui/components/canvas/CanvasToolbar.tsx +139 -131
- ui/components/canvas/Workspace.tsx +8 -0
- ui/components/canvas/canvasViewport.ts +13 -17
- ui/components/panels/LayersPanel.tsx +2 -2
- ui/components/panels/RenderControlsPanel.tsx +4 -6
- ui/components/panels/TextBlocksPanel.tsx +34 -5
- ui/hooks/useBlockContextMenu.ts +2 -2
- ui/hooks/useBlockDrafting.ts +3 -2
- ui/hooks/useBrushLayerDisplay.ts +2 -2
- ui/hooks/useCanvasDrawing.ts +285 -0
- ui/hooks/useCanvasZoom.ts +7 -4
- ui/hooks/useMaskDrawing.ts +85 -309
- ui/hooks/useRenderBrushDrawing.ts +33 -251
- ui/hooks/useRpcConnection.ts +0 -12
- ui/hooks/useTextBlocks.ts +129 -11
- ui/lib/api.ts +0 -822
- ui/lib/api/documents/documents.ts +406 -0
- ui/lib/api/downloads/downloads.ts +123 -0
- ui/lib/api/exports/exports.ts +217 -0
- ui/lib/api/fetch.ts +14 -0
- ui/lib/api/jobs/jobs.ts +348 -0
- ui/lib/api/llm/llm.ts +423 -0
- ui/lib/api/processing/processing.ts +347 -0
- ui/lib/api/providers/providers.ts +194 -0
- ui/lib/api/regions/regions.ts +224 -0
- ui/lib/api/schemas/apiError.ts +11 -0
- ui/lib/api/schemas/apiKeyResponse.ts +10 -0
Cargo.lock
CHANGED
|
@@ -216,28 +216,6 @@ dependencies = [
|
|
| 216 |
"stable_deref_trait",
|
| 217 |
]
|
| 218 |
|
| 219 |
-
[[package]]
|
| 220 |
-
name = "async-stream"
|
| 221 |
-
version = "0.3.6"
|
| 222 |
-
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 223 |
-
checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476"
|
| 224 |
-
dependencies = [
|
| 225 |
-
"async-stream-impl",
|
| 226 |
-
"futures-core",
|
| 227 |
-
"pin-project-lite",
|
| 228 |
-
]
|
| 229 |
-
|
| 230 |
-
[[package]]
|
| 231 |
-
name = "async-stream-impl"
|
| 232 |
-
version = "0.3.6"
|
| 233 |
-
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 234 |
-
checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d"
|
| 235 |
-
dependencies = [
|
| 236 |
-
"proc-macro2",
|
| 237 |
-
"quote",
|
| 238 |
-
"syn 2.0.117",
|
| 239 |
-
]
|
| 240 |
-
|
| 241 |
[[package]]
|
| 242 |
name = "async-trait"
|
| 243 |
version = "0.1.89"
|
|
@@ -3996,7 +3974,7 @@ dependencies = [
|
|
| 3996 |
"serde_json",
|
| 3997 |
"strum",
|
| 3998 |
"tokio",
|
| 3999 |
-
"
|
| 4000 |
"uuid",
|
| 4001 |
]
|
| 4002 |
|
|
@@ -4114,10 +4092,8 @@ name = "koharu-rpc"
|
|
| 4114 |
version = "0.41.4"
|
| 4115 |
dependencies = [
|
| 4116 |
"anyhow",
|
| 4117 |
-
"async-stream",
|
| 4118 |
"axum",
|
| 4119 |
"base64 0.22.1",
|
| 4120 |
-
"futures",
|
| 4121 |
"image",
|
| 4122 |
"imageproc",
|
| 4123 |
"koharu-app",
|
|
@@ -4128,10 +4104,13 @@ dependencies = [
|
|
| 4128 |
"rmp-serde",
|
| 4129 |
"rmpv",
|
| 4130 |
"serde",
|
|
|
|
| 4131 |
"serde_json",
|
| 4132 |
"tokio",
|
| 4133 |
"tower-http",
|
| 4134 |
"tracing",
|
|
|
|
|
|
|
| 4135 |
]
|
| 4136 |
|
| 4137 |
[[package]]
|
|
@@ -7828,15 +7807,6 @@ dependencies = [
|
|
| 7828 |
"utf-8",
|
| 7829 |
]
|
| 7830 |
|
| 7831 |
-
[[package]]
|
| 7832 |
-
name = "termcolor"
|
| 7833 |
-
version = "1.4.1"
|
| 7834 |
-
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 7835 |
-
checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755"
|
| 7836 |
-
dependencies = [
|
| 7837 |
-
"winapi-util",
|
| 7838 |
-
]
|
| 7839 |
-
|
| 7840 |
[[package]]
|
| 7841 |
name = "thiserror"
|
| 7842 |
version = "1.0.69"
|
|
@@ -8376,29 +8346,6 @@ version = "0.2.5"
|
|
| 8376 |
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 8377 |
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
|
| 8378 |
|
| 8379 |
-
[[package]]
|
| 8380 |
-
name = "ts-rs"
|
| 8381 |
-
version = "12.0.1"
|
| 8382 |
-
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 8383 |
-
checksum = "756050066659291d47a554a9f558125db17428b073c5ffce1daf5dcb0f7231d8"
|
| 8384 |
-
dependencies = [
|
| 8385 |
-
"serde_json",
|
| 8386 |
-
"thiserror 2.0.18",
|
| 8387 |
-
"ts-rs-macros",
|
| 8388 |
-
]
|
| 8389 |
-
|
| 8390 |
-
[[package]]
|
| 8391 |
-
name = "ts-rs-macros"
|
| 8392 |
-
version = "12.0.1"
|
| 8393 |
-
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 8394 |
-
checksum = "38d90eea51bc7988ef9e674bf80a85ba6804739e535e9cab48e4bb34a8b652aa"
|
| 8395 |
-
dependencies = [
|
| 8396 |
-
"proc-macro2",
|
| 8397 |
-
"quote",
|
| 8398 |
-
"syn 2.0.117",
|
| 8399 |
-
"termcolor",
|
| 8400 |
-
]
|
| 8401 |
-
|
| 8402 |
[[package]]
|
| 8403 |
name = "ttf-parser"
|
| 8404 |
version = "0.21.1"
|
|
@@ -8678,6 +8625,43 @@ version = "0.2.2"
|
|
| 8678 |
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 8679 |
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
| 8680 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8681 |
[[package]]
|
| 8682 |
name = "uuid"
|
| 8683 |
version = "1.23.0"
|
|
|
|
| 216 |
"stable_deref_trait",
|
| 217 |
]
|
| 218 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 219 |
[[package]]
|
| 220 |
name = "async-trait"
|
| 221 |
version = "0.1.89"
|
|
|
|
| 3974 |
"serde_json",
|
| 3975 |
"strum",
|
| 3976 |
"tokio",
|
| 3977 |
+
"utoipa",
|
| 3978 |
"uuid",
|
| 3979 |
]
|
| 3980 |
|
|
|
|
| 4092 |
version = "0.41.4"
|
| 4093 |
dependencies = [
|
| 4094 |
"anyhow",
|
|
|
|
| 4095 |
"axum",
|
| 4096 |
"base64 0.22.1",
|
|
|
|
| 4097 |
"image",
|
| 4098 |
"imageproc",
|
| 4099 |
"koharu-app",
|
|
|
|
| 4104 |
"rmp-serde",
|
| 4105 |
"rmpv",
|
| 4106 |
"serde",
|
| 4107 |
+
"serde_bytes",
|
| 4108 |
"serde_json",
|
| 4109 |
"tokio",
|
| 4110 |
"tower-http",
|
| 4111 |
"tracing",
|
| 4112 |
+
"utoipa",
|
| 4113 |
+
"utoipa-axum",
|
| 4114 |
]
|
| 4115 |
|
| 4116 |
[[package]]
|
|
|
|
| 7807 |
"utf-8",
|
| 7808 |
]
|
| 7809 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7810 |
[[package]]
|
| 7811 |
name = "thiserror"
|
| 7812 |
version = "1.0.69"
|
|
|
|
| 8346 |
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 8347 |
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
|
| 8348 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8349 |
[[package]]
|
| 8350 |
name = "ttf-parser"
|
| 8351 |
version = "0.21.1"
|
|
|
|
| 8625 |
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 8626 |
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
| 8627 |
|
| 8628 |
+
[[package]]
|
| 8629 |
+
name = "utoipa"
|
| 8630 |
+
version = "5.4.0"
|
| 8631 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 8632 |
+
checksum = "2fcc29c80c21c31608227e0912b2d7fddba57ad76b606890627ba8ee7964e993"
|
| 8633 |
+
dependencies = [
|
| 8634 |
+
"indexmap 2.13.0",
|
| 8635 |
+
"serde",
|
| 8636 |
+
"serde_json",
|
| 8637 |
+
"utoipa-gen",
|
| 8638 |
+
]
|
| 8639 |
+
|
| 8640 |
+
[[package]]
|
| 8641 |
+
name = "utoipa-axum"
|
| 8642 |
+
version = "0.2.0"
|
| 8643 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 8644 |
+
checksum = "7c25bae5bccc842449ec0c5ddc5cbb6a3a1eaeac4503895dc105a1138f8234a0"
|
| 8645 |
+
dependencies = [
|
| 8646 |
+
"axum",
|
| 8647 |
+
"paste",
|
| 8648 |
+
"tower-layer",
|
| 8649 |
+
"tower-service",
|
| 8650 |
+
"utoipa",
|
| 8651 |
+
]
|
| 8652 |
+
|
| 8653 |
+
[[package]]
|
| 8654 |
+
name = "utoipa-gen"
|
| 8655 |
+
version = "5.4.0"
|
| 8656 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 8657 |
+
checksum = "6d79d08d92ab8af4c5e8a6da20c47ae3f61a0f1dabc1997cdf2d082b757ca08b"
|
| 8658 |
+
dependencies = [
|
| 8659 |
+
"proc-macro2",
|
| 8660 |
+
"quote",
|
| 8661 |
+
"regex",
|
| 8662 |
+
"syn 2.0.117",
|
| 8663 |
+
]
|
| 8664 |
+
|
| 8665 |
[[package]]
|
| 8666 |
name = "uuid"
|
| 8667 |
version = "1.23.0"
|
Cargo.toml
CHANGED
|
@@ -126,7 +126,8 @@ rmcp = { version = "0.16", features = [
|
|
| 126 |
"transport-streamable-http-server",
|
| 127 |
] }
|
| 128 |
schemars = "1.2"
|
| 129 |
-
|
|
|
|
| 130 |
uuid = { version = "1.18", features = ["v4", "serde"] }
|
| 131 |
base64 = "0.22"
|
| 132 |
tower-http = { version = "0.6", features = ["cors"] }
|
|
|
|
| 126 |
"transport-streamable-http-server",
|
| 127 |
] }
|
| 128 |
schemars = "1.2"
|
| 129 |
+
utoipa = { version = "5", features = ["axum_extras"] }
|
| 130 |
+
utoipa-axum = "0.2"
|
| 131 |
uuid = { version = "1.18", features = ["v4", "serde"] }
|
| 132 |
base64 = "0.22"
|
| 133 |
tower-http = { version = "0.6", features = ["cors"] }
|
bun.lock
CHANGED
|
@@ -29,23 +29,22 @@
|
|
| 29 |
"@radix-ui/react-switch": "^1.2.6",
|
| 30 |
"@radix-ui/react-tabs": "^1.1.13",
|
| 31 |
"@radix-ui/react-tooltip": "^1.2.8",
|
| 32 |
-
"@tanstack/query-sync-storage-persister": "^5.96.1",
|
| 33 |
"@tanstack/react-query": "^5.96.1",
|
| 34 |
-
"@tanstack/react-query-persist-client": "^5.96.1",
|
| 35 |
"@tanstack/react-virtual": "^3.13.23",
|
| 36 |
"@tauri-apps/api": "^2.10.1",
|
| 37 |
"@use-gesture/react": "^10.3.1",
|
|
|
|
| 38 |
"browser-fs-access": "^0.38.0",
|
| 39 |
"class-variance-authority": "^0.7.1",
|
| 40 |
"clsx": "^2.1.1",
|
| 41 |
"i18next": "^25.10.10",
|
| 42 |
"i18next-browser-languagedetector": "^8.2.1",
|
| 43 |
"i18next-localstorage-backend": "^4.3.1",
|
|
|
|
| 44 |
"lucide-react": "^0.577.0",
|
| 45 |
"motion": "^12.38.0",
|
| 46 |
"next": "^16.2.2",
|
| 47 |
"next-themes": "^0.4.6",
|
| 48 |
-
"p-queue": "^9.1.1",
|
| 49 |
"react": "^19.2.4",
|
| 50 |
"react-colorful": "^5.6.1",
|
| 51 |
"react-dom": "^19.2.4",
|
|
@@ -57,6 +56,7 @@
|
|
| 57 |
"react-rnd": "^10.5.3",
|
| 58 |
"remark-gfm": "^4.0.1",
|
| 59 |
"tailwind-merge": "^3.5.0",
|
|
|
|
| 60 |
"zod": "^4.3.6",
|
| 61 |
"zustand": "^5.0.12",
|
| 62 |
},
|
|
@@ -66,6 +66,7 @@
|
|
| 66 |
"@types/react": "^19.2.14",
|
| 67 |
"@types/react-dom": "^19.2.3",
|
| 68 |
"babel-plugin-react-compiler": "^1.0.0",
|
|
|
|
| 69 |
"tailwindcss": "^4.2.2",
|
| 70 |
"tw-animate-css": "^1.4.0",
|
| 71 |
"typescript": "^6.0.2",
|
|
@@ -83,8 +84,62 @@
|
|
| 83 |
|
| 84 |
"@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="],
|
| 85 |
|
|
|
|
|
|
|
| 86 |
"@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="],
|
| 87 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 88 |
"@floating-ui/core": ["@floating-ui/core@1.7.3", "", { "dependencies": { "@floating-ui/utils": "^0.2.10" } }, "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w=="],
|
| 89 |
|
| 90 |
"@floating-ui/dom": ["@floating-ui/dom@1.7.4", "", { "dependencies": { "@floating-ui/core": "^1.7.3", "@floating-ui/utils": "^0.2.10" } }, "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA=="],
|
|
@@ -93,6 +148,8 @@
|
|
| 93 |
|
| 94 |
"@floating-ui/utils": ["@floating-ui/utils@0.2.10", "", {}, "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ=="],
|
| 95 |
|
|
|
|
|
|
|
| 96 |
"@img/colour": ["@img/colour@1.0.0", "", {}, "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw=="],
|
| 97 |
|
| 98 |
"@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w=="],
|
|
@@ -173,6 +230,34 @@
|
|
| 173 |
|
| 174 |
"@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@16.2.2", "", { "os": "win32", "cpu": "x64" }, "sha512-aHaKceJgdySReT7qeck5oShucxWRiiEuwCGK8HHALe6yZga8uyFpLkPgaRw3kkF04U7ROogL/suYCNt/+CuXGA=="],
|
| 175 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 176 |
"@playwright/test": ["@playwright/test@1.59.1", "", { "dependencies": { "playwright": "1.59.1" }, "bin": { "playwright": "cli.js" } }, "sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg=="],
|
| 177 |
|
| 178 |
"@radix-ui/number": ["@radix-ui/number@1.1.1", "", {}, "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g=="],
|
|
@@ -257,8 +342,28 @@
|
|
| 257 |
|
| 258 |
"@radix-ui/rect": ["@radix-ui/rect@1.1.1", "", {}, "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="],
|
| 259 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 260 |
"@sec-ant/readable-stream": ["@sec-ant/readable-stream@0.4.1", "", {}, "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg=="],
|
| 261 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 262 |
"@sindresorhus/merge-streams": ["@sindresorhus/merge-streams@4.0.0", "", {}, "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ=="],
|
| 263 |
|
| 264 |
"@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="],
|
|
@@ -295,14 +400,8 @@
|
|
| 295 |
|
| 296 |
"@tanstack/query-core": ["@tanstack/query-core@5.96.1", "", {}, "sha512-u1yBgtavSy+N8wgtW3PiER6UpxcplMje65yXnnVgiHTqiMwLlxiw4WvQDrXyn+UD6lnn8kHaxmerJUzQcV/MMg=="],
|
| 297 |
|
| 298 |
-
"@tanstack/query-persist-client-core": ["@tanstack/query-persist-client-core@5.96.1", "", { "dependencies": { "@tanstack/query-core": "5.96.1" } }, "sha512-61jJdGjhBaAJiQ6TRt4uFI8EoAPyf7bJjbtc59AxZwMyid9kV9FlUEtNs2TkEzYHPTYvDZzz0qOvvkn1YD/VEg=="],
|
| 299 |
-
|
| 300 |
-
"@tanstack/query-sync-storage-persister": ["@tanstack/query-sync-storage-persister@5.96.1", "", { "dependencies": { "@tanstack/query-core": "5.96.1", "@tanstack/query-persist-client-core": "5.96.1" } }, "sha512-DpeEVOA8AmUlt+D+X7zZCgfb+xwXj9aJBhALlaFE9uWm0btHT5Xc+GGMnDdtwkxWZLNyEuVarudLArIBsT8N+Q=="],
|
| 301 |
-
|
| 302 |
"@tanstack/react-query": ["@tanstack/react-query@5.96.1", "", { "dependencies": { "@tanstack/query-core": "5.96.1" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-2X7KYK5KKWUKGeWCVcqxXAkYefJtrKB7tSKWgeG++b0H6BRHxQaLSSi8AxcgjmUnnosHuh9WsFZqvE16P1WCzA=="],
|
| 303 |
|
| 304 |
-
"@tanstack/react-query-persist-client": ["@tanstack/react-query-persist-client@5.96.1", "", { "dependencies": { "@tanstack/query-persist-client-core": "5.96.1" }, "peerDependencies": { "@tanstack/react-query": "^5.96.1", "react": "^18 || ^19" } }, "sha512-HkYAI7T2uc12gvVR8P1D7dHgA/AdGDhqDN1HeaNP561OFa0H425qk61ykWtpF8Vnv82ElX6ao0nuNr1oPh89Uw=="],
|
| 305 |
-
|
| 306 |
"@tanstack/react-virtual": ["@tanstack/react-virtual@3.13.23", "", { "dependencies": { "@tanstack/virtual-core": "3.13.23" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-XnMRnHQ23piOVj2bzJqHrRrLg4r+F86fuBcwteKfbIjJrtGxb4z7tIvPVAe4B+4UVwo9G4Giuz5fmapcrnZ0OQ=="],
|
| 307 |
|
| 308 |
"@tanstack/virtual-core": ["@tanstack/virtual-core@3.13.23", "", {}, "sha512-zSz2Z2HNyLjCplANTDyl3BcdQJc2k1+yyFoKhNRmCr7V7dY8o8q5m8uFTI1/Pg1kL+Hgrz6u3Xo6eFUB7l66cg=="],
|
|
@@ -359,14 +458,36 @@
|
|
| 359 |
|
| 360 |
"@use-gesture/react": ["@use-gesture/react@10.3.1", "", { "dependencies": { "@use-gesture/core": "10.3.1" }, "peerDependencies": { "react": ">= 16.8.0" } }, "sha512-Yy19y6O2GJq8f7CHf7L0nxL8bf4PZCPaVOCgJrusOeFHY1LvHgYXnmnXg6N5iwAnbgbZCDjo60SiM6IPJi9C5g=="],
|
| 361 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 362 |
"aria-hidden": ["aria-hidden@1.2.6", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA=="],
|
| 363 |
|
| 364 |
"babel-plugin-react-compiler": ["babel-plugin-react-compiler@1.0.0", "", { "dependencies": { "@babel/types": "^7.26.0" } }, "sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw=="],
|
| 365 |
|
| 366 |
"bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="],
|
| 367 |
|
|
|
|
|
|
|
| 368 |
"baseline-browser-mapping": ["baseline-browser-mapping@2.10.8", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-PCLz/LXGBsNTErbtB6i5u4eLpHeMfi93aUv5duMmj6caNu6IphS4q6UevDnL36sZQv9lrP11dbPKGMaXPwMKfQ=="],
|
| 369 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 370 |
"browser-fs-access": ["browser-fs-access@0.38.0", "", {}, "sha512-JveqW2w6pEZqFEEfMgCszXzYpE89dG+nPsmOdcs741mFFAROeL+iqjGEpR07RI+s0YY0EFr+4KnOoACprJTpOw=="],
|
| 371 |
|
| 372 |
"caniuse-lite": ["caniuse-lite@1.0.30001752", "", {}, "sha512-vKUk7beoukxE47P5gcVNKkDRzXdVofotshHwfR9vmpeFKxmI5PBpgOMC18LUJUA/DvJ70Y7RveasIBraqsyO/g=="],
|
|
@@ -381,6 +502,8 @@
|
|
| 381 |
|
| 382 |
"character-reference-invalid": ["character-reference-invalid@2.0.1", "", {}, "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw=="],
|
| 383 |
|
|
|
|
|
|
|
| 384 |
"class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="],
|
| 385 |
|
| 386 |
"client-only": ["client-only@0.0.1", "", {}, "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="],
|
|
@@ -389,6 +512,10 @@
|
|
| 389 |
|
| 390 |
"comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="],
|
| 391 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 392 |
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
|
| 393 |
|
| 394 |
"csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
|
|
@@ -407,20 +534,40 @@
|
|
| 407 |
|
| 408 |
"enhanced-resolve": ["enhanced-resolve@5.19.0", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.0" } }, "sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg=="],
|
| 409 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 410 |
"escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="],
|
| 411 |
|
| 412 |
"estree-util-is-identifier-name": ["estree-util-is-identifier-name@3.0.0", "", {}, "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg=="],
|
| 413 |
|
| 414 |
-
"
|
| 415 |
|
| 416 |
"execa": ["execa@9.6.1", "", { "dependencies": { "@sindresorhus/merge-streams": "^4.0.0", "cross-spawn": "^7.0.6", "figures": "^6.1.0", "get-stream": "^9.0.0", "human-signals": "^8.0.1", "is-plain-obj": "^4.1.0", "is-stream": "^4.0.1", "npm-run-path": "^6.0.0", "pretty-ms": "^9.2.0", "signal-exit": "^4.1.0", "strip-final-newline": "^4.0.0", "yoctocolors": "^2.1.1" } }, "sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA=="],
|
| 417 |
|
| 418 |
"extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="],
|
| 419 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 420 |
"figures": ["figures@6.1.0", "", { "dependencies": { "is-unicode-supported": "^2.0.0" } }, "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg=="],
|
| 421 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 422 |
"framer-motion": ["framer-motion@12.38.0", "", { "dependencies": { "motion-dom": "^12.38.0", "motion-utils": "^12.36.0", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-rFYkY/pigbcswl1XQSb7q424kSTQ8q6eAC+YUsSKooHQYuLdzdHjrt6uxUC+PRAO++q5IS7+TamgIw1AphxR+g=="],
|
| 423 |
|
|
|
|
|
|
|
| 424 |
"fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="],
|
| 425 |
|
| 426 |
"get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="],
|
|
@@ -441,6 +588,10 @@
|
|
| 441 |
|
| 442 |
"git-cliff-windows-x64": ["git-cliff-windows-x64@2.12.0", "", { "os": "win32", "cpu": "x64" }, "sha512-jskb3nyVGr4dekHSCDM/J6iho45t37wnmMGkPNq42kOoUp04JS96yMBrNRdXfXV9ViZsaZq3NaNu1e3QkhFlyA=="],
|
| 443 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 444 |
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
|
| 445 |
|
| 446 |
"hast-util-to-jsx-runtime": ["hast-util-to-jsx-runtime@2.3.6", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "hast-util-whitespace": "^3.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "style-to-js": "^1.0.0", "unist-util-position": "^5.0.0", "vfile-message": "^4.0.0" } }, "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg=="],
|
|
@@ -459,6 +610,10 @@
|
|
| 459 |
|
| 460 |
"i18next-localstorage-backend": ["i18next-localstorage-backend@4.3.1", "", { "dependencies": { "@babel/runtime": "^7.28.4" } }, "sha512-ry8WNBanUs55rsRZs9+xaZWRxCoTEMMOf+2vNSfzzJqDPbHaf0eMFMrtYp/2ocxU6Xrxfwz17Fdz0rSs3Kw39Q=="],
|
| 461 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 462 |
"inline-style-parser": ["inline-style-parser@0.2.7", "", {}, "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA=="],
|
| 463 |
|
| 464 |
"is-alphabetical": ["is-alphabetical@2.0.1", "", {}, "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ=="],
|
|
@@ -467,8 +622,16 @@
|
|
| 467 |
|
| 468 |
"is-decimal": ["is-decimal@2.0.1", "", {}, "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A=="],
|
| 469 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 470 |
"is-hexadecimal": ["is-hexadecimal@2.0.1", "", {}, "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg=="],
|
| 471 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 472 |
"is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="],
|
| 473 |
|
| 474 |
"is-stream": ["is-stream@4.0.1", "", {}, "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A=="],
|
|
@@ -481,6 +644,16 @@
|
|
| 481 |
|
| 482 |
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
|
| 483 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 484 |
"lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="],
|
| 485 |
|
| 486 |
"lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="],
|
|
@@ -505,14 +678,22 @@
|
|
| 505 |
|
| 506 |
"lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="],
|
| 507 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 508 |
"longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="],
|
| 509 |
|
| 510 |
"loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="],
|
| 511 |
|
| 512 |
"lucide-react": ["lucide-react@0.577.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-4LjoFv2eEPwYDPg/CUdBJQSDfPyzXCRrVW1X7jrx/trgxnxkHFjnVZINbzvzxjN70dxychOfg+FTYwBiS3pQ5A=="],
|
| 513 |
|
|
|
|
|
|
|
| 514 |
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
|
| 515 |
|
|
|
|
|
|
|
| 516 |
"markdown-table": ["markdown-table@3.0.4", "", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="],
|
| 517 |
|
| 518 |
"mdast-util-find-and-replace": ["mdast-util-find-and-replace@3.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "escape-string-regexp": "^5.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg=="],
|
|
@@ -545,6 +726,10 @@
|
|
| 545 |
|
| 546 |
"mdast-util-to-string": ["mdast-util-to-string@4.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0" } }, "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg=="],
|
| 547 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 548 |
"micromark": ["micromark@4.0.2", "", { "dependencies": { "@types/debug": "^4.0.0", "debug": "^4.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA=="],
|
| 549 |
|
| 550 |
"micromark-core-commonmark": ["micromark-core-commonmark@2.0.3", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-destination": "^2.0.0", "micromark-factory-label": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-factory-title": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-html-tag-name": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg=="],
|
|
@@ -601,6 +786,10 @@
|
|
| 601 |
|
| 602 |
"micromark-util-types": ["micromark-util-types@2.0.2", "", {}, "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA=="],
|
| 603 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 604 |
"motion": ["motion@12.38.0", "", { "dependencies": { "framer-motion": "^12.38.0", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-uYfXzeHlgThchzwz5Te47dlv5JOUC7OB4rjJ/7XTUgtBZD8CchMN8qEJ4ZVsUmTyYA44zjV0fBwsiktRuFnn+w=="],
|
| 605 |
|
| 606 |
"motion-dom": ["motion-dom@12.38.0", "", { "dependencies": { "motion-utils": "^12.36.0" } }, "sha512-pdkHLD8QYRp8VfiNLb8xIBJis1byQ9gPT3Jnh2jqfFtAsWUA3dEepDlsWe/xMpO8McV+VdpKVcp+E+TGJEtOoA=="],
|
|
@@ -619,9 +808,11 @@
|
|
| 619 |
|
| 620 |
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
|
| 621 |
|
| 622 |
-
"
|
| 623 |
|
| 624 |
-
"p-
|
|
|
|
|
|
|
| 625 |
|
| 626 |
"parse-entities": ["parse-entities@4.0.2", "", { "dependencies": { "@types/unist": "^2.0.0", "character-entities-legacy": "^3.0.0", "character-reference-invalid": "^2.0.0", "decode-named-character-reference": "^1.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0", "is-hexadecimal": "^2.0.0" } }, "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw=="],
|
| 627 |
|
|
@@ -629,8 +820,12 @@
|
|
| 629 |
|
| 630 |
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
|
| 631 |
|
|
|
|
|
|
|
| 632 |
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
| 633 |
|
|
|
|
|
|
|
| 634 |
"playwright": ["playwright@1.59.1", "", { "dependencies": { "playwright-core": "1.59.1" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw=="],
|
| 635 |
|
| 636 |
"playwright-core": ["playwright-core@1.59.1", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg=="],
|
|
@@ -647,6 +842,10 @@
|
|
| 647 |
|
| 648 |
"property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="],
|
| 649 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 650 |
"re-resizable": ["re-resizable@6.11.2", "", { "peerDependencies": { "react": "^16.13.1 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.13.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-2xI2P3OHs5qw7K0Ud1aLILK6MQxW50TcO+DetD9eIV58j84TqYeHoZcL9H4GXFXXIh7afhH8mv5iUCXII7OW7A=="],
|
| 651 |
|
| 652 |
"react": ["react@19.2.4", "", {}, "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ=="],
|
|
@@ -677,6 +876,8 @@
|
|
| 677 |
|
| 678 |
"react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="],
|
| 679 |
|
|
|
|
|
|
|
| 680 |
"remark-gfm": ["remark-gfm@4.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-gfm": "^3.0.0", "micromark-extension-gfm": "^3.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "unified": "^11.0.0" } }, "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg=="],
|
| 681 |
|
| 682 |
"remark-parse": ["remark-parse@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "micromark-util-types": "^2.0.0", "unified": "^11.0.0" } }, "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA=="],
|
|
@@ -685,6 +886,14 @@
|
|
| 685 |
|
| 686 |
"remark-stringify": ["remark-stringify@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-to-markdown": "^2.0.0", "unified": "^11.0.0" } }, "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw=="],
|
| 687 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 688 |
"scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
|
| 689 |
|
| 690 |
"semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
|
|
@@ -697,12 +906,18 @@
|
|
| 697 |
|
| 698 |
"signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
|
| 699 |
|
|
|
|
|
|
|
| 700 |
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
| 701 |
|
| 702 |
"space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="],
|
| 703 |
|
|
|
|
|
|
|
| 704 |
"stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="],
|
| 705 |
|
|
|
|
|
|
|
| 706 |
"strip-final-newline": ["strip-final-newline@4.0.0", "", {}, "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw=="],
|
| 707 |
|
| 708 |
"style-to-js": ["style-to-js@1.1.21", "", { "dependencies": { "style-to-object": "1.0.14" } }, "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ=="],
|
|
@@ -717,16 +932,28 @@
|
|
| 717 |
|
| 718 |
"tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="],
|
| 719 |
|
|
|
|
|
|
|
| 720 |
"trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="],
|
| 721 |
|
| 722 |
"trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="],
|
| 723 |
|
|
|
|
|
|
|
| 724 |
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
| 725 |
|
| 726 |
"tw-animate-css": ["tw-animate-css@1.4.0", "", {}, "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ=="],
|
| 727 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 728 |
"typescript": ["typescript@6.0.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ=="],
|
| 729 |
|
|
|
|
|
|
|
| 730 |
"ui": ["ui@workspace:ui"],
|
| 731 |
|
| 732 |
"undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
|
|
@@ -745,8 +972,12 @@
|
|
| 745 |
|
| 746 |
"unist-util-visit-parents": ["unist-util-visit-parents@6.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ=="],
|
| 747 |
|
|
|
|
|
|
|
| 748 |
"use-callback-ref": ["use-callback-ref@1.3.3", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="],
|
| 749 |
|
|
|
|
|
|
|
| 750 |
"use-sidecar": ["use-sidecar@1.1.3", "", { "dependencies": { "detect-node-es": "^1.1.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ=="],
|
| 751 |
|
| 752 |
"use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="],
|
|
@@ -759,6 +990,12 @@
|
|
| 759 |
|
| 760 |
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
| 761 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 762 |
"yoctocolors": ["yoctocolors@2.1.2", "", {}, "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug=="],
|
| 763 |
|
| 764 |
"zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="],
|
|
@@ -783,6 +1020,10 @@
|
|
| 783 |
|
| 784 |
"@radix-ui/react-tooltip/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
| 785 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 786 |
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.8.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="],
|
| 787 |
|
| 788 |
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="],
|
|
@@ -795,6 +1036,8 @@
|
|
| 795 |
|
| 796 |
"@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
| 797 |
|
|
|
|
|
|
|
| 798 |
"i18next-browser-languagedetector/@babel/runtime": ["@babel/runtime@7.28.4", "", {}, "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ=="],
|
| 799 |
|
| 800 |
"i18next-localstorage-backend/@babel/runtime": ["@babel/runtime@7.28.4", "", {}, "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ=="],
|
|
|
|
| 29 |
"@radix-ui/react-switch": "^1.2.6",
|
| 30 |
"@radix-ui/react-tabs": "^1.1.13",
|
| 31 |
"@radix-ui/react-tooltip": "^1.2.8",
|
|
|
|
| 32 |
"@tanstack/react-query": "^5.96.1",
|
|
|
|
| 33 |
"@tanstack/react-virtual": "^3.13.23",
|
| 34 |
"@tauri-apps/api": "^2.10.1",
|
| 35 |
"@use-gesture/react": "^10.3.1",
|
| 36 |
+
"@xstate/react": "^6.1.0",
|
| 37 |
"browser-fs-access": "^0.38.0",
|
| 38 |
"class-variance-authority": "^0.7.1",
|
| 39 |
"clsx": "^2.1.1",
|
| 40 |
"i18next": "^25.10.10",
|
| 41 |
"i18next-browser-languagedetector": "^8.2.1",
|
| 42 |
"i18next-localstorage-backend": "^4.3.1",
|
| 43 |
+
"immer": "^11.1.4",
|
| 44 |
"lucide-react": "^0.577.0",
|
| 45 |
"motion": "^12.38.0",
|
| 46 |
"next": "^16.2.2",
|
| 47 |
"next-themes": "^0.4.6",
|
|
|
|
| 48 |
"react": "^19.2.4",
|
| 49 |
"react-colorful": "^5.6.1",
|
| 50 |
"react-dom": "^19.2.4",
|
|
|
|
| 56 |
"react-rnd": "^10.5.3",
|
| 57 |
"remark-gfm": "^4.0.1",
|
| 58 |
"tailwind-merge": "^3.5.0",
|
| 59 |
+
"xstate": "^5.30.0",
|
| 60 |
"zod": "^4.3.6",
|
| 61 |
"zustand": "^5.0.12",
|
| 62 |
},
|
|
|
|
| 66 |
"@types/react": "^19.2.14",
|
| 67 |
"@types/react-dom": "^19.2.3",
|
| 68 |
"babel-plugin-react-compiler": "^1.0.0",
|
| 69 |
+
"orval": "^8.6.2",
|
| 70 |
"tailwindcss": "^4.2.2",
|
| 71 |
"tw-animate-css": "^1.4.0",
|
| 72 |
"typescript": "^6.0.2",
|
|
|
|
| 84 |
|
| 85 |
"@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="],
|
| 86 |
|
| 87 |
+
"@commander-js/extra-typings": ["@commander-js/extra-typings@14.0.0", "", { "peerDependencies": { "commander": "~14.0.0" } }, "sha512-hIn0ncNaJRLkZrxBIp5AsW/eXEHNKYQBh0aPdoUqNgD+Io3NIykQqpKFyKcuasZhicGaEZJX/JBSIkZ4e5x8Dg=="],
|
| 88 |
+
|
| 89 |
"@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="],
|
| 90 |
|
| 91 |
+
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.5", "", { "os": "aix", "cpu": "ppc64" }, "sha512-nGsF/4C7uzUj+Nj/4J+Zt0bYQ6bz33Phz8Lb2N80Mti1HjGclTJdXZ+9APC4kLvONbjxN1zfvYNd8FEcbBK/MQ=="],
|
| 92 |
+
|
| 93 |
+
"@esbuild/android-arm": ["@esbuild/android-arm@0.27.5", "", { "os": "android", "cpu": "arm" }, "sha512-Cv781jd0Rfj/paoNrul1/r4G0HLvuFKYh7C9uHZ2Pl8YXstzvCyyeWENTFR9qFnRzNMCjXmsulZuvosDg10Mog=="],
|
| 94 |
+
|
| 95 |
+
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.5", "", { "os": "android", "cpu": "arm64" }, "sha512-Oeghq+XFgh1pUGd1YKs4DDoxzxkoUkvko+T/IVKwlghKLvvjbGFB3ek8VEDBmNvqhwuL0CQS3cExdzpmUyIrgA=="],
|
| 96 |
+
|
| 97 |
+
"@esbuild/android-x64": ["@esbuild/android-x64@0.27.5", "", { "os": "android", "cpu": "x64" }, "sha512-nQD7lspbzerlmtNOxYMFAGmhxgzn8Z7m9jgFkh6kpkjsAhZee1w8tJW3ZlW+N9iRePz0oPUDrYrXidCPSImD0Q=="],
|
| 98 |
+
|
| 99 |
+
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-I+Ya/MgC6rr8oRWGRDF3BXDfP8K1BVUggHqN6VI2lUZLdDi1IM1v2cy0e3lCPbP+pVcK3Tv8cgUhHse1kaNZZw=="],
|
| 100 |
+
|
| 101 |
+
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-MCjQUtC8wWJn/pIPM7vQaO69BFgwPD1jriEdqwTCKzWjGgkMbcg+M5HzrOhPhuYe1AJjXlHmD142KQf+jnYj8A=="],
|
| 102 |
+
|
| 103 |
+
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.5", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-X6xVS+goSH0UelYXnuf4GHLwpOdc8rgK/zai+dKzBMnncw7BTQIwquOodE7EKvY2UVUetSqyAfyZC1D+oqLQtg=="],
|
| 104 |
+
|
| 105 |
+
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-233X1FGo3a8x1ekLB6XT69LfZ83vqz+9z3TSEQCTYfMNY880A97nr81KbPcAMl9rmOFp11wO0dP+eB18KU/Ucg=="],
|
| 106 |
+
|
| 107 |
+
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.5", "", { "os": "linux", "cpu": "arm" }, "sha512-0wkVrYHG4sdCCN/bcwQ7yYMXACkaHc3UFeaEOwSVW6e5RycMageYAFv+JS2bKLwHyeKVUvtoVH+5/RHq0fgeFw=="],
|
| 108 |
+
|
| 109 |
+
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-euKkilsNOv7x/M1NKsx5znyprbpsRFIzTV6lWziqJch7yWYayfLtZzDxDTl+LSQDJYAjd9TVb/Kt5UKIrj2e4A=="],
|
| 110 |
+
|
| 111 |
+
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.5", "", { "os": "linux", "cpu": "ia32" }, "sha512-hVRQX4+P3MS36NxOy24v/Cdsimy/5HYePw+tmPqnNN1fxV0bPrFWR6TMqwXPwoTM2VzbkA+4lbHWUKDd5ZDA/w=="],
|
| 112 |
+
|
| 113 |
+
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.5", "", { "os": "linux", "cpu": "none" }, "sha512-mKqqRuOPALI8nDzhOBmIS0INvZOOFGGg5n1osGIXAx8oersceEbKd4t1ACNTHM3sJBXGFAlEgqM+svzjPot+ZQ=="],
|
| 114 |
+
|
| 115 |
+
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.5", "", { "os": "linux", "cpu": "none" }, "sha512-EE/QXH9IyaAj1qeuIV5+/GZkBTipgGO782Ff7Um3vPS9cvLhJJeATy4Ggxikz2inZ46KByamMn6GqtqyVjhenA=="],
|
| 116 |
+
|
| 117 |
+
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-0V2iF1RGxBf1b7/BjurA5jfkl7PtySjom1r6xOK2q9KWw/XCpAdtB6KNMO+9xx69yYfSCRR9FE0TyKfHA2eQMw=="],
|
| 118 |
+
|
| 119 |
+
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.5", "", { "os": "linux", "cpu": "none" }, "sha512-rYxThBx6G9HN6tFNuvB/vykeLi4VDsm5hE5pVwzqbAjZEARQrWu3noZSfbEnPZ/CRXP3271GyFk/49up2W190g=="],
|
| 120 |
+
|
| 121 |
+
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-uEP2q/4qgd8goEUc4QIdU/1P2NmEtZ/zX5u3OpLlCGhJIuBIv0s0wr7TB2nBrd3/A5XIdEkkS5ZLF0ULuvaaYQ=="],
|
| 122 |
+
|
| 123 |
+
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.5", "", { "os": "linux", "cpu": "x64" }, "sha512-+Gq47Wqq6PLOOZuBzVSII2//9yyHNKZLuwfzCemqexqOQCSz0zy0O26kIzyp9EMNMK+nZ0tFHBZrCeVUuMs/ew=="],
|
| 124 |
+
|
| 125 |
+
"@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.5", "", { "os": "none", "cpu": "arm64" }, "sha512-3F/5EG8VHfN/I+W5cO1/SV2H9Q/5r7vcHabMnBqhHK2lTWOh3F8vixNzo8lqxrlmBtZVFpW8pmITHnq54+Tq4g=="],
|
| 126 |
+
|
| 127 |
+
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.5", "", { "os": "none", "cpu": "x64" }, "sha512-28t+Sj3CPN8vkMOlZotOmDgilQwVvxWZl7b8rxpn73Tt/gCnvrHxQUMng4uu3itdFvrtba/1nHejvxqz8xgEMA=="],
|
| 128 |
+
|
| 129 |
+
"@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.5", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-Doz/hKtiuVAi9hMsBMpwBANhIZc8l238U2Onko3t2xUp8xtM0ZKdDYHMnm/qPFVthY8KtxkXaocwmMh6VolzMA=="],
|
| 130 |
+
|
| 131 |
+
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.5", "", { "os": "openbsd", "cpu": "x64" }, "sha512-WfGVaa1oz5A7+ZFPkERIbIhKT4olvGl1tyzTRaB5yoZRLqC0KwaO95FeZtOdQj/oKkjW57KcVF944m62/0GYtA=="],
|
| 132 |
+
|
| 133 |
+
"@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.5", "", { "os": "none", "cpu": "arm64" }, "sha512-Xh+VRuh6OMh3uJ0JkCjI57l+DVe7VRGBYymen8rFPnTVgATBwA6nmToxM2OwTlSvrnWpPKkrQUj93+K9huYC6A=="],
|
| 134 |
+
|
| 135 |
+
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.5", "", { "os": "sunos", "cpu": "x64" }, "sha512-aC1gpJkkaUADHuAdQfuVTnqVUTLqqUNhAvEwHwVWcnVVZvNlDPGA0UveZsfXJJ9T6k9Po4eHi3c02gbdwO3g6w=="],
|
| 136 |
+
|
| 137 |
+
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-0UNx2aavV0fk6UpZcwXFLztA2r/k9jTUa7OW7SAea1VYUhkug99MW1uZeXEnPn5+cHOd0n8myQay6TlFnBR07w=="],
|
| 138 |
+
|
| 139 |
+
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-5nlJ3AeJWCTSzR7AEqVjT/faWyqKU86kCi1lLmxVqmNR+j4HrYdns+eTGjS/vmrzCIe8inGQckUadvS0+JkKdQ=="],
|
| 140 |
+
|
| 141 |
+
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.5", "", { "os": "win32", "cpu": "x64" }, "sha512-PWypQR+d4FLfkhBIV+/kHsUELAnMpx1bRvvsn3p+/sAERbnCzFrtDRG2Xw5n+2zPxBK2+iaP+vetsRl4Ti7WgA=="],
|
| 142 |
+
|
| 143 |
"@floating-ui/core": ["@floating-ui/core@1.7.3", "", { "dependencies": { "@floating-ui/utils": "^0.2.10" } }, "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w=="],
|
| 144 |
|
| 145 |
"@floating-ui/dom": ["@floating-ui/dom@1.7.4", "", { "dependencies": { "@floating-ui/core": "^1.7.3", "@floating-ui/utils": "^0.2.10" } }, "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA=="],
|
|
|
|
| 148 |
|
| 149 |
"@floating-ui/utils": ["@floating-ui/utils@0.2.10", "", {}, "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ=="],
|
| 150 |
|
| 151 |
+
"@gerrit0/mini-shiki": ["@gerrit0/mini-shiki@3.23.0", "", { "dependencies": { "@shikijs/engine-oniguruma": "^3.23.0", "@shikijs/langs": "^3.23.0", "@shikijs/themes": "^3.23.0", "@shikijs/types": "^3.23.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-bEMORlG0cqdjVyCEuU0cDQbORWX+kYCeo0kV1lbxF5bt4r7SID2l9bqsxJEM0zndaxpOUT7riCyIVEuqq/Ynxg=="],
|
| 152 |
+
|
| 153 |
"@img/colour": ["@img/colour@1.0.0", "", {}, "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw=="],
|
| 154 |
|
| 155 |
"@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w=="],
|
|
|
|
| 230 |
|
| 231 |
"@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@16.2.2", "", { "os": "win32", "cpu": "x64" }, "sha512-aHaKceJgdySReT7qeck5oShucxWRiiEuwCGK8HHALe6yZga8uyFpLkPgaRw3kkF04U7ROogL/suYCNt/+CuXGA=="],
|
| 232 |
|
| 233 |
+
"@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="],
|
| 234 |
+
|
| 235 |
+
"@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="],
|
| 236 |
+
|
| 237 |
+
"@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="],
|
| 238 |
+
|
| 239 |
+
"@orval/angular": ["@orval/angular@8.6.2", "", { "dependencies": { "@orval/core": "8.6.2" } }, "sha512-XNTYgfbsjnyGWG12jKkC5URFILTRUASOizypqDLKJWxA3ZEJ6/Dwnuz4wgdiTxb8Ltv1IyuJABkkxk2OM8eCMg=="],
|
| 240 |
+
|
| 241 |
+
"@orval/axios": ["@orval/axios@8.6.2", "", { "dependencies": { "@orval/core": "8.6.2" } }, "sha512-76xSbiwqbYNbM1pasbPv2fSo2Dmwe879HnAWi406GSHAmwlDu5qCN+E53vsvceidUWWuMjh/JuEWgptx/tPGBQ=="],
|
| 242 |
+
|
| 243 |
+
"@orval/core": ["@orval/core@8.6.2", "", { "dependencies": { "@scalar/openapi-types": "0.5.3", "acorn": "^8.15.0", "compare-versions": "^6.1.1", "debug": "^4.4.3", "esbuild": "^0.27.3", "esutils": "2.0.3", "fs-extra": "^11.3.2", "globby": "16.1.0", "jiti": "^2.6.1", "remeda": "^2.33.6", "typedoc": "^0.28.17" }, "peerDependencies": { "@faker-js/faker": ">=10" }, "optionalPeers": ["@faker-js/faker"] }, "sha512-spwJFnqyEm96R7pt+DXgpqmOA2g1iJVN8R/xhub8z6uZVvxEsRMWxZs93Idk0fpZOFkfmq+d6LuZCd5/lbIzCg=="],
|
| 244 |
+
|
| 245 |
+
"@orval/fetch": ["@orval/fetch@8.6.2", "", { "dependencies": { "@orval/core": "8.6.2", "@scalar/openapi-types": "0.5.3" } }, "sha512-MBQ88xSQmx+5hAirCBiHSYL/rtDy6T3e7eAXaCGZ8trjeWryGpW9hF+dJukFw0QmWL8ql6ez0O9VnRkEDeVYhQ=="],
|
| 246 |
+
|
| 247 |
+
"@orval/hono": ["@orval/hono@8.6.2", "", { "dependencies": { "@orval/core": "8.6.2", "@orval/zod": "8.6.2", "fs-extra": "^11.3.2", "remeda": "^2.33.6" } }, "sha512-zv3ZomWvGuVqSoWIUujA6Z0rpxGo9g0dA58+tcPCRaxBIq6ld1YYhLea9QVE96xvb3RfFrOYhGgS840/yVpwyQ=="],
|
| 248 |
+
|
| 249 |
+
"@orval/mcp": ["@orval/mcp@8.6.2", "", { "dependencies": { "@orval/core": "8.6.2", "@orval/fetch": "8.6.2", "@orval/zod": "8.6.2" } }, "sha512-iw4opul8nI97y4bpoTzQS611HefBEQUPfnLS4RCilQyi23mCH0/Ee60USHf7aUeBqdp5yboyWOZxNRLr2T/fuQ=="],
|
| 250 |
+
|
| 251 |
+
"@orval/mock": ["@orval/mock@8.6.2", "", { "dependencies": { "@orval/core": "8.6.2", "remeda": "^2.33.6" } }, "sha512-5/21u5RdROmXyzpuBzj1+sbMwP+YgLuzR2YDxx7I9nNZd/IB77o5mZyzaPXxmZ5AG4ONQJ/07gBsapxmhQrT8A=="],
|
| 252 |
+
|
| 253 |
+
"@orval/query": ["@orval/query@8.6.2", "", { "dependencies": { "@orval/core": "8.6.2", "@orval/fetch": "8.6.2", "remeda": "^2.33.6" } }, "sha512-cyIqk25w6R8VehCGZT2KunOpen3V1qKVqJy1Jn+oDkPDDr9aGAU5f1ohBqNQuL4/bowYmzay0VApD73PqIjbgA=="],
|
| 254 |
+
|
| 255 |
+
"@orval/solid-start": ["@orval/solid-start@8.6.2", "", { "dependencies": { "@orval/core": "8.6.2", "@scalar/openapi-types": "0.5.3" } }, "sha512-FAskL7OhN0/oV2S5sEA4gz6J8Cp+9GpqTfwQTeZAJ7Iq1wH6xzQBSkjhuJ0pveSrONGTDLj6PEO3g0NqP/AakQ=="],
|
| 256 |
+
|
| 257 |
+
"@orval/swr": ["@orval/swr@8.6.2", "", { "dependencies": { "@orval/core": "8.6.2", "@orval/fetch": "8.6.2" } }, "sha512-vQmE3AwQE45PxMXYyuHLIXd7NjDKmJNlCg4VSeO0hznAAwPhubussCrFDeq/x6QbnBp/IOUBZh2SSf85W/AsPQ=="],
|
| 258 |
+
|
| 259 |
+
"@orval/zod": ["@orval/zod@8.6.2", "", { "dependencies": { "@orval/core": "8.6.2", "remeda": "^2.33.6" } }, "sha512-bVEXzO7W8kXtRNzTDoN+Pq8NVJnYGS+P4PbqpO5k56rWiTqEsYKzQU0vR2jHOeao2RGoIF3OiX3qMrhfak5GfA=="],
|
| 260 |
+
|
| 261 |
"@playwright/test": ["@playwright/test@1.59.1", "", { "dependencies": { "playwright": "1.59.1" }, "bin": { "playwright": "cli.js" } }, "sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg=="],
|
| 262 |
|
| 263 |
"@radix-ui/number": ["@radix-ui/number@1.1.1", "", {}, "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g=="],
|
|
|
|
| 342 |
|
| 343 |
"@radix-ui/rect": ["@radix-ui/rect@1.1.1", "", {}, "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="],
|
| 344 |
|
| 345 |
+
"@scalar/helpers": ["@scalar/helpers@0.2.18", "", {}, "sha512-w1d4tpNEVZ293oB2BAgLrS0kVPUtG3eByNmOCJA5eK9vcT4D3cmsGtWjUaaqit0BQCsBFHK51rasGvSWnApYTw=="],
|
| 346 |
+
|
| 347 |
+
"@scalar/json-magic": ["@scalar/json-magic@0.11.7", "", { "dependencies": { "@scalar/helpers": "0.2.18", "pathe": "^2.0.3", "yaml": "^2.8.0" } }, "sha512-GVz9E0vXu+ecypkdn0biK1gbQVkK4QTTX1Hq3eMgxlLQC91wwiqWfCqwfhuX0LRu+Z5OmYhLhufDJEEh56rVgA=="],
|
| 348 |
+
|
| 349 |
+
"@scalar/openapi-parser": ["@scalar/openapi-parser@0.24.17", "", { "dependencies": { "@scalar/helpers": "0.2.18", "@scalar/json-magic": "0.11.7", "@scalar/openapi-types": "0.5.4", "@scalar/openapi-upgrader": "0.1.11", "ajv": "^8.17.1", "ajv-draft-04": "^1.0.0", "ajv-formats": "^3.0.1", "jsonpointer": "^5.0.1", "leven": "^4.0.0", "yaml": "^2.8.0" } }, "sha512-aM9UVrzlMreC3X/sZbyj+7XDZmat3ecGC3RpU8dqEO/HIH+CEX0xMLuP+41DhePCYg5+9TtDomSfWuMq4x1Z1A=="],
|
| 350 |
+
|
| 351 |
+
"@scalar/openapi-types": ["@scalar/openapi-types@0.5.3", "", { "dependencies": { "zod": "^4.1.11" } }, "sha512-m4n/Su3K01d15dmdWO1LlqecdSPKuNjuokrJLdiQ485kW/hRHbXW1QP6tJL75myhw/XhX5YhYAR+jrwnGjXiMw=="],
|
| 352 |
+
|
| 353 |
+
"@scalar/openapi-upgrader": ["@scalar/openapi-upgrader@0.1.11", "", { "dependencies": { "@scalar/openapi-types": "0.5.4" } }, "sha512-ngJcHGoCHmpWgYtNy08vmzFfLdQEkMpvaCQqNPPMNKq0QEXOv89e/rn+TZJZgPnRlY7fDIoIhn9lNgr+azBW+w=="],
|
| 354 |
+
|
| 355 |
"@sec-ant/readable-stream": ["@sec-ant/readable-stream@0.4.1", "", {}, "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg=="],
|
| 356 |
|
| 357 |
+
"@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-1nWINwKXxKKLqPibT5f4pAFLej9oZzQTsby8942OTlsJzOBZ0MWKiwzMsd+jhzu8YPCHAswGnnN1YtQfirL35g=="],
|
| 358 |
+
|
| 359 |
+
"@shikijs/langs": ["@shikijs/langs@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0" } }, "sha512-2Ep4W3Re5aB1/62RSYQInK9mM3HsLeB91cHqznAJMuylqjzNVAVCMnNWRHFtcNHXsoNRayP9z1qj4Sq3nMqYXg=="],
|
| 360 |
+
|
| 361 |
+
"@shikijs/themes": ["@shikijs/themes@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0" } }, "sha512-5qySYa1ZgAT18HR/ypENL9cUSGOeI2x+4IvYJu4JgVJdizn6kG4ia5Q1jDEOi7gTbN4RbuYtmHh0W3eccOrjMA=="],
|
| 362 |
+
|
| 363 |
+
"@shikijs/types": ["@shikijs/types@3.23.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ=="],
|
| 364 |
+
|
| 365 |
+
"@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="],
|
| 366 |
+
|
| 367 |
"@sindresorhus/merge-streams": ["@sindresorhus/merge-streams@4.0.0", "", {}, "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ=="],
|
| 368 |
|
| 369 |
"@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="],
|
|
|
|
| 400 |
|
| 401 |
"@tanstack/query-core": ["@tanstack/query-core@5.96.1", "", {}, "sha512-u1yBgtavSy+N8wgtW3PiER6UpxcplMje65yXnnVgiHTqiMwLlxiw4WvQDrXyn+UD6lnn8kHaxmerJUzQcV/MMg=="],
|
| 402 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 403 |
"@tanstack/react-query": ["@tanstack/react-query@5.96.1", "", { "dependencies": { "@tanstack/query-core": "5.96.1" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-2X7KYK5KKWUKGeWCVcqxXAkYefJtrKB7tSKWgeG++b0H6BRHxQaLSSi8AxcgjmUnnosHuh9WsFZqvE16P1WCzA=="],
|
| 404 |
|
|
|
|
|
|
|
| 405 |
"@tanstack/react-virtual": ["@tanstack/react-virtual@3.13.23", "", { "dependencies": { "@tanstack/virtual-core": "3.13.23" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-XnMRnHQ23piOVj2bzJqHrRrLg4r+F86fuBcwteKfbIjJrtGxb4z7tIvPVAe4B+4UVwo9G4Giuz5fmapcrnZ0OQ=="],
|
| 406 |
|
| 407 |
"@tanstack/virtual-core": ["@tanstack/virtual-core@3.13.23", "", {}, "sha512-zSz2Z2HNyLjCplANTDyl3BcdQJc2k1+yyFoKhNRmCr7V7dY8o8q5m8uFTI1/Pg1kL+Hgrz6u3Xo6eFUB7l66cg=="],
|
|
|
|
| 458 |
|
| 459 |
"@use-gesture/react": ["@use-gesture/react@10.3.1", "", { "dependencies": { "@use-gesture/core": "10.3.1" }, "peerDependencies": { "react": ">= 16.8.0" } }, "sha512-Yy19y6O2GJq8f7CHf7L0nxL8bf4PZCPaVOCgJrusOeFHY1LvHgYXnmnXg6N5iwAnbgbZCDjo60SiM6IPJi9C5g=="],
|
| 460 |
|
| 461 |
+
"@xstate/react": ["@xstate/react@6.1.0", "", { "dependencies": { "use-isomorphic-layout-effect": "^1.1.2", "use-sync-external-store": "^1.2.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "xstate": "^5.28.0" }, "optionalPeers": ["xstate"] }, "sha512-ep9F0jGTI63B/jE8GHdMpUqtuz7yRebNaKv8EMUaiSi29NOglywc2X2YSOV/ygbIK+LtmgZ0q9anoEA2iBSEOw=="],
|
| 462 |
+
|
| 463 |
+
"acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="],
|
| 464 |
+
|
| 465 |
+
"ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="],
|
| 466 |
+
|
| 467 |
+
"ajv-draft-04": ["ajv-draft-04@1.0.0", "", { "peerDependencies": { "ajv": "^8.5.0" }, "optionalPeers": ["ajv"] }, "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw=="],
|
| 468 |
+
|
| 469 |
+
"ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="],
|
| 470 |
+
|
| 471 |
+
"ansi-colors": ["ansi-colors@4.1.3", "", {}, "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw=="],
|
| 472 |
+
|
| 473 |
+
"ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
| 474 |
+
|
| 475 |
+
"argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
|
| 476 |
+
|
| 477 |
"aria-hidden": ["aria-hidden@1.2.6", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA=="],
|
| 478 |
|
| 479 |
"babel-plugin-react-compiler": ["babel-plugin-react-compiler@1.0.0", "", { "dependencies": { "@babel/types": "^7.26.0" } }, "sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw=="],
|
| 480 |
|
| 481 |
"bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="],
|
| 482 |
|
| 483 |
+
"balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
|
| 484 |
+
|
| 485 |
"baseline-browser-mapping": ["baseline-browser-mapping@2.10.8", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-PCLz/LXGBsNTErbtB6i5u4eLpHeMfi93aUv5duMmj6caNu6IphS4q6UevDnL36sZQv9lrP11dbPKGMaXPwMKfQ=="],
|
| 486 |
|
| 487 |
+
"brace-expansion": ["brace-expansion@5.0.5", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ=="],
|
| 488 |
+
|
| 489 |
+
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
|
| 490 |
+
|
| 491 |
"browser-fs-access": ["browser-fs-access@0.38.0", "", {}, "sha512-JveqW2w6pEZqFEEfMgCszXzYpE89dG+nPsmOdcs741mFFAROeL+iqjGEpR07RI+s0YY0EFr+4KnOoACprJTpOw=="],
|
| 492 |
|
| 493 |
"caniuse-lite": ["caniuse-lite@1.0.30001752", "", {}, "sha512-vKUk7beoukxE47P5gcVNKkDRzXdVofotshHwfR9vmpeFKxmI5PBpgOMC18LUJUA/DvJ70Y7RveasIBraqsyO/g=="],
|
|
|
|
| 502 |
|
| 503 |
"character-reference-invalid": ["character-reference-invalid@2.0.1", "", {}, "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw=="],
|
| 504 |
|
| 505 |
+
"chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="],
|
| 506 |
+
|
| 507 |
"class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="],
|
| 508 |
|
| 509 |
"client-only": ["client-only@0.0.1", "", {}, "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="],
|
|
|
|
| 512 |
|
| 513 |
"comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="],
|
| 514 |
|
| 515 |
+
"commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="],
|
| 516 |
+
|
| 517 |
+
"compare-versions": ["compare-versions@6.1.1", "", {}, "sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg=="],
|
| 518 |
+
|
| 519 |
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
|
| 520 |
|
| 521 |
"csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
|
|
|
|
| 534 |
|
| 535 |
"enhanced-resolve": ["enhanced-resolve@5.19.0", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.0" } }, "sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg=="],
|
| 536 |
|
| 537 |
+
"enquirer": ["enquirer@2.4.1", "", { "dependencies": { "ansi-colors": "^4.1.1", "strip-ansi": "^6.0.1" } }, "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ=="],
|
| 538 |
+
|
| 539 |
+
"entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
|
| 540 |
+
|
| 541 |
+
"esbuild": ["esbuild@0.27.5", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.5", "@esbuild/android-arm": "0.27.5", "@esbuild/android-arm64": "0.27.5", "@esbuild/android-x64": "0.27.5", "@esbuild/darwin-arm64": "0.27.5", "@esbuild/darwin-x64": "0.27.5", "@esbuild/freebsd-arm64": "0.27.5", "@esbuild/freebsd-x64": "0.27.5", "@esbuild/linux-arm": "0.27.5", "@esbuild/linux-arm64": "0.27.5", "@esbuild/linux-ia32": "0.27.5", "@esbuild/linux-loong64": "0.27.5", "@esbuild/linux-mips64el": "0.27.5", "@esbuild/linux-ppc64": "0.27.5", "@esbuild/linux-riscv64": "0.27.5", "@esbuild/linux-s390x": "0.27.5", "@esbuild/linux-x64": "0.27.5", "@esbuild/netbsd-arm64": "0.27.5", "@esbuild/netbsd-x64": "0.27.5", "@esbuild/openbsd-arm64": "0.27.5", "@esbuild/openbsd-x64": "0.27.5", "@esbuild/openharmony-arm64": "0.27.5", "@esbuild/sunos-x64": "0.27.5", "@esbuild/win32-arm64": "0.27.5", "@esbuild/win32-ia32": "0.27.5", "@esbuild/win32-x64": "0.27.5" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-zdQoHBjuDqKsvV5OPaWansOwfSQ0Js+Uj9J85TBvj3bFW1JjWTSULMRwdQAc8qMeIScbClxeMK0jlrtB9linhA=="],
|
| 542 |
+
|
| 543 |
"escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="],
|
| 544 |
|
| 545 |
"estree-util-is-identifier-name": ["estree-util-is-identifier-name@3.0.0", "", {}, "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg=="],
|
| 546 |
|
| 547 |
+
"esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="],
|
| 548 |
|
| 549 |
"execa": ["execa@9.6.1", "", { "dependencies": { "@sindresorhus/merge-streams": "^4.0.0", "cross-spawn": "^7.0.6", "figures": "^6.1.0", "get-stream": "^9.0.0", "human-signals": "^8.0.1", "is-plain-obj": "^4.1.0", "is-stream": "^4.0.1", "npm-run-path": "^6.0.0", "pretty-ms": "^9.2.0", "signal-exit": "^4.1.0", "strip-final-newline": "^4.0.0", "yoctocolors": "^2.1.1" } }, "sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA=="],
|
| 550 |
|
| 551 |
"extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="],
|
| 552 |
|
| 553 |
+
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
|
| 554 |
+
|
| 555 |
+
"fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="],
|
| 556 |
+
|
| 557 |
+
"fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="],
|
| 558 |
+
|
| 559 |
+
"fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="],
|
| 560 |
+
|
| 561 |
"figures": ["figures@6.1.0", "", { "dependencies": { "is-unicode-supported": "^2.0.0" } }, "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg=="],
|
| 562 |
|
| 563 |
+
"fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
|
| 564 |
+
|
| 565 |
+
"find-up": ["find-up@8.0.0", "", { "dependencies": { "locate-path": "^8.0.0", "unicorn-magic": "^0.3.0" } }, "sha512-JGG8pvDi2C+JxidYdIwQDyS/CgcrIdh18cvgxcBge3wSHRQOrooMD3GlFBcmMJAN9M42SAZjDp5zv1dglJjwww=="],
|
| 566 |
+
|
| 567 |
"framer-motion": ["framer-motion@12.38.0", "", { "dependencies": { "motion-dom": "^12.38.0", "motion-utils": "^12.36.0", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-rFYkY/pigbcswl1XQSb7q424kSTQ8q6eAC+YUsSKooHQYuLdzdHjrt6uxUC+PRAO++q5IS7+TamgIw1AphxR+g=="],
|
| 568 |
|
| 569 |
+
"fs-extra": ["fs-extra@11.3.4", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA=="],
|
| 570 |
+
|
| 571 |
"fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="],
|
| 572 |
|
| 573 |
"get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="],
|
|
|
|
| 588 |
|
| 589 |
"git-cliff-windows-x64": ["git-cliff-windows-x64@2.12.0", "", { "os": "win32", "cpu": "x64" }, "sha512-jskb3nyVGr4dekHSCDM/J6iho45t37wnmMGkPNq42kOoUp04JS96yMBrNRdXfXV9ViZsaZq3NaNu1e3QkhFlyA=="],
|
| 590 |
|
| 591 |
+
"glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
| 592 |
+
|
| 593 |
+
"globby": ["globby@16.1.0", "", { "dependencies": { "@sindresorhus/merge-streams": "^4.0.0", "fast-glob": "^3.3.3", "ignore": "^7.0.5", "is-path-inside": "^4.0.0", "slash": "^5.1.0", "unicorn-magic": "^0.4.0" } }, "sha512-+A4Hq7m7Ze592k9gZRy4gJ27DrXRNnC1vPjxTt1qQxEY8RxagBkBxivkCwg7FxSTG0iLLEMaUx13oOr0R2/qcQ=="],
|
| 594 |
+
|
| 595 |
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
|
| 596 |
|
| 597 |
"hast-util-to-jsx-runtime": ["hast-util-to-jsx-runtime@2.3.6", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "hast-util-whitespace": "^3.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "style-to-js": "^1.0.0", "unist-util-position": "^5.0.0", "vfile-message": "^4.0.0" } }, "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg=="],
|
|
|
|
| 610 |
|
| 611 |
"i18next-localstorage-backend": ["i18next-localstorage-backend@4.3.1", "", { "dependencies": { "@babel/runtime": "^7.28.4" } }, "sha512-ry8WNBanUs55rsRZs9+xaZWRxCoTEMMOf+2vNSfzzJqDPbHaf0eMFMrtYp/2ocxU6Xrxfwz17Fdz0rSs3Kw39Q=="],
|
| 612 |
|
| 613 |
+
"ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
|
| 614 |
+
|
| 615 |
+
"immer": ["immer@11.1.4", "", {}, "sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw=="],
|
| 616 |
+
|
| 617 |
"inline-style-parser": ["inline-style-parser@0.2.7", "", {}, "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA=="],
|
| 618 |
|
| 619 |
"is-alphabetical": ["is-alphabetical@2.0.1", "", {}, "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ=="],
|
|
|
|
| 622 |
|
| 623 |
"is-decimal": ["is-decimal@2.0.1", "", {}, "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A=="],
|
| 624 |
|
| 625 |
+
"is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
|
| 626 |
+
|
| 627 |
+
"is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
|
| 628 |
+
|
| 629 |
"is-hexadecimal": ["is-hexadecimal@2.0.1", "", {}, "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg=="],
|
| 630 |
|
| 631 |
+
"is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="],
|
| 632 |
+
|
| 633 |
+
"is-path-inside": ["is-path-inside@4.0.0", "", {}, "sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA=="],
|
| 634 |
+
|
| 635 |
"is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="],
|
| 636 |
|
| 637 |
"is-stream": ["is-stream@4.0.1", "", {}, "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A=="],
|
|
|
|
| 644 |
|
| 645 |
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
|
| 646 |
|
| 647 |
+
"js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="],
|
| 648 |
+
|
| 649 |
+
"json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
|
| 650 |
+
|
| 651 |
+
"jsonfile": ["jsonfile@6.2.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg=="],
|
| 652 |
+
|
| 653 |
+
"jsonpointer": ["jsonpointer@5.0.1", "", {}, "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ=="],
|
| 654 |
+
|
| 655 |
+
"leven": ["leven@4.1.0", "", {}, "sha512-KZ9W9nWDT7rF7Dazg8xyLHGLrmpgq2nVNFUckhqdW3szVP6YhCpp/RAnpmVExA9JvrMynjwSLVrEj3AepHR6ew=="],
|
| 656 |
+
|
| 657 |
"lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="],
|
| 658 |
|
| 659 |
"lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="],
|
|
|
|
| 678 |
|
| 679 |
"lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="],
|
| 680 |
|
| 681 |
+
"linkify-it": ["linkify-it@5.0.0", "", { "dependencies": { "uc.micro": "^2.0.0" } }, "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ=="],
|
| 682 |
+
|
| 683 |
+
"locate-path": ["locate-path@8.0.0", "", { "dependencies": { "p-locate": "^6.0.0" } }, "sha512-XT9ewWAC43tiAV7xDAPflMkG0qOPn2QjHqlgX8FOqmWa/rxnyYDulF9T0F7tRy1u+TVTmK/M//6VIOye+2zDXg=="],
|
| 684 |
+
|
| 685 |
"longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="],
|
| 686 |
|
| 687 |
"loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="],
|
| 688 |
|
| 689 |
"lucide-react": ["lucide-react@0.577.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-4LjoFv2eEPwYDPg/CUdBJQSDfPyzXCRrVW1X7jrx/trgxnxkHFjnVZINbzvzxjN70dxychOfg+FTYwBiS3pQ5A=="],
|
| 690 |
|
| 691 |
+
"lunr": ["lunr@2.3.9", "", {}, "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow=="],
|
| 692 |
+
|
| 693 |
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
|
| 694 |
|
| 695 |
+
"markdown-it": ["markdown-it@14.1.1", "", { "dependencies": { "argparse": "^2.0.1", "entities": "^4.4.0", "linkify-it": "^5.0.0", "mdurl": "^2.0.0", "punycode.js": "^2.3.1", "uc.micro": "^2.1.0" }, "bin": { "markdown-it": "bin/markdown-it.mjs" } }, "sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA=="],
|
| 696 |
+
|
| 697 |
"markdown-table": ["markdown-table@3.0.4", "", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="],
|
| 698 |
|
| 699 |
"mdast-util-find-and-replace": ["mdast-util-find-and-replace@3.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "escape-string-regexp": "^5.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg=="],
|
|
|
|
| 726 |
|
| 727 |
"mdast-util-to-string": ["mdast-util-to-string@4.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0" } }, "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg=="],
|
| 728 |
|
| 729 |
+
"mdurl": ["mdurl@2.0.0", "", {}, "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w=="],
|
| 730 |
+
|
| 731 |
+
"merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="],
|
| 732 |
+
|
| 733 |
"micromark": ["micromark@4.0.2", "", { "dependencies": { "@types/debug": "^4.0.0", "debug": "^4.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA=="],
|
| 734 |
|
| 735 |
"micromark-core-commonmark": ["micromark-core-commonmark@2.0.3", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-destination": "^2.0.0", "micromark-factory-label": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-factory-title": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-html-tag-name": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg=="],
|
|
|
|
| 786 |
|
| 787 |
"micromark-util-types": ["micromark-util-types@2.0.2", "", {}, "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA=="],
|
| 788 |
|
| 789 |
+
"micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
|
| 790 |
+
|
| 791 |
+
"minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="],
|
| 792 |
+
|
| 793 |
"motion": ["motion@12.38.0", "", { "dependencies": { "framer-motion": "^12.38.0", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-uYfXzeHlgThchzwz5Te47dlv5JOUC7OB4rjJ/7XTUgtBZD8CchMN8qEJ4ZVsUmTyYA44zjV0fBwsiktRuFnn+w=="],
|
| 794 |
|
| 795 |
"motion-dom": ["motion-dom@12.38.0", "", { "dependencies": { "motion-utils": "^12.36.0" } }, "sha512-pdkHLD8QYRp8VfiNLb8xIBJis1byQ9gPT3Jnh2jqfFtAsWUA3dEepDlsWe/xMpO8McV+VdpKVcp+E+TGJEtOoA=="],
|
|
|
|
| 808 |
|
| 809 |
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
|
| 810 |
|
| 811 |
+
"orval": ["orval@8.6.2", "", { "dependencies": { "@commander-js/extra-typings": "^14.0.0", "@orval/angular": "8.6.2", "@orval/axios": "8.6.2", "@orval/core": "8.6.2", "@orval/fetch": "8.6.2", "@orval/hono": "8.6.2", "@orval/mcp": "8.6.2", "@orval/mock": "8.6.2", "@orval/query": "8.6.2", "@orval/solid-start": "8.6.2", "@orval/swr": "8.6.2", "@orval/zod": "8.6.2", "@scalar/json-magic": "^0.11.5", "@scalar/openapi-parser": "^0.24.13", "@scalar/openapi-types": "0.5.3", "chokidar": "^5.0.0", "commander": "^14.0.2", "enquirer": "^2.4.1", "execa": "^9.6.1", "find-up": "8.0.0", "fs-extra": "^11.3.2", "jiti": "^2.6.1", "js-yaml": "4.1.1", "remeda": "^2.33.6", "string-argv": "^0.3.2", "tsconfck": "^3.1.6", "typedoc": "^0.28.17", "typedoc-plugin-coverage": "^4.0.2", "typedoc-plugin-markdown": "^4.10.0" }, "peerDependencies": { "prettier": ">=3.0.0" }, "optionalPeers": ["prettier"], "bin": { "orval": "dist/bin/orval.mjs" } }, "sha512-hnWdF7jrdgZFhRy4ZVMpLMYpkmfLDkO1wTh881Ll0w18XOmvJauYvrsTmTby3gf4qEChtWxNULI3HlY173NqvA=="],
|
| 812 |
|
| 813 |
+
"p-limit": ["p-limit@4.0.0", "", { "dependencies": { "yocto-queue": "^1.0.0" } }, "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ=="],
|
| 814 |
+
|
| 815 |
+
"p-locate": ["p-locate@6.0.0", "", { "dependencies": { "p-limit": "^4.0.0" } }, "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw=="],
|
| 816 |
|
| 817 |
"parse-entities": ["parse-entities@4.0.2", "", { "dependencies": { "@types/unist": "^2.0.0", "character-entities-legacy": "^3.0.0", "character-reference-invalid": "^2.0.0", "decode-named-character-reference": "^1.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0", "is-hexadecimal": "^2.0.0" } }, "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw=="],
|
| 818 |
|
|
|
|
| 820 |
|
| 821 |
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
|
| 822 |
|
| 823 |
+
"pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
|
| 824 |
+
|
| 825 |
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
| 826 |
|
| 827 |
+
"picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="],
|
| 828 |
+
|
| 829 |
"playwright": ["playwright@1.59.1", "", { "dependencies": { "playwright-core": "1.59.1" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw=="],
|
| 830 |
|
| 831 |
"playwright-core": ["playwright-core@1.59.1", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg=="],
|
|
|
|
| 842 |
|
| 843 |
"property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="],
|
| 844 |
|
| 845 |
+
"punycode.js": ["punycode.js@2.3.1", "", {}, "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA=="],
|
| 846 |
+
|
| 847 |
+
"queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
|
| 848 |
+
|
| 849 |
"re-resizable": ["re-resizable@6.11.2", "", { "peerDependencies": { "react": "^16.13.1 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.13.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-2xI2P3OHs5qw7K0Ud1aLILK6MQxW50TcO+DetD9eIV58j84TqYeHoZcL9H4GXFXXIh7afhH8mv5iUCXII7OW7A=="],
|
| 850 |
|
| 851 |
"react": ["react@19.2.4", "", {}, "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ=="],
|
|
|
|
| 876 |
|
| 877 |
"react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="],
|
| 878 |
|
| 879 |
+
"readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="],
|
| 880 |
+
|
| 881 |
"remark-gfm": ["remark-gfm@4.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-gfm": "^3.0.0", "micromark-extension-gfm": "^3.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "unified": "^11.0.0" } }, "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg=="],
|
| 882 |
|
| 883 |
"remark-parse": ["remark-parse@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "micromark-util-types": "^2.0.0", "unified": "^11.0.0" } }, "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA=="],
|
|
|
|
| 886 |
|
| 887 |
"remark-stringify": ["remark-stringify@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-to-markdown": "^2.0.0", "unified": "^11.0.0" } }, "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw=="],
|
| 888 |
|
| 889 |
+
"remeda": ["remeda@2.33.7", "", {}, "sha512-cXlyjevWx5AcslOUEETG4o8XYi9UkoCXcJmj7XhPFVbla+ITuOBxv6ijBrmbeg+ZhzmDThkNdO+iXKUfrJep1w=="],
|
| 890 |
+
|
| 891 |
+
"require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
|
| 892 |
+
|
| 893 |
+
"reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="],
|
| 894 |
+
|
| 895 |
+
"run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="],
|
| 896 |
+
|
| 897 |
"scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
|
| 898 |
|
| 899 |
"semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
|
|
|
|
| 906 |
|
| 907 |
"signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
|
| 908 |
|
| 909 |
+
"slash": ["slash@5.1.0", "", {}, "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg=="],
|
| 910 |
+
|
| 911 |
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
| 912 |
|
| 913 |
"space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="],
|
| 914 |
|
| 915 |
+
"string-argv": ["string-argv@0.3.2", "", {}, "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q=="],
|
| 916 |
+
|
| 917 |
"stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="],
|
| 918 |
|
| 919 |
+
"strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
| 920 |
+
|
| 921 |
"strip-final-newline": ["strip-final-newline@4.0.0", "", {}, "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw=="],
|
| 922 |
|
| 923 |
"style-to-js": ["style-to-js@1.1.21", "", { "dependencies": { "style-to-object": "1.0.14" } }, "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ=="],
|
|
|
|
| 932 |
|
| 933 |
"tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="],
|
| 934 |
|
| 935 |
+
"to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
|
| 936 |
+
|
| 937 |
"trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="],
|
| 938 |
|
| 939 |
"trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="],
|
| 940 |
|
| 941 |
+
"tsconfck": ["tsconfck@3.1.6", "", { "peerDependencies": { "typescript": "^5.0.0" }, "optionalPeers": ["typescript"], "bin": { "tsconfck": "bin/tsconfck.js" } }, "sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w=="],
|
| 942 |
+
|
| 943 |
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
| 944 |
|
| 945 |
"tw-animate-css": ["tw-animate-css@1.4.0", "", {}, "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ=="],
|
| 946 |
|
| 947 |
+
"typedoc": ["typedoc@0.28.18", "", { "dependencies": { "@gerrit0/mini-shiki": "^3.23.0", "lunr": "^2.3.9", "markdown-it": "^14.1.1", "minimatch": "^10.2.4", "yaml": "^2.8.2" }, "peerDependencies": { "typescript": "5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x || 5.6.x || 5.7.x || 5.8.x || 5.9.x || 6.0.x" }, "bin": { "typedoc": "bin/typedoc" } }, "sha512-NTWTUOFRQ9+SGKKTuWKUioUkjxNwtS3JDRPVKZAXGHZy2wCA8bdv2iJiyeePn0xkmK+TCCqZFT0X7+2+FLjngA=="],
|
| 948 |
+
|
| 949 |
+
"typedoc-plugin-coverage": ["typedoc-plugin-coverage@4.0.2", "", { "peerDependencies": { "typedoc": "0.28.x" } }, "sha512-mfn0e7NCqB8x2PfvhXrtmd7KWlsNf1+B2N9y8gR/jexXBLrXl/0e+b2HdG5HaTXGi7i0t2pyQY2VRmq7gtdEHQ=="],
|
| 950 |
+
|
| 951 |
+
"typedoc-plugin-markdown": ["typedoc-plugin-markdown@4.11.0", "", { "peerDependencies": { "typedoc": "0.28.x" } }, "sha512-2iunh2ALyfyh204OF7h2u0kuQ84xB3jFZtFyUr01nThJkLvR8oGGSSDlyt2gyO4kXhvUxDcVbO0y43+qX+wFbw=="],
|
| 952 |
+
|
| 953 |
"typescript": ["typescript@6.0.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ=="],
|
| 954 |
|
| 955 |
+
"uc.micro": ["uc.micro@2.1.0", "", {}, "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A=="],
|
| 956 |
+
|
| 957 |
"ui": ["ui@workspace:ui"],
|
| 958 |
|
| 959 |
"undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
|
|
|
|
| 972 |
|
| 973 |
"unist-util-visit-parents": ["unist-util-visit-parents@6.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ=="],
|
| 974 |
|
| 975 |
+
"universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="],
|
| 976 |
+
|
| 977 |
"use-callback-ref": ["use-callback-ref@1.3.3", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="],
|
| 978 |
|
| 979 |
+
"use-isomorphic-layout-effect": ["use-isomorphic-layout-effect@1.2.1", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-tpZZ+EX0gaghDAiFR37hj5MgY6ZN55kLiPkJsKxBMZ6GZdOSPJXiOzPM984oPYZ5AnehYx5WQp1+ME8I/P/pRA=="],
|
| 980 |
+
|
| 981 |
"use-sidecar": ["use-sidecar@1.1.3", "", { "dependencies": { "detect-node-es": "^1.1.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ=="],
|
| 982 |
|
| 983 |
"use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="],
|
|
|
|
| 990 |
|
| 991 |
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
| 992 |
|
| 993 |
+
"xstate": ["xstate@5.30.0", "", {}, "sha512-mIzIuMjtYVkqXq9dUzYQoag7b/dF1CBS/yhliuPLfR0FwKPC18HiUivb/crcqY2gknhR8gJEhnppLg6ubQ0gGw=="],
|
| 994 |
+
|
| 995 |
+
"yaml": ["yaml@2.8.3", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg=="],
|
| 996 |
+
|
| 997 |
+
"yocto-queue": ["yocto-queue@1.2.2", "", {}, "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ=="],
|
| 998 |
+
|
| 999 |
"yoctocolors": ["yoctocolors@2.1.2", "", {}, "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug=="],
|
| 1000 |
|
| 1001 |
"zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="],
|
|
|
|
| 1020 |
|
| 1021 |
"@radix-ui/react-tooltip/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
| 1022 |
|
| 1023 |
+
"@scalar/openapi-parser/@scalar/openapi-types": ["@scalar/openapi-types@0.5.4", "", { "dependencies": { "zod": "^4.3.5" } }, "sha512-2pEbhprh8lLGDfUI6mNm9EV104pjb3+aJsXrFaqfgOSre7r6NlgM5HcSbsLjzDAnTikjJhJ3IMal1Rz8WVwiOw=="],
|
| 1024 |
+
|
| 1025 |
+
"@scalar/openapi-upgrader/@scalar/openapi-types": ["@scalar/openapi-types@0.5.4", "", { "dependencies": { "zod": "^4.3.5" } }, "sha512-2pEbhprh8lLGDfUI6mNm9EV104pjb3+aJsXrFaqfgOSre7r6NlgM5HcSbsLjzDAnTikjJhJ3IMal1Rz8WVwiOw=="],
|
| 1026 |
+
|
| 1027 |
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.8.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="],
|
| 1028 |
|
| 1029 |
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="],
|
|
|
|
| 1036 |
|
| 1037 |
"@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
| 1038 |
|
| 1039 |
+
"globby/unicorn-magic": ["unicorn-magic@0.4.0", "", {}, "sha512-wH590V9VNgYH9g3lH9wWjTrUoKsjLF6sGLjhR4sH1LWpLmCOH0Zf7PukhDA8BiS7KHe4oPNkcTHqYkj7SOGUOw=="],
|
| 1040 |
+
|
| 1041 |
"i18next-browser-languagedetector/@babel/runtime": ["@babel/runtime@7.28.4", "", {}, "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ=="],
|
| 1042 |
|
| 1043 |
"i18next-localstorage-backend/@babel/runtime": ["@babel/runtime@7.28.4", "", {}, "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ=="],
|
koharu-core/Cargo.toml
CHANGED
|
@@ -20,7 +20,7 @@ serde = { workspace = true }
|
|
| 20 |
serde_bytes = { workspace = true }
|
| 21 |
strum = { workspace = true }
|
| 22 |
tokio = { workspace = true }
|
| 23 |
-
|
| 24 |
uuid = { workspace = true }
|
| 25 |
|
| 26 |
[dev-dependencies]
|
|
|
|
| 20 |
serde_bytes = { workspace = true }
|
| 21 |
strum = { workspace = true }
|
| 22 |
tokio = { workspace = true }
|
| 23 |
+
utoipa = { workspace = true }
|
| 24 |
uuid = { workspace = true }
|
| 25 |
|
| 26 |
[dev-dependencies]
|
koharu-core/src/effect.rs
CHANGED
|
@@ -3,7 +3,7 @@ use serde::{Deserialize, Deserializer, Serialize};
|
|
| 3 |
use std::fmt;
|
| 4 |
use std::str::FromStr;
|
| 5 |
use strum::IntoEnumIterator;
|
| 6 |
-
use
|
| 7 |
|
| 8 |
#[derive(Debug, Clone, Copy, PartialEq, Eq, strum::Display, strum::EnumIter, strum::EnumString)]
|
| 9 |
#[strum(serialize_all = "lowercase")]
|
|
@@ -12,9 +12,8 @@ enum TextShaderEffectFlag {
|
|
| 12 |
Bold,
|
| 13 |
}
|
| 14 |
|
| 15 |
-
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Default,
|
| 16 |
#[serde(rename_all = "camelCase")]
|
| 17 |
-
#[ts(export)]
|
| 18 |
pub struct TextShaderEffect {
|
| 19 |
#[serde(default)]
|
| 20 |
pub italic: bool,
|
|
|
|
| 3 |
use std::fmt;
|
| 4 |
use std::str::FromStr;
|
| 5 |
use strum::IntoEnumIterator;
|
| 6 |
+
use utoipa::ToSchema;
|
| 7 |
|
| 8 |
#[derive(Debug, Clone, Copy, PartialEq, Eq, strum::Display, strum::EnumIter, strum::EnumString)]
|
| 9 |
#[strum(serialize_all = "lowercase")]
|
|
|
|
| 12 |
Bold,
|
| 13 |
}
|
| 14 |
|
| 15 |
+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Default, ToSchema, JsonSchema)]
|
| 16 |
#[serde(rename_all = "camelCase")]
|
|
|
|
| 17 |
pub struct TextShaderEffect {
|
| 18 |
#[serde(default)]
|
| 19 |
pub italic: bool,
|
koharu-core/src/font.rs
CHANGED
|
@@ -1,16 +1,14 @@
|
|
| 1 |
use schemars::JsonSchema;
|
| 2 |
use serde::{Deserialize, Serialize};
|
| 3 |
-
use
|
| 4 |
|
| 5 |
-
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq,
|
| 6 |
-
#[ts(export)]
|
| 7 |
pub enum TextDirection {
|
| 8 |
Horizontal,
|
| 9 |
Vertical,
|
| 10 |
}
|
| 11 |
|
| 12 |
-
#[derive(Debug, Clone, Serialize, Deserialize,
|
| 13 |
-
#[ts(export)]
|
| 14 |
pub struct NamedFontPrediction {
|
| 15 |
pub index: usize,
|
| 16 |
pub name: String,
|
|
@@ -19,8 +17,7 @@ pub struct NamedFontPrediction {
|
|
| 19 |
pub serif: bool,
|
| 20 |
}
|
| 21 |
|
| 22 |
-
#[derive(Debug, Clone, Serialize, Deserialize,
|
| 23 |
-
#[ts(export)]
|
| 24 |
pub struct FontPrediction {
|
| 25 |
pub top_fonts: Vec<(usize, f32)>,
|
| 26 |
pub named_fonts: Vec<NamedFontPrediction>,
|
|
|
|
| 1 |
use schemars::JsonSchema;
|
| 2 |
use serde::{Deserialize, Serialize};
|
| 3 |
+
use utoipa::ToSchema;
|
| 4 |
|
| 5 |
+
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, ToSchema, JsonSchema)]
|
|
|
|
| 6 |
pub enum TextDirection {
|
| 7 |
Horizontal,
|
| 8 |
Vertical,
|
| 9 |
}
|
| 10 |
|
| 11 |
+
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, JsonSchema)]
|
|
|
|
| 12 |
pub struct NamedFontPrediction {
|
| 13 |
pub index: usize,
|
| 14 |
pub name: String,
|
|
|
|
| 17 |
pub serif: bool,
|
| 18 |
}
|
| 19 |
|
| 20 |
+
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, JsonSchema)]
|
|
|
|
| 21 |
pub struct FontPrediction {
|
| 22 |
pub top_fonts: Vec<(usize, f32)>,
|
| 23 |
pub named_fonts: Vec<NamedFontPrediction>,
|
koharu-core/src/lib.rs
CHANGED
|
@@ -21,7 +21,7 @@ use ::image::GenericImageView;
|
|
| 21 |
use schemars::JsonSchema;
|
| 22 |
use serde::{Deserialize, Serialize};
|
| 23 |
use tokio::sync::RwLock;
|
| 24 |
-
use
|
| 25 |
use uuid::Uuid;
|
| 26 |
|
| 27 |
fn new_text_block_id() -> String {
|
|
@@ -96,9 +96,8 @@ impl TextBlock {
|
|
| 96 |
}
|
| 97 |
}
|
| 98 |
|
| 99 |
-
#[derive(Debug, Clone, Serialize, Deserialize,
|
| 100 |
#[serde(rename_all = "camelCase")]
|
| 101 |
-
#[ts(export)]
|
| 102 |
pub struct TextStrokeStyle {
|
| 103 |
#[serde(default = "default_true")]
|
| 104 |
pub enabled: bool,
|
|
@@ -126,9 +125,8 @@ const fn default_stroke_color() -> [u8; 4] {
|
|
| 126 |
[255, 255, 255, 255]
|
| 127 |
}
|
| 128 |
|
| 129 |
-
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default,
|
| 130 |
#[serde(rename_all = "camelCase")]
|
| 131 |
-
#[ts(export)]
|
| 132 |
pub enum TextAlign {
|
| 133 |
#[default]
|
| 134 |
Left,
|
|
@@ -136,9 +134,8 @@ pub enum TextAlign {
|
|
| 136 |
Right,
|
| 137 |
}
|
| 138 |
|
| 139 |
-
#[derive(Debug, Clone, Serialize, Deserialize,
|
| 140 |
#[serde(rename_all = "camelCase")]
|
| 141 |
-
#[ts(export)]
|
| 142 |
pub struct TextStyle {
|
| 143 |
pub font_families: Vec<String>,
|
| 144 |
pub font_size: Option<f32>,
|
|
|
|
| 21 |
use schemars::JsonSchema;
|
| 22 |
use serde::{Deserialize, Serialize};
|
| 23 |
use tokio::sync::RwLock;
|
| 24 |
+
use utoipa::ToSchema;
|
| 25 |
use uuid::Uuid;
|
| 26 |
|
| 27 |
fn new_text_block_id() -> String {
|
|
|
|
| 96 |
}
|
| 97 |
}
|
| 98 |
|
| 99 |
+
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, JsonSchema)]
|
| 100 |
#[serde(rename_all = "camelCase")]
|
|
|
|
| 101 |
pub struct TextStrokeStyle {
|
| 102 |
#[serde(default = "default_true")]
|
| 103 |
pub enabled: bool,
|
|
|
|
| 125 |
[255, 255, 255, 255]
|
| 126 |
}
|
| 127 |
|
| 128 |
+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default, ToSchema, JsonSchema)]
|
| 129 |
#[serde(rename_all = "camelCase")]
|
|
|
|
| 130 |
pub enum TextAlign {
|
| 131 |
#[default]
|
| 132 |
Left,
|
|
|
|
| 134 |
Right,
|
| 135 |
}
|
| 136 |
|
| 137 |
+
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, JsonSchema)]
|
| 138 |
#[serde(rename_all = "camelCase")]
|
|
|
|
| 139 |
pub struct TextStyle {
|
| 140 |
pub font_families: Vec<String>,
|
| 141 |
pub font_size: Option<f32>,
|
koharu-core/src/protocol.rs
CHANGED
|
@@ -1,51 +1,45 @@
|
|
| 1 |
use schemars::JsonSchema;
|
| 2 |
use serde::{Deserialize, Serialize};
|
| 3 |
-
use
|
| 4 |
|
| 5 |
use crate::{Document, FontPrediction, TextBlock, TextShaderEffect, TextStrokeStyle, TextStyle};
|
| 6 |
|
| 7 |
-
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, JsonSchema,
|
| 8 |
#[serde(rename_all = "camelCase")]
|
| 9 |
-
#[ts(export)]
|
| 10 |
pub struct FontFaceInfo {
|
| 11 |
pub family_name: String,
|
| 12 |
pub post_script_name: String,
|
| 13 |
}
|
| 14 |
|
| 15 |
-
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema,
|
| 16 |
#[serde(rename_all = "camelCase")]
|
| 17 |
-
#[ts(export)]
|
| 18 |
pub struct MetaInfo {
|
| 19 |
pub version: String,
|
| 20 |
pub ml_device: String,
|
| 21 |
}
|
| 22 |
|
| 23 |
-
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema,
|
| 24 |
#[serde(rename_all = "camelCase")]
|
| 25 |
-
#[ts(export)]
|
| 26 |
pub struct BootstrapConfig {
|
| 27 |
pub runtime: BootstrapPathConfig,
|
| 28 |
pub models: BootstrapPathConfig,
|
| 29 |
pub http: BootstrapHttpConfig,
|
| 30 |
}
|
| 31 |
|
| 32 |
-
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema,
|
| 33 |
#[serde(rename_all = "camelCase")]
|
| 34 |
-
#[ts(export)]
|
| 35 |
pub struct BootstrapPathConfig {
|
| 36 |
pub path: String,
|
| 37 |
}
|
| 38 |
|
| 39 |
-
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema,
|
| 40 |
#[serde(rename_all = "camelCase")]
|
| 41 |
-
#[ts(export)]
|
| 42 |
pub struct BootstrapHttpConfig {
|
| 43 |
pub proxy: Option<String>,
|
| 44 |
}
|
| 45 |
|
| 46 |
-
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema,
|
| 47 |
#[serde(rename_all = "camelCase")]
|
| 48 |
-
#[ts(export)]
|
| 49 |
pub struct DocumentSummary {
|
| 50 |
pub id: String,
|
| 51 |
pub name: String,
|
|
@@ -76,9 +70,8 @@ impl From<&Document> for DocumentSummary {
|
|
| 76 |
}
|
| 77 |
}
|
| 78 |
|
| 79 |
-
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema,
|
| 80 |
#[serde(rename_all = "camelCase")]
|
| 81 |
-
#[ts(export)]
|
| 82 |
pub struct TextBlockDetail {
|
| 83 |
pub id: String,
|
| 84 |
pub x: f32,
|
|
@@ -123,9 +116,8 @@ impl From<&TextBlock> for TextBlockDetail {
|
|
| 123 |
}
|
| 124 |
}
|
| 125 |
|
| 126 |
-
#[derive(Debug, Clone, Serialize, Deserialize,
|
| 127 |
#[serde(rename_all = "camelCase")]
|
| 128 |
-
#[ts(export)]
|
| 129 |
pub struct DocumentDetail {
|
| 130 |
pub id: String,
|
| 131 |
pub path: String,
|
|
@@ -134,29 +126,38 @@ pub struct DocumentDetail {
|
|
| 134 |
pub height: u32,
|
| 135 |
pub revision: u64,
|
| 136 |
pub text_blocks: Vec<TextBlockDetail>,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 137 |
}
|
| 138 |
|
| 139 |
-
|
| 140 |
-
fn from(document: &Document) -> Self {
|
| 141 |
-
Self {
|
| 142 |
-
id: document.id.clone(),
|
| 143 |
-
path: document.path.to_string_lossy().to_string(),
|
| 144 |
-
name: document.name.clone(),
|
| 145 |
-
width: document.width,
|
| 146 |
-
height: document.height,
|
| 147 |
-
revision: document.revision,
|
| 148 |
-
text_blocks: document
|
| 149 |
-
.text_blocks
|
| 150 |
-
.iter()
|
| 151 |
-
.map(TextBlockDetail::from)
|
| 152 |
-
.collect(),
|
| 153 |
-
}
|
| 154 |
-
}
|
| 155 |
-
}
|
| 156 |
-
|
| 157 |
-
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, TS)]
|
| 158 |
#[serde(rename_all = "camelCase")]
|
| 159 |
-
#[ts(export)]
|
| 160 |
pub struct TextBlockPatch {
|
| 161 |
pub text: Option<String>,
|
| 162 |
pub translation: Option<String>,
|
|
@@ -167,9 +168,8 @@ pub struct TextBlockPatch {
|
|
| 167 |
pub style: Option<TextStyle>,
|
| 168 |
}
|
| 169 |
|
| 170 |
-
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema,
|
| 171 |
#[serde(rename_all = "camelCase")]
|
| 172 |
-
#[ts(export)]
|
| 173 |
pub struct CreateTextBlock {
|
| 174 |
pub x: f32,
|
| 175 |
pub y: f32,
|
|
@@ -177,49 +177,43 @@ pub struct CreateTextBlock {
|
|
| 177 |
pub height: f32,
|
| 178 |
}
|
| 179 |
|
| 180 |
-
#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema,
|
| 181 |
#[serde(rename_all = "snake_case")]
|
| 182 |
-
#[ts(export)]
|
| 183 |
pub enum ImportMode {
|
| 184 |
Replace,
|
| 185 |
Append,
|
| 186 |
}
|
| 187 |
|
| 188 |
-
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema,
|
| 189 |
#[serde(rename_all = "camelCase")]
|
| 190 |
-
#[ts(export)]
|
| 191 |
pub struct ImportResult {
|
| 192 |
pub total_count: usize,
|
| 193 |
pub documents: Vec<DocumentSummary>,
|
| 194 |
}
|
| 195 |
|
| 196 |
-
#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema,
|
| 197 |
#[serde(rename_all = "snake_case")]
|
| 198 |
-
#[ts(export)]
|
| 199 |
pub enum ExportLayer {
|
| 200 |
Rendered,
|
| 201 |
Inpainted,
|
| 202 |
}
|
| 203 |
|
| 204 |
-
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema,
|
| 205 |
#[serde(rename_all = "camelCase")]
|
| 206 |
-
#[ts(export)]
|
| 207 |
pub struct ExportResult {
|
| 208 |
pub count: usize,
|
| 209 |
}
|
| 210 |
|
| 211 |
-
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema,
|
| 212 |
#[serde(rename_all = "camelCase")]
|
| 213 |
-
#[ts(export)]
|
| 214 |
pub struct LlmModelInfo {
|
| 215 |
pub id: String,
|
| 216 |
pub languages: Vec<String>,
|
| 217 |
pub source: String,
|
| 218 |
}
|
| 219 |
|
| 220 |
-
#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema,
|
| 221 |
#[serde(rename_all = "snake_case")]
|
| 222 |
-
#[ts(export)]
|
| 223 |
pub enum LlmStateStatus {
|
| 224 |
Empty,
|
| 225 |
Loading,
|
|
@@ -227,9 +221,8 @@ pub enum LlmStateStatus {
|
|
| 227 |
Failed,
|
| 228 |
}
|
| 229 |
|
| 230 |
-
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema,
|
| 231 |
#[serde(rename_all = "camelCase")]
|
| 232 |
-
#[ts(export)]
|
| 233 |
pub struct LlmState {
|
| 234 |
pub status: LlmStateStatus,
|
| 235 |
pub model_id: Option<String>,
|
|
@@ -237,9 +230,8 @@ pub struct LlmState {
|
|
| 237 |
pub error: Option<String>,
|
| 238 |
}
|
| 239 |
|
| 240 |
-
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema,
|
| 241 |
#[serde(rename_all = "camelCase")]
|
| 242 |
-
#[ts(export)]
|
| 243 |
pub struct LlmLoadRequest {
|
| 244 |
pub id: String,
|
| 245 |
pub api_key: Option<String>,
|
|
@@ -249,17 +241,15 @@ pub struct LlmLoadRequest {
|
|
| 249 |
pub custom_system_prompt: Option<String>,
|
| 250 |
}
|
| 251 |
|
| 252 |
-
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema,
|
| 253 |
#[serde(rename_all = "camelCase")]
|
| 254 |
-
#[ts(export)]
|
| 255 |
pub struct LlmPingRequest {
|
| 256 |
pub base_url: String,
|
| 257 |
pub api_key: Option<String>,
|
| 258 |
}
|
| 259 |
|
| 260 |
-
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema,
|
| 261 |
#[serde(rename_all = "camelCase")]
|
| 262 |
-
#[ts(export)]
|
| 263 |
pub struct LlmPingResponse {
|
| 264 |
pub ok: bool,
|
| 265 |
pub models: Vec<String>,
|
|
@@ -267,9 +257,8 @@ pub struct LlmPingResponse {
|
|
| 267 |
pub error: Option<String>,
|
| 268 |
}
|
| 269 |
|
| 270 |
-
#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema,
|
| 271 |
#[serde(rename_all = "snake_case")]
|
| 272 |
-
#[ts(export)]
|
| 273 |
pub enum JobStatus {
|
| 274 |
Running,
|
| 275 |
Completed,
|
|
@@ -277,9 +266,8 @@ pub enum JobStatus {
|
|
| 277 |
Failed,
|
| 278 |
}
|
| 279 |
|
| 280 |
-
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema,
|
| 281 |
#[serde(rename_all = "camelCase")]
|
| 282 |
-
#[ts(export)]
|
| 283 |
pub struct JobState {
|
| 284 |
pub id: String,
|
| 285 |
pub kind: String,
|
|
@@ -293,9 +281,8 @@ pub struct JobState {
|
|
| 293 |
pub error: Option<String>,
|
| 294 |
}
|
| 295 |
|
| 296 |
-
#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema,
|
| 297 |
#[serde(rename_all = "snake_case")]
|
| 298 |
-
#[ts(export)]
|
| 299 |
pub enum TransferStatus {
|
| 300 |
Started,
|
| 301 |
Downloading,
|
|
@@ -303,9 +290,8 @@ pub enum TransferStatus {
|
|
| 303 |
Failed,
|
| 304 |
}
|
| 305 |
|
| 306 |
-
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema,
|
| 307 |
#[serde(rename_all = "camelCase")]
|
| 308 |
-
#[ts(export)]
|
| 309 |
pub struct DownloadState {
|
| 310 |
pub id: String,
|
| 311 |
pub filename: String,
|
|
@@ -315,9 +301,8 @@ pub struct DownloadState {
|
|
| 315 |
pub error: Option<String>,
|
| 316 |
}
|
| 317 |
|
| 318 |
-
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema,
|
| 319 |
#[serde(rename_all = "camelCase")]
|
| 320 |
-
#[ts(export)]
|
| 321 |
pub struct SnapshotEvent {
|
| 322 |
pub documents: Vec<DocumentSummary>,
|
| 323 |
pub llm: LlmState,
|
|
@@ -325,39 +310,34 @@ pub struct SnapshotEvent {
|
|
| 325 |
pub downloads: Vec<DownloadState>,
|
| 326 |
}
|
| 327 |
|
| 328 |
-
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema,
|
| 329 |
#[serde(rename_all = "camelCase")]
|
| 330 |
-
#[ts(export)]
|
| 331 |
pub struct DocumentsChangedEvent {
|
| 332 |
pub documents: Vec<DocumentSummary>,
|
| 333 |
}
|
| 334 |
|
| 335 |
-
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema,
|
| 336 |
#[serde(rename_all = "camelCase")]
|
| 337 |
-
#[ts(export)]
|
| 338 |
pub struct DocumentChangedEvent {
|
| 339 |
pub document_id: String,
|
| 340 |
pub revision: u64,
|
| 341 |
pub changed: Vec<String>,
|
| 342 |
}
|
| 343 |
|
| 344 |
-
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema,
|
| 345 |
#[serde(rename_all = "camelCase")]
|
| 346 |
-
#[ts(export)]
|
| 347 |
pub struct ApiKeyValue {
|
| 348 |
pub api_key: String,
|
| 349 |
}
|
| 350 |
|
| 351 |
-
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema,
|
| 352 |
#[serde(rename_all = "camelCase")]
|
| 353 |
-
#[ts(export)]
|
| 354 |
pub struct ApiKeyResponse {
|
| 355 |
pub api_key: Option<String>,
|
| 356 |
}
|
| 357 |
|
| 358 |
-
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema,
|
| 359 |
#[serde(rename_all = "camelCase")]
|
| 360 |
-
#[ts(export)]
|
| 361 |
pub struct RenderRequest {
|
| 362 |
pub text_block_id: Option<String>,
|
| 363 |
pub shader_effect: Option<TextShaderEffect>,
|
|
@@ -365,17 +345,15 @@ pub struct RenderRequest {
|
|
| 365 |
pub font_family: Option<String>,
|
| 366 |
}
|
| 367 |
|
| 368 |
-
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema,
|
| 369 |
#[serde(rename_all = "camelCase")]
|
| 370 |
-
#[ts(export)]
|
| 371 |
pub struct TranslateRequest {
|
| 372 |
pub text_block_id: Option<String>,
|
| 373 |
pub language: Option<String>,
|
| 374 |
}
|
| 375 |
|
| 376 |
-
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema,
|
| 377 |
#[serde(rename_all = "camelCase")]
|
| 378 |
-
#[ts(export)]
|
| 379 |
pub struct PipelineJobRequest {
|
| 380 |
pub document_id: Option<String>,
|
| 381 |
pub llm_model_id: Option<String>,
|
|
@@ -390,9 +368,8 @@ pub struct PipelineJobRequest {
|
|
| 390 |
pub font_family: Option<String>,
|
| 391 |
}
|
| 392 |
|
| 393 |
-
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema,
|
| 394 |
#[serde(rename_all = "camelCase")]
|
| 395 |
-
#[ts(export)]
|
| 396 |
pub struct Region {
|
| 397 |
pub x: u32,
|
| 398 |
pub y: u32,
|
|
@@ -400,25 +377,22 @@ pub struct Region {
|
|
| 400 |
pub height: u32,
|
| 401 |
}
|
| 402 |
|
| 403 |
-
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema,
|
| 404 |
#[serde(rename_all = "camelCase")]
|
| 405 |
-
#[ts(export)]
|
| 406 |
pub struct MaskRegionRequest {
|
| 407 |
pub data: Vec<u8>,
|
| 408 |
pub region: Option<Region>,
|
| 409 |
}
|
| 410 |
|
| 411 |
-
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema,
|
| 412 |
#[serde(rename_all = "camelCase")]
|
| 413 |
-
#[ts(export)]
|
| 414 |
pub struct BrushRegionRequest {
|
| 415 |
pub data: Vec<u8>,
|
| 416 |
pub region: Region,
|
| 417 |
}
|
| 418 |
|
| 419 |
-
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema,
|
| 420 |
#[serde(rename_all = "camelCase")]
|
| 421 |
-
#[ts(export)]
|
| 422 |
pub struct InpaintRegionRequest {
|
| 423 |
pub region: Region,
|
| 424 |
}
|
|
|
|
| 1 |
use schemars::JsonSchema;
|
| 2 |
use serde::{Deserialize, Serialize};
|
| 3 |
+
use utoipa::ToSchema;
|
| 4 |
|
| 5 |
use crate::{Document, FontPrediction, TextBlock, TextShaderEffect, TextStrokeStyle, TextStyle};
|
| 6 |
|
| 7 |
+
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, JsonSchema, ToSchema)]
|
| 8 |
#[serde(rename_all = "camelCase")]
|
|
|
|
| 9 |
pub struct FontFaceInfo {
|
| 10 |
pub family_name: String,
|
| 11 |
pub post_script_name: String,
|
| 12 |
}
|
| 13 |
|
| 14 |
+
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, ToSchema)]
|
| 15 |
#[serde(rename_all = "camelCase")]
|
|
|
|
| 16 |
pub struct MetaInfo {
|
| 17 |
pub version: String,
|
| 18 |
pub ml_device: String,
|
| 19 |
}
|
| 20 |
|
| 21 |
+
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema, ToSchema)]
|
| 22 |
#[serde(rename_all = "camelCase")]
|
|
|
|
| 23 |
pub struct BootstrapConfig {
|
| 24 |
pub runtime: BootstrapPathConfig,
|
| 25 |
pub models: BootstrapPathConfig,
|
| 26 |
pub http: BootstrapHttpConfig,
|
| 27 |
}
|
| 28 |
|
| 29 |
+
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema, ToSchema)]
|
| 30 |
#[serde(rename_all = "camelCase")]
|
|
|
|
| 31 |
pub struct BootstrapPathConfig {
|
| 32 |
pub path: String,
|
| 33 |
}
|
| 34 |
|
| 35 |
+
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema, ToSchema)]
|
| 36 |
#[serde(rename_all = "camelCase")]
|
|
|
|
| 37 |
pub struct BootstrapHttpConfig {
|
| 38 |
pub proxy: Option<String>,
|
| 39 |
}
|
| 40 |
|
| 41 |
+
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, ToSchema)]
|
| 42 |
#[serde(rename_all = "camelCase")]
|
|
|
|
| 43 |
pub struct DocumentSummary {
|
| 44 |
pub id: String,
|
| 45 |
pub name: String,
|
|
|
|
| 70 |
}
|
| 71 |
}
|
| 72 |
|
| 73 |
+
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, ToSchema)]
|
| 74 |
#[serde(rename_all = "camelCase")]
|
|
|
|
| 75 |
pub struct TextBlockDetail {
|
| 76 |
pub id: String,
|
| 77 |
pub x: f32,
|
|
|
|
| 116 |
}
|
| 117 |
}
|
| 118 |
|
| 119 |
+
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
| 120 |
#[serde(rename_all = "camelCase")]
|
|
|
|
| 121 |
pub struct DocumentDetail {
|
| 122 |
pub id: String,
|
| 123 |
pub path: String,
|
|
|
|
| 126 |
pub height: u32,
|
| 127 |
pub revision: u64,
|
| 128 |
pub text_blocks: Vec<TextBlockDetail>,
|
| 129 |
+
#[schema(value_type = Vec<u8>)]
|
| 130 |
+
pub image: serde_bytes::ByteBuf,
|
| 131 |
+
#[serde(skip_serializing_if = "Option::is_none")]
|
| 132 |
+
#[schema(value_type = Option<Vec<u8>>)]
|
| 133 |
+
pub segment: Option<serde_bytes::ByteBuf>,
|
| 134 |
+
#[serde(skip_serializing_if = "Option::is_none")]
|
| 135 |
+
#[schema(value_type = Option<Vec<u8>>)]
|
| 136 |
+
pub inpainted: Option<serde_bytes::ByteBuf>,
|
| 137 |
+
#[serde(skip_serializing_if = "Option::is_none")]
|
| 138 |
+
#[schema(value_type = Option<Vec<u8>>)]
|
| 139 |
+
pub brush_layer: Option<serde_bytes::ByteBuf>,
|
| 140 |
+
#[serde(skip_serializing_if = "Option::is_none")]
|
| 141 |
+
#[schema(value_type = Option<Vec<u8>>)]
|
| 142 |
+
pub rendered: Option<serde_bytes::ByteBuf>,
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
|
| 146 |
+
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, ToSchema)]
|
| 147 |
+
#[serde(rename_all = "camelCase")]
|
| 148 |
+
pub struct TextBlockInput {
|
| 149 |
+
pub id: Option<String>,
|
| 150 |
+
pub x: f32,
|
| 151 |
+
pub y: f32,
|
| 152 |
+
pub width: f32,
|
| 153 |
+
pub height: f32,
|
| 154 |
+
pub text: Option<String>,
|
| 155 |
+
pub translation: Option<String>,
|
| 156 |
+
pub style: Option<TextStyle>,
|
| 157 |
}
|
| 158 |
|
| 159 |
+
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, ToSchema)]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 160 |
#[serde(rename_all = "camelCase")]
|
|
|
|
| 161 |
pub struct TextBlockPatch {
|
| 162 |
pub text: Option<String>,
|
| 163 |
pub translation: Option<String>,
|
|
|
|
| 168 |
pub style: Option<TextStyle>,
|
| 169 |
}
|
| 170 |
|
| 171 |
+
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, ToSchema)]
|
| 172 |
#[serde(rename_all = "camelCase")]
|
|
|
|
| 173 |
pub struct CreateTextBlock {
|
| 174 |
pub x: f32,
|
| 175 |
pub y: f32,
|
|
|
|
| 177 |
pub height: f32,
|
| 178 |
}
|
| 179 |
|
| 180 |
+
#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, ToSchema)]
|
| 181 |
#[serde(rename_all = "snake_case")]
|
|
|
|
| 182 |
pub enum ImportMode {
|
| 183 |
Replace,
|
| 184 |
Append,
|
| 185 |
}
|
| 186 |
|
| 187 |
+
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, ToSchema)]
|
| 188 |
#[serde(rename_all = "camelCase")]
|
|
|
|
| 189 |
pub struct ImportResult {
|
| 190 |
pub total_count: usize,
|
| 191 |
pub documents: Vec<DocumentSummary>,
|
| 192 |
}
|
| 193 |
|
| 194 |
+
#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, ToSchema)]
|
| 195 |
#[serde(rename_all = "snake_case")]
|
|
|
|
| 196 |
pub enum ExportLayer {
|
| 197 |
Rendered,
|
| 198 |
Inpainted,
|
| 199 |
}
|
| 200 |
|
| 201 |
+
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, ToSchema)]
|
| 202 |
#[serde(rename_all = "camelCase")]
|
|
|
|
| 203 |
pub struct ExportResult {
|
| 204 |
pub count: usize,
|
| 205 |
}
|
| 206 |
|
| 207 |
+
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, ToSchema)]
|
| 208 |
#[serde(rename_all = "camelCase")]
|
|
|
|
| 209 |
pub struct LlmModelInfo {
|
| 210 |
pub id: String,
|
| 211 |
pub languages: Vec<String>,
|
| 212 |
pub source: String,
|
| 213 |
}
|
| 214 |
|
| 215 |
+
#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, ToSchema)]
|
| 216 |
#[serde(rename_all = "snake_case")]
|
|
|
|
| 217 |
pub enum LlmStateStatus {
|
| 218 |
Empty,
|
| 219 |
Loading,
|
|
|
|
| 221 |
Failed,
|
| 222 |
}
|
| 223 |
|
| 224 |
+
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, ToSchema)]
|
| 225 |
#[serde(rename_all = "camelCase")]
|
|
|
|
| 226 |
pub struct LlmState {
|
| 227 |
pub status: LlmStateStatus,
|
| 228 |
pub model_id: Option<String>,
|
|
|
|
| 230 |
pub error: Option<String>,
|
| 231 |
}
|
| 232 |
|
| 233 |
+
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, ToSchema)]
|
| 234 |
#[serde(rename_all = "camelCase")]
|
|
|
|
| 235 |
pub struct LlmLoadRequest {
|
| 236 |
pub id: String,
|
| 237 |
pub api_key: Option<String>,
|
|
|
|
| 241 |
pub custom_system_prompt: Option<String>,
|
| 242 |
}
|
| 243 |
|
| 244 |
+
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, ToSchema)]
|
| 245 |
#[serde(rename_all = "camelCase")]
|
|
|
|
| 246 |
pub struct LlmPingRequest {
|
| 247 |
pub base_url: String,
|
| 248 |
pub api_key: Option<String>,
|
| 249 |
}
|
| 250 |
|
| 251 |
+
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, ToSchema)]
|
| 252 |
#[serde(rename_all = "camelCase")]
|
|
|
|
| 253 |
pub struct LlmPingResponse {
|
| 254 |
pub ok: bool,
|
| 255 |
pub models: Vec<String>,
|
|
|
|
| 257 |
pub error: Option<String>,
|
| 258 |
}
|
| 259 |
|
| 260 |
+
#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, ToSchema)]
|
| 261 |
#[serde(rename_all = "snake_case")]
|
|
|
|
| 262 |
pub enum JobStatus {
|
| 263 |
Running,
|
| 264 |
Completed,
|
|
|
|
| 266 |
Failed,
|
| 267 |
}
|
| 268 |
|
| 269 |
+
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, ToSchema)]
|
| 270 |
#[serde(rename_all = "camelCase")]
|
|
|
|
| 271 |
pub struct JobState {
|
| 272 |
pub id: String,
|
| 273 |
pub kind: String,
|
|
|
|
| 281 |
pub error: Option<String>,
|
| 282 |
}
|
| 283 |
|
| 284 |
+
#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, ToSchema)]
|
| 285 |
#[serde(rename_all = "snake_case")]
|
|
|
|
| 286 |
pub enum TransferStatus {
|
| 287 |
Started,
|
| 288 |
Downloading,
|
|
|
|
| 290 |
Failed,
|
| 291 |
}
|
| 292 |
|
| 293 |
+
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, ToSchema)]
|
| 294 |
#[serde(rename_all = "camelCase")]
|
|
|
|
| 295 |
pub struct DownloadState {
|
| 296 |
pub id: String,
|
| 297 |
pub filename: String,
|
|
|
|
| 301 |
pub error: Option<String>,
|
| 302 |
}
|
| 303 |
|
| 304 |
+
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, ToSchema)]
|
| 305 |
#[serde(rename_all = "camelCase")]
|
|
|
|
| 306 |
pub struct SnapshotEvent {
|
| 307 |
pub documents: Vec<DocumentSummary>,
|
| 308 |
pub llm: LlmState,
|
|
|
|
| 310 |
pub downloads: Vec<DownloadState>,
|
| 311 |
}
|
| 312 |
|
| 313 |
+
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, ToSchema)]
|
| 314 |
#[serde(rename_all = "camelCase")]
|
|
|
|
| 315 |
pub struct DocumentsChangedEvent {
|
| 316 |
pub documents: Vec<DocumentSummary>,
|
| 317 |
}
|
| 318 |
|
| 319 |
+
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, ToSchema)]
|
| 320 |
#[serde(rename_all = "camelCase")]
|
|
|
|
| 321 |
pub struct DocumentChangedEvent {
|
| 322 |
pub document_id: String,
|
| 323 |
pub revision: u64,
|
| 324 |
pub changed: Vec<String>,
|
| 325 |
}
|
| 326 |
|
| 327 |
+
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, ToSchema)]
|
| 328 |
#[serde(rename_all = "camelCase")]
|
|
|
|
| 329 |
pub struct ApiKeyValue {
|
| 330 |
pub api_key: String,
|
| 331 |
}
|
| 332 |
|
| 333 |
+
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, ToSchema)]
|
| 334 |
#[serde(rename_all = "camelCase")]
|
|
|
|
| 335 |
pub struct ApiKeyResponse {
|
| 336 |
pub api_key: Option<String>,
|
| 337 |
}
|
| 338 |
|
| 339 |
+
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, ToSchema)]
|
| 340 |
#[serde(rename_all = "camelCase")]
|
|
|
|
| 341 |
pub struct RenderRequest {
|
| 342 |
pub text_block_id: Option<String>,
|
| 343 |
pub shader_effect: Option<TextShaderEffect>,
|
|
|
|
| 345 |
pub font_family: Option<String>,
|
| 346 |
}
|
| 347 |
|
| 348 |
+
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, ToSchema)]
|
| 349 |
#[serde(rename_all = "camelCase")]
|
|
|
|
| 350 |
pub struct TranslateRequest {
|
| 351 |
pub text_block_id: Option<String>,
|
| 352 |
pub language: Option<String>,
|
| 353 |
}
|
| 354 |
|
| 355 |
+
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, ToSchema)]
|
| 356 |
#[serde(rename_all = "camelCase")]
|
|
|
|
| 357 |
pub struct PipelineJobRequest {
|
| 358 |
pub document_id: Option<String>,
|
| 359 |
pub llm_model_id: Option<String>,
|
|
|
|
| 368 |
pub font_family: Option<String>,
|
| 369 |
}
|
| 370 |
|
| 371 |
+
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, ToSchema)]
|
| 372 |
#[serde(rename_all = "camelCase")]
|
|
|
|
| 373 |
pub struct Region {
|
| 374 |
pub x: u32,
|
| 375 |
pub y: u32,
|
|
|
|
| 377 |
pub height: u32,
|
| 378 |
}
|
| 379 |
|
| 380 |
+
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, ToSchema)]
|
| 381 |
#[serde(rename_all = "camelCase")]
|
|
|
|
| 382 |
pub struct MaskRegionRequest {
|
| 383 |
pub data: Vec<u8>,
|
| 384 |
pub region: Option<Region>,
|
| 385 |
}
|
| 386 |
|
| 387 |
+
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, ToSchema)]
|
| 388 |
#[serde(rename_all = "camelCase")]
|
|
|
|
| 389 |
pub struct BrushRegionRequest {
|
| 390 |
pub data: Vec<u8>,
|
| 391 |
pub region: Region,
|
| 392 |
}
|
| 393 |
|
| 394 |
+
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, ToSchema)]
|
| 395 |
#[serde(rename_all = "camelCase")]
|
|
|
|
| 396 |
pub struct InpaintRegionRequest {
|
| 397 |
pub region: Region,
|
| 398 |
}
|
koharu-rpc/Cargo.toml
CHANGED
|
@@ -17,17 +17,22 @@ koharu-core = { workspace = true }
|
|
| 17 |
koharu-psd = { workspace = true }
|
| 18 |
koharu-runtime = { workspace = true }
|
| 19 |
anyhow = { workspace = true }
|
| 20 |
-
async-stream = { workspace = true }
|
| 21 |
axum = { workspace = true }
|
| 22 |
base64 = { workspace = true }
|
| 23 |
-
futures = { workspace = true }
|
| 24 |
image = { workspace = true }
|
| 25 |
imageproc = { workspace = true }
|
| 26 |
rmcp = { workspace = true }
|
| 27 |
rmp-serde = { workspace = true }
|
| 28 |
rmpv = { workspace = true }
|
| 29 |
serde = { workspace = true }
|
|
|
|
| 30 |
serde_json = { workspace = true }
|
| 31 |
tokio = { workspace = true }
|
| 32 |
tower-http = { workspace = true }
|
|
|
|
|
|
|
| 33 |
tracing = { workspace = true }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
koharu-psd = { workspace = true }
|
| 18 |
koharu-runtime = { workspace = true }
|
| 19 |
anyhow = { workspace = true }
|
|
|
|
| 20 |
axum = { workspace = true }
|
| 21 |
base64 = { workspace = true }
|
|
|
|
| 22 |
image = { workspace = true }
|
| 23 |
imageproc = { workspace = true }
|
| 24 |
rmcp = { workspace = true }
|
| 25 |
rmp-serde = { workspace = true }
|
| 26 |
rmpv = { workspace = true }
|
| 27 |
serde = { workspace = true }
|
| 28 |
+
serde_bytes = { workspace = true }
|
| 29 |
serde_json = { workspace = true }
|
| 30 |
tokio = { workspace = true }
|
| 31 |
tower-http = { workspace = true }
|
| 32 |
+
utoipa = { workspace = true }
|
| 33 |
+
utoipa-axum = { workspace = true }
|
| 34 |
tracing = { workspace = true }
|
| 35 |
+
|
| 36 |
+
[[bin]]
|
| 37 |
+
name = "openapi"
|
| 38 |
+
path = "src/bin/openapi.rs"
|
koharu-rpc/src/api.rs
CHANGED
|
@@ -1,20 +1,15 @@
|
|
| 1 |
-
use std::
|
| 2 |
|
| 3 |
use anyhow::Context;
|
| 4 |
-
use async_stream::stream;
|
| 5 |
use axum::{
|
| 6 |
-
Json,
|
| 7 |
body::Body,
|
| 8 |
extract::{DefaultBodyLimit, Multipart, Path, Query, State},
|
| 9 |
http::{
|
| 10 |
HeaderValue, StatusCode,
|
| 11 |
header::{CONTENT_DISPOSITION, CONTENT_TYPE},
|
| 12 |
},
|
| 13 |
-
response::{
|
| 14 |
-
IntoResponse, Response,
|
| 15 |
-
sse::{Event, KeepAlive, Sse},
|
| 16 |
-
},
|
| 17 |
-
routing::{delete, get, patch, post, put},
|
| 18 |
};
|
| 19 |
use image::ImageFormat;
|
| 20 |
use koharu_app::{
|
|
@@ -22,20 +17,20 @@ use koharu_app::{
|
|
| 22 |
state_tx::{self, ChangedField},
|
| 23 |
};
|
| 24 |
use koharu_core::{
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
RenderRequest, SerializableDynamicImage, TextBlock, TextBlockDetail, TextBlockPatch,
|
| 31 |
-
TranslateRequest, UpdateBrushLayerPayload, UpdateInpaintMaskPayload,
|
| 32 |
};
|
| 33 |
use koharu_psd::{PsdExportOptions, TextLayerMode};
|
| 34 |
-
use serde::Deserialize;
|
|
|
|
|
|
|
| 35 |
|
| 36 |
use crate::{
|
| 37 |
-
events::{ApiEvent, EventHub},
|
| 38 |
shared::{SharedState, get_resources},
|
|
|
|
| 39 |
};
|
| 40 |
|
| 41 |
const MAX_BODY_SIZE: usize = 1024 * 1024 * 1024;
|
|
@@ -43,7 +38,7 @@ const MAX_BODY_SIZE: usize = 1024 * 1024 * 1024;
|
|
| 43 |
#[derive(Clone)]
|
| 44 |
pub struct ApiState {
|
| 45 |
pub resources: SharedState,
|
| 46 |
-
pub
|
| 47 |
}
|
| 48 |
|
| 49 |
impl ApiState {
|
|
@@ -52,84 +47,58 @@ impl ApiState {
|
|
| 52 |
}
|
| 53 |
}
|
| 54 |
|
| 55 |
-
pub fn
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
.
|
| 60 |
-
.
|
| 61 |
-
.
|
| 62 |
-
.
|
| 63 |
-
.
|
| 64 |
-
.
|
| 65 |
-
.
|
| 66 |
-
.
|
| 67 |
-
.
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
)
|
| 71 |
-
.
|
| 72 |
-
.
|
| 73 |
-
.
|
| 74 |
-
.
|
| 75 |
-
.
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
)
|
| 79 |
-
.
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
)
|
| 83 |
-
.
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
.route(
|
| 92 |
-
"/documents/{document_id}/text-blocks",
|
| 93 |
-
post(create_text_block),
|
| 94 |
-
)
|
| 95 |
-
.route(
|
| 96 |
-
"/documents/{document_id}/text-blocks/{text_block_id}",
|
| 97 |
-
patch(patch_text_block).delete(delete_text_block),
|
| 98 |
-
)
|
| 99 |
-
.route("/documents/{document_id}/export", get(export_document))
|
| 100 |
-
.route(
|
| 101 |
-
"/documents/{document_id}/export/psd",
|
| 102 |
-
get(export_document_psd),
|
| 103 |
-
)
|
| 104 |
-
.route("/llm/models", get(list_llm_models))
|
| 105 |
-
.route("/llm/state", get(get_llm_state))
|
| 106 |
-
.route("/llm/load", post(load_llm))
|
| 107 |
-
.route("/llm/offload", post(offload_llm))
|
| 108 |
-
.route("/llm/ping", post(ping_llm))
|
| 109 |
-
.route(
|
| 110 |
-
"/providers/{provider}/api-key",
|
| 111 |
-
get(get_api_key).put(set_api_key),
|
| 112 |
-
)
|
| 113 |
-
.route("/jobs/pipeline", post(start_pipeline_job))
|
| 114 |
-
.route("/jobs/{job_id}", delete(cancel_pipeline_job))
|
| 115 |
-
.route("/exports", post(export_all))
|
| 116 |
-
.route("/events", get(events_stream))
|
| 117 |
.layer(DefaultBodyLimit::max(MAX_BODY_SIZE))
|
| 118 |
.with_state(state)
|
| 119 |
}
|
| 120 |
|
| 121 |
type ApiResult<T> = Result<T, ApiError>;
|
| 122 |
|
| 123 |
-
#[derive(Debug)]
|
| 124 |
pub struct ApiError {
|
| 125 |
-
status:
|
| 126 |
-
message: String,
|
| 127 |
}
|
| 128 |
|
| 129 |
impl ApiError {
|
| 130 |
fn new(status: StatusCode, message: impl Into<String>) -> Self {
|
| 131 |
Self {
|
| 132 |
-
status,
|
| 133 |
message: message.into(),
|
| 134 |
}
|
| 135 |
}
|
|
@@ -168,35 +137,75 @@ impl From<anyhow::Error> for ApiError {
|
|
| 168 |
|
| 169 |
impl IntoResponse for ApiError {
|
| 170 |
fn into_response(self) -> Response {
|
| 171 |
-
|
|
|
|
|
|
|
| 172 |
}
|
| 173 |
}
|
| 174 |
|
| 175 |
-
#[derive(Debug,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 176 |
#[serde(rename_all = "camelCase")]
|
| 177 |
struct ImportQuery {
|
| 178 |
mode: Option<koharu_core::ImportMode>,
|
| 179 |
}
|
| 180 |
|
| 181 |
-
#[derive(Debug, Deserialize)]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 182 |
#[serde(rename_all = "camelCase")]
|
| 183 |
-
struct
|
| 184 |
layer: Option<ExportLayer>,
|
| 185 |
}
|
| 186 |
|
| 187 |
-
#[derive(Debug, Deserialize)]
|
| 188 |
#[serde(rename_all = "camelCase")]
|
| 189 |
-
struct
|
| 190 |
-
|
| 191 |
-
openai_compatible_base_url: Option<String>,
|
| 192 |
}
|
| 193 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 194 |
async fn get_config(State(state): State<ApiState>) -> ApiResult<Json<BootstrapConfig>> {
|
| 195 |
let config = state.resources.get_config().map_err(ApiError::internal)?;
|
| 196 |
Ok(Json(config))
|
| 197 |
}
|
| 198 |
|
| 199 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 200 |
State(state): State<ApiState>,
|
| 201 |
Json(config): Json<BootstrapConfig>,
|
| 202 |
) -> ApiResult<Json<BootstrapConfig>> {
|
|
@@ -204,7 +213,18 @@ async fn put_config(
|
|
| 204 |
Ok(Json(saved))
|
| 205 |
}
|
| 206 |
|
| 207 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 208 |
match state.resources.initialize().await {
|
| 209 |
Ok(()) => Ok(StatusCode::NO_CONTENT),
|
| 210 |
Err(error) => {
|
|
@@ -218,6 +238,16 @@ async fn initialize_app(State(state): State<ApiState>) -> ApiResult<StatusCode>
|
|
| 218 |
}
|
| 219 |
}
|
| 220 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 221 |
async fn get_meta(State(state): State<ApiState>) -> ApiResult<Json<MetaInfo>> {
|
| 222 |
let resources = state.resources()?;
|
| 223 |
let device = operations::device(resources.clone()).await?;
|
|
@@ -227,7 +257,17 @@ async fn get_meta(State(state): State<ApiState>) -> ApiResult<Json<MetaInfo>> {
|
|
| 227 |
}))
|
| 228 |
}
|
| 229 |
|
| 230 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 231 |
let resources = state.resources()?;
|
| 232 |
let fonts = operations::list_font_families(resources)
|
| 233 |
.await
|
|
@@ -235,6 +275,20 @@ async fn get_fonts(State(state): State<ApiState>) -> ApiResult<Json<Vec<FontFace
|
|
| 235 |
Ok(Json(fonts))
|
| 236 |
}
|
| 237 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 238 |
async fn list_documents(State(state): State<ApiState>) -> ApiResult<Json<Vec<DocumentSummary>>> {
|
| 239 |
let resources = state.resources()?;
|
| 240 |
let guard = resources.state.read().await;
|
|
@@ -242,6 +296,18 @@ async fn list_documents(State(state): State<ApiState>) -> ApiResult<Json<Vec<Doc
|
|
| 242 |
Ok(Json(documents))
|
| 243 |
}
|
| 244 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 245 |
async fn get_document(
|
| 246 |
State(state): State<ApiState>,
|
| 247 |
Path(document_id): Path<String>,
|
|
@@ -253,13 +319,58 @@ async fn get_document(
|
|
| 253 |
.iter()
|
| 254 |
.find(|d| d.id == document_id)
|
| 255 |
.ok_or_else(|| ApiError::not_found("Document not found"))?;
|
| 256 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 257 |
}
|
| 258 |
|
| 259 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 260 |
State(state): State<ApiState>,
|
| 261 |
Path(document_id): Path<String>,
|
|
|
|
| 262 |
) -> ApiResult<Response> {
|
|
|
|
| 263 |
let resources = state.resources()?;
|
| 264 |
let guard = resources.state.read().await;
|
| 265 |
let doc = guard
|
|
@@ -268,29 +379,25 @@ async fn get_thumbnail(
|
|
| 268 |
.find(|d| d.id == document_id)
|
| 269 |
.ok_or_else(|| ApiError::not_found("Document not found"))?;
|
| 270 |
let source = doc.rendered.as_ref().unwrap_or(&doc.image);
|
| 271 |
-
let thumbnail = source.thumbnail(
|
| 272 |
let bytes = encode_webp(&thumbnail.into())?;
|
| 273 |
drop(guard);
|
| 274 |
Ok(binary_response(bytes, "image/webp", None))
|
| 275 |
}
|
| 276 |
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
drop(guard);
|
| 291 |
-
Ok(binary_response(bytes, "image/webp", None))
|
| 292 |
-
}
|
| 293 |
-
|
| 294 |
async fn import_documents(
|
| 295 |
State(state): State<ApiState>,
|
| 296 |
Query(query): Query<ImportQuery>,
|
|
@@ -312,7 +419,7 @@ async fn import_documents(
|
|
| 312 |
.bytes()
|
| 313 |
.await
|
| 314 |
.map_err(|error| ApiError::bad_request(error.to_string()))?;
|
| 315 |
-
files.push(FileEntry {
|
| 316 |
name: filename,
|
| 317 |
data: data.to_vec(),
|
| 318 |
});
|
|
@@ -322,7 +429,7 @@ async fn import_documents(
|
|
| 322 |
return Err(ApiError::bad_request("No files uploaded"));
|
| 323 |
}
|
| 324 |
|
| 325 |
-
let payload = OpenDocumentsPayload { files };
|
| 326 |
match query.mode.unwrap_or(koharu_core::ImportMode::Replace) {
|
| 327 |
koharu_core::ImportMode::Replace => {
|
| 328 |
operations::open_documents(resources.clone(), payload).await?;
|
|
@@ -344,36 +451,89 @@ async fn import_documents(
|
|
| 344 |
}))
|
| 345 |
}
|
| 346 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 347 |
async fn detect_document(
|
| 348 |
State(state): State<ApiState>,
|
| 349 |
Path(document_id): Path<String>,
|
| 350 |
) -> ApiResult<StatusCode> {
|
| 351 |
let resources = state.resources()?;
|
| 352 |
let (index, _) = find_document(&resources, &document_id).await?;
|
| 353 |
-
operations::detect(resources, IndexPayload { index }).await?;
|
| 354 |
Ok(StatusCode::NO_CONTENT)
|
| 355 |
}
|
| 356 |
|
| 357 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 358 |
State(state): State<ApiState>,
|
| 359 |
Path(document_id): Path<String>,
|
| 360 |
) -> ApiResult<StatusCode> {
|
| 361 |
let resources = state.resources()?;
|
| 362 |
let (index, _) = find_document(&resources, &document_id).await?;
|
| 363 |
-
operations::ocr(resources, IndexPayload { index }).await?;
|
| 364 |
Ok(StatusCode::NO_CONTENT)
|
| 365 |
}
|
| 366 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 367 |
async fn inpaint_document(
|
| 368 |
State(state): State<ApiState>,
|
| 369 |
Path(document_id): Path<String>,
|
| 370 |
) -> ApiResult<StatusCode> {
|
| 371 |
let resources = state.resources()?;
|
| 372 |
let (index, _) = find_document(&resources, &document_id).await?;
|
| 373 |
-
operations::inpaint(resources, IndexPayload { index }).await?;
|
| 374 |
Ok(StatusCode::NO_CONTENT)
|
| 375 |
}
|
| 376 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 377 |
async fn render_document(
|
| 378 |
State(state): State<ApiState>,
|
| 379 |
Path(document_id): Path<String>,
|
|
@@ -389,7 +549,7 @@ async fn render_document(
|
|
| 389 |
|
| 390 |
operations::render(
|
| 391 |
resources,
|
| 392 |
-
RenderPayload {
|
| 393 |
index,
|
| 394 |
text_block_index,
|
| 395 |
shader_effect: request.shader_effect,
|
|
@@ -402,6 +562,19 @@ async fn render_document(
|
|
| 402 |
Ok(StatusCode::NO_CONTENT)
|
| 403 |
}
|
| 404 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 405 |
async fn translate_document(
|
| 406 |
State(state): State<ApiState>,
|
| 407 |
Path(document_id): Path<String>,
|
|
@@ -428,7 +601,24 @@ async fn translate_document(
|
|
| 428 |
Ok(StatusCode::NO_CONTENT)
|
| 429 |
}
|
| 430 |
|
| 431 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 432 |
State(state): State<ApiState>,
|
| 433 |
Path(document_id): Path<String>,
|
| 434 |
Json(request): Json<MaskRegionRequest>,
|
|
@@ -437,7 +627,7 @@ async fn update_mask_region(
|
|
| 437 |
let (index, _) = find_document(&resources, &document_id).await?;
|
| 438 |
operations::update_inpaint_mask(
|
| 439 |
resources,
|
| 440 |
-
UpdateInpaintMaskPayload {
|
| 441 |
index,
|
| 442 |
mask: request.data,
|
| 443 |
region: request.region.map(to_inpaint_region),
|
|
@@ -447,7 +637,19 @@ async fn update_mask_region(
|
|
| 447 |
Ok(StatusCode::NO_CONTENT)
|
| 448 |
}
|
| 449 |
|
| 450 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 451 |
State(state): State<ApiState>,
|
| 452 |
Path(document_id): Path<String>,
|
| 453 |
Json(request): Json<koharu_core::BrushRegionRequest>,
|
|
@@ -456,7 +658,7 @@ async fn update_brush_region(
|
|
| 456 |
let (index, _) = find_document(&resources, &document_id).await?;
|
| 457 |
operations::update_brush_layer(
|
| 458 |
resources,
|
| 459 |
-
UpdateBrushLayerPayload {
|
| 460 |
index,
|
| 461 |
patch: request.data,
|
| 462 |
region: to_inpaint_region(request.region),
|
|
@@ -466,6 +668,18 @@ async fn update_brush_region(
|
|
| 466 |
Ok(StatusCode::NO_CONTENT)
|
| 467 |
}
|
| 468 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 469 |
async fn inpaint_region(
|
| 470 |
State(state): State<ApiState>,
|
| 471 |
Path(document_id): Path<String>,
|
|
@@ -475,7 +689,7 @@ async fn inpaint_region(
|
|
| 475 |
let (index, _) = find_document(&resources, &document_id).await?;
|
| 476 |
operations::inpaint_partial(
|
| 477 |
resources,
|
| 478 |
-
InpaintPartialPayload {
|
| 479 |
index,
|
| 480 |
region: to_inpaint_region(request.region),
|
| 481 |
},
|
|
@@ -484,6 +698,23 @@ async fn inpaint_region(
|
|
| 484 |
Ok(StatusCode::NO_CONTENT)
|
| 485 |
}
|
| 486 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 487 |
async fn create_text_block(
|
| 488 |
State(state): State<ApiState>,
|
| 489 |
Path(document_id): Path<String>,
|
|
@@ -519,6 +750,131 @@ async fn create_text_block(
|
|
| 519 |
Ok(Json(detail))
|
| 520 |
}
|
| 521 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 522 |
async fn patch_text_block(
|
| 523 |
State(state): State<ApiState>,
|
| 524 |
Path((document_id, text_block_id)): Path<(String, String)>,
|
|
@@ -546,6 +902,21 @@ async fn patch_text_block(
|
|
| 546 |
Ok(Json(detail))
|
| 547 |
}
|
| 548 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 549 |
async fn delete_text_block(
|
| 550 |
State(state): State<ApiState>,
|
| 551 |
Path((document_id, text_block_id)): Path<(String, String)>,
|
|
@@ -572,6 +943,21 @@ async fn delete_text_block(
|
|
| 572 |
Ok(StatusCode::NO_CONTENT)
|
| 573 |
}
|
| 574 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 575 |
async fn list_llm_models(
|
| 576 |
State(state): State<ApiState>,
|
| 577 |
Query(query): Query<LlmModelsQuery>,
|
|
@@ -595,11 +981,33 @@ async fn list_llm_models(
|
|
| 595 |
Ok(Json(models))
|
| 596 |
}
|
| 597 |
|
| 598 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 599 |
let resources = state.resources()?;
|
| 600 |
Ok(Json(resources.llm.snapshot().await))
|
| 601 |
}
|
| 602 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 603 |
async fn load_llm(
|
| 604 |
State(state): State<ApiState>,
|
| 605 |
Json(request): Json<LlmLoadRequest>,
|
|
@@ -607,7 +1015,7 @@ async fn load_llm(
|
|
| 607 |
let resources = state.resources()?;
|
| 608 |
operations::llm_load(
|
| 609 |
resources.clone(),
|
| 610 |
-
LlmLoadPayload {
|
| 611 |
id: request.id,
|
| 612 |
api_key: request.api_key,
|
| 613 |
base_url: request.base_url,
|
|
@@ -620,13 +1028,33 @@ async fn load_llm(
|
|
| 620 |
Ok(Json(resources.llm.snapshot().await))
|
| 621 |
}
|
| 622 |
|
| 623 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 624 |
let resources = state.resources()?;
|
| 625 |
operations::llm_offload(resources.clone()).await?;
|
| 626 |
Ok(Json(resources.llm.snapshot().await))
|
| 627 |
}
|
| 628 |
|
| 629 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 630 |
State(state): State<ApiState>,
|
| 631 |
Json(request): Json<LlmPingRequest>,
|
| 632 |
) -> ApiResult<Json<LlmPingResponse>> {
|
|
@@ -652,17 +1080,45 @@ async fn ping_llm(
|
|
| 652 |
}
|
| 653 |
}
|
| 654 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 655 |
async fn get_api_key(
|
| 656 |
State(state): State<ApiState>,
|
| 657 |
Path(provider): Path<String>,
|
| 658 |
) -> ApiResult<Json<ApiKeyResponse>> {
|
| 659 |
let resources = state.resources()?;
|
| 660 |
-
let result =
|
|
|
|
| 661 |
Ok(Json(ApiKeyResponse {
|
| 662 |
api_key: result.api_key,
|
| 663 |
}))
|
| 664 |
}
|
| 665 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 666 |
async fn set_api_key(
|
| 667 |
State(state): State<ApiState>,
|
| 668 |
Path(provider): Path<String>,
|
|
@@ -671,7 +1127,7 @@ async fn set_api_key(
|
|
| 671 |
let resources = state.resources()?;
|
| 672 |
operations::set_api_key(
|
| 673 |
resources,
|
| 674 |
-
ApiKeySetPayload {
|
| 675 |
provider,
|
| 676 |
api_key: request.api_key,
|
| 677 |
},
|
|
@@ -680,7 +1136,22 @@ async fn set_api_key(
|
|
| 680 |
Ok(StatusCode::NO_CONTENT)
|
| 681 |
}
|
| 682 |
|
| 683 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 684 |
State(state): State<ApiState>,
|
| 685 |
Json(request): Json<PipelineJobRequest>,
|
| 686 |
) -> ApiResult<Json<JobState>> {
|
|
@@ -725,12 +1196,60 @@ async fn start_pipeline_job(
|
|
| 725 |
overall_percent: 0,
|
| 726 |
error: None,
|
| 727 |
};
|
| 728 |
-
state.
|
| 729 |
|
| 730 |
Ok(Json(job))
|
| 731 |
}
|
| 732 |
|
| 733 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 734 |
State(state): State<ApiState>,
|
| 735 |
Path(job_id): Path<String>,
|
| 736 |
) -> ApiResult<StatusCode> {
|
|
@@ -750,13 +1269,61 @@ async fn cancel_pipeline_job(
|
|
| 750 |
Ok(StatusCode::NO_CONTENT)
|
| 751 |
}
|
| 752 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 753 |
async fn export_document(
|
| 754 |
State(state): State<ApiState>,
|
| 755 |
-
Path(document_id): Path<String>,
|
| 756 |
-
Query(query): Query<
|
| 757 |
) -> ApiResult<Response> {
|
| 758 |
let resources = state.resources()?;
|
| 759 |
let (_, document) = find_document(&resources, &document_id).await?;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 760 |
let layer = query.layer.unwrap_or(ExportLayer::Rendered);
|
| 761 |
let (image, filename) = export_target(&document, layer)?;
|
| 762 |
let ext = document
|
|
@@ -770,83 +1337,29 @@ async fn export_document(
|
|
| 770 |
Ok(binary_response(data, content_type, Some(filename)))
|
| 771 |
}
|
| 772 |
|
| 773 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 774 |
State(state): State<ApiState>,
|
| 775 |
-
|
| 776 |
-
) -> ApiResult<Response> {
|
| 777 |
-
let resources = state.resources()?;
|
| 778 |
-
let (_, document) = find_document(&resources, &document_id).await?;
|
| 779 |
-
let data = koharu_psd::export_document(&document, &app_psd_export_options())
|
| 780 |
-
.map_err(|error| ApiError::bad_request(error.to_string()))?;
|
| 781 |
-
Ok(binary_response(
|
| 782 |
-
data,
|
| 783 |
-
"image/vnd.adobe.photoshop",
|
| 784 |
-
Some(psd_export_filename(&document)),
|
| 785 |
-
))
|
| 786 |
-
}
|
| 787 |
-
|
| 788 |
-
async fn export_all(
|
| 789 |
-
State(state): State<ApiState>,
|
| 790 |
-
Query(query): Query<LayerQuery>,
|
| 791 |
) -> ApiResult<Json<ExportResult>> {
|
| 792 |
let resources = state.resources()?;
|
| 793 |
-
let count = match
|
| 794 |
ExportLayer::Rendered => operations::export_all_rendered(resources).await?,
|
| 795 |
ExportLayer::Inpainted => operations::export_all_inpainted(resources).await?,
|
| 796 |
};
|
| 797 |
Ok(Json(ExportResult { count }))
|
| 798 |
}
|
| 799 |
|
| 800 |
-
async fn events_stream(
|
| 801 |
-
State(state): State<ApiState>,
|
| 802 |
-
) -> ApiResult<Sse<impl futures::Stream<Item = Result<Event, Infallible>>>> {
|
| 803 |
-
let events = state.events.clone();
|
| 804 |
-
let snapshot = events.snapshot().await?;
|
| 805 |
-
let mut rx = events.subscribe();
|
| 806 |
-
|
| 807 |
-
let stream = stream! {
|
| 808 |
-
yield Ok(sse_event("snapshot", &snapshot));
|
| 809 |
-
loop {
|
| 810 |
-
match rx.recv().await {
|
| 811 |
-
Ok(event) => {
|
| 812 |
-
if let Some(event) = api_event_to_sse(event) {
|
| 813 |
-
yield Ok(event);
|
| 814 |
-
}
|
| 815 |
-
}
|
| 816 |
-
Err(tokio::sync::broadcast::error::RecvError::Lagged(n)) => {
|
| 817 |
-
tracing::warn!("SSE client lagged behind by {n} events, re-sending snapshot");
|
| 818 |
-
match events.snapshot().await {
|
| 819 |
-
Ok(snap) => yield Ok(sse_event("snapshot", &snap)),
|
| 820 |
-
Err(e) => tracing::warn!("Failed to build resync snapshot: {e}"),
|
| 821 |
-
}
|
| 822 |
-
}
|
| 823 |
-
Err(tokio::sync::broadcast::error::RecvError::Closed) => break,
|
| 824 |
-
}
|
| 825 |
-
}
|
| 826 |
-
};
|
| 827 |
-
|
| 828 |
-
Ok(Sse::new(stream).keep_alive(
|
| 829 |
-
KeepAlive::new()
|
| 830 |
-
.interval(Duration::from_secs(15))
|
| 831 |
-
.text("keep-alive"),
|
| 832 |
-
))
|
| 833 |
-
}
|
| 834 |
-
|
| 835 |
-
fn api_event_to_sse(event: ApiEvent) -> Option<Event> {
|
| 836 |
-
match event {
|
| 837 |
-
ApiEvent::DocumentsChanged(payload) => Some(sse_event("documents.changed", &payload)),
|
| 838 |
-
ApiEvent::DocumentChanged(payload) => Some(sse_event("document.changed", &payload)),
|
| 839 |
-
ApiEvent::JobChanged(payload) => Some(sse_event("job.changed", &payload)),
|
| 840 |
-
ApiEvent::DownloadChanged(payload) => Some(sse_event("download.changed", &payload)),
|
| 841 |
-
ApiEvent::LlmChanged(payload) => Some(sse_event("llm.changed", &payload)),
|
| 842 |
-
}
|
| 843 |
-
}
|
| 844 |
-
|
| 845 |
-
fn sse_event<T: serde::Serialize>(name: &str, payload: &T) -> Event {
|
| 846 |
-
let data = serde_json::to_string(payload).unwrap_or_else(|_| "{}".to_string());
|
| 847 |
-
Event::default().event(name).data(data)
|
| 848 |
-
}
|
| 849 |
-
|
| 850 |
async fn find_document(
|
| 851 |
resources: &AppResources,
|
| 852 |
document_id: &str,
|
|
@@ -868,37 +1381,11 @@ fn find_text_block_index(document: &Document, text_block_id: &str) -> ApiResult<
|
|
| 868 |
.ok_or_else(|| ApiError::not_found(format!("Text block not found: {text_block_id}")))
|
| 869 |
}
|
| 870 |
|
| 871 |
-
fn
|
| 872 |
-
document: &'a Document,
|
| 873 |
-
layer: &str,
|
| 874 |
-
) -> ApiResult<&'a SerializableDynamicImage> {
|
| 875 |
-
match layer {
|
| 876 |
-
"original" => Ok(&document.image),
|
| 877 |
-
"segment" => document
|
| 878 |
-
.segment
|
| 879 |
-
.as_ref()
|
| 880 |
-
.ok_or_else(|| ApiError::not_found("No segment layer available")),
|
| 881 |
-
"inpainted" => document
|
| 882 |
-
.inpainted
|
| 883 |
-
.as_ref()
|
| 884 |
-
.ok_or_else(|| ApiError::not_found("No inpainted layer available")),
|
| 885 |
-
"rendered" => document
|
| 886 |
-
.rendered
|
| 887 |
-
.as_ref()
|
| 888 |
-
.ok_or_else(|| ApiError::not_found("No rendered layer available")),
|
| 889 |
-
"brush" => document
|
| 890 |
-
.brush_layer
|
| 891 |
-
.as_ref()
|
| 892 |
-
.ok_or_else(|| ApiError::not_found("No brush layer available")),
|
| 893 |
-
other => Err(ApiError::bad_request(format!("Unknown layer: {other}"))),
|
| 894 |
-
}
|
| 895 |
-
}
|
| 896 |
-
|
| 897 |
-
fn encode_webp(image: &SerializableDynamicImage) -> ApiResult<Vec<u8>> {
|
| 898 |
encode_image(image, "webp")
|
| 899 |
}
|
| 900 |
|
| 901 |
-
fn encode_image(image: &SerializableDynamicImage, ext: &str) -> ApiResult<Vec<u8>> {
|
| 902 |
let format = ImageFormat::from_extension(ext).unwrap_or(ImageFormat::Jpeg);
|
| 903 |
let mut cursor = Cursor::new(Vec::new());
|
| 904 |
image
|
|
@@ -934,7 +1421,7 @@ fn mime_from_ext(ext: &str) -> &'static str {
|
|
| 934 |
fn export_target(
|
| 935 |
document: &Document,
|
| 936 |
layer: ExportLayer,
|
| 937 |
-
) -> ApiResult<(&SerializableDynamicImage, String)> {
|
| 938 |
let ext = document
|
| 939 |
.path
|
| 940 |
.extension()
|
|
|
|
| 1 |
+
use std::io::Cursor;
|
| 2 |
|
| 3 |
use anyhow::Context;
|
|
|
|
| 4 |
use axum::{
|
| 5 |
+
Json,
|
| 6 |
body::Body,
|
| 7 |
extract::{DefaultBodyLimit, Multipart, Path, Query, State},
|
| 8 |
http::{
|
| 9 |
HeaderValue, StatusCode,
|
| 10 |
header::{CONTENT_DISPOSITION, CONTENT_TYPE},
|
| 11 |
},
|
| 12 |
+
response::{IntoResponse, Response},
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
};
|
| 14 |
use image::ImageFormat;
|
| 15 |
use koharu_app::{
|
|
|
|
| 17 |
state_tx::{self, ChangedField},
|
| 18 |
};
|
| 19 |
use koharu_core::{
|
| 20 |
+
ApiKeyResponse, ApiKeyValue, BootstrapConfig, CreateTextBlock, Document, DocumentDetail,
|
| 21 |
+
DocumentSummary, DownloadState, ExportLayer, ExportResult, FontFaceInfo, InpaintRegion,
|
| 22 |
+
JobState, JobStatus, LlmLoadRequest, LlmModelInfo, LlmPingRequest, LlmPingResponse,
|
| 23 |
+
MaskRegionRequest, MetaInfo, PipelineJobRequest, Region, RenderRequest, TextBlock,
|
| 24 |
+
TextBlockDetail, TextBlockInput, TextBlockPatch, TranslateRequest,
|
|
|
|
|
|
|
| 25 |
};
|
| 26 |
use koharu_psd::{PsdExportOptions, TextLayerMode};
|
| 27 |
+
use serde::{Deserialize, Serialize};
|
| 28 |
+
use utoipa::IntoParams;
|
| 29 |
+
use utoipa_axum::{router::OpenApiRouter, routes};
|
| 30 |
|
| 31 |
use crate::{
|
|
|
|
| 32 |
shared::{SharedState, get_resources},
|
| 33 |
+
tracker::Tracker,
|
| 34 |
};
|
| 35 |
|
| 36 |
const MAX_BODY_SIZE: usize = 1024 * 1024 * 1024;
|
|
|
|
| 38 |
#[derive(Clone)]
|
| 39 |
pub struct ApiState {
|
| 40 |
pub resources: SharedState,
|
| 41 |
+
pub tracker: Tracker,
|
| 42 |
}
|
| 43 |
|
| 44 |
impl ApiState {
|
|
|
|
| 47 |
}
|
| 48 |
}
|
| 49 |
|
| 50 |
+
pub fn api() -> (axum::Router<ApiState>, utoipa::openapi::OpenApi) {
|
| 51 |
+
OpenApiRouter::default()
|
| 52 |
+
.routes(routes!(list_documents, import_documents))
|
| 53 |
+
.routes(routes!(get_document))
|
| 54 |
+
.routes(routes!(get_document_thumbnail))
|
| 55 |
+
.routes(routes!(detect_document))
|
| 56 |
+
.routes(routes!(recognize_document))
|
| 57 |
+
.routes(routes!(inpaint_document))
|
| 58 |
+
.routes(routes!(render_document))
|
| 59 |
+
.routes(routes!(translate_document))
|
| 60 |
+
.routes(routes!(update_mask))
|
| 61 |
+
.routes(routes!(update_brush_layer))
|
| 62 |
+
.routes(routes!(inpaint_region))
|
| 63 |
+
.routes(routes!(create_text_block, put_text_blocks))
|
| 64 |
+
.routes(routes!(patch_text_block, delete_text_block))
|
| 65 |
+
.routes(routes!(export_document))
|
| 66 |
+
.routes(routes!(batch_export))
|
| 67 |
+
.routes(routes!(get_llm, load_llm, unload_llm))
|
| 68 |
+
.routes(routes!(list_llm_models))
|
| 69 |
+
.routes(routes!(check_llm_health))
|
| 70 |
+
.routes(routes!(get_api_key, set_api_key))
|
| 71 |
+
.routes(routes!(start_pipeline))
|
| 72 |
+
.routes(routes!(list_jobs))
|
| 73 |
+
.routes(routes!(get_job, cancel_job))
|
| 74 |
+
.routes(routes!(list_downloads))
|
| 75 |
+
.routes(routes!(get_meta))
|
| 76 |
+
.routes(routes!(list_fonts))
|
| 77 |
+
.routes(routes!(get_config, update_config))
|
| 78 |
+
.routes(routes!(initialize))
|
| 79 |
+
.split_for_parts()
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
pub fn router(resources: SharedState, tracker: Tracker) -> axum::Router {
|
| 83 |
+
let state = ApiState { resources, tracker };
|
| 84 |
+
let (router, _) = api();
|
| 85 |
+
router
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 86 |
.layer(DefaultBodyLimit::max(MAX_BODY_SIZE))
|
| 87 |
.with_state(state)
|
| 88 |
}
|
| 89 |
|
| 90 |
type ApiResult<T> = Result<T, ApiError>;
|
| 91 |
|
| 92 |
+
#[derive(Debug, Serialize, utoipa::ToSchema)]
|
| 93 |
pub struct ApiError {
|
| 94 |
+
pub status: u16,
|
| 95 |
+
pub message: String,
|
| 96 |
}
|
| 97 |
|
| 98 |
impl ApiError {
|
| 99 |
fn new(status: StatusCode, message: impl Into<String>) -> Self {
|
| 100 |
Self {
|
| 101 |
+
status: status.as_u16(),
|
| 102 |
message: message.into(),
|
| 103 |
}
|
| 104 |
}
|
|
|
|
| 137 |
|
| 138 |
impl IntoResponse for ApiError {
|
| 139 |
fn into_response(self) -> Response {
|
| 140 |
+
let status =
|
| 141 |
+
StatusCode::from_u16(self.status).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
|
| 142 |
+
(status, Json(self)).into_response()
|
| 143 |
}
|
| 144 |
}
|
| 145 |
|
| 146 |
+
#[derive(Debug, utoipa::ToSchema)]
|
| 147 |
+
#[allow(dead_code)]
|
| 148 |
+
struct MultipartUpload {
|
| 149 |
+
#[schema(value_type = Vec<String>, format = Binary)]
|
| 150 |
+
files: Vec<Vec<u8>>,
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
#[derive(Debug, Deserialize, IntoParams)]
|
| 154 |
#[serde(rename_all = "camelCase")]
|
| 155 |
struct ImportQuery {
|
| 156 |
mode: Option<koharu_core::ImportMode>,
|
| 157 |
}
|
| 158 |
|
| 159 |
+
#[derive(Debug, Deserialize, IntoParams)]
|
| 160 |
+
#[serde(rename_all = "camelCase")]
|
| 161 |
+
struct LlmModelsQuery {
|
| 162 |
+
language: Option<String>,
|
| 163 |
+
openai_compatible_base_url: Option<String>,
|
| 164 |
+
}
|
| 165 |
+
|
| 166 |
+
#[derive(Debug, Deserialize, IntoParams)]
|
| 167 |
#[serde(rename_all = "camelCase")]
|
| 168 |
+
struct ExportQuery {
|
| 169 |
layer: Option<ExportLayer>,
|
| 170 |
}
|
| 171 |
|
| 172 |
+
#[derive(Debug, Deserialize, utoipa::ToSchema)]
|
| 173 |
#[serde(rename_all = "camelCase")]
|
| 174 |
+
struct ExportBatchRequest {
|
| 175 |
+
layer: Option<ExportLayer>,
|
|
|
|
| 176 |
}
|
| 177 |
|
| 178 |
+
// ---------------------------------------------------------------------------
|
| 179 |
+
// System
|
| 180 |
+
// ---------------------------------------------------------------------------
|
| 181 |
+
|
| 182 |
+
#[utoipa::path(
|
| 183 |
+
get,
|
| 184 |
+
path = "/config",
|
| 185 |
+
operation_id = "getConfig",
|
| 186 |
+
tag = "system",
|
| 187 |
+
responses(
|
| 188 |
+
(status = 200, body = BootstrapConfig),
|
| 189 |
+
(status = 503, body = ApiError),
|
| 190 |
+
),
|
| 191 |
+
)]
|
| 192 |
async fn get_config(State(state): State<ApiState>) -> ApiResult<Json<BootstrapConfig>> {
|
| 193 |
let config = state.resources.get_config().map_err(ApiError::internal)?;
|
| 194 |
Ok(Json(config))
|
| 195 |
}
|
| 196 |
|
| 197 |
+
#[utoipa::path(
|
| 198 |
+
put,
|
| 199 |
+
path = "/config",
|
| 200 |
+
operation_id = "updateConfig",
|
| 201 |
+
tag = "system",
|
| 202 |
+
request_body = BootstrapConfig,
|
| 203 |
+
responses(
|
| 204 |
+
(status = 200, body = BootstrapConfig),
|
| 205 |
+
(status = 400, body = ApiError),
|
| 206 |
+
),
|
| 207 |
+
)]
|
| 208 |
+
async fn update_config(
|
| 209 |
State(state): State<ApiState>,
|
| 210 |
Json(config): Json<BootstrapConfig>,
|
| 211 |
) -> ApiResult<Json<BootstrapConfig>> {
|
|
|
|
| 213 |
Ok(Json(saved))
|
| 214 |
}
|
| 215 |
|
| 216 |
+
#[utoipa::path(
|
| 217 |
+
post,
|
| 218 |
+
path = "/initialize",
|
| 219 |
+
operation_id = "initialize",
|
| 220 |
+
tag = "system",
|
| 221 |
+
responses(
|
| 222 |
+
(status = 204),
|
| 223 |
+
(status = 409, body = ApiError),
|
| 224 |
+
(status = 500, body = ApiError),
|
| 225 |
+
),
|
| 226 |
+
)]
|
| 227 |
+
async fn initialize(State(state): State<ApiState>) -> ApiResult<StatusCode> {
|
| 228 |
match state.resources.initialize().await {
|
| 229 |
Ok(()) => Ok(StatusCode::NO_CONTENT),
|
| 230 |
Err(error) => {
|
|
|
|
| 238 |
}
|
| 239 |
}
|
| 240 |
|
| 241 |
+
#[utoipa::path(
|
| 242 |
+
get,
|
| 243 |
+
path = "/meta",
|
| 244 |
+
operation_id = "getMeta",
|
| 245 |
+
tag = "system",
|
| 246 |
+
responses(
|
| 247 |
+
(status = 200, body = MetaInfo),
|
| 248 |
+
(status = 503, body = ApiError),
|
| 249 |
+
),
|
| 250 |
+
)]
|
| 251 |
async fn get_meta(State(state): State<ApiState>) -> ApiResult<Json<MetaInfo>> {
|
| 252 |
let resources = state.resources()?;
|
| 253 |
let device = operations::device(resources.clone()).await?;
|
|
|
|
| 257 |
}))
|
| 258 |
}
|
| 259 |
|
| 260 |
+
#[utoipa::path(
|
| 261 |
+
get,
|
| 262 |
+
path = "/fonts",
|
| 263 |
+
operation_id = "listFonts",
|
| 264 |
+
tag = "system",
|
| 265 |
+
responses(
|
| 266 |
+
(status = 200, body = Vec<FontFaceInfo>),
|
| 267 |
+
(status = 503, body = ApiError),
|
| 268 |
+
),
|
| 269 |
+
)]
|
| 270 |
+
async fn list_fonts(State(state): State<ApiState>) -> ApiResult<Json<Vec<FontFaceInfo>>> {
|
| 271 |
let resources = state.resources()?;
|
| 272 |
let fonts = operations::list_font_families(resources)
|
| 273 |
.await
|
|
|
|
| 275 |
Ok(Json(fonts))
|
| 276 |
}
|
| 277 |
|
| 278 |
+
// ---------------------------------------------------------------------------
|
| 279 |
+
// Documents
|
| 280 |
+
// ---------------------------------------------------------------------------
|
| 281 |
+
|
| 282 |
+
#[utoipa::path(
|
| 283 |
+
get,
|
| 284 |
+
path = "/documents",
|
| 285 |
+
operation_id = "listDocuments",
|
| 286 |
+
tag = "documents",
|
| 287 |
+
responses(
|
| 288 |
+
(status = 200, body = Vec<DocumentSummary>),
|
| 289 |
+
(status = 503, body = ApiError),
|
| 290 |
+
),
|
| 291 |
+
)]
|
| 292 |
async fn list_documents(State(state): State<ApiState>) -> ApiResult<Json<Vec<DocumentSummary>>> {
|
| 293 |
let resources = state.resources()?;
|
| 294 |
let guard = resources.state.read().await;
|
|
|
|
| 296 |
Ok(Json(documents))
|
| 297 |
}
|
| 298 |
|
| 299 |
+
#[utoipa::path(
|
| 300 |
+
get,
|
| 301 |
+
path = "/documents/{document_id}",
|
| 302 |
+
operation_id = "getDocument",
|
| 303 |
+
tag = "documents",
|
| 304 |
+
params(("document_id" = String, Path,)),
|
| 305 |
+
responses(
|
| 306 |
+
(status = 200, body = DocumentDetail),
|
| 307 |
+
(status = 404, body = ApiError),
|
| 308 |
+
(status = 503, body = ApiError),
|
| 309 |
+
),
|
| 310 |
+
)]
|
| 311 |
async fn get_document(
|
| 312 |
State(state): State<ApiState>,
|
| 313 |
Path(document_id): Path<String>,
|
|
|
|
| 319 |
.iter()
|
| 320 |
.find(|d| d.id == document_id)
|
| 321 |
.ok_or_else(|| ApiError::not_found("Document not found"))?;
|
| 322 |
+
|
| 323 |
+
let image = serde_bytes::ByteBuf::from(encode_webp(&doc.image)?);
|
| 324 |
+
let segment = doc.segment.as_ref().map(|s| encode_webp(s).map(serde_bytes::ByteBuf::from)).transpose()?;
|
| 325 |
+
let inpainted = doc.inpainted.as_ref().map(|s| encode_webp(s).map(serde_bytes::ByteBuf::from)).transpose()?;
|
| 326 |
+
let brush_layer = doc.brush_layer.as_ref().map(|s| encode_webp(s).map(serde_bytes::ByteBuf::from)).transpose()?;
|
| 327 |
+
let rendered = doc.rendered.as_ref().map(|s| encode_webp(s).map(serde_bytes::ByteBuf::from)).transpose()?;
|
| 328 |
+
|
| 329 |
+
let text_blocks = doc.text_blocks.iter().map(koharu_core::TextBlockDetail::from).collect();
|
| 330 |
+
|
| 331 |
+
let detail = DocumentDetail {
|
| 332 |
+
id: doc.id.clone(),
|
| 333 |
+
path: doc.path.to_string_lossy().to_string(),
|
| 334 |
+
name: doc.name.clone(),
|
| 335 |
+
width: doc.width,
|
| 336 |
+
height: doc.height,
|
| 337 |
+
revision: doc.revision,
|
| 338 |
+
text_blocks,
|
| 339 |
+
image,
|
| 340 |
+
segment,
|
| 341 |
+
inpainted,
|
| 342 |
+
brush_layer,
|
| 343 |
+
rendered,
|
| 344 |
+
};
|
| 345 |
+
|
| 346 |
+
drop(guard);
|
| 347 |
+
Ok(Json(detail))
|
| 348 |
}
|
| 349 |
|
| 350 |
+
#[derive(Debug, Deserialize, IntoParams)]
|
| 351 |
+
#[serde(rename_all = "camelCase")]
|
| 352 |
+
struct ThumbnailQuery {
|
| 353 |
+
size: Option<u32>,
|
| 354 |
+
}
|
| 355 |
+
|
| 356 |
+
#[utoipa::path(
|
| 357 |
+
get,
|
| 358 |
+
path = "/documents/{document_id}/thumbnail",
|
| 359 |
+
operation_id = "getDocumentThumbnail",
|
| 360 |
+
tag = "documents",
|
| 361 |
+
params(("document_id" = String, Path,), ThumbnailQuery),
|
| 362 |
+
responses(
|
| 363 |
+
(status = 200, content_type = "image/webp", body = inline(String)),
|
| 364 |
+
(status = 404, body = ApiError),
|
| 365 |
+
(status = 503, body = ApiError),
|
| 366 |
+
),
|
| 367 |
+
)]
|
| 368 |
+
async fn get_document_thumbnail(
|
| 369 |
State(state): State<ApiState>,
|
| 370 |
Path(document_id): Path<String>,
|
| 371 |
+
Query(query): Query<ThumbnailQuery>,
|
| 372 |
) -> ApiResult<Response> {
|
| 373 |
+
let size = query.size.unwrap_or(200).min(800);
|
| 374 |
let resources = state.resources()?;
|
| 375 |
let guard = resources.state.read().await;
|
| 376 |
let doc = guard
|
|
|
|
| 379 |
.find(|d| d.id == document_id)
|
| 380 |
.ok_or_else(|| ApiError::not_found("Document not found"))?;
|
| 381 |
let source = doc.rendered.as_ref().unwrap_or(&doc.image);
|
| 382 |
+
let thumbnail = source.thumbnail(size, size);
|
| 383 |
let bytes = encode_webp(&thumbnail.into())?;
|
| 384 |
drop(guard);
|
| 385 |
Ok(binary_response(bytes, "image/webp", None))
|
| 386 |
}
|
| 387 |
|
| 388 |
+
#[utoipa::path(
|
| 389 |
+
post,
|
| 390 |
+
path = "/documents",
|
| 391 |
+
operation_id = "importDocuments",
|
| 392 |
+
tag = "documents",
|
| 393 |
+
params(ImportQuery),
|
| 394 |
+
request_body(content_type = "multipart/form-data", content = inline(MultipartUpload)),
|
| 395 |
+
responses(
|
| 396 |
+
(status = 200, body = koharu_core::ImportResult),
|
| 397 |
+
(status = 400, body = ApiError),
|
| 398 |
+
(status = 503, body = ApiError),
|
| 399 |
+
),
|
| 400 |
+
)]
|
|
|
|
|
|
|
|
|
|
|
|
|
| 401 |
async fn import_documents(
|
| 402 |
State(state): State<ApiState>,
|
| 403 |
Query(query): Query<ImportQuery>,
|
|
|
|
| 419 |
.bytes()
|
| 420 |
.await
|
| 421 |
.map_err(|error| ApiError::bad_request(error.to_string()))?;
|
| 422 |
+
files.push(koharu_core::FileEntry {
|
| 423 |
name: filename,
|
| 424 |
data: data.to_vec(),
|
| 425 |
});
|
|
|
|
| 429 |
return Err(ApiError::bad_request("No files uploaded"));
|
| 430 |
}
|
| 431 |
|
| 432 |
+
let payload = koharu_core::OpenDocumentsPayload { files };
|
| 433 |
match query.mode.unwrap_or(koharu_core::ImportMode::Replace) {
|
| 434 |
koharu_core::ImportMode::Replace => {
|
| 435 |
operations::open_documents(resources.clone(), payload).await?;
|
|
|
|
| 451 |
}))
|
| 452 |
}
|
| 453 |
|
| 454 |
+
// ---------------------------------------------------------------------------
|
| 455 |
+
// Processing
|
| 456 |
+
// ---------------------------------------------------------------------------
|
| 457 |
+
|
| 458 |
+
#[utoipa::path(
|
| 459 |
+
post,
|
| 460 |
+
path = "/documents/{document_id}/detect",
|
| 461 |
+
operation_id = "detectDocument",
|
| 462 |
+
tag = "processing",
|
| 463 |
+
params(("document_id" = String, Path,)),
|
| 464 |
+
responses(
|
| 465 |
+
(status = 204),
|
| 466 |
+
(status = 404, body = ApiError),
|
| 467 |
+
(status = 503, body = ApiError),
|
| 468 |
+
),
|
| 469 |
+
)]
|
| 470 |
async fn detect_document(
|
| 471 |
State(state): State<ApiState>,
|
| 472 |
Path(document_id): Path<String>,
|
| 473 |
) -> ApiResult<StatusCode> {
|
| 474 |
let resources = state.resources()?;
|
| 475 |
let (index, _) = find_document(&resources, &document_id).await?;
|
| 476 |
+
operations::detect(resources, koharu_core::IndexPayload { index }).await?;
|
| 477 |
Ok(StatusCode::NO_CONTENT)
|
| 478 |
}
|
| 479 |
|
| 480 |
+
#[utoipa::path(
|
| 481 |
+
post,
|
| 482 |
+
path = "/documents/{document_id}/recognize",
|
| 483 |
+
operation_id = "recognizeDocument",
|
| 484 |
+
tag = "processing",
|
| 485 |
+
params(("document_id" = String, Path,)),
|
| 486 |
+
responses(
|
| 487 |
+
(status = 204),
|
| 488 |
+
(status = 404, body = ApiError),
|
| 489 |
+
(status = 503, body = ApiError),
|
| 490 |
+
),
|
| 491 |
+
)]
|
| 492 |
+
async fn recognize_document(
|
| 493 |
State(state): State<ApiState>,
|
| 494 |
Path(document_id): Path<String>,
|
| 495 |
) -> ApiResult<StatusCode> {
|
| 496 |
let resources = state.resources()?;
|
| 497 |
let (index, _) = find_document(&resources, &document_id).await?;
|
| 498 |
+
operations::ocr(resources, koharu_core::IndexPayload { index }).await?;
|
| 499 |
Ok(StatusCode::NO_CONTENT)
|
| 500 |
}
|
| 501 |
|
| 502 |
+
#[utoipa::path(
|
| 503 |
+
post,
|
| 504 |
+
path = "/documents/{document_id}/inpaint",
|
| 505 |
+
operation_id = "inpaintDocument",
|
| 506 |
+
tag = "processing",
|
| 507 |
+
params(("document_id" = String, Path,)),
|
| 508 |
+
responses(
|
| 509 |
+
(status = 204),
|
| 510 |
+
(status = 404, body = ApiError),
|
| 511 |
+
(status = 503, body = ApiError),
|
| 512 |
+
),
|
| 513 |
+
)]
|
| 514 |
async fn inpaint_document(
|
| 515 |
State(state): State<ApiState>,
|
| 516 |
Path(document_id): Path<String>,
|
| 517 |
) -> ApiResult<StatusCode> {
|
| 518 |
let resources = state.resources()?;
|
| 519 |
let (index, _) = find_document(&resources, &document_id).await?;
|
| 520 |
+
operations::inpaint(resources, koharu_core::IndexPayload { index }).await?;
|
| 521 |
Ok(StatusCode::NO_CONTENT)
|
| 522 |
}
|
| 523 |
|
| 524 |
+
#[utoipa::path(
|
| 525 |
+
post,
|
| 526 |
+
path = "/documents/{document_id}/render",
|
| 527 |
+
operation_id = "renderDocument",
|
| 528 |
+
tag = "processing",
|
| 529 |
+
params(("document_id" = String, Path,)),
|
| 530 |
+
request_body = RenderRequest,
|
| 531 |
+
responses(
|
| 532 |
+
(status = 204),
|
| 533 |
+
(status = 404, body = ApiError),
|
| 534 |
+
(status = 503, body = ApiError),
|
| 535 |
+
),
|
| 536 |
+
)]
|
| 537 |
async fn render_document(
|
| 538 |
State(state): State<ApiState>,
|
| 539 |
Path(document_id): Path<String>,
|
|
|
|
| 549 |
|
| 550 |
operations::render(
|
| 551 |
resources,
|
| 552 |
+
koharu_core::RenderPayload {
|
| 553 |
index,
|
| 554 |
text_block_index,
|
| 555 |
shader_effect: request.shader_effect,
|
|
|
|
| 562 |
Ok(StatusCode::NO_CONTENT)
|
| 563 |
}
|
| 564 |
|
| 565 |
+
#[utoipa::path(
|
| 566 |
+
post,
|
| 567 |
+
path = "/documents/{document_id}/translate",
|
| 568 |
+
operation_id = "translateDocument",
|
| 569 |
+
tag = "processing",
|
| 570 |
+
params(("document_id" = String, Path,)),
|
| 571 |
+
request_body = TranslateRequest,
|
| 572 |
+
responses(
|
| 573 |
+
(status = 204),
|
| 574 |
+
(status = 404, body = ApiError),
|
| 575 |
+
(status = 503, body = ApiError),
|
| 576 |
+
),
|
| 577 |
+
)]
|
| 578 |
async fn translate_document(
|
| 579 |
State(state): State<ApiState>,
|
| 580 |
Path(document_id): Path<String>,
|
|
|
|
| 601 |
Ok(StatusCode::NO_CONTENT)
|
| 602 |
}
|
| 603 |
|
| 604 |
+
// ---------------------------------------------------------------------------
|
| 605 |
+
// Regions
|
| 606 |
+
// ---------------------------------------------------------------------------
|
| 607 |
+
|
| 608 |
+
#[utoipa::path(
|
| 609 |
+
put,
|
| 610 |
+
path = "/documents/{document_id}/mask",
|
| 611 |
+
operation_id = "updateMask",
|
| 612 |
+
tag = "regions",
|
| 613 |
+
params(("document_id" = String, Path,)),
|
| 614 |
+
request_body = MaskRegionRequest,
|
| 615 |
+
responses(
|
| 616 |
+
(status = 204),
|
| 617 |
+
(status = 404, body = ApiError),
|
| 618 |
+
(status = 503, body = ApiError),
|
| 619 |
+
),
|
| 620 |
+
)]
|
| 621 |
+
async fn update_mask(
|
| 622 |
State(state): State<ApiState>,
|
| 623 |
Path(document_id): Path<String>,
|
| 624 |
Json(request): Json<MaskRegionRequest>,
|
|
|
|
| 627 |
let (index, _) = find_document(&resources, &document_id).await?;
|
| 628 |
operations::update_inpaint_mask(
|
| 629 |
resources,
|
| 630 |
+
koharu_core::UpdateInpaintMaskPayload {
|
| 631 |
index,
|
| 632 |
mask: request.data,
|
| 633 |
region: request.region.map(to_inpaint_region),
|
|
|
|
| 637 |
Ok(StatusCode::NO_CONTENT)
|
| 638 |
}
|
| 639 |
|
| 640 |
+
#[utoipa::path(
|
| 641 |
+
put,
|
| 642 |
+
path = "/documents/{document_id}/brush-layer",
|
| 643 |
+
operation_id = "updateBrushLayer",
|
| 644 |
+
tag = "regions",
|
| 645 |
+
params(("document_id" = String, Path,)),
|
| 646 |
+
responses(
|
| 647 |
+
(status = 204),
|
| 648 |
+
(status = 404, body = ApiError),
|
| 649 |
+
(status = 503, body = ApiError),
|
| 650 |
+
),
|
| 651 |
+
)]
|
| 652 |
+
async fn update_brush_layer(
|
| 653 |
State(state): State<ApiState>,
|
| 654 |
Path(document_id): Path<String>,
|
| 655 |
Json(request): Json<koharu_core::BrushRegionRequest>,
|
|
|
|
| 658 |
let (index, _) = find_document(&resources, &document_id).await?;
|
| 659 |
operations::update_brush_layer(
|
| 660 |
resources,
|
| 661 |
+
koharu_core::UpdateBrushLayerPayload {
|
| 662 |
index,
|
| 663 |
patch: request.data,
|
| 664 |
region: to_inpaint_region(request.region),
|
|
|
|
| 668 |
Ok(StatusCode::NO_CONTENT)
|
| 669 |
}
|
| 670 |
|
| 671 |
+
#[utoipa::path(
|
| 672 |
+
post,
|
| 673 |
+
path = "/documents/{document_id}/inpaint-region",
|
| 674 |
+
operation_id = "inpaintRegion",
|
| 675 |
+
tag = "regions",
|
| 676 |
+
params(("document_id" = String, Path,)),
|
| 677 |
+
responses(
|
| 678 |
+
(status = 204),
|
| 679 |
+
(status = 404, body = ApiError),
|
| 680 |
+
(status = 503, body = ApiError),
|
| 681 |
+
),
|
| 682 |
+
)]
|
| 683 |
async fn inpaint_region(
|
| 684 |
State(state): State<ApiState>,
|
| 685 |
Path(document_id): Path<String>,
|
|
|
|
| 689 |
let (index, _) = find_document(&resources, &document_id).await?;
|
| 690 |
operations::inpaint_partial(
|
| 691 |
resources,
|
| 692 |
+
koharu_core::InpaintPartialPayload {
|
| 693 |
index,
|
| 694 |
region: to_inpaint_region(request.region),
|
| 695 |
},
|
|
|
|
| 698 |
Ok(StatusCode::NO_CONTENT)
|
| 699 |
}
|
| 700 |
|
| 701 |
+
// ---------------------------------------------------------------------------
|
| 702 |
+
// Text Blocks
|
| 703 |
+
// ---------------------------------------------------------------------------
|
| 704 |
+
|
| 705 |
+
#[utoipa::path(
|
| 706 |
+
post,
|
| 707 |
+
path = "/documents/{document_id}/text-blocks",
|
| 708 |
+
operation_id = "createTextBlock",
|
| 709 |
+
tag = "text-blocks",
|
| 710 |
+
params(("document_id" = String, Path,)),
|
| 711 |
+
request_body = CreateTextBlock,
|
| 712 |
+
responses(
|
| 713 |
+
(status = 200, body = TextBlockDetail),
|
| 714 |
+
(status = 404, body = ApiError),
|
| 715 |
+
(status = 503, body = ApiError),
|
| 716 |
+
),
|
| 717 |
+
)]
|
| 718 |
async fn create_text_block(
|
| 719 |
State(state): State<ApiState>,
|
| 720 |
Path(document_id): Path<String>,
|
|
|
|
| 750 |
Ok(Json(detail))
|
| 751 |
}
|
| 752 |
|
| 753 |
+
#[utoipa::path(
|
| 754 |
+
put,
|
| 755 |
+
path = "/documents/{document_id}/text-blocks",
|
| 756 |
+
operation_id = "putTextBlocks",
|
| 757 |
+
tag = "text-blocks",
|
| 758 |
+
params(("document_id" = String, Path,)),
|
| 759 |
+
request_body = Vec<TextBlockInput>,
|
| 760 |
+
responses(
|
| 761 |
+
(status = 204),
|
| 762 |
+
(status = 404, body = ApiError),
|
| 763 |
+
(status = 503, body = ApiError),
|
| 764 |
+
),
|
| 765 |
+
)]
|
| 766 |
+
async fn put_text_blocks(
|
| 767 |
+
State(state): State<ApiState>,
|
| 768 |
+
Path(document_id): Path<String>,
|
| 769 |
+
Json(inputs): Json<Vec<TextBlockInput>>,
|
| 770 |
+
) -> ApiResult<StatusCode> {
|
| 771 |
+
let resources = state.resources()?;
|
| 772 |
+
let (index, _) = find_document(&resources, &document_id).await?;
|
| 773 |
+
|
| 774 |
+
let any_content_changed = state_tx::mutate_doc(
|
| 775 |
+
&resources.state,
|
| 776 |
+
index,
|
| 777 |
+
&[ChangedField::TextBlocks],
|
| 778 |
+
|document| {
|
| 779 |
+
let mut any_changed = false;
|
| 780 |
+
|
| 781 |
+
// Build a set of incoming IDs for deletion detection
|
| 782 |
+
let incoming_ids: std::collections::HashSet<&str> = inputs
|
| 783 |
+
.iter()
|
| 784 |
+
.filter_map(|input| input.id.as_deref())
|
| 785 |
+
.collect();
|
| 786 |
+
|
| 787 |
+
// Delete blocks not present in the incoming array
|
| 788 |
+
let before_len = document.text_blocks.len();
|
| 789 |
+
document
|
| 790 |
+
.text_blocks
|
| 791 |
+
.retain(|block| incoming_ids.contains(block.id.as_str()));
|
| 792 |
+
if document.text_blocks.len() != before_len {
|
| 793 |
+
any_changed = true;
|
| 794 |
+
}
|
| 795 |
+
|
| 796 |
+
for input in &inputs {
|
| 797 |
+
if let Some(ref id) = input.id {
|
| 798 |
+
// Update existing block
|
| 799 |
+
if let Some(block) = document
|
| 800 |
+
.text_blocks
|
| 801 |
+
.iter_mut()
|
| 802 |
+
.find(|b| &b.id == id)
|
| 803 |
+
{
|
| 804 |
+
let patch = TextBlockPatch {
|
| 805 |
+
text: input.text.clone(),
|
| 806 |
+
translation: input.translation.clone(),
|
| 807 |
+
x: Some(input.x),
|
| 808 |
+
y: Some(input.y),
|
| 809 |
+
width: Some(input.width),
|
| 810 |
+
height: Some(input.height),
|
| 811 |
+
style: input.style.clone(),
|
| 812 |
+
};
|
| 813 |
+
let had_render = block.rendered.is_some();
|
| 814 |
+
apply_text_block_patch(block, patch);
|
| 815 |
+
// Content changed if the render was invalidated
|
| 816 |
+
if had_render && block.rendered.is_none() {
|
| 817 |
+
any_changed = true;
|
| 818 |
+
}
|
| 819 |
+
}
|
| 820 |
+
} else {
|
| 821 |
+
// Create new block
|
| 822 |
+
let mut block = TextBlock {
|
| 823 |
+
x: input.x,
|
| 824 |
+
y: input.y,
|
| 825 |
+
width: input.width,
|
| 826 |
+
height: input.height,
|
| 827 |
+
text: input.text.clone(),
|
| 828 |
+
translation: input.translation.clone(),
|
| 829 |
+
style: input.style.clone(),
|
| 830 |
+
confidence: 1.0,
|
| 831 |
+
..Default::default()
|
| 832 |
+
};
|
| 833 |
+
block.set_layout_seed(block.x, block.y, block.width, block.height);
|
| 834 |
+
document.text_blocks.push(block);
|
| 835 |
+
any_changed = true;
|
| 836 |
+
}
|
| 837 |
+
}
|
| 838 |
+
|
| 839 |
+
Ok(any_changed)
|
| 840 |
+
},
|
| 841 |
+
)
|
| 842 |
+
.await?;
|
| 843 |
+
|
| 844 |
+
// Auto-render if any block content/geometry changed
|
| 845 |
+
if any_content_changed {
|
| 846 |
+
let _ = operations::render(
|
| 847 |
+
resources,
|
| 848 |
+
koharu_core::RenderPayload {
|
| 849 |
+
index,
|
| 850 |
+
text_block_index: None,
|
| 851 |
+
shader_effect: None,
|
| 852 |
+
shader_stroke: None,
|
| 853 |
+
font_family: None,
|
| 854 |
+
},
|
| 855 |
+
)
|
| 856 |
+
.await;
|
| 857 |
+
}
|
| 858 |
+
|
| 859 |
+
Ok(StatusCode::NO_CONTENT)
|
| 860 |
+
}
|
| 861 |
+
|
| 862 |
+
#[utoipa::path(
|
| 863 |
+
patch,
|
| 864 |
+
path = "/documents/{document_id}/text-blocks/{text_block_id}",
|
| 865 |
+
operation_id = "patchTextBlock",
|
| 866 |
+
tag = "text-blocks",
|
| 867 |
+
params(
|
| 868 |
+
("document_id" = String, Path,),
|
| 869 |
+
("text_block_id" = String, Path,),
|
| 870 |
+
),
|
| 871 |
+
request_body = TextBlockPatch,
|
| 872 |
+
responses(
|
| 873 |
+
(status = 200, body = TextBlockDetail),
|
| 874 |
+
(status = 404, body = ApiError),
|
| 875 |
+
(status = 503, body = ApiError),
|
| 876 |
+
),
|
| 877 |
+
)]
|
| 878 |
async fn patch_text_block(
|
| 879 |
State(state): State<ApiState>,
|
| 880 |
Path((document_id, text_block_id)): Path<(String, String)>,
|
|
|
|
| 902 |
Ok(Json(detail))
|
| 903 |
}
|
| 904 |
|
| 905 |
+
#[utoipa::path(
|
| 906 |
+
delete,
|
| 907 |
+
path = "/documents/{document_id}/text-blocks/{text_block_id}",
|
| 908 |
+
operation_id = "deleteTextBlock",
|
| 909 |
+
tag = "text-blocks",
|
| 910 |
+
params(
|
| 911 |
+
("document_id" = String, Path,),
|
| 912 |
+
("text_block_id" = String, Path,),
|
| 913 |
+
),
|
| 914 |
+
responses(
|
| 915 |
+
(status = 204),
|
| 916 |
+
(status = 404, body = ApiError),
|
| 917 |
+
(status = 503, body = ApiError),
|
| 918 |
+
),
|
| 919 |
+
)]
|
| 920 |
async fn delete_text_block(
|
| 921 |
State(state): State<ApiState>,
|
| 922 |
Path((document_id, text_block_id)): Path<(String, String)>,
|
|
|
|
| 943 |
Ok(StatusCode::NO_CONTENT)
|
| 944 |
}
|
| 945 |
|
| 946 |
+
// ---------------------------------------------------------------------------
|
| 947 |
+
// LLM
|
| 948 |
+
// ---------------------------------------------------------------------------
|
| 949 |
+
|
| 950 |
+
#[utoipa::path(
|
| 951 |
+
get,
|
| 952 |
+
path = "/llm/models",
|
| 953 |
+
operation_id = "listLlmModels",
|
| 954 |
+
tag = "llm",
|
| 955 |
+
params(LlmModelsQuery),
|
| 956 |
+
responses(
|
| 957 |
+
(status = 200, body = Vec<LlmModelInfo>),
|
| 958 |
+
(status = 503, body = ApiError),
|
| 959 |
+
),
|
| 960 |
+
)]
|
| 961 |
async fn list_llm_models(
|
| 962 |
State(state): State<ApiState>,
|
| 963 |
Query(query): Query<LlmModelsQuery>,
|
|
|
|
| 981 |
Ok(Json(models))
|
| 982 |
}
|
| 983 |
|
| 984 |
+
#[utoipa::path(
|
| 985 |
+
get,
|
| 986 |
+
path = "/llm",
|
| 987 |
+
operation_id = "getLlm",
|
| 988 |
+
tag = "llm",
|
| 989 |
+
responses(
|
| 990 |
+
(status = 200, body = koharu_core::LlmState),
|
| 991 |
+
(status = 503, body = ApiError),
|
| 992 |
+
),
|
| 993 |
+
)]
|
| 994 |
+
async fn get_llm(State(state): State<ApiState>) -> ApiResult<Json<koharu_core::LlmState>> {
|
| 995 |
let resources = state.resources()?;
|
| 996 |
Ok(Json(resources.llm.snapshot().await))
|
| 997 |
}
|
| 998 |
|
| 999 |
+
#[utoipa::path(
|
| 1000 |
+
put,
|
| 1001 |
+
path = "/llm",
|
| 1002 |
+
operation_id = "loadLlm",
|
| 1003 |
+
tag = "llm",
|
| 1004 |
+
request_body = LlmLoadRequest,
|
| 1005 |
+
responses(
|
| 1006 |
+
(status = 200, body = koharu_core::LlmState),
|
| 1007 |
+
(status = 400, body = ApiError),
|
| 1008 |
+
(status = 503, body = ApiError),
|
| 1009 |
+
),
|
| 1010 |
+
)]
|
| 1011 |
async fn load_llm(
|
| 1012 |
State(state): State<ApiState>,
|
| 1013 |
Json(request): Json<LlmLoadRequest>,
|
|
|
|
| 1015 |
let resources = state.resources()?;
|
| 1016 |
operations::llm_load(
|
| 1017 |
resources.clone(),
|
| 1018 |
+
koharu_core::LlmLoadPayload {
|
| 1019 |
id: request.id,
|
| 1020 |
api_key: request.api_key,
|
| 1021 |
base_url: request.base_url,
|
|
|
|
| 1028 |
Ok(Json(resources.llm.snapshot().await))
|
| 1029 |
}
|
| 1030 |
|
| 1031 |
+
#[utoipa::path(
|
| 1032 |
+
delete,
|
| 1033 |
+
path = "/llm",
|
| 1034 |
+
operation_id = "unloadLlm",
|
| 1035 |
+
tag = "llm",
|
| 1036 |
+
responses(
|
| 1037 |
+
(status = 200, body = koharu_core::LlmState),
|
| 1038 |
+
(status = 503, body = ApiError),
|
| 1039 |
+
),
|
| 1040 |
+
)]
|
| 1041 |
+
async fn unload_llm(State(state): State<ApiState>) -> ApiResult<Json<koharu_core::LlmState>> {
|
| 1042 |
let resources = state.resources()?;
|
| 1043 |
operations::llm_offload(resources.clone()).await?;
|
| 1044 |
Ok(Json(resources.llm.snapshot().await))
|
| 1045 |
}
|
| 1046 |
|
| 1047 |
+
#[utoipa::path(
|
| 1048 |
+
post,
|
| 1049 |
+
path = "/llm/health",
|
| 1050 |
+
operation_id = "checkLlmHealth",
|
| 1051 |
+
tag = "llm",
|
| 1052 |
+
request_body = LlmPingRequest,
|
| 1053 |
+
responses(
|
| 1054 |
+
(status = 200, body = LlmPingResponse),
|
| 1055 |
+
),
|
| 1056 |
+
)]
|
| 1057 |
+
async fn check_llm_health(
|
| 1058 |
State(state): State<ApiState>,
|
| 1059 |
Json(request): Json<LlmPingRequest>,
|
| 1060 |
) -> ApiResult<Json<LlmPingResponse>> {
|
|
|
|
| 1080 |
}
|
| 1081 |
}
|
| 1082 |
|
| 1083 |
+
// ---------------------------------------------------------------------------
|
| 1084 |
+
// Providers
|
| 1085 |
+
// ---------------------------------------------------------------------------
|
| 1086 |
+
|
| 1087 |
+
#[utoipa::path(
|
| 1088 |
+
get,
|
| 1089 |
+
path = "/providers/{provider}/api-key",
|
| 1090 |
+
operation_id = "getApiKey",
|
| 1091 |
+
tag = "providers",
|
| 1092 |
+
params(("provider" = String, Path,)),
|
| 1093 |
+
responses(
|
| 1094 |
+
(status = 200, body = ApiKeyResponse),
|
| 1095 |
+
(status = 503, body = ApiError),
|
| 1096 |
+
),
|
| 1097 |
+
)]
|
| 1098 |
async fn get_api_key(
|
| 1099 |
State(state): State<ApiState>,
|
| 1100 |
Path(provider): Path<String>,
|
| 1101 |
) -> ApiResult<Json<ApiKeyResponse>> {
|
| 1102 |
let resources = state.resources()?;
|
| 1103 |
+
let result =
|
| 1104 |
+
operations::get_api_key(resources, koharu_core::ApiKeyGetPayload { provider }).await?;
|
| 1105 |
Ok(Json(ApiKeyResponse {
|
| 1106 |
api_key: result.api_key,
|
| 1107 |
}))
|
| 1108 |
}
|
| 1109 |
|
| 1110 |
+
#[utoipa::path(
|
| 1111 |
+
put,
|
| 1112 |
+
path = "/providers/{provider}/api-key",
|
| 1113 |
+
operation_id = "setApiKey",
|
| 1114 |
+
tag = "providers",
|
| 1115 |
+
params(("provider" = String, Path,)),
|
| 1116 |
+
request_body = ApiKeyValue,
|
| 1117 |
+
responses(
|
| 1118 |
+
(status = 204),
|
| 1119 |
+
(status = 503, body = ApiError),
|
| 1120 |
+
),
|
| 1121 |
+
)]
|
| 1122 |
async fn set_api_key(
|
| 1123 |
State(state): State<ApiState>,
|
| 1124 |
Path(provider): Path<String>,
|
|
|
|
| 1127 |
let resources = state.resources()?;
|
| 1128 |
operations::set_api_key(
|
| 1129 |
resources,
|
| 1130 |
+
koharu_core::ApiKeySetPayload {
|
| 1131 |
provider,
|
| 1132 |
api_key: request.api_key,
|
| 1133 |
},
|
|
|
|
| 1136 |
Ok(StatusCode::NO_CONTENT)
|
| 1137 |
}
|
| 1138 |
|
| 1139 |
+
// ---------------------------------------------------------------------------
|
| 1140 |
+
// Jobs
|
| 1141 |
+
// ---------------------------------------------------------------------------
|
| 1142 |
+
|
| 1143 |
+
#[utoipa::path(
|
| 1144 |
+
post,
|
| 1145 |
+
path = "/jobs/pipeline",
|
| 1146 |
+
operation_id = "startPipeline",
|
| 1147 |
+
tag = "jobs",
|
| 1148 |
+
request_body = PipelineJobRequest,
|
| 1149 |
+
responses(
|
| 1150 |
+
(status = 200, body = JobState),
|
| 1151 |
+
(status = 503, body = ApiError),
|
| 1152 |
+
),
|
| 1153 |
+
)]
|
| 1154 |
+
async fn start_pipeline(
|
| 1155 |
State(state): State<ApiState>,
|
| 1156 |
Json(request): Json<PipelineJobRequest>,
|
| 1157 |
) -> ApiResult<Json<JobState>> {
|
|
|
|
| 1196 |
overall_percent: 0,
|
| 1197 |
error: None,
|
| 1198 |
};
|
| 1199 |
+
state.tracker.publish_job(job.clone()).await;
|
| 1200 |
|
| 1201 |
Ok(Json(job))
|
| 1202 |
}
|
| 1203 |
|
| 1204 |
+
#[utoipa::path(
|
| 1205 |
+
get,
|
| 1206 |
+
path = "/jobs",
|
| 1207 |
+
operation_id = "listJobs",
|
| 1208 |
+
tag = "jobs",
|
| 1209 |
+
responses(
|
| 1210 |
+
(status = 200, body = Vec<JobState>),
|
| 1211 |
+
),
|
| 1212 |
+
)]
|
| 1213 |
+
async fn list_jobs(State(state): State<ApiState>) -> Json<Vec<JobState>> {
|
| 1214 |
+
Json(state.tracker.list_jobs().await)
|
| 1215 |
+
}
|
| 1216 |
+
|
| 1217 |
+
#[utoipa::path(
|
| 1218 |
+
get,
|
| 1219 |
+
path = "/jobs/{job_id}",
|
| 1220 |
+
operation_id = "getJob",
|
| 1221 |
+
tag = "jobs",
|
| 1222 |
+
params(("job_id" = String, Path,)),
|
| 1223 |
+
responses(
|
| 1224 |
+
(status = 200, body = JobState),
|
| 1225 |
+
(status = 404, body = ApiError),
|
| 1226 |
+
),
|
| 1227 |
+
)]
|
| 1228 |
+
async fn get_job(
|
| 1229 |
+
State(state): State<ApiState>,
|
| 1230 |
+
Path(job_id): Path<String>,
|
| 1231 |
+
) -> ApiResult<Json<JobState>> {
|
| 1232 |
+
state
|
| 1233 |
+
.tracker
|
| 1234 |
+
.get_job(&job_id)
|
| 1235 |
+
.await
|
| 1236 |
+
.map(Json)
|
| 1237 |
+
.ok_or_else(|| ApiError::not_found(format!("Job not found: {job_id}")))
|
| 1238 |
+
}
|
| 1239 |
+
|
| 1240 |
+
#[utoipa::path(
|
| 1241 |
+
delete,
|
| 1242 |
+
path = "/jobs/{job_id}",
|
| 1243 |
+
operation_id = "cancelJob",
|
| 1244 |
+
tag = "jobs",
|
| 1245 |
+
params(("job_id" = String, Path,)),
|
| 1246 |
+
responses(
|
| 1247 |
+
(status = 204),
|
| 1248 |
+
(status = 404, body = ApiError),
|
| 1249 |
+
(status = 503, body = ApiError),
|
| 1250 |
+
),
|
| 1251 |
+
)]
|
| 1252 |
+
async fn cancel_job(
|
| 1253 |
State(state): State<ApiState>,
|
| 1254 |
Path(job_id): Path<String>,
|
| 1255 |
) -> ApiResult<StatusCode> {
|
|
|
|
| 1269 |
Ok(StatusCode::NO_CONTENT)
|
| 1270 |
}
|
| 1271 |
|
| 1272 |
+
// ---------------------------------------------------------------------------
|
| 1273 |
+
// Downloads
|
| 1274 |
+
// ---------------------------------------------------------------------------
|
| 1275 |
+
|
| 1276 |
+
#[utoipa::path(
|
| 1277 |
+
get,
|
| 1278 |
+
path = "/downloads",
|
| 1279 |
+
operation_id = "listDownloads",
|
| 1280 |
+
tag = "downloads",
|
| 1281 |
+
responses(
|
| 1282 |
+
(status = 200, body = Vec<DownloadState>),
|
| 1283 |
+
),
|
| 1284 |
+
)]
|
| 1285 |
+
async fn list_downloads(State(state): State<ApiState>) -> Json<Vec<DownloadState>> {
|
| 1286 |
+
Json(state.tracker.list_downloads().await)
|
| 1287 |
+
}
|
| 1288 |
+
|
| 1289 |
+
// ---------------------------------------------------------------------------
|
| 1290 |
+
// Exports
|
| 1291 |
+
// ---------------------------------------------------------------------------
|
| 1292 |
+
|
| 1293 |
+
#[utoipa::path(
|
| 1294 |
+
get,
|
| 1295 |
+
path = "/documents/{document_id}/export/{format}",
|
| 1296 |
+
operation_id = "exportDocument",
|
| 1297 |
+
tag = "exports",
|
| 1298 |
+
params(
|
| 1299 |
+
("document_id" = String, Path,),
|
| 1300 |
+
("format" = String, Path,),
|
| 1301 |
+
ExportQuery,
|
| 1302 |
+
),
|
| 1303 |
+
responses(
|
| 1304 |
+
(status = 200, content_type = "application/octet-stream", body = inline(String)),
|
| 1305 |
+
(status = 404, body = ApiError),
|
| 1306 |
+
(status = 503, body = ApiError),
|
| 1307 |
+
),
|
| 1308 |
+
)]
|
| 1309 |
async fn export_document(
|
| 1310 |
State(state): State<ApiState>,
|
| 1311 |
+
Path((document_id, format)): Path<(String, String)>,
|
| 1312 |
+
Query(query): Query<ExportQuery>,
|
| 1313 |
) -> ApiResult<Response> {
|
| 1314 |
let resources = state.resources()?;
|
| 1315 |
let (_, document) = find_document(&resources, &document_id).await?;
|
| 1316 |
+
|
| 1317 |
+
if format == "psd" {
|
| 1318 |
+
let data = koharu_psd::export_document(&document, &app_psd_export_options())
|
| 1319 |
+
.map_err(|error| ApiError::bad_request(error.to_string()))?;
|
| 1320 |
+
return Ok(binary_response(
|
| 1321 |
+
data,
|
| 1322 |
+
"image/vnd.adobe.photoshop",
|
| 1323 |
+
Some(psd_export_filename(&document)),
|
| 1324 |
+
));
|
| 1325 |
+
}
|
| 1326 |
+
|
| 1327 |
let layer = query.layer.unwrap_or(ExportLayer::Rendered);
|
| 1328 |
let (image, filename) = export_target(&document, layer)?;
|
| 1329 |
let ext = document
|
|
|
|
| 1337 |
Ok(binary_response(data, content_type, Some(filename)))
|
| 1338 |
}
|
| 1339 |
|
| 1340 |
+
#[utoipa::path(
|
| 1341 |
+
post,
|
| 1342 |
+
path = "/exports",
|
| 1343 |
+
operation_id = "batchExport",
|
| 1344 |
+
tag = "exports",
|
| 1345 |
+
request_body = ExportBatchRequest,
|
| 1346 |
+
responses(
|
| 1347 |
+
(status = 200, body = ExportResult),
|
| 1348 |
+
(status = 503, body = ApiError),
|
| 1349 |
+
),
|
| 1350 |
+
)]
|
| 1351 |
+
async fn batch_export(
|
| 1352 |
State(state): State<ApiState>,
|
| 1353 |
+
Json(request): Json<ExportBatchRequest>,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1354 |
) -> ApiResult<Json<ExportResult>> {
|
| 1355 |
let resources = state.resources()?;
|
| 1356 |
+
let count = match request.layer.unwrap_or(ExportLayer::Rendered) {
|
| 1357 |
ExportLayer::Rendered => operations::export_all_rendered(resources).await?,
|
| 1358 |
ExportLayer::Inpainted => operations::export_all_inpainted(resources).await?,
|
| 1359 |
};
|
| 1360 |
Ok(Json(ExportResult { count }))
|
| 1361 |
}
|
| 1362 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1363 |
async fn find_document(
|
| 1364 |
resources: &AppResources,
|
| 1365 |
document_id: &str,
|
|
|
|
| 1381 |
.ok_or_else(|| ApiError::not_found(format!("Text block not found: {text_block_id}")))
|
| 1382 |
}
|
| 1383 |
|
| 1384 |
+
fn encode_webp(image: &koharu_core::SerializableDynamicImage) -> ApiResult<Vec<u8>> {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1385 |
encode_image(image, "webp")
|
| 1386 |
}
|
| 1387 |
|
| 1388 |
+
fn encode_image(image: &koharu_core::SerializableDynamicImage, ext: &str) -> ApiResult<Vec<u8>> {
|
| 1389 |
let format = ImageFormat::from_extension(ext).unwrap_or(ImageFormat::Jpeg);
|
| 1390 |
let mut cursor = Cursor::new(Vec::new());
|
| 1391 |
image
|
|
|
|
| 1421 |
fn export_target(
|
| 1422 |
document: &Document,
|
| 1423 |
layer: ExportLayer,
|
| 1424 |
+
) -> ApiResult<(&koharu_core::SerializableDynamicImage, String)> {
|
| 1425 |
let ext = document
|
| 1426 |
.path
|
| 1427 |
.extension()
|
koharu-rpc/src/bin/openapi.rs
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fn main() {
|
| 2 |
+
let (_, mut spec) = koharu_rpc::api::api();
|
| 3 |
+
|
| 4 |
+
// Register schemas that are referenced via IntoParams but not auto-collected
|
| 5 |
+
let extras = utoipa::openapi::OpenApiBuilder::new()
|
| 6 |
+
.components(Some(
|
| 7 |
+
utoipa::openapi::ComponentsBuilder::new()
|
| 8 |
+
.schema_from::<koharu_core::ImportMode>()
|
| 9 |
+
.schema_from::<koharu_core::ExportLayer>()
|
| 10 |
+
.build(),
|
| 11 |
+
))
|
| 12 |
+
.build();
|
| 13 |
+
spec.merge(extras);
|
| 14 |
+
|
| 15 |
+
println!("{}", spec.to_pretty_json().unwrap());
|
| 16 |
+
}
|
koharu-rpc/src/lib.rs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
| 1 |
pub mod api;
|
| 2 |
-
pub mod events;
|
| 3 |
mod mcp;
|
| 4 |
pub mod server;
|
| 5 |
pub mod shared;
|
|
|
|
| 6 |
|
| 7 |
pub use shared::{BootstrapHooks, SharedState};
|
|
|
|
| 1 |
pub mod api;
|
|
|
|
| 2 |
mod mcp;
|
| 3 |
pub mod server;
|
| 4 |
pub mod shared;
|
| 5 |
+
pub mod tracker;
|
| 6 |
|
| 7 |
pub use shared::{BootstrapHooks, SharedState};
|
koharu-rpc/src/server.rs
CHANGED
|
@@ -14,9 +14,9 @@ use tokio::net::TcpListener;
|
|
| 14 |
use tower_http::cors::CorsLayer;
|
| 15 |
|
| 16 |
use crate::api;
|
| 17 |
-
use crate::events::EventHub;
|
| 18 |
use crate::mcp::KoharuMcp;
|
| 19 |
use crate::shared::SharedState;
|
|
|
|
| 20 |
|
| 21 |
/// An asset returned by the resolver: raw bytes + MIME type.
|
| 22 |
pub struct Asset {
|
|
@@ -36,7 +36,7 @@ where
|
|
| 36 |
}
|
| 37 |
|
| 38 |
fn build_router(shared: SharedState, resolver: SharedAssetResolver) -> Router {
|
| 39 |
-
let
|
| 40 |
let cors = CorsLayer::very_permissive();
|
| 41 |
|
| 42 |
let mcp_service = StreamableHttpService::new(
|
|
@@ -52,7 +52,7 @@ fn build_router(shared: SharedState, resolver: SharedAssetResolver) -> Router {
|
|
| 52 |
);
|
| 53 |
|
| 54 |
Router::new()
|
| 55 |
-
.nest("/api/v1", api::router(shared.clone(),
|
| 56 |
.nest_service("/mcp", mcp_service)
|
| 57 |
.layer(cors)
|
| 58 |
.fallback(move |uri: Uri| {
|
|
|
|
| 14 |
use tower_http::cors::CorsLayer;
|
| 15 |
|
| 16 |
use crate::api;
|
|
|
|
| 17 |
use crate::mcp::KoharuMcp;
|
| 18 |
use crate::shared::SharedState;
|
| 19 |
+
use crate::tracker::Tracker;
|
| 20 |
|
| 21 |
/// An asset returned by the resolver: raw bytes + MIME type.
|
| 22 |
pub struct Asset {
|
|
|
|
| 36 |
}
|
| 37 |
|
| 38 |
fn build_router(shared: SharedState, resolver: SharedAssetResolver) -> Router {
|
| 39 |
+
let tracker = Tracker::new(&shared);
|
| 40 |
let cors = CorsLayer::very_permissive();
|
| 41 |
|
| 42 |
let mcp_service = StreamableHttpService::new(
|
|
|
|
| 52 |
);
|
| 53 |
|
| 54 |
Router::new()
|
| 55 |
+
.nest("/api/v1", api::router(shared.clone(), tracker))
|
| 56 |
.nest_service("/mcp", mcp_service)
|
| 57 |
.layer(cors)
|
| 58 |
.fallback(move |uri: Uri| {
|
koharu-rpc/src/{events.rs → tracker.rs}
RENAMED
|
@@ -1,158 +1,57 @@
|
|
| 1 |
-
use std::{collections::HashMap, sync::Arc
|
| 2 |
|
| 3 |
-
use koharu_app::
|
| 4 |
use koharu_core::{
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
TransferStatus,
|
| 8 |
};
|
| 9 |
-
use tokio::sync::
|
| 10 |
|
| 11 |
-
use crate::shared::
|
| 12 |
-
|
| 13 |
-
#[derive(Debug, Clone)]
|
| 14 |
-
pub enum ApiEvent {
|
| 15 |
-
DocumentsChanged(DocumentsChangedEvent),
|
| 16 |
-
DocumentChanged(DocumentChangedEvent),
|
| 17 |
-
JobChanged(JobState),
|
| 18 |
-
DownloadChanged(DownloadState),
|
| 19 |
-
LlmChanged(LlmState),
|
| 20 |
-
}
|
| 21 |
|
|
|
|
| 22 |
#[derive(Clone)]
|
| 23 |
-
pub struct
|
| 24 |
inner: Arc<Inner>,
|
| 25 |
}
|
| 26 |
|
| 27 |
struct Inner {
|
| 28 |
-
shared: SharedState,
|
| 29 |
-
tx: broadcast::Sender<ApiEvent>,
|
| 30 |
jobs: RwLock<HashMap<String, JobState>>,
|
| 31 |
downloads: RwLock<HashMap<String, DownloadState>>,
|
| 32 |
}
|
| 33 |
|
| 34 |
-
impl
|
| 35 |
-
pub fn new(shared: SharedState) -> Self {
|
| 36 |
let inner = Arc::new(Inner {
|
| 37 |
-
shared,
|
| 38 |
-
tx: broadcast::channel(256).0,
|
| 39 |
jobs: RwLock::new(HashMap::new()),
|
| 40 |
downloads: RwLock::new(HashMap::new()),
|
| 41 |
});
|
| 42 |
|
| 43 |
-
spawn_state_listener(inner.clone());
|
| 44 |
spawn_pipeline_listener(inner.clone());
|
| 45 |
-
spawn_download_listener(inner.clone());
|
| 46 |
-
spawn_llm_listener(inner.clone());
|
| 47 |
|
| 48 |
Self { inner }
|
| 49 |
}
|
| 50 |
|
| 51 |
-
pub fn
|
| 52 |
-
self.inner.
|
|
|
|
|
|
|
| 53 |
}
|
| 54 |
|
| 55 |
-
pub async fn
|
| 56 |
-
|
| 57 |
-
let guard = resources.state.read().await;
|
| 58 |
-
let documents = guard.documents.iter().map(DocumentSummary::from).collect();
|
| 59 |
-
drop(guard);
|
| 60 |
-
(documents, resources.llm.snapshot().await)
|
| 61 |
-
} else {
|
| 62 |
-
(
|
| 63 |
-
Vec::new(),
|
| 64 |
-
LlmState {
|
| 65 |
-
status: koharu_core::LlmStateStatus::Empty,
|
| 66 |
-
model_id: None,
|
| 67 |
-
source: None,
|
| 68 |
-
error: None,
|
| 69 |
-
},
|
| 70 |
-
)
|
| 71 |
-
};
|
| 72 |
-
|
| 73 |
-
let mut jobs = self
|
| 74 |
-
.inner
|
| 75 |
-
.jobs
|
| 76 |
-
.read()
|
| 77 |
-
.await
|
| 78 |
-
.values()
|
| 79 |
-
.cloned()
|
| 80 |
-
.collect::<Vec<_>>();
|
| 81 |
-
jobs.sort_by(|left, right| left.id.cmp(&right.id));
|
| 82 |
-
|
| 83 |
-
let mut downloads = self
|
| 84 |
-
.inner
|
| 85 |
-
.downloads
|
| 86 |
-
.read()
|
| 87 |
-
.await
|
| 88 |
-
.values()
|
| 89 |
-
.cloned()
|
| 90 |
-
.collect::<Vec<_>>();
|
| 91 |
-
downloads.sort_by(|left, right| left.filename.cmp(&right.filename));
|
| 92 |
-
|
| 93 |
-
Ok(SnapshotEvent {
|
| 94 |
-
documents,
|
| 95 |
-
llm,
|
| 96 |
-
jobs,
|
| 97 |
-
downloads,
|
| 98 |
-
})
|
| 99 |
}
|
| 100 |
|
| 101 |
pub async fn publish_job(&self, job: JobState) {
|
| 102 |
update_job_state(&self.inner, job).await;
|
| 103 |
}
|
| 104 |
-
}
|
| 105 |
|
| 106 |
-
fn
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
Ok(state_tx::StateEvent::DocumentsChanged) => {
|
| 112 |
-
let Ok(resources) = get_resources(&inner.shared) else {
|
| 113 |
-
continue;
|
| 114 |
-
};
|
| 115 |
-
let guard = resources.state.read().await;
|
| 116 |
-
let documents = guard.documents.iter().map(DocumentSummary::from).collect();
|
| 117 |
-
drop(guard);
|
| 118 |
-
emit(
|
| 119 |
-
&inner,
|
| 120 |
-
ApiEvent::DocumentsChanged(DocumentsChangedEvent { documents }),
|
| 121 |
-
);
|
| 122 |
-
}
|
| 123 |
-
Ok(state_tx::StateEvent::DocumentChanged {
|
| 124 |
-
document_id,
|
| 125 |
-
revision,
|
| 126 |
-
changed,
|
| 127 |
-
}) => {
|
| 128 |
-
emit(
|
| 129 |
-
&inner,
|
| 130 |
-
ApiEvent::DocumentChanged(DocumentChangedEvent {
|
| 131 |
-
document_id,
|
| 132 |
-
revision,
|
| 133 |
-
changed,
|
| 134 |
-
}),
|
| 135 |
-
);
|
| 136 |
-
}
|
| 137 |
-
Err(broadcast::error::RecvError::Lagged(n)) => {
|
| 138 |
-
tracing::warn!(
|
| 139 |
-
"State event listener lagged by {n} events, re-emitting documents"
|
| 140 |
-
);
|
| 141 |
-
let Ok(resources) = get_resources(&inner.shared) else {
|
| 142 |
-
continue;
|
| 143 |
-
};
|
| 144 |
-
let guard = resources.state.read().await;
|
| 145 |
-
let documents = guard.documents.iter().map(DocumentSummary::from).collect();
|
| 146 |
-
drop(guard);
|
| 147 |
-
emit(
|
| 148 |
-
&inner,
|
| 149 |
-
ApiEvent::DocumentsChanged(DocumentsChangedEvent { documents }),
|
| 150 |
-
);
|
| 151 |
-
}
|
| 152 |
-
Err(broadcast::error::RecvError::Closed) => break,
|
| 153 |
-
}
|
| 154 |
-
}
|
| 155 |
-
});
|
| 156 |
}
|
| 157 |
|
| 158 |
fn spawn_pipeline_listener(inner: Arc<Inner>) {
|
|
@@ -163,16 +62,16 @@ fn spawn_pipeline_listener(inner: Arc<Inner>) {
|
|
| 163 |
Ok(progress) => {
|
| 164 |
update_job_state(&inner, pipeline_job_state(progress)).await;
|
| 165 |
}
|
| 166 |
-
Err(broadcast::error::RecvError::Lagged(_)) => continue,
|
| 167 |
-
Err(broadcast::error::RecvError::Closed) => break,
|
| 168 |
}
|
| 169 |
}
|
| 170 |
});
|
| 171 |
}
|
| 172 |
|
| 173 |
-
fn spawn_download_listener(inner: Arc<Inner>) {
|
| 174 |
tokio::spawn(async move {
|
| 175 |
-
let mut runtime_rx =
|
| 176 |
let mut download_rx = runtime_rx.borrow().clone().subscribe_downloads();
|
| 177 |
|
| 178 |
loop {
|
|
@@ -188,8 +87,8 @@ fn spawn_download_listener(inner: Arc<Inner>) {
|
|
| 188 |
Ok(progress) => {
|
| 189 |
update_download_state(&inner, download_state(progress)).await;
|
| 190 |
}
|
| 191 |
-
Err(broadcast::error::RecvError::Lagged(_)) => continue,
|
| 192 |
-
Err(broadcast::error::RecvError::Closed) => {
|
| 193 |
download_rx = runtime_rx.borrow().clone().subscribe_downloads();
|
| 194 |
}
|
| 195 |
}
|
|
@@ -198,41 +97,14 @@ fn spawn_download_listener(inner: Arc<Inner>) {
|
|
| 198 |
});
|
| 199 |
}
|
| 200 |
|
| 201 |
-
fn spawn_llm_listener(inner: Arc<Inner>) {
|
| 202 |
-
tokio::spawn(async move {
|
| 203 |
-
let resources = loop {
|
| 204 |
-
if let Ok(resources) = get_resources(&inner.shared) {
|
| 205 |
-
break resources;
|
| 206 |
-
}
|
| 207 |
-
tokio::time::sleep(Duration::from_millis(100)).await;
|
| 208 |
-
};
|
| 209 |
-
|
| 210 |
-
let mut rx = resources.llm.subscribe();
|
| 211 |
-
loop {
|
| 212 |
-
match rx.recv().await {
|
| 213 |
-
Ok(state) => emit(&inner, ApiEvent::LlmChanged(state)),
|
| 214 |
-
Err(broadcast::error::RecvError::Lagged(_)) => continue,
|
| 215 |
-
Err(broadcast::error::RecvError::Closed) => break,
|
| 216 |
-
}
|
| 217 |
-
}
|
| 218 |
-
});
|
| 219 |
-
}
|
| 220 |
-
|
| 221 |
-
fn emit(inner: &Arc<Inner>, event: ApiEvent) {
|
| 222 |
-
let _ = inner.tx.send(event);
|
| 223 |
-
}
|
| 224 |
-
|
| 225 |
async fn update_job_state(inner: &Arc<Inner>, job: JobState) {
|
| 226 |
let terminal = !matches!(job.status, JobStatus::Running);
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
jobs.insert(job.id.clone(), job.clone());
|
| 233 |
-
}
|
| 234 |
}
|
| 235 |
-
emit(inner, ApiEvent::JobChanged(job));
|
| 236 |
}
|
| 237 |
|
| 238 |
async fn update_download_state(inner: &Arc<Inner>, download: DownloadState) {
|
|
@@ -240,15 +112,12 @@ async fn update_download_state(inner: &Arc<Inner>, download: DownloadState) {
|
|
| 240 |
download.status,
|
| 241 |
TransferStatus::Started | TransferStatus::Downloading
|
| 242 |
);
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
downloads.insert(download.id.clone(), download.clone());
|
| 249 |
-
}
|
| 250 |
}
|
| 251 |
-
emit(inner, ApiEvent::DownloadChanged(download));
|
| 252 |
}
|
| 253 |
|
| 254 |
fn pipeline_job_state(progress: PipelineProgress) -> JobState {
|
|
@@ -295,8 +164,6 @@ fn download_state(progress: DownloadProgress) -> DownloadState {
|
|
| 295 |
}
|
| 296 |
}
|
| 297 |
|
| 298 |
-
use koharu_core::DownloadProgress;
|
| 299 |
-
|
| 300 |
#[cfg(test)]
|
| 301 |
mod tests {
|
| 302 |
use koharu_core::{PipelineStatus, PipelineStep};
|
|
|
|
| 1 |
+
use std::{collections::HashMap, sync::Arc};
|
| 2 |
|
| 3 |
+
use koharu_app::pipeline;
|
| 4 |
use koharu_core::{
|
| 5 |
+
DownloadProgress, DownloadState, DownloadStatus, JobState, JobStatus, PipelineProgress,
|
| 6 |
+
PipelineStatus, PipelineStep, TransferStatus,
|
|
|
|
| 7 |
};
|
| 8 |
+
use tokio::sync::RwLock;
|
| 9 |
|
| 10 |
+
use crate::shared::SharedState;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
|
| 12 |
+
/// Tracks active jobs and downloads for polling endpoints.
|
| 13 |
#[derive(Clone)]
|
| 14 |
+
pub struct Tracker {
|
| 15 |
inner: Arc<Inner>,
|
| 16 |
}
|
| 17 |
|
| 18 |
struct Inner {
|
|
|
|
|
|
|
| 19 |
jobs: RwLock<HashMap<String, JobState>>,
|
| 20 |
downloads: RwLock<HashMap<String, DownloadState>>,
|
| 21 |
}
|
| 22 |
|
| 23 |
+
impl Tracker {
|
| 24 |
+
pub fn new(shared: &SharedState) -> Self {
|
| 25 |
let inner = Arc::new(Inner {
|
|
|
|
|
|
|
| 26 |
jobs: RwLock::new(HashMap::new()),
|
| 27 |
downloads: RwLock::new(HashMap::new()),
|
| 28 |
});
|
| 29 |
|
|
|
|
| 30 |
spawn_pipeline_listener(inner.clone());
|
| 31 |
+
spawn_download_listener(inner.clone(), shared.clone());
|
|
|
|
| 32 |
|
| 33 |
Self { inner }
|
| 34 |
}
|
| 35 |
|
| 36 |
+
pub async fn list_jobs(&self) -> Vec<JobState> {
|
| 37 |
+
let mut jobs: Vec<_> = self.inner.jobs.read().await.values().cloned().collect();
|
| 38 |
+
jobs.sort_by(|a, b| a.id.cmp(&b.id));
|
| 39 |
+
jobs
|
| 40 |
}
|
| 41 |
|
| 42 |
+
pub async fn get_job(&self, job_id: &str) -> Option<JobState> {
|
| 43 |
+
self.inner.jobs.read().await.get(job_id).cloned()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
}
|
| 45 |
|
| 46 |
pub async fn publish_job(&self, job: JobState) {
|
| 47 |
update_job_state(&self.inner, job).await;
|
| 48 |
}
|
|
|
|
| 49 |
|
| 50 |
+
pub async fn list_downloads(&self) -> Vec<DownloadState> {
|
| 51 |
+
let mut downloads: Vec<_> = self.inner.downloads.read().await.values().cloned().collect();
|
| 52 |
+
downloads.sort_by(|a, b| a.filename.cmp(&b.filename));
|
| 53 |
+
downloads
|
| 54 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
}
|
| 56 |
|
| 57 |
fn spawn_pipeline_listener(inner: Arc<Inner>) {
|
|
|
|
| 62 |
Ok(progress) => {
|
| 63 |
update_job_state(&inner, pipeline_job_state(progress)).await;
|
| 64 |
}
|
| 65 |
+
Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => continue,
|
| 66 |
+
Err(tokio::sync::broadcast::error::RecvError::Closed) => break,
|
| 67 |
}
|
| 68 |
}
|
| 69 |
});
|
| 70 |
}
|
| 71 |
|
| 72 |
+
fn spawn_download_listener(inner: Arc<Inner>, shared: SharedState) {
|
| 73 |
tokio::spawn(async move {
|
| 74 |
+
let mut runtime_rx = shared.subscribe_runtime();
|
| 75 |
let mut download_rx = runtime_rx.borrow().clone().subscribe_downloads();
|
| 76 |
|
| 77 |
loop {
|
|
|
|
| 87 |
Ok(progress) => {
|
| 88 |
update_download_state(&inner, download_state(progress)).await;
|
| 89 |
}
|
| 90 |
+
Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => continue,
|
| 91 |
+
Err(tokio::sync::broadcast::error::RecvError::Closed) => {
|
| 92 |
download_rx = runtime_rx.borrow().clone().subscribe_downloads();
|
| 93 |
}
|
| 94 |
}
|
|
|
|
| 97 |
});
|
| 98 |
}
|
| 99 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 100 |
async fn update_job_state(inner: &Arc<Inner>, job: JobState) {
|
| 101 |
let terminal = !matches!(job.status, JobStatus::Running);
|
| 102 |
+
let mut jobs = inner.jobs.write().await;
|
| 103 |
+
if terminal {
|
| 104 |
+
jobs.remove(&job.id);
|
| 105 |
+
} else {
|
| 106 |
+
jobs.insert(job.id.clone(), job);
|
|
|
|
|
|
|
| 107 |
}
|
|
|
|
| 108 |
}
|
| 109 |
|
| 110 |
async fn update_download_state(inner: &Arc<Inner>, download: DownloadState) {
|
|
|
|
| 112 |
download.status,
|
| 113 |
TransferStatus::Started | TransferStatus::Downloading
|
| 114 |
);
|
| 115 |
+
let mut downloads = inner.downloads.write().await;
|
| 116 |
+
if terminal {
|
| 117 |
+
downloads.remove(&download.id);
|
| 118 |
+
} else {
|
| 119 |
+
downloads.insert(download.id.clone(), download);
|
|
|
|
|
|
|
| 120 |
}
|
|
|
|
| 121 |
}
|
| 122 |
|
| 123 |
fn pipeline_job_state(progress: PipelineProgress) -> JobState {
|
|
|
|
| 164 |
}
|
| 165 |
}
|
| 166 |
|
|
|
|
|
|
|
| 167 |
#[cfg(test)]
|
| 168 |
mod tests {
|
| 169 |
use koharu_core::{PipelineStatus, PipelineStep};
|
ui/app/(app)/about/page.tsx
CHANGED
|
@@ -11,8 +11,7 @@ import {
|
|
| 11 |
} from 'lucide-react'
|
| 12 |
import { ScrollArea } from '@/components/ui/scroll-area'
|
| 13 |
import { isTauri } from '@/lib/backend'
|
| 14 |
-
import {
|
| 15 |
-
import { useDocumentMutations } from '@/lib/query/mutations'
|
| 16 |
import Image from 'next/image'
|
| 17 |
|
| 18 |
const GITHUB_REPO = 'mayocream/koharu'
|
|
@@ -21,7 +20,9 @@ type VersionStatus = 'loading' | 'latest' | 'outdated' | 'error'
|
|
| 21 |
|
| 22 |
export default function AboutPage() {
|
| 23 |
const { t } = useTranslation()
|
| 24 |
-
const
|
|
|
|
|
|
|
| 25 |
|
| 26 |
const [appVersion, setAppVersion] = useState<string>()
|
| 27 |
const [latestVersion, setLatestVersion] = useState<string>()
|
|
@@ -31,7 +32,8 @@ export default function AboutPage() {
|
|
| 31 |
const checkVersion = async () => {
|
| 32 |
try {
|
| 33 |
if (isTauri()) {
|
| 34 |
-
const
|
|
|
|
| 35 |
setAppVersion(version)
|
| 36 |
|
| 37 |
const res = await fetch(
|
|
|
|
| 11 |
} from 'lucide-react'
|
| 12 |
import { ScrollArea } from '@/components/ui/scroll-area'
|
| 13 |
import { isTauri } from '@/lib/backend'
|
| 14 |
+
import { getMeta } from '@/lib/api/system/system'
|
|
|
|
| 15 |
import Image from 'next/image'
|
| 16 |
|
| 17 |
const GITHUB_REPO = 'mayocream/koharu'
|
|
|
|
| 20 |
|
| 21 |
export default function AboutPage() {
|
| 22 |
const { t } = useTranslation()
|
| 23 |
+
const openExternal = (url: string) => {
|
| 24 |
+
window.open(url, '_blank', 'noopener,noreferrer')
|
| 25 |
+
}
|
| 26 |
|
| 27 |
const [appVersion, setAppVersion] = useState<string>()
|
| 28 |
const [latestVersion, setLatestVersion] = useState<string>()
|
|
|
|
| 32 |
const checkVersion = async () => {
|
| 33 |
try {
|
| 34 |
if (isTauri()) {
|
| 35 |
+
const meta = await getMeta()
|
| 36 |
+
const version = meta.version
|
| 37 |
setAppVersion(version)
|
| 38 |
|
| 39 |
const res = await fetch(
|
ui/app/(app)/settings/page.tsx
CHANGED
|
@@ -10,12 +10,8 @@ import {
|
|
| 10 |
MonitorIcon,
|
| 11 |
ChevronLeftIcon,
|
| 12 |
ChevronRightIcon,
|
| 13 |
-
ChevronDownIcon,
|
| 14 |
EyeIcon,
|
| 15 |
EyeOffIcon,
|
| 16 |
-
CheckCircleIcon,
|
| 17 |
-
XCircleIcon,
|
| 18 |
-
LoaderIcon,
|
| 19 |
} from 'lucide-react'
|
| 20 |
import { ScrollArea } from '@/components/ui/scroll-area'
|
| 21 |
import {
|
|
@@ -26,14 +22,11 @@ import {
|
|
| 26 |
SelectValue,
|
| 27 |
} from '@/components/ui/select'
|
| 28 |
import { isTauri } from '@/lib/backend'
|
| 29 |
-
import {
|
| 30 |
-
import {
|
| 31 |
-
|
| 32 |
-
getActivePresetConfig,
|
| 33 |
-
type LocalLlmPreset,
|
| 34 |
-
} from '@/lib/stores/preferencesStore'
|
| 35 |
import { supportedLanguages } from '@/lib/i18n'
|
| 36 |
-
import type { BootstrapConfig } from '@/lib/
|
| 37 |
|
| 38 |
const THEME_OPTIONS = [
|
| 39 |
{ value: 'light', icon: SunIcon, labelKey: 'settings.themeLight' },
|
|
@@ -54,16 +47,6 @@ const API_PROVIDERS: ApiProvider[] = [
|
|
| 54 |
{ id: 'deepseek', name: 'DeepSeek', free_tier: false },
|
| 55 |
]
|
| 56 |
|
| 57 |
-
const PRESET_BUTTONS: { value: LocalLlmPreset; labelKey: string }[] = [
|
| 58 |
-
{ value: 'ollama', labelKey: 'settings.localLlmPresetOllama' },
|
| 59 |
-
{ value: 'lmstudio', labelKey: 'settings.localLlmPresetLmStudio' },
|
| 60 |
-
{ value: 'preset1', labelKey: 'settings.localLlmPresetPreset1' },
|
| 61 |
-
{ value: 'preset2', labelKey: 'settings.localLlmPresetPreset2' },
|
| 62 |
-
]
|
| 63 |
-
|
| 64 |
-
const DEFAULT_SYSTEM_PROMPT =
|
| 65 |
-
'You are a professional manga translator. Translate Japanese manga dialogue into natural {target_language} that fits inside speech bubbles. Preserve character voice, emotional tone, relationship nuance, emphasis, and sound effects naturally. Keep the wording concise. Do not add notes, explanations, or romanization. If the input contains <block id="N">...</block>, translate only the text inside each block. Keep every block tag exactly unchanged, including ids, order, and block count. Do not merge blocks, split blocks, or add any text outside the blocks.'
|
| 66 |
-
|
| 67 |
const inputClass =
|
| 68 |
'border-border bg-card text-foreground placeholder:text-muted-foreground focus:ring-primary w-full rounded-md border px-3 py-1.5 text-sm focus:ring-1 focus:outline-none'
|
| 69 |
|
|
@@ -74,9 +57,6 @@ export default function SettingsPage() {
|
|
| 74 |
const [deviceInfo, setDeviceInfo] = useState<{ mlDevice: string }>()
|
| 75 |
const apiKeys = usePreferencesStore((state) => state.apiKeys)
|
| 76 |
const setApiKey = usePreferencesStore((state) => state.setApiKey)
|
| 77 |
-
const localLlm = usePreferencesStore((state) => state.localLlm)
|
| 78 |
-
const setLocalLlm = usePreferencesStore((state) => state.setLocalLlm)
|
| 79 |
-
const setActivePreset = usePreferencesStore((state) => state.setActivePreset)
|
| 80 |
const [visibleKeys, setVisibleKeys] = useState<Record<string, boolean>>({})
|
| 81 |
const saveTimersRef = useRef<Record<string, ReturnType<typeof setTimeout>>>(
|
| 82 |
{},
|
|
@@ -85,23 +65,16 @@ export default function SettingsPage() {
|
|
| 85 |
const proxySaveTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
| 86 |
const pendingBootstrapConfigRef = useRef<BootstrapConfig | null>(null)
|
| 87 |
|
| 88 |
-
const [showAdvanced, setShowAdvanced] = useState(false)
|
| 89 |
-
const [pingState, setPingState] = useState<{
|
| 90 |
-
loading: boolean
|
| 91 |
-
result?: { ok: boolean; count: number; latency: number; error?: string }
|
| 92 |
-
}>({ loading: false })
|
| 93 |
const [bootstrapConfig, setBootstrapConfig] =
|
| 94 |
useState<BootstrapConfig | null>(null)
|
| 95 |
|
| 96 |
-
const activeConfig = getActivePresetConfig(localLlm)
|
| 97 |
-
|
| 98 |
useEffect(() => {
|
| 99 |
if (!isTauri()) return
|
| 100 |
|
| 101 |
const loadDeviceInfo = async () => {
|
| 102 |
try {
|
| 103 |
-
const
|
| 104 |
-
setDeviceInfo(
|
| 105 |
} catch (error) {
|
| 106 |
console.error('Failed to load device info', error)
|
| 107 |
}
|
|
@@ -113,7 +86,7 @@ export default function SettingsPage() {
|
|
| 113 |
useEffect(() => {
|
| 114 |
const loadBootstrapConfig = async () => {
|
| 115 |
try {
|
| 116 |
-
const config = await
|
| 117 |
setBootstrapConfig(config)
|
| 118 |
} catch (error) {
|
| 119 |
console.error('Failed to load bootstrap config', error)
|
|
@@ -125,7 +98,7 @@ export default function SettingsPage() {
|
|
| 125 |
|
| 126 |
const persistApiKey = async (provider: string, value: string) => {
|
| 127 |
try {
|
| 128 |
-
await
|
| 129 |
} catch (error) {
|
| 130 |
console.error(`Failed to save API key for ${provider}`, error)
|
| 131 |
}
|
|
@@ -133,7 +106,7 @@ export default function SettingsPage() {
|
|
| 133 |
|
| 134 |
const persistBootstrapConfig = async (nextConfig: BootstrapConfig) => {
|
| 135 |
try {
|
| 136 |
-
const saved = await
|
| 137 |
setBootstrapConfig(saved)
|
| 138 |
} catch (error) {
|
| 139 |
console.error('Failed to save bootstrap config', error)
|
|
@@ -219,40 +192,6 @@ export default function SettingsPage() {
|
|
| 219 |
}, 300)
|
| 220 |
}
|
| 221 |
|
| 222 |
-
const handleTestConnection = async () => {
|
| 223 |
-
setPingState({ loading: true })
|
| 224 |
-
try {
|
| 225 |
-
const result = await api.llmPing(
|
| 226 |
-
activeConfig.baseUrl,
|
| 227 |
-
activeConfig.apiKey || undefined,
|
| 228 |
-
)
|
| 229 |
-
setPingState({
|
| 230 |
-
loading: false,
|
| 231 |
-
result: {
|
| 232 |
-
ok: result.ok,
|
| 233 |
-
count: result.models.length,
|
| 234 |
-
latency: result.latencyMs ?? 0,
|
| 235 |
-
error: result.error,
|
| 236 |
-
},
|
| 237 |
-
})
|
| 238 |
-
} catch (error) {
|
| 239 |
-
setPingState({
|
| 240 |
-
loading: false,
|
| 241 |
-
result: {
|
| 242 |
-
ok: false,
|
| 243 |
-
count: 0,
|
| 244 |
-
latency: 0,
|
| 245 |
-
error: String(error),
|
| 246 |
-
},
|
| 247 |
-
})
|
| 248 |
-
}
|
| 249 |
-
}
|
| 250 |
-
|
| 251 |
-
const handlePresetChange = (preset: LocalLlmPreset) => {
|
| 252 |
-
setActivePreset(preset)
|
| 253 |
-
setPingState({ loading: false })
|
| 254 |
-
}
|
| 255 |
-
|
| 256 |
return (
|
| 257 |
<div className='bg-muted flex min-h-0 flex-1 flex-col overflow-hidden'>
|
| 258 |
<ScrollArea className='min-h-0 flex-1' viewportClassName='h-full'>
|
|
@@ -427,247 +366,6 @@ export default function SettingsPage() {
|
|
| 427 |
</div>
|
| 428 |
</section>
|
| 429 |
|
| 430 |
-
{/* Local LLM & OpenAI Compatible Providers Section */}
|
| 431 |
-
<section className='mb-8'>
|
| 432 |
-
<h2 className='text-foreground mb-1 text-sm font-bold'>
|
| 433 |
-
{t('settings.localLlmTitle')}
|
| 434 |
-
</h2>
|
| 435 |
-
<p className='text-muted-foreground mb-4 text-sm'>
|
| 436 |
-
{t('settings.localLlmDescription')}
|
| 437 |
-
</p>
|
| 438 |
-
|
| 439 |
-
<div className='space-y-3'>
|
| 440 |
-
{/* Provider Preset */}
|
| 441 |
-
<div className='space-y-1'>
|
| 442 |
-
<label className='text-foreground text-sm'>
|
| 443 |
-
{t('settings.localLlmPreset')}
|
| 444 |
-
</label>
|
| 445 |
-
<div className='grid grid-cols-4 gap-2'>
|
| 446 |
-
{PRESET_BUTTONS.map(({ value, labelKey }) => (
|
| 447 |
-
<button
|
| 448 |
-
key={value}
|
| 449 |
-
onClick={() => handlePresetChange(value)}
|
| 450 |
-
data-active={localLlm.activePreset === value}
|
| 451 |
-
className='border-border bg-card text-muted-foreground hover:border-foreground/30 data-[active=true]:border-primary data-[active=true]:text-foreground rounded-lg border px-3 py-2 text-sm font-medium transition'
|
| 452 |
-
>
|
| 453 |
-
{t(labelKey)}
|
| 454 |
-
</button>
|
| 455 |
-
))}
|
| 456 |
-
</div>
|
| 457 |
-
</div>
|
| 458 |
-
|
| 459 |
-
{/* Base URL */}
|
| 460 |
-
<div className='space-y-1'>
|
| 461 |
-
<label className='text-foreground text-sm'>
|
| 462 |
-
{t('settings.localLlmBaseUrl')}
|
| 463 |
-
</label>
|
| 464 |
-
<input
|
| 465 |
-
type='url'
|
| 466 |
-
value={activeConfig.baseUrl}
|
| 467 |
-
onChange={(e) => setLocalLlm({ baseUrl: e.target.value })}
|
| 468 |
-
placeholder='http://127.0.0.1:1234/v1'
|
| 469 |
-
className={inputClass}
|
| 470 |
-
/>
|
| 471 |
-
</div>
|
| 472 |
-
|
| 473 |
-
{/* API Key */}
|
| 474 |
-
<div className='space-y-1'>
|
| 475 |
-
<label className='text-foreground text-sm'>
|
| 476 |
-
{t('settings.localLlmApiKey')}
|
| 477 |
-
</label>
|
| 478 |
-
<div className='relative'>
|
| 479 |
-
<input
|
| 480 |
-
type={
|
| 481 |
-
visibleKeys[`llm-${localLlm.activePreset}`]
|
| 482 |
-
? 'text'
|
| 483 |
-
: 'password'
|
| 484 |
-
}
|
| 485 |
-
value={activeConfig.apiKey}
|
| 486 |
-
onChange={(e) => setLocalLlm({ apiKey: e.target.value })}
|
| 487 |
-
placeholder='API key'
|
| 488 |
-
className={`${inputClass} pr-9`}
|
| 489 |
-
/>
|
| 490 |
-
<button
|
| 491 |
-
type='button'
|
| 492 |
-
onClick={() =>
|
| 493 |
-
setVisibleKeys((v) => ({
|
| 494 |
-
...v,
|
| 495 |
-
[`llm-${localLlm.activePreset}`]:
|
| 496 |
-
!v[`llm-${localLlm.activePreset}`],
|
| 497 |
-
}))
|
| 498 |
-
}
|
| 499 |
-
className='text-muted-foreground hover:text-foreground absolute top-1/2 right-2.5 -translate-y-1/2 transition'
|
| 500 |
-
>
|
| 501 |
-
{visibleKeys[`llm-${localLlm.activePreset}`] ? (
|
| 502 |
-
<EyeOffIcon className='size-4' />
|
| 503 |
-
) : (
|
| 504 |
-
<EyeIcon className='size-4' />
|
| 505 |
-
)}
|
| 506 |
-
</button>
|
| 507 |
-
</div>
|
| 508 |
-
</div>
|
| 509 |
-
|
| 510 |
-
{/* Model Name */}
|
| 511 |
-
<div className='space-y-1'>
|
| 512 |
-
<label className='text-foreground text-sm'>
|
| 513 |
-
{t('settings.localLlmModelName')}
|
| 514 |
-
</label>
|
| 515 |
-
<input
|
| 516 |
-
type='text'
|
| 517 |
-
value={activeConfig.modelName}
|
| 518 |
-
onChange={(e) => setLocalLlm({ modelName: e.target.value })}
|
| 519 |
-
placeholder={t('settings.localLlmModelNamePlaceholder')}
|
| 520 |
-
className={inputClass}
|
| 521 |
-
/>
|
| 522 |
-
</div>
|
| 523 |
-
|
| 524 |
-
{/* Advanced Section Toggle */}
|
| 525 |
-
<button
|
| 526 |
-
type='button'
|
| 527 |
-
onClick={() => setShowAdvanced((v) => !v)}
|
| 528 |
-
className='text-muted-foreground hover:text-foreground flex items-center gap-1 text-sm transition'
|
| 529 |
-
>
|
| 530 |
-
<ChevronDownIcon
|
| 531 |
-
className={`size-4 transition-transform ${showAdvanced ? 'rotate-180' : ''}`}
|
| 532 |
-
/>
|
| 533 |
-
{t('settings.localLlmAdvanced')}
|
| 534 |
-
</button>
|
| 535 |
-
|
| 536 |
-
{showAdvanced && (
|
| 537 |
-
<div className='space-y-3 pl-1'>
|
| 538 |
-
{/* Temperature */}
|
| 539 |
-
<div className='space-y-1'>
|
| 540 |
-
<label className='text-foreground text-sm'>
|
| 541 |
-
{t('settings.localLlmTemperature')}
|
| 542 |
-
</label>
|
| 543 |
-
<input
|
| 544 |
-
type='number'
|
| 545 |
-
value={activeConfig.temperature ?? ''}
|
| 546 |
-
onChange={(e) =>
|
| 547 |
-
setLocalLlm({
|
| 548 |
-
temperature:
|
| 549 |
-
e.target.value === ''
|
| 550 |
-
? null
|
| 551 |
-
: parseFloat(e.target.value),
|
| 552 |
-
})
|
| 553 |
-
}
|
| 554 |
-
placeholder={t(
|
| 555 |
-
'settings.localLlmTemperaturePlaceholder',
|
| 556 |
-
)}
|
| 557 |
-
step={0.1}
|
| 558 |
-
min={0}
|
| 559 |
-
max={2}
|
| 560 |
-
className={inputClass}
|
| 561 |
-
/>
|
| 562 |
-
</div>
|
| 563 |
-
|
| 564 |
-
{/* Max Tokens */}
|
| 565 |
-
<div className='space-y-1'>
|
| 566 |
-
<label className='text-foreground text-sm'>
|
| 567 |
-
{t('settings.localLlmMaxTokens')}
|
| 568 |
-
</label>
|
| 569 |
-
<input
|
| 570 |
-
type='number'
|
| 571 |
-
value={activeConfig.maxTokens ?? ''}
|
| 572 |
-
onChange={(e) =>
|
| 573 |
-
setLocalLlm({
|
| 574 |
-
maxTokens:
|
| 575 |
-
e.target.value === ''
|
| 576 |
-
? null
|
| 577 |
-
: parseInt(e.target.value, 10),
|
| 578 |
-
})
|
| 579 |
-
}
|
| 580 |
-
placeholder={t('settings.localLlmMaxTokensPlaceholder')}
|
| 581 |
-
step={100}
|
| 582 |
-
min={1}
|
| 583 |
-
className={inputClass}
|
| 584 |
-
/>
|
| 585 |
-
</div>
|
| 586 |
-
|
| 587 |
-
{/* Custom System Prompt */}
|
| 588 |
-
<div className='space-y-1'>
|
| 589 |
-
<div className='flex items-center justify-between'>
|
| 590 |
-
<label className='text-foreground text-sm'>
|
| 591 |
-
{t('settings.localLlmSystemPrompt')}
|
| 592 |
-
</label>
|
| 593 |
-
{activeConfig.customSystemPrompt && (
|
| 594 |
-
<button
|
| 595 |
-
type='button'
|
| 596 |
-
onClick={() =>
|
| 597 |
-
setLocalLlm({ customSystemPrompt: '' })
|
| 598 |
-
}
|
| 599 |
-
className='text-primary text-xs hover:underline'
|
| 600 |
-
>
|
| 601 |
-
{t('settings.localLlmSystemPromptReset')}
|
| 602 |
-
</button>
|
| 603 |
-
)}
|
| 604 |
-
</div>
|
| 605 |
-
<textarea
|
| 606 |
-
value={activeConfig.customSystemPrompt}
|
| 607 |
-
onChange={(e) =>
|
| 608 |
-
setLocalLlm({
|
| 609 |
-
customSystemPrompt: e.target.value,
|
| 610 |
-
})
|
| 611 |
-
}
|
| 612 |
-
placeholder={DEFAULT_SYSTEM_PROMPT}
|
| 613 |
-
rows={4}
|
| 614 |
-
className={`${inputClass} resize-y`}
|
| 615 |
-
/>
|
| 616 |
-
<span className='text-muted-foreground text-xs'>
|
| 617 |
-
{t('settings.localLlmSystemPromptPlaceholder')}
|
| 618 |
-
</span>
|
| 619 |
-
</div>
|
| 620 |
-
</div>
|
| 621 |
-
)}
|
| 622 |
-
|
| 623 |
-
{/* Test Connection */}
|
| 624 |
-
<div className='space-y-2'>
|
| 625 |
-
<button
|
| 626 |
-
type='button'
|
| 627 |
-
onClick={handleTestConnection}
|
| 628 |
-
disabled={pingState.loading || !activeConfig.baseUrl.trim()}
|
| 629 |
-
className='border-border bg-card text-foreground hover:bg-accent disabled:text-muted-foreground inline-flex items-center gap-2 rounded-md border px-4 py-1.5 text-sm font-medium transition disabled:opacity-50'
|
| 630 |
-
>
|
| 631 |
-
{pingState.loading ? (
|
| 632 |
-
<>
|
| 633 |
-
<LoaderIcon className='size-4 animate-spin' />
|
| 634 |
-
{t('settings.localLlmTesting')}
|
| 635 |
-
</>
|
| 636 |
-
) : (
|
| 637 |
-
t('settings.localLlmTestConnection')
|
| 638 |
-
)}
|
| 639 |
-
</button>
|
| 640 |
-
|
| 641 |
-
{pingState.result && !pingState.loading && (
|
| 642 |
-
<div
|
| 643 |
-
className={`flex items-start gap-2 text-sm ${pingState.result.ok ? 'text-green-500' : 'text-red-500'}`}
|
| 644 |
-
>
|
| 645 |
-
{pingState.result.ok ? (
|
| 646 |
-
<>
|
| 647 |
-
<CheckCircleIcon className='mt-0.5 size-4 shrink-0' />
|
| 648 |
-
<span>
|
| 649 |
-
{t('settings.localLlmTestSuccess', {
|
| 650 |
-
count: pingState.result.count,
|
| 651 |
-
latency: pingState.result.latency,
|
| 652 |
-
})}
|
| 653 |
-
</span>
|
| 654 |
-
</>
|
| 655 |
-
) : (
|
| 656 |
-
<>
|
| 657 |
-
<XCircleIcon className='mt-0.5 size-4 shrink-0' />
|
| 658 |
-
<span>
|
| 659 |
-
{t('settings.localLlmTestFailed', {
|
| 660 |
-
error: pingState.result.error,
|
| 661 |
-
})}
|
| 662 |
-
</span>
|
| 663 |
-
</>
|
| 664 |
-
)}
|
| 665 |
-
</div>
|
| 666 |
-
)}
|
| 667 |
-
</div>
|
| 668 |
-
</div>
|
| 669 |
-
</section>
|
| 670 |
-
|
| 671 |
{/* Divider */}
|
| 672 |
<div className='border-border mb-8 border-t' />
|
| 673 |
|
|
|
|
| 10 |
MonitorIcon,
|
| 11 |
ChevronLeftIcon,
|
| 12 |
ChevronRightIcon,
|
|
|
|
| 13 |
EyeIcon,
|
| 14 |
EyeOffIcon,
|
|
|
|
|
|
|
|
|
|
| 15 |
} from 'lucide-react'
|
| 16 |
import { ScrollArea } from '@/components/ui/scroll-area'
|
| 17 |
import {
|
|
|
|
| 22 |
SelectValue,
|
| 23 |
} from '@/components/ui/select'
|
| 24 |
import { isTauri } from '@/lib/backend'
|
| 25 |
+
import { getMeta, getConfig, updateConfig } from '@/lib/api/system/system'
|
| 26 |
+
import { setApiKey as setApiKeyApi } from '@/lib/api/providers/providers'
|
| 27 |
+
import { usePreferencesStore } from '@/lib/stores/preferencesStore'
|
|
|
|
|
|
|
|
|
|
| 28 |
import { supportedLanguages } from '@/lib/i18n'
|
| 29 |
+
import type { BootstrapConfig } from '@/lib/api/schemas'
|
| 30 |
|
| 31 |
const THEME_OPTIONS = [
|
| 32 |
{ value: 'light', icon: SunIcon, labelKey: 'settings.themeLight' },
|
|
|
|
| 47 |
{ id: 'deepseek', name: 'DeepSeek', free_tier: false },
|
| 48 |
]
|
| 49 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
const inputClass =
|
| 51 |
'border-border bg-card text-foreground placeholder:text-muted-foreground focus:ring-primary w-full rounded-md border px-3 py-1.5 text-sm focus:ring-1 focus:outline-none'
|
| 52 |
|
|
|
|
| 57 |
const [deviceInfo, setDeviceInfo] = useState<{ mlDevice: string }>()
|
| 58 |
const apiKeys = usePreferencesStore((state) => state.apiKeys)
|
| 59 |
const setApiKey = usePreferencesStore((state) => state.setApiKey)
|
|
|
|
|
|
|
|
|
|
| 60 |
const [visibleKeys, setVisibleKeys] = useState<Record<string, boolean>>({})
|
| 61 |
const saveTimersRef = useRef<Record<string, ReturnType<typeof setTimeout>>>(
|
| 62 |
{},
|
|
|
|
| 65 |
const proxySaveTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
| 66 |
const pendingBootstrapConfigRef = useRef<BootstrapConfig | null>(null)
|
| 67 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 68 |
const [bootstrapConfig, setBootstrapConfig] =
|
| 69 |
useState<BootstrapConfig | null>(null)
|
| 70 |
|
|
|
|
|
|
|
| 71 |
useEffect(() => {
|
| 72 |
if (!isTauri()) return
|
| 73 |
|
| 74 |
const loadDeviceInfo = async () => {
|
| 75 |
try {
|
| 76 |
+
const meta = await getMeta()
|
| 77 |
+
setDeviceInfo({ mlDevice: meta.mlDevice })
|
| 78 |
} catch (error) {
|
| 79 |
console.error('Failed to load device info', error)
|
| 80 |
}
|
|
|
|
| 86 |
useEffect(() => {
|
| 87 |
const loadBootstrapConfig = async () => {
|
| 88 |
try {
|
| 89 |
+
const config = await getConfig()
|
| 90 |
setBootstrapConfig(config)
|
| 91 |
} catch (error) {
|
| 92 |
console.error('Failed to load bootstrap config', error)
|
|
|
|
| 98 |
|
| 99 |
const persistApiKey = async (provider: string, value: string) => {
|
| 100 |
try {
|
| 101 |
+
await setApiKeyApi(provider, { apiKey: value })
|
| 102 |
} catch (error) {
|
| 103 |
console.error(`Failed to save API key for ${provider}`, error)
|
| 104 |
}
|
|
|
|
| 106 |
|
| 107 |
const persistBootstrapConfig = async (nextConfig: BootstrapConfig) => {
|
| 108 |
try {
|
| 109 |
+
const saved = await updateConfig(nextConfig)
|
| 110 |
setBootstrapConfig(saved)
|
| 111 |
} catch (error) {
|
| 112 |
console.error('Failed to save bootstrap config', error)
|
|
|
|
| 192 |
}, 300)
|
| 193 |
}
|
| 194 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 195 |
return (
|
| 196 |
<div className='bg-muted flex min-h-0 flex-1 flex-col overflow-hidden'>
|
| 197 |
<ScrollArea className='min-h-0 flex-1' viewportClassName='h-full'>
|
|
|
|
| 366 |
</div>
|
| 367 |
</section>
|
| 368 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 369 |
{/* Divider */}
|
| 370 |
<div className='border-border mb-8 border-t' />
|
| 371 |
|
ui/app/bootstrap/page.tsx
CHANGED
|
@@ -18,10 +18,11 @@ import {
|
|
| 18 |
SelectTrigger,
|
| 19 |
SelectValue,
|
| 20 |
} from '@/components/ui/select'
|
| 21 |
-
import {
|
| 22 |
-
import {
|
|
|
|
|
|
|
| 23 |
import i18n, { supportedLanguages } from '@/lib/i18n'
|
| 24 |
-
import type { BootstrapConfig, DownloadState } from '@/lib/protocol'
|
| 25 |
|
| 26 |
const DEFAULT_CONFIG: BootstrapConfig = {
|
| 27 |
runtime: { path: '' },
|
|
@@ -77,9 +78,9 @@ export default function BootstrapPage() {
|
|
| 77 |
setError(null)
|
| 78 |
|
| 79 |
try {
|
| 80 |
-
const saved = await
|
| 81 |
setConfig(saved)
|
| 82 |
-
await
|
| 83 |
} catch (cause) {
|
| 84 |
setInitializing(false)
|
| 85 |
setFailed(true)
|
|
@@ -90,7 +91,7 @@ export default function BootstrapPage() {
|
|
| 90 |
useEffect(() => {
|
| 91 |
void (async () => {
|
| 92 |
try {
|
| 93 |
-
const saved = await
|
| 94 |
setConfig(saved)
|
| 95 |
} catch (cause) {
|
| 96 |
const message =
|
|
@@ -105,39 +106,33 @@ export default function BootstrapPage() {
|
|
| 105 |
}, [])
|
| 106 |
|
| 107 |
useEffect(() => {
|
| 108 |
-
const
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
|
|
|
| 130 |
}
|
| 131 |
-
})
|
| 132 |
|
| 133 |
-
|
| 134 |
-
updateDownload(progress)
|
| 135 |
-
})
|
| 136 |
-
|
| 137 |
-
return () => {
|
| 138 |
-
unsubscribeSnapshot()
|
| 139 |
-
unsubscribeDownload()
|
| 140 |
-
}
|
| 141 |
}, [])
|
| 142 |
|
| 143 |
const goNext = async () => {
|
|
|
|
| 18 |
SelectTrigger,
|
| 19 |
SelectValue,
|
| 20 |
} from '@/components/ui/select'
|
| 21 |
+
import { getConfig, updateConfig, initialize } from '@/lib/api/system/system'
|
| 22 |
+
import { listDownloads } from '@/lib/api/downloads/downloads'
|
| 23 |
+
import type { DownloadState } from '@/lib/api/schemas'
|
| 24 |
+
import type { BootstrapConfig } from '@/lib/api/schemas'
|
| 25 |
import i18n, { supportedLanguages } from '@/lib/i18n'
|
|
|
|
| 26 |
|
| 27 |
const DEFAULT_CONFIG: BootstrapConfig = {
|
| 28 |
runtime: { path: '' },
|
|
|
|
| 78 |
setError(null)
|
| 79 |
|
| 80 |
try {
|
| 81 |
+
const saved = await updateConfig(nextConfig)
|
| 82 |
setConfig(saved)
|
| 83 |
+
await initialize()
|
| 84 |
} catch (cause) {
|
| 85 |
setInitializing(false)
|
| 86 |
setFailed(true)
|
|
|
|
| 91 |
useEffect(() => {
|
| 92 |
void (async () => {
|
| 93 |
try {
|
| 94 |
+
const saved = await getConfig()
|
| 95 |
setConfig(saved)
|
| 96 |
} catch (cause) {
|
| 97 |
const message =
|
|
|
|
| 106 |
}, [])
|
| 107 |
|
| 108 |
useEffect(() => {
|
| 109 |
+
const interval = setInterval(async () => {
|
| 110 |
+
try {
|
| 111 |
+
const downloads = await listDownloads()
|
| 112 |
+
const active =
|
| 113 |
+
downloads.find(
|
| 114 |
+
(entry) =>
|
| 115 |
+
entry.status === 'started' || entry.status === 'downloading',
|
| 116 |
+
) ??
|
| 117 |
+
downloads
|
| 118 |
+
.slice()
|
| 119 |
+
.sort((left, right) => left.filename.localeCompare(right.filename))
|
| 120 |
+
.at(-1) ??
|
| 121 |
+
null
|
| 122 |
+
|
| 123 |
+
if (active) {
|
| 124 |
+
setDownload({
|
| 125 |
+
filename: active.filename,
|
| 126 |
+
percent: computePercent(active),
|
| 127 |
+
failed: active.status === 'failed',
|
| 128 |
+
})
|
| 129 |
+
}
|
| 130 |
+
} catch {
|
| 131 |
+
// Backend may not be ready yet during bootstrap
|
| 132 |
}
|
| 133 |
+
}, 1500)
|
| 134 |
|
| 135 |
+
return () => clearInterval(interval)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 136 |
}, [])
|
| 137 |
|
| 138 |
const goNext = async () => {
|
ui/app/providers.tsx
CHANGED
|
@@ -1,309 +1,74 @@
|
|
| 1 |
'use client'
|
| 2 |
|
| 3 |
-
import { useEffect,
|
| 4 |
import { I18nextProvider } from 'react-i18next'
|
| 5 |
import { ThemeProvider } from 'next-themes'
|
| 6 |
-
import {
|
| 7 |
-
import { usePathname } from 'next/navigation'
|
| 8 |
import ClientOnly from '@/components/ClientOnly'
|
| 9 |
import { TooltipProvider } from '@/components/ui/tooltip'
|
| 10 |
-
import {
|
| 11 |
-
|
| 12 |
-
getCurrentWindow,
|
| 13 |
-
listen,
|
| 14 |
-
subscribeDocumentChanged,
|
| 15 |
-
subscribeDocumentsChanged,
|
| 16 |
-
subscribeJobChanged,
|
| 17 |
-
subscribeLlmChanged,
|
| 18 |
-
subscribeSnapshot,
|
| 19 |
-
} from '@/lib/backend'
|
| 20 |
import i18n from '@/lib/i18n'
|
| 21 |
-
import { getQueryClient } from '@/lib/query/client'
|
| 22 |
-
import { queryKeys } from '@/lib/query/keys'
|
| 23 |
-
import { useApiKeyQuery, useDocumentsCountQuery } from '@/lib/query/hooks'
|
| 24 |
-
import { useDownloadStore } from '@/lib/downloads'
|
| 25 |
-
import { useEditorUiStore } from '@/lib/stores/editorUiStore'
|
| 26 |
-
import { useLlmUiStore } from '@/lib/stores/llmUiStore'
|
| 27 |
-
import { useOperationStore } from '@/lib/stores/operationStore'
|
| 28 |
import { usePreferencesStore } from '@/lib/stores/preferencesStore'
|
| 29 |
-
import {
|
| 30 |
-
import { useRpcConnection } from '@/hooks/useRpcConnection'
|
| 31 |
-
import type {
|
| 32 |
-
DocumentSummary,
|
| 33 |
-
JobState,
|
| 34 |
-
LlmState,
|
| 35 |
-
SnapshotEvent,
|
| 36 |
-
} from '@/lib/protocol'
|
| 37 |
-
|
| 38 |
-
function ProvidersBootstrap({ children }: { children: ReactNode }) {
|
| 39 |
-
const queryClient = useQueryClient()
|
| 40 |
-
const pathname = usePathname()
|
| 41 |
-
const isStartupRoute =
|
| 42 |
-
pathname === '/bootstrap' || pathname === '/splashscreen'
|
| 43 |
-
const hasConnectedRef = useRef(false)
|
| 44 |
-
const setTotalPages = useEditorUiStore((state) => state.setTotalPages)
|
| 45 |
-
const setApiKey = usePreferencesStore((state) => state.setApiKey)
|
| 46 |
-
const rpcConnected = useRpcConnection()
|
| 47 |
-
const shouldQueryApiKeys = rpcConnected && !isStartupRoute && isTauri()
|
| 48 |
-
const { data: documentsCount } = useDocumentsCountQuery(
|
| 49 |
-
rpcConnected && !isStartupRoute,
|
| 50 |
-
)
|
| 51 |
-
const openAiApiKeyQuery = useApiKeyQuery('openai', shouldQueryApiKeys)
|
| 52 |
-
const openAiCompatibleApiKeyQuery = useApiKeyQuery(
|
| 53 |
-
'openai-compatible',
|
| 54 |
-
shouldQueryApiKeys,
|
| 55 |
-
)
|
| 56 |
-
const geminiApiKeyQuery = useApiKeyQuery('gemini', shouldQueryApiKeys)
|
| 57 |
-
const claudeApiKeyQuery = useApiKeyQuery('claude', shouldQueryApiKeys)
|
| 58 |
-
const deepSeekApiKeyQuery = useApiKeyQuery('deepseek', shouldQueryApiKeys)
|
| 59 |
-
|
| 60 |
-
const applyDocumentsSnapshot = (documents: DocumentSummary[]) => {
|
| 61 |
-
const count = documents.length
|
| 62 |
-
useEditorUiStore.setState((state) => ({
|
| 63 |
-
totalPages: count,
|
| 64 |
-
currentDocumentIndex:
|
| 65 |
-
count === 0 ? 0 : Math.min(state.currentDocumentIndex, count - 1),
|
| 66 |
-
selectedBlockIndex: count === 0 ? undefined : state.selectedBlockIndex,
|
| 67 |
-
documentsVersion: state.documentsVersion + 1,
|
| 68 |
-
}))
|
| 69 |
-
queryClient.setQueryData(queryKeys.documents.count, count)
|
| 70 |
-
queryClient.invalidateQueries({
|
| 71 |
-
queryKey: queryKeys.documents.currentRoot,
|
| 72 |
-
})
|
| 73 |
-
queryClient.invalidateQueries({
|
| 74 |
-
queryKey: queryKeys.documents.thumbnailRoot,
|
| 75 |
-
})
|
| 76 |
-
}
|
| 77 |
-
|
| 78 |
-
const applyLlmSnapshot = (llm: LlmState) => {
|
| 79 |
-
const selectedModel = useLlmUiStore.getState().selectedModel
|
| 80 |
-
const isReady =
|
| 81 |
-
llm.status === 'ready' &&
|
| 82 |
-
(!selectedModel || !llm.modelId || llm.modelId === selectedModel)
|
| 83 |
-
queryClient.setQueryData(queryKeys.llm.ready(selectedModel), isReady)
|
| 84 |
-
useLlmUiStore.getState().setLoading(llm.status === 'loading')
|
| 85 |
-
|
| 86 |
-
if (llm.status !== 'loading') {
|
| 87 |
-
const operation = useOperationStore.getState().operation
|
| 88 |
-
if (operation?.type === 'llm-load') {
|
| 89 |
-
useOperationStore.getState().finishOperation()
|
| 90 |
-
getCurrentWindow()
|
| 91 |
-
.setProgressBar({
|
| 92 |
-
status: ProgressBarStatus.None,
|
| 93 |
-
progress: 0,
|
| 94 |
-
})
|
| 95 |
-
.catch(() => {})
|
| 96 |
-
}
|
| 97 |
-
}
|
| 98 |
-
}
|
| 99 |
-
|
| 100 |
-
const updatePipelineUi = (job: JobState | null) => {
|
| 101 |
-
const operationStore = useOperationStore.getState()
|
| 102 |
-
|
| 103 |
-
if (!job) {
|
| 104 |
-
return
|
| 105 |
-
}
|
| 106 |
-
|
| 107 |
-
if (job.status === 'running') {
|
| 108 |
-
const isSingleDoc = job.totalDocuments <= 1
|
| 109 |
-
operationStore.updateOperation({
|
| 110 |
-
step: job.step ?? undefined,
|
| 111 |
-
current: isSingleDoc
|
| 112 |
-
? job.currentStepIndex
|
| 113 |
-
: job.currentDocument +
|
| 114 |
-
(job.totalSteps > 0 ? job.currentStepIndex / job.totalSteps : 0),
|
| 115 |
-
total: isSingleDoc ? job.totalSteps : job.totalDocuments,
|
| 116 |
-
})
|
| 117 |
-
|
| 118 |
-
getCurrentWindow()
|
| 119 |
-
.setProgressBar({
|
| 120 |
-
status: ProgressBarStatus.Normal,
|
| 121 |
-
progress: job.overallPercent,
|
| 122 |
-
})
|
| 123 |
-
.catch(() => {})
|
| 124 |
-
return
|
| 125 |
-
}
|
| 126 |
-
|
| 127 |
-
operationStore.updateOperation({
|
| 128 |
-
current: operationStore.operation?.total,
|
| 129 |
-
total: operationStore.operation?.total,
|
| 130 |
-
})
|
| 131 |
-
|
| 132 |
-
getCurrentWindow()
|
| 133 |
-
.setProgressBar({ status: ProgressBarStatus.Normal, progress: 100 })
|
| 134 |
-
.catch(() => {})
|
| 135 |
-
|
| 136 |
-
queryClient.invalidateQueries({
|
| 137 |
-
queryKey: queryKeys.documents.currentRoot,
|
| 138 |
-
})
|
| 139 |
-
queryClient.invalidateQueries({
|
| 140 |
-
queryKey: queryKeys.documents.thumbnailRoot,
|
| 141 |
-
})
|
| 142 |
-
|
| 143 |
-
setTimeout(() => {
|
| 144 |
-
useOperationStore.getState().finishOperation()
|
| 145 |
-
getCurrentWindow()
|
| 146 |
-
.setProgressBar({
|
| 147 |
-
status: ProgressBarStatus.None,
|
| 148 |
-
progress: 0,
|
| 149 |
-
})
|
| 150 |
-
.catch(() => {})
|
| 151 |
-
}, 1000)
|
| 152 |
-
}
|
| 153 |
-
|
| 154 |
-
useEffect(() => {
|
| 155 |
-
if (!rpcConnected) return
|
| 156 |
-
|
| 157 |
-
if (hasConnectedRef.current) {
|
| 158 |
-
queryClient.invalidateQueries({ type: 'active' })
|
| 159 |
-
return
|
| 160 |
-
}
|
| 161 |
-
|
| 162 |
-
hasConnectedRef.current = true
|
| 163 |
-
}, [queryClient, rpcConnected])
|
| 164 |
-
|
| 165 |
-
useEffect(() => {
|
| 166 |
-
if (typeof documentsCount === 'number') {
|
| 167 |
-
setTotalPages(documentsCount)
|
| 168 |
-
}
|
| 169 |
-
}, [documentsCount, setTotalPages])
|
| 170 |
|
| 171 |
-
|
| 172 |
-
if (openAiApiKeyQuery.status === 'success') {
|
| 173 |
-
setApiKey('openai', openAiApiKeyQuery.data ?? '')
|
| 174 |
-
}
|
| 175 |
-
}, [openAiApiKeyQuery.data, openAiApiKeyQuery.status, setApiKey])
|
| 176 |
|
| 177 |
-
|
| 178 |
-
if (openAiCompatibleApiKeyQuery.status === 'success') {
|
| 179 |
-
setApiKey('openai-compatible', openAiCompatibleApiKeyQuery.data ?? '')
|
| 180 |
-
}
|
| 181 |
-
}, [
|
| 182 |
-
openAiCompatibleApiKeyQuery.data,
|
| 183 |
-
openAiCompatibleApiKeyQuery.status,
|
| 184 |
-
setApiKey,
|
| 185 |
-
])
|
| 186 |
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
setApiKey('gemini', geminiApiKeyQuery.data ?? '')
|
| 190 |
-
}
|
| 191 |
-
}, [geminiApiKeyQuery.data, geminiApiKeyQuery.status, setApiKey])
|
| 192 |
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
|
|
|
|
|
|
|
|
|
| 198 |
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 204 |
|
| 205 |
useEffect(() => {
|
| 206 |
-
if (
|
| 207 |
-
|
| 208 |
-
let unlisten: (() => void) | undefined
|
| 209 |
-
;(async () => {
|
| 210 |
-
try {
|
| 211 |
-
unlisten = await listen<number>('documents:opened', (event) => {
|
| 212 |
-
const count = event.payload ?? 0
|
| 213 |
-
setTotalPages(count)
|
| 214 |
-
queryClient.setQueryData(queryKeys.documents.count, count)
|
| 215 |
-
queryClient.invalidateQueries({
|
| 216 |
-
queryKey: queryKeys.documents.currentRoot,
|
| 217 |
-
})
|
| 218 |
-
queryClient.invalidateQueries({
|
| 219 |
-
queryKey: queryKeys.documents.thumbnailRoot,
|
| 220 |
-
})
|
| 221 |
-
})
|
| 222 |
-
} catch (_) {}
|
| 223 |
-
})()
|
| 224 |
|
| 225 |
-
|
| 226 |
-
applyDocumentsSnapshot(payload.documents)
|
| 227 |
-
applyLlmSnapshot(payload.llm)
|
| 228 |
-
const pipelineJob =
|
| 229 |
-
payload.jobs.find((job) => job.kind === 'pipeline') ?? null
|
| 230 |
-
updatePipelineUi(pipelineJob)
|
| 231 |
-
})
|
| 232 |
-
|
| 233 |
-
const unsubscribeDocuments = subscribeDocumentsChanged((payload) => {
|
| 234 |
-
applyDocumentsSnapshot(payload.documents)
|
| 235 |
-
})
|
| 236 |
-
|
| 237 |
-
const unsubscribeDocument = subscribeDocumentChanged(() => {
|
| 238 |
-
queryClient.invalidateQueries({
|
| 239 |
-
queryKey: queryKeys.documents.currentRoot,
|
| 240 |
-
})
|
| 241 |
-
queryClient.invalidateQueries({
|
| 242 |
-
queryKey: queryKeys.documents.thumbnailRoot,
|
| 243 |
-
})
|
| 244 |
-
})
|
| 245 |
-
|
| 246 |
-
const unsubscribeJobs = subscribeJobChanged((job) => {
|
| 247 |
-
if (job.kind !== 'pipeline') return
|
| 248 |
-
updatePipelineUi(job)
|
| 249 |
-
queryClient.invalidateQueries({
|
| 250 |
-
queryKey: queryKeys.documents.currentRoot,
|
| 251 |
-
})
|
| 252 |
-
queryClient.invalidateQueries({
|
| 253 |
-
queryKey: queryKeys.documents.thumbnailRoot,
|
| 254 |
-
})
|
| 255 |
-
})
|
| 256 |
-
|
| 257 |
-
const unsubscribeLlm = subscribeLlmChanged((llm) => {
|
| 258 |
-
applyLlmSnapshot(llm)
|
| 259 |
-
})
|
| 260 |
-
|
| 261 |
-
return () => {
|
| 262 |
-
unlisten?.()
|
| 263 |
-
unsubscribeSnapshot()
|
| 264 |
-
unsubscribeDocuments()
|
| 265 |
-
unsubscribeDocument()
|
| 266 |
-
unsubscribeJobs()
|
| 267 |
-
unsubscribeLlm()
|
| 268 |
-
}
|
| 269 |
-
}, [isStartupRoute, queryClient, setTotalPages])
|
| 270 |
-
|
| 271 |
-
return children
|
| 272 |
}
|
| 273 |
|
| 274 |
export function Providers({ children }: { children: ReactNode }) {
|
| 275 |
-
const queryClient = getQueryClient()
|
| 276 |
-
const ensureDownloadSubscribed = useDownloadStore(
|
| 277 |
-
(state) => state.ensureSubscribed,
|
| 278 |
-
)
|
| 279 |
-
|
| 280 |
useEffect(() => {
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
const handleLanguageChange = (lng: string) => {
|
| 286 |
-
document.documentElement.lang = lng
|
| 287 |
-
}
|
| 288 |
-
|
| 289 |
-
handleLanguageChange(i18n.language)
|
| 290 |
-
i18n.on('languageChanged', handleLanguageChange)
|
| 291 |
-
return () => {
|
| 292 |
-
i18n.off('languageChanged', handleLanguageChange)
|
| 293 |
-
}
|
| 294 |
}, [])
|
| 295 |
|
| 296 |
return (
|
| 297 |
<QueryClientProvider client={queryClient}>
|
| 298 |
-
<
|
| 299 |
-
<
|
| 300 |
-
<
|
|
|
|
| 301 |
<I18nextProvider i18n={i18n}>
|
| 302 |
<TooltipProvider delayDuration={0}>{children}</TooltipProvider>
|
| 303 |
</I18nextProvider>
|
| 304 |
-
</
|
| 305 |
-
</
|
| 306 |
-
</
|
| 307 |
</QueryClientProvider>
|
| 308 |
)
|
| 309 |
}
|
|
|
|
| 1 |
'use client'
|
| 2 |
|
| 3 |
+
import { useEffect, type ReactNode } from 'react'
|
| 4 |
import { I18nextProvider } from 'react-i18next'
|
| 5 |
import { ThemeProvider } from 'next-themes'
|
| 6 |
+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
|
|
|
| 7 |
import ClientOnly from '@/components/ClientOnly'
|
| 8 |
import { TooltipProvider } from '@/components/ui/tooltip'
|
| 9 |
+
import { isTauri } from '@/lib/backend'
|
| 10 |
+
import { useGetApiKey } from '@/lib/api/providers/providers'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
import i18n from '@/lib/i18n'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
import { usePreferencesStore } from '@/lib/stores/preferencesStore'
|
| 13 |
+
import { ProcessingProvider } from '@/lib/machines'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
|
| 15 |
+
const queryClient = new QueryClient()
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
|
| 17 |
+
const API_KEY_PROVIDERS = ['openai', 'openai-compatible', 'gemini', 'claude', 'deepseek'] as const
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
|
| 19 |
+
function ApiKeySyncer() {
|
| 20 |
+
const setApiKey = usePreferencesStore((state) => state.setApiKey)
|
|
|
|
|
|
|
|
|
|
| 21 |
|
| 22 |
+
return (
|
| 23 |
+
<>
|
| 24 |
+
{API_KEY_PROVIDERS.map((provider) => (
|
| 25 |
+
<ApiKeySync key={provider} provider={provider} setApiKey={setApiKey} />
|
| 26 |
+
))}
|
| 27 |
+
</>
|
| 28 |
+
)
|
| 29 |
+
}
|
| 30 |
|
| 31 |
+
function ApiKeySync({
|
| 32 |
+
provider,
|
| 33 |
+
setApiKey,
|
| 34 |
+
}: {
|
| 35 |
+
provider: string
|
| 36 |
+
setApiKey: (provider: string, key: string) => void
|
| 37 |
+
}) {
|
| 38 |
+
const { data, status } = useGetApiKey(provider, {
|
| 39 |
+
query: {
|
| 40 |
+
enabled: isTauri(),
|
| 41 |
+
select: (res: { apiKey?: string | null }) => res?.apiKey ?? '',
|
| 42 |
+
},
|
| 43 |
+
})
|
| 44 |
|
| 45 |
useEffect(() => {
|
| 46 |
+
if (status === 'success') setApiKey(provider, data ?? '')
|
| 47 |
+
}, [data, status, provider, setApiKey])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
|
| 49 |
+
return null
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
}
|
| 51 |
|
| 52 |
export function Providers({ children }: { children: ReactNode }) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
useEffect(() => {
|
| 54 |
+
const onLang = (lng: string) => { document.documentElement.lang = lng }
|
| 55 |
+
onLang(i18n.language)
|
| 56 |
+
i18n.on('languageChanged', onLang)
|
| 57 |
+
return () => { i18n.off('languageChanged', onLang) }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
}, [])
|
| 59 |
|
| 60 |
return (
|
| 61 |
<QueryClientProvider client={queryClient}>
|
| 62 |
+
<ProcessingProvider>
|
| 63 |
+
<ThemeProvider attribute='class' defaultTheme='system' enableSystem>
|
| 64 |
+
<ClientOnly>
|
| 65 |
+
<ApiKeySyncer />
|
| 66 |
<I18nextProvider i18n={i18n}>
|
| 67 |
<TooltipProvider delayDuration={0}>{children}</TooltipProvider>
|
| 68 |
</I18nextProvider>
|
| 69 |
+
</ClientOnly>
|
| 70 |
+
</ThemeProvider>
|
| 71 |
+
</ProcessingProvider>
|
| 72 |
</QueryClientProvider>
|
| 73 |
)
|
| 74 |
}
|
ui/app/splashscreen/page.tsx
CHANGED
|
@@ -1,9 +1,8 @@
|
|
| 1 |
'use client'
|
| 2 |
|
| 3 |
-
import { useEffect,
|
| 4 |
import { useTranslation } from 'react-i18next'
|
| 5 |
-
import {
|
| 6 |
-
import type { DownloadState } from '@/lib/protocol'
|
| 7 |
|
| 8 |
type AggregateProgress = {
|
| 9 |
filename: string
|
|
@@ -13,65 +12,43 @@ type AggregateProgress = {
|
|
| 13 |
export default function SplashScreen() {
|
| 14 |
const { t } = useTranslation()
|
| 15 |
const [progress, setProgress] = useState<AggregateProgress | null>(null)
|
| 16 |
-
const filesRef = useRef<Map<string, { downloaded: number; total: number }>>(
|
| 17 |
-
new Map(),
|
| 18 |
-
)
|
| 19 |
|
| 20 |
useEffect(() => {
|
| 21 |
-
const
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
files.set(msg.filename, { downloaded: 0, total: msg.total ?? 0 })
|
| 26 |
-
} else if (msg.status === 'downloading') {
|
| 27 |
-
const entry = files.get(msg.filename)
|
| 28 |
-
if (entry) {
|
| 29 |
-
entry.downloaded = msg.downloaded
|
| 30 |
-
if (msg.total) entry.total = msg.total
|
| 31 |
-
} else {
|
| 32 |
-
files.set(msg.filename, {
|
| 33 |
-
downloaded: msg.downloaded,
|
| 34 |
-
total: msg.total ?? 0,
|
| 35 |
-
})
|
| 36 |
-
}
|
| 37 |
-
} else {
|
| 38 |
-
// Completed or Failed — lock this file at 100%
|
| 39 |
-
const entry = files.get(msg.filename)
|
| 40 |
-
if (entry) {
|
| 41 |
-
entry.downloaded = entry.total
|
| 42 |
-
}
|
| 43 |
-
}
|
| 44 |
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
for (const entry of files.values()) {
|
| 49 |
-
totalBytes += entry.total
|
| 50 |
-
downloadedBytes += entry.downloaded
|
| 51 |
-
}
|
| 52 |
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
|
|
|
|
|
|
| 58 |
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
|
|
|
|
|
|
| 71 |
}
|
| 72 |
-
})
|
| 73 |
|
| 74 |
-
return () =>
|
| 75 |
}, [])
|
| 76 |
|
| 77 |
return (
|
|
|
|
| 1 |
'use client'
|
| 2 |
|
| 3 |
+
import { useEffect, useState } from 'react'
|
| 4 |
import { useTranslation } from 'react-i18next'
|
| 5 |
+
import { listDownloads } from '@/lib/api/downloads/downloads'
|
|
|
|
| 6 |
|
| 7 |
type AggregateProgress = {
|
| 8 |
filename: string
|
|
|
|
| 12 |
export default function SplashScreen() {
|
| 13 |
const { t } = useTranslation()
|
| 14 |
const [progress, setProgress] = useState<AggregateProgress | null>(null)
|
|
|
|
|
|
|
|
|
|
| 15 |
|
| 16 |
useEffect(() => {
|
| 17 |
+
const interval = setInterval(async () => {
|
| 18 |
+
try {
|
| 19 |
+
const downloads = await listDownloads()
|
| 20 |
+
if (downloads.length === 0) return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
|
| 22 |
+
let totalBytes = 0
|
| 23 |
+
let downloadedBytes = 0
|
| 24 |
+
let activeFilename: string | null = null
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
|
| 26 |
+
for (const d of downloads) {
|
| 27 |
+
totalBytes += d.total ?? 0
|
| 28 |
+
downloadedBytes += d.downloaded
|
| 29 |
+
if (d.status === 'started' || d.status === 'downloading') {
|
| 30 |
+
activeFilename = d.filename
|
| 31 |
+
}
|
| 32 |
+
}
|
| 33 |
|
| 34 |
+
const percent =
|
| 35 |
+
totalBytes > 0
|
| 36 |
+
? Math.min(100, Math.round((downloadedBytes / totalBytes) * 100))
|
| 37 |
+
: undefined
|
| 38 |
|
| 39 |
+
if (activeFilename) {
|
| 40 |
+
setProgress({ filename: activeFilename, percent })
|
| 41 |
+
} else {
|
| 42 |
+
setProgress((prev) =>
|
| 43 |
+
prev ? { ...prev, percent: percent ?? 100 } : null,
|
| 44 |
+
)
|
| 45 |
+
}
|
| 46 |
+
} catch {
|
| 47 |
+
// Backend may not be ready yet
|
| 48 |
}
|
| 49 |
+
}, 1500)
|
| 50 |
|
| 51 |
+
return () => clearInterval(interval)
|
| 52 |
}, [])
|
| 53 |
|
| 54 |
return (
|
ui/components/ActivityBubble.tsx
CHANGED
|
@@ -1,14 +1,13 @@
|
|
| 1 |
'use client'
|
| 2 |
|
| 3 |
-
import { type ReactNode
|
| 4 |
import { useTranslation } from 'react-i18next'
|
| 5 |
import { CircleXIcon } from 'lucide-react'
|
| 6 |
-
import {
|
|
|
|
| 7 |
import { Button } from '@/components/ui/button'
|
| 8 |
-
import {
|
| 9 |
-
import {
|
| 10 |
-
import { useUiErrorStore } from '@/lib/stores/uiErrorStore'
|
| 11 |
-
import { useDocumentMutations } from '@/lib/query/mutations'
|
| 12 |
|
| 13 |
type TranslateFunc = ReturnType<typeof useTranslation>['t']
|
| 14 |
|
|
@@ -115,40 +114,43 @@ function ErrorCard({
|
|
| 115 |
)
|
| 116 |
}
|
| 117 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 118 |
function OperationCard({
|
| 119 |
-
|
| 120 |
onCancel,
|
|
|
|
| 121 |
t,
|
| 122 |
}: {
|
| 123 |
-
|
| 124 |
onCancel: () => void
|
|
|
|
| 125 |
t: TranslateFunc
|
| 126 |
}) {
|
| 127 |
-
const
|
| 128 |
-
const hasProgressNumbers =
|
| 129 |
-
typeof operation.current === 'number' &&
|
| 130 |
-
typeof operation.total === 'number' &&
|
| 131 |
-
operation.total > 0
|
| 132 |
-
const currentValue = hasProgressNumbers ? operation.current : undefined
|
| 133 |
-
const total = hasProgressNumbers ? operation.total : undefined
|
| 134 |
const progress = clampProgress(
|
| 135 |
-
|
| 136 |
-
? (currentValue / total) * 100
|
| 137 |
-
: undefined,
|
| 138 |
)
|
| 139 |
-
const displayCurrent =
|
| 140 |
-
total
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
const titles: Record<OperationState['type'], string> = {
|
| 147 |
-
'load-khr': t('operations.loadKhr'),
|
| 148 |
-
'process-current': t('operations.processCurrent'),
|
| 149 |
-
'process-all': t('operations.processAll'),
|
| 150 |
-
'llm-load': t('operations.loadModel'),
|
| 151 |
-
}
|
| 152 |
const stepLabels: Record<string, string> = {
|
| 153 |
detect: t('processing.detect'),
|
| 154 |
ocr: t('processing.ocr'),
|
|
@@ -157,8 +159,8 @@ function OperationCard({
|
|
| 157 |
render: t('processing.render'),
|
| 158 |
}
|
| 159 |
|
| 160 |
-
const stepLabel =
|
| 161 |
-
? (stepLabels[
|
| 162 |
: undefined
|
| 163 |
const stepText =
|
| 164 |
stepLabel && total && typeof displayCurrent === 'number'
|
|
@@ -170,34 +172,25 @@ function OperationCard({
|
|
| 170 |
: undefined
|
| 171 |
|
| 172 |
const imageText =
|
| 173 |
-
|
| 174 |
-
total &&
|
| 175 |
-
typeof displayCurrent === 'number'
|
| 176 |
? t('operations.imageProgress', {
|
| 177 |
current: displayCurrent,
|
| 178 |
total,
|
| 179 |
})
|
| 180 |
: undefined
|
| 181 |
|
| 182 |
-
const subtitleParts =
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
: [imageText, stepText ?? stepLabel].filter(Boolean)
|
| 186 |
const subtitle =
|
| 187 |
subtitleParts.filter(Boolean).join(' \u00b7 ') || t('operations.inProgress')
|
| 188 |
|
|
|
|
|
|
|
| 189 |
return (
|
| 190 |
<BubbleCard>
|
| 191 |
<div
|
| 192 |
data-testid='operation-card'
|
| 193 |
-
data-operation-type={operation.type}
|
| 194 |
-
data-cancel-requested={operation.cancelRequested ? 'true' : 'false'}
|
| 195 |
-
data-current={
|
| 196 |
-
typeof operation.current === 'number' ? String(operation.current) : ''
|
| 197 |
-
}
|
| 198 |
-
data-total={
|
| 199 |
-
typeof operation.total === 'number' ? String(operation.total) : ''
|
| 200 |
-
}
|
| 201 |
className='flex items-start gap-3'
|
| 202 |
>
|
| 203 |
<div className='bg-primary mt-1 h-2.5 w-2.5 rounded-full shadow-[0_0_0_6px_hsl(var(--primary)/0.16)]' />
|
|
@@ -205,13 +198,13 @@ function OperationCard({
|
|
| 205 |
<div className='flex items-start justify-between gap-2'>
|
| 206 |
<div className='flex flex-col gap-1'>
|
| 207 |
<div className='text-foreground text-sm font-semibold'>
|
| 208 |
-
{
|
| 209 |
</div>
|
| 210 |
<div className='text-muted-foreground text-xs'>
|
| 211 |
{subtitle || t('operations.inProgress')}
|
| 212 |
</div>
|
| 213 |
</div>
|
| 214 |
-
{
|
| 215 |
<span className='bg-muted text-muted-foreground rounded-full px-2 py-0.5 text-[11px] font-medium'>
|
| 216 |
{t('operations.imageProgress', {
|
| 217 |
current: displayCurrent,
|
|
@@ -221,19 +214,16 @@ function OperationCard({
|
|
| 221 |
) : null}
|
| 222 |
</div>
|
| 223 |
<ProgressBar percent={progress} />
|
| 224 |
-
{
|
| 225 |
<div className='mt-3 flex justify-end'>
|
| 226 |
<Button
|
| 227 |
data-testid='operation-cancel'
|
| 228 |
variant='outline'
|
| 229 |
size='sm'
|
| 230 |
onClick={onCancel}
|
| 231 |
-
disabled={operation.cancelRequested}
|
| 232 |
className='text-xs font-semibold'
|
| 233 |
>
|
| 234 |
-
{
|
| 235 |
-
? t('operations.cancelling')
|
| 236 |
-
: t('operations.cancel')}
|
| 237 |
</Button>
|
| 238 |
</div>
|
| 239 |
)}
|
|
@@ -245,30 +235,46 @@ function OperationCard({
|
|
| 245 |
|
| 246 |
export function ActivityBubble() {
|
| 247 |
const { t } = useTranslation()
|
| 248 |
-
const
|
| 249 |
-
const
|
| 250 |
-
const
|
| 251 |
-
const { cancelOperation } = useDocumentMutations()
|
| 252 |
-
const downloads = useDownloadStore((s) => s.downloads)
|
| 253 |
-
const ensureSubscribed = useDownloadStore((s) => s.ensureSubscribed)
|
| 254 |
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
}
|
| 258 |
|
| 259 |
-
const
|
| 260 |
-
|
| 261 |
-
)
|
| 262 |
|
| 263 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 264 |
|
| 265 |
return (
|
| 266 |
<div className='pointer-events-auto fixed right-6 bottom-6 z-100 flex w-80 max-w-[calc(100%-1.5rem)] flex-col gap-3'>
|
| 267 |
-
{
|
| 268 |
-
<ErrorCard message={
|
| 269 |
)}
|
| 270 |
-
{
|
| 271 |
-
<OperationCard
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 272 |
)}
|
| 273 |
{activeDownloads.map((d) => (
|
| 274 |
<DownloadCard
|
|
|
|
| 1 |
'use client'
|
| 2 |
|
| 3 |
+
import { type ReactNode } from 'react'
|
| 4 |
import { useTranslation } from 'react-i18next'
|
| 5 |
import { CircleXIcon } from 'lucide-react'
|
| 6 |
+
import { useListDownloads } from '@/lib/api/downloads/downloads'
|
| 7 |
+
import type { DownloadState } from '@/lib/api/schemas'
|
| 8 |
import { Button } from '@/components/ui/button'
|
| 9 |
+
import { useEditorUiStore } from '@/lib/stores/editorUiStore'
|
| 10 |
+
import { useProcessing } from '@/lib/machines'
|
|
|
|
|
|
|
| 11 |
|
| 12 |
type TranslateFunc = ReturnType<typeof useTranslation>['t']
|
| 13 |
|
|
|
|
| 114 |
)
|
| 115 |
}
|
| 116 |
|
| 117 |
+
/** Map machine state name to an operation type label key */
|
| 118 |
+
function getOperationTitle(
|
| 119 |
+
state: ReturnType<typeof useProcessing>['state'],
|
| 120 |
+
t: TranslateFunc,
|
| 121 |
+
): string {
|
| 122 |
+
if (state.matches('importing')) return t('operations.loadKhr')
|
| 123 |
+
if (state.matches('loadingLlm')) return t('operations.loadModel')
|
| 124 |
+
if (state.matches('pipeline')) {
|
| 125 |
+
const docId = state.context.documentId
|
| 126 |
+
return docId ? t('operations.processCurrent') : t('operations.processAll')
|
| 127 |
+
}
|
| 128 |
+
return t('operations.processCurrent')
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
function OperationCard({
|
| 132 |
+
machineState,
|
| 133 |
onCancel,
|
| 134 |
+
canCancel,
|
| 135 |
t,
|
| 136 |
}: {
|
| 137 |
+
machineState: ReturnType<typeof useProcessing>['state']
|
| 138 |
onCancel: () => void
|
| 139 |
+
canCancel: boolean
|
| 140 |
t: TranslateFunc
|
| 141 |
}) {
|
| 142 |
+
const ctx = machineState.context
|
| 143 |
+
const hasProgressNumbers = ctx.total > 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 144 |
const progress = clampProgress(
|
| 145 |
+
hasProgressNumbers ? (ctx.current / ctx.total) * 100 : undefined,
|
|
|
|
|
|
|
| 146 |
)
|
| 147 |
+
const displayCurrent = hasProgressNumbers
|
| 148 |
+
? Math.min(ctx.total, Math.floor(ctx.current) + (ctx.current >= ctx.total ? 0 : 1))
|
| 149 |
+
: undefined
|
| 150 |
+
const total = hasProgressNumbers ? ctx.total : undefined
|
| 151 |
+
|
| 152 |
+
const isPipelineAll = machineState.matches('pipeline') && !ctx.documentId
|
| 153 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 154 |
const stepLabels: Record<string, string> = {
|
| 155 |
detect: t('processing.detect'),
|
| 156 |
ocr: t('processing.ocr'),
|
|
|
|
| 159 |
render: t('processing.render'),
|
| 160 |
}
|
| 161 |
|
| 162 |
+
const stepLabel = ctx.step
|
| 163 |
+
? (stepLabels[ctx.step] ?? ctx.step)
|
| 164 |
: undefined
|
| 165 |
const stepText =
|
| 166 |
stepLabel && total && typeof displayCurrent === 'number'
|
|
|
|
| 172 |
: undefined
|
| 173 |
|
| 174 |
const imageText =
|
| 175 |
+
isPipelineAll && total && typeof displayCurrent === 'number'
|
|
|
|
|
|
|
| 176 |
? t('operations.imageProgress', {
|
| 177 |
current: displayCurrent,
|
| 178 |
total,
|
| 179 |
})
|
| 180 |
: undefined
|
| 181 |
|
| 182 |
+
const subtitleParts = isPipelineAll
|
| 183 |
+
? [stepLabel]
|
| 184 |
+
: [imageText, stepText ?? stepLabel].filter(Boolean)
|
|
|
|
| 185 |
const subtitle =
|
| 186 |
subtitleParts.filter(Boolean).join(' \u00b7 ') || t('operations.inProgress')
|
| 187 |
|
| 188 |
+
const title = getOperationTitle(machineState, t)
|
| 189 |
+
|
| 190 |
return (
|
| 191 |
<BubbleCard>
|
| 192 |
<div
|
| 193 |
data-testid='operation-card'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 194 |
className='flex items-start gap-3'
|
| 195 |
>
|
| 196 |
<div className='bg-primary mt-1 h-2.5 w-2.5 rounded-full shadow-[0_0_0_6px_hsl(var(--primary)/0.16)]' />
|
|
|
|
| 198 |
<div className='flex items-start justify-between gap-2'>
|
| 199 |
<div className='flex flex-col gap-1'>
|
| 200 |
<div className='text-foreground text-sm font-semibold'>
|
| 201 |
+
{title}
|
| 202 |
</div>
|
| 203 |
<div className='text-muted-foreground text-xs'>
|
| 204 |
{subtitle || t('operations.inProgress')}
|
| 205 |
</div>
|
| 206 |
</div>
|
| 207 |
+
{isPipelineAll && total && typeof displayCurrent === 'number' ? (
|
| 208 |
<span className='bg-muted text-muted-foreground rounded-full px-2 py-0.5 text-[11px] font-medium'>
|
| 209 |
{t('operations.imageProgress', {
|
| 210 |
current: displayCurrent,
|
|
|
|
| 214 |
) : null}
|
| 215 |
</div>
|
| 216 |
<ProgressBar percent={progress} />
|
| 217 |
+
{canCancel && (
|
| 218 |
<div className='mt-3 flex justify-end'>
|
| 219 |
<Button
|
| 220 |
data-testid='operation-cancel'
|
| 221 |
variant='outline'
|
| 222 |
size='sm'
|
| 223 |
onClick={onCancel}
|
|
|
|
| 224 |
className='text-xs font-semibold'
|
| 225 |
>
|
| 226 |
+
{t('operations.cancel')}
|
|
|
|
|
|
|
| 227 |
</Button>
|
| 228 |
</div>
|
| 229 |
)}
|
|
|
|
| 235 |
|
| 236 |
export function ActivityBubble() {
|
| 237 |
const { t } = useTranslation()
|
| 238 |
+
const { isProcessing, state: machineState, send, canCancel, error: machineError } = useProcessing()
|
| 239 |
+
const uiError = useEditorUiStore((state) => state.error)
|
| 240 |
+
const clearUiError = useEditorUiStore((state) => state.clearError)
|
|
|
|
|
|
|
|
|
|
| 241 |
|
| 242 |
+
const handleCancel = () => {
|
| 243 |
+
send({ type: 'CANCEL' })
|
| 244 |
+
}
|
| 245 |
|
| 246 |
+
const { data: allDownloads = [] } = useListDownloads({
|
| 247 |
+
query: { refetchInterval: 2000 },
|
| 248 |
+
})
|
| 249 |
|
| 250 |
+
const activeDownloads = allDownloads
|
| 251 |
+
.filter((d) => d.status === 'started' || d.status === 'downloading')
|
| 252 |
+
.map((d) => ({
|
| 253 |
+
...d,
|
| 254 |
+
percent:
|
| 255 |
+
d.total && d.total > 0
|
| 256 |
+
? Math.round((d.downloaded / d.total) * 100)
|
| 257 |
+
: undefined,
|
| 258 |
+
}))
|
| 259 |
+
|
| 260 |
+
// Show machine errors as well as UI errors
|
| 261 |
+
const errorMessage = machineError ?? uiError?.message
|
| 262 |
+
const clearError = machineError ? () => { /* machine clears on next operation */ } : clearUiError
|
| 263 |
+
|
| 264 |
+
if (!errorMessage && !isProcessing && activeDownloads.length === 0) return null
|
| 265 |
|
| 266 |
return (
|
| 267 |
<div className='pointer-events-auto fixed right-6 bottom-6 z-100 flex w-80 max-w-[calc(100%-1.5rem)] flex-col gap-3'>
|
| 268 |
+
{errorMessage && (
|
| 269 |
+
<ErrorCard message={errorMessage} onDismiss={clearError} t={t} />
|
| 270 |
)}
|
| 271 |
+
{isProcessing && (
|
| 272 |
+
<OperationCard
|
| 273 |
+
machineState={machineState}
|
| 274 |
+
onCancel={handleCancel}
|
| 275 |
+
canCancel={canCancel}
|
| 276 |
+
t={t}
|
| 277 |
+
/>
|
| 278 |
)}
|
| 279 |
{activeDownloads.map((d) => (
|
| 280 |
<DownloadCard
|
ui/components/AppErrorBoundary.tsx
CHANGED
|
@@ -1,14 +1,13 @@
|
|
| 1 |
'use client'
|
| 2 |
|
| 3 |
import { type ReactNode } from 'react'
|
|
|
|
| 4 |
import { ErrorBoundary, type FallbackProps } from 'react-error-boundary'
|
| 5 |
import { Button } from '@/components/ui/button'
|
| 6 |
-
import { getQueryClient } from '@/lib/query/client'
|
| 7 |
import { useEditorUiStore } from '@/lib/stores/editorUiStore'
|
| 8 |
-
import { useLlmUiStore } from '@/lib/stores/llmUiStore'
|
| 9 |
-
import { useOperationStore } from '@/lib/stores/operationStore'
|
| 10 |
|
| 11 |
function ErrorFallback({ error, resetErrorBoundary }: FallbackProps) {
|
|
|
|
| 12 |
const errorMessage =
|
| 13 |
error instanceof Error ? error.message : 'Unexpected error'
|
| 14 |
|
|
@@ -27,8 +26,6 @@ function ErrorFallback({ error, resetErrorBoundary }: FallbackProps) {
|
|
| 27 |
variant='outline'
|
| 28 |
onClick={() => {
|
| 29 |
useEditorUiStore.getState().resetUiState()
|
| 30 |
-
useLlmUiStore.getState().resetLlmUiState()
|
| 31 |
-
useOperationStore.getState().resetOperationState()
|
| 32 |
resetErrorBoundary()
|
| 33 |
}}
|
| 34 |
>
|
|
@@ -38,7 +35,7 @@ function ErrorFallback({ error, resetErrorBoundary }: FallbackProps) {
|
|
| 38 |
size='sm'
|
| 39 |
variant='outline'
|
| 40 |
onClick={() => {
|
| 41 |
-
|
| 42 |
resetErrorBoundary()
|
| 43 |
}}
|
| 44 |
>
|
|
|
|
| 1 |
'use client'
|
| 2 |
|
| 3 |
import { type ReactNode } from 'react'
|
| 4 |
+
import { useQueryClient } from '@tanstack/react-query'
|
| 5 |
import { ErrorBoundary, type FallbackProps } from 'react-error-boundary'
|
| 6 |
import { Button } from '@/components/ui/button'
|
|
|
|
| 7 |
import { useEditorUiStore } from '@/lib/stores/editorUiStore'
|
|
|
|
|
|
|
| 8 |
|
| 9 |
function ErrorFallback({ error, resetErrorBoundary }: FallbackProps) {
|
| 10 |
+
const queryClient = useQueryClient()
|
| 11 |
const errorMessage =
|
| 12 |
error instanceof Error ? error.message : 'Unexpected error'
|
| 13 |
|
|
|
|
| 26 |
variant='outline'
|
| 27 |
onClick={() => {
|
| 28 |
useEditorUiStore.getState().resetUiState()
|
|
|
|
|
|
|
| 29 |
resetErrorBoundary()
|
| 30 |
}}
|
| 31 |
>
|
|
|
|
| 35 |
size='sm'
|
| 36 |
variant='outline'
|
| 37 |
onClick={() => {
|
| 38 |
+
queryClient.clear()
|
| 39 |
resetErrorBoundary()
|
| 40 |
}}
|
| 41 |
>
|
ui/components/MenuBar.tsx
CHANGED
|
@@ -3,8 +3,31 @@
|
|
| 3 |
import { useCallback, useEffect, useState } from 'react'
|
| 4 |
import Link from 'next/link'
|
| 5 |
import { MinusIcon, SquareIcon, XIcon, CopyIcon } from 'lucide-react'
|
| 6 |
-
import { isTauri
|
| 7 |
import { useTranslation } from 'react-i18next'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
import { fitCanvasToViewport, resetCanvasScale } from '@/components/Canvas'
|
| 9 |
import Image from 'next/image'
|
| 10 |
import {
|
|
@@ -15,7 +38,10 @@ import {
|
|
| 15 |
MenubarSeparator,
|
| 16 |
MenubarTrigger,
|
| 17 |
} from '@/components/ui/menubar'
|
| 18 |
-
import {
|
|
|
|
|
|
|
|
|
|
| 19 |
|
| 20 |
type MenuItem = {
|
| 21 |
label: string
|
|
@@ -30,62 +56,74 @@ type MenuSection = {
|
|
| 30 |
triggerTestId?: string
|
| 31 |
}
|
| 32 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
export function MenuBar() {
|
| 34 |
const { t } = useTranslation()
|
| 35 |
-
const {
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
|
| 50 |
const fileMenuItems: MenuItem[] = [
|
| 51 |
{
|
| 52 |
label: t('menu.openFile'),
|
| 53 |
-
onSelect:
|
| 54 |
testId: 'menu-file-open',
|
| 55 |
},
|
| 56 |
{
|
| 57 |
label: t('menu.addFile'),
|
| 58 |
-
onSelect:
|
| 59 |
testId: 'menu-file-add',
|
| 60 |
},
|
| 61 |
{
|
| 62 |
label: t('menu.openFolder'),
|
| 63 |
-
onSelect:
|
| 64 |
testId: 'menu-file-open-folder',
|
| 65 |
},
|
| 66 |
{
|
| 67 |
label: t('menu.addFolder'),
|
| 68 |
-
onSelect:
|
| 69 |
testId: 'menu-file-add-folder',
|
| 70 |
},
|
| 71 |
{
|
| 72 |
label: t('menu.export'),
|
| 73 |
-
onSelect:
|
| 74 |
testId: 'menu-file-export',
|
| 75 |
},
|
| 76 |
{
|
| 77 |
label: t('menu.exportPsd'),
|
| 78 |
-
onSelect:
|
| 79 |
testId: 'menu-file-export-psd',
|
| 80 |
},
|
| 81 |
{
|
| 82 |
label: t('menu.exportAllInpainted'),
|
| 83 |
-
onSelect:
|
| 84 |
testId: 'menu-file-export-all-inpainted',
|
| 85 |
},
|
| 86 |
{
|
| 87 |
label: t('menu.exportAllRendered'),
|
| 88 |
-
onSelect:
|
| 89 |
testId: 'menu-file-export-all-rendered',
|
| 90 |
},
|
| 91 |
]
|
|
@@ -104,17 +142,23 @@ export function MenuBar() {
|
|
| 104 |
items: [
|
| 105 |
{
|
| 106 |
label: t('menu.processCurrent'),
|
| 107 |
-
onSelect:
|
|
|
|
|
|
|
|
|
|
| 108 |
testId: 'menu-process-current',
|
| 109 |
},
|
| 110 |
{
|
| 111 |
label: t('menu.redoInpaintRender'),
|
| 112 |
-
onSelect:
|
|
|
|
|
|
|
|
|
|
| 113 |
testId: 'menu-process-rerender',
|
| 114 |
},
|
| 115 |
{
|
| 116 |
label: t('menu.processAll'),
|
| 117 |
-
onSelect:
|
| 118 |
testId: 'menu-process-all',
|
| 119 |
},
|
| 120 |
],
|
|
|
|
| 3 |
import { useCallback, useEffect, useState } from 'react'
|
| 4 |
import Link from 'next/link'
|
| 5 |
import { MinusIcon, SquareIcon, XIcon, CopyIcon } from 'lucide-react'
|
| 6 |
+
import { isTauri } from '@/lib/backend'
|
| 7 |
import { useTranslation } from 'react-i18next'
|
| 8 |
+
|
| 9 |
+
const isMacOS = () =>
|
| 10 |
+
typeof navigator !== 'undefined' &&
|
| 11 |
+
/Mac|iPhone|iPad|iPod/.test(navigator.userAgent)
|
| 12 |
+
|
| 13 |
+
const windowControls = {
|
| 14 |
+
async close() {
|
| 15 |
+
const { getCurrentWindow } = await import('@tauri-apps/api/window')
|
| 16 |
+
return getCurrentWindow().close()
|
| 17 |
+
},
|
| 18 |
+
async minimize() {
|
| 19 |
+
const { getCurrentWindow } = await import('@tauri-apps/api/window')
|
| 20 |
+
return getCurrentWindow().minimize()
|
| 21 |
+
},
|
| 22 |
+
async toggleMaximize() {
|
| 23 |
+
const { getCurrentWindow } = await import('@tauri-apps/api/window')
|
| 24 |
+
return getCurrentWindow().toggleMaximize()
|
| 25 |
+
},
|
| 26 |
+
async isMaximized() {
|
| 27 |
+
const { getCurrentWindow } = await import('@tauri-apps/api/window')
|
| 28 |
+
return getCurrentWindow().isMaximized()
|
| 29 |
+
},
|
| 30 |
+
}
|
| 31 |
import { fitCanvasToViewport, resetCanvasScale } from '@/components/Canvas'
|
| 32 |
import Image from 'next/image'
|
| 33 |
import {
|
|
|
|
| 38 |
MenubarSeparator,
|
| 39 |
MenubarTrigger,
|
| 40 |
} from '@/components/ui/menubar'
|
| 41 |
+
import { useProcessing } from '@/lib/machines'
|
| 42 |
+
import { useEditorUiStore } from '@/lib/stores/editorUiStore'
|
| 43 |
+
import { usePreferencesStore } from '@/lib/stores/preferencesStore'
|
| 44 |
+
import type { PipelineJobRequest } from '@/lib/api/schemas'
|
| 45 |
|
| 46 |
type MenuItem = {
|
| 47 |
label: string
|
|
|
|
| 56 |
triggerTestId?: string
|
| 57 |
}
|
| 58 |
|
| 59 |
+
const openExternal = (url: string) => {
|
| 60 |
+
window.open(url, '_blank', 'noopener,noreferrer')
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
export function MenuBar() {
|
| 64 |
const { t } = useTranslation()
|
| 65 |
+
const { send } = useProcessing()
|
| 66 |
+
|
| 67 |
+
const buildPipelineRequest = (documentId?: string): PipelineJobRequest => {
|
| 68 |
+
const { selectedModel, selectedLanguage, renderEffect, renderStroke } =
|
| 69 |
+
useEditorUiStore.getState()
|
| 70 |
+
const { fontFamily, apiKeys } = usePreferencesStore.getState()
|
| 71 |
+
return {
|
| 72 |
+
documentId,
|
| 73 |
+
llmModelId: selectedModel,
|
| 74 |
+
llmApiKey: selectedModel ? apiKeys[selectedModel.split(':')[0]] : undefined,
|
| 75 |
+
language: selectedLanguage,
|
| 76 |
+
shaderEffect: renderEffect,
|
| 77 |
+
shaderStroke: renderStroke,
|
| 78 |
+
fontFamily,
|
| 79 |
+
}
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
const requireDocumentId = () => {
|
| 83 |
+
const id = useEditorUiStore.getState().currentDocumentId
|
| 84 |
+
if (!id) throw new Error('No current document selected')
|
| 85 |
+
return id
|
| 86 |
+
}
|
| 87 |
|
| 88 |
const fileMenuItems: MenuItem[] = [
|
| 89 |
{
|
| 90 |
label: t('menu.openFile'),
|
| 91 |
+
onSelect: () => send({ type: 'START_IMPORT', mode: 'replace', source: 'files' }),
|
| 92 |
testId: 'menu-file-open',
|
| 93 |
},
|
| 94 |
{
|
| 95 |
label: t('menu.addFile'),
|
| 96 |
+
onSelect: () => send({ type: 'START_IMPORT', mode: 'append', source: 'files' }),
|
| 97 |
testId: 'menu-file-add',
|
| 98 |
},
|
| 99 |
{
|
| 100 |
label: t('menu.openFolder'),
|
| 101 |
+
onSelect: () => send({ type: 'START_IMPORT', mode: 'replace', source: 'folder' }),
|
| 102 |
testId: 'menu-file-open-folder',
|
| 103 |
},
|
| 104 |
{
|
| 105 |
label: t('menu.addFolder'),
|
| 106 |
+
onSelect: () => send({ type: 'START_IMPORT', mode: 'append', source: 'folder' }),
|
| 107 |
testId: 'menu-file-add-folder',
|
| 108 |
},
|
| 109 |
{
|
| 110 |
label: t('menu.export'),
|
| 111 |
+
onSelect: () => send({ type: 'START_EXPORT', documentId: requireDocumentId(), format: 'webp', params: { layer: 'rendered' } }),
|
| 112 |
testId: 'menu-file-export',
|
| 113 |
},
|
| 114 |
{
|
| 115 |
label: t('menu.exportPsd'),
|
| 116 |
+
onSelect: () => send({ type: 'START_EXPORT', documentId: requireDocumentId(), format: 'psd' }),
|
| 117 |
testId: 'menu-file-export-psd',
|
| 118 |
},
|
| 119 |
{
|
| 120 |
label: t('menu.exportAllInpainted'),
|
| 121 |
+
onSelect: () => send({ type: 'START_BATCH_EXPORT', layer: 'inpainted' }),
|
| 122 |
testId: 'menu-file-export-all-inpainted',
|
| 123 |
},
|
| 124 |
{
|
| 125 |
label: t('menu.exportAllRendered'),
|
| 126 |
+
onSelect: () => send({ type: 'START_BATCH_EXPORT', layer: 'rendered' }),
|
| 127 |
testId: 'menu-file-export-all-rendered',
|
| 128 |
},
|
| 129 |
]
|
|
|
|
| 142 |
items: [
|
| 143 |
{
|
| 144 |
label: t('menu.processCurrent'),
|
| 145 |
+
onSelect: () => {
|
| 146 |
+
const documentId = requireDocumentId()
|
| 147 |
+
send({ type: 'START_PIPELINE', request: buildPipelineRequest(documentId) })
|
| 148 |
+
},
|
| 149 |
testId: 'menu-process-current',
|
| 150 |
},
|
| 151 |
{
|
| 152 |
label: t('menu.redoInpaintRender'),
|
| 153 |
+
onSelect: () => {
|
| 154 |
+
const documentId = requireDocumentId()
|
| 155 |
+
send({ type: 'START_INPAINT', documentId })
|
| 156 |
+
},
|
| 157 |
testId: 'menu-process-rerender',
|
| 158 |
},
|
| 159 |
{
|
| 160 |
label: t('menu.processAll'),
|
| 161 |
+
onSelect: () => send({ type: 'START_PIPELINE', request: buildPipelineRequest() }),
|
| 162 |
testId: 'menu-process-all',
|
| 163 |
},
|
| 164 |
],
|
ui/components/Navigator.tsx
CHANGED
|
@@ -1,43 +1,43 @@
|
|
| 1 |
'use client'
|
| 2 |
|
| 3 |
-
import {
|
| 4 |
import { useVirtualizer } from '@tanstack/react-virtual'
|
| 5 |
import { useTranslation } from 'react-i18next'
|
| 6 |
-
import {
|
|
|
|
|
|
|
|
|
|
| 7 |
import { useEditorUiStore } from '@/lib/stores/editorUiStore'
|
| 8 |
import { Button } from '@/components/ui/button'
|
| 9 |
import { ScrollArea } from '@/components/ui/scroll-area'
|
| 10 |
-
|
| 11 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
|
| 13 |
export function Navigator() {
|
| 14 |
-
const { data:
|
| 15 |
-
const totalPages =
|
| 16 |
-
const
|
| 17 |
-
|
| 18 |
-
(state) => state.currentDocumentIndex,
|
| 19 |
-
)
|
| 20 |
-
const setCurrentDocumentIndex = useEditorUiStore(
|
| 21 |
-
(state) => state.setCurrentDocumentIndex,
|
| 22 |
)
|
| 23 |
-
const
|
| 24 |
-
|
| 25 |
-
() => Array.from({ length: totalPages }, (_, idx) => idx),
|
| 26 |
-
[totalPages],
|
| 27 |
)
|
| 28 |
-
const
|
| 29 |
-
|
| 30 |
-
getScrollElement: () => listRef.current,
|
| 31 |
-
getItemKey: (index) => indices[index] ?? index,
|
| 32 |
-
estimateSize: () => 320,
|
| 33 |
-
overscan: 8,
|
| 34 |
-
measureElement: (element) => element.getBoundingClientRect().height,
|
| 35 |
-
})
|
| 36 |
const { t } = useTranslation()
|
| 37 |
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
|
|
|
|
|
|
|
|
|
| 41 |
|
| 42 |
return (
|
| 43 |
<div
|
|
@@ -66,47 +66,34 @@ export function Navigator() {
|
|
| 66 |
)}
|
| 67 |
</div>
|
| 68 |
|
| 69 |
-
<ScrollArea className='min-h-0 flex-1' viewportRef={
|
| 70 |
-
<div
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
}
|
| 93 |
-
>
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
selected={idx === currentDocumentIndex}
|
| 98 |
-
onSelect={() => {
|
| 99 |
-
void flushTextBlockSync()
|
| 100 |
-
.catch(() => {})
|
| 101 |
-
.finally(() => {
|
| 102 |
-
setCurrentDocumentIndex(idx)
|
| 103 |
-
})
|
| 104 |
-
}}
|
| 105 |
-
/>
|
| 106 |
-
</div>
|
| 107 |
-
)
|
| 108 |
-
})}
|
| 109 |
-
</div>
|
| 110 |
</div>
|
| 111 |
</ScrollArea>
|
| 112 |
</div>
|
|
@@ -115,36 +102,20 @@ export function Navigator() {
|
|
| 115 |
|
| 116 |
type PagePreviewProps = {
|
| 117 |
index: number
|
| 118 |
-
|
| 119 |
selected: boolean
|
| 120 |
onSelect: () => void
|
| 121 |
}
|
| 122 |
|
| 123 |
function PagePreview({
|
| 124 |
index,
|
| 125 |
-
|
| 126 |
selected,
|
| 127 |
onSelect,
|
| 128 |
}: PagePreviewProps) {
|
| 129 |
-
const
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
isPending: loading,
|
| 133 |
-
isError: error,
|
| 134 |
-
} = useThumbnailQuery(index, documentsVersion)
|
| 135 |
-
|
| 136 |
-
useLayoutEffect(() => {
|
| 137 |
-
if (!thumbnailBlob) {
|
| 138 |
-
setPreview(undefined)
|
| 139 |
-
return
|
| 140 |
-
}
|
| 141 |
-
const url = URL.createObjectURL(thumbnailBlob)
|
| 142 |
-
cancelObjectUrlRevoke(url)
|
| 143 |
-
setPreview(url)
|
| 144 |
-
return () => {
|
| 145 |
-
revokeObjectUrlLater(url)
|
| 146 |
-
}
|
| 147 |
-
}, [thumbnailBlob])
|
| 148 |
|
| 149 |
return (
|
| 150 |
<Button
|
|
@@ -153,26 +124,22 @@ function PagePreview({
|
|
| 153 |
data-testid={`navigator-page-${index}`}
|
| 154 |
data-page-index={index}
|
| 155 |
data-selected={selected}
|
| 156 |
-
className='bg-card data-[selected=true]:border-primary flex h-
|
| 157 |
>
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
<div className='bg-muted aspect-3/4 w-full rounded' />
|
| 173 |
-
)}
|
| 174 |
-
<div className='text-muted-foreground flex flex-1 items-center text-xs'>
|
| 175 |
-
<div className='text-foreground mx-auto flex text-center font-semibold'>
|
| 176 |
{index + 1}
|
| 177 |
</div>
|
| 178 |
</div>
|
|
|
|
| 1 |
'use client'
|
| 2 |
|
| 3 |
+
import { useRef } from 'react'
|
| 4 |
import { useVirtualizer } from '@tanstack/react-virtual'
|
| 5 |
import { useTranslation } from 'react-i18next'
|
| 6 |
+
import {
|
| 7 |
+
useListDocuments,
|
| 8 |
+
getGetDocumentThumbnailUrl,
|
| 9 |
+
} from '@/lib/api/documents/documents'
|
| 10 |
import { useEditorUiStore } from '@/lib/stores/editorUiStore'
|
| 11 |
import { Button } from '@/components/ui/button'
|
| 12 |
import { ScrollArea } from '@/components/ui/scroll-area'
|
| 13 |
+
|
| 14 |
+
const THUMBNAIL_DPR = typeof window !== 'undefined'
|
| 15 |
+
? Math.min(Math.ceil(window.devicePixelRatio || 1), 3)
|
| 16 |
+
: 2
|
| 17 |
+
|
| 18 |
+
// Fixed row height: thumbnail (aspect 3:4 in ~150px width ≈ 200px) + page number + padding
|
| 19 |
+
const ROW_HEIGHT = 230
|
| 20 |
+
const OVERSCAN = 5
|
| 21 |
|
| 22 |
export function Navigator() {
|
| 23 |
+
const { data: documents = [] } = useListDocuments()
|
| 24 |
+
const totalPages = documents.length
|
| 25 |
+
const currentDocumentId = useEditorUiStore(
|
| 26 |
+
(state) => state.currentDocumentId,
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
)
|
| 28 |
+
const setCurrentDocumentId = useEditorUiStore(
|
| 29 |
+
(state) => state.setCurrentDocumentId,
|
|
|
|
|
|
|
| 30 |
)
|
| 31 |
+
const currentDocumentIndex = documents.findIndex((d) => d.id === currentDocumentId)
|
| 32 |
+
const viewportRef = useRef<HTMLDivElement | null>(null)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
const { t } = useTranslation()
|
| 34 |
|
| 35 |
+
const virtualizer = useVirtualizer({
|
| 36 |
+
count: totalPages,
|
| 37 |
+
getScrollElement: () => viewportRef.current,
|
| 38 |
+
estimateSize: () => ROW_HEIGHT,
|
| 39 |
+
overscan: OVERSCAN,
|
| 40 |
+
})
|
| 41 |
|
| 42 |
return (
|
| 43 |
<div
|
|
|
|
| 66 |
)}
|
| 67 |
</div>
|
| 68 |
|
| 69 |
+
<ScrollArea className='min-h-0 flex-1' viewportRef={viewportRef}>
|
| 70 |
+
<div
|
| 71 |
+
className='relative w-full'
|
| 72 |
+
style={{ height: virtualizer.getTotalSize() }}
|
| 73 |
+
>
|
| 74 |
+
{virtualizer.getVirtualItems().map((virtualRow) => {
|
| 75 |
+
const doc = documents[virtualRow.index]
|
| 76 |
+
return (
|
| 77 |
+
<div
|
| 78 |
+
key={doc?.id ?? virtualRow.index}
|
| 79 |
+
className='absolute left-0 w-full px-1.5 pb-1'
|
| 80 |
+
style={{
|
| 81 |
+
height: ROW_HEIGHT,
|
| 82 |
+
top: 0,
|
| 83 |
+
transform: `translateY(${virtualRow.start}px)`,
|
| 84 |
+
}}
|
| 85 |
+
>
|
| 86 |
+
<PagePreview
|
| 87 |
+
index={virtualRow.index}
|
| 88 |
+
documentId={doc?.id}
|
| 89 |
+
selected={doc?.id === currentDocumentId}
|
| 90 |
+
onSelect={() =>
|
| 91 |
+
doc && setCurrentDocumentId(doc.id)
|
| 92 |
+
}
|
| 93 |
+
/>
|
| 94 |
+
</div>
|
| 95 |
+
)
|
| 96 |
+
})}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 97 |
</div>
|
| 98 |
</ScrollArea>
|
| 99 |
</div>
|
|
|
|
| 102 |
|
| 103 |
type PagePreviewProps = {
|
| 104 |
index: number
|
| 105 |
+
documentId?: string
|
| 106 |
selected: boolean
|
| 107 |
onSelect: () => void
|
| 108 |
}
|
| 109 |
|
| 110 |
function PagePreview({
|
| 111 |
index,
|
| 112 |
+
documentId,
|
| 113 |
selected,
|
| 114 |
onSelect,
|
| 115 |
}: PagePreviewProps) {
|
| 116 |
+
const src = documentId
|
| 117 |
+
? getGetDocumentThumbnailUrl(documentId, { size: 200 * THUMBNAIL_DPR })
|
| 118 |
+
: undefined
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 119 |
|
| 120 |
return (
|
| 121 |
<Button
|
|
|
|
| 124 |
data-testid={`navigator-page-${index}`}
|
| 125 |
data-page-index={index}
|
| 126 |
data-selected={selected}
|
| 127 |
+
className='bg-card data-[selected=true]:border-primary flex h-full w-full flex-col gap-0.5 rounded border border-transparent p-1.5 text-left shadow-sm'
|
| 128 |
>
|
| 129 |
+
<div className='flex min-h-0 flex-1 items-center justify-center overflow-hidden rounded'>
|
| 130 |
+
{src ? (
|
| 131 |
+
<img
|
| 132 |
+
src={src}
|
| 133 |
+
alt={`Page ${index + 1}`}
|
| 134 |
+
loading='lazy'
|
| 135 |
+
className='max-h-full max-w-full rounded object-contain'
|
| 136 |
+
/>
|
| 137 |
+
) : (
|
| 138 |
+
<div className='bg-muted h-full w-full rounded' />
|
| 139 |
+
)}
|
| 140 |
+
</div>
|
| 141 |
+
<div className='text-muted-foreground flex shrink-0 items-center text-xs'>
|
| 142 |
+
<div className='text-foreground mx-auto font-semibold'>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 143 |
{index + 1}
|
| 144 |
</div>
|
| 145 |
</div>
|
ui/components/canvas/CanvasToolbar.tsx
CHANGED
|
@@ -1,7 +1,8 @@
|
|
| 1 |
'use client'
|
| 2 |
|
| 3 |
-
import { useEffect, useMemo, useState } from 'react'
|
| 4 |
import { useTranslation } from 'react-i18next'
|
|
|
|
| 5 |
import { motion } from 'motion/react'
|
| 6 |
import {
|
| 7 |
ScanIcon,
|
|
@@ -25,17 +26,46 @@ import {
|
|
| 25 |
PopoverContent,
|
| 26 |
PopoverTrigger,
|
| 27 |
} from '@/components/ui/popover'
|
| 28 |
-
import {
|
| 29 |
import {
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
} from '@/lib/
|
| 35 |
-
import {
|
| 36 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
import { usePreferencesStore } from '@/lib/stores/preferencesStore'
|
| 38 |
import { getProviderDisplayName } from '@/lib/providers'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
|
| 40 |
export function CanvasToolbar() {
|
| 41 |
return (
|
|
@@ -48,26 +78,34 @@ export function CanvasToolbar() {
|
|
| 48 |
}
|
| 49 |
|
| 50 |
function WorkflowButtons() {
|
| 51 |
-
const
|
| 52 |
-
const {
|
| 53 |
-
const { data:
|
|
|
|
| 54 |
const [generating, setGenerating] = useState(false)
|
| 55 |
const { t } = useTranslation()
|
| 56 |
-
const operation = useOperationStore((state) => state.operation)
|
| 57 |
|
| 58 |
-
const isDetecting =
|
| 59 |
-
|
| 60 |
-
const
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
|
|
|
|
|
|
| 66 |
|
| 67 |
const handleTranslate = async () => {
|
|
|
|
|
|
|
|
|
|
| 68 |
setGenerating(true)
|
| 69 |
try {
|
| 70 |
-
await
|
|
|
|
|
|
|
|
|
|
| 71 |
} catch (error) {
|
| 72 |
console.error(error)
|
| 73 |
} finally {
|
|
@@ -80,9 +118,9 @@ function WorkflowButtons() {
|
|
| 80 |
<Button
|
| 81 |
variant='ghost'
|
| 82 |
size='xs'
|
| 83 |
-
onClick={
|
| 84 |
data-testid='toolbar-detect'
|
| 85 |
-
disabled={isDetecting}
|
| 86 |
>
|
| 87 |
{isDetecting ? (
|
| 88 |
<LoaderCircleIcon className='size-4 animate-spin' />
|
|
@@ -97,9 +135,9 @@ function WorkflowButtons() {
|
|
| 97 |
<Button
|
| 98 |
variant='ghost'
|
| 99 |
size='xs'
|
| 100 |
-
onClick={
|
| 101 |
data-testid='toolbar-ocr'
|
| 102 |
-
disabled={isOcr}
|
| 103 |
>
|
| 104 |
{isOcr ? (
|
| 105 |
<LoaderCircleIcon className='size-4 animate-spin' />
|
|
@@ -131,9 +169,9 @@ function WorkflowButtons() {
|
|
| 131 |
<Button
|
| 132 |
variant='ghost'
|
| 133 |
size='xs'
|
| 134 |
-
onClick={
|
| 135 |
data-testid='toolbar-inpaint'
|
| 136 |
-
disabled={isInpainting}
|
| 137 |
>
|
| 138 |
{isInpainting ? (
|
| 139 |
<LoaderCircleIcon className='size-4 animate-spin' />
|
|
@@ -148,9 +186,14 @@ function WorkflowButtons() {
|
|
| 148 |
<Button
|
| 149 |
variant='ghost'
|
| 150 |
size='xs'
|
| 151 |
-
onClick={
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 152 |
data-testid='toolbar-render'
|
| 153 |
-
disabled={isRendering}
|
| 154 |
>
|
| 155 |
{isRendering ? (
|
| 156 |
<LoaderCircleIcon className='size-4 animate-spin' />
|
|
@@ -165,15 +208,14 @@ function WorkflowButtons() {
|
|
| 165 |
|
| 166 |
function LlmStatusPopover() {
|
| 167 |
const { data: llmModels = [] } = useLlmModelsQuery()
|
| 168 |
-
const llmSelectedModel =
|
| 169 |
-
const llmSelectedLanguage =
|
| 170 |
-
const
|
| 171 |
-
const
|
| 172 |
-
const
|
| 173 |
-
|
| 174 |
const { t } = useTranslation()
|
| 175 |
const apiKeys = usePreferencesStore((state) => state.apiKeys)
|
| 176 |
-
const localLlm = usePreferencesStore((state) => state.localLlm)
|
| 177 |
|
| 178 |
const selectedModelInfo = useMemo(
|
| 179 |
() => llmModels.find((m) => m.id === llmSelectedModel),
|
|
@@ -192,6 +234,56 @@ function LlmStatusPopover() {
|
|
| 192 |
[selectedModelInfo],
|
| 193 |
)
|
| 194 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 195 |
useEffect(() => {
|
| 196 |
if (llmModels.length === 0) return
|
| 197 |
const hasCurrent = llmModels.some((model) => model.id === llmSelectedModel)
|
|
@@ -203,18 +295,17 @@ function LlmStatusPopover() {
|
|
| 203 |
llmSelectedLanguage && languages.includes(llmSelectedLanguage)
|
| 204 |
? llmSelectedLanguage
|
| 205 |
: languages[0]
|
| 206 |
-
const currentState =
|
| 207 |
if (
|
| 208 |
currentState.selectedModel === nextModel &&
|
| 209 |
currentState.selectedLanguage === nextLanguage
|
| 210 |
) {
|
| 211 |
return
|
| 212 |
}
|
| 213 |
-
|
| 214 |
selectedModel: nextModel,
|
| 215 |
selectedLanguage: nextLanguage,
|
| 216 |
-
|
| 217 |
-
}))
|
| 218 |
}, [llmModels, llmSelectedLanguage, llmSelectedModel])
|
| 219 |
|
| 220 |
return (
|
|
@@ -242,11 +333,6 @@ function LlmStatusPopover() {
|
|
| 242 |
}
|
| 243 |
/>
|
| 244 |
LLM
|
| 245 |
-
{llmReady && selectedModelInfo?.source === 'openai-compatible' && (
|
| 246 |
-
<span className='max-w-[80px] truncate text-[10px] opacity-80'>
|
| 247 |
-
{selectedModelInfo.id.split(':')[1] ?? selectedModelInfo.id}
|
| 248 |
-
</span>
|
| 249 |
-
)}
|
| 250 |
</button>
|
| 251 |
</PopoverTrigger>
|
| 252 |
<PopoverContent align='end' className='w-72' data-testid='llm-popover'>
|
|
@@ -255,7 +341,7 @@ function LlmStatusPopover() {
|
|
| 255 |
{t('panels.llm')}
|
| 256 |
</p>
|
| 257 |
|
| 258 |
-
<Select value={llmSelectedModel} onValueChange={
|
| 259 |
<SelectTrigger data-testid='llm-model-select' className='w-full'>
|
| 260 |
<SelectValue placeholder={t('llm.selectPlaceholder')} />
|
| 261 |
</SelectTrigger>
|
|
@@ -267,37 +353,14 @@ function LlmStatusPopover() {
|
|
| 267 |
data-testid={`llm-model-option-${index}`}
|
| 268 |
>
|
| 269 |
<span className='flex items-center gap-2'>
|
| 270 |
-
{model.source ==
|
| 271 |
-
(() => {
|
| 272 |
-
const preset = parsePresetFromModelId(model.id)
|
| 273 |
-
const isTeal =
|
| 274 |
-
preset === 'preset1' || preset === 'preset2'
|
| 275 |
-
return (
|
| 276 |
-
<span
|
| 277 |
-
className={`rounded px-1 py-0.5 text-[10px] leading-none font-semibold uppercase ${
|
| 278 |
-
isTeal
|
| 279 |
-
? 'bg-teal-500/10 text-teal-600 dark:text-teal-400'
|
| 280 |
-
: 'bg-emerald-500/10 text-emerald-600 dark:text-emerald-400'
|
| 281 |
-
}`}
|
| 282 |
-
>
|
| 283 |
-
{preset
|
| 284 |
-
? (LOCAL_LLM_PRESET_LABELS[preset] ?? preset)
|
| 285 |
-
: 'OpenAI-like'}
|
| 286 |
-
</span>
|
| 287 |
-
)
|
| 288 |
-
})()
|
| 289 |
-
) : model.source !== 'local' ? (
|
| 290 |
<span className='bg-primary/10 text-primary rounded px-1 py-0.5 text-[10px] leading-none font-semibold uppercase'>
|
| 291 |
{getProviderDisplayName(model.source)}
|
| 292 |
</span>
|
| 293 |
) : null}
|
| 294 |
-
{
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
? model.id.split(':').slice(2).join(':')
|
| 298 |
-
: model.id.includes(':')
|
| 299 |
-
? model.id.split(':')[1]
|
| 300 |
-
: model.id}
|
| 301 |
</span>
|
| 302 |
</SelectItem>
|
| 303 |
))}
|
|
@@ -313,65 +376,10 @@ function LlmStatusPopover() {
|
|
| 313 |
</p>
|
| 314 |
)}
|
| 315 |
|
| 316 |
-
{/* Loaded model info card */}
|
| 317 |
-
{llmReady &&
|
| 318 |
-
selectedModelInfo?.source === 'openai-compatible' &&
|
| 319 |
-
(() => {
|
| 320 |
-
const infoPreset = parsePresetFromModelId(selectedModelInfo.id)
|
| 321 |
-
const isTeal =
|
| 322 |
-
infoPreset === 'preset1' || infoPreset === 'preset2'
|
| 323 |
-
const presetLabel = infoPreset
|
| 324 |
-
? (LOCAL_LLM_PRESET_LABELS[infoPreset] ?? infoPreset)
|
| 325 |
-
: 'OpenAI-like'
|
| 326 |
-
const presetCfg = infoPreset
|
| 327 |
-
? localLlm.presets[infoPreset]
|
| 328 |
-
: undefined
|
| 329 |
-
const modelName =
|
| 330 |
-
selectedModelInfo.id.split(':').length >= 3
|
| 331 |
-
? selectedModelInfo.id.split(':').slice(2).join(':')
|
| 332 |
-
: (selectedModelInfo.id.split(':')[1] ?? selectedModelInfo.id)
|
| 333 |
-
return (
|
| 334 |
-
<div
|
| 335 |
-
className={`rounded-md px-2.5 py-2 text-xs ${
|
| 336 |
-
isTeal
|
| 337 |
-
? 'border border-teal-500/20 bg-teal-500/5'
|
| 338 |
-
: 'border border-emerald-500/20 bg-emerald-500/5'
|
| 339 |
-
}`}
|
| 340 |
-
>
|
| 341 |
-
<div className='flex items-center gap-1.5'>
|
| 342 |
-
<span
|
| 343 |
-
className={`size-1.5 rounded-full ${
|
| 344 |
-
isTeal ? 'bg-teal-500' : 'bg-emerald-500'
|
| 345 |
-
}`}
|
| 346 |
-
/>
|
| 347 |
-
<span
|
| 348 |
-
className={`font-medium ${
|
| 349 |
-
isTeal
|
| 350 |
-
? 'text-teal-700 dark:text-teal-400'
|
| 351 |
-
: 'text-emerald-700 dark:text-emerald-400'
|
| 352 |
-
}`}
|
| 353 |
-
>
|
| 354 |
-
{t('llm.localModelActive')}
|
| 355 |
-
</span>
|
| 356 |
-
</div>
|
| 357 |
-
<p className='text-muted-foreground mt-1'>
|
| 358 |
-
{t('llm.localModelName', { name: modelName })}
|
| 359 |
-
</p>
|
| 360 |
-
<p className='text-muted-foreground mt-0.5'>
|
| 361 |
-
{presetLabel}
|
| 362 |
-
{presetCfg?.temperature != null &&
|
| 363 |
-
` · temp ${presetCfg.temperature}`}
|
| 364 |
-
{presetCfg?.maxTokens != null &&
|
| 365 |
-
` · ${presetCfg.maxTokens} tokens`}
|
| 366 |
-
</p>
|
| 367 |
-
</div>
|
| 368 |
-
)
|
| 369 |
-
})()}
|
| 370 |
-
|
| 371 |
{activeLanguages.length > 0 && (
|
| 372 |
<Select
|
| 373 |
value={llmSelectedLanguage ?? activeLanguages[0]}
|
| 374 |
-
onValueChange={
|
| 375 |
>
|
| 376 |
<SelectTrigger
|
| 377 |
data-testid='llm-language-select'
|
|
@@ -399,7 +407,7 @@ function LlmStatusPopover() {
|
|
| 399 |
data-llm-loading={llmLoading ? 'true' : 'false'}
|
| 400 |
variant='outline'
|
| 401 |
size='sm'
|
| 402 |
-
onClick={
|
| 403 |
disabled={!llmSelectedModel || llmLoading}
|
| 404 |
className='w-full gap-1.5 text-xs'
|
| 405 |
>
|
|
|
|
| 1 |
'use client'
|
| 2 |
|
| 3 |
+
import { useCallback, useEffect, useMemo, useState } from 'react'
|
| 4 |
import { useTranslation } from 'react-i18next'
|
| 5 |
+
import { useQuery, useQueryClient } from '@tanstack/react-query'
|
| 6 |
import { motion } from 'motion/react'
|
| 7 |
import {
|
| 8 |
ScanIcon,
|
|
|
|
| 26 |
PopoverContent,
|
| 27 |
PopoverTrigger,
|
| 28 |
} from '@/components/ui/popover'
|
| 29 |
+
import { useEditorUiStore } from '@/lib/stores/editorUiStore'
|
| 30 |
import {
|
| 31 |
+
useGetLlm,
|
| 32 |
+
getListLlmModelsQueryKey,
|
| 33 |
+
listLlmModels,
|
| 34 |
+
unloadLlm,
|
| 35 |
+
} from '@/lib/api/llm/llm'
|
| 36 |
+
import {
|
| 37 |
+
translateDocument,
|
| 38 |
+
} from '@/lib/api/processing/processing'
|
| 39 |
+
import {
|
| 40 |
+
getGetDocumentQueryKey,
|
| 41 |
+
getListDocumentsQueryKey,
|
| 42 |
+
} from '@/lib/api/documents/documents'
|
| 43 |
+
import type { LlmModelInfo } from '@/lib/api/schemas'
|
| 44 |
+
import { useProcessing } from '@/lib/machines'
|
| 45 |
import { usePreferencesStore } from '@/lib/stores/preferencesStore'
|
| 46 |
import { getProviderDisplayName } from '@/lib/providers'
|
| 47 |
+
import i18n from '@/lib/i18n'
|
| 48 |
+
|
| 49 |
+
function useLlmModelsQuery() {
|
| 50 |
+
const [language, setLanguage] = useState(i18n.language)
|
| 51 |
+
useEffect(() => {
|
| 52 |
+
const handleLanguageChange = (nextLanguage: string) => {
|
| 53 |
+
setLanguage(nextLanguage)
|
| 54 |
+
}
|
| 55 |
+
i18n.on('languageChanged', handleLanguageChange)
|
| 56 |
+
return () => {
|
| 57 |
+
i18n.off('languageChanged', handleLanguageChange)
|
| 58 |
+
}
|
| 59 |
+
}, [])
|
| 60 |
+
|
| 61 |
+
return useQuery<LlmModelInfo[]>({
|
| 62 |
+
queryKey: getListLlmModelsQueryKey({ language: language ?? 'default' }),
|
| 63 |
+
queryFn: async () => {
|
| 64 |
+
return listLlmModels({ language })
|
| 65 |
+
},
|
| 66 |
+
staleTime: 5 * 60 * 1000,
|
| 67 |
+
})
|
| 68 |
+
}
|
| 69 |
|
| 70 |
export function CanvasToolbar() {
|
| 71 |
return (
|
|
|
|
| 78 |
}
|
| 79 |
|
| 80 |
function WorkflowButtons() {
|
| 81 |
+
const queryClient = useQueryClient()
|
| 82 |
+
const { send, isProcessing, state } = useProcessing()
|
| 83 |
+
const { data: llmState } = useGetLlm()
|
| 84 |
+
const llmReady = llmState?.status === 'ready'
|
| 85 |
const [generating, setGenerating] = useState(false)
|
| 86 |
const { t } = useTranslation()
|
|
|
|
| 87 |
|
| 88 |
+
const isDetecting = state.matches('detecting')
|
| 89 |
+
const isOcr = state.matches('recognizing')
|
| 90 |
+
const isInpainting = state.matches('inpainting')
|
| 91 |
+
const isRendering = state.matches('rendering')
|
| 92 |
+
|
| 93 |
+
const requireDocumentId = () => {
|
| 94 |
+
const id = useEditorUiStore.getState().currentDocumentId
|
| 95 |
+
if (!id) throw new Error('No current document selected')
|
| 96 |
+
return id
|
| 97 |
+
}
|
| 98 |
|
| 99 |
const handleTranslate = async () => {
|
| 100 |
+
const documentId = useEditorUiStore.getState().currentDocumentId
|
| 101 |
+
if (!documentId) return
|
| 102 |
+
const selectedLanguage = useEditorUiStore.getState().selectedLanguage
|
| 103 |
setGenerating(true)
|
| 104 |
try {
|
| 105 |
+
await translateDocument(documentId, { language: selectedLanguage })
|
| 106 |
+
await queryClient.invalidateQueries({ queryKey: getGetDocumentQueryKey(documentId) })
|
| 107 |
+
await queryClient.invalidateQueries({ queryKey: getListDocumentsQueryKey() })
|
| 108 |
+
useEditorUiStore.getState().setShowTextBlocksOverlay(true)
|
| 109 |
} catch (error) {
|
| 110 |
console.error(error)
|
| 111 |
} finally {
|
|
|
|
| 118 |
<Button
|
| 119 |
variant='ghost'
|
| 120 |
size='xs'
|
| 121 |
+
onClick={() => send({ type: 'START_DETECT', documentId: requireDocumentId() })}
|
| 122 |
data-testid='toolbar-detect'
|
| 123 |
+
disabled={isDetecting || isProcessing}
|
| 124 |
>
|
| 125 |
{isDetecting ? (
|
| 126 |
<LoaderCircleIcon className='size-4 animate-spin' />
|
|
|
|
| 135 |
<Button
|
| 136 |
variant='ghost'
|
| 137 |
size='xs'
|
| 138 |
+
onClick={() => send({ type: 'START_RECOGNIZE', documentId: requireDocumentId() })}
|
| 139 |
data-testid='toolbar-ocr'
|
| 140 |
+
disabled={isOcr || isProcessing}
|
| 141 |
>
|
| 142 |
{isOcr ? (
|
| 143 |
<LoaderCircleIcon className='size-4 animate-spin' />
|
|
|
|
| 169 |
<Button
|
| 170 |
variant='ghost'
|
| 171 |
size='xs'
|
| 172 |
+
onClick={() => send({ type: 'START_INPAINT', documentId: requireDocumentId() })}
|
| 173 |
data-testid='toolbar-inpaint'
|
| 174 |
+
disabled={isInpainting || isProcessing}
|
| 175 |
>
|
| 176 |
{isInpainting ? (
|
| 177 |
<LoaderCircleIcon className='size-4 animate-spin' />
|
|
|
|
| 186 |
<Button
|
| 187 |
variant='ghost'
|
| 188 |
size='xs'
|
| 189 |
+
onClick={() => {
|
| 190 |
+
const documentId = requireDocumentId()
|
| 191 |
+
const { renderEffect, renderStroke } = useEditorUiStore.getState()
|
| 192 |
+
const { fontFamily } = usePreferencesStore.getState()
|
| 193 |
+
send({ type: 'START_RENDER', documentId, options: { shaderEffect: renderEffect, shaderStroke: renderStroke, fontFamily } })
|
| 194 |
+
}}
|
| 195 |
data-testid='toolbar-render'
|
| 196 |
+
disabled={isRendering || isProcessing}
|
| 197 |
>
|
| 198 |
{isRendering ? (
|
| 199 |
<LoaderCircleIcon className='size-4 animate-spin' />
|
|
|
|
| 208 |
|
| 209 |
function LlmStatusPopover() {
|
| 210 |
const { data: llmModels = [] } = useLlmModelsQuery()
|
| 211 |
+
const llmSelectedModel = useEditorUiStore((state) => state.selectedModel)
|
| 212 |
+
const llmSelectedLanguage = useEditorUiStore((state) => state.selectedLanguage)
|
| 213 |
+
const { data: llmState } = useGetLlm()
|
| 214 |
+
const llmReady = llmState?.status === 'ready'
|
| 215 |
+
const llmLoading = llmState?.status === 'loading'
|
| 216 |
+
const { send } = useProcessing()
|
| 217 |
const { t } = useTranslation()
|
| 218 |
const apiKeys = usePreferencesStore((state) => state.apiKeys)
|
|
|
|
| 219 |
|
| 220 |
const selectedModelInfo = useMemo(
|
| 221 |
() => llmModels.find((m) => m.id === llmSelectedModel),
|
|
|
|
| 234 |
[selectedModelInfo],
|
| 235 |
)
|
| 236 |
|
| 237 |
+
const handleSetSelectedModel = useCallback(
|
| 238 |
+
async (id: string) => {
|
| 239 |
+
await unloadLlm()
|
| 240 |
+
const models = llmModels
|
| 241 |
+
const languages = models.find((m) => m.id === id)?.languages ?? []
|
| 242 |
+
const nextLanguage =
|
| 243 |
+
llmSelectedLanguage && languages.includes(llmSelectedLanguage)
|
| 244 |
+
? llmSelectedLanguage
|
| 245 |
+
: languages[0]
|
| 246 |
+
useEditorUiStore.setState({
|
| 247 |
+
selectedModel: id,
|
| 248 |
+
selectedLanguage: nextLanguage,
|
| 249 |
+
})
|
| 250 |
+
},
|
| 251 |
+
[llmModels, llmSelectedLanguage],
|
| 252 |
+
)
|
| 253 |
+
|
| 254 |
+
const handleSetSelectedLanguage = useCallback(
|
| 255 |
+
(language: string) => {
|
| 256 |
+
const languages = selectedModelInfo?.languages ?? []
|
| 257 |
+
if (!languages.includes(language)) return
|
| 258 |
+
useEditorUiStore.setState({ selectedLanguage: language })
|
| 259 |
+
},
|
| 260 |
+
[selectedModelInfo],
|
| 261 |
+
)
|
| 262 |
+
|
| 263 |
+
const handleToggleLoadUnload = useCallback(async () => {
|
| 264 |
+
const selectedModel = useEditorUiStore.getState().selectedModel
|
| 265 |
+
if (!selectedModel) return
|
| 266 |
+
|
| 267 |
+
if (llmReady) {
|
| 268 |
+
await unloadLlm()
|
| 269 |
+
return
|
| 270 |
+
}
|
| 271 |
+
|
| 272 |
+
const modelInfo = llmModels.find((m) => m.id === selectedModel)
|
| 273 |
+
const apiKey =
|
| 274 |
+
modelInfo && modelInfo.source !== 'local'
|
| 275 |
+
? apiKeys[modelInfo.source]
|
| 276 |
+
: undefined
|
| 277 |
+
|
| 278 |
+
send({
|
| 279 |
+
type: 'START_LLM_LOAD',
|
| 280 |
+
request: {
|
| 281 |
+
id: selectedModel,
|
| 282 |
+
apiKey,
|
| 283 |
+
},
|
| 284 |
+
})
|
| 285 |
+
}, [llmReady, llmModels, apiKeys, send])
|
| 286 |
+
|
| 287 |
useEffect(() => {
|
| 288 |
if (llmModels.length === 0) return
|
| 289 |
const hasCurrent = llmModels.some((model) => model.id === llmSelectedModel)
|
|
|
|
| 295 |
llmSelectedLanguage && languages.includes(llmSelectedLanguage)
|
| 296 |
? llmSelectedLanguage
|
| 297 |
: languages[0]
|
| 298 |
+
const currentState = useEditorUiStore.getState()
|
| 299 |
if (
|
| 300 |
currentState.selectedModel === nextModel &&
|
| 301 |
currentState.selectedLanguage === nextLanguage
|
| 302 |
) {
|
| 303 |
return
|
| 304 |
}
|
| 305 |
+
useEditorUiStore.setState({
|
| 306 |
selectedModel: nextModel,
|
| 307 |
selectedLanguage: nextLanguage,
|
| 308 |
+
})
|
|
|
|
| 309 |
}, [llmModels, llmSelectedLanguage, llmSelectedModel])
|
| 310 |
|
| 311 |
return (
|
|
|
|
| 333 |
}
|
| 334 |
/>
|
| 335 |
LLM
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 336 |
</button>
|
| 337 |
</PopoverTrigger>
|
| 338 |
<PopoverContent align='end' className='w-72' data-testid='llm-popover'>
|
|
|
|
| 341 |
{t('panels.llm')}
|
| 342 |
</p>
|
| 343 |
|
| 344 |
+
<Select value={llmSelectedModel} onValueChange={handleSetSelectedModel}>
|
| 345 |
<SelectTrigger data-testid='llm-model-select' className='w-full'>
|
| 346 |
<SelectValue placeholder={t('llm.selectPlaceholder')} />
|
| 347 |
</SelectTrigger>
|
|
|
|
| 353 |
data-testid={`llm-model-option-${index}`}
|
| 354 |
>
|
| 355 |
<span className='flex items-center gap-2'>
|
| 356 |
+
{model.source !== 'local' ? (
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 357 |
<span className='bg-primary/10 text-primary rounded px-1 py-0.5 text-[10px] leading-none font-semibold uppercase'>
|
| 358 |
{getProviderDisplayName(model.source)}
|
| 359 |
</span>
|
| 360 |
) : null}
|
| 361 |
+
{model.id.includes(':')
|
| 362 |
+
? model.id.split(':')[1]
|
| 363 |
+
: model.id}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 364 |
</span>
|
| 365 |
</SelectItem>
|
| 366 |
))}
|
|
|
|
| 376 |
</p>
|
| 377 |
)}
|
| 378 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 379 |
{activeLanguages.length > 0 && (
|
| 380 |
<Select
|
| 381 |
value={llmSelectedLanguage ?? activeLanguages[0]}
|
| 382 |
+
onValueChange={handleSetSelectedLanguage}
|
| 383 |
>
|
| 384 |
<SelectTrigger
|
| 385 |
data-testid='llm-language-select'
|
|
|
|
| 407 |
data-llm-loading={llmLoading ? 'true' : 'false'}
|
| 408 |
variant='outline'
|
| 409 |
size='sm'
|
| 410 |
+
onClick={handleToggleLoadUnload}
|
| 411 |
disabled={!llmSelectedModel || llmLoading}
|
| 412 |
className='w-full gap-1.5 text-xs'
|
| 413 |
>
|
ui/components/canvas/Workspace.tsx
CHANGED
|
@@ -15,6 +15,7 @@ import { listen } from '@/lib/backend'
|
|
| 15 |
import { Image } from '@/components/Image'
|
| 16 |
import {
|
| 17 |
setCanvasViewport,
|
|
|
|
| 18 |
fitCanvasToViewport,
|
| 19 |
} from '@/components/canvas/canvasViewport'
|
| 20 |
import { ToolRail } from '@/components/canvas/ToolRail'
|
|
@@ -61,6 +62,13 @@ export function Workspace() {
|
|
| 61 |
appendBlock,
|
| 62 |
removeBlock,
|
| 63 |
} = useTextBlocks()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
const viewportRef = useRef<HTMLDivElement | null>(null)
|
| 65 |
const { setScale: applyScale } = useCanvasZoom()
|
| 66 |
const scaleRatio = scale / 100
|
|
|
|
| 15 |
import { Image } from '@/components/Image'
|
| 16 |
import {
|
| 17 |
setCanvasViewport,
|
| 18 |
+
setCanvasDocumentSize,
|
| 19 |
fitCanvasToViewport,
|
| 20 |
} from '@/components/canvas/canvasViewport'
|
| 21 |
import { ToolRail } from '@/components/canvas/ToolRail'
|
|
|
|
| 62 |
appendBlock,
|
| 63 |
removeBlock,
|
| 64 |
} = useTextBlocks()
|
| 65 |
+
|
| 66 |
+
useEffect(() => {
|
| 67 |
+
if (currentDocument) {
|
| 68 |
+
setCanvasDocumentSize(currentDocument.width, currentDocument.height)
|
| 69 |
+
}
|
| 70 |
+
}, [currentDocument?.width, currentDocument?.height])
|
| 71 |
+
|
| 72 |
const viewportRef = useRef<HTMLDivElement | null>(null)
|
| 73 |
const { setScale: applyScale } = useCanvasZoom()
|
| 74 |
const scaleRatio = scale / 100
|
ui/components/canvas/canvasViewport.ts
CHANGED
|
@@ -1,35 +1,31 @@
|
|
| 1 |
'use client'
|
| 2 |
|
| 3 |
-
import { queryKeys } from '@/lib/query/keys'
|
| 4 |
-
import { getQueryClient } from '@/lib/query/client'
|
| 5 |
import { useEditorUiStore } from '@/lib/stores/editorUiStore'
|
| 6 |
-
import type { Document } from '@/types'
|
| 7 |
|
| 8 |
const canvasViewportRef: { current: HTMLDivElement | null } = { current: null }
|
|
|
|
| 9 |
|
| 10 |
export function setCanvasViewport(element: HTMLDivElement | null) {
|
| 11 |
canvasViewportRef.current = element
|
| 12 |
}
|
| 13 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
export function fitCanvasToViewport() {
|
| 15 |
-
const { setScale, setAutoFitEnabled, currentDocumentIndex } =
|
| 16 |
-
useEditorUiStore.getState()
|
| 17 |
-
const doc = getQueryClient().getQueryData<Document>(
|
| 18 |
-
queryKeys.documents.current(currentDocumentIndex),
|
| 19 |
-
)
|
| 20 |
const viewport = canvasViewportRef.current
|
| 21 |
-
if (!
|
| 22 |
const rect = viewport.getBoundingClientRect()
|
| 23 |
-
if (!rect.width || !rect.height || !
|
| 24 |
-
const scaleW = ((rect.width - 10) /
|
| 25 |
-
const scaleH = ((rect.height - 10) /
|
| 26 |
const fit = Math.max(10, Math.min(100, Math.min(scaleW, scaleH)))
|
| 27 |
-
setAutoFitEnabled(true)
|
| 28 |
-
setScale(fit)
|
| 29 |
}
|
| 30 |
|
| 31 |
export function resetCanvasScale() {
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
setScale(100)
|
| 35 |
}
|
|
|
|
| 1 |
'use client'
|
| 2 |
|
|
|
|
|
|
|
| 3 |
import { useEditorUiStore } from '@/lib/stores/editorUiStore'
|
|
|
|
| 4 |
|
| 5 |
const canvasViewportRef: { current: HTMLDivElement | null } = { current: null }
|
| 6 |
+
let docSize: { width: number; height: number } | null = null
|
| 7 |
|
| 8 |
export function setCanvasViewport(element: HTMLDivElement | null) {
|
| 9 |
canvasViewportRef.current = element
|
| 10 |
}
|
| 11 |
|
| 12 |
+
export function setCanvasDocumentSize(width: number, height: number) {
|
| 13 |
+
docSize = { width, height }
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
export function fitCanvasToViewport() {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
const viewport = canvasViewportRef.current
|
| 18 |
+
if (!docSize || !viewport) return
|
| 19 |
const rect = viewport.getBoundingClientRect()
|
| 20 |
+
if (!rect.width || !rect.height || !docSize.width || !docSize.height) return
|
| 21 |
+
const scaleW = ((rect.width - 10) / docSize.width) * 100
|
| 22 |
+
const scaleH = ((rect.height - 10) / docSize.height) * 100
|
| 23 |
const fit = Math.max(10, Math.min(100, Math.min(scaleW, scaleH)))
|
| 24 |
+
useEditorUiStore.getState().setAutoFitEnabled(true)
|
| 25 |
+
useEditorUiStore.getState().setScale(fit)
|
| 26 |
}
|
| 27 |
|
| 28 |
export function resetCanvasScale() {
|
| 29 |
+
useEditorUiStore.getState().setAutoFitEnabled(false)
|
| 30 |
+
useEditorUiStore.getState().setScale(100)
|
|
|
|
| 31 |
}
|
ui/components/panels/LayersPanel.tsx
CHANGED
|
@@ -14,7 +14,7 @@ import {
|
|
| 14 |
import { cn } from '@/lib/utils'
|
| 15 |
import { Button } from '@/components/ui/button'
|
| 16 |
import { useEditorUiStore } from '@/lib/stores/editorUiStore'
|
| 17 |
-
import {
|
| 18 |
|
| 19 |
type Layer = {
|
| 20 |
id: string
|
|
@@ -27,7 +27,7 @@ type Layer = {
|
|
| 27 |
}
|
| 28 |
|
| 29 |
export function LayersPanel() {
|
| 30 |
-
const
|
| 31 |
const showInpaintedImage = useEditorUiStore(
|
| 32 |
(state) => state.showInpaintedImage,
|
| 33 |
)
|
|
|
|
| 14 |
import { cn } from '@/lib/utils'
|
| 15 |
import { Button } from '@/components/ui/button'
|
| 16 |
import { useEditorUiStore } from '@/lib/stores/editorUiStore'
|
| 17 |
+
import { useCurrentDocument } from '@/hooks/useTextBlocks'
|
| 18 |
|
| 19 |
type Layer = {
|
| 20 |
id: string
|
|
|
|
| 27 |
}
|
| 28 |
|
| 29 |
export function LayersPanel() {
|
| 30 |
+
const currentDocument = useCurrentDocument()
|
| 31 |
const showInpaintedImage = useEditorUiStore(
|
| 32 |
(state) => state.showInpaintedImage,
|
| 33 |
)
|
ui/components/panels/RenderControlsPanel.tsx
CHANGED
|
@@ -20,7 +20,7 @@ import {
|
|
| 20 |
TextAlign,
|
| 21 |
TextStyle,
|
| 22 |
} from '@/types'
|
| 23 |
-
import type { FontFaceInfo } from '@/lib/
|
| 24 |
import { Button } from '@/components/ui/button'
|
| 25 |
import { ColorPicker } from '@/components/ui/color-picker'
|
| 26 |
import { Input } from '@/components/ui/input'
|
|
@@ -38,8 +38,7 @@ import {
|
|
| 38 |
} from '@/components/ui/select'
|
| 39 |
import { useEditorUiStore } from '@/lib/stores/editorUiStore'
|
| 40 |
import { usePreferencesStore } from '@/lib/stores/preferencesStore'
|
| 41 |
-
import {
|
| 42 |
-
import { useTextBlockMutations } from '@/lib/query/mutations'
|
| 43 |
import { cn } from '@/lib/utils'
|
| 44 |
|
| 45 |
const DEFAULT_COLOR: RgbaColor = [0, 0, 0, 255]
|
|
@@ -183,11 +182,10 @@ export function RenderControlsPanel() {
|
|
| 183 |
const renderStroke = useEditorUiStore((state) => state.renderStroke)
|
| 184 |
const setRenderEffect = useEditorUiStore((state) => state.setRenderEffect)
|
| 185 |
const setRenderStroke = useEditorUiStore((state) => state.setRenderStroke)
|
| 186 |
-
const {
|
| 187 |
-
const { data: availableFonts = [] } = useFontsQuery()
|
| 188 |
const fontFamily = usePreferencesStore((state) => state.fontFamily)
|
| 189 |
const setFontFamily = usePreferencesStore((state) => state.setFontFamily)
|
| 190 |
-
const { textBlocks, selectedBlockIndex, replaceBlock } = useTextBlocks()
|
| 191 |
const { t } = useTranslation()
|
| 192 |
const selectedBlock =
|
| 193 |
selectedBlockIndex !== undefined
|
|
|
|
| 20 |
TextAlign,
|
| 21 |
TextStyle,
|
| 22 |
} from '@/types'
|
| 23 |
+
import type { FontFaceInfo } from '@/lib/api/schemas'
|
| 24 |
import { Button } from '@/components/ui/button'
|
| 25 |
import { ColorPicker } from '@/components/ui/color-picker'
|
| 26 |
import { Input } from '@/components/ui/input'
|
|
|
|
| 38 |
} from '@/components/ui/select'
|
| 39 |
import { useEditorUiStore } from '@/lib/stores/editorUiStore'
|
| 40 |
import { usePreferencesStore } from '@/lib/stores/preferencesStore'
|
| 41 |
+
import { useListFonts } from '@/lib/api/system/system'
|
|
|
|
| 42 |
import { cn } from '@/lib/utils'
|
| 43 |
|
| 44 |
const DEFAULT_COLOR: RgbaColor = [0, 0, 0, 255]
|
|
|
|
| 182 |
const renderStroke = useEditorUiStore((state) => state.renderStroke)
|
| 183 |
const setRenderEffect = useEditorUiStore((state) => state.setRenderEffect)
|
| 184 |
const setRenderStroke = useEditorUiStore((state) => state.setRenderStroke)
|
| 185 |
+
const { data: availableFonts = [] } = useListFonts()
|
|
|
|
| 186 |
const fontFamily = usePreferencesStore((state) => state.fontFamily)
|
| 187 |
const setFontFamily = usePreferencesStore((state) => state.setFontFamily)
|
| 188 |
+
const { textBlocks, selectedBlockIndex, replaceBlock, updateTextBlocks } = useTextBlocks()
|
| 189 |
const { t } = useTranslation()
|
| 190 |
const selectedBlock =
|
| 191 |
selectedBlockIndex !== undefined
|
ui/components/panels/TextBlocksPanel.tsx
CHANGED
|
@@ -2,12 +2,20 @@
|
|
| 2 |
|
| 3 |
import { useState } from 'react'
|
| 4 |
import { useTranslation } from 'react-i18next'
|
|
|
|
| 5 |
import { motion } from 'motion/react'
|
| 6 |
import { TextBlock } from '@/types'
|
| 7 |
import { Languages, LoaderCircleIcon, Trash2Icon } from 'lucide-react'
|
| 8 |
import { useTextBlocks } from '@/hooks/useTextBlocks'
|
| 9 |
-
import {
|
| 10 |
-
import {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
import {
|
| 12 |
Accordion,
|
| 13 |
AccordionContent,
|
|
@@ -24,6 +32,7 @@ import { DraftTextarea } from '@/components/ui/draft-textarea'
|
|
| 24 |
import { ScrollArea } from '@/components/ui/scroll-area'
|
| 25 |
|
| 26 |
export function TextBlocksPanel() {
|
|
|
|
| 27 |
const {
|
| 28 |
document,
|
| 29 |
textBlocks,
|
|
@@ -33,8 +42,8 @@ export function TextBlocksPanel() {
|
|
| 33 |
removeBlock,
|
| 34 |
} = useTextBlocks()
|
| 35 |
const { t } = useTranslation()
|
| 36 |
-
const {
|
| 37 |
-
const
|
| 38 |
const [generatingIndex, setGeneratingIndex] = useState<number | null>(null)
|
| 39 |
const generating = generatingIndex !== null
|
| 40 |
|
|
@@ -49,10 +58,30 @@ export function TextBlocksPanel() {
|
|
| 49 |
const accordionValue =
|
| 50 |
selectedBlockIndex !== undefined ? selectedBlockIndex.toString() : ''
|
| 51 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
const handleGenerate = async (blockIndex: number) => {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
setGeneratingIndex(blockIndex)
|
| 54 |
try {
|
| 55 |
-
await
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 56 |
} catch (error) {
|
| 57 |
console.error(error)
|
| 58 |
} finally {
|
|
|
|
| 2 |
|
| 3 |
import { useState } from 'react'
|
| 4 |
import { useTranslation } from 'react-i18next'
|
| 5 |
+
import { useQueryClient } from '@tanstack/react-query'
|
| 6 |
import { motion } from 'motion/react'
|
| 7 |
import { TextBlock } from '@/types'
|
| 8 |
import { Languages, LoaderCircleIcon, Trash2Icon } from 'lucide-react'
|
| 9 |
import { useTextBlocks } from '@/hooks/useTextBlocks'
|
| 10 |
+
import { useGetLlm } from '@/lib/api/llm/llm'
|
| 11 |
+
import { translateDocument } from '@/lib/api/processing/processing'
|
| 12 |
+
import { renderDocument } from '@/lib/api/processing/processing'
|
| 13 |
+
import {
|
| 14 |
+
getGetDocumentQueryKey,
|
| 15 |
+
getListDocumentsQueryKey,
|
| 16 |
+
} from '@/lib/api/documents/documents'
|
| 17 |
+
import { useEditorUiStore } from '@/lib/stores/editorUiStore'
|
| 18 |
+
import { usePreferencesStore } from '@/lib/stores/preferencesStore'
|
| 19 |
import {
|
| 20 |
Accordion,
|
| 21 |
AccordionContent,
|
|
|
|
| 32 |
import { ScrollArea } from '@/components/ui/scroll-area'
|
| 33 |
|
| 34 |
export function TextBlocksPanel() {
|
| 35 |
+
const queryClient = useQueryClient()
|
| 36 |
const {
|
| 37 |
document,
|
| 38 |
textBlocks,
|
|
|
|
| 42 |
removeBlock,
|
| 43 |
} = useTextBlocks()
|
| 44 |
const { t } = useTranslation()
|
| 45 |
+
const { data: llm } = useGetLlm()
|
| 46 |
+
const llmReady = llm?.status === 'ready'
|
| 47 |
const [generatingIndex, setGeneratingIndex] = useState<number | null>(null)
|
| 48 |
const generating = generatingIndex !== null
|
| 49 |
|
|
|
|
| 58 |
const accordionValue =
|
| 59 |
selectedBlockIndex !== undefined ? selectedBlockIndex.toString() : ''
|
| 60 |
|
| 61 |
+
const invalidateDocument = async (documentId: string) => {
|
| 62 |
+
await queryClient.invalidateQueries({ queryKey: getGetDocumentQueryKey(documentId) })
|
| 63 |
+
await queryClient.invalidateQueries({ queryKey: getListDocumentsQueryKey() })
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
const handleGenerate = async (blockIndex: number) => {
|
| 67 |
+
const documentId = useEditorUiStore.getState().currentDocumentId
|
| 68 |
+
if (!documentId) return
|
| 69 |
+
const selectedLanguage = useEditorUiStore.getState().selectedLanguage
|
| 70 |
+
const textBlockId = document.textBlocks[blockIndex]?.id
|
| 71 |
setGeneratingIndex(blockIndex)
|
| 72 |
try {
|
| 73 |
+
await translateDocument(documentId, { textBlockId, language: selectedLanguage })
|
| 74 |
+
await invalidateDocument(documentId)
|
| 75 |
+
useEditorUiStore.getState().setShowTextBlocksOverlay(true)
|
| 76 |
+
// Re-render the block's sprite
|
| 77 |
+
const { renderEffect, renderStroke } = useEditorUiStore.getState()
|
| 78 |
+
const { fontFamily } = usePreferencesStore.getState()
|
| 79 |
+
await renderDocument(documentId, {
|
| 80 |
+
shaderEffect: renderEffect,
|
| 81 |
+
shaderStroke: renderStroke,
|
| 82 |
+
fontFamily,
|
| 83 |
+
})
|
| 84 |
+
await invalidateDocument(documentId)
|
| 85 |
} catch (error) {
|
| 86 |
console.error(error)
|
| 87 |
} finally {
|
ui/hooks/useBlockContextMenu.ts
CHANGED
|
@@ -2,11 +2,11 @@
|
|
| 2 |
|
| 3 |
import { useState } from 'react'
|
| 4 |
import type React from 'react'
|
| 5 |
-
import {
|
| 6 |
import type { PointerToDocumentFn } from '@/hooks/usePointerToDocument'
|
| 7 |
|
| 8 |
type BlockContextMenuOptions = {
|
| 9 |
-
currentDocument:
|
| 10 |
pointerToDocument: PointerToDocumentFn
|
| 11 |
selectBlock: (index?: number) => void
|
| 12 |
removeBlock: (index: number) => void
|
|
|
|
| 2 |
|
| 3 |
import { useState } from 'react'
|
| 4 |
import type React from 'react'
|
| 5 |
+
import type { MappedDocument } from '@/hooks/useTextBlocks'
|
| 6 |
import type { PointerToDocumentFn } from '@/hooks/usePointerToDocument'
|
| 7 |
|
| 8 |
type BlockContextMenuOptions = {
|
| 9 |
+
currentDocument: MappedDocument | null
|
| 10 |
pointerToDocument: PointerToDocumentFn
|
| 11 |
selectBlock: (index?: number) => void
|
| 12 |
removeBlock: (index: number) => void
|
ui/hooks/useBlockDrafting.ts
CHANGED
|
@@ -2,7 +2,8 @@
|
|
| 2 |
|
| 3 |
import { useRef, useState } from 'react'
|
| 4 |
import { useDrag } from '@use-gesture/react'
|
| 5 |
-
import {
|
|
|
|
| 6 |
import type {
|
| 7 |
PointerToDocumentFn,
|
| 8 |
DocumentPointer,
|
|
@@ -10,7 +11,7 @@ import type {
|
|
| 10 |
|
| 11 |
type BlockDraftingOptions = {
|
| 12 |
mode: ToolMode
|
| 13 |
-
currentDocument:
|
| 14 |
pointerToDocument: PointerToDocumentFn
|
| 15 |
clearSelection: () => void
|
| 16 |
onCreateBlock: (block: TextBlock) => void
|
|
|
|
| 2 |
|
| 3 |
import { useRef, useState } from 'react'
|
| 4 |
import { useDrag } from '@use-gesture/react'
|
| 5 |
+
import { TextBlock, ToolMode } from '@/types'
|
| 6 |
+
import type { MappedDocument } from '@/hooks/useTextBlocks'
|
| 7 |
import type {
|
| 8 |
PointerToDocumentFn,
|
| 9 |
DocumentPointer,
|
|
|
|
| 11 |
|
| 12 |
type BlockDraftingOptions = {
|
| 13 |
mode: ToolMode
|
| 14 |
+
currentDocument: MappedDocument | null
|
| 15 |
pointerToDocument: PointerToDocumentFn
|
| 16 |
clearSelection: () => void
|
| 17 |
onCreateBlock: (block: TextBlock) => void
|
ui/hooks/useBrushLayerDisplay.ts
CHANGED
|
@@ -2,10 +2,10 @@
|
|
| 2 |
|
| 3 |
import { useEffect, useRef } from 'react'
|
| 4 |
import { convertToImageBitmap } from '@/lib/util'
|
| 5 |
-
import {
|
| 6 |
|
| 7 |
type BrushLayerDisplayOptions = {
|
| 8 |
-
currentDocument:
|
| 9 |
visible: boolean
|
| 10 |
}
|
| 11 |
|
|
|
|
| 2 |
|
| 3 |
import { useEffect, useRef } from 'react'
|
| 4 |
import { convertToImageBitmap } from '@/lib/util'
|
| 5 |
+
import type { MappedDocument } from '@/hooks/useTextBlocks'
|
| 6 |
|
| 7 |
type BrushLayerDisplayOptions = {
|
| 8 |
+
currentDocument: MappedDocument | null
|
| 9 |
visible: boolean
|
| 10 |
}
|
| 11 |
|
ui/hooks/useCanvasDrawing.ts
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'use client'
|
| 2 |
+
|
| 3 |
+
import { useEffect, useRef, type RefObject } from 'react'
|
| 4 |
+
import { useDrag } from '@use-gesture/react'
|
| 5 |
+
import { blobToUint8Array } from '@/lib/util'
|
| 6 |
+
import type { InpaintRegion } from '@/types'
|
| 7 |
+
import type { MappedDocument } from '@/hooks/useTextBlocks'
|
| 8 |
+
import {
|
| 9 |
+
type PointerToDocumentFn,
|
| 10 |
+
type DocumentPointer,
|
| 11 |
+
} from '@/hooks/usePointerToDocument'
|
| 12 |
+
|
| 13 |
+
// ---------------------------------------------------------------------------
|
| 14 |
+
// Types
|
| 15 |
+
// ---------------------------------------------------------------------------
|
| 16 |
+
|
| 17 |
+
type Bounds = {
|
| 18 |
+
minX: number
|
| 19 |
+
minY: number
|
| 20 |
+
maxX: number
|
| 21 |
+
maxY: number
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
export type CanvasDrawingConfig = {
|
| 25 |
+
getColor: () => string
|
| 26 |
+
blendMode: GlobalCompositeOperation
|
| 27 |
+
getBrushSize: () => number
|
| 28 |
+
onFinalize: (patch: Uint8Array, region: InpaintRegion) => Promise<void>
|
| 29 |
+
/** Called after finalize with the full-canvas PNG and the patch region. */
|
| 30 |
+
onFinalizeFullCanvas?: (
|
| 31 |
+
fullPng: Uint8Array,
|
| 32 |
+
patchRegion: InpaintRegion,
|
| 33 |
+
) => Promise<void>
|
| 34 |
+
enabled: boolean
|
| 35 |
+
/** Optional second canvas to mirror strokes to. */
|
| 36 |
+
targetCanvasRef?: RefObject<HTMLCanvasElement | null>
|
| 37 |
+
/** When true, clear the drawing canvas after each stroke finalize. */
|
| 38 |
+
clearAfterStroke?: boolean
|
| 39 |
+
/** Called to set up the canvas content when the document changes (e.g. draw existing mask). */
|
| 40 |
+
onCanvasInit?: (ctx: CanvasRenderingContext2D, doc: MappedDocument) => void | Promise<void>
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
// ---------------------------------------------------------------------------
|
| 44 |
+
// Helpers
|
| 45 |
+
// ---------------------------------------------------------------------------
|
| 46 |
+
|
| 47 |
+
const clampToDocument = (
|
| 48 |
+
point: DocumentPointer,
|
| 49 |
+
doc?: MappedDocument,
|
| 50 |
+
): DocumentPointer => {
|
| 51 |
+
if (!doc) return point
|
| 52 |
+
return {
|
| 53 |
+
x: Math.max(0, Math.min(doc.width, point.x)),
|
| 54 |
+
y: Math.max(0, Math.min(doc.height, point.y)),
|
| 55 |
+
}
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
const expandBounds = (
|
| 59 |
+
bounds: Bounds,
|
| 60 |
+
point: DocumentPointer,
|
| 61 |
+
radius: number,
|
| 62 |
+
): Bounds => ({
|
| 63 |
+
minX: Math.min(bounds.minX, point.x - radius),
|
| 64 |
+
minY: Math.min(bounds.minY, point.y - radius),
|
| 65 |
+
maxX: Math.max(bounds.maxX, point.x + radius),
|
| 66 |
+
maxY: Math.max(bounds.maxY, point.y + radius),
|
| 67 |
+
})
|
| 68 |
+
|
| 69 |
+
const boundsToRegion = (bounds: Bounds, doc: MappedDocument): InpaintRegion => {
|
| 70 |
+
const x0 = Math.max(0, Math.floor(bounds.minX))
|
| 71 |
+
const y0 = Math.max(0, Math.floor(bounds.minY))
|
| 72 |
+
const x1 = Math.min(doc.width, Math.ceil(bounds.maxX))
|
| 73 |
+
const y1 = Math.min(doc.height, Math.ceil(bounds.maxY))
|
| 74 |
+
return { x: x0, y: y0, width: Math.max(1, x1 - x0), height: Math.max(1, y1 - y0) }
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
const exportCanvasRegion = async (
|
| 78 |
+
canvas: HTMLCanvasElement,
|
| 79 |
+
region: InpaintRegion,
|
| 80 |
+
): Promise<Uint8Array | null> => {
|
| 81 |
+
if (region.width <= 0 || region.height <= 0) return null
|
| 82 |
+
const tmp = document.createElement('canvas')
|
| 83 |
+
tmp.width = region.width
|
| 84 |
+
tmp.height = region.height
|
| 85 |
+
const ctx = tmp.getContext('2d')
|
| 86 |
+
if (!ctx) return null
|
| 87 |
+
ctx.drawImage(canvas, region.x, region.y, region.width, region.height, 0, 0, region.width, region.height)
|
| 88 |
+
const blob = await new Promise<Blob | null>((r) => tmp.toBlob(r, 'image/png'))
|
| 89 |
+
return blob ? blobToUint8Array(blob) : null
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
const exportFullCanvas = async (
|
| 93 |
+
canvas: HTMLCanvasElement,
|
| 94 |
+
): Promise<Uint8Array | null> => {
|
| 95 |
+
const blob = await new Promise<Blob | null>((r) => canvas.toBlob(r, 'image/png'))
|
| 96 |
+
return blob ? blobToUint8Array(blob) : null
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
const initBounds = (point: DocumentPointer, radius: number): Bounds => ({
|
| 100 |
+
minX: point.x - radius,
|
| 101 |
+
minY: point.y - radius,
|
| 102 |
+
maxX: point.x + radius,
|
| 103 |
+
maxY: point.y + radius,
|
| 104 |
+
})
|
| 105 |
+
|
| 106 |
+
// ---------------------------------------------------------------------------
|
| 107 |
+
// Hook
|
| 108 |
+
// ---------------------------------------------------------------------------
|
| 109 |
+
|
| 110 |
+
export function useCanvasDrawing(
|
| 111 |
+
currentDocument: MappedDocument | null,
|
| 112 |
+
pointerToDocument: PointerToDocumentFn,
|
| 113 |
+
config: CanvasDrawingConfig,
|
| 114 |
+
) {
|
| 115 |
+
const canvasRef = useRef<HTMLCanvasElement | null>(null)
|
| 116 |
+
const ctxRef = useRef<CanvasRenderingContext2D | null>(null)
|
| 117 |
+
const drawingRef = useRef(false)
|
| 118 |
+
const lastPointRef = useRef<DocumentPointer | null>(null)
|
| 119 |
+
const boundsRef = useRef<Bounds | null>(null)
|
| 120 |
+
|
| 121 |
+
// Reset drawing state when disabled
|
| 122 |
+
useEffect(() => {
|
| 123 |
+
if (config.enabled) return
|
| 124 |
+
drawingRef.current = false
|
| 125 |
+
lastPointRef.current = null
|
| 126 |
+
boundsRef.current = null
|
| 127 |
+
}, [config.enabled])
|
| 128 |
+
|
| 129 |
+
// Canvas setup and resize
|
| 130 |
+
useEffect(() => {
|
| 131 |
+
const canvas = canvasRef.current
|
| 132 |
+
if (!canvas) return
|
| 133 |
+
const ctx = canvas.getContext('2d')
|
| 134 |
+
ctxRef.current = ctx
|
| 135 |
+
|
| 136 |
+
if (!currentDocument || !config.enabled) {
|
| 137 |
+
canvas.width = 0
|
| 138 |
+
canvas.height = 0
|
| 139 |
+
ctx?.clearRect(0, 0, canvas.width, canvas.height)
|
| 140 |
+
return () => {
|
| 141 |
+
drawingRef.current = false
|
| 142 |
+
lastPointRef.current = null
|
| 143 |
+
boundsRef.current = null
|
| 144 |
+
}
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
+
const needsResize =
|
| 148 |
+
canvas.width !== currentDocument.width ||
|
| 149 |
+
canvas.height !== currentDocument.height
|
| 150 |
+
|
| 151 |
+
if (needsResize) {
|
| 152 |
+
canvas.width = currentDocument.width
|
| 153 |
+
canvas.height = currentDocument.height
|
| 154 |
+
}
|
| 155 |
+
ctx?.clearRect(0, 0, canvas.width, canvas.height)
|
| 156 |
+
|
| 157 |
+
let cancelled = false
|
| 158 |
+
if (config.onCanvasInit && ctx) {
|
| 159 |
+
const result = config.onCanvasInit(ctx, currentDocument)
|
| 160 |
+
if (result && typeof (result as any).then === 'function') {
|
| 161 |
+
void (result as Promise<void>).catch(console.error)
|
| 162 |
+
}
|
| 163 |
+
}
|
| 164 |
+
|
| 165 |
+
return () => {
|
| 166 |
+
cancelled = true
|
| 167 |
+
drawingRef.current = false
|
| 168 |
+
lastPointRef.current = null
|
| 169 |
+
boundsRef.current = null
|
| 170 |
+
}
|
| 171 |
+
}, [
|
| 172 |
+
currentDocument?.id,
|
| 173 |
+
currentDocument?.width,
|
| 174 |
+
currentDocument?.height,
|
| 175 |
+
config.enabled,
|
| 176 |
+
])
|
| 177 |
+
|
| 178 |
+
const drawStroke = (from: DocumentPointer, to: DocumentPointer) => {
|
| 179 |
+
const color = config.getColor()
|
| 180 |
+
const brushSize = config.getBrushSize()
|
| 181 |
+
|
| 182 |
+
const stroke = (ctx: CanvasRenderingContext2D) => {
|
| 183 |
+
ctx.save()
|
| 184 |
+
ctx.lineCap = 'round'
|
| 185 |
+
ctx.lineJoin = 'round'
|
| 186 |
+
ctx.lineWidth = brushSize
|
| 187 |
+
ctx.strokeStyle = color
|
| 188 |
+
ctx.fillStyle = color
|
| 189 |
+
ctx.globalCompositeOperation = config.blendMode
|
| 190 |
+
ctx.beginPath()
|
| 191 |
+
ctx.moveTo(from.x, from.y)
|
| 192 |
+
ctx.lineTo(to.x, to.y)
|
| 193 |
+
ctx.stroke()
|
| 194 |
+
ctx.restore()
|
| 195 |
+
}
|
| 196 |
+
|
| 197 |
+
const ctx = ctxRef.current
|
| 198 |
+
if (ctx) stroke(ctx)
|
| 199 |
+
|
| 200 |
+
const targetCtx = config.targetCanvasRef?.current?.getContext('2d')
|
| 201 |
+
if (targetCtx) stroke(targetCtx)
|
| 202 |
+
}
|
| 203 |
+
|
| 204 |
+
const finalizeStroke = () => {
|
| 205 |
+
if (!config.enabled) return
|
| 206 |
+
const strokeBounds = boundsRef.current
|
| 207 |
+
if (!currentDocument || !strokeBounds) return
|
| 208 |
+
const patchRegion = boundsToRegion(strokeBounds, currentDocument)
|
| 209 |
+
boundsRef.current = null
|
| 210 |
+
drawingRef.current = false
|
| 211 |
+
lastPointRef.current = null
|
| 212 |
+
|
| 213 |
+
void (async () => {
|
| 214 |
+
const sourceCanvas = config.targetCanvasRef?.current ?? canvasRef.current
|
| 215 |
+
if (!sourceCanvas) return
|
| 216 |
+
const patchBytes = await exportCanvasRegion(sourceCanvas, patchRegion)
|
| 217 |
+
|
| 218 |
+
if (config.onFinalizeFullCanvas) {
|
| 219 |
+
const fullBytes = await exportFullCanvas(canvasRef.current!)
|
| 220 |
+
if (fullBytes) {
|
| 221 |
+
try {
|
| 222 |
+
await config.onFinalizeFullCanvas(fullBytes, patchRegion)
|
| 223 |
+
} catch (e) {
|
| 224 |
+
console.error(e)
|
| 225 |
+
}
|
| 226 |
+
}
|
| 227 |
+
}
|
| 228 |
+
|
| 229 |
+
if (patchBytes) {
|
| 230 |
+
try {
|
| 231 |
+
await config.onFinalize(patchBytes, patchRegion)
|
| 232 |
+
} catch (e) {
|
| 233 |
+
console.error(e)
|
| 234 |
+
}
|
| 235 |
+
}
|
| 236 |
+
|
| 237 |
+
if (config.clearAfterStroke) {
|
| 238 |
+
const ctx = ctxRef.current
|
| 239 |
+
const canvas = canvasRef.current
|
| 240 |
+
if (ctx && canvas) ctx.clearRect(0, 0, canvas.width, canvas.height)
|
| 241 |
+
}
|
| 242 |
+
})()
|
| 243 |
+
}
|
| 244 |
+
|
| 245 |
+
const bind = useDrag(
|
| 246 |
+
({ first, last, event, active }) => {
|
| 247 |
+
if (!config.enabled || !currentDocument) return
|
| 248 |
+
const sourceEvent = event as MouseEvent
|
| 249 |
+
const point = pointerToDocument(sourceEvent)
|
| 250 |
+
if (!point) {
|
| 251 |
+
if ((last || !active) && drawingRef.current) finalizeStroke()
|
| 252 |
+
return
|
| 253 |
+
}
|
| 254 |
+
const clamped = clampToDocument(point, currentDocument)
|
| 255 |
+
const brushSize = config.getBrushSize()
|
| 256 |
+
const radius = brushSize / 2
|
| 257 |
+
|
| 258 |
+
if (first) {
|
| 259 |
+
drawingRef.current = true
|
| 260 |
+
lastPointRef.current = clamped
|
| 261 |
+
boundsRef.current = initBounds(clamped, radius)
|
| 262 |
+
drawStroke(clamped, clamped)
|
| 263 |
+
return
|
| 264 |
+
}
|
| 265 |
+
|
| 266 |
+
if (!drawingRef.current) return
|
| 267 |
+
const lastPoint = lastPointRef.current ?? clamped
|
| 268 |
+
drawStroke(lastPoint, clamped)
|
| 269 |
+
lastPointRef.current = clamped
|
| 270 |
+
boundsRef.current = boundsRef.current
|
| 271 |
+
? expandBounds(boundsRef.current, clamped, radius)
|
| 272 |
+
: initBounds(clamped, radius)
|
| 273 |
+
|
| 274 |
+
if (last || !active) finalizeStroke()
|
| 275 |
+
},
|
| 276 |
+
{
|
| 277 |
+
pointer: { buttons: 1, touch: true },
|
| 278 |
+
preventDefault: true,
|
| 279 |
+
filterTaps: true,
|
| 280 |
+
eventOptions: { passive: false },
|
| 281 |
+
},
|
| 282 |
+
)
|
| 283 |
+
|
| 284 |
+
return { canvasRef, visible: config.enabled, bind }
|
| 285 |
+
}
|
ui/hooks/useCanvasZoom.ts
CHANGED
|
@@ -1,16 +1,19 @@
|
|
| 1 |
'use client'
|
| 2 |
|
| 3 |
import { useEditorUiStore } from '@/lib/stores/editorUiStore'
|
| 4 |
-
import {
|
| 5 |
|
| 6 |
export function useCanvasZoom() {
|
| 7 |
const scale = useEditorUiStore((state) => state.scale)
|
| 8 |
const setScaleRaw = useEditorUiStore((state) => state.setScale)
|
| 9 |
const setAutoFitEnabled = useEditorUiStore((state) => state.setAutoFitEnabled)
|
| 10 |
-
const
|
|
|
|
|
|
|
|
|
|
| 11 |
|
| 12 |
-
const summary =
|
| 13 |
-
? `${
|
| 14 |
: '--'
|
| 15 |
|
| 16 |
const applyScale = (value: number) => {
|
|
|
|
| 1 |
'use client'
|
| 2 |
|
| 3 |
import { useEditorUiStore } from '@/lib/stores/editorUiStore'
|
| 4 |
+
import { useGetDocument } from '@/lib/api/documents/documents'
|
| 5 |
|
| 6 |
export function useCanvasZoom() {
|
| 7 |
const scale = useEditorUiStore((state) => state.scale)
|
| 8 |
const setScaleRaw = useEditorUiStore((state) => state.setScale)
|
| 9 |
const setAutoFitEnabled = useEditorUiStore((state) => state.setAutoFitEnabled)
|
| 10 |
+
const documentId = useEditorUiStore((s) => s.currentDocumentId)
|
| 11 |
+
const { data: document } = useGetDocument(documentId ?? '', {
|
| 12 |
+
query: { enabled: !!documentId },
|
| 13 |
+
})
|
| 14 |
|
| 15 |
+
const summary = document
|
| 16 |
+
? `${document.width} x ${document.height}`
|
| 17 |
: '--'
|
| 18 |
|
| 19 |
const applyScale = (value: number) => {
|
ui/hooks/useMaskDrawing.ts
CHANGED
|
@@ -1,87 +1,31 @@
|
|
| 1 |
'use client'
|
| 2 |
|
| 3 |
-
import {
|
| 4 |
-
import {
|
| 5 |
import { usePreferencesStore } from '@/lib/stores/preferencesStore'
|
| 6 |
-
import { useEditorUiStore } from '@/lib/stores/editorUiStore'
|
| 7 |
-
import { useMaskMutations } from '@/lib/query/mutations'
|
| 8 |
-
import { blobToUint8Array, convertToImageBitmap } from '@/lib/util'
|
| 9 |
-
import { Document, InpaintRegion, ToolMode } from '@/types'
|
| 10 |
import {
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
} from '@/
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
|
| 15 |
type MaskDrawingOptions = {
|
| 16 |
mode: ToolMode
|
| 17 |
-
currentDocument:
|
| 18 |
pointerToDocument: PointerToDocumentFn
|
| 19 |
showMask: boolean
|
| 20 |
enabled: boolean
|
| 21 |
}
|
| 22 |
|
| 23 |
-
type Bounds = {
|
| 24 |
-
minX: number
|
| 25 |
-
minY: number
|
| 26 |
-
maxX: number
|
| 27 |
-
maxY: number
|
| 28 |
-
}
|
| 29 |
-
|
| 30 |
-
const clampToDocument = (
|
| 31 |
-
point: DocumentPointer,
|
| 32 |
-
doc?: Document,
|
| 33 |
-
): DocumentPointer => {
|
| 34 |
-
if (!doc) return point
|
| 35 |
-
return {
|
| 36 |
-
x: Math.max(0, Math.min(doc.width, point.x)),
|
| 37 |
-
y: Math.max(0, Math.min(doc.height, point.y)),
|
| 38 |
-
}
|
| 39 |
-
}
|
| 40 |
-
|
| 41 |
-
const expandBounds = (bounds: Bounds, point: DocumentPointer, radius: number) =>
|
| 42 |
-
({
|
| 43 |
-
minX: Math.min(bounds.minX, point.x - radius),
|
| 44 |
-
minY: Math.min(bounds.minY, point.y - radius),
|
| 45 |
-
maxX: Math.max(bounds.maxX, point.x + radius),
|
| 46 |
-
maxY: Math.max(bounds.maxY, point.y + radius),
|
| 47 |
-
}) satisfies Bounds
|
| 48 |
-
|
| 49 |
-
const withMargin = (
|
| 50 |
-
bounds: Bounds,
|
| 51 |
-
brushSize: number,
|
| 52 |
-
doc: Document,
|
| 53 |
-
): InpaintRegion => {
|
| 54 |
-
const width = Math.max(brushSize, bounds.maxX - bounds.minX)
|
| 55 |
-
const height = Math.max(brushSize, bounds.maxY - bounds.minY)
|
| 56 |
-
const margin = Math.min(width * 0.2, 32)
|
| 57 |
-
|
| 58 |
-
const x0 = Math.max(0, Math.floor(bounds.minX - margin))
|
| 59 |
-
const y0 = Math.max(0, Math.floor(bounds.minY - margin))
|
| 60 |
-
const x1 = Math.min(doc.width, Math.ceil(bounds.maxX + margin))
|
| 61 |
-
const y1 = Math.min(doc.height, Math.ceil(bounds.maxY + margin))
|
| 62 |
-
|
| 63 |
-
return {
|
| 64 |
-
x: x0,
|
| 65 |
-
y: y0,
|
| 66 |
-
width: Math.max(1, x1 - x0),
|
| 67 |
-
height: Math.max(1, y1 - y0),
|
| 68 |
-
}
|
| 69 |
-
}
|
| 70 |
-
|
| 71 |
-
const boundsToRegion = (bounds: Bounds, doc: Document): InpaintRegion => {
|
| 72 |
-
const x0 = Math.max(0, Math.floor(bounds.minX))
|
| 73 |
-
const y0 = Math.max(0, Math.floor(bounds.minY))
|
| 74 |
-
const x1 = Math.min(doc.width, Math.ceil(bounds.maxX))
|
| 75 |
-
const y1 = Math.min(doc.height, Math.ceil(bounds.maxY))
|
| 76 |
-
|
| 77 |
-
return {
|
| 78 |
-
x: x0,
|
| 79 |
-
y: y0,
|
| 80 |
-
width: Math.max(1, x1 - x0),
|
| 81 |
-
height: Math.max(1, y1 - y0),
|
| 82 |
-
}
|
| 83 |
-
}
|
| 84 |
-
|
| 85 |
export function useMaskDrawing({
|
| 86 |
mode,
|
| 87 |
currentDocument,
|
|
@@ -89,254 +33,86 @@ export function useMaskDrawing({
|
|
| 89 |
showMask,
|
| 90 |
enabled,
|
| 91 |
}: MaskDrawingOptions) {
|
| 92 |
-
const
|
| 93 |
-
brushConfig: { size: brushSize },
|
| 94 |
-
} = usePreferencesStore()
|
| 95 |
-
const { updateMask, inpaintPartial } = useMaskMutations()
|
| 96 |
-
const currentDocumentIndex = useEditorUiStore(
|
| 97 |
-
(state) => state.currentDocumentIndex,
|
| 98 |
-
)
|
| 99 |
-
const canvasRef = useRef<HTMLCanvasElement | null>(null)
|
| 100 |
-
const ctxRef = useRef<CanvasRenderingContext2D | null>(null)
|
| 101 |
-
const drawingRef = useRef(false)
|
| 102 |
-
const lastPointRef = useRef<DocumentPointer | null>(null)
|
| 103 |
-
const boundsRef = useRef<Bounds | null>(null)
|
| 104 |
const inpaintQueueRef = useRef<Promise<void>>(Promise.resolve())
|
| 105 |
-
const isRepairMode = mode === 'repairBrush'
|
| 106 |
const isEraseMode = mode === 'eraser'
|
| 107 |
-
const isActive = enabled && (
|
| 108 |
-
|
| 109 |
-
// Reset drawing state when interaction is disabled so stale strokes don't carry over.
|
| 110 |
-
useEffect(() => {
|
| 111 |
-
if (enabled) return
|
| 112 |
-
drawingRef.current = false
|
| 113 |
-
lastPointRef.current = null
|
| 114 |
-
boundsRef.current = null
|
| 115 |
-
inpaintQueueRef.current = Promise.resolve()
|
| 116 |
-
}, [enabled, mode])
|
| 117 |
-
|
| 118 |
-
useEffect(() => {
|
| 119 |
-
const canvas = canvasRef.current
|
| 120 |
-
if (!canvas) return
|
| 121 |
-
const ctx = canvas.getContext('2d')
|
| 122 |
-
ctxRef.current = ctx
|
| 123 |
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
const needsResize =
|
| 132 |
-
canvas.width !== currentDocument.width ||
|
| 133 |
-
canvas.height !== currentDocument.height
|
| 134 |
-
|
| 135 |
-
if (needsResize) {
|
| 136 |
-
canvas.width = currentDocument.width
|
| 137 |
-
canvas.height = currentDocument.height
|
| 138 |
-
ctx?.clearRect(0, 0, canvas.width, canvas.height)
|
| 139 |
-
ctx?.save()
|
| 140 |
-
ctx && (ctx.fillStyle = '#000')
|
| 141 |
-
ctx?.fillRect(0, 0, canvas.width, canvas.height)
|
| 142 |
-
ctx?.restore()
|
| 143 |
-
}
|
| 144 |
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 151 |
bitmap.close()
|
| 152 |
-
|
|
|
|
| 153 |
}
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
0,
|
| 161 |
-
0,
|
| 162 |
-
currentDocument.width,
|
| 163 |
-
currentDocument.height,
|
| 164 |
-
)
|
| 165 |
-
ctx?.restore()
|
| 166 |
-
bitmap.close()
|
| 167 |
-
} catch (error) {
|
| 168 |
-
console.error(error)
|
| 169 |
-
}
|
| 170 |
-
})()
|
| 171 |
-
}
|
| 172 |
-
|
| 173 |
-
return () => {
|
| 174 |
-
cancelled = true
|
| 175 |
-
drawingRef.current = false
|
| 176 |
-
lastPointRef.current = null
|
| 177 |
-
boundsRef.current = null
|
| 178 |
-
inpaintQueueRef.current = Promise.resolve()
|
| 179 |
-
}
|
| 180 |
-
}, [
|
| 181 |
-
currentDocument?.id,
|
| 182 |
-
currentDocument?.width,
|
| 183 |
-
currentDocument?.height,
|
| 184 |
-
currentDocument?.segment,
|
| 185 |
-
])
|
| 186 |
-
|
| 187 |
-
const drawStroke = (from: DocumentPointer, to: DocumentPointer) => {
|
| 188 |
-
const ctx = ctxRef.current
|
| 189 |
-
if (!ctx) return
|
| 190 |
-
ctx.save()
|
| 191 |
-
ctx.lineCap = 'round'
|
| 192 |
-
ctx.lineJoin = 'round'
|
| 193 |
-
ctx.lineWidth = brushSize
|
| 194 |
-
ctx.strokeStyle = isEraseMode ? '#000000' : '#ffffff'
|
| 195 |
-
ctx.fillStyle = ctx.strokeStyle
|
| 196 |
-
ctx.globalCompositeOperation = 'source-over'
|
| 197 |
-
|
| 198 |
-
ctx.beginPath()
|
| 199 |
-
ctx.moveTo(from.x, from.y)
|
| 200 |
-
ctx.lineTo(to.x, to.y)
|
| 201 |
-
ctx.stroke()
|
| 202 |
-
ctx.restore()
|
| 203 |
-
}
|
| 204 |
-
|
| 205 |
-
const exportMaskBytes = async (): Promise<Uint8Array | null> => {
|
| 206 |
-
const canvas = canvasRef.current
|
| 207 |
-
if (!canvas) return null
|
| 208 |
-
const blob = await new Promise<Blob | null>((resolve) => {
|
| 209 |
-
canvas.toBlob((result) => resolve(result), 'image/png')
|
| 210 |
-
})
|
| 211 |
-
if (!blob) return null
|
| 212 |
-
return blobToUint8Array(blob)
|
| 213 |
-
}
|
| 214 |
-
|
| 215 |
-
const exportMaskPatch = async (
|
| 216 |
-
region: InpaintRegion,
|
| 217 |
-
): Promise<Uint8Array | null> => {
|
| 218 |
-
const canvas = canvasRef.current
|
| 219 |
-
if (!canvas || region.width <= 0 || region.height <= 0) return null
|
| 220 |
-
|
| 221 |
-
const tempCanvas = document.createElement('canvas')
|
| 222 |
-
tempCanvas.width = region.width
|
| 223 |
-
tempCanvas.height = region.height
|
| 224 |
-
const tempCtx = tempCanvas.getContext('2d')
|
| 225 |
-
if (!tempCtx) return null
|
| 226 |
-
|
| 227 |
-
tempCtx.drawImage(
|
| 228 |
-
canvas,
|
| 229 |
-
region.x,
|
| 230 |
-
region.y,
|
| 231 |
-
region.width,
|
| 232 |
-
region.height,
|
| 233 |
-
0,
|
| 234 |
-
0,
|
| 235 |
-
region.width,
|
| 236 |
-
region.height,
|
| 237 |
-
)
|
| 238 |
-
|
| 239 |
-
const blob = await new Promise<Blob | null>((resolve) => {
|
| 240 |
-
tempCanvas.toBlob((result) => resolve(result), 'image/png')
|
| 241 |
-
})
|
| 242 |
-
if (!blob) return null
|
| 243 |
-
return blobToUint8Array(blob)
|
| 244 |
-
}
|
| 245 |
-
|
| 246 |
-
const queueInpaint = (task: () => Promise<void>) => {
|
| 247 |
-
inpaintQueueRef.current = inpaintQueueRef.current.catch(() => {}).then(task)
|
| 248 |
-
}
|
| 249 |
-
|
| 250 |
-
const finalizeStroke = () => {
|
| 251 |
-
if (!isActive) return
|
| 252 |
-
const strokeBounds = boundsRef.current
|
| 253 |
-
if (!currentDocument || !strokeBounds) return
|
| 254 |
-
const patchRegion = boundsToRegion(strokeBounds, currentDocument)
|
| 255 |
-
const region = withMargin(strokeBounds, brushSize, currentDocument)
|
| 256 |
-
boundsRef.current = null
|
| 257 |
-
drawingRef.current = false
|
| 258 |
-
lastPointRef.current = null
|
| 259 |
-
|
| 260 |
-
void (async () => {
|
| 261 |
-
const [maskBytes, patchBytes] = await Promise.all([
|
| 262 |
-
exportMaskBytes(),
|
| 263 |
-
exportMaskPatch(patchRegion),
|
| 264 |
-
])
|
| 265 |
-
if (!maskBytes) return
|
| 266 |
try {
|
| 267 |
-
await
|
| 268 |
-
|
| 269 |
-
patch: patchBytes ?? undefined,
|
| 270 |
})
|
| 271 |
-
} catch (
|
| 272 |
-
console.error(
|
| 273 |
-
}
|
| 274 |
-
queueInpaint(async () => {
|
| 275 |
-
try {
|
| 276 |
-
await inpaintPartial(region, {
|
| 277 |
-
index: currentDocumentIndex,
|
| 278 |
-
})
|
| 279 |
-
} catch (error) {
|
| 280 |
-
console.error(error)
|
| 281 |
-
}
|
| 282 |
-
})
|
| 283 |
-
})()
|
| 284 |
-
}
|
| 285 |
-
|
| 286 |
-
const bind = useDrag(
|
| 287 |
-
({ first, last, event, active }) => {
|
| 288 |
-
if (!isActive || !currentDocument) return
|
| 289 |
-
const sourceEvent = event as MouseEvent
|
| 290 |
-
const point = pointerToDocument(sourceEvent)
|
| 291 |
-
if (!point) {
|
| 292 |
-
if ((last || !active) && drawingRef.current) {
|
| 293 |
-
finalizeStroke()
|
| 294 |
-
}
|
| 295 |
-
return
|
| 296 |
}
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 310 |
}
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
minY: clamped.y - brushSize / 2,
|
| 321 |
-
maxX: clamped.x + brushSize / 2,
|
| 322 |
-
maxY: clamped.y + brushSize / 2,
|
| 323 |
}
|
| 324 |
-
|
| 325 |
-
if (last || !active) {
|
| 326 |
-
finalizeStroke()
|
| 327 |
-
}
|
| 328 |
-
},
|
| 329 |
-
{
|
| 330 |
-
pointer: { buttons: 1, touch: true },
|
| 331 |
-
preventDefault: true,
|
| 332 |
-
filterTaps: true,
|
| 333 |
-
eventOptions: { passive: false },
|
| 334 |
},
|
| 335 |
-
)
|
| 336 |
|
| 337 |
-
return {
|
| 338 |
-
canvasRef,
|
| 339 |
-
visible: showMask,
|
| 340 |
-
bind,
|
| 341 |
-
}
|
| 342 |
}
|
|
|
|
| 1 |
'use client'
|
| 2 |
|
| 3 |
+
import { useCallback, useRef } from 'react'
|
| 4 |
+
import { useQueryClient } from '@tanstack/react-query'
|
| 5 |
import { usePreferencesStore } from '@/lib/stores/preferencesStore'
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
import {
|
| 7 |
+
updateMask as updateMaskApi,
|
| 8 |
+
inpaintRegion as inpaintRegionApi,
|
| 9 |
+
} from '@/lib/api/regions/regions'
|
| 10 |
+
import {
|
| 11 |
+
getGetDocumentQueryKey,
|
| 12 |
+
getListDocumentsQueryKey,
|
| 13 |
+
} from '@/lib/api/documents/documents'
|
| 14 |
+
import { convertToImageBitmap } from '@/lib/util'
|
| 15 |
+
import { useEditorUiStore } from '@/lib/stores/editorUiStore'
|
| 16 |
+
import type { ToolMode } from '@/types'
|
| 17 |
+
import type { MappedDocument } from '@/hooks/useTextBlocks'
|
| 18 |
+
import type { PointerToDocumentFn } from '@/hooks/usePointerToDocument'
|
| 19 |
+
import { useCanvasDrawing } from '@/hooks/useCanvasDrawing'
|
| 20 |
|
| 21 |
type MaskDrawingOptions = {
|
| 22 |
mode: ToolMode
|
| 23 |
+
currentDocument: MappedDocument | null
|
| 24 |
pointerToDocument: PointerToDocumentFn
|
| 25 |
showMask: boolean
|
| 26 |
enabled: boolean
|
| 27 |
}
|
| 28 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
export function useMaskDrawing({
|
| 30 |
mode,
|
| 31 |
currentDocument,
|
|
|
|
| 33 |
showMask,
|
| 34 |
enabled,
|
| 35 |
}: MaskDrawingOptions) {
|
| 36 |
+
const queryClient = useQueryClient()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
const inpaintQueueRef = useRef<Promise<void>>(Promise.resolve())
|
|
|
|
| 38 |
const isEraseMode = mode === 'eraser'
|
| 39 |
+
const isActive = enabled && (mode === 'repairBrush' || isEraseMode)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
|
| 41 |
+
const invalidateDocument = useCallback(
|
| 42 |
+
async (documentId: string) => {
|
| 43 |
+
await queryClient.invalidateQueries({ queryKey: getGetDocumentQueryKey(documentId) })
|
| 44 |
+
await queryClient.invalidateQueries({ queryKey: getListDocumentsQueryKey() })
|
| 45 |
+
},
|
| 46 |
+
[queryClient],
|
| 47 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
|
| 49 |
+
const { canvasRef, bind } = useCanvasDrawing(currentDocument, pointerToDocument, {
|
| 50 |
+
getColor: () => (isEraseMode ? '#000000' : '#ffffff'),
|
| 51 |
+
blendMode: 'source-over',
|
| 52 |
+
getBrushSize: () => usePreferencesStore.getState().brushConfig.size,
|
| 53 |
+
enabled: isActive,
|
| 54 |
+
onCanvasInit: (ctx, doc) => {
|
| 55 |
+
// Fill black then draw existing segment mask on top
|
| 56 |
+
ctx.fillStyle = '#000'
|
| 57 |
+
ctx.fillRect(0, 0, doc.width, doc.height)
|
| 58 |
+
if (doc.segment) {
|
| 59 |
+
void (async () => {
|
| 60 |
+
try {
|
| 61 |
+
const bitmap = await convertToImageBitmap(doc.segment!)
|
| 62 |
+
ctx.save()
|
| 63 |
+
ctx.clearRect(0, 0, doc.width, doc.height)
|
| 64 |
+
ctx.drawImage(bitmap, 0, 0, doc.width, doc.height)
|
| 65 |
+
ctx.restore()
|
| 66 |
bitmap.close()
|
| 67 |
+
} catch (e) {
|
| 68 |
+
console.error(e)
|
| 69 |
}
|
| 70 |
+
})()
|
| 71 |
+
}
|
| 72 |
+
},
|
| 73 |
+
onFinalizeFullCanvas: async (fullPng) => {
|
| 74 |
+
const documentId = useEditorUiStore.getState().currentDocumentId
|
| 75 |
+
if (!documentId) return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 76 |
try {
|
| 77 |
+
await updateMaskApi(documentId, {
|
| 78 |
+
data: Array.from(fullPng),
|
|
|
|
| 79 |
})
|
| 80 |
+
} catch (e) {
|
| 81 |
+
console.error(e)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 82 |
}
|
| 83 |
+
},
|
| 84 |
+
onFinalize: async (_patch, region) => {
|
| 85 |
+
const documentId = useEditorUiStore.getState().currentDocumentId
|
| 86 |
+
if (!documentId) return
|
| 87 |
+
// Compute the inpaint region with margin
|
| 88 |
+
const brushSize = usePreferencesStore.getState().brushConfig.size
|
| 89 |
+
const width = Math.max(brushSize, region.width)
|
| 90 |
+
const height = Math.max(brushSize, region.height)
|
| 91 |
+
const margin = Math.min(width * 0.2, 32)
|
| 92 |
+
const doc = currentDocument!
|
| 93 |
+
const x0 = Math.max(0, Math.floor(region.x - margin))
|
| 94 |
+
const y0 = Math.max(0, Math.floor(region.y - margin))
|
| 95 |
+
const x1 = Math.min(doc.width, Math.ceil(region.x + region.width + margin))
|
| 96 |
+
const y1 = Math.min(doc.height, Math.ceil(region.y + region.height + margin))
|
| 97 |
+
const inpaintRegion = {
|
| 98 |
+
x: x0,
|
| 99 |
+
y: y0,
|
| 100 |
+
width: Math.max(1, x1 - x0),
|
| 101 |
+
height: Math.max(1, y1 - y0),
|
| 102 |
}
|
| 103 |
+
inpaintQueueRef.current = inpaintQueueRef.current
|
| 104 |
+
.catch(() => {})
|
| 105 |
+
.then(async () => {
|
| 106 |
+
try {
|
| 107 |
+
await inpaintRegionApi(documentId, { region: inpaintRegion })
|
| 108 |
+
await invalidateDocument(documentId)
|
| 109 |
+
useEditorUiStore.getState().setShowInpaintedImage(true)
|
| 110 |
+
} catch (e) {
|
| 111 |
+
console.error(e)
|
|
|
|
|
|
|
|
|
|
| 112 |
}
|
| 113 |
+
})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 114 |
},
|
| 115 |
+
})
|
| 116 |
|
| 117 |
+
return { canvasRef, visible: showMask, bind }
|
|
|
|
|
|
|
|
|
|
|
|
|
| 118 |
}
|
ui/hooks/useRenderBrushDrawing.ts
CHANGED
|
@@ -1,274 +1,56 @@
|
|
| 1 |
'use client'
|
| 2 |
|
| 3 |
-
import
|
| 4 |
-
import {
|
| 5 |
import { usePreferencesStore } from '@/lib/stores/preferencesStore'
|
| 6 |
-
import {
|
| 7 |
-
import { useMaskMutations } from '@/lib/query/mutations'
|
| 8 |
-
import { Document, InpaintRegion, ToolMode } from '@/types'
|
| 9 |
-
import { blobToUint8Array } from '@/lib/util'
|
| 10 |
import {
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
} from '@/
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
|
| 15 |
type RenderBrushOptions = {
|
| 16 |
mode: ToolMode
|
| 17 |
-
currentDocument:
|
| 18 |
pointerToDocument: PointerToDocumentFn
|
| 19 |
enabled: boolean
|
| 20 |
action: 'paint' | 'erase'
|
| 21 |
targetCanvasRef?: RefObject<HTMLCanvasElement | null>
|
| 22 |
}
|
| 23 |
|
| 24 |
-
type Bounds = {
|
| 25 |
-
minX: number
|
| 26 |
-
minY: number
|
| 27 |
-
maxX: number
|
| 28 |
-
maxY: number
|
| 29 |
-
}
|
| 30 |
-
|
| 31 |
-
const clampToDocument = (
|
| 32 |
-
point: DocumentPointer,
|
| 33 |
-
doc?: Document,
|
| 34 |
-
): DocumentPointer => {
|
| 35 |
-
if (!doc) return point
|
| 36 |
-
return {
|
| 37 |
-
x: Math.max(0, Math.min(doc.width, point.x)),
|
| 38 |
-
y: Math.max(0, Math.min(doc.height, point.y)),
|
| 39 |
-
}
|
| 40 |
-
}
|
| 41 |
-
|
| 42 |
-
const expandBounds = (bounds: Bounds, point: DocumentPointer, radius: number) =>
|
| 43 |
-
({
|
| 44 |
-
minX: Math.min(bounds.minX, point.x - radius),
|
| 45 |
-
minY: Math.min(bounds.minY, point.y - radius),
|
| 46 |
-
maxX: Math.max(bounds.maxX, point.x + radius),
|
| 47 |
-
maxY: Math.max(bounds.maxY, point.y + radius),
|
| 48 |
-
}) satisfies Bounds
|
| 49 |
-
|
| 50 |
-
const boundsToRegion = (bounds: Bounds, doc: Document): InpaintRegion => {
|
| 51 |
-
const x0 = Math.max(0, Math.floor(bounds.minX))
|
| 52 |
-
const y0 = Math.max(0, Math.floor(bounds.minY))
|
| 53 |
-
const x1 = Math.min(doc.width, Math.ceil(bounds.maxX))
|
| 54 |
-
const y1 = Math.min(doc.height, Math.ceil(bounds.maxY))
|
| 55 |
-
|
| 56 |
-
return {
|
| 57 |
-
x: x0,
|
| 58 |
-
y: y0,
|
| 59 |
-
width: Math.max(1, x1 - x0),
|
| 60 |
-
height: Math.max(1, y1 - y0),
|
| 61 |
-
}
|
| 62 |
-
}
|
| 63 |
-
|
| 64 |
export function useRenderBrushDrawing({
|
| 65 |
-
mode,
|
| 66 |
currentDocument,
|
| 67 |
pointerToDocument,
|
| 68 |
enabled,
|
| 69 |
action,
|
| 70 |
targetCanvasRef,
|
| 71 |
}: RenderBrushOptions) {
|
| 72 |
-
const
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
const ctxRef = useRef<CanvasRenderingContext2D | null>(null)
|
| 81 |
-
const drawingRef = useRef(false)
|
| 82 |
-
const lastPointRef = useRef<DocumentPointer | null>(null)
|
| 83 |
-
const boundsRef = useRef<Bounds | null>(null)
|
| 84 |
-
|
| 85 |
-
useEffect(() => {
|
| 86 |
-
const canvas = canvasRef.current
|
| 87 |
-
if (!canvas) return
|
| 88 |
-
const ctx = canvas.getContext('2d')
|
| 89 |
-
ctxRef.current = ctx
|
| 90 |
-
|
| 91 |
-
if (!currentDocument || !enabled) {
|
| 92 |
-
canvas.width = 0
|
| 93 |
-
canvas.height = 0
|
| 94 |
-
ctx?.clearRect(0, 0, canvas.width, canvas.height)
|
| 95 |
-
return () => {
|
| 96 |
-
drawingRef.current = false
|
| 97 |
-
lastPointRef.current = null
|
| 98 |
-
boundsRef.current = null
|
| 99 |
-
}
|
| 100 |
-
}
|
| 101 |
-
|
| 102 |
-
const needsResize =
|
| 103 |
-
canvas.width !== currentDocument.width ||
|
| 104 |
-
canvas.height !== currentDocument.height
|
| 105 |
-
|
| 106 |
-
if (needsResize) {
|
| 107 |
-
canvas.width = currentDocument.width
|
| 108 |
-
canvas.height = currentDocument.height
|
| 109 |
-
}
|
| 110 |
-
ctx?.clearRect(0, 0, canvas.width, canvas.height)
|
| 111 |
-
|
| 112 |
-
return () => {
|
| 113 |
-
drawingRef.current = false
|
| 114 |
-
lastPointRef.current = null
|
| 115 |
-
boundsRef.current = null
|
| 116 |
-
}
|
| 117 |
-
}, [
|
| 118 |
-
currentDocument?.id,
|
| 119 |
-
currentDocument?.width,
|
| 120 |
-
currentDocument?.height,
|
| 121 |
-
mode,
|
| 122 |
enabled,
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
ctx.globalCompositeOperation = isErasing
|
| 136 |
-
? 'destination-out'
|
| 137 |
-
: 'source-over'
|
| 138 |
-
ctx.beginPath()
|
| 139 |
-
ctx.moveTo(from.x, from.y)
|
| 140 |
-
ctx.lineTo(to.x, to.y)
|
| 141 |
-
ctx.stroke()
|
| 142 |
-
ctx.restore()
|
| 143 |
-
}
|
| 144 |
-
|
| 145 |
-
const ctx = ctxRef.current
|
| 146 |
-
if (ctx) stroke(ctx)
|
| 147 |
-
|
| 148 |
-
const targetCanvas = targetCanvasRef?.current
|
| 149 |
-
const targetCtx = targetCanvas?.getContext('2d')
|
| 150 |
-
if (targetCtx) stroke(targetCtx)
|
| 151 |
-
}
|
| 152 |
-
|
| 153 |
-
const exportPatch = async (
|
| 154 |
-
region: InpaintRegion,
|
| 155 |
-
): Promise<Uint8Array | null> => {
|
| 156 |
-
const canvas = targetCanvasRef?.current ?? canvasRef.current
|
| 157 |
-
if (!canvas || region.width <= 0 || region.height <= 0) return null
|
| 158 |
-
|
| 159 |
-
const tempCanvas = document.createElement('canvas')
|
| 160 |
-
tempCanvas.width = region.width
|
| 161 |
-
tempCanvas.height = region.height
|
| 162 |
-
const tempCtx = tempCanvas.getContext('2d')
|
| 163 |
-
if (!tempCtx) return null
|
| 164 |
-
|
| 165 |
-
tempCtx.drawImage(
|
| 166 |
-
canvas,
|
| 167 |
-
region.x,
|
| 168 |
-
region.y,
|
| 169 |
-
region.width,
|
| 170 |
-
region.height,
|
| 171 |
-
0,
|
| 172 |
-
0,
|
| 173 |
-
region.width,
|
| 174 |
-
region.height,
|
| 175 |
-
)
|
| 176 |
-
|
| 177 |
-
const blob = await new Promise<Blob | null>((resolve) => {
|
| 178 |
-
tempCanvas.toBlob((result) => resolve(result), 'image/png')
|
| 179 |
-
})
|
| 180 |
-
if (!blob) return null
|
| 181 |
-
return blobToUint8Array(blob)
|
| 182 |
-
}
|
| 183 |
-
|
| 184 |
-
const finalizeStroke = () => {
|
| 185 |
-
if (!enabled) return
|
| 186 |
-
const strokeBounds = boundsRef.current
|
| 187 |
-
if (!currentDocument || !strokeBounds) return
|
| 188 |
-
const patchRegion = boundsToRegion(strokeBounds, currentDocument)
|
| 189 |
-
boundsRef.current = null
|
| 190 |
-
drawingRef.current = false
|
| 191 |
-
lastPointRef.current = null
|
| 192 |
-
|
| 193 |
-
void (async () => {
|
| 194 |
-
const patchBytes = await exportPatch(patchRegion)
|
| 195 |
-
if (!patchBytes) {
|
| 196 |
-
const canvas = canvasRef.current
|
| 197 |
-
const ctx = ctxRef.current
|
| 198 |
-
if (canvas && ctx) {
|
| 199 |
-
ctx.clearRect(0, 0, canvas.width, canvas.height)
|
| 200 |
-
}
|
| 201 |
-
return
|
| 202 |
-
}
|
| 203 |
-
try {
|
| 204 |
-
await paintRendered(patchBytes, patchRegion, {
|
| 205 |
-
index: currentDocumentIndex,
|
| 206 |
-
})
|
| 207 |
-
} catch (error) {
|
| 208 |
-
console.error(error)
|
| 209 |
-
}
|
| 210 |
-
const canvas = canvasRef.current
|
| 211 |
-
const ctx = ctxRef.current
|
| 212 |
-
if (canvas && ctx) {
|
| 213 |
-
ctx.clearRect(0, 0, canvas.width, canvas.height)
|
| 214 |
-
}
|
| 215 |
-
})()
|
| 216 |
-
}
|
| 217 |
-
|
| 218 |
-
const bind = useDrag(
|
| 219 |
-
({ first, last, event, active }) => {
|
| 220 |
-
if (!enabled || !currentDocument) return
|
| 221 |
-
const sourceEvent = event as MouseEvent
|
| 222 |
-
const point = pointerToDocument(sourceEvent)
|
| 223 |
-
if (!point) {
|
| 224 |
-
if ((last || !active) && drawingRef.current) {
|
| 225 |
-
finalizeStroke()
|
| 226 |
-
}
|
| 227 |
-
return
|
| 228 |
-
}
|
| 229 |
-
const clamped = clampToDocument(point, currentDocument)
|
| 230 |
-
|
| 231 |
-
if (first) {
|
| 232 |
-
drawingRef.current = true
|
| 233 |
-
lastPointRef.current = clamped
|
| 234 |
-
boundsRef.current = {
|
| 235 |
-
minX: clamped.x - brushSize / 2,
|
| 236 |
-
minY: clamped.y - brushSize / 2,
|
| 237 |
-
maxX: clamped.x + brushSize / 2,
|
| 238 |
-
maxY: clamped.y + brushSize / 2,
|
| 239 |
-
}
|
| 240 |
-
drawStroke(clamped, clamped)
|
| 241 |
-
return
|
| 242 |
-
}
|
| 243 |
-
|
| 244 |
-
if (!drawingRef.current) return
|
| 245 |
-
const lastPoint = lastPointRef.current ?? clamped
|
| 246 |
-
drawStroke(lastPoint, clamped)
|
| 247 |
-
lastPointRef.current = clamped
|
| 248 |
-
boundsRef.current = boundsRef.current
|
| 249 |
-
? expandBounds(boundsRef.current, clamped, brushSize / 2)
|
| 250 |
-
: {
|
| 251 |
-
minX: clamped.x - brushSize / 2,
|
| 252 |
-
minY: clamped.y - brushSize / 2,
|
| 253 |
-
maxX: clamped.x + brushSize / 2,
|
| 254 |
-
maxY: clamped.y + brushSize / 2,
|
| 255 |
-
}
|
| 256 |
-
|
| 257 |
-
if (last || !active) {
|
| 258 |
-
finalizeStroke()
|
| 259 |
-
}
|
| 260 |
-
},
|
| 261 |
-
{
|
| 262 |
-
pointer: { buttons: 1, touch: true },
|
| 263 |
-
preventDefault: true,
|
| 264 |
-
filterTaps: true,
|
| 265 |
-
eventOptions: { passive: false },
|
| 266 |
},
|
| 267 |
-
)
|
| 268 |
-
|
| 269 |
-
return {
|
| 270 |
-
canvasRef,
|
| 271 |
-
visible: enabled,
|
| 272 |
-
bind,
|
| 273 |
-
}
|
| 274 |
}
|
|
|
|
| 1 |
'use client'
|
| 2 |
|
| 3 |
+
import type { RefObject } from 'react'
|
| 4 |
+
import { useQueryClient } from '@tanstack/react-query'
|
| 5 |
import { usePreferencesStore } from '@/lib/stores/preferencesStore'
|
| 6 |
+
import { updateBrushLayer } from '@/lib/api/regions/regions'
|
|
|
|
|
|
|
|
|
|
| 7 |
import {
|
| 8 |
+
getGetDocumentQueryKey,
|
| 9 |
+
getListDocumentsQueryKey,
|
| 10 |
+
} from '@/lib/api/documents/documents'
|
| 11 |
+
import { useEditorUiStore } from '@/lib/stores/editorUiStore'
|
| 12 |
+
import type { ToolMode } from '@/types'
|
| 13 |
+
import type { MappedDocument } from '@/hooks/useTextBlocks'
|
| 14 |
+
import type { PointerToDocumentFn } from '@/hooks/usePointerToDocument'
|
| 15 |
+
import { useCanvasDrawing } from '@/hooks/useCanvasDrawing'
|
| 16 |
|
| 17 |
type RenderBrushOptions = {
|
| 18 |
mode: ToolMode
|
| 19 |
+
currentDocument: MappedDocument | null
|
| 20 |
pointerToDocument: PointerToDocumentFn
|
| 21 |
enabled: boolean
|
| 22 |
action: 'paint' | 'erase'
|
| 23 |
targetCanvasRef?: RefObject<HTMLCanvasElement | null>
|
| 24 |
}
|
| 25 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
export function useRenderBrushDrawing({
|
|
|
|
| 27 |
currentDocument,
|
| 28 |
pointerToDocument,
|
| 29 |
enabled,
|
| 30 |
action,
|
| 31 |
targetCanvasRef,
|
| 32 |
}: RenderBrushOptions) {
|
| 33 |
+
const queryClient = useQueryClient()
|
| 34 |
+
const isErasing = action === 'erase'
|
| 35 |
+
|
| 36 |
+
return useCanvasDrawing(currentDocument, pointerToDocument, {
|
| 37 |
+
getColor: () =>
|
| 38 |
+
isErasing ? '#000000' : usePreferencesStore.getState().brushConfig.color,
|
| 39 |
+
blendMode: isErasing ? 'destination-out' : 'source-over',
|
| 40 |
+
getBrushSize: () => usePreferencesStore.getState().brushConfig.size,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
enabled,
|
| 42 |
+
targetCanvasRef,
|
| 43 |
+
clearAfterStroke: true,
|
| 44 |
+
onFinalize: async (patch, region) => {
|
| 45 |
+
const documentId = useEditorUiStore.getState().currentDocumentId
|
| 46 |
+
if (!documentId) return
|
| 47 |
+
await updateBrushLayer(documentId, {
|
| 48 |
+
data: Array.from(patch),
|
| 49 |
+
region,
|
| 50 |
+
})
|
| 51 |
+
await queryClient.invalidateQueries({ queryKey: getGetDocumentQueryKey(documentId) })
|
| 52 |
+
await queryClient.invalidateQueries({ queryKey: getListDocumentsQueryKey() })
|
| 53 |
+
useEditorUiStore.getState().setShowBrushLayer(true)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
},
|
| 55 |
+
})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 56 |
}
|
ui/hooks/useRpcConnection.ts
DELETED
|
@@ -1,12 +0,0 @@
|
|
| 1 |
-
'use client'
|
| 2 |
-
|
| 3 |
-
import { useEffect, useState } from 'react'
|
| 4 |
-
import { subscribeRpcConnection } from '@/lib/backend'
|
| 5 |
-
|
| 6 |
-
export const useRpcConnection = () => {
|
| 7 |
-
const [connected, setConnected] = useState(false)
|
| 8 |
-
|
| 9 |
-
useEffect(() => subscribeRpcConnection(setConnected), [])
|
| 10 |
-
|
| 11 |
-
return connected
|
| 12 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ui/hooks/useTextBlocks.ts
CHANGED
|
@@ -1,11 +1,21 @@
|
|
| 1 |
'use client'
|
| 2 |
|
| 3 |
-
import { useEffect, useRef } from 'react'
|
| 4 |
-
import {
|
| 5 |
-
import {
|
| 6 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
import { useEditorUiStore } from '@/lib/stores/editorUiStore'
|
|
|
|
| 8 |
import { TextBlock } from '@/types'
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
|
| 10 |
const TEXT_BLOCK_RENDER_DEBOUNCE_MS = 250
|
| 11 |
|
|
@@ -25,9 +35,86 @@ const hasGeometryChange = (updates: Partial<TextBlock>) =>
|
|
| 25 |
Object.prototype.hasOwnProperty.call(updates, 'width') ||
|
| 26 |
Object.prototype.hasOwnProperty.call(updates, 'height')
|
| 27 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
export function useTextBlocks() {
|
| 29 |
-
const
|
| 30 |
-
|
|
|
|
| 31 |
const textBlocks = document?.textBlocks ?? []
|
| 32 |
const selectedBlockIndex = useEditorUiStore(
|
| 33 |
(state) => state.selectedBlockIndex,
|
|
@@ -35,11 +122,43 @@ export function useTextBlocks() {
|
|
| 35 |
const setSelectedBlockIndex = useEditorUiStore(
|
| 36 |
(state) => state.setSelectedBlockIndex,
|
| 37 |
)
|
| 38 |
-
const { updateTextBlocks, renderTextBlock } = useTextBlockMutations()
|
| 39 |
const renderTimersRef = useRef<Map<number, ReturnType<typeof setTimeout>>>(
|
| 40 |
new Map(),
|
| 41 |
)
|
| 42 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
useEffect(() => {
|
| 44 |
const timers = renderTimersRef.current
|
| 45 |
return () => {
|
|
@@ -59,7 +178,7 @@ export function useTextBlocks() {
|
|
| 59 |
clearScheduledRender(index)
|
| 60 |
const timer = setTimeout(() => {
|
| 61 |
renderTimersRef.current.delete(index)
|
| 62 |
-
void renderTextBlock(
|
| 63 |
}, TEXT_BLOCK_RENDER_DEBOUNCE_MS)
|
| 64 |
renderTimersRef.current.set(index, timer)
|
| 65 |
}
|
|
@@ -77,12 +196,10 @@ export function useTextBlocks() {
|
|
| 77 |
ui.setShowTextBlocksOverlay(true)
|
| 78 |
}
|
| 79 |
|
| 80 |
-
const doc = document
|
| 81 |
-
|
| 82 |
if (shouldRenderSprite(updates)) {
|
| 83 |
if (shouldRenderSpriteImmediately(updates)) {
|
| 84 |
clearScheduledRender(index)
|
| 85 |
-
void renderTextBlock(
|
| 86 |
} else {
|
| 87 |
scheduleRender(index)
|
| 88 |
}
|
|
@@ -123,5 +240,6 @@ export function useTextBlocks() {
|
|
| 123 |
replaceBlock,
|
| 124 |
appendBlock,
|
| 125 |
removeBlock,
|
|
|
|
| 126 |
}
|
| 127 |
}
|
|
|
|
| 1 |
'use client'
|
| 2 |
|
| 3 |
+
import { useCallback, useEffect, useRef } from 'react'
|
| 4 |
+
import { useQueryClient } from '@tanstack/react-query'
|
| 5 |
+
import {
|
| 6 |
+
useGetDocument,
|
| 7 |
+
getGetDocumentQueryKey,
|
| 8 |
+
getListDocumentsQueryKey,
|
| 9 |
+
} from '@/lib/api/documents/documents'
|
| 10 |
+
import { putTextBlocks } from '@/lib/api/text-blocks/text-blocks'
|
| 11 |
+
import { renderDocument } from '@/lib/api/processing/processing'
|
| 12 |
import { useEditorUiStore } from '@/lib/stores/editorUiStore'
|
| 13 |
+
import { usePreferencesStore } from '@/lib/stores/preferencesStore'
|
| 14 |
import { TextBlock } from '@/types'
|
| 15 |
+
import type { DocumentDetail, TextBlockInput } from '@/lib/api/schemas'
|
| 16 |
+
|
| 17 |
+
const createTempTextBlockId = () =>
|
| 18 |
+
`temp:${globalThis.crypto?.randomUUID?.() ?? Math.random().toString(36).slice(2)}`
|
| 19 |
|
| 20 |
const TEXT_BLOCK_RENDER_DEBOUNCE_MS = 250
|
| 21 |
|
|
|
|
| 35 |
Object.prototype.hasOwnProperty.call(updates, 'width') ||
|
| 36 |
Object.prototype.hasOwnProperty.call(updates, 'height')
|
| 37 |
|
| 38 |
+
const toUint8Array = (data: number[] | null | undefined): Uint8Array | undefined =>
|
| 39 |
+
data ? new Uint8Array(data) : undefined
|
| 40 |
+
|
| 41 |
+
const mapTextBlock = (
|
| 42 |
+
block: DocumentDetail['textBlocks'][number],
|
| 43 |
+
): TextBlock => ({
|
| 44 |
+
id: block.id,
|
| 45 |
+
x: block.x,
|
| 46 |
+
y: block.y,
|
| 47 |
+
width: block.width,
|
| 48 |
+
height: block.height,
|
| 49 |
+
confidence: block.confidence,
|
| 50 |
+
linePolygons: block.linePolygons as TextBlock['linePolygons'],
|
| 51 |
+
sourceDirection: block.sourceDirection ?? undefined,
|
| 52 |
+
renderedDirection: block.renderedDirection ?? undefined,
|
| 53 |
+
sourceLanguage: block.sourceLanguage ?? undefined,
|
| 54 |
+
rotationDeg: block.rotationDeg ?? undefined,
|
| 55 |
+
detectedFontSizePx: block.detectedFontSizePx ?? undefined,
|
| 56 |
+
detector: block.detector ?? undefined,
|
| 57 |
+
text: block.text ?? undefined,
|
| 58 |
+
translation: block.translation ?? undefined,
|
| 59 |
+
style: block.style as TextBlock['style'],
|
| 60 |
+
fontPrediction: block.fontPrediction as TextBlock['fontPrediction'],
|
| 61 |
+
rendered: undefined,
|
| 62 |
+
})
|
| 63 |
+
|
| 64 |
+
export type MappedDocument = {
|
| 65 |
+
id: string
|
| 66 |
+
path: string
|
| 67 |
+
name: string
|
| 68 |
+
width: number
|
| 69 |
+
height: number
|
| 70 |
+
revision: number
|
| 71 |
+
textBlocks: TextBlock[]
|
| 72 |
+
image: Uint8Array
|
| 73 |
+
segment?: Uint8Array
|
| 74 |
+
inpainted?: Uint8Array
|
| 75 |
+
brushLayer?: Uint8Array
|
| 76 |
+
rendered?: Uint8Array
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
const mapDocumentDetail = (detail: DocumentDetail): MappedDocument => ({
|
| 80 |
+
id: detail.id,
|
| 81 |
+
path: detail.path,
|
| 82 |
+
name: detail.name,
|
| 83 |
+
width: detail.width,
|
| 84 |
+
height: detail.height,
|
| 85 |
+
revision: detail.revision,
|
| 86 |
+
textBlocks: detail.textBlocks.map(mapTextBlock),
|
| 87 |
+
image: new Uint8Array(detail.image),
|
| 88 |
+
segment: toUint8Array(detail.segment),
|
| 89 |
+
inpainted: toUint8Array(detail.inpainted),
|
| 90 |
+
brushLayer: toUint8Array(detail.brushLayer),
|
| 91 |
+
rendered: toUint8Array(detail.rendered),
|
| 92 |
+
})
|
| 93 |
+
|
| 94 |
+
const toTextBlockInput = (block: TextBlock): TextBlockInput => ({
|
| 95 |
+
id: block.id ?? null,
|
| 96 |
+
x: block.x,
|
| 97 |
+
y: block.y,
|
| 98 |
+
width: block.width,
|
| 99 |
+
height: block.height,
|
| 100 |
+
text: block.text ?? null,
|
| 101 |
+
translation: block.translation ?? null,
|
| 102 |
+
style: (block.style as any) ?? null,
|
| 103 |
+
})
|
| 104 |
+
|
| 105 |
+
export function useCurrentDocument(): MappedDocument | null {
|
| 106 |
+
const documentId = useEditorUiStore((s) => s.currentDocumentId)
|
| 107 |
+
const { data: detail } = useGetDocument(documentId ?? '', {
|
| 108 |
+
query: { enabled: !!documentId, structuralSharing: false },
|
| 109 |
+
})
|
| 110 |
+
if (!detail) return null
|
| 111 |
+
return mapDocumentDetail(detail)
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
export function useTextBlocks() {
|
| 115 |
+
const queryClient = useQueryClient()
|
| 116 |
+
const document = useCurrentDocument()
|
| 117 |
+
const documentId = useEditorUiStore((s) => s.currentDocumentId)
|
| 118 |
const textBlocks = document?.textBlocks ?? []
|
| 119 |
const selectedBlockIndex = useEditorUiStore(
|
| 120 |
(state) => state.selectedBlockIndex,
|
|
|
|
| 122 |
const setSelectedBlockIndex = useEditorUiStore(
|
| 123 |
(state) => state.setSelectedBlockIndex,
|
| 124 |
)
|
|
|
|
| 125 |
const renderTimersRef = useRef<Map<number, ReturnType<typeof setTimeout>>>(
|
| 126 |
new Map(),
|
| 127 |
)
|
| 128 |
|
| 129 |
+
const invalidateDocument = useCallback(
|
| 130 |
+
async (docId: string) => {
|
| 131 |
+
await queryClient.invalidateQueries({ queryKey: getGetDocumentQueryKey(docId) })
|
| 132 |
+
await queryClient.invalidateQueries({ queryKey: getListDocumentsQueryKey() })
|
| 133 |
+
},
|
| 134 |
+
[queryClient],
|
| 135 |
+
)
|
| 136 |
+
|
| 137 |
+
const updateTextBlocks = useCallback(
|
| 138 |
+
async (blocks: TextBlock[]) => {
|
| 139 |
+
const docId = useEditorUiStore.getState().currentDocumentId
|
| 140 |
+
if (!docId) return
|
| 141 |
+
await putTextBlocks(docId, blocks.map(toTextBlockInput))
|
| 142 |
+
await invalidateDocument(docId)
|
| 143 |
+
},
|
| 144 |
+
[invalidateDocument],
|
| 145 |
+
)
|
| 146 |
+
|
| 147 |
+
const renderTextBlock = useCallback(
|
| 148 |
+
async (docId: string, textBlockIndex: number) => {
|
| 149 |
+
if (typeof textBlockIndex !== 'number') return
|
| 150 |
+
const { renderEffect, renderStroke } = useEditorUiStore.getState()
|
| 151 |
+
const { fontFamily } = usePreferencesStore.getState()
|
| 152 |
+
await renderDocument(docId, {
|
| 153 |
+
shaderEffect: renderEffect,
|
| 154 |
+
shaderStroke: renderStroke,
|
| 155 |
+
fontFamily,
|
| 156 |
+
})
|
| 157 |
+
await invalidateDocument(docId)
|
| 158 |
+
},
|
| 159 |
+
[invalidateDocument],
|
| 160 |
+
)
|
| 161 |
+
|
| 162 |
useEffect(() => {
|
| 163 |
const timers = renderTimersRef.current
|
| 164 |
return () => {
|
|
|
|
| 178 |
clearScheduledRender(index)
|
| 179 |
const timer = setTimeout(() => {
|
| 180 |
renderTimersRef.current.delete(index)
|
| 181 |
+
void renderTextBlock(documentId!, index)
|
| 182 |
}, TEXT_BLOCK_RENDER_DEBOUNCE_MS)
|
| 183 |
renderTimersRef.current.set(index, timer)
|
| 184 |
}
|
|
|
|
| 196 |
ui.setShowTextBlocksOverlay(true)
|
| 197 |
}
|
| 198 |
|
|
|
|
|
|
|
| 199 |
if (shouldRenderSprite(updates)) {
|
| 200 |
if (shouldRenderSpriteImmediately(updates)) {
|
| 201 |
clearScheduledRender(index)
|
| 202 |
+
void renderTextBlock(documentId!, index)
|
| 203 |
} else {
|
| 204 |
scheduleRender(index)
|
| 205 |
}
|
|
|
|
| 240 |
replaceBlock,
|
| 241 |
appendBlock,
|
| 242 |
removeBlock,
|
| 243 |
+
updateTextBlocks,
|
| 244 |
}
|
| 245 |
}
|
ui/lib/api.ts
DELETED
|
@@ -1,822 +0,0 @@
|
|
| 1 |
-
'use client'
|
| 2 |
-
|
| 3 |
-
import { directoryOpen, fileOpen, fileSave } from 'browser-fs-access'
|
| 4 |
-
import {
|
| 5 |
-
fetchBinary,
|
| 6 |
-
fetchJson,
|
| 7 |
-
getActivePipelineJobId,
|
| 8 |
-
setActivePipelineJobId,
|
| 9 |
-
} from '@/lib/backend'
|
| 10 |
-
import { reportRpcError } from '@/lib/errors'
|
| 11 |
-
import type {
|
| 12 |
-
ApiKeyResponse,
|
| 13 |
-
BootstrapConfig,
|
| 14 |
-
DocumentDetail,
|
| 15 |
-
DocumentSummary,
|
| 16 |
-
ExportResult,
|
| 17 |
-
FontFaceInfo,
|
| 18 |
-
JobState,
|
| 19 |
-
LlmModelInfo,
|
| 20 |
-
LlmState,
|
| 21 |
-
MetaInfo,
|
| 22 |
-
TextBlockPatch,
|
| 23 |
-
} from '@/lib/protocol'
|
| 24 |
-
import {
|
| 25 |
-
Document,
|
| 26 |
-
InpaintRegion,
|
| 27 |
-
RenderEffect,
|
| 28 |
-
RenderStroke,
|
| 29 |
-
TextBlock,
|
| 30 |
-
TextStyle,
|
| 31 |
-
} from '@/types'
|
| 32 |
-
import { toArrayBuffer } from '@/lib/util'
|
| 33 |
-
|
| 34 |
-
const documentDetailCache = new Map<string, DocumentDetail>()
|
| 35 |
-
const textBlockIdAliases = new Map<string, string>()
|
| 36 |
-
|
| 37 |
-
const isTempTextBlockId = (id?: string) => !!id && id.startsWith('temp:')
|
| 38 |
-
|
| 39 |
-
const textBlockAliasKey = (documentId: string, textBlockId: string) =>
|
| 40 |
-
`${documentId}:${textBlockId}`
|
| 41 |
-
|
| 42 |
-
const resolveTextBlockIdAlias = (documentId: string, textBlockId?: string) => {
|
| 43 |
-
if (!textBlockId) return undefined
|
| 44 |
-
return (
|
| 45 |
-
textBlockIdAliases.get(textBlockAliasKey(documentId, textBlockId)) ??
|
| 46 |
-
textBlockId
|
| 47 |
-
)
|
| 48 |
-
}
|
| 49 |
-
|
| 50 |
-
const rememberTextBlockAlias = (
|
| 51 |
-
documentId: string,
|
| 52 |
-
tempId: string | undefined,
|
| 53 |
-
realId: string,
|
| 54 |
-
) => {
|
| 55 |
-
if (!tempId || !isTempTextBlockId(tempId)) return
|
| 56 |
-
textBlockIdAliases.set(textBlockAliasKey(documentId, tempId), realId)
|
| 57 |
-
}
|
| 58 |
-
|
| 59 |
-
const sameJson = (left: unknown, right: unknown) =>
|
| 60 |
-
JSON.stringify(left ?? null) === JSON.stringify(right ?? null)
|
| 61 |
-
|
| 62 |
-
const withRpcError = async <T>(
|
| 63 |
-
method: string,
|
| 64 |
-
fn: () => Promise<T>,
|
| 65 |
-
): Promise<T> => {
|
| 66 |
-
try {
|
| 67 |
-
return await fn()
|
| 68 |
-
} catch (error) {
|
| 69 |
-
reportRpcError(method, error)
|
| 70 |
-
throw error
|
| 71 |
-
}
|
| 72 |
-
}
|
| 73 |
-
|
| 74 |
-
const toBinaryArray = (value: Uint8Array) => Array.from(value)
|
| 75 |
-
|
| 76 |
-
const getDocuments = async (): Promise<DocumentSummary[]> => {
|
| 77 |
-
const documents = await fetchJson<DocumentSummary[]>('/documents')
|
| 78 |
-
const prunedIds = new Set(documents.map((document) => document.id))
|
| 79 |
-
for (const documentId of documentDetailCache.keys()) {
|
| 80 |
-
if (!prunedIds.has(documentId)) {
|
| 81 |
-
documentDetailCache.delete(documentId)
|
| 82 |
-
}
|
| 83 |
-
}
|
| 84 |
-
return documents
|
| 85 |
-
}
|
| 86 |
-
|
| 87 |
-
const getDocumentSummaryAtIndex = async (index: number) => {
|
| 88 |
-
const documents = await getDocuments()
|
| 89 |
-
const summary = documents[index]
|
| 90 |
-
if (!summary) {
|
| 91 |
-
throw new Error(`Document not found at index ${index}`)
|
| 92 |
-
}
|
| 93 |
-
return summary
|
| 94 |
-
}
|
| 95 |
-
|
| 96 |
-
const getDocumentDetail = async (
|
| 97 |
-
documentId: string,
|
| 98 |
-
): Promise<DocumentDetail> => {
|
| 99 |
-
const detail = await fetchJson<DocumentDetail>(`/documents/${documentId}`)
|
| 100 |
-
documentDetailCache.set(documentId, detail)
|
| 101 |
-
return detail
|
| 102 |
-
}
|
| 103 |
-
|
| 104 |
-
const getCachedOrFetchDocumentDetail = async (documentId: string) =>
|
| 105 |
-
documentDetailCache.get(documentId) ?? (await getDocumentDetail(documentId))
|
| 106 |
-
|
| 107 |
-
const fetchLayer = async (documentId: string, layer: string) => {
|
| 108 |
-
const binary = await fetchBinary(`/documents/${documentId}/layers/${layer}`)
|
| 109 |
-
return binary.data
|
| 110 |
-
}
|
| 111 |
-
|
| 112 |
-
const mapTextStyle = (style?: TextStyle) =>
|
| 113 |
-
style
|
| 114 |
-
? {
|
| 115 |
-
fontFamilies: style.fontFamilies,
|
| 116 |
-
fontSize: style.fontSize,
|
| 117 |
-
color: style.color,
|
| 118 |
-
effect: style.effect,
|
| 119 |
-
stroke: style.stroke,
|
| 120 |
-
textAlign: style.textAlign,
|
| 121 |
-
}
|
| 122 |
-
: undefined
|
| 123 |
-
|
| 124 |
-
const mapTextBlock = (
|
| 125 |
-
block: DocumentDetail['textBlocks'][number],
|
| 126 |
-
): TextBlock => ({
|
| 127 |
-
id: block.id,
|
| 128 |
-
x: block.x,
|
| 129 |
-
y: block.y,
|
| 130 |
-
width: block.width,
|
| 131 |
-
height: block.height,
|
| 132 |
-
confidence: block.confidence,
|
| 133 |
-
linePolygons: block.linePolygons ?? undefined,
|
| 134 |
-
sourceDirection: block.sourceDirection ?? undefined,
|
| 135 |
-
renderedDirection: block.renderedDirection ?? undefined,
|
| 136 |
-
sourceLanguage: block.sourceLanguage ?? undefined,
|
| 137 |
-
rotationDeg: block.rotationDeg ?? undefined,
|
| 138 |
-
detectedFontSizePx: block.detectedFontSizePx ?? undefined,
|
| 139 |
-
detector: block.detector ?? undefined,
|
| 140 |
-
text: block.text ?? undefined,
|
| 141 |
-
translation: block.translation ?? undefined,
|
| 142 |
-
style: block.style ?? undefined,
|
| 143 |
-
fontPrediction: block.fontPrediction ?? undefined,
|
| 144 |
-
rendered: undefined,
|
| 145 |
-
})
|
| 146 |
-
|
| 147 |
-
const toDocumentDetailBlock = (
|
| 148 |
-
block: TextBlock,
|
| 149 |
-
id: string,
|
| 150 |
-
): DocumentDetail['textBlocks'][number] => ({
|
| 151 |
-
id,
|
| 152 |
-
x: block.x,
|
| 153 |
-
y: block.y,
|
| 154 |
-
width: block.width,
|
| 155 |
-
height: block.height,
|
| 156 |
-
confidence: block.confidence,
|
| 157 |
-
linePolygons: block.linePolygons ?? null,
|
| 158 |
-
sourceDirection: block.sourceDirection ?? null,
|
| 159 |
-
renderedDirection: block.renderedDirection ?? null,
|
| 160 |
-
sourceLanguage: block.sourceLanguage ?? null,
|
| 161 |
-
rotationDeg: block.rotationDeg ?? null,
|
| 162 |
-
detectedFontSizePx: block.detectedFontSizePx ?? null,
|
| 163 |
-
detector: block.detector ?? null,
|
| 164 |
-
text: block.text ?? null,
|
| 165 |
-
translation: block.translation ?? null,
|
| 166 |
-
style: block.style ?? null,
|
| 167 |
-
fontPrediction: block.fontPrediction ?? null,
|
| 168 |
-
})
|
| 169 |
-
|
| 170 |
-
const buildTextBlockPatch = (
|
| 171 |
-
next: TextBlock,
|
| 172 |
-
previous: DocumentDetail['textBlocks'][number],
|
| 173 |
-
): TextBlockPatch | null => {
|
| 174 |
-
const patch: TextBlockPatch = {
|
| 175 |
-
text: undefined,
|
| 176 |
-
translation: undefined,
|
| 177 |
-
x: undefined,
|
| 178 |
-
y: undefined,
|
| 179 |
-
width: undefined,
|
| 180 |
-
height: undefined,
|
| 181 |
-
style: undefined,
|
| 182 |
-
}
|
| 183 |
-
|
| 184 |
-
if ((next.text ?? null) !== previous.text) {
|
| 185 |
-
patch.text = next.text ?? ''
|
| 186 |
-
}
|
| 187 |
-
if ((next.translation ?? null) !== previous.translation) {
|
| 188 |
-
patch.translation = next.translation ?? ''
|
| 189 |
-
}
|
| 190 |
-
if (next.x !== previous.x) {
|
| 191 |
-
patch.x = next.x
|
| 192 |
-
}
|
| 193 |
-
if (next.y !== previous.y) {
|
| 194 |
-
patch.y = next.y
|
| 195 |
-
}
|
| 196 |
-
if (next.width !== previous.width) {
|
| 197 |
-
patch.width = next.width
|
| 198 |
-
}
|
| 199 |
-
if (next.height !== previous.height) {
|
| 200 |
-
patch.height = next.height
|
| 201 |
-
}
|
| 202 |
-
if (!sameJson(mapTextStyle(next.style), previous.style)) {
|
| 203 |
-
patch.style = mapTextStyle(next.style)
|
| 204 |
-
}
|
| 205 |
-
|
| 206 |
-
return Object.values(patch).some((value) => value !== undefined)
|
| 207 |
-
? patch
|
| 208 |
-
: null
|
| 209 |
-
}
|
| 210 |
-
|
| 211 |
-
const createTextBlockRemotely = async (
|
| 212 |
-
documentId: string,
|
| 213 |
-
block: TextBlock,
|
| 214 |
-
) => {
|
| 215 |
-
const created = await fetchJson<DocumentDetail['textBlocks'][number]>(
|
| 216 |
-
`/documents/${documentId}/text-blocks`,
|
| 217 |
-
{
|
| 218 |
-
method: 'POST',
|
| 219 |
-
headers: { 'content-type': 'application/json' },
|
| 220 |
-
body: JSON.stringify({
|
| 221 |
-
x: block.x,
|
| 222 |
-
y: block.y,
|
| 223 |
-
width: block.width,
|
| 224 |
-
height: block.height,
|
| 225 |
-
}),
|
| 226 |
-
},
|
| 227 |
-
)
|
| 228 |
-
|
| 229 |
-
const patch = buildTextBlockPatch(block, created)
|
| 230 |
-
if (patch) {
|
| 231 |
-
const updated = await fetchJson<DocumentDetail['textBlocks'][number]>(
|
| 232 |
-
`/documents/${documentId}/text-blocks/${created.id}`,
|
| 233 |
-
{
|
| 234 |
-
method: 'PATCH',
|
| 235 |
-
headers: { 'content-type': 'application/json' },
|
| 236 |
-
body: JSON.stringify(patch),
|
| 237 |
-
},
|
| 238 |
-
)
|
| 239 |
-
return updated
|
| 240 |
-
}
|
| 241 |
-
|
| 242 |
-
return created
|
| 243 |
-
}
|
| 244 |
-
|
| 245 |
-
const getTextBlockIdByIndex = async (
|
| 246 |
-
index: number,
|
| 247 |
-
textBlockIndex?: number,
|
| 248 |
-
) => {
|
| 249 |
-
if (typeof textBlockIndex !== 'number') return undefined
|
| 250 |
-
const summary = await getDocumentSummaryAtIndex(index)
|
| 251 |
-
const detail = await getCachedOrFetchDocumentDetail(summary.id)
|
| 252 |
-
return detail.textBlocks[textBlockIndex]?.id
|
| 253 |
-
}
|
| 254 |
-
|
| 255 |
-
export const createTempTextBlockId = () =>
|
| 256 |
-
`temp:${globalThis.crypto?.randomUUID?.() ?? Math.random().toString(36).slice(2)}`
|
| 257 |
-
|
| 258 |
-
export const api = {
|
| 259 |
-
async getBootstrapConfig(): Promise<BootstrapConfig> {
|
| 260 |
-
return fetchJson<BootstrapConfig>('/config')
|
| 261 |
-
},
|
| 262 |
-
|
| 263 |
-
async saveBootstrapConfig(config: BootstrapConfig): Promise<BootstrapConfig> {
|
| 264 |
-
return fetchJson<BootstrapConfig>('/config', {
|
| 265 |
-
method: 'PUT',
|
| 266 |
-
headers: { 'content-type': 'application/json' },
|
| 267 |
-
body: JSON.stringify(config),
|
| 268 |
-
})
|
| 269 |
-
},
|
| 270 |
-
|
| 271 |
-
async initializeBootstrap(): Promise<void> {
|
| 272 |
-
await fetchJson<void>('/initialize', {
|
| 273 |
-
method: 'POST',
|
| 274 |
-
})
|
| 275 |
-
},
|
| 276 |
-
|
| 277 |
-
async appVersion(): Promise<string> {
|
| 278 |
-
const meta = await fetchJson<MetaInfo>('/meta')
|
| 279 |
-
return meta.version
|
| 280 |
-
},
|
| 281 |
-
|
| 282 |
-
async deviceInfo(): Promise<{ mlDevice: string }> {
|
| 283 |
-
const meta = await fetchJson<MetaInfo>('/meta')
|
| 284 |
-
return { mlDevice: meta.mlDevice }
|
| 285 |
-
},
|
| 286 |
-
|
| 287 |
-
async openExternal(url: string): Promise<void> {
|
| 288 |
-
if (typeof window !== 'undefined') {
|
| 289 |
-
window.open(url, '_blank', 'noopener,noreferrer')
|
| 290 |
-
}
|
| 291 |
-
},
|
| 292 |
-
|
| 293 |
-
async getDocumentsCount(): Promise<number> {
|
| 294 |
-
const documents = await getDocuments()
|
| 295 |
-
return documents.length
|
| 296 |
-
},
|
| 297 |
-
|
| 298 |
-
async getDocument(index: number): Promise<Document> {
|
| 299 |
-
return withRpcError('get_document', async () => {
|
| 300 |
-
const summary = await getDocumentSummaryAtIndex(index)
|
| 301 |
-
const detail = await getDocumentDetail(summary.id)
|
| 302 |
-
const [image, segment, inpainted, brushLayer, rendered] =
|
| 303 |
-
await Promise.all([
|
| 304 |
-
fetchLayer(summary.id, 'original'),
|
| 305 |
-
summary.hasSegment
|
| 306 |
-
? fetchLayer(summary.id, 'segment')
|
| 307 |
-
: Promise.resolve(undefined),
|
| 308 |
-
summary.hasInpainted
|
| 309 |
-
? fetchLayer(summary.id, 'inpainted')
|
| 310 |
-
: Promise.resolve(undefined),
|
| 311 |
-
summary.hasBrushLayer
|
| 312 |
-
? fetchLayer(summary.id, 'brush')
|
| 313 |
-
: Promise.resolve(undefined),
|
| 314 |
-
summary.hasRendered
|
| 315 |
-
? fetchLayer(summary.id, 'rendered')
|
| 316 |
-
: Promise.resolve(undefined),
|
| 317 |
-
])
|
| 318 |
-
|
| 319 |
-
return {
|
| 320 |
-
id: detail.id,
|
| 321 |
-
path: detail.path,
|
| 322 |
-
name: detail.name,
|
| 323 |
-
image,
|
| 324 |
-
width: detail.width,
|
| 325 |
-
height: detail.height,
|
| 326 |
-
revision: detail.revision,
|
| 327 |
-
textBlocks: detail.textBlocks.map(mapTextBlock),
|
| 328 |
-
segment,
|
| 329 |
-
inpainted,
|
| 330 |
-
brushLayer,
|
| 331 |
-
rendered,
|
| 332 |
-
}
|
| 333 |
-
})
|
| 334 |
-
},
|
| 335 |
-
|
| 336 |
-
async getThumbnail(index: number): Promise<Blob> {
|
| 337 |
-
return withRpcError('get_thumbnail', async () => {
|
| 338 |
-
const summary = await getDocumentSummaryAtIndex(index)
|
| 339 |
-
const binary = await fetchBinary(`/documents/${summary.id}/thumbnail`)
|
| 340 |
-
return new Blob([toArrayBuffer(binary.data)], {
|
| 341 |
-
type: binary.contentType,
|
| 342 |
-
})
|
| 343 |
-
})
|
| 344 |
-
},
|
| 345 |
-
|
| 346 |
-
async addDocuments(): Promise<number> {
|
| 347 |
-
return withRpcError('add_documents', async () => {
|
| 348 |
-
const files = await pickDocuments()
|
| 349 |
-
if (!files?.length) return 0
|
| 350 |
-
const formData = new FormData()
|
| 351 |
-
files.forEach((file) => formData.append('files', file, file.name))
|
| 352 |
-
const result = await fetchJson<{ totalCount: number }>(
|
| 353 |
-
'/documents/import?mode=append',
|
| 354 |
-
{
|
| 355 |
-
method: 'POST',
|
| 356 |
-
body: formData,
|
| 357 |
-
},
|
| 358 |
-
)
|
| 359 |
-
documentDetailCache.clear()
|
| 360 |
-
return result.totalCount
|
| 361 |
-
})
|
| 362 |
-
},
|
| 363 |
-
|
| 364 |
-
async openDocuments(): Promise<number> {
|
| 365 |
-
return withRpcError('open_documents', async () => {
|
| 366 |
-
const files = await pickDocuments()
|
| 367 |
-
if (!files?.length) return 0
|
| 368 |
-
const formData = new FormData()
|
| 369 |
-
files.forEach((file) => formData.append('files', file, file.name))
|
| 370 |
-
const result = await fetchJson<{ totalCount: number }>(
|
| 371 |
-
'/documents/import?mode=replace',
|
| 372 |
-
{
|
| 373 |
-
method: 'POST',
|
| 374 |
-
body: formData,
|
| 375 |
-
},
|
| 376 |
-
)
|
| 377 |
-
documentDetailCache.clear()
|
| 378 |
-
return result.totalCount
|
| 379 |
-
})
|
| 380 |
-
},
|
| 381 |
-
|
| 382 |
-
async openFolder(): Promise<number> {
|
| 383 |
-
return withRpcError('open_documents', async () => {
|
| 384 |
-
const files = await pickFolder()
|
| 385 |
-
if (!files?.length) return 0
|
| 386 |
-
const formData = new FormData()
|
| 387 |
-
files.forEach((file) => formData.append('files', file, file.name))
|
| 388 |
-
const result = await fetchJson<{ totalCount: number }>(
|
| 389 |
-
'/documents/import?mode=replace',
|
| 390 |
-
{
|
| 391 |
-
method: 'POST',
|
| 392 |
-
body: formData,
|
| 393 |
-
},
|
| 394 |
-
)
|
| 395 |
-
documentDetailCache.clear()
|
| 396 |
-
return result.totalCount
|
| 397 |
-
})
|
| 398 |
-
},
|
| 399 |
-
|
| 400 |
-
async addFolder(): Promise<number> {
|
| 401 |
-
return withRpcError('add_documents', async () => {
|
| 402 |
-
const files = await pickFolder()
|
| 403 |
-
if (!files?.length) return 0
|
| 404 |
-
const formData = new FormData()
|
| 405 |
-
files.forEach((file) => formData.append('files', file, file.name))
|
| 406 |
-
const result = await fetchJson<{ totalCount: number }>(
|
| 407 |
-
'/documents/import?mode=append',
|
| 408 |
-
{
|
| 409 |
-
method: 'POST',
|
| 410 |
-
body: formData,
|
| 411 |
-
},
|
| 412 |
-
)
|
| 413 |
-
documentDetailCache.clear()
|
| 414 |
-
return result.totalCount
|
| 415 |
-
})
|
| 416 |
-
},
|
| 417 |
-
|
| 418 |
-
async exportDocument(index: number): Promise<void> {
|
| 419 |
-
return withRpcError('export_document', async () => {
|
| 420 |
-
const summary = await getDocumentSummaryAtIndex(index)
|
| 421 |
-
const file = await fetchBinary(
|
| 422 |
-
`/documents/${summary.id}/export?layer=rendered`,
|
| 423 |
-
)
|
| 424 |
-
const blob = new Blob([toArrayBuffer(file.data)], {
|
| 425 |
-
type: file.contentType,
|
| 426 |
-
})
|
| 427 |
-
try {
|
| 428 |
-
await fileSave(blob, {
|
| 429 |
-
fileName: file.filename ?? `${summary.name}_koharu.webp`,
|
| 430 |
-
})
|
| 431 |
-
} catch {}
|
| 432 |
-
})
|
| 433 |
-
},
|
| 434 |
-
|
| 435 |
-
async exportPsdDocument(index: number): Promise<void> {
|
| 436 |
-
return withRpcError('export_psd_document', async () => {
|
| 437 |
-
const summary = await getDocumentSummaryAtIndex(index)
|
| 438 |
-
const file = await fetchBinary(`/documents/${summary.id}/export/psd`)
|
| 439 |
-
const blob = new Blob([toArrayBuffer(file.data)], {
|
| 440 |
-
type: file.contentType,
|
| 441 |
-
})
|
| 442 |
-
try {
|
| 443 |
-
await fileSave(blob, {
|
| 444 |
-
fileName: file.filename ?? `${summary.name}_koharu.psd`,
|
| 445 |
-
})
|
| 446 |
-
} catch {}
|
| 447 |
-
})
|
| 448 |
-
},
|
| 449 |
-
|
| 450 |
-
async exportAllInpainted(): Promise<number> {
|
| 451 |
-
return withRpcError('export_all_inpainted', async () => {
|
| 452 |
-
const result = await fetchJson<ExportResult>('/exports?layer=inpainted', {
|
| 453 |
-
method: 'POST',
|
| 454 |
-
})
|
| 455 |
-
return result.count
|
| 456 |
-
})
|
| 457 |
-
},
|
| 458 |
-
|
| 459 |
-
async exportAllRendered(): Promise<number> {
|
| 460 |
-
return withRpcError('export_all_rendered', async () => {
|
| 461 |
-
const result = await fetchJson<ExportResult>('/exports?layer=rendered', {
|
| 462 |
-
method: 'POST',
|
| 463 |
-
})
|
| 464 |
-
return result.count
|
| 465 |
-
})
|
| 466 |
-
},
|
| 467 |
-
|
| 468 |
-
async detect(index: number): Promise<void> {
|
| 469 |
-
return withRpcError('detect', async () => {
|
| 470 |
-
const summary = await getDocumentSummaryAtIndex(index)
|
| 471 |
-
await fetchJson<void>(`/documents/${summary.id}/detect`, {
|
| 472 |
-
method: 'POST',
|
| 473 |
-
})
|
| 474 |
-
documentDetailCache.delete(summary.id)
|
| 475 |
-
})
|
| 476 |
-
},
|
| 477 |
-
|
| 478 |
-
async ocr(index: number): Promise<void> {
|
| 479 |
-
return withRpcError('ocr', async () => {
|
| 480 |
-
const summary = await getDocumentSummaryAtIndex(index)
|
| 481 |
-
await fetchJson<void>(`/documents/${summary.id}/ocr`, { method: 'POST' })
|
| 482 |
-
documentDetailCache.delete(summary.id)
|
| 483 |
-
})
|
| 484 |
-
},
|
| 485 |
-
|
| 486 |
-
async inpaint(index: number): Promise<void> {
|
| 487 |
-
return withRpcError('inpaint', async () => {
|
| 488 |
-
const summary = await getDocumentSummaryAtIndex(index)
|
| 489 |
-
await fetchJson<void>(`/documents/${summary.id}/inpaint`, {
|
| 490 |
-
method: 'POST',
|
| 491 |
-
})
|
| 492 |
-
documentDetailCache.delete(summary.id)
|
| 493 |
-
})
|
| 494 |
-
},
|
| 495 |
-
|
| 496 |
-
async updateInpaintMask(
|
| 497 |
-
index: number,
|
| 498 |
-
mask: Uint8Array,
|
| 499 |
-
region?: InpaintRegion,
|
| 500 |
-
): Promise<void> {
|
| 501 |
-
return withRpcError('update_inpaint_mask', async () => {
|
| 502 |
-
const summary = await getDocumentSummaryAtIndex(index)
|
| 503 |
-
await fetchJson<void>(`/documents/${summary.id}/mask-region`, {
|
| 504 |
-
method: 'PUT',
|
| 505 |
-
headers: { 'content-type': 'application/json' },
|
| 506 |
-
body: JSON.stringify({
|
| 507 |
-
data: toBinaryArray(mask),
|
| 508 |
-
region,
|
| 509 |
-
}),
|
| 510 |
-
})
|
| 511 |
-
})
|
| 512 |
-
},
|
| 513 |
-
|
| 514 |
-
async updateBrushLayer(
|
| 515 |
-
index: number,
|
| 516 |
-
patch: Uint8Array,
|
| 517 |
-
region: InpaintRegion,
|
| 518 |
-
): Promise<void> {
|
| 519 |
-
return withRpcError('update_brush_layer', async () => {
|
| 520 |
-
const summary = await getDocumentSummaryAtIndex(index)
|
| 521 |
-
await fetchJson<void>(`/documents/${summary.id}/brush-region`, {
|
| 522 |
-
method: 'PUT',
|
| 523 |
-
headers: { 'content-type': 'application/json' },
|
| 524 |
-
body: JSON.stringify({
|
| 525 |
-
data: toBinaryArray(patch),
|
| 526 |
-
region,
|
| 527 |
-
}),
|
| 528 |
-
})
|
| 529 |
-
})
|
| 530 |
-
},
|
| 531 |
-
|
| 532 |
-
async inpaintPartial(index: number, region: InpaintRegion): Promise<void> {
|
| 533 |
-
return withRpcError('inpaint_partial', async () => {
|
| 534 |
-
const summary = await getDocumentSummaryAtIndex(index)
|
| 535 |
-
await fetchJson<void>(`/documents/${summary.id}/inpaint-region`, {
|
| 536 |
-
method: 'POST',
|
| 537 |
-
headers: { 'content-type': 'application/json' },
|
| 538 |
-
body: JSON.stringify({ region }),
|
| 539 |
-
})
|
| 540 |
-
documentDetailCache.delete(summary.id)
|
| 541 |
-
})
|
| 542 |
-
},
|
| 543 |
-
|
| 544 |
-
async render(
|
| 545 |
-
index: number,
|
| 546 |
-
options?: {
|
| 547 |
-
textBlockIndex?: number
|
| 548 |
-
shaderEffect?: RenderEffect
|
| 549 |
-
shaderStroke?: RenderStroke
|
| 550 |
-
fontFamily?: string
|
| 551 |
-
},
|
| 552 |
-
): Promise<void> {
|
| 553 |
-
return withRpcError('render', async () => {
|
| 554 |
-
const summary = await getDocumentSummaryAtIndex(index)
|
| 555 |
-
const textBlockId = await getTextBlockIdByIndex(
|
| 556 |
-
index,
|
| 557 |
-
options?.textBlockIndex,
|
| 558 |
-
)
|
| 559 |
-
await fetchJson<void>(`/documents/${summary.id}/render`, {
|
| 560 |
-
method: 'POST',
|
| 561 |
-
headers: { 'content-type': 'application/json' },
|
| 562 |
-
body: JSON.stringify({
|
| 563 |
-
textBlockId,
|
| 564 |
-
shaderEffect: options?.shaderEffect,
|
| 565 |
-
shaderStroke: options?.shaderStroke,
|
| 566 |
-
fontFamily: options?.fontFamily,
|
| 567 |
-
}),
|
| 568 |
-
})
|
| 569 |
-
documentDetailCache.delete(summary.id)
|
| 570 |
-
})
|
| 571 |
-
},
|
| 572 |
-
|
| 573 |
-
async updateTextBlocks(
|
| 574 |
-
index: number,
|
| 575 |
-
textBlocks: TextBlock[],
|
| 576 |
-
): Promise<void> {
|
| 577 |
-
return withRpcError('update_text_blocks', async () => {
|
| 578 |
-
const summary = await getDocumentSummaryAtIndex(index)
|
| 579 |
-
const previous = await getCachedOrFetchDocumentDetail(summary.id)
|
| 580 |
-
const previousMap = new Map(
|
| 581 |
-
previous.textBlocks.map((block) => [block.id, block]),
|
| 582 |
-
)
|
| 583 |
-
|
| 584 |
-
const normalizedBlocks = textBlocks.map((block) => ({
|
| 585 |
-
...block,
|
| 586 |
-
id: resolveTextBlockIdAlias(summary.id, block.id),
|
| 587 |
-
}))
|
| 588 |
-
|
| 589 |
-
const retainedIds = new Set(
|
| 590 |
-
normalizedBlocks
|
| 591 |
-
.map((block) => block.id)
|
| 592 |
-
.filter((id): id is string => !!id && !isTempTextBlockId(id)),
|
| 593 |
-
)
|
| 594 |
-
|
| 595 |
-
for (const previousBlock of previous.textBlocks) {
|
| 596 |
-
if (!retainedIds.has(previousBlock.id)) {
|
| 597 |
-
await fetchJson<void>(
|
| 598 |
-
`/documents/${summary.id}/text-blocks/${previousBlock.id}`,
|
| 599 |
-
{
|
| 600 |
-
method: 'DELETE',
|
| 601 |
-
},
|
| 602 |
-
)
|
| 603 |
-
}
|
| 604 |
-
}
|
| 605 |
-
|
| 606 |
-
const synchronizedBlocks: DocumentDetail['textBlocks'] = []
|
| 607 |
-
|
| 608 |
-
for (const block of normalizedBlocks) {
|
| 609 |
-
const existingId =
|
| 610 |
-
block.id && !isTempTextBlockId(block.id) ? block.id : undefined
|
| 611 |
-
|
| 612 |
-
if (!existingId || !previousMap.has(existingId)) {
|
| 613 |
-
const created = await createTextBlockRemotely(summary.id, block)
|
| 614 |
-
rememberTextBlockAlias(summary.id, block.id, created.id)
|
| 615 |
-
synchronizedBlocks.push(
|
| 616 |
-
toDocumentDetailBlock({ ...block, id: created.id }, created.id),
|
| 617 |
-
)
|
| 618 |
-
continue
|
| 619 |
-
}
|
| 620 |
-
|
| 621 |
-
const previousBlock = previousMap.get(existingId)!
|
| 622 |
-
const patch = buildTextBlockPatch(block, previousBlock)
|
| 623 |
-
if (patch) {
|
| 624 |
-
await fetchJson<DocumentDetail['textBlocks'][number]>(
|
| 625 |
-
`/documents/${summary.id}/text-blocks/${existingId}`,
|
| 626 |
-
{
|
| 627 |
-
method: 'PATCH',
|
| 628 |
-
headers: { 'content-type': 'application/json' },
|
| 629 |
-
body: JSON.stringify(patch),
|
| 630 |
-
},
|
| 631 |
-
)
|
| 632 |
-
}
|
| 633 |
-
|
| 634 |
-
synchronizedBlocks.push(toDocumentDetailBlock(block, existingId))
|
| 635 |
-
}
|
| 636 |
-
|
| 637 |
-
documentDetailCache.set(summary.id, {
|
| 638 |
-
...previous,
|
| 639 |
-
textBlocks: synchronizedBlocks,
|
| 640 |
-
})
|
| 641 |
-
})
|
| 642 |
-
},
|
| 643 |
-
|
| 644 |
-
async listFonts(): Promise<FontFaceInfo[]> {
|
| 645 |
-
return fetchJson<FontFaceInfo[]>('/fonts')
|
| 646 |
-
},
|
| 647 |
-
|
| 648 |
-
async getApiKey(provider: string): Promise<string | null> {
|
| 649 |
-
const response = await fetchJson<ApiKeyResponse>(
|
| 650 |
-
`/providers/${provider}/api-key`,
|
| 651 |
-
)
|
| 652 |
-
return response.apiKey ?? null
|
| 653 |
-
},
|
| 654 |
-
|
| 655 |
-
async setApiKey(provider: string, apiKey: string): Promise<void> {
|
| 656 |
-
await fetchJson<void>(`/providers/${provider}/api-key`, {
|
| 657 |
-
method: 'PUT',
|
| 658 |
-
headers: { 'content-type': 'application/json' },
|
| 659 |
-
body: JSON.stringify({ apiKey }),
|
| 660 |
-
})
|
| 661 |
-
},
|
| 662 |
-
|
| 663 |
-
async llmList(
|
| 664 |
-
language?: string,
|
| 665 |
-
openAiCompatibleBaseUrl?: string,
|
| 666 |
-
): Promise<LlmModelInfo[]> {
|
| 667 |
-
const params = new URLSearchParams()
|
| 668 |
-
if (language) {
|
| 669 |
-
params.set('language', language)
|
| 670 |
-
}
|
| 671 |
-
if (openAiCompatibleBaseUrl) {
|
| 672 |
-
params.set('openaiCompatibleBaseUrl', openAiCompatibleBaseUrl)
|
| 673 |
-
}
|
| 674 |
-
const queryString = params.toString()
|
| 675 |
-
const query = queryString ? `?${queryString}` : ''
|
| 676 |
-
return fetchJson<LlmModelInfo[]>(`/llm/models${query}`)
|
| 677 |
-
},
|
| 678 |
-
|
| 679 |
-
async llmLoad(
|
| 680 |
-
id: string,
|
| 681 |
-
apiKey?: string,
|
| 682 |
-
baseUrl?: string,
|
| 683 |
-
temperature?: number | null,
|
| 684 |
-
maxTokens?: number | null,
|
| 685 |
-
customSystemPrompt?: string,
|
| 686 |
-
): Promise<void> {
|
| 687 |
-
await fetchJson<LlmState>('/llm/load', {
|
| 688 |
-
method: 'POST',
|
| 689 |
-
headers: { 'content-type': 'application/json' },
|
| 690 |
-
body: JSON.stringify({
|
| 691 |
-
id,
|
| 692 |
-
apiKey,
|
| 693 |
-
baseUrl,
|
| 694 |
-
temperature: temperature ?? undefined,
|
| 695 |
-
maxTokens: maxTokens ?? undefined,
|
| 696 |
-
customSystemPrompt: customSystemPrompt || undefined,
|
| 697 |
-
}),
|
| 698 |
-
})
|
| 699 |
-
},
|
| 700 |
-
|
| 701 |
-
async llmPing(
|
| 702 |
-
baseUrl: string,
|
| 703 |
-
apiKey?: string,
|
| 704 |
-
): Promise<{
|
| 705 |
-
ok: boolean
|
| 706 |
-
models: string[]
|
| 707 |
-
latencyMs?: number
|
| 708 |
-
error?: string
|
| 709 |
-
}> {
|
| 710 |
-
return fetchJson('/llm/ping', {
|
| 711 |
-
method: 'POST',
|
| 712 |
-
headers: { 'content-type': 'application/json' },
|
| 713 |
-
body: JSON.stringify({ baseUrl, apiKey: apiKey || undefined }),
|
| 714 |
-
})
|
| 715 |
-
},
|
| 716 |
-
|
| 717 |
-
async llmOffload(): Promise<void> {
|
| 718 |
-
await fetchJson<LlmState>('/llm/offload', {
|
| 719 |
-
method: 'POST',
|
| 720 |
-
})
|
| 721 |
-
},
|
| 722 |
-
|
| 723 |
-
async llmReady(selectedModel?: string): Promise<boolean> {
|
| 724 |
-
const state = await fetchJson<LlmState>('/llm/state')
|
| 725 |
-
return (
|
| 726 |
-
state.status === 'ready' &&
|
| 727 |
-
(!selectedModel || !state.modelId || state.modelId === selectedModel)
|
| 728 |
-
)
|
| 729 |
-
},
|
| 730 |
-
|
| 731 |
-
async llmGenerate(
|
| 732 |
-
index: number,
|
| 733 |
-
textBlockIndex?: number,
|
| 734 |
-
language?: string,
|
| 735 |
-
): Promise<void> {
|
| 736 |
-
return withRpcError('llm_generate', async () => {
|
| 737 |
-
const summary = await getDocumentSummaryAtIndex(index)
|
| 738 |
-
const textBlockId = await getTextBlockIdByIndex(index, textBlockIndex)
|
| 739 |
-
await fetchJson<void>(`/documents/${summary.id}/translate`, {
|
| 740 |
-
method: 'POST',
|
| 741 |
-
headers: { 'content-type': 'application/json' },
|
| 742 |
-
body: JSON.stringify({
|
| 743 |
-
textBlockId,
|
| 744 |
-
language,
|
| 745 |
-
}),
|
| 746 |
-
})
|
| 747 |
-
documentDetailCache.delete(summary.id)
|
| 748 |
-
})
|
| 749 |
-
},
|
| 750 |
-
|
| 751 |
-
async process(options: {
|
| 752 |
-
index?: number
|
| 753 |
-
llmModelId?: string
|
| 754 |
-
llmApiKey?: string
|
| 755 |
-
llmBaseUrl?: string
|
| 756 |
-
llmTemperature?: number | null
|
| 757 |
-
llmMaxTokens?: number | null
|
| 758 |
-
llmCustomSystemPrompt?: string
|
| 759 |
-
language?: string
|
| 760 |
-
shaderEffect?: RenderEffect
|
| 761 |
-
shaderStroke?: RenderStroke
|
| 762 |
-
fontFamily?: string
|
| 763 |
-
}): Promise<void> {
|
| 764 |
-
return withRpcError('process', async () => {
|
| 765 |
-
const documentId =
|
| 766 |
-
typeof options.index === 'number'
|
| 767 |
-
? (await getDocumentSummaryAtIndex(options.index)).id
|
| 768 |
-
: undefined
|
| 769 |
-
|
| 770 |
-
const job = await fetchJson<JobState>('/jobs/pipeline', {
|
| 771 |
-
method: 'POST',
|
| 772 |
-
headers: { 'content-type': 'application/json' },
|
| 773 |
-
body: JSON.stringify({
|
| 774 |
-
documentId,
|
| 775 |
-
llmModelId: options.llmModelId,
|
| 776 |
-
llmApiKey: options.llmApiKey,
|
| 777 |
-
llmBaseUrl: options.llmBaseUrl,
|
| 778 |
-
llmTemperature: options.llmTemperature ?? undefined,
|
| 779 |
-
llmMaxTokens: options.llmMaxTokens ?? undefined,
|
| 780 |
-
llmCustomSystemPrompt: options.llmCustomSystemPrompt || undefined,
|
| 781 |
-
language: options.language,
|
| 782 |
-
shaderEffect: options.shaderEffect,
|
| 783 |
-
shaderStroke: options.shaderStroke,
|
| 784 |
-
fontFamily: options.fontFamily,
|
| 785 |
-
}),
|
| 786 |
-
})
|
| 787 |
-
setActivePipelineJobId(job.id)
|
| 788 |
-
})
|
| 789 |
-
},
|
| 790 |
-
|
| 791 |
-
async processCancel(): Promise<void> {
|
| 792 |
-
const jobId = getActivePipelineJobId()
|
| 793 |
-
if (!jobId) return
|
| 794 |
-
await fetchJson<void>(`/jobs/${jobId}`, { method: 'DELETE' })
|
| 795 |
-
},
|
| 796 |
-
}
|
| 797 |
-
|
| 798 |
-
const IMAGE_EXTENSIONS = ['.png', '.jpg', '.jpeg', '.webp']
|
| 799 |
-
|
| 800 |
-
const pickDocuments = async (): Promise<File[] | null> => {
|
| 801 |
-
try {
|
| 802 |
-
return await fileOpen({
|
| 803 |
-
description: 'Documents',
|
| 804 |
-
mimeTypes: ['image/*'],
|
| 805 |
-
extensions: IMAGE_EXTENSIONS,
|
| 806 |
-
multiple: true,
|
| 807 |
-
})
|
| 808 |
-
} catch {
|
| 809 |
-
return null
|
| 810 |
-
}
|
| 811 |
-
}
|
| 812 |
-
|
| 813 |
-
const pickFolder = async (): Promise<File[] | null> => {
|
| 814 |
-
try {
|
| 815 |
-
const files = await directoryOpen({ recursive: true })
|
| 816 |
-
return files.filter((file) =>
|
| 817 |
-
IMAGE_EXTENSIONS.some((ext) => file.name.toLowerCase().endsWith(ext)),
|
| 818 |
-
)
|
| 819 |
-
} catch {
|
| 820 |
-
return null
|
| 821 |
-
}
|
| 822 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ui/lib/api/documents/documents.ts
ADDED
|
@@ -0,0 +1,406 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Generated by orval v8.6.2 🍺
|
| 3 |
+
* Do not edit manually.
|
| 4 |
+
* OpenAPI spec version: 0.0.1
|
| 5 |
+
*/
|
| 6 |
+
import {
|
| 7 |
+
useMutation,
|
| 8 |
+
useQuery
|
| 9 |
+
} from '@tanstack/react-query';
|
| 10 |
+
import type {
|
| 11 |
+
DataTag,
|
| 12 |
+
DefinedInitialDataOptions,
|
| 13 |
+
DefinedUseQueryResult,
|
| 14 |
+
MutationFunction,
|
| 15 |
+
QueryClient,
|
| 16 |
+
QueryFunction,
|
| 17 |
+
QueryKey,
|
| 18 |
+
UndefinedInitialDataOptions,
|
| 19 |
+
UseMutationOptions,
|
| 20 |
+
UseMutationResult,
|
| 21 |
+
UseQueryOptions,
|
| 22 |
+
UseQueryResult
|
| 23 |
+
} from '@tanstack/react-query';
|
| 24 |
+
|
| 25 |
+
import type {
|
| 26 |
+
ApiError,
|
| 27 |
+
DocumentDetail,
|
| 28 |
+
DocumentSummary,
|
| 29 |
+
GetDocumentThumbnailParams,
|
| 30 |
+
ImportDocumentsBody,
|
| 31 |
+
ImportDocumentsParams,
|
| 32 |
+
ImportResult
|
| 33 |
+
} from '../schemas';
|
| 34 |
+
|
| 35 |
+
import { fetchApi } from '.././fetch';
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
type SecondParameter<T extends (...args: never) => unknown> = Parameters<T>[1];
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
export const getListDocumentsUrl = () => {
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
return `/api/v1/documents`
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
export const listDocuments = async ( options?: RequestInit): Promise<DocumentSummary[]> => {
|
| 51 |
+
|
| 52 |
+
return fetchApi<DocumentSummary[]>(getListDocumentsUrl(),
|
| 53 |
+
{
|
| 54 |
+
...options,
|
| 55 |
+
method: 'GET'
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
}
|
| 59 |
+
);}
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
export const getListDocumentsQueryKey = () => {
|
| 66 |
+
return [
|
| 67 |
+
`/api/v1/documents`
|
| 68 |
+
] as const;
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
export const getListDocumentsQueryOptions = <TData = Awaited<ReturnType<typeof listDocuments>>, TError = ApiError>( options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof listDocuments>>, TError, TData>>, request?: SecondParameter<typeof fetchApi>}
|
| 73 |
+
) => {
|
| 74 |
+
|
| 75 |
+
const {query: queryOptions, request: requestOptions} = options ?? {};
|
| 76 |
+
|
| 77 |
+
const queryKey = queryOptions?.queryKey ?? getListDocumentsQueryKey();
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
const queryFn: QueryFunction<Awaited<ReturnType<typeof listDocuments>>> = ({ signal }) => listDocuments({ signal, ...requestOptions });
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
return { queryKey, queryFn, gcTime: 300000, retry: 1, ...queryOptions} as UseQueryOptions<Awaited<ReturnType<typeof listDocuments>>, TError, TData> & { queryKey: DataTag<QueryKey, TData, TError> }
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
export type ListDocumentsQueryResult = NonNullable<Awaited<ReturnType<typeof listDocuments>>>
|
| 91 |
+
export type ListDocumentsQueryError = ApiError
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
export function useListDocuments<TData = Awaited<ReturnType<typeof listDocuments>>, TError = ApiError>(
|
| 95 |
+
options: { query:Partial<UseQueryOptions<Awaited<ReturnType<typeof listDocuments>>, TError, TData>> & Pick<
|
| 96 |
+
DefinedInitialDataOptions<
|
| 97 |
+
Awaited<ReturnType<typeof listDocuments>>,
|
| 98 |
+
TError,
|
| 99 |
+
Awaited<ReturnType<typeof listDocuments>>
|
| 100 |
+
> , 'initialData'
|
| 101 |
+
>, request?: SecondParameter<typeof fetchApi>}
|
| 102 |
+
, queryClient?: QueryClient
|
| 103 |
+
): DefinedUseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }
|
| 104 |
+
export function useListDocuments<TData = Awaited<ReturnType<typeof listDocuments>>, TError = ApiError>(
|
| 105 |
+
options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof listDocuments>>, TError, TData>> & Pick<
|
| 106 |
+
UndefinedInitialDataOptions<
|
| 107 |
+
Awaited<ReturnType<typeof listDocuments>>,
|
| 108 |
+
TError,
|
| 109 |
+
Awaited<ReturnType<typeof listDocuments>>
|
| 110 |
+
> , 'initialData'
|
| 111 |
+
>, request?: SecondParameter<typeof fetchApi>}
|
| 112 |
+
, queryClient?: QueryClient
|
| 113 |
+
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }
|
| 114 |
+
export function useListDocuments<TData = Awaited<ReturnType<typeof listDocuments>>, TError = ApiError>(
|
| 115 |
+
options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof listDocuments>>, TError, TData>>, request?: SecondParameter<typeof fetchApi>}
|
| 116 |
+
, queryClient?: QueryClient
|
| 117 |
+
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }
|
| 118 |
+
|
| 119 |
+
export function useListDocuments<TData = Awaited<ReturnType<typeof listDocuments>>, TError = ApiError>(
|
| 120 |
+
options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof listDocuments>>, TError, TData>>, request?: SecondParameter<typeof fetchApi>}
|
| 121 |
+
, queryClient?: QueryClient
|
| 122 |
+
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> } {
|
| 123 |
+
|
| 124 |
+
const queryOptions = getListDocumentsQueryOptions(options)
|
| 125 |
+
|
| 126 |
+
const query = useQuery(queryOptions, queryClient) as UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> };
|
| 127 |
+
|
| 128 |
+
return { ...query, queryKey: queryOptions.queryKey };
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
export const getImportDocumentsUrl = (params?: ImportDocumentsParams,) => {
|
| 135 |
+
const normalizedParams = new URLSearchParams();
|
| 136 |
+
|
| 137 |
+
Object.entries(params || {}).forEach(([key, value]) => {
|
| 138 |
+
|
| 139 |
+
if (value !== undefined) {
|
| 140 |
+
normalizedParams.append(key, value === null ? 'null' : value.toString())
|
| 141 |
+
}
|
| 142 |
+
});
|
| 143 |
+
|
| 144 |
+
const stringifiedParams = normalizedParams.toString();
|
| 145 |
+
|
| 146 |
+
return stringifiedParams.length > 0 ? `/api/v1/documents?${stringifiedParams}` : `/api/v1/documents`
|
| 147 |
+
}
|
| 148 |
+
|
| 149 |
+
export const importDocuments = async (importDocumentsBody: ImportDocumentsBody,
|
| 150 |
+
params?: ImportDocumentsParams, options?: RequestInit): Promise<ImportResult> => {
|
| 151 |
+
const formData = new FormData();
|
| 152 |
+
importDocumentsBody.files.forEach(value => formData.append(`files`, value));
|
| 153 |
+
|
| 154 |
+
return fetchApi<ImportResult>(getImportDocumentsUrl(params),
|
| 155 |
+
{
|
| 156 |
+
...options,
|
| 157 |
+
method: 'POST'
|
| 158 |
+
,
|
| 159 |
+
body:
|
| 160 |
+
formData,
|
| 161 |
+
}
|
| 162 |
+
);}
|
| 163 |
+
|
| 164 |
+
|
| 165 |
+
|
| 166 |
+
|
| 167 |
+
export const getImportDocumentsMutationOptions = <TError = ApiError,
|
| 168 |
+
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof importDocuments>>, TError,{data: ImportDocumentsBody;params?: ImportDocumentsParams}, TContext>, request?: SecondParameter<typeof fetchApi>}
|
| 169 |
+
): UseMutationOptions<Awaited<ReturnType<typeof importDocuments>>, TError,{data: ImportDocumentsBody;params?: ImportDocumentsParams}, TContext> => {
|
| 170 |
+
|
| 171 |
+
const mutationKey = ['importDocuments'];
|
| 172 |
+
const {mutation: mutationOptions, request: requestOptions} = options ?
|
| 173 |
+
options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey ?
|
| 174 |
+
options
|
| 175 |
+
: {...options, mutation: {...options.mutation, mutationKey}}
|
| 176 |
+
: {mutation: { mutationKey, }, request: undefined};
|
| 177 |
+
|
| 178 |
+
|
| 179 |
+
|
| 180 |
+
|
| 181 |
+
const mutationFn: MutationFunction<Awaited<ReturnType<typeof importDocuments>>, {data: ImportDocumentsBody;params?: ImportDocumentsParams}> = (props) => {
|
| 182 |
+
const {data,params} = props ?? {};
|
| 183 |
+
|
| 184 |
+
return importDocuments(data,params,requestOptions)
|
| 185 |
+
}
|
| 186 |
+
|
| 187 |
+
|
| 188 |
+
|
| 189 |
+
|
| 190 |
+
|
| 191 |
+
|
| 192 |
+
return { mutationFn, ...mutationOptions }}
|
| 193 |
+
|
| 194 |
+
export type ImportDocumentsMutationResult = NonNullable<Awaited<ReturnType<typeof importDocuments>>>
|
| 195 |
+
export type ImportDocumentsMutationBody = ImportDocumentsBody
|
| 196 |
+
export type ImportDocumentsMutationError = ApiError
|
| 197 |
+
|
| 198 |
+
export const useImportDocuments = <TError = ApiError,
|
| 199 |
+
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof importDocuments>>, TError,{data: ImportDocumentsBody;params?: ImportDocumentsParams}, TContext>, request?: SecondParameter<typeof fetchApi>}
|
| 200 |
+
, queryClient?: QueryClient): UseMutationResult<
|
| 201 |
+
Awaited<ReturnType<typeof importDocuments>>,
|
| 202 |
+
TError,
|
| 203 |
+
{data: ImportDocumentsBody;params?: ImportDocumentsParams},
|
| 204 |
+
TContext
|
| 205 |
+
> => {
|
| 206 |
+
return useMutation(getImportDocumentsMutationOptions(options), queryClient);
|
| 207 |
+
}
|
| 208 |
+
export const getGetDocumentUrl = (documentId: string,) => {
|
| 209 |
+
|
| 210 |
+
|
| 211 |
+
|
| 212 |
+
|
| 213 |
+
return `/api/v1/documents/${documentId}`
|
| 214 |
+
}
|
| 215 |
+
|
| 216 |
+
export const getDocument = async (documentId: string, options?: RequestInit): Promise<DocumentDetail> => {
|
| 217 |
+
|
| 218 |
+
return fetchApi<DocumentDetail>(getGetDocumentUrl(documentId),
|
| 219 |
+
{
|
| 220 |
+
...options,
|
| 221 |
+
method: 'GET'
|
| 222 |
+
|
| 223 |
+
|
| 224 |
+
}
|
| 225 |
+
);}
|
| 226 |
+
|
| 227 |
+
|
| 228 |
+
|
| 229 |
+
|
| 230 |
+
|
| 231 |
+
export const getGetDocumentQueryKey = (documentId: string,) => {
|
| 232 |
+
return [
|
| 233 |
+
`/api/v1/documents/${documentId}`
|
| 234 |
+
] as const;
|
| 235 |
+
}
|
| 236 |
+
|
| 237 |
+
|
| 238 |
+
export const getGetDocumentQueryOptions = <TData = Awaited<ReturnType<typeof getDocument>>, TError = ApiError>(documentId: string, options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof getDocument>>, TError, TData>>, request?: SecondParameter<typeof fetchApi>}
|
| 239 |
+
) => {
|
| 240 |
+
|
| 241 |
+
const {query: queryOptions, request: requestOptions} = options ?? {};
|
| 242 |
+
|
| 243 |
+
const queryKey = queryOptions?.queryKey ?? getGetDocumentQueryKey(documentId);
|
| 244 |
+
|
| 245 |
+
|
| 246 |
+
|
| 247 |
+
const queryFn: QueryFunction<Awaited<ReturnType<typeof getDocument>>> = ({ signal }) => getDocument(documentId, { signal, ...requestOptions });
|
| 248 |
+
|
| 249 |
+
|
| 250 |
+
|
| 251 |
+
|
| 252 |
+
|
| 253 |
+
return { queryKey, queryFn, enabled: !!(documentId), gcTime: 300000, retry: 1, ...queryOptions} as UseQueryOptions<Awaited<ReturnType<typeof getDocument>>, TError, TData> & { queryKey: DataTag<QueryKey, TData, TError> }
|
| 254 |
+
}
|
| 255 |
+
|
| 256 |
+
export type GetDocumentQueryResult = NonNullable<Awaited<ReturnType<typeof getDocument>>>
|
| 257 |
+
export type GetDocumentQueryError = ApiError
|
| 258 |
+
|
| 259 |
+
|
| 260 |
+
export function useGetDocument<TData = Awaited<ReturnType<typeof getDocument>>, TError = ApiError>(
|
| 261 |
+
documentId: string, options: { query:Partial<UseQueryOptions<Awaited<ReturnType<typeof getDocument>>, TError, TData>> & Pick<
|
| 262 |
+
DefinedInitialDataOptions<
|
| 263 |
+
Awaited<ReturnType<typeof getDocument>>,
|
| 264 |
+
TError,
|
| 265 |
+
Awaited<ReturnType<typeof getDocument>>
|
| 266 |
+
> , 'initialData'
|
| 267 |
+
>, request?: SecondParameter<typeof fetchApi>}
|
| 268 |
+
, queryClient?: QueryClient
|
| 269 |
+
): DefinedUseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }
|
| 270 |
+
export function useGetDocument<TData = Awaited<ReturnType<typeof getDocument>>, TError = ApiError>(
|
| 271 |
+
documentId: string, options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof getDocument>>, TError, TData>> & Pick<
|
| 272 |
+
UndefinedInitialDataOptions<
|
| 273 |
+
Awaited<ReturnType<typeof getDocument>>,
|
| 274 |
+
TError,
|
| 275 |
+
Awaited<ReturnType<typeof getDocument>>
|
| 276 |
+
> , 'initialData'
|
| 277 |
+
>, request?: SecondParameter<typeof fetchApi>}
|
| 278 |
+
, queryClient?: QueryClient
|
| 279 |
+
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }
|
| 280 |
+
export function useGetDocument<TData = Awaited<ReturnType<typeof getDocument>>, TError = ApiError>(
|
| 281 |
+
documentId: string, options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof getDocument>>, TError, TData>>, request?: SecondParameter<typeof fetchApi>}
|
| 282 |
+
, queryClient?: QueryClient
|
| 283 |
+
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }
|
| 284 |
+
|
| 285 |
+
export function useGetDocument<TData = Awaited<ReturnType<typeof getDocument>>, TError = ApiError>(
|
| 286 |
+
documentId: string, options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof getDocument>>, TError, TData>>, request?: SecondParameter<typeof fetchApi>}
|
| 287 |
+
, queryClient?: QueryClient
|
| 288 |
+
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> } {
|
| 289 |
+
|
| 290 |
+
const queryOptions = getGetDocumentQueryOptions(documentId,options)
|
| 291 |
+
|
| 292 |
+
const query = useQuery(queryOptions, queryClient) as UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> };
|
| 293 |
+
|
| 294 |
+
return { ...query, queryKey: queryOptions.queryKey };
|
| 295 |
+
}
|
| 296 |
+
|
| 297 |
+
|
| 298 |
+
|
| 299 |
+
|
| 300 |
+
export const getGetDocumentThumbnailUrl = (documentId: string,
|
| 301 |
+
params?: GetDocumentThumbnailParams,) => {
|
| 302 |
+
const normalizedParams = new URLSearchParams();
|
| 303 |
+
|
| 304 |
+
Object.entries(params || {}).forEach(([key, value]) => {
|
| 305 |
+
|
| 306 |
+
if (value !== undefined) {
|
| 307 |
+
normalizedParams.append(key, value === null ? 'null' : value.toString())
|
| 308 |
+
}
|
| 309 |
+
});
|
| 310 |
+
|
| 311 |
+
const stringifiedParams = normalizedParams.toString();
|
| 312 |
+
|
| 313 |
+
return stringifiedParams.length > 0 ? `/api/v1/documents/${documentId}/thumbnail?${stringifiedParams}` : `/api/v1/documents/${documentId}/thumbnail`
|
| 314 |
+
}
|
| 315 |
+
|
| 316 |
+
export const getDocumentThumbnail = async (documentId: string,
|
| 317 |
+
params?: GetDocumentThumbnailParams, options?: RequestInit): Promise<Blob> => {
|
| 318 |
+
|
| 319 |
+
return fetchApi<Blob>(getGetDocumentThumbnailUrl(documentId,params),
|
| 320 |
+
{
|
| 321 |
+
...options,
|
| 322 |
+
method: 'GET'
|
| 323 |
+
|
| 324 |
+
|
| 325 |
+
}
|
| 326 |
+
);}
|
| 327 |
+
|
| 328 |
+
|
| 329 |
+
|
| 330 |
+
|
| 331 |
+
|
| 332 |
+
export const getGetDocumentThumbnailQueryKey = (documentId: string,
|
| 333 |
+
params?: GetDocumentThumbnailParams,) => {
|
| 334 |
+
return [
|
| 335 |
+
`/api/v1/documents/${documentId}/thumbnail`, ...(params ? [params] : [])
|
| 336 |
+
] as const;
|
| 337 |
+
}
|
| 338 |
+
|
| 339 |
+
|
| 340 |
+
export const getGetDocumentThumbnailQueryOptions = <TData = Awaited<ReturnType<typeof getDocumentThumbnail>>, TError = ApiError>(documentId: string,
|
| 341 |
+
params?: GetDocumentThumbnailParams, options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof getDocumentThumbnail>>, TError, TData>>, request?: SecondParameter<typeof fetchApi>}
|
| 342 |
+
) => {
|
| 343 |
+
|
| 344 |
+
const {query: queryOptions, request: requestOptions} = options ?? {};
|
| 345 |
+
|
| 346 |
+
const queryKey = queryOptions?.queryKey ?? getGetDocumentThumbnailQueryKey(documentId,params);
|
| 347 |
+
|
| 348 |
+
|
| 349 |
+
|
| 350 |
+
const queryFn: QueryFunction<Awaited<ReturnType<typeof getDocumentThumbnail>>> = ({ signal }) => getDocumentThumbnail(documentId,params, { signal, ...requestOptions });
|
| 351 |
+
|
| 352 |
+
|
| 353 |
+
|
| 354 |
+
|
| 355 |
+
|
| 356 |
+
return { queryKey, queryFn, enabled: !!(documentId), gcTime: 300000, retry: 1, ...queryOptions} as UseQueryOptions<Awaited<ReturnType<typeof getDocumentThumbnail>>, TError, TData> & { queryKey: DataTag<QueryKey, TData, TError> }
|
| 357 |
+
}
|
| 358 |
+
|
| 359 |
+
export type GetDocumentThumbnailQueryResult = NonNullable<Awaited<ReturnType<typeof getDocumentThumbnail>>>
|
| 360 |
+
export type GetDocumentThumbnailQueryError = ApiError
|
| 361 |
+
|
| 362 |
+
|
| 363 |
+
export function useGetDocumentThumbnail<TData = Awaited<ReturnType<typeof getDocumentThumbnail>>, TError = ApiError>(
|
| 364 |
+
documentId: string,
|
| 365 |
+
params: undefined | GetDocumentThumbnailParams, options: { query:Partial<UseQueryOptions<Awaited<ReturnType<typeof getDocumentThumbnail>>, TError, TData>> & Pick<
|
| 366 |
+
DefinedInitialDataOptions<
|
| 367 |
+
Awaited<ReturnType<typeof getDocumentThumbnail>>,
|
| 368 |
+
TError,
|
| 369 |
+
Awaited<ReturnType<typeof getDocumentThumbnail>>
|
| 370 |
+
> , 'initialData'
|
| 371 |
+
>, request?: SecondParameter<typeof fetchApi>}
|
| 372 |
+
, queryClient?: QueryClient
|
| 373 |
+
): DefinedUseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }
|
| 374 |
+
export function useGetDocumentThumbnail<TData = Awaited<ReturnType<typeof getDocumentThumbnail>>, TError = ApiError>(
|
| 375 |
+
documentId: string,
|
| 376 |
+
params?: GetDocumentThumbnailParams, options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof getDocumentThumbnail>>, TError, TData>> & Pick<
|
| 377 |
+
UndefinedInitialDataOptions<
|
| 378 |
+
Awaited<ReturnType<typeof getDocumentThumbnail>>,
|
| 379 |
+
TError,
|
| 380 |
+
Awaited<ReturnType<typeof getDocumentThumbnail>>
|
| 381 |
+
> , 'initialData'
|
| 382 |
+
>, request?: SecondParameter<typeof fetchApi>}
|
| 383 |
+
, queryClient?: QueryClient
|
| 384 |
+
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }
|
| 385 |
+
export function useGetDocumentThumbnail<TData = Awaited<ReturnType<typeof getDocumentThumbnail>>, TError = ApiError>(
|
| 386 |
+
documentId: string,
|
| 387 |
+
params?: GetDocumentThumbnailParams, options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof getDocumentThumbnail>>, TError, TData>>, request?: SecondParameter<typeof fetchApi>}
|
| 388 |
+
, queryClient?: QueryClient
|
| 389 |
+
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }
|
| 390 |
+
|
| 391 |
+
export function useGetDocumentThumbnail<TData = Awaited<ReturnType<typeof getDocumentThumbnail>>, TError = ApiError>(
|
| 392 |
+
documentId: string,
|
| 393 |
+
params?: GetDocumentThumbnailParams, options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof getDocumentThumbnail>>, TError, TData>>, request?: SecondParameter<typeof fetchApi>}
|
| 394 |
+
, queryClient?: QueryClient
|
| 395 |
+
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> } {
|
| 396 |
+
|
| 397 |
+
const queryOptions = getGetDocumentThumbnailQueryOptions(documentId,params,options)
|
| 398 |
+
|
| 399 |
+
const query = useQuery(queryOptions, queryClient) as UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> };
|
| 400 |
+
|
| 401 |
+
return { ...query, queryKey: queryOptions.queryKey };
|
| 402 |
+
}
|
| 403 |
+
|
| 404 |
+
|
| 405 |
+
|
| 406 |
+
|
ui/lib/api/downloads/downloads.ts
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Generated by orval v8.6.2 🍺
|
| 3 |
+
* Do not edit manually.
|
| 4 |
+
* OpenAPI spec version: 0.0.1
|
| 5 |
+
*/
|
| 6 |
+
import {
|
| 7 |
+
useQuery
|
| 8 |
+
} from '@tanstack/react-query';
|
| 9 |
+
import type {
|
| 10 |
+
DataTag,
|
| 11 |
+
DefinedInitialDataOptions,
|
| 12 |
+
DefinedUseQueryResult,
|
| 13 |
+
QueryClient,
|
| 14 |
+
QueryFunction,
|
| 15 |
+
QueryKey,
|
| 16 |
+
UndefinedInitialDataOptions,
|
| 17 |
+
UseQueryOptions,
|
| 18 |
+
UseQueryResult
|
| 19 |
+
} from '@tanstack/react-query';
|
| 20 |
+
|
| 21 |
+
import type {
|
| 22 |
+
DownloadState
|
| 23 |
+
} from '../schemas';
|
| 24 |
+
|
| 25 |
+
import { fetchApi } from '.././fetch';
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
type SecondParameter<T extends (...args: never) => unknown> = Parameters<T>[1];
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
export const getListDownloadsUrl = () => {
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
return `/api/v1/downloads`
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
export const listDownloads = async ( options?: RequestInit): Promise<DownloadState[]> => {
|
| 41 |
+
|
| 42 |
+
return fetchApi<DownloadState[]>(getListDownloadsUrl(),
|
| 43 |
+
{
|
| 44 |
+
...options,
|
| 45 |
+
method: 'GET'
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
}
|
| 49 |
+
);}
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
export const getListDownloadsQueryKey = () => {
|
| 56 |
+
return [
|
| 57 |
+
`/api/v1/downloads`
|
| 58 |
+
] as const;
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
export const getListDownloadsQueryOptions = <TData = Awaited<ReturnType<typeof listDownloads>>, TError = unknown>( options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof listDownloads>>, TError, TData>>, request?: SecondParameter<typeof fetchApi>}
|
| 63 |
+
) => {
|
| 64 |
+
|
| 65 |
+
const {query: queryOptions, request: requestOptions} = options ?? {};
|
| 66 |
+
|
| 67 |
+
const queryKey = queryOptions?.queryKey ?? getListDownloadsQueryKey();
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
const queryFn: QueryFunction<Awaited<ReturnType<typeof listDownloads>>> = ({ signal }) => listDownloads({ signal, ...requestOptions });
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
return { queryKey, queryFn, gcTime: 300000, retry: 1, ...queryOptions} as UseQueryOptions<Awaited<ReturnType<typeof listDownloads>>, TError, TData> & { queryKey: DataTag<QueryKey, TData, TError> }
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
export type ListDownloadsQueryResult = NonNullable<Awaited<ReturnType<typeof listDownloads>>>
|
| 81 |
+
export type ListDownloadsQueryError = unknown
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
export function useListDownloads<TData = Awaited<ReturnType<typeof listDownloads>>, TError = unknown>(
|
| 85 |
+
options: { query:Partial<UseQueryOptions<Awaited<ReturnType<typeof listDownloads>>, TError, TData>> & Pick<
|
| 86 |
+
DefinedInitialDataOptions<
|
| 87 |
+
Awaited<ReturnType<typeof listDownloads>>,
|
| 88 |
+
TError,
|
| 89 |
+
Awaited<ReturnType<typeof listDownloads>>
|
| 90 |
+
> , 'initialData'
|
| 91 |
+
>, request?: SecondParameter<typeof fetchApi>}
|
| 92 |
+
, queryClient?: QueryClient
|
| 93 |
+
): DefinedUseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }
|
| 94 |
+
export function useListDownloads<TData = Awaited<ReturnType<typeof listDownloads>>, TError = unknown>(
|
| 95 |
+
options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof listDownloads>>, TError, TData>> & Pick<
|
| 96 |
+
UndefinedInitialDataOptions<
|
| 97 |
+
Awaited<ReturnType<typeof listDownloads>>,
|
| 98 |
+
TError,
|
| 99 |
+
Awaited<ReturnType<typeof listDownloads>>
|
| 100 |
+
> , 'initialData'
|
| 101 |
+
>, request?: SecondParameter<typeof fetchApi>}
|
| 102 |
+
, queryClient?: QueryClient
|
| 103 |
+
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }
|
| 104 |
+
export function useListDownloads<TData = Awaited<ReturnType<typeof listDownloads>>, TError = unknown>(
|
| 105 |
+
options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof listDownloads>>, TError, TData>>, request?: SecondParameter<typeof fetchApi>}
|
| 106 |
+
, queryClient?: QueryClient
|
| 107 |
+
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }
|
| 108 |
+
|
| 109 |
+
export function useListDownloads<TData = Awaited<ReturnType<typeof listDownloads>>, TError = unknown>(
|
| 110 |
+
options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof listDownloads>>, TError, TData>>, request?: SecondParameter<typeof fetchApi>}
|
| 111 |
+
, queryClient?: QueryClient
|
| 112 |
+
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> } {
|
| 113 |
+
|
| 114 |
+
const queryOptions = getListDownloadsQueryOptions(options)
|
| 115 |
+
|
| 116 |
+
const query = useQuery(queryOptions, queryClient) as UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> };
|
| 117 |
+
|
| 118 |
+
return { ...query, queryKey: queryOptions.queryKey };
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
|
ui/lib/api/exports/exports.ts
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Generated by orval v8.6.2 🍺
|
| 3 |
+
* Do not edit manually.
|
| 4 |
+
* OpenAPI spec version: 0.0.1
|
| 5 |
+
*/
|
| 6 |
+
import {
|
| 7 |
+
useMutation,
|
| 8 |
+
useQuery
|
| 9 |
+
} from '@tanstack/react-query';
|
| 10 |
+
import type {
|
| 11 |
+
DataTag,
|
| 12 |
+
DefinedInitialDataOptions,
|
| 13 |
+
DefinedUseQueryResult,
|
| 14 |
+
MutationFunction,
|
| 15 |
+
QueryClient,
|
| 16 |
+
QueryFunction,
|
| 17 |
+
QueryKey,
|
| 18 |
+
UndefinedInitialDataOptions,
|
| 19 |
+
UseMutationOptions,
|
| 20 |
+
UseMutationResult,
|
| 21 |
+
UseQueryOptions,
|
| 22 |
+
UseQueryResult
|
| 23 |
+
} from '@tanstack/react-query';
|
| 24 |
+
|
| 25 |
+
import type {
|
| 26 |
+
ApiError,
|
| 27 |
+
ExportBatchRequest,
|
| 28 |
+
ExportDocumentParams,
|
| 29 |
+
ExportResult
|
| 30 |
+
} from '../schemas';
|
| 31 |
+
|
| 32 |
+
import { fetchApi } from '.././fetch';
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
type SecondParameter<T extends (...args: never) => unknown> = Parameters<T>[1];
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
export const getExportDocumentUrl = (documentId: string,
|
| 40 |
+
format: string,
|
| 41 |
+
params?: ExportDocumentParams,) => {
|
| 42 |
+
const normalizedParams = new URLSearchParams();
|
| 43 |
+
|
| 44 |
+
Object.entries(params || {}).forEach(([key, value]) => {
|
| 45 |
+
|
| 46 |
+
if (value !== undefined) {
|
| 47 |
+
normalizedParams.append(key, value === null ? 'null' : value.toString())
|
| 48 |
+
}
|
| 49 |
+
});
|
| 50 |
+
|
| 51 |
+
const stringifiedParams = normalizedParams.toString();
|
| 52 |
+
|
| 53 |
+
return stringifiedParams.length > 0 ? `/api/v1/documents/${documentId}/export/${format}?${stringifiedParams}` : `/api/v1/documents/${documentId}/export/${format}`
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
export const exportDocument = async (documentId: string,
|
| 57 |
+
format: string,
|
| 58 |
+
params?: ExportDocumentParams, options?: RequestInit): Promise<Blob> => {
|
| 59 |
+
|
| 60 |
+
return fetchApi<Blob>(getExportDocumentUrl(documentId,format,params),
|
| 61 |
+
{
|
| 62 |
+
...options,
|
| 63 |
+
method: 'GET'
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
}
|
| 67 |
+
);}
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
export const getExportDocumentQueryKey = (documentId: string,
|
| 74 |
+
format: string,
|
| 75 |
+
params?: ExportDocumentParams,) => {
|
| 76 |
+
return [
|
| 77 |
+
`/api/v1/documents/${documentId}/export/${format}`, ...(params ? [params] : [])
|
| 78 |
+
] as const;
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
export const getExportDocumentQueryOptions = <TData = Awaited<ReturnType<typeof exportDocument>>, TError = ApiError>(documentId: string,
|
| 83 |
+
format: string,
|
| 84 |
+
params?: ExportDocumentParams, options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof exportDocument>>, TError, TData>>, request?: SecondParameter<typeof fetchApi>}
|
| 85 |
+
) => {
|
| 86 |
+
|
| 87 |
+
const {query: queryOptions, request: requestOptions} = options ?? {};
|
| 88 |
+
|
| 89 |
+
const queryKey = queryOptions?.queryKey ?? getExportDocumentQueryKey(documentId,format,params);
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
const queryFn: QueryFunction<Awaited<ReturnType<typeof exportDocument>>> = ({ signal }) => exportDocument(documentId,format,params, { signal, ...requestOptions });
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
return { queryKey, queryFn, enabled: !!(documentId && format), gcTime: 300000, retry: 1, ...queryOptions} as UseQueryOptions<Awaited<ReturnType<typeof exportDocument>>, TError, TData> & { queryKey: DataTag<QueryKey, TData, TError> }
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
export type ExportDocumentQueryResult = NonNullable<Awaited<ReturnType<typeof exportDocument>>>
|
| 103 |
+
export type ExportDocumentQueryError = ApiError
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
export function useExportDocument<TData = Awaited<ReturnType<typeof exportDocument>>, TError = ApiError>(
|
| 107 |
+
documentId: string,
|
| 108 |
+
format: string,
|
| 109 |
+
params: undefined | ExportDocumentParams, options: { query:Partial<UseQueryOptions<Awaited<ReturnType<typeof exportDocument>>, TError, TData>> & Pick<
|
| 110 |
+
DefinedInitialDataOptions<
|
| 111 |
+
Awaited<ReturnType<typeof exportDocument>>,
|
| 112 |
+
TError,
|
| 113 |
+
Awaited<ReturnType<typeof exportDocument>>
|
| 114 |
+
> , 'initialData'
|
| 115 |
+
>, request?: SecondParameter<typeof fetchApi>}
|
| 116 |
+
, queryClient?: QueryClient
|
| 117 |
+
): DefinedUseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }
|
| 118 |
+
export function useExportDocument<TData = Awaited<ReturnType<typeof exportDocument>>, TError = ApiError>(
|
| 119 |
+
documentId: string,
|
| 120 |
+
format: string,
|
| 121 |
+
params?: ExportDocumentParams, options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof exportDocument>>, TError, TData>> & Pick<
|
| 122 |
+
UndefinedInitialDataOptions<
|
| 123 |
+
Awaited<ReturnType<typeof exportDocument>>,
|
| 124 |
+
TError,
|
| 125 |
+
Awaited<ReturnType<typeof exportDocument>>
|
| 126 |
+
> , 'initialData'
|
| 127 |
+
>, request?: SecondParameter<typeof fetchApi>}
|
| 128 |
+
, queryClient?: QueryClient
|
| 129 |
+
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }
|
| 130 |
+
export function useExportDocument<TData = Awaited<ReturnType<typeof exportDocument>>, TError = ApiError>(
|
| 131 |
+
documentId: string,
|
| 132 |
+
format: string,
|
| 133 |
+
params?: ExportDocumentParams, options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof exportDocument>>, TError, TData>>, request?: SecondParameter<typeof fetchApi>}
|
| 134 |
+
, queryClient?: QueryClient
|
| 135 |
+
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }
|
| 136 |
+
|
| 137 |
+
export function useExportDocument<TData = Awaited<ReturnType<typeof exportDocument>>, TError = ApiError>(
|
| 138 |
+
documentId: string,
|
| 139 |
+
format: string,
|
| 140 |
+
params?: ExportDocumentParams, options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof exportDocument>>, TError, TData>>, request?: SecondParameter<typeof fetchApi>}
|
| 141 |
+
, queryClient?: QueryClient
|
| 142 |
+
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> } {
|
| 143 |
+
|
| 144 |
+
const queryOptions = getExportDocumentQueryOptions(documentId,format,params,options)
|
| 145 |
+
|
| 146 |
+
const query = useQuery(queryOptions, queryClient) as UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> };
|
| 147 |
+
|
| 148 |
+
return { ...query, queryKey: queryOptions.queryKey };
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
|
| 152 |
+
|
| 153 |
+
|
| 154 |
+
export const getBatchExportUrl = () => {
|
| 155 |
+
|
| 156 |
+
|
| 157 |
+
|
| 158 |
+
|
| 159 |
+
return `/api/v1/exports`
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
export const batchExport = async (exportBatchRequest: ExportBatchRequest, options?: RequestInit): Promise<ExportResult> => {
|
| 163 |
+
|
| 164 |
+
return fetchApi<ExportResult>(getBatchExportUrl(),
|
| 165 |
+
{
|
| 166 |
+
...options,
|
| 167 |
+
method: 'POST',
|
| 168 |
+
headers: { 'Content-Type': 'application/json', ...options?.headers },
|
| 169 |
+
body: JSON.stringify(
|
| 170 |
+
exportBatchRequest,)
|
| 171 |
+
}
|
| 172 |
+
);}
|
| 173 |
+
|
| 174 |
+
|
| 175 |
+
|
| 176 |
+
|
| 177 |
+
export const getBatchExportMutationOptions = <TError = ApiError,
|
| 178 |
+
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof batchExport>>, TError,{data: ExportBatchRequest}, TContext>, request?: SecondParameter<typeof fetchApi>}
|
| 179 |
+
): UseMutationOptions<Awaited<ReturnType<typeof batchExport>>, TError,{data: ExportBatchRequest}, TContext> => {
|
| 180 |
+
|
| 181 |
+
const mutationKey = ['batchExport'];
|
| 182 |
+
const {mutation: mutationOptions, request: requestOptions} = options ?
|
| 183 |
+
options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey ?
|
| 184 |
+
options
|
| 185 |
+
: {...options, mutation: {...options.mutation, mutationKey}}
|
| 186 |
+
: {mutation: { mutationKey, }, request: undefined};
|
| 187 |
+
|
| 188 |
+
|
| 189 |
+
|
| 190 |
+
|
| 191 |
+
const mutationFn: MutationFunction<Awaited<ReturnType<typeof batchExport>>, {data: ExportBatchRequest}> = (props) => {
|
| 192 |
+
const {data} = props ?? {};
|
| 193 |
+
|
| 194 |
+
return batchExport(data,requestOptions)
|
| 195 |
+
}
|
| 196 |
+
|
| 197 |
+
|
| 198 |
+
|
| 199 |
+
|
| 200 |
+
|
| 201 |
+
|
| 202 |
+
return { mutationFn, ...mutationOptions }}
|
| 203 |
+
|
| 204 |
+
export type BatchExportMutationResult = NonNullable<Awaited<ReturnType<typeof batchExport>>>
|
| 205 |
+
export type BatchExportMutationBody = ExportBatchRequest
|
| 206 |
+
export type BatchExportMutationError = ApiError
|
| 207 |
+
|
| 208 |
+
export const useBatchExport = <TError = ApiError,
|
| 209 |
+
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof batchExport>>, TError,{data: ExportBatchRequest}, TContext>, request?: SecondParameter<typeof fetchApi>}
|
| 210 |
+
, queryClient?: QueryClient): UseMutationResult<
|
| 211 |
+
Awaited<ReturnType<typeof batchExport>>,
|
| 212 |
+
TError,
|
| 213 |
+
{data: ExportBatchRequest},
|
| 214 |
+
TContext
|
| 215 |
+
> => {
|
| 216 |
+
return useMutation(getBatchExportMutationOptions(options), queryClient);
|
| 217 |
+
}
|
ui/lib/api/fetch.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export const fetchApi = async <T>(url: string, options?: RequestInit): Promise<T> => {
|
| 2 |
+
const res = await fetch(url, options)
|
| 3 |
+
if (!res.ok) {
|
| 4 |
+
throw await res.json().catch(() => ({ status: res.status, message: res.statusText }))
|
| 5 |
+
}
|
| 6 |
+
if ([204, 205, 304].includes(res.status)) {
|
| 7 |
+
return undefined as T
|
| 8 |
+
}
|
| 9 |
+
const contentType = res.headers.get('content-type') ?? ''
|
| 10 |
+
if (!contentType.includes('json')) {
|
| 11 |
+
return (await res.blob()) as T
|
| 12 |
+
}
|
| 13 |
+
return res.json()
|
| 14 |
+
}
|
ui/lib/api/jobs/jobs.ts
ADDED
|
@@ -0,0 +1,348 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Generated by orval v8.6.2 🍺
|
| 3 |
+
* Do not edit manually.
|
| 4 |
+
* OpenAPI spec version: 0.0.1
|
| 5 |
+
*/
|
| 6 |
+
import {
|
| 7 |
+
useMutation,
|
| 8 |
+
useQuery
|
| 9 |
+
} from '@tanstack/react-query';
|
| 10 |
+
import type {
|
| 11 |
+
DataTag,
|
| 12 |
+
DefinedInitialDataOptions,
|
| 13 |
+
DefinedUseQueryResult,
|
| 14 |
+
MutationFunction,
|
| 15 |
+
QueryClient,
|
| 16 |
+
QueryFunction,
|
| 17 |
+
QueryKey,
|
| 18 |
+
UndefinedInitialDataOptions,
|
| 19 |
+
UseMutationOptions,
|
| 20 |
+
UseMutationResult,
|
| 21 |
+
UseQueryOptions,
|
| 22 |
+
UseQueryResult
|
| 23 |
+
} from '@tanstack/react-query';
|
| 24 |
+
|
| 25 |
+
import type {
|
| 26 |
+
ApiError,
|
| 27 |
+
JobState,
|
| 28 |
+
PipelineJobRequest
|
| 29 |
+
} from '../schemas';
|
| 30 |
+
|
| 31 |
+
import { fetchApi } from '.././fetch';
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
type SecondParameter<T extends (...args: never) => unknown> = Parameters<T>[1];
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
export const getListJobsUrl = () => {
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
return `/api/v1/jobs`
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
export const listJobs = async ( options?: RequestInit): Promise<JobState[]> => {
|
| 47 |
+
|
| 48 |
+
return fetchApi<JobState[]>(getListJobsUrl(),
|
| 49 |
+
{
|
| 50 |
+
...options,
|
| 51 |
+
method: 'GET'
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
}
|
| 55 |
+
);}
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
export const getListJobsQueryKey = () => {
|
| 62 |
+
return [
|
| 63 |
+
`/api/v1/jobs`
|
| 64 |
+
] as const;
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
export const getListJobsQueryOptions = <TData = Awaited<ReturnType<typeof listJobs>>, TError = unknown>( options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof listJobs>>, TError, TData>>, request?: SecondParameter<typeof fetchApi>}
|
| 69 |
+
) => {
|
| 70 |
+
|
| 71 |
+
const {query: queryOptions, request: requestOptions} = options ?? {};
|
| 72 |
+
|
| 73 |
+
const queryKey = queryOptions?.queryKey ?? getListJobsQueryKey();
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
const queryFn: QueryFunction<Awaited<ReturnType<typeof listJobs>>> = ({ signal }) => listJobs({ signal, ...requestOptions });
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
return { queryKey, queryFn, gcTime: 300000, retry: 1, ...queryOptions} as UseQueryOptions<Awaited<ReturnType<typeof listJobs>>, TError, TData> & { queryKey: DataTag<QueryKey, TData, TError> }
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
export type ListJobsQueryResult = NonNullable<Awaited<ReturnType<typeof listJobs>>>
|
| 87 |
+
export type ListJobsQueryError = unknown
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
export function useListJobs<TData = Awaited<ReturnType<typeof listJobs>>, TError = unknown>(
|
| 91 |
+
options: { query:Partial<UseQueryOptions<Awaited<ReturnType<typeof listJobs>>, TError, TData>> & Pick<
|
| 92 |
+
DefinedInitialDataOptions<
|
| 93 |
+
Awaited<ReturnType<typeof listJobs>>,
|
| 94 |
+
TError,
|
| 95 |
+
Awaited<ReturnType<typeof listJobs>>
|
| 96 |
+
> , 'initialData'
|
| 97 |
+
>, request?: SecondParameter<typeof fetchApi>}
|
| 98 |
+
, queryClient?: QueryClient
|
| 99 |
+
): DefinedUseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }
|
| 100 |
+
export function useListJobs<TData = Awaited<ReturnType<typeof listJobs>>, TError = unknown>(
|
| 101 |
+
options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof listJobs>>, TError, TData>> & Pick<
|
| 102 |
+
UndefinedInitialDataOptions<
|
| 103 |
+
Awaited<ReturnType<typeof listJobs>>,
|
| 104 |
+
TError,
|
| 105 |
+
Awaited<ReturnType<typeof listJobs>>
|
| 106 |
+
> , 'initialData'
|
| 107 |
+
>, request?: SecondParameter<typeof fetchApi>}
|
| 108 |
+
, queryClient?: QueryClient
|
| 109 |
+
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }
|
| 110 |
+
export function useListJobs<TData = Awaited<ReturnType<typeof listJobs>>, TError = unknown>(
|
| 111 |
+
options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof listJobs>>, TError, TData>>, request?: SecondParameter<typeof fetchApi>}
|
| 112 |
+
, queryClient?: QueryClient
|
| 113 |
+
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }
|
| 114 |
+
|
| 115 |
+
export function useListJobs<TData = Awaited<ReturnType<typeof listJobs>>, TError = unknown>(
|
| 116 |
+
options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof listJobs>>, TError, TData>>, request?: SecondParameter<typeof fetchApi>}
|
| 117 |
+
, queryClient?: QueryClient
|
| 118 |
+
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> } {
|
| 119 |
+
|
| 120 |
+
const queryOptions = getListJobsQueryOptions(options)
|
| 121 |
+
|
| 122 |
+
const query = useQuery(queryOptions, queryClient) as UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> };
|
| 123 |
+
|
| 124 |
+
return { ...query, queryKey: queryOptions.queryKey };
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
|
| 129 |
+
|
| 130 |
+
export const getStartPipelineUrl = () => {
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
return `/api/v1/jobs/pipeline`
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
export const startPipeline = async (pipelineJobRequest: PipelineJobRequest, options?: RequestInit): Promise<JobState> => {
|
| 139 |
+
|
| 140 |
+
return fetchApi<JobState>(getStartPipelineUrl(),
|
| 141 |
+
{
|
| 142 |
+
...options,
|
| 143 |
+
method: 'POST',
|
| 144 |
+
headers: { 'Content-Type': 'application/json', ...options?.headers },
|
| 145 |
+
body: JSON.stringify(
|
| 146 |
+
pipelineJobRequest,)
|
| 147 |
+
}
|
| 148 |
+
);}
|
| 149 |
+
|
| 150 |
+
|
| 151 |
+
|
| 152 |
+
|
| 153 |
+
export const getStartPipelineMutationOptions = <TError = ApiError,
|
| 154 |
+
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof startPipeline>>, TError,{data: PipelineJobRequest}, TContext>, request?: SecondParameter<typeof fetchApi>}
|
| 155 |
+
): UseMutationOptions<Awaited<ReturnType<typeof startPipeline>>, TError,{data: PipelineJobRequest}, TContext> => {
|
| 156 |
+
|
| 157 |
+
const mutationKey = ['startPipeline'];
|
| 158 |
+
const {mutation: mutationOptions, request: requestOptions} = options ?
|
| 159 |
+
options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey ?
|
| 160 |
+
options
|
| 161 |
+
: {...options, mutation: {...options.mutation, mutationKey}}
|
| 162 |
+
: {mutation: { mutationKey, }, request: undefined};
|
| 163 |
+
|
| 164 |
+
|
| 165 |
+
|
| 166 |
+
|
| 167 |
+
const mutationFn: MutationFunction<Awaited<ReturnType<typeof startPipeline>>, {data: PipelineJobRequest}> = (props) => {
|
| 168 |
+
const {data} = props ?? {};
|
| 169 |
+
|
| 170 |
+
return startPipeline(data,requestOptions)
|
| 171 |
+
}
|
| 172 |
+
|
| 173 |
+
|
| 174 |
+
|
| 175 |
+
|
| 176 |
+
|
| 177 |
+
|
| 178 |
+
return { mutationFn, ...mutationOptions }}
|
| 179 |
+
|
| 180 |
+
export type StartPipelineMutationResult = NonNullable<Awaited<ReturnType<typeof startPipeline>>>
|
| 181 |
+
export type StartPipelineMutationBody = PipelineJobRequest
|
| 182 |
+
export type StartPipelineMutationError = ApiError
|
| 183 |
+
|
| 184 |
+
export const useStartPipeline = <TError = ApiError,
|
| 185 |
+
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof startPipeline>>, TError,{data: PipelineJobRequest}, TContext>, request?: SecondParameter<typeof fetchApi>}
|
| 186 |
+
, queryClient?: QueryClient): UseMutationResult<
|
| 187 |
+
Awaited<ReturnType<typeof startPipeline>>,
|
| 188 |
+
TError,
|
| 189 |
+
{data: PipelineJobRequest},
|
| 190 |
+
TContext
|
| 191 |
+
> => {
|
| 192 |
+
return useMutation(getStartPipelineMutationOptions(options), queryClient);
|
| 193 |
+
}
|
| 194 |
+
export const getGetJobUrl = (jobId: string,) => {
|
| 195 |
+
|
| 196 |
+
|
| 197 |
+
|
| 198 |
+
|
| 199 |
+
return `/api/v1/jobs/${jobId}`
|
| 200 |
+
}
|
| 201 |
+
|
| 202 |
+
export const getJob = async (jobId: string, options?: RequestInit): Promise<JobState> => {
|
| 203 |
+
|
| 204 |
+
return fetchApi<JobState>(getGetJobUrl(jobId),
|
| 205 |
+
{
|
| 206 |
+
...options,
|
| 207 |
+
method: 'GET'
|
| 208 |
+
|
| 209 |
+
|
| 210 |
+
}
|
| 211 |
+
);}
|
| 212 |
+
|
| 213 |
+
|
| 214 |
+
|
| 215 |
+
|
| 216 |
+
|
| 217 |
+
export const getGetJobQueryKey = (jobId: string,) => {
|
| 218 |
+
return [
|
| 219 |
+
`/api/v1/jobs/${jobId}`
|
| 220 |
+
] as const;
|
| 221 |
+
}
|
| 222 |
+
|
| 223 |
+
|
| 224 |
+
export const getGetJobQueryOptions = <TData = Awaited<ReturnType<typeof getJob>>, TError = ApiError>(jobId: string, options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof getJob>>, TError, TData>>, request?: SecondParameter<typeof fetchApi>}
|
| 225 |
+
) => {
|
| 226 |
+
|
| 227 |
+
const {query: queryOptions, request: requestOptions} = options ?? {};
|
| 228 |
+
|
| 229 |
+
const queryKey = queryOptions?.queryKey ?? getGetJobQueryKey(jobId);
|
| 230 |
+
|
| 231 |
+
|
| 232 |
+
|
| 233 |
+
const queryFn: QueryFunction<Awaited<ReturnType<typeof getJob>>> = ({ signal }) => getJob(jobId, { signal, ...requestOptions });
|
| 234 |
+
|
| 235 |
+
|
| 236 |
+
|
| 237 |
+
|
| 238 |
+
|
| 239 |
+
return { queryKey, queryFn, enabled: !!(jobId), gcTime: 300000, retry: 1, ...queryOptions} as UseQueryOptions<Awaited<ReturnType<typeof getJob>>, TError, TData> & { queryKey: DataTag<QueryKey, TData, TError> }
|
| 240 |
+
}
|
| 241 |
+
|
| 242 |
+
export type GetJobQueryResult = NonNullable<Awaited<ReturnType<typeof getJob>>>
|
| 243 |
+
export type GetJobQueryError = ApiError
|
| 244 |
+
|
| 245 |
+
|
| 246 |
+
export function useGetJob<TData = Awaited<ReturnType<typeof getJob>>, TError = ApiError>(
|
| 247 |
+
jobId: string, options: { query:Partial<UseQueryOptions<Awaited<ReturnType<typeof getJob>>, TError, TData>> & Pick<
|
| 248 |
+
DefinedInitialDataOptions<
|
| 249 |
+
Awaited<ReturnType<typeof getJob>>,
|
| 250 |
+
TError,
|
| 251 |
+
Awaited<ReturnType<typeof getJob>>
|
| 252 |
+
> , 'initialData'
|
| 253 |
+
>, request?: SecondParameter<typeof fetchApi>}
|
| 254 |
+
, queryClient?: QueryClient
|
| 255 |
+
): DefinedUseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }
|
| 256 |
+
export function useGetJob<TData = Awaited<ReturnType<typeof getJob>>, TError = ApiError>(
|
| 257 |
+
jobId: string, options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof getJob>>, TError, TData>> & Pick<
|
| 258 |
+
UndefinedInitialDataOptions<
|
| 259 |
+
Awaited<ReturnType<typeof getJob>>,
|
| 260 |
+
TError,
|
| 261 |
+
Awaited<ReturnType<typeof getJob>>
|
| 262 |
+
> , 'initialData'
|
| 263 |
+
>, request?: SecondParameter<typeof fetchApi>}
|
| 264 |
+
, queryClient?: QueryClient
|
| 265 |
+
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }
|
| 266 |
+
export function useGetJob<TData = Awaited<ReturnType<typeof getJob>>, TError = ApiError>(
|
| 267 |
+
jobId: string, options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof getJob>>, TError, TData>>, request?: SecondParameter<typeof fetchApi>}
|
| 268 |
+
, queryClient?: QueryClient
|
| 269 |
+
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }
|
| 270 |
+
|
| 271 |
+
export function useGetJob<TData = Awaited<ReturnType<typeof getJob>>, TError = ApiError>(
|
| 272 |
+
jobId: string, options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof getJob>>, TError, TData>>, request?: SecondParameter<typeof fetchApi>}
|
| 273 |
+
, queryClient?: QueryClient
|
| 274 |
+
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> } {
|
| 275 |
+
|
| 276 |
+
const queryOptions = getGetJobQueryOptions(jobId,options)
|
| 277 |
+
|
| 278 |
+
const query = useQuery(queryOptions, queryClient) as UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> };
|
| 279 |
+
|
| 280 |
+
return { ...query, queryKey: queryOptions.queryKey };
|
| 281 |
+
}
|
| 282 |
+
|
| 283 |
+
|
| 284 |
+
|
| 285 |
+
|
| 286 |
+
export const getCancelJobUrl = (jobId: string,) => {
|
| 287 |
+
|
| 288 |
+
|
| 289 |
+
|
| 290 |
+
|
| 291 |
+
return `/api/v1/jobs/${jobId}`
|
| 292 |
+
}
|
| 293 |
+
|
| 294 |
+
export const cancelJob = async (jobId: string, options?: RequestInit): Promise<void> => {
|
| 295 |
+
|
| 296 |
+
return fetchApi<void>(getCancelJobUrl(jobId),
|
| 297 |
+
{
|
| 298 |
+
...options,
|
| 299 |
+
method: 'DELETE'
|
| 300 |
+
|
| 301 |
+
|
| 302 |
+
}
|
| 303 |
+
);}
|
| 304 |
+
|
| 305 |
+
|
| 306 |
+
|
| 307 |
+
|
| 308 |
+
export const getCancelJobMutationOptions = <TError = ApiError,
|
| 309 |
+
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof cancelJob>>, TError,{jobId: string}, TContext>, request?: SecondParameter<typeof fetchApi>}
|
| 310 |
+
): UseMutationOptions<Awaited<ReturnType<typeof cancelJob>>, TError,{jobId: string}, TContext> => {
|
| 311 |
+
|
| 312 |
+
const mutationKey = ['cancelJob'];
|
| 313 |
+
const {mutation: mutationOptions, request: requestOptions} = options ?
|
| 314 |
+
options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey ?
|
| 315 |
+
options
|
| 316 |
+
: {...options, mutation: {...options.mutation, mutationKey}}
|
| 317 |
+
: {mutation: { mutationKey, }, request: undefined};
|
| 318 |
+
|
| 319 |
+
|
| 320 |
+
|
| 321 |
+
|
| 322 |
+
const mutationFn: MutationFunction<Awaited<ReturnType<typeof cancelJob>>, {jobId: string}> = (props) => {
|
| 323 |
+
const {jobId} = props ?? {};
|
| 324 |
+
|
| 325 |
+
return cancelJob(jobId,requestOptions)
|
| 326 |
+
}
|
| 327 |
+
|
| 328 |
+
|
| 329 |
+
|
| 330 |
+
|
| 331 |
+
|
| 332 |
+
|
| 333 |
+
return { mutationFn, ...mutationOptions }}
|
| 334 |
+
|
| 335 |
+
export type CancelJobMutationResult = NonNullable<Awaited<ReturnType<typeof cancelJob>>>
|
| 336 |
+
|
| 337 |
+
export type CancelJobMutationError = ApiError
|
| 338 |
+
|
| 339 |
+
export const useCancelJob = <TError = ApiError,
|
| 340 |
+
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof cancelJob>>, TError,{jobId: string}, TContext>, request?: SecondParameter<typeof fetchApi>}
|
| 341 |
+
, queryClient?: QueryClient): UseMutationResult<
|
| 342 |
+
Awaited<ReturnType<typeof cancelJob>>,
|
| 343 |
+
TError,
|
| 344 |
+
{jobId: string},
|
| 345 |
+
TContext
|
| 346 |
+
> => {
|
| 347 |
+
return useMutation(getCancelJobMutationOptions(options), queryClient);
|
| 348 |
+
}
|
ui/lib/api/llm/llm.ts
ADDED
|
@@ -0,0 +1,423 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Generated by orval v8.6.2 🍺
|
| 3 |
+
* Do not edit manually.
|
| 4 |
+
* OpenAPI spec version: 0.0.1
|
| 5 |
+
*/
|
| 6 |
+
import {
|
| 7 |
+
useMutation,
|
| 8 |
+
useQuery
|
| 9 |
+
} from '@tanstack/react-query';
|
| 10 |
+
import type {
|
| 11 |
+
DataTag,
|
| 12 |
+
DefinedInitialDataOptions,
|
| 13 |
+
DefinedUseQueryResult,
|
| 14 |
+
MutationFunction,
|
| 15 |
+
QueryClient,
|
| 16 |
+
QueryFunction,
|
| 17 |
+
QueryKey,
|
| 18 |
+
UndefinedInitialDataOptions,
|
| 19 |
+
UseMutationOptions,
|
| 20 |
+
UseMutationResult,
|
| 21 |
+
UseQueryOptions,
|
| 22 |
+
UseQueryResult
|
| 23 |
+
} from '@tanstack/react-query';
|
| 24 |
+
|
| 25 |
+
import type {
|
| 26 |
+
ApiError,
|
| 27 |
+
ListLlmModelsParams,
|
| 28 |
+
LlmLoadRequest,
|
| 29 |
+
LlmModelInfo,
|
| 30 |
+
LlmPingRequest,
|
| 31 |
+
LlmPingResponse,
|
| 32 |
+
LlmState
|
| 33 |
+
} from '../schemas';
|
| 34 |
+
|
| 35 |
+
import { fetchApi } from '.././fetch';
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
type SecondParameter<T extends (...args: never) => unknown> = Parameters<T>[1];
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
export const getGetLlmUrl = () => {
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
return `/api/v1/llm`
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
export const getLlm = async ( options?: RequestInit): Promise<LlmState> => {
|
| 51 |
+
|
| 52 |
+
return fetchApi<LlmState>(getGetLlmUrl(),
|
| 53 |
+
{
|
| 54 |
+
...options,
|
| 55 |
+
method: 'GET'
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
}
|
| 59 |
+
);}
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
export const getGetLlmQueryKey = () => {
|
| 66 |
+
return [
|
| 67 |
+
`/api/v1/llm`
|
| 68 |
+
] as const;
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
export const getGetLlmQueryOptions = <TData = Awaited<ReturnType<typeof getLlm>>, TError = ApiError>( options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof getLlm>>, TError, TData>>, request?: SecondParameter<typeof fetchApi>}
|
| 73 |
+
) => {
|
| 74 |
+
|
| 75 |
+
const {query: queryOptions, request: requestOptions} = options ?? {};
|
| 76 |
+
|
| 77 |
+
const queryKey = queryOptions?.queryKey ?? getGetLlmQueryKey();
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
const queryFn: QueryFunction<Awaited<ReturnType<typeof getLlm>>> = ({ signal }) => getLlm({ signal, ...requestOptions });
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
return { queryKey, queryFn, gcTime: 300000, retry: 1, ...queryOptions} as UseQueryOptions<Awaited<ReturnType<typeof getLlm>>, TError, TData> & { queryKey: DataTag<QueryKey, TData, TError> }
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
export type GetLlmQueryResult = NonNullable<Awaited<ReturnType<typeof getLlm>>>
|
| 91 |
+
export type GetLlmQueryError = ApiError
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
export function useGetLlm<TData = Awaited<ReturnType<typeof getLlm>>, TError = ApiError>(
|
| 95 |
+
options: { query:Partial<UseQueryOptions<Awaited<ReturnType<typeof getLlm>>, TError, TData>> & Pick<
|
| 96 |
+
DefinedInitialDataOptions<
|
| 97 |
+
Awaited<ReturnType<typeof getLlm>>,
|
| 98 |
+
TError,
|
| 99 |
+
Awaited<ReturnType<typeof getLlm>>
|
| 100 |
+
> , 'initialData'
|
| 101 |
+
>, request?: SecondParameter<typeof fetchApi>}
|
| 102 |
+
, queryClient?: QueryClient
|
| 103 |
+
): DefinedUseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }
|
| 104 |
+
export function useGetLlm<TData = Awaited<ReturnType<typeof getLlm>>, TError = ApiError>(
|
| 105 |
+
options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof getLlm>>, TError, TData>> & Pick<
|
| 106 |
+
UndefinedInitialDataOptions<
|
| 107 |
+
Awaited<ReturnType<typeof getLlm>>,
|
| 108 |
+
TError,
|
| 109 |
+
Awaited<ReturnType<typeof getLlm>>
|
| 110 |
+
> , 'initialData'
|
| 111 |
+
>, request?: SecondParameter<typeof fetchApi>}
|
| 112 |
+
, queryClient?: QueryClient
|
| 113 |
+
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }
|
| 114 |
+
export function useGetLlm<TData = Awaited<ReturnType<typeof getLlm>>, TError = ApiError>(
|
| 115 |
+
options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof getLlm>>, TError, TData>>, request?: SecondParameter<typeof fetchApi>}
|
| 116 |
+
, queryClient?: QueryClient
|
| 117 |
+
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }
|
| 118 |
+
|
| 119 |
+
export function useGetLlm<TData = Awaited<ReturnType<typeof getLlm>>, TError = ApiError>(
|
| 120 |
+
options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof getLlm>>, TError, TData>>, request?: SecondParameter<typeof fetchApi>}
|
| 121 |
+
, queryClient?: QueryClient
|
| 122 |
+
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> } {
|
| 123 |
+
|
| 124 |
+
const queryOptions = getGetLlmQueryOptions(options)
|
| 125 |
+
|
| 126 |
+
const query = useQuery(queryOptions, queryClient) as UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> };
|
| 127 |
+
|
| 128 |
+
return { ...query, queryKey: queryOptions.queryKey };
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
export const getLoadLlmUrl = () => {
|
| 135 |
+
|
| 136 |
+
|
| 137 |
+
|
| 138 |
+
|
| 139 |
+
return `/api/v1/llm`
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
export const loadLlm = async (llmLoadRequest: LlmLoadRequest, options?: RequestInit): Promise<LlmState> => {
|
| 143 |
+
|
| 144 |
+
return fetchApi<LlmState>(getLoadLlmUrl(),
|
| 145 |
+
{
|
| 146 |
+
...options,
|
| 147 |
+
method: 'PUT',
|
| 148 |
+
headers: { 'Content-Type': 'application/json', ...options?.headers },
|
| 149 |
+
body: JSON.stringify(
|
| 150 |
+
llmLoadRequest,)
|
| 151 |
+
}
|
| 152 |
+
);}
|
| 153 |
+
|
| 154 |
+
|
| 155 |
+
|
| 156 |
+
|
| 157 |
+
export const getLoadLlmMutationOptions = <TError = ApiError,
|
| 158 |
+
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof loadLlm>>, TError,{data: LlmLoadRequest}, TContext>, request?: SecondParameter<typeof fetchApi>}
|
| 159 |
+
): UseMutationOptions<Awaited<ReturnType<typeof loadLlm>>, TError,{data: LlmLoadRequest}, TContext> => {
|
| 160 |
+
|
| 161 |
+
const mutationKey = ['loadLlm'];
|
| 162 |
+
const {mutation: mutationOptions, request: requestOptions} = options ?
|
| 163 |
+
options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey ?
|
| 164 |
+
options
|
| 165 |
+
: {...options, mutation: {...options.mutation, mutationKey}}
|
| 166 |
+
: {mutation: { mutationKey, }, request: undefined};
|
| 167 |
+
|
| 168 |
+
|
| 169 |
+
|
| 170 |
+
|
| 171 |
+
const mutationFn: MutationFunction<Awaited<ReturnType<typeof loadLlm>>, {data: LlmLoadRequest}> = (props) => {
|
| 172 |
+
const {data} = props ?? {};
|
| 173 |
+
|
| 174 |
+
return loadLlm(data,requestOptions)
|
| 175 |
+
}
|
| 176 |
+
|
| 177 |
+
|
| 178 |
+
|
| 179 |
+
|
| 180 |
+
|
| 181 |
+
|
| 182 |
+
return { mutationFn, ...mutationOptions }}
|
| 183 |
+
|
| 184 |
+
export type LoadLlmMutationResult = NonNullable<Awaited<ReturnType<typeof loadLlm>>>
|
| 185 |
+
export type LoadLlmMutationBody = LlmLoadRequest
|
| 186 |
+
export type LoadLlmMutationError = ApiError
|
| 187 |
+
|
| 188 |
+
export const useLoadLlm = <TError = ApiError,
|
| 189 |
+
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof loadLlm>>, TError,{data: LlmLoadRequest}, TContext>, request?: SecondParameter<typeof fetchApi>}
|
| 190 |
+
, queryClient?: QueryClient): UseMutationResult<
|
| 191 |
+
Awaited<ReturnType<typeof loadLlm>>,
|
| 192 |
+
TError,
|
| 193 |
+
{data: LlmLoadRequest},
|
| 194 |
+
TContext
|
| 195 |
+
> => {
|
| 196 |
+
return useMutation(getLoadLlmMutationOptions(options), queryClient);
|
| 197 |
+
}
|
| 198 |
+
export const getUnloadLlmUrl = () => {
|
| 199 |
+
|
| 200 |
+
|
| 201 |
+
|
| 202 |
+
|
| 203 |
+
return `/api/v1/llm`
|
| 204 |
+
}
|
| 205 |
+
|
| 206 |
+
export const unloadLlm = async ( options?: RequestInit): Promise<LlmState> => {
|
| 207 |
+
|
| 208 |
+
return fetchApi<LlmState>(getUnloadLlmUrl(),
|
| 209 |
+
{
|
| 210 |
+
...options,
|
| 211 |
+
method: 'DELETE'
|
| 212 |
+
|
| 213 |
+
|
| 214 |
+
}
|
| 215 |
+
);}
|
| 216 |
+
|
| 217 |
+
|
| 218 |
+
|
| 219 |
+
|
| 220 |
+
export const getUnloadLlmMutationOptions = <TError = ApiError,
|
| 221 |
+
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof unloadLlm>>, TError,void, TContext>, request?: SecondParameter<typeof fetchApi>}
|
| 222 |
+
): UseMutationOptions<Awaited<ReturnType<typeof unloadLlm>>, TError,void, TContext> => {
|
| 223 |
+
|
| 224 |
+
const mutationKey = ['unloadLlm'];
|
| 225 |
+
const {mutation: mutationOptions, request: requestOptions} = options ?
|
| 226 |
+
options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey ?
|
| 227 |
+
options
|
| 228 |
+
: {...options, mutation: {...options.mutation, mutationKey}}
|
| 229 |
+
: {mutation: { mutationKey, }, request: undefined};
|
| 230 |
+
|
| 231 |
+
|
| 232 |
+
|
| 233 |
+
|
| 234 |
+
const mutationFn: MutationFunction<Awaited<ReturnType<typeof unloadLlm>>, void> = () => {
|
| 235 |
+
|
| 236 |
+
|
| 237 |
+
return unloadLlm(requestOptions)
|
| 238 |
+
}
|
| 239 |
+
|
| 240 |
+
|
| 241 |
+
|
| 242 |
+
|
| 243 |
+
|
| 244 |
+
|
| 245 |
+
return { mutationFn, ...mutationOptions }}
|
| 246 |
+
|
| 247 |
+
export type UnloadLlmMutationResult = NonNullable<Awaited<ReturnType<typeof unloadLlm>>>
|
| 248 |
+
|
| 249 |
+
export type UnloadLlmMutationError = ApiError
|
| 250 |
+
|
| 251 |
+
export const useUnloadLlm = <TError = ApiError,
|
| 252 |
+
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof unloadLlm>>, TError,void, TContext>, request?: SecondParameter<typeof fetchApi>}
|
| 253 |
+
, queryClient?: QueryClient): UseMutationResult<
|
| 254 |
+
Awaited<ReturnType<typeof unloadLlm>>,
|
| 255 |
+
TError,
|
| 256 |
+
void,
|
| 257 |
+
TContext
|
| 258 |
+
> => {
|
| 259 |
+
return useMutation(getUnloadLlmMutationOptions(options), queryClient);
|
| 260 |
+
}
|
| 261 |
+
export const getCheckLlmHealthUrl = () => {
|
| 262 |
+
|
| 263 |
+
|
| 264 |
+
|
| 265 |
+
|
| 266 |
+
return `/api/v1/llm/health`
|
| 267 |
+
}
|
| 268 |
+
|
| 269 |
+
export const checkLlmHealth = async (llmPingRequest: LlmPingRequest, options?: RequestInit): Promise<LlmPingResponse> => {
|
| 270 |
+
|
| 271 |
+
return fetchApi<LlmPingResponse>(getCheckLlmHealthUrl(),
|
| 272 |
+
{
|
| 273 |
+
...options,
|
| 274 |
+
method: 'POST',
|
| 275 |
+
headers: { 'Content-Type': 'application/json', ...options?.headers },
|
| 276 |
+
body: JSON.stringify(
|
| 277 |
+
llmPingRequest,)
|
| 278 |
+
}
|
| 279 |
+
);}
|
| 280 |
+
|
| 281 |
+
|
| 282 |
+
|
| 283 |
+
|
| 284 |
+
export const getCheckLlmHealthMutationOptions = <TError = unknown,
|
| 285 |
+
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof checkLlmHealth>>, TError,{data: LlmPingRequest}, TContext>, request?: SecondParameter<typeof fetchApi>}
|
| 286 |
+
): UseMutationOptions<Awaited<ReturnType<typeof checkLlmHealth>>, TError,{data: LlmPingRequest}, TContext> => {
|
| 287 |
+
|
| 288 |
+
const mutationKey = ['checkLlmHealth'];
|
| 289 |
+
const {mutation: mutationOptions, request: requestOptions} = options ?
|
| 290 |
+
options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey ?
|
| 291 |
+
options
|
| 292 |
+
: {...options, mutation: {...options.mutation, mutationKey}}
|
| 293 |
+
: {mutation: { mutationKey, }, request: undefined};
|
| 294 |
+
|
| 295 |
+
|
| 296 |
+
|
| 297 |
+
|
| 298 |
+
const mutationFn: MutationFunction<Awaited<ReturnType<typeof checkLlmHealth>>, {data: LlmPingRequest}> = (props) => {
|
| 299 |
+
const {data} = props ?? {};
|
| 300 |
+
|
| 301 |
+
return checkLlmHealth(data,requestOptions)
|
| 302 |
+
}
|
| 303 |
+
|
| 304 |
+
|
| 305 |
+
|
| 306 |
+
|
| 307 |
+
|
| 308 |
+
|
| 309 |
+
return { mutationFn, ...mutationOptions }}
|
| 310 |
+
|
| 311 |
+
export type CheckLlmHealthMutationResult = NonNullable<Awaited<ReturnType<typeof checkLlmHealth>>>
|
| 312 |
+
export type CheckLlmHealthMutationBody = LlmPingRequest
|
| 313 |
+
export type CheckLlmHealthMutationError = unknown
|
| 314 |
+
|
| 315 |
+
export const useCheckLlmHealth = <TError = unknown,
|
| 316 |
+
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof checkLlmHealth>>, TError,{data: LlmPingRequest}, TContext>, request?: SecondParameter<typeof fetchApi>}
|
| 317 |
+
, queryClient?: QueryClient): UseMutationResult<
|
| 318 |
+
Awaited<ReturnType<typeof checkLlmHealth>>,
|
| 319 |
+
TError,
|
| 320 |
+
{data: LlmPingRequest},
|
| 321 |
+
TContext
|
| 322 |
+
> => {
|
| 323 |
+
return useMutation(getCheckLlmHealthMutationOptions(options), queryClient);
|
| 324 |
+
}
|
| 325 |
+
export const getListLlmModelsUrl = (params?: ListLlmModelsParams,) => {
|
| 326 |
+
const normalizedParams = new URLSearchParams();
|
| 327 |
+
|
| 328 |
+
Object.entries(params || {}).forEach(([key, value]) => {
|
| 329 |
+
|
| 330 |
+
if (value !== undefined) {
|
| 331 |
+
normalizedParams.append(key, value === null ? 'null' : value.toString())
|
| 332 |
+
}
|
| 333 |
+
});
|
| 334 |
+
|
| 335 |
+
const stringifiedParams = normalizedParams.toString();
|
| 336 |
+
|
| 337 |
+
return stringifiedParams.length > 0 ? `/api/v1/llm/models?${stringifiedParams}` : `/api/v1/llm/models`
|
| 338 |
+
}
|
| 339 |
+
|
| 340 |
+
export const listLlmModels = async (params?: ListLlmModelsParams, options?: RequestInit): Promise<LlmModelInfo[]> => {
|
| 341 |
+
|
| 342 |
+
return fetchApi<LlmModelInfo[]>(getListLlmModelsUrl(params),
|
| 343 |
+
{
|
| 344 |
+
...options,
|
| 345 |
+
method: 'GET'
|
| 346 |
+
|
| 347 |
+
|
| 348 |
+
}
|
| 349 |
+
);}
|
| 350 |
+
|
| 351 |
+
|
| 352 |
+
|
| 353 |
+
|
| 354 |
+
|
| 355 |
+
export const getListLlmModelsQueryKey = (params?: ListLlmModelsParams,) => {
|
| 356 |
+
return [
|
| 357 |
+
`/api/v1/llm/models`, ...(params ? [params] : [])
|
| 358 |
+
] as const;
|
| 359 |
+
}
|
| 360 |
+
|
| 361 |
+
|
| 362 |
+
export const getListLlmModelsQueryOptions = <TData = Awaited<ReturnType<typeof listLlmModels>>, TError = ApiError>(params?: ListLlmModelsParams, options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof listLlmModels>>, TError, TData>>, request?: SecondParameter<typeof fetchApi>}
|
| 363 |
+
) => {
|
| 364 |
+
|
| 365 |
+
const {query: queryOptions, request: requestOptions} = options ?? {};
|
| 366 |
+
|
| 367 |
+
const queryKey = queryOptions?.queryKey ?? getListLlmModelsQueryKey(params);
|
| 368 |
+
|
| 369 |
+
|
| 370 |
+
|
| 371 |
+
const queryFn: QueryFunction<Awaited<ReturnType<typeof listLlmModels>>> = ({ signal }) => listLlmModels(params, { signal, ...requestOptions });
|
| 372 |
+
|
| 373 |
+
|
| 374 |
+
|
| 375 |
+
|
| 376 |
+
|
| 377 |
+
return { queryKey, queryFn, gcTime: 300000, retry: 1, ...queryOptions} as UseQueryOptions<Awaited<ReturnType<typeof listLlmModels>>, TError, TData> & { queryKey: DataTag<QueryKey, TData, TError> }
|
| 378 |
+
}
|
| 379 |
+
|
| 380 |
+
export type ListLlmModelsQueryResult = NonNullable<Awaited<ReturnType<typeof listLlmModels>>>
|
| 381 |
+
export type ListLlmModelsQueryError = ApiError
|
| 382 |
+
|
| 383 |
+
|
| 384 |
+
export function useListLlmModels<TData = Awaited<ReturnType<typeof listLlmModels>>, TError = ApiError>(
|
| 385 |
+
params: undefined | ListLlmModelsParams, options: { query:Partial<UseQueryOptions<Awaited<ReturnType<typeof listLlmModels>>, TError, TData>> & Pick<
|
| 386 |
+
DefinedInitialDataOptions<
|
| 387 |
+
Awaited<ReturnType<typeof listLlmModels>>,
|
| 388 |
+
TError,
|
| 389 |
+
Awaited<ReturnType<typeof listLlmModels>>
|
| 390 |
+
> , 'initialData'
|
| 391 |
+
>, request?: SecondParameter<typeof fetchApi>}
|
| 392 |
+
, queryClient?: QueryClient
|
| 393 |
+
): DefinedUseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }
|
| 394 |
+
export function useListLlmModels<TData = Awaited<ReturnType<typeof listLlmModels>>, TError = ApiError>(
|
| 395 |
+
params?: ListLlmModelsParams, options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof listLlmModels>>, TError, TData>> & Pick<
|
| 396 |
+
UndefinedInitialDataOptions<
|
| 397 |
+
Awaited<ReturnType<typeof listLlmModels>>,
|
| 398 |
+
TError,
|
| 399 |
+
Awaited<ReturnType<typeof listLlmModels>>
|
| 400 |
+
> , 'initialData'
|
| 401 |
+
>, request?: SecondParameter<typeof fetchApi>}
|
| 402 |
+
, queryClient?: QueryClient
|
| 403 |
+
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }
|
| 404 |
+
export function useListLlmModels<TData = Awaited<ReturnType<typeof listLlmModels>>, TError = ApiError>(
|
| 405 |
+
params?: ListLlmModelsParams, options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof listLlmModels>>, TError, TData>>, request?: SecondParameter<typeof fetchApi>}
|
| 406 |
+
, queryClient?: QueryClient
|
| 407 |
+
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }
|
| 408 |
+
|
| 409 |
+
export function useListLlmModels<TData = Awaited<ReturnType<typeof listLlmModels>>, TError = ApiError>(
|
| 410 |
+
params?: ListLlmModelsParams, options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof listLlmModels>>, TError, TData>>, request?: SecondParameter<typeof fetchApi>}
|
| 411 |
+
, queryClient?: QueryClient
|
| 412 |
+
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> } {
|
| 413 |
+
|
| 414 |
+
const queryOptions = getListLlmModelsQueryOptions(params,options)
|
| 415 |
+
|
| 416 |
+
const query = useQuery(queryOptions, queryClient) as UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> };
|
| 417 |
+
|
| 418 |
+
return { ...query, queryKey: queryOptions.queryKey };
|
| 419 |
+
}
|
| 420 |
+
|
| 421 |
+
|
| 422 |
+
|
| 423 |
+
|
ui/lib/api/processing/processing.ts
ADDED
|
@@ -0,0 +1,347 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Generated by orval v8.6.2 🍺
|
| 3 |
+
* Do not edit manually.
|
| 4 |
+
* OpenAPI spec version: 0.0.1
|
| 5 |
+
*/
|
| 6 |
+
import {
|
| 7 |
+
useMutation
|
| 8 |
+
} from '@tanstack/react-query';
|
| 9 |
+
import type {
|
| 10 |
+
MutationFunction,
|
| 11 |
+
QueryClient,
|
| 12 |
+
UseMutationOptions,
|
| 13 |
+
UseMutationResult
|
| 14 |
+
} from '@tanstack/react-query';
|
| 15 |
+
|
| 16 |
+
import type {
|
| 17 |
+
ApiError,
|
| 18 |
+
RenderRequest,
|
| 19 |
+
TranslateRequest
|
| 20 |
+
} from '../schemas';
|
| 21 |
+
|
| 22 |
+
import { fetchApi } from '.././fetch';
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
type SecondParameter<T extends (...args: never) => unknown> = Parameters<T>[1];
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
export const getDetectDocumentUrl = (documentId: string,) => {
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
return `/api/v1/documents/${documentId}/detect`
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
export const detectDocument = async (documentId: string, options?: RequestInit): Promise<void> => {
|
| 38 |
+
|
| 39 |
+
return fetchApi<void>(getDetectDocumentUrl(documentId),
|
| 40 |
+
{
|
| 41 |
+
...options,
|
| 42 |
+
method: 'POST'
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
}
|
| 46 |
+
);}
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
export const getDetectDocumentMutationOptions = <TError = ApiError,
|
| 52 |
+
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof detectDocument>>, TError,{documentId: string}, TContext>, request?: SecondParameter<typeof fetchApi>}
|
| 53 |
+
): UseMutationOptions<Awaited<ReturnType<typeof detectDocument>>, TError,{documentId: string}, TContext> => {
|
| 54 |
+
|
| 55 |
+
const mutationKey = ['detectDocument'];
|
| 56 |
+
const {mutation: mutationOptions, request: requestOptions} = options ?
|
| 57 |
+
options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey ?
|
| 58 |
+
options
|
| 59 |
+
: {...options, mutation: {...options.mutation, mutationKey}}
|
| 60 |
+
: {mutation: { mutationKey, }, request: undefined};
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
const mutationFn: MutationFunction<Awaited<ReturnType<typeof detectDocument>>, {documentId: string}> = (props) => {
|
| 66 |
+
const {documentId} = props ?? {};
|
| 67 |
+
|
| 68 |
+
return detectDocument(documentId,requestOptions)
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
return { mutationFn, ...mutationOptions }}
|
| 77 |
+
|
| 78 |
+
export type DetectDocumentMutationResult = NonNullable<Awaited<ReturnType<typeof detectDocument>>>
|
| 79 |
+
|
| 80 |
+
export type DetectDocumentMutationError = ApiError
|
| 81 |
+
|
| 82 |
+
export const useDetectDocument = <TError = ApiError,
|
| 83 |
+
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof detectDocument>>, TError,{documentId: string}, TContext>, request?: SecondParameter<typeof fetchApi>}
|
| 84 |
+
, queryClient?: QueryClient): UseMutationResult<
|
| 85 |
+
Awaited<ReturnType<typeof detectDocument>>,
|
| 86 |
+
TError,
|
| 87 |
+
{documentId: string},
|
| 88 |
+
TContext
|
| 89 |
+
> => {
|
| 90 |
+
return useMutation(getDetectDocumentMutationOptions(options), queryClient);
|
| 91 |
+
}
|
| 92 |
+
export const getInpaintDocumentUrl = (documentId: string,) => {
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
return `/api/v1/documents/${documentId}/inpaint`
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
export const inpaintDocument = async (documentId: string, options?: RequestInit): Promise<void> => {
|
| 101 |
+
|
| 102 |
+
return fetchApi<void>(getInpaintDocumentUrl(documentId),
|
| 103 |
+
{
|
| 104 |
+
...options,
|
| 105 |
+
method: 'POST'
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
}
|
| 109 |
+
);}
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
export const getInpaintDocumentMutationOptions = <TError = ApiError,
|
| 115 |
+
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof inpaintDocument>>, TError,{documentId: string}, TContext>, request?: SecondParameter<typeof fetchApi>}
|
| 116 |
+
): UseMutationOptions<Awaited<ReturnType<typeof inpaintDocument>>, TError,{documentId: string}, TContext> => {
|
| 117 |
+
|
| 118 |
+
const mutationKey = ['inpaintDocument'];
|
| 119 |
+
const {mutation: mutationOptions, request: requestOptions} = options ?
|
| 120 |
+
options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey ?
|
| 121 |
+
options
|
| 122 |
+
: {...options, mutation: {...options.mutation, mutationKey}}
|
| 123 |
+
: {mutation: { mutationKey, }, request: undefined};
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
const mutationFn: MutationFunction<Awaited<ReturnType<typeof inpaintDocument>>, {documentId: string}> = (props) => {
|
| 129 |
+
const {documentId} = props ?? {};
|
| 130 |
+
|
| 131 |
+
return inpaintDocument(documentId,requestOptions)
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
|
| 137 |
+
|
| 138 |
+
|
| 139 |
+
return { mutationFn, ...mutationOptions }}
|
| 140 |
+
|
| 141 |
+
export type InpaintDocumentMutationResult = NonNullable<Awaited<ReturnType<typeof inpaintDocument>>>
|
| 142 |
+
|
| 143 |
+
export type InpaintDocumentMutationError = ApiError
|
| 144 |
+
|
| 145 |
+
export const useInpaintDocument = <TError = ApiError,
|
| 146 |
+
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof inpaintDocument>>, TError,{documentId: string}, TContext>, request?: SecondParameter<typeof fetchApi>}
|
| 147 |
+
, queryClient?: QueryClient): UseMutationResult<
|
| 148 |
+
Awaited<ReturnType<typeof inpaintDocument>>,
|
| 149 |
+
TError,
|
| 150 |
+
{documentId: string},
|
| 151 |
+
TContext
|
| 152 |
+
> => {
|
| 153 |
+
return useMutation(getInpaintDocumentMutationOptions(options), queryClient);
|
| 154 |
+
}
|
| 155 |
+
export const getRecognizeDocumentUrl = (documentId: string,) => {
|
| 156 |
+
|
| 157 |
+
|
| 158 |
+
|
| 159 |
+
|
| 160 |
+
return `/api/v1/documents/${documentId}/recognize`
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
export const recognizeDocument = async (documentId: string, options?: RequestInit): Promise<void> => {
|
| 164 |
+
|
| 165 |
+
return fetchApi<void>(getRecognizeDocumentUrl(documentId),
|
| 166 |
+
{
|
| 167 |
+
...options,
|
| 168 |
+
method: 'POST'
|
| 169 |
+
|
| 170 |
+
|
| 171 |
+
}
|
| 172 |
+
);}
|
| 173 |
+
|
| 174 |
+
|
| 175 |
+
|
| 176 |
+
|
| 177 |
+
export const getRecognizeDocumentMutationOptions = <TError = ApiError,
|
| 178 |
+
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof recognizeDocument>>, TError,{documentId: string}, TContext>, request?: SecondParameter<typeof fetchApi>}
|
| 179 |
+
): UseMutationOptions<Awaited<ReturnType<typeof recognizeDocument>>, TError,{documentId: string}, TContext> => {
|
| 180 |
+
|
| 181 |
+
const mutationKey = ['recognizeDocument'];
|
| 182 |
+
const {mutation: mutationOptions, request: requestOptions} = options ?
|
| 183 |
+
options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey ?
|
| 184 |
+
options
|
| 185 |
+
: {...options, mutation: {...options.mutation, mutationKey}}
|
| 186 |
+
: {mutation: { mutationKey, }, request: undefined};
|
| 187 |
+
|
| 188 |
+
|
| 189 |
+
|
| 190 |
+
|
| 191 |
+
const mutationFn: MutationFunction<Awaited<ReturnType<typeof recognizeDocument>>, {documentId: string}> = (props) => {
|
| 192 |
+
const {documentId} = props ?? {};
|
| 193 |
+
|
| 194 |
+
return recognizeDocument(documentId,requestOptions)
|
| 195 |
+
}
|
| 196 |
+
|
| 197 |
+
|
| 198 |
+
|
| 199 |
+
|
| 200 |
+
|
| 201 |
+
|
| 202 |
+
return { mutationFn, ...mutationOptions }}
|
| 203 |
+
|
| 204 |
+
export type RecognizeDocumentMutationResult = NonNullable<Awaited<ReturnType<typeof recognizeDocument>>>
|
| 205 |
+
|
| 206 |
+
export type RecognizeDocumentMutationError = ApiError
|
| 207 |
+
|
| 208 |
+
export const useRecognizeDocument = <TError = ApiError,
|
| 209 |
+
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof recognizeDocument>>, TError,{documentId: string}, TContext>, request?: SecondParameter<typeof fetchApi>}
|
| 210 |
+
, queryClient?: QueryClient): UseMutationResult<
|
| 211 |
+
Awaited<ReturnType<typeof recognizeDocument>>,
|
| 212 |
+
TError,
|
| 213 |
+
{documentId: string},
|
| 214 |
+
TContext
|
| 215 |
+
> => {
|
| 216 |
+
return useMutation(getRecognizeDocumentMutationOptions(options), queryClient);
|
| 217 |
+
}
|
| 218 |
+
export const getRenderDocumentUrl = (documentId: string,) => {
|
| 219 |
+
|
| 220 |
+
|
| 221 |
+
|
| 222 |
+
|
| 223 |
+
return `/api/v1/documents/${documentId}/render`
|
| 224 |
+
}
|
| 225 |
+
|
| 226 |
+
export const renderDocument = async (documentId: string,
|
| 227 |
+
renderRequest: RenderRequest, options?: RequestInit): Promise<void> => {
|
| 228 |
+
|
| 229 |
+
return fetchApi<void>(getRenderDocumentUrl(documentId),
|
| 230 |
+
{
|
| 231 |
+
...options,
|
| 232 |
+
method: 'POST',
|
| 233 |
+
headers: { 'Content-Type': 'application/json', ...options?.headers },
|
| 234 |
+
body: JSON.stringify(
|
| 235 |
+
renderRequest,)
|
| 236 |
+
}
|
| 237 |
+
);}
|
| 238 |
+
|
| 239 |
+
|
| 240 |
+
|
| 241 |
+
|
| 242 |
+
export const getRenderDocumentMutationOptions = <TError = ApiError,
|
| 243 |
+
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof renderDocument>>, TError,{documentId: string;data: RenderRequest}, TContext>, request?: SecondParameter<typeof fetchApi>}
|
| 244 |
+
): UseMutationOptions<Awaited<ReturnType<typeof renderDocument>>, TError,{documentId: string;data: RenderRequest}, TContext> => {
|
| 245 |
+
|
| 246 |
+
const mutationKey = ['renderDocument'];
|
| 247 |
+
const {mutation: mutationOptions, request: requestOptions} = options ?
|
| 248 |
+
options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey ?
|
| 249 |
+
options
|
| 250 |
+
: {...options, mutation: {...options.mutation, mutationKey}}
|
| 251 |
+
: {mutation: { mutationKey, }, request: undefined};
|
| 252 |
+
|
| 253 |
+
|
| 254 |
+
|
| 255 |
+
|
| 256 |
+
const mutationFn: MutationFunction<Awaited<ReturnType<typeof renderDocument>>, {documentId: string;data: RenderRequest}> = (props) => {
|
| 257 |
+
const {documentId,data} = props ?? {};
|
| 258 |
+
|
| 259 |
+
return renderDocument(documentId,data,requestOptions)
|
| 260 |
+
}
|
| 261 |
+
|
| 262 |
+
|
| 263 |
+
|
| 264 |
+
|
| 265 |
+
|
| 266 |
+
|
| 267 |
+
return { mutationFn, ...mutationOptions }}
|
| 268 |
+
|
| 269 |
+
export type RenderDocumentMutationResult = NonNullable<Awaited<ReturnType<typeof renderDocument>>>
|
| 270 |
+
export type RenderDocumentMutationBody = RenderRequest
|
| 271 |
+
export type RenderDocumentMutationError = ApiError
|
| 272 |
+
|
| 273 |
+
export const useRenderDocument = <TError = ApiError,
|
| 274 |
+
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof renderDocument>>, TError,{documentId: string;data: RenderRequest}, TContext>, request?: SecondParameter<typeof fetchApi>}
|
| 275 |
+
, queryClient?: QueryClient): UseMutationResult<
|
| 276 |
+
Awaited<ReturnType<typeof renderDocument>>,
|
| 277 |
+
TError,
|
| 278 |
+
{documentId: string;data: RenderRequest},
|
| 279 |
+
TContext
|
| 280 |
+
> => {
|
| 281 |
+
return useMutation(getRenderDocumentMutationOptions(options), queryClient);
|
| 282 |
+
}
|
| 283 |
+
export const getTranslateDocumentUrl = (documentId: string,) => {
|
| 284 |
+
|
| 285 |
+
|
| 286 |
+
|
| 287 |
+
|
| 288 |
+
return `/api/v1/documents/${documentId}/translate`
|
| 289 |
+
}
|
| 290 |
+
|
| 291 |
+
export const translateDocument = async (documentId: string,
|
| 292 |
+
translateRequest: TranslateRequest, options?: RequestInit): Promise<void> => {
|
| 293 |
+
|
| 294 |
+
return fetchApi<void>(getTranslateDocumentUrl(documentId),
|
| 295 |
+
{
|
| 296 |
+
...options,
|
| 297 |
+
method: 'POST',
|
| 298 |
+
headers: { 'Content-Type': 'application/json', ...options?.headers },
|
| 299 |
+
body: JSON.stringify(
|
| 300 |
+
translateRequest,)
|
| 301 |
+
}
|
| 302 |
+
);}
|
| 303 |
+
|
| 304 |
+
|
| 305 |
+
|
| 306 |
+
|
| 307 |
+
export const getTranslateDocumentMutationOptions = <TError = ApiError,
|
| 308 |
+
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof translateDocument>>, TError,{documentId: string;data: TranslateRequest}, TContext>, request?: SecondParameter<typeof fetchApi>}
|
| 309 |
+
): UseMutationOptions<Awaited<ReturnType<typeof translateDocument>>, TError,{documentId: string;data: TranslateRequest}, TContext> => {
|
| 310 |
+
|
| 311 |
+
const mutationKey = ['translateDocument'];
|
| 312 |
+
const {mutation: mutationOptions, request: requestOptions} = options ?
|
| 313 |
+
options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey ?
|
| 314 |
+
options
|
| 315 |
+
: {...options, mutation: {...options.mutation, mutationKey}}
|
| 316 |
+
: {mutation: { mutationKey, }, request: undefined};
|
| 317 |
+
|
| 318 |
+
|
| 319 |
+
|
| 320 |
+
|
| 321 |
+
const mutationFn: MutationFunction<Awaited<ReturnType<typeof translateDocument>>, {documentId: string;data: TranslateRequest}> = (props) => {
|
| 322 |
+
const {documentId,data} = props ?? {};
|
| 323 |
+
|
| 324 |
+
return translateDocument(documentId,data,requestOptions)
|
| 325 |
+
}
|
| 326 |
+
|
| 327 |
+
|
| 328 |
+
|
| 329 |
+
|
| 330 |
+
|
| 331 |
+
|
| 332 |
+
return { mutationFn, ...mutationOptions }}
|
| 333 |
+
|
| 334 |
+
export type TranslateDocumentMutationResult = NonNullable<Awaited<ReturnType<typeof translateDocument>>>
|
| 335 |
+
export type TranslateDocumentMutationBody = TranslateRequest
|
| 336 |
+
export type TranslateDocumentMutationError = ApiError
|
| 337 |
+
|
| 338 |
+
export const useTranslateDocument = <TError = ApiError,
|
| 339 |
+
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof translateDocument>>, TError,{documentId: string;data: TranslateRequest}, TContext>, request?: SecondParameter<typeof fetchApi>}
|
| 340 |
+
, queryClient?: QueryClient): UseMutationResult<
|
| 341 |
+
Awaited<ReturnType<typeof translateDocument>>,
|
| 342 |
+
TError,
|
| 343 |
+
{documentId: string;data: TranslateRequest},
|
| 344 |
+
TContext
|
| 345 |
+
> => {
|
| 346 |
+
return useMutation(getTranslateDocumentMutationOptions(options), queryClient);
|
| 347 |
+
}
|
ui/lib/api/providers/providers.ts
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Generated by orval v8.6.2 🍺
|
| 3 |
+
* Do not edit manually.
|
| 4 |
+
* OpenAPI spec version: 0.0.1
|
| 5 |
+
*/
|
| 6 |
+
import {
|
| 7 |
+
useMutation,
|
| 8 |
+
useQuery
|
| 9 |
+
} from '@tanstack/react-query';
|
| 10 |
+
import type {
|
| 11 |
+
DataTag,
|
| 12 |
+
DefinedInitialDataOptions,
|
| 13 |
+
DefinedUseQueryResult,
|
| 14 |
+
MutationFunction,
|
| 15 |
+
QueryClient,
|
| 16 |
+
QueryFunction,
|
| 17 |
+
QueryKey,
|
| 18 |
+
UndefinedInitialDataOptions,
|
| 19 |
+
UseMutationOptions,
|
| 20 |
+
UseMutationResult,
|
| 21 |
+
UseQueryOptions,
|
| 22 |
+
UseQueryResult
|
| 23 |
+
} from '@tanstack/react-query';
|
| 24 |
+
|
| 25 |
+
import type {
|
| 26 |
+
ApiError,
|
| 27 |
+
ApiKeyResponse,
|
| 28 |
+
ApiKeyValue
|
| 29 |
+
} from '../schemas';
|
| 30 |
+
|
| 31 |
+
import { fetchApi } from '.././fetch';
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
type SecondParameter<T extends (...args: never) => unknown> = Parameters<T>[1];
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
export const getGetApiKeyUrl = (provider: string,) => {
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
return `/api/v1/providers/${provider}/api-key`
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
export const getApiKey = async (provider: string, options?: RequestInit): Promise<ApiKeyResponse> => {
|
| 47 |
+
|
| 48 |
+
return fetchApi<ApiKeyResponse>(getGetApiKeyUrl(provider),
|
| 49 |
+
{
|
| 50 |
+
...options,
|
| 51 |
+
method: 'GET'
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
}
|
| 55 |
+
);}
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
export const getGetApiKeyQueryKey = (provider: string,) => {
|
| 62 |
+
return [
|
| 63 |
+
`/api/v1/providers/${provider}/api-key`
|
| 64 |
+
] as const;
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
export const getGetApiKeyQueryOptions = <TData = Awaited<ReturnType<typeof getApiKey>>, TError = ApiError>(provider: string, options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof getApiKey>>, TError, TData>>, request?: SecondParameter<typeof fetchApi>}
|
| 69 |
+
) => {
|
| 70 |
+
|
| 71 |
+
const {query: queryOptions, request: requestOptions} = options ?? {};
|
| 72 |
+
|
| 73 |
+
const queryKey = queryOptions?.queryKey ?? getGetApiKeyQueryKey(provider);
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
const queryFn: QueryFunction<Awaited<ReturnType<typeof getApiKey>>> = ({ signal }) => getApiKey(provider, { signal, ...requestOptions });
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
return { queryKey, queryFn, enabled: !!(provider), gcTime: 300000, retry: 1, ...queryOptions} as UseQueryOptions<Awaited<ReturnType<typeof getApiKey>>, TError, TData> & { queryKey: DataTag<QueryKey, TData, TError> }
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
export type GetApiKeyQueryResult = NonNullable<Awaited<ReturnType<typeof getApiKey>>>
|
| 87 |
+
export type GetApiKeyQueryError = ApiError
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
export function useGetApiKey<TData = Awaited<ReturnType<typeof getApiKey>>, TError = ApiError>(
|
| 91 |
+
provider: string, options: { query:Partial<UseQueryOptions<Awaited<ReturnType<typeof getApiKey>>, TError, TData>> & Pick<
|
| 92 |
+
DefinedInitialDataOptions<
|
| 93 |
+
Awaited<ReturnType<typeof getApiKey>>,
|
| 94 |
+
TError,
|
| 95 |
+
Awaited<ReturnType<typeof getApiKey>>
|
| 96 |
+
> , 'initialData'
|
| 97 |
+
>, request?: SecondParameter<typeof fetchApi>}
|
| 98 |
+
, queryClient?: QueryClient
|
| 99 |
+
): DefinedUseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }
|
| 100 |
+
export function useGetApiKey<TData = Awaited<ReturnType<typeof getApiKey>>, TError = ApiError>(
|
| 101 |
+
provider: string, options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof getApiKey>>, TError, TData>> & Pick<
|
| 102 |
+
UndefinedInitialDataOptions<
|
| 103 |
+
Awaited<ReturnType<typeof getApiKey>>,
|
| 104 |
+
TError,
|
| 105 |
+
Awaited<ReturnType<typeof getApiKey>>
|
| 106 |
+
> , 'initialData'
|
| 107 |
+
>, request?: SecondParameter<typeof fetchApi>}
|
| 108 |
+
, queryClient?: QueryClient
|
| 109 |
+
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }
|
| 110 |
+
export function useGetApiKey<TData = Awaited<ReturnType<typeof getApiKey>>, TError = ApiError>(
|
| 111 |
+
provider: string, options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof getApiKey>>, TError, TData>>, request?: SecondParameter<typeof fetchApi>}
|
| 112 |
+
, queryClient?: QueryClient
|
| 113 |
+
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }
|
| 114 |
+
|
| 115 |
+
export function useGetApiKey<TData = Awaited<ReturnType<typeof getApiKey>>, TError = ApiError>(
|
| 116 |
+
provider: string, options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof getApiKey>>, TError, TData>>, request?: SecondParameter<typeof fetchApi>}
|
| 117 |
+
, queryClient?: QueryClient
|
| 118 |
+
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> } {
|
| 119 |
+
|
| 120 |
+
const queryOptions = getGetApiKeyQueryOptions(provider,options)
|
| 121 |
+
|
| 122 |
+
const query = useQuery(queryOptions, queryClient) as UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> };
|
| 123 |
+
|
| 124 |
+
return { ...query, queryKey: queryOptions.queryKey };
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
|
| 129 |
+
|
| 130 |
+
export const getSetApiKeyUrl = (provider: string,) => {
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
return `/api/v1/providers/${provider}/api-key`
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
export const setApiKey = async (provider: string,
|
| 139 |
+
apiKeyValue: ApiKeyValue, options?: RequestInit): Promise<void> => {
|
| 140 |
+
|
| 141 |
+
return fetchApi<void>(getSetApiKeyUrl(provider),
|
| 142 |
+
{
|
| 143 |
+
...options,
|
| 144 |
+
method: 'PUT',
|
| 145 |
+
headers: { 'Content-Type': 'application/json', ...options?.headers },
|
| 146 |
+
body: JSON.stringify(
|
| 147 |
+
apiKeyValue,)
|
| 148 |
+
}
|
| 149 |
+
);}
|
| 150 |
+
|
| 151 |
+
|
| 152 |
+
|
| 153 |
+
|
| 154 |
+
export const getSetApiKeyMutationOptions = <TError = ApiError,
|
| 155 |
+
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof setApiKey>>, TError,{provider: string;data: ApiKeyValue}, TContext>, request?: SecondParameter<typeof fetchApi>}
|
| 156 |
+
): UseMutationOptions<Awaited<ReturnType<typeof setApiKey>>, TError,{provider: string;data: ApiKeyValue}, TContext> => {
|
| 157 |
+
|
| 158 |
+
const mutationKey = ['setApiKey'];
|
| 159 |
+
const {mutation: mutationOptions, request: requestOptions} = options ?
|
| 160 |
+
options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey ?
|
| 161 |
+
options
|
| 162 |
+
: {...options, mutation: {...options.mutation, mutationKey}}
|
| 163 |
+
: {mutation: { mutationKey, }, request: undefined};
|
| 164 |
+
|
| 165 |
+
|
| 166 |
+
|
| 167 |
+
|
| 168 |
+
const mutationFn: MutationFunction<Awaited<ReturnType<typeof setApiKey>>, {provider: string;data: ApiKeyValue}> = (props) => {
|
| 169 |
+
const {provider,data} = props ?? {};
|
| 170 |
+
|
| 171 |
+
return setApiKey(provider,data,requestOptions)
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
+
|
| 175 |
+
|
| 176 |
+
|
| 177 |
+
|
| 178 |
+
|
| 179 |
+
return { mutationFn, ...mutationOptions }}
|
| 180 |
+
|
| 181 |
+
export type SetApiKeyMutationResult = NonNullable<Awaited<ReturnType<typeof setApiKey>>>
|
| 182 |
+
export type SetApiKeyMutationBody = ApiKeyValue
|
| 183 |
+
export type SetApiKeyMutationError = ApiError
|
| 184 |
+
|
| 185 |
+
export const useSetApiKey = <TError = ApiError,
|
| 186 |
+
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof setApiKey>>, TError,{provider: string;data: ApiKeyValue}, TContext>, request?: SecondParameter<typeof fetchApi>}
|
| 187 |
+
, queryClient?: QueryClient): UseMutationResult<
|
| 188 |
+
Awaited<ReturnType<typeof setApiKey>>,
|
| 189 |
+
TError,
|
| 190 |
+
{provider: string;data: ApiKeyValue},
|
| 191 |
+
TContext
|
| 192 |
+
> => {
|
| 193 |
+
return useMutation(getSetApiKeyMutationOptions(options), queryClient);
|
| 194 |
+
}
|
ui/lib/api/regions/regions.ts
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Generated by orval v8.6.2 🍺
|
| 3 |
+
* Do not edit manually.
|
| 4 |
+
* OpenAPI spec version: 0.0.1
|
| 5 |
+
*/
|
| 6 |
+
import {
|
| 7 |
+
useMutation
|
| 8 |
+
} from '@tanstack/react-query';
|
| 9 |
+
import type {
|
| 10 |
+
MutationFunction,
|
| 11 |
+
QueryClient,
|
| 12 |
+
UseMutationOptions,
|
| 13 |
+
UseMutationResult
|
| 14 |
+
} from '@tanstack/react-query';
|
| 15 |
+
|
| 16 |
+
import type {
|
| 17 |
+
ApiError,
|
| 18 |
+
BrushRegionRequest,
|
| 19 |
+
InpaintRegionRequest,
|
| 20 |
+
MaskRegionRequest
|
| 21 |
+
} from '../schemas';
|
| 22 |
+
|
| 23 |
+
import { fetchApi } from '.././fetch';
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
type SecondParameter<T extends (...args: never) => unknown> = Parameters<T>[1];
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
export const getUpdateBrushLayerUrl = (documentId: string,) => {
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
return `/api/v1/documents/${documentId}/brush-layer`
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
export const updateBrushLayer = async (documentId: string,
|
| 39 |
+
brushRegionRequest: BrushRegionRequest, options?: RequestInit): Promise<void> => {
|
| 40 |
+
|
| 41 |
+
return fetchApi<void>(getUpdateBrushLayerUrl(documentId),
|
| 42 |
+
{
|
| 43 |
+
...options,
|
| 44 |
+
method: 'PUT',
|
| 45 |
+
headers: { 'Content-Type': 'application/json', ...options?.headers },
|
| 46 |
+
body: JSON.stringify(
|
| 47 |
+
brushRegionRequest,)
|
| 48 |
+
}
|
| 49 |
+
);}
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
export const getUpdateBrushLayerMutationOptions = <TError = ApiError,
|
| 55 |
+
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof updateBrushLayer>>, TError,{documentId: string;data: BrushRegionRequest}, TContext>, request?: SecondParameter<typeof fetchApi>}
|
| 56 |
+
): UseMutationOptions<Awaited<ReturnType<typeof updateBrushLayer>>, TError,{documentId: string;data: BrushRegionRequest}, TContext> => {
|
| 57 |
+
|
| 58 |
+
const mutationKey = ['updateBrushLayer'];
|
| 59 |
+
const {mutation: mutationOptions, request: requestOptions} = options ?
|
| 60 |
+
options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey ?
|
| 61 |
+
options
|
| 62 |
+
: {...options, mutation: {...options.mutation, mutationKey}}
|
| 63 |
+
: {mutation: { mutationKey, }, request: undefined};
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
const mutationFn: MutationFunction<Awaited<ReturnType<typeof updateBrushLayer>>, {documentId: string;data: BrushRegionRequest}> = (props) => {
|
| 69 |
+
const {documentId,data} = props ?? {};
|
| 70 |
+
|
| 71 |
+
return updateBrushLayer(documentId,data,requestOptions)
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
return { mutationFn, ...mutationOptions }}
|
| 80 |
+
|
| 81 |
+
export type UpdateBrushLayerMutationResult = NonNullable<Awaited<ReturnType<typeof updateBrushLayer>>>
|
| 82 |
+
export type UpdateBrushLayerMutationBody = BrushRegionRequest
|
| 83 |
+
export type UpdateBrushLayerMutationError = ApiError
|
| 84 |
+
|
| 85 |
+
export const useUpdateBrushLayer = <TError = ApiError,
|
| 86 |
+
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof updateBrushLayer>>, TError,{documentId: string;data: BrushRegionRequest}, TContext>, request?: SecondParameter<typeof fetchApi>}
|
| 87 |
+
, queryClient?: QueryClient): UseMutationResult<
|
| 88 |
+
Awaited<ReturnType<typeof updateBrushLayer>>,
|
| 89 |
+
TError,
|
| 90 |
+
{documentId: string;data: BrushRegionRequest},
|
| 91 |
+
TContext
|
| 92 |
+
> => {
|
| 93 |
+
return useMutation(getUpdateBrushLayerMutationOptions(options), queryClient);
|
| 94 |
+
}
|
| 95 |
+
export const getInpaintRegionUrl = (documentId: string,) => {
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
return `/api/v1/documents/${documentId}/inpaint-region`
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
export const inpaintRegion = async (documentId: string,
|
| 104 |
+
inpaintRegionRequest: InpaintRegionRequest, options?: RequestInit): Promise<void> => {
|
| 105 |
+
|
| 106 |
+
return fetchApi<void>(getInpaintRegionUrl(documentId),
|
| 107 |
+
{
|
| 108 |
+
...options,
|
| 109 |
+
method: 'POST',
|
| 110 |
+
headers: { 'Content-Type': 'application/json', ...options?.headers },
|
| 111 |
+
body: JSON.stringify(
|
| 112 |
+
inpaintRegionRequest,)
|
| 113 |
+
}
|
| 114 |
+
);}
|
| 115 |
+
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
export const getInpaintRegionMutationOptions = <TError = ApiError,
|
| 120 |
+
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof inpaintRegion>>, TError,{documentId: string;data: InpaintRegionRequest}, TContext>, request?: SecondParameter<typeof fetchApi>}
|
| 121 |
+
): UseMutationOptions<Awaited<ReturnType<typeof inpaintRegion>>, TError,{documentId: string;data: InpaintRegionRequest}, TContext> => {
|
| 122 |
+
|
| 123 |
+
const mutationKey = ['inpaintRegion'];
|
| 124 |
+
const {mutation: mutationOptions, request: requestOptions} = options ?
|
| 125 |
+
options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey ?
|
| 126 |
+
options
|
| 127 |
+
: {...options, mutation: {...options.mutation, mutationKey}}
|
| 128 |
+
: {mutation: { mutationKey, }, request: undefined};
|
| 129 |
+
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
const mutationFn: MutationFunction<Awaited<ReturnType<typeof inpaintRegion>>, {documentId: string;data: InpaintRegionRequest}> = (props) => {
|
| 134 |
+
const {documentId,data} = props ?? {};
|
| 135 |
+
|
| 136 |
+
return inpaintRegion(documentId,data,requestOptions)
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
|
| 141 |
+
|
| 142 |
+
|
| 143 |
+
|
| 144 |
+
return { mutationFn, ...mutationOptions }}
|
| 145 |
+
|
| 146 |
+
export type InpaintRegionMutationResult = NonNullable<Awaited<ReturnType<typeof inpaintRegion>>>
|
| 147 |
+
export type InpaintRegionMutationBody = InpaintRegionRequest
|
| 148 |
+
export type InpaintRegionMutationError = ApiError
|
| 149 |
+
|
| 150 |
+
export const useInpaintRegion = <TError = ApiError,
|
| 151 |
+
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof inpaintRegion>>, TError,{documentId: string;data: InpaintRegionRequest}, TContext>, request?: SecondParameter<typeof fetchApi>}
|
| 152 |
+
, queryClient?: QueryClient): UseMutationResult<
|
| 153 |
+
Awaited<ReturnType<typeof inpaintRegion>>,
|
| 154 |
+
TError,
|
| 155 |
+
{documentId: string;data: InpaintRegionRequest},
|
| 156 |
+
TContext
|
| 157 |
+
> => {
|
| 158 |
+
return useMutation(getInpaintRegionMutationOptions(options), queryClient);
|
| 159 |
+
}
|
| 160 |
+
export const getUpdateMaskUrl = (documentId: string,) => {
|
| 161 |
+
|
| 162 |
+
|
| 163 |
+
|
| 164 |
+
|
| 165 |
+
return `/api/v1/documents/${documentId}/mask`
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
export const updateMask = async (documentId: string,
|
| 169 |
+
maskRegionRequest: MaskRegionRequest, options?: RequestInit): Promise<void> => {
|
| 170 |
+
|
| 171 |
+
return fetchApi<void>(getUpdateMaskUrl(documentId),
|
| 172 |
+
{
|
| 173 |
+
...options,
|
| 174 |
+
method: 'PUT',
|
| 175 |
+
headers: { 'Content-Type': 'application/json', ...options?.headers },
|
| 176 |
+
body: JSON.stringify(
|
| 177 |
+
maskRegionRequest,)
|
| 178 |
+
}
|
| 179 |
+
);}
|
| 180 |
+
|
| 181 |
+
|
| 182 |
+
|
| 183 |
+
|
| 184 |
+
export const getUpdateMaskMutationOptions = <TError = ApiError,
|
| 185 |
+
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof updateMask>>, TError,{documentId: string;data: MaskRegionRequest}, TContext>, request?: SecondParameter<typeof fetchApi>}
|
| 186 |
+
): UseMutationOptions<Awaited<ReturnType<typeof updateMask>>, TError,{documentId: string;data: MaskRegionRequest}, TContext> => {
|
| 187 |
+
|
| 188 |
+
const mutationKey = ['updateMask'];
|
| 189 |
+
const {mutation: mutationOptions, request: requestOptions} = options ?
|
| 190 |
+
options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey ?
|
| 191 |
+
options
|
| 192 |
+
: {...options, mutation: {...options.mutation, mutationKey}}
|
| 193 |
+
: {mutation: { mutationKey, }, request: undefined};
|
| 194 |
+
|
| 195 |
+
|
| 196 |
+
|
| 197 |
+
|
| 198 |
+
const mutationFn: MutationFunction<Awaited<ReturnType<typeof updateMask>>, {documentId: string;data: MaskRegionRequest}> = (props) => {
|
| 199 |
+
const {documentId,data} = props ?? {};
|
| 200 |
+
|
| 201 |
+
return updateMask(documentId,data,requestOptions)
|
| 202 |
+
}
|
| 203 |
+
|
| 204 |
+
|
| 205 |
+
|
| 206 |
+
|
| 207 |
+
|
| 208 |
+
|
| 209 |
+
return { mutationFn, ...mutationOptions }}
|
| 210 |
+
|
| 211 |
+
export type UpdateMaskMutationResult = NonNullable<Awaited<ReturnType<typeof updateMask>>>
|
| 212 |
+
export type UpdateMaskMutationBody = MaskRegionRequest
|
| 213 |
+
export type UpdateMaskMutationError = ApiError
|
| 214 |
+
|
| 215 |
+
export const useUpdateMask = <TError = ApiError,
|
| 216 |
+
TContext = unknown>(options?: { mutation?:UseMutationOptions<Awaited<ReturnType<typeof updateMask>>, TError,{documentId: string;data: MaskRegionRequest}, TContext>, request?: SecondParameter<typeof fetchApi>}
|
| 217 |
+
, queryClient?: QueryClient): UseMutationResult<
|
| 218 |
+
Awaited<ReturnType<typeof updateMask>>,
|
| 219 |
+
TError,
|
| 220 |
+
{documentId: string;data: MaskRegionRequest},
|
| 221 |
+
TContext
|
| 222 |
+
> => {
|
| 223 |
+
return useMutation(getUpdateMaskMutationOptions(options), queryClient);
|
| 224 |
+
}
|
ui/lib/api/schemas/apiError.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Generated by orval v8.6.2 🍺
|
| 3 |
+
* Do not edit manually.
|
| 4 |
+
* OpenAPI spec version: 0.0.1
|
| 5 |
+
*/
|
| 6 |
+
|
| 7 |
+
export interface ApiError {
|
| 8 |
+
message: string;
|
| 9 |
+
/** @minimum 0 */
|
| 10 |
+
status: number;
|
| 11 |
+
}
|
ui/lib/api/schemas/apiKeyResponse.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Generated by orval v8.6.2 🍺
|
| 3 |
+
* Do not edit manually.
|
| 4 |
+
* OpenAPI spec version: 0.0.1
|
| 5 |
+
*/
|
| 6 |
+
|
| 7 |
+
export interface ApiKeyResponse {
|
| 8 |
+
/** @nullable */
|
| 9 |
+
apiKey?: string | null;
|
| 10 |
+
}
|