@camox/api-contract 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.
- package/dist/analytics.d.ts +19 -0
- package/dist/analytics.js +18 -0
- package/dist/drafts.d.ts +82 -0
- package/dist/drafts.js +145 -0
- package/dist/icon-catalog.d.ts +7 -0
- package/dist/icon-catalog.js +48 -0
- package/dist/icons-BAxwOBBX.d.ts +19 -0
- package/dist/icons-Bk4igP2j.js +15 -0
- package/dist/ids.d.ts +26 -0
- package/dist/ids.js +40 -0
- package/dist/index.d.ts +12358 -8035
- package/dist/index.js +64 -96
- package/dist/projects.d.ts +28 -0
- package/dist/projects.js +50 -0
- package/dist/query-keys.d.ts +58 -42
- package/dist/query-keys.js +118 -32
- package/dist/threads.d.ts +277 -0
- package/dist/threads.js +263 -0
- package/package.json +53 -6
- package/src/analytics.ts +22 -0
- package/src/artifact.ts +38 -0
- package/src/drafts.ts +197 -0
- package/src/icon-catalog.ts +60 -0
- package/src/icons.ts +0 -57
- package/src/ids.ts +43 -0
- package/src/index.ts +5 -1
- package/src/projects.ts +67 -0
- package/src/query-keys.ts +64 -30
- package/src/reference-query.ts +48 -0
- package/src/threads.ts +327 -0
- package/dist/icons.d.ts +0 -20
- package/dist/icons.js +0 -72
- package/src/comments.ts +0 -33
package/src/threads.ts
ADDED
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
import type { AssistantMessage, Message } from "@earendil-works/pi-ai";
|
|
2
|
+
import type { AgentEvent, EntryRecord } from "@earendil-works/pi-durable";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
|
|
5
|
+
// A thread with Camox's agent, as the studio shows it. The thread's Durable Object sends Pi's own
|
|
6
|
+
// agent events over its socket, and this module is the one place that interprets them: the
|
|
7
|
+
// studio and the API's tests fold them with the same code.
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The kind of the transcript entry recording an editor's message, with its sender and the
|
|
11
|
+
* operation id the studio sent it with as data (`editorMessageDataSchema`).
|
|
12
|
+
*/
|
|
13
|
+
export const EDITOR_MESSAGE_ENTRY = "camox.editor-message";
|
|
14
|
+
|
|
15
|
+
/** The kind of the transcript entry recording that an editor stopped a run, with them as data. */
|
|
16
|
+
export const RUN_STOPPED_ENTRY = "camox.run-stopped";
|
|
17
|
+
|
|
18
|
+
/** Who sent a message in a thread. */
|
|
19
|
+
export const threadSenderSchema = z.object({
|
|
20
|
+
id: z.string(),
|
|
21
|
+
name: z.string(),
|
|
22
|
+
image: z.string().nullable(),
|
|
23
|
+
});
|
|
24
|
+
export type ThreadSender = z.infer<typeof threadSenderSchema>;
|
|
25
|
+
|
|
26
|
+
/** Whether `data` names a sender. */
|
|
27
|
+
export const isSender = (data: unknown): data is ThreadSender =>
|
|
28
|
+
threadSenderSchema.safeParse(data).success;
|
|
29
|
+
|
|
30
|
+
/** What an editor's message entry records: who sent it, and the operation id it was sent with. */
|
|
31
|
+
export const editorMessageDataSchema = threadSenderSchema.extend({
|
|
32
|
+
operationId: z.string().optional(),
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* A draft a Live thread started with its first write, becoming its thread, and the operation ids
|
|
37
|
+
* of the messages of the run that made the write: the studio that sent one of them enters the
|
|
38
|
+
* draft with the thread.
|
|
39
|
+
*/
|
|
40
|
+
export const startedDraftSchema = z.object({
|
|
41
|
+
draftId: z.string(),
|
|
42
|
+
operationIds: z.array(z.string()),
|
|
43
|
+
});
|
|
44
|
+
export type StartedDraft = z.infer<typeof startedDraftSchema>;
|
|
45
|
+
|
|
46
|
+
/** How a write touched what it names. */
|
|
47
|
+
export const touchKindSchema = z.enum(["created", "edited", "moved", "deleted", "settings"]);
|
|
48
|
+
export type TouchKind = z.infer<typeof touchKindSchema>;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* One thing a script's successful write touched, named so the studio can locate it: a Page (and
|
|
52
|
+
* its Block, if any), a Synced Block's shared content, by its block definition, or a Layout (and
|
|
53
|
+
* its Block, if any). `settings` is a Page's settings only (nickname, path, SEO, layout). `locale`
|
|
54
|
+
* is the Locale the write was in, for a localized tool. Writes to Collection Records, Files and
|
|
55
|
+
* Locales aren't on the Canvas, so they're left out.
|
|
56
|
+
*/
|
|
57
|
+
const touchBaseSchema = z.object({ kind: touchKindSchema, locale: z.string().optional() });
|
|
58
|
+
|
|
59
|
+
export const touchSchema = z.discriminatedUnion("entity", [
|
|
60
|
+
touchBaseSchema.extend({
|
|
61
|
+
entity: z.literal("page"),
|
|
62
|
+
pageId: z.string(),
|
|
63
|
+
blockId: z.string().optional(),
|
|
64
|
+
}),
|
|
65
|
+
touchBaseSchema.extend({
|
|
66
|
+
entity: z.literal("synced-block"),
|
|
67
|
+
/** The Synced Block's block definition id. */
|
|
68
|
+
blockType: z.string(),
|
|
69
|
+
}),
|
|
70
|
+
touchBaseSchema.extend({
|
|
71
|
+
entity: z.literal("layout"),
|
|
72
|
+
layoutId: z.string(),
|
|
73
|
+
blockId: z.string().optional(),
|
|
74
|
+
}),
|
|
75
|
+
]);
|
|
76
|
+
export type Touch = z.infer<typeof touchSchema>;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* What one code-mode script's successful writes touched, in write order, and the operation ids
|
|
80
|
+
* of the messages of the run it belongs to: only the studio that sent one of them follows it.
|
|
81
|
+
*/
|
|
82
|
+
export const touchedBatchSchema = z.object({
|
|
83
|
+
operationIds: z.array(z.string()),
|
|
84
|
+
touches: z.array(touchSchema),
|
|
85
|
+
});
|
|
86
|
+
export type TouchedBatch = z.infer<typeof touchedBatchSchema>;
|
|
87
|
+
|
|
88
|
+
/** A message the studio shows in a thread. */
|
|
89
|
+
export type ThreadMessage =
|
|
90
|
+
| { id: string; from: "editor"; sender: ThreadSender; text: string }
|
|
91
|
+
/** The agent's final message of a turn, in Markdown. */
|
|
92
|
+
| { id: string; from: "agent"; text: string }
|
|
93
|
+
/**
|
|
94
|
+
* In place of the agent's message, for a turn that ended without one: the model failed (after
|
|
95
|
+
* Pi's retries), or its answer was cut short. Sending again retries.
|
|
96
|
+
*/
|
|
97
|
+
| { id: string; from: "failed" }
|
|
98
|
+
/** In place of the agent's message, for a turn an editor stopped: `sender` stopped it. */
|
|
99
|
+
| { id: string; from: "stopped"; sender: ThreadSender };
|
|
100
|
+
|
|
101
|
+
/** Where a Live thread's messages went when it started a draft, beside its transcript. */
|
|
102
|
+
type LiveThreadHandoff = {
|
|
103
|
+
/**
|
|
104
|
+
* The draft this Live thread started with its first write, whose thread it became. The studio
|
|
105
|
+
* that sent a message of that run enters the draft with the thread; other tabs on Live move to
|
|
106
|
+
* the editor's fresh Live thread.
|
|
107
|
+
*/
|
|
108
|
+
startedDraft?: StartedDraft;
|
|
109
|
+
/** The draft the editor's previous Live thread continued in, for a fresh Live thread. */
|
|
110
|
+
continuedIn?: StartedDraft;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
/** Everything the studio shows for a thread. */
|
|
114
|
+
export type ThreadView = LiveThreadHandoff & {
|
|
115
|
+
messages: readonly ThreadMessage[];
|
|
116
|
+
/** Whether a run is going: the agent is working on a turn, and stopping would end it. */
|
|
117
|
+
running: boolean;
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
export const EMPTY_THREAD_VIEW: ThreadView = { messages: [], running: false };
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* What a thread's view is folded from: the steps of its transcript that matter to the studio,
|
|
124
|
+
* and whether a run is going. The studio and the tests fold Pi's events into this, and derive
|
|
125
|
+
* the view from it with `threadViewOf`, so a snapshot and the events that led to it show the same.
|
|
126
|
+
*/
|
|
127
|
+
export type ThreadState = LiveThreadHandoff & { steps: readonly ThreadStep[]; running: boolean };
|
|
128
|
+
|
|
129
|
+
export const EMPTY_THREAD_STATE: ThreadState = { steps: [], running: false };
|
|
130
|
+
|
|
131
|
+
/** One transcript entry, as far as the studio's view is concerned. */
|
|
132
|
+
type ThreadStep = { id: string } & (
|
|
133
|
+
| { type: "editor"; sender: ThreadSender; text: string }
|
|
134
|
+
| { type: "answer"; text: string }
|
|
135
|
+
/** A round of tool calls, or their results: the run goes on after it. */
|
|
136
|
+
| { type: "tools" }
|
|
137
|
+
/** A model call that ended without an answer: failed, aborted, or cut short. */
|
|
138
|
+
| { type: "unfinished" }
|
|
139
|
+
| { type: "stopped"; sender: ThreadSender }
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
/** Studio → thread. */
|
|
143
|
+
export const threadClientMessageSchema = z.discriminatedUnion("type", [
|
|
144
|
+
/**
|
|
145
|
+
* An editor's message. The operation id is generated by the studio and kept across resends, so
|
|
146
|
+
* a message resent after a dropped connection is one submission.
|
|
147
|
+
*/
|
|
148
|
+
z.object({ type: z.literal("send"), operationId: z.string().min(1), text: z.string().min(1) }),
|
|
149
|
+
/** Stops the run going, if any. What the agent changed before stays. */
|
|
150
|
+
z.object({ type: z.literal("stop") }),
|
|
151
|
+
/** Asks for a fresh snapshot of the thread. */
|
|
152
|
+
z.object({ type: z.literal("resync") }),
|
|
153
|
+
]);
|
|
154
|
+
export type ThreadClientMessage = z.infer<typeof threadClientMessageSchema>;
|
|
155
|
+
|
|
156
|
+
/** Thread → studio. */
|
|
157
|
+
export type ThreadServerMessage =
|
|
158
|
+
/**
|
|
159
|
+
* Pi's agent events. The first batch after connecting or resyncing starts with a `snapshot`,
|
|
160
|
+
* which replaces whatever the studio folded before.
|
|
161
|
+
*/
|
|
162
|
+
| { type: "events"; events: readonly AgentEvent[] }
|
|
163
|
+
/** The thread recorded the message sent with this operation id (once, however often sent). */
|
|
164
|
+
| { type: "accepted"; operationId: string }
|
|
165
|
+
/** This Live thread started a draft with its first write, and became that draft's thread. */
|
|
166
|
+
| { type: "started-draft"; startedDraft: StartedDraft }
|
|
167
|
+
/**
|
|
168
|
+
* The editor's previous Live thread continued in this draft: this one is fresh. Sent before
|
|
169
|
+
* the snapshot.
|
|
170
|
+
*/
|
|
171
|
+
| { type: "continued-in"; continuedIn: StartedDraft }
|
|
172
|
+
/**
|
|
173
|
+
* A code-mode script that wrote something ended, failed or not: what its successful writes
|
|
174
|
+
* touched. Sent once per such script, live only: a snapshot never replays past touches.
|
|
175
|
+
*/
|
|
176
|
+
| { type: "touched"; touched: TouchedBatch }
|
|
177
|
+
| { type: "error"; operationId?: string; message: string };
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* The code a thread closes its sockets with once it ended with its draft, and for good: it's
|
|
181
|
+
* deleted, so reconnecting reaches nothing. A development draft, which stays open, gets a new
|
|
182
|
+
* thread the next time the studio connects.
|
|
183
|
+
*/
|
|
184
|
+
export const THREAD_ENDED_CLOSE_CODE = 4000;
|
|
185
|
+
|
|
186
|
+
const textOf = (content: Message["content"]) =>
|
|
187
|
+
typeof content === "string"
|
|
188
|
+
? content
|
|
189
|
+
: content
|
|
190
|
+
.flatMap((part) => (part.type === "text" ? [part.text] : []))
|
|
191
|
+
.join("")
|
|
192
|
+
.trim();
|
|
193
|
+
|
|
194
|
+
/** What an assistant message is to the thread: a final answer, a tool round, or neither. */
|
|
195
|
+
function assistantStep(id: string, message: AssistantMessage): ThreadStep {
|
|
196
|
+
if (message.content.some((part) => part.type === "toolCall")) return { id, type: "tools" };
|
|
197
|
+
// Thinking is never shown. An answer cut short by the model's length limit isn't one.
|
|
198
|
+
const text = message.stopReason === "stop" ? textOf(message.content) : "";
|
|
199
|
+
return text ? { id, type: "answer", text } : { id, type: "unfinished" };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** The sender an entry's data names, if any, without other fields it may carry. */
|
|
203
|
+
function senderOf(data: unknown): ThreadSender | undefined {
|
|
204
|
+
if (!isSender(data)) return undefined;
|
|
205
|
+
const { id, name, image } = data;
|
|
206
|
+
return { id, name, image };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** The step an entry is, if it's one the view depends on. */
|
|
210
|
+
function threadStepOf(entry: EntryRecord): ThreadStep | undefined {
|
|
211
|
+
const id = String(entry.id);
|
|
212
|
+
const sender = senderOf(entry.data);
|
|
213
|
+
if (entry.kind === RUN_STOPPED_ENTRY && sender) return { id, type: "stopped", sender };
|
|
214
|
+
const message = entry.model?.find((candidate) => candidate.role !== "system");
|
|
215
|
+
if (!message) return undefined;
|
|
216
|
+
if (entry.kind === EDITOR_MESSAGE_ENTRY && message.role === "user" && sender) {
|
|
217
|
+
return { id, type: "editor", sender, text: textOf(message.content) };
|
|
218
|
+
}
|
|
219
|
+
if (message.role === "assistant") return assistantStep(id, message);
|
|
220
|
+
if (message.role === "toolResult") return { id, type: "tools" };
|
|
221
|
+
return undefined;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* What the studio shows for a thread. A turn starts with an editor's message, and messages sent
|
|
226
|
+
* while it runs join it. It ends with the agent's final message; text written beside tool calls
|
|
227
|
+
* is never shown. A stopped turn shows who stopped it instead, and whatever its run wrote after
|
|
228
|
+
* the stop is left out. A turn that ended without either shows a failed line in its place: its
|
|
229
|
+
* run is over, or a later message started another run after its last model call came back
|
|
230
|
+
* unfinished. While Pi retries a failed call, the run goes on, so the turn shows as working.
|
|
231
|
+
*/
|
|
232
|
+
export function threadViewOf({ steps, running, ...drafts }: ThreadState): ThreadView {
|
|
233
|
+
const messages: ThreadMessage[] = [];
|
|
234
|
+
// The latest editor's message of a turn still waiting for the agent's message.
|
|
235
|
+
let waiting: string | undefined;
|
|
236
|
+
let unfinished = false;
|
|
237
|
+
/** Ends the turn waiting for the agent's message, if any. */
|
|
238
|
+
const endTurn = () => {
|
|
239
|
+
waiting = undefined;
|
|
240
|
+
unfinished = false;
|
|
241
|
+
};
|
|
242
|
+
/** Ends the turn waiting for the agent's message with a failed line, if any. */
|
|
243
|
+
const fail = () => {
|
|
244
|
+
if (waiting !== undefined) messages.push({ id: `${waiting}:failed`, from: "failed" });
|
|
245
|
+
endTurn();
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
for (const step of steps) {
|
|
249
|
+
switch (step.type) {
|
|
250
|
+
case "editor":
|
|
251
|
+
// Pi places a steering message only once a model call is settled: after a round of tool
|
|
252
|
+
// calls, or with the answer ending the run. Never between a failed call and its retry, so
|
|
253
|
+
// an editor's message after an unfinished call means that call's turn is over.
|
|
254
|
+
if (unfinished) fail();
|
|
255
|
+
messages.push({ id: step.id, from: "editor", sender: step.sender, text: step.text });
|
|
256
|
+
waiting = step.id;
|
|
257
|
+
break;
|
|
258
|
+
case "stopped":
|
|
259
|
+
if (waiting === undefined) break;
|
|
260
|
+
messages.push({ id: step.id, from: "stopped", sender: step.sender });
|
|
261
|
+
endTurn();
|
|
262
|
+
break;
|
|
263
|
+
case "answer":
|
|
264
|
+
// A stopped turn's run may still answer before it ends.
|
|
265
|
+
if (waiting === undefined && messages.at(-1)?.from === "stopped") break;
|
|
266
|
+
messages.push({ id: step.id, from: "agent", text: step.text });
|
|
267
|
+
endTurn();
|
|
268
|
+
break;
|
|
269
|
+
case "tools":
|
|
270
|
+
unfinished = false;
|
|
271
|
+
break;
|
|
272
|
+
case "unfinished":
|
|
273
|
+
unfinished = true;
|
|
274
|
+
break;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
if (!running) fail();
|
|
278
|
+
return { ...drafts, messages, running };
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const append = (state: ThreadState, entry: EntryRecord | undefined): ThreadState => {
|
|
282
|
+
const step = entry && threadStepOf(entry);
|
|
283
|
+
if (!step || state.steps.some((known) => known.id === step.id)) return state;
|
|
284
|
+
return { ...state, steps: [...state.steps, step] };
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
/** Folds one of Pi's agent events into the thread's state. */
|
|
288
|
+
export function reduceThreadState(state: ThreadState, event: AgentEvent): ThreadState {
|
|
289
|
+
switch (event.type) {
|
|
290
|
+
case "snapshot":
|
|
291
|
+
return {
|
|
292
|
+
...state,
|
|
293
|
+
steps: event.entries.flatMap((entry) => threadStepOf(entry) ?? []),
|
|
294
|
+
running: event.run !== undefined,
|
|
295
|
+
};
|
|
296
|
+
case "run_start":
|
|
297
|
+
return { ...state, running: true };
|
|
298
|
+
case "run_end":
|
|
299
|
+
return { ...state, running: false };
|
|
300
|
+
case "message_end":
|
|
301
|
+
case "entry_appended":
|
|
302
|
+
case "tool_execution_end":
|
|
303
|
+
return append(state, event.entry);
|
|
304
|
+
default:
|
|
305
|
+
return state;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/** Folds a batch of Pi's agent events into the thread's state. */
|
|
310
|
+
export const reduceThreadEvents = (
|
|
311
|
+
state: ThreadState,
|
|
312
|
+
events: readonly AgentEvent[],
|
|
313
|
+
): ThreadState => events.reduce(reduceThreadState, state);
|
|
314
|
+
|
|
315
|
+
/** Folds what the thread sent over its socket into the thread's state. */
|
|
316
|
+
export function reduceThreadFrame(state: ThreadState, frame: ThreadServerMessage): ThreadState {
|
|
317
|
+
switch (frame.type) {
|
|
318
|
+
case "events":
|
|
319
|
+
return reduceThreadEvents(state, frame.events);
|
|
320
|
+
case "started-draft":
|
|
321
|
+
return { ...state, startedDraft: frame.startedDraft };
|
|
322
|
+
case "continued-in":
|
|
323
|
+
return { ...state, continuedIn: frame.continuedIn };
|
|
324
|
+
default:
|
|
325
|
+
return state;
|
|
326
|
+
}
|
|
327
|
+
}
|
package/dist/icons.d.ts
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import type { IconifyJSON } from "@iconify/types";
|
|
2
|
-
export declare const ICON_CATALOG_VERSION = "2.2.532";
|
|
3
|
-
export type IconSvgData = {
|
|
4
|
-
body: string;
|
|
5
|
-
attributes: Record<string, string>;
|
|
6
|
-
attribution?: {
|
|
7
|
-
collection: string;
|
|
8
|
-
author: string;
|
|
9
|
-
license: string;
|
|
10
|
-
url?: string;
|
|
11
|
-
};
|
|
12
|
-
};
|
|
13
|
-
export declare function loadIconCollection(prefix: string): Promise<IconifyJSON>;
|
|
14
|
-
export declare function iconCollectionIds(data: IconifyJSON): string[];
|
|
15
|
-
export declare function resolveIconSvg(data: IconifyJSON, id: string): IconSvgData;
|
|
16
|
-
export declare function scopeIconSvgIds(body: string, prefix: string): string;
|
|
17
|
-
export declare function validateIconValue(value: unknown, schema: {
|
|
18
|
-
fieldType?: string;
|
|
19
|
-
enum?: unknown;
|
|
20
|
-
}): void;
|
package/dist/icons.js
DELETED
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
import { getIconData, iconToSVG } from "@iconify/utils";
|
|
2
|
-
// One immutable snapshot for declarations, schemas, picker and SVG resolution.
|
|
3
|
-
export const ICON_CATALOG_VERSION = "2.2.532";
|
|
4
|
-
const collections = new Map();
|
|
5
|
-
export async function loadIconCollection(prefix) {
|
|
6
|
-
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(prefix))
|
|
7
|
-
throw new Error("Invalid icon collection");
|
|
8
|
-
const existing = collections.get(prefix);
|
|
9
|
-
if (existing)
|
|
10
|
-
return existing;
|
|
11
|
-
const pending = (async () => {
|
|
12
|
-
const response = await fetch(`https://unpkg.com/@iconify/json@${ICON_CATALOG_VERSION}/json/${prefix}.json`, { signal: AbortSignal.timeout(30_000) });
|
|
13
|
-
if (!response.ok)
|
|
14
|
-
throw new Error(`Unable to load icon collection ${prefix}`);
|
|
15
|
-
const data = (await response.json());
|
|
16
|
-
if (data.prefix !== prefix || !data.icons || !data.info?.license)
|
|
17
|
-
throw new Error("Invalid icon catalog or missing license");
|
|
18
|
-
return data;
|
|
19
|
-
})();
|
|
20
|
-
collections.set(prefix, pending);
|
|
21
|
-
// Bound worker memory; failed fetches must be retryable.
|
|
22
|
-
if (collections.size > 8)
|
|
23
|
-
collections.delete(collections.keys().next().value);
|
|
24
|
-
try {
|
|
25
|
-
return await pending;
|
|
26
|
-
}
|
|
27
|
-
catch (error) {
|
|
28
|
-
collections.delete(prefix);
|
|
29
|
-
throw error;
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
export function iconCollectionIds(data) {
|
|
33
|
-
return [...Object.keys(data.icons), ...Object.keys(data.aliases ?? {})]
|
|
34
|
-
.filter((name) => getIconData(data, name) !== null)
|
|
35
|
-
.sort()
|
|
36
|
-
.map((name) => `${data.prefix}:${name}`);
|
|
37
|
-
}
|
|
38
|
-
export function resolveIconSvg(data, id) {
|
|
39
|
-
const [prefix, name, extra] = id.split(":");
|
|
40
|
-
if (prefix !== data.prefix || !name || extra !== undefined)
|
|
41
|
-
throw new Error("Invalid icon ID");
|
|
42
|
-
const icon = getIconData(data, name);
|
|
43
|
-
if (!icon)
|
|
44
|
-
throw new Error(`Unknown icon: ${id}`);
|
|
45
|
-
const svg = iconToSVG(icon);
|
|
46
|
-
return {
|
|
47
|
-
body: svg.body,
|
|
48
|
-
attributes: svg.attributes,
|
|
49
|
-
attribution: data.info
|
|
50
|
-
? {
|
|
51
|
-
collection: data.info.name,
|
|
52
|
-
author: data.info.author.name,
|
|
53
|
-
license: data.info.license.title,
|
|
54
|
-
url: data.info.license.url,
|
|
55
|
-
}
|
|
56
|
-
: undefined,
|
|
57
|
-
};
|
|
58
|
-
}
|
|
59
|
-
export function scopeIconSvgIds(body, prefix) {
|
|
60
|
-
const ids = [...body.matchAll(/\sid="([^\s"]+)"/g)].map((match) => match[1]);
|
|
61
|
-
if (!ids.length)
|
|
62
|
-
return body;
|
|
63
|
-
const pattern = ids.map((id) => id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|");
|
|
64
|
-
return body.replace(new RegExp(`([#;"])(${pattern})([")]|\\.[a-z])`, "g"), (_match, before, id, after) => `${before}${prefix}${id}${after}`);
|
|
65
|
-
}
|
|
66
|
-
export function validateIconValue(value, schema) {
|
|
67
|
-
if (schema.fieldType !== "Icon")
|
|
68
|
-
return;
|
|
69
|
-
if (typeof value !== "string" || !Array.isArray(schema.enum) || !schema.enum.includes(value)) {
|
|
70
|
-
throw new Error(`Invalid icon ID: ${String(value)}`);
|
|
71
|
-
}
|
|
72
|
-
}
|
package/src/comments.ts
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
|
|
3
|
-
export const COMMENT_MESSAGE_MAX_LENGTH = 10_000;
|
|
4
|
-
|
|
5
|
-
const objectId = z.number().int().positive();
|
|
6
|
-
const fieldName = z.string().min(1).max(200);
|
|
7
|
-
|
|
8
|
-
export const commentTargetSchema = z.discriminatedUnion("kind", [
|
|
9
|
-
z.strictObject({ kind: z.literal("page") }),
|
|
10
|
-
z.strictObject({ kind: z.literal("block"), blockId: objectId }),
|
|
11
|
-
z.strictObject({ kind: z.literal("item"), blockId: objectId, itemId: objectId }),
|
|
12
|
-
z.strictObject({ kind: z.literal("block-field"), blockId: objectId, fieldName }),
|
|
13
|
-
z.strictObject({
|
|
14
|
-
kind: z.literal("item-field"),
|
|
15
|
-
blockId: objectId,
|
|
16
|
-
itemId: objectId,
|
|
17
|
-
fieldName,
|
|
18
|
-
}),
|
|
19
|
-
]);
|
|
20
|
-
|
|
21
|
-
export const commentSchema = z.object({
|
|
22
|
-
id: z.uuid(),
|
|
23
|
-
pageId: objectId,
|
|
24
|
-
environmentId: objectId,
|
|
25
|
-
message: z.string(),
|
|
26
|
-
resolved: z.boolean(),
|
|
27
|
-
target: commentTargetSchema.nullable(),
|
|
28
|
-
author: z.object({ name: z.string(), image: z.string().nullable() }),
|
|
29
|
-
createdAt: z.number(),
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
export type CommentTarget = z.infer<typeof commentTargetSchema>;
|
|
33
|
-
export type Comment = z.infer<typeof commentSchema>;
|