@carlonicora/nextjs-jsonapi 3.25.0 → 3.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/dist/{BlockNoteEditor-HKS6AXDA.js → BlockNoteEditor-4EIKMNY3.js} +20 -10
  2. package/dist/BlockNoteEditor-4EIKMNY3.js.map +1 -0
  3. package/dist/{BlockNoteEditor-46O3JC4Z.mjs → BlockNoteEditor-AXYVDDXM.mjs} +13 -3
  4. package/dist/{BlockNoteEditor-46O3JC4Z.mjs.map → BlockNoteEditor-AXYVDDXM.mjs.map} +1 -1
  5. package/dist/billing/index.js +335 -335
  6. package/dist/billing/index.mjs +1 -1
  7. package/dist/{chunk-Y3OKEQGL.js → chunk-ABVTYQIJ.js} +578 -363
  8. package/dist/chunk-ABVTYQIJ.js.map +1 -0
  9. package/dist/{chunk-KLV6RLDE.js → chunk-GKDHYB2R.js} +2 -2
  10. package/dist/chunk-GKDHYB2R.js.map +1 -0
  11. package/dist/{chunk-XDYILJ2S.mjs → chunk-QWZAGQWJ.mjs} +546 -331
  12. package/dist/chunk-QWZAGQWJ.mjs.map +1 -0
  13. package/dist/{chunk-4Z2UONN2.mjs → chunk-U65JPKO3.mjs} +2 -2
  14. package/dist/chunk-U65JPKO3.mjs.map +1 -0
  15. package/dist/client/index.js +2 -2
  16. package/dist/client/index.mjs +1 -1
  17. package/dist/components/index.d.mts +16 -3
  18. package/dist/components/index.d.ts +16 -3
  19. package/dist/components/index.js +4 -2
  20. package/dist/components/index.js.map +1 -1
  21. package/dist/components/index.mjs +3 -1
  22. package/dist/contexts/index.js +2 -2
  23. package/dist/contexts/index.mjs +1 -1
  24. package/dist/features/analytics/index.js +100 -100
  25. package/dist/features/analytics/index.js.map +1 -1
  26. package/dist/features/analytics/index.mjs +5 -5
  27. package/dist/features/analytics/index.mjs.map +1 -1
  28. package/dist/features/help/index.js +36 -36
  29. package/dist/features/help/index.js.map +1 -1
  30. package/dist/features/help/index.mjs +6 -6
  31. package/dist/features/help/index.mjs.map +1 -1
  32. package/dist/features/tokenusage/index.js +103 -103
  33. package/dist/features/tokenusage/index.js.map +1 -1
  34. package/dist/features/tokenusage/index.mjs +4 -4
  35. package/dist/features/tokenusage/index.mjs.map +1 -1
  36. package/package.json +1 -1
  37. package/src/components/containers/PageContainer.tsx +1 -1
  38. package/src/components/containers/RoundPageContainer.tsx +60 -16
  39. package/src/components/containers/RoundPageContainerTitle.tsx +75 -35
  40. package/src/components/editors/BlockNoteEditor.tsx +13 -1
  41. package/src/components/forms/EntityMultiSelector.tsx +4 -4
  42. package/src/components/forms/EntitySelector.tsx +4 -4
  43. package/src/components/forms/FormBlockNote.tsx +21 -1
  44. package/src/components/forms/FormCheckbox.tsx +6 -2
  45. package/src/components/forms/__tests__/useEditorDialog.test.ts +47 -0
  46. package/src/components/forms/useEditorDialog.ts +22 -0
  47. package/src/components/navigations/Breadcrumb.tsx +45 -16
  48. package/src/components/navigations/Header.tsx +16 -3
  49. package/src/components/navigations/MobileNavigationBar.tsx +12 -0
  50. package/src/components/navigations/__tests__/MobileNavigationBar.spec.tsx +17 -0
  51. package/src/components/tables/ContentListTable.tsx +52 -32
  52. package/src/features/analytics/components/AnalyticsAdminFilterBar.tsx +9 -6
  53. package/src/features/analytics/components/AnalyticsBreakdownTable.tsx +2 -1
  54. package/src/features/assistant/components/parts/AssistantBlockNoteComposer.tsx +1 -1
  55. package/src/features/assistant/components/parts/AssistantComposer.tsx +1 -1
  56. package/src/features/assistant-message/components/MessageItem.tsx +2 -2
  57. package/src/features/assistant-message/components/parts/tabs/CitationsTab.tsx +23 -5
  58. package/src/features/auth/components/containers/AuthContainer.tsx +8 -1
  59. package/src/features/company/components/details/CompanyContent.tsx +3 -1
  60. package/src/features/company/components/details/CompanyDetails.tsx +9 -1
  61. package/src/features/company/components/details/TokenStatusIndicator.tsx +42 -13
  62. package/src/features/help/components/HelpHeader.tsx +7 -5
  63. package/src/features/tokenusage/components/TokenUsageAdminFilterBar.tsx +9 -6
  64. package/src/features/tokenusage/lib/__tests__/formatters.spec.ts +21 -0
  65. package/src/features/tokenusage/lib/formatters.ts +5 -1
  66. package/src/shadcnui/ui/button.tsx +1 -1
  67. package/src/shadcnui/ui/checkbox.tsx +1 -1
  68. package/src/shadcnui/ui/command.tsx +4 -4
  69. package/src/shadcnui/ui/dialog.tsx +2 -0
  70. package/src/shadcnui/ui/input.tsx +1 -1
  71. package/src/shadcnui/ui/popover.tsx +9 -0
  72. package/src/shadcnui/ui/select.tsx +1 -1
  73. package/src/shadcnui/ui/sheet.tsx +8 -1
  74. package/src/shadcnui/ui/table.tsx +2 -2
  75. package/src/shadcnui/ui/textarea.tsx +1 -1
  76. package/dist/BlockNoteEditor-HKS6AXDA.js.map +0 -1
  77. package/dist/chunk-4Z2UONN2.mjs.map +0 -1
  78. package/dist/chunk-KLV6RLDE.js.map +0 -1
  79. package/dist/chunk-XDYILJ2S.mjs.map +0 -1
  80. package/dist/chunk-Y3OKEQGL.js.map +0 -1
@@ -23,6 +23,10 @@ function PopoverContent({
23
23
  ...props
24
24
  }: PopoverPrimitive.Popup.Props &
25
25
  Pick<PopoverPrimitive.Positioner.Props, "align" | "alignOffset" | "side" | "sideOffset" | "anchor">) {
26
+ // Pickers size themselves to their field (`w-(--anchor-width)`). In a narrow
27
+ // field on a phone that leaves a ~100px list where every name wraps, so on a
28
+ // phone they get a usable minimum width (capped to the screen).
29
+ const matchesAnchorWidth = typeof className === "string" && className.includes("--anchor-width");
26
30
  return (
27
31
  <PopoverPrimitive.Portal>
28
32
  <PopoverPrimitive.Positioner
@@ -37,6 +41,11 @@ function PopoverContent({
37
41
  data-slot="popover-content"
38
42
  className={cn(
39
43
  "bg-popover text-popover-foreground data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/10 flex flex-col gap-4 rounded-lg p-2.5 text-xs shadow-md ring-1 duration-100 z-50 w-72 origin-(--transform-origin) outline-hidden",
44
+ // Phone: never taller than the space the positioner reports (the
45
+ // keyboard shrinks it), so a picker list shrinks and scrolls instead
46
+ // of running under the keyboard.
47
+ "max-md:max-h-(--available-height) max-md:overflow-y-auto",
48
+ matchesAnchorWidth && "max-md:min-w-[min(20rem,calc(100vw-2rem))]",
40
49
  className,
41
50
  )}
42
51
  {...props}
@@ -31,7 +31,7 @@ function SelectTrigger({
31
31
  data-slot="select-trigger"
32
32
  data-size={size}
33
33
  className={cn(
34
- "border-input data-[placeholder]:text-muted-foreground bg-input/20 dark:bg-input/30 dark:hover:bg-input/50 focus-visible:border-ring focus-visible:ring-ring/30 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 gap-1.5 rounded-md border px-2 py-1.5 text-xs/relaxed transition-colors focus-visible:ring-[2px] aria-invalid:ring-[2px] data-[size=default]:h-7 data-[size=sm]:h-6 *:data-[slot=select-value]:flex *:data-[slot=select-value]:gap-1.5 [&_svg:not([class*='size-'])]:size-3.5 flex w-fit items-center justify-between whitespace-nowrap outline-none disabled:cursor-not-allowed disabled:opacity-50 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center [&_svg]:pointer-events-none [&_svg]:shrink-0",
34
+ "border-input data-[placeholder]:text-muted-foreground bg-input/20 dark:bg-input/30 dark:hover:bg-input/50 focus-visible:border-ring focus-visible:ring-ring/30 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 gap-1.5 rounded-md border px-2 py-1.5 text-xs/relaxed transition-colors focus-visible:ring-[2px] aria-invalid:ring-[2px] data-[size=default]:h-7 data-[size=sm]:h-6 max-md:data-[size=default]:h-10 max-md:text-base *:data-[slot=select-value]:flex *:data-[slot=select-value]:gap-1.5 [&_svg:not([class*='size-'])]:size-3.5 flex w-fit items-center justify-between whitespace-nowrap outline-none disabled:cursor-not-allowed disabled:opacity-50 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center [&_svg]:pointer-events-none [&_svg]:shrink-0",
35
35
  className,
36
36
  )}
37
37
  {...props}
@@ -75,7 +75,14 @@ function SheetContent({
75
75
  {showCloseButton && (
76
76
  <SheetPrimitive.Close
77
77
  data-slot="sheet-close"
78
- render={<Button variant="ghost" className="absolute top-4 end-4" size="icon-sm" />}
78
+ render={
79
+ // Phone: a 40px tap target, moved by 8px so its centre stays where the 24px one sits.
80
+ <Button
81
+ variant="ghost"
82
+ className="absolute top-4 end-4 max-md:top-2 max-md:end-2 max-md:size-10"
83
+ size="icon-sm"
84
+ />
85
+ }
79
86
  >
80
87
  <XIcon />
81
88
  <span className="sr-only">Close</span>
@@ -6,7 +6,7 @@ import { cn } from "@/lib/utils";
6
6
 
7
7
  function Table({ className, ...props }: React.ComponentProps<"table">) {
8
8
  return (
9
- <div data-slot="table-container" className="relative w-full overflow-x-clip">
9
+ <div data-slot="table-container" className="relative w-full overflow-x-clip max-md:overflow-x-auto">
10
10
  <table data-slot="table" className={cn("w-full caption-bottom text-xs", className)} {...props} />
11
11
  </div>
12
12
  );
@@ -57,7 +57,7 @@ function TableCell({ className, ...props }: React.ComponentProps<"td">) {
57
57
  return (
58
58
  <td
59
59
  data-slot="table-cell"
60
- className={cn("p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pe-0", className)}
60
+ className={cn("p-2 align-middle whitespace-nowrap max-md:whitespace-normal [&:has([role=checkbox])]:pe-0", className)}
61
61
  {...props}
62
62
  />
63
63
  );
@@ -7,7 +7,7 @@ function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
7
7
  <textarea
8
8
  data-slot="textarea"
9
9
  className={cn(
10
- "border-input bg-input/20 dark:bg-input/30 focus-visible:border-ring focus-visible:ring-ring/30 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 resize-none rounded-md border px-2 py-2 text-sm transition-colors focus-visible:ring-[2px] aria-invalid:ring-[2px] md:text-xs/relaxed placeholder:text-muted-foreground flex field-sizing-content min-h-16 w-full outline-none disabled:cursor-not-allowed disabled:opacity-50",
10
+ "border-input bg-input/20 dark:bg-input/30 focus-visible:border-ring focus-visible:ring-ring/30 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 resize-none rounded-md border px-2 py-2 text-base transition-colors focus-visible:ring-[2px] aria-invalid:ring-[2px] md:text-xs/relaxed placeholder:text-muted-foreground flex field-sizing-content min-h-16 w-full outline-none disabled:cursor-not-allowed disabled:opacity-50",
11
11
  className,
12
12
  )}
13
13
  {...props}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["/home/runner/work/nextjs-jsonapi/nextjs-jsonapi/dist/BlockNoteEditor-HKS6AXDA.js","../src/components/editors/BlockNoteEditor.tsx","../src/components/editors/BlockNoteEditorAiActions.tsx","../src/components/editors/BlockNoteEditorFormattingToolbar.tsx"],"names":["jsx","jsxs","SparklesIcon","type"],"mappings":"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","file":"/home/runner/work/nextjs-jsonapi/nextjs-jsonapi/dist/BlockNoteEditor-HKS6AXDA.js","sourcesContent":[null,"\"use client\";\n\nimport { BlockNoteSchema, defaultInlineContentSpecs, filterSuggestionItems, PartialBlock } from \"@blocknote/core\";\nimport { en as coreEn } from \"@blocknote/core/locales\";\nimport {\n createReactInlineContentSpec,\n DefaultReactSuggestionItem,\n getDefaultReactSlashMenuItems,\n SuggestionMenuController,\n SuggestionMenuProps,\n useBlockNoteEditor,\n useCreateBlockNote,\n useExtension,\n useExtensionState,\n} from \"@blocknote/react\";\nimport { BlockNoteView } from \"@blocknote/shadcn\";\nimport \"@blocknote/shadcn/style.css\";\nimport {\n AIExtension,\n AIMenuController,\n getAISlashMenuItems,\n getDefaultAIMenuItems,\n PromptSuggestionMenu,\n useAIDictionary,\n} from \"@blocknote/xl-ai\";\nimport { en as aiEn } from \"@blocknote/xl-ai/locales\";\nimport \"@blocknote/xl-ai/style.css\";\nimport { DefaultChatTransport } from \"ai\";\nimport { CheckIcon, LanguagesIcon, SparklesIcon, TypeIcon, WandSparklesIcon, XIcon } from \"lucide-react\";\nimport { useTranslations } from \"next-intl\";\nimport { useTheme } from \"next-themes\";\nimport React, { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport { getPublicApiUrl } from \"../../client/config\";\nimport { getClientToken } from \"../../client/token\";\nimport { useCurrentUserContextOptional } from \"../../contexts\";\nimport { S3Interface } from \"../../features/s3/data\";\nimport { S3Service } from \"../../features/s3/data/s3.service\";\nimport { UserInterface } from \"../../features/user/data\";\nimport { useI18nLocale } from \"../../i18n/config\";\nimport { Button } from \"../../shadcnui\";\nimport { BlockNoteDiffUtil, BlockNoteWordDiffRendererUtil, cn } from \"../../utils\";\nimport { errorToast } from \"../errors\";\nimport { BlockNoteAiGenerateAction, generateActionItems } from \"./BlockNoteEditorAiActions\";\nimport { BlockNoteEditorFormattingToolbar } from \"./BlockNoteEditorFormattingToolbar\";\nimport { BlockNoteEditorMentionHoverCard } from \"./BlockNoteEditorMentionHoverCard\";\nimport {\n createMentionInlineContentSpec,\n type MentionNameResolver,\n type MentionResolveFn,\n} from \"./BlockNoteEditorMentionInlineContent\";\nimport { BlockNoteEditorMentionSuggestionMenu } from \"./BlockNoteEditorSuggestionMenuController\";\n\n/**\n * Lets app code trigger a whole-document AI action from OUTSIDE the editor\n * (a visible button next to the field label, for instance) without owning the\n * BlockNote instance. Delivered through a plain prop, not a React `ref`:\n * `BlockNoteEditorContainer` is a `next/dynamic` import wrapped in\n * `React.memo`, and refs do not pass through that reliably.\n */\nexport type BlockNoteAiHandle = { invokeAi: (type: BlockNoteAiGenerateAction) => void };\n\nexport type BlockNoteAiConfig = {\n endpoint: string;\n entityType: string;\n entityId?: string;\n /** Whole-document actions shown when nothing is selected. Default: [\"fill-template\"]. */\n actions?: BlockNoteAiGenerateAction[];\n};\n\nexport type BlockNoteEditorProps = {\n id: string;\n type: string;\n initialContent?: PartialBlock[];\n onChange?: (content: any, isEmpty: boolean, hasUnresolvedDiff: boolean) => void;\n size?: \"sm\" | \"md\";\n className?: string;\n markdownContent?: string;\n diffContent?: PartialBlock[];\n placeholder?: string;\n bordered?: boolean;\n inlineContentSpecs?: Record<string, any>;\n renderOverlays?: (editor: any) => React.ReactNode;\n enableMentions?: boolean;\n disableMentions?: boolean;\n mentionSearchFn?: (\n query: string,\n params?: Record<string, string>,\n ) => Promise<import(\"./BlockNoteEditorSuggestionMenuController\").MentionItem[]>;\n mentionSearchParams?: Record<string, string>;\n mentionResolveFn?: MentionResolveFn;\n suggestionMenuComponent?: React.FC<SuggestionMenuProps<DefaultReactSuggestionItem>>;\n mentionNameResolver?: MentionNameResolver;\n onWarmMentions?: (blocks: PartialBlock[]) => void;\n aiConfig?: BlockNoteAiConfig;\n // When the editor is inside a bounded flex column (parent gives it a real\n // height via flex-1+min-h-0), set this so `.bn-container` shrinks to that\n // height and scrolls internally. Without it the editor grows to fit its\n // content and pushes the surrounding form to scroll instead.\n stretch?: boolean;\n // Gates AI on top of `aiConfig` (credit availability, etc.). Defaults to\n // true so existing callers that only pass `aiConfig` are unaffected. When\n // false, no AI extension is created, the AI toolbar button and AI slash\n // items are hidden, and the editor falls back to the plain slash menu.\n aiEnabled?: boolean;\n // Filled with a {@link BlockNoteAiHandle} while the editor is mounted and AI\n // is on, so a control outside the editor can run a whole-document AI action.\n // Cleared on unmount and whenever AI is unavailable.\n aiHandleRef?: React.MutableRefObject<BlockNoteAiHandle | null>;\n};\n\nfunction isBlockEmpty(block: any): boolean {\n if (!block || typeof block !== \"object\") return true;\n if (block.type !== \"paragraph\") return false;\n if (Array.isArray(block.children) && block.children.length > 0 && !isDocumentEmpty(block.children)) {\n return false;\n }\n if (Array.isArray(block.content)) {\n for (const inline of block.content) {\n if (typeof inline === \"string\") {\n if (inline.trim()) return false;\n } else if (inline && typeof inline === \"object\") {\n if (inline.type !== \"text\") return false;\n if (typeof inline.text === \"string\" && inline.text.trim()) return false;\n }\n }\n } else if (typeof block.content === \"string\" && block.content.trim()) {\n return false;\n }\n return true;\n}\n\nfunction isDocumentEmpty(blocks: any[]): boolean {\n if (!Array.isArray(blocks) || blocks.length === 0) return true;\n return blocks.every(isBlockEmpty);\n}\n\nconst createDiffActionsInlineContentSpec = (\n handleAcceptChange: (diffId: string) => void,\n handleRejectChange: (diffId: string) => void,\n) => {\n return createReactInlineContentSpec(\n {\n type: \"diffActions\",\n propSchema: {\n diffIds: {\n default: \"\",\n },\n },\n content: \"none\",\n },\n {\n render: (props) => {\n const diffIds = props.inlineContent.props.diffIds;\n\n return (\n <span className=\"diff-actions-container mx-2 inline-flex items-center gap-1 align-middle\">\n <Button\n title=\"Accept change\"\n onClick={(e) => {\n e.preventDefault();\n e.stopPropagation();\n diffIds.split(\",\").forEach((id: string) => handleAcceptChange(id.trim()));\n }}\n >\n <CheckIcon className=\"text-success h-3 w-3\" />\n </Button>\n <Button\n title=\"Reject change\"\n className=\"mx-2 p-0\"\n onClick={(e) => {\n e.preventDefault();\n e.stopPropagation();\n diffIds.split(\",\").forEach((id: string) => handleRejectChange(id.trim()));\n }}\n >\n <XIcon className=\"text-destructive h-3 w-3\" />\n </Button>\n </span>\n );\n },\n },\n );\n};\n\n/**\n * Custom AI menu wrapper. Surfaces our backend-driven action items\n * (Improve Writing, Fix Spelling) above the free-form prompt input.\n *\n * Why a custom wrapper instead of `<AIMenu items={…}>` with the BlockNote\n * defaults: BlockNote's `PromptSuggestionMenu` hijacks Enter to pick the\n * highlighted item whenever `items.length > 0`. We gate items on\n * - status === \"user-input\"\n * - editor has a selection\n * - prompt input is empty\n * so the moment the user types, items disappear and Enter falls back to\n * free-form submission. Outside `user-input` (review / error states) we\n * keep BlockNote's default review buttons (accept/revert/retry/cancel).\n *\n * Each custom item passes a `type` discriminator on `chatRequestOptions.body`.\n * That field flows through `chat.sendMessage(msg, opts)` to the transport's\n * `prepareSendMessagesRequest({messages, body})` (see types.ts in\n * `@blocknote/xl-ai` — `ChatRequestOptions = Parameters<Chat[\"sendMessage\"]>[1]`).\n * The backend dispatcher reads `body.type` and routes to a per-type handler\n * with its own canonical prompt. NO prompt text lives in this file.\n */\nfunction NarrAIMenu({ actions }: { actions?: BlockNoteAiGenerateAction[] }) {\n const editor = useBlockNoteEditor();\n const ai = useExtension(AIExtension);\n const dict = useAIDictionary();\n const status = useExtensionState(AIExtension, {\n selector: (s) => (s.aiMenuState !== \"closed\" ? s.aiMenuState.status : \"closed\"),\n });\n const [prompt, setPrompt] = useState(\"\");\n // Set by items that pre-fill the input (Translate) so handleSubmit knows\n // which `type` to attach when the user submits the captured text. Cleared\n // after submit or on status change.\n const pendingTypeRef = useRef<string | null>(null);\n\n useEffect(() => {\n if (status === \"ai-writing\" || status === \"user-reviewing\" || status === \"error\" || status === \"closed\") {\n setPrompt(\"\");\n pendingTypeRef.current = null;\n }\n }, [status]);\n\n // Selection-edit ops operate per-block. If the user's selection only\n // covers part of a block (cursor mid-paragraph dragged to mid-next), the\n // rewrite would silently replace the WHOLE containing blocks — beyond\n // what the user highlighted. Expand the selection to whole-block bounds\n // BEFORE invokeAI so the user sees exactly what will be rewritten.\n const expandSelectionToBlocks = useCallback(() => {\n const sel = editor.getSelection?.();\n const blocks = (sel as any)?.blocks;\n if (!Array.isArray(blocks) || blocks.length === 0) return;\n const first = blocks[0];\n const last = blocks[blocks.length - 1];\n try {\n (editor as any).setSelection?.(first, last);\n } catch {\n // If BlockNote's setSelection signature changes, fail silently — the\n // backend still operates per-block, so we just lose the visual hint.\n }\n }, [editor]);\n\n const items = useMemo(() => {\n // Outside user-input (reviewing / error), use BlockNote's default\n // review buttons (accept/revert/retry/cancel). Need to wrap onItemClick\n // because the default items expect a setPrompt argument.\n if (status !== \"user-input\") {\n return getDefaultAIMenuItems(editor, status).map((item) => ({\n ...item,\n onItemClick: () => item.onItemClick(setPrompt),\n }));\n }\n const hasSelection = editor.getSelection() !== undefined;\n const hasTyped = prompt.trim().length > 0;\n // Once the user starts typing, hide all items so Enter submits the\n // free-form (or pending-type) prompt instead of being hijacked.\n if (hasTyped) return [];\n\n const invokeGenerate = (type: BlockNoteAiGenerateAction) => {\n void ai.invokeAI({\n userPrompt: type,\n useSelection: false,\n chatRequestOptions: { body: { type } },\n });\n };\n // Whole-document items are shown in BOTH contexts (with and without\n // selection) because they operate on the whole document — they ignore any\n // active selection. Listed first so the first one is the\n // default-highlighted item.\n const generateItems = generateActionItems(actions, invokeGenerate);\n\n if (hasSelection) {\n // Selection-edit items + the always-available whole-document items.\n // Each selection item invokes ai.invokeAI with chatRequestOptions\n // carrying the `type` body field. The userPrompt is a short tag — the\n // backend ignores it and uses the canonical prompt for the type instead.\n return [\n ...generateItems,\n {\n key: \"improve_writing\",\n title: \"Improve Writing\",\n aliases: [\"improve\", \"rewrite\", \"polish\"],\n icon: <SparklesIcon size={18} />,\n size: \"small\" as const,\n onItemClick: () => {\n expandSelectionToBlocks();\n void ai.invokeAI({\n userPrompt: \"improve-writing\",\n useSelection: true,\n chatRequestOptions: { body: { type: \"improve-writing\" } },\n });\n },\n },\n {\n key: \"fix_spelling\",\n title: \"Fix Spelling\",\n aliases: [\"spelling\", \"grammar\", \"typo\"],\n icon: <TypeIcon size={18} />,\n size: \"small\" as const,\n onItemClick: () => {\n expandSelectionToBlocks();\n void ai.invokeAI({\n userPrompt: \"fix-spelling\",\n useSelection: true,\n chatRequestOptions: { body: { type: \"fix-spelling\" } },\n });\n },\n },\n {\n key: \"translate\",\n title: \"Translate…\",\n aliases: [\"translate\", \"language\"],\n icon: <LanguagesIcon size={18} />,\n size: \"small\" as const,\n // Pre-fills the input with a placeholder. User appends/replaces\n // with the target language and submits via Enter. handleSubmit\n // reads pendingTypeRef and forwards `type: \"translate\"`. We\n // expand the selection now so the user sees the scope before\n // typing the language — handleSubmit doesn't re-expand.\n onItemClick: () => {\n expandSelectionToBlocks();\n pendingTypeRef.current = \"translate\";\n setPrompt(\"Translate to \");\n },\n },\n {\n key: \"simplify\",\n title: \"Simplify\",\n aliases: [\"simplify\", \"easier\", \"plain\"],\n icon: <WandSparklesIcon size={18} />,\n size: \"small\" as const,\n onItemClick: () => {\n expandSelectionToBlocks();\n void ai.invokeAI({\n userPrompt: \"simplify\",\n useSelection: true,\n chatRequestOptions: { body: { type: \"simplify\" } },\n });\n },\n },\n ];\n }\n\n // No selection (the /ai slash menu path): just the whole-document items.\n // Free-form typing still works — once the user types, items hide and\n // Enter submits with no type (backend defaults to fill-template).\n return generateItems;\n }, [editor, status, prompt, ai, expandSelectionToBlocks, actions]);\n\n const handleSubmit = useCallback(\n async (userPrompt: string) => {\n if (!userPrompt.trim()) return;\n const pendingType = pendingTypeRef.current;\n pendingTypeRef.current = null;\n const body = pendingType ? { type: pendingType } : undefined;\n await ai.invokeAI({\n userPrompt,\n useSelection: editor.getSelection() !== undefined,\n ...(body ? { chatRequestOptions: { body } } : {}),\n });\n },\n [ai, editor],\n );\n\n const placeholder =\n status === \"thinking\"\n ? dict.ai_menu.status.thinking\n : status === \"ai-writing\"\n ? dict.ai_menu.status.editing\n : status === \"error\"\n ? dict.ai_menu.status.error\n : dict.ai_menu.input_placeholder;\n\n const disabled = status === \"thinking\" || status === \"ai-writing\";\n\n return (\n <PromptSuggestionMenu\n items={items}\n onManualPromptSubmit={handleSubmit}\n promptText={prompt}\n onPromptTextChange={setPrompt}\n placeholder={placeholder}\n disabled={disabled}\n icon={\n <div className=\"bn-combobox-icon\">\n <SparklesIcon size={16} />\n </div>\n }\n />\n );\n}\n\nexport default function BlockNoteEditor({\n id,\n type,\n initialContent,\n onChange,\n size,\n className,\n markdownContent,\n diffContent,\n placeholder,\n bordered,\n inlineContentSpecs,\n renderOverlays,\n enableMentions,\n disableMentions,\n mentionSearchFn,\n mentionSearchParams,\n mentionResolveFn,\n suggestionMenuComponent,\n mentionNameResolver,\n onWarmMentions,\n aiConfig,\n stretch,\n aiEnabled = true,\n aiHandleRef,\n}: BlockNoteEditorProps): React.JSX.Element {\n const t = useTranslations();\n const locale = useI18nLocale();\n const { resolvedTheme } = useTheme();\n // Optional on purpose: a host app may mount its own user context instead of\n // this package's, and the editor already degrades without a company — image\n // upload reports it (`uploadImage`) and the AI transport simply omits the\n // company header. Throwing here would take the whole page down instead.\n const company = useCurrentUserContextOptional<UserInterface>()?.company ?? null;\n\n const [acceptedChanges, setAcceptedChanges] = useState<Set<string>>(new Set());\n const [rejectedChanges, setRejectedChanges] = useState<Set<string>>(new Set());\n\n const editorRef = useRef<HTMLDivElement>(null);\n\n // Ensure side menu buttons don't trigger form submission\n useEffect(() => {\n if (!editorRef.current) return;\n const setButtonTypes = () => {\n editorRef.current?.querySelectorAll(\".bn-side-menu button\").forEach((btn) => {\n if (!btn.getAttribute(\"type\")) {\n btn.setAttribute(\"type\", \"button\");\n }\n });\n };\n const observer = new MutationObserver(setButtonTypes);\n observer.observe(editorRef.current, { childList: true, subtree: true });\n return () => observer.disconnect();\n }, []);\n\n const handleAcceptChange = useCallback((diffId: string) => {\n setAcceptedChanges((prev) => new Set([...prev, diffId]));\n setRejectedChanges((prev) => {\n const newSet = new Set(prev);\n newSet.delete(diffId);\n return newSet;\n });\n }, []);\n\n const handleRejectChange = useCallback((diffId: string) => {\n setRejectedChanges((prev) => new Set([...prev, diffId]));\n setAcceptedChanges((prev) => {\n const newSet = new Set(prev);\n newSet.delete(diffId);\n return newSet;\n });\n }, []);\n\n const DiffActionsInlineContent = useMemo(\n () => createDiffActionsInlineContentSpec(handleAcceptChange, handleRejectChange),\n [handleAcceptChange, handleRejectChange],\n );\n\n const mentionSpec = useMemo(\n () => createMentionInlineContentSpec(mentionResolveFn, disableMentions, mentionNameResolver),\n [disableMentions, mentionResolveFn, mentionNameResolver],\n );\n\n const schema = useMemo(\n () =>\n BlockNoteSchema.create({\n inlineContentSpecs: {\n ...defaultInlineContentSpecs,\n diffActions: DiffActionsInlineContent,\n mention: mentionSpec,\n ...inlineContentSpecs,\n },\n } as any),\n [DiffActionsInlineContent, mentionSpec, inlineContentSpecs],\n );\n\n const docRef = useRef<{ getDoc: () => any[] }>({ getDoc: () => [] });\n // Selection getter used by the AI transport to attach `selectionBlocks` to\n // the outgoing request metadata. Populated in a useEffect once the editor\n // instance exists; reads the current BlockNote selection on every send.\n const selectionRef = useRef<{ getSelectedBlocks: () => any[] }>({ getSelectedBlocks: () => [] });\n\n const companyId = company?.id;\n const aiExtension = useMemo(() => {\n if (!aiConfig || !aiEnabled) return undefined;\n const base = getPublicApiUrl();\n const url = new URL(aiConfig.endpoint, base.endsWith(\"/\") ? base : base + \"/\").toString();\n return AIExtension({\n transport: new DefaultChatTransport({\n api: url,\n credentials: \"include\",\n headers: async () => {\n const headers: Record<string, string> = { \"x-language\": locale };\n const token = await getClientToken();\n if (token) headers[\"Authorization\"] = `Bearer ${token}`;\n if (companyId) headers[\"x-companyid\"] = companyId;\n return headers;\n },\n prepareSendMessagesRequest: ({ messages, body }: any) => {\n let lastUserIdx = -1;\n for (let i = messages.length - 1; i >= 0; i--) {\n if (messages[i]?.role === \"user\") {\n lastUserIdx = i;\n break;\n }\n }\n const selectedBlocks = selectionRef.current.getSelectedBlocks();\n const augmented = messages.map((m: any, i: number) =>\n i === lastUserIdx\n ? {\n ...m,\n metadata: {\n ...(m.metadata ?? {}),\n entityType: aiConfig.entityType,\n entityId: aiConfig.entityId,\n blocks: docRef.current.getDoc(),\n selectionBlocks: selectedBlocks,\n },\n }\n : m,\n );\n return { body: { ...(body ?? {}), messages: augmented } };\n },\n }),\n });\n }, [aiConfig, aiEnabled, companyId, locale]);\n\n const uploadImage = useCallback(\n async (file: File): Promise<string> => {\n if (!company) {\n errorToast({\n title: t(`common.errors.upload`),\n error: t(`common.errors.upload_description`),\n });\n throw new Error(t(`common.errors.upload`));\n }\n\n const fileType = file.type;\n const key = `companies/${company.id}/${type}/${id}/${file.name}`;\n\n const s3: S3Interface = await S3Service.getPreSignedUrl({\n key: key,\n contentType: fileType,\n isPublic: true,\n });\n\n await fetch(s3.url, {\n method: \"PUT\",\n headers: s3.headers,\n body: file,\n });\n\n const signedImage: S3Interface = await S3Service.getSignedUrl({\n key: key,\n isPublic: true,\n });\n\n return signedImage.url;\n },\n [company, id, t],\n );\n\n // Utility: Remove trailing empty blocks for read-only display\n const removeTrailingEmptyBlocks = useCallback(\n (blocks: PartialBlock[]): PartialBlock[] => {\n if (!blocks || blocks.length === 0) return blocks;\n\n // Only remove trailing empty blocks in read-only mode\n if (onChange !== undefined) return blocks;\n\n const result = [...blocks];\n\n // Remove trailing empty paragraph blocks, but keep at least one block\n while (result.length > 1) {\n const lastBlock = result[result.length - 1];\n\n // Check if it's an empty paragraph\n const isEmptyParagraph =\n lastBlock.type === \"paragraph\" &&\n (!lastBlock.content ||\n lastBlock.content.length === 0 ||\n (Array.isArray(lastBlock.content) && lastBlock.content.every((c: any) => !c.text || c.text.trim() === \"\")));\n\n if (isEmptyParagraph) {\n result.pop();\n } else {\n break;\n }\n }\n\n return result;\n },\n [onChange],\n );\n\n const processedContent = useMemo(() => {\n if (diffContent && initialContent) {\n try {\n const diffResult = BlockNoteDiffUtil.diff(initialContent, diffContent);\n const renderedDiff = BlockNoteWordDiffRendererUtil.renderWordDiffs(\n diffResult.blocks,\n handleAcceptChange,\n handleRejectChange,\n acceptedChanges,\n rejectedChanges,\n );\n return removeTrailingEmptyBlocks(renderedDiff);\n } catch (_error) {\n return initialContent && Array.isArray(initialContent) && initialContent.length > 0\n ? removeTrailingEmptyBlocks(initialContent)\n : [];\n }\n }\n\n if (!initialContent) {\n return [];\n }\n\n if (!Array.isArray(initialContent)) {\n return [];\n }\n\n return initialContent.length > 0 ? removeTrailingEmptyBlocks(initialContent) : [];\n }, [\n initialContent,\n diffContent,\n handleAcceptChange,\n handleRejectChange,\n acceptedChanges,\n rejectedChanges,\n removeTrailingEmptyBlocks,\n ]);\n\n const validatedInitialContent = useMemo(() => {\n if (processedContent && Array.isArray(processedContent) && processedContent.length > 0) {\n const validatedContent = processedContent.filter((block) => {\n if (!block || typeof block !== \"object\") return false;\n if (!(block as any).type) return false;\n return true;\n });\n return validatedContent.length > 0 ? (validatedContent as PartialBlock[]) : undefined;\n }\n return undefined;\n }, [processedContent]);\n\n const editor = useCreateBlockNote(\n useMemo(\n () => ({\n placeholders: {\n emptyDocument: placeholder || t(`common.blocknote.placeholder`),\n },\n schema,\n initialContent: validatedInitialContent,\n uploadFile: uploadImage,\n extensions: aiExtension ? [aiExtension] : undefined,\n dictionary: aiExtension ? { ...coreEn, ai: aiEn } : undefined,\n }),\n [placeholder, t, schema, validatedInitialContent, uploadImage, aiExtension],\n ),\n );\n\n // Publish the outside-the-editor AI trigger. `editor.getExtension(AIExtension)`\n // is BlockNote 0.54's documented lookup by extension factory (core's\n // `getExtension` resolves a function argument through `extensionFactories`),\n // so the instance is reachable without the `useExtension` hook that only\n // works inside the BlockNote context (NarrAIMenu). The menu MUST be opened\n // first: `invokeAI` routes every status update through `setAIResponseStatus`,\n // which is a no-op while `aiMenuState === \"closed\"` — without the open menu\n // the request runs but the accept / reject UI never appears.\n useEffect(() => {\n if (!aiHandleRef) return;\n if (!aiConfig || !aiEnabled) {\n aiHandleRef.current = null;\n return;\n }\n aiHandleRef.current = {\n invokeAi: (type: BlockNoteAiGenerateAction) => {\n const ai = editor.getExtension(AIExtension);\n if (!ai) return;\n editor.focus();\n const blocks = editor.document;\n const anchor = blocks?.[blocks.length - 1];\n if (anchor?.id) ai.openAIMenuAtBlock(anchor.id);\n void ai.invokeAI({\n userPrompt: type,\n useSelection: false,\n chatRequestOptions: { body: { type } },\n });\n },\n };\n return () => {\n aiHandleRef.current = null;\n };\n }, [aiHandleRef, aiConfig, aiEnabled, editor]);\n\n // Tracks the hash of the document the editor itself just emitted via onChange.\n // The sync effect below uses it to skip replaceBlocks when the parent's\n // initialContent is just an echo of our own emission (form-controlled flow).\n const lastEmittedHashRef = useRef<string | null>(null);\n\n const handleChange = useCallback(async () => {\n if (!onChange) return;\n const newBlocks = editor.document;\n lastEmittedHashRef.current = JSON.stringify(newBlocks);\n const isEmpty = isDocumentEmpty(newBlocks);\n\n function hasUnresolvedDiffsRecursive(block: any): boolean {\n if (!block || typeof block !== \"object\") return false;\n let diffId = undefined;\n if (block.props && block.props.diffId) diffId = block.props.diffId;\n if (!diffId && block.attrs && block.attrs.diffId) diffId = block.attrs.diffId;\n if (diffId && !acceptedChanges.has(diffId) && !rejectedChanges.has(diffId)) {\n return true;\n }\n\n if (block.content) {\n const contentArr = Array.isArray(block.content) ? block.content : [block.content];\n for (const inline of contentArr) {\n if (inline && typeof inline === \"object\") {\n if (inline.type === \"diffActions\" && inline.props && inline.props.diffIds) {\n const ids =\n typeof inline.props.diffIds === \"string\" ? inline.props.diffIds.split(\",\") : inline.props.diffIds;\n for (const id of ids) {\n const trimmed = (id || \"\").toString().trim();\n if (trimmed && !acceptedChanges.has(trimmed) && !rejectedChanges.has(trimmed)) {\n return true;\n }\n }\n }\n if (inline.props && inline.props.diffId) {\n const diffIdInline = inline.props.diffId;\n if (diffIdInline && !acceptedChanges.has(diffIdInline) && !rejectedChanges.has(diffIdInline)) {\n return true;\n }\n }\n if (inline.children && Array.isArray(inline.children)) {\n for (const child of inline.children) {\n if (hasUnresolvedDiffsRecursive(child)) return true;\n }\n }\n }\n }\n }\n if (Array.isArray(block.children)) {\n for (const child of block.children) {\n if (hasUnresolvedDiffsRecursive(child)) return true;\n }\n }\n return false;\n }\n\n let hasUnresolvedDiff = false;\n if (Array.isArray(newBlocks)) {\n hasUnresolvedDiff = newBlocks.some((block: any) => hasUnresolvedDiffsRecursive(block));\n }\n\n onChange(newBlocks, isEmpty, hasUnresolvedDiff);\n }, [editor, onChange, id, acceptedChanges, rejectedChanges]);\n\n // Utility: deep equality for arrays of blocks\n const areBlocksEqual = (a: any[], b: any[]): boolean => {\n return JSON.stringify(a) === JSON.stringify(b);\n };\n\n // Only initialize from markdownContent once per value, and only if different\n const hasInitializedFromMarkdown = useRef<string | null>(null);\n useEffect(() => {\n const updateContent = async (markdown: string) => {\n const blocks = await editor.tryParseMarkdownToBlocks(markdown);\n if (!areBlocksEqual(blocks, editor.document)) {\n editor.replaceBlocks(editor.document, blocks);\n }\n };\n\n if (markdownContent && hasInitializedFromMarkdown.current !== markdownContent) {\n hasInitializedFromMarkdown.current = markdownContent;\n updateContent(markdownContent).then(() => handleChange());\n }\n }, [markdownContent, editor]);\n\n // Update editor content when diff content changes, but only if different\n // Prevent unnecessary replaceBlocks calls that reset scroll/cursor.\n const previousContentHashRef = useRef<string | null>(null);\n useEffect(() => {\n if (!processedContent || !editor) return;\n const hash = JSON.stringify(processedContent);\n if (previousContentHashRef.current === hash) return; // no changes\n // Skip replaceBlocks when the new initialContent is just an echo of what\n // the editor itself emitted. Without this, a form-controlled parent\n // (FormBlockNote) round-trips field.value back as initialContent on every\n // keystroke and triggers replaceBlocks, which resets the cursor and can\n // strip just-inserted inline content like mentions.\n if (lastEmittedHashRef.current === hash) {\n previousContentHashRef.current = hash;\n return;\n }\n const currentHash = JSON.stringify(editor.document);\n if (currentHash === hash) {\n previousContentHashRef.current = hash;\n return; // already in sync\n }\n editor.replaceBlocks(editor.document, processedContent as PartialBlock[]);\n previousContentHashRef.current = hash;\n }, [processedContent, editor]);\n\n useEffect(() => {\n if (!onWarmMentions || !initialContent || !Array.isArray(initialContent) || initialContent.length === 0) return;\n onWarmMentions(initialContent);\n }, [onWarmMentions, initialContent]);\n\n useEffect(() => {\n docRef.current.getDoc = () => editor?.document ?? [];\n selectionRef.current.getSelectedBlocks = () => {\n const sel = editor?.getSelection?.();\n const blocks = (sel as any)?.blocks;\n return Array.isArray(blocks) ? blocks : [];\n };\n }, [editor]);\n\n // Handle audio received from whisper transcription\n const _handleAudioReceived = useCallback(\n (message: string) => {\n try {\n // Ensure the editor has focus\n const editorElement = editorRef.current?.querySelector('[contenteditable=\"true\"]') as HTMLElement;\n if (editorElement && document.activeElement !== editorElement) {\n editorElement.focus();\n }\n\n // Insert the transcribed text at the current cursor position\n editor.insertInlineContent(message);\n } catch (error) {\n console.error(\"Error inserting transcribed text:\", error);\n // Fallback: try to insert at the end of the document\n try {\n const blocks = editor.document;\n if (blocks.length > 0) {\n const lastBlock = blocks[blocks.length - 1];\n editor.setTextCursorPosition(lastBlock.id, \"end\");\n editor.insertInlineContent(message);\n }\n } catch (fallbackError) {\n console.error(\"Fallback insertion also failed:\", fallbackError);\n }\n }\n },\n [editor],\n );\n\n return (\n <div\n ref={editorRef}\n className={cn(\n bordered ? \"rounded-md border border-input bg-input/20 dark:bg-input/30\" : \"\",\n \"flex flex-col w-full\",\n // Pin BlockNote's font-size so it doesn't jump from 14→16px when the\n // xl-ai AIMenu mounts. The shadcn theme sets `.bn-default-styles {\n // font-size: 16px }` explicitly; outside AI mode the form's text-sm\n // wins via cascade, but ForkYDocExtension re-evaluates the style\n // context on AI activation and the explicit 16px takes over.\n \"[&_.bn-default-styles]:!text-sm\",\n className,\n )}\n >\n <BlockNoteView\n editor={editor}\n onChange={handleChange}\n editable={onChange !== undefined}\n formattingToolbar={false}\n slashMenu={!aiConfig || !aiEnabled}\n // Follow the app theme: hardcoding \"light\" leaves dark (light-theme)\n // text on the dark card, which is nearly unreadable in dark mode.\n theme={resolvedTheme === \"dark\" ? \"dark\" : \"light\"}\n // `className` is applied by BlockNote to both the main `.bn-container`\n // AND `editor.portalElement` (the floating-UI portal root). Gate `p-4`\n // on `.bn-container` so it doesn't add padding to the empty portal\n // element and produce a phantom scrollbar on the wrapper.\n className={cn(\n \"BlockNoteView flex-1\",\n onChange && \"[&.bn-container]:p-4\",\n // In stretch mode the parent chain caps our height via flex; without\n // these two classes the `.bn-container` keeps `min-height: auto`\n // (its content's intrinsic height) and pushes the bordered wrapper\n // — and the surrounding EditorSheet form — to scroll instead of\n // scrolling internally.\n onChange && stretch && \"[&.bn-container]:min-h-0 [&.bn-container]:overflow-y-auto\",\n size === \"sm\" && \"small\",\n )}\n >\n <BlockNoteEditorFormattingToolbar showAI={!!aiConfig && aiEnabled} />\n {enableMentions && mentionSearchFn && (\n <BlockNoteEditorMentionSuggestionMenu\n editor={editor}\n mentionSearchFn={mentionSearchFn}\n mentionSearchParams={mentionSearchParams}\n suggestionMenuComponent={suggestionMenuComponent}\n />\n )}\n {renderOverlays?.(editor)}\n {enableMentions && mentionResolveFn && (\n <BlockNoteEditorMentionHoverCard containerRef={editorRef} mentionResolveFn={mentionResolveFn} />\n )}\n {aiConfig && aiEnabled && (\n <SuggestionMenuController\n triggerCharacter=\"/\"\n getItems={async (query: string) =>\n filterSuggestionItems([...getDefaultReactSlashMenuItems(editor), ...getAISlashMenuItems(editor)], query)\n }\n />\n )}\n {aiConfig && aiEnabled && <AIMenuController aiMenu={() => <NarrAIMenu actions={aiConfig.actions} />} />}\n </BlockNoteView>\n </div>\n );\n}\n","import { LayoutTemplateIcon, SparklesIcon } from \"lucide-react\";\nimport React from \"react\";\n\n/**\n * Whole-document AI actions an editor can offer in its AI menu when nothing\n * is selected. Each maps to a backend `type` discriminator; NO prompt text\n * lives here. Selection-edit actions (Improve, Fix spelling, …) are separate\n * and always available.\n */\nexport type BlockNoteAiGenerateAction = \"fill-template\" | \"draft-impression\";\n\nexport const DEFAULT_AI_GENERATE_ACTIONS: BlockNoteAiGenerateAction[] = [\"fill-template\"];\n\nexport type AiGenerateMenuItem = {\n key: string;\n title: string;\n aliases: string[];\n /** BlockNote's DefaultReactSuggestionItem types icon as an element, not ReactNode. */\n icon: React.JSX.Element;\n size: \"small\";\n onItemClick: () => void;\n};\n\nconst ITEMS: Record<BlockNoteAiGenerateAction, Omit<AiGenerateMenuItem, \"onItemClick\">> = {\n \"fill-template\": {\n key: \"generate_from_template\",\n title: \"Generate from Template\",\n aliases: [\"generate\", \"template\", \"fill\"],\n icon: <LayoutTemplateIcon size={18} />,\n size: \"small\",\n },\n \"draft-impression\": {\n key: \"suggest_impression\",\n title: \"Suggest impression\",\n aliases: [\"suggest\", \"impression\"],\n icon: <SparklesIcon size={18} />,\n size: \"small\",\n },\n};\n\nexport function generateActionItems(\n actions: BlockNoteAiGenerateAction[] | undefined,\n invoke: (type: BlockNoteAiGenerateAction) => void,\n): AiGenerateMenuItem[] {\n return (actions ?? DEFAULT_AI_GENERATE_ACTIONS).map((type) => ({\n ...ITEMS[type],\n onItemClick: () => invoke(type),\n }));\n}\n","\"use client\";\n\nimport {\n BasicTextStyleButton,\n BlockTypeSelect,\n CreateLinkButton,\n FileCaptionButton,\n FileReplaceButton,\n FormattingToolbar,\n FormattingToolbarController,\n TextAlignButton,\n} from \"@blocknote/react\";\nimport { AIToolbarButton } from \"@blocknote/xl-ai\";\n\nexport function BlockNoteEditorFormattingToolbar({ showAI = false }: { showAI?: boolean }) {\n return (\n <FormattingToolbarController\n formattingToolbar={() => (\n <FormattingToolbar>\n <BlockTypeSelect key={\"blockTypeSelect\"} />\n\n <FileCaptionButton key={\"fileCaptionButton\"} />\n <FileReplaceButton key={\"replaceFileButton\"} />\n\n <BasicTextStyleButton basicTextStyle={\"bold\"} key={\"boldStyleButton\"} />\n <BasicTextStyleButton basicTextStyle={\"italic\"} key={\"italicStyleButton\"} />\n <BasicTextStyleButton basicTextStyle={\"underline\"} key={\"underlineStyleButton\"} />\n <BasicTextStyleButton basicTextStyle={\"strike\"} key={\"strikeStyleButton\"} />\n\n <TextAlignButton textAlignment={\"left\"} key={\"textAlignLeftButton\"} />\n <TextAlignButton textAlignment={\"center\"} key={\"textAlignCenterButton\"} />\n <TextAlignButton textAlignment={\"right\"} key={\"textAlignRightButton\"} />\n\n <CreateLinkButton key={\"createLinkButton\"} />\n\n {showAI ? <AIToolbarButton key={\"aiToolbarButton\"} /> : null}\n </FormattingToolbar>\n )}\n />\n );\n}\n"]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/features/tokenusage/lib/formatters.ts","../src/features/tokenusage/lib/metrics.ts","../src/features/tokenusage/lib/palette.ts"],"sourcesContent":["\"use client\";\n\nimport { useLocale } from \"next-intl\";\nimport { useMemo } from \"react\";\nimport type { Metric } from \"../data/tokenusage-admin.types\";\nimport { getTokenUsageCurrency } from \"./config\";\n\nexport type UsageFormatters = {\n /** Locale-aware fixed-decimal number, e.g. 9.46 → \"9,46\" in it-IT. */\n decimal(value: number, decimals: number): string;\n /** Currency for cost, 2 decimals for credits, whole numbers for tokens. */\n metricValue(value: number, metric: Metric): string;\n /** Currency with an explicit decimal count — the per-call tile needs 4. */\n currency(value: number, decimals: number): string;\n /** Percentage with one decimal by default, e.g. 60 → \"60,0 %\" in it-IT. */\n percent(value: number, decimals?: number): string;\n /** Compact notation for axis ticks, e.g. 1500000 → \"1,5 Mln\" in it-IT. */\n compact(value: number): string;\n /** A \"YYYY-MM-DD\" bucket key rendered as an axis or tooltip label, in UTC. */\n bucketDate(iso: string, granularity: \"day\" | \"week\" | \"month\"): string;\n /** Locale collation for client-side table sorting. */\n compare(a: string, b: string): number;\n};\n\n/**\n * Builds every formatter the token-usage surfaces need, for one locale and one\n * currency.\n *\n * A pure factory on purpose: it takes no React context, so the arithmetic and\n * the formatting are unit-testable without rendering, and a server component or\n * a test can construct a set for an arbitrary locale.\n *\n * The currency symbol is placed with a space rather than through\n * `style: \"currency\"`, which is what the previous hard-coded implementation\n * rendered (\"€ 9,46\"). Keeping that shape means a consuming app on EUR sees\n * byte-identical output after this refactor.\n *\n * `bucketDate` formats in UTC, always. The wire value is a `type: \"date\"` — a\n * calendar day — parsed to UTC midnight; reading it back with local getters\n * would shift the label a day early west of UTC.\n */\nexport function createUsageFormatters(locale: string, currency: string): UsageFormatters {\n const symbol =\n new Intl.NumberFormat(locale, { style: \"currency\", currency, currencyDisplay: \"narrowSymbol\" })\n .formatToParts(0)\n .find((part) => part.type === \"currency\")?.value ?? currency;\n\n const decimal = (value: number, decimals: number): string =>\n value.toLocaleString(locale, { minimumFractionDigits: decimals, maximumFractionDigits: decimals });\n\n const dayFormat = new Intl.DateTimeFormat(locale, { day: \"numeric\", month: \"short\", timeZone: \"UTC\" });\n const monthFormat = new Intl.DateTimeFormat(locale, { month: \"short\", year: \"numeric\", timeZone: \"UTC\" });\n const compactFormat = new Intl.NumberFormat(locale, { notation: \"compact\", maximumFractionDigits: 1 });\n const collator = new Intl.Collator(locale);\n\n return {\n decimal,\n\n metricValue(value, metric) {\n if (metric === \"cost\") return `${symbol} ${decimal(value, 2)}`;\n // Credits are stored to 4 decimals (round4(cost / creditCost)), but that\n // precision is noise to a reader comparing rows: one decimal is enough to\n // separate two values and keeps the columns narrow enough to scan.\n if (metric === \"credits\") return decimal(value, 1);\n return decimal(value, 0);\n },\n\n currency(value, decimals) {\n return `${symbol} ${decimal(value, decimals)}`;\n },\n\n percent(value, decimals = 1) {\n return `${decimal(value, decimals)} %`;\n },\n\n compact(value) {\n return compactFormat.format(value);\n },\n\n bucketDate(iso, granularity) {\n const date = new Date(`${iso}T00:00:00.000Z`);\n return granularity === \"month\" ? monthFormat.format(date) : dayFormat.format(date);\n },\n\n compare(a, b) {\n return collator.compare(a, b);\n },\n };\n}\n\n/**\n * The formatter set for the current request's locale and the app's configured\n * currency. Memoised on both, so the Intl objects are built once per locale\n * rather than on every render.\n */\nexport function useUsageFormatters(): UsageFormatters {\n const locale = useLocale();\n const currency = getTokenUsageCurrency();\n return useMemo(() => createUsageFormatters(locale, currency), [locale, currency]);\n}\n","import type { Metric } from \"../data/tokenusage-admin.types\";\n\n/**\n * Locale-free arithmetic for the token-usage surfaces.\n *\n * Everything that turns a number into a STRING lives in `./formatters` instead,\n * because it depends on the request's locale and the app's configured currency.\n * This file stays pure arithmetic so it needs neither.\n */\n\n/**\n * The metric field set every admin token-usage resource carries. Declared\n * structurally so it accepts the summary, timeline and breakdown interfaces\n * alike — all three expose exactly these getters.\n */\nexport type TokenUsageMetrics = {\n cost: number;\n credits: number;\n tokensIn: number;\n tokensOut: number;\n cached: number;\n calls: number;\n};\n\n/** Reads the single number a row contributes for the currently selected metric. */\nexport function metricValue(row: TokenUsageMetrics, metric: Metric): number {\n if (metric === \"cost\") return row.cost;\n if (metric === \"credits\") return row.credits;\n return row.tokensIn + row.tokensOut;\n}\n\n/**\n * Share of cached input tokens. Returns 0 rather than NaN when the window holds\n * no input tokens at all, so the tile renders \"0,0 %\" instead of \"NaN %\".\n */\nexport function cacheHitPercentage(cached: number, tokensIn: number): number {\n if (!tokensIn) return 0;\n return (cached / tokensIn) * 100;\n}\n\n/**\n * Percentage change of `current` against `previous`, rounded to a whole number.\n *\n * Returns `undefined` when the previous window is zero — there is no meaningful\n * percentage change from nothing, and rendering Infinity would be worse than\n * rendering nothing. Callers show an em dash in that slot.\n */\nexport function percentageDelta(current: number, previous: number): number | undefined {\n if (!previous) return undefined;\n return Math.round(((current - previous) / previous) * 100);\n}\n","/**\n * Chart palette for the administrative token-usage dashboard.\n *\n * Every value here is a documented step of the dataviz skill's reference ramps —\n * nothing was eyeballed, nothing was hand-mixed. The palette was run through the\n * skill's validator against THIS application's real chart surfaces in both modes.\n * The verbatim output is below.\n *\n * The surface is `--card`, NOT `--background`. Every chart that uses this palette\n * is painted inside a `<Card>`, so the card is the surface the marks actually sit\n * on — and in dark mode it is the *lighter* of the two (`oklch(0.205 0 0)` vs the\n * background's `oklch(0.145 0 0)`), which is the harder case for contrast. Taken\n * from `apps/web/src/app/globals.css` (narr8 declares two `:root`/`.dark` pairs;\n * the LATER pair wins, and it is the one read here) — light `oklch(1 0 0)`, dark\n * `oklch(0.205 0 0)`. a360ai declares the same two values.\n *\n * oklch→hex conversion: the algebraic inverse of the Björn Ottosson OKLab\n * matrices — the same matrices the validator applies in the forward direction —\n * giving light `#ffffff` and dark `#171717`. Cross-checked by feeding it the\n * previously recorded background value, which it reproduces exactly:\n * `oklch(0.145 0 0)` → `#0a0a0a`. ✓\n *\n * Light and dark are two SELECTED sets of steps, chosen for their own surface —\n * never one set with its lightness flipped at runtime. Consumers pick a set from\n * the resolved theme (`seriesColor(i, mode)` / `sequentialColor(i, mode)`).\n *\n * ---------------------------------------------------------------------------\n * VALIDATOR OUTPUT — dataviz `scripts/validate_palette.js`, run 2026-08-15\n * ---------------------------------------------------------------------------\n *\n * $ node scripts/validate_palette.js \"#2a78d6,#eb6834,#1baf7a,#eda100,#e87ba4,#008300,#4a3aa7\" --mode light --surface \"#ffffff\"\n *\n * Palette (light, surface #ffffff, categorical): 7 slots\n * [PASS] Lightness band all 7 inside L 0.43–0.77\n * [PASS] Chroma floor all 7 >= 0.1\n * [PASS] CVD separation worst adjacent #eda100↔#1baf7a ΔE 9.1 (protan) · tritan 5.8\n * [PASS] Normal-vision floor worst adjacent #e87ba4↔#eda100 ΔE 19.6 (normal)\n * [WARN] Contrast vs surface below 3:1 — relief required (visible labels or table view): [[\"#1baf7a\",2.82],[\"#eda100\",2.17],[\"#e87ba4\",2.69]]\n *\n * → ALL CHECKS PASS (CVD in the 6–8 floor band is legal ONLY with secondary encoding: direct labels, gaps, or texture)\n * scope: categorical palettes only. For a lone status/text color check WCAG text contrast; for a sequential ramp, lightness monotonicity.\n *\n * $ node scripts/validate_palette.js \"#3987e5,#d95926,#199e70,#c98500,#d55181,#008300,#9085e9\" --mode dark --surface \"#171717\"\n *\n * Palette (dark, surface #171717, categorical): 7 slots\n * [PASS] Lightness band all 7 inside L 0.48–0.67\n * [PASS] Chroma floor all 7 >= 0.1\n * [PASS] CVD separation worst adjacent #c98500↔#199e70 ΔE 8.4 (protan) · tritan 8.7\n * [PASS] Normal-vision floor worst adjacent #d55181↔#c98500 ΔE 19.3 (normal)\n * [PASS] Contrast vs surface all 7 >= 3:1\n *\n * → ALL CHECKS PASS (CVD in the 6–8 floor band is legal ONLY with secondary encoding: direct labels, gaps, or texture)\n * scope: categorical palettes only. For a lone status/text color check WCAG text contrast; for a sequential ramp, lightness monotonicity.\n *\n * $ node scripts/validate_palette.js \"#104281,#1c5cab,#2a78d6,#5598e7,#86b6ef\" --mode light --surface \"#ffffff\" --ordinal\n *\n * Palette (light, surface #ffffff, ordinal ramp): 5 slots\n * [PASS] Lightness monotone steps read light→dark\n * [PASS] Adjacent ΔL all gaps >= 0.06\n * [PASS] Light-end contrast #86b6ef at 2.11:1 vs surface\n * [PASS] Single hue hue spread 3°\n *\n * → ALL CHECKS PASS (ordinal: one hue, monotone L, visible step gaps, light end clears surface)\n *\n * $ node scripts/validate_palette.js \"#9ec5f4,#6da7ec,#3987e5,#256abf,#184f95\" --mode dark --surface \"#171717\" --ordinal\n *\n * Palette (dark, surface #171717, ordinal ramp): 5 slots\n * [PASS] Lightness monotone steps read light→dark\n * [PASS] Adjacent ΔL all gaps >= 0.06\n * [PASS] Light-end contrast #184f95 at 2.21:1 vs surface\n * [PASS] Single hue hue spread 3°\n *\n * → ALL CHECKS PASS (ordinal: one hue, monotone L, visible step gaps, light end clears surface)\n *\n * OTHER_COLOR is achromatic, so the categorical checks (which gate hue identity)\n * do not apply to it; it was gated on contrast alone with the validator's own\n * `contrast()` export: 3.59:1 on `#ffffff`, 4.99:1 on `#171717` — both clear 3:1.\n *\n * ---------------------------------------------------------------------------\n * WHAT THE RESULTS OBLIGE US TO DO\n * ---------------------------------------------------------------------------\n *\n * - NO COLOUR VALUE CHANGED in the move from `--background` to `--card`. Every\n * gate that passed on the background still passes on the card, so re-stepping\n * a hue would have been churn, not a fix. Only the recorded surface, the\n * recorded numbers and this prose changed.\n * - The dark set still clears 3:1 on the LIGHTER card surface: the worst slot is\n * green `#008300` at 3.63:1, and the ordinal ramp's surface-nearest step lands\n * at 2.21:1 (down from 2.44:1 on the background) — still above the 2:1 gate.\n * That headroom is thin, so a future card-surface lightening MUST re-run these\n * four commands rather than assume the set still holds.\n * - The light-mode contrast WARN is NOT dismissable. Three slots (aqua, yellow,\n * magenta) sit below 3:1 on white, so every surface that paints with this\n * palette MUST ship the relief channel: a visible legend, direct labels and a\n * tooltip carrying the value in text. The timeline chart and the breakdown\n * table both do. The light card is `#ffffff`, identical to the light\n * background, so this WARN is unchanged rather than newly incurred.\n * - CVD separation is measured on ADJACENT pairs, which is the correct pairlist\n * for stacked bars, grouped bars and lines — the only forms this palette paints.\n * A scatter / bubble / small-multiples chart would need `--pairs all`, which\n * caps the usable slot count at three; do not reuse this array there without\n * re-running the validator with that flag.\n */\n\n/** Which surface the colours are being painted on. */\nexport type ChartMode = \"light\" | \"dark\";\n\n/**\n * The chart surfaces the palette was validated against (globals.css `--card`).\n *\n * `--card`, never `--background`: the charts are painted inside a `<Card>`.\n */\nexport const CHART_SURFACE: Readonly<Record<ChartMode, string>> = {\n light: \"#ffffff\",\n dark: \"#171717\",\n};\n\n/**\n * Categorical slots, in FIXED order. Slot n is series n — the order is the\n * CVD-safety mechanism, not decoration, so it is never re-ordered and never\n * cycled. The two-series default (customer / platform) takes slots 0 and 1.\n *\n * Hues, in order: blue, orange, aqua, yellow, magenta, green, violet.\n */\nconst CATEGORICAL_LIGHT: readonly string[] = [\n \"#2a78d6\",\n \"#eb6834\",\n \"#1baf7a\",\n \"#eda100\",\n \"#e87ba4\",\n \"#008300\",\n \"#4a3aa7\",\n];\n\n/** The same seven hues, re-stepped for the dark surface. Not a lightness flip. */\nconst CATEGORICAL_DARK: readonly string[] = [\n \"#3987e5\",\n \"#d95926\",\n \"#199e70\",\n \"#c98500\",\n \"#d55181\",\n \"#008300\",\n \"#9085e9\",\n];\n\n/**\n * The default (light-surface) categorical set.\n *\n * Prefer `seriesColor(index, mode)` — this array exists for consumers that only\n * need the documented default order.\n */\nexport const CATEGORICAL: readonly string[] = CATEGORICAL_LIGHT;\n\n/**\n * How many series may carry their own identity hue. An 8th series is NEVER a\n * generated hue: it folds into `other`.\n */\nexport const CATEGORICAL_CEILING = CATEGORICAL_LIGHT.length;\n\n/**\n * The \"other\" rollup colour: deliberately achromatic so it reads as \"not an\n * identity\" beside the seven hues. Same step in both modes — it clears 3:1 on\n * both card surfaces (3.59:1 on `#ffffff`, 4.99:1 on `#171717`).\n */\nexport const OTHER_COLOR = \"#898781\";\n\n/**\n * Sequential (single-hue, blue) ramp for MAGNITUDE, indexed by rank position:\n * index 0 is rank 1 — the largest value — so the strongest step leads.\n *\n * On the light surface \"strongest\" is the darkest step; on the dark surface it\n * is the lightest. The anchor flips with the surface, which is why there are two\n * selected sets rather than one array reversed at runtime.\n *\n * Five steps, not eight: with the adjacent-ΔL ≥ 0.06 gate and the ≥ 2:1\n * surface-contrast gate on the step nearest the surface, the blue ramp fits\n * exactly five distinguishable steps. Consumers clamp past the end.\n */\nconst SEQUENTIAL_RAMP_LIGHT: readonly string[] = [\"#104281\", \"#1c5cab\", \"#2a78d6\", \"#5598e7\", \"#86b6ef\"];\n\nconst SEQUENTIAL_RAMP_DARK: readonly string[] = [\"#9ec5f4\", \"#6da7ec\", \"#3987e5\", \"#256abf\", \"#184f95\"];\n\n/**\n * The default (light-surface) sequential ramp.\n *\n * Prefer `sequentialColor(index, mode)` — this array exists for consumers that\n * only need the documented default.\n */\nexport const SEQUENTIAL_RAMP: readonly string[] = SEQUENTIAL_RAMP_LIGHT;\n\nconst categoricalSet = (mode: ChartMode): readonly string[] => (mode === \"dark\" ? CATEGORICAL_DARK : CATEGORICAL_LIGHT);\n\nconst sequentialSet = (mode: ChartMode): readonly string[] =>\n mode === \"dark\" ? SEQUENTIAL_RAMP_DARK : SEQUENTIAL_RAMP_LIGHT;\n\n/**\n * The identity colour for series `index`.\n *\n * Never generates a hue and never cycles: anything at or past the ceiling gets\n * `OTHER_COLOR`, because a series past the ceiling should already have been\n * folded into the \"other\" bucket by the caller.\n */\nexport function seriesColor(index: number, mode: ChartMode = \"light\"): string {\n if (index < 0 || index >= CATEGORICAL_CEILING) return OTHER_COLOR;\n return categoricalSet(mode)[index];\n}\n\n/**\n * Both mode steps for series `index`, shaped for a shadcn `ChartConfig` entry's\n * `theme` field.\n */\nexport function seriesTheme(index: number): { light: string; dark: string } {\n return { light: seriesColor(index, \"light\"), dark: seriesColor(index, \"dark\") };\n}\n\n/**\n * The magnitude colour for rank position `index`, clamped to the last step so a\n * long list keeps its weakest step rather than falling off the ramp.\n */\nexport function sequentialColor(index: number, mode: ChartMode = \"light\"): string {\n const ramp = sequentialSet(mode);\n const clamped = Math.min(Math.max(index, 0), ramp.length - 1);\n return ramp[clamped];\n}\n"],"mappings":"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","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["/home/runner/work/nextjs-jsonapi/nextjs-jsonapi/dist/chunk-KLV6RLDE.js","../src/features/tokenusage/lib/formatters.ts","../src/features/tokenusage/lib/metrics.ts","../src/features/tokenusage/lib/palette.ts"],"names":[],"mappings":"AAAA;AACE;AACF,sDAA4B;AAC5B;AACE;AACF,sDAA4B;AAC5B;AACA;ACLA,qCAA0B;AAC1B,8BAAwB;AAsCjB,SAAS,qBAAA,CAAsB,MAAA,EAAgB,QAAA,EAAmC;AACvF,EAAA,MAAM,OAAA,mCACJ,IAAI,IAAA,CAAK,YAAA,CAAa,MAAA,EAAQ,EAAE,KAAA,EAAO,UAAA,EAAY,QAAA,EAAU,eAAA,EAAiB,eAAe,CAAC,CAAA,mBAC3F,aAAA,mBAAc,CAAC,CAAA,qBACf,IAAA,mBAAK,CAAC,IAAA,EAAA,GAAS,IAAA,CAAK,KAAA,IAAS,UAAU,CAAA,6BAAG,OAAA,UAAS,UAAA;AAExD,EAAA,MAAM,QAAA,kBAAU,qCAAA,CAAC,KAAA,EAAe,QAAA,EAAA,GAC9B,KAAA,CAAM,cAAA,CAAe,MAAA,EAAQ,EAAE,qBAAA,EAAuB,QAAA,EAAU,qBAAA,EAAuB,SAAS,CAAC,CAAA,EADnF,SAAA,CAAA;AAGhB,EAAA,MAAM,UAAA,EAAY,IAAI,IAAA,CAAK,cAAA,CAAe,MAAA,EAAQ,EAAE,GAAA,EAAK,SAAA,EAAW,KAAA,EAAO,OAAA,EAAS,QAAA,EAAU,MAAM,CAAC,CAAA;AACrG,EAAA,MAAM,YAAA,EAAc,IAAI,IAAA,CAAK,cAAA,CAAe,MAAA,EAAQ,EAAE,KAAA,EAAO,OAAA,EAAS,IAAA,EAAM,SAAA,EAAW,QAAA,EAAU,MAAM,CAAC,CAAA;AACxG,EAAA,MAAM,cAAA,EAAgB,IAAI,IAAA,CAAK,YAAA,CAAa,MAAA,EAAQ,EAAE,QAAA,EAAU,SAAA,EAAW,qBAAA,EAAuB,EAAE,CAAC,CAAA;AACrG,EAAA,MAAM,SAAA,EAAW,IAAI,IAAA,CAAK,QAAA,CAAS,MAAM,CAAA;AAEzC,EAAA,OAAO;AAAA,IACL,OAAA;AAAA,IAEA,WAAA,CAAY,KAAA,EAAO,MAAA,EAAQ;AACzB,MAAA,GAAA,CAAI,OAAA,IAAW,MAAA,EAAQ,OAAO,CAAA,EAAA;AAIJ,MAAA;AACH,MAAA;AACzB,IAAA;AAE0B,IAAA;AACI,MAAA;AAC9B,IAAA;AAE6B,IAAA;AACF,MAAA;AAC3B,IAAA;AAEe,IAAA;AACe,MAAA;AAC9B,IAAA;AAE6B,IAAA;AACd,MAAA;AACU,MAAA;AACzB,IAAA;AAEc,IAAA;AACgB,MAAA;AAC9B,IAAA;AACF,EAAA;AACF;AA/CgB;AAsDsC;AAC3B,EAAA;AACR,EAAA;AACI,EAAA;AACvB;AAJgB;AD9CqB;AACA;AEzBuC;AACxC,EAAA;AACD,EAAA;AACP,EAAA;AAC5B;AAJgB;AAUmC;AAC3B,EAAA;AACO,EAAA;AAC/B;AAHgB;AAYiC;AACzB,EAAA;AACQ,EAAA;AAChC;AAHgB;AFoBqB;AACA;AGwDQ;AAC3C,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACF;AAG4C;AAC1C,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACF;AAcmC;AAOR;AAcuB;AAUA;AAE3B;AAYsC;AACjC,EAAA;AACO,EAAA;AACnC;AAHgB;AHzGqB;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA","file":"/home/runner/work/nextjs-jsonapi/nextjs-jsonapi/dist/chunk-KLV6RLDE.js","sourcesContent":[null,"\"use client\";\n\nimport { useLocale } from \"next-intl\";\nimport { useMemo } from \"react\";\nimport type { Metric } from \"../data/tokenusage-admin.types\";\nimport { getTokenUsageCurrency } from \"./config\";\n\nexport type UsageFormatters = {\n /** Locale-aware fixed-decimal number, e.g. 9.46 → \"9,46\" in it-IT. */\n decimal(value: number, decimals: number): string;\n /** Currency for cost, 2 decimals for credits, whole numbers for tokens. */\n metricValue(value: number, metric: Metric): string;\n /** Currency with an explicit decimal count — the per-call tile needs 4. */\n currency(value: number, decimals: number): string;\n /** Percentage with one decimal by default, e.g. 60 → \"60,0 %\" in it-IT. */\n percent(value: number, decimals?: number): string;\n /** Compact notation for axis ticks, e.g. 1500000 → \"1,5 Mln\" in it-IT. */\n compact(value: number): string;\n /** A \"YYYY-MM-DD\" bucket key rendered as an axis or tooltip label, in UTC. */\n bucketDate(iso: string, granularity: \"day\" | \"week\" | \"month\"): string;\n /** Locale collation for client-side table sorting. */\n compare(a: string, b: string): number;\n};\n\n/**\n * Builds every formatter the token-usage surfaces need, for one locale and one\n * currency.\n *\n * A pure factory on purpose: it takes no React context, so the arithmetic and\n * the formatting are unit-testable without rendering, and a server component or\n * a test can construct a set for an arbitrary locale.\n *\n * The currency symbol is placed with a space rather than through\n * `style: \"currency\"`, which is what the previous hard-coded implementation\n * rendered (\"€ 9,46\"). Keeping that shape means a consuming app on EUR sees\n * byte-identical output after this refactor.\n *\n * `bucketDate` formats in UTC, always. The wire value is a `type: \"date\"` — a\n * calendar day — parsed to UTC midnight; reading it back with local getters\n * would shift the label a day early west of UTC.\n */\nexport function createUsageFormatters(locale: string, currency: string): UsageFormatters {\n const symbol =\n new Intl.NumberFormat(locale, { style: \"currency\", currency, currencyDisplay: \"narrowSymbol\" })\n .formatToParts(0)\n .find((part) => part.type === \"currency\")?.value ?? currency;\n\n const decimal = (value: number, decimals: number): string =>\n value.toLocaleString(locale, { minimumFractionDigits: decimals, maximumFractionDigits: decimals });\n\n const dayFormat = new Intl.DateTimeFormat(locale, { day: \"numeric\", month: \"short\", timeZone: \"UTC\" });\n const monthFormat = new Intl.DateTimeFormat(locale, { month: \"short\", year: \"numeric\", timeZone: \"UTC\" });\n const compactFormat = new Intl.NumberFormat(locale, { notation: \"compact\", maximumFractionDigits: 1 });\n const collator = new Intl.Collator(locale);\n\n return {\n decimal,\n\n metricValue(value, metric) {\n if (metric === \"cost\") return `${symbol} ${decimal(value, 2)}`;\n // Credits are stored to 4 decimals (round4(cost / creditCost)), but that\n // precision is noise to a reader comparing rows: one decimal is enough to\n // separate two values and keeps the columns narrow enough to scan.\n if (metric === \"credits\") return decimal(value, 1);\n return decimal(value, 0);\n },\n\n currency(value, decimals) {\n return `${symbol} ${decimal(value, decimals)}`;\n },\n\n percent(value, decimals = 1) {\n return `${decimal(value, decimals)} %`;\n },\n\n compact(value) {\n return compactFormat.format(value);\n },\n\n bucketDate(iso, granularity) {\n const date = new Date(`${iso}T00:00:00.000Z`);\n return granularity === \"month\" ? monthFormat.format(date) : dayFormat.format(date);\n },\n\n compare(a, b) {\n return collator.compare(a, b);\n },\n };\n}\n\n/**\n * The formatter set for the current request's locale and the app's configured\n * currency. Memoised on both, so the Intl objects are built once per locale\n * rather than on every render.\n */\nexport function useUsageFormatters(): UsageFormatters {\n const locale = useLocale();\n const currency = getTokenUsageCurrency();\n return useMemo(() => createUsageFormatters(locale, currency), [locale, currency]);\n}\n","import type { Metric } from \"../data/tokenusage-admin.types\";\n\n/**\n * Locale-free arithmetic for the token-usage surfaces.\n *\n * Everything that turns a number into a STRING lives in `./formatters` instead,\n * because it depends on the request's locale and the app's configured currency.\n * This file stays pure arithmetic so it needs neither.\n */\n\n/**\n * The metric field set every admin token-usage resource carries. Declared\n * structurally so it accepts the summary, timeline and breakdown interfaces\n * alike — all three expose exactly these getters.\n */\nexport type TokenUsageMetrics = {\n cost: number;\n credits: number;\n tokensIn: number;\n tokensOut: number;\n cached: number;\n calls: number;\n};\n\n/** Reads the single number a row contributes for the currently selected metric. */\nexport function metricValue(row: TokenUsageMetrics, metric: Metric): number {\n if (metric === \"cost\") return row.cost;\n if (metric === \"credits\") return row.credits;\n return row.tokensIn + row.tokensOut;\n}\n\n/**\n * Share of cached input tokens. Returns 0 rather than NaN when the window holds\n * no input tokens at all, so the tile renders \"0,0 %\" instead of \"NaN %\".\n */\nexport function cacheHitPercentage(cached: number, tokensIn: number): number {\n if (!tokensIn) return 0;\n return (cached / tokensIn) * 100;\n}\n\n/**\n * Percentage change of `current` against `previous`, rounded to a whole number.\n *\n * Returns `undefined` when the previous window is zero — there is no meaningful\n * percentage change from nothing, and rendering Infinity would be worse than\n * rendering nothing. Callers show an em dash in that slot.\n */\nexport function percentageDelta(current: number, previous: number): number | undefined {\n if (!previous) return undefined;\n return Math.round(((current - previous) / previous) * 100);\n}\n","/**\n * Chart palette for the administrative token-usage dashboard.\n *\n * Every value here is a documented step of the dataviz skill's reference ramps —\n * nothing was eyeballed, nothing was hand-mixed. The palette was run through the\n * skill's validator against THIS application's real chart surfaces in both modes.\n * The verbatim output is below.\n *\n * The surface is `--card`, NOT `--background`. Every chart that uses this palette\n * is painted inside a `<Card>`, so the card is the surface the marks actually sit\n * on — and in dark mode it is the *lighter* of the two (`oklch(0.205 0 0)` vs the\n * background's `oklch(0.145 0 0)`), which is the harder case for contrast. Taken\n * from `apps/web/src/app/globals.css` (narr8 declares two `:root`/`.dark` pairs;\n * the LATER pair wins, and it is the one read here) — light `oklch(1 0 0)`, dark\n * `oklch(0.205 0 0)`. a360ai declares the same two values.\n *\n * oklch→hex conversion: the algebraic inverse of the Björn Ottosson OKLab\n * matrices — the same matrices the validator applies in the forward direction —\n * giving light `#ffffff` and dark `#171717`. Cross-checked by feeding it the\n * previously recorded background value, which it reproduces exactly:\n * `oklch(0.145 0 0)` → `#0a0a0a`. ✓\n *\n * Light and dark are two SELECTED sets of steps, chosen for their own surface —\n * never one set with its lightness flipped at runtime. Consumers pick a set from\n * the resolved theme (`seriesColor(i, mode)` / `sequentialColor(i, mode)`).\n *\n * ---------------------------------------------------------------------------\n * VALIDATOR OUTPUT — dataviz `scripts/validate_palette.js`, run 2026-08-15\n * ---------------------------------------------------------------------------\n *\n * $ node scripts/validate_palette.js \"#2a78d6,#eb6834,#1baf7a,#eda100,#e87ba4,#008300,#4a3aa7\" --mode light --surface \"#ffffff\"\n *\n * Palette (light, surface #ffffff, categorical): 7 slots\n * [PASS] Lightness band all 7 inside L 0.43–0.77\n * [PASS] Chroma floor all 7 >= 0.1\n * [PASS] CVD separation worst adjacent #eda100↔#1baf7a ΔE 9.1 (protan) · tritan 5.8\n * [PASS] Normal-vision floor worst adjacent #e87ba4↔#eda100 ΔE 19.6 (normal)\n * [WARN] Contrast vs surface below 3:1 — relief required (visible labels or table view): [[\"#1baf7a\",2.82],[\"#eda100\",2.17],[\"#e87ba4\",2.69]]\n *\n * → ALL CHECKS PASS (CVD in the 6–8 floor band is legal ONLY with secondary encoding: direct labels, gaps, or texture)\n * scope: categorical palettes only. For a lone status/text color check WCAG text contrast; for a sequential ramp, lightness monotonicity.\n *\n * $ node scripts/validate_palette.js \"#3987e5,#d95926,#199e70,#c98500,#d55181,#008300,#9085e9\" --mode dark --surface \"#171717\"\n *\n * Palette (dark, surface #171717, categorical): 7 slots\n * [PASS] Lightness band all 7 inside L 0.48–0.67\n * [PASS] Chroma floor all 7 >= 0.1\n * [PASS] CVD separation worst adjacent #c98500↔#199e70 ΔE 8.4 (protan) · tritan 8.7\n * [PASS] Normal-vision floor worst adjacent #d55181↔#c98500 ΔE 19.3 (normal)\n * [PASS] Contrast vs surface all 7 >= 3:1\n *\n * → ALL CHECKS PASS (CVD in the 6–8 floor band is legal ONLY with secondary encoding: direct labels, gaps, or texture)\n * scope: categorical palettes only. For a lone status/text color check WCAG text contrast; for a sequential ramp, lightness monotonicity.\n *\n * $ node scripts/validate_palette.js \"#104281,#1c5cab,#2a78d6,#5598e7,#86b6ef\" --mode light --surface \"#ffffff\" --ordinal\n *\n * Palette (light, surface #ffffff, ordinal ramp): 5 slots\n * [PASS] Lightness monotone steps read light→dark\n * [PASS] Adjacent ΔL all gaps >= 0.06\n * [PASS] Light-end contrast #86b6ef at 2.11:1 vs surface\n * [PASS] Single hue hue spread 3°\n *\n * → ALL CHECKS PASS (ordinal: one hue, monotone L, visible step gaps, light end clears surface)\n *\n * $ node scripts/validate_palette.js \"#9ec5f4,#6da7ec,#3987e5,#256abf,#184f95\" --mode dark --surface \"#171717\" --ordinal\n *\n * Palette (dark, surface #171717, ordinal ramp): 5 slots\n * [PASS] Lightness monotone steps read light→dark\n * [PASS] Adjacent ΔL all gaps >= 0.06\n * [PASS] Light-end contrast #184f95 at 2.21:1 vs surface\n * [PASS] Single hue hue spread 3°\n *\n * → ALL CHECKS PASS (ordinal: one hue, monotone L, visible step gaps, light end clears surface)\n *\n * OTHER_COLOR is achromatic, so the categorical checks (which gate hue identity)\n * do not apply to it; it was gated on contrast alone with the validator's own\n * `contrast()` export: 3.59:1 on `#ffffff`, 4.99:1 on `#171717` — both clear 3:1.\n *\n * ---------------------------------------------------------------------------\n * WHAT THE RESULTS OBLIGE US TO DO\n * ---------------------------------------------------------------------------\n *\n * - NO COLOUR VALUE CHANGED in the move from `--background` to `--card`. Every\n * gate that passed on the background still passes on the card, so re-stepping\n * a hue would have been churn, not a fix. Only the recorded surface, the\n * recorded numbers and this prose changed.\n * - The dark set still clears 3:1 on the LIGHTER card surface: the worst slot is\n * green `#008300` at 3.63:1, and the ordinal ramp's surface-nearest step lands\n * at 2.21:1 (down from 2.44:1 on the background) — still above the 2:1 gate.\n * That headroom is thin, so a future card-surface lightening MUST re-run these\n * four commands rather than assume the set still holds.\n * - The light-mode contrast WARN is NOT dismissable. Three slots (aqua, yellow,\n * magenta) sit below 3:1 on white, so every surface that paints with this\n * palette MUST ship the relief channel: a visible legend, direct labels and a\n * tooltip carrying the value in text. The timeline chart and the breakdown\n * table both do. The light card is `#ffffff`, identical to the light\n * background, so this WARN is unchanged rather than newly incurred.\n * - CVD separation is measured on ADJACENT pairs, which is the correct pairlist\n * for stacked bars, grouped bars and lines — the only forms this palette paints.\n * A scatter / bubble / small-multiples chart would need `--pairs all`, which\n * caps the usable slot count at three; do not reuse this array there without\n * re-running the validator with that flag.\n */\n\n/** Which surface the colours are being painted on. */\nexport type ChartMode = \"light\" | \"dark\";\n\n/**\n * The chart surfaces the palette was validated against (globals.css `--card`).\n *\n * `--card`, never `--background`: the charts are painted inside a `<Card>`.\n */\nexport const CHART_SURFACE: Readonly<Record<ChartMode, string>> = {\n light: \"#ffffff\",\n dark: \"#171717\",\n};\n\n/**\n * Categorical slots, in FIXED order. Slot n is series n — the order is the\n * CVD-safety mechanism, not decoration, so it is never re-ordered and never\n * cycled. The two-series default (customer / platform) takes slots 0 and 1.\n *\n * Hues, in order: blue, orange, aqua, yellow, magenta, green, violet.\n */\nconst CATEGORICAL_LIGHT: readonly string[] = [\n \"#2a78d6\",\n \"#eb6834\",\n \"#1baf7a\",\n \"#eda100\",\n \"#e87ba4\",\n \"#008300\",\n \"#4a3aa7\",\n];\n\n/** The same seven hues, re-stepped for the dark surface. Not a lightness flip. */\nconst CATEGORICAL_DARK: readonly string[] = [\n \"#3987e5\",\n \"#d95926\",\n \"#199e70\",\n \"#c98500\",\n \"#d55181\",\n \"#008300\",\n \"#9085e9\",\n];\n\n/**\n * The default (light-surface) categorical set.\n *\n * Prefer `seriesColor(index, mode)` — this array exists for consumers that only\n * need the documented default order.\n */\nexport const CATEGORICAL: readonly string[] = CATEGORICAL_LIGHT;\n\n/**\n * How many series may carry their own identity hue. An 8th series is NEVER a\n * generated hue: it folds into `other`.\n */\nexport const CATEGORICAL_CEILING = CATEGORICAL_LIGHT.length;\n\n/**\n * The \"other\" rollup colour: deliberately achromatic so it reads as \"not an\n * identity\" beside the seven hues. Same step in both modes — it clears 3:1 on\n * both card surfaces (3.59:1 on `#ffffff`, 4.99:1 on `#171717`).\n */\nexport const OTHER_COLOR = \"#898781\";\n\n/**\n * Sequential (single-hue, blue) ramp for MAGNITUDE, indexed by rank position:\n * index 0 is rank 1 — the largest value — so the strongest step leads.\n *\n * On the light surface \"strongest\" is the darkest step; on the dark surface it\n * is the lightest. The anchor flips with the surface, which is why there are two\n * selected sets rather than one array reversed at runtime.\n *\n * Five steps, not eight: with the adjacent-ΔL ≥ 0.06 gate and the ≥ 2:1\n * surface-contrast gate on the step nearest the surface, the blue ramp fits\n * exactly five distinguishable steps. Consumers clamp past the end.\n */\nconst SEQUENTIAL_RAMP_LIGHT: readonly string[] = [\"#104281\", \"#1c5cab\", \"#2a78d6\", \"#5598e7\", \"#86b6ef\"];\n\nconst SEQUENTIAL_RAMP_DARK: readonly string[] = [\"#9ec5f4\", \"#6da7ec\", \"#3987e5\", \"#256abf\", \"#184f95\"];\n\n/**\n * The default (light-surface) sequential ramp.\n *\n * Prefer `sequentialColor(index, mode)` — this array exists for consumers that\n * only need the documented default.\n */\nexport const SEQUENTIAL_RAMP: readonly string[] = SEQUENTIAL_RAMP_LIGHT;\n\nconst categoricalSet = (mode: ChartMode): readonly string[] => (mode === \"dark\" ? CATEGORICAL_DARK : CATEGORICAL_LIGHT);\n\nconst sequentialSet = (mode: ChartMode): readonly string[] =>\n mode === \"dark\" ? SEQUENTIAL_RAMP_DARK : SEQUENTIAL_RAMP_LIGHT;\n\n/**\n * The identity colour for series `index`.\n *\n * Never generates a hue and never cycles: anything at or past the ceiling gets\n * `OTHER_COLOR`, because a series past the ceiling should already have been\n * folded into the \"other\" bucket by the caller.\n */\nexport function seriesColor(index: number, mode: ChartMode = \"light\"): string {\n if (index < 0 || index >= CATEGORICAL_CEILING) return OTHER_COLOR;\n return categoricalSet(mode)[index];\n}\n\n/**\n * Both mode steps for series `index`, shaped for a shadcn `ChartConfig` entry's\n * `theme` field.\n */\nexport function seriesTheme(index: number): { light: string; dark: string } {\n return { light: seriesColor(index, \"light\"), dark: seriesColor(index, \"dark\") };\n}\n\n/**\n * The magnitude colour for rank position `index`, clamped to the last step so a\n * long list keeps its weakest step rather than falling off the ramp.\n */\nexport function sequentialColor(index: number, mode: ChartMode = \"light\"): string {\n const ramp = sequentialSet(mode);\n const clamped = Math.min(Math.max(index, 0), ramp.length - 1);\n return ramp[clamped];\n}\n"]}