@jskit-ai/assistant-core 0.1.180 → 0.1.181
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -8,14 +8,14 @@
|
|
|
8
8
|
"build": "vite build"
|
|
9
9
|
},
|
|
10
10
|
"dependencies": {
|
|
11
|
-
"@jskit-ai/assistant-core": "0.1.
|
|
12
|
-
"@jskit-ai/http-runtime": "0.1.
|
|
13
|
-
"@jskit-ai/kernel": "0.1.
|
|
11
|
+
"@jskit-ai/assistant-core": "0.1.181",
|
|
12
|
+
"@jskit-ai/http-runtime": "0.1.201",
|
|
13
|
+
"@jskit-ai/kernel": "0.1.203",
|
|
14
14
|
"@mdi/js": "^7.4.47",
|
|
15
15
|
"vue": "^3.5.13",
|
|
16
16
|
"vuetify": "^4.0.0",
|
|
17
|
-
"@jskit-ai/connectors-core": "0.1.
|
|
18
|
-
"@jskit-ai/connectors-catalog": "0.1.
|
|
17
|
+
"@jskit-ai/connectors-core": "0.1.20",
|
|
18
|
+
"@jskit-ai/connectors-catalog": "0.1.20"
|
|
19
19
|
},
|
|
20
20
|
"devDependencies": {
|
|
21
21
|
"@vitejs/plugin-vue": "^6.0.7",
|
|
@@ -7,6 +7,7 @@ const draft = ref("");
|
|
|
7
7
|
const query = new URLSearchParams(location.search);
|
|
8
8
|
const controls = query.has("controls");
|
|
9
9
|
const deferredHistory = query.has("history");
|
|
10
|
+
const pagedHistory = query.has("paged-history");
|
|
10
11
|
const streaming = query.has("streaming");
|
|
11
12
|
const liveText = ref("");
|
|
12
13
|
const liveMessage = computed(() => ({ messageId: "stream-answer", role: "assistant", text: liveText.value, status: "inProgress" }));
|
|
@@ -16,11 +17,18 @@ const loadMoreError = ref("");
|
|
|
16
17
|
const historyRequests = ref(0);
|
|
17
18
|
const hasMoreBefore = ref(controls);
|
|
18
19
|
let completeHistory = null;
|
|
20
|
+
function historyPage(first) {
|
|
21
|
+
return Array.from({ length: 20 }, (_, index) => ["user", "assistant"].map(role => ({
|
|
22
|
+
id: `${role}-${first + index}`, role, status: "completed",
|
|
23
|
+
text: `${role} message ${first + index}. A deliberately detailed update keeps the conversation tall enough to exercise the real overflow container.`
|
|
24
|
+
}))).flat();
|
|
25
|
+
}
|
|
19
26
|
const messages = ref(Array.from({ length: 70 }, (_, index) => ({
|
|
20
27
|
id: `message-${index}`, role: index % 2 ? "assistant" : "user", status: "completed",
|
|
21
28
|
text: `Conversation line ${index + 1}: responsive scroll containment keeps the composer visible.\n\nMore text with **formatting**, a [link](https://example.com), and a_long_word_that_must_wrap_in_narrow_panes_without_overflow.`
|
|
22
29
|
})));
|
|
23
30
|
if (controls) messages.value[0].text = messages.value[0].text.repeat(4);
|
|
31
|
+
if (pagedHistory) messages.value = historyPage(21);
|
|
24
32
|
if (streaming) messages.value.push({ id: "stream-question", role: "user", text: "Show the answer as it arrives." });
|
|
25
33
|
function finishStream() {
|
|
26
34
|
messages.value.push({ ...liveMessage.value, status: "completed" });
|
|
@@ -29,7 +37,12 @@ function finishStream() {
|
|
|
29
37
|
function finishHistory(error = "") {
|
|
30
38
|
loadMoreError.value = error;
|
|
31
39
|
if (!error) {
|
|
32
|
-
|
|
40
|
+
if (pagedHistory) {
|
|
41
|
+
messages.value.unshift(...historyPage(1));
|
|
42
|
+
hasMoreBefore.value = false;
|
|
43
|
+
} else {
|
|
44
|
+
messages.value.unshift({ id: `older-${historyRequests.value}`, role: "assistant", text: "Older conversation paragraph. ".repeat(40), status: "completed" });
|
|
45
|
+
}
|
|
33
46
|
}
|
|
34
47
|
loadingMore.value = false;
|
|
35
48
|
completeHistory({ changed: !error });
|
|
@@ -57,7 +70,7 @@ const adapter = computed(() => ({
|
|
|
57
70
|
<template>
|
|
58
71
|
<v-app>
|
|
59
72
|
<v-main>
|
|
60
|
-
<main class="assistant-responsive-fixture">
|
|
73
|
+
<main class="assistant-responsive-fixture" :class="{ 'assistant-responsive-fixture--paged': pagedHistory }">
|
|
61
74
|
<nav v-if="streaming">
|
|
62
75
|
<button @click="liveText += 'Growing answer. '">Receive text</button>
|
|
63
76
|
<button @click="finishStream">Finish answer</button>
|
|
@@ -80,6 +93,7 @@ const adapter = computed(() => ({
|
|
|
80
93
|
<style>
|
|
81
94
|
html, body, #app { height: 100%; margin: 0; min-height: 0; }
|
|
82
95
|
.assistant-responsive-fixture { display: flex; flex-direction: column; height: 100dvh; min-height: 0; padding: .5rem; }
|
|
96
|
+
.assistant-responsive-fixture--paged { width: min(420px, 100%); margin: auto; }
|
|
83
97
|
.assistant-responsive-fixture nav { display: flex; flex: 0 0 auto; flex-wrap: wrap; gap: 1rem; }
|
|
84
98
|
.assistant-responsive-fixture__conversation { flex: 1 1 auto; }
|
|
85
99
|
</style>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jskit-ai/assistant-core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.181",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "node --test"
|
|
@@ -34,8 +34,8 @@
|
|
|
34
34
|
"@ai-sdk/perplexity": "^4.0.43",
|
|
35
35
|
"@ai-sdk/togetherai": "^3.0.49",
|
|
36
36
|
"@ai-sdk/xai": "^4.0.58",
|
|
37
|
-
"@jskit-ai/resource-core": "0.1.
|
|
38
|
-
"@jskit-ai/resource-crud-core": "0.1.
|
|
37
|
+
"@jskit-ai/resource-core": "0.1.145",
|
|
38
|
+
"@jskit-ai/resource-crud-core": "0.1.145",
|
|
39
39
|
"@mdi/js": "^7.4.47",
|
|
40
40
|
"@openrouter/ai-sdk-provider": "^3.0.0",
|
|
41
41
|
"ai": "^7.0.101",
|
|
@@ -44,8 +44,8 @@
|
|
|
44
44
|
"ws": "^8.20.1"
|
|
45
45
|
},
|
|
46
46
|
"peerDependencies": {
|
|
47
|
-
"@jskit-ai/http-runtime": "0.1.
|
|
48
|
-
"@jskit-ai/kernel": "0.1.
|
|
47
|
+
"@jskit-ai/http-runtime": "0.1.201",
|
|
48
|
+
"@jskit-ai/kernel": "0.1.203",
|
|
49
49
|
"vue": "^3.5.13",
|
|
50
50
|
"vuetify": "^4.0.0"
|
|
51
51
|
},
|
|
@@ -774,6 +774,7 @@ function updateLatestFollowFromScroll(event = {}) {
|
|
|
774
774
|
// Keep following the reader while the request is pending, before rows prepend.
|
|
775
775
|
if (loadMoreScrollSnapshot && target.scrollHeight === loadMoreScrollSnapshot.scrollHeight) {
|
|
776
776
|
loadMoreScrollSnapshot.scrollTop = scrollTop;
|
|
777
|
+
loadMoreScrollSnapshot.anchor = visibleHistoryAnchor(target);
|
|
777
778
|
}
|
|
778
779
|
const shouldFollow = scrollElementNearBottom(target);
|
|
779
780
|
if (!shouldFollow && !userScrollIntent.value && followingLatest.value) {
|
|
@@ -797,6 +798,17 @@ function updateLatestFollowFromScroll(event = {}) {
|
|
|
797
798
|
}
|
|
798
799
|
}
|
|
799
800
|
|
|
801
|
+
function visibleHistoryAnchor(element) {
|
|
802
|
+
const top = element.getBoundingClientRect().top;
|
|
803
|
+
for (const turn of element.querySelectorAll(".assistant-transcript__turn")) {
|
|
804
|
+
const bounds = turn.getBoundingClientRect();
|
|
805
|
+
if (bounds.bottom > top) {
|
|
806
|
+
return { element: turn, offset: bounds.top - top };
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
return null;
|
|
810
|
+
}
|
|
811
|
+
|
|
800
812
|
function requestLoadMore() {
|
|
801
813
|
if (!props.hasMoreBefore || props.loadingMore || loadMoreScrollSnapshot) {
|
|
802
814
|
return;
|
|
@@ -809,6 +821,7 @@ function requestLoadMore() {
|
|
|
809
821
|
loadMoreRequestVersion = version;
|
|
810
822
|
loadMoreScrollSnapshot = element
|
|
811
823
|
? {
|
|
824
|
+
anchor: visibleHistoryAnchor(element),
|
|
812
825
|
scrollHeight: element.scrollHeight,
|
|
813
826
|
scrollKey: props.scrollKey,
|
|
814
827
|
scrollTop: element.scrollTop,
|
|
@@ -842,7 +855,12 @@ async function completeLoadMoreRequest(version, changed) {
|
|
|
842
855
|
}
|
|
843
856
|
const element = bodyElement.value;
|
|
844
857
|
if (element) {
|
|
845
|
-
|
|
858
|
+
// Offscreen turns use estimated heights. Preserve the visible turn instead
|
|
859
|
+
// of moving by the total height difference, which can change during layout.
|
|
860
|
+
const anchor = snapshot.anchor;
|
|
861
|
+
previousScrollTop = anchor && element.contains(anchor.element)
|
|
862
|
+
? element.scrollTop + anchor.element.getBoundingClientRect().top - element.getBoundingClientRect().top - anchor.offset
|
|
863
|
+
: snapshot.scrollTop + (element.scrollHeight - snapshot.scrollHeight);
|
|
846
864
|
element.scrollTop = previousScrollTop;
|
|
847
865
|
}
|
|
848
866
|
clearLoadMoreScrollSnapshot();
|
|
@@ -219,6 +219,41 @@ test("conversation scrolling preserves history anchors and resets expansion on s
|
|
|
219
219
|
}
|
|
220
220
|
});
|
|
221
221
|
|
|
222
|
+
test("a delayed final history page preserves the visible message at every width", {
|
|
223
|
+
skip: !RUN_BROWSER_TEST,
|
|
224
|
+
timeout: 60_000
|
|
225
|
+
}, async () => {
|
|
226
|
+
const vite = await startViteFixture({ fixtureRoot: FIXTURE_ROOT });
|
|
227
|
+
const browser = await chromium.launch(createChromiumLaunchOptions());
|
|
228
|
+
try {
|
|
229
|
+
for (const width of [390, 800, 1365]) {
|
|
230
|
+
const page = await browser.newPage({ viewport: { width, height: 900 } });
|
|
231
|
+
await page.goto(`${vite.baseURL}/?controls=1&history=1&paged-history=1`);
|
|
232
|
+
const body = page.locator(".assistant-transcript__body");
|
|
233
|
+
await expect(body).toBeVisible();
|
|
234
|
+
await expect.poll(() => body.evaluate(element => element.scrollHeight - element.clientHeight - element.scrollTop)).toBeLessThan(2);
|
|
235
|
+
await body.evaluate(element => {
|
|
236
|
+
element.dispatchEvent(new WheelEvent("wheel", { bubbles: true, deltaY: -1000 }));
|
|
237
|
+
element.scrollTop = 0;
|
|
238
|
+
element.dispatchEvent(new Event("scroll", { bubbles: true }));
|
|
239
|
+
});
|
|
240
|
+
await expect(page.getByRole("button", { name: "Loading older messages…", exact: true })).toBeDisabled();
|
|
241
|
+
const anchor = page.getByText("user message 21.", { exact: false });
|
|
242
|
+
const before = (await anchor.boundingBox()).y;
|
|
243
|
+
await page.getByRole("button", { name: "Complete history load", exact: true }).evaluate(element => element.click());
|
|
244
|
+
await expect(page.getByText("user message 1.", { exact: false })).toHaveCount(1);
|
|
245
|
+
await expect(page.getByRole("button", { name: "Loading older messages…", exact: true })).toHaveCount(0);
|
|
246
|
+
await expect.poll(async () => Math.abs((await anchor.boundingBox()).y - before)).toBeLessThan(2);
|
|
247
|
+
await expect(page.locator("output")).toHaveText("History requests: 1");
|
|
248
|
+
assert.ok(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth));
|
|
249
|
+
await page.close();
|
|
250
|
+
}
|
|
251
|
+
} finally {
|
|
252
|
+
await browser.close();
|
|
253
|
+
await stopProcess(vite);
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
|
|
222
257
|
test("upward scrolling loads older history once and preserves retry and selection behavior", {
|
|
223
258
|
skip: !RUN_BROWSER_TEST,
|
|
224
259
|
timeout: 90_000
|
|
@@ -249,14 +284,14 @@ test("upward scrolling loads older history once and preserves retry and selectio
|
|
|
249
284
|
const cdp = await page.context().newCDPSession(page);
|
|
250
285
|
const x = bounds.x + bounds.width / 2;
|
|
251
286
|
const y = bounds.y + 40;
|
|
287
|
+
const stopped = body.evaluate(element => new Promise(resolve => {
|
|
288
|
+
element.addEventListener("scrollend", () => resolve(), { once: true });
|
|
289
|
+
}));
|
|
252
290
|
await cdp.send("Input.dispatchTouchEvent", { type: "touchStart", touchPoints: [{ x, y }] });
|
|
253
291
|
for (let offset = 20; offset <= 380; offset += 20) {
|
|
254
292
|
await cdp.send("Input.dispatchTouchEvent", { type: "touchMove", touchPoints: [{ x, y: y + offset }] });
|
|
255
293
|
await page.evaluate(() => new Promise(requestAnimationFrame));
|
|
256
294
|
}
|
|
257
|
-
const stopped = body.evaluate(element => new Promise(resolve => {
|
|
258
|
-
element.addEventListener("scrollend", () => resolve(), { once: true });
|
|
259
|
-
}));
|
|
260
295
|
await cdp.send("Input.dispatchTouchEvent", { type: "touchEnd", touchPoints: [] });
|
|
261
296
|
await stopped;
|
|
262
297
|
await cdp.detach();
|