stunning-md 0.1.0 → 0.2.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 +106 -14
- package/dist/chunk-MKH2VK55.mjs +377 -0
- package/dist/chunk-MKH2VK55.mjs.map +1 -0
- package/dist/core.d.mts +178 -6
- package/dist/core.mjs +174 -87
- package/dist/core.mjs.map +1 -1
- package/dist/index.d.mts +206 -7
- package/dist/index.mjs +3449 -2290
- package/dist/index.mjs.map +1 -1
- package/dist/server.d.mts +21 -1
- package/dist/server.mjs +48 -0
- package/dist/server.mjs.map +1 -1
- package/dist/styles.css +1 -1
- package/package.json +7 -3
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/stunning-md/chat.ts"],"sourcesContent":["import type { Classify } from \"./classifier\"\n\nexport type ChatMessage = { role: \"system\" | \"user\" | \"assistant\"; content: string }\n\n/**\n * Anything that answers a conversation with a stream of text: usually\n * `createChat` pointed at an OpenAI-compatible endpoint or at your own proxy.\n */\nexport type Chat = (messages: ChatMessage[], signal?: AbortSignal) => AsyncIterable<string>\n\n/**\n * A chat function for an OpenAI-compatible `chat/completions` endpoint, read as\n * a stream. Point it at a route on your own server when the provider needs a key\n * (see `createChatHandler` in `stunning-md/server`); a key in browser code is public.\n */\nexport function createChat(options: { endpoint: string; model?: string; headers?: Record<string, string> }): Chat {\n return async function* (messages, signal) {\n const response = await fetch(options.endpoint, {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\", accept: \"text/event-stream\", ...options.headers },\n body: JSON.stringify({ ...(options.model ? { model: options.model } : {}), messages, stream: true }),\n signal,\n })\n if (!response.ok || !response.body) {\n const detail = await response.text().catch(() => \"\")\n throw new Error(`chat responded ${response.status}${detail ? `: ${detail.slice(0, 200)}` : \"\"}`)\n }\n // Some servers ignore `stream` and answer in one piece.\n if ((response.headers.get(\"content-type\") ?? \"\").includes(\"application/json\")) {\n const data = await response.json()\n const failure = errorIn(data)\n if (failure) throw new Error(failure)\n const whole = data?.choices?.[0]?.message?.content\n if (typeof whole === \"string\") yield whole\n return\n }\n const reader = response.body.getReader()\n const decoder = new TextDecoder()\n let buffer = \"\"\n // Text that is not part of the event stream — some providers report an error this way, under a 200.\n let stray = \"\"\n let yielded = false\n for (;;) {\n const { done, value } = await reader.read()\n buffer += done ? decoder.decode() : decoder.decode(value, { stream: true })\n const lines = buffer.split(\"\\n\")\n buffer = done ? \"\" : (lines.pop() ?? \"\")\n for (const raw of lines) {\n const line = raw.trim()\n if (!line.startsWith(\"data:\")) {\n if (line && !line.startsWith(\":\") && stray.length < 4000) stray += raw\n continue\n }\n const payload = line.slice(5).trim()\n if (payload === \"[DONE]\") return\n let event: unknown\n try {\n event = JSON.parse(payload)\n } catch {\n // A partial line; nothing to show.\n continue\n }\n const failure = errorIn(event)\n if (failure) throw new Error(failure)\n const delta = (event as { choices?: { delta?: { content?: unknown } }[] })?.choices?.[0]?.delta?.content\n if (typeof delta === \"string\" && delta) {\n yielded = true\n yield delta\n }\n }\n if (done) break\n }\n if (!yielded && stray.trim()) {\n let failure: string | null = null\n try {\n failure = errorIn(JSON.parse(stray))\n } catch {\n // Not JSON: nothing recognisable to report.\n }\n throw new Error(failure ?? \"the chat model returned nothing\")\n }\n }\n}\n\n/** The message of an error object, in the shapes providers use: `{ error }` or `[{ error }]`. */\nfunction errorIn(body: unknown): string | null {\n const first = Array.isArray(body) ? body[0] : body\n const error = (first as { error?: unknown } | null)?.error\n if (!error) return null\n if (typeof error === \"string\") return error\n const message = (error as { message?: unknown }).message\n return typeof message === \"string\" && message ? message : \"the chat model reported an error\"\n}\n\n/**\n * The address of an OpenAI-compatible chat endpoint, given either the full\n * `…/chat/completions` address or just the API's base (`…/v1`).\n */\nexport function chatCompletionsUrl(url: string): string {\n const trimmed = url.trim().replace(/\\/+$/, \"\")\n return /\\/chat\\/completions$/.test(trimmed) ? trimmed : `${trimmed}/chat/completions`\n}\n\n// --- splitting a reply into blocks ----------------------------------------------\n\nexport type BlockKind = \"heading\" | \"paragraph\" | \"other\"\n\nexport type ReplyBlock = {\n text: string\n kind: BlockKind\n /** Heading level, for headings. */\n depth?: number\n}\n\nconst HEADING = /^(#{1,6})\\s+\\S/\nconst FENCE = /^\\s{0,3}(```+|~~~+)/\nconst NOT_PROSE = /^(\\s{0,3}([-*+]|\\d+[.)])\\s|\\s{0,3}>|\\s{0,3}\\||\\s{0,3}(-{3,}|\\*{3,}|_{3,})\\s*$|\\s{0,3}<|\\s{2,}\\S|!\\[[^\\]]*\\]\\([^)]*\\)\\s*$|\\[!\\[)/\n\nfunction describe(text: string): ReplyBlock {\n const first = text.split(\"\\n\")[0]\n const heading = HEADING.exec(first)\n if (heading) return { text, kind: \"heading\", depth: heading[1].length }\n const table = /^\\s{0,3}\\|?\\s*:?-{3,}/.test(text.split(\"\\n\")[1] ?? \"\")\n // A line set wholly in bold is a title in all but name.\n const boldLine = /^\\*\\*[^*\\n]+\\*\\*:?$/.test(text)\n if (FENCE.test(first) || first.startsWith(\"$$\") || NOT_PROSE.test(first) || table || boldLine) return { text, kind: \"other\" }\n return { text, kind: \"paragraph\" }\n}\n\n/**\n * Cuts markdown, as it streams in, into the blocks a reader would recognise:\n * paragraphs, headings, lists, tables, code. A block is released once it is\n * known to be complete — at the blank line after it, or at the end.\n */\nexport class BlockSplitter {\n private pending = \"\"\n private lines: string[] = []\n private fence: string | null = null\n private math = false\n\n private flush(out: ReplyBlock[]) {\n const text = this.lines.join(\"\\n\").trim()\n this.lines = []\n if (text) out.push(describe(text))\n }\n\n private take(line: string, out: ReplyBlock[]) {\n const fence = FENCE.exec(line)\n if (this.fence) {\n this.lines.push(line)\n if (fence && fence[1].startsWith(this.fence[0]) && fence[1].length >= this.fence.length && !line.trim().slice(fence[1].length).trim()) {\n this.fence = null\n }\n return\n }\n if (this.math) {\n this.lines.push(line)\n if (line.trim().endsWith(\"$$\")) this.math = false\n return\n }\n if (fence) {\n this.lines.push(line)\n this.fence = fence[1]\n return\n }\n if (line.trim().startsWith(\"$$\") && this.lines.length === 0) {\n this.lines.push(line)\n this.math = !(line.trim().length > 2 && line.trim().endsWith(\"$$\"))\n return\n }\n if (!line.trim()) return this.flush(out)\n // A heading is a block of its own, with or without blank lines around it.\n if (HEADING.test(line)) {\n this.flush(out)\n this.lines.push(line)\n return this.flush(out)\n }\n this.lines.push(line)\n }\n\n /** Feed more text; returns the blocks that are now complete. */\n push(text: string): ReplyBlock[] {\n const out: ReplyBlock[] = []\n this.pending += text\n const lines = this.pending.split(\"\\n\")\n this.pending = lines.pop() ?? \"\"\n for (const line of lines) this.take(line.replace(/\\r$/, \"\"), out)\n return out\n }\n\n /** The stream is over; returns whatever was still open. */\n end(): ReplyBlock[] {\n const out: ReplyBlock[] = []\n if (this.pending) this.take(this.pending, out)\n this.pending = \"\"\n this.fence = null\n this.math = false\n this.flush(out)\n return out\n }\n\n}\n\nconst FRONTMATTER_OPEN = /^---[ \\t]*\\r?\\n/\n\n/**\n * How much of a markdown text that is still being written is ready to be laid\n * out. Given the text so far, returns the part that will not change shape as\n * more arrives, and the heading of the section being written.\n *\n * A section is ready once the next one starts, so its layout is decided once.\n * Text before the first heading, and the opening under a `# Title`, is ready a\n * block at a time. A line, code fence or frontmatter block that is not yet\n * closed is never included.\n */\nexport function settledMarkdown(text: string): { markdown: string; writing: string | null } {\n let start = 0\n if (FRONTMATTER_OPEN.test(text)) {\n const opening = FRONTMATTER_OPEN.exec(text)![0].length\n // Too early to tell frontmatter from a rule: wait for the line after it.\n if (text.length === opening) return { markdown: \"\", writing: null }\n if (/\\S/.test(text[opening])) {\n const close = /\\n(---|\\.\\.\\.)[ \\t]*\\r?\\n/.exec(text.slice(opening - 1))\n if (!close) return { markdown: \"\", writing: null }\n start = opening - 1 + close.index + close[0].length\n }\n }\n\n let settled = start\n let writing: string | null = null\n // The depth of the section being collected, if it is held back until the next one starts.\n let open: number | null = null\n let fence: string | null = null\n let math = false\n let inBlock = false\n\n for (let at = start; ; ) {\n const end = text.indexOf(\"\\n\", at)\n // The last line is still being written.\n if (end < 0) break\n const line = text.slice(at, end).replace(/\\r$/, \"\")\n const lineStart = at\n at = end + 1\n\n const marker = FENCE.exec(line)\n if (fence) {\n if (marker && marker[1].startsWith(fence[0]) && marker[1].length >= fence.length && !line.trim().slice(marker[1].length).trim()) fence = null\n continue\n }\n if (math) {\n if (line.trim().endsWith(\"$$\")) math = false\n continue\n }\n if (marker) {\n fence = marker[1]\n inBlock = true\n continue\n }\n if (line.trim().startsWith(\"$$\") && !inBlock) {\n math = !(line.trim().length > 2 && line.trim().endsWith(\"$$\"))\n inBlock = true\n continue\n }\n if (!line.trim()) {\n // Outside a section, and in the opening under a title, each block stands as soon as it ends.\n if (open === null) settled = at\n else if (open === 1 && inBlock) {\n settled = at\n open = null\n }\n inBlock = false\n continue\n }\n const heading = HEADING.exec(line)\n if (heading) {\n const depth = heading[1].length\n writing = line.replace(/^#{1,6}\\s+/, \"\").replace(/\\s+#+\\s*$/, \"\")\n // A sub-heading continues the open section; anything at its level or above starts a new one.\n if (!(open !== null && open !== 1 && depth > open)) {\n settled = lineStart\n open = depth\n }\n inBlock = false\n continue\n }\n inBlock = true\n }\n return { markdown: text.slice(0, settled), writing }\n}\n\n// --- sorting a reply into commentary and content ---------------------------------\n\nexport type TurnEvent =\n /** Something the assistant said about its answer, not part of it. */\n | { type: \"commentary\"; text: string }\n /** The answer so far: every section that is complete, as one markdown document. */\n | { type: \"content\"; markdown: string }\n /** What is being written now — the heading of the section in progress, if it has one. */\n | { type: \"writing\"; heading: string | null }\n /**\n * Whether the reply is an answer at all. Sent with `false` when the model\n * opens by saying it does not know, cannot help or needs to ask something —\n * there will be nothing for the page — and with `true` if content follows after all.\n */\n | { type: \"answer\"; answered: boolean }\n\nexport type TurnResult = {\n /** Everything the model said, untouched. */\n raw: string\n /** The answer only, without commentary. */\n content: string\n}\n\nconst COMMENTARY_QUESTION = {\n type: \"noul\" as const,\n criteria: {\n true: \"the assistant is addressing the user directly about its answer (for example 'Here is…', 'Sure', 'Let me know…', 'I have…', 'Want me to…')\",\n false: \"a passage of the document itself, giving information to its readers\",\n },\n}\n\n/** Wording that marks a paragraph as addressed to the user rather than written for the page. */\nconst ADDRESSES_USER =\n /^(sure|certainly|of course|absolutely|great|okay|ok|alright|got it|no problem|here[’']?s|here (is|are)|below (is|are|you)|i[’'](ve|ll|d)\\b|i (have|will|can|kept|made|added|drafted|wrote|put|used|assumed|left)\\b|let me\\b|hope (this|that)|want me to|would you like|do you want|should i\\b|feel free|happy to|if you[’']?d like|if you would like|is there anything|anything else)|\\blet me know\\b|\\bwant me to\\b|\\bwould you like me to\\b/i\n\nconst ANSWERS_QUESTION = {\n type: \"noul\" as const,\n criteria: {\n true: \"the assistant knows the answer and is providing it\",\n false: \"the assistant does not know the answer, cannot help, or asks a question back\",\n },\n}\n\n/** Openings that give no answer — used when there is no classifier to ask. */\nconst NO_ANSWER =\n /^(sorry|apologies|unfortunately|i[’']?m (not sure|sorry|afraid|unable|not able)|i (don[’']?t|do not|can[’']?t|cannot|couldn[’']?t|am not able|am unable)\\b|(could|can|would) you (please )?(clarify|tell|share|provide|say|explain|rephrase|give)|what (do you mean|would you like|exactly))/i\n\nconst WANTS_CONTENT_QUESTION = {\n type: \"noul\" as const,\n criteria: {\n true: \"the user asks for something to be written, explained, listed, compared or added to the page\",\n false: \"the user is only making conversation: a greeting, thanks, small talk, feedback, or a question about the assistant itself\",\n },\n}\n\n/** Below this the classifier takes a message for conversation; it leans towards content, which is the costlier thing to miss. */\nconst CONVERSATION_BELOW = 0.42\n\n/** Messages that are conversation and nothing else — used when there is no classifier to ask. */\nconst SMALL_TALK =\n /^(h+i+|he+y+|hello+|hiya|howdy|yo|good (morning|afternoon|evening|night)|thanks?( you)?|thx|ty|cheers|ok(ay)?|cool|nice( (work|one|job))?|great( (work|job))?|good (work|job)|well done|awesome|perfect|lovely|love it|(that |this |it )?looks? (good|nice|great)|lol|ha(ha)+|bye|goodbye|see you|never ?mind|how are you|how('?s| is) it going|what'?s up|who are you|what are you|what can you do|what (model|llm) are you|are you (there|real|a bot|an ai|ok)|can you hear me|(are )?you there|test(ing)?)\\b(?:[\\s,!.?]+(there|again|everyone|all|you|so much|a lot|very much|today|then|cool|nice|great|thanks?( you)?))*[\\s!.?…]*$/i\n\n/**\n * Whether a message asks for something for the page — as opposed to a greeting,\n * thanks or small talk, whose reply belongs in the conversation alone. Asked\n * before the reply arrives, so the page need not make room for an answer that\n * is never coming. The classifier answers this reliably; without one, only\n * messages that are plainly small talk are taken as such.\n */\nexport function wantsContent(request: string, classify?: Classify, signal?: AbortSignal): Promise<boolean> {\n const text = request.trim()\n const byWording = !SMALL_TALK.test(text)\n if (!classify) return Promise.resolve(byWording)\n return classify({ state: `User: ${text.replace(/\\s+/g, \" \").slice(0, 400)}`, questions: { wants: WANTS_CONTENT_QUESTION } }, signal).then(\n (result) => (result.wants?.type === \"noul\" ? result.wants.noul >= CONVERSATION_BELOW : byWording),\n () => byWording,\n )\n}\n\n/** Where a paragraph sits in the reply — remarks to the user live at its edges. */\ntype Position = \"opening\" | \"inside\" | \"closing\"\n\nconst headingText = (block: ReplyBlock) => block.text.replace(/^#{1,6}\\s+/, \"\").replace(/\\s+#+\\s*$/, \"\")\n\n/**\n * Reads a model's reply as it streams and sorts it, block by block, into\n * commentary (remarks to the user) and content (the thing that was asked for).\n *\n * Headings, lists, tables, code and images are content. Plain paragraphs are\n * judged by where they sit and how they read: remarks to the user come at the\n * start or the end of a reply and usually announce themselves (\"Sure, here\n * is…\", \"Let me know…\"). The classifier is asked about the unclear ones.\n * Without a classifier, the first paragraph of the reply is taken as\n * commentary, and so is the last.\n *\n * Content is released a section at a time, once the section is complete, so a\n * section's layout is decided once and does not shift as it grows. Text that\n * comes before any heading is released paragraph by paragraph.\n */\nexport async function sortReply(options: {\n stream: AsyncIterable<string>\n /** What the user asked — needed to judge whether the reply answers it. */\n request?: string\n /**\n * The user was only making conversation (see `wantsContent`): the reply is\n * taken as conversation too, unless it turns out to carry content after all.\n */\n smallTalk?: boolean | Promise<boolean>\n classify?: Classify\n signal?: AbortSignal\n onEvent: (event: TurnEvent) => void\n}): Promise<TurnResult> {\n const { stream, request, smallTalk, classify, signal, onEvent } = options\n const splitter = new BlockSplitter()\n const released: string[] = []\n let raw = \"\"\n let index = 0\n let lastHeading: string | null = null\n // Once a heading, list or table has appeared, the opening remarks are over.\n let structured = false\n // The section being collected: it is held back until the next section starts.\n let open: { depth: number; blocks: string[] } | null = null\n // Blocks are judged as they complete but handled strictly in order.\n let chain: Promise<void> = Promise.resolve()\n // Whether a paragraph is the last thing in the reply is only known once the\n // next block arrives — or does not.\n let settlePosition: ((position: Position) => void) | null = null\n\n // Set when the reply opens without an answer: nothing is for the page unless real content follows.\n let unanswered = false\n\n /**\n * Whether the reply's opening paragraph is an answer, or the start of one —\n * as opposed to \"I don't know\", \"I can't help\" or a question back. Unlike the\n * remark-or-content question, this is one the classifier answers reliably.\n */\n const answers = async (text: string): Promise<boolean> => {\n // Conversation in, conversation out: there is nothing to ask.\n if (await smallTalk) return false\n if (!classify) return !NO_ANSWER.test(text)\n const state = `${request ? `User: ${request.replace(/\\s+/g, \" \").slice(0, 400)}\\n` : \"\"}Assistant: ${text.slice(0, 700)}`\n return classify({ state, questions: { answers: ANSWERS_QUESTION } }, signal).then(\n (result) => (result.answers?.type === \"noul\" ? result.answers.noul >= 0.5 : !NO_ANSWER.test(text)),\n () => !NO_ANSWER.test(text),\n )\n }\n\n const emitContent = () => onEvent({ type: \"content\", markdown: released.join(\"\\n\\n\") })\n const flush = () => {\n if (!open) return\n released.push(...open.blocks)\n open = null\n emitContent()\n }\n\n const ask = (text: string): Promise<number | null> =>\n classify!({ state: text.slice(0, 700), questions: { commentary: COMMENTARY_QUESTION } }, signal).then(\n (answers) => (answers.commentary?.type === \"noul\" ? answers.commentary.noul : null),\n () => null,\n )\n\n /**\n * Position and wording settle the clear cases; the classifier is asked about\n * the rest. On its own it is not a reliable judge of this, so it is never\n * allowed to overrule both.\n */\n const judge = async (text: string, first: boolean, position: Position): Promise<boolean> => {\n if (!classify) return first || position === \"closing\"\n const cue = ADDRESSES_USER.test(text)\n // In the body of the answer, only a paragraph that both reads like a remark and is judged one counts.\n if (position === \"inside\") return cue ? ((await ask(text)) ?? 0) >= 0.5 : false\n if (cue) return true\n const score = await ask(text)\n // If the classifier cannot be reached, fall back to the unaided rule.\n return score === null ? first || position === \"closing\" : score >= 0.3\n }\n\n const isCommentary = (block: ReplyBlock, first: boolean): Promise<boolean> => {\n if (block.kind !== \"paragraph\") return Promise.resolve(false)\n if (first || (classify && !structured)) return judge(block.text, first, \"opening\")\n return new Promise<Position>((resolve) => {\n settlePosition = resolve\n }).then((position) => judge(block.text, first, position))\n }\n\n const handle = (block: ReplyBlock, commentary: boolean) => {\n if (unanswered) {\n // Everything said around a non-answer is conversation…\n if (block.kind === \"paragraph\") return onEvent({ type: \"commentary\", text: block.text })\n // …unless the model goes on to produce something after all.\n unanswered = false\n onEvent({ type: \"answer\", answered: true })\n }\n if (commentary) return onEvent({ type: \"commentary\", text: block.text })\n if (block.kind === \"heading\") {\n const depth = block.depth ?? 2\n // A sub-heading continues the open section; anything at its level or above starts a new one.\n if (open && open.depth !== 1 && depth > open.depth) {\n open.blocks.push(block.text)\n return\n }\n flush()\n open = { depth, blocks: [block.text] }\n return\n }\n if (open) {\n open.blocks.push(block.text)\n // A title's section is the page's opening: it is shown once it has its first\n // block, and what follows — up to the next heading — arrives a block at a time.\n if (open.depth === 1) flush()\n } else {\n released.push(block.text)\n emitContent()\n }\n }\n\n const accept = (blocks: ReplyBlock[]) => {\n for (const block of blocks) {\n // Something followed the previous paragraph, so it was not the closing one.\n settlePosition?.(\"inside\")\n settlePosition = null\n const first = index++ === 0\n const verdict = isCommentary(block, first)\n // The opening paragraph also says whether there is an answer coming at all.\n const answered = first && block.kind === \"paragraph\" ? answers(block.text) : null\n if (block.kind === \"heading\") lastHeading = headingText(block)\n if (block.kind !== \"paragraph\") structured = true\n chain = chain.then(async () => {\n if (answered && !(await answered)) {\n unanswered = true\n onEvent({ type: \"answer\", answered: false })\n }\n // Nothing more needs judging about a paragraph of a non-answer.\n handle(block, unanswered && block.kind === \"paragraph\" ? true : await verdict)\n })\n }\n }\n\n let writing: string | null | undefined\n for await (const delta of stream) {\n if (signal?.aborted) break\n raw += delta\n accept(splitter.push(delta))\n // Tell the page which section is being written, so it can show that work is under way.\n if (lastHeading !== writing) onEvent({ type: \"writing\", heading: (writing = lastHeading) })\n }\n accept(splitter.end())\n // Nothing follows the final paragraph: it closes the reply.\n ;(settlePosition as ((position: Position) => void) | null)?.(\"closing\")\n await chain\n flush()\n return { raw, content: released.join(\"\\n\\n\") }\n}\n\n/** How the assistant is asked to write, so its answer can be laid out as a page. */\nexport const CHAT_INSTRUCTIONS = [\n \"You are writing for a page that turns markdown into a designed website.\",\n \"Answer in markdown. When asked for a document, start with a `# Title`, add a short opening paragraph, and use `##` sections; use `###` for short sub-points.\",\n \"Use tables for figures, schedules and comparisons, lists for short points, and blockquotes for quotations.\",\n \"The page draws charts by itself: a markdown table of numbers becomes a bar, line or area chart, shares of a whole become a donut, a row of key figures becomes stat tiles, and dated events become a timeline. So when a chart or graph is wanted, just write the data as a plain markdown table — one row per category or period, a header row, units in the header or the cells — and never draw one in text, link an image of one, or write chart code.\",\n \"Keep any remarks to the user — acknowledgements, caveats, questions, offers of more help — in their own short paragraphs, separate from the content.\",\n].join(\" \")\n\n/**\n * How the assistant is told about the document already on the page. What it\n * writes is appended below that document, so it must write only what is new.\n */\nexport function documentContext(markdown: string, limit = 16000): string {\n if (!markdown.trim()) return \"\"\n return [\n \"\",\n \"\",\n \"The page already shows the document below. Whatever you write is added to the page beneath it, as a new part.\",\n \"Write only the new material that was asked for — never reproduce or rewrite the existing document, not even to place the new part in context.\",\n \"\",\n \"<document>\",\n markdown.slice(0, limit),\n \"</document>\",\n ].join(\"\\n\")\n}\n"],"mappings":";AAeO,SAAS,WAAW,SAAuF;AAChH,SAAO,iBAAiB,UAAU,QAAQ;AACxC,UAAM,WAAW,MAAM,MAAM,QAAQ,UAAU;AAAA,MAC7C,QAAQ;AAAA,MACR,SAAS,EAAE,gBAAgB,oBAAoB,QAAQ,qBAAqB,GAAG,QAAQ,QAAQ;AAAA,MAC/F,MAAM,KAAK,UAAU,EAAE,GAAI,QAAQ,QAAQ,EAAE,OAAO,QAAQ,MAAM,IAAI,CAAC,GAAI,UAAU,QAAQ,KAAK,CAAC;AAAA,MACnG;AAAA,IACF,CAAC;AACD,QAAI,CAAC,SAAS,MAAM,CAAC,SAAS,MAAM;AAClC,YAAM,SAAS,MAAM,SAAS,KAAK,EAAE,MAAM,MAAM,EAAE;AACnD,YAAM,IAAI,MAAM,kBAAkB,SAAS,MAAM,GAAG,SAAS,KAAK,OAAO,MAAM,GAAG,GAAG,CAAC,KAAK,EAAE,EAAE;AAAA,IACjG;AAEA,SAAK,SAAS,QAAQ,IAAI,cAAc,KAAK,IAAI,SAAS,kBAAkB,GAAG;AAC7E,YAAM,OAAO,MAAM,SAAS,KAAK;AACjC,YAAM,UAAU,QAAQ,IAAI;AAC5B,UAAI,QAAS,OAAM,IAAI,MAAM,OAAO;AACpC,YAAM,QAAQ,MAAM,UAAU,CAAC,GAAG,SAAS;AAC3C,UAAI,OAAO,UAAU,SAAU,OAAM;AACrC;AAAA,IACF;AACA,UAAM,SAAS,SAAS,KAAK,UAAU;AACvC,UAAM,UAAU,IAAI,YAAY;AAChC,QAAI,SAAS;AAEb,QAAI,QAAQ;AACZ,QAAI,UAAU;AACd,eAAS;AACP,YAAM,EAAE,MAAM,MAAM,IAAI,MAAM,OAAO,KAAK;AAC1C,gBAAU,OAAO,QAAQ,OAAO,IAAI,QAAQ,OAAO,OAAO,EAAE,QAAQ,KAAK,CAAC;AAC1E,YAAM,QAAQ,OAAO,MAAM,IAAI;AAC/B,eAAS,OAAO,KAAM,MAAM,IAAI,KAAK;AACrC,iBAAW,OAAO,OAAO;AACvB,cAAM,OAAO,IAAI,KAAK;AACtB,YAAI,CAAC,KAAK,WAAW,OAAO,GAAG;AAC7B,cAAI,QAAQ,CAAC,KAAK,WAAW,GAAG,KAAK,MAAM,SAAS,IAAM,UAAS;AACnE;AAAA,QACF;AACA,cAAM,UAAU,KAAK,MAAM,CAAC,EAAE,KAAK;AACnC,YAAI,YAAY,SAAU;AAC1B,YAAI;AACJ,YAAI;AACF,kBAAQ,KAAK,MAAM,OAAO;AAAA,QAC5B,QAAQ;AAEN;AAAA,QACF;AACA,cAAM,UAAU,QAAQ,KAAK;AAC7B,YAAI,QAAS,OAAM,IAAI,MAAM,OAAO;AACpC,cAAM,QAAS,OAA6D,UAAU,CAAC,GAAG,OAAO;AACjG,YAAI,OAAO,UAAU,YAAY,OAAO;AACtC,oBAAU;AACV,gBAAM;AAAA,QACR;AAAA,MACF;AACA,UAAI,KAAM;AAAA,IACZ;AACA,QAAI,CAAC,WAAW,MAAM,KAAK,GAAG;AAC5B,UAAI,UAAyB;AAC7B,UAAI;AACF,kBAAU,QAAQ,KAAK,MAAM,KAAK,CAAC;AAAA,MACrC,QAAQ;AAAA,MAER;AACA,YAAM,IAAI,MAAM,WAAW,iCAAiC;AAAA,IAC9D;AAAA,EACF;AACF;AAGA,SAAS,QAAQ,MAA8B;AAC7C,QAAM,QAAQ,MAAM,QAAQ,IAAI,IAAI,KAAK,CAAC,IAAI;AAC9C,QAAM,QAAS,OAAsC;AACrD,MAAI,CAAC,MAAO,QAAO;AACnB,MAAI,OAAO,UAAU,SAAU,QAAO;AACtC,QAAM,UAAW,MAAgC;AACjD,SAAO,OAAO,YAAY,YAAY,UAAU,UAAU;AAC5D;AAMO,SAAS,mBAAmB,KAAqB;AACtD,QAAM,UAAU,IAAI,KAAK,EAAE,QAAQ,QAAQ,EAAE;AAC7C,SAAO,uBAAuB,KAAK,OAAO,IAAI,UAAU,GAAG,OAAO;AACpE;AAaA,IAAM,UAAU;AAChB,IAAM,QAAQ;AACd,IAAM,YAAY;AAElB,SAAS,SAAS,MAA0B;AAC1C,QAAM,QAAQ,KAAK,MAAM,IAAI,EAAE,CAAC;AAChC,QAAM,UAAU,QAAQ,KAAK,KAAK;AAClC,MAAI,QAAS,QAAO,EAAE,MAAM,MAAM,WAAW,OAAO,QAAQ,CAAC,EAAE,OAAO;AACtE,QAAM,QAAQ,wBAAwB,KAAK,KAAK,MAAM,IAAI,EAAE,CAAC,KAAK,EAAE;AAEpE,QAAM,WAAW,sBAAsB,KAAK,IAAI;AAChD,MAAI,MAAM,KAAK,KAAK,KAAK,MAAM,WAAW,IAAI,KAAK,UAAU,KAAK,KAAK,KAAK,SAAS,SAAU,QAAO,EAAE,MAAM,MAAM,QAAQ;AAC5H,SAAO,EAAE,MAAM,MAAM,YAAY;AACnC;AAOO,IAAM,gBAAN,MAAoB;AAAA,EAApB;AACL,SAAQ,UAAU;AAClB,SAAQ,QAAkB,CAAC;AAC3B,SAAQ,QAAuB;AAC/B,SAAQ,OAAO;AAAA;AAAA,EAEP,MAAM,KAAmB;AAC/B,UAAM,OAAO,KAAK,MAAM,KAAK,IAAI,EAAE,KAAK;AACxC,SAAK,QAAQ,CAAC;AACd,QAAI,KAAM,KAAI,KAAK,SAAS,IAAI,CAAC;AAAA,EACnC;AAAA,EAEQ,KAAK,MAAc,KAAmB;AAC5C,UAAM,QAAQ,MAAM,KAAK,IAAI;AAC7B,QAAI,KAAK,OAAO;AACd,WAAK,MAAM,KAAK,IAAI;AACpB,UAAI,SAAS,MAAM,CAAC,EAAE,WAAW,KAAK,MAAM,CAAC,CAAC,KAAK,MAAM,CAAC,EAAE,UAAU,KAAK,MAAM,UAAU,CAAC,KAAK,KAAK,EAAE,MAAM,MAAM,CAAC,EAAE,MAAM,EAAE,KAAK,GAAG;AACrI,aAAK,QAAQ;AAAA,MACf;AACA;AAAA,IACF;AACA,QAAI,KAAK,MAAM;AACb,WAAK,MAAM,KAAK,IAAI;AACpB,UAAI,KAAK,KAAK,EAAE,SAAS,IAAI,EAAG,MAAK,OAAO;AAC5C;AAAA,IACF;AACA,QAAI,OAAO;AACT,WAAK,MAAM,KAAK,IAAI;AACpB,WAAK,QAAQ,MAAM,CAAC;AACpB;AAAA,IACF;AACA,QAAI,KAAK,KAAK,EAAE,WAAW,IAAI,KAAK,KAAK,MAAM,WAAW,GAAG;AAC3D,WAAK,MAAM,KAAK,IAAI;AACpB,WAAK,OAAO,EAAE,KAAK,KAAK,EAAE,SAAS,KAAK,KAAK,KAAK,EAAE,SAAS,IAAI;AACjE;AAAA,IACF;AACA,QAAI,CAAC,KAAK,KAAK,EAAG,QAAO,KAAK,MAAM,GAAG;AAEvC,QAAI,QAAQ,KAAK,IAAI,GAAG;AACtB,WAAK,MAAM,GAAG;AACd,WAAK,MAAM,KAAK,IAAI;AACpB,aAAO,KAAK,MAAM,GAAG;AAAA,IACvB;AACA,SAAK,MAAM,KAAK,IAAI;AAAA,EACtB;AAAA;AAAA,EAGA,KAAK,MAA4B;AAC/B,UAAM,MAAoB,CAAC;AAC3B,SAAK,WAAW;AAChB,UAAM,QAAQ,KAAK,QAAQ,MAAM,IAAI;AACrC,SAAK,UAAU,MAAM,IAAI,KAAK;AAC9B,eAAW,QAAQ,MAAO,MAAK,KAAK,KAAK,QAAQ,OAAO,EAAE,GAAG,GAAG;AAChE,WAAO;AAAA,EACT;AAAA;AAAA,EAGA,MAAoB;AAClB,UAAM,MAAoB,CAAC;AAC3B,QAAI,KAAK,QAAS,MAAK,KAAK,KAAK,SAAS,GAAG;AAC7C,SAAK,UAAU;AACf,SAAK,QAAQ;AACb,SAAK,OAAO;AACZ,SAAK,MAAM,GAAG;AACd,WAAO;AAAA,EACT;AAEF;AAEA,IAAM,mBAAmB;AAYlB,SAAS,gBAAgB,MAA4D;AAC1F,MAAI,QAAQ;AACZ,MAAI,iBAAiB,KAAK,IAAI,GAAG;AAC/B,UAAM,UAAU,iBAAiB,KAAK,IAAI,EAAG,CAAC,EAAE;AAEhD,QAAI,KAAK,WAAW,QAAS,QAAO,EAAE,UAAU,IAAI,SAAS,KAAK;AAClE,QAAI,KAAK,KAAK,KAAK,OAAO,CAAC,GAAG;AAC5B,YAAM,QAAQ,4BAA4B,KAAK,KAAK,MAAM,UAAU,CAAC,CAAC;AACtE,UAAI,CAAC,MAAO,QAAO,EAAE,UAAU,IAAI,SAAS,KAAK;AACjD,cAAQ,UAAU,IAAI,MAAM,QAAQ,MAAM,CAAC,EAAE;AAAA,IAC/C;AAAA,EACF;AAEA,MAAI,UAAU;AACd,MAAI,UAAyB;AAE7B,MAAI,OAAsB;AAC1B,MAAI,QAAuB;AAC3B,MAAI,OAAO;AACX,MAAI,UAAU;AAEd,WAAS,KAAK,WAAW;AACvB,UAAM,MAAM,KAAK,QAAQ,MAAM,EAAE;AAEjC,QAAI,MAAM,EAAG;AACb,UAAM,OAAO,KAAK,MAAM,IAAI,GAAG,EAAE,QAAQ,OAAO,EAAE;AAClD,UAAM,YAAY;AAClB,SAAK,MAAM;AAEX,UAAM,SAAS,MAAM,KAAK,IAAI;AAC9B,QAAI,OAAO;AACT,UAAI,UAAU,OAAO,CAAC,EAAE,WAAW,MAAM,CAAC,CAAC,KAAK,OAAO,CAAC,EAAE,UAAU,MAAM,UAAU,CAAC,KAAK,KAAK,EAAE,MAAM,OAAO,CAAC,EAAE,MAAM,EAAE,KAAK,EAAG,SAAQ;AACzI;AAAA,IACF;AACA,QAAI,MAAM;AACR,UAAI,KAAK,KAAK,EAAE,SAAS,IAAI,EAAG,QAAO;AACvC;AAAA,IACF;AACA,QAAI,QAAQ;AACV,cAAQ,OAAO,CAAC;AAChB,gBAAU;AACV;AAAA,IACF;AACA,QAAI,KAAK,KAAK,EAAE,WAAW,IAAI,KAAK,CAAC,SAAS;AAC5C,aAAO,EAAE,KAAK,KAAK,EAAE,SAAS,KAAK,KAAK,KAAK,EAAE,SAAS,IAAI;AAC5D,gBAAU;AACV;AAAA,IACF;AACA,QAAI,CAAC,KAAK,KAAK,GAAG;AAEhB,UAAI,SAAS,KAAM,WAAU;AAAA,eACpB,SAAS,KAAK,SAAS;AAC9B,kBAAU;AACV,eAAO;AAAA,MACT;AACA,gBAAU;AACV;AAAA,IACF;AACA,UAAM,UAAU,QAAQ,KAAK,IAAI;AACjC,QAAI,SAAS;AACX,YAAM,QAAQ,QAAQ,CAAC,EAAE;AACzB,gBAAU,KAAK,QAAQ,cAAc,EAAE,EAAE,QAAQ,aAAa,EAAE;AAEhE,UAAI,EAAE,SAAS,QAAQ,SAAS,KAAK,QAAQ,OAAO;AAClD,kBAAU;AACV,eAAO;AAAA,MACT;AACA,gBAAU;AACV;AAAA,IACF;AACA,cAAU;AAAA,EACZ;AACA,SAAO,EAAE,UAAU,KAAK,MAAM,GAAG,OAAO,GAAG,QAAQ;AACrD;AAyBA,IAAM,sBAAsB;AAAA,EAC1B,MAAM;AAAA,EACN,UAAU;AAAA,IACR,MAAM;AAAA,IACN,OAAO;AAAA,EACT;AACF;AAGA,IAAM,iBACJ;AAEF,IAAM,mBAAmB;AAAA,EACvB,MAAM;AAAA,EACN,UAAU;AAAA,IACR,MAAM;AAAA,IACN,OAAO;AAAA,EACT;AACF;AAGA,IAAM,YACJ;AAEF,IAAM,yBAAyB;AAAA,EAC7B,MAAM;AAAA,EACN,UAAU;AAAA,IACR,MAAM;AAAA,IACN,OAAO;AAAA,EACT;AACF;AAGA,IAAM,qBAAqB;AAG3B,IAAM,aACJ;AASK,SAAS,aAAa,SAAiB,UAAqB,QAAwC;AACzG,QAAM,OAAO,QAAQ,KAAK;AAC1B,QAAM,YAAY,CAAC,WAAW,KAAK,IAAI;AACvC,MAAI,CAAC,SAAU,QAAO,QAAQ,QAAQ,SAAS;AAC/C,SAAO,SAAS,EAAE,OAAO,SAAS,KAAK,QAAQ,QAAQ,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC,IAAI,WAAW,EAAE,OAAO,uBAAuB,EAAE,GAAG,MAAM,EAAE;AAAA,IACnI,CAAC,WAAY,OAAO,OAAO,SAAS,SAAS,OAAO,MAAM,QAAQ,qBAAqB;AAAA,IACvF,MAAM;AAAA,EACR;AACF;AAKA,IAAM,cAAc,CAAC,UAAsB,MAAM,KAAK,QAAQ,cAAc,EAAE,EAAE,QAAQ,aAAa,EAAE;AAiBvG,eAAsB,UAAU,SAYR;AACtB,QAAM,EAAE,QAAQ,SAAS,WAAW,UAAU,QAAQ,QAAQ,IAAI;AAClE,QAAM,WAAW,IAAI,cAAc;AACnC,QAAM,WAAqB,CAAC;AAC5B,MAAI,MAAM;AACV,MAAI,QAAQ;AACZ,MAAI,cAA6B;AAEjC,MAAI,aAAa;AAEjB,MAAI,OAAmD;AAEvD,MAAI,QAAuB,QAAQ,QAAQ;AAG3C,MAAI,iBAAwD;AAG5D,MAAI,aAAa;AAOjB,QAAM,UAAU,OAAO,SAAmC;AAExD,QAAI,MAAM,UAAW,QAAO;AAC5B,QAAI,CAAC,SAAU,QAAO,CAAC,UAAU,KAAK,IAAI;AAC1C,UAAM,QAAQ,GAAG,UAAU,SAAS,QAAQ,QAAQ,QAAQ,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC;AAAA,IAAO,EAAE,cAAc,KAAK,MAAM,GAAG,GAAG,CAAC;AACvH,WAAO,SAAS,EAAE,OAAO,WAAW,EAAE,SAAS,iBAAiB,EAAE,GAAG,MAAM,EAAE;AAAA,MAC3E,CAAC,WAAY,OAAO,SAAS,SAAS,SAAS,OAAO,QAAQ,QAAQ,MAAM,CAAC,UAAU,KAAK,IAAI;AAAA,MAChG,MAAM,CAAC,UAAU,KAAK,IAAI;AAAA,IAC5B;AAAA,EACF;AAEA,QAAM,cAAc,MAAM,QAAQ,EAAE,MAAM,WAAW,UAAU,SAAS,KAAK,MAAM,EAAE,CAAC;AACtF,QAAM,QAAQ,MAAM;AAClB,QAAI,CAAC,KAAM;AACX,aAAS,KAAK,GAAG,KAAK,MAAM;AAC5B,WAAO;AACP,gBAAY;AAAA,EACd;AAEA,QAAM,MAAM,CAAC,SACX,SAAU,EAAE,OAAO,KAAK,MAAM,GAAG,GAAG,GAAG,WAAW,EAAE,YAAY,oBAAoB,EAAE,GAAG,MAAM,EAAE;AAAA,IAC/F,CAACA,aAAaA,SAAQ,YAAY,SAAS,SAASA,SAAQ,WAAW,OAAO;AAAA,IAC9E,MAAM;AAAA,EACR;AAOF,QAAM,QAAQ,OAAO,MAAc,OAAgB,aAAyC;AAC1F,QAAI,CAAC,SAAU,QAAO,SAAS,aAAa;AAC5C,UAAM,MAAM,eAAe,KAAK,IAAI;AAEpC,QAAI,aAAa,SAAU,QAAO,OAAQ,MAAM,IAAI,IAAI,KAAM,MAAM,MAAM;AAC1E,QAAI,IAAK,QAAO;AAChB,UAAM,QAAQ,MAAM,IAAI,IAAI;AAE5B,WAAO,UAAU,OAAO,SAAS,aAAa,YAAY,SAAS;AAAA,EACrE;AAEA,QAAM,eAAe,CAAC,OAAmB,UAAqC;AAC5E,QAAI,MAAM,SAAS,YAAa,QAAO,QAAQ,QAAQ,KAAK;AAC5D,QAAI,SAAU,YAAY,CAAC,WAAa,QAAO,MAAM,MAAM,MAAM,OAAO,SAAS;AACjF,WAAO,IAAI,QAAkB,CAAC,YAAY;AACxC,uBAAiB;AAAA,IACnB,CAAC,EAAE,KAAK,CAAC,aAAa,MAAM,MAAM,MAAM,OAAO,QAAQ,CAAC;AAAA,EAC1D;AAEA,QAAM,SAAS,CAAC,OAAmB,eAAwB;AACzD,QAAI,YAAY;AAEd,UAAI,MAAM,SAAS,YAAa,QAAO,QAAQ,EAAE,MAAM,cAAc,MAAM,MAAM,KAAK,CAAC;AAEvF,mBAAa;AACb,cAAQ,EAAE,MAAM,UAAU,UAAU,KAAK,CAAC;AAAA,IAC5C;AACA,QAAI,WAAY,QAAO,QAAQ,EAAE,MAAM,cAAc,MAAM,MAAM,KAAK,CAAC;AACvE,QAAI,MAAM,SAAS,WAAW;AAC5B,YAAM,QAAQ,MAAM,SAAS;AAE7B,UAAI,QAAQ,KAAK,UAAU,KAAK,QAAQ,KAAK,OAAO;AAClD,aAAK,OAAO,KAAK,MAAM,IAAI;AAC3B;AAAA,MACF;AACA,YAAM;AACN,aAAO,EAAE,OAAO,QAAQ,CAAC,MAAM,IAAI,EAAE;AACrC;AAAA,IACF;AACA,QAAI,MAAM;AACR,WAAK,OAAO,KAAK,MAAM,IAAI;AAG3B,UAAI,KAAK,UAAU,EAAG,OAAM;AAAA,IAC9B,OAAO;AACL,eAAS,KAAK,MAAM,IAAI;AACxB,kBAAY;AAAA,IACd;AAAA,EACF;AAEA,QAAM,SAAS,CAAC,WAAyB;AACvC,eAAW,SAAS,QAAQ;AAE1B,uBAAiB,QAAQ;AACzB,uBAAiB;AACjB,YAAM,QAAQ,YAAY;AAC1B,YAAM,UAAU,aAAa,OAAO,KAAK;AAEzC,YAAM,WAAW,SAAS,MAAM,SAAS,cAAc,QAAQ,MAAM,IAAI,IAAI;AAC7E,UAAI,MAAM,SAAS,UAAW,eAAc,YAAY,KAAK;AAC7D,UAAI,MAAM,SAAS,YAAa,cAAa;AAC7C,cAAQ,MAAM,KAAK,YAAY;AAC7B,YAAI,YAAY,CAAE,MAAM,UAAW;AACjC,uBAAa;AACb,kBAAQ,EAAE,MAAM,UAAU,UAAU,MAAM,CAAC;AAAA,QAC7C;AAEA,eAAO,OAAO,cAAc,MAAM,SAAS,cAAc,OAAO,MAAM,OAAO;AAAA,MAC/E,CAAC;AAAA,IACH;AAAA,EACF;AAEA,MAAI;AACJ,mBAAiB,SAAS,QAAQ;AAChC,QAAI,QAAQ,QAAS;AACrB,WAAO;AACP,WAAO,SAAS,KAAK,KAAK,CAAC;AAE3B,QAAI,gBAAgB,QAAS,SAAQ,EAAE,MAAM,WAAW,SAAU,UAAU,YAAa,CAAC;AAAA,EAC5F;AACA,SAAO,SAAS,IAAI,CAAC;AAEpB,EAAC,iBAA2D,SAAS;AACtE,QAAM;AACN,QAAM;AACN,SAAO,EAAE,KAAK,SAAS,SAAS,KAAK,MAAM,EAAE;AAC/C;AAGO,IAAM,oBAAoB;AAAA,EAC/B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,EAAE,KAAK,GAAG;","names":["answers"]}
|
package/dist/core.d.mts
CHANGED
|
@@ -186,6 +186,11 @@ type PlanInput = {
|
|
|
186
186
|
/** Natural image sizes keyed by the URL written in the markdown. */
|
|
187
187
|
images?: ImageMetaMap;
|
|
188
188
|
judgements?: Judgements;
|
|
189
|
+
/**
|
|
190
|
+
* Put in front of every id the plan generates. Needed when several documents
|
|
191
|
+
* share one page, so their section anchors cannot collide.
|
|
192
|
+
*/
|
|
193
|
+
idPrefix?: string;
|
|
189
194
|
};
|
|
190
195
|
/** Every image URL in the document, for size probing. */
|
|
191
196
|
declare function collectImageUrls(root: Root): string[];
|
|
@@ -269,6 +274,12 @@ type JudgeOptions = {
|
|
|
269
274
|
confidence?: number;
|
|
270
275
|
/** The theme the document's own keywords point to, if any (see `matchTheme`). */
|
|
271
276
|
themeHint?: PaletteId;
|
|
277
|
+
/** Leave the theme question out — for a document whose theme is already settled. */
|
|
278
|
+
skipTheme?: boolean;
|
|
279
|
+
/** Ask the theme question and nothing else. */
|
|
280
|
+
themeOnly?: boolean;
|
|
281
|
+
/** What the document was written in answer to, if anything — it helps place the subject. */
|
|
282
|
+
context?: string;
|
|
272
283
|
/** Requests in flight at once. Default 3. */
|
|
273
284
|
concurrency?: number;
|
|
274
285
|
/** Upper bound on tables sent for a form judgement. */
|
|
@@ -305,6 +316,15 @@ type PaletteTokens = {
|
|
|
305
316
|
border: string;
|
|
306
317
|
accent: string;
|
|
307
318
|
accentFg: string;
|
|
319
|
+
/**
|
|
320
|
+
* A second colour, where the theme has one: quotations are set in it — a band
|
|
321
|
+
* across the page for a quotation that is a section of its own, a block for one
|
|
322
|
+
* inside an article — so the page has more than one note to play.
|
|
323
|
+
*/
|
|
324
|
+
highlight?: {
|
|
325
|
+
bg: string;
|
|
326
|
+
fg: string;
|
|
327
|
+
};
|
|
308
328
|
};
|
|
309
329
|
/**
|
|
310
330
|
* Chart drawing styles: `linework` is monochrome print-style ink with hatching,
|
|
@@ -312,12 +332,31 @@ type PaletteTokens = {
|
|
|
312
332
|
* gradients and depth, and `flat` is plain solid colour.
|
|
313
333
|
*/
|
|
314
334
|
type ChartStyle = "linework" | "instrument" | "soft" | "flat";
|
|
335
|
+
/**
|
|
336
|
+
* How a text-led hero is set: `wash` tints the page with a soft glow of the
|
|
337
|
+
* accent; `block` is a cover in the accent colour itself; `ink` is a cover in
|
|
338
|
+
* the theme's darkest tone, the page's colours reversed.
|
|
339
|
+
*/
|
|
340
|
+
type HeroTone = "wash" | "block" | "ink";
|
|
341
|
+
/** The families the themes fall into by subject — how the picker is arranged. */
|
|
342
|
+
type ThemeTopic = "writing" | "official" | "technology" | "lifestyle" | "wellbeing" | "culture";
|
|
343
|
+
declare const themeTopics: {
|
|
344
|
+
id: ThemeTopic;
|
|
345
|
+
name: string;
|
|
346
|
+
}[];
|
|
315
347
|
type Theme = {
|
|
316
348
|
id: PaletteId;
|
|
317
349
|
name: string;
|
|
318
|
-
/** The
|
|
350
|
+
/** The family of subjects it belongs to. */
|
|
351
|
+
topic: ThemeTopic;
|
|
352
|
+
/**
|
|
353
|
+
* The kinds of document this theme suits — and all the classifier reads when
|
|
354
|
+
* choosing one. Name subjects, not moods: measured against documents of known
|
|
355
|
+
* subject, subjects alone choose better, and in half the time, than subjects
|
|
356
|
+
* with colours and typefaces beside them.
|
|
357
|
+
*/
|
|
319
358
|
description: string;
|
|
320
|
-
/** Its key colours in a few plain words
|
|
359
|
+
/** Its key colours in a few plain words, for people reading the list. */
|
|
321
360
|
look: string;
|
|
322
361
|
/** Typography used unless the caller picks another. */
|
|
323
362
|
fonts: FontPairingId;
|
|
@@ -325,6 +364,8 @@ type Theme = {
|
|
|
325
364
|
formality: number;
|
|
326
365
|
/** How charts are drawn under this theme. */
|
|
327
366
|
chart: ChartStyle;
|
|
367
|
+
/** How the opening of the page is set off from the rest. */
|
|
368
|
+
hero: HeroTone;
|
|
328
369
|
/** Fallback matching when no classifier is configured. */
|
|
329
370
|
keywords: RegExp;
|
|
330
371
|
light: PaletteTokens;
|
|
@@ -349,9 +390,8 @@ type FontPairing = {
|
|
|
349
390
|
declare const fontPairings: Record<FontPairingId, FontPairing>;
|
|
350
391
|
declare const fontPairingList: FontPairing[];
|
|
351
392
|
/**
|
|
352
|
-
* What the classifier reads when choosing a theme: the
|
|
353
|
-
*
|
|
354
|
-
* subject. Kept terse on purpose — classifier latency grows with every
|
|
393
|
+
* What the classifier reads when choosing a theme: the subjects it suits, and
|
|
394
|
+
* nothing else. Kept terse on purpose — classifier latency grows with every
|
|
355
395
|
* character here, across every theme.
|
|
356
396
|
*/
|
|
357
397
|
declare function describeTheme(theme: Theme): string;
|
|
@@ -374,4 +414,136 @@ declare function guessTheme(text: string, stats: {
|
|
|
374
414
|
tables: number;
|
|
375
415
|
}): ThemeChoice;
|
|
376
416
|
|
|
377
|
-
|
|
417
|
+
type ChatMessage = {
|
|
418
|
+
role: "system" | "user" | "assistant";
|
|
419
|
+
content: string;
|
|
420
|
+
};
|
|
421
|
+
/**
|
|
422
|
+
* Anything that answers a conversation with a stream of text: usually
|
|
423
|
+
* `createChat` pointed at an OpenAI-compatible endpoint or at your own proxy.
|
|
424
|
+
*/
|
|
425
|
+
type Chat = (messages: ChatMessage[], signal?: AbortSignal) => AsyncIterable<string>;
|
|
426
|
+
/**
|
|
427
|
+
* A chat function for an OpenAI-compatible `chat/completions` endpoint, read as
|
|
428
|
+
* a stream. Point it at a route on your own server when the provider needs a key
|
|
429
|
+
* (see `createChatHandler` in `stunning-md/server`); a key in browser code is public.
|
|
430
|
+
*/
|
|
431
|
+
declare function createChat(options: {
|
|
432
|
+
endpoint: string;
|
|
433
|
+
model?: string;
|
|
434
|
+
headers?: Record<string, string>;
|
|
435
|
+
}): Chat;
|
|
436
|
+
/**
|
|
437
|
+
* The address of an OpenAI-compatible chat endpoint, given either the full
|
|
438
|
+
* `…/chat/completions` address or just the API's base (`…/v1`).
|
|
439
|
+
*/
|
|
440
|
+
declare function chatCompletionsUrl(url: string): string;
|
|
441
|
+
type BlockKind = "heading" | "paragraph" | "other";
|
|
442
|
+
type ReplyBlock = {
|
|
443
|
+
text: string;
|
|
444
|
+
kind: BlockKind;
|
|
445
|
+
/** Heading level, for headings. */
|
|
446
|
+
depth?: number;
|
|
447
|
+
};
|
|
448
|
+
/**
|
|
449
|
+
* Cuts markdown, as it streams in, into the blocks a reader would recognise:
|
|
450
|
+
* paragraphs, headings, lists, tables, code. A block is released once it is
|
|
451
|
+
* known to be complete — at the blank line after it, or at the end.
|
|
452
|
+
*/
|
|
453
|
+
declare class BlockSplitter {
|
|
454
|
+
private pending;
|
|
455
|
+
private lines;
|
|
456
|
+
private fence;
|
|
457
|
+
private math;
|
|
458
|
+
private flush;
|
|
459
|
+
private take;
|
|
460
|
+
/** Feed more text; returns the blocks that are now complete. */
|
|
461
|
+
push(text: string): ReplyBlock[];
|
|
462
|
+
/** The stream is over; returns whatever was still open. */
|
|
463
|
+
end(): ReplyBlock[];
|
|
464
|
+
}
|
|
465
|
+
/**
|
|
466
|
+
* How much of a markdown text that is still being written is ready to be laid
|
|
467
|
+
* out. Given the text so far, returns the part that will not change shape as
|
|
468
|
+
* more arrives, and the heading of the section being written.
|
|
469
|
+
*
|
|
470
|
+
* A section is ready once the next one starts, so its layout is decided once.
|
|
471
|
+
* Text before the first heading, and the opening under a `# Title`, is ready a
|
|
472
|
+
* block at a time. A line, code fence or frontmatter block that is not yet
|
|
473
|
+
* closed is never included.
|
|
474
|
+
*/
|
|
475
|
+
declare function settledMarkdown(text: string): {
|
|
476
|
+
markdown: string;
|
|
477
|
+
writing: string | null;
|
|
478
|
+
};
|
|
479
|
+
type TurnEvent =
|
|
480
|
+
/** Something the assistant said about its answer, not part of it. */
|
|
481
|
+
{
|
|
482
|
+
type: "commentary";
|
|
483
|
+
text: string;
|
|
484
|
+
}
|
|
485
|
+
/** The answer so far: every section that is complete, as one markdown document. */
|
|
486
|
+
| {
|
|
487
|
+
type: "content";
|
|
488
|
+
markdown: string;
|
|
489
|
+
}
|
|
490
|
+
/** What is being written now — the heading of the section in progress, if it has one. */
|
|
491
|
+
| {
|
|
492
|
+
type: "writing";
|
|
493
|
+
heading: string | null;
|
|
494
|
+
}
|
|
495
|
+
/**
|
|
496
|
+
* Whether the reply is an answer at all. Sent with `false` when the model
|
|
497
|
+
* opens by saying it does not know, cannot help or needs to ask something —
|
|
498
|
+
* there will be nothing for the page — and with `true` if content follows after all.
|
|
499
|
+
*/
|
|
500
|
+
| {
|
|
501
|
+
type: "answer";
|
|
502
|
+
answered: boolean;
|
|
503
|
+
};
|
|
504
|
+
type TurnResult = {
|
|
505
|
+
/** Everything the model said, untouched. */
|
|
506
|
+
raw: string;
|
|
507
|
+
/** The answer only, without commentary. */
|
|
508
|
+
content: string;
|
|
509
|
+
};
|
|
510
|
+
/**
|
|
511
|
+
* Whether a message asks for something for the page — as opposed to a greeting,
|
|
512
|
+
* thanks or small talk, whose reply belongs in the conversation alone. Asked
|
|
513
|
+
* before the reply arrives, so the page need not make room for an answer that
|
|
514
|
+
* is never coming. The classifier answers this reliably; without one, only
|
|
515
|
+
* messages that are plainly small talk are taken as such.
|
|
516
|
+
*/
|
|
517
|
+
declare function wantsContent(request: string, classify?: Classify, signal?: AbortSignal): Promise<boolean>;
|
|
518
|
+
/**
|
|
519
|
+
* Reads a model's reply as it streams and sorts it, block by block, into
|
|
520
|
+
* commentary (remarks to the user) and content (the thing that was asked for).
|
|
521
|
+
*
|
|
522
|
+
* Headings, lists, tables, code and images are content. Plain paragraphs are
|
|
523
|
+
* judged by where they sit and how they read: remarks to the user come at the
|
|
524
|
+
* start or the end of a reply and usually announce themselves ("Sure, here
|
|
525
|
+
* is…", "Let me know…"). The classifier is asked about the unclear ones.
|
|
526
|
+
* Without a classifier, the first paragraph of the reply is taken as
|
|
527
|
+
* commentary, and so is the last.
|
|
528
|
+
*
|
|
529
|
+
* Content is released a section at a time, once the section is complete, so a
|
|
530
|
+
* section's layout is decided once and does not shift as it grows. Text that
|
|
531
|
+
* comes before any heading is released paragraph by paragraph.
|
|
532
|
+
*/
|
|
533
|
+
declare function sortReply(options: {
|
|
534
|
+
stream: AsyncIterable<string>;
|
|
535
|
+
/** What the user asked — needed to judge whether the reply answers it. */
|
|
536
|
+
request?: string;
|
|
537
|
+
/**
|
|
538
|
+
* The user was only making conversation (see `wantsContent`): the reply is
|
|
539
|
+
* taken as conversation too, unless it turns out to carry content after all.
|
|
540
|
+
*/
|
|
541
|
+
smallTalk?: boolean | Promise<boolean>;
|
|
542
|
+
classify?: Classify;
|
|
543
|
+
signal?: AbortSignal;
|
|
544
|
+
onEvent: (event: TurnEvent) => void;
|
|
545
|
+
}): Promise<TurnResult>;
|
|
546
|
+
/** How the assistant is asked to write, so its answer can be laid out as a page. */
|
|
547
|
+
declare const CHAT_INSTRUCTIONS: string;
|
|
548
|
+
|
|
549
|
+
export { type Appearance, type Block, BlockSplitter, CHAT_INSTRUCTIONS, type ChartSpec, type Chat, type ChatMessage, type ClassifierAnswer, type ClassifierAnswers, type ClassifierQuestion, type ClassifierRequest, type Classify, type ColumnType, type DocumentPlan, type FontPairing, type FontPairingId, type Frontmatter, type Hero, type HeroTone, type HeroVariant, type ImageMeta, type ImageMetaMap, type ImageRef, type JudgeOptions, type JudgeProgress, type Judgements, type ListVariant, type MediaVariant, type PaletteId, type PaletteTokens, type ParsedMarkdown, type PlanInput, type QuoteVariant, type ReplyBlock, type Section, type SectionLayout, type SectionTone, type TableColumn, type TableModel, type TableRow, type Theme, type ThemeChoice, type ThemeTopic, type TocEntry, type TurnEvent, type TurnResult, type VizKind, type VizPlan, buildTableModel, chatCompletionsUrl, collectImageUrls, createChat, createClassifier, describeTheme, documentDigest, fontPairingList, fontPairings, formatValue, guessTheme, judgeDocument, matchTheme, parseDate, parseMarkdown, parseNumber, planDocument, planViz, settledMarkdown, sortReply, themeChoice, themeList, themeTopics, themes, wantsContent, withVizKind };
|