@ecsplico/editor 3.0.0 → 3.1.1

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 (62) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/adapters/comments/record.d.ts +15 -0
  3. package/dist/adapters/comments/server.d.ts +2 -1
  4. package/dist/adapters/comments/server.js +2 -1
  5. package/dist/adapters/comments/server.js.map +1 -1
  6. package/dist/adapters/nitro.d.ts +4 -0
  7. package/dist/adapters/nitro.js +2 -1
  8. package/dist/adapters/nitro.js.map +1 -1
  9. package/dist/code/CodeSurface.vue.d.ts +2 -2
  10. package/dist/core/command-types.d.ts +7 -0
  11. package/dist/core/commands.d.ts +2 -2
  12. package/dist/core/commands.js.map +1 -1
  13. package/dist/core/feature.d.ts +8 -2
  14. package/dist/core/feature.js.map +1 -1
  15. package/dist/core/index.d.ts +3 -2
  16. package/dist/core/index.js +2 -1
  17. package/dist/core/surface-session.js +2 -0
  18. package/dist/core/surface-session.js.map +1 -1
  19. package/dist/core/transaction-guards.d.ts +21 -0
  20. package/dist/core/transaction-guards.js +49 -0
  21. package/dist/core/transaction-guards.js.map +1 -0
  22. package/dist/document/DocumentSurface.vue.d.ts +2 -2
  23. package/dist/features/ai/protocol/chat.js +2 -1
  24. package/dist/features/ai/protocol/chat.js.map +1 -1
  25. package/dist/features/composites/ai-brief.js +8 -0
  26. package/dist/features/composites/ai-brief.js.map +1 -1
  27. package/dist/features/composites/components/CompositeChrome.js +4 -4
  28. package/dist/features/composites/components/CompositeChrome.js.map +1 -1
  29. package/dist/features/composites/components/CompositeChrome.vue.d.ts +1 -1
  30. package/dist/features/composites/components/CompositeFieldEditor.js +33 -6
  31. package/dist/features/composites/components/CompositeFieldEditor.js.map +1 -1
  32. package/dist/features/composites/components/CompositeInspector.js +3 -2
  33. package/dist/features/composites/components/CompositeInspector.js.map +1 -1
  34. package/dist/features/composites/inspector-model.js +2 -2
  35. package/dist/features/composites/inspector-model.js.map +1 -1
  36. package/dist/features/composites/kinds.d.ts +10 -3
  37. package/dist/features/composites/kinds.js.map +1 -1
  38. package/dist/features/composites/meta-display.js +5 -1
  39. package/dist/features/composites/meta-display.js.map +1 -1
  40. package/dist/features/composites/react/CompositeChrome.js +3 -3
  41. package/dist/features/composites/react/CompositeChrome.js.map +1 -1
  42. package/dist/features/composites/react/CompositeFieldEditor.js +19 -0
  43. package/dist/features/composites/react/CompositeFieldEditor.js.map +1 -1
  44. package/dist/features/composites/react/CompositeInspector.js +1 -1
  45. package/dist/features/composites/react/CompositeInspector.js.map +1 -1
  46. package/dist/features/composites/rows.d.ts +4 -1
  47. package/dist/features/composites/rows.js +9 -1
  48. package/dist/features/composites/rows.js.map +1 -1
  49. package/dist/features/composites/table-model.js +2 -2
  50. package/dist/features/composites/table-model.js.map +1 -1
  51. package/dist/markdown/MarkdownSurface.vue.d.ts +6 -6
  52. package/dist/server/h3.d.ts +8 -0
  53. package/dist/server/handlers/ai.d.ts +2 -1
  54. package/dist/server/handlers/ai.js +2 -1
  55. package/dist/server/handlers/ai.js.map +1 -1
  56. package/dist/server/routes.d.ts +22 -0
  57. package/dist/tree/tree-host.js +1 -1
  58. package/dist/vue/AdvancedEditor.vue.d.ts +4 -4
  59. package/dist/vue/SimpleEditor.vue.d.ts +4 -4
  60. package/dist/vue/components/EditorSurface.vue.d.ts +4 -4
  61. package/dist/vue/kit/types.d.ts +2 -0
  62. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"CompositeInspector.js","names":[],"sources":["../../../../src/features/composites/react/CompositeInspector.tsx"],"sourcesContent":["import type { Editor } from '@tiptap/core'\nimport { type CSSProperties, type ReactNode, useEffect, useId, useState } from 'react'\nimport { normalizeTagName } from '../../../core/tag-name'\nimport { Icon } from '../../../react/Icon'\nimport { Badge } from '../../../react/ui/badge'\nimport { Command, CommandInput, CommandItem, CommandList } from '../../../react/ui/command'\nimport { Input } from '../../../react/ui/input'\nimport { Label } from '../../../react/ui/label'\nimport { Popover, PopoverAnchor, PopoverContent, PopoverTrigger } from '../../../react/ui/popover'\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../../../react/ui/select'\nimport { Textarea } from '../../../react/ui/textarea'\nimport { cn } from '../../../react/ui/utils'\nimport { useEditorI18n } from '../../../react/use-editor-i18n'\nimport type { CompositeMetaField } from '../kinds'\nimport { compositeFieldText, isStructuredField } from '../inspector-model'\nimport { CompositeFieldEditor } from './CompositeFieldEditor'\nimport { useCompositeInspector } from './use-composite-inspector'\n\nconst HEX_COLOR = /^#[0-9a-f]{3,8}$/i\n// Same tint as the chip in the document (`--mention-color`), only invalid colors fall back to the primary color.\nconst mentionStyle = (color: string | null) => ({ '--mention-color': color && HEX_COLOR.test(color) ? color : 'var(--sk-primary)' }) as CSSProperties\n\n/** A label (+ help) above its control; the counterpart of Nuxt UI's `UFormField` (size `sm`). */\nfunction FormField({ label, help, children }: { label: string, help?: string, children: ReactNode }) {\n return (\n <div className=\"flex flex-col gap-1.5\">\n <Label className=\"text-xs font-medium text-foreground\">{label}</Label>\n {children}\n {help && <p className=\"text-xs text-muted-foreground\">{help}</p>}\n </div>\n )\n}\n\n/**\n * A meta field input that writes through on every change like the Vue `UInput`/`UTextarea` bound to the row. The\n * document is re-read asynchronously (once per frame), so a purely controlled input would snap back while typing:\n * it keeps the typed value locally and follows the document whenever the document's value changes.\n */\nfunction MetaFieldInput({ field, value, readOnly, onChange }: {\n field: CompositeMetaField\n value: string\n readOnly: boolean\n onChange: (value: string) => void\n}) {\n const [local, setLocal] = useState(value)\n useEffect(() => setLocal(value), [value])\n const set = (next: string) => {\n setLocal(next)\n onChange(next)\n }\n return field.type === 'textarea'\n ? (\n <Textarea\n value={local}\n aria-label={field.label}\n rows={2}\n className=\"max-h-40 min-h-12 w-full resize-none overflow-y-auto text-sm md:text-sm\"\n readOnly={readOnly}\n onChange={event => set(event.target.value)}\n />\n )\n : (\n <Input\n value={local}\n type={field.type}\n aria-label={field.label}\n className=\"h-7 w-full text-sm md:text-sm\"\n readOnly={readOnly}\n onChange={event => set(event.target.value)}\n />\n )\n}\n\n/**\n * Side panel with the metadata of the document *or* of the Baustein under the caret — one at a time — connected to\n * `editor`. Fills the height of its container and scrolls on its own; the width comes from the host. Open/close is\n * the host's business. The `document` prop carries host-provided document facts (title, folder, dates, presence) at\n * the top of the „Dokument“ section: render them as a `<dl>` with the same grid as below to look native.\n *\n * The „Baustein“ section shows while the caret is in a Baustein of the editor; the „Dokument“ section shows before\n * the editor was ever focused, while the caret is in editor text outside any Baustein, and while `documentFocused`.\n * `documentFocused`: the host's document-level input (its title field) has the focus. While it is true the panel\n * shows the „Dokument“ section and keeps showing it after the input blurs, until the caret returns to the editor.\n */\nexport function CompositeInspector({ editor, documentFocused, document, className }: {\n editor?: Editor\n documentFocused?: boolean\n /** Document facts known to the host only, rendered above the facts derived from the editor. */\n document?: ReactNode\n className?: string\n}) {\n const { t } = useEditorI18n()\n const { state, inspector } = useCompositeInspector(editor, documentFocused)\n const { section, editable, draft, searchTerm: term, tagItems, kindOptions, summarized, hasSummary, rows, tags, kinds, mentions, focused } = state\n const [tagsOpen, setTagsOpen] = useState(false)\n const uid = useId()\n\n return (\n <aside className={cn('flex h-full min-h-0 flex-col overflow-y-auto text-sm', className)} aria-label={t('composites.inspector.label')}>\n {section === 'document'\n ? (\n <section className=\"flex-1 p-4\" aria-labelledby={`${uid}-document`}>\n <h2 id={`${uid}-document`} className=\"mb-3 text-sm font-semibold text-foreground\">\n {t('composites.inspector.document')}\n </h2>\n <div className=\"flex flex-col gap-y-2\">\n {document}\n <dl className=\"grid grid-cols-[6.5rem_minmax(0,1fr)] items-start gap-x-3 gap-y-2\">\n <dt className=\"min-h-6 leading-6 text-muted-foreground\">\n {t('composites.inspector.composites')}\n </dt>\n {kinds.length\n ? (\n <dd className=\"m-0 flex min-w-0 flex-col gap-y-1\">\n {kinds.map(kind => (\n <span key={kind.id} className=\"flex min-h-6 min-w-0 items-center gap-2\">\n <Icon name={kind.icon} className=\"size-4 shrink-0 text-muted-foreground\" />\n <span className=\"min-w-0 flex-1 truncate text-foreground\">{kind.label}</span>\n <span className=\"text-muted-foreground tabular-nums\">{kind.count}</span>\n </span>\n ))}\n <span className=\"min-h-6 leading-6 text-muted-foreground tabular-nums\">\n {t('composites.inspector.summarized', { n: summarized, total: rows.length })}\n </span>\n </dd>\n )\n : (\n <dd className=\"m-0 min-h-6 leading-6 text-muted-foreground/70\">\n {t('composites.inspector.none')}\n </dd>\n )}\n\n <dt className=\"min-h-6 leading-6 text-muted-foreground\">\n {t('composites.tags.label')}\n </dt>\n {tags.length\n ? (\n <dd className=\"m-0 flex min-w-0 flex-wrap items-center gap-1\">\n {tags.map(tag => <Badge key={tag} variant=\"secondary\" className=\"max-w-full\">{`#${tag}`}</Badge>)}\n </dd>\n )\n : (\n <dd className=\"m-0 min-h-6 leading-6 text-muted-foreground/70\">\n {t('composites.inspector.none')}\n </dd>\n )}\n\n <dt className=\"min-h-6 leading-6 text-muted-foreground\">\n {t('composites.inspector.mentions')}\n </dt>\n {mentions.length\n ? (\n <dd className=\"m-0 flex min-w-0 flex-wrap items-center gap-1\">\n {mentions.map(mention => (\n <span\n key={mention.id}\n className=\"inline-flex max-w-full items-center gap-1.5 truncate rounded-sm px-1.5 py-0.5 text-xs font-medium text-foreground\"\n style={{ ...mentionStyle(mention.color), background: 'color-mix(in oklab, var(--mention-color) 15%, transparent)' }}\n >\n <span className=\"size-1.5 shrink-0 rounded-full\" style={{ background: 'var(--mention-color)' }} />\n <span className=\"truncate\">@{mention.label}</span>\n </span>\n ))}\n </dd>\n )\n : (\n <dd className=\"m-0 min-h-6 leading-6 text-muted-foreground/70\">\n {t('composites.inspector.none')}\n </dd>\n )}\n </dl>\n </div>\n </section>\n )\n : focused && (\n <section className=\"flex-1 p-4\" aria-labelledby={`${uid}-composite`}>\n <h2 id={`${uid}-composite`} className=\"mb-3 text-sm font-semibold text-foreground\">\n {t('composites.inspector.composite')}\n </h2>\n\n <div key={focused.key} className=\"flex flex-col gap-3\">\n <FormField label={t('composites.inspector.kind')}>\n <Select value={focused.kind} onValueChange={inspector.setKind} disabled={!editable}>\n <SelectTrigger size=\"sm\" className=\"w-full\" aria-label={t('composites.kind.select')}>\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {kindOptions.map(item => (\n <SelectItem key={item.value} value={item.value}>\n <Icon name={item.icon} />\n {item.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </FormField>\n\n <FormField label={t('composites.inspector.title')} help={editable ? t('composites.inspector.titleHelp') : undefined}>\n <p className={cn('min-h-6 truncate leading-6', focused.title ? 'text-foreground' : 'text-muted-foreground/70')} title={focused.title}>\n {focused.title || t('composites.inspector.untitled')}\n </p>\n </FormField>\n\n <FormField\n label={t('composites.inspector.description')}\n help={focused.descriptionTextOnly ? undefined : t('composites.inspector.descriptionHelp')}\n >\n <Textarea\n value={draft}\n rows={3}\n className=\"max-h-52 min-h-[4.5rem] w-full resize-none overflow-y-auto text-sm md:text-sm\"\n placeholder={t('composites.inspector.description')}\n aria-label={t('composites.inspector.description')}\n readOnly={!editable || !focused.descriptionTextOnly}\n onChange={event => inspector.inputDescription(event.target.value)}\n onBlur={inspector.blurDescription}\n />\n </FormField>\n\n <FormField label={t('composites.tags.label')}>\n <Popover\n open={tagsOpen}\n onOpenChange={(isOpen) => {\n setTagsOpen(isOpen)\n if (isOpen) inspector.searchTags()\n }}\n >\n <PopoverAnchor asChild>\n <div className={cn('flex min-h-7 w-full flex-wrap items-center gap-1 rounded-lg border border-input px-2 py-1 dark:bg-input/30', !editable && 'opacity-50')}>\n <Icon name=\"i-lucide-hash\" className=\"size-3.5 text-muted-foreground\" />\n {focused.tags.map(tag => (\n <Badge key={tag} variant=\"secondary\" className=\"max-w-full gap-0.5 pr-1\">\n <span className=\"truncate\">{tag}</span>\n <button\n type=\"button\"\n className=\"inline-flex cursor-pointer items-center rounded-full outline-none hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring/50 disabled:cursor-not-allowed\"\n aria-label={t('composites.tag.remove', { tag })}\n disabled={!editable}\n onClick={() => inspector.setTags(focused.tags.filter(item => item !== tag))}\n >\n <Icon name=\"i-lucide-x\" className=\"size-3\" />\n </button>\n </Badge>\n ))}\n <PopoverTrigger asChild>\n <button\n type=\"button\"\n role=\"combobox\"\n aria-expanded={tagsOpen}\n aria-label={t('composites.tags.label')}\n disabled={!editable}\n className=\"min-w-12 flex-1 cursor-text self-stretch rounded text-left text-sm text-muted-foreground outline-none focus-visible:ring-2 focus-visible:ring-ring/50 disabled:cursor-not-allowed\"\n >\n {focused.tags.length ? '' : t('composites.tags.label')}\n </button>\n </PopoverTrigger>\n </div>\n </PopoverAnchor>\n <PopoverContent align=\"start\" className=\"w-64 gap-0 p-0\">\n {/* `shouldFilter` off: the list is the metadata source's answer to the search term. */}\n <Command shouldFilter={false}>\n <CommandInput\n value={term}\n onValueChange={inspector.setSearchTerm}\n placeholder={t('composites.tags.label')}\n aria-label={t('composites.tags.label')}\n onKeyDown={(event) => {\n if (event.key === 'Backspace' && !term && focused.tags.length) inspector.setTags(focused.tags.slice(0, -1))\n }}\n />\n <CommandList>\n {tagItems.map((tag) => {\n const selected = focused.tags.includes(tag)\n return (\n <CommandItem\n key={tag}\n value={tag}\n data-checked={selected}\n onSelect={() => inspector.setTags(selected ? focused.tags.filter(item => item !== tag) : [...focused.tags, tag])}\n >\n {tag}\n </CommandItem>\n )\n })}\n {term.trim() && (\n <CommandItem value={`__create__:${term}`} onSelect={() => inspector.createTag(term)}>\n <Icon name=\"i-lucide-plus\" />\n {t('composites.tags.create', { tag: normalizeTagName(term) || term })}\n </CommandItem>\n )}\n </CommandList>\n </Command>\n </PopoverContent>\n </Popover>\n </FormField>\n\n {focused.fields.map(field => (\n <FormField key={field.key} label={field.label}>\n {isStructuredField(field)\n ? (\n // Not text (reference, bibliography, derivation): own editors as in the details card; a text input would overwrite the stored object with a string.\n editor && <CompositeFieldEditor editor={editor} row={focused} field={field} disabled={!editable} />\n )\n : field.type === 'select'\n ? (\n <Select value={compositeFieldText(focused, field)} onValueChange={value => inspector.setField(field, value)} disabled={!editable}>\n <SelectTrigger size=\"sm\" className=\"w-full\" aria-label={field.label}>\n <SelectValue placeholder=\"–\" />\n </SelectTrigger>\n <SelectContent>\n {(field.options ?? []).map(option => <SelectItem key={option.value} value={option.value}>{option.label}</SelectItem>)}\n </SelectContent>\n </Select>\n )\n : (\n <MetaFieldInput\n field={field}\n value={compositeFieldText(focused, field)}\n readOnly={!editable}\n onChange={value => inspector.setField(field, value)}\n />\n )}\n </FormField>\n ))}\n\n <div className=\"flex items-center gap-2 border-t border-border pt-3\">\n <Icon name=\"i-lucide-sparkles\" className=\"size-4 shrink-0 text-muted-foreground\" />\n <span className=\"text-muted-foreground\">{t('composites.inspector.aiStatus')}</span>\n <span className={cn('ml-auto inline-flex items-center gap-1.5', hasSummary ? 'text-foreground' : 'text-muted-foreground/70')}>\n <span className={cn('size-2 rounded-full', hasSummary ? 'bg-(--sk-success)' : 'bg-border')} />\n {hasSummary ? t('composites.inspector.summaryPresent') : t('composites.inspector.summaryMissing')}\n </span>\n </div>\n </div>\n </section>\n )}\n </aside>\n )\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"CompositeInspector.js","names":[],"sources":["../../../../src/features/composites/react/CompositeInspector.tsx"],"sourcesContent":["import type { Editor } from '@tiptap/core'\nimport { type CSSProperties, type ReactNode, useEffect, useId, useState } from 'react'\nimport { normalizeTagName } from '../../../core/tag-name'\nimport { Icon } from '../../../react/Icon'\nimport { Badge } from '../../../react/ui/badge'\nimport { Command, CommandInput, CommandItem, CommandList } from '../../../react/ui/command'\nimport { Input } from '../../../react/ui/input'\nimport { Label } from '../../../react/ui/label'\nimport { Popover, PopoverAnchor, PopoverContent, PopoverTrigger } from '../../../react/ui/popover'\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../../../react/ui/select'\nimport { Textarea } from '../../../react/ui/textarea'\nimport { cn } from '../../../react/ui/utils'\nimport { useEditorI18n } from '../../../react/use-editor-i18n'\nimport type { CompositeMetaField } from '../kinds'\nimport { compositeFieldText, isStructuredField } from '../inspector-model'\nimport { CompositeFieldEditor } from './CompositeFieldEditor'\nimport { useCompositeInspector } from './use-composite-inspector'\n\nconst HEX_COLOR = /^#[0-9a-f]{3,8}$/i\n// Same tint as the chip in the document (`--mention-color`), only invalid colors fall back to the primary color.\nconst mentionStyle = (color: string | null) => ({ '--mention-color': color && HEX_COLOR.test(color) ? color : 'var(--sk-primary)' }) as CSSProperties\n\n/** A label (+ help) above its control; the counterpart of Nuxt UI's `UFormField` (size `sm`). */\nfunction FormField({ label, help, children }: { label: string, help?: string, children: ReactNode }) {\n return (\n <div className=\"flex flex-col gap-1.5\">\n <Label className=\"text-xs font-medium text-foreground\">{label}</Label>\n {children}\n {help && <p className=\"text-xs text-muted-foreground\">{help}</p>}\n </div>\n )\n}\n\n/**\n * A meta field input that writes through on every change like the Vue `UInput`/`UTextarea` bound to the row. The\n * document is re-read asynchronously (once per frame), so a purely controlled input would snap back while typing:\n * it keeps the typed value locally and follows the document whenever the document's value changes.\n */\nfunction MetaFieldInput({ field, value, readOnly, onChange }: {\n field: CompositeMetaField\n value: string\n readOnly: boolean\n onChange: (value: string) => void\n}) {\n const [local, setLocal] = useState(value)\n useEffect(() => setLocal(value), [value])\n const set = (next: string) => {\n setLocal(next)\n onChange(next)\n }\n return field.type === 'textarea'\n ? (\n <Textarea\n value={local}\n aria-label={field.label}\n rows={2}\n className=\"max-h-40 min-h-12 w-full resize-none overflow-y-auto text-sm md:text-sm\"\n readOnly={readOnly}\n onChange={event => set(event.target.value)}\n />\n )\n : (\n <Input\n value={local}\n type={field.type}\n aria-label={field.label}\n className=\"h-7 w-full text-sm md:text-sm\"\n readOnly={readOnly}\n onChange={event => set(event.target.value)}\n />\n )\n}\n\n/**\n * Side panel with the metadata of the document *or* of the Baustein under the caret — one at a time — connected to\n * `editor`. Fills the height of its container and scrolls on its own; the width comes from the host. Open/close is\n * the host's business. The `document` prop carries host-provided document facts (title, folder, dates, presence) at\n * the top of the „Dokument“ section: render them as a `<dl>` with the same grid as below to look native.\n *\n * The „Baustein“ section shows while the caret is in a Baustein of the editor; the „Dokument“ section shows before\n * the editor was ever focused, while the caret is in editor text outside any Baustein, and while `documentFocused`.\n * `documentFocused`: the host's document-level input (its title field) has the focus. While it is true the panel\n * shows the „Dokument“ section and keeps showing it after the input blurs, until the caret returns to the editor.\n */\nexport function CompositeInspector({ editor, documentFocused, document, className }: {\n editor?: Editor\n documentFocused?: boolean\n /** Document facts known to the host only, rendered above the facts derived from the editor. */\n document?: ReactNode\n className?: string\n}) {\n const { t } = useEditorI18n()\n const { state, inspector } = useCompositeInspector(editor, documentFocused)\n const { section, editable, draft, searchTerm: term, tagItems, kindOptions, summarized, hasSummary, rows, tags, kinds, mentions, focused } = state\n const [tagsOpen, setTagsOpen] = useState(false)\n const uid = useId()\n\n return (\n <aside className={cn('flex h-full min-h-0 flex-col overflow-y-auto text-sm', className)} aria-label={t('composites.inspector.label')}>\n {section === 'document'\n ? (\n <section className=\"flex-1 p-4\" aria-labelledby={`${uid}-document`}>\n <h2 id={`${uid}-document`} className=\"mb-3 text-sm font-semibold text-foreground\">\n {t('composites.inspector.document')}\n </h2>\n <div className=\"flex flex-col gap-y-2\">\n {document}\n <dl className=\"grid grid-cols-[6.5rem_minmax(0,1fr)] items-start gap-x-3 gap-y-2\">\n <dt className=\"min-h-6 leading-6 text-muted-foreground\">\n {t('composites.inspector.composites')}\n </dt>\n {kinds.length\n ? (\n <dd className=\"m-0 flex min-w-0 flex-col gap-y-1\">\n {kinds.map(kind => (\n <span key={kind.id} className=\"flex min-h-6 min-w-0 items-center gap-2\">\n <Icon name={kind.icon} className=\"size-4 shrink-0 text-muted-foreground\" />\n <span className=\"min-w-0 flex-1 truncate text-foreground\">{kind.label}</span>\n <span className=\"text-muted-foreground tabular-nums\">{kind.count}</span>\n </span>\n ))}\n <span className=\"min-h-6 leading-6 text-muted-foreground tabular-nums\">\n {t('composites.inspector.summarized', { n: summarized, total: rows.length })}\n </span>\n </dd>\n )\n : (\n <dd className=\"m-0 min-h-6 leading-6 text-muted-foreground/70\">\n {t('composites.inspector.none')}\n </dd>\n )}\n\n <dt className=\"min-h-6 leading-6 text-muted-foreground\">\n {t('composites.tags.label')}\n </dt>\n {tags.length\n ? (\n <dd className=\"m-0 flex min-w-0 flex-wrap items-center gap-1\">\n {tags.map(tag => <Badge key={tag} variant=\"secondary\" className=\"max-w-full\">{`#${tag}`}</Badge>)}\n </dd>\n )\n : (\n <dd className=\"m-0 min-h-6 leading-6 text-muted-foreground/70\">\n {t('composites.inspector.none')}\n </dd>\n )}\n\n <dt className=\"min-h-6 leading-6 text-muted-foreground\">\n {t('composites.inspector.mentions')}\n </dt>\n {mentions.length\n ? (\n <dd className=\"m-0 flex min-w-0 flex-wrap items-center gap-1\">\n {mentions.map(mention => (\n <span\n key={mention.id}\n className=\"inline-flex max-w-full items-center gap-1.5 truncate rounded-sm px-1.5 py-0.5 text-xs font-medium text-foreground\"\n style={{ ...mentionStyle(mention.color), background: 'color-mix(in oklab, var(--mention-color) 15%, transparent)' }}\n >\n <span className=\"size-1.5 shrink-0 rounded-full\" style={{ background: 'var(--mention-color)' }} />\n <span className=\"truncate\">@{mention.label}</span>\n </span>\n ))}\n </dd>\n )\n : (\n <dd className=\"m-0 min-h-6 leading-6 text-muted-foreground/70\">\n {t('composites.inspector.none')}\n </dd>\n )}\n </dl>\n </div>\n </section>\n )\n : focused && (\n <section className=\"flex-1 p-4\" aria-labelledby={`${uid}-composite`}>\n <h2 id={`${uid}-composite`} className=\"mb-3 text-sm font-semibold text-foreground\">\n {t('composites.inspector.composite')}\n </h2>\n\n <div key={focused.key} className=\"flex flex-col gap-3\">\n <FormField label={t('composites.inspector.kind')}>\n <Select value={focused.kind} onValueChange={inspector.setKind} disabled={!editable}>\n <SelectTrigger size=\"sm\" className=\"w-full\" aria-label={t('composites.kind.select')}>\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {kindOptions.map(item => (\n <SelectItem key={item.value} value={item.value}>\n <Icon name={item.icon} />\n {item.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </FormField>\n\n <FormField label={t('composites.inspector.title')} help={editable ? t('composites.inspector.titleHelp') : undefined}>\n <p className={cn('min-h-6 truncate leading-6', focused.title ? 'text-foreground' : 'text-muted-foreground/70')} title={focused.title}>\n {focused.title || t('composites.inspector.untitled')}\n </p>\n </FormField>\n\n <FormField\n label={t('composites.inspector.description')}\n help={focused.descriptionTextOnly ? undefined : t('composites.inspector.descriptionHelp')}\n >\n <Textarea\n value={draft}\n rows={3}\n className=\"max-h-52 min-h-[4.5rem] w-full resize-none overflow-y-auto text-sm md:text-sm\"\n placeholder={t('composites.inspector.description')}\n aria-label={t('composites.inspector.description')}\n readOnly={!editable || !focused.descriptionTextOnly}\n onChange={event => inspector.inputDescription(event.target.value)}\n onBlur={inspector.blurDescription}\n />\n </FormField>\n\n {focused.tagsVisible && <FormField label={t('composites.tags.label')}>\n <Popover\n open={tagsOpen}\n onOpenChange={(isOpen) => {\n setTagsOpen(isOpen)\n if (isOpen) inspector.searchTags()\n }}\n >\n <PopoverAnchor asChild>\n <div className={cn('flex min-h-7 w-full flex-wrap items-center gap-1 rounded-lg border border-input px-2 py-1 dark:bg-input/30', !editable && 'opacity-50')}>\n <Icon name=\"i-lucide-hash\" className=\"size-3.5 text-muted-foreground\" />\n {focused.tags.map(tag => (\n <Badge key={tag} variant=\"secondary\" className=\"max-w-full gap-0.5 pr-1\">\n <span className=\"truncate\">{tag}</span>\n <button\n type=\"button\"\n className=\"inline-flex cursor-pointer items-center rounded-full outline-none hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring/50 disabled:cursor-not-allowed\"\n aria-label={t('composites.tag.remove', { tag })}\n disabled={!editable}\n onClick={() => inspector.setTags(focused.tags.filter(item => item !== tag))}\n >\n <Icon name=\"i-lucide-x\" className=\"size-3\" />\n </button>\n </Badge>\n ))}\n <PopoverTrigger asChild>\n <button\n type=\"button\"\n role=\"combobox\"\n aria-expanded={tagsOpen}\n aria-label={t('composites.tags.label')}\n disabled={!editable}\n className=\"min-w-12 flex-1 cursor-text self-stretch rounded text-left text-sm text-muted-foreground outline-none focus-visible:ring-2 focus-visible:ring-ring/50 disabled:cursor-not-allowed\"\n >\n {focused.tags.length ? '' : t('composites.tags.label')}\n </button>\n </PopoverTrigger>\n </div>\n </PopoverAnchor>\n <PopoverContent align=\"start\" className=\"w-64 gap-0 p-0\">\n {/* `shouldFilter` off: the list is the metadata source's answer to the search term. */}\n <Command shouldFilter={false}>\n <CommandInput\n value={term}\n onValueChange={inspector.setSearchTerm}\n placeholder={t('composites.tags.label')}\n aria-label={t('composites.tags.label')}\n onKeyDown={(event) => {\n if (event.key === 'Backspace' && !term && focused.tags.length) inspector.setTags(focused.tags.slice(0, -1))\n }}\n />\n <CommandList>\n {tagItems.map((tag) => {\n const selected = focused.tags.includes(tag)\n return (\n <CommandItem\n key={tag}\n value={tag}\n data-checked={selected}\n onSelect={() => inspector.setTags(selected ? focused.tags.filter(item => item !== tag) : [...focused.tags, tag])}\n >\n {tag}\n </CommandItem>\n )\n })}\n {term.trim() && (\n <CommandItem value={`__create__:${term}`} onSelect={() => inspector.createTag(term)}>\n <Icon name=\"i-lucide-plus\" />\n {t('composites.tags.create', { tag: normalizeTagName(term) || term })}\n </CommandItem>\n )}\n </CommandList>\n </Command>\n </PopoverContent>\n </Popover>\n </FormField>}\n\n {focused.fields.map(field => (\n <FormField key={field.key} label={field.label}>\n {isStructuredField(field)\n ? (\n // Not text (reference, bibliography, derivation): own editors as in the details card; a text input would overwrite the stored object with a string.\n editor && <CompositeFieldEditor editor={editor} row={focused} field={field} disabled={!editable} />\n )\n : field.type === 'select'\n ? (\n <Select value={compositeFieldText(focused, field)} onValueChange={value => inspector.setField(field, value)} disabled={!editable}>\n <SelectTrigger size=\"sm\" className=\"w-full\" aria-label={field.label}>\n <SelectValue placeholder=\"–\" />\n </SelectTrigger>\n <SelectContent>\n {(field.options ?? []).map(option => <SelectItem key={option.value} value={option.value}>{option.label}</SelectItem>)}\n </SelectContent>\n </Select>\n )\n : (\n <MetaFieldInput\n field={field}\n value={compositeFieldText(focused, field)}\n readOnly={!editable}\n onChange={value => inspector.setField(field, value)}\n />\n )}\n </FormField>\n ))}\n\n <div className=\"flex items-center gap-2 border-t border-border pt-3\">\n <Icon name=\"i-lucide-sparkles\" className=\"size-4 shrink-0 text-muted-foreground\" />\n <span className=\"text-muted-foreground\">{t('composites.inspector.aiStatus')}</span>\n <span className={cn('ml-auto inline-flex items-center gap-1.5', hasSummary ? 'text-foreground' : 'text-muted-foreground/70')}>\n <span className={cn('size-2 rounded-full', hasSummary ? 'bg-(--sk-success)' : 'bg-border')} />\n {hasSummary ? t('composites.inspector.summaryPresent') : t('composites.inspector.summaryMissing')}\n </span>\n </div>\n </div>\n </section>\n )}\n </aside>\n )\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AAkBA,MAAM,YAAY;AAElB,MAAM,gBAAgB,WAA0B,EAAE,mBAAmB,SAAS,UAAU,KAAK,KAAK,IAAI,QAAQ,oBAAoB;;AAGlI,SAAS,UAAU,EAAE,OAAO,MAAM,YAAmE;CACnG,OACE,qBAAC,OAAD;EAAK,WAAU;EAAf,UAAA;GACE,oBAAC,OAAD;IAAO,WAAU;IAAuC,UAAA;GAAa,CAAA;GACpE;GACA,QAAQ,oBAAC,KAAD;IAAG,WAAU;IAAiC,UAAA;GAAQ,CAAA;EAC5D;;AAET;;;;;;AAOA,SAAS,eAAe,EAAE,OAAO,OAAO,UAAU,YAK/C;CACD,MAAM,CAAC,OAAO,YAAY,SAAS,KAAK;CACxC,gBAAgB,SAAS,KAAK,GAAG,CAAC,KAAK,CAAC;CACxC,MAAM,OAAO,SAAiB;EAC5B,SAAS,IAAI;EACb,SAAS,IAAI;CACf;CACA,OAAO,MAAM,SAAS,aAEhB,oBAAC,UAAD;EACE,OAAO;EACP,cAAY,MAAM;EAClB,MAAM;EACN,WAAU;EACA;EACV,WAAU,UAAS,IAAI,MAAM,OAAO,KAAK;CAC1C,CAAA,IAGD,oBAAC,OAAD;EACE,OAAO;EACP,MAAM,MAAM;EACZ,cAAY,MAAM;EAClB,WAAU;EACA;EACV,WAAU,UAAS,IAAI,MAAM,OAAO,KAAK;CAC1C,CAAA;AAET;;;;;;;;;;;;AAaA,SAAgB,mBAAmB,EAAE,QAAQ,iBAAiB,UAAU,aAMrE;CACD,MAAM,EAAE,MAAM,cAAc;CAC5B,MAAM,EAAE,OAAO,cAAc,sBAAsB,QAAQ,eAAe;CAC1E,MAAM,EAAE,SAAS,UAAU,OAAO,YAAY,MAAM,UAAU,aAAa,YAAY,YAAY,MAAM,MAAM,OAAO,UAAU,YAAY;CAC5I,MAAM,CAAC,UAAU,eAAe,SAAS,KAAK;CAC9C,MAAM,MAAM,MAAM;CAElB,OACE,oBAAC,SAAD;EAAO,WAAW,GAAG,wDAAwD,SAAS;EAAG,cAAY,EAAE,4BAA4B;EAChI,UAAA,YAAY,aAEP,qBAAC,WAAD;GAAS,WAAU;GAAa,mBAAiB,GAAG,IAAI;GAAxD,UAAA,CACE,oBAAC,MAAD;IAAI,IAAI,GAAG,IAAI;IAAY,WAAU;IAClC,UAAA,EAAE,+BAA+B;GAChC,CAAA,GACJ,qBAAC,OAAD;IAAK,WAAU;IAAf,UAAA,CACG,UACD,qBAAC,MAAD;KAAI,WAAU;KAAd,UAAA;MACE,oBAAC,MAAD;OAAI,WAAU;OACX,UAAA,EAAE,iCAAiC;MAClC,CAAA;MACH,MAAM,SAED,qBAAC,MAAD;OAAI,WAAU;OAAd,UAAA,CACG,MAAM,KAAI,SACT,qBAAC,QAAD;QAAoB,WAAU;QAA9B,UAAA;SACE,oBAAC,MAAD;UAAM,MAAM,KAAK;UAAM,WAAU;SAAyC,CAAA;SAC1E,oBAAC,QAAD;UAAM,WAAU;UAA2C,UAAA,KAAK;SAAY,CAAA;SAC5E,oBAAC,QAAD;UAAM,WAAU;UAAsC,UAAA,KAAK;SAAY,CAAA;QACnE;OAJK,GAAA,KAAK,EAIV,CACP,GACD,oBAAC,QAAD;QAAM,WAAU;QACb,UAAA,EAAE,mCAAmC;SAAE,GAAG;SAAY,OAAO,KAAK;QAAO,CAAC;OACvE,CAAA,CACJ;MAGJ,CAAA,IAAA,oBAAC,MAAD;OAAI,WAAU;OACX,UAAA,EAAE,2BAA2B;MAC5B,CAAA;MAGV,oBAAC,MAAD;OAAI,WAAU;OACX,UAAA,EAAE,uBAAuB;MACxB,CAAA;MACH,KAAK,SAEA,oBAAC,MAAD;OAAI,WAAU;OACX,UAAA,KAAK,KAAI,QAAO,oBAAC,OAAD;QAAiB,SAAQ;QAAY,WAAU;QAAc,UAAA,IAAI;OAAa,GAAlE,GAAkE,CAAC;MAC9F,CAAA,IAGJ,oBAAC,MAAD;OAAI,WAAU;OACX,UAAA,EAAE,2BAA2B;MAC5B,CAAA;MAGV,oBAAC,MAAD;OAAI,WAAU;OACX,UAAA,EAAE,+BAA+B;MAChC,CAAA;MACH,SAAS,SAEJ,oBAAC,MAAD;OAAI,WAAU;OACX,UAAA,SAAS,KAAI,YACZ,qBAAC,QAAD;QAEE,WAAU;QACV,OAAO;SAAE,GAAG,aAAa,QAAQ,KAAK;SAAG,YAAY;QAA6D;QAHpH,UAAA,CAKE,oBAAC,QAAD;SAAM,WAAU;SAAiC,OAAO,EAAE,YAAY,uBAAuB;QAAI,CAAA,GACjG,qBAAC,QAAD;SAAM,WAAU;SAAhB,UAAA,CAA2B,KAAE,QAAQ,KAAY;QAC7C,CAAA,CAAA;OANC,GAAA,QAAQ,EAMT,CACP;MACC,CAAA,IAGJ,oBAAC,MAAD;OAAI,WAAU;OACX,UAAA,EAAE,2BAA2B;MAC5B,CAAA;KAER;IACD,CAAA,CAAA;GACE,CAAA,CAAA;EAEX,CAAA,IAAA,WACA,qBAAC,WAAD;GAAS,WAAU;GAAa,mBAAiB,GAAG,IAAI;GAAxD,UAAA,CACE,oBAAC,MAAD;IAAI,IAAI,GAAG,IAAI;IAAa,WAAU;IACnC,UAAA,EAAE,gCAAgC;GACjC,CAAA,GAEJ,qBAAC,OAAD;IAAuB,WAAU;IAAjC,UAAA;KACE,oBAAC,WAAD;MAAW,OAAO,EAAE,2BAA2B;MAC7C,UAAA,qBAAC,QAAD;OAAQ,OAAO,QAAQ;OAAM,eAAe,UAAU;OAAS,UAAU,CAAC;OAA1E,UAAA,CACE,oBAAC,eAAD;QAAe,MAAK;QAAK,WAAU;QAAS,cAAY,EAAE,wBAAwB;QAChF,UAAA,oBAAC,aAAD,CAAc,CAAA;OACD,CAAA,GACf,oBAAC,eAAD,EAAA,UACG,YAAY,KAAI,SACf,qBAAC,YAAD;QAA6B,OAAO,KAAK;QAAzC,UAAA,CACE,oBAAC,MAAD,EAAM,MAAM,KAAK,KAAO,CAAA,GACvB,KAAK,KACI;OAHK,GAAA,KAAK,KAGV,CACb,EACY,CAAA,CACT;;KACC,CAAA;KAEX,oBAAC,WAAD;MAAW,OAAO,EAAE,4BAA4B;MAAG,MAAM,WAAW,EAAE,gCAAgC,IAAI,KAAA;MACxG,UAAA,oBAAC,KAAD;OAAG,WAAW,GAAG,8BAA8B,QAAQ,QAAQ,oBAAoB,0BAA0B;OAAG,OAAO,QAAQ;OAC5H,UAAA,QAAQ,SAAS,EAAE,+BAA+B;MAClD,CAAA;KACM,CAAA;KAEX,oBAAC,WAAD;MACE,OAAO,EAAE,kCAAkC;MAC3C,MAAM,QAAQ,sBAAsB,KAAA,IAAY,EAAE,sCAAsC;MAExF,UAAA,oBAAC,UAAD;OACE,OAAO;OACP,MAAM;OACN,WAAU;OACV,aAAa,EAAE,kCAAkC;OACjD,cAAY,EAAE,kCAAkC;OAChD,UAAU,CAAC,YAAY,CAAC,QAAQ;OAChC,WAAU,UAAS,UAAU,iBAAiB,MAAM,OAAO,KAAK;OAChE,QAAQ,UAAU;MACnB,CAAA;KACQ,CAAA;KAEV,QAAQ,eAAe,oBAAC,WAAD;MAAW,OAAO,EAAE,uBAAuB;MACjE,UAAA,qBAAC,SAAD;OACE,MAAM;OACN,eAAe,WAAW;QACxB,YAAY,MAAM;QAClB,IAAI,QAAQ,UAAU,WAAW;OACnC;OALF,UAAA,CAOE,oBAAC,eAAD;QAAe,SAAA;QACb,UAAA,qBAAC,OAAD;SAAK,WAAW,GAAG,8GAA8G,CAAC,YAAY,YAAY;SAA1J,UAAA;UACE,oBAAC,MAAD;WAAM,MAAK;WAAgB,WAAU;UAAkC,CAAA;UACtE,QAAQ,KAAK,KAAI,QAChB,qBAAC,OAAD;WAAiB,SAAQ;WAAY,WAAU;WAA/C,UAAA,CACE,oBAAC,QAAD;YAAM,WAAU;YAAY,UAAA;WAAU,CAAA,GACtC,oBAAC,UAAD;YACE,MAAK;YACL,WAAU;YACV,cAAY,EAAE,yBAAyB,EAAE,IAAI,CAAC;YAC9C,UAAU,CAAC;YACX,eAAe,UAAU,QAAQ,QAAQ,KAAK,QAAO,SAAQ,SAAS,GAAG,CAAC;YAE1E,UAAA,oBAAC,MAAD;aAAM,MAAK;aAAa,WAAU;YAAU,CAAA;WACtC,CAAA,CACH;UAXK,GAAA,GAWL,CACR;UACD,oBAAC,gBAAD;WAAgB,SAAA;WACd,UAAA,oBAAC,UAAD;YACE,MAAK;YACL,MAAK;YACL,iBAAe;YACf,cAAY,EAAE,uBAAuB;YACrC,UAAU,CAAC;YACX,WAAU;YAET,UAAA,QAAQ,KAAK,SAAS,KAAK,EAAE,uBAAuB;WAC/C,CAAA;UACM,CAAA;SACb;;OACQ,CAAA,GACf,oBAAC,gBAAD;QAAgB,OAAM;QAAQ,WAAU;QAEtC,UAAA,qBAAC,SAAD;SAAS,cAAc;SAAvB,UAAA,CACE,oBAAC,cAAD;UACE,OAAO;UACP,eAAe,UAAU;UACzB,aAAa,EAAE,uBAAuB;UACtC,cAAY,EAAE,uBAAuB;UACrC,YAAY,UAAU;WACpB,IAAI,MAAM,QAAQ,eAAe,CAAC,QAAQ,QAAQ,KAAK,QAAQ,UAAU,QAAQ,QAAQ,KAAK,MAAM,GAAG,EAAE,CAAC;UAC5G;SACD,CAAA,GACD,qBAAC,aAAD,EAAA,UAAA,CACG,SAAS,KAAK,QAAQ;UACrB,MAAM,WAAW,QAAQ,KAAK,SAAS,GAAG;UAC1C,OACE,oBAAC,aAAD;WAEE,OAAO;WACP,gBAAc;WACd,gBAAgB,UAAU,QAAQ,WAAW,QAAQ,KAAK,QAAO,SAAQ,SAAS,GAAG,IAAI,CAAC,GAAG,QAAQ,MAAM,GAAG,CAAC;WAE9G,UAAA;UACU,GANN,GAMM;SAEjB,CAAC,GACA,KAAK,KAAK,KACT,qBAAC,aAAD;UAAa,OAAO,cAAc;UAAQ,gBAAgB,UAAU,UAAU,IAAI;UAAlF,UAAA,CACE,oBAAC,MAAD,EAAM,MAAK,gBAAiB,CAAA,GAC3B,EAAE,0BAA0B,EAAE,KAAK,iBAAiB,IAAI,KAAK,KAAK,CAAC,CACzD;SAEJ,CAAA,CAAA,EAAA,CAAA,CACN;;OACK,CAAA,CACT;;KACA,CAAA;KAEV,QAAQ,OAAO,KAAI,UAClB,oBAAC,WAAD;MAA2B,OAAO,MAAM;MACrC,UAAA,kBAAkB,KAAK,IAGlB,UAAU,oBAAC,sBAAD;OAA8B;OAAQ,KAAK;OAAgB;OAAO,UAAU,CAAC;MAAW,CAAA,IAEpG,MAAM,SAAS,WAEb,qBAAC,QAAD;OAAQ,OAAO,mBAAmB,SAAS,KAAK;OAAG,gBAAe,UAAS,UAAU,SAAS,OAAO,KAAK;OAAG,UAAU,CAAC;OAAxH,UAAA,CACE,oBAAC,eAAD;QAAe,MAAK;QAAK,WAAU;QAAS,cAAY,MAAM;QAC5D,UAAA,oBAAC,aAAD,EAAa,aAAY,IAAK,CAAA;OACjB,CAAA,GACf,oBAAC,eAAD,EAAA,WACI,MAAM,WAAW,CAAC,EAAA,CAAG,KAAI,WAAU,oBAAC,YAAD;QAA+B,OAAO,OAAO;QAAQ,UAAA,OAAO;OAAkB,GAA7D,OAAO,KAAsD,CAAC,EACvG,CAAA,CACT;MAGR,CAAA,IAAA,oBAAC,gBAAD;OACS;OACP,OAAO,mBAAmB,SAAS,KAAK;OACxC,UAAU,CAAC;OACX,WAAU,UAAS,UAAU,SAAS,OAAO,KAAK;MACnD,CAAA;KAEE,GAzBK,MAAM,GAyBX,CACZ;KAED,qBAAC,OAAD;MAAK,WAAU;MAAf,UAAA;OACE,oBAAC,MAAD;QAAM,MAAK;QAAoB,WAAU;OAAyC,CAAA;OAClF,oBAAC,QAAD;QAAM,WAAU;QAAyB,UAAA,EAAE,+BAA+B;OAAQ,CAAA;OAClF,qBAAC,QAAD;QAAM,WAAW,GAAG,4CAA4C,aAAa,oBAAoB,0BAA0B;QAA3H,UAAA,CACE,oBAAC,QAAD,EAAM,WAAW,GAAG,uBAAuB,aAAa,sBAAsB,WAAW,EAAI,CAAA,GAC5F,aAAa,EAAE,qCAAqC,IAAI,EAAE,qCAAqC,CAC5F;;MACH;;IACF;GAzJK,GAAA,QAAQ,GAyJb,CACE;;CAER,CAAA;AAEX"}
@@ -27,6 +27,8 @@ export interface CompositeRow {
27
27
  meta: Record<string, unknown>;
28
28
  /** The meta fields of the composite's kind. */
29
29
  fields: readonly CompositeMetaField[];
30
+ /** `false` when the composite's kind hides its tag row (`CompositeKind.chrome.tags === false`): chips, the add-tag control and the tags field stay away. */
31
+ tagsVisible: boolean;
30
32
  /** Nesting level among composites, 0 = outermost. */
31
33
  depth: number;
32
34
  }
@@ -75,7 +77,8 @@ export declare function compositeOfRow(doc: Node$1, row: Pick<CompositeRow, 'id'
75
77
  } | null;
76
78
  /**
77
79
  * `meta` with `value` stored under `field.key`: a number field converts a numeric string, an empty value (`''`,
78
- * `null`, `undefined`, `NaN`) removes the key. Returns a new object; `meta` is not changed.
80
+ * `null`, `undefined`, `NaN`) removes the key. A `multiselect` field keeps the string entries of an array value and
81
+ * removes the key when none is left. Returns a new object; `meta` is not changed.
79
82
  */
80
83
  export declare function compositeMetaWith(meta: Record<string, unknown>, field: Pick<CompositeMetaField, 'key' | 'type'>, value: unknown): Record<string, unknown>;
81
84
  /** Stores `value` under the meta field `key` of the row's composite (see `compositeMetaWith`), in one transaction. */
@@ -42,6 +42,7 @@ function compositeRows(doc, kinds) {
42
42
  tags: Array.isArray(tags) ? tags.filter((tag) => typeof tag === "string") : [],
43
43
  meta: node.attrs.meta ?? {},
44
44
  fields: kind.fields,
45
+ tagsVisible: kind.chrome?.tags !== false,
45
46
  depth
46
47
  };
47
48
  rows.push(row);
@@ -135,10 +136,17 @@ function compositeOfRow(doc, row) {
135
136
  }
136
137
  /**
137
138
  * `meta` with `value` stored under `field.key`: a number field converts a numeric string, an empty value (`''`,
138
- * `null`, `undefined`, `NaN`) removes the key. Returns a new object; `meta` is not changed.
139
+ * `null`, `undefined`, `NaN`) removes the key. A `multiselect` field keeps the string entries of an array value and
140
+ * removes the key when none is left. Returns a new object; `meta` is not changed.
139
141
  */
140
142
  function compositeMetaWith(meta, field, value) {
141
143
  const next = { ...meta };
144
+ if (field.type === "multiselect") {
145
+ const chosen = Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
146
+ if (chosen.length) next[field.key] = chosen;
147
+ else delete next[field.key];
148
+ return next;
149
+ }
142
150
  const stored = field.type === "number" && typeof value === "string" && value !== "" ? Number(value) : value;
143
151
  if (stored === "" || stored === null || stored === void 0 || Number.isNaN(stored)) delete next[field.key];
144
152
  else next[field.key] = stored;
@@ -1 +1 @@
1
- {"version":3,"file":"rows.js","names":[],"sources":["../../../src/features/composites/rows.ts"],"sourcesContent":["import type { Editor } from '@tiptap/core'\nimport type { Node as PMNode } from '@tiptap/pm/model'\nimport { blockIdOf, findBlockById } from '../../core/schema'\nimport { type CompositeKind, type CompositeMetaField, compositeKind } from './kinds'\nimport { referenceLabel, referenceTarget } from './reference-label'\n\n/** One composite („Baustein“) of a document with its metadata, flattened in document order. */\nexport interface CompositeRow {\n /** Block ID, or `pos:<pos>` for a composite without one yet; unique within the document. */\n key: string\n id: string | null\n /** Position of the composite when the row was read; the write helpers re-resolve it, so a stale value is safe. */\n pos: number\n /** Position right after the composite, as of when the row was read. */\n end: number\n kind: string\n kindLabel: string\n icon: string\n /** Title text, empty for an untitled composite. */\n title: string\n /** Text of the description (summary), empty when there is none. */\n description: string\n /**\n * The description holds text only (marks such as bold, author or comment allowed, no mentions or tags); only then\n * can `setCompositeDescription` write it without losing inline content.\n */\n descriptionTextOnly: boolean\n tags: string[]\n meta: Record<string, unknown>\n /** The meta fields of the composite's kind. */\n fields: readonly CompositeMetaField[]\n /** Nesting level among composites, 0 = outermost. */\n depth: number\n}\n\n/** A distinct `@mention` chip of a document. */\nexport interface CompositeMention {\n id: string\n label: string\n color: string | null\n}\n\n/** A reference to a Baustein: a `reference` chip in the text of block `from`, or a `reference` field of the Baustein `from`. */\nexport interface CompositeReference {\n /** Block ID of the referenced Baustein. */\n target: string\n /** Block ID of the nearest block around the chip (the Baustein itself for a field); `null` when that block has no ID yet. */\n from: string | null\n /** The chip's locator (`S. 12`, `03:14`); `null` for a field or a chip without one. */\n locator: string | null\n}\n\nconst COMPOSITE = 'composite'\n\n/** True when `node`'s content is empty or text only (with any marks, but no inline nodes such as mentions or tags). */\nfunction isTextOnly(node: PMNode): boolean {\n let textOnly = true\n node.forEach((child) => {\n if (!child.isText) textOnly = false\n })\n return textOnly\n}\n\n/**\n * Reads every composite of `doc` (also inside quotes and lists) as a row, in document order, the distinct\n * `@mention` chips anywhere in the document and every reference (`reference` chips and `reference` fields) in document\n * order. Unknown kinds get the `section` kind's presentation under their own ID.\n */\nexport function compositeRows(doc: PMNode, kinds: readonly CompositeKind[]): { rows: CompositeRow[], mentions: CompositeMention[], references: CompositeReference[] } {\n const rows: CompositeRow[] = []\n const mentions = new Map<string, CompositeMention>()\n const references: CompositeReference[] = []\n\n function walk(parent: PMNode, start: number, depth: number, block: string | null) {\n parent.forEach((node, offset) => {\n const pos = start + offset\n if (node.type.name === COMPOSITE) {\n const kind = compositeKind(kinds, String(node.attrs.kind))\n const id = blockIdOf(node)\n const tags: unknown = node.maybeChild(2)?.attrs.tags\n const description = node.maybeChild(1)\n const row: CompositeRow = {\n key: id ?? `pos:${pos}`,\n id,\n pos,\n end: pos + node.nodeSize,\n kind: kind.id,\n kindLabel: kind.label,\n icon: kind.icon,\n title: node.firstChild?.textContent.trim() ?? '',\n description: description?.textContent ?? '',\n descriptionTextOnly: description ? isTextOnly(description) : true,\n tags: Array.isArray(tags) ? tags.filter((tag): tag is string => typeof tag === 'string') : [],\n meta: (node.attrs.meta ?? {}) as Record<string, unknown>,\n fields: kind.fields,\n depth,\n }\n rows.push(row)\n for (const field of kind.fields) {\n const value = field.type === 'reference' ? row.meta[field.key] : undefined\n if (typeof value === 'string' && value) references.push({ target: value, from: id, locator: null })\n }\n walk(node, pos + 1, depth + 1, id ?? block)\n }\n else if (node.type.name === 'reference') {\n const target = String(node.attrs.target ?? '')\n if (target) references.push({ target, from: block, locator: typeof node.attrs.locator === 'string' && node.attrs.locator ? node.attrs.locator : null })\n }\n else if (node.type.name === 'mention') {\n const mentionId = String(node.attrs.id ?? '')\n if (mentionId && !mentions.has(mentionId)) {\n mentions.set(mentionId, {\n id: mentionId,\n label: String(node.attrs.label ?? mentionId),\n color: typeof node.attrs.color === 'string' ? node.attrs.color : null,\n })\n }\n }\n else if (!node.isLeaf) {\n walk(node, pos + 1, depth, blockIdOf(node) ?? block)\n }\n })\n }\n walk(doc, 0, 0, null)\n return { rows, mentions: [...mentions.values()], references }\n}\n\n/** A place that refers to a Baustein: the block holding the chip (or the Baustein with the field) and a short text of it. */\nexport interface CompositeBacklink {\n from: string\n locator: string | null\n excerpt: string\n}\n\nconst EXCERPT_LENGTH = 40\n\n/** The text of `node` as read on screen: inline atoms count as their label (`@name`, `#tag`, a reference chip's short citation + locator), not as nothing. */\nfunction visibleText(doc: PMNode, kinds: readonly CompositeKind[], node: PMNode): string {\n return node.textBetween(0, node.content.size, ' ', (leaf) => {\n const attrs = leaf.attrs\n switch (leaf.type.name) {\n case 'mention': return `@${String(attrs.label ?? attrs.id ?? '')}`\n case 'tag': return `#${String(attrs.name ?? '')}`\n case 'reference': {\n const target = findBlockById(doc, String(attrs.target ?? ''))\n const label = target?.node.type.name === COMPOSITE ? referenceLabel(referenceTarget(target.node, kinds)) : String(attrs.label ?? '')\n return typeof attrs.locator === 'string' && attrs.locator ? `${label}, ${attrs.locator}` : label\n }\n default: return ''\n }\n })\n}\n\n/** Every reference to the Baustein `target` in document order; references whose enclosing block has no ID (or is gone) are left out, as nothing could reveal them. */\nexport function compositeBacklinks(doc: PMNode, kinds: readonly CompositeKind[], target: string): CompositeBacklink[] {\n const links: CompositeBacklink[] = []\n for (const reference of compositeRows(doc, kinds).references) {\n if (reference.target !== target || !reference.from) continue\n const found = findBlockById(doc, reference.from)\n if (!found) continue\n const source = found.node.type.name === COMPOSITE ? found.node.firstChild : found.node\n const text = source ? visibleText(doc, kinds, source).trim() : ''\n links.push({ from: reference.from, locator: reference.locator, excerpt: text.length > EXCERPT_LENGTH ? `${text.slice(0, EXCERPT_LENGTH)}…` : text })\n }\n return links\n}\n\n/** The innermost row around document position `pos`, `null` outside of any composite. */\nexport function compositeRowAt(rows: readonly CompositeRow[], pos: number): CompositeRow | null {\n let found: CompositeRow | null = null\n // Pre-order: the last match is the innermost composite.\n for (const row of rows) {\n if (row.pos <= pos && pos < row.end) found = row\n }\n return found\n}\n\n/**\n * The composite `row` stands for in `doc`: by block ID, else (the row has no ID yet) by its position.\n * Positions are re-resolved on every call, so a stale `row.pos` is safe for rows with an ID.\n */\nexport function compositeOfRow(doc: PMNode, row: Pick<CompositeRow, 'id' | 'pos'>): { pos: number, node: PMNode } | null {\n if (row.id) {\n const found = findBlockById(doc, row.id)\n return found?.node.type.name === COMPOSITE ? found : null\n }\n const node = doc.nodeAt(row.pos)\n return node?.type.name === COMPOSITE ? { pos: row.pos, node } : null\n}\n\n/**\n * `meta` with `value` stored under `field.key`: a number field converts a numeric string, an empty value (`''`,\n * `null`, `undefined`, `NaN`) removes the key. Returns a new object; `meta` is not changed.\n */\nexport function compositeMetaWith(meta: Record<string, unknown>, field: Pick<CompositeMetaField, 'key' | 'type'>, value: unknown): Record<string, unknown> {\n const next = { ...meta }\n const stored = field.type === 'number' && typeof value === 'string' && value !== '' ? Number(value) : value\n if (stored === '' || stored === null || stored === undefined || Number.isNaN(stored)) delete next[field.key]\n else next[field.key] = stored\n return next\n}\n\n/** Stores `value` under the meta field `key` of the row's composite (see `compositeMetaWith`), in one transaction. */\nexport function setCompositeMetaField(editor: Editor, row: Pick<CompositeRow, 'id' | 'pos' | 'fields'>, key: string, value: unknown): boolean {\n const field = row.fields.find(candidate => candidate.key === key) ?? { key, type: 'text' as const }\n return editor.commands.command(({ tr }) => {\n const found = compositeOfRow(tr.doc, row)\n if (!found) return false\n tr.setNodeMarkup(found.pos, undefined, { ...found.node.attrs, meta: compositeMetaWith(found.node.attrs.meta ?? {}, field, value) })\n return true\n })\n}\n\n/** Replaces the tags row of the row's composite (empty names are dropped), in one transaction. */\nexport function setCompositeTags(editor: Editor, row: Pick<CompositeRow, 'id' | 'pos'>, tags: readonly string[]): boolean {\n return editor.commands.command(({ tr }) => {\n const found = compositeOfRow(tr.doc, row)\n const title = found?.node.maybeChild(0)\n const description = found?.node.maybeChild(1)\n const tagsNode = found?.node.maybeChild(2)\n if (!found || !title || !description || tagsNode?.type.name !== 'compositeTags') return false\n const tagsPos = found.pos + 1 + title.nodeSize + description.nodeSize\n tr.setNodeMarkup(tagsPos, undefined, { ...tagsNode.attrs, tags: tags.filter(tag => tag) })\n return true\n })\n}\n\n/** The text `setCompositeDescription` stores for `text`: the description is one line, so line breaks collapse to spaces. */\nexport function compositeDescriptionText(text: string): string {\n return text.replace(/\\s*\\n\\s*/g, ' ').trim()\n}\n\n/**\n * Sets the text of the row's composite description to `compositeDescriptionText(text)`; an empty text empties it.\n * Only the changed span is replaced, so marks on the unchanged text (bold, author, comment) stay. Refuses (`false`)\n * when the description holds a mention or tag (it would be lost), and, when `expected` is given, when the\n * description no longer holds that text (a peer changed it since the caller read it; writing would drop that change).\n */\nexport function setCompositeDescription(editor: Editor, row: Pick<CompositeRow, 'id' | 'pos'>, text: string, expected?: string): boolean {\n const value = compositeDescriptionText(text)\n return editor.commands.command(({ tr }) => {\n const found = compositeOfRow(tr.doc, row)\n const title = found?.node.maybeChild(0)\n const description = found?.node.maybeChild(1)\n if (!found || !title || !description || description.type.name !== 'compositeDescription') return false\n if (!isTextOnly(description)) return false\n const current = description.textContent\n if (expected !== undefined && current !== expected) return false\n if (current === value) return true\n // Text only: every character is one position. Keep the common prefix and suffix, replace what lies between.\n let prefix = 0\n const max = Math.min(current.length, value.length)\n while (prefix < max && current[prefix] === value[prefix]) prefix++\n let suffix = 0\n while (suffix < max - prefix && current[current.length - 1 - suffix] === value[value.length - 1 - suffix]) suffix++\n const start = found.pos + 1 + title.nodeSize + 1\n // `insertText` with an empty text deletes the range; inserted text takes the marks at its start, as typing does.\n tr.insertText(value.slice(prefix, value.length - suffix), start + prefix, start + current.length - suffix)\n return true\n })\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"rows.js","names":[],"sources":["../../../src/features/composites/rows.ts"],"sourcesContent":["import type { Editor } from '@tiptap/core'\nimport type { Node as PMNode } from '@tiptap/pm/model'\nimport { blockIdOf, findBlockById } from '../../core/schema'\nimport { type CompositeKind, type CompositeMetaField, compositeKind } from './kinds'\nimport { referenceLabel, referenceTarget } from './reference-label'\n\n/** One composite („Baustein“) of a document with its metadata, flattened in document order. */\nexport interface CompositeRow {\n /** Block ID, or `pos:<pos>` for a composite without one yet; unique within the document. */\n key: string\n id: string | null\n /** Position of the composite when the row was read; the write helpers re-resolve it, so a stale value is safe. */\n pos: number\n /** Position right after the composite, as of when the row was read. */\n end: number\n kind: string\n kindLabel: string\n icon: string\n /** Title text, empty for an untitled composite. */\n title: string\n /** Text of the description (summary), empty when there is none. */\n description: string\n /**\n * The description holds text only (marks such as bold, author or comment allowed, no mentions or tags); only then\n * can `setCompositeDescription` write it without losing inline content.\n */\n descriptionTextOnly: boolean\n tags: string[]\n meta: Record<string, unknown>\n /** The meta fields of the composite's kind. */\n fields: readonly CompositeMetaField[]\n /** `false` when the composite's kind hides its tag row (`CompositeKind.chrome.tags === false`): chips, the add-tag control and the tags field stay away. */\n tagsVisible: boolean\n /** Nesting level among composites, 0 = outermost. */\n depth: number\n}\n\n/** A distinct `@mention` chip of a document. */\nexport interface CompositeMention {\n id: string\n label: string\n color: string | null\n}\n\n/** A reference to a Baustein: a `reference` chip in the text of block `from`, or a `reference` field of the Baustein `from`. */\nexport interface CompositeReference {\n /** Block ID of the referenced Baustein. */\n target: string\n /** Block ID of the nearest block around the chip (the Baustein itself for a field); `null` when that block has no ID yet. */\n from: string | null\n /** The chip's locator (`S. 12`, `03:14`); `null` for a field or a chip without one. */\n locator: string | null\n}\n\nconst COMPOSITE = 'composite'\n\n/** True when `node`'s content is empty or text only (with any marks, but no inline nodes such as mentions or tags). */\nfunction isTextOnly(node: PMNode): boolean {\n let textOnly = true\n node.forEach((child) => {\n if (!child.isText) textOnly = false\n })\n return textOnly\n}\n\n/**\n * Reads every composite of `doc` (also inside quotes and lists) as a row, in document order, the distinct\n * `@mention` chips anywhere in the document and every reference (`reference` chips and `reference` fields) in document\n * order. Unknown kinds get the `section` kind's presentation under their own ID.\n */\nexport function compositeRows(doc: PMNode, kinds: readonly CompositeKind[]): { rows: CompositeRow[], mentions: CompositeMention[], references: CompositeReference[] } {\n const rows: CompositeRow[] = []\n const mentions = new Map<string, CompositeMention>()\n const references: CompositeReference[] = []\n\n function walk(parent: PMNode, start: number, depth: number, block: string | null) {\n parent.forEach((node, offset) => {\n const pos = start + offset\n if (node.type.name === COMPOSITE) {\n const kind = compositeKind(kinds, String(node.attrs.kind))\n const id = blockIdOf(node)\n const tags: unknown = node.maybeChild(2)?.attrs.tags\n const description = node.maybeChild(1)\n const row: CompositeRow = {\n key: id ?? `pos:${pos}`,\n id,\n pos,\n end: pos + node.nodeSize,\n kind: kind.id,\n kindLabel: kind.label,\n icon: kind.icon,\n title: node.firstChild?.textContent.trim() ?? '',\n description: description?.textContent ?? '',\n descriptionTextOnly: description ? isTextOnly(description) : true,\n tags: Array.isArray(tags) ? tags.filter((tag): tag is string => typeof tag === 'string') : [],\n meta: (node.attrs.meta ?? {}) as Record<string, unknown>,\n fields: kind.fields,\n tagsVisible: kind.chrome?.tags !== false,\n depth,\n }\n rows.push(row)\n for (const field of kind.fields) {\n const value = field.type === 'reference' ? row.meta[field.key] : undefined\n if (typeof value === 'string' && value) references.push({ target: value, from: id, locator: null })\n }\n walk(node, pos + 1, depth + 1, id ?? block)\n }\n else if (node.type.name === 'reference') {\n const target = String(node.attrs.target ?? '')\n if (target) references.push({ target, from: block, locator: typeof node.attrs.locator === 'string' && node.attrs.locator ? node.attrs.locator : null })\n }\n else if (node.type.name === 'mention') {\n const mentionId = String(node.attrs.id ?? '')\n if (mentionId && !mentions.has(mentionId)) {\n mentions.set(mentionId, {\n id: mentionId,\n label: String(node.attrs.label ?? mentionId),\n color: typeof node.attrs.color === 'string' ? node.attrs.color : null,\n })\n }\n }\n else if (!node.isLeaf) {\n walk(node, pos + 1, depth, blockIdOf(node) ?? block)\n }\n })\n }\n walk(doc, 0, 0, null)\n return { rows, mentions: [...mentions.values()], references }\n}\n\n/** A place that refers to a Baustein: the block holding the chip (or the Baustein with the field) and a short text of it. */\nexport interface CompositeBacklink {\n from: string\n locator: string | null\n excerpt: string\n}\n\nconst EXCERPT_LENGTH = 40\n\n/** The text of `node` as read on screen: inline atoms count as their label (`@name`, `#tag`, a reference chip's short citation + locator), not as nothing. */\nfunction visibleText(doc: PMNode, kinds: readonly CompositeKind[], node: PMNode): string {\n return node.textBetween(0, node.content.size, ' ', (leaf) => {\n const attrs = leaf.attrs\n switch (leaf.type.name) {\n case 'mention': return `@${String(attrs.label ?? attrs.id ?? '')}`\n case 'tag': return `#${String(attrs.name ?? '')}`\n case 'reference': {\n const target = findBlockById(doc, String(attrs.target ?? ''))\n const label = target?.node.type.name === COMPOSITE ? referenceLabel(referenceTarget(target.node, kinds)) : String(attrs.label ?? '')\n return typeof attrs.locator === 'string' && attrs.locator ? `${label}, ${attrs.locator}` : label\n }\n default: return ''\n }\n })\n}\n\n/** Every reference to the Baustein `target` in document order; references whose enclosing block has no ID (or is gone) are left out, as nothing could reveal them. */\nexport function compositeBacklinks(doc: PMNode, kinds: readonly CompositeKind[], target: string): CompositeBacklink[] {\n const links: CompositeBacklink[] = []\n for (const reference of compositeRows(doc, kinds).references) {\n if (reference.target !== target || !reference.from) continue\n const found = findBlockById(doc, reference.from)\n if (!found) continue\n const source = found.node.type.name === COMPOSITE ? found.node.firstChild : found.node\n const text = source ? visibleText(doc, kinds, source).trim() : ''\n links.push({ from: reference.from, locator: reference.locator, excerpt: text.length > EXCERPT_LENGTH ? `${text.slice(0, EXCERPT_LENGTH)}…` : text })\n }\n return links\n}\n\n/** The innermost row around document position `pos`, `null` outside of any composite. */\nexport function compositeRowAt(rows: readonly CompositeRow[], pos: number): CompositeRow | null {\n let found: CompositeRow | null = null\n // Pre-order: the last match is the innermost composite.\n for (const row of rows) {\n if (row.pos <= pos && pos < row.end) found = row\n }\n return found\n}\n\n/**\n * The composite `row` stands for in `doc`: by block ID, else (the row has no ID yet) by its position.\n * Positions are re-resolved on every call, so a stale `row.pos` is safe for rows with an ID.\n */\nexport function compositeOfRow(doc: PMNode, row: Pick<CompositeRow, 'id' | 'pos'>): { pos: number, node: PMNode } | null {\n if (row.id) {\n const found = findBlockById(doc, row.id)\n return found?.node.type.name === COMPOSITE ? found : null\n }\n const node = doc.nodeAt(row.pos)\n return node?.type.name === COMPOSITE ? { pos: row.pos, node } : null\n}\n\n/**\n * `meta` with `value` stored under `field.key`: a number field converts a numeric string, an empty value (`''`,\n * `null`, `undefined`, `NaN`) removes the key. A `multiselect` field keeps the string entries of an array value and\n * removes the key when none is left. Returns a new object; `meta` is not changed.\n */\nexport function compositeMetaWith(meta: Record<string, unknown>, field: Pick<CompositeMetaField, 'key' | 'type'>, value: unknown): Record<string, unknown> {\n const next = { ...meta }\n if (field.type === 'multiselect') {\n const chosen = Array.isArray(value) ? value.filter((item): item is string => typeof item === 'string') : []\n if (chosen.length) next[field.key] = chosen\n else delete next[field.key]\n return next\n }\n const stored = field.type === 'number' && typeof value === 'string' && value !== '' ? Number(value) : value\n if (stored === '' || stored === null || stored === undefined || Number.isNaN(stored)) delete next[field.key]\n else next[field.key] = stored\n return next\n}\n\n/** Stores `value` under the meta field `key` of the row's composite (see `compositeMetaWith`), in one transaction. */\nexport function setCompositeMetaField(editor: Editor, row: Pick<CompositeRow, 'id' | 'pos' | 'fields'>, key: string, value: unknown): boolean {\n const field = row.fields.find(candidate => candidate.key === key) ?? { key, type: 'text' as const }\n return editor.commands.command(({ tr }) => {\n const found = compositeOfRow(tr.doc, row)\n if (!found) return false\n tr.setNodeMarkup(found.pos, undefined, { ...found.node.attrs, meta: compositeMetaWith(found.node.attrs.meta ?? {}, field, value) })\n return true\n })\n}\n\n/** Replaces the tags row of the row's composite (empty names are dropped), in one transaction. */\nexport function setCompositeTags(editor: Editor, row: Pick<CompositeRow, 'id' | 'pos'>, tags: readonly string[]): boolean {\n return editor.commands.command(({ tr }) => {\n const found = compositeOfRow(tr.doc, row)\n const title = found?.node.maybeChild(0)\n const description = found?.node.maybeChild(1)\n const tagsNode = found?.node.maybeChild(2)\n if (!found || !title || !description || tagsNode?.type.name !== 'compositeTags') return false\n const tagsPos = found.pos + 1 + title.nodeSize + description.nodeSize\n tr.setNodeMarkup(tagsPos, undefined, { ...tagsNode.attrs, tags: tags.filter(tag => tag) })\n return true\n })\n}\n\n/** The text `setCompositeDescription` stores for `text`: the description is one line, so line breaks collapse to spaces. */\nexport function compositeDescriptionText(text: string): string {\n return text.replace(/\\s*\\n\\s*/g, ' ').trim()\n}\n\n/**\n * Sets the text of the row's composite description to `compositeDescriptionText(text)`; an empty text empties it.\n * Only the changed span is replaced, so marks on the unchanged text (bold, author, comment) stay. Refuses (`false`)\n * when the description holds a mention or tag (it would be lost), and, when `expected` is given, when the\n * description no longer holds that text (a peer changed it since the caller read it; writing would drop that change).\n */\nexport function setCompositeDescription(editor: Editor, row: Pick<CompositeRow, 'id' | 'pos'>, text: string, expected?: string): boolean {\n const value = compositeDescriptionText(text)\n return editor.commands.command(({ tr }) => {\n const found = compositeOfRow(tr.doc, row)\n const title = found?.node.maybeChild(0)\n const description = found?.node.maybeChild(1)\n if (!found || !title || !description || description.type.name !== 'compositeDescription') return false\n if (!isTextOnly(description)) return false\n const current = description.textContent\n if (expected !== undefined && current !== expected) return false\n if (current === value) return true\n // Text only: every character is one position. Keep the common prefix and suffix, replace what lies between.\n let prefix = 0\n const max = Math.min(current.length, value.length)\n while (prefix < max && current[prefix] === value[prefix]) prefix++\n let suffix = 0\n while (suffix < max - prefix && current[current.length - 1 - suffix] === value[value.length - 1 - suffix]) suffix++\n const start = found.pos + 1 + title.nodeSize + 1\n // `insertText` with an empty text deletes the range; inserted text takes the marks at its start, as typing does.\n tr.insertText(value.slice(prefix, value.length - suffix), start + prefix, start + current.length - suffix)\n return true\n })\n}\n"],"mappings":"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"}
@@ -34,7 +34,7 @@ function fieldSort(row, key, context) {
34
34
  const value = row.meta[key];
35
35
  if (!field || isEmpty(value)) return null;
36
36
  if (field.type === "number") return finiteNumber(value) ?? String(value);
37
- if (field.type === "select" || field.type === "reference" || field.type === "bibliography" || field.type === "derivation") return fieldText(row, key, context) || null;
37
+ if (field.type === "select" || field.type === "multiselect" || field.type === "reference" || field.type === "bibliography" || field.type === "derivation") return fieldText(row, key, context) || null;
38
38
  return String(value);
39
39
  }
40
40
  /**
@@ -101,7 +101,7 @@ function toggled(list, value) {
101
101
  }
102
102
  const hasText = (value) => value.trim() !== "";
103
103
  /** Long text fields (and the citation or provenance summaries) show a truncated fragment in a cell, with the full text as its title. */
104
- const isLongText = (field) => field.type === "text" || field.type === "textarea" || field.type === "reference" || field.type === "bibliography" || field.type === "derivation";
104
+ const isLongText = (field) => field.type === "text" || field.type === "textarea" || field.type === "multiselect" || field.type === "reference" || field.type === "bibliography" || field.type === "derivation";
105
105
  //#endregion
106
106
  export { FIELD_PREFIX, FILTER_ALL, collator, columnSpecsOf, fieldColumnsOf, fieldOf, fieldSort, fieldText, hasText, isLongText, sortRows, toggled };
107
107
 
@@ -1 +1 @@
1
- {"version":3,"file":"table-model.js","names":[],"sources":["../../../src/features/composites/table-model.ts"],"sourcesContent":["import type { Node as PMNode } from '@tiptap/pm/model'\nimport type { Translate } from '../../i18n'\nimport type { CompositeKind, CompositeMetaField } from './kinds'\nimport { fieldDisplay, formatDate } from './meta-display'\nimport type { CompositeRow } from './rows'\n\n// The framework-free part of the Bausteine table (`CompositeTable` on Vue and React): column specs, cell text and sort\n// keys. Columns beyond the fixed ones derive from the meta fields of the kinds present in the document.\n\n/** A sort key; `null` is an empty value, which sorts last in either direction. */\nexport type SortValue = string | number | null\n\nexport interface ColumnSpec {\n id: string\n label: string\n /** Number columns read better flush right, next to their tabular numerals. */\n align?: 'end'\n sort: (row: CompositeRow) => SortValue\n}\n\nexport interface FieldColumn extends ColumnSpec {\n field: CompositeMetaField\n}\n\n/** Id prefix of the columns derived from meta fields. */\nexport const FIELD_PREFIX = 'meta:'\n\n/** The \"no filter\" value of a field's select filter. */\nexport const FILTER_ALL = '__all__'\n\nconst isEmpty = (value: unknown): boolean => value === undefined || value === null || value === ''\nconst finiteNumber = (value: unknown): number | null => {\n if (isEmpty(value)) return null\n const number = Number(value)\n return Number.isFinite(number) ? number : null\n}\nconst numberFormat = new Intl.NumberFormat('de-DE')\n\n/** What a cell needs beyond its row to resolve `reference` fields and word `derivation` methods: the document, its kinds and the translator. */\nexport interface FieldContext {\n doc: PMNode\n kinds: readonly CompositeKind[]\n t: Translate\n}\n\n/** German, numeric-aware ordering of two sort keys' text. */\nexport const collator = new Intl.Collator('de', { numeric: true })\n\n/** The field of `row`'s kind with key `key`. */\nexport const fieldOf = (row: CompositeRow, key: string): CompositeMetaField | undefined => row.fields.find(field => field.key === key)\n\n/** The cell text of `row`'s field `key`; empty when the value is unset or the row's kind does not have the field. Without `context`, `reference` fields read empty. */\nexport function fieldText(row: CompositeRow, key: string, context?: FieldContext): string {\n const field = fieldOf(row, key)\n const value = row.meta[key]\n if (!field || isEmpty(value)) return ''\n if (field.type === 'select') return field.options?.find(option => option.value === value)?.label ?? String(value)\n if (field.type === 'date') return formatDate(String(value))\n if (field.type === 'number') {\n const number = finiteNumber(value)\n return number === null ? String(value) : numberFormat.format(number)\n }\n return fieldDisplay(row, field, context?.doc ?? null, context?.kinds ?? [], context?.t)\n}\n\n/** The sort key of `row`'s field `key`: numbers as numbers, selects, references, citations and provenance by their cell text, everything else as text. */\nexport function fieldSort(row: CompositeRow, key: string, context?: FieldContext): SortValue {\n const field = fieldOf(row, key)\n const value = row.meta[key]\n if (!field || isEmpty(value)) return null\n if (field.type === 'number') return finiteNumber(value) ?? String(value)\n // ISO dates order correctly as plain strings; select values are ordered by what the user reads.\n if (field.type === 'select' || field.type === 'reference' || field.type === 'bibliography' || field.type === 'derivation') return fieldText(row, key, context) || null\n return String(value)\n}\n\n/**\n * One column per field key in use, in kind order; the first kind that has the key decides label and type.\n * `textarea` fields (long text such as a transcript) get no column: a table cell can only show a truncated fragment.\n * `context` resolves `reference` cells and sort keys.\n */\nexport function fieldColumnsOf(rows: readonly CompositeRow[], kinds: readonly { id: string }[], context?: FieldContext): FieldColumn[] {\n const firstRowOfKind = new Map<string, CompositeRow>()\n for (const row of rows) if (!firstRowOfKind.has(row.kind)) firstRowOfKind.set(row.kind, row)\n const seen = new Map<string, FieldColumn>()\n for (const kind of kinds) {\n for (const field of firstRowOfKind.get(kind.id)?.fields ?? []) {\n if (field.type === 'textarea' || seen.has(field.key)) continue\n seen.set(field.key, {\n id: `${FIELD_PREFIX}${field.key}`,\n label: field.label,\n align: field.type === 'number' ? 'end' : undefined,\n sort: row => fieldSort(row, field.key, context),\n field,\n })\n }\n }\n return [...seen.values()]\n}\n\n/** All columns: title and kind, then the field columns, then tags and summary. */\nexport function columnSpecsOf(fieldColumns: readonly FieldColumn[], t: Translate): ColumnSpec[] {\n return [\n { id: 'title', label: t('composites.table.column.composite'), sort: row => row.title.trim() || null },\n { id: 'kind', label: t('composites.table.column.kind'), sort: row => row.kindLabel },\n ...fieldColumns,\n { id: 'tags', label: t('composites.table.column.tags'), sort: row => row.tags.join(', ') || null },\n // Never empty: absence is a value here, so descending puts the summarized Bausteine first.\n { id: 'summary', label: t('composites.table.column.summary'), sort: row => (row.description.trim() ? 1 : 0) },\n ]\n}\n\n/** `rows` ordered by `sort`, descending when `desc`; empty values last whatever the direction. */\nexport function sortRows(rows: readonly CompositeRow[], sort: ColumnSpec['sort'], desc: boolean): CompositeRow[] {\n const factor = desc ? -1 : 1\n return [...rows].sort((a, b) => {\n const x = sort(a)\n const y = sort(b)\n if (x === null || y === null) return x === y ? 0 : x === null ? 1 : -1\n if (typeof x === 'number' && typeof y === 'number') return (x - y) * factor\n return collator.compare(String(x), String(y)) * factor\n })\n}\n\n/** `list` with `value` added, or removed when present. Pure: returns the new list, the caller assigns it. */\nexport function toggled(list: readonly string[], value: string): string[] {\n return list.includes(value) ? list.filter(item => item !== value) : [...list, value]\n}\n\nexport const hasText = (value: string) => value.trim() !== ''\n\n/** Long text fields (and the citation or provenance summaries) show a truncated fragment in a cell, with the full text as its title. */\nexport const isLongText = (field: CompositeMetaField) => field.type === 'text' || field.type === 'textarea' || field.type === 'reference' || field.type === 'bibliography' || field.type === 'derivation'\n"],"mappings":";;;AAyBA,MAAa,eAAe;;AAG5B,MAAa,aAAa;AAE1B,MAAM,WAAW,UAA4B,UAAU,KAAA,KAAa,UAAU,QAAQ,UAAU;AAChG,MAAM,gBAAgB,UAAkC;CACtD,IAAI,QAAQ,KAAK,GAAG,OAAO;CAC3B,MAAM,SAAS,OAAO,KAAK;CAC3B,OAAO,OAAO,SAAS,MAAM,IAAI,SAAS;AAC5C;AACA,MAAM,eAAe,IAAI,KAAK,aAAa,OAAO;;AAUlD,MAAa,WAAW,IAAI,KAAK,SAAS,MAAM,EAAE,SAAS,KAAK,CAAC;;AAGjE,MAAa,WAAW,KAAmB,QAAgD,IAAI,OAAO,MAAK,UAAS,MAAM,QAAQ,GAAG;;AAGrI,SAAgB,UAAU,KAAmB,KAAa,SAAgC;CACxF,MAAM,QAAQ,QAAQ,KAAK,GAAG;CAC9B,MAAM,QAAQ,IAAI,KAAK;CACvB,IAAI,CAAC,SAAS,QAAQ,KAAK,GAAG,OAAO;CACrC,IAAI,MAAM,SAAS,UAAU,OAAO,MAAM,SAAS,MAAK,WAAU,OAAO,UAAU,KAAK,CAAC,EAAE,SAAS,OAAO,KAAK;CAChH,IAAI,MAAM,SAAS,QAAQ,OAAO,WAAW,OAAO,KAAK,CAAC;CAC1D,IAAI,MAAM,SAAS,UAAU;EAC3B,MAAM,SAAS,aAAa,KAAK;EACjC,OAAO,WAAW,OAAO,OAAO,KAAK,IAAI,aAAa,OAAO,MAAM;CACrE;CACA,OAAO,aAAa,KAAK,OAAO,SAAS,OAAO,MAAM,SAAS,SAAS,CAAC,GAAG,SAAS,CAAC;AACxF;;AAGA,SAAgB,UAAU,KAAmB,KAAa,SAAmC;CAC3F,MAAM,QAAQ,QAAQ,KAAK,GAAG;CAC9B,MAAM,QAAQ,IAAI,KAAK;CACvB,IAAI,CAAC,SAAS,QAAQ,KAAK,GAAG,OAAO;CACrC,IAAI,MAAM,SAAS,UAAU,OAAO,aAAa,KAAK,KAAK,OAAO,KAAK;CAEvE,IAAI,MAAM,SAAS,YAAY,MAAM,SAAS,eAAe,MAAM,SAAS,kBAAkB,MAAM,SAAS,cAAc,OAAO,UAAU,KAAK,KAAK,OAAO,KAAK;CAClK,OAAO,OAAO,KAAK;AACrB;;;;;;AAOA,SAAgB,eAAe,MAA+B,OAAkC,SAAuC;CACrI,MAAM,iCAAiB,IAAI,IAA0B;CACrD,KAAK,MAAM,OAAO,MAAM,IAAI,CAAC,eAAe,IAAI,IAAI,IAAI,GAAG,eAAe,IAAI,IAAI,MAAM,GAAG;CAC3F,MAAM,uBAAO,IAAI,IAAyB;CAC1C,KAAK,MAAM,QAAQ,OACjB,KAAK,MAAM,SAAS,eAAe,IAAI,KAAK,EAAE,CAAC,EAAE,UAAU,CAAC,GAAG;EAC7D,IAAI,MAAM,SAAS,cAAc,KAAK,IAAI,MAAM,GAAG,GAAG;EACtD,KAAK,IAAI,MAAM,KAAK;GAClB,IAAI,GAAG,eAAe,MAAM;GAC5B,OAAO,MAAM;GACb,OAAO,MAAM,SAAS,WAAW,QAAQ,KAAA;GACzC,OAAM,QAAO,UAAU,KAAK,MAAM,KAAK,OAAO;GAC9C;EACF,CAAC;CACH;CAEF,OAAO,CAAC,GAAG,KAAK,OAAO,CAAC;AAC1B;;AAGA,SAAgB,cAAc,cAAsC,GAA4B;CAC9F,OAAO;EACL;GAAE,IAAI;GAAS,OAAO,EAAE,mCAAmC;GAAG,OAAM,QAAO,IAAI,MAAM,KAAK,KAAK;EAAK;EACpG;GAAE,IAAI;GAAQ,OAAO,EAAE,8BAA8B;GAAG,OAAM,QAAO,IAAI;EAAU;EACnF,GAAG;EACH;GAAE,IAAI;GAAQ,OAAO,EAAE,8BAA8B;GAAG,OAAM,QAAO,IAAI,KAAK,KAAK,IAAI,KAAK;EAAK;EAEjG;GAAE,IAAI;GAAW,OAAO,EAAE,iCAAiC;GAAG,OAAM,QAAQ,IAAI,YAAY,KAAK,IAAI,IAAI;EAAG;CAC9G;AACF;;AAGA,SAAgB,SAAS,MAA+B,MAA0B,MAA+B;CAC/G,MAAM,SAAS,OAAO,KAAK;CAC3B,OAAO,CAAC,GAAG,IAAI,CAAC,CAAC,MAAM,GAAG,MAAM;EAC9B,MAAM,IAAI,KAAK,CAAC;EAChB,MAAM,IAAI,KAAK,CAAC;EAChB,IAAI,MAAM,QAAQ,MAAM,MAAM,OAAO,MAAM,IAAI,IAAI,MAAM,OAAO,IAAI;EACpE,IAAI,OAAO,MAAM,YAAY,OAAO,MAAM,UAAU,QAAQ,IAAI,KAAK;EACrE,OAAO,SAAS,QAAQ,OAAO,CAAC,GAAG,OAAO,CAAC,CAAC,IAAI;CAClD,CAAC;AACH;;AAGA,SAAgB,QAAQ,MAAyB,OAAyB;CACxE,OAAO,KAAK,SAAS,KAAK,IAAI,KAAK,QAAO,SAAQ,SAAS,KAAK,IAAI,CAAC,GAAG,MAAM,KAAK;AACrF;AAEA,MAAa,WAAW,UAAkB,MAAM,KAAK,MAAM;;AAG3D,MAAa,cAAc,UAA8B,MAAM,SAAS,UAAU,MAAM,SAAS,cAAc,MAAM,SAAS,eAAe,MAAM,SAAS,kBAAkB,MAAM,SAAS"}
1
+ {"version":3,"file":"table-model.js","names":[],"sources":["../../../src/features/composites/table-model.ts"],"sourcesContent":["import type { Node as PMNode } from '@tiptap/pm/model'\nimport type { Translate } from '../../i18n'\nimport type { CompositeKind, CompositeMetaField } from './kinds'\nimport { fieldDisplay, formatDate } from './meta-display'\nimport type { CompositeRow } from './rows'\n\n// The framework-free part of the Bausteine table (`CompositeTable` on Vue and React): column specs, cell text and sort\n// keys. Columns beyond the fixed ones derive from the meta fields of the kinds present in the document.\n\n/** A sort key; `null` is an empty value, which sorts last in either direction. */\nexport type SortValue = string | number | null\n\nexport interface ColumnSpec {\n id: string\n label: string\n /** Number columns read better flush right, next to their tabular numerals. */\n align?: 'end'\n sort: (row: CompositeRow) => SortValue\n}\n\nexport interface FieldColumn extends ColumnSpec {\n field: CompositeMetaField\n}\n\n/** Id prefix of the columns derived from meta fields. */\nexport const FIELD_PREFIX = 'meta:'\n\n/** The \"no filter\" value of a field's select filter. */\nexport const FILTER_ALL = '__all__'\n\nconst isEmpty = (value: unknown): boolean => value === undefined || value === null || value === ''\nconst finiteNumber = (value: unknown): number | null => {\n if (isEmpty(value)) return null\n const number = Number(value)\n return Number.isFinite(number) ? number : null\n}\nconst numberFormat = new Intl.NumberFormat('de-DE')\n\n/** What a cell needs beyond its row to resolve `reference` fields and word `derivation` methods: the document, its kinds and the translator. */\nexport interface FieldContext {\n doc: PMNode\n kinds: readonly CompositeKind[]\n t: Translate\n}\n\n/** German, numeric-aware ordering of two sort keys' text. */\nexport const collator = new Intl.Collator('de', { numeric: true })\n\n/** The field of `row`'s kind with key `key`. */\nexport const fieldOf = (row: CompositeRow, key: string): CompositeMetaField | undefined => row.fields.find(field => field.key === key)\n\n/** The cell text of `row`'s field `key`; empty when the value is unset or the row's kind does not have the field. Without `context`, `reference` fields read empty. */\nexport function fieldText(row: CompositeRow, key: string, context?: FieldContext): string {\n const field = fieldOf(row, key)\n const value = row.meta[key]\n if (!field || isEmpty(value)) return ''\n if (field.type === 'select') return field.options?.find(option => option.value === value)?.label ?? String(value)\n if (field.type === 'date') return formatDate(String(value))\n if (field.type === 'number') {\n const number = finiteNumber(value)\n return number === null ? String(value) : numberFormat.format(number)\n }\n return fieldDisplay(row, field, context?.doc ?? null, context?.kinds ?? [], context?.t)\n}\n\n/** The sort key of `row`'s field `key`: numbers as numbers, selects, references, citations and provenance by their cell text, everything else as text. */\nexport function fieldSort(row: CompositeRow, key: string, context?: FieldContext): SortValue {\n const field = fieldOf(row, key)\n const value = row.meta[key]\n if (!field || isEmpty(value)) return null\n if (field.type === 'number') return finiteNumber(value) ?? String(value)\n // ISO dates order correctly as plain strings; select values are ordered by what the user reads.\n if (field.type === 'select' || field.type === 'multiselect' || field.type === 'reference' || field.type === 'bibliography' || field.type === 'derivation') return fieldText(row, key, context) || null\n return String(value)\n}\n\n/**\n * One column per field key in use, in kind order; the first kind that has the key decides label and type.\n * `textarea` fields (long text such as a transcript) get no column: a table cell can only show a truncated fragment.\n * `context` resolves `reference` cells and sort keys.\n */\nexport function fieldColumnsOf(rows: readonly CompositeRow[], kinds: readonly { id: string }[], context?: FieldContext): FieldColumn[] {\n const firstRowOfKind = new Map<string, CompositeRow>()\n for (const row of rows) if (!firstRowOfKind.has(row.kind)) firstRowOfKind.set(row.kind, row)\n const seen = new Map<string, FieldColumn>()\n for (const kind of kinds) {\n for (const field of firstRowOfKind.get(kind.id)?.fields ?? []) {\n if (field.type === 'textarea' || seen.has(field.key)) continue\n seen.set(field.key, {\n id: `${FIELD_PREFIX}${field.key}`,\n label: field.label,\n align: field.type === 'number' ? 'end' : undefined,\n sort: row => fieldSort(row, field.key, context),\n field,\n })\n }\n }\n return [...seen.values()]\n}\n\n/** All columns: title and kind, then the field columns, then tags and summary. */\nexport function columnSpecsOf(fieldColumns: readonly FieldColumn[], t: Translate): ColumnSpec[] {\n return [\n { id: 'title', label: t('composites.table.column.composite'), sort: row => row.title.trim() || null },\n { id: 'kind', label: t('composites.table.column.kind'), sort: row => row.kindLabel },\n ...fieldColumns,\n { id: 'tags', label: t('composites.table.column.tags'), sort: row => row.tags.join(', ') || null },\n // Never empty: absence is a value here, so descending puts the summarized Bausteine first.\n { id: 'summary', label: t('composites.table.column.summary'), sort: row => (row.description.trim() ? 1 : 0) },\n ]\n}\n\n/** `rows` ordered by `sort`, descending when `desc`; empty values last whatever the direction. */\nexport function sortRows(rows: readonly CompositeRow[], sort: ColumnSpec['sort'], desc: boolean): CompositeRow[] {\n const factor = desc ? -1 : 1\n return [...rows].sort((a, b) => {\n const x = sort(a)\n const y = sort(b)\n if (x === null || y === null) return x === y ? 0 : x === null ? 1 : -1\n if (typeof x === 'number' && typeof y === 'number') return (x - y) * factor\n return collator.compare(String(x), String(y)) * factor\n })\n}\n\n/** `list` with `value` added, or removed when present. Pure: returns the new list, the caller assigns it. */\nexport function toggled(list: readonly string[], value: string): string[] {\n return list.includes(value) ? list.filter(item => item !== value) : [...list, value]\n}\n\nexport const hasText = (value: string) => value.trim() !== ''\n\n/** Long text fields (and the citation or provenance summaries) show a truncated fragment in a cell, with the full text as its title. */\nexport const isLongText = (field: CompositeMetaField) => field.type === 'text' || field.type === 'textarea' || field.type === 'multiselect' || field.type === 'reference' || field.type === 'bibliography' || field.type === 'derivation'\n"],"mappings":"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"}
@@ -41,24 +41,24 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {
41
41
  settingsOpen: import("vue").WritableComputedRef<boolean, boolean>;
42
42
  settingsTab: import("vue").WritableComputedRef<string | undefined, string | undefined>;
43
43
  }, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
44
- settingsOpen: (tab?: string | undefined) => any;
45
44
  "update:sidebarOpen": (value: boolean) => any;
46
45
  "update:commentsOpen": (value: boolean) => any;
47
- "update:modelValue": (markdown: string) => any;
48
- frontmatter: (frontmatter: string | null) => any;
49
46
  ready: (editor: Editor) => any;
47
+ "update:modelValue": (markdown: string) => any;
50
48
  metadataCreate: (list: string, item: MetadataItem) => any;
51
49
  contentError: (error: Error) => any;
50
+ settingsOpen: (tab?: string | undefined) => any;
51
+ frontmatter: (frontmatter: string | null) => any;
52
52
  diagnostics: (diagnostics: MarkdownDiagnostic[]) => any;
53
53
  }, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
54
- onSettingsOpen?: ((tab?: string | undefined) => any) | undefined;
55
54
  "onUpdate:sidebarOpen"?: ((value: boolean) => any) | undefined;
56
55
  "onUpdate:commentsOpen"?: ((value: boolean) => any) | undefined;
57
- "onUpdate:modelValue"?: ((markdown: string) => any) | undefined;
58
- onFrontmatter?: ((frontmatter: string | null) => any) | undefined;
59
56
  onReady?: ((editor: Editor) => any) | undefined;
57
+ "onUpdate:modelValue"?: ((markdown: string) => any) | undefined;
60
58
  onMetadataCreate?: ((list: string, item: MetadataItem) => any) | undefined;
61
59
  onContentError?: ((error: Error) => any) | undefined;
60
+ onSettingsOpen?: ((tab?: string | undefined) => any) | undefined;
61
+ onFrontmatter?: ((frontmatter: string | null) => any) | undefined;
62
62
  onDiagnostics?: ((diagnostics: MarkdownDiagnostic[]) => any) | undefined;
63
63
  }>, {
64
64
  editable: boolean;
@@ -0,0 +1,8 @@
1
+ import { HandlerRequest, HandlerResponse } from "./handlers/context.js";
2
+ import "./handlers/index.js";
3
+ import "h3";
4
+ //#region src/server/h3.d.ts
5
+ /** A route handler of `@ecsplico/editor/server/handlers`, over the context `C` its route needs. */
6
+ export type RouteHandler<C> = (ctx: C, request: HandlerRequest) => Promise<HandlerResponse>;
7
+ //#endregion
8
+ //# sourceMappingURL=h3.d.ts.map
@@ -9,8 +9,8 @@ export declare const chatBodySchema: z.ZodObject<{
9
9
  op: z.ZodString;
10
10
  type: z.ZodEnum<{
11
11
  add: "add";
12
- update: "update";
13
12
  delete: "delete";
13
+ update: "update";
14
14
  convert: "convert";
15
15
  }>;
16
16
  target: z.ZodNullable<z.ZodString>;
@@ -76,6 +76,7 @@ export declare const chatBodySchema: z.ZodObject<{
76
76
  text: "text";
77
77
  select: "select";
78
78
  textarea: "textarea";
79
+ multiselect: "multiselect";
79
80
  }>;
80
81
  options: z.ZodOptional<z.ZodArray<z.ZodObject<{
81
82
  value: z.ZodString;
@@ -60,7 +60,8 @@ const kindSchema = z.object({
60
60
  "textarea",
61
61
  "date",
62
62
  "number",
63
- "select"
63
+ "select",
64
+ "multiselect"
64
65
  ]),
65
66
  options: z.array(z.object({
66
67
  value: z.string().min(1).max(100),
@@ -1 +1 @@
1
- {"version":3,"file":"ai.js","names":[],"sources":["../../../src/server/handlers/ai.ts"],"sourcesContent":["import { createHash, randomUUID } from 'node:crypto'\nimport { jsonSchema, type ModelMessage, tool } from 'ai'\nimport { z } from 'zod'\nimport {\n AGENT_LIMITS,\n AGENT_OP_OUTCOMES,\n AGENT_OP_TYPES,\n AGENT_TOOL,\n AI_COMPLETE_LIMITS,\n AI_ID_PATTERN,\n AI_LIMITS,\n AI_TASK_IDS,\n AI_TASKS,\n type AgentOp,\n type AgentOpProblem,\n agentOpTarget,\n agentRetryMessage,\n agentUnreadableProblem,\n aiBlockInputSchema,\n aiBlockSchema,\n checkAgentOps,\n CHAT_LIMITS,\n cleanCompletion,\n completeMessages,\n completionReady,\n documentChatMessages,\n type ModelAgentOp,\n modelRefSchema,\n readBlockLines,\n} from '../../features/ai/protocol'\nimport type { ExtractMethod, ExtractRequest, ExtractResponse } from '../../core/backend-types'\nimport type { CompositeMetaField } from '../../features/composites/schema'\nimport { ASSET_DOCUMENT_ID, ASSET_NAME } from '../../features/media/schema'\nimport {\n parseTranscriptCleanup,\n TRANSCRIPT_CLEANUP_LIMITS,\n type TranscriptCleanupResponse,\n type TranscriptCleanupResult,\n type TranscriptCleanupSegment,\n transcriptCleanupChunks,\n transcriptCleanupMessages,\n transcriptCleanupRequestSchema,\n} from '../../integrations/transcript-ai/protocol'\nimport { AI_LANGUAGES, STRUCTURE_MODES } from '../../settings'\nimport { HttpError } from '../errors'\nimport { BIBLIOGRAPHY_PAGES, bibliographyText, describeImage, extractBibliography, isScanned, joinPages, MAX_OCR_PAGES, MAX_TRANSCRIBE_BYTES, ocrPages, pdfPagesText, RasterError, transcribe, type TranscriptionResult } from '../extract'\nimport { DOI_PATTERN } from '../crossref'\nimport { openPdf } from '../pdf'\nimport { type AiHandlerContext, authenticated, enforceRateLimit, eventStream, type HandlerRequest, type HandlerResponse, json, port, readBody } from './context'\n\nconst blockId = z.string().min(1).max(100)\nconst opId = z.string().regex(/^op\\d+$/)\nconst markdown = z.string().min(1).max(AGENT_LIMITS.markdown)\n/** A `convert` target: a `CONVERT_TARGET_IDS` entry, `composite:<kind>`, or a host block id (`aiContext.blocks` with a conversion provider). */\nconst convertTarget = z.string().min(1).max(100)\n\nconst opSchema = z.discriminatedUnion('type', [\n z.object({ op: opId, type: z.literal('update'), id: blockId, markdown }),\n z.object({ op: opId, type: z.literal('delete'), id: blockId }),\n z.object({ op: opId, type: z.literal('add'), afterId: blockId.nullable(), markdown }),\n z.object({ op: opId, type: z.literal('convert'), id: blockId, to: convertTarget }),\n])\n\nconst label = z.string().min(1).max(100)\nconst kindSchema = z.object({\n // Same characters as the `::: kind` fence opener accepts.\n id: z.string().regex(AI_ID_PATTERN).max(50),\n label,\n purpose: z.string().min(1).max(500).optional(),\n fields: z.array(z.object({\n key: z.string().min(1).max(50),\n label,\n type: z.enum(['text', 'textarea', 'date', 'number', 'select'] as const satisfies readonly CompositeMetaField['type'][]),\n options: z.array(z.object({ value: z.string().min(1).max(100), label })).max(50).optional(),\n })).max(20),\n})\n\nconst messageSchema = z.discriminatedUnion('role', [\n z.object({\n role: z.literal('user'),\n content: z.string().min(1).max(CHAT_LIMITS.message),\n feedback: z.array(z.object({ op: opId, type: z.enum(AGENT_OP_TYPES), target: blockId.nullable(), outcome: z.enum(AGENT_OP_OUTCOMES) })).max(500).optional(),\n }),\n z.object({\n role: z.literal('assistant'),\n content: z.string().max(CHAT_LIMITS.message),\n ops: z.array(opSchema).max(AGENT_LIMITS.ops).optional(),\n dropped: z.number().int().min(0).max(1000).optional(),\n }).refine(m => m.content || m.ops?.length || m.dropped, 'Leere Antwort im Verlauf'),\n])\n\nexport const chatBodySchema = z.object({\n messages: z.array(messageSchema).min(1).max(CHAT_LIMITS.messages).refine(messages => messages.at(-1)!.role === 'user', 'Die letzte Nachricht muss von der Nutzerin bzw. dem Nutzer sein'),\n document: z.string().max(CHAT_LIMITS.document),\n /** Block IDs edits are limited to (the selected blocks); null or missing: the whole document. */\n scope: z.array(blockId).min(1).max(AGENT_LIMITS.scope).nullish(),\n /** Number of the first new operation (`op<n>`), so IDs stay unique across the conversation. */\n nextOp: z.number().int().min(1).max(1_000_000).default(1),\n model: modelRefSchema,\n /** The editor's composite kinds (`compositeKindsOf`), for the block model in the system prompt. */\n kinds: z.array(kindSchema).max(50),\n /** The editor's host blocks (`aiContext.blocks`), for the block model in the system prompt. */\n blocks: z.array(aiBlockSchema).max(50).default([]),\n /** Block types `convert` may turn a block into. */\n targets: z.array(convertTarget).max(100),\n /** The user's preferred structure of new content (settings `blocks.structure`). */\n structure: z.enum(STRUCTURE_MODES),\n})\n\n/** `edit_document`: deliberately loose, so malformed calls reach `checkAgentOps` and get a precise retry message. */\nconst agentTools = {\n [AGENT_TOOL]: tool({\n description: 'Schlägt Änderungen am Dokument vor. Jede Operation wird der Nutzerin bzw. dem Nutzer im Dokument zur Prüfung angezeigt.',\n inputSchema: jsonSchema<{ ops: unknown[] }>({\n type: 'object',\n properties: {\n ops: {\n type: 'array',\n description: 'Die Operationen in Dokumentreihenfolge.',\n items: {\n type: 'object',\n properties: {\n type: { type: 'string', enum: [...AGENT_OP_TYPES], description: 'update: Blockinhalt ersetzen; delete: Block löschen; add: Blöcke einfügen; convert: Blocktyp ändern.' },\n id: { type: 'string', description: 'update/delete/convert: ID des Blocks aus dem Dokument.' },\n afterId: { type: ['string', 'null'], description: 'add (Pflicht): ID des Blocks, nach dem eingefügt wird; für den Dokumentanfang ausdrücklich null.' },\n markdown: { type: 'string', description: 'update/add: der neue Inhalt als Markdown, ohne Block-ID.' },\n to: { type: 'string', description: 'convert: der neue Blocktyp, einer der im Systemprompt erlaubten Werte (z. B. paragraph, heading2, composite:task).' },\n },\n required: ['type'],\n },\n },\n },\n required: ['ops'],\n }),\n }),\n}\n\n/**\n * Document chat: answers the last message of `messages` about `document`, or proposes edit operations through the\n * `edit_document` tool. Operations pointing to IDs that are unknown or outside `scope`, an `add` without `afterId`, an\n * operation inside a block that another operation replaces or deletes, or converting into a type not in\n * `targets`, make the model retry (at most `AGENT_LIMITS.retries` times); what is still invalid then, or left out by a\n * corrected list, is dropped. The system prompt describes the editor's block model from `kinds`, `blocks` and `targets`. SSE like\n * `/api/ai/generate`: `delta` (text), `ops` (`{ ops: AgentOp[], dropped: AgentOpProblem[], retries: number }`, once,\n * before `done`), `done`, `error`.\n */\nexport async function chatPost(ctx: AiHandlerContext, request: HandlerRequest): Promise<HandlerResponse> {\n const user = await enforceRateLimit(ctx, request)\n const { messages, document, scope, nextOp, model, kinds, blocks, targets, structure } = await readBody(request, chatBodySchema)\n // Unknown preset or missing key must fail as a plain JSON error before the SSE response starts.\n const languageModel = await ctx.ai.resolveModel(model, user)\n if (!messages.at(-1)!.content.trim()) throw new HttpError(400, 'Nachricht fehlt')\n\n const lines = readBlockLines(document)\n const allowed = scope ? new Set(scope.filter(id => lines.ids.has(id))) : null\n if (allowed && !allowed.size) throw new HttpError(400, 'Die Auswahl enthält keine Blöcke des Dokuments')\n const context = { lines, allowed, targets: new Set(targets) }\n\n return eventStream(request, async (emit, signal) => {\n try {\n let conversation: ModelMessage[] = documentChatMessages(messages, document, allowed && [...allowed], { kinds, blocks, targets, structure })\n let answered = false\n let result: { valid: ModelAgentOp[], problems: AgentOpProblem[] } | null = null\n // Problems of attempts that were retried: an operation the corrected list leaves out counts as dropped too.\n const retried: AgentOpProblem[] = []\n let retries = 0\n for (let attempt = 0; attempt <= AGENT_LIMITS.retries; attempt++) {\n const calls: { toolCallId: string, input: unknown, invalid: boolean }[] = []\n let text = ''\n for await (const part of ctx.ai.streamChatTurn({\n model: languageModel,\n messages: conversation,\n tools: agentTools,\n // Retries only correct the operations: no second answer text.\n toolChoice: attempt ? { type: 'tool', toolName: AGENT_TOOL } : 'auto',\n signal,\n })) {\n if (part.type === 'tool-call') {\n if (part.toolName === AGENT_TOOL) calls.push(part)\n }\n else if (!attempt) {\n text += part.text\n answered = true\n emit('delta', JSON.stringify({ text: part.text }))\n }\n }\n if (!calls.length) break\n\n const unreadable = calls.some(call => call.invalid || !Array.isArray((call.input as { ops?: unknown } | null)?.ops))\n const raw = calls.flatMap(call => (call.input as { ops?: unknown } | null)?.ops).filter(op => op !== undefined)\n const checked = checkAgentOps(raw, context)\n if (unreadable) checked.problems.unshift(agentUnreadableProblem())\n result = checked\n if (!checked.problems.length || attempt === AGENT_LIMITS.retries) break\n\n retried.push(...checked.problems)\n retries++\n const retry = agentRetryMessage(checked.problems, !!allowed)\n conversation = [\n ...conversation,\n {\n role: 'assistant',\n content: [\n ...(text ? [{ type: 'text' as const, text }] : []),\n ...calls.map(call => ({ type: 'tool-call' as const, toolCallId: call.toolCallId, toolName: AGENT_TOOL, input: call.input ?? {} })),\n ],\n },\n { role: 'tool', content: calls.map(call => ({ type: 'tool-result' as const, toolCallId: call.toolCallId, toolName: AGENT_TOOL, output: { type: 'text' as const, value: retry } })) },\n ]\n }\n\n if (result) {\n const ops: AgentOp[] = result.valid.map((op, i) => ({ ...op, op: `op${nextOp + i}` }))\n const targets = new Set(ops.map(op => agentOpTarget(op)))\n const dropped = [...result.problems]\n for (const problem of retried) {\n if (problem.id !== null && !targets.has(problem.id) && !dropped.some(d => d.id === problem.id && d.type === problem.type)) dropped.push(problem)\n }\n if (!answered && !ops.length) {\n emit('delta', JSON.stringify({ text: 'Ich konnte keine gültigen Änderungen vorschlagen.' }))\n }\n emit('ops', JSON.stringify({ ops, dropped, retries }))\n }\n emit('done', '{}')\n \n }\n catch (error) {\n if (!signal.aborted) {\n const { statusMessage, message } = error as { statusMessage?: string, message?: string }\n emit('error', JSON.stringify({ message: statusMessage ?? message ?? 'Unbekannter Fehler' }))\n }\n }\n })\n}\n\n/** Body schema of `completePost`. */\nexport const completeBodySchema = z.object({ before: z.string().max(AI_COMPLETE_LIMITS.before), model: modelRefSchema })\n\n/** Short inline continuation (ghost text) of the text before the caret. Plain JSON: `{ text }`, empty when nothing fits. */\nexport async function completePost(ctx: AiHandlerContext, request: HandlerRequest): Promise<HandlerResponse> {\n const user = await enforceRateLimit(ctx, request)\n const { before, model } = await readBody(request, completeBodySchema)\n const languageModel = await ctx.ai.resolveModel(model, user)\n if (!before.trim()) return json({ text: '' })\n\n // Typing on the client aborts its fetch; stop the upstream request with it (breaking the loop cancels it as well).\n const abort = new AbortController()\n request.signal?.addEventListener('abort', () => abort.abort(), { once: true })\n let raw = ''\n try {\n for await (const text of ctx.ai.streamChat({ model: languageModel, messages: completeMessages(before), signal: abort.signal })) {\n raw += text\n if (completionReady(raw)) break\n }\n }\n catch (error) {\n if (abort.signal.aborted) return json({ text: '' })\n throw error\n }\n return json({ text: cleanCompletion(raw, before) })\n}\n\n/** Body schema of `generatePost`. */\nexport const generateBodySchema = z.object({\n task: z.enum(AI_TASK_IDS),\n prompt: z.string().max(AI_LIMITS.prompt).default(''),\n selection: z.string().max(AI_LIMITS.selection).default(''),\n // Whole-document tasks may send the longer transcript; the stricter limit for the others is checked per task below.\n context: z.string().max(AI_LIMITS.transcript).default(''),\n language: z.enum(AI_LANGUAGES).optional(),\n blocks: z.array(aiBlockSchema).max(50).default([]),\n block: aiBlockInputSchema.optional(),\n model: modelRefSchema,\n})\n\n/** Streams one text task (rewrite, translate, generate, …) as SSE: `delta` (`{ text }`), `done`, `error` (`{ message }`). */\nexport async function generatePost(ctx: AiHandlerContext, request: HandlerRequest): Promise<HandlerResponse> {\n const user = await enforceRateLimit(ctx, request)\n const { task: taskId, model, ...input } = await readBody(request, generateBodySchema)\n // Unknown preset or missing key must fail as a plain JSON error before the SSE response starts.\n const languageModel = await ctx.ai.resolveModel(model, user)\n const task = AI_TASKS[taskId]\n if (task.needsSelection && !input.selection.trim()) throw new HttpError(400, 'Keine Textauswahl')\n if (task.needsPrompt && !input.prompt.trim()) throw new HttpError(400, 'Prompt fehlt')\n if (task.needsLanguage && !input.language) throw new HttpError(400, 'Zielsprache fehlt')\n if (task.needsBlock && !input.block) throw new HttpError(400, 'Baustein fehlt')\n if (task.wholeDocument && !input.context.trim()) throw new HttpError(400, 'Dokument ist leer')\n if (!task.wholeDocument && input.context.length > AI_LIMITS.context) throw new HttpError(400, 'Kontext zu lang')\n const users = task.needsUsers ? (await port(ctx, 'directory').listUsers()).map(u => u.username) : undefined\n\n return eventStream(request, async (emit, signal) => {\n try {\n for await (const text of ctx.ai.streamChat({ model: languageModel, messages: task.messages({ ...input, users }), signal })) {\n emit('delta', JSON.stringify({ text }))\n }\n emit('done', '{}')\n }\n catch (error) {\n if (!signal.aborted) {\n const { statusMessage, message } = error as { statusMessage?: string, message?: string }\n emit('error', JSON.stringify({ message: statusMessage ?? message ?? 'Unbekannter Fehler' }))\n }\n }\n })\n}\n\n/**\n * The models the client may choose from (without keys) — the `models` port's `listModels` for the user, else the\n * server-defined presets — and the default preset id per model slot, for the client's model profiles. Requires an\n * authenticated user through the `identity` port; not rate limited, since it calls no model.\n */\nexport async function modelsGet(ctx: AiHandlerContext, request: HandlerRequest): Promise<HandlerResponse> {\n const user = await authenticated(ctx, request)\n return json({ presets: await ctx.ai.listModels(user), slots: ctx.ai.slots })\n}\n\nconst sha256Hex = (data: string | Uint8Array): string => createHash('sha256').update(data).digest('hex')\n\n/** PDF pages read by `pdf-text` and `ocr` unless the request caps them lower. */\nconst DEFAULT_MAX_PAGES = 200\n\n/** Body schema of `extractPost`. */\nexport const extractBodySchema = z.object({\n documentId: z.string().regex(ASSET_DOCUMENT_ID),\n name: z.string().regex(ASSET_NAME).optional(),\n method: z.enum(['auto', 'transcription', 'pdf-text', 'ocr', 'description', 'bibliography']),\n model: modelRefSchema,\n bibliography: z.boolean().optional(),\n doi: z.string().regex(DOI_PATTERN).optional(),\n maxPages: z.number().int().min(1).max(500).default(DEFAULT_MAX_PAGES),\n}).refine(body => body.name !== undefined || (body.method === 'bibliography' && body.doi !== undefined), {\n message: 'name fehlt (nur eine Literaturabfrage mit DOI braucht keine Datei)',\n path: ['name'],\n}) satisfies z.ZodType<ExtractRequest>\n\n/** The method an `ExtractRequest` runs for an asset of `mimeType`: `auto` picks by type; 415 when the type does not fit the method. */\nfunction extractMethodOf(requested: ExtractMethod, mimeType: string, hasDoi: boolean): ExtractResponse['method'] {\n const audioVideo = /^(?:audio|video)\\//.test(mimeType)\n const image = mimeType.startsWith('image/')\n const pdf = mimeType === 'application/pdf'\n const fits: Record<ExtractMethod, boolean> = {\n 'auto': audioVideo || image || pdf,\n 'transcription': audioVideo,\n 'description': image,\n 'pdf-text': pdf,\n 'ocr': pdf,\n // A given DOI needs no document text, so any asset can stand for the work.\n 'bibliography': pdf || hasDoi,\n }\n if (!fits[requested]) throw new HttpError(415, `Dateityp ${mimeType} wird für diese Auswertung nicht unterstützt`)\n if (requested !== 'auto') return requested\n if (audioVideo) return 'transcription'\n return image ? 'description' : 'pdf-text'\n}\n\n/**\n * Turns a stored asset into readable content (`ExtractRequest` → `ExtractResponse`): audio/video into a transcription\n * with segment timestamps, a PDF into its text per page (OCR through the model when it has no text layer), an image\n * into a description, bibliographic data from Crossref or the model. The raw Markdown is stored as an asset of the same\n * document (`output`), with the SHA-256 of input and output. Errors: 404 unknown asset, 415 type not supported, 413\n * oversize audio, 422 unreadable PDF, 501 transcription or OCR unavailable, 502 upstream failure.\n */\nexport async function extractPost(ctx: AiHandlerContext, request: HandlerRequest): Promise<HandlerResponse> {\n const user = await enforceRateLimit(ctx, request)\n const body = await readBody(request, extractBodySchema)\n if (body.name === undefined) {\n // Only a DOI lookup comes without a file (the schema allows nothing else): no asset, no output, empty hashes.\n const { bib, warnings } = await extractBibliography({ doi: body.doi, text: '', resolveModel: () => ctx.ai.resolveModel(body.model, user), signal: request.signal })\n return json({ method: 'bibliography', markdown: '', bib, inputHash: '', outputHash: sha256Hex(''), output: null, warnings } satisfies ExtractResponse)\n }\n const assets = port(ctx, 'assets')\n const asset = await assets.loadAsset(body.documentId, body.name)\n if (!asset) throw new HttpError(404, 'Datei nicht gefunden')\n const inputHash = sha256Hex(asset.data)\n let method = extractMethodOf(body.method, asset.record.mimeType, body.doi !== undefined)\n // Closing the page aborts the client's fetch; the upstream requests stop with it.\n const { signal } = request\n const resolveModel = () => ctx.ai.resolveModel(body.model, user)\n\n const warnings: string[] = []\n let markdown = ''\n let model: string | undefined\n let segments: TranscriptionResult['segments']\n let bib: ExtractResponse['bib']\n\n if (method === 'transcription') {\n const config = ctx.ai.transcription\n if (!config?.endpoint || !config.model) throw new HttpError(501, 'Transkription nicht konfiguriert')\n if (asset.data.byteLength > MAX_TRANSCRIBE_BYTES) throw new HttpError(413, `Datei ist größer als ${MAX_TRANSCRIBE_BYTES / 1024 / 1024} MB`)\n ;({ markdown, segments } = await transcribe(config, asset, signal))\n model = config.model\n }\n else if (method === 'description') {\n markdown = await describeImage(await resolveModel(), asset.data, asset.record.mimeType, signal)\n }\n else if (asset.record.mimeType !== 'application/pdf') {\n // `bibliography` of a non-PDF asset: only reachable with a DOI.\n const found = await extractBibliography({ doi: body.doi, text: '', resolveModel, signal })\n bib = found.bib\n warnings.push(...found.warnings)\n }\n else {\n const pdf = await openPdf(asset.data)\n try {\n if (method === 'bibliography') {\n const pages = await pdfPagesText(pdf, Math.min(pdf.pageCount, BIBLIOGRAPHY_PAGES), signal)\n const found = await extractBibliography({ doi: body.doi, text: bibliographyText(pages), resolveModel, signal })\n bib = found.bib\n warnings.push(...found.warnings)\n }\n else {\n const count = Math.min(pdf.pageCount, body.maxPages)\n if (pdf.pageCount > count) warnings.push('pages-truncated')\n let pages = await pdfPagesText(pdf, count, signal)\n if (body.method === 'auto' && isScanned(pages)) method = 'ocr'\n if (method === 'ocr') {\n // One vision call per page: the OCR reads fewer pages than the text layer would.\n const ocrCount = Math.min(count, MAX_OCR_PAGES)\n try {\n pages = await ocrPages(pdf, ocrCount, resolveModel, signal)\n if (ocrCount < count) warnings.push('ocr-truncated')\n }\n catch (error) {\n if (!(error instanceof RasterError)) throw error\n // Pages cannot be rendered here: keep what the text layer has.\n if (!pages.some(Boolean)) throw new HttpError(501, 'OCR nicht verfügbar')\n warnings.push('ocr-unavailable')\n method = 'pdf-text'\n }\n }\n markdown = joinPages(pages)\n if (body.bibliography) {\n try {\n const found = await extractBibliography({ doi: body.doi, text: bibliographyText(pages), resolveModel, signal })\n bib = found.bib\n warnings.push(...found.warnings)\n }\n catch (error) {\n // The text is the result; missing bibliographic data must not lose it.\n if (signal?.aborted) throw error\n warnings.push('bibliography-failed')\n }\n }\n }\n }\n finally {\n await pdf.close()\n }\n }\n\n const outputHash = sha256Hex(markdown)\n let output: string | null = null\n if (method !== 'bibliography' && markdown) {\n const data = Buffer.from(markdown, 'utf8')\n output = `${randomUUID()}.md`\n await assets.storeAsset(body.documentId, {\n name: output,\n filename: `${asset.record.filename}.${method}.md`.slice(0, 255),\n mimeType: 'text/markdown',\n size: data.byteLength,\n createdAt: new Date().toISOString(),\n sha256: outputHash,\n data,\n })\n }\n return json({ method, markdown, ...(segments && { segments }), ...(bib && { bib }), ...(model && { model }), inputHash, outputHash, output, warnings } satisfies ExtractResponse)\n}\n\n/** Body schema of `transcriptCleanupPost`. */\nexport const transcriptCleanupBodySchema = transcriptCleanupRequestSchema\n\n/**\n * Cleans transcript segments (filler words, punctuation, casing, fragmented sentences) without touching speakers or\n * order: the model only returns a new text per segment ID. Plain JSON: `{ cleaned, failed }`.\n */\nexport async function transcriptCleanupPost(ctx: AiHandlerContext, request: HandlerRequest): Promise<HandlerResponse> {\n const user = await enforceRateLimit(ctx, request)\n const { segments, model } = await readBody(request, transcriptCleanupBodySchema)\n const languageModel = await ctx.ai.resolveModel(model, user)\n\n // Closing the menu or the page aborts the client's fetch; stop the upstream requests with it.\n const abort = new AbortController()\n request.signal?.addEventListener('abort', () => abort.abort(), { once: true })\n\n // Long transcripts are cleaned in chunks, a few model calls at a time; a chunk with an unusable answer stays as it is.\n const cleanChunk = async (chunk: TranscriptCleanupSegment[]) => {\n let raw = ''\n for await (const text of ctx.ai.streamChat({ model: languageModel, messages: transcriptCleanupMessages(chunk), signal: abort.signal })) raw += text\n try {\n return parseTranscriptCleanup(raw, chunk)\n }\n catch {\n return null\n }\n }\n const chunks = transcriptCleanupChunks(segments)\n const results: (TranscriptCleanupResult[] | null)[] = []\n let next = 0\n const worker = async () => {\n while (next < chunks.length) {\n const index = next++\n results[index] = await cleanChunk(chunks[index]!)\n }\n }\n try {\n await Promise.all(Array.from({ length: Math.min(TRANSCRIPT_CLEANUP_LIMITS.concurrency, chunks.length) }, worker))\n }\n finally {\n // An upstream error in one chunk fails the request: stop the other chunks' model calls too.\n abort.abort()\n }\n\n if (results.every(r => r === null)) throw new HttpError(502, 'KI-Antwort konnte nicht gelesen werden')\n return json({\n cleaned: results.flatMap(r => r ?? []),\n failed: chunks.reduce((sum, chunk, i) => sum + (results[i] === null ? chunk.length : 0), 0),\n } satisfies TranscriptCleanupResponse)\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"ai.js","names":[],"sources":["../../../src/server/handlers/ai.ts"],"sourcesContent":["import { createHash, randomUUID } from 'node:crypto'\nimport { jsonSchema, type ModelMessage, tool } from 'ai'\nimport { z } from 'zod'\nimport {\n AGENT_LIMITS,\n AGENT_OP_OUTCOMES,\n AGENT_OP_TYPES,\n AGENT_TOOL,\n AI_COMPLETE_LIMITS,\n AI_ID_PATTERN,\n AI_LIMITS,\n AI_TASK_IDS,\n AI_TASKS,\n type AgentOp,\n type AgentOpProblem,\n agentOpTarget,\n agentRetryMessage,\n agentUnreadableProblem,\n aiBlockInputSchema,\n aiBlockSchema,\n checkAgentOps,\n CHAT_LIMITS,\n cleanCompletion,\n completeMessages,\n completionReady,\n documentChatMessages,\n type ModelAgentOp,\n modelRefSchema,\n readBlockLines,\n} from '../../features/ai/protocol'\nimport type { ExtractMethod, ExtractRequest, ExtractResponse } from '../../core/backend-types'\nimport type { CompositeMetaField } from '../../features/composites/schema'\nimport { ASSET_DOCUMENT_ID, ASSET_NAME } from '../../features/media/schema'\nimport {\n parseTranscriptCleanup,\n TRANSCRIPT_CLEANUP_LIMITS,\n type TranscriptCleanupResponse,\n type TranscriptCleanupResult,\n type TranscriptCleanupSegment,\n transcriptCleanupChunks,\n transcriptCleanupMessages,\n transcriptCleanupRequestSchema,\n} from '../../integrations/transcript-ai/protocol'\nimport { AI_LANGUAGES, STRUCTURE_MODES } from '../../settings'\nimport { HttpError } from '../errors'\nimport { BIBLIOGRAPHY_PAGES, bibliographyText, describeImage, extractBibliography, isScanned, joinPages, MAX_OCR_PAGES, MAX_TRANSCRIBE_BYTES, ocrPages, pdfPagesText, RasterError, transcribe, type TranscriptionResult } from '../extract'\nimport { DOI_PATTERN } from '../crossref'\nimport { openPdf } from '../pdf'\nimport { type AiHandlerContext, authenticated, enforceRateLimit, eventStream, type HandlerRequest, type HandlerResponse, json, port, readBody } from './context'\n\nconst blockId = z.string().min(1).max(100)\nconst opId = z.string().regex(/^op\\d+$/)\nconst markdown = z.string().min(1).max(AGENT_LIMITS.markdown)\n/** A `convert` target: a `CONVERT_TARGET_IDS` entry, `composite:<kind>`, or a host block id (`aiContext.blocks` with a conversion provider). */\nconst convertTarget = z.string().min(1).max(100)\n\nconst opSchema = z.discriminatedUnion('type', [\n z.object({ op: opId, type: z.literal('update'), id: blockId, markdown }),\n z.object({ op: opId, type: z.literal('delete'), id: blockId }),\n z.object({ op: opId, type: z.literal('add'), afterId: blockId.nullable(), markdown }),\n z.object({ op: opId, type: z.literal('convert'), id: blockId, to: convertTarget }),\n])\n\nconst label = z.string().min(1).max(100)\nconst kindSchema = z.object({\n // Same characters as the `::: kind` fence opener accepts.\n id: z.string().regex(AI_ID_PATTERN).max(50),\n label,\n purpose: z.string().min(1).max(500).optional(),\n fields: z.array(z.object({\n key: z.string().min(1).max(50),\n label,\n type: z.enum(['text', 'textarea', 'date', 'number', 'select', 'multiselect'] as const satisfies readonly CompositeMetaField['type'][]),\n options: z.array(z.object({ value: z.string().min(1).max(100), label })).max(50).optional(),\n })).max(20),\n})\n\nconst messageSchema = z.discriminatedUnion('role', [\n z.object({\n role: z.literal('user'),\n content: z.string().min(1).max(CHAT_LIMITS.message),\n feedback: z.array(z.object({ op: opId, type: z.enum(AGENT_OP_TYPES), target: blockId.nullable(), outcome: z.enum(AGENT_OP_OUTCOMES) })).max(500).optional(),\n }),\n z.object({\n role: z.literal('assistant'),\n content: z.string().max(CHAT_LIMITS.message),\n ops: z.array(opSchema).max(AGENT_LIMITS.ops).optional(),\n dropped: z.number().int().min(0).max(1000).optional(),\n }).refine(m => m.content || m.ops?.length || m.dropped, 'Leere Antwort im Verlauf'),\n])\n\nexport const chatBodySchema = z.object({\n messages: z.array(messageSchema).min(1).max(CHAT_LIMITS.messages).refine(messages => messages.at(-1)!.role === 'user', 'Die letzte Nachricht muss von der Nutzerin bzw. dem Nutzer sein'),\n document: z.string().max(CHAT_LIMITS.document),\n /** Block IDs edits are limited to (the selected blocks); null or missing: the whole document. */\n scope: z.array(blockId).min(1).max(AGENT_LIMITS.scope).nullish(),\n /** Number of the first new operation (`op<n>`), so IDs stay unique across the conversation. */\n nextOp: z.number().int().min(1).max(1_000_000).default(1),\n model: modelRefSchema,\n /** The editor's composite kinds (`compositeKindsOf`), for the block model in the system prompt. */\n kinds: z.array(kindSchema).max(50),\n /** The editor's host blocks (`aiContext.blocks`), for the block model in the system prompt. */\n blocks: z.array(aiBlockSchema).max(50).default([]),\n /** Block types `convert` may turn a block into. */\n targets: z.array(convertTarget).max(100),\n /** The user's preferred structure of new content (settings `blocks.structure`). */\n structure: z.enum(STRUCTURE_MODES),\n})\n\n/** `edit_document`: deliberately loose, so malformed calls reach `checkAgentOps` and get a precise retry message. */\nconst agentTools = {\n [AGENT_TOOL]: tool({\n description: 'Schlägt Änderungen am Dokument vor. Jede Operation wird der Nutzerin bzw. dem Nutzer im Dokument zur Prüfung angezeigt.',\n inputSchema: jsonSchema<{ ops: unknown[] }>({\n type: 'object',\n properties: {\n ops: {\n type: 'array',\n description: 'Die Operationen in Dokumentreihenfolge.',\n items: {\n type: 'object',\n properties: {\n type: { type: 'string', enum: [...AGENT_OP_TYPES], description: 'update: Blockinhalt ersetzen; delete: Block löschen; add: Blöcke einfügen; convert: Blocktyp ändern.' },\n id: { type: 'string', description: 'update/delete/convert: ID des Blocks aus dem Dokument.' },\n afterId: { type: ['string', 'null'], description: 'add (Pflicht): ID des Blocks, nach dem eingefügt wird; für den Dokumentanfang ausdrücklich null.' },\n markdown: { type: 'string', description: 'update/add: der neue Inhalt als Markdown, ohne Block-ID.' },\n to: { type: 'string', description: 'convert: der neue Blocktyp, einer der im Systemprompt erlaubten Werte (z. B. paragraph, heading2, composite:task).' },\n },\n required: ['type'],\n },\n },\n },\n required: ['ops'],\n }),\n }),\n}\n\n/**\n * Document chat: answers the last message of `messages` about `document`, or proposes edit operations through the\n * `edit_document` tool. Operations pointing to IDs that are unknown or outside `scope`, an `add` without `afterId`, an\n * operation inside a block that another operation replaces or deletes, or converting into a type not in\n * `targets`, make the model retry (at most `AGENT_LIMITS.retries` times); what is still invalid then, or left out by a\n * corrected list, is dropped. The system prompt describes the editor's block model from `kinds`, `blocks` and `targets`. SSE like\n * `/api/ai/generate`: `delta` (text), `ops` (`{ ops: AgentOp[], dropped: AgentOpProblem[], retries: number }`, once,\n * before `done`), `done`, `error`.\n */\nexport async function chatPost(ctx: AiHandlerContext, request: HandlerRequest): Promise<HandlerResponse> {\n const user = await enforceRateLimit(ctx, request)\n const { messages, document, scope, nextOp, model, kinds, blocks, targets, structure } = await readBody(request, chatBodySchema)\n // Unknown preset or missing key must fail as a plain JSON error before the SSE response starts.\n const languageModel = await ctx.ai.resolveModel(model, user)\n if (!messages.at(-1)!.content.trim()) throw new HttpError(400, 'Nachricht fehlt')\n\n const lines = readBlockLines(document)\n const allowed = scope ? new Set(scope.filter(id => lines.ids.has(id))) : null\n if (allowed && !allowed.size) throw new HttpError(400, 'Die Auswahl enthält keine Blöcke des Dokuments')\n const context = { lines, allowed, targets: new Set(targets) }\n\n return eventStream(request, async (emit, signal) => {\n try {\n let conversation: ModelMessage[] = documentChatMessages(messages, document, allowed && [...allowed], { kinds, blocks, targets, structure })\n let answered = false\n let result: { valid: ModelAgentOp[], problems: AgentOpProblem[] } | null = null\n // Problems of attempts that were retried: an operation the corrected list leaves out counts as dropped too.\n const retried: AgentOpProblem[] = []\n let retries = 0\n for (let attempt = 0; attempt <= AGENT_LIMITS.retries; attempt++) {\n const calls: { toolCallId: string, input: unknown, invalid: boolean }[] = []\n let text = ''\n for await (const part of ctx.ai.streamChatTurn({\n model: languageModel,\n messages: conversation,\n tools: agentTools,\n // Retries only correct the operations: no second answer text.\n toolChoice: attempt ? { type: 'tool', toolName: AGENT_TOOL } : 'auto',\n signal,\n })) {\n if (part.type === 'tool-call') {\n if (part.toolName === AGENT_TOOL) calls.push(part)\n }\n else if (!attempt) {\n text += part.text\n answered = true\n emit('delta', JSON.stringify({ text: part.text }))\n }\n }\n if (!calls.length) break\n\n const unreadable = calls.some(call => call.invalid || !Array.isArray((call.input as { ops?: unknown } | null)?.ops))\n const raw = calls.flatMap(call => (call.input as { ops?: unknown } | null)?.ops).filter(op => op !== undefined)\n const checked = checkAgentOps(raw, context)\n if (unreadable) checked.problems.unshift(agentUnreadableProblem())\n result = checked\n if (!checked.problems.length || attempt === AGENT_LIMITS.retries) break\n\n retried.push(...checked.problems)\n retries++\n const retry = agentRetryMessage(checked.problems, !!allowed)\n conversation = [\n ...conversation,\n {\n role: 'assistant',\n content: [\n ...(text ? [{ type: 'text' as const, text }] : []),\n ...calls.map(call => ({ type: 'tool-call' as const, toolCallId: call.toolCallId, toolName: AGENT_TOOL, input: call.input ?? {} })),\n ],\n },\n { role: 'tool', content: calls.map(call => ({ type: 'tool-result' as const, toolCallId: call.toolCallId, toolName: AGENT_TOOL, output: { type: 'text' as const, value: retry } })) },\n ]\n }\n\n if (result) {\n const ops: AgentOp[] = result.valid.map((op, i) => ({ ...op, op: `op${nextOp + i}` }))\n const targets = new Set(ops.map(op => agentOpTarget(op)))\n const dropped = [...result.problems]\n for (const problem of retried) {\n if (problem.id !== null && !targets.has(problem.id) && !dropped.some(d => d.id === problem.id && d.type === problem.type)) dropped.push(problem)\n }\n if (!answered && !ops.length) {\n emit('delta', JSON.stringify({ text: 'Ich konnte keine gültigen Änderungen vorschlagen.' }))\n }\n emit('ops', JSON.stringify({ ops, dropped, retries }))\n }\n emit('done', '{}')\n \n }\n catch (error) {\n if (!signal.aborted) {\n const { statusMessage, message } = error as { statusMessage?: string, message?: string }\n emit('error', JSON.stringify({ message: statusMessage ?? message ?? 'Unbekannter Fehler' }))\n }\n }\n })\n}\n\n/** Body schema of `completePost`. */\nexport const completeBodySchema = z.object({ before: z.string().max(AI_COMPLETE_LIMITS.before), model: modelRefSchema })\n\n/** Short inline continuation (ghost text) of the text before the caret. Plain JSON: `{ text }`, empty when nothing fits. */\nexport async function completePost(ctx: AiHandlerContext, request: HandlerRequest): Promise<HandlerResponse> {\n const user = await enforceRateLimit(ctx, request)\n const { before, model } = await readBody(request, completeBodySchema)\n const languageModel = await ctx.ai.resolveModel(model, user)\n if (!before.trim()) return json({ text: '' })\n\n // Typing on the client aborts its fetch; stop the upstream request with it (breaking the loop cancels it as well).\n const abort = new AbortController()\n request.signal?.addEventListener('abort', () => abort.abort(), { once: true })\n let raw = ''\n try {\n for await (const text of ctx.ai.streamChat({ model: languageModel, messages: completeMessages(before), signal: abort.signal })) {\n raw += text\n if (completionReady(raw)) break\n }\n }\n catch (error) {\n if (abort.signal.aborted) return json({ text: '' })\n throw error\n }\n return json({ text: cleanCompletion(raw, before) })\n}\n\n/** Body schema of `generatePost`. */\nexport const generateBodySchema = z.object({\n task: z.enum(AI_TASK_IDS),\n prompt: z.string().max(AI_LIMITS.prompt).default(''),\n selection: z.string().max(AI_LIMITS.selection).default(''),\n // Whole-document tasks may send the longer transcript; the stricter limit for the others is checked per task below.\n context: z.string().max(AI_LIMITS.transcript).default(''),\n language: z.enum(AI_LANGUAGES).optional(),\n blocks: z.array(aiBlockSchema).max(50).default([]),\n block: aiBlockInputSchema.optional(),\n model: modelRefSchema,\n})\n\n/** Streams one text task (rewrite, translate, generate, …) as SSE: `delta` (`{ text }`), `done`, `error` (`{ message }`). */\nexport async function generatePost(ctx: AiHandlerContext, request: HandlerRequest): Promise<HandlerResponse> {\n const user = await enforceRateLimit(ctx, request)\n const { task: taskId, model, ...input } = await readBody(request, generateBodySchema)\n // Unknown preset or missing key must fail as a plain JSON error before the SSE response starts.\n const languageModel = await ctx.ai.resolveModel(model, user)\n const task = AI_TASKS[taskId]\n if (task.needsSelection && !input.selection.trim()) throw new HttpError(400, 'Keine Textauswahl')\n if (task.needsPrompt && !input.prompt.trim()) throw new HttpError(400, 'Prompt fehlt')\n if (task.needsLanguage && !input.language) throw new HttpError(400, 'Zielsprache fehlt')\n if (task.needsBlock && !input.block) throw new HttpError(400, 'Baustein fehlt')\n if (task.wholeDocument && !input.context.trim()) throw new HttpError(400, 'Dokument ist leer')\n if (!task.wholeDocument && input.context.length > AI_LIMITS.context) throw new HttpError(400, 'Kontext zu lang')\n const users = task.needsUsers ? (await port(ctx, 'directory').listUsers()).map(u => u.username) : undefined\n\n return eventStream(request, async (emit, signal) => {\n try {\n for await (const text of ctx.ai.streamChat({ model: languageModel, messages: task.messages({ ...input, users }), signal })) {\n emit('delta', JSON.stringify({ text }))\n }\n emit('done', '{}')\n }\n catch (error) {\n if (!signal.aborted) {\n const { statusMessage, message } = error as { statusMessage?: string, message?: string }\n emit('error', JSON.stringify({ message: statusMessage ?? message ?? 'Unbekannter Fehler' }))\n }\n }\n })\n}\n\n/**\n * The models the client may choose from (without keys) — the `models` port's `listModels` for the user, else the\n * server-defined presets — and the default preset id per model slot, for the client's model profiles. Requires an\n * authenticated user through the `identity` port; not rate limited, since it calls no model.\n */\nexport async function modelsGet(ctx: AiHandlerContext, request: HandlerRequest): Promise<HandlerResponse> {\n const user = await authenticated(ctx, request)\n return json({ presets: await ctx.ai.listModels(user), slots: ctx.ai.slots })\n}\n\nconst sha256Hex = (data: string | Uint8Array): string => createHash('sha256').update(data).digest('hex')\n\n/** PDF pages read by `pdf-text` and `ocr` unless the request caps them lower. */\nconst DEFAULT_MAX_PAGES = 200\n\n/** Body schema of `extractPost`. */\nexport const extractBodySchema = z.object({\n documentId: z.string().regex(ASSET_DOCUMENT_ID),\n name: z.string().regex(ASSET_NAME).optional(),\n method: z.enum(['auto', 'transcription', 'pdf-text', 'ocr', 'description', 'bibliography']),\n model: modelRefSchema,\n bibliography: z.boolean().optional(),\n doi: z.string().regex(DOI_PATTERN).optional(),\n maxPages: z.number().int().min(1).max(500).default(DEFAULT_MAX_PAGES),\n}).refine(body => body.name !== undefined || (body.method === 'bibliography' && body.doi !== undefined), {\n message: 'name fehlt (nur eine Literaturabfrage mit DOI braucht keine Datei)',\n path: ['name'],\n}) satisfies z.ZodType<ExtractRequest>\n\n/** The method an `ExtractRequest` runs for an asset of `mimeType`: `auto` picks by type; 415 when the type does not fit the method. */\nfunction extractMethodOf(requested: ExtractMethod, mimeType: string, hasDoi: boolean): ExtractResponse['method'] {\n const audioVideo = /^(?:audio|video)\\//.test(mimeType)\n const image = mimeType.startsWith('image/')\n const pdf = mimeType === 'application/pdf'\n const fits: Record<ExtractMethod, boolean> = {\n 'auto': audioVideo || image || pdf,\n 'transcription': audioVideo,\n 'description': image,\n 'pdf-text': pdf,\n 'ocr': pdf,\n // A given DOI needs no document text, so any asset can stand for the work.\n 'bibliography': pdf || hasDoi,\n }\n if (!fits[requested]) throw new HttpError(415, `Dateityp ${mimeType} wird für diese Auswertung nicht unterstützt`)\n if (requested !== 'auto') return requested\n if (audioVideo) return 'transcription'\n return image ? 'description' : 'pdf-text'\n}\n\n/**\n * Turns a stored asset into readable content (`ExtractRequest` → `ExtractResponse`): audio/video into a transcription\n * with segment timestamps, a PDF into its text per page (OCR through the model when it has no text layer), an image\n * into a description, bibliographic data from Crossref or the model. The raw Markdown is stored as an asset of the same\n * document (`output`), with the SHA-256 of input and output. Errors: 404 unknown asset, 415 type not supported, 413\n * oversize audio, 422 unreadable PDF, 501 transcription or OCR unavailable, 502 upstream failure.\n */\nexport async function extractPost(ctx: AiHandlerContext, request: HandlerRequest): Promise<HandlerResponse> {\n const user = await enforceRateLimit(ctx, request)\n const body = await readBody(request, extractBodySchema)\n if (body.name === undefined) {\n // Only a DOI lookup comes without a file (the schema allows nothing else): no asset, no output, empty hashes.\n const { bib, warnings } = await extractBibliography({ doi: body.doi, text: '', resolveModel: () => ctx.ai.resolveModel(body.model, user), signal: request.signal })\n return json({ method: 'bibliography', markdown: '', bib, inputHash: '', outputHash: sha256Hex(''), output: null, warnings } satisfies ExtractResponse)\n }\n const assets = port(ctx, 'assets')\n const asset = await assets.loadAsset(body.documentId, body.name)\n if (!asset) throw new HttpError(404, 'Datei nicht gefunden')\n const inputHash = sha256Hex(asset.data)\n let method = extractMethodOf(body.method, asset.record.mimeType, body.doi !== undefined)\n // Closing the page aborts the client's fetch; the upstream requests stop with it.\n const { signal } = request\n const resolveModel = () => ctx.ai.resolveModel(body.model, user)\n\n const warnings: string[] = []\n let markdown = ''\n let model: string | undefined\n let segments: TranscriptionResult['segments']\n let bib: ExtractResponse['bib']\n\n if (method === 'transcription') {\n const config = ctx.ai.transcription\n if (!config?.endpoint || !config.model) throw new HttpError(501, 'Transkription nicht konfiguriert')\n if (asset.data.byteLength > MAX_TRANSCRIBE_BYTES) throw new HttpError(413, `Datei ist größer als ${MAX_TRANSCRIBE_BYTES / 1024 / 1024} MB`)\n ;({ markdown, segments } = await transcribe(config, asset, signal))\n model = config.model\n }\n else if (method === 'description') {\n markdown = await describeImage(await resolveModel(), asset.data, asset.record.mimeType, signal)\n }\n else if (asset.record.mimeType !== 'application/pdf') {\n // `bibliography` of a non-PDF asset: only reachable with a DOI.\n const found = await extractBibliography({ doi: body.doi, text: '', resolveModel, signal })\n bib = found.bib\n warnings.push(...found.warnings)\n }\n else {\n const pdf = await openPdf(asset.data)\n try {\n if (method === 'bibliography') {\n const pages = await pdfPagesText(pdf, Math.min(pdf.pageCount, BIBLIOGRAPHY_PAGES), signal)\n const found = await extractBibliography({ doi: body.doi, text: bibliographyText(pages), resolveModel, signal })\n bib = found.bib\n warnings.push(...found.warnings)\n }\n else {\n const count = Math.min(pdf.pageCount, body.maxPages)\n if (pdf.pageCount > count) warnings.push('pages-truncated')\n let pages = await pdfPagesText(pdf, count, signal)\n if (body.method === 'auto' && isScanned(pages)) method = 'ocr'\n if (method === 'ocr') {\n // One vision call per page: the OCR reads fewer pages than the text layer would.\n const ocrCount = Math.min(count, MAX_OCR_PAGES)\n try {\n pages = await ocrPages(pdf, ocrCount, resolveModel, signal)\n if (ocrCount < count) warnings.push('ocr-truncated')\n }\n catch (error) {\n if (!(error instanceof RasterError)) throw error\n // Pages cannot be rendered here: keep what the text layer has.\n if (!pages.some(Boolean)) throw new HttpError(501, 'OCR nicht verfügbar')\n warnings.push('ocr-unavailable')\n method = 'pdf-text'\n }\n }\n markdown = joinPages(pages)\n if (body.bibliography) {\n try {\n const found = await extractBibliography({ doi: body.doi, text: bibliographyText(pages), resolveModel, signal })\n bib = found.bib\n warnings.push(...found.warnings)\n }\n catch (error) {\n // The text is the result; missing bibliographic data must not lose it.\n if (signal?.aborted) throw error\n warnings.push('bibliography-failed')\n }\n }\n }\n }\n finally {\n await pdf.close()\n }\n }\n\n const outputHash = sha256Hex(markdown)\n let output: string | null = null\n if (method !== 'bibliography' && markdown) {\n const data = Buffer.from(markdown, 'utf8')\n output = `${randomUUID()}.md`\n await assets.storeAsset(body.documentId, {\n name: output,\n filename: `${asset.record.filename}.${method}.md`.slice(0, 255),\n mimeType: 'text/markdown',\n size: data.byteLength,\n createdAt: new Date().toISOString(),\n sha256: outputHash,\n data,\n })\n }\n return json({ method, markdown, ...(segments && { segments }), ...(bib && { bib }), ...(model && { model }), inputHash, outputHash, output, warnings } satisfies ExtractResponse)\n}\n\n/** Body schema of `transcriptCleanupPost`. */\nexport const transcriptCleanupBodySchema = transcriptCleanupRequestSchema\n\n/**\n * Cleans transcript segments (filler words, punctuation, casing, fragmented sentences) without touching speakers or\n * order: the model only returns a new text per segment ID. Plain JSON: `{ cleaned, failed }`.\n */\nexport async function transcriptCleanupPost(ctx: AiHandlerContext, request: HandlerRequest): Promise<HandlerResponse> {\n const user = await enforceRateLimit(ctx, request)\n const { segments, model } = await readBody(request, transcriptCleanupBodySchema)\n const languageModel = await ctx.ai.resolveModel(model, user)\n\n // Closing the menu or the page aborts the client's fetch; stop the upstream requests with it.\n const abort = new AbortController()\n request.signal?.addEventListener('abort', () => abort.abort(), { once: true })\n\n // Long transcripts are cleaned in chunks, a few model calls at a time; a chunk with an unusable answer stays as it is.\n const cleanChunk = async (chunk: TranscriptCleanupSegment[]) => {\n let raw = ''\n for await (const text of ctx.ai.streamChat({ model: languageModel, messages: transcriptCleanupMessages(chunk), signal: abort.signal })) raw += text\n try {\n return parseTranscriptCleanup(raw, chunk)\n }\n catch {\n return null\n }\n }\n const chunks = transcriptCleanupChunks(segments)\n const results: (TranscriptCleanupResult[] | null)[] = []\n let next = 0\n const worker = async () => {\n while (next < chunks.length) {\n const index = next++\n results[index] = await cleanChunk(chunks[index]!)\n }\n }\n try {\n await Promise.all(Array.from({ length: Math.min(TRANSCRIPT_CLEANUP_LIMITS.concurrency, chunks.length) }, worker))\n }\n finally {\n // An upstream error in one chunk fails the request: stop the other chunks' model calls too.\n abort.abort()\n }\n\n if (results.every(r => r === null)) throw new HttpError(502, 'KI-Antwort konnte nicht gelesen werden')\n return json({\n cleaned: results.flatMap(r => r ?? []),\n failed: chunks.reduce((sum, chunk, i) => sum + (results[i] === null ? chunk.length : 0), 0),\n } satisfies TranscriptCleanupResponse)\n}\n"],"mappings":"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"}