@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
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CopyButton,
|
|
3
|
+
DataTable,
|
|
4
|
+
type DataTableSort,
|
|
5
|
+
DescriptionList,
|
|
6
|
+
StatCell,
|
|
7
|
+
type StatCellAccent,
|
|
8
|
+
StatGrid,
|
|
9
|
+
type StatGridColumns,
|
|
10
|
+
useLocale,
|
|
11
|
+
} from "@k2b/ui";
|
|
12
|
+
import { createMemo, createSignal, createUniqueId, Index, type JSX, Match, onCleanup, onMount, Show, Switch } from "solid-js";
|
|
13
|
+
import type { CloudAiChartRow } from "../chart-block";
|
|
14
|
+
import {
|
|
15
|
+
CLOUD_AI_DETAILS_VIEWPORT_ITEMS,
|
|
16
|
+
CLOUD_AI_VIEW_MAX_BYTES,
|
|
17
|
+
type CloudAiDetailsView,
|
|
18
|
+
type CloudAiExplorerView,
|
|
19
|
+
type CloudAiStatsView,
|
|
20
|
+
type CloudAiTableView,
|
|
21
|
+
type CloudAiView,
|
|
22
|
+
type CloudAiViewSummary,
|
|
23
|
+
cloudAiDetailsRows,
|
|
24
|
+
cloudAiStatsTiles,
|
|
25
|
+
cloudAiTableTsv,
|
|
26
|
+
cloudAiViewSummary,
|
|
27
|
+
cloudAiViewTable,
|
|
28
|
+
isAiViewFile,
|
|
29
|
+
parseCloudAiView,
|
|
30
|
+
sortCloudAiViewRows,
|
|
31
|
+
} from "../view-block";
|
|
32
|
+
import { CloudChartGroupView, CloudChartView } from "./chart-block";
|
|
33
|
+
import { CloudExplorerView } from "./explorer-block";
|
|
34
|
+
import { useAiChatActions } from "./message-actions";
|
|
35
|
+
import { aiChatMessages } from "./messages";
|
|
36
|
+
import { ReferenceText } from "./reference-text";
|
|
37
|
+
|
|
38
|
+
/** Rows a table shows inline, as capability tables do; sorting and Copy data use all of them. */
|
|
39
|
+
const VISIBLE_ROWS = 100;
|
|
40
|
+
/** Rows the viewport shows before it scrolls; it reserves their height from the first frame. */
|
|
41
|
+
const VIEWPORT_ROWS = 8;
|
|
42
|
+
/** A compact row: 12px text on a 16px line, 6px padding above and below, a 1px line. */
|
|
43
|
+
const ROW_REM = 1.8125;
|
|
44
|
+
|
|
45
|
+
/** Loads a view file with the viewer's access and validates it again: a file is untrusted whoever wrote it. */
|
|
46
|
+
async function loadView(url: string, path: string, signal: AbortSignal): Promise<CloudAiView | null> {
|
|
47
|
+
try {
|
|
48
|
+
const response = await fetch(url, { signal });
|
|
49
|
+
if (!response.ok || !isAiViewFile({ path, origin: response.headers.get("X-Cloud-Ai-File-Origin") ?? "" })) return null;
|
|
50
|
+
const blob = await response.blob();
|
|
51
|
+
if (blob.size > CLOUD_AI_VIEW_MAX_BYTES) return null;
|
|
52
|
+
return parseCloudAiView(JSON.parse(await blob.text())) ?? null;
|
|
53
|
+
} catch {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** A view's title and subtitle: the loaded file's once it is there, its summary's before. */
|
|
59
|
+
function ViewHeading(props: { id: string; title: string; subtitle?: string; actions?: JSX.Element }) {
|
|
60
|
+
return (
|
|
61
|
+
<header class="flex flex-wrap items-center justify-between gap-2">
|
|
62
|
+
<div class="min-w-0">
|
|
63
|
+
<h3 id={props.id} class="m-0 text-base font-semibold">
|
|
64
|
+
{props.title}
|
|
65
|
+
</h3>
|
|
66
|
+
<Show when={props.subtitle}>{(subtitle) => <span class="text-sm text-secondary">{subtitle()}</span>}</Show>
|
|
67
|
+
</div>
|
|
68
|
+
{props.actions}
|
|
69
|
+
</header>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
type TableSummary = Extract<CloudAiViewSummary, { view: "table" }>;
|
|
74
|
+
type ChartSummary = Extract<CloudAiViewSummary, { view: "chart" }>;
|
|
75
|
+
type GroupSummary = Extract<CloudAiViewSummary, { view: "group" }>;
|
|
76
|
+
type ExplorerSummary = Extract<CloudAiViewSummary, { view: "explorer" }>;
|
|
77
|
+
|
|
78
|
+
/** The header row, the rows up to the viewport's, and the totals row when there is one. */
|
|
79
|
+
const tableHeight = (summary: TableSummary) =>
|
|
80
|
+
`${(Math.max(1, Math.min(summary.rows, VIEWPORT_ROWS)) + 1 + Number(summary.footer)) * ROW_REM}rem`;
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* A table view: title, Copy data and a sortable table in a viewport whose height follows the row count of its
|
|
84
|
+
* `present` summary, so loading, the table and the unavailable state take the same room.
|
|
85
|
+
*/
|
|
86
|
+
function CloudTableView(props: { summary: TableSummary; input: CloudAiTableView | null | undefined }) {
|
|
87
|
+
const locale = useLocale();
|
|
88
|
+
const t = () => aiChatMessages(locale());
|
|
89
|
+
const id = `cloud-view-${createUniqueId()}`;
|
|
90
|
+
const table = createMemo(() => (props.input ? cloudAiViewTable(props.input, locale()) : null));
|
|
91
|
+
// The view's own order applies until the reader picks one.
|
|
92
|
+
const [picked, setSort] = createSignal<DataTableSort>();
|
|
93
|
+
const sort = (): DataTableSort | null => {
|
|
94
|
+
const own = props.input?.sort;
|
|
95
|
+
return picked() ?? (own ? { key: own.column, direction: own.direction } : null);
|
|
96
|
+
};
|
|
97
|
+
// cloudAiViewTable returns the view's own order; a picked one sorts again.
|
|
98
|
+
const rows = createMemo(() => {
|
|
99
|
+
const value = table();
|
|
100
|
+
const order = picked();
|
|
101
|
+
return !value ? [] : order ? sortCloudAiViewRows(value.rows, order, locale()) : value.rows;
|
|
102
|
+
});
|
|
103
|
+
const copyText = createMemo(() => {
|
|
104
|
+
const value = table();
|
|
105
|
+
return value ? cloudAiTableTsv({ ...value, rows: rows() }) : "";
|
|
106
|
+
});
|
|
107
|
+
const number = () => new Intl.NumberFormat(locale());
|
|
108
|
+
const refColumns = createMemo(() => new Set(props.input?.columns.flatMap((column) => (column.type === "ref" ? [column.id] : []))));
|
|
109
|
+
return (
|
|
110
|
+
<section class="grid min-w-0 gap-3" aria-labelledby={id}>
|
|
111
|
+
<ViewHeading
|
|
112
|
+
id={id}
|
|
113
|
+
title={props.input?.title ?? props.summary.title}
|
|
114
|
+
subtitle={props.input ? props.input.subtitle : props.summary.subtitle}
|
|
115
|
+
actions={<CopyButton text={copyText()} label={t().viewCopyData} iconOnly size="sm" variant="ghost" disabled={!table()} />}
|
|
116
|
+
/>
|
|
117
|
+
<div class="min-w-0" style={{ height: tableHeight(props.summary) }}>
|
|
118
|
+
<Show
|
|
119
|
+
when={table()}
|
|
120
|
+
fallback={
|
|
121
|
+
<div role="status" class="flex h-full items-center justify-center text-center text-xs text-dimmed">
|
|
122
|
+
{props.input === undefined ? t().viewLoading : t().viewUnavailable}
|
|
123
|
+
</div>
|
|
124
|
+
}
|
|
125
|
+
>
|
|
126
|
+
{(value) => (
|
|
127
|
+
<DataTable<CloudAiChartRow>
|
|
128
|
+
rows={rows().slice(0, VISIBLE_ROWS)}
|
|
129
|
+
columns={value().columns.map((column) => ({
|
|
130
|
+
id: column.id,
|
|
131
|
+
header: column.label,
|
|
132
|
+
// A wrapped label would make the header taller than the reserved row; the table scrolls sideways instead.
|
|
133
|
+
headerClass: "whitespace-nowrap",
|
|
134
|
+
value: (row) => row.cells[column.id] ?? "",
|
|
135
|
+
align: column.numeric ? "right" : "left",
|
|
136
|
+
sortable: true,
|
|
137
|
+
}))}
|
|
138
|
+
renderCell={({ row, col, value: text, render }) => {
|
|
139
|
+
// Row keys are row indexes, so a reference cell finds its object in the loaded view.
|
|
140
|
+
const cells = props.input?.rows[Number(row.key)];
|
|
141
|
+
const cell = refColumns().has(col.id) && cells && Object.hasOwn(cells, col.id) ? cells[col.id] : undefined;
|
|
142
|
+
return cell && typeof cell === "object" ? <ReferenceText text={String(text)} reference={cell} /> : render(text);
|
|
143
|
+
}}
|
|
144
|
+
getRowId={(row) => row.key}
|
|
145
|
+
sort={sort()}
|
|
146
|
+
onSortChange={setSort}
|
|
147
|
+
footer={
|
|
148
|
+
value().footer
|
|
149
|
+
? {
|
|
150
|
+
values: value().footer?.cells,
|
|
151
|
+
renderCell: ({ col, value }) => {
|
|
152
|
+
const text = typeof value === "string" ? value : "";
|
|
153
|
+
const cells = props.input?.footer;
|
|
154
|
+
const cell = refColumns().has(col.id) && cells && Object.hasOwn(cells, col.id) ? cells[col.id] : undefined;
|
|
155
|
+
return cell && typeof cell === "object" ? <ReferenceText text={text} reference={cell} /> : text;
|
|
156
|
+
},
|
|
157
|
+
}
|
|
158
|
+
: undefined
|
|
159
|
+
}
|
|
160
|
+
ariaLabelledBy={id}
|
|
161
|
+
density="compact"
|
|
162
|
+
surface="paper"
|
|
163
|
+
stickyHeader
|
|
164
|
+
class="h-full"
|
|
165
|
+
empty={<span>{t().tableEmpty}</span>}
|
|
166
|
+
/>
|
|
167
|
+
)}
|
|
168
|
+
</Show>
|
|
169
|
+
</div>
|
|
170
|
+
<Show when={props.summary.rows > VISIBLE_ROWS}>
|
|
171
|
+
<p class="text-sm text-secondary">
|
|
172
|
+
{t().viewRowsCapped({ shown: number().format(VISIBLE_ROWS), total: number().format(props.summary.rows) })}
|
|
173
|
+
</p>
|
|
174
|
+
</Show>
|
|
175
|
+
</section>
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
type StatsSummary = Extract<CloudAiViewSummary, { view: "stats" }>;
|
|
180
|
+
type DetailsSummary = Extract<CloudAiViewSummary, { view: "details" }>;
|
|
181
|
+
|
|
182
|
+
const DELTA_TONES = { better: "emerald", worse: "red", neutral: "zinc" } as const;
|
|
183
|
+
const DELTA_ICONS = { up: "ti ti-trending-up", down: "ti ti-trending-down", flat: "ti ti-minus" } as const;
|
|
184
|
+
/** One column per tile, up to the grid's six; its ladder narrows them on phones. */
|
|
185
|
+
const STATS_COLUMNS: readonly StatGridColumns[] = [1, 2, 3, 4, 5, 6];
|
|
186
|
+
|
|
187
|
+
/** The host picks the change's color from whether it is better; the model never chooses tones. */
|
|
188
|
+
const deltaAccent = (delta: ReturnType<typeof cloudAiStatsTiles>[number]["delta"]): StatCellAccent | undefined =>
|
|
189
|
+
delta && { tone: DELTA_TONES[delta.tone], icon: DELTA_ICONS[delta.direction], text: delta.text };
|
|
190
|
+
|
|
191
|
+
/** The loading or unavailable message, centered over the room the view reserved. */
|
|
192
|
+
function ViewStatus(props: { loading: boolean }) {
|
|
193
|
+
const locale = useLocale();
|
|
194
|
+
const t = () => aiChatMessages(locale());
|
|
195
|
+
return (
|
|
196
|
+
<div role="status" class="flex h-full items-center justify-center text-center text-xs text-dimmed">
|
|
197
|
+
{props.loading ? t().viewLoading : t().viewUnavailable}
|
|
198
|
+
</div>
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Key figures as stat tiles. The grid's column ladder decides the layout; every tile's height follows from whether it
|
|
204
|
+
* has a sparkline and a support line, which the summary records, so a hidden copy of the same grid reserves the exact
|
|
205
|
+
* room while the file loads or when it is unavailable.
|
|
206
|
+
*/
|
|
207
|
+
function CloudStatsView(props: { summary: StatsSummary; input: CloudAiStatsView | null | undefined }) {
|
|
208
|
+
const locale = useLocale();
|
|
209
|
+
const id = `cloud-view-${createUniqueId()}`;
|
|
210
|
+
const tiles = createMemo(() => (props.input ? cloudAiStatsTiles(props.input, locale()) : null));
|
|
211
|
+
const columns = () => STATS_COLUMNS[props.summary.items.length - 1];
|
|
212
|
+
return (
|
|
213
|
+
<section class="grid min-w-0 gap-3" aria-labelledby={id}>
|
|
214
|
+
<ViewHeading
|
|
215
|
+
id={id}
|
|
216
|
+
title={props.input?.title ?? props.summary.title}
|
|
217
|
+
subtitle={props.input ? props.input.subtitle : props.summary.subtitle}
|
|
218
|
+
/>
|
|
219
|
+
<Show
|
|
220
|
+
when={tiles()}
|
|
221
|
+
fallback={
|
|
222
|
+
<div class="relative min-w-0">
|
|
223
|
+
<div class="invisible" aria-hidden="true">
|
|
224
|
+
<StatGrid columns={columns()}>
|
|
225
|
+
<Index each={props.summary.items}>
|
|
226
|
+
{(item) => (
|
|
227
|
+
<StatCell
|
|
228
|
+
label={item().label}
|
|
229
|
+
value="0"
|
|
230
|
+
trend={item().sparkline ? [0, 1] : undefined}
|
|
231
|
+
sub={item().sub ? "\u00a0" : undefined}
|
|
232
|
+
accent={item().delta ? { tone: "zinc", icon: DELTA_ICONS.flat, text: "0" } : undefined}
|
|
233
|
+
/>
|
|
234
|
+
)}
|
|
235
|
+
</Index>
|
|
236
|
+
</StatGrid>
|
|
237
|
+
</div>
|
|
238
|
+
<div class="absolute inset-0">
|
|
239
|
+
<ViewStatus loading={props.input === undefined} />
|
|
240
|
+
</div>
|
|
241
|
+
</div>
|
|
242
|
+
}
|
|
243
|
+
>
|
|
244
|
+
{(value) => (
|
|
245
|
+
<StatGrid columns={columns()}>
|
|
246
|
+
<Index each={value()}>
|
|
247
|
+
{(tile) => (
|
|
248
|
+
<StatCell
|
|
249
|
+
label={tile().label}
|
|
250
|
+
value={tile().value}
|
|
251
|
+
title={tile().value}
|
|
252
|
+
trend={tile().sparkline}
|
|
253
|
+
sub={tile().sub}
|
|
254
|
+
accent={deltaAccent(tile().delta)}
|
|
255
|
+
/>
|
|
256
|
+
)}
|
|
257
|
+
</Index>
|
|
258
|
+
</StatGrid>
|
|
259
|
+
)}
|
|
260
|
+
</Show>
|
|
261
|
+
</section>
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** The height of the twelve one-line rows the viewport shows at most before it scrolls. */
|
|
266
|
+
const DETAILS_MAX_REM = CLOUD_AI_DETAILS_VIEWPORT_ITEMS * 2.25;
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Stand-in text of a value's length. Words of four digits wrap about as early as real words, and a digit is a little
|
|
270
|
+
* wider than an average letter, so the stand-in usually takes the lines of the value it stands for or one more: a row
|
|
271
|
+
* may keep a little room below it rather than hide its last line behind a scroll. Five-digit words reserved up to three
|
|
272
|
+
* extra lines in fonts whose digits are much wider than their letters, as WebKit's fallback font.
|
|
273
|
+
*/
|
|
274
|
+
const placeholder = (chars: number) =>
|
|
275
|
+
"0000 "
|
|
276
|
+
.repeat(Math.ceil(chars / 5))
|
|
277
|
+
.slice(0, chars)
|
|
278
|
+
.trimEnd();
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* One record as label and value rows. A hidden copy of the first twelve fields, with each label and a stand-in of each
|
|
282
|
+
* value's length from the summary, sizes the viewport, up to twelve one-line rows, so a value that wraps on a phone
|
|
283
|
+
* keeps its room in the first frame. More fields, or a value longer than its stand-in, scroll inside the viewport
|
|
284
|
+
* instead of moving the chat.
|
|
285
|
+
*/
|
|
286
|
+
function CloudDetailsView(props: { summary: DetailsSummary; input: CloudAiDetailsView | null | undefined }) {
|
|
287
|
+
const locale = useLocale();
|
|
288
|
+
const id = `cloud-view-${createUniqueId()}`;
|
|
289
|
+
const rows = createMemo(() => (props.input ? cloudAiDetailsRows(props.input, locale()) : null));
|
|
290
|
+
return (
|
|
291
|
+
<section class="grid min-w-0 gap-3" aria-labelledby={id}>
|
|
292
|
+
<ViewHeading
|
|
293
|
+
id={id}
|
|
294
|
+
title={props.input?.title ?? props.summary.title}
|
|
295
|
+
subtitle={props.input ? props.input.subtitle : props.summary.subtitle}
|
|
296
|
+
/>
|
|
297
|
+
<div class="relative min-w-0 overflow-hidden" style={{ "max-height": `${DETAILS_MAX_REM}rem` }}>
|
|
298
|
+
<div class="invisible" aria-hidden="true">
|
|
299
|
+
<DescriptionList
|
|
300
|
+
layout="rows"
|
|
301
|
+
items={props.summary.fields.map((field) => ({
|
|
302
|
+
term: field.label,
|
|
303
|
+
description: !field.chars ? "—" : field.ref ? <ReferenceText text={placeholder(field.chars)} /> : placeholder(field.chars),
|
|
304
|
+
}))}
|
|
305
|
+
/>
|
|
306
|
+
</div>
|
|
307
|
+
<div class="absolute inset-0 overflow-y-auto">
|
|
308
|
+
<Show when={rows()} fallback={<ViewStatus loading={props.input === undefined} />}>
|
|
309
|
+
{(value) => (
|
|
310
|
+
<DescriptionList
|
|
311
|
+
layout="rows"
|
|
312
|
+
items={value().map((row) => ({
|
|
313
|
+
term: row.label,
|
|
314
|
+
description: row.ref ? (
|
|
315
|
+
<ReferenceText text={row.text} reference={row.ref} />
|
|
316
|
+
) : (
|
|
317
|
+
row.text || <span class="text-dimmed">—</span>
|
|
318
|
+
),
|
|
319
|
+
}))}
|
|
320
|
+
/>
|
|
321
|
+
)}
|
|
322
|
+
</Show>
|
|
323
|
+
</div>
|
|
324
|
+
</div>
|
|
325
|
+
</section>
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
type Heading = { title: string; subtitle?: string };
|
|
330
|
+
const sameHeading = (a: Heading, b: Heading | undefined) => a.title === b?.title && a.subtitle === b.subtitle;
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Whether a loaded file still has everything its summary reserved: kinds, headings, layout and the linked cursor. A
|
|
334
|
+
* changed file shows unavailable instead of moving views or linking charts its summary did not.
|
|
335
|
+
*/
|
|
336
|
+
const matches = (view: CloudAiView, summary: CloudAiViewSummary) => {
|
|
337
|
+
if (view.view === "table") return summary.view === "table" && sameHeading(view, summary);
|
|
338
|
+
const loaded = cloudAiViewSummary(view);
|
|
339
|
+
// A record must keep the field count and the labels and value lengths that sized its viewport.
|
|
340
|
+
if (loaded.view === "details")
|
|
341
|
+
return (
|
|
342
|
+
summary.view === "details" &&
|
|
343
|
+
sameHeading(loaded, summary) &&
|
|
344
|
+
loaded.items === summary.items &&
|
|
345
|
+
JSON.stringify(loaded.fields) === JSON.stringify(summary.fields)
|
|
346
|
+
);
|
|
347
|
+
// A stats file must keep every tile's label and parts, which set the tile heights its summary reserved.
|
|
348
|
+
if (loaded.view === "stats")
|
|
349
|
+
return summary.view === "stats" && sameHeading(loaded, summary) && JSON.stringify(loaded.items) === JSON.stringify(summary.items);
|
|
350
|
+
if (loaded.view === "explorer")
|
|
351
|
+
return (
|
|
352
|
+
summary.view === "explorer" &&
|
|
353
|
+
sameHeading(loaded, summary) &&
|
|
354
|
+
loaded.step === summary.step &&
|
|
355
|
+
loaded.filter === summary.filter &&
|
|
356
|
+
loaded.compare === summary.compare &&
|
|
357
|
+
JSON.stringify(loaded.views) === JSON.stringify(summary.views)
|
|
358
|
+
);
|
|
359
|
+
if (loaded.view === "chart") return summary.view === "chart" && loaded.kind === summary.kind && sameHeading(loaded, summary);
|
|
360
|
+
return (
|
|
361
|
+
loaded.view === "group" &&
|
|
362
|
+
summary.view === "group" &&
|
|
363
|
+
sameHeading(loaded, summary) &&
|
|
364
|
+
loaded.linkX === summary.linkX &&
|
|
365
|
+
loaded.layout === summary.layout &&
|
|
366
|
+
loaded.charts.length === summary.charts.length &&
|
|
367
|
+
loaded.charts.every((chart, index) => chart.kind === summary.charts[index]?.kind && sameHeading(chart, summary.charts[index]))
|
|
368
|
+
);
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* A presented view file. It takes its full size from the `present` summary in the first frame, then loads the file
|
|
373
|
+
* with the viewer's access; a missing file or one that no longer matches its summary shows the unavailable state in
|
|
374
|
+
* the same frame.
|
|
375
|
+
*/
|
|
376
|
+
export function CloudViewBlock(props: { path: string; summary: CloudAiViewSummary }) {
|
|
377
|
+
const actions = useAiChatActions();
|
|
378
|
+
// undefined while loading, null when unavailable.
|
|
379
|
+
const [view, setView] = createSignal<CloudAiView | null | undefined>(undefined);
|
|
380
|
+
onMount(() => {
|
|
381
|
+
const url = actions.fileUrl?.(props.path);
|
|
382
|
+
if (!url) return setView(null);
|
|
383
|
+
const abort = new AbortController();
|
|
384
|
+
onCleanup(() => abort.abort());
|
|
385
|
+
void loadView(url, props.path, abort.signal).then((loaded) => {
|
|
386
|
+
if (!abort.signal.aborted) setView(loaded && matches(loaded, props.summary) ? loaded : null);
|
|
387
|
+
});
|
|
388
|
+
});
|
|
389
|
+
const chartSummary = (): ChartSummary | undefined => (props.summary.view === "chart" ? props.summary : undefined);
|
|
390
|
+
const tableSummary = (): TableSummary | undefined => (props.summary.view === "table" ? props.summary : undefined);
|
|
391
|
+
const groupSummary = (): GroupSummary | undefined => (props.summary.view === "group" ? props.summary : undefined);
|
|
392
|
+
const statsSummary = (): StatsSummary | undefined => (props.summary.view === "stats" ? props.summary : undefined);
|
|
393
|
+
const detailsSummary = (): DetailsSummary | undefined => (props.summary.view === "details" ? props.summary : undefined);
|
|
394
|
+
const explorerSummary = (): ExplorerSummary | undefined => (props.summary.view === "explorer" ? props.summary : undefined);
|
|
395
|
+
function loaded(kind: "chart"): Extract<CloudAiView, { view: "chart" }> | null | undefined;
|
|
396
|
+
function loaded(kind: "table"): CloudAiTableView | null | undefined;
|
|
397
|
+
function loaded(kind: "group"): Extract<CloudAiView, { view: "group" }> | null | undefined;
|
|
398
|
+
function loaded(kind: "stats"): CloudAiStatsView | null | undefined;
|
|
399
|
+
function loaded(kind: "details"): CloudAiDetailsView | null | undefined;
|
|
400
|
+
function loaded(kind: "explorer"): CloudAiExplorerView | null | undefined;
|
|
401
|
+
function loaded(kind: CloudAiView["view"]) {
|
|
402
|
+
const value = view();
|
|
403
|
+
return value && value.view !== kind ? null : value;
|
|
404
|
+
}
|
|
405
|
+
return (
|
|
406
|
+
<div data-view-path={props.path} class="min-w-0">
|
|
407
|
+
<Switch>
|
|
408
|
+
<Match when={chartSummary()}>{(summary) => <CloudChartView summary={summary()} input={loaded("chart")} />}</Match>
|
|
409
|
+
<Match when={tableSummary()}>{(summary) => <CloudTableView summary={summary()} input={loaded("table")} />}</Match>
|
|
410
|
+
<Match when={groupSummary()}>{(summary) => <CloudChartGroupView summary={summary()} group={loaded("group")} />}</Match>
|
|
411
|
+
<Match when={statsSummary()}>{(summary) => <CloudStatsView summary={summary()} input={loaded("stats")} />}</Match>
|
|
412
|
+
<Match when={detailsSummary()}>{(summary) => <CloudDetailsView summary={summary()} input={loaded("details")} />}</Match>
|
|
413
|
+
<Match when={explorerSummary()}>{(summary) => <CloudExplorerView summary={summary()} input={loaded("explorer")} />}</Match>
|
|
414
|
+
</Switch>
|
|
415
|
+
</div>
|
|
416
|
+
);
|
|
417
|
+
}
|
|
@@ -1,77 +1,56 @@
|
|
|
1
1
|
import { mutation } from "@k2b/stdlib/solid";
|
|
2
|
-
import { Button, MarkdownEditor, prompts, Slider, useLocale } from "@k2b/ui";
|
|
2
|
+
import { Button, MarkdownEditor, prompts, Slider, StatCell, type StatCellAccent, StatGrid, useLocale } from "@k2b/ui";
|
|
3
3
|
import { createSignal, createUniqueId, For, Show } from "solid-js";
|
|
4
4
|
import { CLOUD_AI_TEXT_EDITOR_FEEDBACK_MAX_CHARS, CLOUD_AI_TEXT_EDITOR_MAX_CHARS } from "../default-tool-contracts";
|
|
5
5
|
import { isRecord, jsonPreview } from "./message-utils";
|
|
6
6
|
import { aiChatMessages } from "./messages";
|
|
7
7
|
import { useAiToolDisclosure } from "./tool-disclosure";
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
return "border-amber-200 bg-amber-50/70 text-amber-950 dark:border-amber-900/70 dark:bg-amber-950/25 dark:text-amber-100";
|
|
15
|
-
if (tone === "red") return "border-red-200 bg-red-50/70 text-red-950 dark:border-red-900/70 dark:bg-red-950/25 dark:text-red-100";
|
|
16
|
-
if (tone === "teal") return "border-cyan-200 bg-teal-50/70 text-cyan-950 dark:border-cyan-900/70 dark:bg-cyan-950/25 dark:text-cyan-100";
|
|
17
|
-
return "border-zinc-200 bg-white text-primary dark:border-zinc-800 dark:bg-zinc-900";
|
|
18
|
-
};
|
|
19
|
-
|
|
9
|
+
/**
|
|
10
|
+
* One `card` tool call as a single stat tile, the same look as key figures in views. Its arguments stay as they
|
|
11
|
+
* were; the card has no better or worse direction, so a trend stays neutral and `tone` no longer colors anything.
|
|
12
|
+
* Nothing compares the tile with a neighbor, so its text wraps: older cards with a long value or caption stay whole.
|
|
13
|
+
*/
|
|
20
14
|
export function CloudCardBlock(props: { args: unknown }) {
|
|
21
15
|
const card = () => (isRecord(props.args) ? props.args : null);
|
|
22
16
|
const emoji = () => (typeof card()?.emoji === "string" ? String(card()?.emoji).trim() : "");
|
|
23
17
|
const title = () => String(card()?.title ?? "Card");
|
|
24
18
|
const value = () => String(card()?.value ?? "");
|
|
25
19
|
const caption = () => (typeof card()?.caption === "string" ? String(card()!.caption) : "");
|
|
26
|
-
const legacyTrend = () =>
|
|
20
|
+
const legacyTrend = () => {
|
|
21
|
+
const trend = card()?.trend;
|
|
22
|
+
return isRecord(trend) ? trend : null;
|
|
23
|
+
};
|
|
27
24
|
const trendValue = () => (typeof card()?.trendValue === "string" ? String(card()!.trendValue) : String(legacyTrend()?.value ?? ""));
|
|
28
25
|
const trendLabel = () => (typeof card()?.trendLabel === "string" ? String(card()!.trendLabel) : String(legacyTrend()?.label ?? ""));
|
|
29
26
|
const trendDirection = () => {
|
|
30
27
|
const direction = card()?.trendDirection ?? legacyTrend()?.direction;
|
|
31
28
|
return direction === "up" || direction === "down" || direction === "flat" ? direction : "flat";
|
|
32
29
|
};
|
|
33
|
-
const
|
|
30
|
+
const accent = (): StatCellAccent | undefined => {
|
|
31
|
+
const text = [trendValue(), trendLabel()].filter(Boolean).join(" ");
|
|
32
|
+
if (!text) return undefined;
|
|
33
|
+
const direction = trendDirection();
|
|
34
|
+
return {
|
|
35
|
+
tone: "zinc",
|
|
36
|
+
icon: direction === "up" ? "ti ti-trending-up" : direction === "down" ? "ti ti-trending-down" : "ti ti-minus",
|
|
37
|
+
text,
|
|
38
|
+
};
|
|
39
|
+
};
|
|
34
40
|
|
|
35
41
|
return (
|
|
36
|
-
<
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
>
|
|
45
|
-
<
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
{emoji()}
|
|
49
|
-
</span>
|
|
50
|
-
</Show>
|
|
51
|
-
<div class="min-w-0 flex-1">
|
|
52
|
-
<p class="text-sm font-semibold">{title()}</p>
|
|
53
|
-
<p class="mt-2 text-3xl font-semibold tracking-normal">{value()}</p>
|
|
54
|
-
<Show when={hasTrend()}>
|
|
55
|
-
<p class="mt-1 inline-flex items-center gap-1 rounded-md bg-white/55 px-1.5 py-0.5 text-xs dark:bg-white/10">
|
|
56
|
-
<i
|
|
57
|
-
class={`ti ${
|
|
58
|
-
trendDirection() === "up" ? "ti-trending-up" : trendDirection() === "down" ? "ti-trending-down" : "ti-minus"
|
|
59
|
-
} text-sm`}
|
|
60
|
-
aria-hidden="true"
|
|
61
|
-
/>
|
|
62
|
-
<Show when={trendValue()}>{trendValue()}</Show>
|
|
63
|
-
<Show when={trendLabel()}>
|
|
64
|
-
<span class="opacity-70">{trendLabel()}</span>
|
|
65
|
-
</Show>
|
|
66
|
-
</p>
|
|
67
|
-
</Show>
|
|
68
|
-
<Show when={caption()}>
|
|
69
|
-
<p class="mt-2 text-xs opacity-70">{caption()}</p>
|
|
70
|
-
</Show>
|
|
71
|
-
</div>
|
|
72
|
-
</div>
|
|
73
|
-
</Show>
|
|
74
|
-
</div>
|
|
42
|
+
<Show
|
|
43
|
+
when={card()}
|
|
44
|
+
fallback={
|
|
45
|
+
<pre class="max-h-52 overflow-auto rounded-md bg-zinc-950/5 p-2 text-xs text-primary dark:bg-white/5">
|
|
46
|
+
{jsonPreview(props.args)}
|
|
47
|
+
</pre>
|
|
48
|
+
}
|
|
49
|
+
>
|
|
50
|
+
<StatGrid columns={1} class="max-w-sm">
|
|
51
|
+
<StatCell label={emoji() ? `${emoji()} ${title()}` : title()} value={value()} sub={caption() || undefined} accent={accent()} wrap />
|
|
52
|
+
</StatGrid>
|
|
53
|
+
</Show>
|
|
75
54
|
);
|
|
76
55
|
}
|
|
77
56
|
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { z } from "zod";
|
|
2
|
+
import { createCloudAiLocalBashTool, createCloudAiLocalTools } from "./default-tools";
|
|
3
|
+
|
|
4
|
+
// Two 512 KiB Bash streams, fully JSON-escaped at six bytes per character, plus a 64 KiB envelope.
|
|
5
|
+
export const AI_CLIENT_TOOL_ACTION_MAX_BYTES = 2 * 512 * 1024 * 6 + 64 * 1024;
|
|
6
|
+
|
|
7
|
+
export const aiClientToolOutputSchemas = new Map<string, z.ZodType>(
|
|
8
|
+
[createCloudAiLocalBashTool(), ...createCloudAiLocalTools()].map((tool) => [tool.def.name, tool.def.outputSchema]),
|
|
9
|
+
);
|
|
@@ -283,9 +283,24 @@ export const CODE_SOURCE_TOOLS = {
|
|
|
283
283
|
input: Id.extend(ArtifactMetadata.shape),
|
|
284
284
|
},
|
|
285
285
|
code_create: {
|
|
286
|
+
promptHint:
|
|
287
|
+
"build every app, calculator, converter, timer, form or other tool the person asks for as a saved Studio app. In one turn, load_skill assistant-code-mode and load_tools code_create, code_write, code_check and code_open.",
|
|
286
288
|
description:
|
|
287
|
-
"Create a
|
|
288
|
-
input: ArtifactCreate.pick({ title: true, description: true, icon: true })
|
|
289
|
+
"Create a saved Studio app; it appears in the person's Studio Apps and can be opened, reused and shared. Use it for every app, calculator, converter, timer, form or other interactive tool the person asks for, even when it keeps no data. Pass the complete first version as html (a small app is this one file) and steps (its steps.json), so one call creates it; add style.css, app.js or more files with code_write. Without html it starts with a minimal index.html. Returns id, revision and the lines and length of every file; with html or steps also diagnostics, like code_write. Optional icon uses the complete class, e.g. ti ti-calculator; omit it when unsure. For a one-off analysis use code_run instead. Does not run, check or share anything.",
|
|
290
|
+
input: ArtifactCreate.pick({ title: true, description: true, icon: true }).extend({
|
|
291
|
+
html: z
|
|
292
|
+
.string()
|
|
293
|
+
.max(1024 * 1024)
|
|
294
|
+
.optional()
|
|
295
|
+
.describe(
|
|
296
|
+
'Complete index.html. A small app fits in this one file: put its script in <script type="module"> and its CSS in <style>. Omit it to start with a minimal page.',
|
|
297
|
+
),
|
|
298
|
+
steps: z
|
|
299
|
+
.string()
|
|
300
|
+
.max(1024 * 1024)
|
|
301
|
+
.optional()
|
|
302
|
+
.describe("Complete steps.json text for code_check: the main flow, at most 20 steps."),
|
|
303
|
+
}),
|
|
289
304
|
},
|
|
290
305
|
// Source text stays top-level because some providers corrupt strings nested in arrays.
|
|
291
306
|
// Imports carry only references, so they keep an array and one review per batch.
|
|
@@ -50,6 +50,7 @@ export function createCodeSourceTool(name: CodeSourceToolName) {
|
|
|
50
50
|
inputSchema: definition.input,
|
|
51
51
|
outputSchema: z.unknown(),
|
|
52
52
|
approval: "never",
|
|
53
|
+
promptHint: "promptHint" in definition ? definition.promptHint : undefined,
|
|
53
54
|
}).server(async (input, context) => {
|
|
54
55
|
if (!context.conversationId) throw new Error("Code tools require an Assistant conversation.");
|
|
55
56
|
const { actor } = await resolveAiCapabilityActor({
|