@k2b/cloud 0.39.0 → 0.40.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/README.md +8 -0
- package/package.json +8 -4
- package/src/_internal/app-favicon.ts +4 -2
- package/src/_internal/capabilities.ts +72 -48
- package/src/_internal/capability-reviews.ts +81 -0
- package/src/_internal/capability-schemas.ts +29 -0
- package/src/_internal/capability-sentences.ts +216 -1
- package/src/_internal/capability-streams.ts +7 -0
- package/src/_internal/define-app.ts +36 -15
- package/src/_internal/font-preloads.ts +2 -1
- package/src/_internal/runtime-watcher.ts +6 -0
- package/src/_internal/static-assets.ts +52 -3
- package/src/_internal/status-preserving-ssr.ts +2 -0
- package/src/access/PermissionEditor.tsx +2 -2
- package/src/account/EntitySearch.tsx +1 -1
- package/src/ai/action-review.ts +101 -0
- package/src/ai/background-review.ts +72 -0
- package/src/ai/capabilities.ts +29 -11
- package/src/ai/capability-execution.ts +76 -43
- package/src/ai/capability-result.ts +95 -0
- package/src/ai/chat/action-card.ts +398 -0
- package/src/ai/chat/action-messages.ts +129 -0
- package/src/ai/chat/action-question.browser-harness.tsx +245 -0
- package/src/ai/chat/action-question.fixtures.ts +733 -0
- package/src/ai/chat/action-question.tsx +669 -0
- package/src/ai/chat/blocks.tsx +19 -25
- package/src/ai/chat/message-actions.tsx +6 -0
- package/src/ai/chat/tool-action.tsx +481 -0
- package/src/ai/chat/turn-view.tsx +10 -160
- package/src/ai/chat/view-block.tsx +15 -5
- package/src/ai/chat-task-contracts.ts +4 -1
- package/src/ai/chat-task-reasons-migrate.ts +7 -0
- package/src/ai/chat-task-reasons.ts +1 -1
- package/src/ai/chat-tasks.ts +55 -25
- package/src/ai/client/controller.ts +59 -0
- package/src/ai/default-tools.ts +9 -1
- package/src/ai/executor.ts +190 -39
- package/src/ai/index.ts +2 -0
- package/src/ai/migrate.ts +11 -2
- package/src/ai/projects-routes.ts +3 -0
- package/src/ai/protocol.ts +14 -2
- package/src/ai/recent-resources.ts +240 -0
- package/src/ai/recent-sent-refs.ts +38 -0
- package/src/ai/routes.ts +145 -1
- package/src/ai/runtime.ts +1 -0
- package/src/ai/skills-routes.ts +3 -0
- package/src/ai/skills.ts +10 -1
- package/src/ai/store.ts +227 -24
- package/src/ai/tool-audit.ts +8 -0
- package/src/ai/tool-wording.ts +24 -2
- package/src/ai/types.ts +28 -3
- package/src/api/accounts-entities.ts +2 -1
- package/src/api/admin-ai-projects.ts +2 -0
- package/src/api/admin-ai-skills.ts +2 -0
- package/src/api/admin-ai-usage.ts +2 -0
- package/src/api/admin-geoip.ts +80 -0
- package/src/api/admin-identity.ts +3 -0
- package/src/api/admin-lifecycle.ts +3 -0
- package/src/api/admin-outgoing-mail.ts +2 -0
- package/src/api/app-approval.ts +46 -5
- package/src/api/approvals-loader.ts +23 -1
- package/src/api/approvals.ts +2 -0
- package/src/api/auth.ts +25 -6
- package/src/api/capabilities.ts +44 -11
- package/src/api/capability-streams.ts +12 -0
- package/src/api/help.ts +4 -28
- package/src/api/index.ts +6 -1
- package/src/api/lasting-access-messages.ts +17 -0
- package/src/api/mcp.ts +3 -0
- package/src/api/me-panel-consent.ts +71 -0
- package/src/api/me.ts +80 -33
- package/src/api/search.ts +3 -1
- package/src/api/sync-ops.ts +2 -0
- package/src/api/widgets.ts +3 -0
- package/src/approvals/Approvals.tsx +180 -238
- package/src/approvals/messages.ts +12 -24
- package/src/browser/CloudResourceSearch.browser-harness.tsx +10 -2
- package/src/browser/app-approval.ts +54 -5
- package/src/browser/commands.ts +22 -6
- package/src/browser/composer-catalog.ts +45 -3
- package/src/browser/composer-insert.ts +10 -0
- package/src/browser/composer-open-messages.ts +17 -0
- package/src/browser/composer-open.ts +110 -0
- package/src/browser/discoverable-command.ts +6 -0
- package/src/browser/hints.ts +6 -0
- package/src/browser/live-connection.ts +26 -2
- package/src/browser/navigation-apps.ts +7 -0
- package/src/browser/panel-context-bridge.ts +30 -0
- package/src/browser/panel-context-receiver.ts +53 -0
- package/src/browser/panel-menu.tsx +56 -0
- package/src/browser/panel-messages.ts +11 -0
- package/src/browser/panel-shortcuts.ts +31 -0
- package/src/browser/panel-slot.ts +412 -10
- package/src/browser/panel.ts +37 -0
- package/src/browser/search-commands.ts +6 -3
- package/src/capabilities/client.ts +8 -2
- package/src/capabilities/reviews.ts +205 -0
- package/src/capabilities/server.ts +10 -3
- package/src/capabilities/testing.ts +47 -0
- package/src/capabilities/types.ts +2 -2
- package/src/cli/account.ts +13 -3
- package/src/cli/admin/geoip.ts +115 -0
- package/src/cli/admin/index.ts +3 -0
- package/src/contracts/app-approval.ts +120 -13
- package/src/contracts/approvals.ts +2 -2
- package/src/contracts/capabilities.ts +410 -18
- package/src/contracts/geoip.ts +42 -0
- package/src/contracts/index.ts +3 -0
- package/src/contracts/panel-context-plain.ts +99 -0
- package/src/contracts/panel-context.ts +93 -0
- package/src/contracts/seen-hints.ts +35 -0
- package/src/server/client-ip.ts +5 -0
- package/src/server/help-payload.ts +38 -0
- package/src/server/middleware/auth.ts +11 -1
- package/src/server/middleware/middleware.ts +9 -0
- package/src/server/middleware/no-transform.ts +19 -0
- package/src/server/middleware/rate-limit.ts +4 -6
- package/src/server/request-context/address.ts +45 -0
- package/src/server/request-context/geoip.ts +380 -0
- package/src/server/request-context/index.ts +33 -0
- package/src/server/request-context/user-agent.ts +95 -0
- package/src/services/app-approval.ts +382 -59
- package/src/services/approvals/device.ts +7 -5
- package/src/services/approvals/index.ts +6 -5
- package/src/services/approvals/list.ts +26 -5
- package/src/services/auth-flows/notification-sender.ts +3 -0
- package/src/services/geoip/index.ts +164 -0
- package/src/services/geoip/store.ts +30 -0
- package/src/services/geoip/upload.ts +150 -0
- package/src/services/identity/routes.ts +5 -1
- package/src/services/index.ts +11 -0
- package/src/services/known-browsers.ts +312 -0
- package/src/services/login-address-store.ts +51 -0
- package/src/services/login-context.ts +118 -0
- package/src/services/mandates/index.ts +3 -0
- package/src/services/mandates/policy.ts +15 -3
- package/src/services/notifications/email-frame.ts +1 -1
- package/src/services/oauth-tokens.ts +16 -2
- package/src/services/panel-context-consent.ts +58 -0
- package/src/services/service-account-credentials.ts +21 -0
- package/src/services/session/index.ts +97 -16
- package/src/services/session/recent.ts +13 -0
- package/src/services/webauthn.ts +12 -0
- package/src/shared/email-html.ts +2 -0
- package/src/shared/help.ts +3 -0
- package/src/shared-text/cursor.ts +22 -0
- package/src/shared-text/documents.ts +142 -0
- package/src/shared-text/events.ts +43 -0
- package/src/shared-text/index.ts +12 -0
- package/src/shared-text/log.ts +48 -0
- package/src/shared-text/text-diff.ts +23 -0
- package/src/shared-text/types.ts +41 -0
- package/src/ssr/AdminLayout.tsx +2 -0
- package/src/ssr/AppLaunchpadPanel.tsx +2 -2
- package/src/ssr/CloudShell.island.tsx +10 -2
- package/src/ssr/Layout.tsx +25 -2
- package/src/ssr/LayoutHelp.browser-harness.tsx +2 -0
- package/src/ssr/LayoutHelp.tsx +435 -274
- package/src/ssr/LayoutHelpPanel.browser-harness.tsx +136 -0
- package/src/ssr/LayoutHelpTrigger.tsx +27 -2
- package/src/ssr/MobileNavigation.tsx +2 -2
- package/src/ssr/PanelLayout.tsx +9 -1
- package/src/ssr/PanelRuntime.island.tsx +7 -0
- package/src/ssr/PanelSlot.tsx +53 -3
- package/src/ssr/ShellRuntime.island.tsx +11 -2
- package/src/ssr/app-color.ts +14 -9
- package/src/ssr/app-navigation.ts +16 -3
- package/src/ssr/compact-shell.browser-harness.ts +30 -22
- package/src/ssr/dialog-app-colors.browser-harness.tsx +43 -0
- package/src/ssr/help-messages.ts +4 -0
- package/src/ssr/help.ts +20 -0
- package/src/ssr/hints.ts +25 -0
- package/src/ssr/index.ts +2 -0
- package/src/ssr/islands/index.ts +4 -3
- package/src/ssr/layout-context.ts +2 -0
- package/src/ssr/layout-help-article.server.ts +50 -0
- package/src/ssr/layout-help-start.ts +33 -7
- package/src/ssr/panel-consent.ts +29 -0
- package/src/ssr/panel-context.ts +10 -0
- package/src/ssr/panel-slot.browser-harness.tsx +53 -4
- package/src/ssr/platform-messages.ts +18 -0
- package/src/ssr/shell-data.tsx +2 -1
- package/src/styles/effects.css +198 -9
- package/src/styles/file-chooser.css +2 -1
- package/src/styles/global.css +49 -12
- package/src/styles/panel.css +47 -4
- package/src/styles/resource-search.css +6 -2
- package/src/styles/tokens.css +17 -11
- package/src/styles/utilities-buttons.css +3 -2
- package/src/styles/utilities-data.css +2 -1
- package/src/styles/utilities-feedback.css +2 -2
- package/src/styles/utilities-layout.css +3 -13
- package/src/styles/utilities-markdown-editor.css +6 -6
- package/src/styles/utilities-navigation.css +17 -22
|
@@ -0,0 +1,669 @@
|
|
|
1
|
+
import { dates, fileIcons } from "@k2b/stdlib";
|
|
2
|
+
import {
|
|
3
|
+
Button,
|
|
4
|
+
Chat,
|
|
5
|
+
type ChatDecisionState,
|
|
6
|
+
type ChatOutcomeLine,
|
|
7
|
+
isStructuredDataValue,
|
|
8
|
+
LocaleProvider,
|
|
9
|
+
MarkdownView,
|
|
10
|
+
ReferenceChip,
|
|
11
|
+
StructuredDataPreview,
|
|
12
|
+
} from "@k2b/ui";
|
|
13
|
+
import { Lexer, type Token, type Tokens } from "marked";
|
|
14
|
+
import { createMemo, createSignal, createUniqueId, For, type JSX, Match, Show, Switch } from "solid-js";
|
|
15
|
+
import type { CapabilityPerson, CapabilityPreview } from "../../contracts/capabilities";
|
|
16
|
+
import { formatBytes } from "../../shared/format";
|
|
17
|
+
import type { ActionCard, ActionChip, ActionPreviewModel, ActionText } from "./action-card";
|
|
18
|
+
import { actionMessages } from "./action-messages";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The look of every Assistant question and its outcome (card v4): one sentence with chips, Cloud's reason line, the
|
|
22
|
+
* thing itself, two answers that name deed and target, and after the answer one outcome line in the same place. The
|
|
23
|
+
* chat, the approvals page, and Code Mode render the same components from an `ActionCard`.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
// Sentence
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
function PersonChips(props: { chip: Extract<ActionChip, { kind: "person" }> }) {
|
|
31
|
+
// List words such as ", " and " und " come from the locale; each name is its own chip, the rest stays text.
|
|
32
|
+
const parts = createMemo(() => {
|
|
33
|
+
const names = props.chip.names;
|
|
34
|
+
const items =
|
|
35
|
+
props.chip.rest > 0 ? [...names.map((_, index) => `\u0000${index}`), props.chip.more] : names.map((_, index) => `\u0000${index}`);
|
|
36
|
+
return new Intl.ListFormat(props.chip.locale, { type: "conjunction" }).formatToParts(items).map((part) => {
|
|
37
|
+
const match = part.type === "element" ? /^\u0000(\d+)$/.exec(part.value) : null;
|
|
38
|
+
return match ? { name: names[Number(match[1])]! } : { text: part.value };
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
return <For each={parts()}>{(part) => ("name" in part ? <ReferenceChip kind="person" name={part.name ?? ""} /> : part.text)}</For>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** A sentence of text and typed chips, in the app's colour. */
|
|
45
|
+
export function ActionTextView(props: { text: ActionText; accent?: string }) {
|
|
46
|
+
return (
|
|
47
|
+
<For each={props.text}>
|
|
48
|
+
{(part) => (
|
|
49
|
+
<Switch fallback={part.kind === "text" ? part.text : null}>
|
|
50
|
+
<Match when={part.kind === "chip" ? part.chip : undefined}>
|
|
51
|
+
{(chip) => (
|
|
52
|
+
<Switch>
|
|
53
|
+
<Match when={chip().kind === "person" ? (chip() as Extract<ActionChip, { kind: "person" }>) : undefined}>
|
|
54
|
+
{(person) => <PersonChips chip={person()} />}
|
|
55
|
+
</Match>
|
|
56
|
+
<Match when={chip().kind === "time" ? (chip() as Extract<ActionChip, { kind: "time" }>) : undefined}>
|
|
57
|
+
{(time) => <ReferenceChip kind="time" label={time().label} dateTime={time().dateTime} />}
|
|
58
|
+
</Match>
|
|
59
|
+
<Match
|
|
60
|
+
when={
|
|
61
|
+
chip().kind === "object" || chip().kind === "place"
|
|
62
|
+
? (chip() as Extract<ActionChip, { kind: "object" | "place" }>)
|
|
63
|
+
: undefined
|
|
64
|
+
}
|
|
65
|
+
>
|
|
66
|
+
{(thing) => (
|
|
67
|
+
<ReferenceChip
|
|
68
|
+
kind={thing().kind}
|
|
69
|
+
title={thing().title}
|
|
70
|
+
href={thing().href}
|
|
71
|
+
icon={thing().icon}
|
|
72
|
+
accent={props.accent}
|
|
73
|
+
gone={thing().gone}
|
|
74
|
+
/>
|
|
75
|
+
)}
|
|
76
|
+
</Match>
|
|
77
|
+
</Switch>
|
|
78
|
+
)}
|
|
79
|
+
</Match>
|
|
80
|
+
</Switch>
|
|
81
|
+
)}
|
|
82
|
+
</For>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
// Preview
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
|
|
90
|
+
const DAY = /^\d{4}-\d{2}-\d{2}$/;
|
|
91
|
+
|
|
92
|
+
/** A calendar day stays that day everywhere; an instant shows in the reader's zone. */
|
|
93
|
+
const formatWhen = (value: string, locale: string, timeZone: string, withTime = true): string =>
|
|
94
|
+
DAY.test(value)
|
|
95
|
+
? dates.formatDate(`${value}T12:00:00.000Z`, { locale, timeZone: "UTC" })
|
|
96
|
+
: withTime
|
|
97
|
+
? dates.formatDateTime(value, { locale, timeZone })
|
|
98
|
+
: dates.formatDate(value, { locale, timeZone });
|
|
99
|
+
|
|
100
|
+
const rangeText = (start: string, end: string | undefined, locale: string, timeZone: string, allDay?: boolean): string => {
|
|
101
|
+
const from = formatWhen(start, locale, timeZone, !allDay);
|
|
102
|
+
if (!end) return from;
|
|
103
|
+
// An all-day event never shows an end time, even when its bounds are instants.
|
|
104
|
+
const sameDay =
|
|
105
|
+
!allDay &&
|
|
106
|
+
!DAY.test(start) &&
|
|
107
|
+
!DAY.test(end) &&
|
|
108
|
+
formatWhen(start, locale, timeZone, false) === formatWhen(end, locale, timeZone, false);
|
|
109
|
+
const to = sameDay ? dates.formatTime(end, { locale, timeZone }) : formatWhen(end, locale, timeZone, !allDay);
|
|
110
|
+
return to === from ? from : `${from} – ${to}`;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const personRow = (person: CapabilityPerson) => ({
|
|
114
|
+
...(person.name ? { name: person.name } : {}),
|
|
115
|
+
...(person.address ? { address: person.address } : {}),
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
/** Block tokens end their line, so the excerpt keeps the text's lines. */
|
|
119
|
+
const MARKDOWN_BLOCKS = new Set(["heading", "paragraph", "code", "blockquote", "html", "hr"]);
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* The words of Markdown as the parser reads them: marks dropped, every literal character kept (an intraword `_` is
|
|
123
|
+
* text), and link targets kept, so a link never hides where it goes.
|
|
124
|
+
*/
|
|
125
|
+
const markdownWords = (tokens: readonly Token[]): string =>
|
|
126
|
+
tokens
|
|
127
|
+
.map((token): string => {
|
|
128
|
+
switch (token.type) {
|
|
129
|
+
case "space":
|
|
130
|
+
return "";
|
|
131
|
+
case "link": {
|
|
132
|
+
const text = markdownWords(token.tokens ?? []);
|
|
133
|
+
return text === token.href ? token.href : `${text} (${token.href})`;
|
|
134
|
+
}
|
|
135
|
+
case "image":
|
|
136
|
+
return token.text;
|
|
137
|
+
case "br":
|
|
138
|
+
return "\n";
|
|
139
|
+
case "list":
|
|
140
|
+
return `${(token as Tokens.List).items.map((item) => markdownWords(item.tokens).trimEnd()).join("\n")}\n`;
|
|
141
|
+
case "table": {
|
|
142
|
+
const table = token as Tokens.Table;
|
|
143
|
+
return `${[table.header, ...table.rows].map((row) => row.map((cell) => markdownWords(cell.tokens)).join(" · ")).join("\n")}\n`;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
const words = "tokens" in token && token.tokens ? markdownWords(token.tokens) : "text" in token ? String(token.text) : token.raw;
|
|
147
|
+
return MARKDOWN_BLOCKS.has(token.type) ? `${words.trimEnd()}\n` : words;
|
|
148
|
+
})
|
|
149
|
+
.join("");
|
|
150
|
+
|
|
151
|
+
/** About what one line holds in the narrowest column a card stands in, a 20 rem panel. */
|
|
152
|
+
const EXCERPT_LINE_CHARS = 40;
|
|
153
|
+
|
|
154
|
+
/** Whether a text may need more than the excerpt's three lines; counted alike on the server and in the browser. */
|
|
155
|
+
const mayOverflow = (text: string): boolean =>
|
|
156
|
+
text.split("\n").reduce((lines, line) => lines + Math.max(1, Math.ceil(line.length / EXCERPT_LINE_CHARS)), 0) > 3;
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* The first lines of a text. With the app's link, its "read" link follows the preview; without one, a text that may
|
|
160
|
+
* not fit offers to show the rest in place, and any other text stands whole, so nothing a person approves stays out of
|
|
161
|
+
* reach even where the estimate is short, such as for wide characters.
|
|
162
|
+
*/
|
|
163
|
+
function PreviewExcerpt(props: { text: string; readMore?: string; label: string }) {
|
|
164
|
+
const id = createUniqueId();
|
|
165
|
+
const [expanded, setExpanded] = createSignal(false);
|
|
166
|
+
const offer = () => !props.readMore && mayOverflow(props.text);
|
|
167
|
+
return (
|
|
168
|
+
<>
|
|
169
|
+
<Chat.Preview.Excerpt id={id} expanded={expanded() || (!props.readMore && !offer())}>
|
|
170
|
+
{props.text}
|
|
171
|
+
</Chat.Preview.Excerpt>
|
|
172
|
+
<Show when={offer() && !expanded()}>
|
|
173
|
+
<Chat.Preview.ShowAll controls={id} onShow={() => setExpanded(true)}>
|
|
174
|
+
{props.label}
|
|
175
|
+
</Chat.Preview.ShowAll>
|
|
176
|
+
</Show>
|
|
177
|
+
</>
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** A short excerpt of a body: plain text as it is, Markdown as its words. */
|
|
182
|
+
const excerptText = (body: string, format: "plain" | "markdown"): string =>
|
|
183
|
+
format === "plain" ? body : markdownWords(Lexer.lex(body)).trimEnd();
|
|
184
|
+
|
|
185
|
+
const peopleNames = (people: readonly CapabilityPerson[], locale: string, more = 0): string => {
|
|
186
|
+
const names = people.map((person) => person.name ?? person.address ?? "");
|
|
187
|
+
return new Intl.ListFormat(locale, { type: "conjunction" }).format(
|
|
188
|
+
more > 0 ? [...names, actionMessages(locale).moreCount({ count: more })] : names,
|
|
189
|
+
);
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
const attachmentLook = (name: string) => {
|
|
193
|
+
// The icon name only: its colour comes from the shared file-type tokens, not from a utility class.
|
|
194
|
+
const glyph = fileIcons.getFileIcon({ name, type: "file" }).split(/\s+/)[0] ?? "ti-file";
|
|
195
|
+
return { icon: `ti ${glyph}`, accent: glyph.includes("pdf") ? "var(--k2b-file-type-pdf)" : undefined };
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
/** An app's warning leads with its first short sentence in bold, such as "1 external recipient." */
|
|
199
|
+
function WarningText(props: { text: string }) {
|
|
200
|
+
const split = () => {
|
|
201
|
+
const match = /^(.{1,80}?[.!?])\s+(\S.*)$/s.exec(props.text);
|
|
202
|
+
return match ? { lead: match[1]!, rest: match[2]! } : null;
|
|
203
|
+
};
|
|
204
|
+
return (
|
|
205
|
+
<Show when={split()} fallback={props.text}>
|
|
206
|
+
{(parts) => (
|
|
207
|
+
<>
|
|
208
|
+
<b>{parts().lead}</b> {parts().rest}
|
|
209
|
+
</>
|
|
210
|
+
)}
|
|
211
|
+
</Show>
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function MessagePreview(props: {
|
|
216
|
+
preview: Extract<CapabilityPreview, { kind: "message" }>;
|
|
217
|
+
locale: string;
|
|
218
|
+
timeZone: string;
|
|
219
|
+
readMore?: string;
|
|
220
|
+
}) {
|
|
221
|
+
const t = () => actionMessages(props.locale);
|
|
222
|
+
const rows = () => {
|
|
223
|
+
const p = props.preview;
|
|
224
|
+
const more = (count?: number) => (count ? { value: <> {t().andMore({ count })}</> } : {});
|
|
225
|
+
return [
|
|
226
|
+
...(p.from ? [{ label: t().from, people: [personRow(p.from)] }] : []),
|
|
227
|
+
{ label: t().to, people: p.to.map(personRow), ...more(p.more?.to) },
|
|
228
|
+
...(p.cc?.length || p.more?.cc ? [{ label: t().cc, people: (p.cc ?? []).map(personRow), ...more(p.more?.cc) }] : []),
|
|
229
|
+
...(p.bcc?.length || p.more?.bcc
|
|
230
|
+
? [{ label: t().bcc, people: (p.bcc ?? []).map(personRow), emphasis: true, ...more(p.more?.bcc) }]
|
|
231
|
+
: []),
|
|
232
|
+
];
|
|
233
|
+
};
|
|
234
|
+
const note = () => {
|
|
235
|
+
const p = props.preview;
|
|
236
|
+
if (p.sendAt) {
|
|
237
|
+
const [before, after] = t().sendsAt({ at: "\u0000" }).split("\u0000");
|
|
238
|
+
return (
|
|
239
|
+
<>
|
|
240
|
+
{before}
|
|
241
|
+
<b>{formatWhen(p.sendAt, props.locale, props.timeZone)}</b>
|
|
242
|
+
{after}
|
|
243
|
+
</>
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
return p.cc?.length || p.bcc?.length || p.more?.cc || p.more?.bcc ? t().sendsNowWithCopies : t().sendsNow;
|
|
247
|
+
};
|
|
248
|
+
return (
|
|
249
|
+
<>
|
|
250
|
+
<Chat.Preview.Header rows={rows()} note={note()} />
|
|
251
|
+
<Chat.Preview.Title>{props.preview.subject || t().noSubject}</Chat.Preview.Title>
|
|
252
|
+
<Show when={props.preview.body.trim()}>
|
|
253
|
+
<PreviewExcerpt text={excerptText(props.preview.body, props.preview.format)} readMore={props.readMore} label={t().readMail} />
|
|
254
|
+
</Show>
|
|
255
|
+
<For each={props.preview.attachments ?? []}>
|
|
256
|
+
{(attachment) => (
|
|
257
|
+
<Chat.Preview.Line
|
|
258
|
+
icon={attachmentLook(attachment.name).icon}
|
|
259
|
+
accent={attachmentLook(attachment.name).accent}
|
|
260
|
+
meta={attachment.size !== undefined ? formatBytes(attachment.size, { locale: props.locale }) : undefined}
|
|
261
|
+
>
|
|
262
|
+
{attachment.name}
|
|
263
|
+
</Chat.Preview.Line>
|
|
264
|
+
)}
|
|
265
|
+
</For>
|
|
266
|
+
<Show when={props.preview.moreAttachments}>
|
|
267
|
+
{(count) => (
|
|
268
|
+
<Chat.Preview.Line icon="ti ti-paperclip" tone="muted">
|
|
269
|
+
{t().moreAttachments({ count: count() })}
|
|
270
|
+
</Chat.Preview.Line>
|
|
271
|
+
)}
|
|
272
|
+
</Show>
|
|
273
|
+
<For each={props.preview.warnings ?? []}>
|
|
274
|
+
{(warning) => (
|
|
275
|
+
<Chat.Preview.Line icon="ti ti-alert-triangle" tone="warning">
|
|
276
|
+
<WarningText text={warning} />
|
|
277
|
+
</Chat.Preview.Line>
|
|
278
|
+
)}
|
|
279
|
+
</For>
|
|
280
|
+
<Show when={props.readMore}>
|
|
281
|
+
{(href) => (
|
|
282
|
+
<Chat.Preview.ReadMore href={href()} title={t().readMoreTitle}>
|
|
283
|
+
{t().readMail}
|
|
284
|
+
</Chat.Preview.ReadMore>
|
|
285
|
+
)}
|
|
286
|
+
</Show>
|
|
287
|
+
</>
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function ReviewPreviewBody(props: { model: Extract<ActionPreviewModel, { kind: "review" }>; card: ActionCard; readMore?: string }) {
|
|
292
|
+
const t = () => actionMessages(props.model.locale);
|
|
293
|
+
const locale = () => props.model.locale;
|
|
294
|
+
const tz = () => props.model.timeZone;
|
|
295
|
+
const markIcon = (mark?: "added" | "removed") => (mark === "added" ? "ti ti-plus" : mark === "removed" ? "ti ti-minus" : "ti ti-point");
|
|
296
|
+
const readMore = (label: string) => (
|
|
297
|
+
<Show when={props.readMore}>
|
|
298
|
+
{(href) => (
|
|
299
|
+
<Chat.Preview.ReadMore href={href()} title={t().readMoreTitle}>
|
|
300
|
+
{label}
|
|
301
|
+
</Chat.Preview.ReadMore>
|
|
302
|
+
)}
|
|
303
|
+
</Show>
|
|
304
|
+
);
|
|
305
|
+
const preview = () => props.model.preview;
|
|
306
|
+
return (
|
|
307
|
+
<Chat.Preview>
|
|
308
|
+
<Switch>
|
|
309
|
+
<Match when={preview()?.kind === "message" ? (preview() as Extract<CapabilityPreview, { kind: "message" }>) : undefined}>
|
|
310
|
+
{(message) => <MessagePreview preview={message()} locale={locale()} timeZone={tz()} readMore={props.readMore} />}
|
|
311
|
+
</Match>
|
|
312
|
+
<Match when={preview()?.kind === "event" ? (preview() as Extract<CapabilityPreview, { kind: "event" }>) : undefined}>
|
|
313
|
+
{(event) => (
|
|
314
|
+
<>
|
|
315
|
+
<Chat.Preview.Title>{event().title}</Chat.Preview.Title>
|
|
316
|
+
{/* When, where, and in which calendar, as one line of text like the person would say it. Times show in the
|
|
317
|
+
reader's zone, the same as the sentence's chips, so the question and the preview never disagree. */}
|
|
318
|
+
<p>
|
|
319
|
+
{[
|
|
320
|
+
`${rangeText(event().start, event().end, locale(), tz(), event().allDay)}${
|
|
321
|
+
event().allDay && !DAY.test(event().start) ? ` · ${t().allDay}` : ""
|
|
322
|
+
}`,
|
|
323
|
+
event().location,
|
|
324
|
+
event().place,
|
|
325
|
+
]
|
|
326
|
+
.filter(Boolean)
|
|
327
|
+
.join(", ")}
|
|
328
|
+
</p>
|
|
329
|
+
<Show when={event().recurrence}>
|
|
330
|
+
{(recurrence) => <Chat.Preview.Line icon="ti ti-repeat">{recurrence()}</Chat.Preview.Line>}
|
|
331
|
+
</Show>
|
|
332
|
+
<Show when={event().attendees?.length ? event().attendees : undefined}>
|
|
333
|
+
{(attendees) => (
|
|
334
|
+
<Chat.Preview.People
|
|
335
|
+
people={attendees()
|
|
336
|
+
.slice(0, 3)
|
|
337
|
+
.map((person) => ({ name: person.name ?? person.address ?? "" }))}
|
|
338
|
+
>
|
|
339
|
+
{peopleNames(attendees().slice(0, 3), locale(), attendees().length - 3)}
|
|
340
|
+
</Chat.Preview.People>
|
|
341
|
+
)}
|
|
342
|
+
</Show>
|
|
343
|
+
<Show when={event().description?.trim()}>
|
|
344
|
+
{(text) => <PreviewExcerpt text={text()} readMore={props.readMore} label={t().readMore} />}
|
|
345
|
+
</Show>
|
|
346
|
+
{readMore(t().readMore)}
|
|
347
|
+
</>
|
|
348
|
+
)}
|
|
349
|
+
</Match>
|
|
350
|
+
<Match when={preview()?.kind === "task" ? (preview() as Extract<CapabilityPreview, { kind: "task" }>) : undefined}>
|
|
351
|
+
{(task) => (
|
|
352
|
+
<>
|
|
353
|
+
<Chat.Preview.Title meta={task().place}>{task().title}</Chat.Preview.Title>
|
|
354
|
+
<Show when={task().due}>
|
|
355
|
+
{(due) => (
|
|
356
|
+
<Chat.Preview.Line icon="ti ti-calendar-due">{t().due({ at: formatWhen(due(), locale(), tz()) })}</Chat.Preview.Line>
|
|
357
|
+
)}
|
|
358
|
+
</Show>
|
|
359
|
+
<Show when={task().status}>{(status) => <Chat.Preview.Line icon="ti ti-progress">{status()}</Chat.Preview.Line>}</Show>
|
|
360
|
+
<Show when={task().assignees?.length ? task().assignees : undefined}>
|
|
361
|
+
{(assignees) => (
|
|
362
|
+
<Chat.Preview.People
|
|
363
|
+
people={assignees()
|
|
364
|
+
.slice(0, 3)
|
|
365
|
+
.map((person) => ({ name: person.name ?? person.address ?? "" }))}
|
|
366
|
+
>
|
|
367
|
+
{peopleNames(assignees().slice(0, 3), locale(), assignees().length - 3)}
|
|
368
|
+
</Chat.Preview.People>
|
|
369
|
+
)}
|
|
370
|
+
</Show>
|
|
371
|
+
<Show when={task().checklist?.length ? task().checklist : undefined}>
|
|
372
|
+
{(checklist) => (
|
|
373
|
+
<Chat.Preview.Lines>
|
|
374
|
+
<For each={checklist()}>
|
|
375
|
+
{(item) => (
|
|
376
|
+
<Chat.Preview.Line icon={item.done ? "ti ti-square-check" : "ti ti-square"}>{item.label}</Chat.Preview.Line>
|
|
377
|
+
)}
|
|
378
|
+
</For>
|
|
379
|
+
</Chat.Preview.Lines>
|
|
380
|
+
)}
|
|
381
|
+
</Show>
|
|
382
|
+
<Show when={task().tags?.length ? task().tags : undefined}>
|
|
383
|
+
{(tags) => (
|
|
384
|
+
<Chat.Preview.Line icon="ti ti-tag" tone="muted">
|
|
385
|
+
{tags().join(", ")}
|
|
386
|
+
</Chat.Preview.Line>
|
|
387
|
+
)}
|
|
388
|
+
</Show>
|
|
389
|
+
<Show when={task().description?.trim()}>
|
|
390
|
+
{(text) => <PreviewExcerpt text={text()} readMore={props.readMore} label={t().readMore} />}
|
|
391
|
+
</Show>
|
|
392
|
+
{readMore(t().readMore)}
|
|
393
|
+
</>
|
|
394
|
+
)}
|
|
395
|
+
</Match>
|
|
396
|
+
<Match when={preview()?.kind === "page" ? (preview() as Extract<CapabilityPreview, { kind: "page" }>) : undefined}>
|
|
397
|
+
{(page) => (
|
|
398
|
+
<>
|
|
399
|
+
<Chat.Preview.Title meta={page().place}>{page().title}</Chat.Preview.Title>
|
|
400
|
+
<Show when={page().body.trim()}>
|
|
401
|
+
<PreviewExcerpt text={excerptText(page().body, page().format)} readMore={props.readMore} label={t().readPage} />
|
|
402
|
+
</Show>
|
|
403
|
+
<Show when={page().changes?.length ? page().changes : undefined}>
|
|
404
|
+
{(changes) => (
|
|
405
|
+
<Chat.Preview.Lines label={t().changes}>
|
|
406
|
+
<For each={changes()}>
|
|
407
|
+
{(change) => (
|
|
408
|
+
<Chat.Preview.Line icon={markIcon(change.change)} mark={change.change}>
|
|
409
|
+
{change.text}
|
|
410
|
+
</Chat.Preview.Line>
|
|
411
|
+
)}
|
|
412
|
+
</For>
|
|
413
|
+
</Chat.Preview.Lines>
|
|
414
|
+
)}
|
|
415
|
+
</Show>
|
|
416
|
+
{readMore(t().readPage)}
|
|
417
|
+
</>
|
|
418
|
+
)}
|
|
419
|
+
</Match>
|
|
420
|
+
<Match when={preview()?.kind === "file" ? (preview() as Extract<CapabilityPreview, { kind: "file" }>) : undefined}>
|
|
421
|
+
{(file) => (
|
|
422
|
+
<>
|
|
423
|
+
<Chat.Preview.Title
|
|
424
|
+
meta={
|
|
425
|
+
[file().size !== undefined ? formatBytes(file().size, { locale: locale() }) : "", file().place ?? file().path ?? ""]
|
|
426
|
+
.filter(Boolean)
|
|
427
|
+
.join(" · ") || undefined
|
|
428
|
+
}
|
|
429
|
+
>
|
|
430
|
+
{file().name}
|
|
431
|
+
</Chat.Preview.Title>
|
|
432
|
+
<Show when={file().to}>
|
|
433
|
+
{(to) => <Chat.Preview.Line icon="ti ti-arrow-right">{t().moveTo({ to: to() })}</Chat.Preview.Line>}
|
|
434
|
+
</Show>
|
|
435
|
+
{readMore(t().readMore)}
|
|
436
|
+
</>
|
|
437
|
+
)}
|
|
438
|
+
</Match>
|
|
439
|
+
<Match when={preview()?.kind === "rule" ? (preview() as Extract<CapabilityPreview, { kind: "rule" }>) : undefined}>
|
|
440
|
+
{(rule) => (
|
|
441
|
+
<>
|
|
442
|
+
<Chat.Preview.Sentence>
|
|
443
|
+
<ActionTextView text={props.model.when ?? []} accent={props.card.app.accent} />
|
|
444
|
+
</Chat.Preview.Sentence>
|
|
445
|
+
<Show when={rule().does.trim()}>
|
|
446
|
+
<PreviewExcerpt text={rule().does} readMore={props.readMore} label={t().readRule} />
|
|
447
|
+
</Show>
|
|
448
|
+
{readMore(t().readRule)}
|
|
449
|
+
<Chat.Preview.Lines label={rule().seesChat ? t().changes : t().ruleMay}>
|
|
450
|
+
<For each={props.model.may ?? []}>
|
|
451
|
+
{(line) => (
|
|
452
|
+
<Chat.Preview.Line icon={line.mark === "removed" ? "ti ti-minus" : "ti ti-check"} mark={line.mark}>
|
|
453
|
+
<ActionTextView text={line.text} accent={props.card.app.accent} />
|
|
454
|
+
</Chat.Preview.Line>
|
|
455
|
+
)}
|
|
456
|
+
</For>
|
|
457
|
+
{/* What a routine cannot know is Cloud's fixed line; a Skill that works inside chats needs none. */}
|
|
458
|
+
<Show when={!rule().seesChat}>
|
|
459
|
+
<Chat.Preview.Line icon="ti ti-info-circle" tone="muted">
|
|
460
|
+
{t().ruleBlindToChat}
|
|
461
|
+
</Chat.Preview.Line>
|
|
462
|
+
</Show>
|
|
463
|
+
</Chat.Preview.Lines>
|
|
464
|
+
</>
|
|
465
|
+
)}
|
|
466
|
+
</Match>
|
|
467
|
+
<Match when={preview()?.kind === "fields" ? (preview() as Extract<CapabilityPreview, { kind: "fields" }>) : undefined}>
|
|
468
|
+
{(fields) => {
|
|
469
|
+
const value = (text: string, format?: "date" | "date-time") =>
|
|
470
|
+
format ? formatWhen(text, locale(), tz(), format === "date-time") : text;
|
|
471
|
+
return (
|
|
472
|
+
<>
|
|
473
|
+
<Chat.Preview.Header
|
|
474
|
+
rows={fields().rows.map((row) => ({
|
|
475
|
+
label: row.label,
|
|
476
|
+
value:
|
|
477
|
+
row.before !== undefined ? (
|
|
478
|
+
<>
|
|
479
|
+
<Chat.Mark change="removed">{value(row.before, row.format)}</Chat.Mark>{" "}
|
|
480
|
+
<Chat.Mark change="added">{value(row.value, row.format)}</Chat.Mark>
|
|
481
|
+
</>
|
|
482
|
+
) : (
|
|
483
|
+
<span class="cloud-action-preview__value">{value(row.value, row.format)}</span>
|
|
484
|
+
),
|
|
485
|
+
}))}
|
|
486
|
+
/>
|
|
487
|
+
{readMore(t().readMore)}
|
|
488
|
+
</>
|
|
489
|
+
);
|
|
490
|
+
}}
|
|
491
|
+
</Match>
|
|
492
|
+
</Switch>
|
|
493
|
+
<Show when={!preview()}>{readMore(t().readMore)}</Show>
|
|
494
|
+
<Show when={props.model.consequences.length > 0}>
|
|
495
|
+
<Chat.Preview.Lines label={t().changes}>
|
|
496
|
+
<For each={props.model.consequences}>
|
|
497
|
+
{(line) => (
|
|
498
|
+
<Chat.Preview.Line icon={markIcon(line.mark)} mark={line.mark}>
|
|
499
|
+
<ActionTextView text={line.text} accent={props.card.app.accent} />
|
|
500
|
+
</Chat.Preview.Line>
|
|
501
|
+
)}
|
|
502
|
+
</For>
|
|
503
|
+
</Chat.Preview.Lines>
|
|
504
|
+
</Show>
|
|
505
|
+
</Chat.Preview>
|
|
506
|
+
);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function LegacyPreview(props: { model: Extract<ActionPreviewModel, { kind: "legacy" }>; card: ActionCard; timeZone: string }) {
|
|
510
|
+
const t = () => actionMessages(props.card.locale);
|
|
511
|
+
const [inputOpen, setInputOpen] = createSignal(false);
|
|
512
|
+
const value = (row: { value: string; format?: "date" | "date-time" }) =>
|
|
513
|
+
row.format ? formatWhen(row.value, props.card.locale, props.timeZone, row.format === "date-time") : row.value;
|
|
514
|
+
const input = () => (isStructuredDataValue(props.model.input) ? props.model.input : undefined);
|
|
515
|
+
return (
|
|
516
|
+
<Chat.Preview class="cloud-action-preview--legacy">
|
|
517
|
+
<Show when={props.model.rows.length > 0}>
|
|
518
|
+
<Chat.Preview.Header
|
|
519
|
+
rows={props.model.rows.map((row) => ({
|
|
520
|
+
label: row.label,
|
|
521
|
+
value: row.format ? (
|
|
522
|
+
<time dateTime={row.value}>{value(row)}</time>
|
|
523
|
+
) : (
|
|
524
|
+
<span class="cloud-action-preview__value">{value(row)}</span>
|
|
525
|
+
),
|
|
526
|
+
}))}
|
|
527
|
+
/>
|
|
528
|
+
</Show>
|
|
529
|
+
<Show when={props.model.markdown}>
|
|
530
|
+
{(markdown) => (
|
|
531
|
+
<MarkdownView class="cloud-action-preview__markdown" markdown={markdown()} headingScale="compact" allowImages={false} />
|
|
532
|
+
)}
|
|
533
|
+
</Show>
|
|
534
|
+
<Show when={props.model.input !== undefined}>
|
|
535
|
+
<div class="cloud-action-preview__input">
|
|
536
|
+
<Button variant="text" size="sm" aria-expanded={inputOpen()} onClick={() => setInputOpen((open) => !open)}>
|
|
537
|
+
{inputOpen() ? t().hideInput : t().showInput}
|
|
538
|
+
</Button>
|
|
539
|
+
<Show when={inputOpen()}>
|
|
540
|
+
<div role="region" aria-label={t().input}>
|
|
541
|
+
<Show when={input()} fallback={<pre class="cloud-action-preview__raw">{JSON.stringify(props.model.input, null, 2)}</pre>}>
|
|
542
|
+
{(data) => <StructuredDataPreview data={data()} class="w-full" />}
|
|
543
|
+
</Show>
|
|
544
|
+
</div>
|
|
545
|
+
</Show>
|
|
546
|
+
</div>
|
|
547
|
+
</Show>
|
|
548
|
+
<Show when={props.card.readMore}>
|
|
549
|
+
{(href) => (
|
|
550
|
+
<Chat.Preview.ReadMore href={href()} title={t().readMoreTitle}>
|
|
551
|
+
{t().readMore}
|
|
552
|
+
</Chat.Preview.ReadMore>
|
|
553
|
+
)}
|
|
554
|
+
</Show>
|
|
555
|
+
</Chat.Preview>
|
|
556
|
+
);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/** The thing the question is about, from its review, or an old review's rows and Markdown. */
|
|
560
|
+
export function ActionPreview(props: { card: ActionCard; timeZone: string }) {
|
|
561
|
+
return (
|
|
562
|
+
<>
|
|
563
|
+
<Show when={props.card.preview}>
|
|
564
|
+
{(model) => (
|
|
565
|
+
<Switch>
|
|
566
|
+
<Match when={model().kind === "review" ? (model() as Extract<ActionPreviewModel, { kind: "review" }>) : undefined}>
|
|
567
|
+
{(review) => <ReviewPreviewBody model={review()} card={props.card} readMore={props.card.readMore} />}
|
|
568
|
+
</Match>
|
|
569
|
+
<Match when={model().kind === "legacy" ? (model() as Extract<ActionPreviewModel, { kind: "legacy" }>) : undefined}>
|
|
570
|
+
{(legacy) => <LegacyPreview model={legacy()} card={props.card} timeZone={props.timeZone} />}
|
|
571
|
+
</Match>
|
|
572
|
+
</Switch>
|
|
573
|
+
)}
|
|
574
|
+
</Show>
|
|
575
|
+
{/* The review's other links, such as editing the draft, stay one quiet click away before the answer. */}
|
|
576
|
+
<Show when={props.card.links.length > 0 || (!props.card.preview && props.card.readMore)}>
|
|
577
|
+
<Chat.Preview class="cloud-action-preview__links">
|
|
578
|
+
<Show when={!props.card.preview && props.card.readMore}>
|
|
579
|
+
{(href) => (
|
|
580
|
+
<Chat.Preview.ReadMore href={href()} title={actionMessages(props.card.locale).readMoreTitle}>
|
|
581
|
+
{actionMessages(props.card.locale).readMore}
|
|
582
|
+
</Chat.Preview.ReadMore>
|
|
583
|
+
)}
|
|
584
|
+
</Show>
|
|
585
|
+
<For each={props.card.links}>{(link) => <Chat.Preview.ReadMore href={link.href}>{link.label}</Chat.Preview.ReadMore>}</For>
|
|
586
|
+
</Chat.Preview>
|
|
587
|
+
</Show>
|
|
588
|
+
</>
|
|
589
|
+
);
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// ---------------------------------------------------------------------------
|
|
593
|
+
// Outcome and question
|
|
594
|
+
// ---------------------------------------------------------------------------
|
|
595
|
+
|
|
596
|
+
/** One outcome line: what happened, in the card's sentence, with Cloud's detail under it. */
|
|
597
|
+
export type ActionLine = Omit<ChatOutcomeLine, "text" | "detail"> & { text: ActionText; detail?: JSX.Element };
|
|
598
|
+
|
|
599
|
+
/**
|
|
600
|
+
* The outcome line of one call. Pass every line one flow can reach (running and done, or the lines of a check): they
|
|
601
|
+
* share one cell, so switching between them moves nothing.
|
|
602
|
+
*/
|
|
603
|
+
export function ActionReceipt(props: {
|
|
604
|
+
card: ActionCard;
|
|
605
|
+
lines: Readonly<Record<string, ActionLine>>;
|
|
606
|
+
current: string;
|
|
607
|
+
class?: string;
|
|
608
|
+
anchorId?: string | number;
|
|
609
|
+
}) {
|
|
610
|
+
const lines = createMemo(() =>
|
|
611
|
+
Object.fromEntries(
|
|
612
|
+
Object.entries(props.lines).map(([key, line]) => [
|
|
613
|
+
key,
|
|
614
|
+
{ ...line, text: <ActionTextView text={line.text} accent={props.card.app.accent} /> } satisfies ChatOutcomeLine,
|
|
615
|
+
]),
|
|
616
|
+
),
|
|
617
|
+
);
|
|
618
|
+
return (
|
|
619
|
+
<LocaleProvider locale={props.card.locale}>
|
|
620
|
+
<Chat.Outcome class={`cloud-action-outcome ${props.class ?? ""}`} lines={lines()} current={props.current} anchorId={props.anchorId} />
|
|
621
|
+
</LocaleProvider>
|
|
622
|
+
);
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
export type ActionQuestionProps = {
|
|
626
|
+
card: ActionCard;
|
|
627
|
+
/** `open` shows the question; every other state shows `outcome` in its place. */
|
|
628
|
+
state: ChatDecisionState;
|
|
629
|
+
/** Usually `ActionReceipt`. */
|
|
630
|
+
outcome: JSX.Element;
|
|
631
|
+
timeZone: string;
|
|
632
|
+
onConfirm: () => void | Promise<void>;
|
|
633
|
+
onDecline: () => void | Promise<void>;
|
|
634
|
+
/** The quiet third answer, where remembering for this chat exists. */
|
|
635
|
+
remember?: { label: string; onSelect: () => void | Promise<void> };
|
|
636
|
+
/** Turns the confirming answers off; this reason replaces the asking reason in its place. */
|
|
637
|
+
unavailable?: string;
|
|
638
|
+
anchorId?: string | number;
|
|
639
|
+
class?: string;
|
|
640
|
+
};
|
|
641
|
+
|
|
642
|
+
const confirmIcon = (card: ActionCard): string | undefined =>
|
|
643
|
+
card.reason.code === "sends" ? "ti ti-send" : card.confirm.variant === "danger" ? "ti ti-trash" : undefined;
|
|
644
|
+
|
|
645
|
+
/** One Assistant question in card v4. An answer collapses it at once to its outcome line, which takes the focus. */
|
|
646
|
+
export function ActionQuestion(props: ActionQuestionProps) {
|
|
647
|
+
return (
|
|
648
|
+
<LocaleProvider locale={props.card.locale}>
|
|
649
|
+
<Chat.Decision
|
|
650
|
+
class={`cloud-action-question ${props.class ?? ""}`}
|
|
651
|
+
state={props.state}
|
|
652
|
+
anchorId={props.anchorId}
|
|
653
|
+
sentence={<ActionTextView text={props.card.question} accent={props.card.app.accent} />}
|
|
654
|
+
reason={{ text: props.card.reason.text, icon: props.card.reason.icon, tone: props.card.reason.tone }}
|
|
655
|
+
unavailable={props.unavailable ? { text: props.unavailable, icon: "ti ti-player-pause", tone: "neutral" } : undefined}
|
|
656
|
+
preview={<ActionPreview card={props.card} timeZone={props.timeZone} />}
|
|
657
|
+
confirm={{
|
|
658
|
+
label: props.card.confirm.label,
|
|
659
|
+
variant: props.card.confirm.variant,
|
|
660
|
+
icon: confirmIcon(props.card),
|
|
661
|
+
onSelect: props.onConfirm,
|
|
662
|
+
}}
|
|
663
|
+
decline={{ label: props.card.decline, onSelect: props.onDecline }}
|
|
664
|
+
remember={props.remember}
|
|
665
|
+
outcome={props.outcome}
|
|
666
|
+
/>
|
|
667
|
+
</LocaleProvider>
|
|
668
|
+
);
|
|
669
|
+
}
|