Skip to content

Conversation

myftija
Copy link
Member

@myftija myftija commented Apr 10, 2025

This reference project shows a possible approach to implement workflows using Trigger.dev and ReactFlow. It makes use of the Trigger.dev Realtime API and the new waitpoint primitive to implement a human-in-the-loop approach for approving the result of an AI workflow.

✅ Checklist

  • I have followed every step in the contributing guide
  • The PR title follows the convention.
  • I ran and tested the code works

Changelog

  • New reference project that showcases waitpoint tokens for building a workflow with a human-in-the-loop step.

Screenshots

waitpoint-project

Summary by CodeRabbit

  • New Features

    • Launched an AI-driven article workflow that automates content scraping, summarization, and text-to-speech conversion.
    • Introduced an interactive, visual interface to track process progress and manage human-in-the-loop review decisions.
    • Enabled real-time notifications for workflow outcomes.
  • Documentation

    • Added comprehensive setup guides to help users configure and deploy the platform seamlessly.

Copy link

changeset-bot bot commented Apr 10, 2025

⚠️ No Changeset found

Latest commit: bee650a

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

Copy link
Contributor

coderabbitai bot commented Apr 10, 2025

Walkthrough

This update introduces a comprehensive project setup for the waitpoint tokens reference implementation. It adds essential configuration files, a Next.js application structure, and detailed environment and dependency settings. Several new asynchronous tasks and functions manage an article workflow that includes scraping, summarization, text-to-speech conversion, review handling, and parallel notifications. Additionally, new React components and utilities facilitate interactive workflow visualization and node management using ReactFlow.

Changes

File(s) Change Summary
references/waitpoint-tokens/.env.example,
references/waitpoint-tokens/.gitignore,
references/waitpoint-tokens/README.md
Added environment configuration with key placeholders, Git ignore rules for development artifacts, and documentation for setting up an AI workflow.
references/waitpoint-tokens/(next.config.ts, package.json, postcss.config.mjs, trigger.config.ts, tsconfig.json) Introduced essential project configuration files including Next.js, build/dependency settings, TypeScript options, PostCSS setup, and Trigger.dev environment configuration.
references/waitpoint-tokens/src/app/(actions.ts, globals.css, layout.tsx, page.tsx) Added application-specific files for handling workflow actions, global styling with Tailwind CSS, a root layout with metadata, and the main entry page.
references/waitpoint-tokens/src/components/(ActionNode.tsx, InputNode.tsx, ReviewNode.tsx, Flow.tsx) Implemented new React components representing interactive workflow nodes (action, input, review) and a visual flow diagram for workflow monitoring.
references/waitpoint-tokens/src/lib/cn.ts Provided a utility function to merge and manage Tailwind CSS class names using conditional logic.
references/waitpoint-tokens/src/trigger/(articleWorkflow.ts, convertTextToSpeech.ts, publishSummary.ts, reviewSummary.ts, scrapeSite.ts, sendSlackNotification.ts, summarizeArticle.ts) Introduced a suite of asynchronous tasks and helper functions for article processing workflows, including scraping, summarizing, converting text to speech, handling review responses, and managing notifications and publishing.

Sequence Diagram(s)

sequenceDiagram
    participant User as User
    participant WF as Workflow Engine
    participant Scrape as Scrape Site
    participant Summarizer as Summarize Article
    participant TTS as Text-to-Speech
    participant Review as Review Summary
    participant Slack as Slack Notification
    participant Publish as Publish Summary

    User->>WF: Trigger Article Workflow
    WF->>Scrape: Request article scraping
    Scrape-->>WF: Return article content
    WF->>Summarizer: Summarize article content
    Summarizer-->>WF: Return summary
    WF->>TTS: Convert text to speech
    TTS-->>WF: Return audio URL
    WF->>Review: Initiate review process
    Review-->>WF: Return review outcome
    alt Review Approved
        WF->>Slack: Send Slack notification
        WF->>Publish: Publish summary
    else Review Rejected/Timed out
        WF-->>User: Notify rejection/timeout
    end
Loading

Poem

I'm a hopping coder, quick and keen,
With configs and workflows bright and clean.
I nibble on code carrots with delight,
Crafting streams and tasks both day and night.
From React to Trigger, my heart sings true—
A bunny’s joy in every line anew! 🐇💻

Tip

⚡💬 Agentic Chat (Pro Plan, General Availability)
  • We're introducing multi-step agentic chat in review comments and issue comments, within and outside of PR's. This feature enhances review and issue discussions with the CodeRabbit agentic chat by enabling advanced interactions, including the ability to create pull requests directly from comments and add commits to existing pull requests.
✨ Finishing Touches
  • 📝 Generate Docstrings

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai plan to trigger planning for file edits and PR creation.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

🧹 Nitpick comments (21)
references/waitpoint-tokens/src/lib/cn.ts (1)

7-7: Check for trailing character on line 7.

There appears to be a stray 7 character at the end of the file that should be removed.

-7
references/waitpoint-tokens/next.config.ts (1)

1-7: Empty Next.js configuration

The configuration file is properly structured but doesn't contain any actual configuration options yet. It's also using a TypeScript file extension instead of the more common JavaScript extension.

Consider either:

  1. Adding actual configuration options if needed for the project
  2. Using next.config.js or next.config.mjs which are more standard naming conventions
-import type { NextConfig } from "next";
-
-const nextConfig: NextConfig = {
-  /* config options here */
-};
-
-export default nextConfig;
+/**
+ * @type {import('next').NextConfig}
+ */
+const nextConfig = {
+  /* config options here */
+};
+
+module.exports = nextConfig;
references/waitpoint-tokens/src/trigger/scrapeSite.ts (1)

9-10: Consider limiting content length for performance

There's no limit on the amount of content fetched and processed, which could lead to performance issues for very large websites.

Add a content size limit to prevent processing excessively large pages:

-    const response = await fetch(payload.url);
-    const html = await response.text();
+    const response = await fetch(payload.url);
+    
+    // Check content size before processing
+    const contentLength = response.headers.get('content-length');
+    if (contentLength && parseInt(contentLength) > 5 * 1024 * 1024) { // 5MB limit
+      logger.warn("Content too large", { url: payload.url, size: contentLength });
+      throw new Error(`Content too large: ${contentLength} bytes`);
+    }
+    
+    const html = await response.text();
references/waitpoint-tokens/src/app/globals.css (2)

4-5: Remove unnecessary empty lines.

These consecutive empty lines can be removed to maintain a cleaner stylesheet.

@import "tailwindcss";
@import "react-tippy/dist/tippy.css";

-

-
:root {

28-28: Use the defined font variable in body styling.

For consistency, the body should use the --font-sans variable that was defined in the theme section.

body {
  background: var(--background);
  color: var(--foreground);
-  font-family: Arial, Helvetica, sans-serif;
+  font-family: var(--font-sans);
}
references/waitpoint-tokens/src/trigger/sendSlackNotification.ts (1)

14-14: Return a result from the task.

It would be helpful to return a result object that includes status information, which can be useful for workflow orchestration and debugging.

This is already addressed in the previous suggestion that adds error handling.

references/waitpoint-tokens/README.md (2)

51-51: Fix grammatical issue: "checkout" should be "check out"

When used as a verb, "check out" should be written as two separate words.

-    Open [http://localhost:3000](http://localhost:3000) on your browser to checkout the workflow.
+    Open [http://localhost:3000](http://localhost:3000) on your browser to check out the workflow.
🧰 Tools
🪛 LanguageTool

[grammar] ~51-~51: The word “checkout” is a noun. The verb is spelled with a white space.
Context: ...tp://localhost:3000) on your browser to checkout the workflow. ## Learn More To learn ...

(NOUN_VERB_CONFUSION)


1-65: Consider adding screenshots or diagrams of the workflow.

The README would benefit from visual aids such as screenshots or diagrams that illustrate the human-in-the-loop workflow. This would help users better understand what they're building before diving into the code.

Add a section after the introduction (around line 5) with a screenshot or diagram, for example:

## Workflow Overview

![Waitpoint Tokens Workflow](./screenshots/workflow-diagram.png)

This diagram illustrates the workflow process, showing how the human approval step is integrated using waitpoint tokens.
🧰 Tools
🪛 LanguageTool

[grammar] ~51-~51: The word “checkout” is a noun. The verb is spelled with a white space.
Context: ...tp://localhost:3000) on your browser to checkout the workflow. ## Learn More To learn ...

(NOUN_VERB_CONFUSION)

references/waitpoint-tokens/src/trigger/summarizeArticle.ts (1)

9-15: Consider handling large article inputs.

For very large articles, you might encounter token limits with the OpenAI API. Consider adding logic to truncate extremely long articles before sending them to the API.

run: async (payload: { content: string }) => {
+   // Approximate token count (rough estimate: 1 token ≈ 4 characters)
+   const estimatedTokenCount = payload.content.length / 4;
+   const MAX_TOKENS = 16000; // Adjust based on model limits
+   
+   // Truncate content if it exceeds token limits (leaving room for prompt and completion)
+   let content = payload.content;
+   if (estimatedTokenCount > MAX_TOKENS * 0.8) {
+     logger.warn(`Article content may exceed token limits. Truncating.`);
+     content = payload.content.substring(0, Math.floor(MAX_TOKENS * 0.8 * 4));
+   }

    const { text } = await generateText({
      model: openai("gpt-4o-mini"),
      prompt: `
      Summarize the following article in a concise manner, focus on the main body of the article and generate a summary that sounds good in speech too.
      The result will be converted to speech for a news report. Make it no longer than 500 words.
-     Content: ${payload.content}`,
+     Content: ${content}`,
    });
references/waitpoint-tokens/.env.example (1)

1-11: Add comments to explain environment variables.

The environment variables example file lists all required keys but lacks comments explaining their purpose. Adding descriptions would improve developer onboarding and reduce potential misconfiguration.

-TRIGGER_SECRET_KEY=
+# Your Trigger.dev secret key for authentication
+TRIGGER_SECRET_KEY=
-TRIGGER_API_URL=
+# The URL of the Trigger.dev API (usually https://api.trigger.dev)
+TRIGGER_API_URL=
-NEXT_PUBLIC_TRIGGER_API_URL=
+# The public URL of the Trigger.dev API for client-side usage
+NEXT_PUBLIC_TRIGGER_API_URL=
-OPENAI_API_KEY=
+# Required for AI text generation (article summarization)
+OPENAI_API_KEY=
-ELEVENLABS_API_KEY=
+# Required for text-to-speech conversion
+ELEVENLABS_API_KEY=
-AWS_ACCESS_KEY_ID=
+# AWS credentials for storage of media files
+AWS_ACCESS_KEY_ID=
-AWS_SECRET_ACCESS_KEY=
+# AWS secret key for authentication
+AWS_SECRET_ACCESS_KEY=
-AWS_ENDPOINT_URL_S3=
+# Custom endpoint URL for S3 (only needed for local development or custom S3 providers)
+AWS_ENDPOINT_URL_S3=
-AWS_ENDPOINT_URL_IAM=
+# Custom endpoint URL for IAM (only needed for local development or custom providers)
+AWS_ENDPOINT_URL_IAM=
-AWS_REGION=
+# AWS region for S3 and other services
+AWS_REGION=
-SLACK_WEBHOOK_URL=
+# Webhook URL for sending notifications to Slack
+SLACK_WEBHOOK_URL=
references/waitpoint-tokens/src/app/layout.tsx (1)

15-30: Consider adding a responsive meta tag for mobile devices.
Although Next.js often handles metadata in the new App Router, including a viewport meta tag could improve responsiveness and accessibility.

 export const metadata: Metadata = {
   title: "Trigger Flow",
   description: "Trigger.dev reference project with ReactFlow",
 };
 
 export default function RootLayout({
   children,
 }: Readonly<{
   children: React.ReactNode;
 }>) {
   return (
     <html lang="en">
+      <head>
+        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+      </head>
       <body className={`${geistSans.variable} ${geistMono.variable} antialiased`}>{children}</body>
     </html>
   );
 }
references/waitpoint-tokens/src/trigger/publishSummary.ts (1)

1-20: Optional improvement: Provide user-facing logs or fallback logic for repeated failures.
The forced error for the first two attempts is a good demonstration of retry logic. To further enhance user or operator visibility, consider adding more context to the thrown error message or logging additional details during each attempt.

 if (ctx.attempt.number <= 2) {
-  throw new Error("Unlucky attempt!");
+  logger.warn(`Attempt #${ctx.attempt.number} failed. Retrying...`);
+  throw new Error(`Unlucky attempt on attempt #${ctx.attempt.number}!`);
}
references/waitpoint-tokens/src/trigger/articleWorkflow.ts (2)

26-36: Summarize step: Logical flow with consistent error handling.
This structure ensures the user knows exactly which step failed. Consider logging the original error for improved debugging.

 if (!summarizeResult.ok) {
-  throw new Error("Failed to summarize article");
+  logger.error("Failed to summarize article", { error: summarizeResult.error });
+  throw new Error("Failed to summarize article.");
 }

60-88: Parallel tasks for Slack notification and publishing are well-handled.
The use of batch.triggerByTaskAndWait is clean. However, consider partial failure handling. For example, if Slack fails but publish succeeds, it still throws an error. This might be correct, but if your use case warrants partial success, handle accordingly.

 if (!sendSlackNotificationRun.ok && publishSummaryRun.ok) {
   logger.error("Slack failed but summary published successfully");
   // Optionally handle partial success or add fallback
-  throw new Error("Failed to send Slack notification");
+  // throw new Error("Failed to send Slack notification");
 }
 
 if (!publishSummaryRun.ok) {
   throw new Error("Failed to publish summary");
 }
references/waitpoint-tokens/src/app/actions.ts (2)

12-20: Consider using a more secure random string generation method.

The current implementation works for demo purposes, but for production code, consider using a more cryptographically secure method like crypto.randomUUID() or the crypto.randomBytes() function.

-const randomStr = (length: number) =>
-  [...Array(length)]
-    .map(
-      () =>
-        "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"[
-          Math.floor(Math.random() * 62)
-        ]
-    )
-    .join("");
+const randomStr = (length: number) => {
+  // Use crypto for more secure random values
+  if (typeof crypto !== 'undefined') {
+    return Array.from(crypto.getRandomValues(new Uint8Array(length)))
+      .map(b => 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'.charAt(b % 62))
+      .join('');
+  }
+  // Fallback for environments without crypto
+  return [...Array(length)]
+    .map(() => 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'[Math.floor(Math.random() * 62)])
+    .join('');
+};

62-72: Consider making the rejection reason configurable.

The current implementation hardcodes the rejection reason to "It's no good". For a more flexible implementation, consider accepting the reason as a parameter.

-export async function rejectArticleSummary(tokenId: string) {
+export async function rejectArticleSummary(tokenId: string, reason: string = "It's no good") {
  await wait.completeToken<ReviewPayload>(
    { id: tokenId },
    {
      approved: false,
      rejectedAt: new Date(),
      rejectedBy: user,
-      reason: "It's no good",
+      reason,
    }
  );
}
references/waitpoint-tokens/src/components/InputNode.tsx (1)

13-53: Well-implemented input node component using React Server Actions.

The component:

  1. Uses useActionState to handle the form submission
  2. Updates the node data when the action completes
  3. Provides visual feedback with URL display
  4. Disables the submit button while the action is pending

This provides a good example of integrating React Server Actions with ReactFlow.

Consider adding more robust error handling or validation feedback beyond the browser's built-in validation for URLs.

references/waitpoint-tokens/src/components/ActionNode.tsx (2)

11-39: Consider consolidating custom node data into a more conventional type.

While defining ActionNodeData as a Node<{}, "action"> is valid, consider isolating your custom node data into a separate interface (e.g., ActionNodeDataProps) and then mapping it to NodeProps<ActionNodeDataProps, "action">. This design can improve type clarity and scale better as your node data grows in complexity.


83-111: Optimize tooltip rendering.

You are rendering a tooltip component around the status icon. If every node re-renders frequently, performance might degrade from repeated tooltip initialization. Consider memoizing or conditionally rendering tooltips only when the status is present. For large diagrams with many nodes, this can keep UI performance smooth.

references/waitpoint-tokens/src/components/ReviewNode.tsx (1)

10-35: Ensure consistent naming and usage for ReviewNodeData.

You’ve introduced ReviewNodeData using the same pattern as in ActionNodeData. For consistency and future maintainability, consider extracting common fields (e.g., trigger) into a shared interface or a base node data type if both node types share logic or shape.

references/waitpoint-tokens/src/trigger/convertTextToSpeech.ts (1)

32-41: Use environment variables for bucket name and expiration.

Currently, the bucket name articles and Expires: 3600 are hard-coded. Moving them to environment variables (or a config file) will:

  1. Allow dynamic configuration across environments.
  2. Streamline deployments to different buckets or changes in presigned URL expiration.
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 61232ab and 65418be.

⛔ Files ignored due to path filters (3)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
  • references/waitpoint-tokens/src/app/favicon.ico is excluded by !**/*.ico
  • references/waitpoint-tokens/src/app/logo.svg is excluded by !**/*.svg
📒 Files selected for processing (25)
  • references/nextjs-realtime/trigger.config.ts (1 hunks)
  • references/waitpoint-tokens/.env.example (1 hunks)
  • references/waitpoint-tokens/.gitignore (1 hunks)
  • references/waitpoint-tokens/README.md (1 hunks)
  • references/waitpoint-tokens/next.config.ts (1 hunks)
  • references/waitpoint-tokens/package.json (1 hunks)
  • references/waitpoint-tokens/postcss.config.mjs (1 hunks)
  • references/waitpoint-tokens/src/app/actions.ts (1 hunks)
  • references/waitpoint-tokens/src/app/globals.css (1 hunks)
  • references/waitpoint-tokens/src/app/layout.tsx (1 hunks)
  • references/waitpoint-tokens/src/app/page.tsx (1 hunks)
  • references/waitpoint-tokens/src/components/ActionNode.tsx (1 hunks)
  • references/waitpoint-tokens/src/components/InputNode.tsx (1 hunks)
  • references/waitpoint-tokens/src/components/ReviewNode.tsx (1 hunks)
  • references/waitpoint-tokens/src/components/flow.tsx (1 hunks)
  • references/waitpoint-tokens/src/lib/cn.ts (1 hunks)
  • references/waitpoint-tokens/src/trigger/articleWorkflow.ts (1 hunks)
  • references/waitpoint-tokens/src/trigger/convertTextToSpeech.ts (1 hunks)
  • references/waitpoint-tokens/src/trigger/publishSummary.ts (1 hunks)
  • references/waitpoint-tokens/src/trigger/reviewSummary.ts (1 hunks)
  • references/waitpoint-tokens/src/trigger/scrapeSite.ts (1 hunks)
  • references/waitpoint-tokens/src/trigger/sendSlackNotification.ts (1 hunks)
  • references/waitpoint-tokens/src/trigger/summarizeArticle.ts (1 hunks)
  • references/waitpoint-tokens/trigger.config.ts (1 hunks)
  • references/waitpoint-tokens/tsconfig.json (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (5)
references/waitpoint-tokens/src/trigger/publishSummary.ts (1)
packages/core/src/v3/taskContext/index.ts (1)
  • ctx (26-28)
references/waitpoint-tokens/src/trigger/reviewSummary.ts (2)
references/waitpoint-tokens/src/app/layout.tsx (1)
  • metadata (15-18)
packages/trigger-sdk/src/v3/wait.ts (1)
  • WaitpointTimeoutError (365-370)
references/waitpoint-tokens/src/trigger/articleWorkflow.ts (7)
references/waitpoint-tokens/src/trigger/scrapeSite.ts (1)
  • scrape (4-18)
packages/core/src/v3/taskContext/index.ts (1)
  • ctx (26-28)
references/waitpoint-tokens/src/trigger/summarizeArticle.ts (1)
  • summarizeArticle (5-23)
references/waitpoint-tokens/src/trigger/convertTextToSpeech.ts (1)
  • convertTextToSpeech (55-72)
references/waitpoint-tokens/src/trigger/reviewSummary.ts (1)
  • reviewSummary (17-42)
references/waitpoint-tokens/src/trigger/sendSlackNotification.ts (1)
  • sendSlackNotification (3-16)
references/waitpoint-tokens/src/trigger/publishSummary.ts (1)
  • publishSummary (3-20)
references/waitpoint-tokens/src/components/flow.tsx (9)
references/waitpoint-tokens/src/components/InputNode.tsx (1)
  • InputNodeData (7-7)
references/waitpoint-tokens/src/components/ActionNode.tsx (1)
  • ActionNodeData (11-39)
references/waitpoint-tokens/src/components/ReviewNode.tsx (1)
  • ReviewNodeData (10-35)
references/waitpoint-tokens/src/trigger/scrapeSite.ts (1)
  • scrape (4-18)
references/waitpoint-tokens/src/trigger/summarizeArticle.ts (1)
  • summarizeArticle (5-23)
references/waitpoint-tokens/src/trigger/convertTextToSpeech.ts (1)
  • convertTextToSpeech (55-72)
references/waitpoint-tokens/src/trigger/reviewSummary.ts (1)
  • reviewSummary (17-42)
references/waitpoint-tokens/src/trigger/publishSummary.ts (1)
  • publishSummary (3-20)
packages/react-hooks/src/contexts.tsx (1)
  • TriggerAuthContext (10-10)
references/waitpoint-tokens/src/components/ActionNode.tsx (2)
packages/react-hooks/src/hooks/useRealtime.ts (1)
  • useRealtimeRunsWithTag (353-433)
references/waitpoint-tokens/src/lib/cn.ts (1)
  • cn (4-6)
🪛 LanguageTool
references/waitpoint-tokens/README.md

[grammar] ~51-~51: The word “checkout” is a noun. The verb is spelled with a white space.
Context: ...tp://localhost:3000) on your browser to checkout the workflow. ## Learn More To learn ...

(NOUN_VERB_CONFUSION)

⏰ Context from checks skipped due to timeout of 90000ms (3)
  • GitHub Check: e2e / 🧪 CLI v3 tests (windows-latest - pnpm)
  • GitHub Check: e2e / 🧪 CLI v3 tests (windows-latest - npm)
  • GitHub Check: typecheck / typecheck
🔇 Additional comments (27)
references/waitpoint-tokens/src/lib/cn.ts (1)

1-6: Well-implemented utility function for managing Tailwind CSS classes.

This is a standard implementation of a utility function that combines the clsx library (for conditional class name rendering) with tailwind-merge (for resolving conflicting Tailwind classes). This pattern is widely used in React projects with Tailwind CSS and follows best practices.

references/waitpoint-tokens/trigger.config.ts (1)

1-15: Configuration looks good and aligns with project needs

The configuration is well-structured and includes all necessary parameters for the Trigger.dev SDK. The maxDuration of 3600 seconds (1 hour) should be sufficient for running long-running tasks in the waitpoint primitive showcase. The configuration also enables retries during development which is good for reliability.

references/waitpoint-tokens/.gitignore (1)

1-44: Well-structured .gitignore file

This .gitignore file follows best practices for Next.js/Node.js projects, properly excluding build artifacts, dependencies, and environment files while allowing the example env file to be committed.

references/waitpoint-tokens/postcss.config.mjs (1)

1-5:

❓ Verification inconclusive

Verify Tailwind CSS plugin name

The plugin name "@tailwindcss/postcss" appears incorrect. The standard Tailwind CSS PostCSS plugin is typically named "tailwindcss".

Update the plugin name to the correct one:

const config = {
-  plugins: ["@tailwindcss/postcss"],
+  plugins: ["tailwindcss"],
};

export default config;

🌐 Web query:

What is the correct plugin name for Tailwind CSS in PostCSS configuration?

💡 Result:

The correct plugin name for Tailwind CSS in a PostCSS configuration depends on the Tailwind version:

For Tailwind CSS v3.x or earlier:

  • Use tailwindcss as the plugin name in your PostCSS config:
    module.exports = {
      plugins: {
        'tailwindcss': {},
        'autoprefixer': {},
      }
    }

For Tailwind CSS v4.x:

  • Use @tailwindcss/postcss instead:
    module.exports = {
      plugins: {
        '@tailwindcss/postcss': {},
        'autoprefixer': {},
      }
    }
    This change addresses a deprecation warning where Tailwind's PostCSS integration moved to a separate package[6][9].

Key details:

  • The v4 migration requires installing @tailwindcss/postcss[7][8]
  • Ensure your postcss.config.js matches your Tailwind version to avoid errors like:

    "It looks like you're trying to use tailwindcss directly as a PostCSS plugin"[6][9]

  • Both versions still require the standard Tailwind setup steps (config file, directives in CSS)[1][4]

Check your package.json to confirm which Tailwind version you're using before choosing the plugin name.

Citations:


Action Required: Verify Tailwind Plugin Name Based on Version

The current configuration uses "@tailwindcss/postcss", which is correct if your project is using Tailwind CSS v4.x. However, if you’re on Tailwind CSS v3.x or earlier, the plugin should be named "tailwindcss" instead. Please verify your Tailwind version in your package.json and update accordingly.

  • For Tailwind CSS v3.x or earlier:
    Update as follows:
    const config = {
    -  plugins: ["@tailwindcss/postcss"],
    +  plugins: ["tailwindcss"],
    };
    
    export default config;
  • For Tailwind CSS v4.x and above:
    No change is needed.
references/nextjs-realtime/trigger.config.ts (1)

6-6: Project identifier updated correctly.

The project identifier has been updated from "proj_bzhdaqhlymtuhlrcgbqy" to "proj_jlodmrnldlncmkmscdyu" as required for the new waitpoint tokens reference project.

references/waitpoint-tokens/src/app/page.tsx (2)

7-24: Well-structured authentication approach for the reference project.

The implementation uses a hardcoded user identifier with clear documentation about its purpose in this demo context. The public token generation follows security best practices by limiting scopes appropriately, with helpful comments explaining the approach and linking to further documentation.


25-39: Clean and responsive UI layout.

The component structure provides a good demonstration of integrating Trigger.dev with ReactFlow. The layout is well-organized with proper spacing using Tailwind CSS classes, and passes the necessary props to the Flow component.

references/waitpoint-tokens/tsconfig.json (1)

1-33: LGTM - TypeScript configuration is properly set up for Next.js.

The TypeScript configuration includes all necessary options for a Next.js project, with appropriate settings for:

  • Target ECMAScript version
  • Module resolution
  • Type checking
  • Path aliases
  • File inclusions and exclusions

This setup will provide good type safety and development experience.

references/waitpoint-tokens/.env.example (1)

1-11:

✅ Verification successful

Verify required environment variables are complete.

This environment file seems to cover all the services mentioned in the PR, but it would be good to verify that all variables required by the application are included. For instance, if the application is using any database integrations or additional APIs, those might be missing.


🏁 Script executed:

#!/bin/bash
# Check if there are any environment variables used in the codebase that are not included in .env.example

echo "Searching for potential missing environment variables..."
# Find all potential environment variable references in the code
ENV_VARS_REFERENCED=$(grep -r "process.env" --include="*.ts" --include="*.tsx" --include="*.js" references/waitpoint-tokens/src | grep -o "process.env\.[A-Z_0-9]*" | sort | uniq | sed 's/process.env\.//')

# Get the environment variables defined in .env.example
ENV_VARS_DEFINED=$(grep -v "^#" references/waitpoint-tokens/.env.example | grep "=" | cut -d "=" -f 1)

# Find environment variables that are referenced but not defined
for var in $ENV_VARS_REFERENCED; do
  if ! echo "$ENV_VARS_DEFINED" | grep -q "^$var$"; then
    echo "Potential missing environment variable: $var"
  fi
done

echo "Checking complete!"

Length of output: 1312


Environment Variables Verified as Complete

The verification script confirms that all environment variables referenced in the code (specifically NEXT_PUBLIC_TRIGGER_API_URL and SLACK_WEBHOOK_URL) are present in the .env.example file. No additional environment variables appear to be missing. Please continue to monitor for any new service integrations that might require further updates.

references/waitpoint-tokens/src/app/layout.tsx (1)

1-14: Good usage of Google fonts with subsets for smaller font bundles.
This setup helps to reduce the overall bundle size by limiting glyph coverage to “latin” only. No major concerns.

references/waitpoint-tokens/package.json (2)

1-11: Scripts look good.
The dev scripts utilize Next.js with Turbopack, and there’s a separate trigger:dev script, which helps maintain a quick development workflow.


12-41:

✅ Verification successful

Verify React and Next.js versions.
React 19 and Next.js 15 might be experimental or unreleased. Please confirm that these releases are stable or officially supported in your project timeline.


🌐 Web query:

Is React version 19.0.0 and Next version 15.2.4 officially available and stable as of April 2025?

💡 Result:

React 19.0.0 and Next.js 15.2.4 are both officially available and stable as of April 2025.

React 19.0.0

  • Stable Release: React 19.0.0 was officially released on December 5, 2024[1][5][9].
  • Key Features:
    • Actions: Simplifies async data mutations with automatic handling of pending states, errors, and optimistic updates[1][5].
    • Enhanced Suspense: Introduces pre-warming for suspended trees to improve rendering efficiency[1].
    • Error Handling Improvements: Reduces error duplication and adds granular controls for caught/uncaught errors via onCaughtError and onUncaughtError[1][9].

Next.js 15.2.4

  • Stable Release: Next.js 15.2.4 was released on March 24, 2025[6][10].
  • Compatibility: Fully supports React 19 in both Pages and App Router configurations[4].
  • Key Updates:
    • Turbopack Improvements: 57.6% faster compile times and 30% reduced memory usage compared to earlier versions[2].
    • Streaming Metadata: Allows async metadata generation without blocking initial page rendering[2].
    • Security Fixes: Includes patches for critical vulnerabilities like CVE-2025-29927 (authorization bypass) in earlier 15.x versions[10].

Both versions are recommended for production use, with Next.js 15.2.4 requiring React 19.1.0 or later for full compatibility[6][9].

Citations:


Dependency Versions Confirmed: React 19.0.0 & Next.js 15.2.4 are Stable

The versions specified in references/waitpoint-tokens/package.json (Lines 12-41) have been verified. Both React 19.0.0 and Next.js 15.2.4 are officially available and stable as of April 2025, so no changes are required to these dependency versions.

references/waitpoint-tokens/src/trigger/articleWorkflow.ts (5)

1-8: Imports are well-structured.
All relevant tasks are imported clearly, keeping the top-level file neat and maintainable.


9-14: Workflow definition is clear and concise.
Declaring a single articleWorkflow task with an id ensures clarity on the workflow’s identity.


15-21: Scrape step: Good error handling on a failed result.
The code checks scrapeResult.ok and throws an error if scraping fails, which is appropriate.


37-47: Convert-to-speech step is straightforward and consistent.
No issues identified. Retries are handled by the triggered task definition if configured.


48-59: Waitpoint-based review is properly integrated.
The system waits for a human-in-the-loop approval. Good approach to pause the workflow.

references/waitpoint-tokens/src/trigger/reviewSummary.ts (2)

1-15: Well-structured type definitions for review payloads.

The types are well defined using discriminated unions with the approved property as the discriminator. This makes it type-safe to handle different review outcomes.


17-42: Good implementation of the human-in-the-loop pattern with waitpoint tokens.

The task correctly:

  1. Sets metadata for tracking the review process
  2. Waits for the token to be completed
  3. Handles timeout errors appropriately
  4. Updates metadata based on the review outcome

This is a solid implementation of the waitpoint primitive for human approval in workflows.

One minor note: After handling the WaitpointTimeoutError case, the function still throws the error at line 32, which means callers will need to handle this exception. This is likely intentional but worth confirming.

references/waitpoint-tokens/src/components/flow.tsx (4)

26-129: Well-structured workflow node definitions.

The nodes and their connections are clearly defined with appropriate task identifiers and position information. The use of satisfies ensures type safety when referring to task IDs.


172-175: Good organization of node types.

The node types mapping is properly memoized and includes all the necessary custom node components.


182-252: Effective implementation of run tag propagation and edge animation.

The component:

  1. Detects changes in the input node's run tag
  2. Propagates the run tag to all other nodes
  3. Animates edges based on node status

The comments explaining the alternative approach (lines 184-185) and simplified approach (lines 245-246) are helpful for future developers.


253-271: Proper context provider setup for authentication.

The component correctly wraps ReactFlow with the TriggerAuthContext.Provider, ensuring that child components have access to the authentication details needed for the workflow actions.

references/waitpoint-tokens/src/app/actions.ts (1)

22-49: Well-implemented workflow trigger function.

The function correctly:

  1. Extracts the article URL from form data
  2. Generates a unique tag
  3. Creates a waitpoint token with appropriate timeout
  4. Triggers the article workflow task
  5. Returns all necessary data

This provides a good example of how to set up a human-in-the-loop workflow with waitpoint tokens.

references/waitpoint-tokens/src/components/InputNode.tsx (1)

7-11: Good type definitions and type guard.

The InputNodeData type and isInputNode type guard are well defined, making it easy to safely work with input nodes in the flow.

references/waitpoint-tokens/src/components/ReviewNode.tsx (1)

112-136: Confirm concurrency safety for review transitions.

Buttons trigger review transitions via useTransition. Verify that simultaneous user clicks or concurrency do not cause inconsistent states. Consider:

  1. Disabling the opposite button (Approve vs. Reject) immediately after one click.
  2. Checking the server response before re-enabling UI.

This ensures predictable user experience if the user quickly clicks multiple times.

references/waitpoint-tokens/src/trigger/convertTextToSpeech.ts (1)

12-30: Add failure handling for ElevenLabs API calls.

createAudioStreamFromText relies on eleventLabs.textToSpeech.convertAsStream. If the external service fails or returns unexpected data, your code silently accumulates an empty buffer. Consider:

  1. Adding try/catch to log errors.
  2. Validating the returned stream or chunk data length.

Comment on lines +4 to +18
export const scrape = task({
id: "scrape-site",
maxDuration: 300,
run: async (payload: { url: string }) => {
const response = await fetch(payload.url);
const html = await response.text();
const content = htmlToText(html);

logger.info("Site scraped successfully", { url: payload.url });

return {
content,
};
},
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add error handling and enhance scraping robustness

The current implementation lacks error handling and web scraping best practices.

Consider implementing the following improvements:

  1. Add error handling for failed requests
  2. Validate input URLs
  3. Add proper User-Agent headers
  4. Respect rate limits and robots.txt
export const scrape = task({
  id: "scrape-site",
  maxDuration: 300,
  run: async (payload: { url: string }) => {
+    // Validate URL
+    try {
+      new URL(payload.url);
+    } catch (error) {
+      logger.error("Invalid URL provided", { url: payload.url });
+      throw new Error(`Invalid URL: ${payload.url}`);
+    }
+
+    // Define request headers with proper user agent
+    const headers = {
+      'User-Agent': 'Trigger.dev Reference Project Bot (+https://trigger.dev)',
+    };
+
+    try {
-    const response = await fetch(payload.url);
+      const response = await fetch(payload.url, { headers });
+      
+      if (!response.ok) {
+        throw new Error(`Failed to fetch URL: ${payload.url}, status: ${response.status}`);
+      }
+      
      const html = await response.text();
      const content = htmlToText(html);

      logger.info("Site scraped successfully", { url: payload.url });

      return {
        content,
      };
+    } catch (error) {
+      logger.error("Error scraping site", { 
+        url: payload.url, 
+        error: error instanceof Error ? error.message : String(error) 
+      });
+      throw error;
+    }
  },
});
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const scrape = task({
id: "scrape-site",
maxDuration: 300,
run: async (payload: { url: string }) => {
const response = await fetch(payload.url);
const html = await response.text();
const content = htmlToText(html);
logger.info("Site scraped successfully", { url: payload.url });
return {
content,
};
},
});
export const scrape = task({
id: "scrape-site",
maxDuration: 300,
run: async (payload: { url: string }) => {
// Validate URL
try {
new URL(payload.url);
} catch (error) {
logger.error("Invalid URL provided", { url: payload.url });
throw new Error(`Invalid URL: ${payload.url}`);
}
// Define request headers with proper user agent
const headers = {
'User-Agent': 'Trigger.dev Reference Project Bot (+https://trigger.dev)',
};
try {
const response = await fetch(payload.url, { headers });
if (!response.ok) {
throw new Error(`Failed to fetch URL: ${payload.url}, status: ${response.status}`);
}
const html = await response.text();
const content = htmlToText(html);
logger.info("Site scraped successfully", { url: payload.url });
return {
content,
};
} catch (error) {
logger.error("Error scraping site", {
url: payload.url,
error: error instanceof Error ? error.message : String(error)
});
throw error;
}
},
});

Comment on lines +14 to +15
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Font variables reference undefined fonts.

The CSS variables --font-geist-sans and --font-geist-mono are referenced but not defined anywhere in the file. Either import the Geist font family or define these variables properly.

@theme inline {
  --color-background: var(--background);
  --color-foreground: var(--foreground);
-  --font-sans: var(--font-geist-sans);
-  --font-mono: var(--font-geist-mono);
+  --font-sans: Arial, Helvetica, sans-serif;
+  --font-mono: monospace;
}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: Arial, Helvetica, sans-serif;
--font-mono: monospace;
}

Comment on lines +5 to +13
run: async (payload: { message: string }) => {
await fetch(process.env.SLACK_WEBHOOK_URL, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ text: payload.message }),
});

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Add error handling for the Slack webhook request.

The current implementation doesn't handle potential errors when calling the Slack webhook. If the webhook URL is invalid, the network is down, or Slack returns an error response, this would fail without proper error reporting.

export const sendSlackNotification = task({
  id: "send-slack-notification",
  run: async (payload: { message: string }) => {
+   if (!process.env.SLACK_WEBHOOK_URL) {
+     logger.error("SLACK_WEBHOOK_URL environment variable is not defined");
+     throw new Error("SLACK_WEBHOOK_URL environment variable is not defined");
+   }
+   
+   try {
      await fetch(process.env.SLACK_WEBHOOK_URL, {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify({ text: payload.message }),
      });
+
+     logger.info("Slack notification sent");
+     return { success: true, message: "Slack notification sent successfully" };
+   } catch (error) {
+     logger.error("Failed to send Slack notification", error);
+     throw error;
+   }
-
-   logger.info("Slack notification sent");
  },
});
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
run: async (payload: { message: string }) => {
await fetch(process.env.SLACK_WEBHOOK_URL, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ text: payload.message }),
});
export const sendSlackNotification = task({
id: "send-slack-notification",
run: async (payload: { message: string }) => {
if (!process.env.SLACK_WEBHOOK_URL) {
logger.error("SLACK_WEBHOOK_URL environment variable is not defined");
throw new Error("SLACK_WEBHOOK_URL environment variable is not defined");
}
try {
await fetch(process.env.SLACK_WEBHOOK_URL, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ text: payload.message }),
});
logger.info("Slack notification sent");
return { success: true, message: "Slack notification sent successfully" };
} catch (error) {
logger.error("Failed to send Slack notification", error);
throw error;
}
},
});

Comment on lines +25 to +26
cd references/trigger-flow
pnpm exec trigger login -a http://localhost:3030
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Directory path inconsistency.

The README instructs users to navigate to "references/trigger-flow", but based on the PR and file structure, the project appears to be in "references/waitpoint-tokens".

-    cd references/trigger-flow
+    cd references/waitpoint-tokens
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
cd references/trigger-flow
pnpm exec trigger login -a http://localhost:3030
cd references/waitpoint-tokens
pnpm exec trigger login -a http://localhost:3030

Comment on lines +5 to +23
export const summarizeArticle = task({
id: "summarize-article",
maxDuration: 300,
run: async (payload: { content: string }) => {
const { text } = await generateText({
model: openai("gpt-4o-mini"),
prompt: `
Summarize the following article in a concise manner, focus on the main body of the article and generate a summary that sounds good in speech too.
The result will be converted to speech for a news report. Make it no longer than 500 words.
Content: ${payload.content}`,
});

logger.info("Article summary generated successfully");

return {
summary: text,
};
},
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add error handling to the summarization task.

While the task is well-structured, it lacks error handling for potential API failures. Additionally, there's no validation that the summary adheres to the 500-word limit specified in the prompt.

Consider implementing this improvement:

export const summarizeArticle = task({
  id: "summarize-article",
  maxDuration: 300,
  run: async (payload: { content: string }) => {
+   try {
      const { text } = await generateText({
        model: openai("gpt-4o-mini"),
        prompt: `
        Summarize the following article in a concise manner, focus on the main body of the article and generate a summary that sounds good in speech too.
        The result will be converted to speech for a news report. Make it no longer than 500 words.
        Content: ${payload.content}`,
      });
+     
+     const wordCount = text.split(/\s+/).length;
+     logger.info(`Article summary generated successfully. Word count: ${wordCount}`);
+
+     if (wordCount > 500) {
+       logger.warn(`Summary exceeds requested 500-word limit with ${wordCount} words.`);
+     }

      return {
        summary: text,
      };
+   } catch (error) {
+     logger.error("Failed to generate article summary", { error });
+     throw error;
+   }
  },
});
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const summarizeArticle = task({
id: "summarize-article",
maxDuration: 300,
run: async (payload: { content: string }) => {
const { text } = await generateText({
model: openai("gpt-4o-mini"),
prompt: `
Summarize the following article in a concise manner, focus on the main body of the article and generate a summary that sounds good in speech too.
The result will be converted to speech for a news report. Make it no longer than 500 words.
Content: ${payload.content}`,
});
logger.info("Article summary generated successfully");
return {
summary: text,
};
},
});
export const summarizeArticle = task({
id: "summarize-article",
maxDuration: 300,
run: async (payload: { content: string }) => {
try {
const { text } = await generateText({
model: openai("gpt-4o-mini"),
prompt: `
Summarize the following article in a concise manner, focus on the main body of the article and generate a summary that sounds good in speech too.
The result will be converted to speech for a news report. Make it no longer than 500 words.
Content: ${payload.content}`,
});
const wordCount = text.split(/\s+/).length;
logger.info(`Article summary generated successfully. Word count: ${wordCount}`);
if (wordCount > 500) {
logger.warn(`Summary exceeds requested 500-word limit with ${wordCount} words.`);
}
logger.info("Article summary generated successfully");
return {
summary: text,
};
} catch (error) {
logger.error("Failed to generate article summary", { error });
throw error;
}
},
});

Comment on lines 54 to 56
const { runs } = useRealtimeRunsWithTag(data.trigger.userTag);
const { updateNodeData } = useReactFlow<ActionNodeData>();

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Include data in useEffect dependency array.

Inside the useEffect, you rely on data.trigger.currentRunTag and data.trigger.currentRunStatus. However, these fields are not explicitly in your dependency array. If data or data.trigger changes, the effect might not run. Consider including data or at least data.trigger in the dependency array to ensure correct synchronization.

 useEffect(() => {
   ...
-}, [runs, id, updateNodeData]);
+}, [runs, id, data.trigger, updateNodeData]);

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines 50 to 77
useEffect(() => {
if (!data.trigger.currentRunTag && data.trigger.currentRunStatus !== undefined) {
updateNodeData(id, { trigger: { ...data.trigger, currentRunStatus: undefined } });
setWaitpointTokenId(undefined);
setAudioSummaryUrl(undefined);
setReviewStatus(undefined);
return;
}

const run = runs.find(
(run) =>
run.tags.includes(data.trigger.currentRunTag as string) &&
run.taskIdentifier === data.trigger.taskIdentifier
);
if (!run) {
if (data.trigger.currentRunStatus !== undefined) {
updateNodeData(id, { trigger: { ...data.trigger, currentRunStatus: undefined } });
setWaitpointTokenId(undefined);
setAudioSummaryUrl(undefined);
setReviewStatus(undefined);
}
return;
}
setWaitpointTokenId(run.metadata?.waitpointTokenId as string);
setAudioSummaryUrl(run.metadata?.audioSummaryUrl as string);
setReviewStatus(run.metadata?.reviewStatus as ReviewStatus);
updateNodeData(id, { trigger: { ...data.trigger, currentRunStatus: run.status } });
}, [runs, id, updateNodeData]);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add explicit error handling for missing run metadata.

You're setting local component state (like waitpointTokenId, audioSummaryUrl, reviewStatus) from run.metadata. If metadata were unexpectedly undefined or incomplete, it could lead to runtime errors. Consider additional defensive checks or default values to avoid potential crashes.

Comment on lines 55 to 78
export const convertTextToSpeech = task({
id: "convert-text-to-speech",
maxDuration: 300,
run: async (payload: { text: string }) => {
const audioStream = await createAudioStreamFromText(payload.text);
logger.info("Audio stream created");

const s3path = await uploadAudioStreamToS3(audioStream);
logger.info("Audio stream uploaded to S3");

const audioUrl = await generatePresignedUrl(s3path);
logger.info("Audio URL generated");

return {
audioUrl,
};
},
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Include robust error handling in the convertTextToSpeech task.

In the task definition, consider catching potential exceptions (e.g., invalid S3 credentials, API failures) and returning a helpful error message. Otherwise, unexpected failures will propagate and might stall workflows without clarity.

myftija added 3 commits April 12, 2025 13:25
This reference project shows a possible approach to implement workflows using Trigger.dev and ReactFlow. It makes use of the Trigger.dev Realtime API and the new waitpoint primitive to implement a human-in-the-loop approach for approving the result of an AI workflow.
@myftija myftija force-pushed the waitpoint-reference-project branch from 65418be to bee650a Compare April 12, 2025 20:05
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

♻️ Duplicate comments (5)
references/waitpoint-tokens/README.md (1)

25-26: Directory path inconsistency.

The README instructs users to navigate to "references/trigger-flow", but based on the PR and file structure, the project appears to be in "references/waitpoint-tokens".

-    cd references/trigger-flow
+    cd references/waitpoint-tokens
references/waitpoint-tokens/src/trigger/summarizeArticle.ts (1)

5-23: Add error handling to the summarization task.

While the task is well-structured, it lacks error handling for potential API failures. Additionally, there's no validation that the summary adheres to the 500-word limit specified in the prompt.

Consider implementing this improvement:

export const summarizeArticle = task({
  id: "summarize-article",
  maxDuration: 300,
  run: async (payload: { content: string }) => {
+   try {
      const { text } = await generateText({
        model: openai("gpt-4o-mini"),
        prompt: `
        Summarize the following article in a concise manner, focus on the main body of the article and generate a summary that sounds good in speech too.
        The result will be converted to speech for a news report. Make it no longer than 500 words.
        Content: ${payload.content}`,
      });
+     
+     const wordCount = text.split(/\s+/).length;
+     logger.info(`Article summary generated successfully. Word count: ${wordCount}`);
+
+     if (wordCount > 500) {
+       logger.warn(`Summary exceeds requested 500-word limit with ${wordCount} words.`);
+     }

      return {
        summary: text,
      };
+   } catch (error) {
+     logger.error("Failed to generate article summary", { error });
+     throw error;
+   }
  },
});
references/waitpoint-tokens/src/components/ReviewNode.tsx (1)

26-28: Add explicit error handling for missing run metadata.

The component extracts waitpointTokenId, audioSummaryUrl, and reviewStatus directly from currentRun?.metadata with type assertions, but doesn't handle cases where these values might be undefined or malformed. This could lead to runtime errors if the metadata structure changes.

Consider adding default values or more robust checks:

-  const waitpointTokenId = currentRun?.metadata?.waitpointTokenId as string | undefined;
-  const audioSummaryUrl = currentRun?.metadata?.audioSummaryUrl as string | undefined;
-  const reviewStatus = currentRun?.metadata?.reviewStatus as ReviewStatus | undefined;
+  const waitpointTokenId = (currentRun?.metadata?.waitpointTokenId as string) || undefined;
+  const audioSummaryUrl = (currentRun?.metadata?.audioSummaryUrl as string) || undefined;
+  const reviewStatus = (currentRun?.metadata?.reviewStatus as ReviewStatus) || undefined;
references/waitpoint-tokens/src/trigger/convertTextToSpeech.ts (2)

56-78: Include robust error handling in the convertTextToSpeech task.

The task needs comprehensive error handling to gracefully handle failures at any step of the process.

 export const convertTextToSpeech = task({
   id: "convert-text-to-speech",
   maxDuration: 300,
   run: async (payload: { text: string }) => {
-    const bucket = process.env.AWS_S3_BUCKET;
-    if (!bucket) {
-      throw new Error("AWS_S3_BUCKET is not set");
-    }
-
-    const audioStream = await createAudioStreamFromText(payload.text);
-    logger.info("Audio stream created");
-
-    const s3path = await uploadAudioStreamToS3(audioStream, bucket);
-    logger.info("Audio stream uploaded to S3");
-
-    const audioUrl = await generatePresignedUrl(s3path, bucket);
-    logger.info("Audio URL generated");
-
-    return {
-      audioUrl,
-    };
+    try {
+      // Validate input
+      if (!payload.text || payload.text.trim().length === 0) {
+        throw new Error("Text input is required");
+      }
+      
+      // Validate environment
+      validateEnvironment();
+      const bucket = process.env.AWS_S3_BUCKET!;
+      
+      // Step 1: Convert text to speech
+      logger.info("Starting text-to-speech conversion");
+      let audioStream: Buffer;
+      try {
+        audioStream = await createAudioStreamFromText(payload.text);
+        logger.info("Audio stream created successfully", { sizeBytes: audioStream.length });
+      } catch (error) {
+        logger.error("Text-to-speech conversion failed", { error });
+        throw new Error(`Text-to-speech conversion failed: ${error instanceof Error ? error.message : String(error)}`);
+      }
+      
+      // Step 2: Upload to S3
+      let s3path: string;
+      try {
+        s3path = await uploadAudioStreamToS3(audioStream, bucket);
+        logger.info("Audio stream uploaded to S3", { path: s3path });
+      } catch (error) {
+        logger.error("S3 upload failed", { error });
+        throw new Error(`Failed to upload audio to S3: ${error instanceof Error ? error.message : String(error)}`);
+      }
+      
+      // Step 3: Generate URL
+      let audioUrl: string;
+      try {
+        audioUrl = await generatePresignedUrl(s3path, bucket);
+        logger.info("Audio URL generated successfully");
+      } catch (error) {
+        logger.error("URL generation failed", { error });
+        throw new Error(`Failed to generate audio URL: ${error instanceof Error ? error.message : String(error)}`);
+      }
+      
+      return {
+        audioUrl,
+        s3path,
+        sizeBytes: audioStream.length,
+      };
+    } catch (error) {
+      const errorMessage = error instanceof Error ? error.message : String(error);
+      logger.error("Text-to-speech task failed", { error: errorMessage });
+      throw new Error(`Text-to-speech task failed: ${errorMessage}`);
+    }
   },
 });

12-30: 🛠️ Refactor suggestion

Add error handling to text-to-speech conversion.

The createAudioStreamFromText function doesn't include error handling for API failures.

 export const createAudioStreamFromText = async (text: string): Promise<Buffer> => {
-  const audioStream = await eleventLabs.textToSpeech.convertAsStream("M6N6IdXhi5YNZyZSDe7k", {
-    model_id: "eleven_multilingual_v2",
-    text,
-    output_format: "mp3_44100_128",
-    voice_settings: {
-      stability: 0,
-      similarity_boost: 1.0,
-      use_speaker_boost: true,
-      speed: 1.0,
-    },
-  });
-  const chunks: Buffer[] = [];
-  for await (const chunk of audioStream) {
-    chunks.push(chunk);
-  }
-  const content = Buffer.concat(chunks);
-  return content;
+  try {
+    if (!text || text.trim().length === 0) {
+      throw new Error("Text cannot be empty");
+    }
+    
+    const audioStream = await eleventLabs.textToSpeech.convertAsStream("M6N6IdXhi5YNZyZSDe7k", {
+      model_id: "eleven_multilingual_v2",
+      text,
+      output_format: "mp3_44100_128",
+      voice_settings: {
+        stability: 0,
+        similarity_boost: 1.0,
+        use_speaker_boost: true,
+        speed: 1.0,
+      },
+    });
+    
+    const chunks: Buffer[] = [];
+    for await (const chunk of audioStream) {
+      chunks.push(chunk);
+    }
+    
+    const content = Buffer.concat(chunks);
+    if (content.length === 0) {
+      throw new Error("Generated audio is empty");
+    }
+    
+    return content;
+  } catch (error) {
+    logger.error("Failed to create audio stream from text", { error });
+    throw new Error(`Text-to-speech conversion failed: ${error instanceof Error ? error.message : String(error)}`);
+  }
 };
🧹 Nitpick comments (18)
references/waitpoint-tokens/src/components/ActionNode.tsx (3)

11-22: Consider renaming or clarifying the type definition for better readability.

Although ActionNodeData is descriptive, you might find it clearer to define it as a separate interface or type for the data field itself, then refer to that interface in the generic parameter of Node<>. This can aid maintainability when more fields are added in the future.


28-34: Handle unexpected status values gracefully.

You’ve provided a default icon (Asterisk) for unrecognized statuses, which is great. However, consider logging a warning or providing more user-friendly feedback if an unknown status arises (e.g., from an unexpected backend state).


53-53: Avoid @ts-ignore if possible by resolving the type issue.

Directly ignoring TypeScript checks can hide potential errors. Investigate installing or configuring the type definitions for react-tippy or switch to an alternative tooltip library with proper typings.

Would you like help troubleshooting the type definitions or verifying an alternative approach?

references/waitpoint-tokens/src/components/InputNode.tsx (2)

31-37: Improve visibility of article URL display.

The article URL display appears as white text on a semi-transparent black background, positioned below the form. This might be difficult to read, especially for longer URLs.

Consider using a more accessible design with improved contrast and readability:

- <span className="block w-full bg-black/80 text-white px-2 py-1 rounded-sm text-xs font-mono overflow-hidden text-ellipsis whitespace-nowrap absolute top-[calc(100%+10px)] inset-x-0">
+ <span className="block w-full bg-indigo-100 text-indigo-800 border border-indigo-200 px-2 py-1 rounded-sm text-xs font-mono overflow-hidden text-ellipsis whitespace-nowrap mt-2 mb-2">

39-56: Add input validation feedback.

The form has a URL input with the required attribute and type="url", but doesn't provide custom validation feedback to users.

Consider enhancing the user experience with custom validation feedback:

+ const [inputValue, setInputValue] = useState("");
+ const [validationMessage, setValidationMessage] = useState("");
+
+ const validateUrl = (url: string) => {
+   try {
+     new URL(url);
+     setValidationMessage("");
+     return true;
+   } catch (e) {
+     setValidationMessage("Please enter a valid URL (e.g., https://example.com)");
+     return false;
+   }
+ };

// Then in the input:
  <input
    name="articleUrl"
    type="url"
    required
    className="grow border-1 border-zinc-300 rounded-sm p-2"
    placeholder="Enter article URL"
+   value={inputValue}
+   onChange={(e) => {
+     setInputValue(e.target.value);
+     if (e.target.value) validateUrl(e.target.value);
+   }}
+   onBlur={(e) => validateUrl(e.target.value)}
  />
+ {validationMessage && (
+   <div className="text-red-500 text-xs mt-1">{validationMessage}</div>
+ )}
references/waitpoint-tokens/README.md (2)

37-37: Improve clarity on environment variable setup.

The README mentions creating an .env file by copying .env.example but doesn't provide a specific command for this, which could be helpful for users.

-4. Create an `.env` file by copying [.env.example](.env.example) and fill in the required environment variables. The example file includes a description for each variable.
+4. Create an `.env` file by copying [.env.example](.env.example):
+
+    ```bash
+    cp .env.example .env
+    ```
+
+    Then fill in the required environment variables. The example file includes a description for each variable.

53-53: Correct grammar in instructions.

The word "checkout" is being used as a verb, but it should be "check out" when used in this context.

-    Open [http://localhost:3000](http://localhost:3000) on your browser to checkout the workflow.
+    Open [http://localhost:3000](http://localhost:3000) on your browser to check out the workflow.
🧰 Tools
🪛 LanguageTool

[grammar] ~53-~53: The word “checkout” is a noun. The verb is spelled with a white space.
Context: ...tp://localhost:3000) on your browser to checkout the workflow. ## Learn More To learn ...

(NOUN_VERB_CONFUSION)

references/waitpoint-tokens/src/trigger/summarizeArticle.ts (1)

10-15: Consider model fallback mechanism for stability.

The task uses a single model (gpt-4o-mini) without a fallback strategy. If OpenAI API is temporarily unavailable or the model is experiencing issues, the entire task will fail.

Implement a fallback mechanism to improve resilience:

-    const { text } = await generateText({
-      model: openai("gpt-4o-mini"),
-      prompt: `
+    const generateSummary = async (modelName: string) => {
+      logger.info(`Attempting to generate summary using ${modelName}`);
+      return await generateText({
+        model: openai(modelName),
+        prompt: `
        Summarize the following article in a concise manner, focus on the main body of the article and generate a summary that sounds good in speech too.
        The result will be converted to speech for a news report. Make it no longer than 500 words.
        Content: ${payload.content}`,
+      });
+    };
+
+    let text;
+    try {
+      const result = await generateSummary("gpt-4o-mini");
+      text = result.text;
+    } catch (error) {
+      logger.warn("Failed with primary model, falling back to alternative model", { error });
+      const result = await generateSummary("gpt-3.5-turbo");
+      text = result.text;
+    }
references/waitpoint-tokens/.env.example (3)

3-5: Add example values or clarify Trigger.dev API endpoints.

While the API key variable is clear, the API URLs don't have example values or guidance on what to use for local development vs. production.

Add comments to clarify the expected values:

# Trigger.dev secret key
# https://trigger.dev/docs/apikeys
-TRIGGER_SECRET_KEY=
+TRIGGER_SECRET_KEY= # e.g., tr_dev_1234abcd...
-TRIGGER_API_URL=
+TRIGGER_API_URL= # For local development: http://localhost:3030/api/v1
-NEXT_PUBLIC_TRIGGER_API_URL=
+NEXT_PUBLIC_TRIGGER_API_URL= # For local development: http://localhost:3030/api/v1

15-23: Improve S3 configuration with more detailed guidance.

The S3 configuration section could use more specific examples, especially since it mentions compatibility with non-AWS S3 providers.

Enhance the comments with more specific guidance:

# Credentials for writing audio streams to an S3-compatible bucket.
# Does not necessarily need to be AWS S3 bucket, as long as it is compatible to the S3 SDK, e.g., https://www.tigrisdata.com/
# https://docs.aws.amazon.com/sdkref/latest/guide/environment-variables.html
-AWS_ACCESS_KEY_ID=
+AWS_ACCESS_KEY_ID= # Your access key ID
-AWS_SECRET_ACCESS_KEY=
+AWS_SECRET_ACCESS_KEY= # Your secret access key
-AWS_ENDPOINT_URL_S3=
+AWS_ENDPOINT_URL_S3= # For AWS: leave blank. For others: https://your-s3-provider.com
-AWS_ENDPOINT_URL_IAM=
+AWS_ENDPOINT_URL_IAM= # For AWS: leave blank. For others: https://your-iam-endpoint.com
-AWS_S3_BUCKET=
+AWS_S3_BUCKET= # Name of your bucket (e.g., my-app-media)
-AWS_REGION=
+AWS_REGION= # e.g., us-east-1

1-27: Consider adding validation for required vs. optional environment variables.

The .env.example file doesn't distinguish between required and optional variables, which could confuse users about which ones are necessary to run the application.

Consider adding annotations to clarify which variables are required:

# Trigger.dev secret key
# https://trigger.dev/docs/apikeys
-TRIGGER_SECRET_KEY=
+TRIGGER_SECRET_KEY= # [REQUIRED]
-TRIGGER_API_URL=
+TRIGGER_API_URL= # [REQUIRED]
-NEXT_PUBLIC_TRIGGER_API_URL=
+NEXT_PUBLIC_TRIGGER_API_URL= # [REQUIRED]

# OpenAI API key for generating article summaries.
# https://platform.openai.com
-OPENAI_API_KEY=
+OPENAI_API_KEY= # [REQUIRED]

# ElevenLabs API key for converting text to speech.
# https://elevenlabs.io/docs/quickstart#create-an-api-key
-ELEVENLABS_API_KEY=
+ELEVENLABS_API_KEY= # [REQUIRED for text-to-speech features]

# Credentials for writing audio streams to an S3-compatible bucket.
# Does not necessarily need to be AWS S3 bucket, as long as it is compatible to the S3 SDK, e.g., https://www.tigrisdata.com/
# https://docs.aws.amazon.com/sdkref/latest/guide/environment-variables.html
-AWS_ACCESS_KEY_ID=
+AWS_ACCESS_KEY_ID= # [REQUIRED for audio storage]
-AWS_SECRET_ACCESS_KEY=
+AWS_SECRET_ACCESS_KEY= # [REQUIRED for audio storage]
-AWS_ENDPOINT_URL_S3=
+AWS_ENDPOINT_URL_S3= # [OPTIONAL - only for non-AWS S3 providers]
-AWS_ENDPOINT_URL_IAM=
+AWS_ENDPOINT_URL_IAM= # [OPTIONAL - only for non-AWS S3 providers]
-AWS_S3_BUCKET=
+AWS_S3_BUCKET= # [REQUIRED for audio storage]
-AWS_REGION=
+AWS_REGION= # [REQUIRED for audio storage]

# Slack webhook URL for sending messages to a Slack channel.
# Follow the guide in https://api.slack.com/messaging/webhooks to generate a webhook URL.
-SLACK_WEBHOOK_URL=
+SLACK_WEBHOOK_URL= # [OPTIONAL - for Slack notifications]
references/waitpoint-tokens/src/components/Flow.tsx (1)

247-248: Consider enhancing edge styling based on node status

As noted in your comment, this is a simplified approach to edge animation. You might want to consider differentiating the edge styling further based on different node statuses (e.g., FAILED, COMPLETED, RUNNING).

- animated: sourceNode?.data?.trigger.currentRun?.status !== "COMPLETED",
+ animated: sourceNode?.data?.trigger.currentRun?.status === "RUNNING",
+ style: {
+   stroke: sourceNode?.data?.trigger.currentRun?.status === "FAILED" ? 
+     "red" : sourceNode?.data?.trigger.currentRun?.status === "RUNNING" ? 
+     "#3288f7" : "#ccc",
+   strokeWidth: sourceNode?.data?.trigger.currentRun?.status === "RUNNING" ? 
+     2 : 1,
+ },
references/waitpoint-tokens/src/trigger/articleWorkflow.ts (1)

60-88: Consider adding handling for rejected summaries

Currently, when a summary is rejected, no further actions are taken. It might be valuable to send a notification or log the rejection reason for better user experience and workflow visibility.

if (reviewSummaryResult.output.approved) {
  // Existing code for approved summaries
} else {
+  // Handle rejected summary
+  const { runs: [sendRejectionNotificationRun] } = await batch.triggerByTaskAndWait([
+    {
+      task: sendSlackNotification,
+      payload: {
+        message: `Article summary was rejected by ${reviewSummaryResult.output.rejectedBy} at ${reviewSummaryResult.output.rejectedAt}. Reason: ${reviewSummaryResult.output.reason}`,
+      },
+      options: { tags: ctx.run.tags },
+    }
+  ]);
+
+  if (!sendRejectionNotificationRun.ok) {
+    throw new Error("Failed to send rejection notification");
+  }
}
references/waitpoint-tokens/src/app/actions.ts (1)

7-15: Consider using crypto for more secure random string generation

The current implementation of randomStr uses Math.random() which isn't cryptographically secure. For token generation, consider using the Node.js crypto module.

-const randomStr = (length: number) =>
-  [...Array(length)]
-    .map(
-      () =>
-        "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"[
-          Math.floor(Math.random() * 62)
-        ]
-    )
-    .join("");
+import crypto from 'crypto';
+
+const randomStr = (length: number) => {
+  return crypto
+    .randomBytes(Math.ceil(length * 3/4))
+    .toString('base64')
+    .replace(/[+/=]/g, '')
+    .slice(0, length);
+};
references/waitpoint-tokens/src/components/ReviewNode.tsx (2)

46-48: Improve audio player accessibility.

The audio element lacks proper accessibility attributes which could make it difficult for screen reader users to interact with.

-        <audio controls className="h-[30px] w-[170px] mb-1">
+        <audio 
+          controls 
+          className="h-[30px] w-[170px] mb-1"
+          aria-label="Article summary audio"
+          preload="metadata">
           {audioSummaryUrl && <source src={audioSummaryUrl} />}
         </audio>

64-89: Consider extracting button logic to reduce duplication.

Both approve and reject buttons share very similar logic and styling with only minor differences.

You could extract a common button component to reduce duplication:

-        {(reviewStatus === undefined || reviewStatus === "pending") && (
-          <div className="flex items-center gap-2">
-            <button
-              className="bg-green-700 hover:bg-green-600 disabled:opacity-50 transition-colors text-white px-2 py-1 rounded-sm text-xs font-semibold"
-              onClick={() =>
-                startReviewActionTransition(() => {
-                  waitpointTokenId && approveArticleSummary(waitpointTokenId);
-                })
-              }
-              disabled={!waitpointTokenId || reviewStatus !== "pending" || isReviewActionPending}
-            >
-              Approve
-            </button>
-            <button
-              className="bg-red-700 hover:bg-red-600 disabled:opacity-50 transition-colors text-white px-2 py-1 rounded-sm text-xs font-semibold"
-              onClick={() => {
-                startReviewActionTransition(() => {
-                  waitpointTokenId && rejectArticleSummary(waitpointTokenId);
-                });
-              }}
-              disabled={!waitpointTokenId || reviewStatus !== "pending" || isReviewActionPending}
-            >
-              Reject
-            </button>
-          </div>
+        {(reviewStatus === undefined || reviewStatus === "pending") && (
+          <div className="flex items-center gap-2">
+            <ActionButton
+              label="Approve"
+              bgColor="green"
+              onClick={() => waitpointTokenId && approveArticleSummary(waitpointTokenId)}
+              disabled={!waitpointTokenId || reviewStatus !== "pending" || isReviewActionPending}
+              startTransition={startReviewActionTransition}
+            />
+            <ActionButton
+              label="Reject"
+              bgColor="red"
+              onClick={() => waitpointTokenId && rejectArticleSummary(waitpointTokenId)}
+              disabled={!waitpointTokenId || reviewStatus !== "pending" || isReviewActionPending}
+              startTransition={startReviewActionTransition}
+            />
+          </div>

In a separate component:

type ActionButtonProps = {
  label: string;
  bgColor: "red" | "green";
  onClick: () => void;
  disabled: boolean;
  startTransition: (callback: () => void) => void;
};

const ActionButton = ({ label, bgColor, onClick, disabled, startTransition }: ActionButtonProps) => (
  <button
    className={`bg-${bgColor}-700 hover:bg-${bgColor}-600 disabled:opacity-50 transition-colors text-white px-2 py-1 rounded-sm text-xs font-semibold`}
    onClick={() => startTransition(onClick)}
    disabled={disabled}
  >
    {label}
  </button>
);
references/waitpoint-tokens/src/trigger/convertTextToSpeech.ts (2)

32-41: Make URL expiration configurable and add error handling.

The presigned URL expiration is hardcoded and the function lacks proper error handling.

-export const generatePresignedUrl = async (objectKey: string, bucket: string) => {
-  const getObjectParams = {
-    Bucket: bucket,
-    Key: objectKey,
-    Expires: 3600,
-  };
-  const command = new GetObjectCommand(getObjectParams);
-  const url = await getSignedUrl(s3, command, { expiresIn: 3600 });
-  return url;
+export const generatePresignedUrl = async (objectKey: string, bucket: string, expiresInSeconds = 3600) => {
+  try {
+    if (!objectKey || !bucket) {
+      throw new Error("Object key and bucket are required");
+    }
+    
+    const getObjectParams = {
+      Bucket: bucket,
+      Key: objectKey,
+    };
+    
+    const command = new GetObjectCommand(getObjectParams);
+    const url = await getSignedUrl(s3, command, { expiresIn: expiresInSeconds });
+    
+    if (!url) {
+      throw new Error("Failed to generate presigned URL");
+    }
+    
+    return url;
+  } catch (error) {
+    logger.error("Failed to generate presigned URL", { bucket, objectKey, error });
+    throw new Error(`URL generation failed: ${error instanceof Error ? error.message : String(error)}`);
+  }
 };

7-8: Externalize configuration with environment variables.

The ElevenLabs voice ID and model ID are hard-coded, which reduces flexibility.

-const eleventLabs = new ElevenLabsClient();
+// Use environment variables for configuration
+const ELEVEN_LABS_VOICE_ID = process.env.ELEVEN_LABS_VOICE_ID || 'M6N6IdXhi5YNZyZSDe7k';
+const ELEVEN_LABS_MODEL_ID = process.env.ELEVEN_LABS_MODEL_ID || 'eleven_multilingual_v2';
+
+const eleventLabs = new ElevenLabsClient({
+  apiKey: process.env.ELEVEN_LABS_API_KEY,
+});

Then update line 13 to use these variables:

-  const audioStream = await eleventLabs.textToSpeech.convertAsStream("M6N6IdXhi5YNZyZSDe7k", {
+  const audioStream = await eleventLabs.textToSpeech.convertAsStream(ELEVEN_LABS_VOICE_ID, {
-    model_id: "eleven_multilingual_v2",
+    model_id: ELEVEN_LABS_MODEL_ID,
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 65418be and bee650a.

⛔ Files ignored due to path filters (3)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
  • references/waitpoint-tokens/src/app/favicon.ico is excluded by !**/*.ico
  • references/waitpoint-tokens/src/app/logo.svg is excluded by !**/*.svg
📒 Files selected for processing (24)
  • references/waitpoint-tokens/.env.example (1 hunks)
  • references/waitpoint-tokens/.gitignore (1 hunks)
  • references/waitpoint-tokens/README.md (1 hunks)
  • references/waitpoint-tokens/next.config.ts (1 hunks)
  • references/waitpoint-tokens/package.json (1 hunks)
  • references/waitpoint-tokens/postcss.config.mjs (1 hunks)
  • references/waitpoint-tokens/src/app/actions.ts (1 hunks)
  • references/waitpoint-tokens/src/app/globals.css (1 hunks)
  • references/waitpoint-tokens/src/app/layout.tsx (1 hunks)
  • references/waitpoint-tokens/src/app/page.tsx (1 hunks)
  • references/waitpoint-tokens/src/components/ActionNode.tsx (1 hunks)
  • references/waitpoint-tokens/src/components/Flow.tsx (1 hunks)
  • references/waitpoint-tokens/src/components/InputNode.tsx (1 hunks)
  • references/waitpoint-tokens/src/components/ReviewNode.tsx (1 hunks)
  • references/waitpoint-tokens/src/lib/cn.ts (1 hunks)
  • references/waitpoint-tokens/src/trigger/articleWorkflow.ts (1 hunks)
  • references/waitpoint-tokens/src/trigger/convertTextToSpeech.ts (1 hunks)
  • references/waitpoint-tokens/src/trigger/publishSummary.ts (1 hunks)
  • references/waitpoint-tokens/src/trigger/reviewSummary.ts (1 hunks)
  • references/waitpoint-tokens/src/trigger/scrapeSite.ts (1 hunks)
  • references/waitpoint-tokens/src/trigger/sendSlackNotification.ts (1 hunks)
  • references/waitpoint-tokens/src/trigger/summarizeArticle.ts (1 hunks)
  • references/waitpoint-tokens/trigger.config.ts (1 hunks)
  • references/waitpoint-tokens/tsconfig.json (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (13)
  • references/waitpoint-tokens/postcss.config.mjs
  • references/waitpoint-tokens/next.config.ts
  • references/waitpoint-tokens/src/trigger/scrapeSite.ts
  • references/waitpoint-tokens/trigger.config.ts
  • references/waitpoint-tokens/src/app/globals.css
  • references/waitpoint-tokens/src/lib/cn.ts
  • references/waitpoint-tokens/.gitignore
  • references/waitpoint-tokens/src/app/layout.tsx
  • references/waitpoint-tokens/src/app/page.tsx
  • references/waitpoint-tokens/src/trigger/sendSlackNotification.ts
  • references/waitpoint-tokens/src/trigger/publishSummary.ts
  • references/waitpoint-tokens/tsconfig.json
  • references/waitpoint-tokens/src/trigger/reviewSummary.ts
🧰 Additional context used
🧬 Code Graph Analysis (4)
references/waitpoint-tokens/src/app/actions.ts (2)
references/waitpoint-tokens/src/trigger/articleWorkflow.ts (1)
  • articleWorkflow (9-90)
references/waitpoint-tokens/src/trigger/reviewSummary.ts (1)
  • ReviewPayload (3-14)
references/waitpoint-tokens/src/trigger/articleWorkflow.ts (5)
references/waitpoint-tokens/src/trigger/scrapeSite.ts (1)
  • scrape (4-18)
packages/core/src/v3/taskContext/index.ts (1)
  • ctx (26-28)
references/waitpoint-tokens/src/trigger/summarizeArticle.ts (1)
  • summarizeArticle (5-23)
references/waitpoint-tokens/src/trigger/convertTextToSpeech.ts (1)
  • convertTextToSpeech (56-78)
references/waitpoint-tokens/src/trigger/reviewSummary.ts (1)
  • reviewSummary (17-42)
references/waitpoint-tokens/src/components/ActionNode.tsx (2)
references/waitpoint-tokens/src/lib/cn.ts (1)
  • cn (4-6)
apps/webapp/app/components/primitives/Tooltip.tsx (1)
  • Tooltip (118-118)
references/waitpoint-tokens/src/components/ReviewNode.tsx (2)
references/waitpoint-tokens/src/trigger/reviewSummary.ts (1)
  • ReviewStatus (15-15)
references/waitpoint-tokens/src/app/actions.ts (2)
  • approveArticleSummary (57-66)
  • rejectArticleSummary (68-78)
🪛 Biome (1.9.4)
references/waitpoint-tokens/src/app/actions.ts

[error] 27-48: This array contains an empty slot.

Unsafe fix: Replace hole with undefined

(lint/suspicious/noSparseArray)

🪛 LanguageTool
references/waitpoint-tokens/README.md

[grammar] ~53-~53: The word “checkout” is a noun. The verb is spelled with a white space.
Context: ...tp://localhost:3000) on your browser to checkout the workflow. ## Learn More To learn ...

(NOUN_VERB_CONFUSION)

⏰ Context from checks skipped due to timeout of 90000ms (5)
  • GitHub Check: e2e / 🧪 CLI v3 tests (windows-latest - pnpm)
  • GitHub Check: e2e / 🧪 CLI v3 tests (windows-latest - npm)
  • GitHub Check: e2e / 🧪 CLI v3 tests (ubuntu-latest - pnpm)
  • GitHub Check: e2e / 🧪 CLI v3 tests (ubuntu-latest - npm)
  • GitHub Check: typecheck / typecheck
🔇 Additional comments (6)
references/waitpoint-tokens/src/components/ActionNode.tsx (1)

72-75: Implementation of handles for terminal vs. non-terminal actions looks good.

The conditional logic for rendering the source handle aligns well with isTerminalAction. This ensures no outgoing edges can connect to terminal nodes. Nicely done!

references/waitpoint-tokens/src/components/Flow.tsx (2)

1-267: Well-implemented ReactFlow component for visualizing workflow

The Flow component is well-structured, using ReactFlow to create a visual representation of the waitpoint workflow. It correctly defines nodes and edges that match the article workflow structure defined elsewhere in the codebase.

I especially appreciate:

  1. The type safety with task identifiers being validated against imported types
  2. The clear separation of concerns in the useEffect hooks
  3. The detailed comments explaining the implementation choices

167-171: Make sure NEXT_PUBLIC_TRIGGER_API_URL is configured in .env

The component relies on this environment variable for connecting to the Trigger.dev API. Ensure this is documented in the README or .env.example file to help other developers set up the project.

references/waitpoint-tokens/src/trigger/articleWorkflow.ts (1)

1-90: Well-structured workflow with proper error handling

The article workflow is well-organized and implements a clear sequence of steps with proper error handling at each stage. The use of triggerAndWait and batch.triggerByTaskAndWait is appropriate for this orchestration pattern.

references/waitpoint-tokens/package.json (2)

24-26:

✅ Verification successful

Verify React 19 compatibility

React 19 is not yet officially released for production use. Make sure your code is compatible with this version or consider downgrading to a stable version (React 18).


🌐 Web query:

Is React 19 released yet? What's the latest stable version of React?

💡 Result:

React 19 has been officially released and is now stable as of December 5, 2024 [4][6][9]. The latest stable version is 19.1.0, released on March 28, 2025 [2]. This marks a significant update from React 18, which was released in March 2022.

Key Details:

  • Current stable version: 19.1.0 (as of March 28, 2025) [2]
  • Release date: December 5, 2024 [4][6]
  • Major improvements:
    • Actions: Simplified async state management for forms and mutations [4][6]
    • React Compiler: Automatic component optimization [3]
    • Server Components: Stable implementation for server-side rendering [5][6]
    • Enhanced Hooks: Improved useActionState, useOptimistic, and use for resource handling [6][8]

Upgrade Information:

  • Use npm install react@19 react-dom@19 to install [3][6]
  • Breaking changes include removal of legacy APIs like propTypes and string refs [9]
  • React Native 0.78 (February 2025) added React 19 support [8]

This release focuses on performance optimizations, developer experience improvements, and better handling of asynchronous operations.

Citations:


React 19 is now stable. The latest stable version is 19.1.0, and using "react": "^19.0.0" (and similarly for "react-dom") will allow updates to that release series. Please verify that the code functions correctly with React 19 features and any breaking changes, but there's no need to downgrade to React 18.


14-15:

❓ Verification inconclusive

Verify AWS SDK version numbers

The AWS SDK versions (3.777.0) seem unusually high. Please verify these are the correct versions you intend to use.


🌐 Web query:

What is the latest version of @aws-sdk/client-s3?

💡 Result:

The latest version of @aws-sdk/client-s3 is v3.788.0, released on April 11, 2025[1]. This version includes updates such as:

  • New features:
    • Added deletion protection support for Amazon Verified Permissions policy stores[1]
    • Enabled dual-stack endpoints for AWS Marketplace Metering Service in CN regions[1]

Key details about the SDK's release model:

  • The AWS SDK for JavaScript v3 follows a modular architecture with frequent updates[2][4]
  • It replaced v2 in December 2020 and is the recommended version for new projects[4][8]
  • Version 3.x receives regular updates while v2 entered maintenance mode in September 2024[4][8]

For installation:

npm install @aws-sdk/[email protected]

Developers should monitor the GitHub releases page for real-time version updates[1].

Citations:


Action: Verify AWS SDK Dependency Versions

  • The file references/waitpoint-tokens/package.json currently specifies:
        "@aws-sdk/client-s3": "^3.777.0",
        "@aws-sdk/s3-request-presigner": "^3.777.0",
  • A recent web query confirms that the latest version of @aws-sdk/client-s3 is v3.788.0 (released on April 11, 2025).
  • Please verify whether locking the dependency at ^3.777.0 is intentional (e.g., for compatibility reasons) or if you’d like to update it to ^3.788.0 to leverage the latest features and fixes.

Comment on lines +3 to +5
import React, { useActionState, useEffect } from "react";
import { Handle, Position, NodeProps, Node, useReactFlow } from "@xyflow/react";
import { triggerArticleWorkflow } from "@/app/actions";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

The component relies on triggerArticleWorkflow but lacks error handling.

The component imports triggerArticleWorkflow but doesn't handle potential errors that might occur during the action execution. While the pending state is handled correctly, there's no error state management.

Consider adding error handling to provide feedback to users when the form submission fails:

- const [state, formAction, pending] = useActionState(triggerArticleWorkflow, undefined);
+ const [state, formAction, pending, error] = useActionState(triggerArticleWorkflow, undefined);

// Then in the JSX:
+ {error && (
+   <div className="text-red-500 text-xs mt-2">
+     Failed to start workflow: {error.message || "Unknown error"}
+   </div>
+ )}

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +57 to +66
export async function approveArticleSummary(tokenId: string) {
await wait.completeToken<ReviewPayload>(
{ id: tokenId },
{
approved: true,
approvedAt: new Date(),
approvedBy: "Alice",
}
);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Make approver name a parameter instead of hardcoding

Currently, the approver name is hardcoded as "Alice". Consider making it a parameter to allow different users to approve summaries.

-export async function approveArticleSummary(tokenId: string) {
+export async function approveArticleSummary(tokenId: string, approverName: string = "Alice") {
  await wait.completeToken<ReviewPayload>(
    { id: tokenId },
    {
      approved: true,
      approvedAt: new Date(),
-      approvedBy: "Alice",
+      approvedBy: approverName,
    }
  );
}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export async function approveArticleSummary(tokenId: string) {
await wait.completeToken<ReviewPayload>(
{ id: tokenId },
{
approved: true,
approvedAt: new Date(),
approvedBy: "Alice",
}
);
}
export async function approveArticleSummary(tokenId: string, approverName: string = "Alice") {
await wait.completeToken<ReviewPayload>(
{ id: tokenId },
{
approved: true,
approvedAt: new Date(),
approvedBy: approverName,
}
);
}

Comment on lines +68 to +78
export async function rejectArticleSummary(tokenId: string) {
await wait.completeToken<ReviewPayload>(
{ id: tokenId },
{
approved: false,
rejectedAt: new Date(),
rejectedBy: "Alice",
reason: "It's no good",
}
);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Make rejection parameters configurable

Similar to the approve function, the rejection function has hardcoded values for the rejector name and reason. Make these configurable parameters.

-export async function rejectArticleSummary(tokenId: string) {
+export async function rejectArticleSummary(
+  tokenId: string, 
+  rejectorName: string = "Alice", 
+  reason: string = "It's no good"
+) {
  await wait.completeToken<ReviewPayload>(
    { id: tokenId },
    {
      approved: false,
      rejectedAt: new Date(),
-      rejectedBy: "Alice",
-      reason: "It's no good",
+      rejectedBy: rejectorName,
+      reason: reason,
    }
  );
}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export async function rejectArticleSummary(tokenId: string) {
await wait.completeToken<ReviewPayload>(
{ id: tokenId },
{
approved: false,
rejectedAt: new Date(),
rejectedBy: "Alice",
reason: "It's no good",
}
);
}
export async function rejectArticleSummary(
tokenId: string,
rejectorName: string = "Alice",
reason: string = "It's no good"
) {
await wait.completeToken<ReviewPayload>(
{ id: tokenId },
{
approved: false,
rejectedAt: new Date(),
rejectedBy: rejectorName,
reason: reason,
}
);
}

Comment on lines +27 to +48
const [workflowPublicAccessToken] = await Promise.all([
// We generate a public access token to use the Trigger.dev realtime API and listen to changes in task runs using react hooks.
// This token has access to all runs tagged with the unique workflow tag.
auth.createPublicToken({
scopes: {
read: {
tags: [workflowTag],
},
},
}),
,
tasks.trigger<typeof articleWorkflow>(
"article-workflow",
{
articleUrl,
approvalWaitpointTokenId: reviewWaitpointToken.id,
},
{
tags: [workflowTag],
}
),
]);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix empty slot in Promise.all array

There's an empty slot in the Promise.all array (line 38) which could cause unexpected behavior. Remove the extra comma.

  const [workflowPublicAccessToken] = await Promise.all([
    // We generate a public access token to use the Trigger.dev realtime API and listen to changes in task runs using react hooks.
    // This token has access to all runs tagged with the unique workflow tag.
    auth.createPublicToken({
      scopes: {
        read: {
          tags: [workflowTag],
        },
      },
    }),
-    ,
    tasks.trigger<typeof articleWorkflow>(
      "article-workflow",
      {
        articleUrl,
        approvalWaitpointTokenId: reviewWaitpointToken.id,
      },
      {
        tags: [workflowTag],
      }
    ),
  ]);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const [workflowPublicAccessToken] = await Promise.all([
// We generate a public access token to use the Trigger.dev realtime API and listen to changes in task runs using react hooks.
// This token has access to all runs tagged with the unique workflow tag.
auth.createPublicToken({
scopes: {
read: {
tags: [workflowTag],
},
},
}),
,
tasks.trigger<typeof articleWorkflow>(
"article-workflow",
{
articleUrl,
approvalWaitpointTokenId: reviewWaitpointToken.id,
},
{
tags: [workflowTag],
}
),
]);
const [workflowPublicAccessToken] = await Promise.all([
// We generate a public access token to use the Trigger.dev realtime API and listen to changes in task runs using react hooks.
// This token has access to all runs tagged with the unique workflow tag.
auth.createPublicToken({
scopes: {
read: {
tags: [workflowTag],
},
},
}),
tasks.trigger<typeof articleWorkflow>(
"article-workflow",
{
articleUrl,
approvalWaitpointTokenId: reviewWaitpointToken.id,
},
{
tags: [workflowTag],
}
),
]);
🧰 Tools
🪛 Biome (1.9.4)

[error] 27-48: This array contains an empty slot.

Unsafe fix: Replace hole with undefined

(lint/suspicious/noSparseArray)

Comment on lines +17 to +55
export async function triggerArticleWorkflow(prevState: any, formData: FormData) {
const articleUrl = formData.get("articleUrl") as string;
const workflowTag = `reactflow_${randomStr(20)}`;

const reviewWaitpointToken = await wait.createToken({
tags: [workflowTag],
timeout: "1h",
idempotencyKey: `review-summary-${workflowTag}`,
});

const [workflowPublicAccessToken] = await Promise.all([
// We generate a public access token to use the Trigger.dev realtime API and listen to changes in task runs using react hooks.
// This token has access to all runs tagged with the unique workflow tag.
auth.createPublicToken({
scopes: {
read: {
tags: [workflowTag],
},
},
}),
,
tasks.trigger<typeof articleWorkflow>(
"article-workflow",
{
articleUrl,
approvalWaitpointTokenId: reviewWaitpointToken.id,
},
{
tags: [workflowTag],
}
),
]);

return {
articleUrl,
workflowTag,
workflowPublicAccessToken,
};
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add validation for article URL

The function doesn't validate the article URL before triggering the workflow. Consider adding basic validation to ensure it's a valid URL.

export async function triggerArticleWorkflow(prevState: any, formData: FormData) {
  const articleUrl = formData.get("articleUrl") as string;
+  
+  // Validate URL
+  try {
+    new URL(articleUrl);
+  } catch (error) {
+    return {
+      error: "Please enter a valid URL",
+    };
+  }
  
  const workflowTag = `reactflow_${randomStr(20)}`;
  
  // Rest of function remains the same
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export async function triggerArticleWorkflow(prevState: any, formData: FormData) {
const articleUrl = formData.get("articleUrl") as string;
const workflowTag = `reactflow_${randomStr(20)}`;
const reviewWaitpointToken = await wait.createToken({
tags: [workflowTag],
timeout: "1h",
idempotencyKey: `review-summary-${workflowTag}`,
});
const [workflowPublicAccessToken] = await Promise.all([
// We generate a public access token to use the Trigger.dev realtime API and listen to changes in task runs using react hooks.
// This token has access to all runs tagged with the unique workflow tag.
auth.createPublicToken({
scopes: {
read: {
tags: [workflowTag],
},
},
}),
,
tasks.trigger<typeof articleWorkflow>(
"article-workflow",
{
articleUrl,
approvalWaitpointTokenId: reviewWaitpointToken.id,
},
{
tags: [workflowTag],
}
),
]);
return {
articleUrl,
workflowTag,
workflowPublicAccessToken,
};
}
export async function triggerArticleWorkflow(prevState: any, formData: FormData) {
const articleUrl = formData.get("articleUrl") as string;
// Validate URL
try {
new URL(articleUrl);
} catch (error) {
return {
error: "Please enter a valid URL",
};
}
const workflowTag = `reactflow_${randomStr(20)}`;
const reviewWaitpointToken = await wait.createToken({
tags: [workflowTag],
timeout: "1h",
idempotencyKey: `review-summary-${workflowTag}`,
});
const [workflowPublicAccessToken] = await Promise.all([
// We generate a public access token to use the Trigger.dev realtime API and listen to changes in task runs using react hooks.
// This token has access to all runs tagged with the unique workflow tag.
auth.createPublicToken({
scopes: {
read: {
tags: [workflowTag],
},
},
}),
,
tasks.trigger<typeof articleWorkflow>(
"article-workflow",
{
articleUrl,
approvalWaitpointTokenId: reviewWaitpointToken.id,
},
{
tags: [workflowTag],
}
),
]);
return {
articleUrl,
workflowTag,
workflowPublicAccessToken,
};
}
🧰 Tools
🪛 Biome (1.9.4)

[error] 27-48: This array contains an empty slot.

Unsafe fix: Replace hole with undefined

(lint/suspicious/noSparseArray)

"tailwind-merge": "^3.2.0"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add PostCSS as a direct dependency

You're using @tailwindcss/postcss but don't have PostCSS listed as a direct dependency. Consider adding it to ensure proper build compatibility.

  "devDependencies": {
    "@tailwindcss/postcss": "^4",
    "@trigger.dev/build": "workspace:*",
    "@types/html-to-text": "^9.0.4",
    "@types/node": "^20",
    "@types/react": "^19",
    "@types/react-dom": "^19",
+   "postcss": "^8",
    "tailwindcss": "^4",
    "trigger.dev": "workspace:*",
    "typescript": "^5"
  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"@tailwindcss/postcss": "^4",
"devDependencies": {
"@tailwindcss/postcss": "^4",
"@trigger.dev/build": "workspace:*",
"@types/html-to-text": "^9.0.4",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"postcss": "^8",
"tailwindcss": "^4",
"trigger.dev": "workspace:*",
"typescript": "^5"
}

Comment on lines +7 to +10
const eleventLabs = new ElevenLabsClient();
const s3 = new S3Client({
forcePathStyle: false,
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Improve client initialization with proper error handling.

The ElevenLabs and S3 clients are initialized without any error handling or validation of environment variables/credentials.

-const eleventLabs = new ElevenLabsClient();
-const s3 = new S3Client({
-  forcePathStyle: false,
-});
+const eleventLabs = new ElevenLabsClient({
+  apiKey: process.env.ELEVEN_LABS_API_KEY,
+});
+
+// Initialize S3 client with proper error handling
+const s3 = new S3Client({
+  forcePathStyle: false,
+  region: process.env.AWS_REGION,
+  credentials: {
+    accessKeyId: process.env.AWS_ACCESS_KEY_ID || '',
+    secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY || '',
+  },
+});
+
+// Validate critical environment variables early
+function validateEnvironment() {
+  const requiredVars = [
+    'ELEVEN_LABS_API_KEY', 
+    'AWS_S3_BUCKET', 
+    'AWS_ACCESS_KEY_ID', 
+    'AWS_SECRET_ACCESS_KEY'
+  ];
+  
+  const missing = requiredVars.filter(varName => !process.env[varName]);
+  if (missing.length > 0) {
+    throw new Error(`Missing required environment variables: ${missing.join(', ')}`);
+  }
+}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const eleventLabs = new ElevenLabsClient();
const s3 = new S3Client({
forcePathStyle: false,
});
const eleventLabs = new ElevenLabsClient({
apiKey: process.env.ELEVEN_LABS_API_KEY,
});
// Initialize S3 client with proper error handling
const s3 = new S3Client({
forcePathStyle: false,
region: process.env.AWS_REGION,
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID || '',
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY || '',
},
});
// Validate critical environment variables early
function validateEnvironment() {
const requiredVars = [
'ELEVEN_LABS_API_KEY',
'AWS_S3_BUCKET',
'AWS_ACCESS_KEY_ID',
'AWS_SECRET_ACCESS_KEY'
];
const missing = requiredVars.filter(varName => !process.env[varName]);
if (missing.length > 0) {
throw new Error(`Missing required environment variables: ${missing.join(', ')}`);
}
}

Comment on lines +43 to +54
export const uploadAudioStreamToS3 = async (audioStream: Buffer, bucket: string) => {
const remotePath = `${randomUUID()}.mp3`;
await s3.send(
new PutObjectCommand({
Bucket: bucket,
Key: remotePath,
Body: audioStream,
ContentType: "audio/mpeg",
})
);
return remotePath;
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add validation and error handling to S3 upload function.

The uploadAudioStreamToS3 function doesn't validate inputs or handle potential S3 errors.

-export const uploadAudioStreamToS3 = async (audioStream: Buffer, bucket: string) => {
-  const remotePath = `${randomUUID()}.mp3`;
-  await s3.send(
-    new PutObjectCommand({
-      Bucket: bucket,
-      Key: remotePath,
-      Body: audioStream,
-      ContentType: "audio/mpeg",
-    })
-  );
-  return remotePath;
+export const uploadAudioStreamToS3 = async (audioStream: Buffer, bucket: string): Promise<string> => {
+  try {
+    if (!audioStream || audioStream.length === 0) {
+      throw new Error("Audio stream is empty or invalid");
+    }
+    
+    if (!bucket) {
+      throw new Error("S3 bucket name is required");
+    }
+    
+    const remotePath = `audio/${randomUUID()}.mp3`;
+    
+    const response = await s3.send(
+      new PutObjectCommand({
+        Bucket: bucket,
+        Key: remotePath,
+        Body: audioStream,
+        ContentType: "audio/mpeg",
+      })
+    );
+    
+    if (!response) {
+      throw new Error("S3 upload failed");
+    }
+    
+    return remotePath;
+  } catch (error) {
+    logger.error("Failed to upload audio to S3", { bucketName: bucket, error });
+    throw new Error(`S3 upload failed: ${error instanceof Error ? error.message : String(error)}`);
+  }
 };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const uploadAudioStreamToS3 = async (audioStream: Buffer, bucket: string) => {
const remotePath = `${randomUUID()}.mp3`;
await s3.send(
new PutObjectCommand({
Bucket: bucket,
Key: remotePath,
Body: audioStream,
ContentType: "audio/mpeg",
})
);
return remotePath;
};
export const uploadAudioStreamToS3 = async (audioStream: Buffer, bucket: string): Promise<string> => {
try {
if (!audioStream || audioStream.length === 0) {
throw new Error("Audio stream is empty or invalid");
}
if (!bucket) {
throw new Error("S3 bucket name is required");
}
const remotePath = `audio/${randomUUID()}.mp3`;
const response = await s3.send(
new PutObjectCommand({
Bucket: bucket,
Key: remotePath,
Body: audioStream,
ContentType: "audio/mpeg",
})
);
if (!response) {
throw new Error("S3 upload failed");
}
return remotePath;
} catch (error) {
logger.error("Failed to upload audio to S3", { bucketName: bucket, error });
throw new Error(`S3 upload failed: ${error instanceof Error ? error.message : String(error)}`);
}
};

@myftija myftija closed this Apr 15, 2025
@myftija
Copy link
Member Author

myftija commented Apr 15, 2025

Closing this PR in favor of triggerdotdev/examples#7, which is also referenced in the docs. This helps reduce the maintenance effort and avoid adding more dependencies in this repo.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant