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
20 changes: 14 additions & 6 deletions components/Chat/ChatGreeting.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,17 @@ import { useArtistProvider } from "@/providers/ArtistProvider";
import ImageWithFallback from "@/components/ImageWithFallback";
import ValuationHero from "@/components/Home/ValuationHero";
import useHomeValuation from "@/hooks/useHomeValuation";
import TasksModule from "@/components/Home/TasksModule";
import { VALUATION_URL } from "@/lib/consts";

/**
* The empty chat's artist header (recoupable/chat#1850): 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. All context arrives via provider-backed hooks — no props beyond
* The empty chat's home header (recoupable/chat#1850), valuation first —
* it's the headline number customers arrive with from the marketing
* 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
* the fade flag the chat empty state already owned, so the chat component
* needs no knowledge of what this header shows.
*/
Expand All @@ -24,13 +28,14 @@ export function ChatGreeting({ isVisible }: { isVisible: boolean }) {

if (valuation.show) {
return (
<div className={`mb-6 mt-4 py-3 ${fadeClass}`}>
<div className={`mb-6 mt-4 flex flex-col gap-4 py-3 ${fadeClass}`}>
<ValuationHero
artistName={valuation.artistName}
artistImage={valuation.artistImage}
valuation={valuation.valuation}
measuredTrackCount={valuation.measuredTrackCount}
/>
<TasksModule />
</div>
);
}
Expand Down Expand Up @@ -60,7 +65,7 @@ export function ChatGreeting({ isVisible }: { isVisible: boolean }) {
)}
{isArtistSelected ? artistName : "your roster"}
</span>
<div className="mt-4">
<div className="mb-4 mt-4">
<a
href={VALUATION_URL}
target="_blank"
Expand All @@ -70,6 +75,9 @@ export function ChatGreeting({ isVisible }: { isVisible: boolean }) {
Get a free catalog valuation &rarr;
</a>
</div>
<div className="text-left text-sm font-normal tracking-normal">
<TasksModule />
</div>
</div>
);
}
Expand Down
39 changes: 39 additions & 0 deletions components/Home/HomeRunRow.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import Link from "next/link";
import { cn } from "@/lib/utils";
import type { TaskRunItem } from "@/lib/tasks/getTaskRuns";
import { getTaskDisplayName } from "@/lib/tasks/getTaskDisplayName";
import { getStatusColor } from "@/lib/tasks/getStatusColor";
import { getStatusLabel } from "@/lib/tasks/getStatusLabel";
import { formatTimestamp } from "@/lib/tasks/formatTimestamp";

/**
* Compact task-run row for the homepage tasks module. Links to the
* existing run detail page. Per-run sent-email subjects are not exposed
* by `GET /api/tasks/runs` yet, so this shows what is available: run
* name, status, and finished-at (recoupable/chat#1850).
*/
const HomeRunRow = ({ run }: { run: TaskRunItem }) => (
<Link
href={`/tasks/${run.id}`}
className="group -mx-2 flex items-center justify-between gap-3 rounded-lg px-2 py-2.5 transition-colors hover:bg-muted"
>
<div className="min-w-0">
<p className="truncate text-sm font-medium text-foreground">
{getTaskDisplayName(run.taskIdentifier)}
</p>
<p className="text-xs text-muted-foreground">
{formatTimestamp(run.finishedAt ?? run.createdAt)}
</p>
</div>
<span
className={cn(
"shrink-0 rounded-full px-2 py-1 text-xs font-medium",
getStatusColor(run.status),
)}
>
{getStatusLabel(run.status)}
</span>
</Link>
);

export default HomeRunRow;
50 changes: 50 additions & 0 deletions components/Home/StarterTaskCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
"use client";

import { useArtistProvider } from "@/providers/ArtistProvider";
import { useCreateStarterTask } from "@/hooks/useCreateStarterTask";

/**
* Empty-state suggestion for the homepage tasks module: one click
* schedules an existing /agents Report template for the selected artist,
* Mondays at 9am, via the existing task-creation path
* (recoupable/chat#1850). Hidden when no Report template exists.
*/
const StarterTaskCard = () => {
const { selectedArtist } = useArtistProvider();
const { handleCreateStarterTask, isCreating, isScheduled, starterTemplate } =
useCreateStarterTask();
const artistName = selectedArtist?.name || "your artist";

if (!starterTemplate) return null;

if (isScheduled) {
return (
<p className="text-sm text-muted-foreground" role="status">
Scheduled: {starterTemplate.title} for {artistName}, Mondays at 9am.
</p>
);
}

return (
<div className="flex flex-col items-start justify-between gap-3 sm:flex-row sm:items-center">
<div>
<p className="text-sm font-medium text-foreground">
{starterTemplate.title} for {artistName}, Mondays
</p>
<p className="text-xs text-muted-foreground">
{starterTemplate.description}
</p>
</div>
<button
type="button"
onClick={() => handleCreateStarterTask()}
disabled={isCreating}

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: Users can click "Schedule it" in a state where creation is guaranteed to fail, causing an avoidable error toast loop. The button is only disabled during pending state, but useCreateStarterTask requires a non-null selectedArtist.name, so disabling when required artist fields are missing would prevent this broken path.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At components/Home/StarterTaskCard.tsx, line 39:

<comment>Users can click "Schedule it" in a state where creation is guaranteed to fail, causing an avoidable error toast loop. The button is only disabled during pending state, but `useCreateStarterTask` requires a non-null `selectedArtist.name`, so disabling when required artist fields are missing would prevent this broken path.</comment>

<file context>
@@ -0,0 +1,48 @@
+      <button
+        type="button"
+        onClick={() => handleCreateStarterTask()}
+        disabled={isCreating}
+        className="shrink-0 rounded-xl bg-primary px-4 py-2 text-sm font-semibold text-primary-foreground transition-colors hover:opacity-90 disabled:cursor-not-allowed disabled:opacity-50"
+      >
</file context>

className="shrink-0 rounded-xl bg-primary px-4 py-2 text-sm font-semibold text-primary-foreground transition-colors hover:opacity-90 disabled:cursor-not-allowed disabled:opacity-50"
>
{isCreating ? "Scheduling..." : "Schedule it"}
</button>
</div>
);
};

export default StarterTaskCard;
58 changes: 58 additions & 0 deletions components/Home/TasksModule.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
"use client";

import Link from "next/link";
import { useArtistProvider } from "@/providers/ArtistProvider";
import { useTaskRuns } from "@/hooks/useTaskRuns";
import { getHomeTasksModuleState } from "@/lib/home/getHomeTasksModuleState";
import HomeRunRow from "./HomeRunRow";
import StarterTaskCard from "./StarterTaskCard";

/**
* "Your label at work" homepage module: recent task runs (existing
* GET /api/tasks/runs) or the one-click starter suggestion for fresh
* accounts (recoupable/chat#1850). Renders nothing while loading or on
* 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,
});

if (state.view === "hidden") return null;

return (
<section
aria-label="Your label at work"
className="w-full rounded-xl bg-card p-6 shadow-[0px_0px_0px_1px_var(--border),0px_2px_4px_rgba(0,0,0,0.04)] dark:shadow-[0px_0px_0px_1px_var(--border),0px_2px_4px_rgba(0,0,0,0.2)]"
>
<div className="mb-3 flex items-center justify-between">
<h2 className="text-xs uppercase tracking-[0.05em] text-muted-foreground">
Your label at work
</h2>
<Link
href="/tasks"
className="text-xs font-medium text-foreground hover:underline"
>
View all
</Link>
</div>
{state.view === "runs" ? (
<div className="flex flex-col">
{state.runs.map((run) => (
<HomeRunRow key={run.id} run={run} />
))}
</div>
) : (
<StarterTaskCard />
)}
</section>
);
};

export default TasksModule;
79 changes: 79 additions & 0 deletions hooks/useCreateStarterTask.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
"use client";

import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { usePrivy } from "@privy-io/react-auth";
import { toast } from "sonner";
import { createStarterTask } from "@/lib/home/createStarterTask";
import { findStarterTemplate } from "@/lib/home/findStarterTemplate";
import fetchAgentTemplates from "@/lib/agent-templates/fetchAgentTemplates";
import type { AgentTemplateRow } from "@/types/AgentTemplates";
import { useArtistProvider } from "@/providers/ArtistProvider";
import { useUserProvider } from "@/providers/UserProvder";

/**
* One-click creation of the homepage starter task. The task is sourced
* from an existing /agents template (DRY — findStarterTemplate picks it
* from the shared agent-templates query); auth + artist come from
* providers and the mutation goes through the existing `POST /api/tasks`
* path which also mints the schedule (recoupable/chat#1850).
*/
export function useCreateStarterTask() {
const { getAccessToken } = usePrivy();
const { selectedArtist } = useArtistProvider();
const { userData } = useUserProvider();
const queryClient = useQueryClient();

const { data: templates } = useQuery<AgentTemplateRow[]>({
queryKey: ["agent-templates"],

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: Agent templates can be reused from a previous signed-in user/session because this user-specific fetch is cached under the global ["agent-templates"] key. Including userData.id in the query key would keep starter task template selection scoped to the current account.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At hooks/useCreateStarterTask.ts, line 27:

<comment>Agent templates can be reused from a previous signed-in user/session because this user-specific fetch is cached under the global ["agent-templates"] key. Including userData.id in the query key would keep starter task template selection scoped to the current account.</comment>

<file context>
@@ -1,22 +1,36 @@
   const queryClient = useQueryClient();
 
+  const { data: templates } = useQuery<AgentTemplateRow[]>({
+    queryKey: ["agent-templates"],
+    queryFn: () => fetchAgentTemplates(userData!),
+    retry: 1,
</file context>
Suggested change
queryKey: ["agent-templates"],
queryKey: ["agent-templates", userData?.id],

queryFn: () => fetchAgentTemplates(userData!),
retry: 1,
enabled: !!userData?.id,
});
const starterTemplate = findStarterTemplate(templates);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: When no Report starter template exists, the card disappears but the homepage module shell still renders with an empty body. Consider lifting template availability into the module state or otherwise hiding the parent section when starterTemplate is absent.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At hooks/useCreateStarterTask.ts, line 32:

<comment>When no Report starter template exists, the card disappears but the homepage module shell still renders with an empty body. Consider lifting template availability into the module state or otherwise hiding the parent section when starterTemplate is absent.</comment>

<file context>
@@ -1,22 +1,36 @@
+    retry: 1,
+    enabled: !!userData?.id,
+  });
+  const starterTemplate = findStarterTemplate(templates);
+
   const {
</file context>


const {
mutate: handleCreateStarterTask,
isPending: isCreating,
isSuccess: isScheduled,

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: The starter card can show a false "scheduled" state after artist switching, because isScheduled is tied to mutation success and never reset. Consider resetting mutation state when selectedArtist changes (or deriving scheduled status from fetched tasks) so the UI reflects the current artist.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At hooks/useCreateStarterTask.ts, line 23:

<comment>The starter card can show a false "scheduled" state after artist switching, because `isScheduled` is tied to mutation success and never reset. Consider resetting mutation state when `selectedArtist` changes (or deriving scheduled status from fetched tasks) so the UI reflects the current artist.</comment>

<file context>
@@ -0,0 +1,58 @@
+  const {
+    mutate: handleCreateStarterTask,
+    isPending: isCreating,
+    isSuccess: isScheduled,
+  } = useMutation({
+    mutationFn: async () => {
</file context>

} = useMutation({
mutationFn: async () => {
const artistAccountId = selectedArtist?.account_id;
const artistName = selectedArtist?.name;
if (!artistAccountId || !artistName) {
throw new Error("ARTIST_REQUIRED");
}
Comment on lines +42 to +44

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 Badge Handle selected artists with missing names

For a selected artist/workspace whose name is null or empty, the module still renders because it only checks account_id, and the card displays the fallback "your artist"; however this mutation throws ARTIST_REQUIRED, so clicking the visible CTA fails with "Please select an artist first" even though an artist is selected. Use the same fallback for task creation or require a usable name before showing the starter card.

Useful? React with 👍 / 👎.

if (!starterTemplate) {
throw new Error("TEMPLATE_REQUIRED");
}
const accessToken = await getAccessToken();
if (!accessToken) {
throw new Error("AUTH_REQUIRED");
}
return createStarterTask(accessToken, {
template: starterTemplate,
artistName,
artistAccountId,
});
},
onSuccess: async () => {
await queryClient.invalidateQueries({
queryKey: ["scheduled-actions"],
exact: false,
});
toast.success("Task scheduled for Mondays at 9am");
},
onError: (error) => {
if (error instanceof Error && error.message === "ARTIST_REQUIRED") {
toast.error("Please select an artist first.");
return;
}
if (error instanceof Error && error.message === "AUTH_REQUIRED") {
toast.error("Please sign in to create a task.");
return;
}
toast.error("Failed to create the task. Please try again.");
},
});

return { handleCreateStarterTask, isCreating, isScheduled, starterTemplate };
}
41 changes: 41 additions & 0 deletions lib/home/__tests__/buildStarterTaskParams.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { describe, expect, it } from "vitest";
import { buildStarterTaskParams } from "@/lib/home/buildStarterTaskParams";
import type { AgentTemplateRow } from "@/types/AgentTemplates";

const template = {
id: "9046c7e9-fd5a-4f08-a472-381d51bd6c90",
title: "Weekly Performance Dashboard",
description: "Weekly stats email",
prompt:
"Set up a weekly performance dashboard that emails me every Monday with my stats from all platforms.",
tags: ["Report"],
creator: null,
is_private: false,
created_at: null,
favorites_count: null,
updated_at: null,
} as AgentTemplateRow;

describe("buildStarterTaskParams", () => {
const params = buildStarterTaskParams({
template,
artistName: "Del Water Gap",
artistAccountId: "artist-1",
});

it("titles the task after the template and the artist", () => {
expect(params.title).toBe("Weekly Performance Dashboard — Del Water Gap");
});

it("uses the agent template's prompt verbatim (DRY — no hand-written prompt)", () => {
expect(params.prompt).toBe(template.prompt);
});

it("schedules Mondays", () => {
expect(params.schedule).toBe("0 9 * * 1");
});

it("targets the artist account", () => {
expect(params.artist_account_id).toBe("artist-1");
});
});
Loading
Loading