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. */}