@jskit-ai/assistant-core 0.1.171 → 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.
Files changed (31) 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/fixtures/responsive-assistant/ProgressFixture.vue +61 -0
  9. package/fixtures/responsive-assistant/main.js +3 -1
  10. package/package.json +21 -7
  11. package/src/client/conversation/AssistantAttachmentPreview.vue +38 -0
  12. package/src/client/conversation/AssistantAttachmentQueue.vue +563 -0
  13. package/src/client/conversation/AssistantComposerSupport.vue +235 -0
  14. package/src/client/conversation/AssistantConversationElement.vue +103 -9
  15. package/src/client/conversation/AssistantGoalControl.vue +81 -0
  16. package/src/client/conversation/AssistantMessageAttachments.vue +114 -0
  17. package/src/client/conversation/AssistantModelControl.vue +334 -0
  18. package/src/client/conversation/AssistantQuestionInputs.vue +124 -0
  19. package/src/client/conversation/AssistantTranscript.vue +139 -174
  20. package/src/client/conversation/index.js +9 -0
  21. package/src/client/conversation/useAssistantAttachments.js +991 -0
  22. package/src/client/conversation/useAssistantSuggestions.js +99 -0
  23. package/src/server/index.js +1 -0
  24. package/src/server/lib/aiConnectionClient.js +90 -0
  25. package/src/server/lib/providers/anthropicClient.js +6 -21
  26. package/src/shared/assistantResource.js +7 -0
  27. package/src/shared/conversation/attachments.js +13 -0
  28. package/src/shared/conversation/index.js +1 -0
  29. package/test/aiConnectionClient.test.js +159 -0
  30. package/test/assistantScroll.browser.test.js +217 -0
  31. 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>