Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions components/Chat/ChatGreeting.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";

/**
Expand All @@ -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.
*/
Expand All @@ -36,6 +37,7 @@ export function ChatGreeting({ isVisible }: { isVisible: boolean }) {
measuredTrackCount={valuation.measuredTrackCount}
/>
<TasksModule />
<HomeSuggestedPrompts />
</div>
);
}
Expand Down Expand Up @@ -78,6 +80,9 @@ export function ChatGreeting({ isVisible }: { isVisible: boolean }) {
<div className="text-left text-sm font-normal tracking-normal">
<TasksModule />
</div>
<div className="mt-4 text-sm font-normal tracking-normal">
<HomeSuggestedPrompts />
</div>
</div>
);
}
Expand Down
51 changes: 51 additions & 0 deletions components/Home/HomeSuggestedPrompts.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div
className="flex flex-wrap justify-center gap-2"
role="group"
aria-label="Suggested prompts"
>
{prompts.map((p) => (
<button
key={p.label}
type="button"
onClick={() => setInput(p.prompt)}
className="rounded-full bg-background px-3 py-1.5 text-xs font-medium text-foreground shadow-[0px_0px_0px_1px_var(--border)] transition-colors hover:bg-muted"
>
{p.label}
</button>
))}
</div>
);
};

export default HomeSuggestedPrompts;
14 changes: 2 additions & 12 deletions components/Home/TasksModule.tsx
Original file line number Diff line number Diff line change
@@ -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";

Expand All @@ -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;

Expand Down
25 changes: 25 additions & 0 deletions hooks/useHomeTasksModuleState.ts
Original file line number Diff line number Diff line change
@@ -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;
63 changes: 63 additions & 0 deletions lib/home/__tests__/getHomeSuggestedPrompts.test.ts
Original file line number Diff line number Diff line change
@@ -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);
}
});
});
60 changes: 60 additions & 0 deletions lib/home/getHomeSuggestedPrompts.ts
Original file line number Diff line number Diff line change
@@ -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) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: artistName should be trimmed before truthiness checks and interpolation. A whitespace-only string (e.g. " ") is truthy in JavaScript, so it passes the if (artistName) gate and produces broken prompt copy like "What should do this week?". Consider normalizing with const trimmedArtistName = artistName.trim(); and using trimmedArtistName throughout the function for both the conditional and the interpolations.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At lib/home/getHomeSuggestedPrompts.ts, line 50:

<comment>`artistName` should be trimmed before truthiness checks and interpolation. A whitespace-only string (e.g. `"   "`) is truthy in JavaScript, so it passes the `if (artistName)` gate and produces broken prompt copy like `"What should    do this week?"`. Consider normalizing with `const trimmedArtistName = artistName.trim();` and using `trimmedArtistName` throughout the function for both the conditional and the interpolations.</comment>

<file context>
@@ -0,0 +1,60 @@
+    });
+  }
+
+  if (artistName) {
+    prompts.push({
+      label: `What should ${artistName} do this week?`,
</file context>

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);
}
Loading