Compare commits

...

4 Commits

Author SHA1 Message Date
Vikhyath Mondreti
33cf0de31e improvement(grain): make trigger names in line with API since resource type not known 2026-03-14 19:28:56 -07:00
Siddharth Ganesan
aad620c456 fix(mothership): run workflow tools (run from block, run until block) (#3595)
* Fix redis queuing and run

* Fix dynimp
2026-03-14 18:57:55 -07:00
Vikhyath Mondreti
f57294936b fix(embedded): viewport options breaking autolayout (#3596) 2026-03-14 18:57:36 -07:00
Waleed
8837f14194 feat(home): expand template examples with 83 categorized templates (#3592)
* feat(home): expand template examples with 83 categorized templates

- Extract template data into consts.ts with rich categorization (category, modules, tags)
- Expand from 6 to 83 templates across 7 categories: sales, support, engineering, marketing, productivity, operations
- Add show more/collapse UI with category groupings for non-featured templates
- Add connector-powered knowledge search templates (Gmail, Slack, Notion, Jira, Linear, Salesforce, etc.)
- Add platform-native templates (document summarizer, bulk data classifier, knowledge assistant, etc.)
- Optimize prompts for mothership execution with explicit resource creation and integration names
- Add tags field for future cross-cutting filtering by persona, pattern, and domain
- React 19.2.1 → 19.2.4 upgrade

* fix(home): remove WhatsApp customer notifications template

* fix(home): add aria-expanded to toggle button, skip popular in expanded view

* fix(home): fix category display order, add aria-label to template cards
2026-03-14 17:53:12 -07:00
24 changed files with 1485 additions and 255 deletions

View File

@@ -29,8 +29,8 @@
"next": "16.1.6",
"next-themes": "^0.4.6",
"postgres": "^3.4.5",
"react": "19.2.1",
"react-dom": "19.2.1",
"react": "19.2.4",
"react-dom": "19.2.4",
"shiki": "4.0.0",
"tailwind-merge": "^3.0.2"
},

View File

@@ -14,6 +14,7 @@ import {
} from '@/lib/copilot/chat-streaming'
import { COPILOT_REQUEST_MODES } from '@/lib/copilot/models'
import { orchestrateCopilotStream } from '@/lib/copilot/orchestrator'
import { getStreamMeta, readStreamEvents } from '@/lib/copilot/orchestrator/stream/buffer'
import {
authenticateCopilotRequestSessionOnly,
createBadRequestResponse,
@@ -454,6 +455,30 @@ export async function GET(req: NextRequest) {
return NextResponse.json({ success: false, error: 'Chat not found' }, { status: 404 })
}
let streamSnapshot: {
events: Array<{ eventId: number; streamId: string; event: Record<string, unknown> }>
status: string
} | null = null
if (chat.conversationId) {
try {
const [meta, events] = await Promise.all([
getStreamMeta(chat.conversationId),
readStreamEvents(chat.conversationId, 0),
])
streamSnapshot = {
events: events || [],
status: meta?.status || 'unknown',
}
} catch (err) {
logger.warn('Failed to read stream snapshot for chat', {
chatId,
conversationId: chat.conversationId,
error: err instanceof Error ? err.message : String(err),
})
}
}
const transformedChat = {
id: chat.id,
title: chat.title,
@@ -466,6 +491,7 @@ export async function GET(req: NextRequest) {
resources: Array.isArray(chat.resources) ? chat.resources : [],
createdAt: chat.createdAt,
updatedAt: chat.updatedAt,
...(streamSnapshot ? { streamSnapshot } : {}),
}
logger.info(`Retrieved chat ${chatId}`)

View File

@@ -0,0 +1,886 @@
import type { ComponentType, SVGProps } from 'react'
import {
BookOpen,
Bug,
Calendar,
Card,
ClipboardList,
DocumentAttachment,
File,
FolderCode,
Hammer,
Integration,
Layout,
Library,
Mail,
Pencil,
Rocket,
Search,
Send,
ShieldCheck,
Table,
Users,
Wrench,
} from '@/components/emcn/icons'
import {
AirtableIcon,
AmplitudeIcon,
ApolloIcon,
CalendlyIcon,
ConfluenceIcon,
DatadogIcon,
DiscordIcon,
FirecrawlIcon,
GithubIcon,
GmailIcon,
GongIcon,
GoogleCalendarIcon,
GoogleDriveIcon,
GoogleSheetsIcon,
GreenhouseIcon,
HubspotIcon,
IntercomIcon,
JiraIcon,
LemlistIcon,
LinearIcon,
LinkedInIcon,
MicrosoftTeamsIcon,
NotionIcon,
PagerDutyIcon,
RedditIcon,
SalesforceIcon,
ShopifyIcon,
SlackIcon,
StripeIcon,
TwilioIcon,
TypeformIcon,
WebflowIcon,
WordpressIcon,
YouTubeIcon,
ZendeskIcon,
} from '@/components/icons'
import { MarkdownIcon } from '@/components/icons/document-icons'
/**
* Modules that a template leverages.
* Used to show pill badges so users understand what platform features are involved.
*/
export const MODULE_META = {
'knowledge-base': { label: 'Knowledge Base' },
tables: { label: 'Tables' },
files: { label: 'Files' },
workflows: { label: 'Workflows' },
scheduled: { label: 'Scheduled Tasks' },
agent: { label: 'Agent' },
} as const
export type ModuleTag = keyof typeof MODULE_META
/**
* Categories for grouping templates in the UI.
*/
export const CATEGORY_META = {
popular: { label: 'Popular' },
sales: { label: 'Sales & CRM' },
support: { label: 'Support' },
engineering: { label: 'Engineering' },
marketing: { label: 'Marketing & Content' },
productivity: { label: 'Productivity' },
operations: { label: 'Operations' },
} as const
export type Category = keyof typeof CATEGORY_META
/**
* Freeform tags for cross-cutting concerns that don't fit neatly into a single category.
* Use these to filter templates by persona, pattern, or domain in the future.
*
* Persona tags: founder, sales, engineering, marketing, support, hr, finance, product, community, devops
* Pattern tags: monitoring, reporting, automation, research, sync, communication, analysis
* Domain tags: ecommerce, legal, recruiting, infrastructure, content, crm
*/
export type Tag = string
export interface TemplatePrompt {
icon: ComponentType<SVGProps<SVGSVGElement>>
title: string
prompt: string
image?: string
modules: ModuleTag[]
category: Category
tags: Tag[]
featured?: boolean
}
/**
* To add a new template:
* 1. Add an entry to this array with the required fields.
* 2. Set `featured: true` if it should appear in the initial grid.
* 3. Optionally add a screenshot to `/public/templates/` and reference it in `image`.
* 4. Add relevant `tags` for cross-cutting filtering (persona, pattern, domain).
*/
export const TEMPLATES: TemplatePrompt[] = [
// ── Popular / Featured ──────────────────────────────────────────────────
{
icon: Table,
title: 'Self-populating CRM',
prompt:
'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.',
image: '/templates/crm-light.png',
modules: ['tables', 'scheduled', 'workflows'],
category: 'popular',
tags: ['founder', 'sales', 'crm', 'sync', 'automation'],
featured: true,
},
{
icon: GoogleCalendarIcon,
title: 'Meeting prep agent',
prompt:
'Create an agent that checks my Google Calendar each morning, researches every attendee and topic on the web, and prepares a brief for each meeting so I walk in fully prepared. Schedule it to run every weekday morning.',
image: '/templates/meeting-prep-dark.png',
modules: ['agent', 'scheduled', 'workflows'],
category: 'popular',
tags: ['founder', 'sales', 'research', 'automation'],
featured: true,
},
{
icon: MarkdownIcon,
title: 'Resolve todo list',
prompt:
'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.',
image: '/templates/todo-list-light.png',
modules: ['files', 'agent', 'workflows'],
category: 'popular',
tags: ['individual', 'automation'],
featured: true,
},
{
icon: Search,
title: 'Research assistant',
prompt:
'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.',
image: '/templates/research-assistant-dark.png',
modules: ['agent', 'files', 'workflows'],
category: 'popular',
tags: ['founder', 'research', 'content', 'individual'],
featured: true,
},
{
icon: GmailIcon,
title: 'Auto-reply agent',
prompt:
'Create a workflow that reads my Gmail inbox, identifies emails that need a response, and drafts contextual replies for each one. Schedule it to run every hour.',
image: '/templates/gmail-agent-dark.png',
modules: ['agent', 'workflows'],
category: 'popular',
tags: ['individual', 'communication', 'automation'],
featured: true,
},
{
icon: Table,
title: 'Expense tracker',
prompt:
'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.',
image: '/templates/expense-tracker-light.png',
modules: ['tables', 'scheduled', 'workflows'],
category: 'popular',
tags: ['finance', 'individual', 'reporting'],
featured: true,
},
// ── Sales & CRM ────────────────────────────────────────────────────────
{
icon: FolderCode,
title: 'RFP and proposal drafter',
prompt:
'Create a knowledge base from my past proposals, case studies, and company information. Then build an agent that drafts responses to new RFPs by matching requirements to relevant past work, generating tailored sections, and compiling a complete proposal file.',
modules: ['knowledge-base', 'files', 'agent'],
category: 'sales',
tags: ['sales', 'content', 'enterprise'],
},
{
icon: Library,
title: 'Competitive battle cards',
prompt:
'Create an agent that deep-researches each of my competitors using web search — their product features, pricing, positioning, strengths, and weaknesses — and generates a structured battle card document for each one that my sales team can reference during calls.',
modules: ['agent', 'files', 'workflows'],
category: 'sales',
tags: ['sales', 'research', 'content'],
},
{
icon: ClipboardList,
title: 'QBR prep agent',
prompt:
'Build a workflow that compiles everything needed for a quarterly business review — pulling customer usage data, support ticket history, billing summary, and key milestones from my tables — and generates a polished QBR document ready to present.',
modules: ['tables', 'files', 'agent', 'workflows'],
category: 'sales',
tags: ['sales', 'support', 'reporting'],
},
{
icon: SalesforceIcon,
title: 'CRM knowledge search',
prompt:
'Create a knowledge base connected to my Salesforce account so all deals, contacts, notes, and activities are automatically synced and searchable. Then build an agent I can ask things like "what\'s the history with Acme Corp?" or "who was involved in the last enterprise deal?" and get instant answers with CRM record citations.',
modules: ['knowledge-base', 'agent'],
category: 'sales',
tags: ['sales', 'crm', 'research'],
},
{
icon: HubspotIcon,
title: 'HubSpot deal search',
prompt:
'Create a knowledge base connected to my HubSpot account so all deals, contacts, and activity history are automatically synced and searchable. Then build an agent I can ask things like "what happened with the Stripe integration deal?" or "which deals closed last quarter over $50k?" and get answers with HubSpot record links.',
modules: ['knowledge-base', 'agent'],
category: 'sales',
tags: ['sales', 'crm', 'research'],
},
{
icon: Users,
title: 'Lead enrichment pipeline',
prompt:
'Build a workflow that watches my leads table for new entries, enriches each lead with company size, funding, tech stack, and decision-maker contacts using Apollo and web search, then updates the table with the enriched information.',
modules: ['tables', 'agent', 'workflows'],
category: 'sales',
tags: ['sales', 'crm', 'automation', 'research'],
},
{
icon: ApolloIcon,
title: 'Prospect researcher',
prompt:
'Create an agent that takes a company name, deep-researches them across the web, finds key decision-makers, recent news, funding rounds, and pain points, then compiles a prospect brief I can review before outreach.',
modules: ['agent', 'files', 'workflows'],
category: 'sales',
tags: ['sales', 'research'],
},
{
icon: LemlistIcon,
title: 'Outbound sequence builder',
prompt:
'Build a workflow that reads leads from my table, researches each prospect and their company on the web, writes a personalized cold email tailored to their role and pain points, and sends it via Gmail. Schedule it to run daily to process new leads automatically.',
modules: ['tables', 'agent', 'workflows'],
category: 'sales',
tags: ['sales', 'communication', 'automation'],
},
{
icon: SalesforceIcon,
title: 'Deal pipeline tracker',
prompt:
'Create a table with columns for deal name, stage, amount, close date, and next steps. Build a workflow that syncs open deals from Salesforce into this table daily, and sends me a Slack summary each morning of deals that need attention or are at risk of slipping.',
modules: ['tables', 'scheduled', 'agent', 'workflows'],
category: 'sales',
tags: ['sales', 'crm', 'monitoring', 'reporting'],
},
{
icon: HubspotIcon,
title: 'Win/loss analyzer',
prompt:
'Build a workflow that pulls closed deals from HubSpot each week, analyzes patterns in wins vs losses — deal size, industry, sales cycle length, objections — and generates a report file with actionable insights on what to change. Schedule it to run every Monday.',
modules: ['agent', 'files', 'scheduled', 'workflows'],
category: 'sales',
tags: ['sales', 'crm', 'analysis', 'reporting'],
},
{
icon: GongIcon,
title: 'Sales call analyzer',
prompt:
'Build a workflow that pulls call transcripts from Gong after each sales call, identifies key objections raised, action items promised, and competitor mentions, updates the deal record in my CRM, and posts a call summary with next steps to the Slack deal channel.',
modules: ['agent', 'tables', 'workflows'],
category: 'sales',
tags: ['sales', 'analysis', 'communication'],
},
{
icon: WebflowIcon,
title: 'Webflow lead capture pipeline',
prompt:
'Create a workflow that monitors new Webflow form submissions, enriches each lead with company and contact data using Apollo and web search, adds them to a tracking table with a lead score, and sends a Slack notification to the sales team for high-potential leads.',
modules: ['tables', 'agent', 'workflows'],
category: 'sales',
tags: ['sales', 'crm', 'automation'],
},
// ── Support ─────────────────────────────────────────────────────────────
{
icon: Send,
title: 'Customer support bot',
prompt:
'Create a knowledge base and connect it to my Notion or Google Docs so it stays synced with my product documentation automatically. Then build an agent that answers customer questions using it with sourced citations and deploy it as a chat endpoint.',
modules: ['knowledge-base', 'agent', 'workflows'],
category: 'support',
tags: ['support', 'communication', 'automation'],
},
{
icon: SlackIcon,
title: 'Slack Q&A bot',
prompt:
'Create a knowledge base connected to my Notion workspace so it stays synced with my company wiki. Then build a workflow that monitors Slack channels for questions and answers them using the knowledge base with source citations.',
modules: ['knowledge-base', 'agent', 'workflows'],
category: 'support',
tags: ['support', 'communication', 'team'],
},
{
icon: IntercomIcon,
title: 'Customer feedback analyzer',
prompt:
'Build a scheduled workflow that pulls support tickets and conversations from Intercom daily, categorizes them by theme and sentiment, tracks trends in a table, and sends a weekly Slack report highlighting the top feature requests and pain points.',
modules: ['tables', 'scheduled', 'agent', 'workflows'],
category: 'support',
tags: ['support', 'product', 'analysis', 'reporting'],
},
{
icon: Table,
title: 'Churn risk detector',
prompt:
'Create a workflow that monitors customer activity — support ticket frequency, response sentiment, usage patterns — scores each account for churn risk in a table, and triggers a Slack alert to the account team when a customer crosses the risk threshold.',
modules: ['tables', 'scheduled', 'agent', 'workflows'],
category: 'support',
tags: ['support', 'sales', 'monitoring', 'analysis'],
},
{
icon: DiscordIcon,
title: 'Discord community manager',
prompt:
'Create a knowledge base connected to my Google Docs or Notion with product documentation. Then build a workflow that monitors my Discord server for unanswered questions, answers them using the knowledge base, tracks common questions in a table, and sends a weekly community summary to Slack.',
modules: ['knowledge-base', 'tables', 'agent', 'scheduled', 'workflows'],
category: 'support',
tags: ['community', 'support', 'communication'],
},
{
icon: TypeformIcon,
title: 'Survey response analyzer',
prompt:
'Create a workflow that pulls new Typeform responses daily, categorizes feedback by theme and sentiment, logs structured results to a table, and sends a Slack digest when a new batch of responses comes in with the key takeaways.',
modules: ['tables', 'scheduled', 'agent', 'workflows'],
category: 'support',
tags: ['product', 'analysis', 'reporting'],
},
{
icon: GmailIcon,
title: 'Email knowledge search',
prompt:
'Create a knowledge base connected to my Gmail so all my emails are automatically synced, chunked, and searchable. Then build an agent I can ask things like "what did Sarah say about the pricing proposal?" or "find the contract John sent last month" and get instant answers with the original email cited.',
modules: ['knowledge-base', 'agent'],
category: 'support',
tags: ['individual', 'research', 'communication'],
},
{
icon: ZendeskIcon,
title: 'Support ticket knowledge search',
prompt:
'Create a knowledge base connected to my Zendesk account so all past tickets, resolutions, and agent notes are automatically synced and searchable. Then build an agent my support team can ask things like "how do we usually resolve the SSO login issue?" or "has anyone reported this billing bug before?" to find past solutions instantly.',
modules: ['knowledge-base', 'agent'],
category: 'support',
tags: ['support', 'research', 'team'],
},
// ── Engineering ─────────────────────────────────────────────────────────
{
icon: Wrench,
title: 'Feature spec writer',
prompt:
'Create an agent that takes a rough feature idea or user story, researches how similar features work in competing products, and writes a complete product requirements document with user stories, acceptance criteria, edge cases, and technical considerations.',
modules: ['agent', 'files', 'workflows'],
category: 'engineering',
tags: ['product', 'engineering', 'research', 'content'],
},
{
icon: JiraIcon,
title: 'Jira knowledge search',
prompt:
'Create a knowledge base connected to my Jira project so all tickets, comments, and resolutions are automatically synced and searchable. Then build an agent I can ask things like "how did we fix the auth timeout issue?" or "what was decided about the API redesign?" and get answers with ticket citations.',
modules: ['knowledge-base', 'agent'],
category: 'engineering',
tags: ['engineering', 'research'],
},
{
icon: LinearIcon,
title: 'Linear knowledge search',
prompt:
'Create a knowledge base connected to my Linear workspace so all issues, comments, project updates, and decisions are automatically synced and searchable. Then build an agent I can ask things like "why did we deprioritize the mobile app?" or "what was the root cause of the checkout bug?" and get answers traced back to specific issues.',
modules: ['knowledge-base', 'agent'],
category: 'engineering',
tags: ['engineering', 'research', 'product'],
},
{
icon: Bug,
title: 'Bug triage agent',
prompt:
'Build an agent that monitors Sentry for new errors, automatically triages them by severity and affected users, creates Linear tickets for critical issues with full stack traces, and sends a Slack notification to the on-call channel.',
modules: ['agent', 'workflows'],
category: 'engineering',
tags: ['engineering', 'devops', 'automation'],
},
{
icon: GithubIcon,
title: 'PR review assistant',
prompt:
'Create a knowledge base connected to my GitHub repo so it stays synced with my style guide and coding standards. Then build a workflow that reviews new pull requests against it, checks for common issues and security vulnerabilities, and posts a review comment with specific suggestions.',
modules: ['knowledge-base', 'agent', 'workflows'],
category: 'engineering',
tags: ['engineering', 'automation'],
},
{
icon: GithubIcon,
title: 'Changelog generator',
prompt:
'Build a scheduled workflow that runs every Friday, pulls all merged PRs from GitHub for the week, categorizes changes as features, fixes, or improvements, and generates a user-facing changelog document with clear descriptions.',
modules: ['scheduled', 'agent', 'files', 'workflows'],
category: 'engineering',
tags: ['engineering', 'product', 'reporting', 'content'],
},
{
icon: LinearIcon,
title: 'Incident postmortem writer',
prompt:
'Create a workflow that when triggered after an incident, pulls the Slack thread from the incident channel, gathers relevant Sentry errors and deployment logs, and drafts a structured postmortem with timeline, root cause, and action items.',
modules: ['agent', 'files', 'workflows'],
category: 'engineering',
tags: ['engineering', 'devops', 'analysis'],
},
{
icon: NotionIcon,
title: 'Documentation auto-updater',
prompt:
'Create a knowledge base connected to my GitHub repository so code and docs stay synced. Then build a scheduled weekly workflow that detects API changes, compares them against the knowledge base to find outdated documentation, and either updates Notion pages directly or creates Linear tickets for the needed changes.',
modules: ['scheduled', 'agent', 'workflows'],
category: 'engineering',
tags: ['engineering', 'sync', 'automation'],
},
{
icon: PagerDutyIcon,
title: 'Incident response coordinator',
prompt:
'Create a knowledge base connected to my Confluence or Notion with runbooks and incident procedures. Then build a workflow triggered by PagerDuty incidents that searches the runbooks, gathers related Datadog alerts, identifies the on-call rotation, and posts a comprehensive incident brief to Slack.',
modules: ['knowledge-base', 'agent', 'workflows'],
category: 'engineering',
tags: ['devops', 'engineering', 'automation'],
},
{
icon: JiraIcon,
title: 'Sprint report generator',
prompt:
'Create a scheduled workflow that runs at the end of each sprint, pulls all completed, in-progress, and blocked Jira tickets, calculates velocity and carry-over, and generates a sprint summary document with charts and trends to share with the team.',
modules: ['scheduled', 'agent', 'files', 'workflows'],
category: 'engineering',
tags: ['engineering', 'reporting', 'team'],
},
{
icon: ConfluenceIcon,
title: 'Knowledge base sync',
prompt:
'Create a knowledge base connected to my Confluence workspace so all wiki pages are automatically synced and searchable. Then build a scheduled workflow that identifies stale pages not updated in 90 days and sends a Slack reminder to page owners to review them.',
modules: ['knowledge-base', 'scheduled', 'agent', 'workflows'],
category: 'engineering',
tags: ['engineering', 'sync', 'team'],
},
// ── Marketing & Content ─────────────────────────────────────────────────
{
icon: Pencil,
title: 'Long-form content writer',
prompt:
'Build a workflow that takes a topic or brief, researches it deeply across the web, generates a detailed outline, then writes a full long-form article with sections, examples, and a conclusion. Save the final draft as a document for review.',
modules: ['agent', 'files', 'workflows'],
category: 'marketing',
tags: ['content', 'research', 'marketing'],
},
{
icon: Layout,
title: 'Case study generator',
prompt:
'Create a knowledge base from my customer data and interview notes, then build a workflow that generates a polished case study file with the challenge, solution, results, and a pull quote — formatted and ready to publish.',
modules: ['knowledge-base', 'files', 'agent'],
category: 'marketing',
tags: ['marketing', 'content', 'sales'],
},
{
icon: Table,
title: 'Social media content calendar',
prompt:
'Build a workflow that generates a full month of social media content for my brand. Research trending topics in my industry, create a table with post dates, platforms, copy drafts, and hashtags, then schedule a weekly refresh to keep the calendar filled with fresh ideas.',
modules: ['tables', 'agent', 'scheduled', 'workflows'],
category: 'marketing',
tags: ['marketing', 'content', 'automation'],
},
{
icon: Integration,
title: 'Multi-language content translator',
prompt:
'Create a workflow that takes a document or blog post and translates it into multiple target languages while preserving tone, formatting, and brand voice. Save each translation as a separate file and flag sections that may need human review for cultural nuance.',
modules: ['files', 'agent', 'workflows'],
category: 'marketing',
tags: ['content', 'enterprise', 'automation'],
},
{
icon: YouTubeIcon,
title: 'Content repurposer',
prompt:
'Build a workflow that takes a YouTube video URL, pulls the video details and description, researches the topic on the web for additional context, and generates a Twitter thread, LinkedIn post, and blog summary optimized for each platform.',
modules: ['agent', 'files', 'workflows'],
category: 'marketing',
tags: ['marketing', 'content', 'automation'],
},
{
icon: RedditIcon,
title: 'Social mention tracker',
prompt:
'Create a scheduled workflow that monitors Reddit and X for mentions of my brand and competitors, scores each mention by sentiment and reach, logs them to a table, and sends a daily Slack digest of notable mentions.',
modules: ['tables', 'scheduled', 'agent', 'workflows'],
category: 'marketing',
tags: ['marketing', 'monitoring', 'analysis'],
},
{
icon: FirecrawlIcon,
title: 'SEO content brief generator',
prompt:
'Build a workflow that takes a target keyword, scrapes the top 10 ranking pages, analyzes their content structure and subtopics, then generates a detailed content brief with outline, word count target, questions to answer, and internal linking suggestions.',
modules: ['agent', 'files', 'workflows'],
category: 'marketing',
tags: ['marketing', 'content', 'research'],
},
{
icon: Mail,
title: 'Newsletter curator',
prompt:
'Create a scheduled weekly workflow that scrapes my favorite industry news sites and blogs, picks the top stories relevant to my audience, writes summaries for each, and drafts a ready-to-send newsletter in Mailchimp.',
modules: ['scheduled', 'agent', 'files', 'workflows'],
category: 'marketing',
tags: ['marketing', 'content', 'communication'],
},
{
icon: LinkedInIcon,
title: 'LinkedIn content engine',
prompt:
'Build a workflow that scrapes my company blog for new posts, generates LinkedIn posts with hooks, insights, and calls-to-action optimized for engagement, and saves drafts as files for my review before posting to LinkedIn.',
modules: ['agent', 'files', 'scheduled', 'workflows'],
category: 'marketing',
tags: ['marketing', 'content', 'automation'],
},
{
icon: WordpressIcon,
title: 'Blog auto-publisher',
prompt:
'Build a workflow that takes a draft document, optimizes it for SEO by researching target keywords, formats it for WordPress with proper headings and meta description, and publishes it as a draft post for final review.',
modules: ['agent', 'files', 'workflows'],
category: 'marketing',
tags: ['marketing', 'content', 'automation'],
},
// ── Productivity ────────────────────────────────────────────────────────
{
icon: BookOpen,
title: 'Personal knowledge assistant',
prompt:
'Create a knowledge base and connect it to my Google Drive, Notion, or Obsidian so all my notes, docs, and articles are automatically synced and embedded. Then build an agent that I can ask anything — it should answer with citations and deploy as a chat endpoint.',
modules: ['knowledge-base', 'agent'],
category: 'productivity',
tags: ['individual', 'research', 'team'],
},
{
icon: SlackIcon,
title: 'Slack knowledge search',
prompt:
'Create a knowledge base connected to my Slack workspace so all channel conversations and threads are automatically synced and searchable. Then build an agent I can ask things like "what did the team decide about the launch date?" or "what was the outcome of the design review?" and get answers with links to the original messages.',
modules: ['knowledge-base', 'agent'],
category: 'productivity',
tags: ['team', 'research', 'communication'],
},
{
icon: NotionIcon,
title: 'Notion knowledge search',
prompt:
'Create a knowledge base connected to my Notion workspace so all pages, databases, meeting notes, and wikis are automatically synced and searchable. Then build an agent I can ask things like "what\'s our refund policy?" or "what was decided in the Q3 planning doc?" and get instant answers with page links.',
modules: ['knowledge-base', 'agent'],
category: 'productivity',
tags: ['team', 'research'],
},
{
icon: GoogleDriveIcon,
title: 'Google Drive knowledge search',
prompt:
'Create a knowledge base connected to my Google Drive so all documents, spreadsheets, and presentations are automatically synced and searchable. Then build an agent I can ask things like "find the board deck from last quarter" or "what were the KPIs in the marketing plan?" and get answers with doc links.',
modules: ['knowledge-base', 'agent'],
category: 'productivity',
tags: ['individual', 'team', 'research'],
},
{
icon: DocumentAttachment,
title: 'Document summarizer',
prompt:
'Create a workflow that takes any uploaded document — PDF, contract, report, research paper — and generates a structured summary with key takeaways, action items, important dates, and a one-paragraph executive overview.',
modules: ['files', 'agent', 'workflows'],
category: 'productivity',
tags: ['individual', 'analysis', 'team'],
},
{
icon: Table,
title: 'Bulk data classifier',
prompt:
'Build a workflow that takes a table of unstructured data — support tickets, feedback, survey responses, leads, or any text — runs each row through an agent to classify, tag, score, and enrich it, then writes the structured results back to the table.',
modules: ['tables', 'agent', 'workflows'],
category: 'productivity',
tags: ['analysis', 'automation', 'team'],
},
{
icon: File,
title: 'Automated narrative report',
prompt:
'Build a scheduled workflow that pulls key data from my tables every week, analyzes trends and anomalies, and writes a narrative report — not just charts and numbers, but written insights explaining what changed, why it matters, and what to do next. Save it as a document and send a summary to Slack.',
modules: ['tables', 'scheduled', 'agent', 'files', 'workflows'],
category: 'productivity',
tags: ['founder', 'reporting', 'analysis'],
},
{
icon: Rocket,
title: 'Investor update writer',
prompt:
'Build a workflow that pulls key metrics from my tables — revenue, growth, burn rate, headcount, milestones — and drafts a concise investor update with highlights, lowlights, asks, and KPIs. Save it as a file I can review before sending. Schedule it to run on the first of each month.',
modules: ['tables', 'scheduled', 'agent', 'files', 'workflows'],
category: 'productivity',
tags: ['founder', 'reporting', 'communication'],
},
{
icon: BookOpen,
title: 'Email digest curator',
prompt:
'Create a scheduled daily workflow that searches the web for the latest articles, papers, and news on topics I care about, picks the top 5 most relevant pieces, writes a one-paragraph summary for each, and delivers a curated reading digest to my inbox or Slack.',
modules: ['scheduled', 'agent', 'files', 'workflows'],
category: 'productivity',
tags: ['individual', 'research', 'content'],
},
{
icon: Search,
title: 'Knowledge extractor',
prompt:
'Build a workflow that takes raw meeting notes, brainstorm dumps, or research transcripts, extracts the key insights, decisions, and facts, organizes them by topic, and saves them into my knowledge base so they are searchable and reusable in future conversations.',
modules: ['files', 'knowledge-base', 'agent', 'workflows'],
category: 'productivity',
tags: ['individual', 'team', 'research'],
},
{
icon: Calendar,
title: 'Weekly team digest',
prompt:
"Build a scheduled workflow that runs every Friday, pulls the week's GitHub commits, closed Linear issues, and key Slack conversations, then emails a formatted weekly summary to the team.",
modules: ['scheduled', 'agent', 'workflows'],
category: 'productivity',
tags: ['engineering', 'team', 'reporting'],
},
{
icon: ClipboardList,
title: 'Daily standup summary',
prompt:
'Create a scheduled workflow that reads the #standup Slack channel each morning, summarizes what everyone is working on, identifies blockers, and posts a structured recap to a Google Doc.',
modules: ['scheduled', 'agent', 'files', 'workflows'],
category: 'productivity',
tags: ['team', 'reporting', 'communication'],
},
{
icon: GmailIcon,
title: 'Email triage assistant',
prompt:
'Build a workflow that scans my Gmail inbox every hour, categorizes emails by urgency and type (action needed, FYI, follow-up), drafts replies for routine messages, and sends me a prioritized summary in Slack so I only open what matters. Schedule it to run hourly.',
modules: ['agent', 'scheduled', 'workflows'],
category: 'productivity',
tags: ['individual', 'communication', 'automation'],
},
{
icon: SlackIcon,
title: 'Meeting notes to action items',
prompt:
'Create a workflow that takes meeting notes or a transcript, extracts action items with owners and due dates, creates tasks in Linear or Asana for each one, and posts a summary to the relevant Slack channel.',
modules: ['agent', 'workflows'],
category: 'productivity',
tags: ['team', 'automation'],
},
{
icon: GoogleSheetsIcon,
title: 'Weekly metrics report',
prompt:
'Build a scheduled workflow that pulls data from Stripe and my database every Monday, calculates key metrics like MRR, churn, new subscriptions, and failed payments, populates a Google Sheet, and Slacks the team a summary with week-over-week trends.',
modules: ['scheduled', 'tables', 'agent', 'workflows'],
category: 'productivity',
tags: ['founder', 'finance', 'reporting'],
},
{
icon: AmplitudeIcon,
title: 'Product analytics digest',
prompt:
'Create a scheduled weekly workflow that pulls key product metrics from Amplitude — active users, feature adoption rates, retention cohorts, and top events — generates an executive summary with week-over-week trends, and posts it to Slack.',
modules: ['scheduled', 'agent', 'workflows'],
category: 'productivity',
tags: ['product', 'reporting', 'analysis'],
},
{
icon: CalendlyIcon,
title: 'Scheduling follow-up automator',
prompt:
'Build a workflow that monitors new Calendly bookings, researches each attendee and their company, prepares a pre-meeting brief with relevant context, and sends a personalized confirmation email with an agenda and any prep materials.',
modules: ['agent', 'workflows'],
category: 'productivity',
tags: ['sales', 'research', 'automation'],
},
{
icon: TwilioIcon,
title: 'SMS appointment reminders',
prompt:
'Create a scheduled workflow that checks Google Calendar each morning for appointments in the next 24 hours, and sends an SMS reminder to each attendee via Twilio with the meeting time, location, and any prep notes.',
modules: ['scheduled', 'agent', 'workflows'],
category: 'productivity',
tags: ['individual', 'communication', 'automation'],
},
{
icon: MicrosoftTeamsIcon,
title: 'Microsoft Teams daily brief',
prompt:
'Build a scheduled workflow that pulls updates from your project tools — GitHub commits, Jira ticket status changes, and calendar events — and posts a formatted daily brief to your Microsoft Teams channel each morning.',
modules: ['scheduled', 'agent', 'workflows'],
category: 'productivity',
tags: ['team', 'reporting', 'enterprise'],
},
// ── Operations ──────────────────────────────────────────────────────────
{
icon: Table,
title: 'Data cleanup agent',
prompt:
'Create a workflow that takes a messy table — inconsistent formatting, duplicates, missing fields, typos — and cleans it up by standardizing values, merging duplicates, filling gaps where possible, and flagging rows that need human review.',
modules: ['tables', 'agent', 'workflows'],
category: 'operations',
tags: ['automation', 'analysis'],
},
{
icon: Hammer,
title: 'Training material generator',
prompt:
'Create a knowledge base from my product documentation, then build a workflow that generates training materials from it — onboarding guides, FAQ documents, step-by-step tutorials, and quiz questions. Schedule it to regenerate weekly so materials stay current as docs change.',
modules: ['knowledge-base', 'files', 'agent', 'scheduled'],
category: 'operations',
tags: ['hr', 'content', 'team', 'automation'],
},
{
icon: File,
title: 'SOP generator',
prompt:
'Create an agent that takes a brief description of any business process — from employee onboarding to incident response to content publishing — and generates a detailed standard operating procedure document with numbered steps, responsible roles, decision points, and checklists.',
modules: ['files', 'agent'],
category: 'operations',
tags: ['team', 'enterprise', 'content'],
},
{
icon: Card,
title: 'Invoice processor',
prompt:
'Build a workflow that processes invoice PDFs from Gmail, extracts vendor name, amount, due date, and line items, then logs everything to a tracking table and sends a Slack alert for invoices due within 7 days.',
modules: ['files', 'tables', 'agent', 'workflows'],
category: 'operations',
tags: ['finance', 'automation'],
},
{
icon: File,
title: 'Contract analyzer',
prompt:
'Create a knowledge base from my standard contract terms, then build a workflow that reviews uploaded contracts against it — extracting key clauses like payment terms, liability caps, and termination conditions, flagging deviations, and outputting a summary to a table.',
modules: ['knowledge-base', 'files', 'tables', 'agent'],
category: 'operations',
tags: ['legal', 'analysis'],
},
{
icon: FirecrawlIcon,
title: 'Competitive intel monitor',
prompt:
'Build a scheduled workflow that scrapes competitor websites, pricing pages, and changelog pages weekly using Firecrawl, compares against previous snapshots, summarizes any changes, logs them to a tracking table, and sends a Slack alert for major updates.',
modules: ['scheduled', 'tables', 'agent', 'workflows'],
category: 'operations',
tags: ['founder', 'product', 'monitoring', 'research'],
},
{
icon: StripeIcon,
title: 'Revenue operations dashboard',
prompt:
'Create a scheduled daily workflow that pulls payment data from Stripe, calculates MRR, net revenue, failed payments, and new subscriptions, logs everything to a table with historical tracking, and sends a daily Slack summary with trends and anomalies.',
modules: ['tables', 'scheduled', 'agent', 'workflows'],
category: 'operations',
tags: ['finance', 'founder', 'reporting', 'monitoring'],
},
{
icon: ShopifyIcon,
title: 'E-commerce order monitor',
prompt:
'Build a workflow that monitors Shopify orders, flags high-value or unusual orders for review, tracks fulfillment status in a table, and sends daily inventory and sales summaries to Slack with restock alerts when items run low.',
modules: ['tables', 'scheduled', 'agent', 'workflows'],
category: 'operations',
tags: ['ecommerce', 'monitoring', 'reporting'],
},
{
icon: ShieldCheck,
title: 'Compliance document checker',
prompt:
'Create a knowledge base from my compliance requirements and policies, then build an agent that reviews uploaded policy documents and SOC 2 evidence against it, identifies gaps or outdated sections, and generates a remediation checklist file with priority levels.',
modules: ['knowledge-base', 'files', 'agent'],
category: 'operations',
tags: ['legal', 'enterprise', 'analysis'],
},
{
icon: Users,
title: 'New hire onboarding automation',
prompt:
"Build a workflow that when triggered with a new hire's info, creates their accounts, sends a personalized welcome message in Slack, schedules 1:1s with their team on Google Calendar, shares relevant onboarding docs from the knowledge base, and tracks completion in a table.",
modules: ['knowledge-base', 'tables', 'agent', 'workflows'],
category: 'operations',
tags: ['hr', 'automation', 'team'],
},
{
icon: Library,
title: 'Candidate screening assistant',
prompt:
'Create a knowledge base from my job descriptions and hiring criteria, then build a workflow that takes uploaded resumes, evaluates candidates against the requirements, scores them on experience, skills, and culture fit, and populates a comparison table with a summary and recommendation for each.',
modules: ['knowledge-base', 'files', 'tables', 'agent'],
category: 'operations',
tags: ['hr', 'recruiting', 'analysis'],
},
{
icon: GreenhouseIcon,
title: 'Recruiting pipeline automator',
prompt:
'Build a scheduled workflow that syncs open jobs and candidates from Greenhouse to a tracking table daily, flags candidates who have been in the same stage for more than 5 days, and sends a Slack summary to hiring managers with pipeline stats and bottlenecks.',
modules: ['tables', 'scheduled', 'agent', 'workflows'],
category: 'operations',
tags: ['hr', 'recruiting', 'monitoring', 'reporting'],
},
{
icon: DatadogIcon,
title: 'Infrastructure health report',
prompt:
'Create a scheduled daily workflow that queries Datadog for key infrastructure metrics — error rates, latency percentiles, CPU and memory usage — logs them to a table for trend tracking, and sends a morning Slack report highlighting any anomalies or degradations.',
modules: ['tables', 'scheduled', 'agent', 'workflows'],
category: 'operations',
tags: ['devops', 'infrastructure', 'monitoring', 'reporting'],
},
{
icon: AirtableIcon,
title: 'Airtable data sync',
prompt:
'Create a scheduled workflow that syncs records from my Airtable base into a Sim table every hour, keeping both in sync. Use an agent to detect changes, resolve conflicts, and flag any discrepancies for review in Slack.',
modules: ['tables', 'scheduled', 'agent', 'workflows'],
category: 'operations',
tags: ['sync', 'automation'],
},
{
icon: Search,
title: 'Multi-source knowledge hub',
prompt:
'Create a knowledge base and connect it to Confluence, Notion, and Google Drive so all my company documentation is automatically synced, chunked, and embedded. Then deploy a Q&A agent that can answer questions across all sources with citations.',
modules: ['knowledge-base', 'scheduled', 'agent', 'workflows'],
category: 'operations',
tags: ['enterprise', 'team', 'sync', 'automation'],
},
{
icon: Table,
title: 'Customer 360 view',
prompt:
'Create a comprehensive customer table that aggregates data from my CRM, support tickets, billing history, and product usage into a single unified view per customer. Schedule it to sync daily and send a Slack alert when any customer shows signs of trouble across multiple signals.',
modules: ['tables', 'scheduled', 'agent', 'workflows'],
category: 'operations',
tags: ['founder', 'sales', 'support', 'enterprise', 'sync'],
},
]

View File

@@ -1 +1,3 @@
export type { Category, ModuleTag, Tag, TemplatePrompt } from './consts'
export { CATEGORY_META, MODULE_META, TEMPLATES } from './consts'
export { TemplatePrompts } from './template-prompts'

View File

@@ -1,96 +1,152 @@
import type { ComponentType, SVGProps } from 'react'
import Image from 'next/image'
import { Search, Table } from '@/components/emcn/icons'
import { GmailIcon, GoogleCalendarIcon } from '@/components/icons'
import { MarkdownIcon } from '@/components/icons/document-icons'
'use client'
interface TemplatePrompt {
icon: ComponentType<SVGProps<SVGSVGElement>>
title: string
prompt: string
image: string
import { useState } from 'react'
import Image from 'next/image'
import { ChevronDown } from '@/components/emcn/icons'
import type { Category, ModuleTag } from './consts'
import { CATEGORY_META, MODULE_META, TEMPLATES } from './consts'
const FEATURED_TEMPLATES = TEMPLATES.filter((t) => t.featured)
const EXTRA_TEMPLATES = TEMPLATES.filter((t) => !t.featured)
/** Group non-featured templates by category, preserving category order. */
function getGroupedExtras() {
const groups: { category: Category; label: string; templates: typeof TEMPLATES }[] = []
const byCategory = new Map<Category, typeof TEMPLATES>()
for (const t of EXTRA_TEMPLATES) {
const existing = byCategory.get(t.category)
if (existing) {
existing.push(t)
} else {
const arr = [t]
byCategory.set(t.category, arr)
}
}
for (const [key, meta] of Object.entries(CATEGORY_META)) {
const cat = key as Category
if (cat === 'popular') continue
const items = byCategory.get(cat)
if (items?.length) {
groups.push({ category: cat, label: meta.label, templates: items })
}
}
return groups
}
const TEMPLATES: TemplatePrompt[] = [
{
icon: Table,
title: 'Self-populating CRM',
prompt:
'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.',
image: '/templates/crm-light.png',
},
{
icon: GoogleCalendarIcon,
title: 'Meeting prep agent',
prompt:
'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.',
image: '/templates/meeting-prep-dark.png',
},
{
icon: MarkdownIcon,
title: 'Resolve todo list',
prompt:
'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.',
image: '/templates/todo-list-light.png',
},
{
icon: Search,
title: 'Research assistant',
prompt:
'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.',
image: '/templates/research-assistant-dark.png',
},
{
icon: GmailIcon,
title: 'Auto-reply agent',
prompt: 'Create a Gmail agent that drafts responses to relevant emails automatically.',
image: '/templates/gmail-agent-dark.png',
},
{
icon: Table,
title: 'Expense tracker',
prompt:
'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.',
image: '/templates/expense-tracker-light.png',
},
]
const GROUPED_EXTRAS = getGroupedExtras()
function ModulePills({ modules }: { modules: ModuleTag[] }) {
return (
<div className='flex flex-wrap gap-[4px]'>
{modules.map((mod) => (
<span
key={mod}
className='rounded-full bg-[var(--surface-3)] px-[6px] py-[1px] text-[11px] text-[var(--text-secondary)]'
>
{MODULE_META[mod].label}
</span>
))}
</div>
)
}
interface TemplatePromptsProps {
onSelect: (prompt: string) => void
}
export function TemplatePrompts({ onSelect }: TemplatePromptsProps) {
const [expanded, setExpanded] = useState(false)
return (
<div className='grid grid-cols-3 gap-[16px]'>
{TEMPLATES.map((template) => {
const Icon = template.icon
return (
<button
key={template.title}
type='button'
onClick={() => onSelect(template.prompt)}
className='group flex cursor-pointer flex-col text-left'
>
<div className='overflow-hidden rounded-[10px] border border-[var(--border-1)]'>
<div className='relative h-[120px] w-full overflow-hidden'>
<Image
src={template.image}
alt={template.title}
fill
unoptimized
className='object-cover transition-transform duration-300 group-hover:scale-105'
/>
</div>
<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)]'>
<Icon className='h-[14px] w-[14px] shrink-0 text-[var(--text-icon)]' />
<span className='font-base text-[14px] text-[var(--text-body)]'>
{template.title}
</span>
<div className='flex flex-col gap-[24px]'>
{/* Featured grid */}
<div className='grid grid-cols-3 gap-[16px]'>
{FEATURED_TEMPLATES.map((template) => (
<TemplateCard key={template.title} template={template} onSelect={onSelect} />
))}
</div>
{/* Expand / collapse */}
<button
type='button'
onClick={() => setExpanded((prev) => !prev)}
aria-expanded={expanded}
className='flex items-center justify-center gap-[6px] text-[13px] text-[var(--text-secondary)] transition-colors hover:text-[var(--text-body)]'
>
{expanded ? (
<>
Show less <ChevronDown className='h-[14px] w-[14px] rotate-180' />
</>
) : (
<>
More examples <ChevronDown className='h-[14px] w-[14px]' />
</>
)}
</button>
{/* Categorized extras */}
{expanded && (
<div className='flex flex-col gap-[32px]'>
{GROUPED_EXTRAS.map((group) => (
<div key={group.category} className='flex flex-col gap-[12px]'>
<h3 className='font-medium text-[13px] text-[var(--text-secondary)]'>
{group.label}
</h3>
<div className='grid grid-cols-3 gap-[16px]'>
{group.templates.map((template) => (
<TemplateCard key={template.title} template={template} onSelect={onSelect} />
))}
</div>
</div>
</button>
)
})}
))}
</div>
)}
</div>
)
}
interface TemplateCardProps {
template: (typeof TEMPLATES)[number]
onSelect: (prompt: string) => void
}
function TemplateCard({ template, onSelect }: TemplateCardProps) {
const Icon = template.icon
return (
<button
type='button'
onClick={() => onSelect(template.prompt)}
aria-label={`Select template: ${template.title}`}
className='group flex cursor-pointer flex-col text-left'
>
<div className='overflow-hidden rounded-[10px] border border-[var(--border-1)]'>
<div className='relative h-[120px] w-full overflow-hidden'>
{template.image ? (
<Image
src={template.image}
alt={template.title}
fill
unoptimized
className='object-cover transition-transform duration-300 group-hover:scale-105'
/>
) : (
<div className='flex h-full w-full items-center justify-center bg-[var(--surface-3)] transition-colors group-hover:bg-[var(--surface-4)]'>
<Icon className='h-[32px] w-[32px] text-[var(--text-icon)] opacity-40' />
</div>
)}
</div>
<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)]'>
<div className='flex items-center gap-[6px]'>
<Icon className='h-[14px] w-[14px] shrink-0 text-[var(--text-icon)]' />
<span className='font-base text-[14px] text-[var(--text-body)]'>{template.title}</span>
</div>
<ModulePills modules={template.modules} />
</div>
</div>
</button>
)
}

View File

@@ -178,6 +178,7 @@ export function Home({ chatId }: HomeProps = {}) {
const {
messages,
isSending,
isReconnecting,
sendMessage,
stopGeneration,
resolvedChatId,
@@ -330,7 +331,7 @@ export function Home({ chatId }: HomeProps = {}) {
return () => ro.disconnect()
}, [hasMessages])
if (!hasMessages && chatId && isLoadingHistory) {
if (chatId && (isLoadingHistory || isReconnecting)) {
return (
<ChatSkeleton>
<UserInput

View File

@@ -45,6 +45,7 @@ import type {
export interface UseChatReturn {
messages: ChatMessage[]
isSending: boolean
isReconnecting: boolean
error: string | null
resolvedChatId: string | undefined
sendMessage: (
@@ -250,6 +251,7 @@ export function useChat(
const queryClient = useQueryClient()
const [messages, setMessages] = useState<ChatMessage[]>([])
const [isSending, setIsSending] = useState(false)
const [isReconnecting, setIsReconnecting] = useState(false)
const [error, setError] = useState<string | null>(null)
const [resolvedChatId, setResolvedChatId] = useState<string | undefined>(initialChatId)
const [resources, setResources] = useState<MothershipResource[]>([])
@@ -268,6 +270,10 @@ export function useChat(
}, [messageQueue])
const sendMessageRef = useRef<UseChatReturn['sendMessage']>(async () => {})
const processSSEStreamRef = useRef<
(reader: ReadableStreamDefaultReader<Uint8Array>, assistantId: string) => Promise<void>
>(async () => {})
const finalizeRef = useRef<(options?: { error?: boolean }) => void>(() => {})
const abortControllerRef = useRef<AbortController | null>(null)
const chatIdRef = useRef<string | undefined>(initialChatId)
@@ -329,6 +335,7 @@ export function useChat(
setMessages([])
setError(null)
setIsSending(false)
setIsReconnecting(false)
setResources([])
setActiveResourceId(null)
setMessageQueue([])
@@ -346,6 +353,7 @@ export function useChat(
setMessages([])
setError(null)
setIsSending(false)
setIsReconnecting(false)
setResources([])
setActiveResourceId(null)
setMessageQueue([])
@@ -365,6 +373,95 @@ export function useChat(
ensureWorkflowInRegistry(resource.id, resource.title, workspaceId)
}
}
// Kick off stream reconnection immediately if there's an active stream.
// The stream snapshot was fetched in parallel with the chat history (same
// API call), so there's no extra round-trip.
const activeStreamId = chatHistory.activeStreamId
const snapshot = chatHistory.streamSnapshot
if (activeStreamId && !sendingRef.current) {
const gen = ++streamGenRef.current
const abortController = new AbortController()
abortControllerRef.current = abortController
streamIdRef.current = activeStreamId
sendingRef.current = true
setIsReconnecting(true)
const assistantId = crypto.randomUUID()
const reconnect = async () => {
try {
const encoder = new TextEncoder()
const batchEvents = snapshot?.events ?? []
const streamStatus = snapshot?.status ?? ''
if (!snapshot || (batchEvents.length === 0 && streamStatus === 'unknown')) {
// No snapshot available — stream buffer expired. Clean up.
const cid = chatIdRef.current
if (cid) {
fetch('/api/mothership/chat/stop', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ chatId: cid, streamId: activeStreamId, content: '' }),
}).catch(() => {})
}
return
}
setIsSending(true)
setIsReconnecting(false)
const lastEventId =
batchEvents.length > 0 ? batchEvents[batchEvents.length - 1].eventId : 0
const isStreamDone = streamStatus === 'complete' || streamStatus === 'error'
const combinedStream = new ReadableStream<Uint8Array>({
async start(controller) {
if (batchEvents.length > 0) {
const sseText = batchEvents
.map((e) => `data: ${JSON.stringify(e.event)}\n`)
.join('\n')
controller.enqueue(encoder.encode(`${sseText}\n`))
}
if (!isStreamDone) {
try {
const sseRes = await fetch(
`/api/copilot/chat/stream?streamId=${activeStreamId}&from=${lastEventId}`,
{ signal: abortController.signal }
)
if (sseRes.ok && sseRes.body) {
const reader = sseRes.body.getReader()
while (true) {
const { done, value } = await reader.read()
if (done) break
controller.enqueue(value)
}
}
} catch (err) {
if (!(err instanceof Error && err.name === 'AbortError')) {
logger.warn('SSE tail failed during reconnect', err)
}
}
}
controller.close()
},
})
await processSSEStreamRef.current(combinedStream.getReader(), assistantId)
} catch (err) {
if (err instanceof Error && err.name === 'AbortError') return
} finally {
setIsReconnecting(false)
if (streamGenRef.current === gen) {
finalizeRef.current()
}
}
}
reconnect()
}
}, [chatHistory, workspaceId])
useEffect(() => {
@@ -405,11 +502,14 @@ export function useChat(
const flush = () => {
streamingBlocksRef.current = [...blocks]
setMessages((prev) =>
prev.map((m) =>
m.id === assistantId ? { ...m, content: runningText, contentBlocks: [...blocks] } : m
)
)
const snapshot = { content: runningText, contentBlocks: [...blocks] }
setMessages((prev) => {
const idx = prev.findIndex((m) => m.id === assistantId)
if (idx >= 0) {
return prev.map((m) => (m.id === assistantId ? { ...m, ...snapshot } : m))
}
return [...prev, { id: assistantId, role: 'assistant' as const, ...snapshot }]
})
}
while (true) {
@@ -662,6 +762,9 @@ export function useChat(
},
[workspaceId, queryClient, addResource, removeResource]
)
useLayoutEffect(() => {
processSSEStreamRef.current = processSSEStream
})
const persistPartialResponse = useCallback(async () => {
const chatId = chatIdRef.current
@@ -750,50 +853,9 @@ export function useChat(
},
[invalidateChatQueries]
)
useEffect(() => {
const activeStreamId = chatHistory?.activeStreamId
if (!activeStreamId || !appliedChatIdRef.current || sendingRef.current) return
const gen = ++streamGenRef.current
const abortController = new AbortController()
abortControllerRef.current = abortController
sendingRef.current = true
setIsSending(true)
const assistantId = crypto.randomUUID()
setMessages((prev) => [
...prev,
{
id: assistantId,
role: 'assistant' as const,
content: '',
contentBlocks: [],
},
])
const reconnect = async () => {
try {
const response = await fetch(`/api/copilot/chat/stream?streamId=${activeStreamId}&from=0`, {
signal: abortController.signal,
})
if (!response.ok || !response.body) return
await processSSEStream(response.body.getReader(), assistantId)
} catch (err) {
if (err instanceof Error && err.name === 'AbortError') return
} finally {
if (streamGenRef.current === gen) {
finalize()
}
}
}
reconnect()
return () => {
abortController.abort()
appliedChatIdRef.current = undefined
}
}, [chatHistory?.activeStreamId, processSSEStream, finalize])
useLayoutEffect(() => {
finalizeRef.current = finalize
})
const sendMessage = useCallback(
async (message: string, fileAttachments?: FileAttachmentForApi[], contexts?: ChatContext[]) => {
@@ -937,7 +999,11 @@ export function useChat(
if (sendingRef.current) {
await persistPartialResponse()
}
const sid = streamIdRef.current
const sid =
streamIdRef.current ||
queryClient.getQueryData<TaskChatHistory>(taskKeys.detail(chatIdRef.current))
?.activeStreamId ||
undefined
streamGenRef.current++
abortControllerRef.current?.abort()
abortControllerRef.current = null
@@ -1054,6 +1120,7 @@ export function useChat(
return {
messages,
isSending,
isReconnecting,
error,
resolvedChatId,
sendMessage,

View File

@@ -14,11 +14,18 @@ import { useSubBlockStore } from '@/stores/workflows/subblock/store'
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
/**
* Dropdown option type - can be a simple string or an object with label, id, and optional icon
* Dropdown option type - can be a simple string or an object with label, id, and optional icon.
* Options with `hidden: true` are excluded from the picker but still resolve for label display,
* so existing workflows that reference them continue to work.
*/
type DropdownOption =
| string
| { label: string; id: string; icon?: React.ComponentType<{ className?: string }> }
| {
label: string
id: string
icon?: React.ComponentType<{ className?: string }>
hidden?: boolean
}
/**
* Props for the Dropdown component
@@ -185,13 +192,12 @@ export const Dropdown = memo(function Dropdown({
return fetchedOptions.map((opt) => ({ label: opt.label, id: opt.id }))
}, [fetchedOptions])
const availableOptions = useMemo(() => {
const allOptions = useMemo(() => {
let opts: DropdownOption[] =
fetchOptions && normalizedFetchedOptions.length > 0
? normalizedFetchedOptions
: evaluatedOptions
// Merge hydrated option if not already present
if (hydratedOption) {
const alreadyPresent = opts.some((o) =>
typeof o === 'string' ? o === hydratedOption.id : o.id === hydratedOption.id
@@ -204,11 +210,12 @@ export const Dropdown = memo(function Dropdown({
return opts
}, [fetchOptions, normalizedFetchedOptions, evaluatedOptions, hydratedOption])
/**
* Convert dropdown options to Combobox format
*/
const selectableOptions = useMemo(() => {
return allOptions.filter((opt) => typeof opt === 'string' || !opt.hidden)
}, [allOptions])
const comboboxOptions = useMemo((): ComboboxOption[] => {
return availableOptions.map((opt) => {
return selectableOptions.map((opt) => {
if (typeof opt === 'string') {
return { label: opt.toLowerCase(), value: opt }
}
@@ -218,11 +225,16 @@ export const Dropdown = memo(function Dropdown({
icon: 'icon' in opt ? opt.icon : undefined,
}
})
}, [availableOptions])
}, [selectableOptions])
const optionMap = useMemo(() => {
return new Map(comboboxOptions.map((opt) => [opt.value, opt.label]))
}, [comboboxOptions])
return new Map(
allOptions.map((opt) => {
if (typeof opt === 'string') return [opt, opt.toLowerCase()]
return [opt.id, opt.label.toLowerCase()]
})
)
}, [allOptions])
const defaultOptionValue = useMemo(() => {
if (multiSelect) return undefined

View File

@@ -521,62 +521,69 @@ export async function executeWorkflowWithFullLogging(
const data = line.substring(6).trim()
if (data === '[DONE]') continue
let event: any
try {
const event = JSON.parse(data)
event = JSON.parse(data)
} catch {
continue
}
switch (event.type) {
case 'execution:started': {
setCurrentExecutionId(wfId, event.executionId)
executionIdRef.current = event.executionId || executionId
break
}
case 'block:started':
blockHandlers.onBlockStarted(event.data)
break
case 'block:completed':
blockHandlers.onBlockCompleted(event.data)
break
case 'block:error':
blockHandlers.onBlockError(event.data)
break
case 'block:childWorkflowStarted':
blockHandlers.onBlockChildWorkflowStarted(event.data)
break
case 'execution:completed':
setCurrentExecutionId(wfId, null)
executionResult = {
success: event.data.success,
output: event.data.output,
logs: [],
metadata: {
duration: event.data.duration,
startTime: event.data.startTime,
endTime: event.data.endTime,
},
}
break
case 'execution:cancelled':
setCurrentExecutionId(wfId, null)
executionResult = {
success: false,
output: {},
error: 'Execution was cancelled',
logs: [],
}
break
case 'execution:error':
setCurrentExecutionId(wfId, null)
throw new Error(event.data.error || 'Execution failed')
switch (event.type) {
case 'execution:started': {
setCurrentExecutionId(wfId, event.executionId)
executionIdRef.current = event.executionId || executionId
break
}
} catch (parseError) {
// Skip malformed SSE events
case 'block:started':
blockHandlers.onBlockStarted(event.data)
break
case 'block:completed':
blockHandlers.onBlockCompleted(event.data)
break
case 'block:error':
blockHandlers.onBlockError(event.data)
break
case 'block:childWorkflowStarted':
blockHandlers.onBlockChildWorkflowStarted(event.data)
break
case 'execution:completed':
setCurrentExecutionId(wfId, null)
executionResult = {
success: event.data.success,
output: event.data.output,
logs: [],
metadata: {
duration: event.data.duration,
startTime: event.data.startTime,
endTime: event.data.endTime,
},
}
break
case 'execution:cancelled':
setCurrentExecutionId(wfId, null)
executionResult = {
success: false,
output: {},
error: 'Execution was cancelled',
logs: [],
}
break
case 'execution:error':
setCurrentExecutionId(wfId, null)
executionResult = {
success: false,
output: {},
error: event.data.error || 'Execution failed',
logs: [],
}
break
}
}
}

View File

@@ -207,6 +207,7 @@ const reactFlowStyles = [
'[&_.react-flow__node-subflowNode.selected]:!shadow-none',
].join(' ')
const reactFlowFitViewOptions = { padding: 0.6, maxZoom: 1.0 } as const
const embeddedFitViewOptions = { padding: 0.15, maxZoom: 0.85, minZoom: 0.35 } as const
const reactFlowProOptions = { hideAttribution: true } as const
/**
@@ -3851,11 +3852,11 @@ const WorkflowContent = React.memo(
onDragOver={effectivePermissions.canEdit ? onDragOver : undefined}
onInit={(instance) => {
requestAnimationFrame(() => {
instance.fitView(reactFlowFitViewOptions)
instance.fitView(embedded ? embeddedFitViewOptions : reactFlowFitViewOptions)
setIsCanvasReady(true)
})
}}
fitViewOptions={reactFlowFitViewOptions}
fitViewOptions={embedded ? embeddedFitViewOptions : reactFlowFitViewOptions}
minZoom={0.1}
maxZoom={1.3}
panOnScroll

View File

@@ -268,15 +268,17 @@ Return ONLY the search term - no explanations, no quotes, no extra text.`,
type: 'dropdown',
mode: 'trigger',
options: grainTriggerOptions,
value: () => 'grain_webhook',
value: () => 'grain_item_added',
required: true,
},
...getTrigger('grain_item_added').subBlocks,
...getTrigger('grain_item_updated').subBlocks,
...getTrigger('grain_webhook').subBlocks,
...getTrigger('grain_recording_created').subBlocks,
...getTrigger('grain_recording_updated').subBlocks,
...getTrigger('grain_highlight_created').subBlocks,
...getTrigger('grain_highlight_updated').subBlocks,
...getTrigger('grain_story_created').subBlocks,
...getTrigger('grain_webhook').subBlocks,
],
tools: {
access: [
@@ -447,12 +449,14 @@ Return ONLY the search term - no explanations, no quotes, no extra text.`,
triggers: {
enabled: true,
available: [
'grain_item_added',
'grain_item_updated',
'grain_webhook',
'grain_recording_created',
'grain_recording_updated',
'grain_highlight_created',
'grain_highlight_updated',
'grain_story_created',
'grain_webhook',
],
},
}

View File

@@ -233,12 +233,14 @@ export interface SubBlockConfig {
id: string
icon?: React.ComponentType<{ className?: string }>
group?: string
hidden?: boolean
}[]
| (() => {
label: string
id: string
icon?: React.ComponentType<{ className?: string }>
group?: string
hidden?: boolean
}[])
min?: number
max?: number

View File

@@ -9,12 +9,18 @@ export interface TaskMetadata {
isUnread: boolean
}
export interface StreamSnapshot {
events: Array<{ eventId: number; streamId: string; event: Record<string, unknown> }>
status: string
}
export interface TaskChatHistory {
id: string
title: string | null
messages: TaskStoredMessage[]
activeStreamId: string | null
resources: MothershipResource[]
streamSnapshot?: StreamSnapshot | null
}
export interface TaskStoredToolCall {
@@ -135,6 +141,7 @@ async function fetchChatHistory(chatId: string, signal?: AbortSignal): Promise<T
messages: Array.isArray(chat.messages) ? chat.messages : [],
activeStreamId: chat.conversationId || null,
resources: Array.isArray(chat.resources) ? chat.resources : [],
streamSnapshot: chat.streamSnapshot || null,
}
}

View File

@@ -21,12 +21,6 @@ import type {
import { executeToolAndReport, waitForToolCompletion, waitForToolDecision } from './tool-execution'
const logger = createLogger('CopilotSseHandlers')
const CLIENT_WORKFLOW_TOOLS = new Set([
'run_workflow',
'run_workflow_until_block',
'run_block',
'run_from_block',
])
/**
* Extract the `ui` object from a Go SSE event. The Go backend enriches
@@ -320,17 +314,6 @@ export const sseHandlers: Record<string, SSEHandler> = {
}
if (options.interactive === false) {
if (clientExecutable && CLIENT_WORKFLOW_TOOLS.has(toolName)) {
toolCall.status = 'executing'
const completion = await waitForToolCompletion(
toolCallId,
options.timeout || STREAM_TIMEOUT_MS,
options.abortSignal
)
handleClientCompletion(toolCall, toolCallId, completion)
await emitSyntheticToolResult(toolCallId, toolCall.name, completion, options)
return
}
if (options.autoExecuteTools !== false) {
fireToolExecution()
}
@@ -580,17 +563,6 @@ export const subAgentHandlers: Record<string, SSEHandler> = {
}
if (options.interactive === false) {
if (clientExecutable && CLIENT_WORKFLOW_TOOLS.has(toolName)) {
toolCall.status = 'executing'
const completion = await waitForToolCompletion(
toolCallId,
options.timeout || STREAM_TIMEOUT_MS,
options.abortSignal
)
handleClientCompletion(toolCall, toolCallId, completion)
await emitSyntheticToolResult(toolCallId, toolCall.name, completion, options)
return
}
if (options.autoExecuteTools !== false) {
fireToolExecution()
}

View File

@@ -1258,6 +1258,8 @@ export async function createGrainWebhookSubscription(
}
const actionMap: Record<string, Array<'added' | 'updated' | 'removed'>> = {
grain_item_added: ['added'],
grain_item_updated: ['updated'],
grain_recording_created: ['added'],
grain_recording_updated: ['updated'],
grain_highlight_created: ['added'],
@@ -1267,6 +1269,8 @@ export async function createGrainWebhookSubscription(
const eventTypeMap: Record<string, string[]> = {
grain_webhook: [],
grain_item_added: [],
grain_item_updated: [],
grain_recording_created: ['recording_added'],
grain_recording_updated: ['recording_updated'],
grain_highlight_created: ['highlight_added'],

View File

@@ -144,8 +144,8 @@
"posthog-js": "1.334.1",
"posthog-node": "5.9.2",
"prismjs": "^1.30.0",
"react": "19.2.1",
"react-dom": "19.2.1",
"react": "19.2.4",
"react-dom": "19.2.4",
"react-hook-form": "^7.54.2",
"react-markdown": "^10.1.0",
"react-simple-code-editor": "^0.14.1",

View File

@@ -1,5 +1,7 @@
export { grainHighlightCreatedTrigger } from './highlight_created'
export { grainHighlightUpdatedTrigger } from './highlight_updated'
export { grainItemAddedTrigger } from './item_added'
export { grainItemUpdatedTrigger } from './item_updated'
export { grainRecordingCreatedTrigger } from './recording_created'
export { grainRecordingUpdatedTrigger } from './recording_updated'
export { grainStoryCreatedTrigger } from './story_created'

View File

@@ -0,0 +1,76 @@
import { GrainIcon } from '@/components/icons'
import type { TriggerConfig } from '@/triggers/types'
import { buildGenericOutputs, grainV2SetupInstructions } from './utils'
export const grainItemAddedTrigger: TriggerConfig = {
id: 'grain_item_added',
name: 'Grain Item Added',
provider: 'grain',
description: 'Trigger when a new item is added to a Grain view (recording, highlight, or story)',
version: '1.0.0',
icon: GrainIcon,
subBlocks: [
{
id: 'apiKey',
title: 'API Key',
type: 'short-input',
placeholder: 'Enter your Grain API key (Personal Access Token)',
description: 'Required to create the webhook in Grain.',
password: true,
required: true,
mode: 'trigger',
condition: {
field: 'selectedTriggerId',
value: 'grain_item_added',
},
},
{
id: 'viewId',
title: 'View ID',
type: 'short-input',
placeholder: 'Enter Grain view UUID',
description:
'The view determines which content type fires events (recordings, highlights, or stories).',
required: true,
mode: 'trigger',
condition: {
field: 'selectedTriggerId',
value: 'grain_item_added',
},
},
{
id: 'triggerSave',
title: '',
type: 'trigger-save',
hideFromPreview: true,
mode: 'trigger',
triggerId: 'grain_item_added',
condition: {
field: 'selectedTriggerId',
value: 'grain_item_added',
},
},
{
id: 'triggerInstructions',
title: 'Setup Instructions',
hideFromPreview: true,
type: 'text',
defaultValue: grainV2SetupInstructions('item added'),
mode: 'trigger',
condition: {
field: 'selectedTriggerId',
value: 'grain_item_added',
},
},
],
outputs: buildGenericOutputs(),
webhook: {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
},
}

View File

@@ -0,0 +1,76 @@
import { GrainIcon } from '@/components/icons'
import type { TriggerConfig } from '@/triggers/types'
import { buildGenericOutputs, grainV2SetupInstructions } from './utils'
export const grainItemUpdatedTrigger: TriggerConfig = {
id: 'grain_item_updated',
name: 'Grain Item Updated',
provider: 'grain',
description: 'Trigger when an item is updated in a Grain view (recording, highlight, or story)',
version: '1.0.0',
icon: GrainIcon,
subBlocks: [
{
id: 'apiKey',
title: 'API Key',
type: 'short-input',
placeholder: 'Enter your Grain API key (Personal Access Token)',
description: 'Required to create the webhook in Grain.',
password: true,
required: true,
mode: 'trigger',
condition: {
field: 'selectedTriggerId',
value: 'grain_item_updated',
},
},
{
id: 'viewId',
title: 'View ID',
type: 'short-input',
placeholder: 'Enter Grain view UUID',
description:
'The view determines which content type fires events (recordings, highlights, or stories).',
required: true,
mode: 'trigger',
condition: {
field: 'selectedTriggerId',
value: 'grain_item_updated',
},
},
{
id: 'triggerSave',
title: '',
type: 'trigger-save',
hideFromPreview: true,
mode: 'trigger',
triggerId: 'grain_item_updated',
condition: {
field: 'selectedTriggerId',
value: 'grain_item_updated',
},
},
{
id: 'triggerInstructions',
title: 'Setup Instructions',
hideFromPreview: true,
type: 'text',
defaultValue: grainV2SetupInstructions('item updated'),
mode: 'trigger',
condition: {
field: 'selectedTriggerId',
value: 'grain_item_updated',
},
},
],
outputs: buildGenericOutputs(),
webhook: {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
},
}

View File

@@ -1,15 +1,19 @@
import type { TriggerOutput } from '@/triggers/types'
/**
* Shared trigger dropdown options for all Grain triggers
* Trigger dropdown options for Grain triggers.
* New options (Item Added / Item Updated / All Events) correctly scope by view_id only.
* Legacy options are hidden from the picker but still resolve for existing workflows.
*/
export const grainTriggerOptions = [
{ label: 'General Webhook (All Events)', id: 'grain_webhook' },
{ label: 'Recording Created', id: 'grain_recording_created' },
{ label: 'Recording Updated', id: 'grain_recording_updated' },
{ label: 'Highlight Created', id: 'grain_highlight_created' },
{ label: 'Highlight Updated', id: 'grain_highlight_updated' },
{ label: 'Story Created', id: 'grain_story_created' },
{ label: 'Item Added', id: 'grain_item_added' },
{ label: 'Item Updated', id: 'grain_item_updated' },
{ label: 'All Events', id: 'grain_webhook' },
{ label: 'Recording Created', id: 'grain_recording_created', hidden: true },
{ label: 'Recording Updated', id: 'grain_recording_updated', hidden: true },
{ label: 'Highlight Created', id: 'grain_highlight_created', hidden: true },
{ label: 'Highlight Updated', id: 'grain_highlight_updated', hidden: true },
{ label: 'Story Created', id: 'grain_story_created', hidden: true },
]
/**
@@ -32,6 +36,25 @@ export function grainSetupInstructions(eventType: string): string {
.join('')
}
/**
* Setup instructions for the v2 triggers that correctly explain view-based scoping.
*/
export function grainV2SetupInstructions(action: string): string {
const instructions = [
'Enter your Grain API Key (Personal Access Token). You can find or create one in Grain at <strong>Workspace Settings &gt; API</strong> under Integrations on <a href="https://grain.com/app/settings/integrations?tab=api" target="_blank" rel="noopener noreferrer">grain.com</a>.',
`Enter a Grain <strong>view ID</strong>. Each view has a type &mdash; <em>recordings</em>, <em>highlights</em>, or <em>stories</em> &mdash; and only items matching that type will fire the <strong>${action}</strong> event.`,
'To find your view IDs, use the <strong>List Views</strong> operation on this block or call <code>GET /_/public-api/views</code> directly.',
'The webhook is created automatically when you save and will be deleted when you remove this trigger.',
]
return instructions
.map(
(instruction, index) =>
`<div class="mb-3"><strong>${index + 1}.</strong> ${instruction}</div>`
)
.join('')
}
/**
* Build output schema for recording events
* Webhook payload structure: { type, user_id, data: { ...recording } }

View File

@@ -1,12 +1,12 @@
import { GrainIcon } from '@/components/icons'
import type { TriggerConfig } from '@/triggers/types'
import { buildGenericOutputs, grainSetupInstructions } from './utils'
import { buildGenericOutputs, grainV2SetupInstructions } from './utils'
export const grainWebhookTrigger: TriggerConfig = {
id: 'grain_webhook',
name: 'Grain Webhook',
name: 'Grain All Events',
provider: 'grain',
description: 'Generic webhook trigger for all actions in a selected Grain view',
description: 'Trigger on all actions (added, updated, removed) in a Grain view',
version: '1.0.0',
icon: GrainIcon,
@@ -30,7 +30,8 @@ export const grainWebhookTrigger: TriggerConfig = {
title: 'View ID',
type: 'short-input',
placeholder: 'Enter Grain view UUID',
description: 'Required by Grain to create the webhook subscription.',
description:
'The view determines which content type fires events (recordings, highlights, or stories).',
required: true,
mode: 'trigger',
condition: {
@@ -55,7 +56,7 @@ export const grainWebhookTrigger: TriggerConfig = {
title: 'Setup Instructions',
hideFromPreview: true,
type: 'text',
defaultValue: grainSetupInstructions('All events'),
defaultValue: grainV2SetupInstructions('all'),
mode: 'trigger',
condition: {
field: 'selectedTriggerId',

View File

@@ -89,6 +89,8 @@ import { googleFormsWebhookTrigger } from '@/triggers/googleforms'
import {
grainHighlightCreatedTrigger,
grainHighlightUpdatedTrigger,
grainItemAddedTrigger,
grainItemUpdatedTrigger,
grainRecordingCreatedTrigger,
grainRecordingUpdatedTrigger,
grainStoryCreatedTrigger,
@@ -245,6 +247,8 @@ export const TRIGGER_REGISTRY: TriggerRegistry = {
fathom_webhook: fathomWebhookTrigger,
gmail_poller: gmailPollingTrigger,
grain_webhook: grainWebhookTrigger,
grain_item_added: grainItemAddedTrigger,
grain_item_updated: grainItemUpdatedTrigger,
grain_recording_created: grainRecordingCreatedTrigger,
grain_recording_updated: grainRecordingUpdatedTrigger,
grain_highlight_created: grainHighlightCreatedTrigger,

View File

@@ -1,5 +1,6 @@
{
"lockfileVersion": 1,
"configVersion": 0,
"workspaces": {
"": {
"name": "simstudio",
@@ -30,8 +31,8 @@
"next": "16.1.6",
"next-themes": "^0.4.6",
"postgres": "^3.4.5",
"react": "19.2.1",
"react-dom": "19.2.1",
"react": "19.2.4",
"react-dom": "19.2.4",
"shiki": "4.0.0",
"tailwind-merge": "^3.0.2",
},
@@ -169,8 +170,8 @@
"posthog-js": "1.334.1",
"posthog-node": "5.9.2",
"prismjs": "^1.30.0",
"react": "19.2.1",
"react-dom": "19.2.1",
"react": "19.2.4",
"react-dom": "19.2.4",
"react-hook-form": "^7.54.2",
"react-markdown": "^10.1.0",
"react-simple-code-editor": "^0.14.1",
@@ -325,8 +326,8 @@
"drizzle-orm": "^0.44.5",
"next": "16.1.6",
"postgres": "^3.4.5",
"react": "19.2.1",
"react-dom": "19.2.1",
"react": "19.2.4",
"react-dom": "19.2.4",
},
"packages": {
"@1password/sdk": ["@1password/sdk@0.3.1", "", { "dependencies": { "@1password/sdk-core": "0.3.1" } }, "sha512-20zbQfqsjcECT0gvnAw4zONJDt3XQgNH946pZR0NV1Qxukyaz/DKB0cBnBNCCEWZg93Bah8poaR6gJCyuNX14w=="],
@@ -3107,9 +3108,9 @@
"rc9": ["rc9@2.1.2", "", { "dependencies": { "defu": "^6.1.4", "destr": "^2.0.3" } }, "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg=="],
"react": ["react@19.2.1", "", {}, "sha512-DGrYcCWK7tvYMnWh79yrPHt+vdx9tY+1gPZa7nJQtO/p8bLTDaHp4dzwEhQB7pZ4Xe3ok4XKuEPrVuc+wlpkmw=="],
"react": ["react@19.2.4", "", {}, "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ=="],
"react-dom": ["react-dom@19.2.1", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.1" } }, "sha512-ibrK8llX2a4eOskq1mXKu/TGZj9qzomO+sNfO98M6d9zIPOEhlBkMkBUBLd1vgS0gQsLDBzA+8jJBVXDnfHmJg=="],
"react-dom": ["react-dom@19.2.4", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.4" } }, "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ=="],
"react-email": ["react-email@4.3.2", "", { "dependencies": { "@babel/parser": "^7.27.0", "@babel/traverse": "^7.27.0", "chokidar": "^4.0.3", "commander": "^13.0.0", "debounce": "^2.0.0", "esbuild": "^0.25.0", "glob": "^11.0.0", "jiti": "2.4.2", "log-symbols": "^7.0.0", "mime-types": "^3.0.0", "normalize-path": "^3.0.0", "nypm": "0.6.0", "ora": "^8.0.0", "prompts": "2.4.2", "socket.io": "^4.8.1", "tsconfig-paths": "4.2.0" }, "bin": { "email": "dist/index.js" } }, "sha512-WaZcnv9OAIRULY236zDRdk+8r511ooJGH5UOb7FnVsV33hGPI+l5aIZ6drVjXi4QrlLTmLm8PsYvmXRSv31MPA=="],

View File

@@ -26,8 +26,8 @@
"release": "bun run scripts/create-single-release.ts"
},
"overrides": {
"react": "19.2.1",
"react-dom": "19.2.1",
"react": "19.2.4",
"react-dom": "19.2.4",
"next": "16.1.6",
"@next/env": "16.1.6",
"drizzle-orm": "^0.44.5",