@k2b/cloud 0.44.0 → 0.46.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.
Files changed (137) hide show
  1. package/package.json +9 -2
  2. package/src/_internal/app-queries.ts +73 -0
  3. package/src/_internal/capabilities.ts +137 -9
  4. package/src/_internal/define-app.ts +2 -0
  5. package/src/_internal/registry-validation.ts +17 -4
  6. package/src/_internal/registry.ts +32 -4
  7. package/src/_internal/resource-principals.ts +36 -0
  8. package/src/access/GroupCoverage.tsx +20 -5
  9. package/src/access/PermissionEditor.tsx +11 -2
  10. package/src/access/PrincipalAvatar.tsx +30 -21
  11. package/src/access/messages.ts +4 -4
  12. package/src/account/EntitySearch.tsx +12 -0
  13. package/src/ai/capabilities.ts +4 -2
  14. package/src/ai/chat/map-messages.ts +270 -0
  15. package/src/ai/chat/map-notebook.browser-harness.tsx +28 -0
  16. package/src/ai/chat/map-notebook.tsx +301 -0
  17. package/src/ai/chat/map-widget.browser-harness.tsx +71 -0
  18. package/src/ai/chat/map-widget.css +107 -0
  19. package/src/ai/chat/map-widget.tsx +1279 -0
  20. package/src/ai/chat/message-utils.ts +1 -0
  21. package/src/ai/chat/tool-names.ts +2 -0
  22. package/src/ai/chat/turn-layout.ts +2 -1
  23. package/src/ai/chat/turn-view.tsx +17 -1
  24. package/src/ai/default-tools.ts +3 -0
  25. package/src/ai/dictation-runtime.ts +6 -2
  26. package/src/ai/executor.ts +22 -4
  27. package/src/ai/index.ts +1 -0
  28. package/src/ai/map-contracts.ts +209 -0
  29. package/src/ai/map-execution.ts +14 -0
  30. package/src/ai/map-notebook-save.ts +276 -0
  31. package/src/ai/map-notebook.ts +32 -0
  32. package/src/ai/map-opening-hours.ts +40 -0
  33. package/src/ai/map-routes.ts +84 -0
  34. package/src/ai/map-service.ts +288 -0
  35. package/src/ai/map-store.ts +137 -0
  36. package/src/ai/map-tool.ts +73 -0
  37. package/src/ai/migrate.ts +3 -0
  38. package/src/ai/project-tool.ts +4 -4
  39. package/src/ai/projects-authorization.ts +53 -0
  40. package/src/ai/projects-routes.ts +56 -46
  41. package/src/ai/projects.ts +100 -79
  42. package/src/ai/protocol.ts +3 -1
  43. package/src/ai/recent-resources.ts +3 -3
  44. package/src/ai/routes.ts +32 -30
  45. package/src/ai/skill-tool.ts +4 -4
  46. package/src/ai/skills-routes.ts +27 -26
  47. package/src/ai/skills.ts +120 -81
  48. package/src/ai/store.ts +19 -6
  49. package/src/ai/tools.ts +14 -0
  50. package/src/ai/types.ts +3 -0
  51. package/src/api/accounts-entities.ts +6 -3
  52. package/src/api/admin-ai-skills.ts +4 -1
  53. package/src/api/auth.ts +9 -2
  54. package/src/api/capabilities.ts +90 -12
  55. package/src/api/mcp.ts +4 -2
  56. package/src/browser/notifications.ts +25 -8
  57. package/src/browser/session.ts +39 -0
  58. package/src/browser/shared-text-history-validation.ts +151 -0
  59. package/src/browser/shared-text-history.ts +464 -0
  60. package/src/browser/shared-text-offline-authorized.ts +29 -0
  61. package/src/browser/shared-text-offline.ts +351 -0
  62. package/src/browser/shared-text-provider.ts +714 -0
  63. package/src/browser/shared-text-table-contract.ts +150 -0
  64. package/src/browser/shared-text.ts +19 -0
  65. package/src/capabilities/discussion-providers.ts +99 -0
  66. package/src/capabilities/executions.ts +2 -2
  67. package/src/capabilities/migrate.ts +10 -1
  68. package/src/capabilities/resource-readers.ts +58 -0
  69. package/src/capabilities/server-invoke.ts +23 -13
  70. package/src/capabilities/server.ts +17 -6
  71. package/src/capabilities/testing.ts +13 -0
  72. package/src/contracts/capabilities.ts +137 -4
  73. package/src/contracts/discussions.ts +91 -0
  74. package/src/contracts/geo-document.ts +60 -0
  75. package/src/contracts/geo-snapshot.ts +86 -0
  76. package/src/contracts/geo.ts +14 -0
  77. package/src/contracts/index.ts +2 -0
  78. package/src/contracts/outgoing-mail.ts +1 -1
  79. package/src/contracts/resource-types.ts +21 -0
  80. package/src/contracts/shared.ts +2 -1
  81. package/src/geo/AddressAutocomplete.tsx +213 -0
  82. package/src/geo/messages.ts +29 -0
  83. package/src/geo/navigation.ts +25 -0
  84. package/src/geo/ui.ts +2 -0
  85. package/src/server/api/respond.ts +10 -2
  86. package/src/server/index.ts +1 -0
  87. package/src/server/services/access.ts +23 -10
  88. package/src/server/services/index.ts +1 -0
  89. package/src/services/accounts/app.ts +56 -17
  90. package/src/services/accounts/entities.ts +36 -16
  91. package/src/services/accounts/group-sql.ts +23 -0
  92. package/src/services/accounts/groups.ts +26 -17
  93. package/src/services/accounts/identities.ts +8 -1
  94. package/src/services/accounts/identity-reconciliation.ts +3 -1
  95. package/src/services/accounts/local-groups.ts +141 -27
  96. package/src/services/accounts/machine-group-management.ts +94 -0
  97. package/src/services/accounts/users.ts +38 -8
  98. package/src/services/geo/cache.ts +2 -1
  99. package/src/services/geo/geoapify.ts +3 -0
  100. package/src/services/identity/invocation-token.ts +110 -0
  101. package/src/services/mandates/index.ts +2 -2
  102. package/src/services/mandates/policy.ts +1 -0
  103. package/src/services/pdf/geo-snapshot.ts +107 -0
  104. package/src/services/pdf/index.ts +2 -0
  105. package/src/services/session/index.ts +12 -3
  106. package/src/shared/email-html.ts +1 -0
  107. package/src/shared/markdown/extensions/katex.ts +8 -1
  108. package/src/shared/markdown/extensions/tables.ts +14 -1
  109. package/src/shared/markdown/index.ts +22 -11
  110. package/src/shared-text/anchors.ts +29 -0
  111. package/src/shared-text/awareness.ts +87 -0
  112. package/src/shared-text/changes.ts +27 -0
  113. package/src/shared-text/documents.ts +428 -77
  114. package/src/shared-text/events.ts +12 -2
  115. package/src/shared-text/index.ts +16 -1
  116. package/src/shared-text/log.ts +24 -5
  117. package/src/shared-text/snapshots.ts +69 -23
  118. package/src/shared-text/structural-history.ts +377 -0
  119. package/src/shared-text/table-actions.ts +432 -0
  120. package/src/shared-text/types.ts +24 -5
  121. package/src/ssr/AdminArea.tsx +2 -2
  122. package/src/ssr/AdminLayout.tsx +1 -1
  123. package/src/ssr/AdminSidebar.tsx +1 -1
  124. package/src/ssr/AppLaunchpadPanel.tsx +1 -1
  125. package/src/ssr/CloudShell.island.tsx +19 -3
  126. package/src/ssr/Layout.tsx +1 -1
  127. package/src/ssr/MobileProfileActions.tsx +2 -2
  128. package/src/ssr/ProfilePreferences.tsx +2 -1
  129. package/src/ssr/__snapshots__/Layout.unchanged.render.test.tsx.snap +3 -3
  130. package/src/ssr/admin-navigation.ts +1 -11
  131. package/src/ssr/app-launchpad.tsx +6 -1
  132. package/src/ssr/platform-messages.ts +2 -0
  133. package/src/ssr/profile-actions.ts +3 -6
  134. package/src/ssr/shell-data.tsx +1 -1
  135. package/src/styles/global.css +2 -0
  136. package/src/workflows/language/expressions.ts +3 -2
  137. package/src/workflows/runtime/executor.ts +2 -1
@@ -0,0 +1,301 @@
1
+ import { Button, ButtonLink, ProgressBar, Select, useLocale } from "@k2b/ui";
2
+ import { createEffect, createSignal, onCleanup, onMount, Show } from "solid-js";
3
+ import { z } from "zod";
4
+ import { invokeCapabilityWithDataSchema } from "../../capabilities/client";
5
+ import { cloudResourceHref } from "../../contracts/resource-href";
6
+ import type { AiMap } from "../map-contracts";
7
+ import { type MapNotebookSaveCheckpoint, MapNotebookSaveCheckpointSchema, saveMapNotebook } from "../map-notebook-save";
8
+ import { mapMessages, mapSaveMessage } from "./map-messages";
9
+
10
+ const notebooks = z.array(z.object({ id: z.string(), name: z.string() }));
11
+ const legacySchema = z.object({
12
+ status: z.enum(["pending", "unknown", "saved"]),
13
+ notebookId: z.string().regex(/^[A-Za-z0-9]{6}$/),
14
+ href: z.string().optional(),
15
+ });
16
+ const snapshotSchema = z.object({ content: z.string(), revision: z.number().int(), ownerUserId: z.string().min(1) });
17
+ const storedSchema = z
18
+ .object({
19
+ checkpoint: MapNotebookSaveCheckpointSchema,
20
+ writeId: z.uuid().optional(),
21
+ status: z.enum(["paused", "saved", "cancelled", "conflict", "unknown", "error"]),
22
+ code: z.string().optional(),
23
+ message: z.string().optional(),
24
+ })
25
+ .strict()
26
+ .refine(
27
+ (stored) =>
28
+ stored.status !== "saved" ||
29
+ Boolean(stored.checkpoint.noteId && !stored.checkpoint.pending && stored.checkpoint.completed === stored.checkpoint.total),
30
+ );
31
+
32
+ export function MapNotebookSave(props: { map: AiMap; onCancel: () => void }) {
33
+ const source = { id: props.map.id, conversationId: props.map.conversationId, revision: props.map.revision };
34
+ const sourceChanged = () =>
35
+ props.map.id !== source.id || props.map.conversationId !== source.conversationId || props.map.revision !== source.revision;
36
+ const locale = useLocale();
37
+ const t = () => mapMessages(locale());
38
+ const [selected, setSelected] = createSignal<string | null>(null);
39
+ const [error, setError] = createSignal("");
40
+ const [busy, setBusy] = createSignal(false);
41
+ const [ready, setReady] = createSignal(false);
42
+ const [checkpoint, setCheckpoint] = createSignal<MapNotebookSaveCheckpoint>();
43
+ const [legacy, setLegacy] = createSignal(false);
44
+ const [status, setStatus] = createSignal<z.infer<typeof storedSchema>["status"]>();
45
+ let controller: AbortController | undefined;
46
+ let storageKey = "";
47
+ let sourceKey = "";
48
+ let unpersisted: z.infer<typeof storedSchema> | undefined;
49
+ let observedRaw: string | null = null;
50
+ let unpersistedBases: (string | null)[] = [];
51
+ const noteHref = () => (checkpoint()?.noteId ? cloudResourceHref({ type: "notebooks.note", id: checkpoint()!.noteId! }) : undefined);
52
+ const notebookHref = () => (selected() ? cloudResourceHref({ type: "notebooks.notebook", id: selected()! }) : undefined);
53
+ const unknownCreate = () => checkpoint()?.pending?.kind === "create";
54
+ const saved = () => status() === "saved" && !sourceChanged();
55
+ const failureMessage = (cause: unknown) =>
56
+ cause instanceof Error && !(cause instanceof z.ZodError) && !(cause instanceof SyntaxError) && !(cause instanceof TypeError)
57
+ ? cause.message
58
+ : t().saveFailed;
59
+ const snapshot = async (signal: AbortSignal) => {
60
+ const response = await fetch(`/api/ai/conversations/${encodeURIComponent(source.conversationId)}/maps/${source.id}/export`, {
61
+ signal,
62
+ });
63
+ if (!response.ok) throw new Error(t().saveFailed);
64
+ // WebKit can reject invalid JSON with a DOMException named SyntaxError,
65
+ // which is not instanceof SyntaxError. Normalize at the response boundary.
66
+ const payload: unknown = await response.json().catch(() => {
67
+ throw new Error(t().saveFailed);
68
+ });
69
+ const value = snapshotSchema.parse(payload);
70
+ if (value.revision !== source.revision) throw new Error(t().conflict);
71
+ return value;
72
+ };
73
+ const restore = () => {
74
+ // A failed write may have reached storage. Only replace our own observed values;
75
+ // a different receipt belongs to a later lock holder and always takes precedence.
76
+ // Each write has its own identity, even when two tabs begin with identical helper checkpoints.
77
+ const raw = localStorage.getItem(storageKey);
78
+ observedRaw = raw;
79
+ const pending = unpersisted && unpersistedBases.includes(raw) ? unpersisted : undefined;
80
+ if (!pending) {
81
+ unpersisted = undefined;
82
+ unpersistedBases = [];
83
+ }
84
+ if (raw === null && !pending) {
85
+ // The old flow used an unscoped session receipt and different create keys.
86
+ // Do not silently abandon it or attribute it to a new owner: recover in Notebook.
87
+ const previous = sessionStorage.getItem(`cloud.ai.map-save:${source.id}:${source.revision}`);
88
+ if (previous !== null) {
89
+ const value = legacySchema.parse(JSON.parse(previous));
90
+ setSelected(value.notebookId);
91
+ setLegacy(true);
92
+ setError(t().previousSave);
93
+ }
94
+ return;
95
+ }
96
+ const stored = pending ?? storedSchema.parse(JSON.parse(raw!));
97
+ if (stored.checkpoint.sourceKey !== sourceKey) throw new Error(t().saveStorage);
98
+ setCheckpoint(stored.checkpoint);
99
+ setSelected(stored.checkpoint.notebookId);
100
+ setStatus(stored.status);
101
+ if (stored.status !== "saved")
102
+ setError(stored.checkpoint.pending?.kind === "create" ? t().unknownSave : mapSaveMessage(locale(), stored.code, stored.message));
103
+ };
104
+ const persist = (next: MapNotebookSaveCheckpoint, state: z.infer<typeof storedSchema>["status"], code?: string, message?: string) => {
105
+ setCheckpoint(next);
106
+ setSelected(next.notebookId);
107
+ if (!unpersisted) unpersistedBases = [observedRaw];
108
+ unpersisted = { checkpoint: next, writeId: crypto.randomUUID(), status: state, code, message };
109
+ const serialized = JSON.stringify(unpersisted);
110
+ const before = localStorage.getItem(storageKey);
111
+ unpersistedBases = [before, serialized];
112
+ localStorage.setItem(storageKey, serialized);
113
+ observedRaw = serialized;
114
+ unpersisted = undefined;
115
+ unpersistedBases = [];
116
+ };
117
+ const initialize = async () => {
118
+ if (busy()) return;
119
+ setBusy(true);
120
+ setError("");
121
+ controller = new AbortController();
122
+ try {
123
+ const value = await snapshot(controller.signal);
124
+ // Identity comes from the authorized export, never from a browser account hint.
125
+ const nextSourceKey = JSON.stringify([value.ownerUserId, source.conversationId, source.id, source.revision]);
126
+ if (sourceKey && nextSourceKey !== sourceKey) throw new Error(t().saveOwnerChanged);
127
+ sourceKey = nextSourceKey;
128
+ storageKey = `cloud.ai.map-save:v1:${sourceKey}`;
129
+ try {
130
+ restore();
131
+ } catch {
132
+ throw new Error(t().saveStorage);
133
+ }
134
+ if (!navigator.locks) throw new Error(t().saveLockUnavailable);
135
+ setReady(true);
136
+ } catch (cause) {
137
+ setError(controller.signal.aborted ? t().saveCancelled : failureMessage(cause));
138
+ } finally {
139
+ setBusy(false);
140
+ }
141
+ };
142
+ createEffect(() => {
143
+ if (sourceChanged()) controller?.abort();
144
+ });
145
+ onMount(() => void initialize());
146
+ onCleanup(() => controller?.abort());
147
+ const save = async () => {
148
+ if (sourceChanged() || !ready() || !selected() || busy() || legacy() || saved() || unknownCreate() || status() === "conflict") return;
149
+ setBusy(true);
150
+ setError("");
151
+ controller = new AbortController();
152
+ const signal = controller.signal;
153
+ try {
154
+ await navigator.locks.request(storageKey, { ifAvailable: true }, async (lock) => {
155
+ if (!lock) {
156
+ setError(t().saveLocked);
157
+ return;
158
+ }
159
+ // Another tab may have advanced or completed this save since the panel opened.
160
+ try {
161
+ restore();
162
+ if (unpersisted) persist(unpersisted.checkpoint, unpersisted.status, unpersisted.code, unpersisted.message);
163
+ } catch {
164
+ setReady(false);
165
+ throw new Error(t().saveStorage);
166
+ }
167
+ if (legacy() || saved() || unknownCreate() || status() === "conflict") return;
168
+ setError("");
169
+ const value = await snapshot(signal);
170
+ if (JSON.stringify([value.ownerUserId, source.conversationId, source.id, value.revision]) !== sourceKey)
171
+ throw new Error(t().saveOwnerChanged);
172
+ let persistenceFailed = false;
173
+ const result = await saveMapNotebook({
174
+ content: value.content,
175
+ notebookId: selected()!,
176
+ sourceKey,
177
+ checkpoint: checkpoint(),
178
+ signal,
179
+ onCheckpoint: (next) => {
180
+ try {
181
+ persist(next, "paused");
182
+ } catch {
183
+ persistenceFailed = true;
184
+ throw new Error(t().saveStorage);
185
+ }
186
+ },
187
+ });
188
+ setCheckpoint(result.checkpoint);
189
+ setStatus(result.status);
190
+ setError(
191
+ persistenceFailed
192
+ ? t().saveStorage
193
+ : result.status === "saved"
194
+ ? ""
195
+ : result.checkpoint?.pending?.kind === "create"
196
+ ? t().unknownSave
197
+ : result.status === "cancelled"
198
+ ? t().saveCancelled
199
+ : mapSaveMessage(locale(), result.code, result.message),
200
+ );
201
+ if (result.checkpoint) {
202
+ try {
203
+ persist(
204
+ result.checkpoint,
205
+ result.status,
206
+ persistenceFailed ? "persistence_failed" : result.status === "cancelled" ? "cancelled" : result.code,
207
+ persistenceFailed ? undefined : result.message,
208
+ );
209
+ } catch {
210
+ setStatus("error");
211
+ setError(t().saveStorage);
212
+ }
213
+ }
214
+ });
215
+ } catch (cause) {
216
+ setError(signal.aborted ? t().saveCancelled : failureMessage(cause));
217
+ } finally {
218
+ setBusy(false);
219
+ }
220
+ };
221
+ const action = () =>
222
+ unknownCreate() ? t().save : checkpoint() ? (checkpoint()?.pending ? t().reconcileSave : t().resumeSave) : t().save;
223
+ return (
224
+ <div class="ai-map__save">
225
+ <Show
226
+ when={!saved()}
227
+ fallback={
228
+ <div role="status">
229
+ <ButtonLink href={noteHref()!} variant="ghost">
230
+ {t().saved}
231
+ </ButtonLink>
232
+ </div>
233
+ }
234
+ >
235
+ <Select
236
+ label={t().notebook}
237
+ value={selected()}
238
+ onValueChange={setSelected}
239
+ disabled={sourceChanged() || !ready() || busy() || legacy() || Boolean(checkpoint())}
240
+ loadOptions={async (query, signal) => {
241
+ const result = await invokeCapabilityWithDataSchema(
242
+ {
243
+ kind: "query",
244
+ appId: "notebooks",
245
+ capabilityId: "notebook.list",
246
+ input: { query, minimumPermission: "write", limit: 100 },
247
+ signal,
248
+ },
249
+ notebooks,
250
+ );
251
+ if (!result.ok) throw new Error(result.error.message);
252
+ return result.data.data.map((n) => ({ value: n.id, label: n.name }));
253
+ }}
254
+ />
255
+ <Show when={checkpoint()}>
256
+ <div role="status" aria-live="polite">
257
+ <ProgressBar
258
+ label={t().saveProgress}
259
+ value={checkpoint()!.total ? (checkpoint()!.completed / checkpoint()!.total) * 100 : 0}
260
+ showValue
261
+ />
262
+ </div>
263
+ </Show>
264
+ <div class="ai-map__actions">
265
+ <Show
266
+ when={ready()}
267
+ fallback={
268
+ <Button size="sm" disabled={busy() || sourceChanged()} onClick={() => void initialize()}>
269
+ {busy() ? t().saveLoading : t().retry}
270
+ </Button>
271
+ }
272
+ >
273
+ <Button
274
+ size="sm"
275
+ disabled={sourceChanged() || !selected() || busy() || legacy() || unknownCreate() || status() === "conflict"}
276
+ onClick={() => void save()}
277
+ >
278
+ {busy() ? t().pending : action()}
279
+ </Button>
280
+ </Show>
281
+ <Button size="sm" variant="ghost" onClick={() => (busy() ? controller?.abort() : props.onCancel())}>
282
+ {t().cancel}
283
+ </Button>
284
+ </div>
285
+ </Show>
286
+ <Show when={sourceChanged() || error()}>
287
+ <p role="alert">{sourceChanged() ? t().saveSourceChanged : error()}</p>
288
+ </Show>
289
+ <Show when={noteHref() && !saved()}>
290
+ <ButtonLink size="sm" variant="ghost" href={noteHref()!}>
291
+ {t().partialNote}
292
+ </ButtonLink>
293
+ </Show>
294
+ <Show when={notebookHref() && (checkpoint() || legacy()) && !saved()}>
295
+ <ButtonLink size="sm" variant="ghost" href={notebookHref()!}>
296
+ {t().inspectNotebook}
297
+ </ButtonLink>
298
+ </Show>
299
+ </div>
300
+ );
301
+ }
@@ -0,0 +1,71 @@
1
+ import { LocaleProvider } from "@k2b/ui";
2
+ import { createMemo, createSignal, For } from "solid-js";
3
+ import { render } from "solid-js/web";
4
+ import type { AiMap, AiMapCommand, AiMapToolResult } from "../map-contracts";
5
+ import type { AiTurnBlock } from "../protocol";
6
+ import { AiReaderTimeZoneProvider } from "./action-sentences";
7
+ import { AiChatActionsProvider } from "./presentation";
8
+ import { createAiToolDisclosureState } from "./tool-disclosure";
9
+ import { layoutAiTurn } from "./turn-layout";
10
+ import { AiTurnView } from "./turn-view";
11
+
12
+ const snapshot: { map: AiMap; followups: AiMapToolResult[] } = JSON.parse(document.getElementById("snapshot")!.textContent!);
13
+ const initial = snapshot.map;
14
+ const block = (map: unknown, anchor: boolean, id = "original"): AiTurnBlock => ({
15
+ kind: "tool",
16
+ id,
17
+ callId: id,
18
+ name: "map",
19
+ status: initial.pending ? "running" : "completed",
20
+ args: {},
21
+ result: { map, anchor },
22
+ });
23
+ const text = (id: string, followup = false): AiTurnBlock => ({
24
+ kind: "text",
25
+ id,
26
+ text: followup
27
+ ? "Ich habe dieselbe Karte ergänzt. Deine bisherigen Antworten bleiben erhalten."
28
+ : "Hier sind die verifizierten Orte. Die Namen und die Karte sind fiktive Testdaten.",
29
+ });
30
+ const [turns, setTurns] = createSignal<AiTurnBlock[][]>([
31
+ [block(initial, true), text("answer")],
32
+ ...snapshot.followups.map((result, i) => [block(result.map, false, `followup-${i + 1}`), text(`followup-text-${i + 1}`, true)]),
33
+ ]);
34
+ const mount = document.getElementById("fixture-mount")!;
35
+ Object.assign(window, {
36
+ mapFollowup: async (command: AiMapCommand) => {
37
+ const response = await fetch(`/fixtures/${initial.conversationId}/followup`, {
38
+ method: "POST",
39
+ headers: { "content-type": "application/json" },
40
+ body: JSON.stringify(command),
41
+ });
42
+ if (!response.ok) throw new Error("FIXTURE_FOLLOWUP_FAILED");
43
+ const result: AiMapToolResult = await response.json();
44
+ setTurns([...turns(), [block(result.map, false, `followup-${turns().length}`), text(`followup-text-${turns().length}`, true)]]);
45
+ },
46
+ });
47
+ mount.replaceChildren();
48
+ render(
49
+ () => (
50
+ <LocaleProvider locale={document.documentElement.lang}>
51
+ <AiReaderTimeZoneProvider value="Europe/Berlin">
52
+ <AiChatActionsProvider actions={{}}>
53
+ <For each={turns()}>
54
+ {(blocks) => {
55
+ const segment = createMemo(() => ({
56
+ id: blocks[0]!.id,
57
+ turnId: blocks[0]!.id,
58
+ phase: "completed" as const,
59
+ layout: layoutAiTurn(blocks, { phase: "completed" }),
60
+ earlier: false,
61
+ duration: () => null,
62
+ }));
63
+ return <AiTurnView segment={segment} disclosureState={createAiToolDisclosureState()} />;
64
+ }}
65
+ </For>
66
+ </AiChatActionsProvider>
67
+ </AiReaderTimeZoneProvider>
68
+ </LocaleProvider>
69
+ ),
70
+ mount,
71
+ );
@@ -0,0 +1,107 @@
1
+ .ai-map-host { container: assistant-map / inline-size; min-width:0; width:100%; }
2
+ .ai-map { --map-second: #0f766e; display:flex; flex-direction:column; gap:14px; width:100%; min-width:0; color:var(--k2b-text); font-size:14px; line-height:1.45; scroll-margin-top:24px; }
3
+ .ai-map p { margin:0; }
4
+ .ai-map a,.ai-map__jump { color:var(--k2b-action); }
5
+ .ai-map__head { display:flex; align-items:center; gap:6px; height:29px; overflow:hidden; white-space:nowrap; font-size:18px; }
6
+ .ai-map__chip { display:inline-flex; align-items:center; flex:none; height:28px; border-radius:7px; padding:0 9px; background:var(--ui-field); font-size:14px; }
7
+ .ai-map__split { display:grid; grid-template-columns:minmax(0,1fr) minmax(0,1fr); gap:20px; height:344px; }
8
+ .ai-map__info { grid-column:2; grid-row:1; height:232px; min-width:0; display:flex; flex-direction:column; gap:8px; padding:4px 4px 0; overflow:hidden; font-size:13.5px; }
9
+ .ai-map__info h4 { margin:0; font-size:17px; font-weight:600; line-height:1.3; }
10
+ .ai-map__info>div { overflow:hidden; text-overflow:ellipsis; }
11
+ .ai-map__info small,.ai-map__provenance { color:var(--ui-text-dimmed); font-size:12.5px; }
12
+ .ai-map__description { display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden; flex-shrink:0; }
13
+ .ai-map__list { grid-column:1; grid-row:1; display:flex; flex-direction:column; gap:2px; height:344px; overflow:auto; }
14
+ .ai-map__list:focus-visible { outline:2px solid var(--k2b-focus-ring); outline-offset:2px; border-radius:10px; }
15
+ .ai-map__place { display:grid; grid-template-columns:26px minmax(0,1fr) auto; gap:10px; align-items:center; min-height:52px; flex:none; padding:6px 10px; border-radius:10px; cursor:pointer; }
16
+ .ai-map__place:hover { background:var(--ui-hover); }
17
+ .ai-map__place[aria-selected=true] { background:color-mix(in srgb,var(--k2b-action-solid) 12%,transparent); }
18
+ .ai-map__place[data-second=true][aria-selected=true] { background:color-mix(in srgb,var(--map-second) 12%,transparent); }
19
+ .ai-map__mark { width:24px; height:24px; border-radius:50%; display:grid; place-items:center; font-size:12px; font-weight:600; box-shadow:inset 0 0 0 2px var(--k2b-action-solid); }
20
+ .ai-map__place[data-second=true] .ai-map__mark { box-shadow:inset 0 0 0 2px var(--map-second); }
21
+ .ai-map__place-text { display:flex; flex-direction:column; min-width:0; }
22
+ .ai-map__place-text>span,.ai-map__place-text>small { white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
23
+ .ai-map__place-text>span { font-weight:500; }
24
+ .ai-map__place-text>small { font-size:12.5px; color:var(--ui-text-dimmed); }
25
+ .ai-map__distance { font-size:13px; font-variant-numeric:tabular-nums; }
26
+ .ai-map__actions { display:flex; gap:8px; align-items:center; flex-wrap:wrap; }
27
+ .ai-map__actions>.k2b-field { flex:1; min-width:100px; }
28
+ .ai-map__zone { min-height:34px; }
29
+ .ai-map__provenance { height:18px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
30
+ .ai-map__facts { display:flex; gap:32px; align-items:baseline; height:53px; font-variant-numeric:tabular-nums; }
31
+ .ai-map__facts>div { display:flex; flex-direction:column; }
32
+ .ai-map__facts strong { font-size:24px; font-weight:600; line-height:1.2; }
33
+ .ai-map__facts [data-gain=true] { color:var(--k2b-success-text); }
34
+ .ai-map__facts small { font-size:13px; }
35
+ .ai-map__stages { height:241px; overflow:auto; display:flex; flex-direction:column; gap:0; }
36
+ .ai-map__leg { display:grid; grid-template-columns:56px minmax(0,1fr) 76px 76px; align-items:center; gap:10px; min-height:40px; padding:0 10px; border-radius:9px; font-size:13.5px; flex:none; }
37
+ .ai-map__leg>span { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
38
+ .ai-map__leg>span:nth-last-child(-n+2) { text-align:right; font-variant-numeric:tabular-nums; }
39
+ .ai-map__leg small { margin-left:6px; color:var(--ui-text-dimmed); font-size:12px; }
40
+ .ai-map__leg[data-changed=true] { background:color-mix(in srgb,var(--map-second) 10%,transparent); }
41
+ .ai-map[data-kind=route] .ai-map__zone { min-height:128px; }
42
+ .ai-map__confirm,.ai-map__opts,.ai-map__search,.ai-map__save { display:flex; flex-direction:column; gap:8px; }
43
+ .ai-map__confirm>p { font-size:17px; line-height:1.6; }
44
+ .ai-map__receipt { display:flex; align-items:center; gap:10px; min-height:40px; }
45
+ .ai-map__receipt time { margin-left:auto; color:var(--ui-text-dimmed); font-size:12.5px; }
46
+ .ai-map__state { padding:14px 10px; display:flex; flex-direction:column; gap:8px; }
47
+ .ai-map__state>p { color:var(--ui-text-dimmed); }
48
+ .ai-map__error { display:flex; align-items:center; flex-wrap:wrap; gap:8px; font-size:13px; }
49
+ .ai-map__more { align-self:flex-start; min-height:40px; }
50
+ @container assistant-map (max-width:599px) {
51
+ .ai-map[data-kind=places] .ai-map__zone { min-height:76px; }
52
+ .ai-map__head { height:36px; font-size:16px; }
53
+ .ai-map__split { display:flex; flex-direction:column; gap:6px; height:544px; }
54
+ .ai-map__info { height:228px; flex:none; padding:0 2px; }
55
+ .ai-map__list { height:310px; flex:none; overflow:hidden; padding-top:14px; }
56
+ .ai-map__list .ai-map__place:nth-child(n+6) { display:none; }
57
+ .ai-map__list[data-expanded=true] .ai-map__place { display:grid; }
58
+ .ai-map[data-expanded=true] .ai-map__list,.ai-map[data-expanded=true] .ai-map__split { height:auto; overflow:visible; }
59
+ .ai-map[data-kind=places] .k2b-map-view { height:230px !important; }
60
+ .ai-map[data-kind=places] .ai-map__zone>.ai-map__actions { display:grid; grid-template-columns:1fr 1fr; }
61
+ .ai-map[data-kind=places] .ai-map__zone>.ai-map__actions>:nth-child(3) { grid-column:1 / -1; grid-row:2; }
62
+ .ai-map[data-kind=route] .k2b-map-view { height:240px !important; }
63
+ .ai-map__stages { height:286px; overflow:visible; }
64
+ .ai-map__leg { grid-template-columns:42px minmax(0,1fr) 54px 48px; gap:6px; padding:0 4px; font-size:12px; }
65
+ .ai-map__leg small { display:block; margin:0; }
66
+ .ai-map[data-kind=route] .ai-map__zone { min-height:150px; }
67
+ .ai-map__facts { gap:14px; }
68
+ .ai-map__facts strong { font-size:21px; }
69
+ .ai-map__opts { font-size:12px; }
70
+ }
71
+ @media (min-width:800px) { @container assistant-map (max-width:368px) {
72
+ .ai-map[data-kind=places] .k2b-map-view { height:200px !important; }
73
+ }
74
+
75
+ }
76
+
77
+ .ai-map__placeholder { min-height:811px; justify-content:center; align-items:center; }
78
+ .ai-map__placeholder[data-kind=route] { min-height:869px; }
79
+ .ai-map__skeleton { display:flex; flex-direction:column; gap:18px; padding:14px 10px; width:100%; }
80
+ .ai-map__skeleton span { height:34px; border-radius:8px; background:var(--ui-field); }
81
+ .ai-map__route-place { padding:10px; font-size:12px; color:var(--ui-text-dimmed); }
82
+ .ai-map__stages { overflow:visible; }
83
+ .ai-map__stages:has(.ai-map__leg:nth-child(n+7)) { height:auto; }
84
+ @container assistant-map (max-width:599px) { .ai-map__placeholder { min-height:960px; } .ai-map__placeholder[data-kind=route] { min-height:853px; } }
85
+
86
+ :where(.dark,[data-theme="dark"]) .ai-map { --map-second:#14b8a6; }
87
+
88
+ .ai-map__candidate { flex:none; height:40px; justify-content:flex-start; text-align:left; }
89
+ .ai-map__candidate span { white-space:nowrap; text-overflow:ellipsis; overflow:hidden; }
90
+ .ai-map__saved-stops { list-style:none; margin:0; padding:0 10px; display:grid; gap:8px; }
91
+ .ai-map__order { display:grid; grid-template-columns:minmax(0,1fr) minmax(0,1fr); gap:20px; padding:8px 10px; }
92
+ .ai-map__order ol { margin:8px 0 0; padding-left:22px; }
93
+ .ai-map__order li { line-height:1.8; overflow-wrap:anywhere; }
94
+ .ai-map__order>div:first-child ol { color:var(--ui-text-dimmed); }
95
+ .ai-map__stages:has(.ai-map__order li:nth-child(n+7)) { height:auto; }
96
+
97
+ .ai-map__group { height:24px; flex:none; display:flex; align-items:center; gap:6px; padding:0 10px; font-size:12.5px; font-weight:500; color:var(--k2b-action-solid); }
98
+ .ai-map__group::before { content:""; width:7px; height:7px; border-radius:50%; background:currentColor; }
99
+ .ai-map__group[data-second=true] { color:var(--map-second); }
100
+ .ai-map__chip[data-second=true] { color:var(--map-second); background:color-mix(in srgb,var(--map-second) 12%,transparent); }
101
+ .ai-map__place[aria-selected=true] .ai-map__mark { background:var(--k2b-action-solid); color:var(--k2b-surface); box-shadow:none; }
102
+ .ai-map__place[data-second=true][aria-selected=true] .ai-map__mark { background:var(--map-second); }
103
+ .ai-map__zone>.ai-map__actions>.k2b-button { min-height:34px; height:34px; }
104
+ .ai-map__actions[data-quiet=true] { visibility:hidden; }
105
+ .ai-map__receipt { min-height:24px; height:24px; }
106
+ .ai-map__receipt>span { min-width:0; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
107
+ .ai-map__receipt time,.ai-map__receipt .k2b-button { flex:none; }