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/dist/index.d.mts CHANGED
@@ -230,6 +230,12 @@ type JudgeOptions = {
230
230
  confidence?: number;
231
231
  /** The theme the document's own keywords point to, if any (see `matchTheme`). */
232
232
  themeHint?: PaletteId;
233
+ /** Leave the theme question out — for a document whose theme is already settled. */
234
+ skipTheme?: boolean;
235
+ /** Ask the theme question and nothing else. */
236
+ themeOnly?: boolean;
237
+ /** What the document was written in answer to, if anything — it helps place the subject. */
238
+ context?: string;
233
239
  /** Requests in flight at once. Default 3. */
234
240
  concurrency?: number;
235
241
  /** Upper bound on tables sent for a form judgement. */
@@ -256,8 +262,154 @@ type JudgeProgress = {
256
262
  */
257
263
  declare function judgeDocument(plan: DocumentPlan, prose: string, classify: Classify, options?: JudgeOptions): Promise<Judgements>;
258
264
 
265
+ type ChatMessage = {
266
+ role: "system" | "user" | "assistant";
267
+ content: string;
268
+ };
269
+ /**
270
+ * Anything that answers a conversation with a stream of text: usually
271
+ * `createChat` pointed at an OpenAI-compatible endpoint or at your own proxy.
272
+ */
273
+ type Chat = (messages: ChatMessage[], signal?: AbortSignal) => AsyncIterable<string>;
274
+ /**
275
+ * A chat function for an OpenAI-compatible `chat/completions` endpoint, read as
276
+ * a stream. Point it at a route on your own server when the provider needs a key
277
+ * (see `createChatHandler` in `stunning-md/server`); a key in browser code is public.
278
+ */
279
+ declare function createChat(options: {
280
+ endpoint: string;
281
+ model?: string;
282
+ headers?: Record<string, string>;
283
+ }): Chat;
284
+ /**
285
+ * The address of an OpenAI-compatible chat endpoint, given either the full
286
+ * `…/chat/completions` address or just the API's base (`…/v1`).
287
+ */
288
+ declare function chatCompletionsUrl(url: string): string;
289
+ type BlockKind = "heading" | "paragraph" | "other";
290
+ type ReplyBlock = {
291
+ text: string;
292
+ kind: BlockKind;
293
+ /** Heading level, for headings. */
294
+ depth?: number;
295
+ };
296
+ /**
297
+ * Cuts markdown, as it streams in, into the blocks a reader would recognise:
298
+ * paragraphs, headings, lists, tables, code. A block is released once it is
299
+ * known to be complete — at the blank line after it, or at the end.
300
+ */
301
+ declare class BlockSplitter {
302
+ private pending;
303
+ private lines;
304
+ private fence;
305
+ private math;
306
+ private flush;
307
+ private take;
308
+ /** Feed more text; returns the blocks that are now complete. */
309
+ push(text: string): ReplyBlock[];
310
+ /** The stream is over; returns whatever was still open. */
311
+ end(): ReplyBlock[];
312
+ }
313
+ /**
314
+ * How much of a markdown text that is still being written is ready to be laid
315
+ * out. Given the text so far, returns the part that will not change shape as
316
+ * more arrives, and the heading of the section being written.
317
+ *
318
+ * A section is ready once the next one starts, so its layout is decided once.
319
+ * Text before the first heading, and the opening under a `# Title`, is ready a
320
+ * block at a time. A line, code fence or frontmatter block that is not yet
321
+ * closed is never included.
322
+ */
323
+ declare function settledMarkdown(text: string): {
324
+ markdown: string;
325
+ writing: string | null;
326
+ };
327
+ type TurnEvent =
328
+ /** Something the assistant said about its answer, not part of it. */
329
+ {
330
+ type: "commentary";
331
+ text: string;
332
+ }
333
+ /** The answer so far: every section that is complete, as one markdown document. */
334
+ | {
335
+ type: "content";
336
+ markdown: string;
337
+ }
338
+ /** What is being written now — the heading of the section in progress, if it has one. */
339
+ | {
340
+ type: "writing";
341
+ heading: string | null;
342
+ }
343
+ /**
344
+ * Whether the reply is an answer at all. Sent with `false` when the model
345
+ * opens by saying it does not know, cannot help or needs to ask something —
346
+ * there will be nothing for the page — and with `true` if content follows after all.
347
+ */
348
+ | {
349
+ type: "answer";
350
+ answered: boolean;
351
+ };
352
+ type TurnResult = {
353
+ /** Everything the model said, untouched. */
354
+ raw: string;
355
+ /** The answer only, without commentary. */
356
+ content: string;
357
+ };
358
+ /**
359
+ * Whether a message asks for something for the page — as opposed to a greeting,
360
+ * thanks or small talk, whose reply belongs in the conversation alone. Asked
361
+ * before the reply arrives, so the page need not make room for an answer that
362
+ * is never coming. The classifier answers this reliably; without one, only
363
+ * messages that are plainly small talk are taken as such.
364
+ */
365
+ declare function wantsContent(request: string, classify?: Classify, signal?: AbortSignal): Promise<boolean>;
366
+ /**
367
+ * Reads a model's reply as it streams and sorts it, block by block, into
368
+ * commentary (remarks to the user) and content (the thing that was asked for).
369
+ *
370
+ * Headings, lists, tables, code and images are content. Plain paragraphs are
371
+ * judged by where they sit and how they read: remarks to the user come at the
372
+ * start or the end of a reply and usually announce themselves ("Sure, here
373
+ * is…", "Let me know…"). The classifier is asked about the unclear ones.
374
+ * Without a classifier, the first paragraph of the reply is taken as
375
+ * commentary, and so is the last.
376
+ *
377
+ * Content is released a section at a time, once the section is complete, so a
378
+ * section's layout is decided once and does not shift as it grows. Text that
379
+ * comes before any heading is released paragraph by paragraph.
380
+ */
381
+ declare function sortReply(options: {
382
+ stream: AsyncIterable<string>;
383
+ /** What the user asked — needed to judge whether the reply answers it. */
384
+ request?: string;
385
+ /**
386
+ * The user was only making conversation (see `wantsContent`): the reply is
387
+ * taken as conversation too, unless it turns out to carry content after all.
388
+ */
389
+ smallTalk?: boolean | Promise<boolean>;
390
+ classify?: Classify;
391
+ signal?: AbortSignal;
392
+ onEvent: (event: TurnEvent) => void;
393
+ }): Promise<TurnResult>;
394
+ /** How the assistant is asked to write, so its answer can be laid out as a page. */
395
+ declare const CHAT_INSTRUCTIONS: string;
396
+
259
397
  type StunningMarkdownProps = {
398
+ /** The document to show. May be empty when `chat` is given: the page then starts blank. */
260
399
  markdown: string;
400
+ /**
401
+ * The text is still being written — by a model, say — and `markdown` will
402
+ * keep growing. The page is then laid out as the text arrives: a section at a
403
+ * time, as each is completed, in a theme chosen once from the opening. Pass
404
+ * the text so far on every update, and `false` (or nothing) when it is done.
405
+ */
406
+ streaming?: boolean;
407
+ /**
408
+ * Turns the page into a conversation: a floating input sends requests to this
409
+ * function, and each answer is laid out below as content while the model's
410
+ * remarks about it stay in the sidebar.
411
+ */
412
+ chat?: Chat;
261
413
  /**
262
414
  * Answers the judgement calls structure cannot settle (theme, typography,
263
415
  * ambiguous tables, hero image). Without it everything is decided by rules.
@@ -265,6 +417,13 @@ type StunningMarkdownProps = {
265
417
  classifier?: Classify;
266
418
  /** Fixes parts of the theme — palette, typefaces or corner style — overriding everything else. */
267
419
  theme?: Partial<ThemeChoice>;
420
+ /**
421
+ * Choose a theme to suit each document and each answer. Default `true`. Set
422
+ * it to `false` to stay on one theme throughout — `theme.palette` if given,
423
+ * otherwise `paper` — with nothing asked of the classifier about it. The
424
+ * reader can switch it either way from the theme menu.
425
+ */
426
+ autoTheme?: boolean;
268
427
  /** `auto` follows the reader's system setting. */
269
428
  appearance?: Appearance | "auto";
270
429
  /** Maps URLs in the markdown (e.g. relative image paths) to loadable ones. */
@@ -285,6 +444,11 @@ type StunningMarkdownProps = {
285
444
  * sections have stopped moving. Default 8000.
286
445
  */
287
446
  maxWaitMs?: number;
447
+ /**
448
+ * A control of your own — a single button or link with an icon — shown as a
449
+ * round button to the left of the chat input.
450
+ */
451
+ chatAccessory?: React.ReactNode;
288
452
  className?: string;
289
453
  /** Called whenever the plan or theme changes — useful for debugging and tooling. */
290
454
  onPlan?: (plan: DocumentPlan, theme: ThemeChoice) => void;
@@ -292,7 +456,8 @@ type StunningMarkdownProps = {
292
456
  /**
293
457
  * Renders a markdown string as a designed, responsive page: a hero, sections
294
458
  * laid out by the shape of their content, charts for tabular data, a contents
295
- * menu, and a theme chosen to suit the text.
459
+ * menu, and a theme chosen to suit the text. With `chat`, the page also takes
460
+ * requests, and lays each answer out below as it is written.
296
461
  */
297
462
  declare function StunningMarkdown(props: StunningMarkdownProps): react.JSX.Element;
298
463
 
@@ -310,6 +475,11 @@ type PlanInput = {
310
475
  /** Natural image sizes keyed by the URL written in the markdown. */
311
476
  images?: ImageMetaMap;
312
477
  judgements?: Judgements;
478
+ /**
479
+ * Put in front of every id the plan generates. Needed when several documents
480
+ * share one page, so their section anchors cannot collide.
481
+ */
482
+ idPrefix?: string;
313
483
  };
314
484
  /** Every image URL in the document, for size probing. */
315
485
  declare function collectImageUrls(root: Root): string[];
@@ -353,6 +523,15 @@ type PaletteTokens = {
353
523
  border: string;
354
524
  accent: string;
355
525
  accentFg: string;
526
+ /**
527
+ * A second colour, where the theme has one: quotations are set in it — a band
528
+ * across the page for a quotation that is a section of its own, a block for one
529
+ * inside an article — so the page has more than one note to play.
530
+ */
531
+ highlight?: {
532
+ bg: string;
533
+ fg: string;
534
+ };
356
535
  };
357
536
  /**
358
537
  * Chart drawing styles: `linework` is monochrome print-style ink with hatching,
@@ -360,12 +539,31 @@ type PaletteTokens = {
360
539
  * gradients and depth, and `flat` is plain solid colour.
361
540
  */
362
541
  type ChartStyle = "linework" | "instrument" | "soft" | "flat";
542
+ /**
543
+ * How a text-led hero is set: `wash` tints the page with a soft glow of the
544
+ * accent; `block` is a cover in the accent colour itself; `ink` is a cover in
545
+ * the theme's darkest tone, the page's colours reversed.
546
+ */
547
+ type HeroTone = "wash" | "block" | "ink";
548
+ /** The families the themes fall into by subject — how the picker is arranged. */
549
+ type ThemeTopic = "writing" | "official" | "technology" | "lifestyle" | "wellbeing" | "culture";
550
+ declare const themeTopics: {
551
+ id: ThemeTopic;
552
+ name: string;
553
+ }[];
363
554
  type Theme = {
364
555
  id: PaletteId;
365
556
  name: string;
366
- /** The kinds of document this theme suits. */
557
+ /** The family of subjects it belongs to. */
558
+ topic: ThemeTopic;
559
+ /**
560
+ * The kinds of document this theme suits — and all the classifier reads when
561
+ * choosing one. Name subjects, not moods: measured against documents of known
562
+ * subject, subjects alone choose better, and in half the time, than subjects
563
+ * with colours and typefaces beside them.
564
+ */
367
565
  description: string;
368
- /** Its key colours in a few plain words — read by the classifier alongside the description. */
566
+ /** Its key colours in a few plain words, for people reading the list. */
369
567
  look: string;
370
568
  /** Typography used unless the caller picks another. */
371
569
  fonts: FontPairingId;
@@ -373,6 +571,8 @@ type Theme = {
373
571
  formality: number;
374
572
  /** How charts are drawn under this theme. */
375
573
  chart: ChartStyle;
574
+ /** How the opening of the page is set off from the rest. */
575
+ hero: HeroTone;
376
576
  /** Fallback matching when no classifier is configured. */
377
577
  keywords: RegExp;
378
578
  light: PaletteTokens;
@@ -397,9 +597,8 @@ type FontPairing = {
397
597
  declare const fontPairings: Record<FontPairingId, FontPairing>;
398
598
  declare const fontPairingList: FontPairing[];
399
599
  /**
400
- * What the classifier reads when choosing a theme: the content it suits, its
401
- * key colours and its typeface, so the choice can weigh appearance as well as
402
- * subject. Kept terse on purpose — classifier latency grows with every
600
+ * What the classifier reads when choosing a theme: the subjects it suits, and
601
+ * nothing else. Kept terse on purpose — classifier latency grows with every
403
602
  * character here, across every theme.
404
603
  */
405
604
  declare function describeTheme(theme: Theme): string;
@@ -427,4 +626,4 @@ declare function guessTheme(text: string, stats: {
427
626
  */
428
627
  declare function themeVars(choice: ThemeChoice, appearance: Appearance): CSSProperties;
429
628
 
430
- export { type Appearance, type Block, type ChartSpec, type ClassifierAnswer, type ClassifierAnswers, type ClassifierQuestion, type ClassifierRequest, type Classify, type ColumnType, type DocumentPlan, type FontPairing, type FontPairingId, type Frontmatter, type Hero, type HeroVariant, type ImageMeta, type ImageMetaMap, type ImageRef, type JudgeOptions, type JudgeProgress, type Judgements, type ListVariant, type MediaVariant, type PaletteId, type ParsedMarkdown, type PlanInput, type QuoteVariant, type Section, type SectionLayout, type SectionTone, StunningMarkdown, type StunningMarkdownProps, type TableColumn, type TableModel, type TableRow, type Theme, type ThemeChoice, type TocEntry, type VizKind, type VizPlan, buildTableModel, collectImageUrls, createClassifier, describeTheme, documentDigest, fontPairingList, fontPairings, formatValue, guessTheme, judgeDocument, matchTheme, parseDate, parseMarkdown, parseNumber, planDocument, planViz, probeImages, themeChoice, themeList, themeVars, themes, withVizKind };
629
+ 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, StunningMarkdown, type StunningMarkdownProps, 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, probeImages, settledMarkdown, sortReply, themeChoice, themeList, themeTopics, themeVars, themes, wantsContent, withVizKind };