diff --git a/components/Chat/ChatGreeting.tsx b/components/Chat/ChatGreeting.tsx index e95c002f4..b19457e0e 100644 --- a/components/Chat/ChatGreeting.tsx +++ b/components/Chat/ChatGreeting.tsx @@ -3,6 +3,7 @@ import ImageWithFallback from "@/components/ImageWithFallback"; import ValuationHero from "@/components/Home/ValuationHero"; import useHomeValuation from "@/hooks/useHomeValuation"; import TasksModule from "@/components/Home/TasksModule"; +import HomeSuggestedPrompts from "@/components/Home/HomeSuggestedPrompts"; import { VALUATION_URL } from "@/lib/consts"; /** @@ -11,8 +12,8 @@ import { VALUATION_URL } from "@/lib/consts"; * funnel: the artist-scoped catalog valuation hero when the account has * measured data for the current scope, otherwise the "Ask me about" * greeting plus a CTA into the valuation funnel; the "label at work" - * tasks module renders beneath it (self-hiding when it has nothing to - * show). All context arrives via provider-backed hooks — no props beyond + * tasks module and the suggested prompt chips render beneath it (each + * self-hiding when it has nothing to show). All context arrives via provider-backed hooks — no props beyond * the fade flag the chat empty state already owned, so the chat component * needs no knowledge of what this header shows. */ @@ -36,6 +37,7 @@ export function ChatGreeting({ isVisible }: { isVisible: boolean }) { measuredTrackCount={valuation.measuredTrackCount} /> + ); } @@ -78,6 +80,9 @@ export function ChatGreeting({ isVisible }: { isVisible: boolean }) {
+
+ +
); } diff --git a/components/Home/HomeSuggestedPrompts.tsx b/components/Home/HomeSuggestedPrompts.tsx new file mode 100644 index 000000000..793fedadf --- /dev/null +++ b/components/Home/HomeSuggestedPrompts.tsx @@ -0,0 +1,51 @@ +"use client"; + +import { useVercelChatContext } from "@/providers/VercelChatProvider"; +import { useArtistProvider } from "@/providers/ArtistProvider"; +import useHomeValuation from "@/hooks/useHomeValuation"; +import useHomeTasksModuleState from "@/hooks/useHomeTasksModuleState"; +import { getHomeSuggestedPrompts } from "@/lib/home/getHomeSuggestedPrompts"; + +/** + * Suggested prompt chips for the homepage command bar (recoupable/chat#1850). + * Fully self-contained per the KISS review: module relevance comes from the + * same provider-backed hooks the header uses (react-query dedupes by key), + * and clicking a chip prefills the existing chat input via the provider's + * `setInput` — the send itself goes through the untouched send path. Renders + * nothing when no module has data, so no mount needs to know it exists. + */ +const HomeSuggestedPrompts = () => { + const { setInput } = useVercelChatContext(); + const { selectedArtist } = useArtistProvider(); + const valuation = useHomeValuation(); + const tasksState = useHomeTasksModuleState(); + + const prompts = getHomeSuggestedPrompts({ + hasValuation: valuation.show, + hasRuns: tasksState.view === "runs", + artistName: selectedArtist?.name || "", + }); + + if (prompts.length === 0) return null; + + return ( +
+ {prompts.map((p) => ( + + ))} +
+ ); +}; + +export default HomeSuggestedPrompts; diff --git a/components/Home/TasksModule.tsx b/components/Home/TasksModule.tsx index f06a7eeec..0f46d4735 100644 --- a/components/Home/TasksModule.tsx +++ b/components/Home/TasksModule.tsx @@ -1,9 +1,7 @@ "use client"; import Link from "next/link"; -import { useArtistProvider } from "@/providers/ArtistProvider"; -import { useTaskRuns } from "@/hooks/useTaskRuns"; -import { getHomeTasksModuleState } from "@/lib/home/getHomeTasksModuleState"; +import useHomeTasksModuleState from "@/hooks/useHomeTasksModuleState"; import HomeRunRow from "./HomeRunRow"; import StarterTaskCard from "./StarterTaskCard"; @@ -14,15 +12,7 @@ import StarterTaskCard from "./StarterTaskCard"; * failure so the homepage never blocks on it. */ const TasksModule = () => { - const { data: runs, isLoading, isError } = useTaskRuns(); - const { selectedArtist } = useArtistProvider(); - - const state = getHomeTasksModuleState({ - runs, - runsFailed: isError, - isLoading, - hasArtist: !!selectedArtist?.account_id, - }); + const state = useHomeTasksModuleState(); if (state.view === "hidden") return null; diff --git a/hooks/useHomeTasksModuleState.ts b/hooks/useHomeTasksModuleState.ts new file mode 100644 index 000000000..46f2cd431 --- /dev/null +++ b/hooks/useHomeTasksModuleState.ts @@ -0,0 +1,25 @@ +import { useTaskRuns } from "@/hooks/useTaskRuns"; +import { useArtistProvider } from "@/providers/ArtistProvider"; +import { + getHomeTasksModuleState, + HomeTasksModuleState, +} from "@/lib/home/getHomeTasksModuleState"; + +/** + * Composed hook behind the homepage tasks module. Shared by `TasksModule` + * and `HomePage` (which needs the visibility to dock the command bar); + * react-query dedupes the underlying `GET /api/tasks/runs` by key. + */ +const useHomeTasksModuleState = (): HomeTasksModuleState => { + const { data: runs, isLoading, isError } = useTaskRuns(); + const { selectedArtist } = useArtistProvider(); + + return getHomeTasksModuleState({ + runs, + runsFailed: isError, + isLoading, + hasArtist: !!selectedArtist?.account_id, + }); +}; + +export default useHomeTasksModuleState; diff --git a/lib/home/__tests__/getHomeSuggestedPrompts.test.ts b/lib/home/__tests__/getHomeSuggestedPrompts.test.ts new file mode 100644 index 000000000..762b99894 --- /dev/null +++ b/lib/home/__tests__/getHomeSuggestedPrompts.test.ts @@ -0,0 +1,63 @@ +import { describe, expect, it } from "vitest"; +import { getHomeSuggestedPrompts } from "@/lib/home/getHomeSuggestedPrompts"; + +describe("getHomeSuggestedPrompts", () => { + it("wires chips to the visible modules", () => { + const prompts = getHomeSuggestedPrompts({ + hasValuation: true, + hasRuns: true, + artistName: "Del Water Gap", + }); + + expect(prompts.map((p) => p.label)).toEqual([ + "Why did my valuation change?", + "What did my last task run do?", + "What should Del Water Gap do this week?", + ]); + }); + + it("caps at three chips", () => { + const prompts = getHomeSuggestedPrompts({ + hasValuation: true, + hasRuns: true, + artistName: "Del Water Gap", + }); + expect(prompts.length).toBeLessThanOrEqual(3); + }); + + it("falls back to a catalog-value chip when no valuation is shown", () => { + const prompts = getHomeSuggestedPrompts({ + hasValuation: false, + hasRuns: false, + artistName: "Del Water Gap", + }); + + expect(prompts.map((p) => p.label)).toEqual([ + "What's my catalog worth?", + "What should Del Water Gap do this week?", + ]); + }); + + it("omits the artist chip when no artist is selected", () => { + const prompts = getHomeSuggestedPrompts({ + hasValuation: true, + hasRuns: false, + artistName: "", + }); + + expect(prompts.map((p) => p.label)).toEqual([ + "Why did my valuation change?", + ]); + }); + + it("provides a sendable prompt for every chip", () => { + const prompts = getHomeSuggestedPrompts({ + hasValuation: true, + hasRuns: true, + artistName: "Del Water Gap", + }); + for (const p of prompts) { + expect(p.prompt.length).toBeGreaterThan(10); + } + }); +}); diff --git a/lib/home/getHomeSuggestedPrompts.ts b/lib/home/getHomeSuggestedPrompts.ts new file mode 100644 index 000000000..45c9f17ee --- /dev/null +++ b/lib/home/getHomeSuggestedPrompts.ts @@ -0,0 +1,60 @@ +export interface HomeSuggestedPrompt { + label: string; + prompt: string; +} + +interface GetHomeSuggestedPromptsParams { + hasValuation: boolean; + hasRuns: boolean; + artistName: string; +} + +const MAX_PROMPTS = 3; + +/** + * Suggested prompt chips for the homepage command bar, wired to whichever + * modules are visible (valuation hero, tasks module) so the chat's job is + * interrogating the dashboard data (recoupable/chat#1850). + */ +export function getHomeSuggestedPrompts({ + hasValuation, + hasRuns, + artistName, +}: GetHomeSuggestedPromptsParams): HomeSuggestedPrompt[] { + const prompts: HomeSuggestedPrompt[] = []; + + if (hasValuation) { + prompts.push({ + label: "Why did my valuation change?", + prompt: + "Why did my catalog valuation change? Walk through the latest " + + "measurements and explain what moved the estimate.", + }); + } else { + prompts.push({ + label: "What's my catalog worth?", + prompt: `Estimate the catalog value for ${ + artistName || "my roster" + } from public streaming data.`, + }); + } + + if (hasRuns) { + prompts.push({ + label: "What did my last task run do?", + prompt: + "What did my last task run do? Summarize its output and anything it sent.", + }); + } + + if (artistName) { + prompts.push({ + label: `What should ${artistName} do this week?`, + prompt: + `Given ${artistName}'s latest streams and catalog value, ` + + "what should we focus on this week?", + }); + } + + return prompts.slice(0, MAX_PROMPTS); +}