@k2b/cloud 0.31.0 → 0.33.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 +8 -6
- package/src/_internal/define-app.ts +20 -9
- package/src/_internal/page-responses.ts +5 -1
- package/src/ai/app-skills.ts +1 -7
- 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 +11 -7
- 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/message-actions.tsx +6 -0
- package/src/ai/chat/message-utils.ts +7 -0
- package/src/ai/chat/messages.ts +16 -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 +1 -0
- package/src/ai/chat/tool-names.ts +14 -0
- package/src/ai/chat/turn-view.tsx +17 -7
- 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/client-tool-results.ts +9 -0
- package/src/ai/code-source-contracts.ts +17 -2
- package/src/ai/code-source-tools.ts +1 -0
- package/src/ai/default-tool-contracts.ts +334 -91
- package/src/ai/default-tools.ts +89 -13
- package/src/ai/executor.ts +173 -31
- 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/html-pdf-tool.ts +3 -2
- package/src/ai/http.ts +37 -12
- package/src/ai/index.ts +90 -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/memories.ts +48 -1
- package/src/ai/memory-learning.ts +17 -6
- package/src/ai/memory-tool.ts +1 -0
- package/src/ai/migrate.ts +15 -0
- 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 +6 -2
- package/src/ai/provider.ts +17 -4
- package/src/ai/request-context.ts +33 -0
- package/src/ai/routes.ts +96 -57
- package/src/ai/runtime.ts +48 -12
- 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 +25 -0
- package/src/ai/system-prompt.ts +69 -10
- package/src/ai/timeline.ts +3 -1
- package/src/ai/tools.ts +3 -0
- package/src/ai/types.ts +9 -0
- package/src/ai/view-block.ts +941 -0
- package/src/ai/vision-tool.ts +2 -1
- 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/index.ts +3 -0
- package/src/api/me.ts +12 -3
- 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/app-approval.ts +32 -0
- package/src/browser/choose-files.tsx +26 -13
- 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/navigation-search.ts +4 -1
- package/src/browser/provider-list.ts +2 -1
- package/src/browser/save-files.tsx +13 -2
- package/src/cli/account.ts +21 -2
- package/src/cli/commands.ts +4 -2
- package/src/contracts/app-approval.ts +120 -0
- 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/shared.ts +5 -0
- 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 +2 -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/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 +27 -11
- package/src/services/approvals/list.ts +120 -0
- package/src/services/approvals/migrate.ts +9 -1
- package/src/services/identity/models-only-auth.ts +25 -0
- package/src/services/notifications/email-frame.ts +16 -3
- package/src/services/outbox.ts +13 -6
- 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 +29 -3
- package/src/shared/email-html.ts +4 -0
- 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/shared/theme.ts +41 -20
- package/src/ssr/AnnouncementBanners.tsx +30 -0
- package/src/ssr/AppBadge.tsx +4 -4
- package/src/ssr/AppLaunchpadPanel.tsx +6 -6
- package/src/ssr/CloudShell.island.tsx +91 -0
- package/src/ssr/GlobalSearchTrigger.tsx +51 -0
- package/src/ssr/Layout.tsx +56 -85
- package/src/ssr/LayoutHeader.tsx +31 -77
- package/src/ssr/LayoutHelp.tsx +2 -57
- package/src/ssr/LayoutHelpTrigger.tsx +51 -0
- package/src/ssr/{LayoutPreferences.island.tsx → LayoutPreferences.tsx} +2 -2
- package/src/ssr/LayoutRail.tsx +15 -69
- package/src/ssr/MinimalLayout.tsx +2 -3
- package/src/ssr/MinimalLayoutPreferences.island.tsx +12 -15
- package/src/ssr/MobileNavigation.tsx +13 -8
- package/src/ssr/MobileProfileActions.tsx +10 -7
- package/src/ssr/PageTrailRow.tsx +14 -0
- package/src/ssr/{ProfilePreferences.island.tsx → ProfilePreferences.tsx} +12 -7
- package/src/ssr/PwaLayout.tsx +0 -2
- package/src/ssr/PwaRuntime.island.tsx +1 -1
- package/src/ssr/{RailApps.island.tsx → RailApps.tsx} +2 -2
- package/src/ssr/ShellRuntime.island.tsx +170 -0
- package/src/ssr/TimezoneCookie.island.tsx +3 -19
- package/src/ssr/WorkspaceNavigationProvider.tsx +20 -1
- package/src/ssr/announcements-dialog.tsx +66 -0
- package/src/ssr/app-badges.ts +50 -6
- package/src/ssr/{AppLaunchpad.island.tsx → app-launchpad.tsx} +42 -62
- package/src/ssr/busy-action.ts +19 -0
- package/src/ssr/islands/index.ts +9 -6
- package/src/ssr/launchpad-surface.tsx +15 -0
- package/src/ssr/layout-help-registry.tsx +60 -0
- package/src/ssr/mobile-navigation.browser-harness.tsx +20 -0
- package/src/ssr/page-trail.browser-harness.tsx +52 -3
- package/src/ssr/preference-controller.ts +30 -21
- package/src/ssr/profile-actions.ts +26 -15
- package/src/ssr/profile-preferences-messages.ts +8 -4
- package/src/ssr/rail-context.ts +8 -12
- package/src/ssr/rail-navigation.ts +5 -3
- package/src/ssr/runtime.ts +6 -1
- package/src/ssr/shell-data.tsx +48 -0
- package/src/ssr/timezone-cookie.ts +22 -0
- package/src/ssr/workspace-navigation.ts +25 -2
- package/src/styles/approvals.css +390 -0
- package/src/styles/global.css +22 -7
- package/src/styles/utilities-navigation.css +13 -0
- package/src/workflows/language/compiler.ts +1 -1
- package/src/ssr/AppWorkspaceController.island.tsx +0 -21
- package/src/ssr/BrowserPushRegistration.island.tsx +0 -12
- package/src/ssr/GlobalAnnouncements.island.tsx +0 -117
- package/src/ssr/GlobalSearchTrigger.island.tsx +0 -75
- package/src/ssr/LayoutHelpTrigger.island.tsx +0 -61
- package/src/ssr/PageTrailRow.island.tsx +0 -20
- package/src/ssr/PageTrailRuntime.island.tsx +0 -26
- package/src/ssr/RegisteredHelpDocuments.island.tsx +0 -3
- /package/src/ssr/{LayoutBreadcrumbs.island.tsx → LayoutBreadcrumbs.tsx} +0 -0
|
@@ -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";
|
|
@@ -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";
|
|
@@ -221,7 +222,8 @@ export function ApprovalBlockView(props: { turnId: string; block: ToolBlock; onD
|
|
|
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.
|
|
@@ -545,12 +547,13 @@ function CapabilityToolView(props: { block: ToolBlock }) {
|
|
|
545
547
|
|
|
546
548
|
function RejectedToolView(props: { block: ToolBlock }) {
|
|
547
549
|
const locale = useLocale();
|
|
550
|
+
const timeZone = useReaderTimeZone();
|
|
548
551
|
const presentation = () => props.block.presentation;
|
|
549
|
-
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());
|
|
550
553
|
return (
|
|
551
554
|
<Chat.Activity
|
|
552
555
|
icon={aiToolIcon(props.block.name, presentation()?.appIcon)}
|
|
553
|
-
label={toolOutcome(props.block, "rejected", locale()) ?? aiChatMessages(locale()).toolRejected({ title: title() })}
|
|
556
|
+
label={toolOutcome(props.block, "rejected", locale(), timeZone) ?? aiChatMessages(locale()).toolRejected({ title: title() })}
|
|
554
557
|
accent={presentation()?.appAccent}
|
|
555
558
|
/>
|
|
556
559
|
);
|
|
@@ -823,6 +826,7 @@ function StepImage(props: { path: string; src: string; description: string }) {
|
|
|
823
826
|
*/
|
|
824
827
|
export function CompactToolRow(props: { block: ToolBlock; busy?: boolean }) {
|
|
825
828
|
const locale = useLocale();
|
|
829
|
+
const timeZone = useReaderTimeZone();
|
|
826
830
|
const t = () => aiChatMessages(locale());
|
|
827
831
|
const args = () => (isRecord(props.block.args) ? props.block.args : {});
|
|
828
832
|
/**
|
|
@@ -833,11 +837,11 @@ export function CompactToolRow(props: { block: ToolBlock; busy?: boolean }) {
|
|
|
833
837
|
const status = props.block.status;
|
|
834
838
|
const outcome =
|
|
835
839
|
status === "completed" && !props.block.isError
|
|
836
|
-
? toolOutcome(props.block, "done", locale())
|
|
840
|
+
? toolOutcome(props.block, "done", locale(), timeZone)
|
|
837
841
|
: status === "rejected"
|
|
838
|
-
? toolOutcome(props.block, "rejected", locale())
|
|
842
|
+
? toolOutcome(props.block, "rejected", locale(), timeZone)
|
|
839
843
|
: null;
|
|
840
|
-
return outcome ?? toolSubject(props.block, locale());
|
|
844
|
+
return outcome ?? toolSubject(props.block, locale(), timeZone);
|
|
841
845
|
};
|
|
842
846
|
const detail = () =>
|
|
843
847
|
sentence() ? undefined : [args().path, args().name, args().query, args().title].find((value) => typeof value === "string");
|
|
@@ -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
|
+
}
|