@jskit-ai/assistant-core 0.1.165 → 0.1.167
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/App.vue +104 -0
- package/examples/conversation/README.md +42 -0
- package/examples/conversation/index.html +2 -0
- package/examples/conversation/main.js +9 -0
- package/examples/conversation/package.json +22 -0
- package/examples/conversation/server.js +123 -0
- package/examples/conversation/vite.config.js +8 -0
- package/package.json +59 -7
- package/src/client/conversation/AssistantComposerActions.vue +43 -0
- package/src/client/conversation/AssistantConversationElement.vue +78 -0
- package/src/client/conversation/AssistantProgress.vue +88 -0
- package/src/client/conversation/AssistantPromptInput.vue +467 -0
- package/src/client/conversation/AssistantTranscript.vue +1361 -0
- package/src/client/conversation/LongTextInlineParts.vue +64 -0
- package/src/client/conversation/LongTextPreviewBlocks.vue +324 -0
- package/src/client/conversation/index.js +9 -0
- package/src/client/conversation/submitText.js +29 -0
- package/src/client/conversation/useScrollToBottom.js +151 -0
- package/src/server/conversation/codexClient.js +328 -0
- package/src/server/conversation/codexEvents.js +623 -0
- package/src/server/conversation/codexTurn.js +280 -0
- package/src/server/conversation/index.js +2 -0
- package/src/server/conversation/memoryStorage.js +76 -0
- package/src/server/conversation/normalize.js +7 -0
- package/src/server/conversation/openCodeClient.js +512 -0
- package/src/server/conversation/transcript.js +92 -0
- package/src/shared/conversation/answerChoiceSugar.js +191 -0
- package/src/shared/conversation/index.js +6 -0
- package/src/shared/conversation/longTextBlocks.js +403 -0
- package/src/shared/conversation/numberedQuestionSugar.js +295 -0
- package/src/shared/conversation/scrollFollowState.js +32 -0
- package/src/shared/conversation/thinkingText.js +13 -0
- package/src/shared/conversation/turns.js +30 -0
- package/src/testing/conversationStorageContract.js +35 -0
- package/test/conversation.test.js +57 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<template
|
|
3
|
+
v-for="(part, partIndex) in parts"
|
|
4
|
+
:key="partKey(part, partIndex)"
|
|
5
|
+
>
|
|
6
|
+
<strong v-if="part.type === 'strong'">{{ part.text }}</strong>
|
|
7
|
+
<code v-else-if="part.type === 'code'">{{ part.text }}</code>
|
|
8
|
+
<a
|
|
9
|
+
v-else-if="part.type === 'link'"
|
|
10
|
+
:href="part.href"
|
|
11
|
+
@click="handleLinkClick($event, part)"
|
|
12
|
+
>{{ part.text }}</a>
|
|
13
|
+
<span v-else>{{ part.text }}</span>
|
|
14
|
+
</template>
|
|
15
|
+
</template>
|
|
16
|
+
|
|
17
|
+
<script setup>
|
|
18
|
+
import { computed } from "vue";
|
|
19
|
+
|
|
20
|
+
import { parseLongTextInlineParts } from "../../shared/conversation/longTextBlocks.js";
|
|
21
|
+
|
|
22
|
+
const props = defineProps({
|
|
23
|
+
text: {
|
|
24
|
+
default: "",
|
|
25
|
+
type: String
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
const emit = defineEmits(["link-click"]);
|
|
29
|
+
|
|
30
|
+
const parts = computed(() => parseLongTextInlineParts(props.text));
|
|
31
|
+
|
|
32
|
+
function partKey(part, partIndex) {
|
|
33
|
+
return `${part.type}:${partIndex}:${part.text}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function handleLinkClick(event, part = {}) {
|
|
37
|
+
emit("link-click", {
|
|
38
|
+
event,
|
|
39
|
+
href: part.href || "",
|
|
40
|
+
text: part.text || ""
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
</script>
|
|
44
|
+
|
|
45
|
+
<style scoped>
|
|
46
|
+
code {
|
|
47
|
+
background: rgba(var(--v-theme-primary), 0.07);
|
|
48
|
+
border: 1px solid rgba(var(--v-theme-primary), 0.14);
|
|
49
|
+
border-radius: 3px;
|
|
50
|
+
color: rgba(var(--v-theme-on-surface), 0.9);
|
|
51
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
|
|
52
|
+
font-size: 0.9em;
|
|
53
|
+
overflow-wrap: anywhere;
|
|
54
|
+
padding: 0.02rem 0.2rem;
|
|
55
|
+
word-break: break-word;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
span,
|
|
59
|
+
strong,
|
|
60
|
+
a {
|
|
61
|
+
overflow-wrap: anywhere;
|
|
62
|
+
word-break: break-word;
|
|
63
|
+
}
|
|
64
|
+
</style>
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div
|
|
3
|
+
class="studio-long-text-review__blocks"
|
|
4
|
+
:class="{ 'studio-long-text-review__blocks--compact': compact }"
|
|
5
|
+
>
|
|
6
|
+
<template
|
|
7
|
+
v-for="(block, blockIndex) in blocks"
|
|
8
|
+
:key="blockKey(block, blockIndex)"
|
|
9
|
+
>
|
|
10
|
+
<component
|
|
11
|
+
:is="headingTag(block.level)"
|
|
12
|
+
v-if="block.type === 'heading'"
|
|
13
|
+
class="studio-long-text-review__heading"
|
|
14
|
+
>
|
|
15
|
+
<LongTextInlineParts
|
|
16
|
+
:text="block.text"
|
|
17
|
+
@link-click="emit('link-click', $event)"
|
|
18
|
+
/>
|
|
19
|
+
</component>
|
|
20
|
+
<component
|
|
21
|
+
:is="block.type"
|
|
22
|
+
v-else-if="isListBlock(block)"
|
|
23
|
+
class="studio-long-text-review__list"
|
|
24
|
+
>
|
|
25
|
+
<li
|
|
26
|
+
v-for="(item, itemIndex) in block.items"
|
|
27
|
+
:key="`item:${blockIndex}:${itemIndex}`"
|
|
28
|
+
>
|
|
29
|
+
<LongTextInlineParts
|
|
30
|
+
:text="item.text"
|
|
31
|
+
@link-click="emit('link-click', $event)"
|
|
32
|
+
/>
|
|
33
|
+
</li>
|
|
34
|
+
</component>
|
|
35
|
+
<pre
|
|
36
|
+
v-else-if="block.type === 'code'"
|
|
37
|
+
class="studio-long-text-review__code"
|
|
38
|
+
><code>{{ block.text }}</code></pre>
|
|
39
|
+
<div
|
|
40
|
+
v-else-if="block.type === 'table'"
|
|
41
|
+
class="studio-long-text-review__table-wrap"
|
|
42
|
+
role="region"
|
|
43
|
+
aria-label="Table"
|
|
44
|
+
tabindex="0"
|
|
45
|
+
>
|
|
46
|
+
<table class="studio-long-text-review__table">
|
|
47
|
+
<thead>
|
|
48
|
+
<tr>
|
|
49
|
+
<th
|
|
50
|
+
v-for="(header, columnIndex) in block.headers"
|
|
51
|
+
:key="`header:${blockIndex}:${columnIndex}`"
|
|
52
|
+
:class="tableCellClass(block, columnIndex)"
|
|
53
|
+
scope="col"
|
|
54
|
+
>
|
|
55
|
+
<LongTextInlineParts
|
|
56
|
+
:text="header"
|
|
57
|
+
@link-click="emit('link-click', $event)"
|
|
58
|
+
/>
|
|
59
|
+
</th>
|
|
60
|
+
</tr>
|
|
61
|
+
</thead>
|
|
62
|
+
<tbody>
|
|
63
|
+
<tr
|
|
64
|
+
v-for="(row, rowIndex) in block.rows"
|
|
65
|
+
:key="`row:${blockIndex}:${rowIndex}`"
|
|
66
|
+
>
|
|
67
|
+
<td
|
|
68
|
+
v-for="(cell, columnIndex) in row"
|
|
69
|
+
:key="`cell:${blockIndex}:${rowIndex}:${columnIndex}`"
|
|
70
|
+
:class="tableCellClass(block, columnIndex)"
|
|
71
|
+
>
|
|
72
|
+
<LongTextInlineParts
|
|
73
|
+
:text="cell"
|
|
74
|
+
@link-click="emit('link-click', $event)"
|
|
75
|
+
/>
|
|
76
|
+
</td>
|
|
77
|
+
</tr>
|
|
78
|
+
</tbody>
|
|
79
|
+
</table>
|
|
80
|
+
</div>
|
|
81
|
+
<details
|
|
82
|
+
v-else-if="block.type === 'details'"
|
|
83
|
+
class="studio-long-text-review__details"
|
|
84
|
+
>
|
|
85
|
+
<summary class="studio-long-text-review__details-summary">
|
|
86
|
+
<LongTextInlineParts
|
|
87
|
+
:text="block.summary || 'Details'"
|
|
88
|
+
@link-click="emit('link-click', $event)"
|
|
89
|
+
/>
|
|
90
|
+
</summary>
|
|
91
|
+
<LongTextPreviewBlocks
|
|
92
|
+
class="studio-long-text-review__details-body"
|
|
93
|
+
compact
|
|
94
|
+
:blocks="block.blocks || []"
|
|
95
|
+
@link-click="emit('link-click', $event)"
|
|
96
|
+
/>
|
|
97
|
+
</details>
|
|
98
|
+
<p
|
|
99
|
+
v-else
|
|
100
|
+
class="studio-long-text-review__paragraph"
|
|
101
|
+
>
|
|
102
|
+
<LongTextInlineParts
|
|
103
|
+
:text="block.text"
|
|
104
|
+
@link-click="emit('link-click', $event)"
|
|
105
|
+
/>
|
|
106
|
+
</p>
|
|
107
|
+
</template>
|
|
108
|
+
</div>
|
|
109
|
+
</template>
|
|
110
|
+
|
|
111
|
+
<script setup>
|
|
112
|
+
import LongTextInlineParts from "./LongTextInlineParts.vue";
|
|
113
|
+
|
|
114
|
+
defineProps({
|
|
115
|
+
blocks: {
|
|
116
|
+
type: Array,
|
|
117
|
+
required: true
|
|
118
|
+
},
|
|
119
|
+
compact: {
|
|
120
|
+
type: Boolean,
|
|
121
|
+
default: false
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
const emit = defineEmits(["link-click"]);
|
|
125
|
+
|
|
126
|
+
function isListBlock(block) {
|
|
127
|
+
return block.type === "ul" || block.type === "ol";
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function blockKey(block, blockIndex) {
|
|
131
|
+
return `${block.type}:${blockIndex}`;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function headingTag(level) {
|
|
135
|
+
if (level <= 1) {
|
|
136
|
+
return "h3";
|
|
137
|
+
}
|
|
138
|
+
if (level === 2) {
|
|
139
|
+
return "h4";
|
|
140
|
+
}
|
|
141
|
+
return "h5";
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function tableCellClass(block = {}, columnIndex = 0) {
|
|
145
|
+
const alignment = Array.isArray(block.alignments) ? block.alignments[columnIndex] : "";
|
|
146
|
+
return {
|
|
147
|
+
"studio-long-text-review__table-cell--center": alignment === "center",
|
|
148
|
+
"studio-long-text-review__table-cell--right": alignment === "right"
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
</script>
|
|
152
|
+
|
|
153
|
+
<style scoped>
|
|
154
|
+
.studio-long-text-review__blocks {
|
|
155
|
+
color: rgb(var(--v-theme-on-surface));
|
|
156
|
+
display: grid;
|
|
157
|
+
gap: 0.36rem;
|
|
158
|
+
line-height: 1.4;
|
|
159
|
+
max-width: 100%;
|
|
160
|
+
min-width: 0;
|
|
161
|
+
overflow-wrap: anywhere;
|
|
162
|
+
word-break: break-word;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
.studio-long-text-review__blocks--compact {
|
|
166
|
+
gap: 0.24rem;
|
|
167
|
+
line-height: 1.28;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
.studio-long-text-review__heading {
|
|
171
|
+
font-weight: 760;
|
|
172
|
+
letter-spacing: 0;
|
|
173
|
+
line-height: 1.15;
|
|
174
|
+
margin: 0;
|
|
175
|
+
overflow-wrap: anywhere;
|
|
176
|
+
word-break: break-word;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
h3.studio-long-text-review__heading {
|
|
180
|
+
font-size: 1.1rem;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
h4.studio-long-text-review__heading {
|
|
184
|
+
font-size: 0.98rem;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
h5.studio-long-text-review__heading {
|
|
188
|
+
font-size: 0.9rem;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
.studio-long-text-review__paragraph {
|
|
192
|
+
font-size: 0.88rem;
|
|
193
|
+
margin: 0;
|
|
194
|
+
overflow-wrap: anywhere;
|
|
195
|
+
word-break: break-word;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
.studio-long-text-review__blocks--compact .studio-long-text-review__paragraph {
|
|
199
|
+
font-size: 0.8rem;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
.studio-long-text-review__list {
|
|
203
|
+
display: grid;
|
|
204
|
+
gap: 0.16rem;
|
|
205
|
+
margin: 0;
|
|
206
|
+
min-width: 0;
|
|
207
|
+
overflow-wrap: anywhere;
|
|
208
|
+
padding-inline-start: 1.3rem;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
.studio-long-text-review__list li {
|
|
212
|
+
font-size: 0.92rem;
|
|
213
|
+
line-height: 1.36;
|
|
214
|
+
min-width: 0;
|
|
215
|
+
overflow-wrap: anywhere;
|
|
216
|
+
padding-inline-start: 0.1rem;
|
|
217
|
+
word-break: break-word;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
.studio-long-text-review__blocks--compact .studio-long-text-review__list li {
|
|
221
|
+
font-size: 0.8rem;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
.studio-long-text-review__code {
|
|
225
|
+
background: rgba(var(--v-theme-surface-variant), 0.58);
|
|
226
|
+
border: 1px solid rgba(var(--v-border-color), 0.26);
|
|
227
|
+
border-radius: 8px;
|
|
228
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
|
|
229
|
+
font-size: 0.82rem;
|
|
230
|
+
line-height: 1.36;
|
|
231
|
+
margin: 0;
|
|
232
|
+
max-width: 100%;
|
|
233
|
+
min-width: 0;
|
|
234
|
+
overflow: auto;
|
|
235
|
+
padding: 0.46rem 0.55rem;
|
|
236
|
+
white-space: pre;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
.studio-long-text-review__table-wrap {
|
|
240
|
+
border: 1px solid rgba(var(--v-border-color), 0.28);
|
|
241
|
+
border-radius: 8px;
|
|
242
|
+
inline-size: fit-content;
|
|
243
|
+
margin-block: 0.35rem;
|
|
244
|
+
max-width: 100%;
|
|
245
|
+
min-width: 0;
|
|
246
|
+
overflow: auto;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
.studio-long-text-review__table-wrap:focus-visible {
|
|
250
|
+
outline: 2px solid rgb(var(--v-theme-primary));
|
|
251
|
+
outline-offset: -2px;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
.studio-long-text-review__table {
|
|
255
|
+
border-collapse: separate;
|
|
256
|
+
border-spacing: 0;
|
|
257
|
+
font-size: 0.88rem;
|
|
258
|
+
inline-size: auto;
|
|
259
|
+
line-height: 1.45;
|
|
260
|
+
overflow-wrap: break-word;
|
|
261
|
+
word-break: normal;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
.studio-long-text-review__table th,
|
|
265
|
+
.studio-long-text-review__table td {
|
|
266
|
+
border-bottom: 1px solid rgba(var(--v-border-color), 0.22);
|
|
267
|
+
max-inline-size: 28rem;
|
|
268
|
+
padding: 0.55rem 0.75rem;
|
|
269
|
+
text-align: left;
|
|
270
|
+
vertical-align: top;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
.studio-long-text-review__table th {
|
|
274
|
+
background: rgba(var(--v-theme-surface-variant), 0.48);
|
|
275
|
+
color: rgb(var(--v-theme-on-surface));
|
|
276
|
+
font-weight: 760;
|
|
277
|
+
position: sticky;
|
|
278
|
+
top: 0;
|
|
279
|
+
z-index: 1;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
.studio-long-text-review__table tbody tr:last-child td {
|
|
283
|
+
border-bottom: 0;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
.studio-long-text-review__table :deep(span),
|
|
287
|
+
.studio-long-text-review__table :deep(strong),
|
|
288
|
+
.studio-long-text-review__table :deep(a),
|
|
289
|
+
.studio-long-text-review__table :deep(code) {
|
|
290
|
+
overflow-wrap: break-word;
|
|
291
|
+
word-break: normal;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
.studio-long-text-review__table-cell--right {
|
|
295
|
+
font-variant-numeric: tabular-nums;
|
|
296
|
+
text-align: right !important;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
.studio-long-text-review__table-cell--center {
|
|
300
|
+
text-align: center !important;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
.studio-long-text-review__details {
|
|
304
|
+
border: 1px solid rgba(var(--v-border-color), 0.34);
|
|
305
|
+
border-radius: 8px;
|
|
306
|
+
display: block;
|
|
307
|
+
min-width: 0;
|
|
308
|
+
overflow: hidden;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
.studio-long-text-review__details-summary {
|
|
312
|
+
background: rgba(var(--v-theme-surface-variant), 0.48);
|
|
313
|
+
cursor: pointer;
|
|
314
|
+
font-size: 0.82rem;
|
|
315
|
+
font-weight: 650;
|
|
316
|
+
line-height: 1.2;
|
|
317
|
+
list-style-position: inside;
|
|
318
|
+
padding: 0.42rem 0.55rem;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
.studio-long-text-review__details-body {
|
|
322
|
+
padding: 0.5rem 0.6rem 0.58rem;
|
|
323
|
+
}
|
|
324
|
+
</style>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { default as AssistantConversationElement } from "./AssistantConversationElement.vue";
|
|
2
|
+
export { default as AssistantTranscript } from "./AssistantTranscript.vue";
|
|
3
|
+
export { default as AssistantPromptInput } from "./AssistantPromptInput.vue";
|
|
4
|
+
export { default as AssistantComposerActions } from "./AssistantComposerActions.vue";
|
|
5
|
+
export { default as AssistantProgress } from "./AssistantProgress.vue";
|
|
6
|
+
export { default as LongTextPreviewBlocks } from "./LongTextPreviewBlocks.vue";
|
|
7
|
+
export { default as LongTextInlineParts } from "./LongTextInlineParts.vue";
|
|
8
|
+
export { useScrollToBottom } from "./useScrollToBottom.js";
|
|
9
|
+
export { createAssistantTextSubmission } from "./submitText.js";
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/** A companion's text enters the same draft and send action as keyboard input. */
|
|
2
|
+
export function createAssistantTextSubmission({ getState, setDraft, submit, afterDraftChange = async () => {} }) {
|
|
3
|
+
let revision = 0;
|
|
4
|
+
return async function submitText(text, { sendImmediately = true, signal, timeoutMs = 2000 } = {}) {
|
|
5
|
+
const request = ++revision;
|
|
6
|
+
const value = String(text || "").trim();
|
|
7
|
+
const initial = getState();
|
|
8
|
+
if (!value || !initial.active || signal?.aborted) throw new Error("The chat composer is not ready yet.");
|
|
9
|
+
const hasDraft = Boolean(initial.draft?.trim());
|
|
10
|
+
const writtenValue = hasDraft ? `${initial.draft}\n${value}` : value;
|
|
11
|
+
setDraft(writtenValue);
|
|
12
|
+
if (!sendImmediately || hasDraft) return "draft";
|
|
13
|
+
await afterDraftChange();
|
|
14
|
+
const deadline = Date.now() + timeoutMs;
|
|
15
|
+
while (true) {
|
|
16
|
+
const state = getState();
|
|
17
|
+
if (signal?.aborted || request !== revision || !state.active || state.id !== initial.id || state.draft !== writtenValue) {
|
|
18
|
+
throw new Error("The chat changed before sending. Review the draft before sending.");
|
|
19
|
+
}
|
|
20
|
+
if (state.canSend) {
|
|
21
|
+
const result = await submit();
|
|
22
|
+
if (result === false) throw new Error("Your words are in the chat box, but this conversation cannot send yet.");
|
|
23
|
+
return state.turnActive ? "steer" : "send";
|
|
24
|
+
}
|
|
25
|
+
if (Date.now() >= deadline) throw new Error("Your words are in the chat box, but this conversation cannot send yet.");
|
|
26
|
+
await new Promise((resolve) => setTimeout(resolve, 16));
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { nextTick, onBeforeUnmount, unref } from "vue";
|
|
2
|
+
|
|
3
|
+
const DEFAULT_SETTLE_DELAYS_MS = [0, 80];
|
|
4
|
+
|
|
5
|
+
function readElement(value) {
|
|
6
|
+
return unref(value) || null;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function hasWindowTimer(name) {
|
|
10
|
+
return typeof window !== "undefined" && typeof window[name] === "function";
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function waitForLayoutFrame() {
|
|
14
|
+
if (typeof window === "undefined" || typeof window.requestAnimationFrame !== "function") {
|
|
15
|
+
return Promise.resolve();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return new Promise((resolve) => {
|
|
19
|
+
window.requestAnimationFrame(resolve);
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function normalizedScrollBehavior(value = "") {
|
|
24
|
+
return value === "smooth" ? "smooth" : "auto";
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function scrollElementToBottom(targetElement, behavior = "auto") {
|
|
28
|
+
if (behavior === "smooth" && typeof targetElement.scrollTo === "function") {
|
|
29
|
+
targetElement.scrollTo({
|
|
30
|
+
behavior,
|
|
31
|
+
top: targetElement.scrollHeight
|
|
32
|
+
});
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
targetElement.scrollTop = targetElement.scrollHeight;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function useScrollToBottom({
|
|
39
|
+
anchor = null,
|
|
40
|
+
enabled = true,
|
|
41
|
+
scrollAnchorIntoView = true,
|
|
42
|
+
settleDelaysMs = DEFAULT_SETTLE_DELAYS_MS,
|
|
43
|
+
target
|
|
44
|
+
} = {}) {
|
|
45
|
+
const scheduledTimers = new Set();
|
|
46
|
+
let disposed = false;
|
|
47
|
+
let scheduleVersion = 0;
|
|
48
|
+
|
|
49
|
+
function isEnabled() {
|
|
50
|
+
return unref(enabled) !== false;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function clearScheduledTimers() {
|
|
54
|
+
if (hasWindowTimer("clearTimeout")) {
|
|
55
|
+
scheduledTimers.forEach((timer) => {
|
|
56
|
+
window.clearTimeout(timer);
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
scheduledTimers.clear();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function clearScheduledScrolls() {
|
|
63
|
+
scheduleVersion += 1;
|
|
64
|
+
clearScheduledTimers();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function scrollNow(options = {}) {
|
|
68
|
+
if (disposed || !isEnabled()) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const targetElement = readElement(target);
|
|
73
|
+
if (!targetElement) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const behavior = normalizedScrollBehavior(options?.behavior);
|
|
78
|
+
scrollElementToBottom(targetElement, behavior);
|
|
79
|
+
if (scrollAnchorIntoView !== false) {
|
|
80
|
+
const anchorOptions = {
|
|
81
|
+
block: "end"
|
|
82
|
+
};
|
|
83
|
+
if (behavior === "smooth") {
|
|
84
|
+
anchorOptions.behavior = behavior;
|
|
85
|
+
}
|
|
86
|
+
readElement(anchor)?.scrollIntoView?.(anchorOptions);
|
|
87
|
+
}
|
|
88
|
+
scrollElementToBottom(targetElement, behavior);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function scheduleScroll(delayMs, options = {}, version = scheduleVersion) {
|
|
92
|
+
if (
|
|
93
|
+
disposed ||
|
|
94
|
+
version !== scheduleVersion ||
|
|
95
|
+
!isEnabled() ||
|
|
96
|
+
!hasWindowTimer("setTimeout")
|
|
97
|
+
) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const timer = window.setTimeout(() => {
|
|
102
|
+
scheduledTimers.delete(timer);
|
|
103
|
+
if (version !== scheduleVersion) {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
scrollNow(options);
|
|
107
|
+
}, delayMs);
|
|
108
|
+
scheduledTimers.add(timer);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function scrollAfterLayout(options = {}) {
|
|
112
|
+
if (disposed || !isEnabled()) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
const version = scheduleVersion + 1;
|
|
116
|
+
scheduleVersion = version;
|
|
117
|
+
clearScheduledTimers();
|
|
118
|
+
|
|
119
|
+
await nextTick();
|
|
120
|
+
if (version !== scheduleVersion) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
scrollNow(options);
|
|
124
|
+
|
|
125
|
+
await waitForLayoutFrame();
|
|
126
|
+
if (version !== scheduleVersion) {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
scrollNow(options);
|
|
130
|
+
|
|
131
|
+
clearScheduledTimers();
|
|
132
|
+
settleDelaysMs.forEach((delayMs) => {
|
|
133
|
+
scheduleScroll(delayMs, options, version);
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
onBeforeUnmount(() => {
|
|
138
|
+
disposed = true;
|
|
139
|
+
clearScheduledScrolls();
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
clearScheduledScrolls,
|
|
144
|
+
scrollAfterLayout,
|
|
145
|
+
scrollNow
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export {
|
|
150
|
+
useScrollToBottom
|
|
151
|
+
};
|