@k2b/cloud 0.29.0 → 0.29.1
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 +4 -3
- package/src/ai/chat/blocks.tsx +162 -180
- package/src/ai/chat/live-turn.browser-harness.tsx +5 -1
- package/src/ai/chat/messages.ts +4 -24
- package/src/ai/chat/tool-disclosure.tsx +21 -14
- package/src/ai/chat/tool-groups.ts +64 -37
- package/src/ai/chat/turn-layout.ts +2 -2
- package/src/ai/chat/turn-view.tsx +119 -65
- package/src/ai/protocol.ts +9 -4
- package/src/ai/store.ts +20 -13
- package/src/ai/types.ts +2 -1
- package/src/api/admin-outgoing-mail.ts +18 -0
- package/src/cli/admin/outgoing-mail.ts +30 -2
- package/src/contracts/outgoing-mail.ts +56 -11
- package/src/services/outbox.ts +24 -9
- package/src/services/outgoing-mail/index.ts +33 -5
- package/src/services/outgoing-mail/messages.ts +60 -19
- package/src/services/outgoing-mail/store.ts +63 -4
- package/src/shared/network-address.ts +1 -0
- package/src/styles/effects.css +142 -0
- package/src/styles/utilities-markdown-table.css +5 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@k2b/cloud",
|
|
3
|
-
"version": "0.29.
|
|
3
|
+
"version": "0.29.1",
|
|
4
4
|
"description": "Application platform library for independently deployed Hono and SolidJS services behind a dynamic gateway.",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"repository": {
|
|
@@ -55,6 +55,7 @@
|
|
|
55
55
|
"./services": "./src/services/index.ts",
|
|
56
56
|
"./services/image-metadata": "./src/services/image-metadata.ts",
|
|
57
57
|
"./services/document-extraction": "./src/services/document-extraction.ts",
|
|
58
|
+
"./services/outbox": "./src/services/outbox.ts",
|
|
58
59
|
"./services/ipa/service-account": null,
|
|
59
60
|
"./services/*": "./src/services/*",
|
|
60
61
|
"./ai": "./src/ai/index.ts",
|
|
@@ -91,7 +92,7 @@
|
|
|
91
92
|
},
|
|
92
93
|
"scripts": {
|
|
93
94
|
"test": "bun test --isolate --path-ignore-patterns '**/*.behavior.test.*'",
|
|
94
|
-
"typecheck": "tsc -p tsconfig.json --noEmit --pretty false"
|
|
95
|
+
"typecheck": "tsc --checkers 2 -p tsconfig.json --noEmit --pretty false"
|
|
95
96
|
},
|
|
96
97
|
"dependencies": {
|
|
97
98
|
"@firecrawl/anydoc": "0.2.4",
|
|
@@ -102,7 +103,7 @@
|
|
|
102
103
|
"@tailwindcss/typography": "0.5.20",
|
|
103
104
|
"@k2b/nessi": "0.17.0",
|
|
104
105
|
"@k2b/ssr": "0.15.0",
|
|
105
|
-
"@k2b/ui": "0.
|
|
106
|
+
"@k2b/ui": "0.20.0",
|
|
106
107
|
"@k2b/stdlib": "0.28.0",
|
|
107
108
|
"@k2b/sync": "7.0.0",
|
|
108
109
|
"@nats-io/jetstream": "3.4.0",
|
package/src/ai/chat/blocks.tsx
CHANGED
|
@@ -183,7 +183,14 @@ function ToolResultDisclosure(props: {
|
|
|
183
183
|
);
|
|
184
184
|
}
|
|
185
185
|
|
|
186
|
-
|
|
186
|
+
/** A decision the server accepted, for the call it answered. */
|
|
187
|
+
export type AiApprovalDecision = { callId: string; approved: boolean };
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* The pending approval: a calm tinted card that says what will happen, with its fields and the decision. `onDecided`
|
|
191
|
+
* hears an accepted decision with the call it answered, so the turn can show its receipt in the card's place right away.
|
|
192
|
+
*/
|
|
193
|
+
export function ApprovalBlockView(props: { turnId: string; block: ToolBlock; onDecided?: (decision: AiApprovalDecision) => void }) {
|
|
187
194
|
const actions = useAiChatActions();
|
|
188
195
|
const locale = useLocale();
|
|
189
196
|
const t = () => aiChatMessages(locale());
|
|
@@ -192,15 +199,22 @@ export function ApprovalBlockView(props: { turnId: string; block: ToolBlock }) {
|
|
|
192
199
|
const actionDisabled = () => actions.actionDisabled?.() ?? false;
|
|
193
200
|
const [submitted, setSubmitted] = createSignal(false);
|
|
194
201
|
const [detailsOpen, setDetailsOpen] = createSignal(false);
|
|
202
|
+
// The call this card answered: a later approval for the same block may already have arrived when the server accepts.
|
|
203
|
+
let decision: AiApprovalDecision | null = null;
|
|
195
204
|
const approval = mutation.create<void, { approved: boolean; remember?: "always" }>({
|
|
196
205
|
mutation: async (input) => {
|
|
197
206
|
if (!actions.onApproval) throw new Error("Approval is unavailable.");
|
|
198
207
|
await actions.onApproval(request(), input);
|
|
199
208
|
},
|
|
200
|
-
onSuccess: () =>
|
|
209
|
+
onSuccess: () => {
|
|
210
|
+
setSubmitted(true);
|
|
211
|
+
if (decision) props.onDecided?.(decision);
|
|
212
|
+
},
|
|
201
213
|
});
|
|
202
214
|
const submit = (input: { approved: boolean; remember?: "always" }) => {
|
|
203
|
-
if (
|
|
215
|
+
if (actionDisabled() || approval.loading()) return;
|
|
216
|
+
decision = { callId: props.block.callId, approved: input.approved };
|
|
217
|
+
void approval.mutate(input);
|
|
204
218
|
};
|
|
205
219
|
const title = () => props.block.presentation?.title ?? displayToolName(props.block.name, locale());
|
|
206
220
|
const ownerName = () => props.block.presentation?.appName ?? t().assistant;
|
|
@@ -239,199 +253,166 @@ export function ApprovalBlockView(props: { turnId: string; block: ToolBlock }) {
|
|
|
239
253
|
download: t().download,
|
|
240
254
|
})[rel];
|
|
241
255
|
const detailData = () => (isStructuredDataValue(props.block.args) ? props.block.args : undefined);
|
|
242
|
-
const appAccent = () => props.block.presentation?.appAccent;
|
|
243
256
|
const detailsId = `approval-details-${props.block.callId}`;
|
|
244
257
|
return (
|
|
245
|
-
<
|
|
246
|
-
<
|
|
247
|
-
class={`
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
<
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
<
|
|
286
|
-
<
|
|
287
|
-
<Show
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
258
|
+
<section class="ai-approval" aria-label={t().approvalRequired({ title: title() })}>
|
|
259
|
+
<div class="ai-approval__head">
|
|
260
|
+
<i class={`ai-approval__icon ${aiToolIcon(props.block.name, props.block.presentation?.appIcon)}`} aria-hidden="true" />
|
|
261
|
+
<div class="min-w-0">
|
|
262
|
+
<h3 class="ai-approval__title">
|
|
263
|
+
{ownerName()} · {title()}
|
|
264
|
+
</h3>
|
|
265
|
+
<p class="ai-approval__sub">{t().approvalRunsAfter}</p>
|
|
266
|
+
</div>
|
|
267
|
+
</div>
|
|
268
|
+
<Show when={reviewLines().length > 0}>
|
|
269
|
+
<div class="ai-approval__text">
|
|
270
|
+
<For each={reviewLines()}>
|
|
271
|
+
{(line) => (
|
|
272
|
+
<p class="whitespace-pre-wrap">
|
|
273
|
+
<Show when={line.label}>{(label) => <strong class="ai-approval__label">{label()}: </strong>}</Show>
|
|
274
|
+
{line.value}
|
|
275
|
+
</p>
|
|
276
|
+
)}
|
|
277
|
+
</For>
|
|
278
|
+
</div>
|
|
279
|
+
</Show>
|
|
280
|
+
<Show when={props.block.approval?.review}>
|
|
281
|
+
<p class="ai-approval__text whitespace-pre-wrap">{description()}</p>
|
|
282
|
+
<Show when={inlineReviewDetails().length > 0}>
|
|
283
|
+
<dl class="ai-approval__fields">
|
|
284
|
+
<For each={inlineReviewDetails()}>
|
|
285
|
+
{(detail) => (
|
|
286
|
+
<>
|
|
287
|
+
<dt>{detail.label}</dt>
|
|
288
|
+
<dd class="whitespace-pre-wrap">
|
|
289
|
+
<ReviewDetailValue detail={detail} />
|
|
290
|
+
</dd>
|
|
291
|
+
</>
|
|
292
|
+
)}
|
|
293
|
+
</For>
|
|
294
|
+
</dl>
|
|
295
|
+
</Show>
|
|
296
|
+
<For each={blockReviewDetails()}>
|
|
297
|
+
{(detail) => (
|
|
298
|
+
<section class="ai-approval__text min-w-0" aria-label={detail.label}>
|
|
299
|
+
<h4 class="ai-approval__label mb-1">{detail.label}</h4>
|
|
300
|
+
<Show
|
|
301
|
+
when={detail.format === "markdown"}
|
|
302
|
+
fallback={
|
|
303
|
+
<pre
|
|
304
|
+
class="ai-approval__preview whitespace-pre-wrap break-words font-sans"
|
|
305
|
+
role="region"
|
|
306
|
+
tabIndex={0}
|
|
307
|
+
aria-label={t().contentOf({ label: detail.label })}
|
|
308
|
+
>
|
|
309
|
+
<ReviewDetailValue detail={detail} />
|
|
310
|
+
</pre>
|
|
311
|
+
}
|
|
312
|
+
>
|
|
313
|
+
<div class="ai-approval__preview" role="region" tabIndex={0} aria-label={detail.label}>
|
|
314
|
+
<MarkdownView class="cloud-ai-approval-detail" markdown={detail.value} headingScale="compact" allowImages={false} />
|
|
315
|
+
</div>
|
|
300
316
|
</Show>
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
class="max-h-72 overflow-auto whitespace-pre-wrap break-words pr-2 font-sans text-xs leading-5 text-secondary"
|
|
310
|
-
role="region"
|
|
311
|
-
tabIndex={0}
|
|
312
|
-
aria-label={t().contentOf({ label: detail.label })}
|
|
313
|
-
>
|
|
314
|
-
<ReviewDetailValue detail={detail} />
|
|
315
|
-
</pre>
|
|
316
|
-
}
|
|
317
|
-
>
|
|
318
|
-
<div
|
|
319
|
-
class="-mx-2 max-h-72 overflow-auto px-2 text-xs text-secondary"
|
|
320
|
-
role="region"
|
|
321
|
-
tabIndex={0}
|
|
322
|
-
aria-label={detail.label}
|
|
323
|
-
>
|
|
324
|
-
<MarkdownView class="cloud-ai-approval-detail" markdown={detail.value} headingScale="compact" allowImages={false} />
|
|
325
|
-
</div>
|
|
326
|
-
</Show>
|
|
327
|
-
</section>
|
|
328
|
-
)}
|
|
329
|
-
</For>
|
|
330
|
-
</div>
|
|
317
|
+
</section>
|
|
318
|
+
)}
|
|
319
|
+
</For>
|
|
320
|
+
</Show>
|
|
321
|
+
<Show when={detailsOpen()}>
|
|
322
|
+
<div id={detailsId} class="w-full min-w-0" role="region" aria-label={t().detailsFor({ title: title() })}>
|
|
323
|
+
<Show when={detailData()} fallback={<pre class="ai-approval__preview text-[11px]">{jsonPreview(props.block.args)}</pre>}>
|
|
324
|
+
{(data) => <StructuredDataPreview data={data()} class="w-full" />}
|
|
331
325
|
</Show>
|
|
332
326
|
</div>
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
</
|
|
349
|
-
</
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
</
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
327
|
+
</Show>
|
|
328
|
+
{/* The decision continues the card below its content: spacing, not a line or a band, sets it apart. */}
|
|
329
|
+
<footer class="ai-approval__foot" data-ai-approval-footer>
|
|
330
|
+
{/* Present from the start, so screen readers announce progress after the decision buttons leave. */}
|
|
331
|
+
<span class="k2b-sr-only" role="status" aria-live="polite">
|
|
332
|
+
{approval.loading() ? t().submitting : submitted() ? t().submitted : ""}
|
|
333
|
+
</span>
|
|
334
|
+
<Show when={reviewLinks().length > 0}>
|
|
335
|
+
<nav class="flex flex-wrap gap-1" aria-label={t().linksFor({ title: title() })}>
|
|
336
|
+
<For each={reviewLinks()}>
|
|
337
|
+
{(link) => (
|
|
338
|
+
<ButtonLink href={link.href} target="_blank" rel="noopener noreferrer" size="xs" variant="ghost">
|
|
339
|
+
{link.title ?? reviewLinkTitle(link.rel)}
|
|
340
|
+
</ButtonLink>
|
|
341
|
+
)}
|
|
342
|
+
</For>
|
|
343
|
+
</nav>
|
|
344
|
+
</Show>
|
|
345
|
+
<Show when={approval.error()}>
|
|
346
|
+
<p class="text-xs text-red-700 dark:text-red-300" role="alert">
|
|
347
|
+
{t().approvalFailed}
|
|
348
|
+
</p>
|
|
349
|
+
</Show>
|
|
350
|
+
<div class="ai-approval__actions">
|
|
351
|
+
<Show
|
|
352
|
+
when={pending()}
|
|
353
|
+
fallback={
|
|
354
|
+
<span class="ai-approval__state">
|
|
355
|
+
{title()} · {props.block.status === "rejected" ? t().rejected : t().approved}
|
|
356
|
+
</span>
|
|
357
|
+
}
|
|
358
|
+
>
|
|
359
|
+
<Show when={actions.onApproval} fallback={<span class="ai-approval__state">{t().approvalUnavailable}</span>}>
|
|
360
|
+
<Show
|
|
361
|
+
when={!actionDisabled()}
|
|
362
|
+
fallback={
|
|
363
|
+
<span class="ai-approval__state">
|
|
364
|
+
<i class="ti ti-player-stop" aria-hidden="true" />
|
|
365
|
+
{t().stoppingResponse}
|
|
366
|
+
</span>
|
|
367
|
+
}
|
|
368
|
+
>
|
|
365
369
|
<Show
|
|
366
|
-
when={!
|
|
370
|
+
when={!submitted()}
|
|
367
371
|
fallback={
|
|
368
|
-
<span class="
|
|
369
|
-
<i class="ti ti-
|
|
370
|
-
{t().
|
|
372
|
+
<span class="ai-approval__state">
|
|
373
|
+
<i class="ti ti-check" aria-hidden="true" />
|
|
374
|
+
{t().submitted}
|
|
371
375
|
</span>
|
|
372
376
|
}
|
|
373
377
|
>
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
378
|
+
{/* The buttons stay while the decision is sent, so the card does not change its height before it
|
|
379
|
+
becomes its receipt. */}
|
|
380
|
+
<Button size="sm" variant="ghost" disabled={approval.loading()} onClick={() => submit({ approved: false })}>
|
|
381
|
+
{t().reject}
|
|
382
|
+
</Button>
|
|
383
|
+
<SplitButton
|
|
384
|
+
size="sm"
|
|
385
|
+
variant="ai"
|
|
386
|
+
loading={approval.loading()}
|
|
387
|
+
onClick={() => submit({ approved: true })}
|
|
388
|
+
menuLabel={t().moreOptions({ title: title() })}
|
|
389
|
+
menuPosition="bottom-right"
|
|
390
|
+
items={[
|
|
391
|
+
{
|
|
392
|
+
label: detailsOpen() ? t().hideDetails : t().details,
|
|
393
|
+
icon: detailsOpen() ? "ti ti-eye-off" : "ti ti-eye",
|
|
394
|
+
action: () => setDetailsOpen((open) => !open),
|
|
395
|
+
},
|
|
396
|
+
...(props.block.approval?.allowAlways
|
|
397
|
+
? [
|
|
398
|
+
{
|
|
399
|
+
label: t().alwaysApprove,
|
|
400
|
+
icon: "ti ti-shield-check",
|
|
401
|
+
action: () => submit({ approved: true, remember: "always" }),
|
|
402
|
+
},
|
|
403
|
+
]
|
|
404
|
+
: []),
|
|
405
|
+
]}
|
|
382
406
|
>
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
{t().reject}
|
|
386
|
-
</Button>
|
|
387
|
-
<SplitButton
|
|
388
|
-
size="xs"
|
|
389
|
-
variant="ai"
|
|
390
|
-
onClick={() => submit({ approved: true })}
|
|
391
|
-
menuLabel={t().moreOptions({ title: title() })}
|
|
392
|
-
menuPosition="bottom-right"
|
|
393
|
-
items={[
|
|
394
|
-
{
|
|
395
|
-
label: detailsOpen() ? t().hideDetails : t().details,
|
|
396
|
-
icon: detailsOpen() ? "ti ti-eye-off" : "ti ti-eye",
|
|
397
|
-
action: () => setDetailsOpen((open) => !open),
|
|
398
|
-
},
|
|
399
|
-
...(props.block.approval?.allowAlways
|
|
400
|
-
? [
|
|
401
|
-
{
|
|
402
|
-
label: t().alwaysApprove,
|
|
403
|
-
icon: "ti ti-shield-check",
|
|
404
|
-
action: () => submit({ approved: true, remember: "always" }),
|
|
405
|
-
},
|
|
406
|
-
]
|
|
407
|
-
: []),
|
|
408
|
-
]}
|
|
409
|
-
>
|
|
410
|
-
{title()}
|
|
411
|
-
</SplitButton>
|
|
412
|
-
</div>
|
|
413
|
-
</Show>
|
|
407
|
+
{title()}
|
|
408
|
+
</SplitButton>
|
|
414
409
|
</Show>
|
|
415
410
|
</Show>
|
|
416
411
|
</Show>
|
|
417
|
-
</div>
|
|
418
|
-
</footer>
|
|
419
|
-
</section>
|
|
420
|
-
<Show when={detailsOpen()}>
|
|
421
|
-
<div id={detailsId} class="mt-2 w-full" role="region" aria-label={t().detailsFor({ title: title() })}>
|
|
422
|
-
<Show
|
|
423
|
-
when={detailData()}
|
|
424
|
-
fallback={
|
|
425
|
-
<pre class="max-h-40 overflow-auto rounded-md bg-white/55 p-2 text-[11px] text-primary dark:bg-black/20">
|
|
426
|
-
{jsonPreview(props.block.args)}
|
|
427
|
-
</pre>
|
|
428
|
-
}
|
|
429
|
-
>
|
|
430
|
-
{(data) => <StructuredDataPreview data={data()} class="w-full" />}
|
|
431
412
|
</Show>
|
|
432
413
|
</div>
|
|
433
|
-
</
|
|
434
|
-
</
|
|
414
|
+
</footer>
|
|
415
|
+
</section>
|
|
435
416
|
);
|
|
436
417
|
}
|
|
437
418
|
|
|
@@ -849,6 +830,7 @@ export function CompactToolRow(props: { block: ToolBlock; busy?: boolean }) {
|
|
|
849
830
|
.join(" · ")}
|
|
850
831
|
icon={icon()}
|
|
851
832
|
busy={props.busy && props.block.status === "running"}
|
|
833
|
+
bodyInset={false}
|
|
852
834
|
renderBody={() => (
|
|
853
835
|
<div class="flex min-w-0 flex-col gap-2">
|
|
854
836
|
<div
|
|
@@ -11,15 +11,18 @@ import { AiChatActionsProvider, createAiChatTimeline } from "./presentation";
|
|
|
11
11
|
* controller does. `window.emit(event)` applies one event,
|
|
12
12
|
* `window.steer(text)` appends a pending steer as the controller does,
|
|
13
13
|
* `window.reconnect(on)` sets whether the stream reconnects,
|
|
14
|
-
* `window.continued` lists the messages that **Continue** sent,
|
|
14
|
+
* `window.continued` lists the messages that **Continue** sent,
|
|
15
|
+
* `window.approvals` lists the approval decisions, and
|
|
15
16
|
* `window.opened` lists the chat files the timeline opened. Chat files load from `/files?path=`.
|
|
16
17
|
*/
|
|
17
18
|
const [state, setState] = createStore(emptyProjection());
|
|
18
19
|
const [reconnecting, setReconnecting] = createSignal(false);
|
|
19
20
|
const continued: string[] = [];
|
|
20
21
|
const opened: string[] = [];
|
|
22
|
+
const approvals: unknown[] = [];
|
|
21
23
|
Object.assign(window, {
|
|
22
24
|
continued,
|
|
25
|
+
approvals,
|
|
23
26
|
opened,
|
|
24
27
|
reconnect: (on: boolean) => setReconnecting(on),
|
|
25
28
|
emit: (event: AiWireEvent) => setState(reconcile(reduceProjection(state, event), { key: "id", merge: true })),
|
|
@@ -38,6 +41,7 @@ render(
|
|
|
38
41
|
<AiChatActionsProvider
|
|
39
42
|
actions={{
|
|
40
43
|
onContinueTurn: (message) => void continued.push(message),
|
|
44
|
+
onApproval: async (request, input) => void approvals.push({ callId: request.callId, ...input }),
|
|
41
45
|
fileUrl: (path) => `/files?path=${encodeURIComponent(path)}`,
|
|
42
46
|
onOpenFile: (path) => void opened.push(path),
|
|
43
47
|
}}
|
package/src/ai/chat/messages.ts
CHANGED
|
@@ -95,22 +95,12 @@ const messages = i18n.define({
|
|
|
95
95
|
checkWarnings: ({ count }: { count: number }) => (count === 1 ? "1 warning" : `${count} warnings`),
|
|
96
96
|
openImage: ({ name }: { name: string }) => `Open image ${name}`,
|
|
97
97
|
steered: "Conversation steered",
|
|
98
|
-
reasoning: "Reasoning",
|
|
99
|
-
groupPlan: "Updated working plan",
|
|
100
|
-
groupCode: "Worked with code",
|
|
101
|
-
groupCloud: "Used Cloud integration",
|
|
102
|
-
groupReadFiles: "Read files",
|
|
103
|
-
groupWriteFiles: "Wrote files",
|
|
104
|
-
groupPdf: "Created PDFs",
|
|
105
98
|
groupLoad: "Loaded tools and guidance",
|
|
106
|
-
groupWeb: "Searched the web",
|
|
107
|
-
groupMemory: "Used memory",
|
|
108
|
-
groupDelivered: "Delivered results",
|
|
109
|
-
groupTools: "Used tools",
|
|
110
99
|
groupFailed: ({ count }: { count: number }) => `${count} failed`,
|
|
111
100
|
groupRejected: ({ count }: { count: number }) => `${count} rejected`,
|
|
112
101
|
receiptDone: ({ title }: { title: string }) => `${title} done`,
|
|
113
102
|
receiptApproved: ({ title }: { title: string }) => `Approved: ${title}`,
|
|
103
|
+
receiptRunning: ({ title }: { title: string }) => `Running: ${title}`,
|
|
114
104
|
approvalNeeded: ({ title }: { title: string }) => `Approval needed: ${title}`,
|
|
115
105
|
answerReady: "Answer ready",
|
|
116
106
|
receiptRejected: ({ title }: { title: string }) => `Rejected: ${title}`,
|
|
@@ -121,7 +111,7 @@ const messages = i18n.define({
|
|
|
121
111
|
streamAccessDenied: "You no longer have access to this chat.",
|
|
122
112
|
streamNotFound: "This chat is no longer available.",
|
|
123
113
|
assistant: "Assistant",
|
|
124
|
-
|
|
114
|
+
approvalRunsAfter: "Runs only after you approve it",
|
|
125
115
|
approvalRequired: ({ title }: { title: string }) => `Approval required: ${title}`,
|
|
126
116
|
reject: "Reject",
|
|
127
117
|
approved: "Approved",
|
|
@@ -393,22 +383,12 @@ const messages = i18n.define({
|
|
|
393
383
|
checkWarnings: ({ count }) => (count === 1 ? "1 Warnung" : `${count} Warnungen`),
|
|
394
384
|
openImage: ({ name }) => `Bild ${name} öffnen`,
|
|
395
385
|
steered: "Richtung geändert",
|
|
396
|
-
reasoning: "Überlegungen",
|
|
397
|
-
groupPlan: "Arbeitsplan aktualisiert",
|
|
398
|
-
groupCode: "Mit Code gearbeitet",
|
|
399
|
-
groupCloud: "Cloud-Integration verwendet",
|
|
400
|
-
groupReadFiles: "Dateien gelesen",
|
|
401
|
-
groupWriteFiles: "Dateien geschrieben",
|
|
402
|
-
groupPdf: "PDFs erstellt",
|
|
403
386
|
groupLoad: "Werkzeuge und Wissen geladen",
|
|
404
|
-
groupWeb: "Im Web recherchiert",
|
|
405
|
-
groupMemory: "Gedächtnis genutzt",
|
|
406
|
-
groupDelivered: "Ergebnisse bereitgestellt",
|
|
407
|
-
groupTools: "Werkzeuge verwendet",
|
|
408
387
|
groupFailed: ({ count }) => `${count} fehlgeschlagen`,
|
|
409
388
|
groupRejected: ({ count }) => `${count} abgelehnt`,
|
|
410
389
|
receiptDone: ({ title }) => `${title} erledigt`,
|
|
411
390
|
receiptApproved: ({ title }) => `Freigegeben: ${title}`,
|
|
391
|
+
receiptRunning: ({ title }) => `Wird ausgeführt: ${title}`,
|
|
412
392
|
approvalNeeded: ({ title }) => `Freigabe nötig: ${title}`,
|
|
413
393
|
answerReady: "Antwort fertig",
|
|
414
394
|
receiptRejected: ({ title }) => `Abgelehnt: ${title}`,
|
|
@@ -419,7 +399,7 @@ const messages = i18n.define({
|
|
|
419
399
|
streamAccessDenied: "Du hast keinen Zugriff mehr auf diesen Chat.",
|
|
420
400
|
streamNotFound: "Dieser Chat ist nicht mehr verfügbar.",
|
|
421
401
|
assistant: "Assistent",
|
|
422
|
-
|
|
402
|
+
approvalRunsAfter: "Wird erst nach deiner Freigabe ausgeführt",
|
|
423
403
|
approvalRequired: ({ title }) => `Freigabe erforderlich: ${title}`,
|
|
424
404
|
reject: "Ablehnen",
|
|
425
405
|
approved: "Freigegeben",
|
|
@@ -1,28 +1,35 @@
|
|
|
1
1
|
import { Chat, type ChatActivityProps } from "@k2b/ui";
|
|
2
|
-
import { createContext, type JSX, splitProps, useContext } from "solid-js";
|
|
2
|
+
import { createContext, createSignal, type JSX, type Signal, splitProps, useContext } from "solid-js";
|
|
3
3
|
|
|
4
4
|
export type AiToolDisclosureState = {
|
|
5
5
|
get: (blockId: string) => boolean | undefined;
|
|
6
6
|
set: (blockId: string, open: boolean) => void;
|
|
7
7
|
};
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Disclosure choices by block id. Reactive, so a disclosure that its reader opens renders its body at once, also when
|
|
11
|
+
* nothing else in the turn changes; choices survive a remount and, in session storage, a reload.
|
|
12
|
+
*/
|
|
9
13
|
export const createAiToolDisclosureState = (): AiToolDisclosureState => {
|
|
10
|
-
const
|
|
14
|
+
const signals = new Map<string, Signal<boolean | undefined>>();
|
|
11
15
|
const storageKey = (blockId: string) => `cloud.ai.tool-disclosure:${blockId}`;
|
|
16
|
+
const signal = (blockId: string): Signal<boolean | undefined> => {
|
|
17
|
+
const existing = signals.get(blockId);
|
|
18
|
+
if (existing) return existing;
|
|
19
|
+
let saved: boolean | undefined;
|
|
20
|
+
try {
|
|
21
|
+
if (window.sessionStorage.getItem(storageKey(blockId)) === "open") saved = true;
|
|
22
|
+
} catch {
|
|
23
|
+
/* SSR and disabled storage keep the in-memory state. */
|
|
24
|
+
}
|
|
25
|
+
const created = createSignal(saved);
|
|
26
|
+
signals.set(blockId, created);
|
|
27
|
+
return created;
|
|
28
|
+
};
|
|
12
29
|
return {
|
|
13
|
-
get: (blockId) =>
|
|
14
|
-
if (!openByBlockId.has(blockId)) {
|
|
15
|
-
try {
|
|
16
|
-
const saved = window.sessionStorage.getItem(storageKey(blockId));
|
|
17
|
-
if (saved === "open") openByBlockId.set(blockId, true);
|
|
18
|
-
} catch {
|
|
19
|
-
/* SSR and disabled storage keep the in-memory state. */
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
return openByBlockId.get(blockId);
|
|
23
|
-
},
|
|
30
|
+
get: (blockId) => signal(blockId)[0](),
|
|
24
31
|
set: (blockId, open) => {
|
|
25
|
-
|
|
32
|
+
signal(blockId)[1](open);
|
|
26
33
|
try {
|
|
27
34
|
if (open) window.sessionStorage.setItem(storageKey(blockId), "open");
|
|
28
35
|
else window.sessionStorage.removeItem(storageKey(blockId));
|