@burdenoff/microfe-hookmovies 2026.1031.2 → 2026.1031.4

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 (25) hide show
  1. package/dist/hookmovies/components/RecommendationRail.d.ts +23 -0
  2. package/dist/hookmovies/components/RecommendationRail.d.ts.map +1 -0
  3. package/dist/hookmovies/components/RecommendationRail.js +141 -0
  4. package/dist/hookmovies/components/RecommendationRail.js.map +1 -0
  5. package/dist/hookmovies/components/SearchInterpretation.d.ts +26 -0
  6. package/dist/hookmovies/components/SearchInterpretation.d.ts.map +1 -0
  7. package/dist/hookmovies/components/SearchInterpretation.js +111 -0
  8. package/dist/hookmovies/components/SearchInterpretation.js.map +1 -0
  9. package/dist/hookmovies/components/index.d.ts +1 -0
  10. package/dist/hookmovies/components/index.d.ts.map +1 -1
  11. package/dist/hookmovies/components/index.js +1 -0
  12. package/dist/hookmovies/hooks/useHookMoviesApi.d.ts +112 -0
  13. package/dist/hookmovies/hooks/useHookMoviesApi.d.ts.map +1 -1
  14. package/dist/hookmovies/hooks/useHookMoviesApi.js +165 -91
  15. package/dist/hookmovies/hooks/useHookMoviesApi.js.map +1 -1
  16. package/dist/hookmovies/pages/ai/ConciergePage.d.ts.map +1 -1
  17. package/dist/hookmovies/pages/ai/ConciergePage.js +184 -169
  18. package/dist/hookmovies/pages/ai/ConciergePage.js.map +1 -1
  19. package/dist/hookmovies/pages/consumer/DiscoveryPage.d.ts.map +1 -1
  20. package/dist/hookmovies/pages/consumer/DiscoveryPage.js +244 -206
  21. package/dist/hookmovies/pages/consumer/DiscoveryPage.js.map +1 -1
  22. package/dist/hookmovies/pages/consumer/TitleViewPage.d.ts.map +1 -1
  23. package/dist/hookmovies/pages/consumer/TitleViewPage.js +172 -159
  24. package/dist/hookmovies/pages/consumer/TitleViewPage.js.map +1 -1
  25. package/package.json +1 -1
@@ -0,0 +1,23 @@
1
+ import { FC, ReactNode } from 'react';
2
+ import { HokRecommendation, HokRecommendationSurface, HokRecommendationVariant } from '../hooks/useHookMoviesApi';
3
+
4
+ interface RecommendationRailProps {
5
+ heading: string;
6
+ icon: ReactNode;
7
+ items: HokRecommendation[];
8
+ variant: HokRecommendationVariant;
9
+ surface: HokRecommendationSurface;
10
+ /** Seed title for a "more like this" rail. */
11
+ seedTitleId?: string;
12
+ loading?: boolean;
13
+ /** Optional one-line context under the heading. */
14
+ subheading?: string;
15
+ onOpen: (titleId: string) => void;
16
+ /** Extra classes for the outer section (spacing differs per page). */
17
+ className?: string;
18
+ /** Horizontal padding classes, so the rail aligns with its page gutter. */
19
+ gutterClassName?: string;
20
+ }
21
+ export declare const RecommendationRail: FC<RecommendationRailProps>;
22
+ export {};
23
+ //# sourceMappingURL=RecommendationRail.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RecommendationRail.d.ts","sourceRoot":"","sources":["../../../src/hookmovies/components/RecommendationRail.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,EAAE,KAAK,EAAE,EAAE,KAAK,SAAS,EAAwC,MAAM,OAAO,CAAC;AAItF,OAAO,EACL,KAAK,iBAAiB,EACtB,KAAK,wBAAwB,EAC7B,KAAK,wBAAwB,EAE9B,MAAM,2BAA2B,CAAC;AAInC,UAAU,uBAAuB;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,SAAS,CAAC;IAChB,KAAK,EAAE,iBAAiB,EAAE,CAAC;IAC3B,OAAO,EAAE,wBAAwB,CAAC;IAClC,OAAO,EAAE,wBAAwB,CAAC;IAClC,8CAA8C;IAC9C,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,mDAAmD;IACnD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,sEAAsE;IACtE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,2EAA2E;IAC3E,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AA6FD,eAAO,MAAM,kBAAkB,EAAE,EAAE,CAAC,uBAAuB,CAgF1D,CAAC"}
@@ -0,0 +1,141 @@
1
+ import { useRecordRecommendationEvents as e } from "../hooks/useHookMoviesApi.js";
2
+ import { cn as t } from "../utils/cn.js";
3
+ import { useFileUrl as n } from "../hooks/useFiles.js";
4
+ import { formatEnumLabel as r } from "../utils/formatters.js";
5
+ import { useEffect as i, useMemo as a, useRef as o, useState as s } from "react";
6
+ import { Film as c, Info as l } from "lucide-react";
7
+ import { jsx as u, jsxs as d } from "react/jsx-runtime";
8
+ //#region src/hookmovies/components/RecommendationRail.tsx
9
+ var f = ({ fileId: e, alt: t }) => {
10
+ let { data: r } = n(e), [i, a] = s(!1);
11
+ return e && r && !i ? /* @__PURE__ */ u("img", {
12
+ src: r,
13
+ alt: t,
14
+ loading: "lazy",
15
+ onError: () => a(!0),
16
+ className: "h-full w-full object-cover"
17
+ }) : /* @__PURE__ */ u("div", {
18
+ className: "flex h-full w-full items-center justify-center text-text-muted",
19
+ "aria-hidden": "true",
20
+ children: /* @__PURE__ */ u(c, { className: "h-8 w-8" })
21
+ });
22
+ }, p = ({ item: e, onOpen: n }) => {
23
+ let [i, a] = s(!1), o = e.title;
24
+ if (!o) return null;
25
+ let c = e.reasons[0]?.label ?? null, p = [r(o.type), o.releaseYear ? String(o.releaseYear) : null].filter(Boolean).join(" · ");
26
+ return /* @__PURE__ */ d("div", {
27
+ className: "w-36 shrink-0 snap-start sm:w-44",
28
+ "data-testid": "recommendation-card",
29
+ children: [
30
+ /* @__PURE__ */ d("button", {
31
+ type: "button",
32
+ onClick: n,
33
+ "aria-label": `Open ${o.title}`,
34
+ className: "group block w-full text-left focus:outline-none",
35
+ children: [
36
+ /* @__PURE__ */ u("div", {
37
+ className: t("relative aspect-[2/3] overflow-hidden rounded-xl border border-border-seam bg-bg-sunken", "shadow-elevation-1 transition-[border-color,box-shadow] duration-200", "group-hover:border-border-strong group-hover:shadow-elevation-2", "group-focus-visible:ring-2 group-focus-visible:ring-[var(--color-focus-ring)]"),
38
+ children: /* @__PURE__ */ u("div", {
39
+ className: "absolute inset-0 motion-safe:transition-transform motion-safe:duration-300 motion-safe:group-hover:scale-105",
40
+ children: /* @__PURE__ */ u(f, {
41
+ fileId: o.posterFileId,
42
+ alt: `${o.title} poster`
43
+ })
44
+ })
45
+ }),
46
+ /* @__PURE__ */ u("p", {
47
+ className: "mt-2 line-clamp-1 text-sm font-medium text-text-primary",
48
+ children: o.title
49
+ }),
50
+ p && /* @__PURE__ */ u("p", {
51
+ className: "mt-0.5 line-clamp-1 text-xs text-text-secondary",
52
+ children: p
53
+ })
54
+ ]
55
+ }),
56
+ c && /* @__PURE__ */ d("div", {
57
+ className: "mt-1 flex items-start gap-1",
58
+ children: [/* @__PURE__ */ u("p", {
59
+ className: "line-clamp-2 flex-1 text-xs text-action-primary-bg",
60
+ "data-testid": "recommendation-reason",
61
+ children: c
62
+ }), e.reasons.length > 1 && /* @__PURE__ */ u("button", {
63
+ type: "button",
64
+ onClick: () => a((e) => !e),
65
+ "aria-expanded": i,
66
+ "aria-label": `Why ${o.title} is recommended`,
67
+ className: "rounded text-text-muted transition-colors hover:text-text-primary focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]",
68
+ children: /* @__PURE__ */ u(l, { className: "h-3.5 w-3.5" })
69
+ })]
70
+ }),
71
+ i && /* @__PURE__ */ u("ul", {
72
+ className: "mt-1 space-y-0.5 rounded-lg border border-border-seam bg-bg-surface p-2 text-[11px] text-text-secondary",
73
+ children: e.reasons.map((e) => /* @__PURE__ */ u("li", { children: e.label }, `${e.kind}-${e.relatedTitleId ?? ""}`))
74
+ })
75
+ ]
76
+ });
77
+ }, m = ({ heading: n, icon: r, items: s, variant: c, surface: l, seedTitleId: f, loading: m = !1, subheading: h, onOpen: g, className: _, gutterClassName: v = "px-4 sm:px-6" }) => {
78
+ let { mutate: y } = e(), b = o(null), x = a(() => s.filter((e) => e.title), [s]);
79
+ if (i(() => {
80
+ if (x.length === 0) return;
81
+ let e = `${c}:${l}:${f ?? ""}:${x.map((e) => e.titleId).join(",")}`;
82
+ b.current !== e && (b.current = e, y(x.slice(0, 50).map((e, t) => ({
83
+ titleId: e.titleId,
84
+ variant: c,
85
+ surface: l,
86
+ action: "IMPRESSION",
87
+ seedTitleId: f,
88
+ position: t
89
+ }))));
90
+ }, [
91
+ x,
92
+ c,
93
+ l,
94
+ f,
95
+ y
96
+ ]), !m && x.length === 0) return null;
97
+ let S = (e, t) => {
98
+ y([{
99
+ titleId: e,
100
+ variant: c,
101
+ surface: l,
102
+ action: "CLICK",
103
+ seedTitleId: f,
104
+ position: t
105
+ }]), g(e);
106
+ };
107
+ return /* @__PURE__ */ d("section", {
108
+ className: _,
109
+ "aria-label": n,
110
+ "data-testid": `recommendation-rail-${l.toLowerCase()}`,
111
+ children: [/* @__PURE__ */ d("div", {
112
+ className: t("mb-3", v),
113
+ children: [/* @__PURE__ */ d("div", {
114
+ className: "flex items-center gap-2",
115
+ children: [/* @__PURE__ */ u("span", {
116
+ className: "text-action-primary-bg",
117
+ children: r
118
+ }), /* @__PURE__ */ u("h2", {
119
+ className: "text-base font-semibold text-text-primary sm:text-lg",
120
+ children: n
121
+ })]
122
+ }), h && /* @__PURE__ */ u("p", {
123
+ className: "mt-0.5 text-xs text-text-secondary",
124
+ children: h
125
+ })]
126
+ }), /* @__PURE__ */ u("div", {
127
+ className: t("flex gap-3 overflow-x-auto pb-3 snap-x [scrollbar-width:thin]", v),
128
+ children: m ? Array.from({ length: 6 }).map((e, t) => /* @__PURE__ */ u("div", {
129
+ className: "aspect-[2/3] w-36 shrink-0 rounded-xl border border-border-seam bg-bg-sunken motion-safe:animate-pulse sm:w-44",
130
+ "aria-hidden": "true"
131
+ }, t)) : x.map((e, t) => /* @__PURE__ */ u(p, {
132
+ item: e,
133
+ onOpen: () => S(e.titleId, t)
134
+ }, e.titleId))
135
+ })]
136
+ });
137
+ };
138
+ //#endregion
139
+ export { m as RecommendationRail };
140
+
141
+ //# sourceMappingURL=RecommendationRail.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RecommendationRail.js","names":[],"sources":["../../../src/hookmovies/components/RecommendationRail.tsx"],"sourcesContent":["/**\n * RecommendationRail\n * @module hookmovies/components\n *\n * A horizontally-scrolling shelf of recommended titles (BOFF-4181). Every card\n * shows the strongest reason it was recommended (\"Because you watched X\",\n * \"Fans of Y also liked this\", shared cast …) straight from the ranker's\n * explanation, and an info toggle lists all contributing reasons.\n *\n * The rail also feeds the ranking A/B experiment: it logs one IMPRESSION per\n * rendered item (once per mounted list) and a CLICK when a card is opened,\n * tagged with the experiment arm the server chose. Logging is fire-and-forget\n * and never blocks navigation.\n *\n * Tokenized colors only; motion is `motion-safe:`; the rail scrolls inside its\n * own container so the page never overflows horizontally.\n */\n\nimport { type FC, type ReactNode, useEffect, useMemo, useRef, useState } from 'react';\nimport { Film, Info } from 'lucide-react';\n\nimport { useFileUrl } from '../hooks/useFiles';\nimport {\n type HokRecommendation,\n type HokRecommendationSurface,\n type HokRecommendationVariant,\n useRecordRecommendationEvents,\n} from '../hooks/useHookMoviesApi';\nimport { cn } from '../utils/cn';\nimport { formatEnumLabel } from '../utils/formatters';\n\ninterface RecommendationRailProps {\n heading: string;\n icon: ReactNode;\n items: HokRecommendation[];\n variant: HokRecommendationVariant;\n surface: HokRecommendationSurface;\n /** Seed title for a \"more like this\" rail. */\n seedTitleId?: string;\n loading?: boolean;\n /** Optional one-line context under the heading. */\n subheading?: string;\n onOpen: (titleId: string) => void;\n /** Extra classes for the outer section (spacing differs per page). */\n className?: string;\n /** Horizontal padding classes, so the rail aligns with its page gutter. */\n gutterClassName?: string;\n}\n\nconst Poster: FC<{ fileId: string | null; alt: string }> = ({ fileId, alt }) => {\n const { data: url } = useFileUrl(fileId);\n const [errored, setErrored] = useState(false);\n if (fileId && url && !errored) {\n return (\n <img\n src={url}\n alt={alt}\n loading=\"lazy\"\n onError={() => setErrored(true)}\n className=\"h-full w-full object-cover\"\n />\n );\n }\n return (\n <div\n className=\"flex h-full w-full items-center justify-center text-text-muted\"\n aria-hidden=\"true\"\n >\n <Film className=\"h-8 w-8\" />\n </div>\n );\n};\n\nconst RecommendationCard: FC<{\n item: HokRecommendation;\n onOpen: () => void;\n}> = ({ item, onOpen }) => {\n const [showWhy, setShowWhy] = useState(false);\n const title = item.title;\n if (!title) return null;\n const topReason = item.reasons[0]?.label ?? null;\n const meta = [formatEnumLabel(title.type), title.releaseYear ? String(title.releaseYear) : null]\n .filter(Boolean)\n .join(' · ');\n\n return (\n <div className=\"w-36 shrink-0 snap-start sm:w-44\" data-testid=\"recommendation-card\">\n <button\n type=\"button\"\n onClick={onOpen}\n aria-label={`Open ${title.title}`}\n className=\"group block w-full text-left focus:outline-none\"\n >\n <div\n className={cn(\n 'relative aspect-[2/3] overflow-hidden rounded-xl border border-border-seam bg-bg-sunken',\n 'shadow-elevation-1 transition-[border-color,box-shadow] duration-200',\n 'group-hover:border-border-strong group-hover:shadow-elevation-2',\n 'group-focus-visible:ring-2 group-focus-visible:ring-[var(--color-focus-ring)]'\n )}\n >\n <div className=\"absolute inset-0 motion-safe:transition-transform motion-safe:duration-300 motion-safe:group-hover:scale-105\">\n <Poster fileId={title.posterFileId} alt={`${title.title} poster`} />\n </div>\n </div>\n <p className=\"mt-2 line-clamp-1 text-sm font-medium text-text-primary\">{title.title}</p>\n {meta && <p className=\"mt-0.5 line-clamp-1 text-xs text-text-secondary\">{meta}</p>}\n </button>\n {topReason && (\n <div className=\"mt-1 flex items-start gap-1\">\n <p\n className=\"line-clamp-2 flex-1 text-xs text-action-primary-bg\"\n data-testid=\"recommendation-reason\"\n >\n {topReason}\n </p>\n {item.reasons.length > 1 && (\n <button\n type=\"button\"\n onClick={() => setShowWhy((v) => !v)}\n aria-expanded={showWhy}\n aria-label={`Why ${title.title} is recommended`}\n className=\"rounded text-text-muted transition-colors hover:text-text-primary focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]\"\n >\n <Info className=\"h-3.5 w-3.5\" />\n </button>\n )}\n </div>\n )}\n {showWhy && (\n <ul className=\"mt-1 space-y-0.5 rounded-lg border border-border-seam bg-bg-surface p-2 text-[11px] text-text-secondary\">\n {item.reasons.map((reason) => (\n <li key={`${reason.kind}-${reason.relatedTitleId ?? ''}`}>{reason.label}</li>\n ))}\n </ul>\n )}\n </div>\n );\n};\n\nexport const RecommendationRail: FC<RecommendationRailProps> = ({\n heading,\n icon,\n items,\n variant,\n surface,\n seedTitleId,\n loading = false,\n subheading,\n onOpen,\n className,\n gutterClassName = 'px-4 sm:px-6',\n}) => {\n const recordEvents = useRecordRecommendationEvents();\n const { mutate } = recordEvents;\n const loggedKey = useRef<string | null>(null);\n const visible = useMemo(() => items.filter((item) => item.title), [items]);\n\n // One IMPRESSION batch per distinct rendered list.\n useEffect(() => {\n if (visible.length === 0) return;\n const key = `${variant}:${surface}:${seedTitleId ?? ''}:${visible.map((i) => i.titleId).join(',')}`;\n if (loggedKey.current === key) return;\n loggedKey.current = key;\n mutate(\n visible.slice(0, 50).map((item, position) => ({\n titleId: item.titleId,\n variant,\n surface,\n action: 'IMPRESSION' as const,\n seedTitleId,\n position,\n }))\n );\n }, [visible, variant, surface, seedTitleId, mutate]);\n\n if (!loading && visible.length === 0) return null;\n\n const open = (titleId: string, position: number) => {\n mutate([{ titleId, variant, surface, action: 'CLICK', seedTitleId, position }]);\n onOpen(titleId);\n };\n\n return (\n <section\n className={className}\n aria-label={heading}\n data-testid={`recommendation-rail-${surface.toLowerCase()}`}\n >\n <div className={cn('mb-3', gutterClassName)}>\n <div className=\"flex items-center gap-2\">\n <span className=\"text-action-primary-bg\">{icon}</span>\n <h2 className=\"text-base font-semibold text-text-primary sm:text-lg\">{heading}</h2>\n </div>\n {subheading && <p className=\"mt-0.5 text-xs text-text-secondary\">{subheading}</p>}\n </div>\n <div\n className={cn(\n 'flex gap-3 overflow-x-auto pb-3 snap-x [scrollbar-width:thin]',\n gutterClassName\n )}\n >\n {loading\n ? Array.from({ length: 6 }).map((_, i) => (\n <div\n key={i}\n className=\"aspect-[2/3] w-36 shrink-0 rounded-xl border border-border-seam bg-bg-sunken motion-safe:animate-pulse sm:w-44\"\n aria-hidden=\"true\"\n />\n ))\n : visible.map((item, position) => (\n <RecommendationCard\n key={item.titleId}\n item={item}\n onOpen={() => open(item.titleId, position)}\n />\n ))}\n </div>\n </section>\n );\n};\n"],"mappings":"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"}
@@ -0,0 +1,26 @@
1
+ import { FC } from 'react';
2
+ import { HokSearchInterpretation } from '../hooks/useHookMoviesApi';
3
+
4
+ /** Display name for an ISO 639-1 code ("ta" → "Tamil"); falls back to the code. */
5
+ export declare function languageName(code: string): string;
6
+ /** Human label for a runtime range in minutes. */
7
+ export declare function runtimeLabel(min: number | null, max: number | null): string | null;
8
+ /** Human label for a release-year range. */
9
+ export declare function eraLabel(from: number | null, to: number | null): string | null;
10
+ interface Chip {
11
+ key: string;
12
+ label: string;
13
+ icon: FC<{
14
+ className?: string;
15
+ }>;
16
+ tone: 'constraint' | 'concept' | 'exclude';
17
+ }
18
+ /** Flatten an interpretation into the chips shown to the viewer. */
19
+ export declare function interpretationChips(interpretation: HokSearchInterpretation): Chip[];
20
+ interface SearchInterpretationProps {
21
+ interpretation: HokSearchInterpretation;
22
+ className?: string;
23
+ }
24
+ export declare const SearchInterpretation: FC<SearchInterpretationProps>;
25
+ export {};
26
+ //# sourceMappingURL=SearchInterpretation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SearchInterpretation.d.ts","sourceRoot":"","sources":["../../../src/hookmovies/components/SearchInterpretation.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC;AAYhC,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,2BAA2B,CAAC;AAIzE,mFAAmF;AACnF,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAOjD;AAED,kDAAkD;AAClD,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,GAAG,IAAI,CAKlF;AAED,4CAA4C;AAC5C,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,EAAE,EAAE,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,GAAG,IAAI,CAK9E;AAED,UAAU,IAAI;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,EAAE,CAAC;QAAE,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACjC,IAAI,EAAE,YAAY,GAAG,SAAS,GAAG,SAAS,CAAC;CAC5C;AAED,oEAAoE;AACpE,wBAAgB,mBAAmB,CAAC,cAAc,EAAE,uBAAuB,GAAG,IAAI,EAAE,CAqDnF;AAQD,UAAU,yBAAyB;IACjC,cAAc,EAAE,uBAAuB,CAAC;IACxC,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,eAAO,MAAM,oBAAoB,EAAE,EAAE,CAAC,yBAAyB,CAgC9D,CAAC"}
@@ -0,0 +1,111 @@
1
+ import { cn as e } from "../utils/cn.js";
2
+ import { formatEnumLabel as t } from "../utils/formatters.js";
3
+ import { Ban as n, CalendarRange as r, Clock as i, Film as a, Languages as o, ShieldCheck as s, Sparkles as c, User as l } from "lucide-react";
4
+ import { jsx as u, jsxs as d } from "react/jsx-runtime";
5
+ //#region src/hookmovies/components/SearchInterpretation.tsx
6
+ function f(e) {
7
+ try {
8
+ return new Intl.DisplayNames(["en"], { type: "language" }).of(e) ?? e;
9
+ } catch {
10
+ return e;
11
+ }
12
+ }
13
+ function p(e, t) {
14
+ return e !== null && t !== null ? `${e}–${t} min` : t === null ? e === null ? null : `Over ${e} min` : `Under ${t} min`;
15
+ }
16
+ function m(e, t) {
17
+ return e !== null && t !== null ? e === t ? String(e) : `${e}–${t}` : e === null ? t === null ? null : `${t} or earlier` : `${e} or later`;
18
+ }
19
+ function h(e) {
20
+ let u = [];
21
+ for (let t of e.languages) u.push({
22
+ key: `lang-${t}`,
23
+ label: f(t),
24
+ icon: o,
25
+ tone: "constraint"
26
+ });
27
+ for (let t of e.excludeLanguages) u.push({
28
+ key: `xlang-${t}`,
29
+ label: `Not ${f(t)}`,
30
+ icon: n,
31
+ tone: "exclude"
32
+ });
33
+ for (let t of e.subtitleLanguages) u.push({
34
+ key: `sub-${t}`,
35
+ label: `${f(t)} subtitles`,
36
+ icon: o,
37
+ tone: "constraint"
38
+ });
39
+ for (let n of e.types) u.push({
40
+ key: `type-${n}`,
41
+ label: t(n),
42
+ icon: a,
43
+ tone: "constraint"
44
+ });
45
+ let d = p(e.minRuntimeMinutes, e.maxRuntimeMinutes);
46
+ d && u.push({
47
+ key: "runtime",
48
+ label: d,
49
+ icon: i,
50
+ tone: "constraint"
51
+ });
52
+ let h = m(e.releaseYearFrom, e.releaseYearTo);
53
+ h && u.push({
54
+ key: "era",
55
+ label: h,
56
+ icon: r,
57
+ tone: "constraint"
58
+ }), e.familyFriendly && u.push({
59
+ key: "family",
60
+ label: "Family friendly",
61
+ icon: s,
62
+ tone: "constraint"
63
+ });
64
+ for (let n of e.concepts) u.push({
65
+ key: `concept-${n}`,
66
+ label: t(n.replace(/-/g, "_")),
67
+ icon: c,
68
+ tone: "concept"
69
+ });
70
+ for (let t of e.matchedPeople) u.push({
71
+ key: `person-${t.id}`,
72
+ label: t.name,
73
+ icon: l,
74
+ tone: "concept"
75
+ });
76
+ return u;
77
+ }
78
+ var g = {
79
+ constraint: "border-border-subtle bg-bg-surface text-text-primary",
80
+ concept: "border-border-subtle bg-bg-sunken text-text-secondary",
81
+ exclude: "border-status-warning-border bg-status-warning-bg-subtle text-status-warning-text"
82
+ }, _ = ({ interpretation: t, className: n }) => {
83
+ let r = h(t);
84
+ return r.length === 0 && !t.transliteratedQuery ? null : /* @__PURE__ */ d("div", {
85
+ className: e("flex flex-wrap items-center gap-1.5", n),
86
+ "aria-label": "How your search was understood",
87
+ "data-testid": "search-interpretation",
88
+ children: [
89
+ /* @__PURE__ */ u("span", {
90
+ className: "text-xs text-text-muted",
91
+ children: "Understood as"
92
+ }),
93
+ t.transliteratedQuery && /* @__PURE__ */ d("span", {
94
+ className: "inline-flex items-center gap-1 rounded-full border border-border-subtle bg-bg-sunken px-2 py-0.5 text-xs italic text-text-secondary",
95
+ children: [
96
+ "“",
97
+ t.transliteratedQuery,
98
+ "”"
99
+ ]
100
+ }),
101
+ r.map(({ key: t, label: n, icon: r, tone: i }) => /* @__PURE__ */ d("span", {
102
+ className: e("inline-flex items-center gap-1 rounded-full border px-2 py-0.5 text-xs font-medium", g[i]),
103
+ children: [/* @__PURE__ */ u(r, { className: "h-3 w-3" }), n]
104
+ }, t))
105
+ ]
106
+ });
107
+ };
108
+ //#endregion
109
+ export { _ as SearchInterpretation };
110
+
111
+ //# sourceMappingURL=SearchInterpretation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SearchInterpretation.js","names":[],"sources":["../../../src/hookmovies/components/SearchInterpretation.tsx"],"sourcesContent":["/**\n * Search interpretation chips (BOFF-4187)\n * @module hookmovies/components\n *\n * Shows how `hokSemanticSearch` read a natural-language query — languages,\n * excluded languages, runtime, era, type, family-safety, recognised moods and\n * genres, and the people it resolved — so a viewer can see (and rephrase) the\n * interpretation instead of guessing why a title matched.\n *\n * Fully tokenized (fe-libs semantic tokens only), wraps on narrow screens.\n */\n\nimport type { FC } from 'react';\nimport {\n Languages,\n Clock,\n CalendarRange,\n Sparkles,\n User,\n Ban,\n ShieldCheck,\n Film,\n} from 'lucide-react';\n\nimport type { HokSearchInterpretation } from '../hooks/useHookMoviesApi';\nimport { cn } from '../utils/cn';\nimport { formatEnumLabel } from '../utils/formatters';\n\n/** Display name for an ISO 639-1 code (\"ta\" → \"Tamil\"); falls back to the code. */\nexport function languageName(code: string): string {\n try {\n const names = new Intl.DisplayNames(['en'], { type: 'language' });\n return names.of(code) ?? code;\n } catch {\n return code;\n }\n}\n\n/** Human label for a runtime range in minutes. */\nexport function runtimeLabel(min: number | null, max: number | null): string | null {\n if (min !== null && max !== null) return `${min}–${max} min`;\n if (max !== null) return `Under ${max} min`;\n if (min !== null) return `Over ${min} min`;\n return null;\n}\n\n/** Human label for a release-year range. */\nexport function eraLabel(from: number | null, to: number | null): string | null {\n if (from !== null && to !== null) return from === to ? String(from) : `${from}–${to}`;\n if (from !== null) return `${from} or later`;\n if (to !== null) return `${to} or earlier`;\n return null;\n}\n\ninterface Chip {\n key: string;\n label: string;\n icon: FC<{ className?: string }>;\n tone: 'constraint' | 'concept' | 'exclude';\n}\n\n/** Flatten an interpretation into the chips shown to the viewer. */\nexport function interpretationChips(interpretation: HokSearchInterpretation): Chip[] {\n const chips: Chip[] = [];\n for (const code of interpretation.languages) {\n chips.push({\n key: `lang-${code}`,\n label: languageName(code),\n icon: Languages,\n tone: 'constraint',\n });\n }\n for (const code of interpretation.excludeLanguages) {\n chips.push({\n key: `xlang-${code}`,\n label: `Not ${languageName(code)}`,\n icon: Ban,\n tone: 'exclude',\n });\n }\n for (const code of interpretation.subtitleLanguages) {\n chips.push({\n key: `sub-${code}`,\n label: `${languageName(code)} subtitles`,\n icon: Languages,\n tone: 'constraint',\n });\n }\n for (const type of interpretation.types) {\n chips.push({\n key: `type-${type}`,\n label: formatEnumLabel(type),\n icon: Film,\n tone: 'constraint',\n });\n }\n const runtime = runtimeLabel(interpretation.minRuntimeMinutes, interpretation.maxRuntimeMinutes);\n if (runtime) chips.push({ key: 'runtime', label: runtime, icon: Clock, tone: 'constraint' });\n const era = eraLabel(interpretation.releaseYearFrom, interpretation.releaseYearTo);\n if (era) chips.push({ key: 'era', label: era, icon: CalendarRange, tone: 'constraint' });\n if (interpretation.familyFriendly) {\n chips.push({ key: 'family', label: 'Family friendly', icon: ShieldCheck, tone: 'constraint' });\n }\n for (const concept of interpretation.concepts) {\n chips.push({\n key: `concept-${concept}`,\n label: formatEnumLabel(concept.replace(/-/g, '_')),\n icon: Sparkles,\n tone: 'concept',\n });\n }\n for (const person of interpretation.matchedPeople) {\n chips.push({ key: `person-${person.id}`, label: person.name, icon: User, tone: 'concept' });\n }\n return chips;\n}\n\nconst toneStyles: Record<Chip['tone'], string> = {\n constraint: 'border-border-subtle bg-bg-surface text-text-primary',\n concept: 'border-border-subtle bg-bg-sunken text-text-secondary',\n exclude: 'border-status-warning-border bg-status-warning-bg-subtle text-status-warning-text',\n};\n\ninterface SearchInterpretationProps {\n interpretation: HokSearchInterpretation;\n className?: string;\n}\n\nexport const SearchInterpretation: FC<SearchInterpretationProps> = ({\n interpretation,\n className,\n}) => {\n const chips = interpretationChips(interpretation);\n if (chips.length === 0 && !interpretation.transliteratedQuery) return null;\n return (\n <div\n className={cn('flex flex-wrap items-center gap-1.5', className)}\n aria-label=\"How your search was understood\"\n data-testid=\"search-interpretation\"\n >\n <span className=\"text-xs text-text-muted\">Understood as</span>\n {interpretation.transliteratedQuery && (\n <span className=\"inline-flex items-center gap-1 rounded-full border border-border-subtle bg-bg-sunken px-2 py-0.5 text-xs italic text-text-secondary\">\n “{interpretation.transliteratedQuery}”\n </span>\n )}\n {chips.map(({ key, label, icon: Icon, tone }) => (\n <span\n key={key}\n className={cn(\n 'inline-flex items-center gap-1 rounded-full border px-2 py-0.5 text-xs font-medium',\n toneStyles[tone]\n )}\n >\n <Icon className=\"h-3 w-3\" />\n {label}\n </span>\n ))}\n </div>\n );\n};\n"],"mappings":";;;;;AA6BA,SAAgB,EAAa,GAAsB;AACjD,KAAI;AAEF,SADc,IAAI,KAAK,aAAa,CAAC,KAAK,EAAE,EAAE,MAAM,YAAY,CAAC,CACpD,GAAG,EAAK,IAAI;SACnB;AACN,SAAO;;;AAKX,SAAgB,EAAa,GAAoB,GAAmC;AAIlF,QAHI,MAAQ,QAAQ,MAAQ,OAAa,GAAG,EAAI,GAAG,EAAI,QACnD,MAAQ,OACR,MAAQ,OACL,OADkB,QAAQ,EAAI,QADZ,SAAS,EAAI;;AAMxC,SAAgB,EAAS,GAAqB,GAAkC;AAI9E,QAHI,MAAS,QAAQ,MAAO,OAAa,MAAS,IAAK,OAAO,EAAK,GAAG,GAAG,EAAK,GAAG,MAC7E,MAAS,OACT,MAAO,OACJ,OADiB,GAAG,EAAG,eADJ,GAAG,EAAK;;AAapC,SAAgB,EAAoB,GAAiD;CACnF,IAAM,IAAgB,EAAE;AACxB,MAAK,IAAM,KAAQ,EAAe,UAChC,GAAM,KAAK;EACT,KAAK,QAAQ;EACb,OAAO,EAAa,EAAK;EACzB,MAAM;EACN,MAAM;EACP,CAAC;AAEJ,MAAK,IAAM,KAAQ,EAAe,iBAChC,GAAM,KAAK;EACT,KAAK,SAAS;EACd,OAAO,OAAO,EAAa,EAAK;EAChC,MAAM;EACN,MAAM;EACP,CAAC;AAEJ,MAAK,IAAM,KAAQ,EAAe,kBAChC,GAAM,KAAK;EACT,KAAK,OAAO;EACZ,OAAO,GAAG,EAAa,EAAK,CAAC;EAC7B,MAAM;EACN,MAAM;EACP,CAAC;AAEJ,MAAK,IAAM,KAAQ,EAAe,MAChC,GAAM,KAAK;EACT,KAAK,QAAQ;EACb,OAAO,EAAgB,EAAK;EAC5B,MAAM;EACN,MAAM;EACP,CAAC;CAEJ,IAAM,IAAU,EAAa,EAAe,mBAAmB,EAAe,kBAAkB;AAChG,CAAI,KAAS,EAAM,KAAK;EAAE,KAAK;EAAW,OAAO;EAAS,MAAM;EAAO,MAAM;EAAc,CAAC;CAC5F,IAAM,IAAM,EAAS,EAAe,iBAAiB,EAAe,cAAc;AAElF,CADI,KAAK,EAAM,KAAK;EAAE,KAAK;EAAO,OAAO;EAAK,MAAM;EAAe,MAAM;EAAc,CAAC,EACpF,EAAe,kBACjB,EAAM,KAAK;EAAE,KAAK;EAAU,OAAO;EAAmB,MAAM;EAAa,MAAM;EAAc,CAAC;AAEhG,MAAK,IAAM,KAAW,EAAe,SACnC,GAAM,KAAK;EACT,KAAK,WAAW;EAChB,OAAO,EAAgB,EAAQ,QAAQ,MAAM,IAAI,CAAC;EAClD,MAAM;EACN,MAAM;EACP,CAAC;AAEJ,MAAK,IAAM,KAAU,EAAe,cAClC,GAAM,KAAK;EAAE,KAAK,UAAU,EAAO;EAAM,OAAO,EAAO;EAAM,MAAM;EAAM,MAAM;EAAW,CAAC;AAE7F,QAAO;;AAGT,IAAM,IAA2C;CAC/C,YAAY;CACZ,SAAS;CACT,SAAS;CACV,EAOY,KAAuD,EAClE,mBACA,mBACI;CACJ,IAAM,IAAQ,EAAoB,EAAe;AAEjD,QADI,EAAM,WAAW,KAAK,CAAC,EAAe,sBAA4B,OAEpE,kBAAC,OAAD;EACE,WAAW,EAAG,uCAAuC,EAAU;EAC/D,cAAW;EACX,eAAY;YAHd;GAKE,kBAAC,QAAD;IAAM,WAAU;cAA0B;IAAoB,CAAA;GAC7D,EAAe,uBACd,kBAAC,QAAD;IAAM,WAAU;cAAhB;KAAsJ;KAClJ,EAAe;KAAoB;KAChC;;GAER,EAAM,KAAK,EAAE,QAAK,UAAO,MAAM,GAAM,cACpC,kBAAC,QAAD;IAEE,WAAW,EACT,sFACA,EAAW,GACZ;cALH,CAOE,kBAAC,GAAD,EAAM,WAAU,WAAY,CAAA,EAC3B,EACI;MARA,EAQA,CACP;GACE"}
@@ -10,4 +10,5 @@ export { EmptyState } from './EmptyState';
10
10
  export { LoadingState, ErrorState } from './StateViews';
11
11
  export { Pagination } from './Pagination';
12
12
  export { AddToListButton } from './AddToListButton';
13
+ export { RecommendationRail } from './RecommendationRail';
13
14
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/hookmovies/components/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AACxD,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/hookmovies/components/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AACxD,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC"}
@@ -6,3 +6,4 @@ import "./EmptyState.js";
6
6
  import "./Pagination.js";
7
7
  import "./RatingDisplay.js";
8
8
  import "./AddToListButton.js";
9
+ import "./RecommendationRail.js";
@@ -276,6 +276,63 @@ export interface HokTitleFilters {
276
276
  studioId?: string;
277
277
  }
278
278
  export declare const useTitles: (filters?: HokTitleFilters, pagination?: HokPaginationInput, search?: string) => import('@tanstack/react-query').UseQueryResult<NoInfer<HokConnection<HokTitle>>, Error>;
279
+ /** Explicit constraints merged with whatever the query text implies. */
280
+ export interface HokSemanticSearchFilters {
281
+ types?: HokTitleType[];
282
+ statuses?: HokTitleStatus[];
283
+ languages?: string[];
284
+ excludeLanguages?: string[];
285
+ genres?: string[];
286
+ minRuntimeMinutes?: number;
287
+ maxRuntimeMinutes?: number;
288
+ releaseYearFrom?: number;
289
+ releaseYearTo?: number;
290
+ studioId?: string;
291
+ personId?: string;
292
+ }
293
+ /** How the search read the query — every constraint and concept it applied. */
294
+ export interface HokSearchInterpretation {
295
+ query: string;
296
+ transliteratedQuery: string | null;
297
+ keywords: string[];
298
+ concepts: string[];
299
+ languages: string[];
300
+ excludeLanguages: string[];
301
+ subtitleLanguages: string[];
302
+ originCountries: string[];
303
+ types: HokTitleType[];
304
+ minRuntimeMinutes: number | null;
305
+ maxRuntimeMinutes: number | null;
306
+ releaseYearFrom: number | null;
307
+ releaseYearTo: number | null;
308
+ familyFriendly: boolean;
309
+ matchedPeople: Array<Pick<HokPerson, 'id' | 'name'>>;
310
+ }
311
+ export interface HokSemanticSearchHit {
312
+ title: HokTitle;
313
+ score: number;
314
+ matchedConcepts: string[];
315
+ matchedKeywords: string[];
316
+ matchedLanguage: string | null;
317
+ matchedPeople: Array<Pick<HokPerson, 'id' | 'name'>>;
318
+ }
319
+ export interface HokSemanticSearchResult {
320
+ hits: HokSemanticSearchHit[];
321
+ interpretation: HokSearchInterpretation;
322
+ candidateCount: number;
323
+ totalMatches: number;
324
+ truncated: boolean;
325
+ }
326
+ /**
327
+ * Natural-language, cross-script catalog search (`hokSemanticSearch`).
328
+ *
329
+ * Understands moods/genres, languages ("Tamil", "not in English"), runtime
330
+ * ("under 2 hours"), era and family-safety, matches titles across scripts and
331
+ * localisations, and boosts titles crediting people the query names. Disabled
332
+ * while `query` is blank. Callers fall back to keyword `useTitles` search when
333
+ * this errors (for example when the `hookmovies.semantic_search` flag is off).
334
+ */
335
+ export declare const useSemanticSearch: (query: string | undefined, filters?: HokSemanticSearchFilters, limit?: number) => import('@tanstack/react-query').UseQueryResult<NoInfer<HokSemanticSearchResult>, Error>;
279
336
  export declare const useTitle: (id: string | undefined) => import('@tanstack/react-query').UseQueryResult<NoInfer<HokTitleDetail | null>, Error>;
280
337
  /**
281
338
  * Resolve an arbitrary set of title ids in ONE round trip.
@@ -1187,5 +1244,60 @@ export interface SetRoyaltyRevenuePoolInput {
1187
1244
  export declare const useSetRoyaltyRevenuePool: () => import('@tanstack/react-query').UseMutationResult<{
1188
1245
  id: string;
1189
1246
  }, Error, SetRoyaltyRevenuePoolInput, unknown>;
1247
+ export type HokRecommendationVariant = 'PERSONALIZED' | 'BASELINE';
1248
+ export type HokRecommendationSurface = 'HOME' | 'TITLE';
1249
+ export type HokRecommendationAction = 'IMPRESSION' | 'CLICK';
1250
+ export type HokRecommendationReasonKind = 'SHARED_CAST' | 'SHARED_CREW' | 'SAME_STUDIO' | 'SHARED_GENRE' | 'SHARED_TAG' | 'SAME_LANGUAGE' | 'SAME_TYPE' | 'SIMILAR_ERA' | 'BECAUSE_YOU_WATCHED' | 'FANS_ALSO_LIKED' | 'PREFERRED_LANGUAGE' | 'PREFERRED_TYPE' | 'POPULAR' | 'NEW_RELEASE';
1251
+ export interface HokRecommendationReason {
1252
+ kind: HokRecommendationReasonKind;
1253
+ label: string;
1254
+ weight: number;
1255
+ relatedTitleId: string | null;
1256
+ relatedTitleName: string | null;
1257
+ }
1258
+ export interface HokRecommendation {
1259
+ titleId: string;
1260
+ score: number;
1261
+ reasons: HokRecommendationReason[];
1262
+ title: HokTitle | null;
1263
+ }
1264
+ export interface HokRecommendationFeed {
1265
+ variant: HokRecommendationVariant;
1266
+ coldStart: boolean;
1267
+ seedCount: number;
1268
+ generatedAt: string;
1269
+ items: HokRecommendation[];
1270
+ }
1271
+ export interface HokRecommendationEventInput {
1272
+ titleId: string;
1273
+ variant: HokRecommendationVariant;
1274
+ surface: HokRecommendationSurface;
1275
+ action: HokRecommendationAction;
1276
+ seedTitleId?: string;
1277
+ position?: number;
1278
+ }
1279
+ export interface HokRecommendationArmStats {
1280
+ variant: HokRecommendationVariant;
1281
+ impressions: number;
1282
+ clicks: number;
1283
+ clickThroughRate: number;
1284
+ viewers: number;
1285
+ }
1286
+ export interface HokRecommendationExperimentSummary {
1287
+ sinceDays: number;
1288
+ baselinePercent: number;
1289
+ arms: HokRecommendationArmStats[];
1290
+ }
1291
+ /** Personalised "for you" list for the signed-in viewer (self-scoped). */
1292
+ export declare const useRecommendForViewer: (limit?: number) => import('@tanstack/react-query').UseQueryResult<NoInfer<HokRecommendationFeed>, Error>;
1293
+ /** "More like this" for a title. */
1294
+ export declare const useSimilarTitles: (titleId: string | undefined, limit?: number) => import('@tanstack/react-query').UseQueryResult<NoInfer<HokRecommendation[]>, Error>;
1295
+ /**
1296
+ * Record recommendation impressions / clicks for the ranking A/B experiment.
1297
+ * Fire-and-forget: a failed log never affects browsing.
1298
+ */
1299
+ export declare const useRecordRecommendationEvents: () => import('@tanstack/react-query').UseMutationResult<number, Error, HokRecommendationEventInput[], unknown>;
1300
+ /** Click-through per experiment arm (studio analytics). */
1301
+ export declare const useRecommendationExperimentSummary: (sinceDays?: number) => import('@tanstack/react-query').UseQueryResult<NoInfer<HokRecommendationExperimentSummary>, Error>;
1190
1302
  export {};
1191
1303
  //# sourceMappingURL=useHookMoviesApi.d.ts.map