@jskit-ai/assistant-core 0.1.172 → 0.1.173
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.
- package/examples/conversation/package.json +5 -5
- package/fixtures/responsive-assistant/ProgressFixture.vue +61 -0
- package/fixtures/responsive-assistant/main.js +3 -1
- package/package.json +5 -5
- package/src/client/conversation/AssistantConversationElement.vue +5 -2
- package/src/client/conversation/AssistantTranscript.vue +138 -174
- package/test/assistantScroll.browser.test.js +73 -0
|
@@ -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.173",
|
|
12
|
+
"@jskit-ai/http-runtime": "0.1.193",
|
|
13
|
+
"@jskit-ai/kernel": "0.1.195",
|
|
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.12",
|
|
18
|
+
"@jskit-ai/connectors-catalog": "0.1.12"
|
|
19
19
|
},
|
|
20
20
|
"devDependencies": {
|
|
21
21
|
"@vitejs/plugin-vue": "^6.0.7",
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { computed, ref } from "vue";
|
|
3
|
+
import AssistantConversationElement from "../../src/client/conversation/AssistantConversationElement.vue";
|
|
4
|
+
import { conversationTurnsFromMessages } from "../../src/shared/conversation/turns.js";
|
|
5
|
+
|
|
6
|
+
const working = ref(true);
|
|
7
|
+
const draft = ref("");
|
|
8
|
+
const scope = ref("first");
|
|
9
|
+
const combined = ref(false);
|
|
10
|
+
const messages = ref([1, 2, 3].map(index => ({
|
|
11
|
+
messageId: `progress-${index}`, role: "thinking", text: `Reasoning ${index}`
|
|
12
|
+
})));
|
|
13
|
+
let sequence = 3;
|
|
14
|
+
function append(role) {
|
|
15
|
+
sequence += 1;
|
|
16
|
+
messages.value.push({ messageId: `message-${sequence}`, role, text: `${role} ${sequence}` });
|
|
17
|
+
}
|
|
18
|
+
const adapter = computed(() => ({
|
|
19
|
+
conversation: {
|
|
20
|
+
working: working.value, scrollKey: scope.value, hasMoreBefore: true,
|
|
21
|
+
turns: combined.value
|
|
22
|
+
? conversationTurnsFromMessages(messages.value)
|
|
23
|
+
: messages.value.map(message => ({
|
|
24
|
+
turnId: `row-${message.messageId}`,
|
|
25
|
+
...(message.role === "system" || message.role === "user"
|
|
26
|
+
? { [message.role]: { messageId: message.messageId, text: message.text } } : {}),
|
|
27
|
+
messages: [message]
|
|
28
|
+
}))
|
|
29
|
+
},
|
|
30
|
+
composer: { draft: draft.value, canSend: Boolean(draft.value), canStop: working.value },
|
|
31
|
+
actions: {
|
|
32
|
+
setDraft(value) { draft.value = value; },
|
|
33
|
+
stop() { working.value = false; },
|
|
34
|
+
loadMore({ complete }) {
|
|
35
|
+
messages.value.unshift({ messageId: `older-${sequence++}`, role: "thinking", text: "Older reasoning" });
|
|
36
|
+
complete({ changed: true });
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}));
|
|
40
|
+
</script>
|
|
41
|
+
<template>
|
|
42
|
+
<v-app>
|
|
43
|
+
<v-main>
|
|
44
|
+
<main class="progress-fixture">
|
|
45
|
+
<nav>
|
|
46
|
+
<button @click="working = !working">Toggle working</button>
|
|
47
|
+
<button @click="combined = !combined">Change storage rows</button>
|
|
48
|
+
<button @click="scope = scope === 'first' ? 'second' : 'first'">Change conversation</button>
|
|
49
|
+
<button v-for="role in ['thinking', 'commentary', 'assistant', 'user', 'system']" :key="role" @click="append(role)">Append {{ role }}</button>
|
|
50
|
+
</nav>
|
|
51
|
+
<AssistantConversationElement :adapter="adapter" />
|
|
52
|
+
</main>
|
|
53
|
+
</v-main>
|
|
54
|
+
</v-app>
|
|
55
|
+
</template>
|
|
56
|
+
<style scoped>
|
|
57
|
+
.progress-fixture { display: flex; flex-direction: column; height: 100dvh; padding: 8px; }
|
|
58
|
+
nav { display: flex; flex-wrap: wrap; gap: 8px; flex: 0 0 auto; }
|
|
59
|
+
nav button { padding: 8px; min-height: 48px; border: 1px solid; }
|
|
60
|
+
.assistant-conversation { flex: 1; }
|
|
61
|
+
</style>
|
|
@@ -6,6 +6,7 @@ import * as directives from "vuetify/directives";
|
|
|
6
6
|
import { aliases as mdiAliases, mdi } from "vuetify/iconsets/mdi-svg";
|
|
7
7
|
import App from "./App.vue";
|
|
8
8
|
import ConversationFixture from "./ConversationFixture.vue";
|
|
9
|
+
import ProgressFixture from "./ProgressFixture.vue";
|
|
9
10
|
|
|
10
11
|
const vuetify = createVuetify({
|
|
11
12
|
components,
|
|
@@ -20,6 +21,7 @@ const vuetify = createVuetify({
|
|
|
20
21
|
}
|
|
21
22
|
});
|
|
22
23
|
|
|
23
|
-
|
|
24
|
+
const query = new URLSearchParams(location.search);
|
|
25
|
+
createApp(query.has("progress") ? ProgressFixture : query.has("conversation") ? ConversationFixture : App)
|
|
24
26
|
.use(vuetify)
|
|
25
27
|
.mount("#app");
|
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.173",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "node --test"
|
|
@@ -33,8 +33,8 @@
|
|
|
33
33
|
"@ai-sdk/perplexity": "^4.0.43",
|
|
34
34
|
"@ai-sdk/togetherai": "^3.0.49",
|
|
35
35
|
"@ai-sdk/xai": "^4.0.58",
|
|
36
|
-
"@jskit-ai/resource-core": "0.1.
|
|
37
|
-
"@jskit-ai/resource-crud-core": "0.1.
|
|
36
|
+
"@jskit-ai/resource-core": "0.1.137",
|
|
37
|
+
"@jskit-ai/resource-crud-core": "0.1.137",
|
|
38
38
|
"@mdi/js": "^7.4.47",
|
|
39
39
|
"@openrouter/ai-sdk-provider": "^3.0.0",
|
|
40
40
|
"ai": "^7.0.101",
|
|
@@ -43,8 +43,8 @@
|
|
|
43
43
|
"ws": "^8.20.1"
|
|
44
44
|
},
|
|
45
45
|
"peerDependencies": {
|
|
46
|
-
"@jskit-ai/http-runtime": "0.1.
|
|
47
|
-
"@jskit-ai/kernel": "0.1.
|
|
46
|
+
"@jskit-ai/http-runtime": "0.1.193",
|
|
47
|
+
"@jskit-ai/kernel": "0.1.195",
|
|
48
48
|
"vue": "^3.5.13",
|
|
49
49
|
"vuetify": "^4.0.0"
|
|
50
50
|
},
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<section class="assistant-conversation" :aria-label="label">
|
|
3
3
|
<AssistantTranscript
|
|
4
|
-
v-bind="adapter.conversation" class="assistant-conversation__transcript"
|
|
4
|
+
v-bind="adapter.conversation" :working="working" class="assistant-conversation__transcript"
|
|
5
5
|
@load-more="adapter.actions?.loadMore?.($event)" @reload="adapter.actions?.reload?.()"
|
|
6
6
|
@resend-turn="adapter.actions?.resend?.($event)" @cancel-turn="adapter.actions?.cancel?.($event)"
|
|
7
7
|
@edit-turn="adapter.actions?.edit?.($event)" @link-click="adapter.actions?.openLink?.($event)"
|
|
@@ -115,9 +115,12 @@ const fileInput = ref(null);
|
|
|
115
115
|
const modelsOpen = ref(false);
|
|
116
116
|
const delivery = ref(null);
|
|
117
117
|
const statusId = `assistant-status-${useId()}`;
|
|
118
|
+
const working = computed(() => props.adapter.conversation.working ?? Boolean(
|
|
119
|
+
props.adapter.composer?.canStop || props.adapter.conversation.turns?.some(turn => turn.pending)
|
|
120
|
+
));
|
|
118
121
|
const activity = computed(() => props.adapter.activity ?? {
|
|
119
122
|
label: props.adapter.composer?.stopPending ? "Stopping…"
|
|
120
|
-
:
|
|
123
|
+
: working.value
|
|
121
124
|
? "Assistant is working…" : props.adapter.composer?.pending ? "Sending to assistant…" : ""
|
|
122
125
|
});
|
|
123
126
|
const suggestionPreview = computed(() => !props.adapter.composer?.draft && props.adapter.suggestions?.visible
|
|
@@ -98,12 +98,12 @@
|
|
|
98
98
|
</div>
|
|
99
99
|
|
|
100
100
|
<article
|
|
101
|
-
v-for="
|
|
102
|
-
:key="
|
|
101
|
+
v-for="entry in displayEntries"
|
|
102
|
+
:key="entry.key"
|
|
103
103
|
class="assistant-transcript__turn"
|
|
104
104
|
>
|
|
105
105
|
<div
|
|
106
|
-
v-if="
|
|
106
|
+
v-if="entry.role === 'system'"
|
|
107
107
|
class="assistant-transcript__system"
|
|
108
108
|
>
|
|
109
109
|
<v-icon
|
|
@@ -114,55 +114,55 @@
|
|
|
114
114
|
<div class="assistant-transcript__system-body">
|
|
115
115
|
<div class="assistant-transcript__system-meta">
|
|
116
116
|
<span>{{ systemLabel }}</span>
|
|
117
|
-
<time v-if="
|
|
117
|
+
<time v-if="entry.message.displayAt">{{ entry.message.displayAt }}</time>
|
|
118
118
|
</div>
|
|
119
|
-
<slot name="system-message" :message="
|
|
119
|
+
<slot name="system-message" :message="entry.message"><LongTextPreviewBlocks compact :blocks="entry.message.blocks" @link-click="handleLongTextLinkClick" /></slot>
|
|
120
120
|
</div>
|
|
121
121
|
</div>
|
|
122
122
|
|
|
123
123
|
<div
|
|
124
|
-
v-if="
|
|
124
|
+
v-else-if="entry.role === 'user'"
|
|
125
125
|
class="assistant-transcript__message-row assistant-transcript__message-row--user"
|
|
126
126
|
>
|
|
127
127
|
<div class="assistant-transcript__message assistant-transcript__message--user">
|
|
128
128
|
<div
|
|
129
129
|
class="assistant-transcript__user-content"
|
|
130
130
|
>
|
|
131
|
-
<p v-if="userMessageFormat === 'plain'" class="assistant-transcript__plain-user">{{ userMessageExpanded(turn) ?
|
|
131
|
+
<p v-if="userMessageFormat === 'plain'" class="assistant-transcript__plain-user">{{ userMessageExpanded(entry.turn) ? entry.message.text : userMessagePreviewText(entry.message.text) || entry.message.text }}</p>
|
|
132
132
|
<LongTextPreviewBlocks
|
|
133
133
|
v-else
|
|
134
|
-
:blocks="userMessageExpanded(turn) ?
|
|
134
|
+
:blocks="userMessageExpanded(entry.turn) ? entry.message.blocks : (entry.message.previewBlocks || entry.message.blocks)"
|
|
135
135
|
@link-click="handleLongTextLinkClick"
|
|
136
136
|
/>
|
|
137
137
|
</div>
|
|
138
138
|
<button
|
|
139
|
-
v-if="userMessageCollapsible(
|
|
140
|
-
:aria-expanded="userMessageExpanded(turn)"
|
|
139
|
+
v-if="userMessageCollapsible(entry.message)"
|
|
140
|
+
:aria-expanded="userMessageExpanded(entry.turn)"
|
|
141
141
|
class="assistant-transcript__user-content-toggle"
|
|
142
142
|
type="button"
|
|
143
|
-
@click="toggleUserMessage(turn)"
|
|
143
|
+
@click="toggleUserMessage(entry.turn)"
|
|
144
144
|
>
|
|
145
|
-
{{ userMessageExpanded(turn) ? "Show less" : "Read more" }}
|
|
145
|
+
{{ userMessageExpanded(entry.turn) ? "Show less" : "Read more" }}
|
|
146
146
|
</button>
|
|
147
|
-
<slot name="attachments" :items="
|
|
147
|
+
<slot name="attachments" :items="entry.message.attachments" :message="entry.message"><AssistantMessageAttachments :attachments="entry.message.attachments || []" /></slot>
|
|
148
148
|
<div
|
|
149
|
-
v-if="
|
|
149
|
+
v-if="entry.message.displayAt"
|
|
150
150
|
class="assistant-transcript__message-footer assistant-transcript__message-footer--user"
|
|
151
151
|
>
|
|
152
|
-
<time v-if="
|
|
152
|
+
<time v-if="entry.message.displayAt">{{ entry.message.displayAt }}</time>
|
|
153
153
|
</div>
|
|
154
154
|
<div
|
|
155
|
-
v-if="turn.optimistic?.status === 'failed'"
|
|
155
|
+
v-if="entry.turn.optimistic?.status === 'failed'"
|
|
156
156
|
class="assistant-transcript__optimistic-failure"
|
|
157
157
|
>
|
|
158
|
-
<span>{{ turn.optimistic.error || "Message could not be sent." }}</span>
|
|
158
|
+
<span>{{ entry.turn.optimistic.error || "Message could not be sent." }}</span>
|
|
159
159
|
<div class="assistant-transcript__optimistic-actions">
|
|
160
160
|
<v-btn
|
|
161
161
|
color="primary"
|
|
162
162
|
size="x-small"
|
|
163
163
|
type="button"
|
|
164
164
|
variant="tonal"
|
|
165
|
-
@click="emit('resend-turn', turn.optimistic.id)"
|
|
165
|
+
@click="emit('resend-turn', entry.turn.optimistic.id)"
|
|
166
166
|
>
|
|
167
167
|
Resend
|
|
168
168
|
</v-btn>
|
|
@@ -170,7 +170,7 @@
|
|
|
170
170
|
size="x-small"
|
|
171
171
|
type="button"
|
|
172
172
|
variant="text"
|
|
173
|
-
@click="emit('cancel-turn', turn.optimistic.id)"
|
|
173
|
+
@click="emit('cancel-turn', entry.turn.optimistic.id)"
|
|
174
174
|
>
|
|
175
175
|
Cancel
|
|
176
176
|
</v-btn>
|
|
@@ -178,7 +178,7 @@
|
|
|
178
178
|
size="x-small"
|
|
179
179
|
type="button"
|
|
180
180
|
variant="text"
|
|
181
|
-
@click="emit('edit-turn', turn.optimistic.id)"
|
|
181
|
+
@click="emit('edit-turn', entry.turn.optimistic.id)"
|
|
182
182
|
>
|
|
183
183
|
Edit
|
|
184
184
|
</v-btn>
|
|
@@ -190,84 +190,79 @@
|
|
|
190
190
|
</span>
|
|
191
191
|
</div>
|
|
192
192
|
|
|
193
|
-
<
|
|
194
|
-
v-
|
|
195
|
-
:key="entry.key"
|
|
193
|
+
<AssistantProgress
|
|
194
|
+
v-else-if="entry.role === 'thinking'"
|
|
195
|
+
:key="`${scrollKey}:${entry.key}`"
|
|
196
|
+
class="assistant-transcript__thinking"
|
|
197
|
+
:messages="entry.messages"
|
|
198
|
+
:pending="isWorking && entry === displayEntries.at(-1)"
|
|
199
|
+
:preview-limit="progressPreviewLimit"
|
|
200
|
+
:aria-label="`${assistantLabel} progress`"
|
|
201
|
+
/>
|
|
202
|
+
<div
|
|
203
|
+
v-else
|
|
204
|
+
class="assistant-transcript__message-row assistant-transcript__message-row--assistant"
|
|
205
|
+
:data-message-role="entry.role"
|
|
196
206
|
>
|
|
197
|
-
<
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
:preview-limit="progressPreviewLimit"
|
|
204
|
-
:aria-label="`${assistantLabel} progress`"
|
|
205
|
-
/>
|
|
206
|
-
<div
|
|
207
|
-
v-else
|
|
208
|
-
class="assistant-transcript__message-row assistant-transcript__message-row--assistant"
|
|
209
|
-
:data-message-role="entry.role"
|
|
210
|
-
>
|
|
211
|
-
<div class="assistant-transcript__assistant-header">
|
|
212
|
-
<span class="assistant-transcript__avatar assistant-transcript__avatar--assistant">
|
|
213
|
-
<v-icon :icon="mdiRobotOutline" size="16" />
|
|
214
|
-
</span>
|
|
215
|
-
<div class="assistant-transcript__message-header">
|
|
216
|
-
<span>{{ assistantLabel }}</span>
|
|
217
|
-
</div>
|
|
207
|
+
<div class="assistant-transcript__assistant-header">
|
|
208
|
+
<span class="assistant-transcript__avatar assistant-transcript__avatar--assistant">
|
|
209
|
+
<v-icon :icon="mdiRobotOutline" size="16" />
|
|
210
|
+
</span>
|
|
211
|
+
<div class="assistant-transcript__message-header">
|
|
212
|
+
<span>{{ assistantLabel }}</span>
|
|
218
213
|
</div>
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
214
|
+
</div>
|
|
215
|
+
<div class="assistant-transcript__message assistant-transcript__message--assistant">
|
|
216
|
+
<LongTextPreviewBlocks
|
|
217
|
+
v-if="entry.message.blocks.length"
|
|
218
|
+
:blocks="entry.message.blocks"
|
|
219
|
+
@link-click="handleLongTextLinkClick"
|
|
220
|
+
/>
|
|
221
|
+
<ol
|
|
222
|
+
v-if="entry.message.questions.length"
|
|
223
|
+
class="assistant-transcript__questions"
|
|
224
|
+
>
|
|
225
|
+
<li
|
|
226
|
+
v-for="question in entry.message.questions"
|
|
227
|
+
:key="question.name"
|
|
228
|
+
class="assistant-transcript__question"
|
|
228
229
|
>
|
|
229
|
-
<
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
230
|
+
<span class="assistant-transcript__question-number">{{ question.number }}</span>
|
|
231
|
+
<div class="assistant-transcript__question-content">
|
|
232
|
+
<span class="assistant-transcript__question-text">
|
|
233
|
+
<LongTextInlineParts
|
|
234
|
+
:text="question.label"
|
|
235
|
+
@link-click="handleLongTextLinkClick"
|
|
236
|
+
/>
|
|
237
|
+
</span>
|
|
238
|
+
<ul
|
|
239
|
+
v-if="question.choices.length"
|
|
240
|
+
class="assistant-transcript__question-choices"
|
|
241
|
+
>
|
|
242
|
+
<li v-for="choice in question.choices" :key="choice.value">
|
|
237
243
|
<LongTextInlineParts
|
|
238
|
-
:text="
|
|
244
|
+
:text="choice.label"
|
|
239
245
|
@link-click="handleLongTextLinkClick"
|
|
240
|
-
|
|
241
|
-
</
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
:blocks="entry.message.outroBlocks"
|
|
259
|
-
@link-click="handleLongTextLinkClick"
|
|
260
|
-
/>
|
|
261
|
-
<slot name="message-actions" :message="entry.message" :turn="turn" />
|
|
262
|
-
</div>
|
|
263
|
-
<div
|
|
264
|
-
v-if="entry.message.displayAt"
|
|
265
|
-
class="assistant-transcript__message-footer assistant-transcript__message-footer--assistant"
|
|
266
|
-
>
|
|
267
|
-
<time>{{ entry.message.displayAt }}</time>
|
|
268
|
-
</div>
|
|
246
|
+
/><span v-if="choice.recommended"> · Recommended</span>
|
|
247
|
+
</li>
|
|
248
|
+
</ul>
|
|
249
|
+
</div>
|
|
250
|
+
</li>
|
|
251
|
+
</ol>
|
|
252
|
+
<LongTextPreviewBlocks
|
|
253
|
+
v-if="entry.message.outroBlocks.length"
|
|
254
|
+
:blocks="entry.message.outroBlocks"
|
|
255
|
+
@link-click="handleLongTextLinkClick"
|
|
256
|
+
/>
|
|
257
|
+
<slot name="message-actions" :message="entry.message" :turn="entry.turn" />
|
|
258
|
+
</div>
|
|
259
|
+
<div
|
|
260
|
+
v-if="entry.message.displayAt"
|
|
261
|
+
class="assistant-transcript__message-footer assistant-transcript__message-footer--assistant"
|
|
262
|
+
>
|
|
263
|
+
<time>{{ entry.message.displayAt }}</time>
|
|
269
264
|
</div>
|
|
270
|
-
</
|
|
265
|
+
</div>
|
|
271
266
|
</article>
|
|
272
267
|
|
|
273
268
|
<div
|
|
@@ -303,6 +298,7 @@ import {
|
|
|
303
298
|
|
|
304
299
|
const props = defineProps({
|
|
305
300
|
systemLabel: { type: String, default: "Status" },
|
|
301
|
+
working: { type: Boolean, default: undefined },
|
|
306
302
|
progressPreviewLimit: { type: Number, default: 2 },
|
|
307
303
|
userMessageFormat: { type: String, default: "formatted", validator: (value) => ["plain", "formatted"].includes(value) },
|
|
308
304
|
assistantLabel: {
|
|
@@ -373,6 +369,7 @@ const DISPLAY_MESSAGE_CACHE_LIMIT = 500;
|
|
|
373
369
|
const bodyElement = ref(null);
|
|
374
370
|
const bottomElement = ref(null);
|
|
375
371
|
const expandedUserMessages = ref(new Set());
|
|
372
|
+
const isWorking = computed(() => props.working ?? props.turns.some(turn => turn.pending));
|
|
376
373
|
const followingLatest = ref(true);
|
|
377
374
|
const initialScrollSettled = ref(false);
|
|
378
375
|
const userScrollIntent = ref(false);
|
|
@@ -530,66 +527,49 @@ function conversationMessageKey(message = {}, index = 0) {
|
|
|
530
527
|
].join(":");
|
|
531
528
|
}
|
|
532
529
|
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
const
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
530
|
+
// Storage rows are not visual boundaries. Only an intervening message ends a
|
|
531
|
+
// reasoning group, including when history is prepended or execution continues.
|
|
532
|
+
const displayEntries = computed(() => {
|
|
533
|
+
const entries = [];
|
|
534
|
+
for (const [index, turn] of props.turns.entries()) {
|
|
535
|
+
const turnId = String(turn.turnId || index + 1);
|
|
536
|
+
const messages = [
|
|
537
|
+
turn.system && { ...turn.system, role: "system" },
|
|
538
|
+
turn.user && { ...turn.user, role: "user" },
|
|
539
|
+
...conversationAgentMessages(turn)
|
|
540
|
+
].filter(Boolean);
|
|
541
|
+
for (const [messageIndex, message] of messages.entries()) {
|
|
542
|
+
const role = String(message.role || "").trim();
|
|
543
|
+
const key = `${turnId}:${conversationMessageKey(message, messageIndex)}`;
|
|
544
|
+
const displayed = role === "thinking" ? displayThinkingMessage(message) : displayMessage(message, {
|
|
545
|
+
allowNumberedQuestions: role !== "user" && role !== "system",
|
|
546
|
+
preserveParagraphLineBreaks: role === "user",
|
|
547
|
+
previewUserMessage: role === "user"
|
|
548
|
+
}, key);
|
|
549
|
+
if (!displayed) continue;
|
|
550
|
+
const previous = entries.at(-1);
|
|
551
|
+
if (role === "thinking" && previous?.role === "thinking") {
|
|
552
|
+
previous.messages.push({ ...displayed, key });
|
|
553
|
+
} else {
|
|
554
|
+
entries.push({
|
|
555
|
+
key, role, turn: { ...turn, turnId }, message: displayed,
|
|
556
|
+
...(role === "thinking" ? { messages: [{ ...displayed, key }] } : {})
|
|
557
|
+
});
|
|
550
558
|
}
|
|
551
|
-
timeline.push({
|
|
552
|
-
key: `thinking:${keyedMessage.key}`,
|
|
553
|
-
messages: [keyedMessage],
|
|
554
|
-
role
|
|
555
|
-
});
|
|
556
|
-
continue;
|
|
557
559
|
}
|
|
558
|
-
timeline.push({
|
|
559
|
-
key: conversationMessageKey(message, index),
|
|
560
|
-
message: displayMessage(message, {
|
|
561
|
-
allowNumberedQuestions: true
|
|
562
|
-
}, `${turnKey}:agent:${conversationMessageKey(message, index)}`),
|
|
563
|
-
role
|
|
564
|
-
});
|
|
565
560
|
}
|
|
566
|
-
|
|
567
|
-
|
|
561
|
+
// Anchor a progress group to the following message, or to the transcript end.
|
|
562
|
+
// Appending progress or prepending history then preserves its expansion state.
|
|
563
|
+
for (const [index, entry] of entries.entries()) {
|
|
564
|
+
if (entry.role === "thinking") entry.key = `progress:${entries[index + 1]?.key || "tail"}`;
|
|
565
|
+
}
|
|
566
|
+
return entries;
|
|
567
|
+
});
|
|
568
568
|
|
|
569
569
|
function handleLongTextLinkClick(payload) {
|
|
570
570
|
emit("link-click", payload);
|
|
571
571
|
}
|
|
572
572
|
|
|
573
|
-
const displayTurns = computed(() => (Array.isArray(props.turns) ? props.turns : [])
|
|
574
|
-
.map((turn, index) => {
|
|
575
|
-
const turnId = String(turn.turnId || index + 1);
|
|
576
|
-
return {
|
|
577
|
-
...turn,
|
|
578
|
-
agentTimeline: displayAgentTimeline(turn, turnId),
|
|
579
|
-
optimistic: turn.optimistic && typeof turn.optimistic === "object" && !Array.isArray(turn.optimistic)
|
|
580
|
-
? turn.optimistic
|
|
581
|
-
: null,
|
|
582
|
-
pending: turn.pending === true,
|
|
583
|
-
system: displayMessage(turn.system, {}, `${turnId}:system`),
|
|
584
|
-
turnId,
|
|
585
|
-
user: displayMessage(turn.user, {
|
|
586
|
-
preserveParagraphLineBreaks: true,
|
|
587
|
-
previewUserMessage: true
|
|
588
|
-
}, `${turnId}:user`)
|
|
589
|
-
};
|
|
590
|
-
})
|
|
591
|
-
.filter((turn) => turn.system || turn.user || turn.agentTimeline.length));
|
|
592
|
-
|
|
593
573
|
function userMessageCollapsible(message = null) {
|
|
594
574
|
return Array.isArray(message?.previewBlocks);
|
|
595
575
|
}
|
|
@@ -618,11 +598,11 @@ function toggleUserMessage(turn = {}) {
|
|
|
618
598
|
|
|
619
599
|
const loadingIndicatorVisible = computed(() => Boolean(
|
|
620
600
|
props.loading &&
|
|
621
|
-
!
|
|
601
|
+
!displayEntries.value.length
|
|
622
602
|
));
|
|
623
603
|
const initialScrollPending = computed(() => Boolean(
|
|
624
604
|
props.visible &&
|
|
625
|
-
|
|
605
|
+
displayEntries.value.length &&
|
|
626
606
|
!initialScrollSettled.value
|
|
627
607
|
));
|
|
628
608
|
|
|
@@ -638,40 +618,24 @@ function messageScrollKey(message = null) {
|
|
|
638
618
|
].join("/");
|
|
639
619
|
}
|
|
640
620
|
|
|
641
|
-
function agentTimelineScrollKey(timeline = []) {
|
|
642
|
-
return (Array.isArray(timeline) ? timeline : []).map((entry) => (
|
|
643
|
-
entry?.role === "thinking"
|
|
644
|
-
? (entry.messages || []).map(messageScrollKey).join("+")
|
|
645
|
-
: messageScrollKey(entry?.message)
|
|
646
|
-
)).join("|");
|
|
647
|
-
}
|
|
648
|
-
|
|
649
|
-
function latestRenderedTailScrollKey(turns = []) {
|
|
650
|
-
const turn = Array.isArray(turns) ? turns.at(-1) : null;
|
|
651
|
-
if (!turn) {
|
|
652
|
-
return "";
|
|
653
|
-
}
|
|
654
|
-
const timeline = Array.isArray(turn.agentTimeline) ? turn.agentTimeline : [];
|
|
655
|
-
return [
|
|
656
|
-
turn.turnId,
|
|
657
|
-
messageScrollKey(turn.system),
|
|
658
|
-
messageScrollKey(turn.user),
|
|
659
|
-
turn.optimistic?.id || "",
|
|
660
|
-
turn.optimistic?.status || "",
|
|
661
|
-
turn.optimistic?.error || "",
|
|
662
|
-
turn.pending ? "pending" : "settled",
|
|
663
|
-
agentTimelineScrollKey(timeline)
|
|
664
|
-
].join(":");
|
|
665
|
-
}
|
|
666
|
-
|
|
667
621
|
const timelineScrollTrigger = computed(() => [
|
|
668
622
|
props.visible ? "visible" : "hidden",
|
|
669
623
|
props.error ? "error" : "body",
|
|
670
624
|
loadingIndicatorVisible.value ? "loading" : "ready",
|
|
671
|
-
|
|
625
|
+
displayEntries.value.length ? "has-messages" : "empty",
|
|
672
626
|
props.scrollKey
|
|
673
627
|
].join(":"));
|
|
674
|
-
const latestRenderedTailKey = computed(() =>
|
|
628
|
+
const latestRenderedTailKey = computed(() => {
|
|
629
|
+
const entry = displayEntries.value.at(-1);
|
|
630
|
+
if (!entry) return "";
|
|
631
|
+
return [
|
|
632
|
+
entry.key,
|
|
633
|
+
isWorking.value,
|
|
634
|
+
entry.role === "thinking" ? entry.messages.map(messageScrollKey).join("+") : messageScrollKey(entry.message),
|
|
635
|
+
entry.turn.optimistic?.status || "",
|
|
636
|
+
entry.turn.optimistic?.error || ""
|
|
637
|
+
].join(":");
|
|
638
|
+
});
|
|
675
639
|
const autoScrollEnabled = computed(() => Boolean(
|
|
676
640
|
props.visible &&
|
|
677
641
|
followingLatest.value
|
|
@@ -22,6 +22,79 @@ const VIEWPORTS = Object.freeze([
|
|
|
22
22
|
Object.freeze({ name: "drawer", width: 360, height: 600 })
|
|
23
23
|
]);
|
|
24
24
|
|
|
25
|
+
test("adjacent reasoning stays grouped across storage rows, history pages and execution changes", {
|
|
26
|
+
skip: !RUN_BROWSER_TEST,
|
|
27
|
+
timeout: 90_000
|
|
28
|
+
}, async () => {
|
|
29
|
+
const vite = await startViteFixture({ fixtureRoot: FIXTURE_ROOT });
|
|
30
|
+
const browser = await chromium.launch(createChromiumLaunchOptions());
|
|
31
|
+
try {
|
|
32
|
+
for (const width of [390, 800, 1365]) {
|
|
33
|
+
const page = await browser.newPage({ viewport: { width, height: 900 } });
|
|
34
|
+
const errors = [];
|
|
35
|
+
page.on("pageerror", error => errors.push(error.message));
|
|
36
|
+
await page.goto(`${vite.baseURL}/?progress=1`);
|
|
37
|
+
const groups = page.locator(".assistant-progress");
|
|
38
|
+
const summaries = page.locator(".assistant-progress__message");
|
|
39
|
+
const input = page.getByRole("textbox", { name: "Message AI assistant" });
|
|
40
|
+
const external = name => page.getByRole("button", { name, exact: true }).evaluate(element => element.click());
|
|
41
|
+
await expect(groups).toHaveCount(1);
|
|
42
|
+
await expect(summaries).toHaveText(["Reasoning 2", "Reasoning 3"]);
|
|
43
|
+
await input.fill("Keep my draft");
|
|
44
|
+
await input.evaluate(element => element.setSelectionRange(2, 5));
|
|
45
|
+
await external("Change storage rows");
|
|
46
|
+
await expect(groups).toHaveCount(1);
|
|
47
|
+
await expect(summaries).toHaveText(["Reasoning 2", "Reasoning 3"]);
|
|
48
|
+
await external("Change storage rows");
|
|
49
|
+
await external("Toggle working");
|
|
50
|
+
await expect(summaries).toHaveCount(0);
|
|
51
|
+
await expect(page.getByRole("button", { name: "Show all 3 progress updates" })).toBeVisible();
|
|
52
|
+
await external("Toggle working");
|
|
53
|
+
await expect(summaries).toHaveText(["Reasoning 2", "Reasoning 3"]);
|
|
54
|
+
await external("Show all 3 progress updates");
|
|
55
|
+
await expect(summaries).toHaveCount(3);
|
|
56
|
+
await external("Load older messages");
|
|
57
|
+
await expect(groups).toHaveCount(1);
|
|
58
|
+
await expect(summaries).toHaveText(["Older reasoning", "Reasoning 1", "Reasoning 2", "Reasoning 3"]);
|
|
59
|
+
await external("Append thinking");
|
|
60
|
+
await expect(summaries).toHaveCount(5);
|
|
61
|
+
await expect(input).toBeFocused();
|
|
62
|
+
await expect(input).toHaveValue("Keep my draft");
|
|
63
|
+
assert.deepEqual(await input.evaluate(element => [element.selectionStart, element.selectionEnd]), [2, 5]);
|
|
64
|
+
await external("Show latest 2 progress updates");
|
|
65
|
+
await external("Toggle working");
|
|
66
|
+
await expect(summaries).toHaveCount(0);
|
|
67
|
+
await external("Show all 5 progress updates");
|
|
68
|
+
await external("Change conversation");
|
|
69
|
+
await expect(summaries).toHaveCount(0);
|
|
70
|
+
await external("Toggle working");
|
|
71
|
+
|
|
72
|
+
// Every visible non-reasoning message separates groups, regardless of rows.
|
|
73
|
+
for (const [index, role] of ["commentary", "assistant", "user", "system"].entries()) {
|
|
74
|
+
await external(`Append ${role}`);
|
|
75
|
+
await expect(summaries).toHaveCount(0);
|
|
76
|
+
await external("Append thinking");
|
|
77
|
+
await external("Append thinking");
|
|
78
|
+
await expect(groups).toHaveCount(index + 2);
|
|
79
|
+
await expect(summaries).toHaveCount(2);
|
|
80
|
+
}
|
|
81
|
+
await external("Change storage rows");
|
|
82
|
+
await expect(groups).toHaveCount(5);
|
|
83
|
+
await expect(summaries).toHaveCount(2);
|
|
84
|
+
const bodyText = await page.locator(".assistant-transcript__body").textContent();
|
|
85
|
+
assert.ok(bodyText.indexOf("commentary 6") < bodyText.indexOf("assistant 9"));
|
|
86
|
+
assert.ok(bodyText.indexOf("assistant 9") < bodyText.indexOf("user 12"));
|
|
87
|
+
assert.ok(bodyText.indexOf("user 12") < bodyText.indexOf("system 15"));
|
|
88
|
+
assert.ok(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth));
|
|
89
|
+
assert.deepEqual(errors, []);
|
|
90
|
+
await page.close();
|
|
91
|
+
}
|
|
92
|
+
} finally {
|
|
93
|
+
await browser.close();
|
|
94
|
+
await stopProcess(vite);
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
|
|
25
98
|
async function assertResponsiveAssistant(page, viewport) {
|
|
26
99
|
await page.setViewportSize({ width: viewport.width, height: viewport.height });
|
|
27
100
|
await page.goto("/");
|