@k2b/cloud 0.30.0 → 0.32.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +7 -4
- package/src/_internal/app-presentation.ts +22 -1
- package/src/_internal/capabilities.ts +2 -2
- package/src/_internal/define-app.ts +17 -5
- package/src/_internal/registry.ts +50 -6
- package/src/_internal/widget-declarations.ts +53 -0
- package/src/ai/app-skills.ts +1 -7
- package/src/ai/approval-routes.ts +40 -4
- package/src/ai/approvals.ts +68 -10
- package/src/ai/audio-tool.ts +2 -1
- package/src/ai/browser.ts +109 -0
- package/src/ai/capabilities.ts +6 -2
- package/src/ai/chart-block.ts +431 -81
- package/src/ai/chat/action-sentences.ts +13 -7
- package/src/ai/chat/blocks.tsx +64 -39
- package/src/ai/chat/builtin-tools.tsx +9 -7
- package/src/ai/chat/chart-block.tsx +218 -44
- package/src/ai/chat/explorer-block.tsx +276 -0
- package/src/ai/chat/file-tools.tsx +40 -1
- package/src/ai/chat/live-turn.browser-harness.tsx +4 -1
- package/src/ai/chat/message-actions.tsx +9 -1
- package/src/ai/chat/message-utils.ts +7 -0
- package/src/ai/chat/messages.ts +34 -0
- package/src/ai/chat/presentation.tsx +13 -3
- package/src/ai/chat/reference-text.tsx +34 -0
- package/src/ai/chat/tool-groups.ts +67 -12
- package/src/ai/chat/tool-names.ts +18 -2
- package/src/ai/chat/turn-layout.ts +8 -0
- package/src/ai/chat/turn-view.tsx +111 -32
- package/src/ai/chat/view-block.browser-harness.tsx +35 -0
- package/src/ai/chat/view-block.tsx +417 -0
- package/src/ai/chat/visual-tools.tsx +32 -53
- package/src/ai/chat/web-tools.tsx +95 -1
- package/src/ai/client/controller.ts +6 -3
- package/src/ai/client-tool-results.ts +9 -0
- package/src/ai/code-runtime-tools.ts +50 -17
- package/src/ai/code-source-contracts.ts +25 -21
- package/src/ai/code-source-tools.ts +3 -1
- package/src/ai/default-tool-contracts.ts +334 -91
- package/src/ai/default-tools.ts +87 -11
- package/src/ai/executor.ts +343 -45
- package/src/ai/fetch-file-tool.ts +41 -4
- package/src/ai/file-error.ts +2 -0
- package/src/ai/file-tools.ts +27 -7
- package/src/ai/files-store.ts +60 -0
- package/src/ai/firecrawl-tools.ts +87 -4
- package/src/ai/html-pdf-tool.ts +3 -2
- package/src/ai/http.ts +37 -12
- package/src/ai/index.ts +98 -0
- package/src/ai/local-tool-contracts.ts +244 -0
- package/src/ai/malformed-tool-call-repair.ts +144 -0
- package/src/ai/markdown-pdf-tool.ts +2 -1
- package/src/ai/migrate.ts +120 -2
- package/src/ai/openai-api-messages.ts +53 -0
- package/src/ai/openai-api-slots.ts +40 -0
- package/src/ai/openai-api.ts +383 -0
- package/src/ai/protocol.ts +28 -3
- package/src/ai/provider.ts +16 -4
- package/src/ai/request-context.ts +33 -0
- package/src/ai/routes.ts +99 -49
- package/src/ai/runtime.ts +90 -15
- package/src/ai/settings.ts +5 -0
- package/src/ai/skill-catalog.ts +20 -5
- package/src/ai/skill-format.ts +17 -7
- package/src/ai/skill-tool.ts +15 -5
- package/src/ai/skills-routes.ts +20 -6
- package/src/ai/skills.ts +26 -7
- package/src/ai/store.ts +91 -1
- package/src/ai/system-prompt.ts +69 -10
- package/src/ai/timeline.ts +3 -1
- package/src/ai/tool-audit.ts +11 -1
- package/src/ai/tools.ts +94 -3
- package/src/ai/types.ts +48 -0
- package/src/ai/view-block.ts +941 -0
- package/src/ai/vision-tool.ts +2 -1
- package/src/ai/web-provenance.ts +103 -0
- package/src/api/admin-identity.ts +6 -1
- package/src/api/app-approval-actions.ts +36 -0
- package/src/api/app-approval.ts +10 -2
- package/src/api/approvals-loader.ts +15 -0
- package/src/api/approvals.ts +120 -0
- package/src/api/capabilities.ts +3 -2
- package/src/api/index.ts +3 -0
- package/src/api/me.ts +12 -3
- package/src/api/widgets.ts +36 -5
- package/src/approvals/Approvals.browser-harness.tsx +16 -0
- package/src/approvals/Approvals.tsx +621 -0
- package/src/approvals/ApprovalsNavStatus.tsx +20 -0
- package/src/approvals/messages.ts +95 -0
- package/src/approvals/ui.ts +8 -0
- package/src/browser/CloudResourceSearch.tsx +1 -1
- package/src/browser/app-approval.ts +32 -0
- package/src/browser/choose-files.tsx +26 -13
- package/src/browser/command-shortcuts.ts +21 -6
- package/src/browser/commands.ts +13 -4
- package/src/browser/file-provider-error.ts +11 -0
- package/src/browser/file-providers.ts +3 -12
- package/src/browser/files.tsx +1 -2
- package/src/browser/live-connection.ts +2 -3
- package/src/browser/provider-list.ts +2 -1
- package/src/browser/save-files.tsx +13 -2
- package/src/browser/spotlight-position.ts +4 -1
- package/src/browser/widgets.ts +7 -5
- package/src/capabilities/executions.ts +3 -0
- package/src/cli/account.ts +21 -2
- package/src/cli/commands.ts +24 -2
- package/src/cli/index.ts +16 -0
- package/src/cli/plugin.ts +30 -0
- package/src/contracts/app-approval.ts +120 -0
- package/src/contracts/app.ts +34 -2
- package/src/contracts/approval-paths.ts +4 -0
- package/src/contracts/approvals.ts +81 -0
- package/src/contracts/command-path.ts +17 -0
- package/src/contracts/commands.ts +5 -15
- package/src/contracts/registry.ts +9 -1
- package/src/contracts/shared.ts +5 -0
- package/src/contracts/widgets.ts +47 -2
- package/src/desktop/solid.tsx +6 -7
- package/src/events/live-protocol.ts +2 -1
- package/src/events/live-server-message.ts +40 -0
- package/src/events/testing.ts +88 -0
- package/src/server/index.ts +3 -0
- package/src/server/middleware/auth.ts +11 -1
- package/src/server/request-timeout.ts +22 -0
- package/src/server/services/access.ts +4 -3
- package/src/server/widget-request.ts +27 -0
- package/src/services/app-approval.ts +103 -15
- package/src/services/approvals/cursor.ts +15 -0
- package/src/services/approvals/device.ts +112 -0
- package/src/services/approvals/index.ts +241 -0
- package/src/services/approvals/list.ts +120 -0
- package/src/services/approvals/migrate.ts +41 -0
- package/src/services/identity/models-only-auth.ts +25 -0
- package/src/services/notifications/email-frame.ts +16 -3
- package/src/services/service-account-credentials.ts +22 -10
- package/src/services/settings/core-settings.ts +27 -0
- package/src/shared/ai-platform-prompt.ts +26 -1
- package/src/shared/app-presentation.ts +11 -0
- package/src/shared/email-html.ts +46 -7
- package/src/shared/index.ts +6 -1
- package/src/shared/markdown/extensions/code.ts +14 -0
- package/src/shared/markdown/index.ts +4 -4
- package/src/ssr/AdminLayout.tsx +4 -7
- package/src/ssr/AppBadge.tsx +4 -4
- package/src/ssr/AppLaunchpad.island.tsx +1 -1
- package/src/ssr/AppLaunchpadPanel.tsx +4 -4
- package/src/ssr/Layout.tsx +38 -14
- package/src/ssr/LayoutFooter.tsx +1 -1
- package/src/ssr/LayoutHeader.tsx +17 -20
- package/src/ssr/LayoutHelp.tsx +1 -1
- package/src/ssr/LayoutRail.tsx +1 -2
- package/src/ssr/PageTrail.tsx +23 -0
- package/src/ssr/PageTrailRow.island.tsx +20 -0
- package/src/ssr/PageTrailRuntime.island.tsx +26 -0
- package/src/ssr/ProfilePreferences.island.tsx +7 -3
- package/src/ssr/RailApps.island.tsx +2 -2
- package/src/ssr/app-badges.ts +50 -6
- package/src/ssr/islands/index.ts +1 -0
- package/src/ssr/layout-runtime.ts +60 -0
- package/src/ssr/mobile-menu-history.ts +1 -1
- package/src/ssr/page-trail-runtime.ts +313 -0
- package/src/ssr/page-trail.browser-harness.tsx +265 -0
- package/src/ssr/page-trail.ts +59 -0
- package/src/ssr/platform-messages.ts +4 -0
- package/src/ssr/rail-navigation.ts +5 -3
- package/src/ssr/runtime.ts +6 -1
- package/src/styles/approvals.css +390 -0
- package/src/styles/effects.css +21 -4
- package/src/styles/global.css +39 -3
- package/src/styles/utilities-layout.css +24 -1
- package/src/styles/utilities-navigation.css +6 -18
|
@@ -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);
|
|
@@ -12,7 +12,7 @@ import { AiChatActionsProvider, createAiChatTimeline } from "./presentation";
|
|
|
12
12
|
* `window.steer(text)` appends a pending steer as the controller does,
|
|
13
13
|
* `window.reconnect(on)` sets whether the stream reconnects,
|
|
14
14
|
* `window.continued` lists the messages that **Continue** sent,
|
|
15
|
-
* `window.approvals` lists the approval decisions, and
|
|
15
|
+
* `window.approvals` lists the approval decisions, `window.revoked` the websites a receipt revoked, and
|
|
16
16
|
* `window.opened` lists the chat files the timeline opened. Chat files load from `/files?path=`.
|
|
17
17
|
*/
|
|
18
18
|
const [state, setState] = createStore(emptyProjection());
|
|
@@ -20,9 +20,11 @@ const [reconnecting, setReconnecting] = createSignal(false);
|
|
|
20
20
|
const continued: string[] = [];
|
|
21
21
|
const opened: string[] = [];
|
|
22
22
|
const approvals: unknown[] = [];
|
|
23
|
+
const revoked: string[] = [];
|
|
23
24
|
Object.assign(window, {
|
|
24
25
|
continued,
|
|
25
26
|
approvals,
|
|
27
|
+
revoked,
|
|
26
28
|
opened,
|
|
27
29
|
reconnect: (on: boolean) => setReconnecting(on),
|
|
28
30
|
emit: (event: AiWireEvent) => setState(reconcile(reduceProjection(state, event), { key: "id", merge: true })),
|
|
@@ -42,6 +44,7 @@ render(
|
|
|
42
44
|
actions={{
|
|
43
45
|
onContinueTurn: (message) => void continued.push(message),
|
|
44
46
|
onApproval: async (request, input) => void approvals.push({ callId: request.callId, ...input }),
|
|
47
|
+
onRevokeWebsite: async (origin) => void revoked.push(origin),
|
|
45
48
|
fileUrl: (path) => `/files?path=${encodeURIComponent(path)}`,
|
|
46
49
|
onOpenFile: (path) => void opened.push(path),
|
|
47
50
|
}}
|
|
@@ -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 {
|
|
@@ -26,7 +27,7 @@ import { aiChatMessages } from "./messages";
|
|
|
26
27
|
/** The active-turn coordinates an approval/tool action needs to resolve on the server. */
|
|
27
28
|
export type AiTurnActionRequest = { turnId: string; callId: string; name: string };
|
|
28
29
|
|
|
29
|
-
type ApprovalHandler = (request: AiTurnActionRequest, input: { approved: boolean; remember?: "always" }) => void | Promise<void>;
|
|
30
|
+
type ApprovalHandler = (request: AiTurnActionRequest, input: { approved: boolean; remember?: "chat" | "always" }) => void | Promise<void>;
|
|
30
31
|
type FrontendToolResultHandler = (request: AiTurnActionRequest, result: unknown) => void | Promise<void>;
|
|
31
32
|
type ForkMessageHandler = (entry: AiStoredMessage, input?: AiForkMessageInput) => void | Promise<void>;
|
|
32
33
|
type RetryMessageHandler = (entry: AiStoredMessage, input?: AiRetryMessageInput) => void | Promise<void>;
|
|
@@ -42,6 +43,8 @@ export type AiChatActions = {
|
|
|
42
43
|
/** Prevents turn-continuation actions while the current turn is stopping. */
|
|
43
44
|
actionDisabled?: () => boolean;
|
|
44
45
|
onApproval?: ApprovalHandler;
|
|
46
|
+
/** Revoke the approval that lets this chat read `origin` without asking; the next request asks again. */
|
|
47
|
+
onRevokeWebsite?: (origin: string) => Promise<void>;
|
|
45
48
|
onFrontendToolResult?: FrontendToolResultHandler;
|
|
46
49
|
onForkMessage?: ForkMessageHandler;
|
|
47
50
|
onRetryMessage?: RetryMessageHandler;
|
|
@@ -60,6 +63,11 @@ export type AiChatActions = {
|
|
|
60
63
|
resolveFileLink?: (href: string) => { path: string; href: string } | null;
|
|
61
64
|
/** Download URL for a conversation VFS file (present blocks, attachment chips). */
|
|
62
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;
|
|
63
71
|
};
|
|
64
72
|
|
|
65
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",
|
|
@@ -98,6 +105,7 @@ const messages = i18n.define({
|
|
|
98
105
|
openImage: ({ name }: { name: string }) => `Open image ${name}`,
|
|
99
106
|
steered: "Conversation steered",
|
|
100
107
|
groupLoad: "Loaded tools and guidance",
|
|
108
|
+
repeatCount: ({ count }: { count: number }) => `${count} times`,
|
|
101
109
|
groupFailed: ({ count }: { count: number }) => `${count} failed`,
|
|
102
110
|
groupRejected: ({ count }: { count: number }) => `${count} rejected`,
|
|
103
111
|
receiptDone: ({ title }: { title: string }) => `Done: ${title}`,
|
|
@@ -120,6 +128,14 @@ const messages = i18n.define({
|
|
|
120
128
|
approved: "Approved",
|
|
121
129
|
rejected: "Rejected",
|
|
122
130
|
alwaysApprove: "Always approve",
|
|
131
|
+
chatApprove: "Approve for this chat",
|
|
132
|
+
allowWebsiteForChat: "Allow this website for this chat",
|
|
133
|
+
websiteAllowedForChat: ({ count }: { count: number }) =>
|
|
134
|
+
`Allowed for this chat · ${count === 1 ? "1 request without asking" : `${count} requests without asking`}`,
|
|
135
|
+
revoke: "Revoke",
|
|
136
|
+
revokeFailed: "Could not revoke. Try again.",
|
|
137
|
+
revokeWebsiteFor: ({ host }: { host: string }) => `Revoke the approval for ${host}`,
|
|
138
|
+
websiteRevoked: "Revoked. The next request asks again.",
|
|
123
139
|
details: "Details",
|
|
124
140
|
hideDetails: "Hide details",
|
|
125
141
|
moreOptions: ({ title }: { title: string }) => `More options for ${title}`,
|
|
@@ -180,6 +196,7 @@ const messages = i18n.define({
|
|
|
180
196
|
toolsLoaded: "Loaded",
|
|
181
197
|
toolsAlreadyLoaded: "Already loaded",
|
|
182
198
|
toolUnknown: "Not found",
|
|
199
|
+
toolIsSkill: "A skill, not a tool",
|
|
183
200
|
toolNotOffered: "Not available here",
|
|
184
201
|
toolNotAllowed: "Not allowed",
|
|
185
202
|
toolAppOffline: "App not reachable",
|
|
@@ -294,6 +311,13 @@ const messages = i18n.define({
|
|
|
294
311
|
tableOpen: "Öffnen",
|
|
295
312
|
tableEmpty: "Keine passenden Zeilen.",
|
|
296
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.",
|
|
297
321
|
tableCapped:
|
|
298
322
|
"Die ersten 100 zurückgegebenen Zeilen werden angezeigt. Das vollständige Werkzeugergebnis bleibt in den Details verfügbar.",
|
|
299
323
|
tableMore: "Weitere Zeilen sind verfügbar. Frage nach der nächsten Seite oder öffne das Ergebnis in der Anwendung.",
|
|
@@ -389,6 +413,7 @@ const messages = i18n.define({
|
|
|
389
413
|
openImage: ({ name }) => `Bild ${name} öffnen`,
|
|
390
414
|
steered: "Richtung geändert",
|
|
391
415
|
groupLoad: "Werkzeuge und Wissen geladen",
|
|
416
|
+
repeatCount: ({ count }) => `${count}-mal`,
|
|
392
417
|
groupFailed: ({ count }) => `${count} fehlgeschlagen`,
|
|
393
418
|
groupRejected: ({ count }) => `${count} abgelehnt`,
|
|
394
419
|
receiptDone: ({ title }) => `Erledigt: ${title}`,
|
|
@@ -411,6 +436,14 @@ const messages = i18n.define({
|
|
|
411
436
|
approved: "Freigegeben",
|
|
412
437
|
rejected: "Abgelehnt",
|
|
413
438
|
alwaysApprove: "Immer freigeben",
|
|
439
|
+
chatApprove: "Für diesen Chat freigeben",
|
|
440
|
+
allowWebsiteForChat: "Diese Website für diesen Chat erlauben",
|
|
441
|
+
websiteAllowedForChat: ({ count }: { count: number }) =>
|
|
442
|
+
`Für diesen Chat erlaubt · ${count === 1 ? "1 Abruf ohne Rückfrage" : `${count} Abrufe ohne Rückfrage`}`,
|
|
443
|
+
revoke: "Entziehen",
|
|
444
|
+
revokeFailed: "Konnte nicht entzogen werden. Versuche es erneut.",
|
|
445
|
+
revokeWebsiteFor: ({ host }: { host: string }) => `Freigabe für ${host} entziehen`,
|
|
446
|
+
websiteRevoked: "Entzogen. Der nächste Abruf fragt wieder.",
|
|
414
447
|
details: "Details",
|
|
415
448
|
hideDetails: "Details ausblenden",
|
|
416
449
|
moreOptions: ({ title }) => `Weitere Optionen für ${title}`,
|
|
@@ -471,6 +504,7 @@ const messages = i18n.define({
|
|
|
471
504
|
toolsLoaded: "Geladen",
|
|
472
505
|
toolsAlreadyLoaded: "Bereits geladen",
|
|
473
506
|
toolUnknown: "Nicht gefunden",
|
|
507
|
+
toolIsSkill: "Ein Skill, kein Werkzeug",
|
|
474
508
|
toolNotOffered: "Hier nicht verfügbar",
|
|
475
509
|
toolNotAllowed: "Nicht erlaubt",
|
|
476
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
|
+
}
|