@k2b/cloud 0.27.0 → 0.28.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.
- package/package.json +2 -2
- package/src/ai/browser-code-contracts.ts +33 -63
- package/src/ai/browser.ts +1 -0
- package/src/ai/chat/blocks.tsx +16 -7
- package/src/ai/chat/live-turn.browser-harness.tsx +6 -3
- package/src/ai/chat/message-actions.tsx +123 -101
- package/src/ai/chat/message-utils.ts +30 -1
- package/src/ai/chat/messages.ts +198 -0
- package/src/ai/chat/presentation.tsx +66 -20
- package/src/ai/chat/tool-groups.ts +1 -1
- package/src/ai/chat/turn-error.ts +21 -0
- package/src/ai/chat/turn-view.tsx +55 -4
- package/src/ai/chat/user-message.tsx +19 -14
- package/src/ai/chat/visual-tools.tsx +1 -1
- package/src/ai/client/controller.ts +23 -6
- package/src/ai/code-mode-skill.ts +21 -25
- package/src/ai/code-runtime-tools.ts +5 -1
- package/src/ai/code-source-contracts.ts +2 -2
- package/src/ai/data-analysis-skill.ts +3 -3
- package/src/ai/default-tools.ts +15 -16
- package/src/ai/executor.ts +135 -59
- package/src/ai/index.ts +2 -0
- package/src/ai/open-tool-calls.ts +87 -0
- package/src/ai/routes.ts +6 -0
- package/src/ai/run-timeout.ts +4 -5
- package/src/ai/runtime.ts +8 -0
- package/src/ai/skill-seeds.ts +4 -4
- package/src/ai/store.ts +46 -14
- package/src/ai/system-prompt.ts +4 -21
- package/src/ai/turn-failure.ts +100 -0
- package/src/ai/turn-policy.ts +2 -1
- package/src/ai/types.ts +28 -1
- package/src/api/admin-outgoing-mail.ts +185 -5
- package/src/browser/FileChooser.tsx +11 -0
- package/src/browser/file-chooser-messages.ts +2 -0
- package/src/cli/admin/index.ts +3 -1
- package/src/cli/admin/notifications.ts +6 -0
- package/src/cli/admin/outgoing-mail.ts +104 -7
- package/src/contracts/outgoing-mail.ts +189 -1
- package/src/services/help/store.ts +2 -1
- package/src/services/index.ts +11 -1
- package/src/services/notifications/batches.ts +139 -79
- package/src/services/notifications/channels.ts +33 -13
- package/src/services/notifications/dispatcher.ts +57 -5
- package/src/services/notifications/email-frame.fixture.html +51 -0
- package/src/services/notifications/{email.ts → email-frame.ts} +12 -35
- package/src/services/notifications/email-mail.ts +101 -0
- package/src/services/notifications/index.ts +49 -36
- package/src/services/notifications/observability.ts +9 -1
- package/src/services/notifications/platform.ts +1 -1
- package/src/services/notifications/runtime.ts +9 -3
- package/src/services/outgoing-mail/admin.ts +84 -0
- package/src/services/outgoing-mail/attachments.ts +135 -0
- package/src/services/outgoing-mail/bulk.ts +11 -0
- package/src/services/outgoing-mail/dispatcher.ts +231 -0
- package/src/services/outgoing-mail/drain.ts +60 -0
- package/src/services/outgoing-mail/enqueue.ts +180 -0
- package/src/services/outgoing-mail/index.ts +103 -10
- package/src/services/outgoing-mail/message.ts +14 -0
- package/src/services/outgoing-mail/messages.ts +431 -0
- package/src/services/outgoing-mail/retention.ts +48 -0
- package/src/services/outgoing-mail/runtime.ts +72 -0
- package/src/services/outgoing-mail/send.ts +130 -0
- package/src/services/outgoing-mail/store.ts +115 -4
- package/src/services/outgoing-mail/sync.ts +39 -0
- package/src/services/outgoing-mail/transport.ts +5 -1
- package/src/services/pdf/markdown.ts +22 -4
- package/src/services/postgres.ts +15 -0
- package/src/services/settings/core-settings.ts +18 -0
- package/src/shared/ai-platform-prompt.ts +48 -4
- package/src/shared/markdown/extensions/links.ts +28 -20
- package/src/shared/markdown/index.ts +14 -5
- package/src/shared/markdown/shared.ts +0 -7
- package/src/ssr/GlobalAnnouncements.island.tsx +1 -1
- package/src/ssr/platform-messages.ts +1 -1
- package/src/styles/effects.css +15 -17
- package/src/styles/tokens.css +2 -0
- package/src/styles/utilities-markdown-editor.css +4 -39
- package/src/styles/utilities-markdown-table.css +14 -17
|
@@ -7,15 +7,20 @@ import {
|
|
|
7
7
|
PanelDialog,
|
|
8
8
|
panelDialogFixedOptions,
|
|
9
9
|
prompts,
|
|
10
|
-
StatCell,
|
|
11
|
-
StatGrid,
|
|
12
10
|
TextInput,
|
|
13
11
|
useLocale,
|
|
14
12
|
} from "@k2b/ui";
|
|
15
13
|
import { createContext, createSignal, For, type JSX, Show, useContext } from "solid-js";
|
|
16
14
|
import type { AiTurnBlock } from "../protocol";
|
|
17
15
|
import type { AiMessageFeedback, AiMessageFeedbackReason, AiStoredMessage } from "../types";
|
|
18
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
type AiForkMessageInput,
|
|
18
|
+
type AiRetryMessageInput,
|
|
19
|
+
aiToolIcon,
|
|
20
|
+
describeResponseEnd,
|
|
21
|
+
displayToolName,
|
|
22
|
+
formatWorkedDuration,
|
|
23
|
+
} from "./message-utils";
|
|
19
24
|
import { aiChatMessages } from "./messages";
|
|
20
25
|
|
|
21
26
|
/** The active-turn coordinates an approval/tool action needs to resolve on the server. */
|
|
@@ -41,6 +46,11 @@ export type AiChatActions = {
|
|
|
41
46
|
onForkMessage?: ForkMessageHandler;
|
|
42
47
|
onRetryMessage?: RetryMessageHandler;
|
|
43
48
|
onRetrySteer?: RetrySteerHandler;
|
|
49
|
+
/**
|
|
50
|
+
* Continue after a failed turn. Send `message`, a visible request in the reader's language to pick up where the
|
|
51
|
+
* turn stopped, through the host's normal send path. Without it, a failed turn shows its reason only.
|
|
52
|
+
*/
|
|
53
|
+
onContinueTurn?: (message: string) => void | Promise<void>;
|
|
44
54
|
onMessageFeedback?: (entry: AiStoredMessage, feedback: Omit<AiMessageFeedback, "updatedAt"> | null) => void | Promise<void>;
|
|
45
55
|
/** Open the originating background run for a delivered scheduled result. */
|
|
46
56
|
onOpenScheduledTaskRun?: (taskId: string, occurrenceId: string) => void;
|
|
@@ -72,6 +82,7 @@ const formatDateTime = (value: string, locale: string) =>
|
|
|
72
82
|
new Intl.DateTimeFormat(locale, { dateStyle: "medium", timeStyle: "short" }).format(new Date(value));
|
|
73
83
|
|
|
74
84
|
const assistantResponseInfo = (entries: AiStoredMessage[], locale: string) => {
|
|
85
|
+
const t = aiChatMessages(locale);
|
|
75
86
|
const assistantEntries = entries.filter((entry) => entry.kind === "message" && entry.message.role === "assistant");
|
|
76
87
|
const entry = assistantEntries.findLast((candidate) => candidate.loopAggregate) ?? assistantEntries.at(-1) ?? entries.at(-1);
|
|
77
88
|
if (!entry) return null;
|
|
@@ -83,18 +94,18 @@ const assistantResponseInfo = (entries: AiStoredMessage[], locale: string) => {
|
|
|
83
94
|
const timing = aggregate?.timing;
|
|
84
95
|
|
|
85
96
|
const meta = [
|
|
86
|
-
{ label:
|
|
87
|
-
{ label:
|
|
88
|
-
{ label:
|
|
89
|
-
{ label:
|
|
90
|
-
{ label:
|
|
97
|
+
{ label: t.infoProviderModel, value: entry.providerModel ?? t.infoUnknown },
|
|
98
|
+
{ label: t.infoModelProfile, value: entry.modelProfileId ?? t.infoUnknown },
|
|
99
|
+
{ label: t.infoLoopId, value: entry.loopId ?? t.infoLegacy },
|
|
100
|
+
{ label: t.infoFinished, value: describeResponseEnd(entry, locale) },
|
|
101
|
+
{ label: t.infoCreated, value: formatDateTime(entry.createdAt, locale) },
|
|
91
102
|
];
|
|
92
103
|
|
|
93
104
|
const issues = [
|
|
94
|
-
{ label:
|
|
95
|
-
{ label:
|
|
96
|
-
{ label:
|
|
97
|
-
{ label:
|
|
105
|
+
{ label: t.infoIssueErrors, count: aggregate?.toolErrorCount ?? 0 },
|
|
106
|
+
{ label: t.infoIssueStream, count: aggregate?.toolIssueCount ?? 0 },
|
|
107
|
+
{ label: t.infoIssueMalformed, count: aggregate?.toolMalformedCount ?? 0 },
|
|
108
|
+
{ label: t.infoIssueCancelled, count: aggregate?.toolCancelledCount ?? 0 },
|
|
98
109
|
].filter((issue) => issue.count > 0);
|
|
99
110
|
|
|
100
111
|
const toolCounts = new Map<string, number>();
|
|
@@ -117,6 +128,27 @@ const assistantResponseInfo = (entries: AiStoredMessage[], locale: string) => {
|
|
|
117
128
|
};
|
|
118
129
|
};
|
|
119
130
|
|
|
131
|
+
/** Label and value pairs in one quiet list; a hint explains a value in muted text below it. */
|
|
132
|
+
function InfoRows(props: { rows: readonly { label: string; value: string; hint?: string }[] }) {
|
|
133
|
+
return (
|
|
134
|
+
<dl class="ai-response-info__rows">
|
|
135
|
+
<For each={props.rows}>
|
|
136
|
+
{(row) => (
|
|
137
|
+
<div>
|
|
138
|
+
<dt>
|
|
139
|
+
{row.label}
|
|
140
|
+
<Show when={row.hint}>
|
|
141
|
+
<span>{row.hint}</span>
|
|
142
|
+
</Show>
|
|
143
|
+
</dt>
|
|
144
|
+
<dd title={row.value}>{row.value}</dd>
|
|
145
|
+
</div>
|
|
146
|
+
)}
|
|
147
|
+
</For>
|
|
148
|
+
</dl>
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
120
152
|
const openAssistantResponseInfo = (entries: AiStoredMessage[], locale: string) => {
|
|
121
153
|
const info = assistantResponseInfo(entries, locale);
|
|
122
154
|
if (!info) return;
|
|
@@ -130,85 +162,70 @@ const openAssistantResponseInfo = (entries: AiStoredMessage[], locale: string) =
|
|
|
130
162
|
void dialogCore.open<void>(
|
|
131
163
|
(close) => (
|
|
132
164
|
<PanelDialog>
|
|
133
|
-
<PanelDialog.Header title=
|
|
165
|
+
<PanelDialog.Header title={t.actionMessageInfo} subtitle={t.infoSubtitle} icon="ti ti-info-circle" close={close} />
|
|
134
166
|
<PanelDialog.Body>
|
|
135
|
-
<PanelDialog.Section title=
|
|
136
|
-
<
|
|
137
|
-
<For each={info.meta}>
|
|
138
|
-
{(row) => (
|
|
139
|
-
<>
|
|
140
|
-
<dt class="text-dimmed">{row.label}</dt>
|
|
141
|
-
<dd class="min-w-0 truncate text-right text-primary" title={row.value}>
|
|
142
|
-
{row.value}
|
|
143
|
-
</dd>
|
|
144
|
-
</>
|
|
145
|
-
)}
|
|
146
|
-
</For>
|
|
147
|
-
</dl>
|
|
167
|
+
<PanelDialog.Section title={t.infoDetails} icon="ti ti-list-details">
|
|
168
|
+
<InfoRows rows={info.meta} />
|
|
148
169
|
</PanelDialog.Section>
|
|
149
170
|
|
|
150
171
|
<Show when={info.timing}>
|
|
151
172
|
{(timing) => (
|
|
152
|
-
<
|
|
153
|
-
{/* Worked =
|
|
154
|
-
<
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
+
<PanelDialog.Section title={t.infoTiming} icon="ti ti-clock">
|
|
174
|
+
{/* Worked = writing + tool execution; waiting for approvals and answers is listed separately. */}
|
|
175
|
+
<InfoRows
|
|
176
|
+
rows={[
|
|
177
|
+
{ label: t.infoWorked, value: formatWorkedDuration(timing().totalElapsedMs), hint: t.infoWorkedHint },
|
|
178
|
+
{ label: t.infoGeneration, value: formatWorkedDuration(timing().generationMs) },
|
|
179
|
+
{ label: t.infoToolTime, value: timing().toolExecutionMs > 0 ? formatWorkedDuration(timing().toolExecutionMs) : "–" },
|
|
180
|
+
{
|
|
181
|
+
label: t.infoWaiting,
|
|
182
|
+
value: timing().actionWaitMs > 0 ? formatWorkedDuration(timing().actionWaitMs) : "–",
|
|
183
|
+
hint: t.infoWaitingHint,
|
|
184
|
+
},
|
|
185
|
+
{ label: t.infoWallClock, value: formatWorkedDuration(timing().wallMs) },
|
|
186
|
+
{
|
|
187
|
+
label: t.infoSpeed,
|
|
188
|
+
value:
|
|
189
|
+
timing().outputTokensPerSecond !== undefined
|
|
190
|
+
? t.infoTokensPerSecond({
|
|
191
|
+
value: timing().outputTokensPerSecond!.toLocaleString(locale, { maximumFractionDigits: 1 }),
|
|
192
|
+
})
|
|
193
|
+
: "–",
|
|
194
|
+
},
|
|
195
|
+
]}
|
|
173
196
|
/>
|
|
174
|
-
</
|
|
197
|
+
</PanelDialog.Section>
|
|
175
198
|
)}
|
|
176
199
|
</Show>
|
|
177
200
|
|
|
178
|
-
<
|
|
179
|
-
<
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
value={number(issueTotal)}
|
|
191
|
-
sub={issueSummary}
|
|
192
|
-
accent={{ tone: "red", icon: "ti ti-alert-triangle" }}
|
|
193
|
-
/>
|
|
194
|
-
</Show>
|
|
195
|
-
</StatGrid>
|
|
201
|
+
<PanelDialog.Section title={t.infoUsage} icon="ti ti-chart-bar">
|
|
202
|
+
<InfoRows
|
|
203
|
+
rows={[
|
|
204
|
+
{ label: t.infoInputTokens, value: tokens(info.usage.input) },
|
|
205
|
+
{ label: t.infoOutputTokens, value: tokens(info.usage.output) },
|
|
206
|
+
{ label: t.infoTotalTokens, value: tokens(info.usage.total) },
|
|
207
|
+
{ label: t.assistantTurns, value: number(info.turns) },
|
|
208
|
+
{ label: t.toolCalls, value: number(info.toolCallCount) },
|
|
209
|
+
{ label: t.toolIssues, value: info.issues.length > 0 ? `${number(issueTotal)} · ${issueSummary}` : t.none },
|
|
210
|
+
]}
|
|
211
|
+
/>
|
|
212
|
+
</PanelDialog.Section>
|
|
196
213
|
|
|
197
214
|
<Show when={info.tools.length > 0}>
|
|
198
|
-
<PanelDialog.Section title=
|
|
199
|
-
<
|
|
215
|
+
<PanelDialog.Section title={t.infoTools} icon="ti ti-tool" subtitle={t.infoToolsHint}>
|
|
216
|
+
<ul class="ai-response-info__tools">
|
|
200
217
|
<For each={info.tools}>
|
|
201
218
|
{(tool) => (
|
|
202
|
-
<
|
|
203
|
-
<i class={
|
|
219
|
+
<li>
|
|
220
|
+
<i class={aiToolIcon(tool.name)} aria-hidden="true" />
|
|
204
221
|
{displayToolName(tool.name)}
|
|
205
222
|
<Show when={tool.count > 1}>
|
|
206
|
-
<span class="
|
|
223
|
+
<span class="text-dimmed tabular-nums">×{tool.count}</span>
|
|
207
224
|
</Show>
|
|
208
|
-
</
|
|
225
|
+
</li>
|
|
209
226
|
)}
|
|
210
227
|
</For>
|
|
211
|
-
</
|
|
228
|
+
</ul>
|
|
212
229
|
</PanelDialog.Section>
|
|
213
230
|
</Show>
|
|
214
231
|
</PanelDialog.Body>
|
|
@@ -218,12 +235,12 @@ const openAssistantResponseInfo = (entries: AiStoredMessage[], locale: string) =
|
|
|
218
235
|
);
|
|
219
236
|
};
|
|
220
237
|
|
|
221
|
-
const forkTitleFromText = (text: string): string => {
|
|
238
|
+
const forkTitleFromText = (text: string, fallback: string): string => {
|
|
222
239
|
const firstLine = text
|
|
223
240
|
.split("\n")
|
|
224
241
|
.map((line) => line.trim())
|
|
225
242
|
.find(Boolean);
|
|
226
|
-
if (!firstLine) return
|
|
243
|
+
if (!firstLine) return fallback;
|
|
227
244
|
return firstLine.length > 80 ? `${firstLine.slice(0, 77).trim()}...` : firstLine;
|
|
228
245
|
};
|
|
229
246
|
|
|
@@ -231,17 +248,19 @@ const openForkMessageDialog = async (
|
|
|
231
248
|
entry: AiStoredMessage,
|
|
232
249
|
copyText: string,
|
|
233
250
|
onForkMessage: (entry: AiStoredMessage, input?: AiForkMessageInput) => void | Promise<void>,
|
|
251
|
+
locale: string,
|
|
234
252
|
) => {
|
|
253
|
+
const t = aiChatMessages(locale);
|
|
235
254
|
const result = await prompts.form({
|
|
236
|
-
title:
|
|
255
|
+
title: t.forkTitle,
|
|
237
256
|
icon: "ti ti-git-fork",
|
|
238
|
-
confirmText:
|
|
257
|
+
confirmText: t.forkConfirm,
|
|
239
258
|
size: "medium",
|
|
240
259
|
fields: {
|
|
241
260
|
title: {
|
|
242
261
|
type: "text",
|
|
243
|
-
label:
|
|
244
|
-
default: forkTitleFromText(copyText),
|
|
262
|
+
label: t.forkName,
|
|
263
|
+
default: forkTitleFromText(copyText, t.forkDefaultName),
|
|
245
264
|
required: true,
|
|
246
265
|
maxLength: 120,
|
|
247
266
|
},
|
|
@@ -251,16 +270,18 @@ const openForkMessageDialog = async (
|
|
|
251
270
|
if (title) await onForkMessage(entry, { title });
|
|
252
271
|
};
|
|
253
272
|
|
|
254
|
-
const feedbackReasons
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
{ id: "
|
|
258
|
-
{ id: "
|
|
259
|
-
{ id: "
|
|
260
|
-
{ id: "
|
|
273
|
+
const feedbackReasons = (
|
|
274
|
+
t: ReturnType<typeof aiChatMessages>,
|
|
275
|
+
): readonly { id: AiMessageFeedbackReason; label: string; description: string }[] => [
|
|
276
|
+
{ id: "incorrect", label: t.feedbackIncorrect, description: t.feedbackIncorrectHint },
|
|
277
|
+
{ id: "did_not_follow_request", label: t.feedbackIgnoredRequest, description: t.feedbackIgnoredRequestHint },
|
|
278
|
+
{ id: "incomplete", label: t.feedbackIncomplete, description: t.feedbackIncompleteHint },
|
|
279
|
+
{ id: "poor_tool_choice", label: t.feedbackWrongApp, description: t.feedbackWrongAppHint },
|
|
280
|
+
{ id: "too_slow", label: t.feedbackSlow, description: t.feedbackSlowHint },
|
|
281
|
+
{ id: "other", label: t.feedbackOther, description: t.feedbackOtherHint },
|
|
261
282
|
];
|
|
262
283
|
|
|
263
|
-
const openNegativeFeedbackDialog = () =>
|
|
284
|
+
const openNegativeFeedbackDialog = (t: ReturnType<typeof aiChatMessages>) =>
|
|
264
285
|
prompts.dialog<{ reasons: AiMessageFeedbackReason[]; comment: string | null } | undefined>(
|
|
265
286
|
(close) => {
|
|
266
287
|
const [selected, setSelected] = createSignal<AiMessageFeedbackReason[]>([]);
|
|
@@ -270,9 +291,9 @@ const openNegativeFeedbackDialog = () =>
|
|
|
270
291
|
const valid = () => selected().length > 0 || comment().trim().length > 0;
|
|
271
292
|
return (
|
|
272
293
|
<div class="flex min-h-0 flex-col gap-4">
|
|
273
|
-
<p class="text-sm text-secondary">
|
|
294
|
+
<p class="text-sm text-secondary">{t.feedbackIntro}</p>
|
|
274
295
|
<div class="grid grid-cols-1 gap-2 sm:grid-cols-2">
|
|
275
|
-
<For each={feedbackReasons}>
|
|
296
|
+
<For each={feedbackReasons(t)}>
|
|
276
297
|
{(reason) => (
|
|
277
298
|
<CheckboxCard
|
|
278
299
|
label={reason.label}
|
|
@@ -284,8 +305,8 @@ const openNegativeFeedbackDialog = () =>
|
|
|
284
305
|
</For>
|
|
285
306
|
</div>
|
|
286
307
|
<TextInput
|
|
287
|
-
label=
|
|
288
|
-
description=
|
|
308
|
+
label={t.feedbackDetails}
|
|
309
|
+
description={t.feedbackDetailsHint}
|
|
289
310
|
multiline
|
|
290
311
|
lines={3}
|
|
291
312
|
maxLength={1_000}
|
|
@@ -294,7 +315,7 @@ const openNegativeFeedbackDialog = () =>
|
|
|
294
315
|
/>
|
|
295
316
|
<div class="flex justify-end gap-2">
|
|
296
317
|
<Button variant="secondary" size="sm" type="button" onClick={() => close(undefined)}>
|
|
297
|
-
|
|
318
|
+
{t.cancel}
|
|
298
319
|
</Button>
|
|
299
320
|
<Button
|
|
300
321
|
size="sm"
|
|
@@ -302,13 +323,13 @@ const openNegativeFeedbackDialog = () =>
|
|
|
302
323
|
disabled={!valid()}
|
|
303
324
|
onClick={() => close({ reasons: selected(), comment: comment().trim() || null })}
|
|
304
325
|
>
|
|
305
|
-
|
|
326
|
+
{t.feedbackSend}
|
|
306
327
|
</Button>
|
|
307
328
|
</div>
|
|
308
329
|
</div>
|
|
309
330
|
);
|
|
310
331
|
},
|
|
311
|
-
{ title:
|
|
332
|
+
{ title: t.feedbackTitle, icon: "ti ti-message-report", size: "large" },
|
|
312
333
|
);
|
|
313
334
|
|
|
314
335
|
export function createAssistantMessageActions(props: {
|
|
@@ -318,13 +339,14 @@ export function createAssistantMessageActions(props: {
|
|
|
318
339
|
actions: AiChatActions;
|
|
319
340
|
}): ChatAction[] {
|
|
320
341
|
const locale = useLocale();
|
|
342
|
+
const t = aiChatMessages(locale());
|
|
321
343
|
const actions = props.actions;
|
|
322
344
|
const result: ChatAction[] = [
|
|
323
345
|
...(actions.onMessageFeedback
|
|
324
346
|
? [
|
|
325
347
|
{
|
|
326
348
|
id: "feedback-up",
|
|
327
|
-
label: props.entry.feedback?.rating === "up" ?
|
|
349
|
+
label: props.entry.feedback?.rating === "up" ? t.actionHelpfulRemove : t.actionHelpful,
|
|
328
350
|
icon: "ti ti-thumb-up",
|
|
329
351
|
pressed: props.entry.feedback?.rating === "up",
|
|
330
352
|
pressedTone: "success" as const,
|
|
@@ -336,13 +358,13 @@ export function createAssistantMessageActions(props: {
|
|
|
336
358
|
},
|
|
337
359
|
{
|
|
338
360
|
id: "feedback-down",
|
|
339
|
-
label: props.entry.feedback?.rating === "down" ?
|
|
361
|
+
label: props.entry.feedback?.rating === "down" ? t.actionImproveRemove : t.actionImprove,
|
|
340
362
|
icon: "ti ti-thumb-down",
|
|
341
363
|
pressed: props.entry.feedback?.rating === "down",
|
|
342
364
|
pressedTone: "danger" as const,
|
|
343
365
|
onSelect: async () => {
|
|
344
366
|
if (props.entry.feedback?.rating === "down") return actions.onMessageFeedback!(props.entry, null);
|
|
345
|
-
const feedback = await openNegativeFeedbackDialog();
|
|
367
|
+
const feedback = await openNegativeFeedbackDialog(t);
|
|
346
368
|
if (feedback) await actions.onMessageFeedback!(props.entry, { rating: "down", ...feedback });
|
|
347
369
|
},
|
|
348
370
|
},
|
|
@@ -350,7 +372,7 @@ export function createAssistantMessageActions(props: {
|
|
|
350
372
|
: []),
|
|
351
373
|
{
|
|
352
374
|
id: "info",
|
|
353
|
-
label:
|
|
375
|
+
label: t.actionMessageInfo,
|
|
354
376
|
icon: "ti ti-info-circle",
|
|
355
377
|
onSelect: () => openAssistantResponseInfo(props.entries, locale()),
|
|
356
378
|
},
|
|
@@ -358,7 +380,7 @@ export function createAssistantMessageActions(props: {
|
|
|
358
380
|
if (props.copyText) {
|
|
359
381
|
result.push({
|
|
360
382
|
id: "copy",
|
|
361
|
-
label:
|
|
383
|
+
label: t.actionCopy,
|
|
362
384
|
icon: "ti ti-copy",
|
|
363
385
|
copyText: props.copyText,
|
|
364
386
|
});
|
|
@@ -366,9 +388,9 @@ export function createAssistantMessageActions(props: {
|
|
|
366
388
|
if (actions.onForkMessage) {
|
|
367
389
|
result.push({
|
|
368
390
|
id: "fork",
|
|
369
|
-
label:
|
|
391
|
+
label: t.actionFork,
|
|
370
392
|
icon: "ti ti-git-fork",
|
|
371
|
-
onSelect: () => openForkMessageDialog(props.entry, props.copyText, actions.onForkMessage
|
|
393
|
+
onSelect: () => openForkMessageDialog(props.entry, props.copyText, actions.onForkMessage!, locale()),
|
|
372
394
|
});
|
|
373
395
|
}
|
|
374
396
|
return result;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Message, Usage } from "@k2b/nessi";
|
|
1
|
+
import type { DoneReason, Message, Usage } from "@k2b/nessi";
|
|
2
2
|
import { fileIcons } from "@k2b/stdlib";
|
|
3
3
|
import { formatBytes as sharedFormatBytes } from "../../shared/format";
|
|
4
4
|
import { type AiAttachmentRef, parseAiAttachmentMarkers } from "../attachments";
|
|
@@ -122,6 +122,35 @@ export const FILE_INPUT_ACCEPT = [
|
|
|
122
122
|
].join(",");
|
|
123
123
|
|
|
124
124
|
/** Seconds-granular work duration ("8s", "2m 14s") — stdlib's dates.formatDuration is deliberately minute-granular. */
|
|
125
|
+
/**
|
|
126
|
+
* How a response ended, in words: the run's end, plus the last message's own stop when it adds something, such as a
|
|
127
|
+
* cut-off answer. A stop code the list does not know stays as the provider sent it.
|
|
128
|
+
*/
|
|
129
|
+
export const describeResponseEnd = (entry: Pick<AiStoredMessage, "loopDoneReason" | "stopReason">, locale: string): string => {
|
|
130
|
+
const t = aiChatMessages(locale);
|
|
131
|
+
const run: Record<DoneReason, string> = {
|
|
132
|
+
stop: t.infoEndCompleted,
|
|
133
|
+
aborted: t.infoEndStopped,
|
|
134
|
+
error: t.infoEndFailed,
|
|
135
|
+
max_turns: t.infoEndStepLimit,
|
|
136
|
+
no_credits: t.infoEndUsageLimit,
|
|
137
|
+
context_overflow: t.infoEndContextFull,
|
|
138
|
+
};
|
|
139
|
+
const message: Record<string, string | null> = {
|
|
140
|
+
stop: entry.loopDoneReason ? null : t.infoEndCompleted,
|
|
141
|
+
tool_use: null,
|
|
142
|
+
max_tokens: t.infoEndLengthLimit,
|
|
143
|
+
aborted: t.infoEndStopped,
|
|
144
|
+
interrupted: t.infoEndInterrupted,
|
|
145
|
+
error: t.infoEndFailed,
|
|
146
|
+
};
|
|
147
|
+
const parts = [
|
|
148
|
+
entry.loopDoneReason ? run[entry.loopDoneReason] : null,
|
|
149
|
+
entry.stopReason ? (Object.hasOwn(message, entry.stopReason) ? message[entry.stopReason] : entry.stopReason) : null,
|
|
150
|
+
].filter((part): part is string => Boolean(part));
|
|
151
|
+
return [...new Set(parts)].join(" · ") || t.infoUnknown;
|
|
152
|
+
};
|
|
153
|
+
|
|
125
154
|
export const formatWorkedDuration = (ms: number): string => {
|
|
126
155
|
const totalSeconds = Math.max(1, Math.round(ms / 1000));
|
|
127
156
|
const hours = Math.floor(totalSeconds / 3600);
|