@jskit-ai/assistant-core 0.1.244 → 0.2.0

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 (42) hide show
  1. package/CHANGELOG.md +539 -0
  2. package/examples/conversation/package.json +5 -5
  3. package/locales/en.json +383 -0
  4. package/package.json +7 -6
  5. package/src/client/components/AssistantSettingsFormCard.vue +4 -1
  6. package/src/client/conversation/AssistantAttachmentPreview.vue +6 -3
  7. package/src/client/conversation/AssistantAttachmentQueue.vue +36 -37
  8. package/src/client/conversation/AssistantComposerActions.vue +6 -3
  9. package/src/client/conversation/AssistantComposerSupport.vue +8 -5
  10. package/src/client/conversation/AssistantConversationElement.vue +16 -13
  11. package/src/client/conversation/AssistantConversationStatus.vue +5 -2
  12. package/src/client/conversation/AssistantGoalControl.vue +22 -16
  13. package/src/client/conversation/AssistantMessageAttachments.vue +7 -4
  14. package/src/client/conversation/AssistantModelControl.vue +23 -20
  15. package/src/client/conversation/AssistantProgress.vue +6 -3
  16. package/src/client/conversation/AssistantQuestionInputs.vue +9 -6
  17. package/src/client/conversation/AssistantTranscript.vue +39 -28
  18. package/src/client/conversation/LongTextPreviewBlocks.vue +10 -2
  19. package/src/client/conversation/attachmentSize.js +38 -0
  20. package/src/client/conversation/messageDelivery.js +14 -5
  21. package/src/client/conversation/submitText.js +11 -4
  22. package/src/client/conversation/useAssistantAttachments.js +40 -30
  23. package/src/client/conversation/useAssistantSuggestions.js +4 -1
  24. package/src/client/i18n.js +6 -0
  25. package/src/client/lib/assistantApi.js +14 -2
  26. package/src/server/conversation/attachments.js +24 -14
  27. package/src/server/conversation/claudeProcess.js +4 -2
  28. package/src/server/conversation/claudeTurn.js +14 -11
  29. package/src/server/conversation/codexConversations.js +4 -0
  30. package/src/server/conversation/codexGoalCommands.js +9 -9
  31. package/src/server/conversation/codexHelperExecution.js +4 -0
  32. package/src/server/conversation/codexHelperThreads.js +2 -0
  33. package/src/server/conversation/codexMessageCommands.js +7 -1
  34. package/src/server/conversation/codexTurnControl.js +2 -1
  35. package/src/server/conversation/codexTurnState.js +2 -0
  36. package/src/server/conversation/continuity.js +16 -6
  37. package/src/server/conversation/runtime.js +193 -141
  38. package/src/server/lib/ndjson.js +19 -1
  39. package/src/server/lib/providers/common.js +7 -2
  40. package/src/shared/conversation/numberedQuestionSugar.js +7 -1
  41. package/src/shared/conversation/turns.js +10 -2
  42. package/test/i18nCatalog.test.js +22 -0
@@ -8,20 +8,20 @@
8
8
  <template #activator="{ props: menuProps }">
9
9
  <v-btn
10
10
  v-bind="menuProps"
11
- aria-label="Choose AI"
11
+ :aria-label="t('jskit.assistant.model.chooseAi')"
12
12
  class="assistant-model-control__button"
13
13
  density="comfortable"
14
14
  :icon="mdiCogOutline"
15
15
  rounded="lg"
16
16
  size="small"
17
- :title="buttonTitle"
17
+ :title="buttonTitle ?? t('jskit.assistant.model.chooseAi')"
18
18
  type="button"
19
19
  variant="tonal"
20
20
  />
21
21
  </template>
22
22
 
23
23
  <v-sheet
24
- aria-label="AI session selector"
24
+ :aria-label="t('jskit.assistant.model.selector')"
25
25
  border
26
26
  class="assistant-model-control"
27
27
  :elevation="3"
@@ -32,7 +32,7 @@
32
32
  <v-icon :icon="mdiBrain" size="20" />
33
33
  </v-avatar>
34
34
  <div>
35
- <strong class="text-title-small">AI controls</strong>
35
+ <strong class="text-title-small">{{ t("jskit.assistant.model.controls") }}</strong>
36
36
  <span class="text-body-small">{{ selectionSummary }}</span>
37
37
  </div>
38
38
  </header>
@@ -44,12 +44,12 @@
44
44
  role="status"
45
45
  >
46
46
  <v-icon :icon="mdiClockOutline" size="18" />
47
- <span>AI choices are view-only while the assistant is working.</span>
47
+ <span>{{ t("jskit.assistant.model.viewOnly") }}</span>
48
48
  </v-sheet>
49
49
 
50
50
  <div
51
51
  v-if="catalogLoading"
52
- aria-label="Loading available AIs"
52
+ :aria-label="t('jskit.assistant.model.loading')"
53
53
  class="assistant-model-control__loading"
54
54
  >
55
55
  <v-skeleton-loader type="text, chip@4, text, chip@4" />
@@ -61,7 +61,7 @@
61
61
  role="alert"
62
62
  >
63
63
  <span>{{ catalogError }}</span>
64
- <v-btn size="small" variant="text" @click="emit('reload')">Try again</v-btn>
64
+ <v-btn size="small" variant="text" @click="emit('reload')">{{ t("jskit.assistant.model.tryAgain") }}</v-btn>
65
65
  </div>
66
66
 
67
67
  <div
@@ -69,17 +69,17 @@
69
69
  class="assistant-model-control__state"
70
70
  role="status"
71
71
  >
72
- No configured AIs are available for this session.
72
+ {{ t("jskit.assistant.model.noneConfigured") }}
73
73
  </div>
74
74
 
75
75
  <div v-else class="assistant-model-control__choices">
76
76
  <slot name="before-choices" />
77
77
  <section
78
78
  v-if="providerRows.length > 1"
79
- aria-label="Provider"
79
+ :aria-label="t('jskit.assistant.model.provider')"
80
80
  class="assistant-model-control__section"
81
81
  >
82
- <div class="assistant-model-control__label">Provider</div>
82
+ <div class="assistant-model-control__label">{{ t("jskit.assistant.model.provider") }}</div>
83
83
  <div class="assistant-model-control__options">
84
84
  <v-btn
85
85
  v-for="provider in providerRows"
@@ -101,8 +101,8 @@
101
101
  </div>
102
102
  </section>
103
103
 
104
- <section aria-label="Model" class="assistant-model-control__section">
105
- <div class="assistant-model-control__label">Model</div>
104
+ <section :aria-label="t('jskit.assistant.model.model')" class="assistant-model-control__section">
105
+ <div class="assistant-model-control__label">{{ t("jskit.assistant.model.model") }}</div>
106
106
  <v-autocomplete
107
107
  v-if="modelRows.length > 6"
108
108
  auto-select-first="exact"
@@ -111,9 +111,9 @@
111
111
  :items="modelRows"
112
112
  item-title="label"
113
113
  item-value="id"
114
- label="Choose model"
114
+ :label="t('jskit.assistant.model.chooseModel')"
115
115
  :model-value="modelId"
116
- no-data-text="No matching models"
116
+ :no-data-text="t('jskit.assistant.model.noMatchingModels')"
117
117
  variant="outlined"
118
118
  @update:model-value="selectModel"
119
119
  />
@@ -129,7 +129,7 @@
129
129
  :disabled="changesDisabled || saving"
130
130
  rounded="lg"
131
131
  size="small"
132
- :title="changesDisabled ? 'Wait for the active turn to finish before changing models.' : model.label"
132
+ :title="changesDisabled ? t('jskit.assistant.model.waitBeforeChanging') : model.label"
133
133
  type="button"
134
134
  :variant="modelId === model.id ? 'tonal' : 'outlined'"
135
135
  @click="selectModel(model.id)"
@@ -138,17 +138,17 @@
138
138
  <v-icon v-if="modelId === model.id" :icon="mdiCheck" size="15" />
139
139
  </v-btn>
140
140
  </div>
141
- <span v-else class="assistant-model-control__empty">No available models.</span>
141
+ <span v-else class="assistant-model-control__empty">{{ t("jskit.assistant.model.noModels") }}</span>
142
142
  <slot name="model-note" />
143
143
  </section>
144
144
 
145
145
  <slot name="provider-controls" />
146
146
  <section
147
147
  v-if="variantRows.length > 1"
148
- aria-label="Thinking"
148
+ :aria-label="t('jskit.assistant.model.thinking')"
149
149
  class="assistant-model-control__section"
150
150
  >
151
- <div class="assistant-model-control__label">Thinking</div>
151
+ <div class="assistant-model-control__label">{{ t("jskit.assistant.model.thinking") }}</div>
152
152
  <div class="assistant-model-control__options">
153
153
  <v-btn
154
154
  v-for="variant in variantRows"
@@ -181,7 +181,7 @@
181
181
  variant="flat"
182
182
  @click="emit('apply')"
183
183
  >
184
- {{ saving ? "Applying…" : "Apply" }}
184
+ {{ saving ? t("jskit.assistant.model.applying") : t("jskit.assistant.model.apply") }}
185
185
  </v-btn>
186
186
  </footer>
187
187
  </v-sheet>
@@ -190,6 +190,9 @@
190
190
 
191
191
  <script setup>
192
192
  import { mdiBrain, mdiCheck, mdiClockOutline, mdiCogOutline } from "@mdi/js";
193
+ import { useJskitText } from "../i18n.js";
194
+
195
+ const { t } = useJskitText();
193
196
  const menuOpen = defineModel({ type: Boolean, default: false });
194
197
  const props = defineProps({
195
198
  providerRows: { type: Array, default: () => [] },
@@ -199,7 +202,7 @@ const props = defineProps({
199
202
  modelId: { type: String, default: "" },
200
203
  variantId: { type: String, default: "" },
201
204
  selectionSummary: { type: String, default: "" },
202
- buttonTitle: { type: String, default: "Choose AI" },
205
+ buttonTitle: { type: String, default: undefined },
203
206
  changesDisabled: Boolean,
204
207
  saving: Boolean,
205
208
  canSave: Boolean,
@@ -21,6 +21,9 @@
21
21
 
22
22
  <script setup>
23
23
  import { computed, ref } from "vue";
24
+ import { useJskitText } from "../i18n.js";
25
+
26
+ const { t } = useJskitText();
24
27
 
25
28
  const props = defineProps({
26
29
  messages: { type: Array, default: () => [] },
@@ -42,10 +45,10 @@ const visibleMessages = computed(() => {
42
45
  const toggleLabel = computed(() => {
43
46
  if (expanded.value) {
44
47
  return previewMessages.value.length > 0
45
- ? `Show latest ${previewMessages.value.length} progress ${previewMessages.value.length === 1 ? "update" : "updates"}`
46
- : "Hide progress updates";
48
+ ? t("jskit.assistant.progress.showLatest", { count: previewMessages.value.length })
49
+ : t("jskit.assistant.progress.hide");
47
50
  }
48
- return `Show all ${props.messages.length} progress ${props.messages.length === 1 ? "update" : "updates"}`;
51
+ return t("jskit.assistant.progress.showAll", { count: props.messages.length });
49
52
  });
50
53
  </script>
51
54
 
@@ -1,5 +1,8 @@
1
1
  <script setup>
2
2
  import { mdiPencilOutline } from "@mdi/js";
3
+ import { useJskitText } from "../i18n.js";
4
+
5
+ const { t } = useJskitText();
3
6
  defineProps({
4
7
  questions: { type: Array, default: () => [] },
5
8
  selectItems: { type: Object, default: () => ({}) },
@@ -13,11 +16,11 @@ const emit = defineEmits(["dismiss"]);
13
16
  <div
14
17
  v-if="questions.length"
15
18
  class="assistant-questions__question-fields"
16
- aria-label="Assistant questions"
19
+ :aria-label="t('jskit.assistant.questions.label')"
17
20
  >
18
21
  <div class="assistant-questions__question-fields-header">
19
22
  <v-btn
20
- aria-label="Answer normally instead"
23
+ :aria-label="t('jskit.assistant.questions.answerNormally')"
21
24
  class="ml-auto"
22
25
  color="primary"
23
26
  :prepend-icon="mdiPencilOutline"
@@ -25,7 +28,7 @@ const emit = defineEmits(["dismiss"]);
25
28
  variant="tonal"
26
29
  @click="emit('dismiss')"
27
30
  >
28
- Answer normally instead
31
+ {{ t("jskit.assistant.questions.answerNormally") }}
29
32
  </v-btn>
30
33
  </div>
31
34
  <div
@@ -43,7 +46,7 @@ const emit = defineEmits(["dismiss"]);
43
46
  item-title="selectLabel"
44
47
  item-value="value"
45
48
  :items="selectItems[question.name] || question.choices"
46
- :label="`[${question.number}] ${question.label}`"
49
+ :label="t('jskit.assistant.questions.fieldLabel', { number: question.number, label: question.label })"
47
50
  :title="question.label"
48
51
  variant="outlined"
49
52
  />
@@ -54,7 +57,7 @@ const emit = defineEmits(["dismiss"]);
54
57
  autocomplete="off"
55
58
  density="compact"
56
59
  hide-details="auto"
57
- :label="`[${question.number}] ${question.label}`"
60
+ :label="t('jskit.assistant.questions.fieldLabel', { number: question.number, label: question.label })"
58
61
  :title="question.label"
59
62
  variant="outlined"
60
63
  />
@@ -63,7 +66,7 @@ const emit = defineEmits(["dismiss"]);
63
66
  <div
64
67
  v-else-if="choices.length"
65
68
  class="assistant-questions__answer-choices"
66
- aria-label="Suggested answers"
69
+ :aria-label="t('jskit.assistant.questions.suggestedAnswers')"
67
70
  >
68
71
  <v-chip-group
69
72
  v-model="choice"
@@ -4,16 +4,16 @@
4
4
  v-show="visible"
5
5
  class="assistant-transcript"
6
6
  :class="[`assistant-transcript--${variant}`, { 'assistant-transcript--retained': retainWhenHidden, 'assistant-transcript--error': error }]"
7
- aria-label="Conversation history"
7
+ :aria-label="t('jskit.assistant.transcript.history')"
8
8
  >
9
9
  <v-btn
10
10
  v-if="reloadable"
11
- :aria-label="reloading ? 'Reloading chat' : 'Reload chat'"
11
+ :aria-label="reloadLabel"
12
12
  class="assistant-transcript__reload"
13
13
  :disabled="reloading"
14
14
  :icon="mdiRefresh"
15
15
  size="x-small"
16
- :title="reloading ? 'Reloading chat' : 'Reload chat'"
16
+ :title="reloadLabel"
17
17
  type="button"
18
18
  variant="text"
19
19
  @click="emit('reload')"
@@ -21,7 +21,7 @@
21
21
 
22
22
  <v-skeleton-loader
23
23
  v-if="loadingIndicatorVisible && !reloadable"
24
- aria-label="Loading conversation"
24
+ :aria-label="t('jskit.assistant.transcript.loading')"
25
25
  class="assistant-transcript__loading-skeleton"
26
26
  type="list-item-avatar-two-line@3"
27
27
  />
@@ -32,7 +32,7 @@
32
32
  aria-hidden="true"
33
33
  >
34
34
  <v-skeleton-loader
35
- aria-label="Preparing conversation"
35
+ :aria-label="t('jskit.assistant.transcript.preparing')"
36
36
  class="assistant-transcript__settling-skeleton"
37
37
  type="list-item-avatar-two-line@3"
38
38
  />
@@ -49,10 +49,10 @@
49
49
  <div class="assistant-transcript__error-content">
50
50
  <v-btn
51
51
  v-if="errorReloadable && !reloadable" :disabled="reloading" :min-height="32" size="small"
52
- :aria-label="reloading ? 'Reloading chat' : 'Reload chat'"
52
+ :aria-label="reloadLabel"
53
53
  type="button" variant="text" @click="emit('reload')"
54
54
  >
55
- {{ reloading ? 'Reloading…' : 'Reload chat' }}
55
+ {{ reloading ? t('jskit.assistant.transcript.reloadingShort') : t('jskit.assistant.transcript.reload') }}
56
56
  </v-btn>
57
57
  <span>{{ error }}</span>
58
58
  </div>
@@ -60,7 +60,7 @@
60
60
 
61
61
  <div
62
62
  ref="bodyElement"
63
- aria-label="Conversation messages"
63
+ :aria-label="t('jskit.assistant.transcript.messages')"
64
64
  class="assistant-transcript__body"
65
65
  :class="{ 'assistant-transcript__body--settling': initialScrollPending }"
66
66
  tabindex="0"
@@ -80,7 +80,7 @@
80
80
  <v-icon :icon="mdiRobotOutline" size="16" />
81
81
  </span>
82
82
  <div class="assistant-transcript__message-header">
83
- <span>{{ assistantLabel }}</span>
83
+ <span>{{ assistantName }}</span>
84
84
  </div>
85
85
  </div>
86
86
  <slot name="welcome"><p>{{ welcomeMessage }}</p></slot>
@@ -98,7 +98,7 @@
98
98
  variant="tonal"
99
99
  @click="requestLoadMore"
100
100
  >
101
- {{ loadingMore ? "Loading older messages…" : "Load older messages" }}
101
+ {{ loadingMore ? t("jskit.assistant.transcript.loadingOlder") : t("jskit.assistant.transcript.loadOlder") }}
102
102
  </v-btn>
103
103
  <div
104
104
  v-if="loadMoreError"
@@ -124,7 +124,7 @@
124
124
  />
125
125
  <div class="assistant-transcript__system-body">
126
126
  <div class="assistant-transcript__system-meta">
127
- <span>{{ systemLabel }}</span>
127
+ <span>{{ systemName }}</span>
128
128
  <time v-if="entry.message.displayAt">{{ entry.message.displayAt }}</time>
129
129
  </div>
130
130
  <slot name="system-message" :message="entry.message">
@@ -149,7 +149,7 @@
149
149
  >
150
150
  <v-textarea
151
151
  v-if="entry.turn.preview && previewForTurn(entry.turn)?.actions?.editing"
152
- class="assistant-transcript__preview-editor" :model-value="previewForTurn(entry.turn).text" label="Review your message"
152
+ class="assistant-transcript__preview-editor" :model-value="previewForTurn(entry.turn).text" :label="t('jskit.assistant.transcript.reviewMessage')"
153
153
  :disabled="!previewForTurn(entry.turn).actions.canEdit" density="compact" :rows="1" :max-rows="2"
154
154
  auto-grow hide-details @update:model-value="previewForTurn(entry.turn).actions.update(entry.turn.turnId, $event)"
155
155
  />
@@ -167,7 +167,7 @@
167
167
  type="button"
168
168
  @click="toggleUserMessage(entry.turn)"
169
169
  >
170
- {{ userMessageExpanded(entry.turn) ? "Show less" : "Read more" }}
170
+ {{ userMessageExpanded(entry.turn) ? t("jskit.assistant.transcript.showLess") : t("jskit.assistant.transcript.readMore") }}
171
171
  </button>
172
172
  <slot name="attachments" :items="entry.message.attachments" :message="entry.message"><AssistantMessageAttachments :attachments="entry.message.attachments || []" /></slot>
173
173
  <div
@@ -175,14 +175,14 @@
175
175
  class="assistant-transcript__message-footer assistant-transcript__message-footer--user"
176
176
  >
177
177
  <time v-if="entry.message.displayAt">{{ entry.message.displayAt }}</time>
178
- <span v-if="entry.turn.optimistic?.status === 'pending'" role="status">{{ entry.turn.optimistic.waiting ? `Pending: ${entry.turn.optimistic.waiting}` : "Pending" }}</span>
178
+ <span v-if="entry.turn.optimistic?.status === 'pending'" role="status">{{ entry.turn.optimistic.waiting ? t("jskit.assistant.transcript.pendingReason", { reason: entry.turn.optimistic.waiting }) : t("jskit.assistant.transcript.pending") }}</span>
179
179
  </div>
180
180
  <slot name="user-message-actions" :message="entry.message" :turn="entry.turn" />
181
181
  <div
182
182
  v-if="entry.delivery || entry.turn.optimistic?.status === 'failed'"
183
183
  class="assistant-transcript__optimistic-failure"
184
184
  >
185
- <span role="status">{{ entry.delivery ? entry.delivery.delivery.error || 'Delivery unconfirmed.' : `Failed: ${entry.turn.optimistic.error || 'Message could not be sent.'}` }}</span>
185
+ <span role="status">{{ entry.delivery ? entry.delivery.delivery.error || t('jskit.assistant.delivery.unconfirmedShort') : t('jskit.assistant.transcript.failed', { error: entry.turn.optimistic.error || t('jskit.assistant.delivery.notSent') }) }}</span>
186
186
  <div v-if="!entry.turn.preview" class="assistant-transcript__optimistic-actions">
187
187
  <v-btn
188
188
  class="assistant-transcript__delivery-action"
@@ -191,11 +191,11 @@
191
191
  type="button"
192
192
  variant="text"
193
193
  :disabled="entry.delivery?.delivery.checking"
194
- :aria-label="entry.delivery ? 'Retry delivery check' : undefined"
195
- :title="entry.delivery ? entry.delivery.delivery.error || 'Check delivery before resending.' : undefined"
194
+ :aria-label="entry.delivery ? t('jskit.assistant.delivery.retryCheck') : undefined"
195
+ :title="entry.delivery ? entry.delivery.delivery.error || t('jskit.assistant.delivery.checkBeforeResend') : undefined"
196
196
  @click="entry.delivery ? emit('check-delivery', entry.delivery.delivery.messageId) : emit('resend-turn', entry.turn.optimistic.id)"
197
197
  >
198
- {{ entry.delivery?.delivery.checking ? 'Checking…' : 'Retry' }}
198
+ {{ entry.delivery?.delivery.checking ? t('jskit.assistant.delivery.checking') : t('jskit.assistant.delivery.retry') }}
199
199
  </v-btn>
200
200
  <v-btn
201
201
  v-if="!entry.delivery || cancellableDelivery"
@@ -205,7 +205,7 @@
205
205
  variant="text"
206
206
  @click="emit('cancel-turn', entry.delivery?.delivery.messageId || entry.turn.optimistic.id)"
207
207
  >
208
- Cancel
208
+ {{ t("jskit.assistant.transcript.cancel") }}
209
209
  </v-btn>
210
210
  <v-btn
211
211
  v-if="!entry.delivery"
@@ -215,7 +215,7 @@
215
215
  variant="text"
216
216
  @click="emit('edit-turn', entry.turn.optimistic.id)"
217
217
  >
218
- Edit
218
+ {{ t("jskit.assistant.transcript.edit") }}
219
219
  </v-btn>
220
220
  </div>
221
221
  </div>
@@ -232,7 +232,7 @@
232
232
  :messages="entry.messages"
233
233
  :pending="isWorking && entry === displayEntries.at(-1)"
234
234
  :preview-limit="progressPreviewLimit"
235
- :aria-label="`${entry.turn.assistantLabel || assistantLabel} progress`"
235
+ :aria-label="t('jskit.assistant.transcript.progress', { name: entry.turn.assistantLabel || assistantName })"
236
236
  />
237
237
  <div
238
238
  v-else
@@ -245,7 +245,7 @@
245
245
  </span>
246
246
  <div class="assistant-transcript__message-header">
247
247
  <span :title="entry.message.assistantDetails || entry.turn.assistantDetails || undefined">
248
- {{ entry.message.assistantLabel || entry.turn.assistantLabel || assistantLabel }}
248
+ {{ entry.message.assistantLabel || entry.turn.assistantLabel || assistantName }}
249
249
  </span>
250
250
  </div>
251
251
  </div>
@@ -280,7 +280,7 @@
280
280
  <LongTextInlineParts
281
281
  :text="choice.label"
282
282
  @link-click="handleLongTextLinkClick"
283
- /><span v-if="choice.recommended"> · Recommended</span>
283
+ /><span v-if="choice.recommended">{{ t("jskit.assistant.transcript.recommended") }}</span>
284
284
  </li>
285
285
  </ul>
286
286
  </div>
@@ -333,6 +333,10 @@ import {
333
333
  import {
334
334
  normalizeThinkingMessageText
335
335
  } from "../../shared/conversation/thinkingText.js";
336
+ import { useJskitText } from "../i18n.js";
337
+
338
+ const jskitText = useJskitText();
339
+ const { formatDateTime, serverMessage, t } = jskitText;
336
340
 
337
341
  function previewForTurn(turn) {
338
342
  const preview = turn.previewMessage || props.previewMessage;
@@ -341,13 +345,13 @@ function previewForTurn(turn) {
341
345
 
342
346
  const props = defineProps({
343
347
  cancellableDelivery: { type: Boolean, default: false },
344
- systemLabel: { type: String, default: "Status" },
348
+ systemLabel: { type: String, default: undefined },
345
349
  previewMessage: { type: Object, default: null },
346
350
  working: { type: Boolean, default: undefined },
347
351
  progressPreviewLimit: { type: Number, default: 2 },
348
352
  userMessageFormat: { type: String, default: "formatted", validator: (value) => ["plain", "formatted"].includes(value) },
349
353
  assistantLabel: {
350
- default: "Assistant",
354
+ default: undefined,
351
355
  type: String
352
356
  },
353
357
  error: {
@@ -414,6 +418,9 @@ const props = defineProps({
414
418
  });
415
419
 
416
420
  const emit = defineEmits(["cancel-turn", "check-delivery", "edit-turn", "load-more", "link-click", "reload", "resend-turn"]);
421
+ const systemName = computed(() => props.systemLabel ?? t("jskit.assistant.transcript.status"));
422
+ const assistantName = computed(() => props.assistantLabel ?? t("jskit.assistant.transcript.assistant"));
423
+ const reloadLabel = computed(() => props.reloading ? t("jskit.assistant.transcript.reloading") : t("jskit.assistant.transcript.reload"));
417
424
 
418
425
  const USER_MESSAGE_COLLAPSE_MIN_CHARACTERS = 360;
419
426
  const USER_MESSAGE_PREVIEW_MAX_CHARACTERS = 280;
@@ -447,7 +454,7 @@ const KEYBOARD_SCROLL_KEYS = new Set([
447
454
  "PageUp",
448
455
  "Spacebar"
449
456
  ]);
450
- const timeFormatter = new Intl.DateTimeFormat(undefined, {
457
+ const TIME_FORMAT = Object.freeze({
451
458
  year: "numeric",
452
459
  month: "short",
453
460
  day: "2-digit",
@@ -460,7 +467,7 @@ function displayTime(value = "") {
460
467
  if (Number.isNaN(date.getTime())) {
461
468
  return "";
462
469
  }
463
- return timeFormatter.format(date);
470
+ return formatDateTime(date, TIME_FORMAT);
464
471
  }
465
472
 
466
473
  function userMessagePreviewText(value = "") {
@@ -491,8 +498,11 @@ function displayMessage(message = null, {
491
498
  }
492
499
  const normalizedCacheKey = String(cacheKey || "").trim();
493
500
  const cached = normalizedCacheKey ? displayMessageCache.get(normalizedCacheKey) : null;
501
+ // Times and status texts follow the active locale, so a locale change re-renders them.
502
+ const activeLocale = jskitText.locale;
494
503
  if (
495
504
  cached &&
505
+ cached.locale === activeLocale &&
496
506
  cached.allowNumberedQuestions === allowNumberedQuestions &&
497
507
  cached.attachments === message.attachments &&
498
508
  cached.at === message.at &&
@@ -504,7 +514,7 @@ function displayMessage(message = null, {
504
514
  ) {
505
515
  return { ...message, ...cached.value };
506
516
  }
507
- const messageText = message.text;
517
+ const messageText = message.textCode ? serverMessage({ message: message.text, messageCode: message.textCode }) : message.text;
508
518
  const questionInput = allowNumberedQuestions
509
519
  ? parseNumberedQuestionPrompt(messageText)
510
520
  : {
@@ -538,6 +548,7 @@ function displayMessage(message = null, {
538
548
  allowNumberedQuestions,
539
549
  attachments: message.attachments,
540
550
  at: message.at,
551
+ locale: activeLocale,
541
552
  messageId: message.messageId,
542
553
  preserveParagraphLineBreaks,
543
554
  previewUserMessage,
@@ -47,7 +47,7 @@
47
47
  v-else-if="block.type === 'table'"
48
48
  class="studio-long-text-review__table-wrap"
49
49
  role="region"
50
- aria-label="Table"
50
+ :aria-label="t('jskit.assistant.longText.table')"
51
51
  tabindex="0"
52
52
  >
53
53
  <table class="studio-long-text-review__table">
@@ -91,7 +91,7 @@
91
91
  >
92
92
  <summary class="studio-long-text-review__details-summary">
93
93
  <LongTextInlineParts
94
- :text="block.summary || 'Details'"
94
+ :text="detailsSummary(block)"
95
95
  @link-click="emit('link-click', $event)"
96
96
  />
97
97
  </summary>
@@ -117,6 +117,9 @@
117
117
 
118
118
  <script setup>
119
119
  import LongTextInlineParts from "./LongTextInlineParts.vue";
120
+ import { useJskitText } from "../i18n.js";
121
+
122
+ const { serverMessage, t } = useJskitText();
120
123
 
121
124
  defineProps({
122
125
  blocks: {
@@ -130,6 +133,11 @@ defineProps({
130
133
  });
131
134
  const emit = defineEmits(["link-click"]);
132
135
 
136
+ // The parser's default summary is JSKIT's English; an authored summary stays as written.
137
+ function detailsSummary(block) {
138
+ return serverMessage({ message: block.summary, messageCode: "assistant.longText.details" });
139
+ }
140
+
133
141
  function isListBlock(block) {
134
142
  return block.type === "ul" || block.type === "ol";
135
143
  }
@@ -0,0 +1,38 @@
1
+ import { attachmentSizeLabel } from "../../shared/conversation/attachments.js";
2
+ import { useJskitText } from "../i18n.js";
3
+
4
+ const jskitText = useJskitText();
5
+ const { formatNumber, t } = jskitText;
6
+ const WHOLE = Object.freeze({ useGrouping: false });
7
+ const ONE_DECIMAL = Object.freeze({ maximumFractionDigits: 1, minimumFractionDigits: 1, useGrouping: false });
8
+
9
+ /** A file size in the active language: bytes, then KB and MB with one decimal. */
10
+ function attachmentSizeText(size) {
11
+ // Without an app language, sizes keep their fixed English digits.
12
+ if (!jskitText.locale) {
13
+ return attachmentSizeLabel(size);
14
+ }
15
+ const bytes = Number(size);
16
+ if (!Number.isFinite(bytes) || bytes < 0) {
17
+ return "";
18
+ }
19
+ if (bytes < 1024) {
20
+ return t("jskit.assistant.attachments.sizeBytes", { size: formatNumber(bytes, WHOLE) });
21
+ }
22
+ if (bytes < 1024 * 1024) {
23
+ return t("jskit.assistant.attachments.sizeKilobytes", { size: formatNumber(bytes / 1024, ONE_DECIMAL) });
24
+ }
25
+ return t("jskit.assistant.attachments.sizeMegabytes", { size: formatNumber(bytes / (1024 * 1024), ONE_DECIMAL) });
26
+ }
27
+
28
+ /** An upload limit: whole mebibytes when exact, otherwise decimal megabytes. */
29
+ function attachmentLimitText(maxBytes) {
30
+ if (!jskitText.locale) {
31
+ return maxBytes % (1024 * 1024) === 0 ? `${maxBytes / (1024 * 1024)} MiB` : `${maxBytes / 1_000_000} MB`;
32
+ }
33
+ return maxBytes % (1024 * 1024) === 0
34
+ ? t("jskit.assistant.attachments.sizeMebibytes", { size: formatNumber(maxBytes / (1024 * 1024), WHOLE) })
35
+ : t("jskit.assistant.attachments.sizeMegabytes", { size: formatNumber(maxBytes / 1_000_000, { useGrouping: false }) });
36
+ }
37
+
38
+ export { attachmentLimitText, attachmentSizeText };
@@ -1,4 +1,7 @@
1
1
  import { reactive, toRaw, toValue, watch } from "vue";
2
+ import { useJskitText } from "../i18n.js";
3
+
4
+ const { serverMessage, t } = useJskitText();
2
5
 
3
6
  const messageText = value => String(value || "").trim();
4
7
 
@@ -92,7 +95,7 @@ function createAssistantMessageDelivery({ deliver: defaultDeliver } = {}) {
92
95
  text: message.text
93
96
  };
94
97
  const status = message.status === "uncertain" ? {
95
- role: "system", text: "Message delivery is not confirmed.",
98
+ role: "system", text: t("jskit.assistant.delivery.notConfirmed"),
96
99
  delivery: { messageId: message.id, error: message.error, checking: message.checking === true }
97
100
  } : null;
98
101
  return {
@@ -145,7 +148,7 @@ function createAssistantMessageDelivery({ deliver: defaultDeliver } = {}) {
145
148
  if (predecessor) await predecessor;
146
149
  if (!current()) return false;
147
150
  if (uncertainOnError && state.messages.some(entry => entry.id !== messageId && entry.status === "uncertain" && !knownUncertain.has(entry.id))) {
148
- fail(messageId, "Check the previous message's delivery before sending again.");
151
+ fail(messageId, t("jskit.assistant.delivery.checkPrevious"));
149
152
  return false;
150
153
  }
151
154
  let receipt;
@@ -161,7 +164,7 @@ function createAssistantMessageDelivery({ deliver: defaultDeliver } = {}) {
161
164
  const response = await (receipt ? Promise.race([submitted, receipt.promise]) : submitted);
162
165
  if (!current()) return false;
163
166
  if (response === false || response?.ok === false) {
164
- fail(messageId, response?.error || "Message could not be sent.", response?.status === "uncertain" ? "uncertain" : "failed");
167
+ fail(messageId, response?.error || t("jskit.assistant.delivery.notSent"), response?.status === "uncertain" ? "uncertain" : "failed");
165
168
  } else {
166
169
  accept(messageId);
167
170
  }
@@ -185,7 +188,9 @@ function createAssistantMessageDelivery({ deliver: defaultDeliver } = {}) {
185
188
  function fail(messageId, error, status = "failed") {
186
189
  const message = find(messageId);
187
190
  if (message && message.status !== "accepted") {
188
- message.error = String(error?.message || error || "Message could not be sent.").trim();
191
+ // A server or client error renders in the active language; its English is the fallback.
192
+ const text = error && typeof error === "object" ? serverMessage(error) : "";
193
+ message.error = String(text || error?.message || error || t("jskit.assistant.delivery.notSent")).trim();
189
194
  message.status = status;
190
195
  }
191
196
  }
@@ -226,7 +231,11 @@ function createAssistantMessageDelivery({ deliver: defaultDeliver } = {}) {
226
231
  attachments: payload.displayAttachments, createdAt, createdAtMs: Date.parse(createdAt),
227
232
  status: "uncertain", error: "" }];
228
233
  }
229
- fail(messageId, request.error || "Delivery could not be confirmed.", "uncertain");
234
+ // The server's English stays the fallback when its sentence has no translation.
235
+ const error = request.error
236
+ ? serverMessage({ message: request.error, messageCode: request.errorMessageCode, messageParams: request.errorMessageParams })
237
+ : "";
238
+ fail(messageId, error || t("jskit.assistant.delivery.unconfirmed"), "uncertain");
230
239
  pendingSends.get(messageId)?.receipt?.resolve({ ok: false, status: "uncertain", error: request.error });
231
240
  return true;
232
241
  }