@k2b/cloud 0.30.0 → 0.32.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 +7 -4
- package/src/_internal/app-presentation.ts +22 -1
- package/src/_internal/capabilities.ts +2 -2
- package/src/_internal/define-app.ts +17 -5
- package/src/_internal/registry.ts +50 -6
- package/src/_internal/widget-declarations.ts +53 -0
- package/src/ai/app-skills.ts +1 -7
- package/src/ai/approval-routes.ts +40 -4
- package/src/ai/approvals.ts +68 -10
- package/src/ai/audio-tool.ts +2 -1
- package/src/ai/browser.ts +109 -0
- package/src/ai/capabilities.ts +6 -2
- package/src/ai/chart-block.ts +431 -81
- package/src/ai/chat/action-sentences.ts +13 -7
- package/src/ai/chat/blocks.tsx +64 -39
- package/src/ai/chat/builtin-tools.tsx +9 -7
- package/src/ai/chat/chart-block.tsx +218 -44
- package/src/ai/chat/explorer-block.tsx +276 -0
- package/src/ai/chat/file-tools.tsx +40 -1
- package/src/ai/chat/live-turn.browser-harness.tsx +4 -1
- package/src/ai/chat/message-actions.tsx +9 -1
- package/src/ai/chat/message-utils.ts +7 -0
- package/src/ai/chat/messages.ts +34 -0
- package/src/ai/chat/presentation.tsx +13 -3
- package/src/ai/chat/reference-text.tsx +34 -0
- package/src/ai/chat/tool-groups.ts +67 -12
- package/src/ai/chat/tool-names.ts +18 -2
- package/src/ai/chat/turn-layout.ts +8 -0
- package/src/ai/chat/turn-view.tsx +111 -32
- package/src/ai/chat/view-block.browser-harness.tsx +35 -0
- package/src/ai/chat/view-block.tsx +417 -0
- package/src/ai/chat/visual-tools.tsx +32 -53
- package/src/ai/chat/web-tools.tsx +95 -1
- package/src/ai/client/controller.ts +6 -3
- package/src/ai/client-tool-results.ts +9 -0
- package/src/ai/code-runtime-tools.ts +50 -17
- package/src/ai/code-source-contracts.ts +25 -21
- package/src/ai/code-source-tools.ts +3 -1
- package/src/ai/default-tool-contracts.ts +334 -91
- package/src/ai/default-tools.ts +87 -11
- package/src/ai/executor.ts +343 -45
- package/src/ai/fetch-file-tool.ts +41 -4
- package/src/ai/file-error.ts +2 -0
- package/src/ai/file-tools.ts +27 -7
- package/src/ai/files-store.ts +60 -0
- package/src/ai/firecrawl-tools.ts +87 -4
- package/src/ai/html-pdf-tool.ts +3 -2
- package/src/ai/http.ts +37 -12
- package/src/ai/index.ts +98 -0
- package/src/ai/local-tool-contracts.ts +244 -0
- package/src/ai/malformed-tool-call-repair.ts +144 -0
- package/src/ai/markdown-pdf-tool.ts +2 -1
- package/src/ai/migrate.ts +120 -2
- package/src/ai/openai-api-messages.ts +53 -0
- package/src/ai/openai-api-slots.ts +40 -0
- package/src/ai/openai-api.ts +383 -0
- package/src/ai/protocol.ts +28 -3
- package/src/ai/provider.ts +16 -4
- package/src/ai/request-context.ts +33 -0
- package/src/ai/routes.ts +99 -49
- package/src/ai/runtime.ts +90 -15
- package/src/ai/settings.ts +5 -0
- package/src/ai/skill-catalog.ts +20 -5
- package/src/ai/skill-format.ts +17 -7
- package/src/ai/skill-tool.ts +15 -5
- package/src/ai/skills-routes.ts +20 -6
- package/src/ai/skills.ts +26 -7
- package/src/ai/store.ts +91 -1
- package/src/ai/system-prompt.ts +69 -10
- package/src/ai/timeline.ts +3 -1
- package/src/ai/tool-audit.ts +11 -1
- package/src/ai/tools.ts +94 -3
- package/src/ai/types.ts +48 -0
- package/src/ai/view-block.ts +941 -0
- package/src/ai/vision-tool.ts +2 -1
- package/src/ai/web-provenance.ts +103 -0
- package/src/api/admin-identity.ts +6 -1
- package/src/api/app-approval-actions.ts +36 -0
- package/src/api/app-approval.ts +10 -2
- package/src/api/approvals-loader.ts +15 -0
- package/src/api/approvals.ts +120 -0
- package/src/api/capabilities.ts +3 -2
- package/src/api/index.ts +3 -0
- package/src/api/me.ts +12 -3
- package/src/api/widgets.ts +36 -5
- package/src/approvals/Approvals.browser-harness.tsx +16 -0
- package/src/approvals/Approvals.tsx +621 -0
- package/src/approvals/ApprovalsNavStatus.tsx +20 -0
- package/src/approvals/messages.ts +95 -0
- package/src/approvals/ui.ts +8 -0
- package/src/browser/CloudResourceSearch.tsx +1 -1
- package/src/browser/app-approval.ts +32 -0
- package/src/browser/choose-files.tsx +26 -13
- package/src/browser/command-shortcuts.ts +21 -6
- package/src/browser/commands.ts +13 -4
- package/src/browser/file-provider-error.ts +11 -0
- package/src/browser/file-providers.ts +3 -12
- package/src/browser/files.tsx +1 -2
- package/src/browser/live-connection.ts +2 -3
- package/src/browser/provider-list.ts +2 -1
- package/src/browser/save-files.tsx +13 -2
- package/src/browser/spotlight-position.ts +4 -1
- package/src/browser/widgets.ts +7 -5
- package/src/capabilities/executions.ts +3 -0
- package/src/cli/account.ts +21 -2
- package/src/cli/commands.ts +24 -2
- package/src/cli/index.ts +16 -0
- package/src/cli/plugin.ts +30 -0
- package/src/contracts/app-approval.ts +120 -0
- package/src/contracts/app.ts +34 -2
- package/src/contracts/approval-paths.ts +4 -0
- package/src/contracts/approvals.ts +81 -0
- package/src/contracts/command-path.ts +17 -0
- package/src/contracts/commands.ts +5 -15
- package/src/contracts/registry.ts +9 -1
- package/src/contracts/shared.ts +5 -0
- package/src/contracts/widgets.ts +47 -2
- package/src/desktop/solid.tsx +6 -7
- package/src/events/live-protocol.ts +2 -1
- package/src/events/live-server-message.ts +40 -0
- package/src/events/testing.ts +88 -0
- package/src/server/index.ts +3 -0
- package/src/server/middleware/auth.ts +11 -1
- package/src/server/request-timeout.ts +22 -0
- package/src/server/services/access.ts +4 -3
- package/src/server/widget-request.ts +27 -0
- package/src/services/app-approval.ts +103 -15
- package/src/services/approvals/cursor.ts +15 -0
- package/src/services/approvals/device.ts +112 -0
- package/src/services/approvals/index.ts +241 -0
- package/src/services/approvals/list.ts +120 -0
- package/src/services/approvals/migrate.ts +41 -0
- package/src/services/identity/models-only-auth.ts +25 -0
- package/src/services/notifications/email-frame.ts +16 -3
- package/src/services/service-account-credentials.ts +22 -10
- package/src/services/settings/core-settings.ts +27 -0
- package/src/shared/ai-platform-prompt.ts +26 -1
- package/src/shared/app-presentation.ts +11 -0
- package/src/shared/email-html.ts +46 -7
- package/src/shared/index.ts +6 -1
- package/src/shared/markdown/extensions/code.ts +14 -0
- package/src/shared/markdown/index.ts +4 -4
- package/src/ssr/AdminLayout.tsx +4 -7
- package/src/ssr/AppBadge.tsx +4 -4
- package/src/ssr/AppLaunchpad.island.tsx +1 -1
- package/src/ssr/AppLaunchpadPanel.tsx +4 -4
- package/src/ssr/Layout.tsx +38 -14
- package/src/ssr/LayoutFooter.tsx +1 -1
- package/src/ssr/LayoutHeader.tsx +17 -20
- package/src/ssr/LayoutHelp.tsx +1 -1
- package/src/ssr/LayoutRail.tsx +1 -2
- package/src/ssr/PageTrail.tsx +23 -0
- package/src/ssr/PageTrailRow.island.tsx +20 -0
- package/src/ssr/PageTrailRuntime.island.tsx +26 -0
- package/src/ssr/ProfilePreferences.island.tsx +7 -3
- package/src/ssr/RailApps.island.tsx +2 -2
- package/src/ssr/app-badges.ts +50 -6
- package/src/ssr/islands/index.ts +1 -0
- package/src/ssr/layout-runtime.ts +60 -0
- package/src/ssr/mobile-menu-history.ts +1 -1
- package/src/ssr/page-trail-runtime.ts +313 -0
- package/src/ssr/page-trail.browser-harness.tsx +265 -0
- package/src/ssr/page-trail.ts +59 -0
- package/src/ssr/platform-messages.ts +4 -0
- package/src/ssr/rail-navigation.ts +5 -3
- package/src/ssr/runtime.ts +6 -1
- package/src/styles/approvals.css +390 -0
- package/src/styles/effects.css +21 -4
- package/src/styles/global.css +39 -3
- package/src/styles/utilities-layout.css +24 -1
- package/src/styles/utilities-navigation.css +6 -18
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createContext, useContext } from "solid-js";
|
|
1
2
|
import {
|
|
2
3
|
type CapabilityActionWording,
|
|
3
4
|
capabilityActionOutcome,
|
|
@@ -10,9 +11,14 @@ import { displayToolName, isRecord } from "./message-utils";
|
|
|
10
11
|
|
|
11
12
|
type ToolBlock = Extract<AiTurnBlock, { kind: "tool" }>;
|
|
12
13
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
const ReaderTimeZoneContext = createContext<string>();
|
|
15
|
+
|
|
16
|
+
/** Gives the chat the reader's time zone from the request, so server and browser render the same times. */
|
|
17
|
+
export const AiReaderTimeZoneProvider = ReaderTimeZoneContext.Provider;
|
|
18
|
+
|
|
19
|
+
/** Instants show in the provided time zone, else the browser's own; outside a browser they stay in UTC. */
|
|
20
|
+
export const useReaderTimeZone = (): string =>
|
|
21
|
+
useContext(ReaderTimeZoneContext) ?? (typeof window === "undefined" ? "UTC" : Intl.DateTimeFormat().resolvedOptions().timeZone);
|
|
16
22
|
|
|
17
23
|
/**
|
|
18
24
|
* How the chat words one call that acts on something: an app Action through its saved presentation, a
|
|
@@ -33,17 +39,17 @@ export const toolWording = (block: ToolBlock, locale: string): CapabilityActionW
|
|
|
33
39
|
};
|
|
34
40
|
|
|
35
41
|
/** What the call does or would do, as one sentence; `null` for calls that only read. */
|
|
36
|
-
export const toolSubject = (block: ToolBlock, locale: string): string | null => {
|
|
42
|
+
export const toolSubject = (block: ToolBlock, locale: string, timeZone: string): string | null => {
|
|
37
43
|
const wording = toolWording(block, locale);
|
|
38
|
-
return wording ? capabilityActionSubject(wording, block.args, { locale, timeZone
|
|
44
|
+
return wording ? capabilityActionSubject(wording, block.args, { locale, timeZone }) : null;
|
|
39
45
|
};
|
|
40
46
|
|
|
41
47
|
/** The sentence for one outcome, or `null` when the tool has none that renders for this call. */
|
|
42
|
-
export const toolOutcome = (block: ToolBlock, key: "done" | "rejected" | "notRun", locale: string): string | null => {
|
|
48
|
+
export const toolOutcome = (block: ToolBlock, key: "done" | "rejected" | "notRun", locale: string, timeZone: string): string | null => {
|
|
43
49
|
const wording = toolWording(block, locale);
|
|
44
50
|
if (!wording) return null;
|
|
45
51
|
const result = isRecord(block.result) ? block.result : undefined;
|
|
46
52
|
// App Actions return their data inside the result envelope; Cloud tools return the data itself.
|
|
47
53
|
const data = block.presentation?.kind === "capability" ? result?.data : block.result;
|
|
48
|
-
return capabilityActionOutcome(wording, key, { input: block.args, data }, { locale, timeZone
|
|
54
|
+
return capabilityActionOutcome(wording, key, { input: block.args, data }, { locale, timeZone });
|
|
49
55
|
};
|
package/src/ai/chat/blocks.tsx
CHANGED
|
@@ -6,7 +6,7 @@ import { capabilityApprovalReason, capabilityApprovalReasonLabel } from "../../_
|
|
|
6
6
|
import type { CapabilityActionReview } from "../../contracts/capabilities";
|
|
7
7
|
import { markdown } from "../../shared";
|
|
8
8
|
import type { AiTurnBlock } from "../protocol";
|
|
9
|
-
import { toolOutcome, toolSubject } from "./action-sentences";
|
|
9
|
+
import { toolOutcome, toolSubject, useReaderTimeZone } from "./action-sentences";
|
|
10
10
|
import { hasSpecializedBuiltinToolView, SpecializedBuiltinToolBlock } from "./builtin-tools";
|
|
11
11
|
import { hasCapabilityTable } from "./capability-result";
|
|
12
12
|
import { CapabilityTablePreview } from "./capability-table";
|
|
@@ -34,7 +34,7 @@ import { AssistantMarkdownBlock } from "./primitives";
|
|
|
34
34
|
import { AiToolActivity } from "./tool-disclosure";
|
|
35
35
|
import { isFailedTool } from "./tool-groups";
|
|
36
36
|
import { CloudCardBlock, CloudSurveyBlock, CloudSurveyResultBlock, CloudTextEditorBlock, CloudTextEditorResultBlock } from "./visual-tools";
|
|
37
|
-
import { FetchFileToolBlock, WebExtractToolBlock, WebSearchToolBlock } from "./web-tools";
|
|
37
|
+
import { FetchFileToolBlock, WebExtractToolBlock, WebSearchToolBlock, WebsiteReceipts, websiteReceipts } from "./web-tools";
|
|
38
38
|
|
|
39
39
|
type ToolBlock = Extract<AiTurnBlock, { kind: "tool" }>;
|
|
40
40
|
type ReviewDetail = NonNullable<CapabilityActionReview["details"]>[number];
|
|
@@ -197,6 +197,7 @@ export type AiApprovalDecision = { callId: string; approved: boolean };
|
|
|
197
197
|
export function ApprovalBlockView(props: { turnId: string; block: ToolBlock; onDecided?: (decision: AiApprovalDecision) => void }) {
|
|
198
198
|
const actions = useAiChatActions();
|
|
199
199
|
const locale = useLocale();
|
|
200
|
+
const timeZone = useReaderTimeZone();
|
|
200
201
|
const t = () => aiChatMessages(locale());
|
|
201
202
|
const request = () => ({ turnId: props.turnId, callId: props.block.callId, name: props.block.name });
|
|
202
203
|
const pending = () => props.block.status === "awaiting_approval";
|
|
@@ -205,7 +206,7 @@ export function ApprovalBlockView(props: { turnId: string; block: ToolBlock; onD
|
|
|
205
206
|
const [detailsOpen, setDetailsOpen] = createSignal(false);
|
|
206
207
|
// The call this card answered: a later approval for the same block may already have arrived when the server accepts.
|
|
207
208
|
let decision: AiApprovalDecision | null = null;
|
|
208
|
-
const approval = mutation.create<void, { approved: boolean; remember?: "always" }>({
|
|
209
|
+
const approval = mutation.create<void, { approved: boolean; remember?: "chat" | "always" }>({
|
|
209
210
|
mutation: async (input) => {
|
|
210
211
|
if (!actions.onApproval) throw new Error("Approval is unavailable.");
|
|
211
212
|
await actions.onApproval(request(), input);
|
|
@@ -215,13 +216,14 @@ export function ApprovalBlockView(props: { turnId: string; block: ToolBlock; onD
|
|
|
215
216
|
if (decision) props.onDecided?.(decision);
|
|
216
217
|
},
|
|
217
218
|
});
|
|
218
|
-
const submit = (input: { approved: boolean; remember?: "always" }) => {
|
|
219
|
+
const submit = (input: { approved: boolean; remember?: "chat" | "always" }) => {
|
|
219
220
|
if (actionDisabled() || approval.loading()) return;
|
|
220
221
|
decision = { callId: props.block.callId, approved: input.approved };
|
|
221
222
|
void approval.mutate(input);
|
|
222
223
|
};
|
|
223
224
|
// The heading says what will happen in the owning app's words; the app or the assistant name stays beside it.
|
|
224
|
-
const title = () =>
|
|
225
|
+
const title = () =>
|
|
226
|
+
toolSubject(props.block, locale(), timeZone) ?? props.block.presentation?.title ?? displayToolName(props.block.name, locale());
|
|
225
227
|
const ownerName = () => props.block.presentation?.appName ?? t().assistant;
|
|
226
228
|
const capability = () => props.block.presentation?.kind === "capability";
|
|
227
229
|
// The model's own reason, labelled as such, only where Cloud offered it; it adds to the app's sentence and never replaces it.
|
|
@@ -411,6 +413,16 @@ export function ApprovalBlockView(props: { turnId: string; block: ToolBlock; onD
|
|
|
411
413
|
icon: detailsOpen() ? "ti ti-eye-off" : "ti ti-eye",
|
|
412
414
|
action: () => setDetailsOpen((open) => !open),
|
|
413
415
|
},
|
|
416
|
+
// The narrower chat reach comes first: it ends with this chat.
|
|
417
|
+
...(props.block.approval?.allowChat
|
|
418
|
+
? [
|
|
419
|
+
{
|
|
420
|
+
label: props.block.approval.website ? t().allowWebsiteForChat : t().chatApprove,
|
|
421
|
+
icon: props.block.approval.website ? "ti ti-world-check" : "ti ti-message-check",
|
|
422
|
+
action: () => submit({ approved: true, remember: "chat" }),
|
|
423
|
+
},
|
|
424
|
+
]
|
|
425
|
+
: []),
|
|
414
426
|
...(props.block.approval?.allowAlways
|
|
415
427
|
? [
|
|
416
428
|
{
|
|
@@ -535,12 +547,13 @@ function CapabilityToolView(props: { block: ToolBlock }) {
|
|
|
535
547
|
|
|
536
548
|
function RejectedToolView(props: { block: ToolBlock }) {
|
|
537
549
|
const locale = useLocale();
|
|
550
|
+
const timeZone = useReaderTimeZone();
|
|
538
551
|
const presentation = () => props.block.presentation;
|
|
539
|
-
const title = () => toolSubject(props.block, locale()) ?? presentation()?.title ?? displayToolName(props.block.name, locale());
|
|
552
|
+
const title = () => toolSubject(props.block, locale(), timeZone) ?? presentation()?.title ?? displayToolName(props.block.name, locale());
|
|
540
553
|
return (
|
|
541
554
|
<Chat.Activity
|
|
542
555
|
icon={aiToolIcon(props.block.name, presentation()?.appIcon)}
|
|
543
|
-
label={toolOutcome(props.block, "rejected", locale()) ?? aiChatMessages(locale()).toolRejected({ title: title() })}
|
|
556
|
+
label={toolOutcome(props.block, "rejected", locale(), timeZone) ?? aiChatMessages(locale()).toolRejected({ title: title() })}
|
|
544
557
|
accent={presentation()?.appAccent}
|
|
545
558
|
/>
|
|
546
559
|
);
|
|
@@ -725,6 +738,9 @@ function ToolBlockView(props: { turnId: string; block: ToolBlock; active?: boole
|
|
|
725
738
|
<Chat.Activity label={displayToolName(props.block.name, locale())} icon={aiToolIcon(props.block.name)} busy />
|
|
726
739
|
</Match>
|
|
727
740
|
</Switch>
|
|
741
|
+
<Show when={websiteReceipts(props.block).length > 0}>
|
|
742
|
+
<WebsiteReceipts block={props.block} />
|
|
743
|
+
</Show>
|
|
728
744
|
<Show when={hasCapabilityTable(props.block)}>
|
|
729
745
|
<CapabilityTablePreview
|
|
730
746
|
result={props.block.result}
|
|
@@ -810,6 +826,7 @@ function StepImage(props: { path: string; src: string; description: string }) {
|
|
|
810
826
|
*/
|
|
811
827
|
export function CompactToolRow(props: { block: ToolBlock; busy?: boolean }) {
|
|
812
828
|
const locale = useLocale();
|
|
829
|
+
const timeZone = useReaderTimeZone();
|
|
813
830
|
const t = () => aiChatMessages(locale());
|
|
814
831
|
const args = () => (isRecord(props.block.args) ? props.block.args : {});
|
|
815
832
|
/**
|
|
@@ -820,11 +837,11 @@ export function CompactToolRow(props: { block: ToolBlock; busy?: boolean }) {
|
|
|
820
837
|
const status = props.block.status;
|
|
821
838
|
const outcome =
|
|
822
839
|
status === "completed" && !props.block.isError
|
|
823
|
-
? toolOutcome(props.block, "done", locale())
|
|
840
|
+
? toolOutcome(props.block, "done", locale(), timeZone)
|
|
824
841
|
: status === "rejected"
|
|
825
|
-
? toolOutcome(props.block, "rejected", locale())
|
|
842
|
+
? toolOutcome(props.block, "rejected", locale(), timeZone)
|
|
826
843
|
: null;
|
|
827
|
-
return outcome ?? toolSubject(props.block, locale());
|
|
844
|
+
return outcome ?? toolSubject(props.block, locale(), timeZone);
|
|
828
845
|
};
|
|
829
846
|
const detail = () =>
|
|
830
847
|
sentence() ? undefined : [args().path, args().name, args().query, args().title].find((value) => typeof value === "string");
|
|
@@ -853,35 +870,43 @@ export function CompactToolRow(props: { block: ToolBlock; busy?: boolean }) {
|
|
|
853
870
|
return aiToolIcon(props.block.name, props.block.presentation?.appIcon);
|
|
854
871
|
};
|
|
855
872
|
return (
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
<div
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
873
|
+
<>
|
|
874
|
+
<AiToolActivity
|
|
875
|
+
blockId={props.block.id}
|
|
876
|
+
label={sentence() ?? props.block.presentation?.title ?? displayToolName(props.block.name, locale())}
|
|
877
|
+
description={[
|
|
878
|
+
props.block.status === "running" ? (props.block.progress ?? "") : "",
|
|
879
|
+
typeof detail() === "string" ? String(detail()) : "",
|
|
880
|
+
state(),
|
|
881
|
+
outcome(),
|
|
882
|
+
]
|
|
883
|
+
.filter(Boolean)
|
|
884
|
+
.join(" · ")}
|
|
885
|
+
icon={icon()}
|
|
886
|
+
busy={props.busy && props.block.status === "running"}
|
|
887
|
+
bodyInset={false}
|
|
888
|
+
renderBody={() => (
|
|
889
|
+
<div class="flex min-w-0 flex-col gap-2">
|
|
890
|
+
<div
|
|
891
|
+
class="max-h-72 overflow-auto overscroll-contain rounded-md bg-zinc-100 p-3 dark:bg-zinc-950"
|
|
892
|
+
tabIndex={0}
|
|
893
|
+
role="region"
|
|
894
|
+
aria-label={t().toolInputOutput}
|
|
895
|
+
>
|
|
896
|
+
<ToolDetail title={t().input} toolName={props.block.name} value={props.block.args} />
|
|
897
|
+
<ToolDetail title={t().output} toolName={props.block.name} value={props.block.result} />
|
|
898
|
+
</div>
|
|
899
|
+
{/* Below the input and output, so an image that arrives in an open step grows in like its output and moves nothing above. */}
|
|
900
|
+
<Show when={image()}>
|
|
901
|
+
{(viewed) => <StepImage path={viewed().path} src={viewed().src} description={viewed().description} />}
|
|
902
|
+
</Show>
|
|
880
903
|
</div>
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
)}
|
|
885
|
-
|
|
904
|
+
)}
|
|
905
|
+
/>
|
|
906
|
+
{/* Each request a website approval let through is a step of its own, with its full URL. */}
|
|
907
|
+
<Show when={websiteReceipts(props.block).length > 0}>
|
|
908
|
+
<WebsiteReceipts block={props.block} />
|
|
909
|
+
</Show>
|
|
910
|
+
</>
|
|
886
911
|
);
|
|
887
912
|
}
|
|
@@ -161,13 +161,15 @@ function LoadToolsView(props: { block: ToolBlock }) {
|
|
|
161
161
|
};
|
|
162
162
|
const t = useMessages();
|
|
163
163
|
const unavailableLabel = (reason: unknown) =>
|
|
164
|
-
reason === "
|
|
165
|
-
? t().
|
|
166
|
-
: reason === "
|
|
167
|
-
? t().
|
|
168
|
-
: reason === "
|
|
169
|
-
? t().
|
|
170
|
-
:
|
|
164
|
+
reason === "skill"
|
|
165
|
+
? t().toolIsSkill
|
|
166
|
+
: reason === "not_offered_in_turn"
|
|
167
|
+
? t().toolNotOffered
|
|
168
|
+
: reason === "not_allowed"
|
|
169
|
+
? t().toolNotAllowed
|
|
170
|
+
: reason === "app_offline"
|
|
171
|
+
? t().toolAppOffline
|
|
172
|
+
: t().toolUnknown;
|
|
171
173
|
const rows = () => [
|
|
172
174
|
...toolNames(result().loaded).map((name) => ({ name, label: t().toolsLoaded })),
|
|
173
175
|
...toolNames(result().alreadyLoaded).map((name) => ({ name, label: t().toolsAlreadyLoaded })),
|
|
@@ -1,13 +1,26 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
ChartExplorer,
|
|
3
|
+
type ChartExplorerColumn,
|
|
4
|
+
type ChartLegendEntry,
|
|
5
|
+
type ChartSnapshot,
|
|
6
|
+
createChartCursor,
|
|
7
|
+
prepareChartSnapshot,
|
|
8
|
+
useLocale,
|
|
9
|
+
} from "@k2b/ui";
|
|
10
|
+
import { createMemo, createUniqueId, Index, type JSX, Show } from "solid-js";
|
|
3
11
|
import {
|
|
4
12
|
type CloudAiChartColumn,
|
|
5
13
|
type CloudAiChartRow,
|
|
14
|
+
type CloudAiChartTable,
|
|
15
|
+
cloudAiChartLegend,
|
|
6
16
|
cloudAiChartMarkKey,
|
|
17
|
+
cloudAiChartReferenceMark,
|
|
7
18
|
cloudAiChartRenderOptions,
|
|
8
19
|
cloudAiChartRows,
|
|
20
|
+
cloudAiChartXValueFormat,
|
|
9
21
|
} from "../chart-block";
|
|
10
|
-
import {
|
|
22
|
+
import type { CloudAiChartInput } from "../default-tool-contracts";
|
|
23
|
+
import { type CloudAiGroupView, parseCloudAiChartToolView } from "../view-block";
|
|
11
24
|
import { aiChatMessages } from "./messages";
|
|
12
25
|
|
|
13
26
|
const tooltip = (columns: CloudAiChartColumn[], row: CloudAiChartRow) => {
|
|
@@ -19,61 +32,222 @@ const tooltip = (columns: CloudAiChartColumn[], row: CloudAiChartRow) => {
|
|
|
19
32
|
};
|
|
20
33
|
};
|
|
21
34
|
|
|
35
|
+
type Kind = CloudAiChartInput["kind"];
|
|
36
|
+
type DrawnChart = { input: CloudAiChartInput; table: CloudAiChartTable; snapshot: ChartSnapshot; legend: ChartLegendEntry[] };
|
|
37
|
+
|
|
38
|
+
/** Draws a chart for display and fills its table from that drawing; null when nothing can be drawn. */
|
|
39
|
+
const drawChart = (input: CloudAiChartInput, locale: string): DrawnChart | null => {
|
|
40
|
+
try {
|
|
41
|
+
const rows = cloudAiChartRows(input, locale);
|
|
42
|
+
// The chat lays the legend out in HTML: the renderer's single SVG row runs out of a phone-wide column.
|
|
43
|
+
const snapshot = prepareChartSnapshot(cloudAiChartRenderOptions(input, locale, { legend: "outside" }), {
|
|
44
|
+
key: ({ datum }) => cloudAiChartMarkKey(datum),
|
|
45
|
+
tooltip: ({ datum }) => {
|
|
46
|
+
const row = rows.add(datum);
|
|
47
|
+
return tooltip(rows.columns(), row);
|
|
48
|
+
},
|
|
49
|
+
reference: ({ datum }) => cloudAiChartReferenceMark(input, datum),
|
|
50
|
+
});
|
|
51
|
+
return snapshot.marks.length > 0 ? { input, table: rows.table(), snapshot, legend: cloudAiChartLegend(input, locale) } : null;
|
|
52
|
+
} catch {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
22
57
|
/**
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
|
|
58
|
+
* Single values and trends need less room than a plot with axes. Bar gauges hold at most eight 28-unit rows of the
|
|
59
|
+
* 280-unit drawing; at 12rem a row is still taller than its text.
|
|
60
|
+
*/
|
|
61
|
+
const chartHeight = (kind: Kind) => (kind === "sparkline" ? "8rem" : kind === "stat" ? "10rem" : kind === "barGauge" ? "12rem" : "18rem");
|
|
62
|
+
|
|
63
|
+
function ChartFrame(props: {
|
|
64
|
+
title: string;
|
|
65
|
+
subtitle?: string;
|
|
66
|
+
kind: Kind;
|
|
67
|
+
chart: DrawnChart | null;
|
|
68
|
+
empty?: JSX.Element;
|
|
69
|
+
cursor?: ReturnType<typeof createChartCursor>;
|
|
70
|
+
/** A chart of a titled group: its title sits one heading level and one size below the group's. */
|
|
71
|
+
nested?: boolean;
|
|
72
|
+
}) {
|
|
73
|
+
const locale = useLocale();
|
|
74
|
+
const columns = () =>
|
|
75
|
+
props.chart
|
|
76
|
+
? props.chart.table.columns.map(
|
|
77
|
+
(column): ChartExplorerColumn<CloudAiChartRow> => ({
|
|
78
|
+
id: column.id,
|
|
79
|
+
label: column.label,
|
|
80
|
+
value: (row) => row.cells[column.id] ?? "",
|
|
81
|
+
sortValue: (row) => row.values[column.id] ?? null,
|
|
82
|
+
align: column.numeric ? "right" : "left",
|
|
83
|
+
}),
|
|
84
|
+
)
|
|
85
|
+
: // A frame without a drawing keeps the explorer's header and height; its one column is never shown.
|
|
86
|
+
[{ id: "value", label: aiChatMessages(locale()).chartUnavailable, value: () => "" }];
|
|
87
|
+
return (
|
|
88
|
+
<ChartExplorer
|
|
89
|
+
title={props.title}
|
|
90
|
+
headingLevel={props.nested ? 4 : 3}
|
|
91
|
+
description={props.subtitle ? <span class="text-sm text-secondary">{props.subtitle}</span> : undefined}
|
|
92
|
+
height={chartHeight(props.kind)}
|
|
93
|
+
data={props.chart ? { chart: props.chart.snapshot, rows: props.chart.table.rows } : { chart: emptySnapshot(props.kind), rows: [] }}
|
|
94
|
+
columns={columns()}
|
|
95
|
+
legend={props.chart?.legend.length ? props.chart.legend : undefined}
|
|
96
|
+
cursor={props.cursor}
|
|
97
|
+
empty={props.empty}
|
|
98
|
+
renderDetails={false}
|
|
99
|
+
class={props.nested ? "[&_h4]:text-sm!" : undefined}
|
|
100
|
+
/>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const emptySnapshot = (kind: Kind): ChartSnapshot => ({
|
|
105
|
+
kind,
|
|
106
|
+
svg: "",
|
|
107
|
+
width: 480,
|
|
108
|
+
height: 280,
|
|
109
|
+
stretch: true,
|
|
110
|
+
marks: [],
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
type GroupSummary = {
|
|
114
|
+
title: string;
|
|
115
|
+
subtitle?: string;
|
|
116
|
+
linkX: boolean;
|
|
117
|
+
layout: "stack" | "grid";
|
|
118
|
+
charts: { kind: Kind; title: string; subtitle?: string }[];
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* A group of small charts under one heading. Every chart frame stands with the title and height of its summary from
|
|
123
|
+
* the first frame, while the group loads (`group` undefined) and when it cannot be drawn (`group` null); a chart that
|
|
124
|
+
* cannot be drawn alone shows unavailable in its own frame. With `linkX`, one cursor follows the pointer or a tap
|
|
125
|
+
* across every chart. A grid puts two charts side by side once the chat column has room for both.
|
|
126
|
+
*/
|
|
127
|
+
function ChartGroupFrame(props: { summary: GroupSummary; group: CloudAiGroupView | null | undefined; empty?: JSX.Element }) {
|
|
128
|
+
const locale = useLocale();
|
|
129
|
+
const t = () => aiChatMessages(locale());
|
|
130
|
+
const id = `cloud-chart-group-${createUniqueId()}`;
|
|
131
|
+
const drawn = createMemo(() => props.group?.charts.map((input) => drawChart(input, locale())) ?? []);
|
|
132
|
+
// One cursor per group, owned by the component; the first chart's x format titles the shared tooltip.
|
|
133
|
+
const cursor = createChartCursor({
|
|
134
|
+
formatX: (x) => {
|
|
135
|
+
const first = props.group?.charts[0];
|
|
136
|
+
return first ? cloudAiChartXValueFormat(first, locale())(x) : String(x);
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
return (
|
|
140
|
+
<section class="@container grid min-w-0 gap-3" aria-labelledby={id}>
|
|
141
|
+
<header class="min-w-0">
|
|
142
|
+
<h3 id={id} class="m-0 text-base font-semibold">
|
|
143
|
+
{props.group?.title ?? props.summary.title}
|
|
144
|
+
</h3>
|
|
145
|
+
<Show when={props.group ? props.group.subtitle : props.summary.subtitle}>
|
|
146
|
+
{(subtitle) => <span class="text-sm text-secondary">{subtitle()}</span>}
|
|
147
|
+
</Show>
|
|
148
|
+
</header>
|
|
149
|
+
<div class={`grid min-w-0 gap-6 ${props.summary.layout === "grid" ? "@2xl:grid-cols-2" : ""}`}>
|
|
150
|
+
<Index each={props.summary.charts}>
|
|
151
|
+
{(chart, index) => (
|
|
152
|
+
<ChartFrame
|
|
153
|
+
title={props.group?.charts[index]?.title ?? chart().title}
|
|
154
|
+
subtitle={props.group ? props.group.charts[index]?.subtitle : chart().subtitle}
|
|
155
|
+
kind={chart().kind}
|
|
156
|
+
chart={drawn()[index] ?? null}
|
|
157
|
+
cursor={props.summary.linkX ? cursor : undefined}
|
|
158
|
+
nested
|
|
159
|
+
empty={props.empty ?? <span role="status">{t().chartUnavailable}</span>}
|
|
160
|
+
/>
|
|
161
|
+
)}
|
|
162
|
+
</Index>
|
|
163
|
+
</div>
|
|
164
|
+
</section>
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const groupSummary = (group: CloudAiGroupView): GroupSummary => ({
|
|
169
|
+
title: group.title,
|
|
170
|
+
subtitle: group.subtitle,
|
|
171
|
+
linkX: group.linkX ?? false,
|
|
172
|
+
layout: group.layout ?? "stack",
|
|
173
|
+
charts: group.charts.map((chart) => ({ kind: chart.kind, title: chart.title, subtitle: chart.subtitle })),
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* One `chart` tool call: the shared chart with its data table and Copy data, or a group of them. The block takes its
|
|
178
|
+
* full size with its first frame and keeps it; switching to the table scrolls inside the same height. A call whose
|
|
179
|
+
* arguments are not a valid chart shows nothing while it runs, because the model gets the validation error and the
|
|
180
|
+
* call fails.
|
|
26
181
|
*/
|
|
27
182
|
export function CloudChartBlock(props: { args: unknown; completed: boolean }) {
|
|
28
183
|
const locale = useLocale();
|
|
29
184
|
// Live updates replace the block with equal arguments; only a real change redraws.
|
|
30
185
|
const source = createMemo(() => JSON.stringify(props.args ?? null));
|
|
186
|
+
const view = createMemo(() => parseCloudAiChartToolView(JSON.parse(source())));
|
|
187
|
+
// The tool schema drew the chart once to accept it; here it is drawn once more, for display, and the table is
|
|
188
|
+
// filled from that drawing.
|
|
31
189
|
const chart = createMemo(() => {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
const snapshot = prepareChartSnapshot(cloudAiChartRenderOptions(input, locale()), {
|
|
39
|
-
key: ({ datum }) => cloudAiChartMarkKey(datum),
|
|
40
|
-
tooltip: ({ datum }) => {
|
|
41
|
-
const row = rows.add(datum);
|
|
42
|
-
return tooltip(rows.columns(), row);
|
|
43
|
-
},
|
|
44
|
-
});
|
|
45
|
-
return snapshot.marks.length > 0 ? { input, table: rows.table(), snapshot } : null;
|
|
46
|
-
} catch {
|
|
47
|
-
return null;
|
|
48
|
-
}
|
|
190
|
+
const value = view();
|
|
191
|
+
return value?.view === "chart" ? drawChart(value, locale()) : null;
|
|
192
|
+
});
|
|
193
|
+
const group = createMemo(() => {
|
|
194
|
+
const value = view();
|
|
195
|
+
return value?.view === "group" ? value : undefined;
|
|
49
196
|
});
|
|
50
197
|
return (
|
|
51
198
|
<Show
|
|
52
|
-
when={
|
|
199
|
+
when={group()}
|
|
53
200
|
fallback={
|
|
54
|
-
<Show
|
|
55
|
-
|
|
201
|
+
<Show
|
|
202
|
+
when={chart()}
|
|
203
|
+
fallback={
|
|
204
|
+
<Show when={props.completed}>
|
|
205
|
+
<p class="text-sm text-secondary">{aiChatMessages(locale()).chartUnavailable}</p>
|
|
206
|
+
</Show>
|
|
207
|
+
}
|
|
208
|
+
>
|
|
209
|
+
{(value) => (
|
|
210
|
+
<ChartFrame title={value().input.title} subtitle={value().input.subtitle} kind={value().input.kind} chart={value()} />
|
|
211
|
+
)}
|
|
56
212
|
</Show>
|
|
57
213
|
}
|
|
58
214
|
>
|
|
59
|
-
{(value) => (
|
|
60
|
-
<ChartExplorer
|
|
61
|
-
title={value().input.title}
|
|
62
|
-
description={value().input.subtitle ? <span class="text-sm text-secondary">{value().input.subtitle}</span> : undefined}
|
|
63
|
-
height={value().input.kind === "sparkline" ? "8rem" : "18rem"}
|
|
64
|
-
data={{ chart: value().snapshot, rows: value().table.rows }}
|
|
65
|
-
columns={value().table.columns.map(
|
|
66
|
-
(column): ChartExplorerColumn<CloudAiChartRow> => ({
|
|
67
|
-
id: column.id,
|
|
68
|
-
label: column.label,
|
|
69
|
-
value: (row) => row.cells[column.id] ?? "",
|
|
70
|
-
sortValue: (row) => row.values[column.id] ?? null,
|
|
71
|
-
align: column.numeric ? "right" : "left",
|
|
72
|
-
}),
|
|
73
|
-
)}
|
|
74
|
-
renderDetails={false}
|
|
75
|
-
/>
|
|
76
|
-
)}
|
|
215
|
+
{(value) => <ChartGroupFrame summary={groupSummary(value())} group={value()} />}
|
|
77
216
|
</Show>
|
|
78
217
|
);
|
|
79
218
|
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* A chart view file: the frame stands with the title, actions and height of its `present` summary from the first frame,
|
|
222
|
+
* while the file loads (`input` undefined) and when it cannot be drawn (`input` null).
|
|
223
|
+
*/
|
|
224
|
+
export function CloudChartView(props: {
|
|
225
|
+
summary: { title: string; subtitle?: string; kind: Kind };
|
|
226
|
+
input: CloudAiChartInput | null | undefined;
|
|
227
|
+
}) {
|
|
228
|
+
const locale = useLocale();
|
|
229
|
+
const t = () => aiChatMessages(locale());
|
|
230
|
+
const chart = createMemo(() => (props.input ? drawChart(props.input, locale()) : null));
|
|
231
|
+
return (
|
|
232
|
+
<ChartFrame
|
|
233
|
+
title={chart()?.input.title ?? props.summary.title}
|
|
234
|
+
subtitle={chart() ? chart()?.input.subtitle : props.summary.subtitle}
|
|
235
|
+
kind={props.summary.kind}
|
|
236
|
+
chart={chart()}
|
|
237
|
+
empty={<span role="status">{props.input === undefined ? t().viewLoading : t().viewUnavailable}</span>}
|
|
238
|
+
/>
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** A group view file, sized by its `present` summary like a chart view. */
|
|
243
|
+
export function CloudChartGroupView(props: { summary: GroupSummary; group: CloudAiGroupView | null | undefined }) {
|
|
244
|
+
const locale = useLocale();
|
|
245
|
+
const t = () => aiChatMessages(locale());
|
|
246
|
+
return (
|
|
247
|
+
<ChartGroupFrame
|
|
248
|
+
summary={props.summary}
|
|
249
|
+
group={props.group}
|
|
250
|
+
empty={props.group ? undefined : <span role="status">{props.group === undefined ? t().viewLoading : t().viewUnavailable}</span>}
|
|
251
|
+
/>
|
|
252
|
+
);
|
|
253
|
+
}
|