mirror of
https://github.com/ggml-org/llama.cpp.git
synced 2025-10-29 08:41:22 +00:00
Enable per-conversation loading states to allow having parallel conversations (#16327)
* feat: Per-conversation loading states and tracking streaming stats * chore: update webui build output * refactor: Chat state management Consolidates loading state management by using a global `isLoading` store synchronized with individual conversation states. This change ensures proper reactivity and avoids potential race conditions when updating the UI based on the loading status of different conversations. It also improves the accuracy of statistics displayed. Additionally, slots service methods are updated to use conversation IDs for per-conversation state management, avoiding global state pollution. * feat: Adds loading indicator to conversation items * chore: update webui build output * fix: Fix aborting chat streaming Improves the chat stream abortion process by ensuring that partial responses are saved before the abort signal is sent. This avoids a race condition where the onError callback could clear the streaming state before the partial response is saved. Additionally, the stream reading loop and callbacks are now checked for abort signals to prevent further processing after abortion. * refactor: Remove redundant comments * chore: build webui static output * refactor: Cleanup * chore: update webui build output * chore: update webui build output * fix: Conversation loading indicator for regenerating messages * chore: update webui static build * feat: Improve configuration * feat: Install `http-server` as dev dependency to not need to rely on `npx` in CI
This commit is contained in:
committed by
GitHub
parent
06332e2867
commit
13f2cfad41
Binary file not shown.
527
tools/server/webui/package-lock.json
generated
527
tools/server/webui/package-lock.json
generated
@@ -50,6 +50,7 @@
|
|||||||
"eslint-plugin-svelte": "^3.0.0",
|
"eslint-plugin-svelte": "^3.0.0",
|
||||||
"fflate": "^0.8.2",
|
"fflate": "^0.8.2",
|
||||||
"globals": "^16.0.0",
|
"globals": "^16.0.0",
|
||||||
|
"http-server": "^14.1.1",
|
||||||
"mdast": "^3.0.0",
|
"mdast": "^3.0.0",
|
||||||
"mdsvex": "^0.12.3",
|
"mdsvex": "^0.12.3",
|
||||||
"playwright": "^1.53.0",
|
"playwright": "^1.53.0",
|
||||||
@@ -2979,6 +2980,13 @@
|
|||||||
"node": ">=4"
|
"node": ">=4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/async": {
|
||||||
|
"version": "3.2.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
|
||||||
|
"integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/axe-core": {
|
"node_modules/axe-core": {
|
||||||
"version": "4.10.3",
|
"version": "4.10.3",
|
||||||
"resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.10.3.tgz",
|
"resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.10.3.tgz",
|
||||||
@@ -3015,6 +3023,19 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/basic-auth": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"safe-buffer": "5.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/better-opn": {
|
"node_modules/better-opn": {
|
||||||
"version": "3.0.2",
|
"version": "3.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/better-opn/-/better-opn-3.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/better-opn/-/better-opn-3.0.2.tgz",
|
||||||
@@ -3125,6 +3146,37 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/call-bind-apply-helpers": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"function-bind": "^1.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/call-bound": {
|
||||||
|
"version": "1.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
|
||||||
|
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bind-apply-helpers": "^1.0.2",
|
||||||
|
"get-intrinsic": "^1.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/callsites": {
|
"node_modules/callsites": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
|
||||||
@@ -3335,6 +3387,16 @@
|
|||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/corser": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/corser/-/corser-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-utCYNzRSQIZNPIcGZdQc92UVJYAhtGAteCFg0yRaFm8f0P+CPtyGyHXJcGXnffjCybUCEx3FQ2G7U3/o9eIkVQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/cross-spawn": {
|
"node_modules/cross-spawn": {
|
||||||
"version": "7.0.6",
|
"version": "7.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||||
@@ -3520,6 +3582,21 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/dunder-proto": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bind-apply-helpers": "^1.0.1",
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"gopd": "^1.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/enhanced-resolve": {
|
"node_modules/enhanced-resolve": {
|
||||||
"version": "5.18.2",
|
"version": "5.18.2",
|
||||||
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.2.tgz",
|
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.2.tgz",
|
||||||
@@ -3547,6 +3624,26 @@
|
|||||||
"url": "https://github.com/fb55/entities?sponsor=1"
|
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/es-define-property": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/es-errors": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/es-module-lexer": {
|
"node_modules/es-module-lexer": {
|
||||||
"version": "1.7.0",
|
"version": "1.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
|
||||||
@@ -3554,6 +3651,19 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/es-object-atoms": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"es-errors": "^1.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/es-toolkit": {
|
"node_modules/es-toolkit": {
|
||||||
"version": "1.39.7",
|
"version": "1.39.7",
|
||||||
"resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.39.7.tgz",
|
"resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.39.7.tgz",
|
||||||
@@ -3885,6 +3995,13 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/eventemitter3": {
|
||||||
|
"version": "4.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
|
||||||
|
"integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/expect-type": {
|
"node_modules/expect-type": {
|
||||||
"version": "1.2.2",
|
"version": "1.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.2.tgz",
|
||||||
@@ -4058,6 +4175,27 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/follow-redirects": {
|
||||||
|
"version": "1.15.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
|
||||||
|
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
|
||||||
|
"dev": true,
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "individual",
|
||||||
|
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"debug": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/fsevents": {
|
"node_modules/fsevents": {
|
||||||
"version": "2.3.2",
|
"version": "2.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||||
@@ -4073,6 +4211,55 @@
|
|||||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/function-bind": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/get-intrinsic": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bind-apply-helpers": "^1.0.2",
|
||||||
|
"es-define-property": "^1.0.1",
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"es-object-atoms": "^1.1.1",
|
||||||
|
"function-bind": "^1.1.2",
|
||||||
|
"get-proto": "^1.0.1",
|
||||||
|
"gopd": "^1.2.0",
|
||||||
|
"has-symbols": "^1.1.0",
|
||||||
|
"hasown": "^2.0.2",
|
||||||
|
"math-intrinsics": "^1.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/get-proto": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"dunder-proto": "^1.0.1",
|
||||||
|
"es-object-atoms": "^1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/glob-parent": {
|
"node_modules/glob-parent": {
|
||||||
"version": "6.0.2",
|
"version": "6.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
|
||||||
@@ -4099,6 +4286,19 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/gopd": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/graceful-fs": {
|
"node_modules/graceful-fs": {
|
||||||
"version": "4.2.11",
|
"version": "4.2.11",
|
||||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
||||||
@@ -4123,6 +4323,32 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/has-symbols": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/hasown": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"function-bind": "^1.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/hast-util-from-dom": {
|
"node_modules/hast-util-from-dom": {
|
||||||
"version": "5.0.1",
|
"version": "5.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/hast-util-from-dom/-/hast-util-from-dom-5.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/hast-util-from-dom/-/hast-util-from-dom-5.0.1.tgz",
|
||||||
@@ -4363,6 +4589,16 @@
|
|||||||
"url": "https://opencollective.com/unified"
|
"url": "https://opencollective.com/unified"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/he": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"he": "bin/he"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/highlight.js": {
|
"node_modules/highlight.js": {
|
||||||
"version": "11.11.1",
|
"version": "11.11.1",
|
||||||
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz",
|
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz",
|
||||||
@@ -4372,6 +4608,19 @@
|
|||||||
"node": ">=12.0.0"
|
"node": ">=12.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/html-encoding-sniffer": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"whatwg-encoding": "^2.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/html-void-elements": {
|
"node_modules/html-void-elements": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz",
|
||||||
@@ -4382,6 +4631,62 @@
|
|||||||
"url": "https://github.com/sponsors/wooorm"
|
"url": "https://github.com/sponsors/wooorm"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/http-proxy": {
|
||||||
|
"version": "1.18.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz",
|
||||||
|
"integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"eventemitter3": "^4.0.0",
|
||||||
|
"follow-redirects": "^1.0.0",
|
||||||
|
"requires-port": "^1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/http-server": {
|
||||||
|
"version": "14.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/http-server/-/http-server-14.1.1.tgz",
|
||||||
|
"integrity": "sha512-+cbxadF40UXd9T01zUHgA+rlo2Bg1Srer4+B4NwIHdaGxAGGv59nYRnGGDJ9LBk7alpS0US+J+bLLdQOOkJq4A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"basic-auth": "^2.0.1",
|
||||||
|
"chalk": "^4.1.2",
|
||||||
|
"corser": "^2.0.1",
|
||||||
|
"he": "^1.2.0",
|
||||||
|
"html-encoding-sniffer": "^3.0.0",
|
||||||
|
"http-proxy": "^1.18.1",
|
||||||
|
"mime": "^1.6.0",
|
||||||
|
"minimist": "^1.2.6",
|
||||||
|
"opener": "^1.5.1",
|
||||||
|
"portfinder": "^1.0.28",
|
||||||
|
"secure-compare": "3.0.1",
|
||||||
|
"union": "~0.5.0",
|
||||||
|
"url-join": "^4.0.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"http-server": "bin/http-server"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/iconv-lite": {
|
||||||
|
"version": "0.6.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
||||||
|
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ignore": {
|
"node_modules/ignore": {
|
||||||
"version": "5.3.2",
|
"version": "5.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
||||||
@@ -5008,6 +5313,16 @@
|
|||||||
"url": "https://github.com/sponsors/wooorm"
|
"url": "https://github.com/sponsors/wooorm"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/math-intrinsics": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/mdast": {
|
"node_modules/mdast": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/mdast/-/mdast-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/mdast/-/mdast-3.0.0.tgz",
|
||||||
@@ -5976,6 +6291,19 @@
|
|||||||
"url": "https://github.com/sponsors/jonschlinkert"
|
"url": "https://github.com/sponsors/jonschlinkert"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/mime": {
|
||||||
|
"version": "1.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
|
||||||
|
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"mime": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/min-indent": {
|
"node_modules/min-indent": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz",
|
||||||
@@ -6009,6 +6337,16 @@
|
|||||||
"node": "*"
|
"node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/minimist": {
|
||||||
|
"version": "1.2.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
|
||||||
|
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/minipass": {
|
"node_modules/minipass": {
|
||||||
"version": "7.1.2",
|
"version": "7.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
|
||||||
@@ -6124,6 +6462,19 @@
|
|||||||
"tslib": "^2.0.3"
|
"tslib": "^2.0.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/object-inspect": {
|
||||||
|
"version": "1.13.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
|
||||||
|
"integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/open": {
|
"node_modules/open": {
|
||||||
"version": "8.4.2",
|
"version": "8.4.2",
|
||||||
"resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz",
|
"resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz",
|
||||||
@@ -6142,6 +6493,16 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/opener": {
|
||||||
|
"version": "1.5.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz",
|
||||||
|
"integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "(WTFPL OR MIT)",
|
||||||
|
"bin": {
|
||||||
|
"opener": "bin/opener-bin.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/optionator": {
|
"node_modules/optionator": {
|
||||||
"version": "0.9.4",
|
"version": "0.9.4",
|
||||||
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
|
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
|
||||||
@@ -6330,6 +6691,20 @@
|
|||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/portfinder": {
|
||||||
|
"version": "1.0.38",
|
||||||
|
"resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.38.tgz",
|
||||||
|
"integrity": "sha512-rEwq/ZHlJIKw++XtLAO8PPuOQA/zaPJOZJ37BVuN97nLpMJeuDVLVGRwbFoBgLudgdTMP2hdRJP++H+8QOA3vg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"async": "^3.2.6",
|
||||||
|
"debug": "^4.3.6"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10.12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/postcss": {
|
"node_modules/postcss": {
|
||||||
"version": "8.5.6",
|
"version": "8.5.6",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
|
||||||
@@ -6680,6 +7055,22 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/qs": {
|
||||||
|
"version": "6.14.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",
|
||||||
|
"integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"side-channel": "^1.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.6"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/queue-microtask": {
|
"node_modules/queue-microtask": {
|
||||||
"version": "1.2.3",
|
"version": "1.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
|
||||||
@@ -6959,6 +7350,13 @@
|
|||||||
"url": "https://opencollective.com/unified"
|
"url": "https://opencollective.com/unified"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/requires-port": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/resolve-from": {
|
"node_modules/resolve-from": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
|
||||||
@@ -7072,6 +7470,20 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/safe-buffer": {
|
||||||
|
"version": "5.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||||
|
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/safer-buffer": {
|
||||||
|
"version": "2.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||||
|
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/scheduler": {
|
"node_modules/scheduler": {
|
||||||
"version": "0.26.0",
|
"version": "0.26.0",
|
||||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz",
|
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz",
|
||||||
@@ -7079,6 +7491,13 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/secure-compare": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/secure-compare/-/secure-compare-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-AckIIV90rPDcBcglUwXPF3kg0P0qmPsPXAj6BBEENQE1p5yA1xfmDJzfi1Tappj37Pv2mVbKpL3Z1T+Nn7k1Qw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/semver": {
|
"node_modules/semver": {
|
||||||
"version": "7.7.2",
|
"version": "7.7.2",
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
|
||||||
@@ -7122,6 +7541,82 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/side-channel": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"object-inspect": "^1.13.3",
|
||||||
|
"side-channel-list": "^1.0.0",
|
||||||
|
"side-channel-map": "^1.0.1",
|
||||||
|
"side-channel-weakmap": "^1.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/side-channel-list": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"object-inspect": "^1.13.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/side-channel-map": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bound": "^1.0.2",
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"get-intrinsic": "^1.2.5",
|
||||||
|
"object-inspect": "^1.13.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/side-channel-weakmap": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bound": "^1.0.2",
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"get-intrinsic": "^1.2.5",
|
||||||
|
"object-inspect": "^1.13.3",
|
||||||
|
"side-channel-map": "^1.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/siginfo": {
|
"node_modules/siginfo": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
|
||||||
@@ -7904,6 +8399,18 @@
|
|||||||
"integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==",
|
"integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/union": {
|
||||||
|
"version": "0.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/union/-/union-0.5.0.tgz",
|
||||||
|
"integrity": "sha512-N6uOhuW6zO95P3Mel2I2zMsbsanvvtgn6jVqJv4vbVcz/JN0OkL9suomjQGmWtxJQXOCqUJvquc1sMeNz/IwlA==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"qs": "^6.4.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/unist-util-find-after": {
|
"node_modules/unist-util-find-after": {
|
||||||
"version": "5.0.0",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/unist-util-find-after/-/unist-util-find-after-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/unist-util-find-after/-/unist-util-find-after-5.0.0.tgz",
|
||||||
@@ -8073,6 +8580,13 @@
|
|||||||
"punycode": "^2.1.0"
|
"punycode": "^2.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/url-join": {
|
||||||
|
"version": "4.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz",
|
||||||
|
"integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/util-deprecate": {
|
"node_modules/util-deprecate": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||||
@@ -8447,6 +8961,19 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/whatwg-encoding": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"iconv-lite": "0.6.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/which": {
|
"node_modules/which": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||||
|
|||||||
@@ -52,6 +52,7 @@
|
|||||||
"eslint-plugin-svelte": "^3.0.0",
|
"eslint-plugin-svelte": "^3.0.0",
|
||||||
"fflate": "^0.8.2",
|
"fflate": "^0.8.2",
|
||||||
"globals": "^16.0.0",
|
"globals": "^16.0.0",
|
||||||
|
"http-server": "^14.1.1",
|
||||||
"mdast": "^3.0.0",
|
"mdast": "^3.0.0",
|
||||||
"mdsvex": "^0.12.3",
|
"mdsvex": "^0.12.3",
|
||||||
"playwright": "^1.53.0",
|
"playwright": "^1.53.0",
|
||||||
|
|||||||
@@ -2,8 +2,10 @@ import { defineConfig } from '@playwright/test';
|
|||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
webServer: {
|
webServer: {
|
||||||
command: 'npm run build && npx http-server ../public -p 8181',
|
command: 'npm run build && http-server ../public -p 8181',
|
||||||
port: 8181
|
port: 8181,
|
||||||
|
timeout: 120000,
|
||||||
|
reuseExistingServer: false
|
||||||
},
|
},
|
||||||
testDir: 'e2e'
|
testDir: 'e2e'
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,18 +7,19 @@
|
|||||||
|
|
||||||
const processingState = useProcessingState();
|
const processingState = useProcessingState();
|
||||||
|
|
||||||
|
let isCurrentConversationLoading = $derived(isLoading());
|
||||||
let processingDetails = $derived(processingState.getProcessingDetails());
|
let processingDetails = $derived(processingState.getProcessingDetails());
|
||||||
|
let showSlotsInfo = $derived(isCurrentConversationLoading || config().keepStatsVisible);
|
||||||
|
|
||||||
let showSlotsInfo = $derived(isLoading() || config().keepStatsVisible);
|
// Track loading state reactively by checking if conversation ID is in loading conversations array
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
const keepStatsVisible = config().keepStatsVisible;
|
const keepStatsVisible = config().keepStatsVisible;
|
||||||
|
|
||||||
if (keepStatsVisible || isLoading()) {
|
if (keepStatsVisible || isCurrentConversationLoading) {
|
||||||
processingState.startMonitoring();
|
processingState.startMonitoring();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isLoading() && !keepStatsVisible) {
|
if (!isCurrentConversationLoading && !keepStatsVisible) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (!config().keepStatsVisible) {
|
if (!config().keepStatsVisible) {
|
||||||
processingState.stopMonitoring();
|
processingState.stopMonitoring();
|
||||||
@@ -27,18 +28,20 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Update processing state from stored timings
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
activeConversation();
|
const conversation = activeConversation();
|
||||||
|
|
||||||
const messages = activeMessages() as DatabaseMessage[];
|
const messages = activeMessages() as DatabaseMessage[];
|
||||||
const keepStatsVisible = config().keepStatsVisible;
|
const keepStatsVisible = config().keepStatsVisible;
|
||||||
|
|
||||||
if (keepStatsVisible) {
|
if (keepStatsVisible && conversation) {
|
||||||
if (messages.length === 0) {
|
if (messages.length === 0) {
|
||||||
slotsService.clearState();
|
slotsService.clearConversationState(conversation.id);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Search backwards through messages to find most recent assistant message with timing data
|
||||||
|
// Using reverse iteration for performance - avoids array copy and stops at first match
|
||||||
let foundTimingData = false;
|
let foundTimingData = false;
|
||||||
|
|
||||||
for (let i = messages.length - 1; i >= 0; i--) {
|
for (let i = messages.length - 1; i >= 0; i--) {
|
||||||
@@ -47,7 +50,8 @@
|
|||||||
foundTimingData = true;
|
foundTimingData = true;
|
||||||
|
|
||||||
slotsService
|
slotsService
|
||||||
.updateFromTimingData({
|
.updateFromTimingData(
|
||||||
|
{
|
||||||
prompt_n: message.timings.prompt_n || 0,
|
prompt_n: message.timings.prompt_n || 0,
|
||||||
predicted_n: message.timings.predicted_n || 0,
|
predicted_n: message.timings.predicted_n || 0,
|
||||||
predicted_per_second:
|
predicted_per_second:
|
||||||
@@ -55,7 +59,9 @@
|
|||||||
? (message.timings.predicted_n / message.timings.predicted_ms) * 1000
|
? (message.timings.predicted_n / message.timings.predicted_ms) * 1000
|
||||||
: 0,
|
: 0,
|
||||||
cache_n: message.timings.cache_n || 0
|
cache_n: message.timings.cache_n || 0
|
||||||
})
|
},
|
||||||
|
conversation.id
|
||||||
|
)
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.warn('Failed to update processing state from stored timings:', error);
|
console.warn('Failed to update processing state from stored timings:', error);
|
||||||
});
|
});
|
||||||
@@ -64,7 +70,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!foundTimingData) {
|
if (!foundTimingData) {
|
||||||
slotsService.clearState();
|
slotsService.clearConversationState(conversation.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -83,6 +83,8 @@
|
|||||||
let activeErrorDialog = $derived(errorDialog());
|
let activeErrorDialog = $derived(errorDialog());
|
||||||
let isServerLoading = $derived(serverLoading());
|
let isServerLoading = $derived(serverLoading());
|
||||||
|
|
||||||
|
let isCurrentConversationLoading = $derived(isLoading());
|
||||||
|
|
||||||
async function handleDeleteConfirm() {
|
async function handleDeleteConfirm() {
|
||||||
const conversation = activeConversation();
|
const conversation = activeConversation();
|
||||||
if (conversation) {
|
if (conversation) {
|
||||||
@@ -254,7 +256,7 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (isLoading() && autoScrollEnabled) {
|
if (isCurrentConversationLoading && autoScrollEnabled) {
|
||||||
scrollInterval = setInterval(scrollChatToBottom, AUTO_SCROLL_INTERVAL);
|
scrollInterval = setInterval(scrollChatToBottom, AUTO_SCROLL_INTERVAL);
|
||||||
} else if (scrollInterval) {
|
} else if (scrollInterval) {
|
||||||
clearInterval(scrollInterval);
|
clearInterval(scrollInterval);
|
||||||
@@ -305,7 +307,7 @@
|
|||||||
|
|
||||||
<div class="conversation-chat-form pointer-events-auto rounded-t-3xl pb-4">
|
<div class="conversation-chat-form pointer-events-auto rounded-t-3xl pb-4">
|
||||||
<ChatForm
|
<ChatForm
|
||||||
isLoading={isLoading()}
|
isLoading={isCurrentConversationLoading}
|
||||||
onFileRemove={handleFileRemove}
|
onFileRemove={handleFileRemove}
|
||||||
onFileUpload={handleFileUpload}
|
onFileUpload={handleFileUpload}
|
||||||
onSend={handleSendMessage}
|
onSend={handleSendMessage}
|
||||||
@@ -348,7 +350,7 @@
|
|||||||
|
|
||||||
<div in:fly={{ y: 10, duration: 250, delay: 300 }}>
|
<div in:fly={{ y: 10, duration: 250, delay: 300 }}>
|
||||||
<ChatForm
|
<ChatForm
|
||||||
isLoading={isLoading()}
|
isLoading={isCurrentConversationLoading}
|
||||||
onFileRemove={handleFileRemove}
|
onFileRemove={handleFileRemove}
|
||||||
onFileUpload={handleFileUpload}
|
onFileUpload={handleFileUpload}
|
||||||
onSend={handleSendMessage}
|
onSend={handleSendMessage}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Trash2, Pencil, MoreHorizontal, Download } from '@lucide/svelte';
|
import { Trash2, Pencil, MoreHorizontal, Download, Loader2 } from '@lucide/svelte';
|
||||||
import { ActionDropdown } from '$lib/components/app';
|
import { ActionDropdown } from '$lib/components/app';
|
||||||
import { downloadConversation } from '$lib/stores/chat.svelte';
|
import { downloadConversation, getAllLoadingConversations } from '$lib/stores/chat.svelte';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -25,6 +25,8 @@
|
|||||||
let renderActionsDropdown = $state(false);
|
let renderActionsDropdown = $state(false);
|
||||||
let dropdownOpen = $state(false);
|
let dropdownOpen = $state(false);
|
||||||
|
|
||||||
|
let isLoading = $derived(getAllLoadingConversations().includes(conversation.id));
|
||||||
|
|
||||||
function handleEdit(event: Event) {
|
function handleEdit(event: Event) {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
onEdit?.(conversation.id);
|
onEdit?.(conversation.id);
|
||||||
@@ -83,11 +85,16 @@
|
|||||||
onmouseover={handleMouseOver}
|
onmouseover={handleMouseOver}
|
||||||
onmouseleave={handleMouseLeave}
|
onmouseleave={handleMouseLeave}
|
||||||
>
|
>
|
||||||
|
<div class="flex min-w-0 flex-1 items-center gap-2">
|
||||||
|
{#if isLoading}
|
||||||
|
<Loader2 class="h-3.5 w-3.5 shrink-0 animate-spin text-muted-foreground" />
|
||||||
|
{/if}
|
||||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||||
<span class="truncate text-sm font-medium" onclick={handleMobileSidebarItemClick}>
|
<span class="truncate text-sm font-medium" onclick={handleMobileSidebarItemClick}>
|
||||||
{conversation.name}
|
{conversation.name}
|
||||||
</span>
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
{#if renderActionsDropdown}
|
{#if renderActionsDropdown}
|
||||||
<div class="actions flex items-center">
|
<div class="actions flex items-center">
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ import { slotsService } from './slots';
|
|||||||
* - Request lifecycle management (abort, cleanup)
|
* - Request lifecycle management (abort, cleanup)
|
||||||
*/
|
*/
|
||||||
export class ChatService {
|
export class ChatService {
|
||||||
private abortController: AbortController | null = null;
|
private abortControllers: Map<string, AbortController> = new Map();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sends a chat completion request to the llama.cpp server.
|
* Sends a chat completion request to the llama.cpp server.
|
||||||
@@ -43,7 +43,8 @@ export class ChatService {
|
|||||||
*/
|
*/
|
||||||
async sendMessage(
|
async sendMessage(
|
||||||
messages: ApiChatMessageData[] | (DatabaseMessage & { extra?: DatabaseMessageExtra[] })[],
|
messages: ApiChatMessageData[] | (DatabaseMessage & { extra?: DatabaseMessageExtra[] })[],
|
||||||
options: SettingsChatServiceOptions = {}
|
options: SettingsChatServiceOptions = {},
|
||||||
|
conversationId?: string
|
||||||
): Promise<string | void> {
|
): Promise<string | void> {
|
||||||
const {
|
const {
|
||||||
stream,
|
stream,
|
||||||
@@ -79,25 +80,25 @@ export class ChatService {
|
|||||||
|
|
||||||
const currentConfig = config();
|
const currentConfig = config();
|
||||||
|
|
||||||
// Cancel any ongoing request and create a new abort controller
|
const requestId = conversationId || 'default';
|
||||||
this.abort();
|
|
||||||
this.abortController = new AbortController();
|
if (this.abortControllers.has(requestId)) {
|
||||||
|
this.abortControllers.get(requestId)?.abort();
|
||||||
|
}
|
||||||
|
|
||||||
|
const abortController = new AbortController();
|
||||||
|
this.abortControllers.set(requestId, abortController);
|
||||||
|
|
||||||
// Convert database messages with attachments to API format if needed
|
|
||||||
const normalizedMessages: ApiChatMessageData[] = messages
|
const normalizedMessages: ApiChatMessageData[] = messages
|
||||||
.map((msg) => {
|
.map((msg) => {
|
||||||
// Check if this is a DatabaseMessage by checking for DatabaseMessage-specific fields
|
|
||||||
if ('id' in msg && 'convId' in msg && 'timestamp' in msg) {
|
if ('id' in msg && 'convId' in msg && 'timestamp' in msg) {
|
||||||
// This is a DatabaseMessage, convert it
|
|
||||||
const dbMsg = msg as DatabaseMessage & { extra?: DatabaseMessageExtra[] };
|
const dbMsg = msg as DatabaseMessage & { extra?: DatabaseMessageExtra[] };
|
||||||
return ChatService.convertMessageToChatServiceData(dbMsg);
|
return ChatService.convertMessageToChatServiceData(dbMsg);
|
||||||
} else {
|
} else {
|
||||||
// This is already an ApiChatMessageData object
|
|
||||||
return msg as ApiChatMessageData;
|
return msg as ApiChatMessageData;
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.filter((msg) => {
|
.filter((msg) => {
|
||||||
// Filter out empty system messages
|
|
||||||
if (msg.role === 'system') {
|
if (msg.role === 'system') {
|
||||||
const content = typeof msg.content === 'string' ? msg.content : '';
|
const content = typeof msg.content === 'string' ? msg.content : '';
|
||||||
|
|
||||||
@@ -107,7 +108,6 @@ export class ChatService {
|
|||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Build base request body with system message injection
|
|
||||||
const processedMessages = this.injectSystemMessage(normalizedMessages);
|
const processedMessages = this.injectSystemMessage(normalizedMessages);
|
||||||
|
|
||||||
const requestBody: ApiChatCompletionRequest = {
|
const requestBody: ApiChatCompletionRequest = {
|
||||||
@@ -172,11 +172,10 @@ export class ChatService {
|
|||||||
...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {})
|
...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {})
|
||||||
},
|
},
|
||||||
body: JSON.stringify(requestBody),
|
body: JSON.stringify(requestBody),
|
||||||
signal: this.abortController.signal
|
signal: abortController.signal
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
// Use the new parseErrorResponse method to handle structured errors
|
|
||||||
const error = await this.parseErrorResponse(response);
|
const error = await this.parseErrorResponse(response);
|
||||||
if (onError) {
|
if (onError) {
|
||||||
onError(error);
|
onError(error);
|
||||||
@@ -185,13 +184,16 @@ export class ChatService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (stream) {
|
if (stream) {
|
||||||
return this.handleStreamResponse(
|
await this.handleStreamResponse(
|
||||||
response,
|
response,
|
||||||
onChunk,
|
onChunk,
|
||||||
onComplete,
|
onComplete,
|
||||||
onError,
|
onError,
|
||||||
options.onReasoningChunk
|
options.onReasoningChunk,
|
||||||
|
conversationId,
|
||||||
|
abortController.signal
|
||||||
);
|
);
|
||||||
|
return;
|
||||||
} else {
|
} else {
|
||||||
return this.handleNonStreamResponse(response, onComplete, onError);
|
return this.handleNonStreamResponse(response, onComplete, onError);
|
||||||
}
|
}
|
||||||
@@ -227,18 +229,19 @@ export class ChatService {
|
|||||||
onError(userFriendlyError);
|
onError(userFriendlyError);
|
||||||
}
|
}
|
||||||
throw userFriendlyError;
|
throw userFriendlyError;
|
||||||
|
} finally {
|
||||||
|
this.abortControllers.delete(requestId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles streaming response from the chat completion API.
|
* Handles streaming response from the chat completion API
|
||||||
* Processes server-sent events and extracts content chunks from the stream.
|
* @param response - The Response object from the fetch request
|
||||||
*
|
|
||||||
* @param response - The fetch Response object containing the streaming data
|
|
||||||
* @param onChunk - Optional callback invoked for each content chunk received
|
* @param onChunk - Optional callback invoked for each content chunk received
|
||||||
* @param onComplete - Optional callback invoked when the stream is complete with full response
|
* @param onComplete - Optional callback invoked when the stream is complete with full response
|
||||||
* @param onError - Optional callback invoked if an error occurs during streaming
|
* @param onError - Optional callback invoked if an error occurs during streaming
|
||||||
* @param onReasoningChunk - Optional callback invoked for each reasoning content chunk
|
* @param onReasoningChunk - Optional callback invoked for each reasoning content chunk
|
||||||
|
* @param conversationId - Optional conversation ID for per-conversation state tracking
|
||||||
* @returns {Promise<void>} Promise that resolves when streaming is complete
|
* @returns {Promise<void>} Promise that resolves when streaming is complete
|
||||||
* @throws {Error} if the stream cannot be read or parsed
|
* @throws {Error} if the stream cannot be read or parsed
|
||||||
*/
|
*/
|
||||||
@@ -251,7 +254,9 @@ export class ChatService {
|
|||||||
timings?: ChatMessageTimings
|
timings?: ChatMessageTimings
|
||||||
) => void,
|
) => void,
|
||||||
onError?: (error: Error) => void,
|
onError?: (error: Error) => void,
|
||||||
onReasoningChunk?: (chunk: string) => void
|
onReasoningChunk?: (chunk: string) => void,
|
||||||
|
conversationId?: string,
|
||||||
|
abortSignal?: AbortSignal
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const reader = response.body?.getReader();
|
const reader = response.body?.getReader();
|
||||||
|
|
||||||
@@ -269,14 +274,20 @@ export class ChatService {
|
|||||||
try {
|
try {
|
||||||
let chunk = '';
|
let chunk = '';
|
||||||
while (true) {
|
while (true) {
|
||||||
|
if (abortSignal?.aborted) break;
|
||||||
|
|
||||||
const { done, value } = await reader.read();
|
const { done, value } = await reader.read();
|
||||||
if (done) break;
|
if (done) break;
|
||||||
|
|
||||||
|
if (abortSignal?.aborted) break;
|
||||||
|
|
||||||
chunk += decoder.decode(value, { stream: true });
|
chunk += decoder.decode(value, { stream: true });
|
||||||
const lines = chunk.split('\n');
|
const lines = chunk.split('\n');
|
||||||
chunk = lines.pop() || ''; // Save incomplete line for next read
|
chunk = lines.pop() || '';
|
||||||
|
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
|
if (abortSignal?.aborted) break;
|
||||||
|
|
||||||
if (line.startsWith('data: ')) {
|
if (line.startsWith('data: ')) {
|
||||||
const data = line.slice(6);
|
const data = line.slice(6);
|
||||||
if (data === '[DONE]') {
|
if (data === '[DONE]') {
|
||||||
@@ -293,9 +304,7 @@ export class ChatService {
|
|||||||
const promptProgress = parsed.prompt_progress;
|
const promptProgress = parsed.prompt_progress;
|
||||||
|
|
||||||
if (timings || promptProgress) {
|
if (timings || promptProgress) {
|
||||||
this.updateProcessingState(timings, promptProgress);
|
this.updateProcessingState(timings, promptProgress, conversationId);
|
||||||
|
|
||||||
// Store the latest timing data
|
|
||||||
if (timings) {
|
if (timings) {
|
||||||
lastTimings = timings;
|
lastTimings = timings;
|
||||||
}
|
}
|
||||||
@@ -304,21 +313,29 @@ export class ChatService {
|
|||||||
if (content) {
|
if (content) {
|
||||||
hasReceivedData = true;
|
hasReceivedData = true;
|
||||||
aggregatedContent += content;
|
aggregatedContent += content;
|
||||||
|
if (!abortSignal?.aborted) {
|
||||||
onChunk?.(content);
|
onChunk?.(content);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (reasoningContent) {
|
if (reasoningContent) {
|
||||||
hasReceivedData = true;
|
hasReceivedData = true;
|
||||||
fullReasoningContent += reasoningContent;
|
fullReasoningContent += reasoningContent;
|
||||||
|
if (!abortSignal?.aborted) {
|
||||||
onReasoningChunk?.(reasoningContent);
|
onReasoningChunk?.(reasoningContent);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Error parsing JSON chunk:', e);
|
console.error('Error parsing JSON chunk:', e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (abortSignal?.aborted) break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (abortSignal?.aborted) return;
|
||||||
|
|
||||||
if (streamFinished) {
|
if (streamFinished) {
|
||||||
if (!hasReceivedData && aggregatedContent.length === 0) {
|
if (!hasReceivedData && aggregatedContent.length === 0) {
|
||||||
const noResponseError = new Error('No response received from server. Please try again.');
|
const noResponseError = new Error('No response received from server. Please try again.');
|
||||||
@@ -520,10 +537,18 @@ export class ChatService {
|
|||||||
*
|
*
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
public abort(): void {
|
public abort(conversationId?: string): void {
|
||||||
if (this.abortController) {
|
if (conversationId) {
|
||||||
this.abortController.abort();
|
const abortController = this.abortControllers.get(conversationId);
|
||||||
this.abortController = null;
|
if (abortController) {
|
||||||
|
abortController.abort();
|
||||||
|
this.abortControllers.delete(conversationId);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (const controller of this.abortControllers.values()) {
|
||||||
|
controller.abort();
|
||||||
|
}
|
||||||
|
this.abortControllers.clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -581,7 +606,6 @@ export class ChatService {
|
|||||||
|
|
||||||
return error;
|
return error;
|
||||||
} catch {
|
} catch {
|
||||||
// If we can't parse the error response, return a generic error
|
|
||||||
const fallback = new Error(`Server error (${response.status}): ${response.statusText}`);
|
const fallback = new Error(`Server error (${response.status}): ${response.statusText}`);
|
||||||
fallback.name = 'HttpError';
|
fallback.name = 'HttpError';
|
||||||
return fallback;
|
return fallback;
|
||||||
@@ -590,23 +614,25 @@ export class ChatService {
|
|||||||
|
|
||||||
private updateProcessingState(
|
private updateProcessingState(
|
||||||
timings?: ChatMessageTimings,
|
timings?: ChatMessageTimings,
|
||||||
promptProgress?: ChatMessagePromptProgress
|
promptProgress?: ChatMessagePromptProgress,
|
||||||
|
conversationId?: string
|
||||||
): void {
|
): void {
|
||||||
// Calculate tokens per second from timing data
|
|
||||||
const tokensPerSecond =
|
const tokensPerSecond =
|
||||||
timings?.predicted_ms && timings?.predicted_n
|
timings?.predicted_ms && timings?.predicted_n
|
||||||
? (timings.predicted_n / timings.predicted_ms) * 1000
|
? (timings.predicted_n / timings.predicted_ms) * 1000
|
||||||
: 0;
|
: 0;
|
||||||
|
|
||||||
// Update slots service with timing data (async but don't wait)
|
|
||||||
slotsService
|
slotsService
|
||||||
.updateFromTimingData({
|
.updateFromTimingData(
|
||||||
|
{
|
||||||
prompt_n: timings?.prompt_n || 0,
|
prompt_n: timings?.prompt_n || 0,
|
||||||
predicted_n: timings?.predicted_n || 0,
|
predicted_n: timings?.predicted_n || 0,
|
||||||
predicted_per_second: tokensPerSecond,
|
predicted_per_second: tokensPerSecond,
|
||||||
cache_n: timings?.cache_n || 0,
|
cache_n: timings?.cache_n || 0,
|
||||||
prompt_progress: promptProgress
|
prompt_progress: promptProgress
|
||||||
})
|
},
|
||||||
|
conversationId
|
||||||
|
)
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.warn('Failed to update processing state:', error);
|
console.warn('Failed to update processing state:', error);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -37,6 +37,8 @@ export class SlotsService {
|
|||||||
private callbacks: Set<(state: ApiProcessingState | null) => void> = new Set();
|
private callbacks: Set<(state: ApiProcessingState | null) => void> = new Set();
|
||||||
private isStreamingActive: boolean = false;
|
private isStreamingActive: boolean = false;
|
||||||
private lastKnownState: ApiProcessingState | null = null;
|
private lastKnownState: ApiProcessingState | null = null;
|
||||||
|
private conversationStates: Map<string, ApiProcessingState | null> = new Map();
|
||||||
|
private activeConversationId: string | null = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start streaming session tracking
|
* Start streaming session tracking
|
||||||
@@ -75,6 +77,62 @@ export class SlotsService {
|
|||||||
return this.isStreamingActive;
|
return this.isStreamingActive;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the active conversation for statistics display
|
||||||
|
*/
|
||||||
|
setActiveConversation(conversationId: string | null): void {
|
||||||
|
this.activeConversationId = conversationId;
|
||||||
|
this.notifyCallbacks();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update processing state for a specific conversation
|
||||||
|
*/
|
||||||
|
updateConversationState(conversationId: string, state: ApiProcessingState | null): void {
|
||||||
|
this.conversationStates.set(conversationId, state);
|
||||||
|
|
||||||
|
if (conversationId === this.activeConversationId) {
|
||||||
|
this.lastKnownState = state;
|
||||||
|
this.notifyCallbacks();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get processing state for a specific conversation
|
||||||
|
*/
|
||||||
|
getConversationState(conversationId: string): ApiProcessingState | null {
|
||||||
|
return this.conversationStates.get(conversationId) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear state for a specific conversation
|
||||||
|
*/
|
||||||
|
clearConversationState(conversationId: string): void {
|
||||||
|
this.conversationStates.delete(conversationId);
|
||||||
|
|
||||||
|
if (conversationId === this.activeConversationId) {
|
||||||
|
this.lastKnownState = null;
|
||||||
|
this.notifyCallbacks();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notify all callbacks with current state
|
||||||
|
*/
|
||||||
|
private notifyCallbacks(): void {
|
||||||
|
const currentState = this.activeConversationId
|
||||||
|
? this.conversationStates.get(this.activeConversationId) || null
|
||||||
|
: this.lastKnownState;
|
||||||
|
|
||||||
|
for (const callback of this.callbacks) {
|
||||||
|
try {
|
||||||
|
callback(currentState);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in slots service callback:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @deprecated Polling is no longer used - timing data comes from ChatService streaming response
|
* @deprecated Polling is no longer used - timing data comes from ChatService streaming response
|
||||||
* This method logs a warning if called to help identify outdated usage
|
* This method logs a warning if called to help identify outdated usage
|
||||||
@@ -100,29 +158,29 @@ export class SlotsService {
|
|||||||
/**
|
/**
|
||||||
* Updates processing state with timing data from ChatService streaming response
|
* Updates processing state with timing data from ChatService streaming response
|
||||||
*/
|
*/
|
||||||
async updateFromTimingData(timingData: {
|
async updateFromTimingData(
|
||||||
|
timingData: {
|
||||||
prompt_n: number;
|
prompt_n: number;
|
||||||
predicted_n: number;
|
predicted_n: number;
|
||||||
predicted_per_second: number;
|
predicted_per_second: number;
|
||||||
cache_n: number;
|
cache_n: number;
|
||||||
prompt_progress?: ChatMessagePromptProgress;
|
prompt_progress?: ChatMessagePromptProgress;
|
||||||
}): Promise<void> {
|
},
|
||||||
|
conversationId?: string
|
||||||
|
): Promise<void> {
|
||||||
const processingState = await this.parseCompletionTimingData(timingData);
|
const processingState = await this.parseCompletionTimingData(timingData);
|
||||||
|
|
||||||
// Only update if we successfully parsed the state
|
|
||||||
if (processingState === null) {
|
if (processingState === null) {
|
||||||
console.warn('Failed to parse timing data - skipping update');
|
console.warn('Failed to parse timing data - skipping update');
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (conversationId) {
|
||||||
|
this.updateConversationState(conversationId, processingState);
|
||||||
|
} else {
|
||||||
this.lastKnownState = processingState;
|
this.lastKnownState = processingState;
|
||||||
|
this.notifyCallbacks();
|
||||||
for (const callback of this.callbacks) {
|
|
||||||
try {
|
|
||||||
callback(processingState);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error in timing callback:', error);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,6 +201,7 @@ export class SlotsService {
|
|||||||
...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {})
|
...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {})
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const slotsData = await response.json();
|
const slotsData = await response.json();
|
||||||
if (Array.isArray(slotsData) && slotsData.length > 0) {
|
if (Array.isArray(slotsData) && slotsData.length > 0) {
|
||||||
@@ -179,6 +238,7 @@ export class SlotsService {
|
|||||||
|
|
||||||
if (contextTotal === null) {
|
if (contextTotal === null) {
|
||||||
console.warn('No context total available - cannot calculate processing state');
|
console.warn('No context total available - cannot calculate processing state');
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -214,13 +274,21 @@ export class SlotsService {
|
|||||||
/**
|
/**
|
||||||
* Get current processing state
|
* Get current processing state
|
||||||
* Returns the last known state from timing data, or null if no data available
|
* Returns the last known state from timing data, or null if no data available
|
||||||
|
* If activeConversationId is set, returns state for that conversation
|
||||||
*/
|
*/
|
||||||
async getCurrentState(): Promise<ApiProcessingState | null> {
|
async getCurrentState(): Promise<ApiProcessingState | null> {
|
||||||
|
if (this.activeConversationId) {
|
||||||
|
const conversationState = this.conversationStates.get(this.activeConversationId);
|
||||||
|
|
||||||
|
if (conversationState) {
|
||||||
|
return conversationState;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (this.lastKnownState) {
|
if (this.lastKnownState) {
|
||||||
return this.lastKnownState;
|
return this.lastKnownState;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
// Import dynamically to avoid circular dependency
|
|
||||||
const { chatStore } = await import('$lib/stores/chat.svelte');
|
const { chatStore } = await import('$lib/stores/chat.svelte');
|
||||||
const messages = chatStore.activeMessages;
|
const messages = chatStore.activeMessages;
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { filterByLeafNodeId, findLeafNode, findDescendantMessages } from '$lib/u
|
|||||||
import { browser } from '$app/environment';
|
import { browser } from '$app/environment';
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
|
import { SvelteMap } from 'svelte/reactivity';
|
||||||
import type { ExportedConversations } from '$lib/types/database';
|
import type { ExportedConversations } from '$lib/types/database';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -50,6 +51,8 @@ class ChatStore {
|
|||||||
errorDialogState = $state<{ type: 'timeout' | 'server'; message: string } | null>(null);
|
errorDialogState = $state<{ type: 'timeout' | 'server'; message: string } | null>(null);
|
||||||
isInitialized = $state(false);
|
isInitialized = $state(false);
|
||||||
isLoading = $state(false);
|
isLoading = $state(false);
|
||||||
|
conversationLoadingStates = new SvelteMap<string, boolean>();
|
||||||
|
conversationStreamingStates = new SvelteMap<string, { response: string; messageId: string }>();
|
||||||
titleUpdateConfirmationCallback?: (currentTitle: string, newTitle: string) => Promise<boolean>;
|
titleUpdateConfirmationCallback?: (currentTitle: string, newTitle: string) => Promise<boolean>;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -94,6 +97,13 @@ class ChatStore {
|
|||||||
this.activeConversation = conversation;
|
this.activeConversation = conversation;
|
||||||
this.activeMessages = [];
|
this.activeMessages = [];
|
||||||
|
|
||||||
|
slotsService.setActiveConversation(conversation.id);
|
||||||
|
|
||||||
|
const isConvLoading = this.isConversationLoading(conversation.id);
|
||||||
|
this.isLoading = isConvLoading;
|
||||||
|
|
||||||
|
this.currentResponse = '';
|
||||||
|
|
||||||
await goto(`#/chat/${conversation.id}`);
|
await goto(`#/chat/${conversation.id}`);
|
||||||
|
|
||||||
return conversation.id;
|
return conversation.id;
|
||||||
@@ -114,6 +124,14 @@ class ChatStore {
|
|||||||
|
|
||||||
this.activeConversation = conversation;
|
this.activeConversation = conversation;
|
||||||
|
|
||||||
|
slotsService.setActiveConversation(convId);
|
||||||
|
|
||||||
|
const isConvLoading = this.isConversationLoading(convId);
|
||||||
|
this.isLoading = isConvLoading;
|
||||||
|
|
||||||
|
const streamingState = this.getConversationStreaming(convId);
|
||||||
|
this.currentResponse = streamingState?.response || '';
|
||||||
|
|
||||||
if (conversation.currNode) {
|
if (conversation.currNode) {
|
||||||
const allMessages = await DatabaseStore.getConversationMessages(convId);
|
const allMessages = await DatabaseStore.getConversationMessages(convId);
|
||||||
this.activeMessages = filterByLeafNodeId(
|
this.activeMessages = filterByLeafNodeId(
|
||||||
@@ -285,6 +303,47 @@ class ChatStore {
|
|||||||
return apiOptions;
|
return apiOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper methods for per-conversation loading state management
|
||||||
|
*/
|
||||||
|
private setConversationLoading(convId: string, loading: boolean): void {
|
||||||
|
if (loading) {
|
||||||
|
this.conversationLoadingStates.set(convId, true);
|
||||||
|
if (this.activeConversation?.id === convId) {
|
||||||
|
this.isLoading = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.conversationLoadingStates.delete(convId);
|
||||||
|
if (this.activeConversation?.id === convId) {
|
||||||
|
this.isLoading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private isConversationLoading(convId: string): boolean {
|
||||||
|
return this.conversationLoadingStates.get(convId) || false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private setConversationStreaming(convId: string, response: string, messageId: string): void {
|
||||||
|
this.conversationStreamingStates.set(convId, { response, messageId });
|
||||||
|
if (this.activeConversation?.id === convId) {
|
||||||
|
this.currentResponse = response;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private clearConversationStreaming(convId: string): void {
|
||||||
|
this.conversationStreamingStates.delete(convId);
|
||||||
|
if (this.activeConversation?.id === convId) {
|
||||||
|
this.currentResponse = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private getConversationStreaming(
|
||||||
|
convId: string
|
||||||
|
): { response: string; messageId: string } | undefined {
|
||||||
|
return this.conversationStreamingStates.get(convId);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles streaming chat completion with the AI model
|
* Handles streaming chat completion with the AI model
|
||||||
* @param allMessages - All messages in the conversation
|
* @param allMessages - All messages in the conversation
|
||||||
@@ -325,13 +384,20 @@ class ChatStore {
|
|||||||
};
|
};
|
||||||
|
|
||||||
slotsService.startStreaming();
|
slotsService.startStreaming();
|
||||||
|
slotsService.setActiveConversation(assistantMessage.convId);
|
||||||
|
|
||||||
await chatService.sendMessage(allMessages, {
|
await chatService.sendMessage(
|
||||||
|
allMessages,
|
||||||
|
{
|
||||||
...this.getApiOptions(),
|
...this.getApiOptions(),
|
||||||
|
|
||||||
onChunk: (chunk: string) => {
|
onChunk: (chunk: string) => {
|
||||||
streamedContent += chunk;
|
streamedContent += chunk;
|
||||||
this.currentResponse = streamedContent;
|
this.setConversationStreaming(
|
||||||
|
assistantMessage.convId,
|
||||||
|
streamedContent,
|
||||||
|
assistantMessage.id
|
||||||
|
);
|
||||||
|
|
||||||
captureModelIfNeeded();
|
captureModelIfNeeded();
|
||||||
const messageIndex = this.findMessageIndex(assistantMessage.id);
|
const messageIndex = this.findMessageIndex(assistantMessage.id);
|
||||||
@@ -388,30 +454,36 @@ class ChatStore {
|
|||||||
|
|
||||||
this.updateMessageAtIndex(messageIndex, localUpdateData);
|
this.updateMessageAtIndex(messageIndex, localUpdateData);
|
||||||
|
|
||||||
await DatabaseStore.updateCurrentNode(this.activeConversation!.id, assistantMessage.id);
|
await DatabaseStore.updateCurrentNode(assistantMessage.convId, assistantMessage.id);
|
||||||
this.activeConversation!.currNode = assistantMessage.id;
|
|
||||||
|
if (this.activeConversation?.id === assistantMessage.convId) {
|
||||||
|
this.activeConversation.currNode = assistantMessage.id;
|
||||||
await this.refreshActiveMessages();
|
await this.refreshActiveMessages();
|
||||||
|
}
|
||||||
|
|
||||||
if (onComplete) {
|
if (onComplete) {
|
||||||
await onComplete(streamedContent);
|
await onComplete(streamedContent);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.isLoading = false;
|
this.setConversationLoading(assistantMessage.convId, false);
|
||||||
this.currentResponse = '';
|
this.clearConversationStreaming(assistantMessage.convId);
|
||||||
|
slotsService.clearConversationState(assistantMessage.convId);
|
||||||
},
|
},
|
||||||
|
|
||||||
onError: (error: Error) => {
|
onError: (error: Error) => {
|
||||||
slotsService.stopStreaming();
|
slotsService.stopStreaming();
|
||||||
|
|
||||||
if (error.name === 'AbortError' || error instanceof DOMException) {
|
if (this.isAbortError(error)) {
|
||||||
this.isLoading = false;
|
this.setConversationLoading(assistantMessage.convId, false);
|
||||||
this.currentResponse = '';
|
this.clearConversationStreaming(assistantMessage.convId);
|
||||||
|
slotsService.clearConversationState(assistantMessage.convId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.error('Streaming error:', error);
|
console.error('Streaming error:', error);
|
||||||
this.isLoading = false;
|
this.setConversationLoading(assistantMessage.convId, false);
|
||||||
this.currentResponse = '';
|
this.clearConversationStreaming(assistantMessage.convId);
|
||||||
|
slotsService.clearConversationState(assistantMessage.convId);
|
||||||
|
|
||||||
const messageIndex = this.activeMessages.findIndex(
|
const messageIndex = this.activeMessages.findIndex(
|
||||||
(m: DatabaseMessage) => m.id === assistantMessage.id
|
(m: DatabaseMessage) => m.id === assistantMessage.id
|
||||||
@@ -435,15 +507,9 @@ class ChatStore {
|
|||||||
onError(error);
|
onError(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
}
|
assistantMessage.convId
|
||||||
|
);
|
||||||
private showErrorDialog(type: 'timeout' | 'server', message: string): void {
|
|
||||||
this.errorDialogState = { type, message };
|
|
||||||
}
|
|
||||||
|
|
||||||
dismissErrorDialog(): void {
|
|
||||||
this.errorDialogState = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -455,6 +521,14 @@ class ChatStore {
|
|||||||
return error instanceof Error && (error.name === 'AbortError' || error instanceof DOMException);
|
return error instanceof Error && (error.name === 'AbortError' || error instanceof DOMException);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private showErrorDialog(type: 'timeout' | 'server', message: string): void {
|
||||||
|
this.errorDialogState = { type, message };
|
||||||
|
}
|
||||||
|
|
||||||
|
dismissErrorDialog(): void {
|
||||||
|
this.errorDialogState = null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Finds the index of a message in the active messages array
|
* Finds the index of a message in the active messages array
|
||||||
* @param messageId - The message ID to find
|
* @param messageId - The message ID to find
|
||||||
@@ -519,7 +593,12 @@ class ChatStore {
|
|||||||
* @param extras - Optional extra data (files, attachments, etc.)
|
* @param extras - Optional extra data (files, attachments, etc.)
|
||||||
*/
|
*/
|
||||||
async sendMessage(content: string, extras?: DatabaseMessageExtra[]): Promise<void> {
|
async sendMessage(content: string, extras?: DatabaseMessageExtra[]): Promise<void> {
|
||||||
if ((!content.trim() && (!extras || extras.length === 0)) || this.isLoading) return;
|
if (!content.trim() && (!extras || extras.length === 0)) return;
|
||||||
|
|
||||||
|
if (this.activeConversation && this.isConversationLoading(this.activeConversation.id)) {
|
||||||
|
console.log('Cannot send message: current conversation is already processing a message');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let isNewConversation = false;
|
let isNewConversation = false;
|
||||||
|
|
||||||
@@ -534,8 +613,9 @@ class ChatStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.errorDialogState = null;
|
this.errorDialogState = null;
|
||||||
this.isLoading = true;
|
|
||||||
this.currentResponse = '';
|
this.setConversationLoading(this.activeConversation.id, true);
|
||||||
|
this.clearConversationStreaming(this.activeConversation.id);
|
||||||
|
|
||||||
let userMessage: DatabaseMessage | null = null;
|
let userMessage: DatabaseMessage | null = null;
|
||||||
|
|
||||||
@@ -546,7 +626,6 @@ class ChatStore {
|
|||||||
throw new Error('Failed to add user message');
|
throw new Error('Failed to add user message');
|
||||||
}
|
}
|
||||||
|
|
||||||
// If this is a new conversation, update the title with the first user prompt
|
|
||||||
if (isNewConversation && content) {
|
if (isNewConversation && content) {
|
||||||
const title = content.trim();
|
const title = content.trim();
|
||||||
await this.updateConversationName(this.activeConversation.id, title);
|
await this.updateConversationName(this.activeConversation.id, title);
|
||||||
@@ -559,19 +638,18 @@ class ChatStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.activeMessages.push(assistantMessage);
|
this.activeMessages.push(assistantMessage);
|
||||||
// Don't update currNode until after streaming completes to maintain proper conversation path
|
|
||||||
|
|
||||||
const conversationContext = this.activeMessages.slice(0, -1);
|
const conversationContext = this.activeMessages.slice(0, -1);
|
||||||
|
|
||||||
await this.streamChatCompletion(conversationContext, assistantMessage);
|
await this.streamChatCompletion(conversationContext, assistantMessage);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (this.isAbortError(error)) {
|
if (this.isAbortError(error)) {
|
||||||
this.isLoading = false;
|
this.setConversationLoading(this.activeConversation!.id, false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.error('Failed to send message:', error);
|
console.error('Failed to send message:', error);
|
||||||
this.isLoading = false;
|
this.setConversationLoading(this.activeConversation!.id, false);
|
||||||
if (!this.errorDialogState) {
|
if (!this.errorDialogState) {
|
||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
const dialogType = error.name === 'TimeoutError' ? 'timeout' : 'server';
|
const dialogType = error.name === 'TimeoutError' ? 'timeout' : 'server';
|
||||||
@@ -587,12 +665,19 @@ class ChatStore {
|
|||||||
* Stops the current message generation
|
* Stops the current message generation
|
||||||
* Aborts ongoing requests and saves partial response if available
|
* Aborts ongoing requests and saves partial response if available
|
||||||
*/
|
*/
|
||||||
stopGeneration(): void {
|
async stopGeneration(): Promise<void> {
|
||||||
|
if (!this.activeConversation) return;
|
||||||
|
|
||||||
|
const convId = this.activeConversation.id;
|
||||||
|
|
||||||
|
await this.savePartialResponseIfNeeded(convId);
|
||||||
|
|
||||||
slotsService.stopStreaming();
|
slotsService.stopStreaming();
|
||||||
chatService.abort();
|
chatService.abort(convId);
|
||||||
this.savePartialResponseIfNeeded();
|
|
||||||
this.isLoading = false;
|
this.setConversationLoading(convId, false);
|
||||||
this.currentResponse = '';
|
this.clearConversationStreaming(convId);
|
||||||
|
slotsService.clearConversationState(convId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -604,6 +689,9 @@ class ChatStore {
|
|||||||
slotsService.stopStreaming();
|
slotsService.stopStreaming();
|
||||||
chatService.abort();
|
chatService.abort();
|
||||||
await this.savePartialResponseIfNeeded();
|
await this.savePartialResponseIfNeeded();
|
||||||
|
|
||||||
|
this.conversationLoadingStates.clear();
|
||||||
|
this.conversationStreamingStates.clear();
|
||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
this.currentResponse = '';
|
this.currentResponse = '';
|
||||||
}
|
}
|
||||||
@@ -612,12 +700,23 @@ class ChatStore {
|
|||||||
* Saves partial response if generation was interrupted
|
* Saves partial response if generation was interrupted
|
||||||
* Preserves user's partial content and timing data when generation is stopped early
|
* Preserves user's partial content and timing data when generation is stopped early
|
||||||
*/
|
*/
|
||||||
private async savePartialResponseIfNeeded(): Promise<void> {
|
private async savePartialResponseIfNeeded(convId?: string): Promise<void> {
|
||||||
if (!this.currentResponse.trim() || !this.activeMessages.length) {
|
const conversationId = convId || this.activeConversation?.id;
|
||||||
|
if (!conversationId) return;
|
||||||
|
|
||||||
|
const streamingState = this.conversationStreamingStates.get(conversationId);
|
||||||
|
if (!streamingState || !streamingState.response.trim()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const lastMessage = this.activeMessages[this.activeMessages.length - 1];
|
const messages =
|
||||||
|
conversationId === this.activeConversation?.id
|
||||||
|
? this.activeMessages
|
||||||
|
: await DatabaseStore.getConversationMessages(conversationId);
|
||||||
|
|
||||||
|
if (!messages.length) return;
|
||||||
|
|
||||||
|
const lastMessage = messages[messages.length - 1];
|
||||||
|
|
||||||
if (lastMessage && lastMessage.role === 'assistant') {
|
if (lastMessage && lastMessage.role === 'assistant') {
|
||||||
try {
|
try {
|
||||||
@@ -626,7 +725,7 @@ class ChatStore {
|
|||||||
thinking?: string;
|
thinking?: string;
|
||||||
timings?: ChatMessageTimings;
|
timings?: ChatMessageTimings;
|
||||||
} = {
|
} = {
|
||||||
content: this.currentResponse
|
content: streamingState.response
|
||||||
};
|
};
|
||||||
|
|
||||||
if (lastMessage.thinking?.trim()) {
|
if (lastMessage.thinking?.trim()) {
|
||||||
@@ -640,7 +739,6 @@ class ChatStore {
|
|||||||
prompt_n: lastKnownState.promptTokens || 0,
|
prompt_n: lastKnownState.promptTokens || 0,
|
||||||
predicted_n: lastKnownState.tokensDecoded || 0,
|
predicted_n: lastKnownState.tokensDecoded || 0,
|
||||||
cache_n: lastKnownState.cacheTokens || 0,
|
cache_n: lastKnownState.cacheTokens || 0,
|
||||||
// We don't have ms data from the state, but we can estimate
|
|
||||||
predicted_ms:
|
predicted_ms:
|
||||||
lastKnownState.tokensPerSecond && lastKnownState.tokensDecoded
|
lastKnownState.tokensPerSecond && lastKnownState.tokensDecoded
|
||||||
? (lastKnownState.tokensDecoded / lastKnownState.tokensPerSecond) * 1000
|
? (lastKnownState.tokensDecoded / lastKnownState.tokensPerSecond) * 1000
|
||||||
@@ -701,7 +799,6 @@ class ChatStore {
|
|||||||
this.updateMessageAtIndex(messageIndex, { content: newContent });
|
this.updateMessageAtIndex(messageIndex, { content: newContent });
|
||||||
await DatabaseStore.updateMessage(messageId, { content: newContent });
|
await DatabaseStore.updateMessage(messageId, { content: newContent });
|
||||||
|
|
||||||
// If this is the first user message, update the conversation title with confirmation if needed
|
|
||||||
if (isFirstUserMessage && newContent.trim()) {
|
if (isFirstUserMessage && newContent.trim()) {
|
||||||
await this.updateConversationTitleWithConfirmation(
|
await this.updateConversationTitleWithConfirmation(
|
||||||
this.activeConversation.id,
|
this.activeConversation.id,
|
||||||
@@ -718,8 +815,8 @@ class ChatStore {
|
|||||||
this.activeMessages = this.activeMessages.slice(0, messageIndex + 1);
|
this.activeMessages = this.activeMessages.slice(0, messageIndex + 1);
|
||||||
this.updateConversationTimestamp();
|
this.updateConversationTimestamp();
|
||||||
|
|
||||||
this.isLoading = true;
|
this.setConversationLoading(this.activeConversation.id, true);
|
||||||
this.currentResponse = '';
|
this.clearConversationStreaming(this.activeConversation.id);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const assistantMessage = await this.createAssistantMessage();
|
const assistantMessage = await this.createAssistantMessage();
|
||||||
@@ -742,7 +839,7 @@ class ChatStore {
|
|||||||
);
|
);
|
||||||
} catch (regenerateError) {
|
} catch (regenerateError) {
|
||||||
console.error('Failed to regenerate response:', regenerateError);
|
console.error('Failed to regenerate response:', regenerateError);
|
||||||
this.isLoading = false;
|
this.setConversationLoading(this.activeConversation!.id, false);
|
||||||
|
|
||||||
const messageIndex = this.findMessageIndex(messageId);
|
const messageIndex = this.findMessageIndex(messageId);
|
||||||
this.updateMessageAtIndex(messageIndex, { content: originalContent });
|
this.updateMessageAtIndex(messageIndex, { content: originalContent });
|
||||||
@@ -784,8 +881,8 @@ class ChatStore {
|
|||||||
this.activeMessages = this.activeMessages.slice(0, messageIndex);
|
this.activeMessages = this.activeMessages.slice(0, messageIndex);
|
||||||
this.updateConversationTimestamp();
|
this.updateConversationTimestamp();
|
||||||
|
|
||||||
this.isLoading = true;
|
this.setConversationLoading(this.activeConversation.id, true);
|
||||||
this.currentResponse = '';
|
this.clearConversationStreaming(this.activeConversation.id);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const parentMessageId =
|
const parentMessageId =
|
||||||
@@ -806,7 +903,7 @@ class ChatStore {
|
|||||||
await this.streamChatCompletion(conversationContext, assistantMessage);
|
await this.streamChatCompletion(conversationContext, assistantMessage);
|
||||||
} catch (regenerateError) {
|
} catch (regenerateError) {
|
||||||
console.error('Failed to regenerate response:', regenerateError);
|
console.error('Failed to regenerate response:', regenerateError);
|
||||||
this.isLoading = false;
|
this.setConversationLoading(this.activeConversation!.id, false);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (this.isAbortError(error)) return;
|
if (this.isAbortError(error)) return;
|
||||||
@@ -862,7 +959,6 @@ class ChatStore {
|
|||||||
try {
|
try {
|
||||||
const currentConfig = config();
|
const currentConfig = config();
|
||||||
|
|
||||||
// Only ask for confirmation if the setting is enabled and callback is provided
|
|
||||||
if (currentConfig.askForTitleConfirmation && onConfirmationNeeded) {
|
if (currentConfig.askForTitleConfirmation && onConfirmationNeeded) {
|
||||||
const conversation = await DatabaseStore.getConversation(convId);
|
const conversation = await DatabaseStore.getConversation(convId);
|
||||||
if (!conversation) return false;
|
if (!conversation) return false;
|
||||||
@@ -1170,14 +1266,16 @@ class ChatStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clears the active conversation and resets state
|
* Clears the active conversation and messages
|
||||||
* Used when navigating away from chat or starting fresh
|
* Used when navigating away from chat or starting fresh
|
||||||
|
* Note: Does not stop ongoing streaming to allow background completion
|
||||||
*/
|
*/
|
||||||
clearActiveConversation(): void {
|
clearActiveConversation(): void {
|
||||||
this.activeConversation = null;
|
this.activeConversation = null;
|
||||||
this.activeMessages = [];
|
this.activeMessages = [];
|
||||||
this.currentResponse = '';
|
|
||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
|
this.currentResponse = '';
|
||||||
|
slotsService.setActiveConversation(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Refreshes active messages based on currNode after branch navigation */
|
/** Refreshes active messages based on currNode after branch navigation */
|
||||||
@@ -1419,8 +1517,8 @@ class ChatStore {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.isLoading = true;
|
this.setConversationLoading(this.activeConversation.id, true);
|
||||||
this.currentResponse = '';
|
this.clearConversationStreaming(this.activeConversation.id);
|
||||||
|
|
||||||
const newAssistantMessage = await DatabaseStore.createMessageBranch(
|
const newAssistantMessage = await DatabaseStore.createMessageBranch(
|
||||||
{
|
{
|
||||||
@@ -1454,7 +1552,7 @@ class ChatStore {
|
|||||||
if (this.isAbortError(error)) return;
|
if (this.isAbortError(error)) return;
|
||||||
|
|
||||||
console.error('Failed to regenerate message with branching:', error);
|
console.error('Failed to regenerate message with branching:', error);
|
||||||
this.isLoading = false;
|
this.setConversationLoading(this.activeConversation!.id, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1466,8 +1564,8 @@ class ChatStore {
|
|||||||
if (!this.activeConversation) return;
|
if (!this.activeConversation) return;
|
||||||
|
|
||||||
this.errorDialogState = null;
|
this.errorDialogState = null;
|
||||||
this.isLoading = true;
|
this.setConversationLoading(this.activeConversation.id, true);
|
||||||
this.currentResponse = '';
|
this.clearConversationStreaming(this.activeConversation.id);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Get conversation path up to the user message
|
// Get conversation path up to the user message
|
||||||
@@ -1499,9 +1597,30 @@ class ChatStore {
|
|||||||
await this.streamChatCompletion(conversationPath, assistantMessage);
|
await this.streamChatCompletion(conversationPath, assistantMessage);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to generate response:', error);
|
console.error('Failed to generate response:', error);
|
||||||
this.isLoading = false;
|
this.setConversationLoading(this.activeConversation!.id, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Public methods for accessing per-conversation states
|
||||||
|
*/
|
||||||
|
public isConversationLoadingPublic(convId: string): boolean {
|
||||||
|
return this.isConversationLoading(convId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getConversationStreamingPublic(
|
||||||
|
convId: string
|
||||||
|
): { response: string; messageId: string } | undefined {
|
||||||
|
return this.getConversationStreaming(convId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getAllLoadingConversations(): string[] {
|
||||||
|
return Array.from(this.conversationLoadingStates.keys());
|
||||||
|
}
|
||||||
|
|
||||||
|
public getAllStreamingConversations(): string[] {
|
||||||
|
return Array.from(this.conversationStreamingStates.keys());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const chatStore = new ChatStore();
|
export const chatStore = new ChatStore();
|
||||||
@@ -1541,3 +1660,11 @@ export function stopGeneration() {
|
|||||||
chatStore.stopGeneration();
|
chatStore.stopGeneration();
|
||||||
}
|
}
|
||||||
export const messages = () => chatStore.activeMessages;
|
export const messages = () => chatStore.activeMessages;
|
||||||
|
|
||||||
|
// Per-conversation state access
|
||||||
|
export const isConversationLoading = (convId: string) =>
|
||||||
|
chatStore.isConversationLoadingPublic(convId);
|
||||||
|
export const getConversationStreaming = (convId: string) =>
|
||||||
|
chatStore.getConversationStreamingPublic(convId);
|
||||||
|
export const getAllLoadingConversations = () => chatStore.getAllLoadingConversations();
|
||||||
|
export const getAllStreamingConversations = () => chatStore.getAllStreamingConversations();
|
||||||
|
|||||||
@@ -1,45 +1,26 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import { page } from '$app/state';
|
import { page } from '$app/state';
|
||||||
import { beforeNavigate } from '$app/navigation';
|
|
||||||
import { ChatScreen } from '$lib/components/app';
|
import { ChatScreen } from '$lib/components/app';
|
||||||
import {
|
import {
|
||||||
chatStore,
|
chatStore,
|
||||||
activeConversation,
|
activeConversation,
|
||||||
isLoading,
|
isLoading,
|
||||||
stopGeneration,
|
stopGeneration
|
||||||
gracefulStop
|
|
||||||
} from '$lib/stores/chat.svelte';
|
} from '$lib/stores/chat.svelte';
|
||||||
import { onDestroy } from 'svelte';
|
|
||||||
|
|
||||||
let chatId = $derived(page.params.id);
|
let chatId = $derived(page.params.id);
|
||||||
let currentChatId: string | undefined = undefined;
|
let currentChatId: string | undefined = undefined;
|
||||||
|
|
||||||
beforeNavigate(async ({ cancel, to }) => {
|
|
||||||
if (isLoading()) {
|
|
||||||
console.log(
|
|
||||||
'Navigation detected while streaming - aborting stream and saving partial response'
|
|
||||||
);
|
|
||||||
|
|
||||||
cancel();
|
|
||||||
|
|
||||||
await gracefulStop();
|
|
||||||
|
|
||||||
if (to?.url) {
|
|
||||||
await goto(to.url.pathname + to.url.search + to.url.hash);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (chatId && chatId !== currentChatId) {
|
if (chatId && chatId !== currentChatId) {
|
||||||
if (isLoading()) {
|
|
||||||
console.log('Chat switch detected while streaming - aborting stream');
|
|
||||||
stopGeneration();
|
|
||||||
}
|
|
||||||
|
|
||||||
currentChatId = chatId;
|
currentChatId = chatId;
|
||||||
|
|
||||||
|
// Skip loading if this conversation is already active (e.g., just created)
|
||||||
|
if (activeConversation()?.id === chatId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
const success = await chatStore.loadConversation(chatId);
|
const success = await chatStore.loadConversation(chatId);
|
||||||
|
|
||||||
@@ -66,12 +47,6 @@
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
onDestroy(() => {
|
|
||||||
if (isLoading()) {
|
|
||||||
stopGeneration();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ const config = {
|
|||||||
// Consult https://svelte.dev/docs/kit/integrations
|
// Consult https://svelte.dev/docs/kit/integrations
|
||||||
// for more information about preprocessors
|
// for more information about preprocessors
|
||||||
preprocess: [vitePreprocess(), mdsvex()],
|
preprocess: [vitePreprocess(), mdsvex()],
|
||||||
|
|
||||||
kit: {
|
kit: {
|
||||||
paths: {
|
paths: {
|
||||||
relative: true
|
relative: true
|
||||||
@@ -23,6 +24,7 @@ const config = {
|
|||||||
bundleStrategy: 'inline'
|
bundleStrategy: 'inline'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
extensions: ['.svelte', '.svx']
|
extensions: ['.svelte', '.svx']
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -75,7 +75,12 @@ function llamaCppBuildPlugin() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
|
build: {
|
||||||
|
chunkSizeWarningLimit: 3072
|
||||||
|
},
|
||||||
|
|
||||||
plugins: [tailwindcss(), sveltekit(), devtoolsJson(), llamaCppBuildPlugin()],
|
plugins: [tailwindcss(), sveltekit(), devtoolsJson(), llamaCppBuildPlugin()],
|
||||||
|
|
||||||
test: {
|
test: {
|
||||||
projects: [
|
projects: [
|
||||||
{
|
{
|
||||||
@@ -123,6 +128,7 @@ export default defineConfig({
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
server: {
|
server: {
|
||||||
proxy: {
|
proxy: {
|
||||||
'/v1': 'http://localhost:8080',
|
'/v1': 'http://localhost:8080',
|
||||||
|
|||||||
Reference in New Issue
Block a user