@k2b/cloud 0.31.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 +6 -4
- package/src/_internal/define-app.ts +3 -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/default-tool-contracts.ts +334 -91
- package/src/ai/default-tools.ts +87 -11
- 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/migrate.ts +7 -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 +16 -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/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/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/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/ssr/AppBadge.tsx +4 -4
- package/src/ssr/AppLaunchpadPanel.tsx +4 -4
- package/src/ssr/RailApps.island.tsx +2 -2
- package/src/ssr/app-badges.ts +50 -6
- package/src/ssr/page-trail.browser-harness.tsx +52 -3
- 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/global.css +5 -2
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ChartExplorer,
|
|
3
|
+
type ChartExplorerColumn,
|
|
4
|
+
type ChartExplorerData,
|
|
5
|
+
type ChartExplorerRequest,
|
|
6
|
+
ChartFilterControls,
|
|
7
|
+
type ChartSnapshot,
|
|
8
|
+
createChartExplorer,
|
|
9
|
+
prepareChartSnapshot,
|
|
10
|
+
useLocale,
|
|
11
|
+
} from "@k2b/ui";
|
|
12
|
+
import { createMemo, createSignal, type JSX, Show } from "solid-js";
|
|
13
|
+
import { type CloudAiChartRow, cloudAiChartMarkKey, cloudAiChartReferenceMark, cloudAiChartRenderOptions } from "../chart-block";
|
|
14
|
+
import {
|
|
15
|
+
type CloudAiExplorerRequest,
|
|
16
|
+
type CloudAiExplorerView,
|
|
17
|
+
type CloudAiViewSummary,
|
|
18
|
+
cloudAiExplorerControls,
|
|
19
|
+
cloudAiExplorerState,
|
|
20
|
+
cloudAiViewCellOf,
|
|
21
|
+
cloudAiViewTable,
|
|
22
|
+
} from "../view-block";
|
|
23
|
+
import { aiChatMessages } from "./messages";
|
|
24
|
+
import { ReferenceText } from "./reference-text";
|
|
25
|
+
|
|
26
|
+
type ExplorerSummary = Extract<CloudAiViewSummary, { view: "explorer" }>;
|
|
27
|
+
type Charts = Record<string, ChartExplorerData<CloudAiChartRow>>;
|
|
28
|
+
|
|
29
|
+
/** Rows the table renders inline, as table views do; sorting and Copy data use all of them. */
|
|
30
|
+
const VISIBLE_ROWS = 100;
|
|
31
|
+
/** Every explorer chart kind draws a plot with axes, so all share the chart block's viewport. */
|
|
32
|
+
const HEIGHT = "18rem";
|
|
33
|
+
/** createChartExplorer holds the shown rows as its only entry; views are drawn from them on demand. */
|
|
34
|
+
const TABLE = "table";
|
|
35
|
+
|
|
36
|
+
const emptySnapshot = (kind: ChartSnapshot["kind"]): ChartSnapshot => ({
|
|
37
|
+
kind,
|
|
38
|
+
svg: "",
|
|
39
|
+
width: 480,
|
|
40
|
+
height: 280,
|
|
41
|
+
stretch: true,
|
|
42
|
+
marks: [],
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
/** One state of the explorer: the rows its request shows, as the table, and every view projected from them. */
|
|
46
|
+
function explorerState(view: CloudAiExplorerView, table: CloudAiChartRow[], request: CloudAiExplorerRequest, locale: string) {
|
|
47
|
+
const state = cloudAiExplorerState(view, request, locale);
|
|
48
|
+
const rows = state.rows.map((index) => table[index]!);
|
|
49
|
+
const charts: Charts = { [TABLE]: { chart: emptySnapshot("bar"), rows } };
|
|
50
|
+
return { snapshot: { request: { ...request }, charts }, rows, charts: state.charts };
|
|
51
|
+
}
|
|
52
|
+
type ExplorerState = ReturnType<typeof explorerState>;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* One view of a state, drawn. Drawing is the expensive part, so only a view someone shows is drawn. A view that
|
|
56
|
+
* draws no mark of the shown rows returns undefined, so its frame says why instead of showing an empty plot.
|
|
57
|
+
*/
|
|
58
|
+
function drawExplorerView(
|
|
59
|
+
view: CloudAiExplorerView,
|
|
60
|
+
spec: CloudAiExplorerView["views"][number],
|
|
61
|
+
table: CloudAiChartRow[],
|
|
62
|
+
state: ExplorerState,
|
|
63
|
+
locale: string,
|
|
64
|
+
): ChartExplorerData<CloudAiChartRow> | undefined {
|
|
65
|
+
const chart = state.charts[spec.id];
|
|
66
|
+
if (!chart) return undefined;
|
|
67
|
+
const columnLabel = (id: string) => view.columns.find((column) => column.id === id)?.label ?? id;
|
|
68
|
+
const rowOf = (datum: { seriesIndex?: number; index: number }) => chart.marks[datum.seriesIndex ?? 0]?.[datum.index];
|
|
69
|
+
// The tooltip names what this view encodes; the table and Copy data carry every column.
|
|
70
|
+
const encoded = (seriesIndex: number) => {
|
|
71
|
+
const ys = Array.isArray(spec.y) ? spec.y : [spec.y];
|
|
72
|
+
// Several y columns are one series each; with a series column every series shows the single y.
|
|
73
|
+
const y = spec.kind === "line" || spec.kind === "scatter" ? ys[ys.length > 1 ? seriesIndex % 8 : 0] : spec.y;
|
|
74
|
+
return [
|
|
75
|
+
spec.x,
|
|
76
|
+
...(y ? [y] : []),
|
|
77
|
+
...(spec.kind === "heatmap" ? [spec.value] : []),
|
|
78
|
+
...(spec.kind !== "heatmap" && spec.kind !== "bar" && spec.series ? [spec.series] : []),
|
|
79
|
+
...(view.controls?.step ? [view.controls.step] : []),
|
|
80
|
+
];
|
|
81
|
+
};
|
|
82
|
+
try {
|
|
83
|
+
const snapshot = prepareChartSnapshot(cloudAiChartRenderOptions(chart.input, locale, { legend: "outside" }), {
|
|
84
|
+
key: ({ datum }) => cloudAiChartMarkKey(datum),
|
|
85
|
+
rowKey: ({ datum }) => String(rowOf(datum)),
|
|
86
|
+
tooltip: ({ datum }) => {
|
|
87
|
+
const row = table[rowOf(datum) ?? -1];
|
|
88
|
+
const series =
|
|
89
|
+
chart.input.kind === "line" || chart.input.kind === "scatter" ? chart.input.series[datum.seriesIndex ?? 0] : undefined;
|
|
90
|
+
return {
|
|
91
|
+
title: series?.label ?? (row ? row.cells[spec.x] : undefined),
|
|
92
|
+
rows: row
|
|
93
|
+
? [...new Set(encoded(datum.seriesIndex ?? 0))].map((id) => ({ label: columnLabel(id), value: row.cells[id] ?? "" }))
|
|
94
|
+
: [],
|
|
95
|
+
};
|
|
96
|
+
},
|
|
97
|
+
reference: ({ datum }) => cloudAiChartReferenceMark(chart.input, datum),
|
|
98
|
+
});
|
|
99
|
+
// createChartExplorer rejects a mark without a row; such a view shows why instead of failing the block.
|
|
100
|
+
const shown = new Set(state.rows.map((row) => row.key));
|
|
101
|
+
if (!snapshot.marks.length || !snapshot.marks.every((mark) => shown.has(mark.rowKey))) return undefined;
|
|
102
|
+
return { chart: snapshot, rows: state.rows };
|
|
103
|
+
} catch {
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const hasControls = (summary: ExplorerSummary) => summary.step || summary.filter;
|
|
109
|
+
|
|
110
|
+
/** The frame of an explorer whose file is loading or unavailable, at the size the loaded explorer takes. */
|
|
111
|
+
function ExplorerPlaceholder(props: { summary: ExplorerSummary; message: JSX.Element }) {
|
|
112
|
+
const locale = useLocale();
|
|
113
|
+
return (
|
|
114
|
+
<ChartExplorer
|
|
115
|
+
title={props.summary.title}
|
|
116
|
+
description={props.summary.subtitle ? <span class="text-sm text-secondary">{props.summary.subtitle}</span> : undefined}
|
|
117
|
+
height={HEIGHT}
|
|
118
|
+
data={{ chart: emptySnapshot(props.summary.views[0]?.kind ?? "bar"), rows: [] }}
|
|
119
|
+
columns={[{ id: "value", label: aiChatMessages(locale()).chartUnavailable, value: () => "" }]}
|
|
120
|
+
charts={props.summary.views}
|
|
121
|
+
chart={props.summary.views[0]?.id}
|
|
122
|
+
controls={
|
|
123
|
+
hasControls(props.summary) ? (
|
|
124
|
+
<ChartFilterControls
|
|
125
|
+
request={{ step: props.summary.step ? "0" : undefined }}
|
|
126
|
+
displayedRequest={{}}
|
|
127
|
+
steps={props.summary.step ? [{ key: "0", label: "" }] : undefined}
|
|
128
|
+
series={props.summary.filter ? [{ key: "0", label: "", color: "var(--stdlib-chart-c1)" }] : undefined}
|
|
129
|
+
compare={props.summary.compare}
|
|
130
|
+
dimensionLabel={"\u00a0"}
|
|
131
|
+
seriesLabel={"\u00a0"}
|
|
132
|
+
disabled
|
|
133
|
+
onRequest={() => {}}
|
|
134
|
+
/>
|
|
135
|
+
) : undefined
|
|
136
|
+
}
|
|
137
|
+
empty={props.message}
|
|
138
|
+
renderDetails={false}
|
|
139
|
+
/>
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* The loaded explorer. Every filter, step and comparison is computed here from the file's rows; the selection is
|
|
145
|
+
* shared by every view and the table, and all of it is local to this block.
|
|
146
|
+
*/
|
|
147
|
+
function LoadedExplorer(props: { summary: ExplorerSummary; input: CloudAiExplorerView }) {
|
|
148
|
+
const locale = useLocale();
|
|
149
|
+
const t = () => aiChatMessages(locale());
|
|
150
|
+
const table = createMemo(() => cloudAiViewTable({ ...props.input, view: "table" }, locale()).rows);
|
|
151
|
+
const controls = createMemo(() => cloudAiExplorerControls(props.input, locale()));
|
|
152
|
+
// createChartExplorer owns the request and the selection over the shown rows; each view is drawn when shown, once
|
|
153
|
+
// per state, so a filter or step change draws one view instead of all of them.
|
|
154
|
+
const states = new WeakMap<Charts, { state: ExplorerState; drawn: Map<string, ChartExplorerData<CloudAiChartRow> | undefined> }>();
|
|
155
|
+
const build = (request: ChartExplorerRequest) => {
|
|
156
|
+
const next = explorerState(props.input, table(), request, locale());
|
|
157
|
+
states.set(next.snapshot.charts, { state: next, drawn: new Map() });
|
|
158
|
+
return next.snapshot;
|
|
159
|
+
};
|
|
160
|
+
const explorer = createChartExplorer<Charts>({ snapshot: () => build(controls().initial), load: build });
|
|
161
|
+
const [chart, setChart] = createSignal(props.input.views[0]!.id);
|
|
162
|
+
const [view, setView] = createSignal<"chart" | "table">("chart");
|
|
163
|
+
const spec = () => props.input.views.find((entry) => entry.id === chart()) ?? props.input.views[0]!;
|
|
164
|
+
const shown = createMemo(() => {
|
|
165
|
+
const charts = explorer.snapshot().charts;
|
|
166
|
+
const entry = states.get(charts)!;
|
|
167
|
+
if (view() === "table") return { data: charts[TABLE]!, failed: false };
|
|
168
|
+
const id = spec().id;
|
|
169
|
+
if (!entry.drawn.has(id)) entry.drawn.set(id, drawExplorerView(props.input, spec(), table(), entry.state, locale()));
|
|
170
|
+
const data = entry.drawn.get(id);
|
|
171
|
+
return data ? { data, failed: false } : { data: { chart: emptySnapshot(spec().kind), rows: [] }, failed: true };
|
|
172
|
+
});
|
|
173
|
+
const legend = () =>
|
|
174
|
+
view() === "chart" && !shown().failed ? states.get(explorer.snapshot().charts)?.state.charts[spec().id]?.legend : undefined;
|
|
175
|
+
// Swatches name chart colors only where the shown view colors its series by the filter column.
|
|
176
|
+
const colored = () => {
|
|
177
|
+
const current = spec();
|
|
178
|
+
return view() === "chart" && (current.kind === "line" || current.kind === "scatter") && current.series === props.input.controls?.filter;
|
|
179
|
+
};
|
|
180
|
+
const columns = createMemo(() =>
|
|
181
|
+
props.input.columns.map(
|
|
182
|
+
(column): ChartExplorerColumn<CloudAiChartRow> => ({
|
|
183
|
+
id: column.id,
|
|
184
|
+
label: column.label ?? column.id,
|
|
185
|
+
value: (row) => row.cells[column.id] ?? "",
|
|
186
|
+
sortValue: (row) => (Object.hasOwn(row.values, column.id) ? (row.values[column.id] ?? null) : null),
|
|
187
|
+
align: column.type === "number" ? "right" : "left",
|
|
188
|
+
...(column.type === "ref"
|
|
189
|
+
? {
|
|
190
|
+
render: (row: CloudAiChartRow) => {
|
|
191
|
+
const cells = props.input.rows[Number(row.key)];
|
|
192
|
+
const cell = cells ? cloudAiViewCellOf(cells, column.id) : null;
|
|
193
|
+
const text = row.cells[column.id] ?? "";
|
|
194
|
+
return cell && typeof cell === "object" ? <ReferenceText text={text} reference={cell} /> : text;
|
|
195
|
+
},
|
|
196
|
+
}
|
|
197
|
+
: {}),
|
|
198
|
+
}),
|
|
199
|
+
),
|
|
200
|
+
);
|
|
201
|
+
const columnLabel = (id: string | undefined) =>
|
|
202
|
+
id === undefined ? undefined : (props.input.columns.find((column) => column.id === id)?.label ?? id);
|
|
203
|
+
return (
|
|
204
|
+
<ChartExplorer<CloudAiChartRow>
|
|
205
|
+
title={props.input.title}
|
|
206
|
+
description={props.input.subtitle ? <span class="text-sm text-secondary">{props.input.subtitle}</span> : undefined}
|
|
207
|
+
height={HEIGHT}
|
|
208
|
+
data={shown().data}
|
|
209
|
+
columns={columns()}
|
|
210
|
+
charts={props.input.views}
|
|
211
|
+
chart={chart()}
|
|
212
|
+
onChartChange={setChart}
|
|
213
|
+
view={view()}
|
|
214
|
+
onViewChange={setView}
|
|
215
|
+
legend={legend()}
|
|
216
|
+
selectedKey={explorer.selectedKey()}
|
|
217
|
+
onSelectedKeyChange={explorer.select}
|
|
218
|
+
tableRows={VISIBLE_ROWS}
|
|
219
|
+
controls={
|
|
220
|
+
hasControls(props.summary) ? (
|
|
221
|
+
<ChartFilterControls
|
|
222
|
+
request={explorer.desired()}
|
|
223
|
+
displayedRequest={explorer.snapshot().request}
|
|
224
|
+
steps={controls().steps.length ? controls().steps : props.summary.step ? [{ key: "0", label: "" }] : undefined}
|
|
225
|
+
series={
|
|
226
|
+
controls().series.length
|
|
227
|
+
? controls().series.map(({ key, label, color }) => ({
|
|
228
|
+
key,
|
|
229
|
+
label,
|
|
230
|
+
...(colored() ? { color: `var(--stdlib-chart-c${(color % 8) + 1})` } : {}),
|
|
231
|
+
}))
|
|
232
|
+
: props.summary.filter
|
|
233
|
+
? [{ key: "0", label: "", color: "var(--stdlib-chart-c1)" }]
|
|
234
|
+
: undefined
|
|
235
|
+
}
|
|
236
|
+
disabled={!props.input.rows.length}
|
|
237
|
+
compare={props.summary.compare}
|
|
238
|
+
dimensionLabel={columnLabel(props.input.controls?.step)}
|
|
239
|
+
seriesLabel={columnLabel(props.input.controls?.filter)}
|
|
240
|
+
onRequest={explorer.setRequest}
|
|
241
|
+
/>
|
|
242
|
+
) : undefined
|
|
243
|
+
}
|
|
244
|
+
empty={
|
|
245
|
+
<span role="status">
|
|
246
|
+
{shown().failed && explorer.snapshot().charts[TABLE]!.rows.length ? t().explorerUndrawable : t().explorerNoRows}
|
|
247
|
+
</span>
|
|
248
|
+
}
|
|
249
|
+
renderDetails={false}
|
|
250
|
+
/>
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* An explorer view file: one dataset, several charts of it, a shared selection, local filters and the table of the
|
|
256
|
+
* rows shown. It stands at its final size from the `present` summary while the file loads (`input` undefined) and
|
|
257
|
+
* when it is unavailable (`input` null).
|
|
258
|
+
*/
|
|
259
|
+
export function CloudExplorerView(props: { summary: ExplorerSummary; input: CloudAiExplorerView | null | undefined }) {
|
|
260
|
+
const locale = useLocale();
|
|
261
|
+
const t = () => aiChatMessages(locale());
|
|
262
|
+
return (
|
|
263
|
+
<Show
|
|
264
|
+
when={props.input}
|
|
265
|
+
keyed
|
|
266
|
+
fallback={
|
|
267
|
+
<ExplorerPlaceholder
|
|
268
|
+
summary={props.summary}
|
|
269
|
+
message={<span role="status">{props.input === undefined ? t().viewLoading : t().viewUnavailable}</span>}
|
|
270
|
+
/>
|
|
271
|
+
}
|
|
272
|
+
>
|
|
273
|
+
{(input) => <LoadedExplorer summary={props.summary} input={input} />}
|
|
274
|
+
</Show>
|
|
275
|
+
);
|
|
276
|
+
}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { fileIcons } from "@k2b/stdlib";
|
|
2
2
|
import { Chat } from "@k2b/ui";
|
|
3
|
-
import { Show } from "solid-js";
|
|
3
|
+
import { createMemo, Show } from "solid-js";
|
|
4
4
|
import { formatAiFileSize } from "../attachments";
|
|
5
5
|
import type { AiTurnBlock } from "../protocol";
|
|
6
|
+
import { CloudAiViewSummarySchema, isAiViewFile } from "../view-block";
|
|
6
7
|
import { useAiChatActions } from "./message-actions";
|
|
7
8
|
import { aiToolIcon, isRecord } from "./message-utils";
|
|
9
|
+
import { CloudViewBlock } from "./view-block";
|
|
8
10
|
|
|
9
11
|
type ToolBlock = Extract<AiTurnBlock, { kind: "tool" }>;
|
|
10
12
|
type PresentResult = { path: string; size: number; mediaType: string };
|
|
@@ -20,7 +22,44 @@ const presentResult = (block: ToolBlock): PresentResult | null => {
|
|
|
20
22
|
return null;
|
|
21
23
|
};
|
|
22
24
|
|
|
25
|
+
/** The summary `present` returns for a view file; Cloud gives it only for assistant files below /views. */
|
|
26
|
+
const presentedView = (block: ToolBlock) => {
|
|
27
|
+
if (block.status !== "completed" || block.isError || !isRecord(block.result) || typeof block.result.path !== "string") return null;
|
|
28
|
+
const summary = CloudAiViewSummarySchema.safeParse(block.result.view);
|
|
29
|
+
return summary.success && isAiViewFile({ path: block.result.path, origin: "assistant" })
|
|
30
|
+
? { path: block.result.path, summary: summary.data }
|
|
31
|
+
: null;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* A view's size comes with the finished call's summary. Until then the work line names the step and the result slot
|
|
36
|
+
* stays empty, so no one-line placeholder grows into the chart or table.
|
|
37
|
+
*/
|
|
38
|
+
const preparingView = (block: ToolBlock) =>
|
|
39
|
+
block.status === "running" && isRecord(block.args) && typeof block.args.path === "string"
|
|
40
|
+
? isAiViewFile({ path: block.args.path.trim(), origin: "assistant" })
|
|
41
|
+
: false;
|
|
42
|
+
|
|
43
|
+
/** A presented file: a view file shows as its chart or table, every other file as a chip to open and download. */
|
|
23
44
|
export function PresentToolBlock(props: { block: ToolBlock }) {
|
|
45
|
+
// Live updates replace the block with an equal result; only a different view loads again.
|
|
46
|
+
const view = createMemo(() => presentedView(props.block), null, { equals: (a, b) => JSON.stringify(a) === JSON.stringify(b) });
|
|
47
|
+
return (
|
|
48
|
+
<Show
|
|
49
|
+
when={view()}
|
|
50
|
+
keyed
|
|
51
|
+
fallback={
|
|
52
|
+
<Show when={!preparingView(props.block)}>
|
|
53
|
+
<PresentedFile block={props.block} />
|
|
54
|
+
</Show>
|
|
55
|
+
}
|
|
56
|
+
>
|
|
57
|
+
{(view) => <CloudViewBlock path={view.path} summary={view.summary} />}
|
|
58
|
+
</Show>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function PresentedFile(props: { block: ToolBlock }) {
|
|
24
63
|
const actions = useAiChatActions();
|
|
25
64
|
const file = () => presentResult(props.block);
|
|
26
65
|
const title = () => (isRecord(props.block.args) && typeof props.block.args.title === "string" ? props.block.args.title : null);
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
useLocale,
|
|
12
12
|
} from "@k2b/ui";
|
|
13
13
|
import { createContext, createSignal, For, type JSX, Show, useContext } from "solid-js";
|
|
14
|
+
import type { CloudResourceRef } from "../../contracts/capabilities";
|
|
14
15
|
import type { AiTurnBlock } from "../protocol";
|
|
15
16
|
import type { AiMessageFeedback, AiMessageFeedbackReason, AiStoredMessage } from "../types";
|
|
16
17
|
import {
|
|
@@ -62,6 +63,11 @@ export type AiChatActions = {
|
|
|
62
63
|
resolveFileLink?: (href: string) => { path: string; href: string } | null;
|
|
63
64
|
/** Download URL for a conversation VFS file (present blocks, attachment chips). */
|
|
64
65
|
fileUrl?: (path: string) => string | null;
|
|
66
|
+
/**
|
|
67
|
+
* Open a Cloud object a view references. The host resolves it through the owning app's reader with the viewer's
|
|
68
|
+
* access and confirms before it opens; without this action, references show as text.
|
|
69
|
+
*/
|
|
70
|
+
onOpenReference?: (title: string, ref: CloudResourceRef) => void;
|
|
65
71
|
};
|
|
66
72
|
|
|
67
73
|
const assistantBlocks = (message: Message): Extract<Message, { role: "assistant" }>["content"] =>
|
|
@@ -334,6 +334,13 @@ const BUILT_IN_TOOL_ICONS = new Map<string, string>([
|
|
|
334
334
|
["text_editor", "ti ti-edit"],
|
|
335
335
|
["cloud_text_editor", "ti ti-edit"],
|
|
336
336
|
["local_bash", "ti ti-terminal-2"],
|
|
337
|
+
["local_list", "ti ti-folder"],
|
|
338
|
+
["local_read", "ti ti-file-text"],
|
|
339
|
+
["local_upload", "ti ti-upload"],
|
|
340
|
+
["local_download", "ti ti-download"],
|
|
341
|
+
["local_write", "ti ti-file-pencil"],
|
|
342
|
+
["local_move", "ti ti-arrows-move"],
|
|
343
|
+
["load_local_skill", "ti ti-book"],
|
|
337
344
|
["list_files", "ti ti-file-spark"],
|
|
338
345
|
["read_file", "ti ti-file-spark"],
|
|
339
346
|
["write_file", "ti ti-file-spark"],
|
package/src/ai/chat/messages.ts
CHANGED
|
@@ -10,6 +10,13 @@ const messages = i18n.define({
|
|
|
10
10
|
tableOpen: "Open",
|
|
11
11
|
tableEmpty: "No matching rows.",
|
|
12
12
|
chartUnavailable: "This chart cannot be shown.",
|
|
13
|
+
viewLoading: "Loading…",
|
|
14
|
+
viewUnavailable: "This view is no longer available. Its file was changed or deleted.",
|
|
15
|
+
viewCopyData: "Copy data",
|
|
16
|
+
viewRowsCapped: ({ shown, total }: { shown: string; total: string }) =>
|
|
17
|
+
`Showing ${shown} of ${total} rows. Sort to see others; Copy data copies all of them.`,
|
|
18
|
+
explorerNoRows: "No rows match this selection.",
|
|
19
|
+
explorerUndrawable: "This view cannot be drawn for this selection. The table shows its rows.",
|
|
13
20
|
tableCapped: "Showing the first 100 returned rows. The full tool result remains available in details.",
|
|
14
21
|
tableMore: "More rows are available. Ask for the next page or open the result in the application.",
|
|
15
22
|
assistantTurns: "Assistant turns",
|
|
@@ -189,6 +196,7 @@ const messages = i18n.define({
|
|
|
189
196
|
toolsLoaded: "Loaded",
|
|
190
197
|
toolsAlreadyLoaded: "Already loaded",
|
|
191
198
|
toolUnknown: "Not found",
|
|
199
|
+
toolIsSkill: "A skill, not a tool",
|
|
192
200
|
toolNotOffered: "Not available here",
|
|
193
201
|
toolNotAllowed: "Not allowed",
|
|
194
202
|
toolAppOffline: "App not reachable",
|
|
@@ -303,6 +311,13 @@ const messages = i18n.define({
|
|
|
303
311
|
tableOpen: "Öffnen",
|
|
304
312
|
tableEmpty: "Keine passenden Zeilen.",
|
|
305
313
|
chartUnavailable: "Dieses Diagramm kann nicht angezeigt werden.",
|
|
314
|
+
viewLoading: "Wird geladen …",
|
|
315
|
+
viewUnavailable: "Diese Ansicht ist nicht mehr verfügbar. Ihre Datei wurde geändert oder gelöscht.",
|
|
316
|
+
viewCopyData: "Daten kopieren",
|
|
317
|
+
viewRowsCapped: ({ shown, total }: { shown: string; total: string }) =>
|
|
318
|
+
`${shown} von ${total} Zeilen werden angezeigt. Sortiere, um andere zu sehen; „Daten kopieren“ kopiert alle.`,
|
|
319
|
+
explorerNoRows: "Keine Zeilen passen zu dieser Auswahl.",
|
|
320
|
+
explorerUndrawable: "Diese Ansicht lässt sich für diese Auswahl nicht zeichnen. Die Tabelle zeigt ihre Zeilen.",
|
|
306
321
|
tableCapped:
|
|
307
322
|
"Die ersten 100 zurückgegebenen Zeilen werden angezeigt. Das vollständige Werkzeugergebnis bleibt in den Details verfügbar.",
|
|
308
323
|
tableMore: "Weitere Zeilen sind verfügbar. Frage nach der nächsten Seite oder öffne das Ergebnis in der Anwendung.",
|
|
@@ -489,6 +504,7 @@ const messages = i18n.define({
|
|
|
489
504
|
toolsLoaded: "Geladen",
|
|
490
505
|
toolsAlreadyLoaded: "Bereits geladen",
|
|
491
506
|
toolUnknown: "Nicht gefunden",
|
|
507
|
+
toolIsSkill: "Ein Skill, kein Werkzeug",
|
|
492
508
|
toolNotOffered: "Hier nicht verfügbar",
|
|
493
509
|
toolNotAllowed: "Nicht erlaubt",
|
|
494
510
|
toolAppOffline: "App nicht erreichbar",
|
|
@@ -16,6 +16,7 @@ import type { AiActiveTurn } from "../client/projection";
|
|
|
16
16
|
import { type AiActiveTurnSegment, type AiTurnBlock, splitActiveTurnBlocks } from "../protocol";
|
|
17
17
|
import { type AiAssistantTimelineItem, buildAiMessageTimeline } from "../timeline";
|
|
18
18
|
import type { AiConversationTimelineEntry, AiStoredMessage, AiTurnError } from "../types";
|
|
19
|
+
import { AiReaderTimeZoneProvider } from "./action-sentences";
|
|
19
20
|
import { type AiChatActions, AiChatActionsProvider, createAssistantMessageActions, useAiChatActions } from "./message-actions";
|
|
20
21
|
import { isRecord, isSurveyToolName, textFromMessage } from "./message-utils";
|
|
21
22
|
import { aiChatMessages } from "./messages";
|
|
@@ -112,7 +113,7 @@ const isEmptyLayout = (layout: AiTurnLayout) => !isWideLayout(layout) && layout.
|
|
|
112
113
|
* Turn views live in their own roots, keyed by timeline id. A changed segment updates its view instead of
|
|
113
114
|
* replacing it, so host state inside a turn, such as a running Studio session, survives every update.
|
|
114
115
|
*/
|
|
115
|
-
const createTurnViews = (disclosureState: AiToolDisclosureState) => {
|
|
116
|
+
const createTurnViews = (disclosureState: AiToolDisclosureState, timeZone: string | undefined) => {
|
|
116
117
|
const owner = getOwner();
|
|
117
118
|
const views = new Map<string, { set: Setter<AiTurnSegment>; content: JSX.Element; dispose: () => void }>();
|
|
118
119
|
onCleanup(() => {
|
|
@@ -128,7 +129,11 @@ const createTurnViews = (disclosureState: AiToolDisclosureState) => {
|
|
|
128
129
|
}
|
|
129
130
|
return createRoot((dispose) => {
|
|
130
131
|
const [current, set] = createSignal(segment);
|
|
131
|
-
const content =
|
|
132
|
+
const content = (
|
|
133
|
+
<AiReaderTimeZoneProvider value={timeZone}>
|
|
134
|
+
<AiTurnView segment={current} disclosureState={disclosureState} />
|
|
135
|
+
</AiReaderTimeZoneProvider>
|
|
136
|
+
);
|
|
132
137
|
views.set(segment.id, { set, content, dispose });
|
|
133
138
|
return content;
|
|
134
139
|
}, owner);
|
|
@@ -472,6 +477,11 @@ export type AiChatTimelineSource = {
|
|
|
472
477
|
activeTurn: Accessor<AiActiveTurn | null>;
|
|
473
478
|
/** The conversation stream lost its connection and reconnects, such as `streamStatus() === "reconnecting"`. */
|
|
474
479
|
reconnecting?: Accessor<boolean>;
|
|
480
|
+
/**
|
|
481
|
+
* The reader's IANA time zone, such as the page's `getTimeZone(c)`, so times in server-rendered history match the
|
|
482
|
+
* browser. Defaults to the browser's own zone, and to UTC outside a browser.
|
|
483
|
+
*/
|
|
484
|
+
timeZone?: string;
|
|
475
485
|
};
|
|
476
486
|
|
|
477
487
|
/**
|
|
@@ -483,7 +493,7 @@ export function createAiChatTimeline(source: AiChatTimelineSource): Accessor<rea
|
|
|
483
493
|
const actions = useAiChatActions();
|
|
484
494
|
const locale = useLocale();
|
|
485
495
|
const disclosureState = createAiToolDisclosureState();
|
|
486
|
-
const views = createTurnViews(disclosureState);
|
|
496
|
+
const views = createTurnViews(disclosureState, source.timeZone);
|
|
487
497
|
// While the stream reconnects or a model call waits for its retry, the live turn says so and its clock stands.
|
|
488
498
|
const reconnecting = createMemo(() => source.activeTurn()?.providerRetry === true || source.reconnecting?.() === true);
|
|
489
499
|
const clock = createActiveTurnClock(source, reconnecting);
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { markdownReferenceIcon } from "@k2b/ui";
|
|
2
|
+
import { Show } from "solid-js";
|
|
3
|
+
import type { CloudAiViewReference } from "../view-block";
|
|
4
|
+
import { useAiChatActions } from "./message-actions";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A Cloud object a view names. It carries no URL: the host resolves it on click with the viewer's access and confirms
|
|
8
|
+
* before it opens. It looks like a reference to Cloud content in Markdown, a calm pill, and stays text without a host.
|
|
9
|
+
* A Cloud reference names an object of the owning app, not a kind the chat knows, such as a task or an event, so the
|
|
10
|
+
* pill keeps the neutral page icon that relative links in chat Markdown also show. Without a reference it only takes
|
|
11
|
+
* the pill's room, for a hidden copy that reserves it.
|
|
12
|
+
*/
|
|
13
|
+
export function ReferenceText(props: { text: string; reference?: CloudAiViewReference }) {
|
|
14
|
+
const actions = useAiChatActions();
|
|
15
|
+
return (
|
|
16
|
+
<Show when={actions.onOpenReference} fallback={props.text}>
|
|
17
|
+
{(open) => (
|
|
18
|
+
<button
|
|
19
|
+
type="button"
|
|
20
|
+
// The pill's fill reaches past the text without making a compact table row taller than the one reserved.
|
|
21
|
+
class="k2b-reference -my-[0.125em] cursor-pointer text-left"
|
|
22
|
+
data-reference="page"
|
|
23
|
+
onClick={() => {
|
|
24
|
+
const reference = props.reference;
|
|
25
|
+
if (reference) open()(props.text, { type: reference.type, id: reference.id });
|
|
26
|
+
}}
|
|
27
|
+
>
|
|
28
|
+
<i class={`k2b-reference__icon ti ${markdownReferenceIcon("page")}`} aria-hidden="true" />
|
|
29
|
+
{props.text}
|
|
30
|
+
</button>
|
|
31
|
+
)}
|
|
32
|
+
</Show>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
@@ -14,6 +14,13 @@ const toolNames = i18n.define({
|
|
|
14
14
|
survey: "Survey",
|
|
15
15
|
text_editor: "Text editor",
|
|
16
16
|
local_bash: "Local Bash",
|
|
17
|
+
local_list: "List local files",
|
|
18
|
+
local_read: "Read local file",
|
|
19
|
+
local_upload: "Upload local files",
|
|
20
|
+
local_download: "Save to local folder",
|
|
21
|
+
local_write: "Write local file",
|
|
22
|
+
local_move: "Move local files",
|
|
23
|
+
load_local_skill: "Load local Skill",
|
|
17
24
|
todo_write: "Working plan",
|
|
18
25
|
memory: "Memory",
|
|
19
26
|
list_files: "List files",
|
|
@@ -83,6 +90,13 @@ const toolNames = i18n.define({
|
|
|
83
90
|
survey: "Umfrage",
|
|
84
91
|
text_editor: "Texteditor",
|
|
85
92
|
local_bash: "Lokale Bash",
|
|
93
|
+
local_list: "Lokale Dateien auflisten",
|
|
94
|
+
local_read: "Lokale Datei lesen",
|
|
95
|
+
local_upload: "Lokale Dateien hochladen",
|
|
96
|
+
local_download: "In lokalen Ordner speichern",
|
|
97
|
+
local_write: "Lokale Datei schreiben",
|
|
98
|
+
local_move: "Lokale Dateien verschieben",
|
|
99
|
+
load_local_skill: "Lokalen Skill laden",
|
|
86
100
|
todo_write: "Arbeitsplan",
|
|
87
101
|
memory: "Gedächtnis",
|
|
88
102
|
list_files: "Dateien auflisten",
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { dates } from "@k2b/stdlib";
|
|
1
2
|
import { announce, Button, ButtonLink, Chat, NoticeCard, useLocale } from "@k2b/ui";
|
|
2
3
|
import {
|
|
3
4
|
type Accessor,
|
|
@@ -17,7 +18,7 @@ import {
|
|
|
17
18
|
import { markdown } from "../../shared";
|
|
18
19
|
import { AI_TURN_LEASE_MS, type AiTurnBlock } from "../protocol";
|
|
19
20
|
import type { AiTurnError } from "../types";
|
|
20
|
-
import { toolOutcome, toolSubject } from "./action-sentences";
|
|
21
|
+
import { toolOutcome, toolSubject, useReaderTimeZone } from "./action-sentences";
|
|
21
22
|
import {
|
|
22
23
|
type AiApprovalDecision,
|
|
23
24
|
ApprovalBlockView,
|
|
@@ -525,10 +526,11 @@ type ReceiptState = Exclude<AiTurnActionState, "open" | "interaction">;
|
|
|
525
526
|
*/
|
|
526
527
|
function AiReceipt(props: { block: ToolBlock; state: ReceiptState; stopped: boolean }) {
|
|
527
528
|
const locale = useLocale();
|
|
529
|
+
const timeZone = useReaderTimeZone();
|
|
528
530
|
const t = () => aiChatMessages(locale());
|
|
529
531
|
const block = () => props.block;
|
|
530
532
|
const subject = () => {
|
|
531
|
-
const sentence = toolSubject(block(), locale());
|
|
533
|
+
const sentence = toolSubject(block(), locale(), timeZone);
|
|
532
534
|
if (sentence) return sentence;
|
|
533
535
|
const title = block().presentation?.title ?? displayToolName(block().name, locale());
|
|
534
536
|
const target = toolTarget(block());
|
|
@@ -536,22 +538,26 @@ function AiReceipt(props: { block: ToolBlock; state: ReceiptState; stopped: bool
|
|
|
536
538
|
};
|
|
537
539
|
const capabilityAction = () => block().presentation?.kind === "capability";
|
|
538
540
|
const links = () => (props.state === "done" ? resultLinks(block()) : []);
|
|
539
|
-
const
|
|
541
|
+
const outcomeLabel = () => {
|
|
540
542
|
const state = props.state;
|
|
541
543
|
if (state === "running") return t().receiptRunning({ title: subject() });
|
|
542
544
|
if (state === "done") {
|
|
543
545
|
// A tool that is not a Cloud action has no effect of its own to report; the receipt records the decision.
|
|
544
546
|
if (!capabilityAction()) return t().receiptApproved({ title: subject() });
|
|
545
|
-
return toolOutcome(block(), "done", locale()) || resultSummary(block()) || t().receiptDone({ title: subject() });
|
|
547
|
+
return toolOutcome(block(), "done", locale(), timeZone) || resultSummary(block()) || t().receiptDone({ title: subject() });
|
|
546
548
|
}
|
|
547
|
-
if (state === "rejected") return toolOutcome(block(), "rejected", locale()) ?? t().receiptRejected({ title: subject() });
|
|
549
|
+
if (state === "rejected") return toolOutcome(block(), "rejected", locale(), timeZone) ?? t().receiptRejected({ title: subject() });
|
|
548
550
|
if (state === "failed") return t().receiptFailed({ title: subject() });
|
|
549
|
-
const notRun = toolOutcome(block(), "notRun", locale());
|
|
551
|
+
const notRun = toolOutcome(block(), "notRun", locale(), timeZone);
|
|
550
552
|
return notRun
|
|
551
553
|
? t().receiptNotRunSentence({ sentence: notRun, stopped: props.stopped })
|
|
552
554
|
: t().receiptNotRun({ title: subject(), stopped: props.stopped });
|
|
553
555
|
};
|
|
554
556
|
const declined = () => props.state === "rejected" || props.state === "not_run";
|
|
557
|
+
const label = () => {
|
|
558
|
+
const at = block().at;
|
|
559
|
+
return at && props.state !== "running" ? `${outcomeLabel()} · ${dates.formatTime(at, { locale: locale(), timeZone })}` : outcomeLabel();
|
|
560
|
+
};
|
|
555
561
|
return (
|
|
556
562
|
<Chat.Activity
|
|
557
563
|
class="ai-turn-receipt"
|
|
@@ -567,6 +573,7 @@ function AiReceipt(props: { block: ToolBlock; state: ReceiptState; stopped: bool
|
|
|
567
573
|
|
|
568
574
|
function AiTurnActionView(props: { action: Accessor<AiTurnAction | undefined>; turnId: string; phase: AiTurnPhase }) {
|
|
569
575
|
const locale = useLocale();
|
|
576
|
+
const timeZone = useReaderTimeZone();
|
|
570
577
|
let row!: HTMLDivElement;
|
|
571
578
|
// Set while focus is in this place, including after the focused control disappeared with the decided card.
|
|
572
579
|
let focusedHere = false;
|
|
@@ -597,7 +604,10 @@ function AiTurnActionView(props: { action: Accessor<AiTurnAction | undefined>; t
|
|
|
597
604
|
if (next !== "open" || previous === "open" || !isLive(props.phase) || current?.block.status !== "awaiting_approval") return;
|
|
598
605
|
announce(
|
|
599
606
|
aiChatMessages(locale()).approvalNeeded({
|
|
600
|
-
title:
|
|
607
|
+
title:
|
|
608
|
+
toolSubject(current.block, locale(), timeZone) ??
|
|
609
|
+
current.block.presentation?.title ??
|
|
610
|
+
displayToolName(current.block.name, locale()),
|
|
601
611
|
}),
|
|
602
612
|
);
|
|
603
613
|
}),
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { createSignal, For } from "solid-js";
|
|
2
|
+
import { render } from "solid-js/web";
|
|
3
|
+
import type { CloudResourceRef } from "../../contracts/capabilities";
|
|
4
|
+
import type { AiTurnBlock } from "../protocol";
|
|
5
|
+
import { PresentToolBlock } from "./file-tools";
|
|
6
|
+
import { AiChatActionsProvider } from "./presentation";
|
|
7
|
+
import { CloudCardBlock } from "./visual-tools";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Presented files for a browser test, in a chat-wide column. `window.show(blocks)` replaces the shown `present`
|
|
11
|
+
* blocks and `window.showCards(args)` the shown `card` calls. Chat files load from `/files?path=`; `window.opened`
|
|
12
|
+
* records the references a reader opened.
|
|
13
|
+
*/
|
|
14
|
+
const [blocks, setBlocks] = createSignal<Extract<AiTurnBlock, { kind: "tool" }>[]>([]);
|
|
15
|
+
const [cards, setCards] = createSignal<unknown[]>([]);
|
|
16
|
+
const opened: { title: string; ref: CloudResourceRef }[] = [];
|
|
17
|
+
Object.assign(window, { show: setBlocks, showCards: setCards, opened });
|
|
18
|
+
|
|
19
|
+
const root = document.createElement("main");
|
|
20
|
+
root.style.cssText = "display: flex; flex-direction: column; gap: 1.5rem; max-width: 48rem; margin: 0 auto; padding: 1rem;";
|
|
21
|
+
document.body.append(root);
|
|
22
|
+
render(
|
|
23
|
+
() => (
|
|
24
|
+
<AiChatActionsProvider
|
|
25
|
+
actions={{
|
|
26
|
+
fileUrl: (path) => `/files?path=${encodeURIComponent(path)}`,
|
|
27
|
+
onOpenReference: (title, ref) => opened.push({ title, ref }),
|
|
28
|
+
}}
|
|
29
|
+
>
|
|
30
|
+
<For each={blocks()}>{(block) => <PresentToolBlock block={block} />}</For>
|
|
31
|
+
<For each={cards()}>{(args) => <CloudCardBlock args={args} />}</For>
|
|
32
|
+
</AiChatActionsProvider>
|
|
33
|
+
),
|
|
34
|
+
root,
|
|
35
|
+
);
|