From 39ef3766576be8c01dad630f8774841ec015601d Mon Sep 17 00:00:00 2001 From: Kobe Attias Date: Thu, 8 Oct 2026 11:53:51 -0400 Subject: [PATCH] The homepage offers AI drafting in a dismissable toast, and the grill-me skill is added A toast in the bottom-right corner (from the tablet breakpoint up) tells people the workspace drafts an RFP or a proposal from a sentence, with a door to each. Phones get the same card above the feed instead. Closing either closes both, through one dismissable feature. For signed-out visitors the toast rides above the Join banner, which now publishes its height as --join-banner-height. Funding power's Add funds button is dark so it does not compete with the toast's primary button. Fixes signed-out dismissals being erased on reload: the hook cached "not dismissed" before the context had read localStorage, which removed the stored key. Signed-out users' dismissals are now read from storage. Also adds the grill-me skill (.agents/skills, skills-lock.json). Co-Authored-By: Claude Opus 5.5 --- .agents/skills/grill-me/SKILL.md | 7 + .agents/skills/grill-me/agents/openai.yaml | 5 + app/(home)/layout.tsx | 4 + components/AIMode/DraftWithAICard.tsx | 158 +++++++++++++++++++ components/Funding/FundingPowerCard.tsx | 1 + components/banners/JoinResearchHubBanner.tsx | 28 +++- contexts/DismissedFeaturesContext.tsx | 13 ++ hooks/useDismissableFeature.ts | 23 ++- skills-lock.json | 11 ++ 9 files changed, 243 insertions(+), 7 deletions(-) create mode 100644 .agents/skills/grill-me/SKILL.md create mode 100644 .agents/skills/grill-me/agents/openai.yaml create mode 100644 components/AIMode/DraftWithAICard.tsx create mode 100644 skills-lock.json diff --git a/.agents/skills/grill-me/SKILL.md b/.agents/skills/grill-me/SKILL.md new file mode 100644 index 000000000..3947ff9c4 --- /dev/null +++ b/.agents/skills/grill-me/SKILL.md @@ -0,0 +1,7 @@ +--- +name: grill-me +description: A relentless interview to sharpen a plan or design. +disable-model-invocation: true +--- + +Call the Skill tool with "grilling". diff --git a/.agents/skills/grill-me/agents/openai.yaml b/.agents/skills/grill-me/agents/openai.yaml new file mode 100644 index 000000000..4d6fb0c74 --- /dev/null +++ b/.agents/skills/grill-me/agents/openai.yaml @@ -0,0 +1,5 @@ +interface: + display_name: "Grill Me" + short_description: "Sharpen a plan through interview" +policy: + allow_implicit_invocation: false diff --git a/app/(home)/layout.tsx b/app/(home)/layout.tsx index 119f4a139..52cb8c8cd 100644 --- a/app/(home)/layout.tsx +++ b/app/(home)/layout.tsx @@ -2,12 +2,16 @@ import { ReactNode } from 'react'; import { PageLayout } from '@/app/layouts/PageLayout'; import { HomeTabs } from '@/components/Funding/HomeTabs'; import { HomeFeedsProvider } from '@/components/Funding/HomeFeedsProvider'; +import { DraftWithAICard, DraftWithAIToast } from '@/components/AIMode/DraftWithAICard'; export default function HomeLayout({ children }: { children: ReactNode }) { return ( + {/* Phones; from the tablet breakpoint up it is the toast instead. */} + + {children} diff --git a/components/AIMode/DraftWithAICard.tsx b/components/AIMode/DraftWithAICard.tsx new file mode 100644 index 000000000..d11833dba --- /dev/null +++ b/components/AIMode/DraftWithAICard.tsx @@ -0,0 +1,158 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import { Sparkles, X } from 'lucide-react'; +import { Button } from '@/components/ui/Button'; +import { useAuthenticatedAction } from '@/contexts/AuthModalContext'; +import { useFundingDrafting } from '@/components/Funding/useFundingDrafting'; +import type { FundingIntent } from '@/components/Funding/fundingDirection'; +import { useDismissableFeature } from '@/hooks/useDismissableFeature'; +import { cn } from '@/utils/styles'; + +/** One key for the toast and the card, so closing either closes both. */ +export const DRAFT_WITH_AI_CARD_FEATURE = 'draft_with_ai_card'; + +/** Long enough that the page settles before the toast arrives. */ +const TOAST_DELAY_MS = 1200; +/** Matches the toast's transition, so it finishes leaving before it unmounts. */ +const TOAST_EXIT_MS = 300; + +/** + * Tells people the workspace can draft an RFP or a proposal from a sentence, + * with a door to each. Above the feed on phones, where the bottom corner + * belongs to the nav and the funding power bar. Shown until dismissed. + */ +export function DraftWithAICard({ className }: { readonly className?: string }) { + const { isDismissed, dismissFeature, dismissStatus } = useDismissableFeature( + DRAFT_WITH_AI_CARD_FEATURE + ); + + if (dismissStatus !== 'checked' || isDismissed) return null; + + return ( +
+ +
+ ); +} + +/** + * The same pitch as a toast in the bottom-right corner, from the tablet + * breakpoint up. For a signed-out visitor it rides above the Join banner + * while that is up, which publishes its height as `--join-banner-height`. + */ +export function DraftWithAIToast() { + const { isDismissed, dismissFeature, dismissStatus } = useDismissableFeature( + DRAFT_WITH_AI_CARD_FEATURE + ); + const isEligible = dismissStatus === 'checked' && !isDismissed; + const [isShown, setIsShown] = useState(false); + + useEffect(() => { + if (!isEligible) return; + const timer = window.setTimeout(() => setIsShown(true), TOAST_DELAY_MS); + return () => window.clearTimeout(timer); + }, [isEligible]); + + if (!isEligible) return null; + + const dismiss = () => { + setIsShown(false); + window.setTimeout(dismissFeature, TOAST_EXIT_MS); + }; + + return ( + + ); +} + +function DraftWithAIContent({ + onDismiss, + buttonClassName, +}: { + readonly onDismiss: () => void; + readonly buttonClassName?: string; +}) { + const { executeAuthenticatedAction } = useAuthenticatedAction(); + const { startNew } = useFundingDrafting(); + const start = (intent: FundingIntent) => executeAuthenticatedAction(() => startNew(intent)); + + return ( + <> + + +
+ +
+

+ Let AI write the first draft +

+

+ One sentence in, a full RFP or proposal out. +

+
+
+ +
+ + +
+ + ); +} + +/** A request turning into an RFP: what the workspace does, drawn rather than said. */ +function DraftPreview({ className }: { className?: string }) { + return ( +
+

+ Fund early-career labs testing antivirals for long COVID +

+
+

+ + RFP · DRAFTING +

+ + +
+
+ ); +} diff --git a/components/Funding/FundingPowerCard.tsx b/components/Funding/FundingPowerCard.tsx index faeaf7a82..9af903f73 100644 --- a/components/Funding/FundingPowerCard.tsx +++ b/components/Funding/FundingPowerCard.tsx @@ -61,6 +61,7 @@ export const FundingPowerCard = ({ className }: FundingPowerCardProps) => { {/* Two-line so the subtitle can carry the methods, which is the whole reason to open it — "Deposit" alone read as RSC-only. */}