From 48e1e7250bda6d9c0147d45b5a91a93f9f847b8a Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Tue, 10 Jun 2025 17:02:34 +0100 Subject: [PATCH 01/15] v4: fix stuck batch issue when processing happens before transaction commits (#2163) * v4: fix stuck batch issue when processing happens before transaction commits * Fix flaky test --- .../runEngine/services/batchTrigger.server.ts | 21 +++++++++++-------- .../webapp/test/fairDequeuingStrategy.test.ts | 4 ++-- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/apps/webapp/app/runEngine/services/batchTrigger.server.ts b/apps/webapp/app/runEngine/services/batchTrigger.server.ts index 742f3dd1ba..f51c82cdc7 100644 --- a/apps/webapp/app/runEngine/services/batchTrigger.server.ts +++ b/apps/webapp/app/runEngine/services/batchTrigger.server.ts @@ -273,15 +273,18 @@ export class RunEngineBatchTriggerService extends WithRunEngine { switch (this._batchProcessingStrategy) { case "sequential": { - await this.#enqueueBatchTaskRun({ - batchId: batch.id, - processingId: batchId, - range: { start: 0, count: PROCESSING_BATCH_SIZE }, - attemptCount: 0, - strategy: this._batchProcessingStrategy, - parentRunId: body.parentRunId, - resumeParentOnCompletion: body.resumeParentOnCompletion, - }); + await this.#enqueueBatchTaskRun( + { + batchId: batch.id, + processingId: batchId, + range: { start: 0, count: PROCESSING_BATCH_SIZE }, + attemptCount: 0, + strategy: this._batchProcessingStrategy, + parentRunId: body.parentRunId, + resumeParentOnCompletion: body.resumeParentOnCompletion, + }, + tx + ); break; } diff --git a/apps/webapp/test/fairDequeuingStrategy.test.ts b/apps/webapp/test/fairDequeuingStrategy.test.ts index 2202576c9d..486b6ca3c4 100644 --- a/apps/webapp/test/fairDequeuingStrategy.test.ts +++ b/apps/webapp/test/fairDequeuingStrategy.test.ts @@ -263,8 +263,8 @@ describe("FairDequeuingStrategy", () => { console.log("Second distribution took", distribute2Duration, "ms"); - // Make sure the second call is more than 9 times faster than the first - expect(distribute2Duration).toBeLessThan(withTolerance(distribute1Duration / 9)); + // Make sure the second call is more than 2 times faster than the first + expect(distribute2Duration).toBeLessThan(withTolerance(distribute1Duration / 2)); const startDistribute3 = performance.now(); From 498b9a21afb7f81d7294b61c85f7a4c9f44546cf Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Wed, 11 Jun 2025 09:24:11 +0100 Subject: [PATCH 02/15] Improved TaskRun environment indexes (#2164) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### PR: Optimize **TaskRun** indexes for hot-path queries **What changed** | Object | Type | Purpose | | ------------------------------- | --------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- | | `taskrun_runtime_id_desc_idx` | **BTREE** `(runtimeEnvironmentId, id DESC) INCLUDE (createdAt)` | Eliminates explicit sort for the “latest task runs” query (`ORDER BY id DESC`) while remaining index-only. | | `taskrun_runtime_createdat_idx` | **BTREE** `(runtimeEnvironmentId, createdAt DESC) INCLUDE (id)` | Accelerates the filter-only path that scans by `createdAt >= …` without any ordering requirement. | | `taskrun_createdat_brin` | **BRIN** on `createdAt` (`pages_per_range = 128`) | Lets the planner skip whole blocks older than the time window for both queries at < 100 MB cost. | | *(cleanup)* | **DROP** `TaskRun_runtimeEnvironmentId_createdAt_id_idx` | Retires the 3-column index once the new ones are built. | **Key details** * All indexes created **CONCURRENTLY** to avoid write blocking. * `fillfactor = 90` on b-trees for balanced space vs. future growth. * Net disk usage drops **≈ 15–20 GB** while each query now gets a purpose-built access path. **Why** * Remove planner Sort nodes for the top-N “latest runs” view. * Speed up environment-filtered range scans. * Shrink index bloat and improve cache efficiency. --- .../migration.sql | 1 + .../migration.sql | 1 + .../migration.sql | 1 + .../migration.sql | 1 + internal-packages/database/prisma/schema.prisma | 7 +++++-- 5 files changed, 9 insertions(+), 2 deletions(-) create mode 100644 internal-packages/database/prisma/migrations/20250611075637_remove_task_run_environment_created_at_id_index/migration.sql create mode 100644 internal-packages/database/prisma/migrations/20250611080026_add_task_run_environment_id_index_including_created_at/migration.sql create mode 100644 internal-packages/database/prisma/migrations/20250611080322_add_task_run_environment_created_at_index_including_id/migration.sql create mode 100644 internal-packages/database/prisma/migrations/20250611080537_add_task_run_created_at_brin_index/migration.sql diff --git a/internal-packages/database/prisma/migrations/20250611075637_remove_task_run_environment_created_at_id_index/migration.sql b/internal-packages/database/prisma/migrations/20250611075637_remove_task_run_environment_created_at_id_index/migration.sql new file mode 100644 index 0000000000..fef79ff3b8 --- /dev/null +++ b/internal-packages/database/prisma/migrations/20250611075637_remove_task_run_environment_created_at_id_index/migration.sql @@ -0,0 +1 @@ +DROP INDEX CONCURRENTLY IF EXISTS "TaskRun_runtimeEnvironmentId_createdAt_id_idx"; diff --git a/internal-packages/database/prisma/migrations/20250611080026_add_task_run_environment_id_index_including_created_at/migration.sql b/internal-packages/database/prisma/migrations/20250611080026_add_task_run_environment_id_index_including_created_at/migration.sql new file mode 100644 index 0000000000..9ca6e8c596 --- /dev/null +++ b/internal-packages/database/prisma/migrations/20250611080026_add_task_run_environment_id_index_including_created_at/migration.sql @@ -0,0 +1 @@ +CREATE INDEX CONCURRENTLY IF NOT EXISTS "TaskRun_runtimeEnvironmentId_id_idx" ON "TaskRun"("runtimeEnvironmentId", "id" DESC) INCLUDE ("createdAt") WITH (fillfactor = 90); diff --git a/internal-packages/database/prisma/migrations/20250611080322_add_task_run_environment_created_at_index_including_id/migration.sql b/internal-packages/database/prisma/migrations/20250611080322_add_task_run_environment_created_at_index_including_id/migration.sql new file mode 100644 index 0000000000..dfce46195b --- /dev/null +++ b/internal-packages/database/prisma/migrations/20250611080322_add_task_run_environment_created_at_index_including_id/migration.sql @@ -0,0 +1 @@ +CREATE INDEX CONCURRENTLY IF NOT EXISTS "TaskRun_runtimeEnvironmentId_createdAt_idx" ON "TaskRun"("runtimeEnvironmentId", "createdAt" DESC) INCLUDE ("id") WITH (fillfactor = 90); diff --git a/internal-packages/database/prisma/migrations/20250611080537_add_task_run_created_at_brin_index/migration.sql b/internal-packages/database/prisma/migrations/20250611080537_add_task_run_created_at_brin_index/migration.sql new file mode 100644 index 0000000000..fc74ac3fdb --- /dev/null +++ b/internal-packages/database/prisma/migrations/20250611080537_add_task_run_created_at_brin_index/migration.sql @@ -0,0 +1 @@ +CREATE INDEX CONCURRENTLY IF NOT EXISTS "TaskRun_createdAt_idx" ON "TaskRun" USING BRIN ("createdAt"); diff --git a/internal-packages/database/prisma/schema.prisma b/internal-packages/database/prisma/schema.prisma index 9065126290..06ca39d692 100644 --- a/internal-packages/database/prisma/schema.prisma +++ b/internal-packages/database/prisma/schema.prisma @@ -680,9 +680,12 @@ model TaskRun { // Schedule list page @@index([scheduleId, createdAt(sort: Desc)]) // Finding runs in a batch - @@index([runtimeEnvironmentId, batchId]) - @@index([runtimeEnvironmentId, createdAt(sort: Desc), id(sort: Desc)]) @@index([runTags(ops: ArrayOps)], type: Gin) + @@index([runtimeEnvironmentId, batchId]) + // This will include the createdAt index to help speed up the run list page + @@index([runtimeEnvironmentId, id(sort: Desc)]) + @@index([runtimeEnvironmentId, createdAt(sort: Desc)]) + @@index([createdAt], type: Brin) } enum TaskRunStatus { From 808c2990081088c42f881822abc76a2a504d6c19 Mon Sep 17 00:00:00 2001 From: Dan <8297864+D-K-P@users.noreply.github.com> Date: Wed, 11 Jun 2025 16:19:25 +0100 Subject: [PATCH 03/15] Added deep research example (#2166) * Added docs * Improved docs and added video * Added upgrade badge --- docs/docs.json | 61 ++------ .../vercel-ai-sdk-deep-research.mdx | 143 ++++++++++++++++++ docs/guides/introduction.mdx | 23 +-- 3 files changed, 168 insertions(+), 59 deletions(-) create mode 100644 docs/guides/example-projects/vercel-ai-sdk-deep-research.mdx diff --git a/docs/docs.json b/docs/docs.json index da3a71be78..88b0d12a1d 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -10,11 +10,7 @@ }, "favicon": "/images/favicon.png", "contextual": { - "options": [ - "copy", - "view", - "claude" - ] + "options": ["copy", "view", "claude"] }, "navigation": { "dropdowns": [ @@ -39,11 +35,7 @@ "pages": [ { "group": "Tasks", - "pages": [ - "tasks/overview", - "tasks/schemaTask", - "tasks/scheduled" - ] + "pages": ["tasks/overview", "tasks/schemaTask", "tasks/scheduled"] }, "triggering", "runs", @@ -58,12 +50,7 @@ "errors-retrying", { "group": "Wait", - "pages": [ - "wait", - "wait-for", - "wait-until", - "wait-for-token" - ] + "pages": ["wait", "wait-for", "wait-until", "wait-for-token"] }, "queue-concurrency", "versioning", @@ -108,9 +95,7 @@ }, { "group": "Development", - "pages": [ - "cli-dev" - ] + "pages": ["cli-dev"] }, { "group": "Deployment", @@ -122,9 +107,7 @@ "deployment/atomic-deployment", { "group": "Deployment integrations", - "pages": [ - "vercel-integration" - ] + "pages": ["vercel-integration"] } ] }, @@ -177,12 +160,7 @@ }, { "group": "Using the Dashboard", - "pages": [ - "run-tests", - "troubleshooting-alerts", - "replaying", - "bulk-actions" - ] + "pages": ["run-tests", "troubleshooting-alerts", "replaying", "bulk-actions"] }, { "group": "Troubleshooting", @@ -218,11 +196,7 @@ }, { "group": "Help", - "pages": [ - "community", - "help-slack", - "help-email" - ] + "pages": ["community", "help-slack", "help-email"] } ] }, @@ -243,10 +217,7 @@ }, { "group": "Tasks API", - "pages": [ - "management/tasks/trigger", - "management/tasks/batch-trigger" - ] + "pages": ["management/tasks/trigger", "management/tasks/batch-trigger"] }, { "group": "Runs API", @@ -292,9 +263,7 @@ "groups": [ { "group": "Introduction", - "pages": [ - "guides/introduction" - ] + "pages": ["guides/introduction"] }, { "group": "Frameworks", @@ -376,6 +345,7 @@ "guides/example-projects/realtime-csv-importer", "guides/example-projects/realtime-fal-ai", "guides/example-projects/turborepo-monorepo-prisma", + "guides/example-projects/vercel-ai-sdk-deep-research", "guides/example-projects/vercel-ai-sdk-image-generator" ] }, @@ -407,9 +377,7 @@ }, { "group": "Migration guides", - "pages": [ - "migration-mergent" - ] + "pages": ["migration-mergent"] }, { "group": "Community packages", @@ -430,10 +398,7 @@ "href": "https://trigger.dev" }, "api": { - "openapi": [ - "openapi.yml", - "v3-openapi.yaml" - ], + "openapi": ["openapi.yml", "v3-openapi.yaml"], "playground": { "display": "simple" } @@ -608,4 +573,4 @@ "destination": "/management/overview" } ] -} \ No newline at end of file +} diff --git a/docs/guides/example-projects/vercel-ai-sdk-deep-research.mdx b/docs/guides/example-projects/vercel-ai-sdk-deep-research.mdx new file mode 100644 index 0000000000..aa0d54687c --- /dev/null +++ b/docs/guides/example-projects/vercel-ai-sdk-deep-research.mdx @@ -0,0 +1,143 @@ +--- +title: "Deep research agent using Vercel's AI SDK" +sidebarTitle: "Deep research agent" +description: "Deep research agent which generates comprehensive PDF reports using Vercel's AI SDK." +tag: "v4" +--- + +import RealtimeLearnMore from "/snippets/realtime-learn-more.mdx"; + +import UpgradeToV4Note from "/snippets/upgrade-to-v4-note.mdx"; + + + + + Acknowledgements: This example project is derived from the brilliant [deep research + guide](https://aie-feb-25.vercel.app/docs/deep-research) by [Nico + Albanese](https://x.com/nicoalbanese10). + + +## Overview + +This full-stack project is an intelligent deep research agent that autonomously conducts multi-layered web research, generating comprehensive reports which are then converted to PDF and uploaded to storage. + + + +**Tech stack:** + +- **[Next.js](https://nextjs.org/)** for the web app +- **[Vercel's AI SDK](https://sdk.vercel.ai/)** for AI model integration and structured generation +- **[Trigger.dev](https://trigger.dev)** for task orchestration, execution and real-time progress updates +- **[OpenAI's GPT-4o model](https://openai.com/gpt-4)** for intelligent query generation, content analysis, and report creation +- **[Exa API](https://exa.ai/)** for semantic web search with live crawling +- **[LibreOffice](https://www.libreoffice.org/)** for PDF generation +- **[Cloudflare R2](https://developers.cloudflare.com/r2/)** to store the generated reports + +**Features:** + +- **Recursive research**: AI generates search queries, evaluates their relevance, asks follow-up questions and searches deeper based on initial findings. +- **Real-time progress**: Live updates are shown on the frontend using Trigger.dev Realtime as research progresses. +- **Intelligent source evaluation**: AI evaluates search result relevance before processing. +- **Research report generation**: The completed research is converted to a structured HTML report using a detailed system prompt. +- **PDF creation and uploading to Cloud storage**: The completed reports are then converted to PDF using LibreOffice and uploaded to Cloudflare R2. + +## GitHub repo + + + Click here to view the full code for this project in our examples repository on GitHub. You can + fork it and use it as a starting point for your own project. + + +## How the deep research agent works + +### Trigger.dev orchestration + +The research process is orchestrated through three connected Trigger.dev tasks: + +1. `deepResearchOrchestrator` - Main task that coordinates the entire research workflow. +2. `generateReport` - Processes research data into a structured HTML report using OpenAI's GPT-4o model +3. `generatePdfAndUpload` - Converts HTML to PDF using LibreOffice and uploads to R2 cloud storage + +Each task uses `triggerAndWait()` to create a dependency chain, ensuring proper sequencing while maintaining isolation and error handling. + +### The deep research recursive function + +The core research logic uses a recursive depth-first search approach. A query is recursively expanded and the results are collected. + +**Key parameters:** + +- `depth`: Controls recursion levels (default: 2) +- `breadth`: Number of queries per level (default: 2, halved each recursion) + +``` +Level 0 (Initial Query): "AI safety in autonomous vehicles" +│ +├── Level 1 (depth = 1, breadth = 2): +│ ├── Sub-query 1: "Machine learning safety protocols in self-driving cars" +│ │ ├── → Search Web → Evaluate Relevance → Extract Learnings +│ │ └── → Follow-up: "How do neural networks handle edge cases?" +│ │ +│ └── Sub-query 2: "Regulatory frameworks for autonomous vehicle testing" +│ ├── → Search Web → Evaluate Relevance → Extract Learnings +│ └── → Follow-up: "What are current safety certification requirements?" +│ +└── Level 2 (depth = 2, breadth = 1): + ├── From Sub-query 1 follow-up: + │ └── "Neural network edge case handling in autonomous systems" + │ └── → Search Web → Evaluate → Extract → DEPTH LIMIT REACHED + │ + └── From Sub-query 2 follow-up: + └── "Safety certification requirements for self-driving vehicles" + └── → Search Web → Evaluate → Extract → DEPTH LIMIT REACHED +``` + +**Process flow:** + +1. **Query generation**: OpenAI's GPT-4o generates multiple search queries from the input +2. **Web search**: Each query searches the web via the Exa API with live crawling +3. **Relevance evaluation**: OpenAI's GPT-4o evaluates if results help answer the query +4. **Learning extraction**: Relevant results are analyzed for key insights and follow-up questions +5. **Recursive deepening**: Follow-up questions become new queries for the next depth level +6. **Accumulation**: All learnings, sources, and queries are accumulated across recursion levels + +### Using Trigger.dev Realtime to trigger and subscribe to the deep research task + +We use the [`useRealtimeTaskTrigger`](/frontend/react-hooks/triggering#userealtimetasktrigger) React hook to trigger the `deep-research` task and subscribe to it's updates. + +**Frontend (React Hook)**: + +```typescript +const triggerInstance = useRealtimeTaskTrigger("deep-research", { + accessToken: triggerToken, +}); +const { progress, label } = parseStatus(triggerInstance.run?.metadata); +``` + +As the research progresses, the metadata is set within the tasks and the frontend is kept updated with every new status: + +**Task Metadata**: + +```typescript +metadata.set("status", { + progress: 25, + label: `Searching the web for: "${query}"`, +}); +``` + +## Relevant code + +- **Deep research task**: Core logic in [src/trigger/deepResearch.ts](https://github.com/triggerdotdev/examples/blob/main/vercel-ai-sdk-deep-research-agent/src/trigger/deepResearch.ts) - orchestrates the recursive research process. Here you can change the model, the depth and the breadth of the research. +- **Report generation**: [src/trigger/generateReport.ts](https://github.com/triggerdotdev/examples/blob/main/vercel-ai-sdk-deep-research-agent/src/trigger/generateReport.ts) - creates structured HTML reports from research data. The system prompt is defined in the code - this can be updated to be more or less detailed. +- **PDF generation**: [src/trigger/generatePdfAndUpload.ts](https://github.com/triggerdotdev/examples/blob/main/vercel-ai-sdk-deep-research-agent/src/trigger/generatePdfAndUpload.ts) - converts reports to PDF and uploads to R2. This is a simple example of how to use LibreOffice to convert HTML to PDF. +- **Research agent UI**: [src/components/DeepResearchAgent.tsx](https://github.com/triggerdotdev/examples/blob/main/vercel-ai-sdk-deep-research-agent/src/components/DeepResearchAgent.tsx) - handles form submission and real-time progress display using the `useRealtimeTaskTrigger` hook. +- **Progress component**: [src/components/progress-section.tsx](https://github.com/triggerdotdev/examples/blob/main/deep-research-agent/src/components/progress-section.tsx) - displays live research progress. + + diff --git a/docs/guides/introduction.mdx b/docs/guides/introduction.mdx index 451cca43d2..3fba744586 100644 --- a/docs/guides/introduction.mdx +++ b/docs/guides/introduction.mdx @@ -43,17 +43,18 @@ Get set up fast using our detailed walk-through guides. Example projects are full projects with example repos you can fork and use. These are a great way of learning how to encorporate Trigger.dev into your project. -| Example project | Description | Framework | GitHub | -| :-------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------- | :-------- | :------------------------------------------------------------------------------------------------------------ | -| [Batch LLM Evaluator](/guides/example-projects/batch-llm-evaluator) | Evaluate multiple LLM models and stream the results to the frontend. | Next.js | [View the repo](https://github.com/triggerdotdev/examples/tree/main/batch-llm-evaluator) | -| [Claude thinking chatbot](/guides/example-projects/claude-thinking-chatbot) | Use Vercel's AI SDK and Anthropic's Claude 3.7 model to create a thinking chatbot. | Next.js | [View the repo](https://github.com/triggerdotdev/examples/tree/main/claude-thinking-chatbot) | -| [Human-in-the-loop workflow](/guides/example-projects/human-in-the-loop-workflow) | Create audio summaries of newspaper articles using a human-in-the-loop workflow built with ReactFlow and Trigger.dev waitpoint tokens. | Next.js | [View the repo](https://github.com/triggerdotdev/examples/tree/main/article-summary-workflow) | -| [OpenAI Agent SDK guardrails](/guides/example-projects/openai-agent-sdk-guardrails) | Use the OpenAI Agent SDK to create a guardrails system for your AI agents. | — | [View the repo](https://github.com/triggerdotdev/examples/tree/main/openai-agent-sdk-guardrails-examples) | -| [Python web crawler](/guides/python/python-crawl4ai) | Use Python, Crawl4AI and Playwright to create a headless web crawler with Trigger.dev. | — | [View the repo](https://github.com/triggerdotdev/examples/tree/main/python-crawl4ai) | -| [Realtime CSV Importer](/guides/example-projects/realtime-csv-importer) | Upload a CSV file and see the progress of the task streamed to the frontend. | Next.js | [View the repo](https://github.com/triggerdotdev/examples/tree/main/realtime-csv-importer) | -| [Realtime Fal.ai image generation](/guides/example-projects/realtime-fal-ai) | Generate an image from a prompt using Fal.ai and show the progress of the task on the frontend using Realtime. | Next.js | [View the repo](https://github.com/triggerdotdev/examples/tree/main/realtime-fal-ai-image-generation) | -| [Turborepo monorepo with Prisma](/guides/example-projects/turborepo-monorepo-prisma) | Use Prisma in a Turborepo monorepo with Trigger.dev. | Next.js | [View the repo](https://github.com/triggerdotdev/examples/tree/main/monorepos/turborepo-prisma-tasks-package) | -| [Vercel AI SDK image generator](/guides/example-projects/vercel-ai-sdk-image-generator) | Use the Vercel AI SDK to generate images from a prompt. | Next.js | [View the repo](https://github.com/triggerdotdev/examples/tree/main/vercel-ai-sdk-image-generator) | +| Example project | Description | Framework | GitHub | +| :---------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------- | :-------- | :------------------------------------------------------------------------------------------------------------ | +| [Batch LLM Evaluator](/guides/example-projects/batch-llm-evaluator) | Evaluate multiple LLM models and stream the results to the frontend. | Next.js | [View the repo](https://github.com/triggerdotdev/examples/tree/main/batch-llm-evaluator) | +| [Claude thinking chatbot](/guides/example-projects/claude-thinking-chatbot) | Use Vercel's AI SDK and Anthropic's Claude 3.7 model to create a thinking chatbot. | Next.js | [View the repo](https://github.com/triggerdotdev/examples/tree/main/claude-thinking-chatbot) | +| [Human-in-the-loop workflow](/guides/example-projects/human-in-the-loop-workflow) | Create audio summaries of newspaper articles using a human-in-the-loop workflow built with ReactFlow and Trigger.dev waitpoint tokens. | Next.js | [View the repo](https://github.com/triggerdotdev/examples/tree/main/article-summary-workflow) | +| [OpenAI Agent SDK guardrails](/guides/example-projects/openai-agent-sdk-guardrails) | Use the OpenAI Agent SDK to create a guardrails system for your AI agents. | — | [View the repo](https://github.com/triggerdotdev/examples/tree/main/openai-agent-sdk-guardrails-examples) | +| [Python web crawler](/guides/python/python-crawl4ai) | Use Python, Crawl4AI and Playwright to create a headless web crawler with Trigger.dev. | — | [View the repo](https://github.com/triggerdotdev/examples/tree/main/python-crawl4ai) | +| [Realtime CSV Importer](/guides/example-projects/realtime-csv-importer) | Upload a CSV file and see the progress of the task streamed to the frontend. | Next.js | [View the repo](https://github.com/triggerdotdev/examples/tree/main/realtime-csv-importer) | +| [Realtime Fal.ai image generation](/guides/example-projects/realtime-fal-ai) | Generate an image from a prompt using Fal.ai and show the progress of the task on the frontend using Realtime. | Next.js | [View the repo](https://github.com/triggerdotdev/examples/tree/main/realtime-fal-ai-image-generation) | +| [Turborepo monorepo with Prisma](/guides/example-projects/turborepo-monorepo-prisma) | Use Prisma in a Turborepo monorepo with Trigger.dev. | Next.js | [View the repo](https://github.com/triggerdotdev/examples/tree/main/monorepos/turborepo-prisma-tasks-package) | +| [Vercel AI SDK image generator](/guides/example-projects/vercel-ai-sdk-image-generator) | Use the Vercel AI SDK to generate images from a prompt. | Next.js | [View the repo](https://github.com/triggerdotdev/examples/tree/main/vercel-ai-sdk-image-generator) | +| [Vercel AI SDK deep research agent](/guides/example-projects/vercel-ai-sdk-deep-research) | Use the Vercel AI SDK to generate comprehensive PDF reports using a deep research agent. | Next.js | [View the repo](https://github.com/triggerdotdev/examples/tree/main/vercel-ai-sdk-deep-research-agent) | ## Example tasks From e12c82b444d132b7f5fc06a98da56e3c49f22c6b Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Thu, 12 Jun 2025 14:09:03 +0100 Subject: [PATCH 04/15] Use the Kapa AI SDK instead of the Kapa AI widget (#2113) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Install the kapa sdk * WIP using the SDK for the Kapa Ask AI widget * Removes old kapa from root * Now rendering everything inside the dialog component * Fixes min-height of dialog content * Remove kapa from root * prevents kapa using reCaptcha * Adds more functionailty with temporary UI placement for now * Reset conversation button * Adds a new sparkle list icon * Adds some example questions as a blank state * Animate in the example questions * use “marked” package to render markdown * Improve some animations * Submit a question from the URL param * adds custom scroll bar styling * fixes modal to correct height after re-opening it * Add button to stop generating answer mid-stream * Adds buttons states to show submitting, generating, submittable * Adds a helpfull sentence in the blank state * Show a message if the chat returns an error * Adds reset chat and feedback buttons to the bottom of an answer * Makes sure you can give feedback in the different states of chat * Adds a suble background to the dialog * Fix a button inside button error * Improve the shortcut esc key on dialog and sheet component * Fix classname error * organize imports * Use our custom focus-visible * Move the Tooltip for the button into the AskAI component * Improved error message * Organize imports * Animated the modal gradient * Small layout improvements * Adds most asked questions from Kapa * border glow tweak * AskAI component is now a hook that can take a question * remove kapa script * Add a delay before the modal opens when usign the URL params * Remove old component * Update to the latest Kapa version * Rephrased error message * Use correct types for conversation * Fixed types for addFeedback * Adds DOMPurify package * removed unused const * Removed unnecessary platform specification * Reset the timeout when the ai panel pops up Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * Fix for coderabbit bad commit * Clean up imports --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- .../app/assets/icons/SparkleListIcon.tsx | 14 + apps/webapp/app/components/AskAI.tsx | 542 ++++++++++++++++++ .../app/components/navigation/SideMenu.tsx | 18 +- .../app/components/primitives/Dialog.tsx | 25 +- .../app/components/primitives/ShortcutKey.tsx | 4 +- apps/webapp/app/hooks/useKapaWidget.tsx | 145 ----- apps/webapp/app/root.tsx | 24 +- apps/webapp/app/tailwind.css | 42 ++ apps/webapp/package.json | 3 + pnpm-lock.yaml | 105 +++- 10 files changed, 733 insertions(+), 189 deletions(-) create mode 100644 apps/webapp/app/assets/icons/SparkleListIcon.tsx create mode 100644 apps/webapp/app/components/AskAI.tsx delete mode 100644 apps/webapp/app/hooks/useKapaWidget.tsx diff --git a/apps/webapp/app/assets/icons/SparkleListIcon.tsx b/apps/webapp/app/assets/icons/SparkleListIcon.tsx new file mode 100644 index 0000000000..264fc227c8 --- /dev/null +++ b/apps/webapp/app/assets/icons/SparkleListIcon.tsx @@ -0,0 +1,14 @@ +export function SparkleListIcon({ className }: { className?: string }) { + return ( + + + + + + ); +} diff --git a/apps/webapp/app/components/AskAI.tsx b/apps/webapp/app/components/AskAI.tsx new file mode 100644 index 0000000000..23b575b771 --- /dev/null +++ b/apps/webapp/app/components/AskAI.tsx @@ -0,0 +1,542 @@ +import { + ArrowPathIcon, + ArrowUpIcon, + HandThumbDownIcon, + HandThumbUpIcon, + StopIcon, +} from "@heroicons/react/20/solid"; +import { type FeedbackComment, KapaProvider, type QA, useChat } from "@kapaai/react-sdk"; +import { useSearchParams } from "@remix-run/react"; +import { motion } from "framer-motion"; +import { marked } from "marked"; +import { + createContext, + type ReactNode, + useCallback, + useContext, + useEffect, + useRef, + useState, +} from "react"; +import { AISparkleIcon } from "~/assets/icons/AISparkleIcon"; +import { SparkleListIcon } from "~/assets/icons/SparkleListIcon"; +import { Button } from "./primitives/Buttons"; +import { Callout } from "./primitives/Callout"; +import { Dialog, DialogContent, DialogHeader, DialogTitle } from "./primitives/Dialog"; +import { Header2 } from "./primitives/Headers"; +import { Paragraph } from "./primitives/Paragraph"; +import { Spinner } from "./primitives/Spinner"; +import { + SimpleTooltip, + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "./primitives/Tooltip"; +import DOMPurify from "dompurify"; + +type AskAIContextType = { + isOpen: boolean; + openAskAI: (question?: string) => void; + closeAskAI: () => void; + websiteId: string | null; +}; + +const AskAIContext = createContext(null); + +export function useAskAI() { + const context = useContext(AskAIContext); + if (!context) { + throw new Error("useAskAI must be used within an AskAIProvider"); + } + return context; +} + +type AskAIProviderProps = { + children: ReactNode; + websiteId: string | null; +}; + +export function AskAIProvider({ children, websiteId }: AskAIProviderProps) { + const [isOpen, setIsOpen] = useState(false); + const [initialQuery, setInitialQuery] = useState(); + const [searchParams, setSearchParams] = useSearchParams(); + + const openAskAI = useCallback((question?: string) => { + if (question) { + setInitialQuery(question); + } else { + setInitialQuery(undefined); + } + setIsOpen(true); + }, []); + + const closeAskAI = useCallback(() => { + setIsOpen(false); + setInitialQuery(undefined); + }, []); + + // Handle URL param functionality + useEffect(() => { + const aiHelp = searchParams.get("aiHelp"); + if (aiHelp) { + const decodedAiHelp = decodeURIComponent(aiHelp); + + // Delay to avoid hCaptcha bot detection + const timeoutId = window.setTimeout(() => openAskAI(decodedAiHelp), 1000); + + // Clone instead of mutating in place + const next = new URLSearchParams(searchParams); + next.delete("aiHelp"); + setSearchParams(next); + + return () => clearTimeout(timeoutId); + } + }, [searchParams.toString(), openAskAI]); + + const contextValue: AskAIContextType = { + isOpen, + openAskAI, + closeAskAI, + websiteId, + }; + + if (!websiteId) { + return {children}; + } + + return ( + + openAskAI(), + onAnswerGenerationCompleted: () => openAskAI(), + }, + }} + botProtectionMechanism="hcaptcha" + > + {children} + + + + ); +} + +type AskAIDialogProps = { + initialQuery?: string; + isOpen: boolean; + onOpenChange: (open: boolean) => void; +}; + +function AskAIDialog({ initialQuery, isOpen, onOpenChange }: AskAIDialogProps) { + const { closeAskAI } = useAskAI(); + + const handleOpenChange = (open: boolean) => { + if (!open) { + closeAskAI(); + } else { + onOpenChange(open); + } + }; + + return ( + + + +
+ + Ask AI +
+
+ +
+
+ ); +} + +function ChatMessages({ + conversation, + isPreparingAnswer, + isGeneratingAnswer, + onReset, + onExampleClick, + error, + addFeedback, +}: { + conversation: QA[]; + isPreparingAnswer: boolean; + isGeneratingAnswer: boolean; + onReset: () => void; + onExampleClick: (question: string) => void; + error: string | null; + addFeedback: ( + questionAnswerId: string, + reaction: "upvote" | "downvote", + comment?: FeedbackComment + ) => void; +}) { + const [feedbackGivenForQAs, setFeedbackGivenForQAs] = useState>(new Set()); + + // Reset feedback state when conversation is reset + useEffect(() => { + if (conversation.length === 0) { + setFeedbackGivenForQAs(new Set()); + } + }, [conversation.length]); + + // Check if feedback has been given for the latest QA + const latestQA = conversation[conversation.length - 1]; + const hasFeedbackForLatestQA = latestQA?.id ? feedbackGivenForQAs.has(latestQA.id) : false; + + const exampleQuestions = [ + "How do I increase my concurrency limit?", + "How do I debug errors in my task?", + "How do I deploy my task?", + ]; + + return ( +
+ {conversation.length === 0 ? ( + + + I'm trained on docs, examples, and other content. Ask me anything about Trigger.dev. + + {exampleQuestions.map((question, index) => ( + onExampleClick(question)} + variants={{ + hidden: { + opacity: 0, + x: 20, + }, + visible: { + opacity: 1, + x: 0, + transition: { + opacity: { + duration: 0.5, + ease: "linear", + }, + x: { + type: "spring", + stiffness: 300, + damping: 25, + }, + }, + }, + }} + > + + + {question} + + + ))} + + ) : ( + conversation.map((qa) => ( +
+ {qa.question} +
+
+ )) + )} + {conversation.length > 0 && + !isPreparingAnswer && + !isGeneratingAnswer && + !error && + !latestQA?.id && ( +
+ + Answer generation was stopped + + +
+ )} + {conversation.length > 0 && + !isPreparingAnswer && + !isGeneratingAnswer && + !error && + latestQA?.id && ( +
+ {hasFeedbackForLatestQA ? ( + + + Thanks for your feedback! + + + ) : ( +
+ + Was this helpful? + +
+ + +
+
+ )} + +
+ )} + {isPreparingAnswer && ( +
+ + Preparing answer… +
+ )} + {error && ( +
+ + Error generating answer: + + {error} If the problem persists after retrying, please contact support. + + +
+ +
+
+ )} +
+ ); +} + +function ChatInterface({ initialQuery }: { initialQuery?: string }) { + const [message, setMessage] = useState(""); + const [isExpanded, setIsExpanded] = useState(false); + const hasSubmittedInitialQuery = useRef(false); + const { + conversation, + submitQuery, + isGeneratingAnswer, + isPreparingAnswer, + resetConversation, + stopGeneration, + error, + addFeedback, + } = useChat(); + + useEffect(() => { + if (initialQuery && !hasSubmittedInitialQuery.current) { + hasSubmittedInitialQuery.current = true; + setIsExpanded(true); + submitQuery(initialQuery); + } + }, [initialQuery, submitQuery]); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + if (message.trim()) { + setIsExpanded(true); + submitQuery(message); + setMessage(""); + } + }; + + const handleExampleClick = (question: string) => { + setIsExpanded(true); + submitQuery(question); + }; + + const handleReset = () => { + resetConversation(); + setIsExpanded(false); + }; + + return ( + + +
+
+ setMessage(e.target.value)} + placeholder="Ask a question..." + disabled={isGeneratingAnswer} + autoFocus + className="flex-1 rounded-md border border-grid-bright bg-background-dimmed px-3 py-2 text-text-bright placeholder:text-text-dimmed focus-visible:focus-custom" + /> + {isGeneratingAnswer ? ( + stopGeneration()} + className="group relative z-10 flex size-10 min-w-10 cursor-pointer items-center justify-center" + > + + + + } + content="Stop generating" + /> + ) : isPreparingAnswer ? ( + + + + ) : ( +
+
+
+ ); +} + +function GradientSpinnerBackground({ + children, + className, + hoverEffect = false, +}: { + children?: React.ReactNode; + className?: string; + hoverEffect?: boolean; +}) { + return ( +
+
+ {children} +
+
+ ); +} + +export function AskAIButton({ question }: { question?: string }) { + const { openAskAI } = useAskAI(); + + return ( + + + +
+ +
+
+ + Ask AI + +
+
+ ); +} diff --git a/apps/webapp/app/components/navigation/SideMenu.tsx b/apps/webapp/app/components/navigation/SideMenu.tsx index d9170d68e9..0cf70c0153 100644 --- a/apps/webapp/app/components/navigation/SideMenu.tsx +++ b/apps/webapp/app/components/navigation/SideMenu.tsx @@ -22,11 +22,13 @@ import { useNavigation } from "@remix-run/react"; import { useEffect, useRef, useState, type ReactNode } from "react"; import simplur from "simplur"; import { AISparkleIcon } from "~/assets/icons/AISparkleIcon"; +import { BranchEnvironmentIconSmall } from "~/assets/icons/EnvironmentIcons"; import { RunsIconExtraSmall } from "~/assets/icons/RunsIcon"; import { TaskIconSmall } from "~/assets/icons/TaskIcon"; import { WaitpointTokenIcon } from "~/assets/icons/WaitpointTokenIcon"; import { Avatar } from "~/components/primitives/Avatar"; import { type MatchedEnvironment } from "~/hooks/useEnvironment"; +import { useFeatures } from "~/hooks/useFeatures"; import { type MatchedOrganization } from "~/hooks/useOrganizations"; import { type MatchedProject } from "~/hooks/useProject"; import { useHasAdminAccess } from "~/hooks/useUser"; @@ -61,7 +63,7 @@ import { v3UsagePath, v3WaitpointTokensPath, } from "~/utils/pathBuilder"; -import { useKapaWidget } from "../../hooks/useKapaWidget"; +import { useAskAI } from "../AskAI"; import { FreePlanUsage } from "../billing/FreePlanUsage"; import { ConnectionIcon, DevPresencePanel, useDevPresence } from "../DevPresence"; import { ImpersonationBanner } from "../ImpersonationBanner"; @@ -80,14 +82,12 @@ import { TextLink } from "../primitives/TextLink"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../primitives/Tooltip"; import { ShortcutsAutoOpen } from "../Shortcuts"; import { UserProfilePhoto } from "../UserProfilePhoto"; +import { V4Badge } from "../V4Badge"; import { EnvironmentSelector } from "./EnvironmentSelector"; import { HelpAndFeedback } from "./HelpAndFeedbackPopover"; import { SideMenuHeader } from "./SideMenuHeader"; import { SideMenuItem } from "./SideMenuItem"; import { SideMenuSection } from "./SideMenuSection"; -import { BranchEnvironmentIconSmall } from "~/assets/icons/EnvironmentIcons"; -import { V4Badge } from "../V4Badge"; -import { useFeatures } from "~/hooks/useFeatures"; type SideMenuUser = Pick & { isImpersonating: boolean }; export type SideMenuProject = Pick< @@ -586,12 +586,14 @@ function SelectorDivider() { } function HelpAndAI() { - const { isKapaEnabled, isKapaOpen, openKapa } = useKapaWidget(); + const features = useFeatures(); + const { openAskAI, websiteId } = useAskAI(); + const isKapaEnabled = features.isManagedCloud && websiteId; return ( <> - + {isKapaEnabled && ( @@ -603,9 +605,7 @@ function HelpAndAI() { shortcut={{ modifiers: ["mod"], key: "/", enabledOnInputElements: true }} hideShortcutKey data-modal-override-open-class-ask-ai="true" - onClick={() => { - openKapa(); - }} + onClick={() => openAskAI()} > diff --git a/apps/webapp/app/components/primitives/Dialog.tsx b/apps/webapp/app/components/primitives/Dialog.tsx index 7a78b5e02b..5ac179646b 100644 --- a/apps/webapp/app/components/primitives/Dialog.tsx +++ b/apps/webapp/app/components/primitives/Dialog.tsx @@ -50,22 +50,15 @@ const DialogContent = React.forwardRef< >
{children} - -
- - - Close -
+ + + + Close diff --git a/apps/webapp/app/components/primitives/ShortcutKey.tsx b/apps/webapp/app/components/primitives/ShortcutKey.tsx index acfd599c73..04b1f36737 100644 --- a/apps/webapp/app/components/primitives/ShortcutKey.tsx +++ b/apps/webapp/app/components/primitives/ShortcutKey.tsx @@ -13,8 +13,8 @@ const medium = export const variants = { small: - "text-[0.6rem] font-medium min-w-[17px] rounded-[2px] tabular-nums px-1 ml-1 -mr-0.5 flex items-center gap-x-1 border border-dimmed/40 text-text-dimmed group-hover:text-text-bright/80 group-hover:border-dimmed/60 transition uppercase", - medium, + "text-[0.6rem] font-medium min-w-[17px] rounded-[2px] tabular-nums px-1 ml-1 -mr-0.5 flex items-center gap-x-1 border border-text-dimmed/40 text-text-dimmed group-hover:text-text-bright/80 group-hover:border-text-dimmed/60 transition uppercase", + medium: cn(medium, "group-hover:border-charcoal-550"), "medium/bright": cn(medium, "bg-charcoal-750 text-text-bright border-charcoal-650"), }; diff --git a/apps/webapp/app/hooks/useKapaWidget.tsx b/apps/webapp/app/hooks/useKapaWidget.tsx deleted file mode 100644 index 3f0f8eef44..0000000000 --- a/apps/webapp/app/hooks/useKapaWidget.tsx +++ /dev/null @@ -1,145 +0,0 @@ -import { useMatches, useSearchParams } from "@remix-run/react"; -import { useCallback, useEffect, useState } from "react"; -import { useFeatures } from "~/hooks/useFeatures"; -import { type loader } from "~/root"; -import { useShortcuts } from "../components/primitives/ShortcutsProvider"; -import { useTypedMatchesData } from "./useTypedMatchData"; - -type OpenOptions = { mode: string; query: string; submit: boolean }; - -declare global { - interface Window { - Kapa: ( - command: string, - options?: (() => void) | { onRender?: () => void } | OpenOptions, - remove?: string | { onRender?: () => void } - ) => void; - } -} - -export function KapaScripts({ websiteId }: { websiteId?: string }) { - if (!websiteId) return null; - - return ( - <> - -