|
1 | | -import type { ComponentType, SVGProps } from 'react' |
| 1 | +'use client' |
| 2 | + |
| 3 | +import { useState } from 'react' |
2 | 4 | import Image from 'next/image' |
3 | | -import { Search, Table } from '@/components/emcn/icons' |
4 | | -import { GmailIcon, GoogleCalendarIcon } from '@/components/icons' |
5 | | -import { MarkdownIcon } from '@/components/icons/document-icons' |
6 | | - |
7 | | -interface TemplatePrompt { |
8 | | - icon: ComponentType<SVGProps<SVGSVGElement>> |
9 | | - title: string |
10 | | - prompt: string |
11 | | - image: string |
| 5 | +import { ChevronDown } from '@/components/emcn/icons' |
| 6 | +import type { Category, ModuleTag } from './consts' |
| 7 | +import { CATEGORY_META, MODULE_META, TEMPLATES } from './consts' |
| 8 | + |
| 9 | +const FEATURED_TEMPLATES = TEMPLATES.filter((t) => t.featured) |
| 10 | +const EXTRA_TEMPLATES = TEMPLATES.filter((t) => !t.featured) |
| 11 | + |
| 12 | +/** Group non-featured templates by category, preserving category order. */ |
| 13 | +function getGroupedExtras() { |
| 14 | + const groups: { category: Category; label: string; templates: typeof TEMPLATES }[] = [] |
| 15 | + const byCategory = new Map<Category, typeof TEMPLATES>() |
| 16 | + |
| 17 | + for (const t of EXTRA_TEMPLATES) { |
| 18 | + const existing = byCategory.get(t.category) |
| 19 | + if (existing) { |
| 20 | + existing.push(t) |
| 21 | + } else { |
| 22 | + const arr = [t] |
| 23 | + byCategory.set(t.category, arr) |
| 24 | + } |
| 25 | + } |
| 26 | + |
| 27 | + for (const [key, meta] of Object.entries(CATEGORY_META)) { |
| 28 | + const cat = key as Category |
| 29 | + if (cat === 'popular') continue |
| 30 | + const items = byCategory.get(cat) |
| 31 | + if (items?.length) { |
| 32 | + groups.push({ category: cat, label: meta.label, templates: items }) |
| 33 | + } |
| 34 | + } |
| 35 | + |
| 36 | + return groups |
12 | 37 | } |
13 | 38 |
|
14 | | -const TEMPLATES: TemplatePrompt[] = [ |
15 | | - { |
16 | | - icon: Table, |
17 | | - title: 'Self-populating CRM', |
18 | | - prompt: |
19 | | - 'Create a self-healing CRM table that keeps track of all my customers by integrating with my existing data sources. Schedule a recurring job every morning to automatically pull updates from all relevant data sources and keep my CRM up to date.', |
20 | | - image: '/templates/crm-light.png', |
21 | | - }, |
22 | | - { |
23 | | - icon: GoogleCalendarIcon, |
24 | | - title: 'Meeting prep agent', |
25 | | - prompt: |
26 | | - 'Create an agent that checks my calendar each morning, pulls context on every attendee and topic, and prepares a brief for each meeting so I walk in fully prepared.', |
27 | | - image: '/templates/meeting-prep-dark.png', |
28 | | - }, |
29 | | - { |
30 | | - icon: MarkdownIcon, |
31 | | - title: 'Resolve todo list', |
32 | | - prompt: |
33 | | - 'Create a file of all my todos then go one by one and check off every time a todo is done. Look at my calendar and see what I have to do.', |
34 | | - image: '/templates/todo-list-light.png', |
35 | | - }, |
36 | | - { |
37 | | - icon: Search, |
38 | | - title: 'Research assistant', |
39 | | - prompt: |
40 | | - 'Build an agent that takes a topic, searches the web for the latest information, summarizes key findings, and compiles them into a clean document I can review.', |
41 | | - image: '/templates/research-assistant-dark.png', |
42 | | - }, |
43 | | - { |
44 | | - icon: GmailIcon, |
45 | | - title: 'Auto-reply agent', |
46 | | - prompt: 'Create a Gmail agent that drafts responses to relevant emails automatically.', |
47 | | - image: '/templates/gmail-agent-dark.png', |
48 | | - }, |
49 | | - { |
50 | | - icon: Table, |
51 | | - title: 'Expense tracker', |
52 | | - prompt: |
53 | | - 'Create a table that tracks all my expenses by pulling transactions from my connected accounts. Categorize each expense automatically and generate a weekly summary report.', |
54 | | - image: '/templates/expense-tracker-light.png', |
55 | | - }, |
56 | | -] |
| 39 | +const GROUPED_EXTRAS = getGroupedExtras() |
| 40 | + |
| 41 | +function ModulePills({ modules }: { modules: ModuleTag[] }) { |
| 42 | + return ( |
| 43 | + <div className='flex flex-wrap gap-[4px]'> |
| 44 | + {modules.map((mod) => ( |
| 45 | + <span |
| 46 | + key={mod} |
| 47 | + className='rounded-full bg-[var(--surface-3)] px-[6px] py-[1px] text-[11px] text-[var(--text-secondary)]' |
| 48 | + > |
| 49 | + {MODULE_META[mod].label} |
| 50 | + </span> |
| 51 | + ))} |
| 52 | + </div> |
| 53 | + ) |
| 54 | +} |
57 | 55 |
|
58 | 56 | interface TemplatePromptsProps { |
59 | 57 | onSelect: (prompt: string) => void |
60 | 58 | } |
61 | 59 |
|
62 | 60 | export function TemplatePrompts({ onSelect }: TemplatePromptsProps) { |
| 61 | + const [expanded, setExpanded] = useState(false) |
| 62 | + |
63 | 63 | return ( |
64 | | - <div className='grid grid-cols-3 gap-[16px]'> |
65 | | - {TEMPLATES.map((template) => { |
66 | | - const Icon = template.icon |
67 | | - return ( |
68 | | - <button |
69 | | - key={template.title} |
70 | | - type='button' |
71 | | - onClick={() => onSelect(template.prompt)} |
72 | | - className='group flex cursor-pointer flex-col text-left' |
73 | | - > |
74 | | - <div className='overflow-hidden rounded-[10px] border border-[var(--border-1)]'> |
75 | | - <div className='relative h-[120px] w-full overflow-hidden'> |
76 | | - <Image |
77 | | - src={template.image} |
78 | | - alt={template.title} |
79 | | - fill |
80 | | - unoptimized |
81 | | - className='object-cover transition-transform duration-300 group-hover:scale-105' |
82 | | - /> |
83 | | - </div> |
84 | | - <div className='flex items-center gap-[6px] border-[var(--border-1)] border-t bg-[var(--white)] px-[10px] py-[6px] dark:bg-[var(--surface-4)]'> |
85 | | - <Icon className='h-[14px] w-[14px] shrink-0 text-[var(--text-icon)]' /> |
86 | | - <span className='font-base text-[14px] text-[var(--text-body)]'> |
87 | | - {template.title} |
88 | | - </span> |
| 64 | + <div className='flex flex-col gap-[24px]'> |
| 65 | + {/* Featured grid */} |
| 66 | + <div className='grid grid-cols-3 gap-[16px]'> |
| 67 | + {FEATURED_TEMPLATES.map((template) => ( |
| 68 | + <TemplateCard key={template.title} template={template} onSelect={onSelect} /> |
| 69 | + ))} |
| 70 | + </div> |
| 71 | + |
| 72 | + {/* Expand / collapse */} |
| 73 | + <button |
| 74 | + type='button' |
| 75 | + onClick={() => setExpanded((prev) => !prev)} |
| 76 | + aria-expanded={expanded} |
| 77 | + className='flex items-center justify-center gap-[6px] text-[13px] text-[var(--text-secondary)] transition-colors hover:text-[var(--text-body)]' |
| 78 | + > |
| 79 | + {expanded ? ( |
| 80 | + <> |
| 81 | + Show less <ChevronDown className='h-[14px] w-[14px] rotate-180' /> |
| 82 | + </> |
| 83 | + ) : ( |
| 84 | + <> |
| 85 | + More examples <ChevronDown className='h-[14px] w-[14px]' /> |
| 86 | + </> |
| 87 | + )} |
| 88 | + </button> |
| 89 | + |
| 90 | + {/* Categorized extras */} |
| 91 | + {expanded && ( |
| 92 | + <div className='flex flex-col gap-[32px]'> |
| 93 | + {GROUPED_EXTRAS.map((group) => ( |
| 94 | + <div key={group.category} className='flex flex-col gap-[12px]'> |
| 95 | + <h3 className='font-medium text-[13px] text-[var(--text-secondary)]'> |
| 96 | + {group.label} |
| 97 | + </h3> |
| 98 | + <div className='grid grid-cols-3 gap-[16px]'> |
| 99 | + {group.templates.map((template) => ( |
| 100 | + <TemplateCard key={template.title} template={template} onSelect={onSelect} /> |
| 101 | + ))} |
89 | 102 | </div> |
90 | 103 | </div> |
91 | | - </button> |
92 | | - ) |
93 | | - })} |
| 104 | + ))} |
| 105 | + </div> |
| 106 | + )} |
94 | 107 | </div> |
95 | 108 | ) |
96 | 109 | } |
| 110 | + |
| 111 | +interface TemplateCardProps { |
| 112 | + template: (typeof TEMPLATES)[number] |
| 113 | + onSelect: (prompt: string) => void |
| 114 | +} |
| 115 | + |
| 116 | +function TemplateCard({ template, onSelect }: TemplateCardProps) { |
| 117 | + const Icon = template.icon |
| 118 | + |
| 119 | + return ( |
| 120 | + <button |
| 121 | + type='button' |
| 122 | + onClick={() => onSelect(template.prompt)} |
| 123 | + aria-label={`Select template: ${template.title}`} |
| 124 | + className='group flex cursor-pointer flex-col text-left' |
| 125 | + > |
| 126 | + <div className='overflow-hidden rounded-[10px] border border-[var(--border-1)]'> |
| 127 | + <div className='relative h-[120px] w-full overflow-hidden'> |
| 128 | + {template.image ? ( |
| 129 | + <Image |
| 130 | + src={template.image} |
| 131 | + alt={template.title} |
| 132 | + fill |
| 133 | + unoptimized |
| 134 | + className='object-cover transition-transform duration-300 group-hover:scale-105' |
| 135 | + /> |
| 136 | + ) : ( |
| 137 | + <div className='flex h-full w-full items-center justify-center bg-[var(--surface-3)] transition-colors group-hover:bg-[var(--surface-4)]'> |
| 138 | + <Icon className='h-[32px] w-[32px] text-[var(--text-icon)] opacity-40' /> |
| 139 | + </div> |
| 140 | + )} |
| 141 | + </div> |
| 142 | + <div className='flex flex-col gap-[4px] border-[var(--border-1)] border-t bg-[var(--white)] px-[10px] py-[6px] dark:bg-[var(--surface-4)]'> |
| 143 | + <div className='flex items-center gap-[6px]'> |
| 144 | + <Icon className='h-[14px] w-[14px] shrink-0 text-[var(--text-icon)]' /> |
| 145 | + <span className='font-base text-[14px] text-[var(--text-body)]'>{template.title}</span> |
| 146 | + </div> |
| 147 | + <ModulePills modules={template.modules} /> |
| 148 | + </div> |
| 149 | + </div> |
| 150 | + </button> |
| 151 | + ) |
| 152 | +} |
0 commit comments