use-setup-task-groups.ts4.2 KBView on GitHub
import { useCallback, useMemo, useState } from 'react';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { toast } from 'sonner';
import { useTRPC } from '@/providers/query-provider';
import { listGroupsInput } from '@/modules/userTasks/utils/group-cache';
import { CEDAR_COLORS } from '@/components/ui/SexyColourPicker';

export interface StarterGroup {
  name: string;
  /** Lucide icon name, resolved through TASK_GROUP_ICON_MAP. */
  icon: string;
  color: string;
  /** Natural-language rule the AI router reads to file a task into this lane. */
  routingCriteria: string;
  blurb: string;
}

/**
 * The lanes Cedar's own agents already produce work for.
 *
 * They match the agent task types the drafting pipeline emits, so a user who
 * takes all five gets a board where every draft Cedar writes has somewhere to
 * land — rather than a Misc pile they have to sort by hand later.
 */
export const STARTER_TASK_GROUPS: StarterGroup[] = [
  {
    name: 'Follow-ups',
    icon: 'Send',
    color: CEDAR_COLORS.dark[5]!,
    routingCriteria: 'Sent threads that have gone quiet and need a nudge.',
    blurb: 'Threads you sent that nobody answered.',
  },
  {
    name: 'Replies',
    icon: 'Reply',
    color: CEDAR_COLORS.dark[2]!,
    routingCriteria: 'Inbound emails waiting on a response from me.',
    blurb: 'Inbound mail still waiting on you.',
  },
  {
    name: 'Post-meeting',
    icon: 'CalendarCheck',
    color: CEDAR_COLORS.dark[3]!,
    routingCriteria: 'Recap and next steps owed after a call has wrapped.',
    blurb: 'Recaps and next steps after a call.',
  },
  {
    name: 'Meeting prep',
    icon: 'CalendarClock',
    color: CEDAR_COLORS.dark[1]!,
    routingCriteria: 'Research and prep due before an upcoming external meeting.',
    blurb: 'Research due before the next call.',
  },
  {
    name: 'Deals',
    icon: 'Trophy',
    color: CEDAR_COLORS.dark[0]!,
    routingCriteria: 'Anything tied to an open deal moving through the pipeline.',
    blurb: 'Work attached to an open deal.',
  },
];

interface CachedGroupsPayload {
  groups: { id: string | null; name: string }[];
}

/**
 * Create the starter task groups, one click each.
 *
 * The write goes through `createGroup` and the list is refetched after — no
 * optimistic row, because the group's real id comes from the server and every
 * other surface keys off it. The button holds its own pending state instead, so
 * the click still feels answered.
 */
export function useSetupTaskGroups() {
  const trpc = useTRPC();
  const queryClient = useQueryClient();
  const [pending, setPending] = useState<string[]>([]);

  const groupsQuery = useQuery(trpc.taskGroups.listGroups.queryOptions(listGroupsInput()));

  const existingNames = useMemo(
    () =>
      new Set(
        ((groupsQuery.data as CachedGroupsPayload | undefined)?.groups ?? [])
          .filter((group) => group.id !== null)
          .map((group) => group.name.toLowerCase()),
      ),
    [groupsQuery.data],
  );

  const { mutateAsync: createGroup } = useMutation(
    trpc.taskGroups.createGroup.mutationOptions({
      onSettled: () =>
        queryClient.invalidateQueries({ queryKey=[redacted] }),
    }),
  );

  const has = useCallback(
    (name: string) => existingNames.has(name.toLowerCase()) || pending.includes(name),
    [existingNames, pending],
  );

  const create = useCallback(
    async (group: { name: string; icon?: string; color?: string; routingCriteria?: string }) => {
      const name = group.name.trim();
      if (!name || has(name)) return;
      setPending((current) => [...current, name]);
      try {
        await createGroup({
          name,
          icon: group.icon,
          color: group.color,
          routingCriteria: group.routingCriteria,
        });
      } catch (error) {
        toast.error(error instanceof Error ? error.message : `Couldn't create ${name}`);
      } finally {
        setPending((current) => current.filter((n) => n !== name));
      }
    },
    [createGroup, has],
  );

  return {
    starters: STARTER_TASK_GROUPS,
    groups: (groupsQuery.data as CachedGroupsPayload | undefined)?.groups ?? [],
    isLoading: groupsQuery.isPending,
    has,
    create,
  };
}