@jskit-ai/assistant-core 0.2.21 → 0.2.22

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/CHANGELOG.md CHANGED
@@ -2,8 +2,12 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ - The goal menu closes when Cancel succeeds, as it does after Start; Pause and Resume keep it open.
6
+ - `AssistantConversationElement` has an `avatar-thumbnail` slot, with the `{ size, height }` scope of the other avatar slots: while the avatar is hidden, the **Show avatar** button draws its content instead of the anonymous account icon, such as a still miniature of the avatar, clipped to the button's round 32-pixel disc. The button keeps its label, title and 44-pixel target, and shows the icon as before without the slot.
5
7
  - A delivered Codex turn is held as not yet started only until Codex reports it: a `turn/started` that arrives before the `turn/start` answer counts, and a thread read listing exactly that turn as completed or failed settles a turn whose notifications were missed, so its answer is no longer kept waiting for a notification that never comes.
6
8
  - The API driver's state note, which carries the current time, follows the conversation as its last message instead of ending the system prompt, so the provider's prompt cache keeps the whole history from one turn to the next instead of charging it uncached on each turn's first model call (`@jskit-ai/agent` `runLoop`).
9
+ - The assistant settings card is an elevated Material 3 card without a border, and the long-text preview's table alignment no longer needs `!important`.
10
+ - Icons are Material Symbols `$aliases` (from `@jskit-ai/material-web`) instead of `@mdi/js` paths, and text uses Vuetify 4's Material 3 type classes (`text-body-large`, `text-title-medium`, ...) instead of Material 2 ones, which Vuetify 4 no longer styles. Upgrading: see the guide's [Move an app to the Material 3 defaults](https://mobily-enterprises.github.io/jskit-ai/guide/app-setup/material-3-migration).
7
11
  - Assistant failures that reach a person translate: 63 more undotted codes render from `jskit.errors.<code>`, and 98 sentences that share a code with others, or are sent without one, carry a `messageCode` (`assistant.claude.*`, `assistant.codex.*`, `assistant.opencode.*`, `assistant.conversation.*`, `assistant.turn.*`). The English messages are unchanged.
8
12
  - The OpenCode version mismatch, Claude control-request timeout and unreadable saved Claude conversation errors carry `params` for their catalog English.
9
13
  - The assistant settings resource sends its system prompt field messages in the coded `{ code, message, params }` form.
@@ -2,8 +2,9 @@ import { createApp } from "vue";
2
2
  import { createVuetify } from "vuetify";
3
3
  import * as components from "vuetify/components";
4
4
  import * as directives from "vuetify/directives";
5
- import { aliases, mdi } from "vuetify/iconsets/mdi-svg";
6
5
  import "vuetify/styles";
6
+ import "@jskit-ai/material-web/client/material.css";
7
+ import { createMaterialVuetifyOptions } from "@jskit-ai/material-web/client";
7
8
  import App from "./App.vue";
8
9
 
9
- createApp(App).use(createVuetify({ components, directives, icons: { defaultSet: "mdi", aliases, sets: { mdi } } })).mount("#app");
10
+ createApp(App).use(createVuetify({ components, directives, ...createMaterialVuetifyOptions() })).mount("#app");
@@ -8,14 +8,14 @@
8
8
  "build": "vite build"
9
9
  },
10
10
  "dependencies": {
11
- "@jskit-ai/assistant-core": "0.2.21",
12
- "@jskit-ai/http-runtime": "0.2.9",
13
- "@jskit-ai/kernel": "0.2.9",
14
- "@mdi/js": "^7.4.47",
11
+ "@jskit-ai/assistant-core": "0.2.22",
12
+ "@jskit-ai/http-runtime": "0.2.10",
13
+ "@jskit-ai/kernel": "0.2.10",
14
+ "@jskit-ai/material-web": "0.1.0",
15
15
  "vue": "^3.5.13",
16
16
  "vuetify": "^4.0.0",
17
- "@jskit-ai/connectors-core": "0.2.10",
18
- "@jskit-ai/connectors-catalog": "0.2.10"
17
+ "@jskit-ai/connectors-core": "0.2.11",
18
+ "@jskit-ai/connectors-catalog": "0.2.11"
19
19
  },
20
20
  "devDependencies": {
21
21
  "@vitejs/plugin-vue": "^6.0.7",
@@ -1,24 +1,23 @@
1
1
  import { createApp } from "vue";
2
2
  import "vuetify/styles";
3
+ import "@jskit-ai/material-web/client/material.css";
4
+ import { createMaterialVuetifyOptions } from "@jskit-ai/material-web/client";
3
5
  import { createVuetify } from "vuetify";
4
6
  import * as components from "vuetify/components";
5
7
  import * as directives from "vuetify/directives";
6
- import { aliases as mdiAliases, mdi } from "vuetify/iconsets/mdi-svg";
7
8
  import App from "./App.vue";
8
9
  import ConversationFixture from "./ConversationFixture.vue";
9
10
  import ProgressFixture from "./ProgressFixture.vue";
10
11
  import DeliveryFixture from "./DeliveryFixture.vue";
11
12
 
12
13
  const query = new URLSearchParams(location.search);
14
+ const material = createMaterialVuetifyOptions();
13
15
  const vuetify = createVuetify({
14
16
  components,
15
17
  directives,
16
- icons: {
17
- defaultSet: "mdi",
18
- aliases: mdiAliases,
19
- sets: { mdi }
20
- },
18
+ ...material,
21
19
  theme: {
20
+ ...material.theme,
22
21
  defaultTheme: query.has("dark") ? "dark" : "light"
23
22
  }
24
23
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jskit-ai/assistant-core",
3
- "version": "0.2.21",
3
+ "version": "0.2.22",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "test": "node --test --test-concurrency=1"
@@ -52,9 +52,8 @@
52
52
  "@ai-sdk/togetherai": "^3.0.49",
53
53
  "@ai-sdk/xai": "^4.0.58",
54
54
  "@jskit-ai/agent": "0.1.1",
55
- "@jskit-ai/resource-core": "0.1.200",
56
- "@jskit-ai/resource-crud-core": "0.1.200",
57
- "@mdi/js": "^7.4.47",
55
+ "@jskit-ai/resource-core": "0.1.201",
56
+ "@jskit-ai/resource-crud-core": "0.1.201",
58
57
  "@openrouter/ai-sdk-provider": "^3.0.0",
59
58
  "ai": "^7.0.152",
60
59
  "json-rest-schema": "^1.0.17",
@@ -62,10 +61,10 @@
62
61
  "ws": "^8.20.1"
63
62
  },
64
63
  "peerDependencies": {
65
- "@jskit-ai/http-runtime": "0.2.9",
66
- "@jskit-ai/kernel": "0.2.9",
64
+ "@jskit-ai/http-runtime": "0.2.10",
65
+ "@jskit-ai/kernel": "0.2.10",
67
66
  "vue": "^3.5.13",
68
- "vuetify": "^4.0.0"
67
+ "vuetify": "^4.3.0"
69
68
  },
70
69
  "description": "Reusable assistant client/server/shared primitives without surface-specific routes or settings ownership.",
71
70
  "jskit": {
@@ -219,5 +218,8 @@
219
218
  "capabilities": []
220
219
  }
221
220
  }
221
+ },
222
+ "devDependencies": {
223
+ "@jskit-ai/material-web": "0.1.0"
222
224
  }
223
225
  }
@@ -1,8 +1,8 @@
1
1
  <template>
2
2
  <section :class="props.rootClass">
3
- <v-card rounded="lg" elevation="1" border>
3
+ <v-card>
4
4
  <v-card-item>
5
- <v-card-title class="text-h6">{{ props.title }}</v-card-title>
5
+ <v-card-title class="text-title-large">{{ props.title }}</v-card-title>
6
6
  <v-card-subtitle>{{ props.subtitle }}</v-card-subtitle>
7
7
  </v-card-item>
8
8
  <v-divider />
@@ -11,11 +11,11 @@
11
11
  <v-skeleton-loader type="text@2, list-item-two-line@4, button" />
12
12
  </template>
13
13
 
14
- <p v-else-if="props.addEdit.loadError" class="text-body-2 text-medium-emphasis mb-4">
14
+ <p v-else-if="props.addEdit.loadError" class="text-body-medium on-surface-variant mb-4">
15
15
  {{ props.addEdit.loadError }}
16
16
  </p>
17
17
 
18
- <p v-else-if="!props.addEdit.canView" class="text-body-2 text-medium-emphasis mb-4">
18
+ <p v-else-if="!props.addEdit.canView" class="text-body-medium on-surface-variant mb-4">
19
19
  {{ props.noPermissionMessage }}
20
20
  </p>
21
21
 
@@ -13,7 +13,7 @@
13
13
  <p v-else>{{ t("jskit.assistant.attachments.previewUnavailable") }}</p>
14
14
  </v-card-text>
15
15
  <v-card-actions>
16
- <v-btn :href="downloadUrl" :download="attachment.fileName" :prepend-icon="mdiDownload" variant="tonal">{{ t("jskit.assistant.attachments.download") }}</v-btn>
16
+ <v-btn :href="downloadUrl" :download="attachment.fileName" prepend-icon="$download" variant="tonal">{{ t("jskit.assistant.attachments.download") }}</v-btn>
17
17
  <v-spacer />
18
18
  <v-btn @click="emit('close')">{{ t("jskit.assistant.attachments.close") }}</v-btn>
19
19
  </v-card-actions>
@@ -23,7 +23,6 @@
23
23
 
24
24
  <script setup>
25
25
  import { ref, watch } from "vue";
26
- import { mdiDownload } from "@mdi/js";
27
26
  import { useJskitText } from "../i18n.js";
28
27
 
29
28
  const { t } = useJskitText();
@@ -74,7 +74,7 @@
74
74
  :ref="(element) => setActionElement(item, 'cancel', element)"
75
75
  :aria-label="t('jskit.assistant.attachments.cancelItem', { name: itemName(item) })"
76
76
  class="assistant-attachment-queue__action"
77
- :icon="mdiCloseCircleOutline"
77
+ icon="$cancel"
78
78
  size="small"
79
79
  :title="t('jskit.assistant.attachments.cancelItem', { name: itemName(item) })"
80
80
  type="button"
@@ -86,7 +86,7 @@
86
86
  :ref="(element) => setActionElement(item, 'retry', element)"
87
87
  :aria-label="t('jskit.assistant.attachments.retryItem', { name: itemName(item) })"
88
88
  class="assistant-attachment-queue__action"
89
- :icon="mdiRefresh"
89
+ icon="$refresh"
90
90
  size="small"
91
91
  :title="t('jskit.assistant.attachments.retryItem', { name: itemName(item) })"
92
92
  type="button"
@@ -98,7 +98,7 @@
98
98
  :ref="(element) => setActionElement(item, 'remove', element)"
99
99
  :aria-label="t('jskit.assistant.attachments.removeItem', { name: itemName(item) })"
100
100
  class="assistant-attachment-queue__action"
101
- :icon="mdiClose"
101
+ icon="$close"
102
102
  size="small"
103
103
  :title="t('jskit.assistant.attachments.removeItem', { name: itemName(item) })"
104
104
  type="button"
@@ -122,11 +122,6 @@
122
122
 
123
123
  <script setup>
124
124
  import { computed, nextTick, onBeforeUnmount, onMounted, ref } from "vue";
125
- import {
126
- mdiClose,
127
- mdiCloseCircleOutline,
128
- mdiRefresh
129
- } from "@mdi/js";
130
125
 
131
126
  import { hasWindowFunction } from "../browser.js";
132
127
  import { useJskitText } from "../i18n.js";
@@ -4,7 +4,7 @@
4
4
  <div class="assistant-composer-actions__delivery">
5
5
  <v-btn
6
6
  v-if="state.canStop" color="error" :disabled="state.stopDisabled || state.stopPending"
7
- :aria-busy="state.stopPending ? 'true' : undefined" :prepend-icon="mdiStopCircleOutline"
7
+ :aria-busy="state.stopPending ? 'true' : undefined" prepend-icon="$stopCircle"
8
8
  size="small" variant="tonal" @click="emit('stop')"
9
9
  >
10
10
  {{ state.stopPending ? t('jskit.assistant.composer.stopping') : t('jskit.assistant.composer.stop') }}
@@ -12,7 +12,7 @@
12
12
  <v-btn
13
13
  ref="sendButton" class="assistant-composer-actions__send" color="primary" :disabled="!state.canSend"
14
14
  :aria-busy="state.pending && !state.canSend ? 'true' : undefined" :aria-label="state.submitAriaLabel || state.submitLabel || t('jskit.assistant.composer.sendMessage')"
15
- :title="state.submitTitle" :prepend-icon="state.submitIcon || mdiSend" size="small" variant="flat"
15
+ :title="state.submitTitle" :prepend-icon="state.submitIcon || '$send'" size="small" variant="flat"
16
16
  @click="emit('submit')"
17
17
  >
18
18
  {{ state.submitLabel || (state.pending && !state.canSend ? t('jskit.assistant.composer.sending') : t('jskit.assistant.composer.send')) }}
@@ -23,7 +23,6 @@
23
23
  </template>
24
24
  <script setup>
25
25
  import { ref } from "vue";
26
- import { mdiSend, mdiStopCircleOutline } from "@mdi/js";
27
26
  import { useJskitText } from "../i18n.js";
28
27
 
29
28
  const { t } = useJskitText();
@@ -29,7 +29,7 @@
29
29
  <v-icon
30
30
  aria-hidden="true"
31
31
  class="assistant-composer-support__marker"
32
- :icon="mdiLightbulbOnOutline"
32
+ icon="$lightbulb"
33
33
  size="16"
34
34
  />
35
35
 
@@ -73,7 +73,6 @@
73
73
 
74
74
  <script setup>
75
75
  import { computed } from "vue";
76
- import { mdiLightbulbOnOutline } from "@mdi/js";
77
76
  import { useJskitText } from "../i18n.js";
78
77
 
79
78
  const { t } = useJskitText();
@@ -7,14 +7,14 @@
7
7
  <div v-show="avatarSize === 'hidden'" ref="avatarControls" class="assistant-conversation__avatar-controls">
8
8
  <slot name="avatar-control" :size="avatarSize" :height="avatarHeight" />
9
9
  <v-btn
10
- :icon="mdiAccountCircleOutline" size="small"
10
+ icon="$accountCircle" size="small"
11
11
  variant="text" :width="44" :min-width="44" :min-height="44"
12
12
  class="assistant-conversation__avatar-size"
13
13
  :aria-label="t('jskit.assistant.chat.showAvatar')" :title="t('jskit.assistant.chat.showAvatar')"
14
14
  :aria-expanded="avatarSize !== 'hidden'" @click="toggleAvatar"
15
15
  >
16
16
  <span class="assistant-conversation__avatar-size-disc">
17
- <v-icon size="20" :icon="mdiAccountCircleOutline" />
17
+ <slot name="avatar-thumbnail" :size="avatarSize" :height="avatarHeight"><v-icon size="20" icon="$accountCircle" /></slot>
18
18
  </span>
19
19
  </v-btn>
20
20
  </div>
@@ -25,13 +25,13 @@
25
25
  <div class="assistant-conversation__avatar-tools">
26
26
  <slot name="avatar-tools" :size="avatarSize" :height="avatarHeight" />
27
27
  <v-btn
28
- :icon="mdiMinus" size="small" variant="text" color="primary"
28
+ icon="$minus" size="small" variant="text" color="primary"
29
29
  :width="40" :min-width="40" :min-height="44" class="assistant-conversation__avatar-size"
30
30
  :aria-label="t('jskit.assistant.chat.minimiseAvatar')" :title="t('jskit.assistant.chat.minimiseAvatar')" :aria-expanded="true"
31
31
  @click="toggleAvatar"
32
32
  >
33
33
  <span class="assistant-conversation__avatar-size-disc assistant-conversation__avatar-size-disc--minimise">
34
- <v-icon size="20" :icon="mdiMinus" />
34
+ <v-icon size="20" icon="$minus" />
35
35
  </span>
36
36
  </v-btn>
37
37
  </div>
@@ -49,13 +49,13 @@
49
49
  <template #user-message-actions="scope">
50
50
  <div v-if="scope.turn.preview && scope.turn.previewMessage?.actions" class="assistant-conversation__preview-actions">
51
51
  <v-btn
52
- v-if="scope.turn.previewMessage.actions.discard" :icon="mdiClose" size="small" variant="text"
52
+ v-if="scope.turn.previewMessage.actions.discard" icon="$close" size="small" variant="text"
53
53
  :aria-label="t('jskit.assistant.chat.discardUnsent')" :title="t('jskit.assistant.chat.discardUnsent')"
54
54
  :disabled="!scope.turn.previewMessage.actions.canDiscard"
55
55
  @click="scope.turn.previewMessage.actions.discard(scope.turn.turnId)"
56
56
  />
57
57
  <v-btn
58
- v-if="scope.turn.previewMessage.actions.edit" :icon="mdiPencil" size="small" variant="text"
58
+ v-if="scope.turn.previewMessage.actions.edit" icon="$edit" size="small" variant="text"
59
59
  :aria-label="t('jskit.assistant.chat.editUnsent')" :title="t('jskit.assistant.chat.editUnsent')"
60
60
  :disabled="!scope.turn.previewMessage.actions.canEdit"
61
61
  @click="scope.turn.previewMessage.actions.edit(scope.turn.turnId)"
@@ -129,7 +129,7 @@
129
129
  @select-variant="adapter.models.selectVariant?.($event)" @apply="applyModel" @reload="adapter.models.reload?.()"
130
130
  />
131
131
  <v-btn
132
- v-if="attachmentsEnabled" :aria-label="t('jskit.assistant.chat.attachFiles')" :title="t('jskit.assistant.chat.attachFiles')" size="small" variant="text" :icon="mdiPaperclip"
132
+ v-if="attachmentsEnabled" :aria-label="t('jskit.assistant.chat.attachFiles')" :title="t('jskit.assistant.chat.attachFiles')" size="small" variant="text" icon="$attachFile"
133
133
  :disabled="!adapter.attachments.canAddFiles" @click="fileInput?.click()"
134
134
  />
135
135
  <slot
@@ -158,7 +158,6 @@
158
158
  </template>
159
159
  <script setup>
160
160
  import { computed, onBeforeUnmount, onMounted, onUpdated, ref, useId, useSlots, watch } from "vue";
161
- import { mdiAccountCircleOutline, mdiClose, mdiMinus, mdiPaperclip, mdiPencil } from "@mdi/js";
162
161
  import AssistantTranscript from "./AssistantTranscript.vue";
163
162
  import AssistantPromptInput from "./AssistantPromptInput.vue";
164
163
  import AssistantComposerActions from "./AssistantComposerActions.vue";
@@ -371,7 +370,7 @@ defineExpose({ focus: () => input.value?.focus(), submit, stop });
371
370
  .assistant-conversation__avatar-controls { position: absolute; top: 0; right: 100%; display: flex; align-items: center; justify-content: flex-end; min-height: 44px; }
372
371
  .assistant-conversation__avatar--hidden .assistant-conversation__avatar-controls { right: 0; }
373
372
  .assistant-conversation__avatar-size { min-width: 44px; min-height: 44px; }
374
- .assistant-conversation__avatar-size-disc { display: flex; align-items: center; justify-content: center; width: 32px; height: 32px; border-radius: 50%; background: rgba(var(--v-theme-on-surface), .08); }
373
+ .assistant-conversation__avatar-size-disc { display: flex; align-items: center; justify-content: center; width: 32px; height: 32px; overflow: hidden; border-radius: 50%; background: rgba(var(--v-theme-on-surface), .08); }
375
374
  .assistant-conversation__avatar-size-disc--minimise { background: rgba(var(--v-theme-primary), .12); }
376
375
  .assistant-conversation__avatar-presentation { display: flex; flex-direction: column; align-items: center; min-width: 0; }
377
376
  .assistant-conversation__avatar-tools { display: flex; align-items: center; gap: 0; }
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <div v-if="message.delivery" class="d-flex flex-wrap align-center ga-1 text-caption text-error" role="status">
2
+ <div v-if="message.delivery" class="d-flex flex-wrap align-center ga-1 text-body-small text-error" role="status">
3
3
  <span>{{ message.delivery.error || message.text }}</span>
4
4
  <v-btn
5
5
  variant="text"
@@ -63,6 +63,11 @@ async function setGoal() {
63
63
  const result = await props.state.set({ objective: objective.value.trim(), ...(props.state.tokenBudgetSupported === false || tokenBudget.value === "" ? {} : { tokenBudget: Number(tokenBudget.value) }) });
64
64
  if (result !== false && result?.ok !== false && !props.state.error) { objective.value = ""; tokenBudget.value = ""; open.value = false; }
65
65
  }
66
+ // A successful Cancel closes the menu, as Start does; Pause and Resume keep it open.
67
+ async function cancelGoal() {
68
+ const result = await props.state.cancel();
69
+ if (result !== false && result?.ok !== false && !props.state.error) open.value = false;
70
+ }
66
71
  </script>
67
72
 
68
73
  <template>
@@ -78,7 +83,7 @@ async function setGoal() {
78
83
  <strong role="status">{{ label }}</strong>
79
84
  <v-btn v-if="(running || (state.pauseBlocked && goal?.status === 'blocked')) && state.pause" size="small" :disabled="state.stopPending" @click="state.pause()">{{ state.action === 'pause' ? t('jskit.assistant.goal.pausing') : t('jskit.assistant.goal.pause') }}</v-btn>
80
85
  <v-btn v-if="['paused', 'blocked', 'usageLimited'].includes(goal?.status) && state.resume" size="small" :disabled="state.pending" @click="state.resume()">{{ state.action === 'resume' ? t('jskit.assistant.goal.resuming') : t('jskit.assistant.goal.resume') }}</v-btn>
81
- <v-btn v-if="goal && goal.status !== 'complete' && state.cancel" color="error" size="small" variant="text" :disabled="state.stopPending" @click="state.cancel()">{{ state.action === 'cancel' ? t('jskit.assistant.goal.cancelling') : t('jskit.assistant.goal.cancel') }}</v-btn>
86
+ <v-btn v-if="goal && goal.status !== 'complete' && state.cancel" color="error" size="small" variant="text" :disabled="state.stopPending" @click="cancelGoal">{{ state.action === 'cancel' ? t('jskit.assistant.goal.cancelling') : t('jskit.assistant.goal.cancel') }}</v-btn>
82
87
  <p v-if="goal" class="my-2">{{ goal.objective }}</p>
83
88
  <p v-if="goal?.reason" class="text-body-small my-2">{{ goal.reason }}</p>
84
89
  <p v-if="elapsed" class="text-body-small my-2">{{ elapsedText }}</p>
@@ -41,7 +41,6 @@
41
41
  </template>
42
42
 
43
43
  <script setup>
44
- import { mdiFileOutline, mdiImageOutline } from "@mdi/js";
45
44
  import { useJskitText } from "../i18n.js";
46
45
  import { attachmentSizeText } from "./attachmentSize.js";
47
46
 
@@ -53,7 +52,7 @@ defineProps({
53
52
  });
54
53
  const emit = defineEmits(["preview"]);
55
54
  function attachmentIcon(attachment) {
56
- return /\.(?:avif|bmp|gif|heic|heif|jpe?g|png|svg|webp)$/iu.test(attachment.fileName) ? mdiImageOutline : mdiFileOutline;
55
+ return /\.(?:avif|bmp|gif|heic|heif|jpe?g|png|svg|webp)$/iu.test(attachment.fileName) ? "$image" : "$description";
57
56
  }
58
57
  function attachmentKey(attachment, index) {
59
58
  return attachment.attachmentId || `${index}:${attachment.fileName}:${attachment.size ?? ""}`;
@@ -11,7 +11,7 @@
11
11
  :aria-label="t('jskit.assistant.model.chooseAi')"
12
12
  class="assistant-model-control__button"
13
13
  density="comfortable"
14
- :icon="mdiCogOutline"
14
+ icon="$settings"
15
15
  rounded="lg"
16
16
  size="small"
17
17
  :title="buttonTitle ?? t('jskit.assistant.model.chooseAi')"
@@ -29,7 +29,7 @@
29
29
  >
30
30
  <header class="assistant-model-control__header">
31
31
  <v-avatar color="primary" size="36" variant="tonal">
32
- <v-icon :icon="mdiBrain" size="20" />
32
+ <v-icon icon="$psychology" size="20" />
33
33
  </v-avatar>
34
34
  <div>
35
35
  <strong class="text-title-small">{{ t("jskit.assistant.model.controls") }}</strong>
@@ -43,7 +43,7 @@
43
43
  rounded="lg"
44
44
  role="status"
45
45
  >
46
- <v-icon :icon="mdiClockOutline" size="18" />
46
+ <v-icon icon="$schedule" size="18" />
47
47
  <span>{{ t("jskit.assistant.model.viewOnly") }}</span>
48
48
  </v-sheet>
49
49
 
@@ -96,7 +96,7 @@
96
96
  @click="selectProvider(provider.id)"
97
97
  >
98
98
  <span>{{ provider.label }}</span>
99
- <v-icon v-if="modelProviderId === provider.id" :icon="mdiCheck" size="15" />
99
+ <v-icon v-if="modelProviderId === provider.id" icon="$check" size="15" />
100
100
  </v-btn>
101
101
  </div>
102
102
  </section>
@@ -135,7 +135,7 @@
135
135
  @click="selectModel(model.id)"
136
136
  >
137
137
  <span>{{ model.label }}</span>
138
- <v-icon v-if="modelId === model.id" :icon="mdiCheck" size="15" />
138
+ <v-icon v-if="modelId === model.id" icon="$check" size="15" />
139
139
  </v-btn>
140
140
  </div>
141
141
  <span v-else class="assistant-model-control__empty">{{ t("jskit.assistant.model.noModels") }}</span>
@@ -165,7 +165,7 @@
165
165
  @click="selectVariant(variant.id)"
166
166
  >
167
167
  <span>{{ variant.label }}</span>
168
- <v-icon v-if="variantId === variant.id" :icon="mdiCheck" size="15" />
168
+ <v-icon v-if="variantId === variant.id" icon="$check" size="15" />
169
169
  </v-btn>
170
170
  </div>
171
171
  </section>
@@ -189,7 +189,6 @@
189
189
  </template>
190
190
 
191
191
  <script setup>
192
- import { mdiBrain, mdiCheck, mdiClockOutline, mdiCogOutline } from "@mdi/js";
193
192
  import { useJskitText } from "../i18n.js";
194
193
 
195
194
  const { t } = useJskitText();
@@ -1,5 +1,4 @@
1
1
  <script setup>
2
- import { mdiPencilOutline } from "@mdi/js";
3
2
  import { useJskitText } from "../i18n.js";
4
3
 
5
4
  const { t } = useJskitText();
@@ -23,7 +22,7 @@ const emit = defineEmits(["dismiss"]);
23
22
  :aria-label="t('jskit.assistant.questions.answerNormally')"
24
23
  class="ml-auto"
25
24
  color="primary"
26
- :prepend-icon="mdiPencilOutline"
25
+ prepend-icon="$edit"
27
26
  size="small"
28
27
  variant="tonal"
29
28
  @click="emit('dismiss')"
@@ -325,13 +325,13 @@
325
325
  }
326
326
 
327
327
  .assistant-transcript__message-header time {
328
- color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity));
328
+ color: rgb(var(--v-theme-on-surface-variant));
329
329
  font-weight: 650;
330
330
  }
331
331
 
332
332
  .assistant-transcript__message-footer {
333
333
  align-items: center;
334
- color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity));
334
+ color: rgb(var(--v-theme-on-surface-variant));
335
335
  display: flex;
336
336
  font-size: 0.88rem;
337
337
  font-weight: 500;
@@ -11,7 +11,7 @@
11
11
  :aria-label="reloadLabel"
12
12
  class="assistant-transcript__reload"
13
13
  :disabled="reloading"
14
- :icon="mdiRefresh"
14
+ icon="$refresh"
15
15
  size="x-small"
16
16
  :title="reloadLabel"
17
17
  type="button"
@@ -78,7 +78,7 @@
78
78
  >
79
79
  <div class="assistant-transcript__assistant-header">
80
80
  <span class="assistant-transcript__avatar assistant-transcript__avatar--assistant">
81
- <v-icon :icon="mdiRobotOutline" size="16" />
81
+ <v-icon icon="$smartToy" size="16" />
82
82
  </span>
83
83
  <div class="assistant-transcript__message-header">
84
84
  <span>{{ assistantName }}</span>
@@ -120,7 +120,7 @@
120
120
  >
121
121
  <v-icon
122
122
  class="assistant-transcript__system-icon"
123
- :icon="mdiInformationOutline"
123
+ icon="$info"
124
124
  size="15"
125
125
  />
126
126
  <div class="assistant-transcript__system-body">
@@ -225,7 +225,7 @@
225
225
  </div>
226
226
  </div>
227
227
  <span class="assistant-transcript__avatar assistant-transcript__avatar--user">
228
- <v-icon :icon="mdiAccountOutline" size="15" />
228
+ <v-icon icon="$person" size="15" />
229
229
  </span>
230
230
  </div>
231
231
 
@@ -245,7 +245,7 @@
245
245
  >
246
246
  <div class="assistant-transcript__assistant-header">
247
247
  <span class="assistant-transcript__avatar assistant-transcript__avatar--assistant">
248
- <v-icon :icon="mdiRobotOutline" size="16" />
248
+ <v-icon icon="$smartToy" size="16" />
249
249
  </span>
250
250
  <div class="assistant-transcript__message-header">
251
251
  <span :title="entry.message.assistantDetails || entry.turn.assistantDetails || undefined">
@@ -319,12 +319,6 @@
319
319
  import AssistantMessageAttachments from "./AssistantMessageAttachments.vue";
320
320
  import AssistantConversationStatus from "./AssistantConversationStatus.vue";
321
321
  import { computed, ref, watch } from "vue";
322
- import {
323
- mdiAccountOutline,
324
- mdiInformationOutline,
325
- mdiRefresh,
326
- mdiRobotOutline
327
- } from "@mdi/js";
328
322
  import LongTextInlineParts from "./LongTextInlineParts.vue";
329
323
  import LongTextPreviewBlocks from "./LongTextPreviewBlocks.vue";
330
324
  import AssistantProgress from "./AssistantProgress.vue";
@@ -310,13 +310,13 @@ h5.studio-long-text-review__heading {
310
310
  word-break: normal;
311
311
  }
312
312
 
313
- .studio-long-text-review__table-cell--right {
313
+ .studio-long-text-review__table .studio-long-text-review__table-cell--right {
314
314
  font-variant-numeric: tabular-nums;
315
- text-align: right !important;
315
+ text-align: right;
316
316
  }
317
317
 
318
- .studio-long-text-review__table-cell--center {
319
- text-align: center !important;
318
+ .studio-long-text-review__table .studio-long-text-review__table-cell--center {
319
+ text-align: center;
320
320
  }
321
321
 
322
322
  .studio-long-text-review__details {
@@ -1,5 +1,4 @@
1
1
  // How the attachment queue shows each item: its key, name, phase, progress, status text, icon and colour.
2
- import { mdiAlertCircleOutline, mdiCheckCircleOutline, mdiFileOutline } from "@mdi/js";
3
2
  import { useJskitText } from "../i18n.js";
4
3
  import { attachmentSizeText } from "./attachmentSize.js";
5
4
 
@@ -86,12 +85,12 @@ function itemCancelable(item = {}) {
86
85
 
87
86
  function itemIcon(item = {}) {
88
87
  if (itemPhase(item) === "ready") {
89
- return mdiCheckCircleOutline;
88
+ return "$checkCircle";
90
89
  }
91
90
  if (itemPhase(item) === "failed") {
92
- return mdiAlertCircleOutline;
91
+ return "$error";
93
92
  }
94
- return mdiFileOutline;
93
+ return "$description";
95
94
  }
96
95
 
97
96
  function itemColor(item = {}) {
@@ -0,0 +1,62 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { readFileSync } from "node:fs";
4
+ import { registerHooks } from "node:module";
5
+ import { compileScript, parse } from "@vue/compiler-sfc";
6
+ import { createSSRApp, defineComponent, h } from "vue";
7
+ import { renderToString } from "vue/server-renderer";
8
+
9
+ const elementUrl = new URL("../src/client/conversation/AssistantConversationElement.vue", import.meta.url).href;
10
+ const textUrl = new URL("../src/client/i18n.js", import.meta.url).href;
11
+
12
+ async function importElement() {
13
+ const hooks = registerHooks({
14
+ load(url, context, nextLoad) {
15
+ if (url === textUrl) {
16
+ return { format: "module", shortCircuit: true, source: "export function useJskitText() { return { t: (key) => key }; }" };
17
+ }
18
+ if (!url.endsWith(".vue")) return nextLoad(url, context);
19
+ if (url !== elementUrl) return { format: "module", shortCircuit: true, source: "export default { render: () => null };" };
20
+ const { descriptor } = parse(readFileSync(new URL(url), "utf8"));
21
+ return {
22
+ format: "module", shortCircuit: true,
23
+ source: compileScript(descriptor, { id: "assistant-avatar-thumbnail", inlineTemplate: true }).content
24
+ };
25
+ }
26
+ });
27
+ try {
28
+ return (await import(elementUrl)).default;
29
+ } finally {
30
+ hooks.deregister();
31
+ }
32
+ }
33
+
34
+ async function renderShowAvatarButton(element, slots) {
35
+ const app = createSSRApp({ render: () => h(element, { adapter: { conversation: { turns: [] } }, avatarSize: "hidden" }, slots) });
36
+ app.component("VBtn", defineComponent({ setup: (_props, { slots: content }) => () => h("button", content.default?.()) }));
37
+ app.component("VIcon", defineComponent({ props: { icon: { type: String, default: "" } }, setup: (props) => () => h("i", { "data-icon": props.icon }) }));
38
+ const html = await renderToString(app);
39
+ const buttons = [...html.matchAll(/<button([^>]*)>(.*?)<\/button>/gs)].filter(([, attributes]) => attributes.includes('aria-label="jskit.assistant.chat.showAvatar"'));
40
+ assert.equal(buttons.length, 1, "One Show avatar button while the avatar is hidden");
41
+ const [, attributes, content] = buttons[0];
42
+ assert.match(attributes, /title="jskit.assistant.chat.showAvatar"/u);
43
+ assert.match(attributes, /width="44"/u);
44
+ return content;
45
+ }
46
+
47
+ test("the hidden avatar's Show button shows the avatar-thumbnail slot, or the account icon without one", async () => {
48
+ const element = await importElement();
49
+ const avatar = () => h("span", { class: "fixture-face" });
50
+
51
+ const thumbnail = await renderShowAvatarButton(element, {
52
+ avatar,
53
+ "avatar-thumbnail": ({ size, height }) => h("span", { class: "fixture-thumbnail", "data-size": size, "data-height": String(height) })
54
+ });
55
+ assert.match(thumbnail, /class="assistant-conversation__avatar-size-disc"/u);
56
+ assert.match(thumbnail, /<span class="fixture-thumbnail" data-size="hidden" data-height="0"><\/span>/u);
57
+ assert.doesNotMatch(thumbnail, /data-icon=/u);
58
+
59
+ const fallback = await renderShowAvatarButton(element, { avatar });
60
+ assert.match(fallback, /<i data-icon="\$accountCircle"[^>]*><\/i>/u);
61
+ assert.doesNotMatch(fallback, /fixture-thumbnail/u);
62
+ });
@@ -38,7 +38,7 @@ function readText(element) {
38
38
  return {
39
39
  color: [color.red, color.green, color.blue, color.alpha],
40
40
  onSurface: style.getPropertyValue("--v-theme-on-surface").split(",").map(Number),
41
- mediumEmphasis: Number(style.getPropertyValue("--v-medium-emphasis-opacity")),
41
+ onSurfaceVariant: style.getPropertyValue("--v-theme-on-surface-variant").split(",").map(Number),
42
42
  contrast: (lighter + 0.05) / (darker + 0.05)
43
43
  };
44
44
  }
@@ -67,15 +67,17 @@ test("the task transcript's user bubble, header time and footer follow the light
67
67
  element.append(time);
68
68
  });
69
69
  const samples = {
70
- "user bubble": [bubble.locator(".assistant-transcript__user-content"), 1],
71
- "user footer": [bubble.locator(".assistant-transcript__message-footer time"), "medium"],
72
- "assistant footer": [page.locator(".assistant-transcript__message-footer--assistant time").first(), "medium"],
73
- "header time": [header.locator("time"), "medium"]
70
+ "user bubble": [bubble.locator(".assistant-transcript__user-content"), "on-surface"],
71
+ "user footer": [bubble.locator(".assistant-transcript__message-footer time"), "on-surface-variant"],
72
+ "assistant footer": [page.locator(".assistant-transcript__message-footer--assistant time").first(), "on-surface-variant"],
73
+ "header time": [header.locator("time"), "on-surface-variant"]
74
74
  };
75
- for (const [name, [locator, alpha]] of Object.entries(samples)) {
75
+ // Material 3 secondary text is the solid on-surface-variant role, not on-surface at a lower opacity.
76
+ for (const [name, [locator, role]] of Object.entries(samples)) {
76
77
  const text = await locator.evaluate(readText);
77
- assert.deepEqual(text.color.slice(0, 3), text.onSurface, `${theme} ${name}: the colour is the theme's on-surface`);
78
- assert.equal(text.color[3], alpha === "medium" ? text.mediumEmphasis : alpha, `${theme} ${name}: the opacity`);
78
+ const expected = role === "on-surface" ? text.onSurface : text.onSurfaceVariant;
79
+ assert.deepEqual(text.color.slice(0, 3), expected, `${theme} ${name}: the colour is the theme's ${role}`);
80
+ assert.equal(text.color[3], 1, `${theme} ${name}: the opacity`);
79
81
  assert.ok(text.contrast >= 4.5, `${theme} ${name}: contrast ${text.contrast.toFixed(2)}:1 is under 4.5:1`);
80
82
  }
81
83
  await page.close();