@jskit-ai/assistant-core 0.1.170 → 0.1.172

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 (29) hide show
  1. package/examples/conversation/App.vue +62 -10
  2. package/examples/conversation/README.md +29 -4
  3. package/examples/conversation/attachments.js +45 -0
  4. package/examples/conversation/integrations.json +28 -0
  5. package/examples/conversation/package.json +6 -4
  6. package/examples/conversation/server.js +61 -10
  7. package/fixtures/responsive-assistant/ConversationFixture.vue +54 -1
  8. package/package.json +21 -7
  9. package/src/client/conversation/AssistantAttachmentPreview.vue +38 -0
  10. package/src/client/conversation/AssistantAttachmentQueue.vue +563 -0
  11. package/src/client/conversation/AssistantComposerSupport.vue +235 -0
  12. package/src/client/conversation/AssistantConversationElement.vue +99 -8
  13. package/src/client/conversation/AssistantGoalControl.vue +81 -0
  14. package/src/client/conversation/AssistantMessageAttachments.vue +114 -0
  15. package/src/client/conversation/AssistantModelControl.vue +334 -0
  16. package/src/client/conversation/AssistantQuestionInputs.vue +124 -0
  17. package/src/client/conversation/AssistantTranscript.vue +2 -1
  18. package/src/client/conversation/index.js +9 -0
  19. package/src/client/conversation/useAssistantAttachments.js +991 -0
  20. package/src/client/conversation/useAssistantSuggestions.js +99 -0
  21. package/src/server/index.js +1 -0
  22. package/src/server/lib/aiConnectionClient.js +90 -0
  23. package/src/server/lib/providers/anthropicClient.js +6 -21
  24. package/src/shared/assistantResource.js +7 -0
  25. package/src/shared/conversation/attachments.js +13 -0
  26. package/src/shared/conversation/index.js +1 -0
  27. package/test/aiConnectionClient.test.js +159 -0
  28. package/test/assistantScroll.browser.test.js +144 -0
  29. package/test/suggestions.test.js +114 -0
@@ -0,0 +1,334 @@
1
+ <template>
2
+ <v-menu
3
+ v-model="menuOpen"
4
+ :close-on-content-click="false"
5
+ location="top start"
6
+ transition="scale-transition"
7
+ >
8
+ <template #activator="{ props: menuProps }">
9
+ <v-btn
10
+ v-bind="menuProps"
11
+ aria-label="Choose AI"
12
+ class="assistant-model-control__button"
13
+ density="comfortable"
14
+ :icon="mdiCogOutline"
15
+ rounded="lg"
16
+ size="small"
17
+ :title="buttonTitle"
18
+ type="button"
19
+ variant="tonal"
20
+ />
21
+ </template>
22
+
23
+ <v-sheet
24
+ aria-label="AI session selector"
25
+ border
26
+ class="assistant-model-control"
27
+ :elevation="3"
28
+ rounded="xl"
29
+ >
30
+ <header class="assistant-model-control__header">
31
+ <v-avatar color="primary" size="36" variant="tonal">
32
+ <v-icon :icon="mdiBrain" size="20" />
33
+ </v-avatar>
34
+ <div>
35
+ <strong class="text-title-small">AI controls</strong>
36
+ <span class="text-body-small">{{ selectionSummary }}</span>
37
+ </div>
38
+ </header>
39
+
40
+ <v-sheet
41
+ v-if="changesDisabled"
42
+ class="assistant-model-control__view-only"
43
+ rounded="lg"
44
+ role="status"
45
+ >
46
+ <v-icon :icon="mdiClockOutline" size="18" />
47
+ <span>AI choices are view-only while the assistant is working.</span>
48
+ </v-sheet>
49
+
50
+ <div
51
+ v-if="catalogLoading"
52
+ aria-label="Loading available AIs"
53
+ class="assistant-model-control__loading"
54
+ >
55
+ <v-skeleton-loader type="text, chip@4, text, chip@4" />
56
+ </div>
57
+
58
+ <div
59
+ v-else-if="catalogError"
60
+ class="assistant-model-control__state"
61
+ role="alert"
62
+ >
63
+ <span>{{ catalogError }}</span>
64
+ <v-btn size="small" variant="text" @click="emit('reload')">Try again</v-btn>
65
+ </div>
66
+
67
+ <div
68
+ v-else-if="!providerRows.length"
69
+ class="assistant-model-control__state"
70
+ role="status"
71
+ >
72
+ No configured AIs are available for this session.
73
+ </div>
74
+
75
+ <div v-else class="assistant-model-control__choices">
76
+ <slot name="before-choices" />
77
+ <section
78
+ v-if="providerRows.length > 1"
79
+ aria-label="Provider"
80
+ class="assistant-model-control__section"
81
+ >
82
+ <div class="assistant-model-control__label">Provider</div>
83
+ <div class="assistant-model-control__options">
84
+ <v-btn
85
+ v-for="provider in providerRows"
86
+ :key="provider.id"
87
+ :active="modelProviderId === provider.id"
88
+ :aria-pressed="modelProviderId === provider.id"
89
+ class="assistant-model-control__option"
90
+ :color="modelProviderId === provider.id ? 'primary' : undefined"
91
+ :disabled="changesDisabled || saving"
92
+ rounded="lg"
93
+ size="small"
94
+ type="button"
95
+ :variant="modelProviderId === provider.id ? 'tonal' : 'outlined'"
96
+ @click="selectProvider(provider.id)"
97
+ >
98
+ <span>{{ provider.label }}</span>
99
+ <v-icon v-if="modelProviderId === provider.id" :icon="mdiCheck" size="15" />
100
+ </v-btn>
101
+ </div>
102
+ </section>
103
+
104
+ <section aria-label="Model" class="assistant-model-control__section">
105
+ <div class="assistant-model-control__label">Model</div>
106
+ <v-autocomplete
107
+ v-if="modelRows.length > 6"
108
+ auto-select-first="exact"
109
+ hide-details
110
+ :disabled="changesDisabled || saving"
111
+ :items="modelRows"
112
+ item-title="label"
113
+ item-value="id"
114
+ label="Choose model"
115
+ :model-value="modelId"
116
+ no-data-text="No matching models"
117
+ variant="outlined"
118
+ @update:model-value="selectModel"
119
+ />
120
+ <div v-else-if="modelRows.length" class="assistant-model-control__options">
121
+ <v-btn
122
+ v-for="model in modelRows"
123
+ :key="model.id"
124
+ :active="modelId === model.id"
125
+ :aria-label="model.label"
126
+ :aria-pressed="modelId === model.id"
127
+ class="assistant-model-control__option"
128
+ :color="modelId === model.id ? 'primary' : undefined"
129
+ :disabled="changesDisabled || saving"
130
+ rounded="lg"
131
+ size="small"
132
+ :title="changesDisabled ? 'Wait for the active turn to finish before changing models.' : model.label"
133
+ type="button"
134
+ :variant="modelId === model.id ? 'tonal' : 'outlined'"
135
+ @click="selectModel(model.id)"
136
+ >
137
+ <span>{{ model.label }}</span>
138
+ <v-icon v-if="modelId === model.id" :icon="mdiCheck" size="15" />
139
+ </v-btn>
140
+ </div>
141
+ <span v-else class="assistant-model-control__empty">No available models.</span>
142
+ <slot name="model-note" />
143
+ </section>
144
+
145
+ <slot name="provider-controls" />
146
+ <section
147
+ v-if="variantRows.length > 1"
148
+ aria-label="Thinking"
149
+ class="assistant-model-control__section"
150
+ >
151
+ <div class="assistant-model-control__label">Thinking</div>
152
+ <div class="assistant-model-control__options">
153
+ <v-btn
154
+ v-for="variant in variantRows"
155
+ :key="variant.id || 'automatic'"
156
+ :active="variantId === variant.id"
157
+ :aria-pressed="variantId === variant.id"
158
+ class="assistant-model-control__option"
159
+ :color="variantId === variant.id ? 'primary' : undefined"
160
+ :disabled="changesDisabled || saving"
161
+ rounded="lg"
162
+ size="small"
163
+ type="button"
164
+ :variant="variantId === variant.id ? 'tonal' : 'outlined'"
165
+ @click="selectVariant(variant.id)"
166
+ >
167
+ <span>{{ variant.label }}</span>
168
+ <v-icon v-if="variantId === variant.id" :icon="mdiCheck" size="15" />
169
+ </v-btn>
170
+ </div>
171
+ </section>
172
+ </div>
173
+
174
+ <footer class="assistant-model-control__actions">
175
+ <slot name="footer" />
176
+ <span />
177
+ <v-btn
178
+ color="primary"
179
+ :disabled="!canSave || saving"
180
+ size="small"
181
+ variant="flat"
182
+ @click="emit('apply')"
183
+ >
184
+ {{ saving ? "Applying…" : "Apply" }}
185
+ </v-btn>
186
+ </footer>
187
+ </v-sheet>
188
+ </v-menu>
189
+ </template>
190
+
191
+ <script setup>
192
+ import { mdiBrain, mdiCheck, mdiClockOutline, mdiCogOutline } from "@mdi/js";
193
+ const menuOpen = defineModel({ type: Boolean, default: false });
194
+ const props = defineProps({
195
+ providerRows: { type: Array, default: () => [] },
196
+ modelRows: { type: Array, default: () => [] },
197
+ variantRows: { type: Array, default: () => [] },
198
+ modelProviderId: { type: String, default: "" },
199
+ modelId: { type: String, default: "" },
200
+ variantId: { type: String, default: "" },
201
+ selectionSummary: { type: String, default: "" },
202
+ buttonTitle: { type: String, default: "Choose AI" },
203
+ changesDisabled: Boolean,
204
+ saving: Boolean,
205
+ canSave: Boolean,
206
+ catalogLoading: Boolean,
207
+ catalogError: { type: String, default: "" }
208
+ });
209
+ const emit = defineEmits(["select-provider", "select-model", "select-variant", "apply", "reload"]);
210
+ function selectProvider(value) { if (!props.changesDisabled && !props.saving) emit("select-provider", value); }
211
+ function selectModel(value) { if (!props.changesDisabled && !props.saving) emit("select-model", value); }
212
+ function selectVariant(value) { if (!props.changesDisabled && !props.saving) emit("select-variant", value); }
213
+ </script>
214
+ <style scoped>
215
+ .assistant-model-control__button {
216
+ flex: 0 0 2rem;
217
+ height: 2rem;
218
+ min-height: 2rem;
219
+ min-width: 2rem;
220
+ width: 2rem;
221
+ }
222
+
223
+ .assistant-model-control {
224
+ display: grid;
225
+ gap: 0.75rem;
226
+ max-height: calc(100vh - 2rem);
227
+ max-width: calc(100vw - 2rem);
228
+ min-width: min(24rem, calc(100vw - 2rem));
229
+ overflow-y: auto;
230
+ padding: 0.75rem;
231
+ width: min(24rem, calc(100vw - 2rem));
232
+ }
233
+
234
+ .assistant-model-control__header {
235
+ align-items: center;
236
+ border-bottom: 1px solid rgba(var(--v-theme-outline), 0.12);
237
+ display: flex;
238
+ gap: 0.65rem;
239
+ padding: 0.1rem 0.1rem 0.7rem;
240
+ }
241
+
242
+ .assistant-model-control__header > div {
243
+ display: grid;
244
+ min-width: 0;
245
+ }
246
+
247
+ .assistant-model-control__header span {
248
+ color: rgba(var(--v-theme-on-surface), 0.65);
249
+ overflow: hidden;
250
+ text-overflow: ellipsis;
251
+ white-space: nowrap;
252
+ }
253
+
254
+ .assistant-model-control__view-only {
255
+ align-items: center;
256
+ background: rgba(var(--v-theme-primary), 0.08);
257
+ color: rgba(var(--v-theme-on-surface), 0.78);
258
+ display: flex;
259
+ font-size: 0.78rem;
260
+ gap: 0.5rem;
261
+ line-height: 1.35;
262
+ padding: 0.6rem 0.7rem;
263
+ }
264
+
265
+ .assistant-model-control__choices,
266
+ .assistant-model-control__loading {
267
+ display: grid;
268
+ gap: 0.55rem;
269
+ }
270
+
271
+ .assistant-model-control__section {
272
+ display: grid;
273
+ gap: 0.32rem;
274
+ }
275
+
276
+ .assistant-model-control__label {
277
+ color: rgba(var(--v-theme-on-surface), 0.68);
278
+ font-size: 0.72rem;
279
+ font-weight: 650;
280
+ line-height: 1.2;
281
+ padding-inline: 0.12rem;
282
+ text-transform: uppercase;
283
+ }
284
+
285
+ .assistant-model-control__options {
286
+ display: flex;
287
+ flex-wrap: wrap;
288
+ gap: 0.28rem;
289
+ }
290
+
291
+ .assistant-model-control__option {
292
+ letter-spacing: 0;
293
+ min-height: 2.5rem;
294
+ text-transform: none;
295
+ }
296
+
297
+ .assistant-model-control__empty { color: rgba(var(--v-theme-on-surface), 0.62); font-size: 0.82rem; padding: 0.35rem 0.12rem; }
298
+
299
+ .assistant-model-control__state {
300
+ align-items: center;
301
+ color: rgba(var(--v-theme-on-surface), 0.72);
302
+ display: flex;
303
+ font-size: 0.84rem;
304
+ gap: 0.5rem;
305
+ justify-content: space-between;
306
+ min-height: 4rem;
307
+ padding: 0.5rem 0.25rem;
308
+ }
309
+
310
+ .assistant-model-control__actions {
311
+ align-items: center;
312
+ border-top: 1px solid rgba(var(--v-theme-outline), 0.12);
313
+ display: grid;
314
+ gap: 0.4rem;
315
+ grid-template-columns: auto 1fr auto;
316
+ min-height: 2.5rem;
317
+ padding-top: 0.4rem;
318
+ }
319
+
320
+ @media (pointer: coarse) {
321
+ .assistant-model-control__button {
322
+ flex-basis: 3rem;
323
+ height: 3rem;
324
+ min-height: 3rem;
325
+ min-width: 3rem;
326
+ width: 3rem;
327
+ }
328
+
329
+ .assistant-model-control__option {
330
+ min-height: 3rem;
331
+ }
332
+ }
333
+
334
+ </style>
@@ -0,0 +1,124 @@
1
+ <script setup>
2
+ import { mdiPencilOutline } from "@mdi/js";
3
+ defineProps({
4
+ questions: { type: Array, default: () => [] },
5
+ selectItems: { type: Object, default: () => ({}) },
6
+ choices: { type: Array, default: () => [] }
7
+ });
8
+ const answers = defineModel("answers", { type: Object, default: () => ({}) });
9
+ const choice = defineModel("choice", { type: String, default: "" });
10
+ const emit = defineEmits(["dismiss"]);
11
+ </script>
12
+ <template>
13
+ <div
14
+ v-if="questions.length"
15
+ class="assistant-questions__question-fields"
16
+ aria-label="Assistant questions"
17
+ >
18
+ <div class="assistant-questions__question-fields-header">
19
+ <v-btn
20
+ aria-label="Answer normally instead"
21
+ color="primary"
22
+ :prepend-icon="mdiPencilOutline"
23
+ size="small"
24
+ variant="tonal"
25
+ @click="emit('dismiss')"
26
+ >
27
+ Answer normally instead
28
+ </v-btn>
29
+ </div>
30
+ <div
31
+ v-for="question in questions"
32
+ :key="question.name"
33
+ class="assistant-questions__question-field"
34
+ >
35
+ <v-select
36
+ v-if="question.choices.length"
37
+ :model-value="answers[question.name]"
38
+ @update:model-value="answers = { ...answers, [question.name]: $event }"
39
+ class="assistant-questions__question-select"
40
+ density="compact"
41
+ hide-details="auto"
42
+ item-title="selectLabel"
43
+ item-value="value"
44
+ :items="selectItems[question.name] || question.choices"
45
+ :label="`[${question.number}] ${question.label}`"
46
+ :title="question.label"
47
+ variant="outlined"
48
+ />
49
+ <v-text-field
50
+ v-else
51
+ :model-value="answers[question.name]"
52
+ @update:model-value="answers = { ...answers, [question.name]: $event }"
53
+ autocomplete="off"
54
+ density="compact"
55
+ hide-details="auto"
56
+ :label="`[${question.number}] ${question.label}`"
57
+ :title="question.label"
58
+ variant="outlined"
59
+ />
60
+ </div>
61
+ </div>
62
+ <div
63
+ v-else-if="choices.length"
64
+ class="assistant-questions__answer-choices"
65
+ aria-label="Suggested answers"
66
+ >
67
+ <v-chip-group
68
+ v-model="choice"
69
+ column
70
+ selected-class="text-primary"
71
+ >
72
+ <v-chip
73
+ v-for="option in choices"
74
+ :key="option.value"
75
+ filter
76
+ :value="option.value"
77
+ variant="outlined"
78
+ >
79
+ {{ option.label }}
80
+ </v-chip>
81
+ </v-chip-group>
82
+ </div>
83
+ </template>
84
+ <style scoped>
85
+ .assistant-questions__question-fields {
86
+ display: grid;
87
+ gap: 0.3rem;
88
+ padding: 0.3rem 0.45rem 0;
89
+ }
90
+
91
+ .assistant-questions__question-fields-header {
92
+ align-items: center;
93
+ color: rgba(var(--v-theme-on-surface), 0.7);
94
+ display: flex;
95
+ flex-wrap: wrap;
96
+ font-size: 0.78rem;
97
+ gap: 0.25rem 0.5rem;
98
+ justify-content: space-between;
99
+ min-width: 0;
100
+ }
101
+
102
+ .assistant-questions__question-fields-header > span {
103
+ min-width: 0;
104
+ overflow-wrap: anywhere;
105
+ }
106
+
107
+ .assistant-questions__question-fields-header :deep(.v-btn) {
108
+ margin-left: auto;
109
+ }
110
+
111
+ .assistant-questions__question-field {
112
+ min-width: 0;
113
+ }
114
+
115
+ .assistant-questions__question-select {
116
+ max-width: 100%;
117
+ min-width: 0;
118
+ }
119
+
120
+ .assistant-questions__answer-choices {
121
+ padding: 0.35rem 0.55rem 0;
122
+ }
123
+
124
+ </style>
@@ -144,7 +144,7 @@
144
144
  >
145
145
  {{ userMessageExpanded(turn) ? "Show less" : "Read more" }}
146
146
  </button>
147
- <slot name="attachments" :items="turn.user.attachments" :message="turn.user" />
147
+ <slot name="attachments" :items="turn.user.attachments" :message="turn.user"><AssistantMessageAttachments :attachments="turn.user.attachments || []" /></slot>
148
148
  <div
149
149
  v-if="turn.user.displayAt"
150
150
  class="assistant-transcript__message-footer assistant-transcript__message-footer--user"
@@ -280,6 +280,7 @@
280
280
  </template>
281
281
 
282
282
  <script setup>
283
+ import AssistantMessageAttachments from "./AssistantMessageAttachments.vue";
283
284
  import { computed, nextTick, onBeforeUnmount, ref, watch } from "vue";
284
285
  import {
285
286
  mdiAccountOutline,
@@ -3,7 +3,16 @@ export { default as AssistantTranscript } from "./AssistantTranscript.vue";
3
3
  export { default as AssistantPromptInput } from "./AssistantPromptInput.vue";
4
4
  export { default as AssistantComposerActions } from "./AssistantComposerActions.vue";
5
5
  export { default as AssistantProgress } from "./AssistantProgress.vue";
6
+ export { default as AssistantComposerSupport } from "./AssistantComposerSupport.vue";
7
+ export { default as AssistantGoalControl } from "./AssistantGoalControl.vue";
8
+ export { useAssistantSuggestions } from "./useAssistantSuggestions.js";
6
9
  export { default as LongTextPreviewBlocks } from "./LongTextPreviewBlocks.vue";
7
10
  export { default as LongTextInlineParts } from "./LongTextInlineParts.vue";
8
11
  export { useScrollToBottom } from "./useScrollToBottom.js";
9
12
  export { createAssistantTextSubmission } from "./submitText.js";
13
+ export { useAssistantAttachments } from "./useAssistantAttachments.js";
14
+ export { default as AssistantAttachmentQueue } from "./AssistantAttachmentQueue.vue";
15
+ export { default as AssistantModelControl } from "./AssistantModelControl.vue";
16
+ export { default as AssistantMessageAttachments } from "./AssistantMessageAttachments.vue";
17
+ export { default as AssistantAttachmentPreview } from "./AssistantAttachmentPreview.vue";
18
+ export { default as AssistantQuestionInputs } from "./AssistantQuestionInputs.vue";