@carlonicora/nextjs-jsonapi 6.0.0 → 6.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/messages/en.json CHANGED
@@ -1776,17 +1776,40 @@
1776
1776
  "empty": "No usage recorded in this period"
1777
1777
  },
1778
1778
  "types": {
1779
+ "analyser": "Analysis",
1780
+ "blogSummary": "Blog summaries",
1781
+ "blogTranslation": "Blog translations",
1779
1782
  "communitySummariser": "Community overviews",
1783
+ "counterpartIdentificator": "Counterpart identification",
1780
1784
  "customer": "Customer",
1781
1785
  "embedding": "Search indexing",
1786
+ "ethicist": "Ethics review",
1782
1787
  "graphCreator": "Connection mapping",
1788
+ "imageAnalysis": "Image analysis",
1783
1789
  "imageGeneration": "Image generation",
1784
1790
  "operator": "Assistant actions",
1785
1791
  "platform": "Platform",
1786
1792
  "responder": "Assistant answers",
1793
+ "retrievalEval": "Search quality tests",
1794
+ "strategy": "Strategy",
1787
1795
  "summariser": "Summarisation",
1788
1796
  "textGeneration": "Text generation",
1789
1797
  "transcription": "Transcription"
1798
+ },
1799
+ "type_descriptions": {
1800
+ "blogSummary": "Writes the summary of each blog post.",
1801
+ "blogTranslation": "Translates blog posts and their categories into the other languages.",
1802
+ "communitySummariser": "Writes an overview of each group of related content.",
1803
+ "embedding": "Turns content into search vectors, so search and the assistant can find it.",
1804
+ "graphCreator": "Reads new content and extracts its facts and the connections between the people, places and things it mentions.",
1805
+ "imageAnalysis": "Reads the images inside uploaded documents so their content can be searched.",
1806
+ "imageGeneration": "Creates images from a written description.",
1807
+ "operator": "Actions the assistant carries out, such as searching or changing content.",
1808
+ "responder": "The assistant's answers to questions.",
1809
+ "retrievalEval": "Grades search quality during internal tests. Never started by customers.",
1810
+ "summariser": "Writes summaries of processed content.",
1811
+ "textGeneration": "AI writing in the editor, plus any AI call that does not record a more specific category.",
1812
+ "transcription": "Turns recorded audio into text."
1790
1813
  }
1791
1814
  },
1792
1815
  "ui": {
package/messages/it.json CHANGED
@@ -1776,17 +1776,40 @@
1776
1776
  "empty": "Nessun utilizzo registrato in questo periodo"
1777
1777
  },
1778
1778
  "types": {
1779
+ "analyser": "Analisi",
1780
+ "blogSummary": "Riassunti del blog",
1781
+ "blogTranslation": "Traduzioni del blog",
1779
1782
  "communitySummariser": "Panoramiche delle comunità",
1783
+ "counterpartIdentificator": "Identificazione delle controparti",
1780
1784
  "customer": "Cliente",
1781
1785
  "embedding": "Indicizzazione per la ricerca",
1786
+ "ethicist": "Revisione etica",
1782
1787
  "graphCreator": "Mappatura dei collegamenti",
1788
+ "imageAnalysis": "Analisi delle immagini",
1783
1789
  "imageGeneration": "Generazione di immagini",
1784
1790
  "operator": "Azioni dell'assistente",
1785
1791
  "platform": "Piattaforma",
1786
1792
  "responder": "Risposte dell'assistente",
1793
+ "retrievalEval": "Test di qualità della ricerca",
1794
+ "strategy": "Strategia",
1787
1795
  "summariser": "Riassunti",
1788
1796
  "textGeneration": "Generazione di testo",
1789
1797
  "transcription": "Trascrizione"
1798
+ },
1799
+ "type_descriptions": {
1800
+ "blogSummary": "Scrive il riassunto di ogni articolo del blog.",
1801
+ "blogTranslation": "Traduce gli articoli del blog e le loro categorie nelle altre lingue.",
1802
+ "communitySummariser": "Scrive una panoramica di ogni gruppo di contenuti collegati.",
1803
+ "embedding": "Trasforma i contenuti in vettori di ricerca, così la ricerca e l'assistente possono trovarli.",
1804
+ "graphCreator": "Legge i nuovi contenuti ed estrae i fatti e i collegamenti tra le persone, i luoghi e le cose citate.",
1805
+ "imageAnalysis": "Legge le immagini dentro i documenti caricati, così il loro contenuto diventa ricercabile.",
1806
+ "imageGeneration": "Crea immagini a partire da una descrizione scritta.",
1807
+ "operator": "Le azioni svolte dall'assistente, come cercare o modificare contenuti.",
1808
+ "responder": "Le risposte dell'assistente alle domande.",
1809
+ "retrievalEval": "Valuta la qualità della ricerca durante i test interni. Mai avviato dai clienti.",
1810
+ "summariser": "Scrive i riassunti dei contenuti elaborati.",
1811
+ "textGeneration": "La scrittura con l'AI nell'editor, più ogni chiamata AI che non registra una categoria più specifica.",
1812
+ "transcription": "Trasforma l'audio registrato in testo."
1790
1813
  }
1791
1814
  },
1792
1815
  "ui": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@carlonicora/nextjs-jsonapi",
3
- "version": "6.0.0",
3
+ "version": "6.1.0",
4
4
  "description": "Next.js JSON:API client with server/client support and caching",
5
5
  "author": "Carlo Nicora",
6
6
  "license": "GPL-3.0-or-later",
@@ -35,7 +35,10 @@ export const APP_PROVIDED_KEYS = [
35
35
  // Token-usage operation types an app backend defines (operation-label.ts: the
36
36
  // app supplies this vocabulary, unknown types render raw). Only the types the
37
37
  // backend library emits, plus its customer/platform scopes, ship.
38
- /^token_usage\.types\.(?!(?:analyser|communitySummariser|counterpartIdentificator|customer|embedding|ethicist|graphCreator|imageAnalysis|imageGeneration|operator|platform|responder|strategy|summariser|textGeneration|transcription)$)/,
38
+ /^token_usage\.types\.(?!(?:analyser|blogSummary|blogTranslation|communitySummariser|counterpartIdentificator|customer|embedding|ethicist|graphCreator|imageAnalysis|imageGeneration|operator|platform|responder|retrievalEval|strategy|summariser|textGeneration|transcription)$)/,
39
+ // Their optional explanations (operation-label.ts operationDescription): same
40
+ // rule, only the library-emitted types ship one.
41
+ /^token_usage\.type_descriptions\.(?!(?:analyser|blogSummary|blogTranslation|communitySummariser|counterpartIdentificator|embedding|ethicist|graphCreator|imageAnalysis|imageGeneration|operator|responder|retrievalEval|strategy|summariser|textGeneration|transcription)$)/,
39
42
  ];
40
43
 
41
44
  // Values the app-word rule would flag although the word is the library's own
@@ -1,10 +1,11 @@
1
1
  "use client";
2
2
 
3
3
  import { useTranslations } from "next-intl";
4
+ import { Tooltip, TooltipContent, TooltipTrigger } from "../../../shadcnui";
4
5
  import type { Metric } from "../data/tokenusage-admin.types";
5
6
  import { useUsageFormatters } from "../lib/formatters";
6
7
  import { metricValue, type TokenUsageMetrics } from "../lib/metrics";
7
- import { operationLabel } from "../lib/operation-label";
8
+ import { operationDescription, operationLabel } from "../lib/operation-label";
8
9
  import { SEQUENTIAL_RAMP } from "../lib/palette";
9
10
 
10
11
  /**
@@ -65,6 +66,37 @@ export function TokenUsageRankedBar({ rows, metric, emptyLabel, labelsAreOperati
65
66
  );
66
67
  };
67
68
 
69
+ const renderDescription = (row: TokenUsageRankedRow): string | undefined => {
70
+ if (row.id === "other" || !labelsAreOperationTypes) return undefined;
71
+ return operationDescription(
72
+ row.label,
73
+ (key) => t(key),
74
+ (key) => t.has(key),
75
+ );
76
+ };
77
+
78
+ // An operation type the app explains gets its explanation on hover; every
79
+ // other label (names, the folded "other" row) renders as plain text.
80
+ const renderLabelCell = (row: TokenUsageRankedRow) => {
81
+ const label = <span className="text-muted-foreground truncate text-xs">{renderLabel(row)}</span>;
82
+ const description = renderDescription(row);
83
+ if (!description) return label;
84
+ return (
85
+ <Tooltip>
86
+ <TooltipTrigger
87
+ data-testid={`ranked-description-${row.id}`}
88
+ className="min-w-0 cursor-help justify-self-start truncate"
89
+ render={<div />}
90
+ >
91
+ {label}
92
+ </TooltipTrigger>
93
+ <TooltipContent side="top" className="max-w-xs">
94
+ <p className="text-xs">{description}</p>
95
+ </TooltipContent>
96
+ </Tooltip>
97
+ );
98
+ };
99
+
68
100
  if (rows.length === 0) {
69
101
  return <p className="text-muted-foreground py-6 text-center text-sm">{emptyLabel}</p>;
70
102
  }
@@ -89,7 +121,7 @@ export function TokenUsageRankedBar({ rows, metric, emptyLabel, labelsAreOperati
89
121
  return (
90
122
  <div key={row.id} className="grid grid-cols-[minmax(0,1fr)_auto_auto] items-center gap-x-3 gap-y-0.5 py-0.5">
91
123
  <div className="grid min-w-0 gap-0.5">
92
- <span className="text-muted-foreground truncate text-xs">{renderLabel(row)}</span>
124
+ {renderLabelCell(row)}
93
125
  {/* The bar itself is a magnitude drawn on an axis, not prose: it always grows
94
126
  rightward and only its trailing corner is rounded, so the track and its fill
95
127
  stay an LTR island. The label, value and share around it mirror normally.
@@ -40,4 +40,26 @@ describe("TokenUsageRankedBar", () => {
40
40
  expect(new Set(colours).size).toBe(3);
41
41
  expect(colours).toEqual(["0", "1", "2"]);
42
42
  });
43
+
44
+ it("puts the operation description behind a tooltip on operation rows, never on the folded tail", () => {
45
+ render(
46
+ <TokenUsageRankedBar
47
+ rows={[row("blog_translation", "blog_translation", 3), row("other", "other", 1)]}
48
+ metric="cost"
49
+ emptyLabel="nessun dato"
50
+ labelsAreOperationTypes
51
+ />,
52
+ );
53
+
54
+ expect(screen.getByTestId("ranked-description-blog_translation")).toHaveTextContent(
55
+ "token_usage.types.blogTranslation",
56
+ );
57
+ expect(screen.queryByTestId("ranked-description-other")).not.toBeInTheDocument();
58
+ });
59
+
60
+ it("renders entity names without a tooltip", () => {
61
+ render(<TokenUsageRankedBar rows={[row("a", "Studio Rossi", 1)]} metric="cost" emptyLabel="nessun dato" />);
62
+ expect(screen.queryByTestId("ranked-description-a")).not.toBeInTheDocument();
63
+ expect(screen.getByText("Studio Rossi")).toBeInTheDocument();
64
+ });
43
65
  });
@@ -8,6 +8,13 @@
8
8
  */
9
9
  export const OPERATION_LABEL_NAMESPACE = "token_usage.types";
10
10
 
11
+ /**
12
+ * Sibling namespace for an optional one-line explanation of each operation
13
+ * type, keyed exactly like `OPERATION_LABEL_NAMESPACE`. A type without an entry
14
+ * simply has no explanation — nothing falls back to the raw key here.
15
+ */
16
+ export const OPERATION_DESCRIPTION_NAMESPACE = "token_usage.type_descriptions";
17
+
11
18
  /**
12
19
  * `snake_case` / `kebab-case` → `camelCase`, the shape the i18n keys use.
13
20
  *
@@ -39,3 +46,16 @@ export const operationLabel = (
39
46
  const key = `${OPERATION_LABEL_NAMESPACE}.${toCamelCase(type)}`;
40
47
  return has(key) ? translate(key) : type;
41
48
  };
49
+
50
+ /**
51
+ * Resolves an operation type to its explanation, or `undefined` when the
52
+ * consuming app (or the package) provides none.
53
+ */
54
+ export const operationDescription = (
55
+ type: string,
56
+ translate: (key: string) => string,
57
+ has: (key: string) => boolean,
58
+ ): string | undefined => {
59
+ const key = `${OPERATION_DESCRIPTION_NAMESPACE}.${toCamelCase(type)}`;
60
+ return has(key) ? translate(key) : undefined;
61
+ };