@burdenoff/microfe-hookmovies 2026.706.2 → 2026.706.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 (62) hide show
  1. package/dist/hookmovies/HookMoviesRoutes.js +54 -6
  2. package/dist/hookmovies/HookMoviesRoutes.js.map +1 -1
  3. package/dist/hookmovies/components/index.js +1 -1
  4. package/dist/hookmovies/context/HookMoviesContext.js +64 -40
  5. package/dist/hookmovies/context/HookMoviesContext.js.map +1 -1
  6. package/dist/hookmovies/hooks/useHookMoviesApi.js +592 -134
  7. package/dist/hookmovies/hooks/useHookMoviesApi.js.map +1 -1
  8. package/dist/hookmovies/pages/HomePage.js +211 -139
  9. package/dist/hookmovies/pages/HomePage.js.map +1 -1
  10. package/dist/hookmovies/pages/ai/ConciergePage.js +330 -0
  11. package/dist/hookmovies/pages/ai/ConciergePage.js.map +1 -0
  12. package/dist/hookmovies/pages/analytics/AnalyticsPage.js +450 -0
  13. package/dist/hookmovies/pages/analytics/AnalyticsPage.js.map +1 -0
  14. package/dist/hookmovies/pages/analytics/PayoutsPage.js +499 -0
  15. package/dist/hookmovies/pages/analytics/PayoutsPage.js.map +1 -0
  16. package/dist/hookmovies/pages/auditions/AuditionDetailPage.js +223 -140
  17. package/dist/hookmovies/pages/auditions/AuditionDetailPage.js.map +1 -1
  18. package/dist/hookmovies/pages/auditions/AuditionsPage.js +161 -83
  19. package/dist/hookmovies/pages/auditions/AuditionsPage.js.map +1 -1
  20. package/dist/hookmovies/pages/catalog/PeoplePage.js +177 -103
  21. package/dist/hookmovies/pages/catalog/PeoplePage.js.map +1 -1
  22. package/dist/hookmovies/pages/catalog/StudiosPage.js +191 -108
  23. package/dist/hookmovies/pages/catalog/StudiosPage.js.map +1 -1
  24. package/dist/hookmovies/pages/catalog/TitleDetailPage.js +744 -622
  25. package/dist/hookmovies/pages/catalog/TitleDetailPage.js.map +1 -1
  26. package/dist/hookmovies/pages/catalog/TitlesPage.js +218 -138
  27. package/dist/hookmovies/pages/catalog/TitlesPage.js.map +1 -1
  28. package/dist/hookmovies/pages/commerce/CheckoutPage.js +430 -0
  29. package/dist/hookmovies/pages/commerce/CheckoutPage.js.map +1 -0
  30. package/dist/hookmovies/pages/consumer/DiscoveryPage.js +21 -21
  31. package/dist/hookmovies/pages/consumer/HooksPage.js +5 -5
  32. package/dist/hookmovies/pages/consumer/TitleViewPage.js +32 -32
  33. package/dist/hookmovies/pages/consumer/TitleViewPage.js.map +1 -1
  34. package/dist/hookmovies/pages/developer/DevPortalPage.js +815 -0
  35. package/dist/hookmovies/pages/developer/DevPortalPage.js.map +1 -0
  36. package/dist/hookmovies/pages/distribution/RightsWindowsPage.js +235 -162
  37. package/dist/hookmovies/pages/distribution/RightsWindowsPage.js.map +1 -1
  38. package/dist/hookmovies/pages/engagement/ReviewsPage.js +201 -143
  39. package/dist/hookmovies/pages/engagement/ReviewsPage.js.map +1 -1
  40. package/dist/hookmovies/pages/library/EntitlementsPage.js +447 -221
  41. package/dist/hookmovies/pages/library/EntitlementsPage.js.map +1 -1
  42. package/dist/hookmovies/pages/library/ListsPage.js +243 -167
  43. package/dist/hookmovies/pages/library/ListsPage.js.map +1 -1
  44. package/dist/hookmovies/pages/live/LivePage.js +608 -0
  45. package/dist/hookmovies/pages/live/LivePage.js.map +1 -0
  46. package/dist/hookmovies/pages/localization/LocalizationPage.js +656 -0
  47. package/dist/hookmovies/pages/localization/LocalizationPage.js.map +1 -0
  48. package/dist/hookmovies/pages/onboarding/OnboardingPage.js +366 -0
  49. package/dist/hookmovies/pages/onboarding/OnboardingPage.js.map +1 -0
  50. package/dist/hookmovies/pages/productions/ProductionDetailPage.js +265 -179
  51. package/dist/hookmovies/pages/productions/ProductionDetailPage.js.map +1 -1
  52. package/dist/hookmovies/pages/productions/ProductionsPage.js +264 -71
  53. package/dist/hookmovies/pages/productions/ProductionsPage.js.map +1 -1
  54. package/dist/hookmovies/pages/social/ProfilesPage.js +701 -0
  55. package/dist/hookmovies/pages/social/ProfilesPage.js.map +1 -0
  56. package/dist/hookmovies/pages/social/WatchPartyListPage.js +281 -0
  57. package/dist/hookmovies/pages/social/WatchPartyListPage.js.map +1 -0
  58. package/dist/hookmovies/pages/social/WatchPartyRoomPage.js +567 -0
  59. package/dist/hookmovies/pages/social/WatchPartyRoomPage.js.map +1 -0
  60. package/dist/hookmovies/pages/trust/ModerationPage.js +320 -0
  61. package/dist/hookmovies/pages/trust/ModerationPage.js.map +1 -0
  62. package/package.json +1 -1
@@ -0,0 +1,330 @@
1
+ import { useHookMovies as e } from "../../context/HookMoviesContext.js";
2
+ import { cn as t } from "../../utils/cn.js";
3
+ import { PageLayout as n } from "../../components/PageLayout.js";
4
+ import { useTitles as r } from "../../hooks/useHookMoviesApi.js";
5
+ import { useFileUrl as i } from "../../hooks/useFiles.js";
6
+ import { formatEnumLabel as a } from "../../utils/formatters.js";
7
+ import { useCallback as o, useEffect as s, useMemo as c, useRef as l, useState as u } from "react";
8
+ import { AlertCircle as d, Film as f, Info as p, MessageSquareText as m, RotateCcw as h, Send as g, Sparkles as _, Wand2 as v } from "lucide-react";
9
+ import { jsx as y, jsxs as b } from "react/jsx-runtime";
10
+ import { GlassCard as x } from "@burdenoff/fe-libs/ui";
11
+ //#region src/hookmovies/pages/ai/ConciergePage.tsx
12
+ var S = {
13
+ limit: 8,
14
+ offset: 0
15
+ }, C = 700, w = "Simulated response — not a live model";
16
+ function T(e) {
17
+ let { prompt: t, titles: n, failed: r } = e;
18
+ if (r) return {
19
+ text: "I hit a snag reaching the catalog just now, so I couldn't pull recommendations for that. Give it another try in a moment.",
20
+ note: w
21
+ };
22
+ let i = t.trim().toLowerCase(), a = /^(hi|hey|hello|yo|namaste|hola|good (morning|evening|afternoon))\b/.test(i);
23
+ if (n.length === 0) return a ? {
24
+ text: "Hi! I'm your watch-concierge. Tell me a mood, a genre, or a title you loved and I'll surface something from the catalog to match.",
25
+ note: w
26
+ } : {
27
+ text: `I couldn't find anything published that matches “${t.trim()}” yet. Try a broader mood — like “something funny”, “a gripping thriller”, or “short films” — and I'll look again.`,
28
+ note: w
29
+ };
30
+ let o = n.length, s = o === 1 ? "title" : "titles", c = [
31
+ `Great pick of a mood. Based on what's published in your catalog, here ${o === 1 ? "is" : "are"} ${o} ${s} I think you'll enjoy:`,
32
+ `Say no more — I pulled ${o} real ${s} from the published catalog that fit that vibe:`,
33
+ `Here's what's streaming that lines up with that. ${o} ${s} worth a look:`,
34
+ `On it. These ${o} ${s} from your catalog match what you're after:`
35
+ ];
36
+ return {
37
+ text: `${c[i.length % c.length]} tap any poster to dive in.`,
38
+ note: w
39
+ };
40
+ }
41
+ var E = 0, D = () => (E += 1, `turn-${E}-${Date.now().toString(36)}`), O = new Set(/* @__PURE__ */ "a.an.the.i.me.my.want.wanna.like.love.watch.watching.something.anything.show.me.find.recommend.suggest.give.please.tonight.today.now.for.to.of.and.or.with.about.some.any.good.great.best.can.you.is.are.what.whats.that.this.in.on.up.get.got.im.looking.mood".split("."));
42
+ function k(e) {
43
+ let t = e.toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter(Boolean).filter((e) => !O.has(e) && e.length > 1).slice(0, 6).join(" ").trim();
44
+ return t.length > 0 ? t : void 0;
45
+ }
46
+ var A = [
47
+ "Recommend a feel-good movie for tonight",
48
+ "What documentaries are worth watching?",
49
+ "Show me something with high drama",
50
+ "I want a short film under 30 minutes",
51
+ "Surprise me with anime"
52
+ ], j = ({ size: e = "sm", active: n = !1, className: r }) => {
53
+ let i = e === "lg" ? "h-16 w-16" : "h-8 w-8", a = e === "lg" ? "h-7 w-7" : "h-4 w-4";
54
+ return /* @__PURE__ */ b("span", {
55
+ className: t("relative inline-flex shrink-0 items-center justify-center", i, r),
56
+ "aria-hidden": "true",
57
+ children: [/* @__PURE__ */ y("span", { className: t("absolute inset-0 rounded-full bg-action-primary-bg opacity-30 blur-md", n && "motion-safe:animate-ping") }), /* @__PURE__ */ y("span", {
58
+ className: t("relative inline-flex items-center justify-center rounded-full", "bg-action-primary-bg text-action-primary-text shadow-elevation-2", i),
59
+ children: /* @__PURE__ */ y(_, { className: a })
60
+ })]
61
+ });
62
+ }, M = ({ title: e, onOpen: n }) => {
63
+ let { data: r, isLoading: o } = i(e.posterFileId), [s, c] = u(!1), l = !!e.posterFileId && !!r && !s, d = [a(e.type), e.releaseYear ? String(e.releaseYear) : null].filter(Boolean).join(" · ");
64
+ return /* @__PURE__ */ b("button", {
65
+ type: "button",
66
+ onClick: () => n(e.id),
67
+ "aria-label": `Open ${e.title}`,
68
+ className: "group w-28 shrink-0 snap-start text-left focus:outline-none sm:w-32",
69
+ children: [
70
+ /* @__PURE__ */ b("div", {
71
+ className: t("relative aspect-[2/3] overflow-hidden rounded-lg 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)]"),
72
+ children: [l ? /* @__PURE__ */ y("img", {
73
+ src: r ?? void 0,
74
+ alt: `${e.title} poster`,
75
+ loading: "lazy",
76
+ onError: () => c(!0),
77
+ className: "h-full w-full object-cover"
78
+ }) : /* @__PURE__ */ y("div", {
79
+ className: t("flex h-full w-full items-center justify-center text-text-muted", o && !!e.posterFileId && "motion-safe:animate-pulse"),
80
+ "aria-hidden": "true",
81
+ children: /* @__PURE__ */ y(f, { className: "h-6 w-6" })
82
+ }), e.aiDisclosure && /* @__PURE__ */ b("span", {
83
+ className: "absolute right-1.5 top-1.5 inline-flex items-center gap-1 rounded-full bg-bg-canvas/80 px-1.5 py-0.5 text-[10px] font-medium text-text-secondary backdrop-blur-sm",
84
+ title: "Contains AI-generated or AI-altered content",
85
+ children: [/* @__PURE__ */ y(_, { className: "h-2.5 w-2.5" }), "AI"]
86
+ })]
87
+ }),
88
+ /* @__PURE__ */ y("p", {
89
+ className: "mt-1.5 line-clamp-1 text-xs font-medium text-text-primary",
90
+ children: e.title
91
+ }),
92
+ d && /* @__PURE__ */ y("p", {
93
+ className: "line-clamp-1 text-[11px] text-text-secondary",
94
+ children: d
95
+ })
96
+ ]
97
+ });
98
+ }, N = ({ message: e, onOpenTitle: n }) => e.role === "user" ? /* @__PURE__ */ y("div", {
99
+ className: "flex justify-end",
100
+ children: /* @__PURE__ */ y("div", {
101
+ className: "max-w-[85%] rounded-2xl rounded-br-sm bg-action-primary-bg px-4 py-2.5 text-sm text-action-primary-text shadow-elevation-1 sm:max-w-[75%]",
102
+ children: /* @__PURE__ */ y("p", {
103
+ className: "whitespace-pre-wrap break-words",
104
+ children: e.text
105
+ })
106
+ })
107
+ }) : /* @__PURE__ */ b("div", {
108
+ className: "flex items-start gap-3",
109
+ children: [/* @__PURE__ */ y(j, { size: "sm" }), /* @__PURE__ */ b("div", {
110
+ className: "min-w-0 max-w-[85%] sm:max-w-[80%]",
111
+ children: [/* @__PURE__ */ b("div", {
112
+ className: "rounded-2xl rounded-tl-sm border border-border-seam bg-bg-surface px-4 py-2.5 shadow-elevation-1",
113
+ children: [/* @__PURE__ */ y("p", {
114
+ className: "whitespace-pre-wrap break-words text-sm text-text-primary",
115
+ children: e.text
116
+ }), e.note && /* @__PURE__ */ b("p", {
117
+ className: "mt-1.5 flex items-center gap-1 text-[11px] text-text-muted",
118
+ children: [/* @__PURE__ */ y(p, { className: "h-3 w-3" }), e.note]
119
+ })]
120
+ }), e.titles && e.titles.length > 0 && /* @__PURE__ */ y("div", {
121
+ className: t("mt-3 flex gap-3 overflow-x-auto pb-2", "snap-x [scrollbar-width:thin]"),
122
+ children: e.titles.map((e) => /* @__PURE__ */ y(M, {
123
+ title: e,
124
+ onOpen: n
125
+ }, e.id))
126
+ })]
127
+ })]
128
+ }), P = () => /* @__PURE__ */ b("div", {
129
+ className: "flex items-start gap-3",
130
+ "aria-live": "polite",
131
+ "aria-label": "Concierge is thinking",
132
+ children: [/* @__PURE__ */ y(j, {
133
+ size: "sm",
134
+ active: !0
135
+ }), /* @__PURE__ */ y("div", {
136
+ className: "rounded-2xl rounded-tl-sm border border-border-seam bg-bg-surface px-4 py-3 shadow-elevation-1",
137
+ children: /* @__PURE__ */ y("span", {
138
+ className: "flex items-center gap-1",
139
+ children: [
140
+ 0,
141
+ 1,
142
+ 2
143
+ ].map((e) => /* @__PURE__ */ y("span", {
144
+ className: "h-2 w-2 rounded-full bg-text-muted motion-safe:animate-bounce",
145
+ style: { animationDelay: `${e * 150}ms` }
146
+ }, e))
147
+ })
148
+ })]
149
+ }), F = ({ onPick: e, disabled: n, className: r }) => /* @__PURE__ */ y("div", {
150
+ className: t("flex flex-wrap gap-2", r),
151
+ children: A.map((r) => /* @__PURE__ */ b("button", {
152
+ type: "button",
153
+ disabled: n,
154
+ onClick: () => e(r),
155
+ className: t("inline-flex items-center gap-1.5 rounded-full border border-border-subtle bg-bg-surface", "px-3 py-1.5 text-xs font-medium text-text-secondary transition-colors", "hover:border-border-strong hover:text-text-primary", "focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]", "disabled:cursor-not-allowed disabled:opacity-50"),
156
+ children: [/* @__PURE__ */ y(v, { className: "h-3 w-3 text-action-primary-bg" }), r]
157
+ }, r))
158
+ }), I = () => {
159
+ let { navigateTo: i, currentUser: a } = e(), [f, p] = u([]), [_, v] = u(""), [w, E] = u(null), [O, A] = u(!1), M = l(null), I = r({ status: "PUBLISHED" }, S, w?.term), L = o((e) => i(`title/${e}`), [i]);
160
+ s(() => {
161
+ if (!w) return;
162
+ A(!1);
163
+ let e = window.setTimeout(() => A(!0), C);
164
+ return () => window.clearTimeout(e);
165
+ }, [w]), s(() => {
166
+ if (!w || !O || I.isFetching) return;
167
+ let e = I.isSuccess ? I.data.items : [], t = T({
168
+ prompt: w.prompt,
169
+ titles: e,
170
+ failed: I.isError
171
+ });
172
+ p((n) => [...n, {
173
+ id: w.turnId,
174
+ role: "assistant",
175
+ text: t.text,
176
+ note: t.note,
177
+ titles: e
178
+ }]), E(null);
179
+ }, [
180
+ w,
181
+ O,
182
+ I.isFetching,
183
+ I.isSuccess,
184
+ I.isError,
185
+ I.data
186
+ ]), s(() => {
187
+ M.current?.scrollTo({
188
+ top: M.current.scrollHeight,
189
+ behavior: "smooth"
190
+ });
191
+ }, [f, w]);
192
+ let R = o((e) => {
193
+ let t = e.trim();
194
+ !t || w || (p((e) => [...e, {
195
+ id: D(),
196
+ role: "user",
197
+ text: t
198
+ }]), v(""), E({
199
+ turnId: D(),
200
+ prompt: t,
201
+ term: k(t)
202
+ }));
203
+ }, [w]), z = (e) => {
204
+ e.preventDefault(), R(_);
205
+ }, B = (e) => {
206
+ e.key === "Enter" && !e.shiftKey && (e.preventDefault(), R(_));
207
+ }, V = () => {
208
+ p([]), E(null), v("");
209
+ }, H = c(() => {
210
+ let e = a?.name?.trim();
211
+ if (e) return e.split(/\s+/)[0];
212
+ let t = a?.email?.trim();
213
+ return t ? t.split("@")[0] : null;
214
+ }, [a]), U = w !== null, W = f.length > 0 || U;
215
+ return /* @__PURE__ */ y(n, {
216
+ hero: !0,
217
+ title: /* @__PURE__ */ b("span", {
218
+ className: "inline-flex items-center gap-2",
219
+ children: [/* @__PURE__ */ y(m, { className: "h-6 w-6 text-action-primary-bg" }), "AI Watch-Concierge"]
220
+ }),
221
+ description: "Describe a mood or genre and get real catalog picks back, instantly.",
222
+ purpose: "Ask for something to watch in plain language and the concierge surfaces titles from your published catalog as tappable posters. Responses are a simulated preview; the recommendations themselves are real.",
223
+ actions: W ? /* @__PURE__ */ b("button", {
224
+ type: "button",
225
+ onClick: V,
226
+ disabled: U,
227
+ className: t("inline-flex items-center gap-2 rounded-lg border border-border-strong bg-bg-surface px-3 py-2 text-sm font-medium text-text-primary transition-colors", "hover:bg-bg-sunken focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]", "disabled:cursor-not-allowed disabled:opacity-50"),
228
+ children: [/* @__PURE__ */ y(h, { className: "h-4 w-4" }), "New chat"]
229
+ }) : void 0,
230
+ children: /* @__PURE__ */ b("div", {
231
+ className: "mx-auto flex h-full max-w-3xl flex-col",
232
+ children: [
233
+ /* @__PURE__ */ b("div", {
234
+ role: "note",
235
+ className: "mb-4 flex shrink-0 items-center gap-2 rounded-lg border border-status-warning-border bg-status-warning-bg-subtle px-3 py-2 text-xs text-status-warning-text",
236
+ children: [/* @__PURE__ */ y(d, { className: "h-4 w-4 shrink-0" }), /* @__PURE__ */ b("span", { children: [/* @__PURE__ */ y("strong", {
237
+ className: "font-semibold",
238
+ children: "Preview"
239
+ }), " — AI responses are simulated. Title recommendations are pulled live from your real catalog."] })]
240
+ }),
241
+ /* @__PURE__ */ y("div", {
242
+ ref: M,
243
+ className: "flex-1 overflow-y-auto pr-1",
244
+ children: W ? /* @__PURE__ */ b("div", {
245
+ className: "space-y-5 py-2",
246
+ role: "log",
247
+ "aria-live": "polite",
248
+ "aria-relevant": "additions",
249
+ children: [f.map((e) => /* @__PURE__ */ y(N, {
250
+ message: e,
251
+ onOpenTitle: L
252
+ }, e.id)), U && /* @__PURE__ */ y(P, {})]
253
+ }) : /* @__PURE__ */ b("div", {
254
+ className: "flex min-h-full flex-col items-center justify-center py-10 text-center",
255
+ children: [
256
+ /* @__PURE__ */ y(j, {
257
+ size: "lg",
258
+ active: !0,
259
+ className: "mb-5"
260
+ }),
261
+ /* @__PURE__ */ y("h2", {
262
+ className: "text-xl font-semibold text-text-primary sm:text-2xl",
263
+ children: H ? `Hi ${H} — what are you in the mood for?` : "What are you in the mood for?"
264
+ }),
265
+ /* @__PURE__ */ y("p", {
266
+ className: "mt-2 max-w-md text-sm text-text-secondary",
267
+ children: "Tell me a vibe, a genre, or a title you loved. I'll surface real picks from what's published in your catalog."
268
+ }),
269
+ /* @__PURE__ */ y(F, {
270
+ onPick: R,
271
+ disabled: U,
272
+ className: "mt-6 justify-center"
273
+ })
274
+ ]
275
+ })
276
+ }),
277
+ /* @__PURE__ */ b("div", {
278
+ className: "shrink-0 pt-4",
279
+ children: [
280
+ W && /* @__PURE__ */ y(F, {
281
+ onPick: R,
282
+ disabled: U,
283
+ className: "mb-3"
284
+ }),
285
+ /* @__PURE__ */ y(x, {
286
+ treatment: "glass",
287
+ className: "border-border-seam p-2",
288
+ children: /* @__PURE__ */ b("form", {
289
+ onSubmit: z,
290
+ className: "flex items-end gap-2",
291
+ children: [
292
+ /* @__PURE__ */ y("label", {
293
+ htmlFor: "concierge-input",
294
+ className: "sr-only",
295
+ children: "Message the watch-concierge"
296
+ }),
297
+ /* @__PURE__ */ y("textarea", {
298
+ id: "concierge-input",
299
+ rows: 1,
300
+ value: _,
301
+ onChange: (e) => v(e.target.value),
302
+ onKeyDown: B,
303
+ placeholder: "Ask for something to watch…",
304
+ disabled: U,
305
+ className: t("max-h-32 min-h-[2.5rem] flex-1 resize-none bg-transparent px-3 py-2 text-sm text-text-primary", "placeholder:text-text-secondary focus:outline-none disabled:opacity-60")
306
+ }),
307
+ /* @__PURE__ */ y("button", {
308
+ type: "submit",
309
+ disabled: U || _.trim().length === 0,
310
+ "aria-label": "Send message",
311
+ className: t("inline-flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-action-primary-bg text-action-primary-text shadow-elevation-1 transition-opacity", "hover:opacity-90 focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]", "disabled:cursor-not-allowed disabled:opacity-40"),
312
+ children: /* @__PURE__ */ y(g, { className: "h-4 w-4" })
313
+ })
314
+ ]
315
+ })
316
+ }),
317
+ /* @__PURE__ */ y("p", {
318
+ className: "mt-2 text-center text-[11px] text-text-muted",
319
+ children: "Press Enter to send · Shift + Enter for a new line"
320
+ })
321
+ ]
322
+ })
323
+ ]
324
+ })
325
+ });
326
+ };
327
+ //#endregion
328
+ export { I as ConciergePage };
329
+
330
+ //# sourceMappingURL=ConciergePage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ConciergePage.js","names":[],"sources":["../../../../src/hookmovies/pages/ai/ConciergePage.tsx"],"sourcesContent":["/**\n * AI Watch-Concierge (Consumer)\n * @module hookmovies/pages/ai\n *\n * A polished, conversational surface where a viewer can ask for something to\n * watch and get poster recommendations back inline.\n *\n * IMPORTANT — this is a PREVIEW: the assistant's *prose* is MOCKED (canned,\n * deterministic) and is clearly labelled as such throughout the UI (\"Preview —\n * AI responses are simulated\"). It is NOT a live LLM.\n *\n * To keep the experience grounded and honest, every recommendation card is\n * REAL: each user turn drives a live `hokListTitles(search:)` query against the\n * published catalog (via {@link useTitles}) and the returned titles are rendered\n * as tappable poster cards that deep-link into the consumer title view. So the\n * cards are truthful catalog data even though the surrounding prose is canned.\n *\n * The seam for a real model is a single function — {@link getConciergeReply}.\n * Swap its body for a call to a real concierge endpoint that returns the same\n * `ConciergeReply` shape and the rest of this component needs no change.\n *\n * Fully tokenized (fe-libs semantic tokens only — no raw colors), mobile-first,\n * reduce-motion friendly, with loading / empty / error handling for the live\n * recommendation queries.\n */\n\nimport {\n type FC,\n type FormEvent,\n type KeyboardEvent,\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from 'react';\nimport {\n Sparkles,\n Send,\n Film,\n Wand2,\n RotateCcw,\n Info,\n AlertCircle,\n MessageSquareText,\n} from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { useHookMovies } from '../../context/HookMoviesContext';\nimport { useTitles, type HokTitle } from '../../hooks/useHookMoviesApi';\nimport { useFileUrl } from '../../hooks/useFiles';\nimport { cn } from '../../utils/cn';\nimport { formatEnumLabel } from '../../utils/formatters';\n\n// Recommendations returned per turn — enough to fill one scrollable rail.\nconst RECO_PAGE = { limit: 8, offset: 0 } as const;\n\n// A short, deliberate \"thinking\" beat so the simulated reply feels considered\n// (and, incidentally, guarantees the live recommendation query has settled for\n// the current turn before we render the answer — see the resolver effect).\nconst THINKING_MS = 700;\n\nconst SIMULATED_NOTE = 'Simulated response — not a live model';\n\n// ============================================================================\n// Types\n// ============================================================================\n\ntype ChatRole = 'user' | 'assistant';\n\ninterface ChatMessage {\n id: string;\n role: ChatRole;\n text: string;\n /** Optional \"this was simulated\" caption shown under assistant prose. */\n note?: string;\n /** Real catalog titles surfaced as recommendation cards for this turn. */\n titles?: HokTitle[];\n}\n\ninterface PendingTurn {\n turnId: string;\n prompt: string;\n /** Derived search term driving the live `hokListTitles` query. */\n term?: string;\n}\n\ninterface ConciergeReply {\n text: string;\n note?: string;\n}\n\n// ============================================================================\n// Mock reply engine — THE SINGLE SEAM for a real LLM\n// ============================================================================\n\n/**\n * Produce the concierge's prose reply for a turn.\n *\n * TODAY: returns a deterministic, clearly-labelled MOCK grounded in the real\n * titles that were just fetched from the catalog. It never claims to be a live\n * model and always carries the {@link SIMULATED_NOTE}.\n *\n * LATER: replace the body with a call to a real concierge endpoint (streaming\n * or request/response) that returns the same {@link ConciergeReply} shape. No\n * other part of this component needs to change — the UI already renders `text`,\n * the optional `note`, and the real `titles` cards independently.\n */\nfunction getConciergeReply(args: {\n prompt: string;\n titles: HokTitle[];\n failed: boolean;\n}): ConciergeReply {\n const { prompt, titles, failed } = args;\n\n if (failed) {\n return {\n text: \"I hit a snag reaching the catalog just now, so I couldn't pull recommendations for that. Give it another try in a moment.\",\n note: SIMULATED_NOTE,\n };\n }\n\n const normalized = prompt.trim().toLowerCase();\n const isGreeting = /^(hi|hey|hello|yo|namaste|hola|good (morning|evening|afternoon))\\b/.test(\n normalized\n );\n\n if (titles.length === 0) {\n if (isGreeting) {\n return {\n text: \"Hi! I'm your watch-concierge. Tell me a mood, a genre, or a title you loved and I'll surface something from the catalog to match.\",\n note: SIMULATED_NOTE,\n };\n }\n return {\n text: `I couldn't find anything published that matches “${prompt.trim()}” yet. Try a broader mood — like “something funny”, “a gripping thriller”, or “short films” — and I'll look again.`,\n note: SIMULATED_NOTE,\n };\n }\n\n const count = titles.length;\n const plural = count === 1 ? 'title' : 'titles';\n const leadIns = [\n `Great pick of a mood. Based on what's published in your catalog, here ${count === 1 ? 'is' : 'are'} ${count} ${plural} I think you'll enjoy:`,\n `Say no more — I pulled ${count} real ${plural} from the published catalog that fit that vibe:`,\n `Here's what's streaming that lines up with that. ${count} ${plural} worth a look:`,\n `On it. These ${count} ${plural} from your catalog match what you're after:`,\n ];\n // Deterministic variety without randomness (stable across re-renders).\n const lead = leadIns[normalized.length % leadIns.length];\n\n return {\n text: `${lead} tap any poster to dive in.`,\n note: SIMULATED_NOTE,\n };\n}\n\n// ============================================================================\n// Helpers\n// ============================================================================\n\nlet turnCounter = 0;\nconst nextId = (): string => {\n turnCounter += 1;\n return `turn-${turnCounter}-${Date.now().toString(36)}`;\n};\n\n// Words that add no signal to a catalog search — stripped before querying.\nconst STOPWORDS = new Set([\n 'a',\n 'an',\n 'the',\n 'i',\n 'me',\n 'my',\n 'want',\n 'wanna',\n 'like',\n 'love',\n 'watch',\n 'watching',\n 'something',\n 'anything',\n 'show',\n 'me',\n 'find',\n 'recommend',\n 'suggest',\n 'give',\n 'please',\n 'tonight',\n 'today',\n 'now',\n 'for',\n 'to',\n 'of',\n 'and',\n 'or',\n 'with',\n 'about',\n 'some',\n 'any',\n 'good',\n 'great',\n 'best',\n 'can',\n 'you',\n 'is',\n 'are',\n 'what',\n 'whats',\n 'that',\n 'this',\n 'in',\n 'on',\n 'up',\n 'get',\n 'got',\n 'im',\n 'looking',\n 'mood',\n]);\n\n/**\n * Reduce a free-text prompt to a concise search term for `hokListTitles`.\n * Keeps the most meaningful keywords (mood / genre / title words); returns\n * `undefined` when nothing meaningful remains (the catalog then lists its\n * freshest published titles as generic \"surprise me\" picks).\n */\nfunction deriveSearchTerm(prompt: string): string | undefined {\n const words = prompt\n .toLowerCase()\n .replace(/[^a-z0-9\\s]/g, ' ')\n .split(/\\s+/)\n .filter(Boolean)\n .filter((w) => !STOPWORDS.has(w) && w.length > 1);\n const term = words.slice(0, 6).join(' ').trim();\n return term.length > 0 ? term : undefined;\n}\n\nconst SUGGESTED_PROMPTS = [\n 'Recommend a feel-good movie for tonight',\n 'What documentaries are worth watching?',\n 'Show me something with high drama',\n 'I want a short film under 30 minutes',\n 'Surprise me with anime',\n] as const;\n\n// ============================================================================\n// AI orb — the crimson concierge avatar (brand action-primary tokens only)\n// ============================================================================\n\ninterface AiOrbProps {\n size?: 'sm' | 'lg';\n /** Pulse the halo (used on the hero / while thinking). */\n active?: boolean;\n className?: string;\n}\n\nconst AiOrb: FC<AiOrbProps> = ({ size = 'sm', active = false, className }) => {\n const dim = size === 'lg' ? 'h-16 w-16' : 'h-8 w-8';\n const glyph = size === 'lg' ? 'h-7 w-7' : 'h-4 w-4';\n return (\n <span\n className={cn('relative inline-flex shrink-0 items-center justify-center', dim, className)}\n aria-hidden=\"true\"\n >\n {/* Soft crimson halo — brand accent, softened so it reads as a glow. */}\n <span\n className={cn(\n 'absolute inset-0 rounded-full bg-action-primary-bg opacity-30 blur-md',\n active && 'motion-safe:animate-ping'\n )}\n />\n <span\n className={cn(\n 'relative inline-flex items-center justify-center rounded-full',\n 'bg-action-primary-bg text-action-primary-text shadow-elevation-2',\n dim\n )}\n >\n <Sparkles className={glyph} />\n </span>\n </span>\n );\n};\n\n// ============================================================================\n// RecoCard — a compact, real catalog poster surfaced inline in a reply\n// ============================================================================\n\ninterface RecoCardProps {\n title: HokTitle;\n onOpen: (id: string) => void;\n}\n\nconst RecoCard: FC<RecoCardProps> = ({ title, onOpen }) => {\n const { data: url, isLoading } = useFileUrl(title.posterFileId);\n const [errored, setErrored] = useState(false);\n\n const showImage = Boolean(title.posterFileId) && Boolean(url) && !errored;\n const meta = [formatEnumLabel(title.type), title.releaseYear ? String(title.releaseYear) : null]\n .filter(Boolean)\n .join(' · ');\n\n return (\n <button\n type=\"button\"\n onClick={() => onOpen(title.id)}\n aria-label={`Open ${title.title}`}\n className=\"group w-28 shrink-0 snap-start text-left focus:outline-none sm:w-32\"\n >\n <div\n className={cn(\n 'relative aspect-[2/3] overflow-hidden rounded-lg 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 {showImage ? (\n <img\n src={url ?? undefined}\n alt={`${title.title} poster`}\n loading=\"lazy\"\n onError={() => setErrored(true)}\n className=\"h-full w-full object-cover\"\n />\n ) : (\n <div\n className={cn(\n 'flex h-full w-full items-center justify-center text-text-muted',\n isLoading && Boolean(title.posterFileId) && 'motion-safe:animate-pulse'\n )}\n aria-hidden=\"true\"\n >\n <Film className=\"h-6 w-6\" />\n </div>\n )}\n\n {title.aiDisclosure && (\n <span\n className=\"absolute right-1.5 top-1.5 inline-flex items-center gap-1 rounded-full bg-bg-canvas/80 px-1.5 py-0.5 text-[10px] font-medium text-text-secondary backdrop-blur-sm\"\n title=\"Contains AI-generated or AI-altered content\"\n >\n <Sparkles className=\"h-2.5 w-2.5\" />\n AI\n </span>\n )}\n </div>\n <p className=\"mt-1.5 line-clamp-1 text-xs font-medium text-text-primary\">{title.title}</p>\n {meta && <p className=\"line-clamp-1 text-[11px] text-text-secondary\">{meta}</p>}\n </button>\n );\n};\n\n// ============================================================================\n// MessageRow — one user or assistant turn\n// ============================================================================\n\ninterface MessageRowProps {\n message: ChatMessage;\n onOpenTitle: (id: string) => void;\n}\n\nconst MessageRow: FC<MessageRowProps> = ({ message, onOpenTitle }) => {\n const isUser = message.role === 'user';\n\n if (isUser) {\n return (\n <div className=\"flex justify-end\">\n <div className=\"max-w-[85%] rounded-2xl rounded-br-sm bg-action-primary-bg px-4 py-2.5 text-sm text-action-primary-text shadow-elevation-1 sm:max-w-[75%]\">\n <p className=\"whitespace-pre-wrap break-words\">{message.text}</p>\n </div>\n </div>\n );\n }\n\n return (\n <div className=\"flex items-start gap-3\">\n <AiOrb size=\"sm\" />\n <div className=\"min-w-0 max-w-[85%] sm:max-w-[80%]\">\n <div className=\"rounded-2xl rounded-tl-sm border border-border-seam bg-bg-surface px-4 py-2.5 shadow-elevation-1\">\n <p className=\"whitespace-pre-wrap break-words text-sm text-text-primary\">\n {message.text}\n </p>\n {message.note && (\n <p className=\"mt-1.5 flex items-center gap-1 text-[11px] text-text-muted\">\n <Info className=\"h-3 w-3\" />\n {message.note}\n </p>\n )}\n </div>\n\n {message.titles && message.titles.length > 0 && (\n <div\n className={cn('mt-3 flex gap-3 overflow-x-auto pb-2', 'snap-x [scrollbar-width:thin]')}\n >\n {message.titles.map((title) => (\n <RecoCard key={title.id} title={title} onOpen={onOpenTitle} />\n ))}\n </div>\n )}\n </div>\n </div>\n );\n};\n\n// ============================================================================\n// TypingIndicator — the \"thinking\" bubble while a turn resolves\n// ============================================================================\n\nconst TypingIndicator: FC = () => (\n <div className=\"flex items-start gap-3\" aria-live=\"polite\" aria-label=\"Concierge is thinking\">\n <AiOrb size=\"sm\" active />\n <div className=\"rounded-2xl rounded-tl-sm border border-border-seam bg-bg-surface px-4 py-3 shadow-elevation-1\">\n <span className=\"flex items-center gap-1\">\n {[0, 1, 2].map((i) => (\n <span\n key={i}\n className=\"h-2 w-2 rounded-full bg-text-muted motion-safe:animate-bounce\"\n style={{ animationDelay: `${i * 150}ms` }}\n />\n ))}\n </span>\n </div>\n </div>\n);\n\n// ============================================================================\n// ChipRow — suggested-prompt chips\n// ============================================================================\n\ninterface ChipRowProps {\n onPick: (prompt: string) => void;\n disabled: boolean;\n className?: string;\n}\n\nconst ChipRow: FC<ChipRowProps> = ({ onPick, disabled, className }) => (\n <div className={cn('flex flex-wrap gap-2', className)}>\n {SUGGESTED_PROMPTS.map((prompt) => (\n <button\n key={prompt}\n type=\"button\"\n disabled={disabled}\n onClick={() => onPick(prompt)}\n className={cn(\n 'inline-flex items-center gap-1.5 rounded-full border border-border-subtle bg-bg-surface',\n 'px-3 py-1.5 text-xs font-medium text-text-secondary transition-colors',\n 'hover:border-border-strong hover:text-text-primary',\n 'focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]',\n 'disabled:cursor-not-allowed disabled:opacity-50'\n )}\n >\n <Wand2 className=\"h-3 w-3 text-action-primary-bg\" />\n {prompt}\n </button>\n ))}\n </div>\n);\n\n// ============================================================================\n// ConciergePage\n// ============================================================================\n\nexport const ConciergePage: FC = () => {\n const { navigateTo, currentUser } = useHookMovies();\n\n const [messages, setMessages] = useState<ChatMessage[]>([]);\n const [input, setInput] = useState('');\n const [pending, setPending] = useState<PendingTurn | null>(null);\n const [thinkingElapsed, setThinkingElapsed] = useState(false);\n\n const scrollRef = useRef<HTMLDivElement>(null);\n\n // Live recommendation query for the in-flight turn. When there's no pending\n // turn `term` is undefined and this simply lists freshest published titles —\n // harmless, and keeps the hook mounted so the query cache stays warm.\n const recos = useTitles({ status: 'PUBLISHED' }, RECO_PAGE, pending?.term);\n\n const openTitle = useCallback((id: string) => navigateTo(`title/${id}`), [navigateTo]);\n\n // Kick off (and reset) the deliberate \"thinking\" beat for each new turn.\n useEffect(() => {\n if (!pending) return;\n setThinkingElapsed(false);\n const handle = window.setTimeout(() => setThinkingElapsed(true), THINKING_MS);\n return () => window.clearTimeout(handle);\n }, [pending]);\n\n // Resolve the pending turn once the thinking beat has passed AND the live\n // recommendation query has settled for the current term. The timer guarantees\n // the query for the new term has begun, so `!isFetching` here means it has\n // finished for THIS turn (not a stale prior result).\n useEffect(() => {\n if (!pending || !thinkingElapsed || recos.isFetching) return;\n\n const titles = recos.isSuccess ? recos.data.items : [];\n const reply = getConciergeReply({\n prompt: pending.prompt,\n titles,\n failed: recos.isError,\n });\n\n setMessages((prev) => [\n ...prev,\n {\n id: pending.turnId,\n role: 'assistant',\n text: reply.text,\n note: reply.note,\n titles,\n },\n ]);\n setPending(null);\n }, [pending, thinkingElapsed, recos.isFetching, recos.isSuccess, recos.isError, recos.data]);\n\n // Auto-scroll to the newest turn.\n useEffect(() => {\n scrollRef.current?.scrollTo({ top: scrollRef.current.scrollHeight, behavior: 'smooth' });\n }, [messages, pending]);\n\n const send = useCallback(\n (raw: string) => {\n const trimmed = raw.trim();\n if (!trimmed || pending) return;\n setMessages((prev) => [...prev, { id: nextId(), role: 'user', text: trimmed }]);\n setInput('');\n setPending({ turnId: nextId(), prompt: trimmed, term: deriveSearchTerm(trimmed) });\n },\n [pending]\n );\n\n const handleSubmit = (event: FormEvent) => {\n event.preventDefault();\n send(input);\n };\n\n const handleKeyDown = (event: KeyboardEvent<HTMLTextAreaElement>) => {\n if (event.key === 'Enter' && !event.shiftKey) {\n event.preventDefault();\n send(input);\n }\n };\n\n const resetConversation = () => {\n setMessages([]);\n setPending(null);\n setInput('');\n };\n\n const firstName = useMemo(() => {\n const name = currentUser?.name?.trim();\n if (name) return name.split(/\\s+/)[0];\n const email = currentUser?.email?.trim();\n return email ? email.split('@')[0] : null;\n }, [currentUser]);\n\n const isBusy = pending !== null;\n const hasConversation = messages.length > 0 || isBusy;\n\n return (\n <PageLayout\n hero\n title={\n <span className=\"inline-flex items-center gap-2\">\n <MessageSquareText className=\"h-6 w-6 text-action-primary-bg\" />\n AI Watch-Concierge\n </span>\n }\n description=\"Describe a mood or genre and get real catalog picks back, instantly.\"\n purpose=\"Ask for something to watch in plain language and the concierge surfaces titles from your published catalog as tappable posters. Responses are a simulated preview; the recommendations themselves are real.\"\n actions={\n hasConversation ? (\n <button\n type=\"button\"\n onClick={resetConversation}\n disabled={isBusy}\n className={cn(\n 'inline-flex items-center gap-2 rounded-lg border border-border-strong bg-bg-surface px-3 py-2 text-sm font-medium text-text-primary transition-colors',\n 'hover:bg-bg-sunken focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]',\n 'disabled:cursor-not-allowed disabled:opacity-50'\n )}\n >\n <RotateCcw className=\"h-4 w-4\" />\n New chat\n </button>\n ) : undefined\n }\n >\n <div className=\"mx-auto flex h-full max-w-3xl flex-col\">\n {/* Simulated-preview banner — always visible, unambiguous. */}\n <div\n role=\"note\"\n className=\"mb-4 flex shrink-0 items-center gap-2 rounded-lg border border-status-warning-border bg-status-warning-bg-subtle px-3 py-2 text-xs text-status-warning-text\"\n >\n <AlertCircle className=\"h-4 w-4 shrink-0\" />\n <span>\n <strong className=\"font-semibold\">Preview</strong> — AI responses are simulated. Title\n recommendations are pulled live from your real catalog.\n </span>\n </div>\n\n {/* Conversation / intro */}\n <div ref={scrollRef} className=\"flex-1 overflow-y-auto pr-1\">\n {!hasConversation ? (\n <div className=\"flex min-h-full flex-col items-center justify-center py-10 text-center\">\n <AiOrb size=\"lg\" active className=\"mb-5\" />\n <h2 className=\"text-xl font-semibold text-text-primary sm:text-2xl\">\n {firstName\n ? `Hi ${firstName} — what are you in the mood for?`\n : 'What are you in the mood for?'}\n </h2>\n <p className=\"mt-2 max-w-md text-sm text-text-secondary\">\n Tell me a vibe, a genre, or a title you loved. I'll surface real picks from what's\n published in your catalog.\n </p>\n <ChipRow onPick={send} disabled={isBusy} className=\"mt-6 justify-center\" />\n </div>\n ) : (\n <div className=\"space-y-5 py-2\" role=\"log\" aria-live=\"polite\" aria-relevant=\"additions\">\n {messages.map((message) => (\n <MessageRow key={message.id} message={message} onOpenTitle={openTitle} />\n ))}\n {isBusy && <TypingIndicator />}\n </div>\n )}\n </div>\n\n {/* Composer */}\n <div className=\"shrink-0 pt-4\">\n {hasConversation && <ChipRow onPick={send} disabled={isBusy} className=\"mb-3\" />}\n <GlassCard treatment=\"glass\" className=\"border-border-seam p-2\">\n <form onSubmit={handleSubmit} className=\"flex items-end gap-2\">\n <label htmlFor=\"concierge-input\" className=\"sr-only\">\n Message the watch-concierge\n </label>\n <textarea\n id=\"concierge-input\"\n rows={1}\n value={input}\n onChange={(event) => setInput(event.target.value)}\n onKeyDown={handleKeyDown}\n placeholder=\"Ask for something to watch…\"\n disabled={isBusy}\n className={cn(\n 'max-h-32 min-h-[2.5rem] flex-1 resize-none bg-transparent px-3 py-2 text-sm text-text-primary',\n 'placeholder:text-text-secondary focus:outline-none disabled:opacity-60'\n )}\n />\n <button\n type=\"submit\"\n disabled={isBusy || input.trim().length === 0}\n aria-label=\"Send message\"\n className={cn(\n 'inline-flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-action-primary-bg text-action-primary-text shadow-elevation-1 transition-opacity',\n 'hover:opacity-90 focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]',\n 'disabled:cursor-not-allowed disabled:opacity-40'\n )}\n >\n <Send className=\"h-4 w-4\" />\n </button>\n </form>\n </GlassCard>\n <p className=\"mt-2 text-center text-[11px] text-text-muted\">\n Press Enter to send · Shift + Enter for a new line\n </p>\n </div>\n </div>\n </PageLayout>\n );\n};\n"],"mappings":"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"}