@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.
Files changed (35) hide show
  1. package/examples/conversation/App.vue +104 -0
  2. package/examples/conversation/README.md +42 -0
  3. package/examples/conversation/index.html +2 -0
  4. package/examples/conversation/main.js +9 -0
  5. package/examples/conversation/package.json +22 -0
  6. package/examples/conversation/server.js +123 -0
  7. package/examples/conversation/vite.config.js +8 -0
  8. package/package.json +59 -7
  9. package/src/client/conversation/AssistantComposerActions.vue +43 -0
  10. package/src/client/conversation/AssistantConversationElement.vue +78 -0
  11. package/src/client/conversation/AssistantProgress.vue +88 -0
  12. package/src/client/conversation/AssistantPromptInput.vue +467 -0
  13. package/src/client/conversation/AssistantTranscript.vue +1361 -0
  14. package/src/client/conversation/LongTextInlineParts.vue +64 -0
  15. package/src/client/conversation/LongTextPreviewBlocks.vue +324 -0
  16. package/src/client/conversation/index.js +9 -0
  17. package/src/client/conversation/submitText.js +29 -0
  18. package/src/client/conversation/useScrollToBottom.js +151 -0
  19. package/src/server/conversation/codexClient.js +328 -0
  20. package/src/server/conversation/codexEvents.js +623 -0
  21. package/src/server/conversation/codexTurn.js +280 -0
  22. package/src/server/conversation/index.js +2 -0
  23. package/src/server/conversation/memoryStorage.js +76 -0
  24. package/src/server/conversation/normalize.js +7 -0
  25. package/src/server/conversation/openCodeClient.js +512 -0
  26. package/src/server/conversation/transcript.js +92 -0
  27. package/src/shared/conversation/answerChoiceSugar.js +191 -0
  28. package/src/shared/conversation/index.js +6 -0
  29. package/src/shared/conversation/longTextBlocks.js +403 -0
  30. package/src/shared/conversation/numberedQuestionSugar.js +295 -0
  31. package/src/shared/conversation/scrollFollowState.js +32 -0
  32. package/src/shared/conversation/thinkingText.js +13 -0
  33. package/src/shared/conversation/turns.js +30 -0
  34. package/src/testing/conversationStorageContract.js +35 -0
  35. package/test/conversation.test.js +57 -0
@@ -0,0 +1,467 @@
1
+ <template>
2
+ <div
3
+ class="assistant-prompt-input"
4
+ :class="{
5
+ 'assistant-prompt-input--compact': density === 'compact',
6
+ 'assistant-prompt-input--dragging': dragging,
7
+ 'assistant-prompt-input--has-attachments': attachmentState.count,
8
+ 'assistant-prompt-input--has-footer': $slots.footer,
9
+ 'assistant-prompt-input--has-input-start': $slots['input-start']
10
+ }"
11
+ >
12
+ <slot name="attachments" />
13
+
14
+ <div
15
+ class="assistant-prompt-input__field"
16
+ :class="{ 'assistant-prompt-input__field--disabled': disabled }"
17
+ >
18
+ <label
19
+ v-if="label"
20
+ class="assistant-prompt-input__label"
21
+ :for="textareaId"
22
+ >
23
+ {{ label }}
24
+ </label>
25
+
26
+ <div
27
+ v-if="$slots['input-start']"
28
+ class="assistant-prompt-input__input-start"
29
+ >
30
+ <slot name="input-start" />
31
+ </div>
32
+
33
+ <textarea
34
+ :id="textareaId"
35
+ ref="textareaRef"
36
+ :aria-describedby="describedBy || undefined"
37
+ :aria-label="textareaAriaLabel"
38
+ class="assistant-prompt-input__input"
39
+ :disabled="disabled"
40
+ :placeholder="placeholder"
41
+ :rows="rows"
42
+ :value="modelValue"
43
+ @blur="handleTextareaBlur"
44
+ @focus="handleTextareaFocus"
45
+ @input="handleTextareaInput"
46
+ @keydown="handleTextareaKeydown"
47
+ @paste="emit('paste', $event)"
48
+ />
49
+
50
+ <div
51
+ v-if="$slots.footer"
52
+ class="assistant-prompt-input__footer"
53
+ >
54
+ <slot name="footer" :attachment-state="attachmentState" />
55
+ </div>
56
+ </div>
57
+
58
+ <div
59
+ v-if="detailsVisible"
60
+ class="assistant-prompt-input__details"
61
+ >
62
+ <div
63
+ v-for="message in combinedErrorMessages"
64
+ :key="message"
65
+ class="assistant-prompt-input__error"
66
+ >
67
+ {{ message }}
68
+ </div>
69
+ <div
70
+ v-if="hintVisible"
71
+ class="assistant-prompt-input__hint"
72
+ >
73
+ {{ hint }}
74
+ </div>
75
+ </div>
76
+ </div>
77
+ </template>
78
+
79
+ <script setup>
80
+ import { computed, onBeforeUnmount, onMounted, ref, useId, watch } from "vue";
81
+ const emit = defineEmits(["blur", "escape", "focus", "input-activity", "submit", "tab-to-submit", "update:modelValue", "paste"]);
82
+ const props = defineProps({
83
+ submitOnModifierEnter: { type: Boolean, default: false },
84
+ ariaLabel: {
85
+ default: "",
86
+ type: String
87
+ },
88
+ attachmentState: { default: () => ({ canSubmit: true, count: 0 }), type: Object },
89
+ dragging: { default: false, type: Boolean },
90
+ autoGrow: {
91
+ default: true,
92
+ type: Boolean
93
+ },
94
+ disabled: {
95
+ default: false,
96
+ type: Boolean
97
+ },
98
+ describedBy: {
99
+ default: "",
100
+ type: String
101
+ },
102
+ errorMessages: {
103
+ default: () => [],
104
+ type: [Array, String]
105
+ },
106
+ hint: {
107
+ default: "",
108
+ type: String
109
+ },
110
+ label: {
111
+ default: "",
112
+ type: String
113
+ },
114
+ modelValue: {
115
+ default: "",
116
+ type: String
117
+ },
118
+ persistentHint: {
119
+ default: false,
120
+ type: Boolean
121
+ },
122
+ placeholderAffectsHeight: {
123
+ default: true,
124
+ type: Boolean
125
+ },
126
+ placeholder: {
127
+ default: "",
128
+ type: String
129
+ },
130
+ rows: {
131
+ default: 4,
132
+ type: [Number, String]
133
+ },
134
+ density: {
135
+ default: "default",
136
+ type: String
137
+ },
138
+ submitOnEnter: {
139
+ default: false,
140
+ type: Boolean
141
+ },
142
+ submitEnabled: {
143
+ default: true,
144
+ type: Boolean
145
+ },
146
+ tabToSubmit: {
147
+ default: false,
148
+ type: Boolean
149
+ },
150
+ variant: {
151
+ default: "outlined",
152
+ type: String
153
+ }
154
+ });
155
+
156
+ const textareaRef = ref(null);
157
+ const textareaId = `assistant-prompt-${useId()}`;
158
+ let resizeFrame = 0;
159
+ let preserveHeightForNextModelValueChange = false;
160
+ const combinedErrorMessages = computed(() => {
161
+ return Array.isArray(props.errorMessages)
162
+ ? props.errorMessages
163
+ : [props.errorMessages].filter(Boolean);
164
+ });
165
+ const hintVisible = computed(() => Boolean(
166
+ props.hint &&
167
+ (props.persistentHint || combinedErrorMessages.value.length < 1)
168
+ ));
169
+ const detailsVisible = computed(() => Boolean(
170
+ combinedErrorMessages.value.length ||
171
+ hintVisible.value
172
+ ));
173
+ const textareaAriaLabel = computed(() => (
174
+ props.ariaLabel
175
+ ? props.ariaLabel
176
+ : undefined
177
+ ));
178
+
179
+ function resizeTextarea() {
180
+ if (!props.autoGrow) {
181
+ return;
182
+ }
183
+ const textarea = textareaRef.value;
184
+ if (!textarea) {
185
+ return;
186
+ }
187
+ if (!props.placeholderAffectsHeight && !textarea.value) {
188
+ return;
189
+ }
190
+ const style = window.getComputedStyle(textarea);
191
+ const minHeight = Number.parseFloat(style.minHeight) || 0;
192
+ const maxHeight = Number.parseFloat(style.maxHeight) || Number.POSITIVE_INFINITY;
193
+ textarea.style.height = "auto";
194
+ const contentHeight = Math.max(textarea.scrollHeight, minHeight);
195
+ const targetHeight = Math.min(contentHeight, maxHeight);
196
+ textarea.style.height = `${targetHeight}px`;
197
+ textarea.style.overflowY = contentHeight > targetHeight + 1 ? "auto" : "hidden";
198
+ }
199
+
200
+ function queueResizeTextarea() {
201
+ if (!props.autoGrow || typeof window === "undefined") {
202
+ return;
203
+ }
204
+ if (resizeFrame) {
205
+ window.cancelAnimationFrame(resizeFrame);
206
+ }
207
+ resizeFrame = window.requestAnimationFrame(() => {
208
+ resizeFrame = 0;
209
+ resizeTextarea();
210
+ });
211
+ }
212
+
213
+ function preserveHeightForNextModelValue() {
214
+ const textarea = textareaRef.value;
215
+ if (!props.autoGrow || !textarea) {
216
+ return false;
217
+ }
218
+ if (resizeFrame) {
219
+ window.cancelAnimationFrame(resizeFrame);
220
+ resizeFrame = 0;
221
+ }
222
+ preserveHeightForNextModelValueChange = true;
223
+ textarea.style.height = `${textarea.getBoundingClientRect().height}px`;
224
+ textarea.style.overflowY = "auto";
225
+ return true;
226
+ }
227
+
228
+ function handleTextareaInput(event = {}) {
229
+ preserveHeightForNextModelValueChange = false;
230
+ emit("input-activity");
231
+ emit("update:modelValue", String(event?.target?.value || ""));
232
+ queueResizeTextarea();
233
+ }
234
+
235
+ function handleTextareaFocus(event = {}) {
236
+ emit("focus", event);
237
+ }
238
+
239
+ function handleTextareaBlur(event = {}) {
240
+ emit("blur", event);
241
+ }
242
+
243
+ function handleTextareaKeydown(event = {}) {
244
+ if (event.key === "Escape") {
245
+ emit("escape", event);
246
+ return;
247
+ }
248
+ if (
249
+ props.tabToSubmit &&
250
+ props.submitEnabled &&
251
+ String(props.modelValue || "").trim() &&
252
+ props.attachmentState.canSubmit &&
253
+ event.key === "Tab" &&
254
+ !event.shiftKey &&
255
+ !event.altKey &&
256
+ !event.ctrlKey &&
257
+ !event.metaKey
258
+ ) {
259
+ event.preventDefault();
260
+ emit("tab-to-submit");
261
+ return;
262
+ }
263
+ if (props.submitOnModifierEnter && event.key === "Enter" && (event.ctrlKey || event.metaKey) &&
264
+ !event.shiftKey && !event.altKey && !event.isComposing && props.submitEnabled && props.attachmentState.canSubmit) {
265
+ event.preventDefault();
266
+ event.stopPropagation();
267
+ emit("submit");
268
+ return;
269
+ }
270
+ if (event.key === "Enter" && !props.submitOnEnter) {
271
+ event.stopPropagation();
272
+ return;
273
+ }
274
+ if (
275
+ !props.submitOnEnter ||
276
+ event.key !== "Enter" ||
277
+ event.shiftKey ||
278
+ event.altKey ||
279
+ event.ctrlKey ||
280
+ event.metaKey ||
281
+ event.isComposing ||
282
+ !props.submitEnabled ||
283
+ !props.attachmentState.canSubmit
284
+ ) {
285
+ return;
286
+ }
287
+ event.preventDefault();
288
+ emit("submit");
289
+ }
290
+
291
+ function focusTextarea(options = { preventScroll: true }) {
292
+ textareaRef.value?.focus?.(options);
293
+ }
294
+
295
+ onMounted(queueResizeTextarea);
296
+
297
+ onBeforeUnmount(() => {
298
+ if (resizeFrame && typeof window !== "undefined") {
299
+ window.cancelAnimationFrame(resizeFrame);
300
+ resizeFrame = 0;
301
+ }
302
+ });
303
+
304
+ watch(() => [
305
+ props.autoGrow,
306
+ props.modelValue,
307
+ props.placeholder,
308
+ props.placeholderAffectsHeight,
309
+ props.rows
310
+ ], (values, previousValues = []) => {
311
+ const modelValueChanged = values[1] !== previousValues[1];
312
+ if (preserveHeightForNextModelValueChange && modelValueChanged) {
313
+ preserveHeightForNextModelValueChange = false;
314
+ return;
315
+ }
316
+ queueResizeTextarea();
317
+ });
318
+
319
+ defineExpose({ inputElement: textareaRef, focus: focusTextarea, preserveHeightForNextModelValue, queueResizeTextarea });
320
+ </script>
321
+ <style scoped>
322
+ .assistant-prompt-input {
323
+ box-sizing: border-box;
324
+ display: grid;
325
+ gap: 0;
326
+ max-width: 100%;
327
+ min-width: 0;
328
+ overflow: hidden;
329
+ padding: 0;
330
+ position: relative;
331
+ text-align: left;
332
+ width: 100%;
333
+ }
334
+
335
+ .assistant-prompt-input__field {
336
+ background: rgb(var(--v-theme-surface));
337
+ border: 1px solid rgba(var(--v-theme-on-surface), 0.34);
338
+ border-radius: 18px;
339
+ box-sizing: border-box;
340
+ box-shadow: inset 0 0 0 1px rgba(var(--v-theme-on-surface), 0.08);
341
+ display: grid;
342
+ max-width: 100%;
343
+ min-width: 0;
344
+ padding-top: 0.01rem;
345
+ transition: border-color 120ms ease, box-shadow 120ms ease;
346
+ }
347
+
348
+ .assistant-prompt-input__field:not(.assistant-prompt-input__field--disabled):focus-within {
349
+ border-color: rgb(var(--v-theme-primary));
350
+ box-shadow:
351
+ 0 0 0 2px rgba(var(--v-theme-primary), 0.28),
352
+ inset 0 0 0 1px rgba(var(--v-theme-primary), 0.2);
353
+ }
354
+
355
+ .assistant-prompt-input__field--disabled {
356
+ background: rgba(var(--v-theme-on-surface), 0.04);
357
+ border-color: rgba(var(--v-theme-on-surface), 0.16);
358
+ box-shadow: none;
359
+ }
360
+
361
+ .assistant-prompt-input__label {
362
+ align-self: start;
363
+ background: rgb(var(--v-theme-surface));
364
+ color: rgba(var(--v-theme-on-surface), 0.82);
365
+ font-size: 0.78rem;
366
+ line-height: 1.1;
367
+ margin: -0.5rem 0 0 0.9rem;
368
+ max-width: calc(100% - 1.8rem);
369
+ overflow: hidden;
370
+ padding-inline: 0.24rem;
371
+ text-overflow: ellipsis;
372
+ white-space: nowrap;
373
+ width: fit-content;
374
+ z-index: 1;
375
+ }
376
+
377
+ .assistant-prompt-input__input {
378
+ background: transparent;
379
+ border: 0;
380
+ box-sizing: border-box;
381
+ color: rgb(var(--v-theme-on-surface));
382
+ display: block;
383
+ font: inherit;
384
+ line-height: 1.4;
385
+ max-height: min(16rem, 32dvh);
386
+ min-height: 3.55rem;
387
+ min-width: 0;
388
+ outline: 0;
389
+ overflow-x: hidden;
390
+ overflow-y: hidden;
391
+ padding: 0.5rem 1rem 0.2rem;
392
+ resize: none;
393
+ width: 100%;
394
+ word-break: break-word;
395
+ }
396
+
397
+ .assistant-prompt-input__input-start {
398
+ min-width: 0;
399
+ padding: 0.5rem 1rem 0.08rem;
400
+ }
401
+
402
+ .assistant-prompt-input--has-input-start .assistant-prompt-input__input {
403
+ padding-top: 0.24rem;
404
+ }
405
+
406
+ .assistant-prompt-input__input::placeholder {
407
+ color: rgba(var(--v-theme-on-surface), 0.58);
408
+ opacity: 1;
409
+ }
410
+
411
+ .assistant-prompt-input__input:disabled {
412
+ color: rgba(var(--v-theme-on-surface), 0.38);
413
+ cursor: not-allowed;
414
+ opacity: 1;
415
+ -webkit-text-fill-color: currentColor;
416
+ }
417
+
418
+ .assistant-prompt-input__input:disabled::placeholder {
419
+ color: inherit;
420
+ opacity: 1;
421
+ }
422
+
423
+ .assistant-prompt-input__footer {
424
+ min-width: 0;
425
+ padding: 0 0.55rem 0.55rem;
426
+ }
427
+
428
+ .assistant-prompt-input--compact .assistant-prompt-input__input {
429
+ max-height: min(8rem, 20dvh);
430
+ min-height: 2.5rem;
431
+ padding: 0.4rem 0.65rem 0.15rem;
432
+ }
433
+
434
+ .assistant-prompt-input--compact .assistant-prompt-input__footer {
435
+ padding: 0 0.3rem 0.25rem;
436
+ }
437
+
438
+ .assistant-prompt-input__details {
439
+ color: rgba(var(--v-theme-on-surface), 0.62);
440
+ display: grid;
441
+ font-size: 0.76rem;
442
+ gap: 0.12rem;
443
+ line-height: 1.3;
444
+ min-width: 0;
445
+ padding: 0.32rem 0.75rem 0;
446
+ }
447
+
448
+ .assistant-prompt-input__error {
449
+ color: rgb(var(--v-theme-error));
450
+ }
451
+
452
+ .assistant-prompt-input--dragging {
453
+ outline: 2px dashed rgb(var(--v-theme-primary));
454
+ outline-offset: 4px;
455
+ }
456
+
457
+ .assistant-prompt-input__file-input {
458
+ display: none;
459
+ }
460
+
461
+ .assistant-prompt-input--has-attachments .assistant-prompt-input__field {
462
+ border-top-left-radius: 0;
463
+ border-top-right-radius: 0;
464
+ margin-top: -1px;
465
+ }
466
+
467
+ </style>