diff --git a/.changeset/famous-grapes-film.md b/.changeset/famous-grapes-film.md new file mode 100644 index 0000000000..661535f424 --- /dev/null +++ b/.changeset/famous-grapes-film.md @@ -0,0 +1,5 @@ +--- +"trigger.dev": patch +--- + +Support ignoring test/spec files inside trigger dirs (fixes #1593) diff --git a/docs/config/config-file.mdx b/docs/config/config-file.mdx index 08d9021cf9..99a6e8c45b 100644 --- a/docs/config/config-file.mdx +++ b/docs/config/config-file.mdx @@ -47,6 +47,33 @@ The config file handles a lot of things, like: imports used inside build config with be tree-shaken out. +## Dirs + +You can specify the directories where your tasks are located using the `dirs` option: + +```ts trigger.config.ts +import { defineConfig } from "@trigger.dev/sdk/v3"; + +export default defineConfig({ + project: "", + dirs: ["./trigger"], +}); +``` + +If you omit the `dirs` option, we will automatically detect directories that are named `trigger` in your project, but we recommend specifying the directories explicitly. The `dirs` option is an array of strings, so you can specify multiple directories if you have tasks in multiple locations. + +We will search for TypeScript and JavaScript files in the specified directories and include them in the build process. We automatically exclude files that have `.test` or `.spec` in the name, but you can customize this by specifying glob patterns in the `ignorePatterns` option: + +```ts trigger.config.ts +import { defineConfig } from "@trigger.dev/sdk/v3"; + +export default defineConfig({ + project: "", + dirs: ["./trigger"], + ignorePatterns: ["**/*.my-test.ts"], +}); +``` + ## Lifecycle functions You can add lifecycle functions to get notified when any task starts, succeeds, or fails using `onStart`, `onSuccess` and `onFailure`: diff --git a/docs/mint.json b/docs/mint.json index ac16b678ea..747d276ce3 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -1,7 +1,10 @@ { "$schema": "https://mintlify.com/schema.json", "name": "Trigger.dev", - "openapi": ["/openapi.yml", "/v3-openapi.yaml"], + "openapi": [ + "/openapi.yml", + "/v3-openapi.yaml" + ], "api": { "playground": { "mode": "simple" @@ -128,7 +131,6 @@ "quick-start", "video-walkthrough", "how-it-works", - "upgrading-beta", "limits" ] }, @@ -137,20 +139,30 @@ "pages": [ { "group": "Tasks", - "pages": ["tasks/overview", "tasks/schemaTask", "tasks/scheduled"] + "pages": [ + "tasks/overview", + "tasks/schemaTask", + "tasks/scheduled" + ] }, "triggering", "runs", "apikeys", { "group": "Configuration", - "pages": ["config/config-file", "config/extensions/overview"] + "pages": [ + "config/config-file", + "config/extensions/overview" + ] } ] }, { "group": "Development", - "pages": ["cli-dev", "run-tests"] + "pages": [ + "cli-dev", + "run-tests" + ] }, { "group": "Deployment", @@ -160,7 +172,9 @@ "github-actions", { "group": "Deployment integrations", - "pages": ["vercel-integration"] + "pages": [ + "vercel-integration" + ] } ] }, @@ -172,7 +186,13 @@ "errors-retrying", { "group": "Wait", - "pages": ["wait", "wait-for", "wait-until", "wait-for-event", "wait-for-request"] + "pages": [ + "wait", + "wait-for", + "wait-until", + "wait-for-event", + "wait-for-request" + ] }, "queue-concurrency", "versioning", @@ -217,7 +237,10 @@ "management/overview", { "group": "Tasks API", - "pages": ["management/tasks/trigger", "management/tasks/batch-trigger"] + "pages": [ + "management/tasks/trigger", + "management/tasks/batch-trigger" + ] }, { "group": "Runs API", @@ -256,7 +279,9 @@ }, { "group": "Projects API", - "pages": ["management/projects/runs"] + "pages": [ + "management/projects/runs" + ] } ] }, @@ -294,6 +319,7 @@ "pages": [ "troubleshooting", "upgrading-packages", + "upgrading-beta", "troubleshooting-alerts", "troubleshooting-uptime-status", "troubleshooting-github-issues", @@ -302,11 +328,17 @@ }, { "group": "Help", - "pages": ["community", "help-slack", "help-email"] + "pages": [ + "community", + "help-slack", + "help-email" + ] }, { "group": "", - "pages": ["guides/introduction"] + "pages": [ + "guides/introduction" + ] }, { "group": "Frameworks", @@ -380,11 +412,15 @@ }, { "group": "Dashboard", - "pages": ["guides/dashboard/creating-a-project"] + "pages": [ + "guides/dashboard/creating-a-project" + ] }, { "group": "Migrations", - "pages": ["guides/use-cases/upgrading-from-v2"] + "pages": [ + "guides/use-cases/upgrading-from-v2" + ] } ], "footerSocials": { @@ -392,4 +428,4 @@ "github": "https://github.com/triggerdotdev", "linkedin": "https://www.linkedin.com/company/triggerdotdev" } -} +} \ No newline at end of file diff --git a/packages/cli-v3/package.json b/packages/cli-v3/package.json index 794edd3369..706478d648 100644 --- a/packages/cli-v3/package.json +++ b/packages/cli-v3/package.json @@ -91,6 +91,7 @@ "@trigger.dev/core": "workspace:3.3.9", "c12": "^1.11.1", "chalk": "^5.2.0", + "chokidar": "^3.6.0", "cli-table3": "^0.6.3", "commander": "^9.4.1", "defu": "^6.1.4", @@ -119,6 +120,7 @@ "terminal-link": "^3.0.0", "tiny-invariant": "^1.2.0", "tinyexec": "^0.3.1", + "tinyglobby": "^0.2.2", "ws": "^8.18.0", "xdg-app-paths": "^8.3.0", "zod": "3.23.8", diff --git a/packages/cli-v3/src/build/bundle.ts b/packages/cli-v3/src/build/bundle.ts index 251e975e22..d06fdbf9a9 100644 --- a/packages/cli-v3/src/build/bundle.ts +++ b/packages/cli-v3/src/build/bundle.ts @@ -17,6 +17,7 @@ import { telemetryEntryPoint, } from "./packageModules.js"; import { buildPlugins } from "./plugins.js"; +import { createEntryPointManager } from "./entryPoints.js"; export interface BundleOptions { target: BuildTarget; @@ -45,12 +46,30 @@ export type BundleResult = { export async function bundleWorker(options: BundleOptions): Promise { const { resolvedConfig } = options; - // We need to add the package entry points here somehow - // Then we need to get them out of the build result into the build manifest - // taskhero/dist/esm/workers/dev.js - // taskhero/dist/esm/telemetry/loader.js - const entryPoints = await getEntryPoints(options.target, resolvedConfig); - const $buildPlugins = await buildPlugins(options.target, resolvedConfig); + let currentContext: esbuild.BuildContext | undefined; + + const entryPointManager = await createEntryPointManager( + resolvedConfig.dirs, + resolvedConfig, + options.target, + typeof options.watch === "boolean" ? options.watch : false, + async (newEntryPoints) => { + if (currentContext) { + // Rebuild with new entry points + await currentContext.cancel(); + await currentContext.dispose(); + const buildOptions = await createBuildOptions({ + ...options, + entryPoints: newEntryPoints, + }); + + logger.debug("Rebuilding worker with options", buildOptions); + + currentContext = await esbuild.context(buildOptions); + await currentContext.watch(); + } + } + ); let initialBuildResult: (result: esbuild.BuildResult) => void; const initialBuildResultPromise = new Promise( @@ -63,12 +82,63 @@ export async function bundleWorker(options: BundleOptions): Promise; + let stop: BundleResult["stop"]; + + logger.debug("Building worker with options", buildOptions); + + if (options.watch) { + currentContext = await esbuild.context(buildOptions); + await currentContext.watch(); + result = await initialBuildResultPromise; + if (result.errors.length > 0) { + throw new Error("Failed to build"); + } + + stop = async function () { + await entryPointManager.stop(); + await currentContext?.dispose(); + }; + } else { + result = await esbuild.build(buildOptions); + + stop = async function () { + await entryPointManager.stop(); + }; + } + + const bundleResult = await getBundleResultFromBuild( + options.target, + options.cwd, + options.resolvedConfig, + result + ); + + if (!bundleResult) { + throw new Error("Failed to get bundle result"); + } + + return { ...bundleResult, stop }; +} + +// Helper function to create build options +async function createBuildOptions( + options: BundleOptions & { entryPoints: string[]; buildResultPlugin?: esbuild.Plugin } +): Promise { const customConditions = options.resolvedConfig.build?.conditions ?? []; const conditions = [...customConditions, "trigger.dev", "module", "node"]; - const buildOptions: esbuild.BuildOptions & { metafile: true } = { - entryPoints, + const $buildPlugins = await buildPlugins(options.target, options.resolvedConfig); + + return { + entryPoints: options.entryPoints, outdir: options.destination, absWorkingDir: options.cwd, bundle: true, @@ -93,7 +163,11 @@ export async function bundleWorker(options: BundleOptions): Promise; - let stop: BundleResult["stop"]; - - logger.debug("Building worker with options", buildOptions); - - if (options.watch) { - const ctx = await esbuild.context(buildOptions); - await ctx.watch(); - result = await initialBuildResultPromise; - if (result.errors.length > 0) { - throw new Error("Failed to build"); - } - - stop = async function () { - await ctx.dispose(); - }; - } else { - result = await esbuild.build(buildOptions); - // Even when we're not watching, we still want some way of cleaning up the - // temporary directory when we don't need it anymore - stop = async function () {}; - } - - const bundleResult = await getBundleResultFromBuild( - options.target, - options.cwd, - options.resolvedConfig, - result - ); - - if (!bundleResult) { - throw new Error("Failed to get bundle result"); - } - - return { ...bundleResult, stop }; } export async function getBundleResultFromBuild( diff --git a/packages/cli-v3/src/build/entryPoints.ts b/packages/cli-v3/src/build/entryPoints.ts new file mode 100644 index 0000000000..95ca763764 --- /dev/null +++ b/packages/cli-v3/src/build/entryPoints.ts @@ -0,0 +1,128 @@ +import { BuildTarget } from "@trigger.dev/core/v3"; +import { ResolvedConfig } from "@trigger.dev/core/v3/build"; +import * as chokidar from "chokidar"; +import { glob } from "tinyglobby"; +import { logger } from "../utilities/logger.js"; +import { deployEntryPoints, devEntryPoints, telemetryEntryPoint } from "./packageModules.js"; + +type EntryPointManager = { + entryPoints: string[]; + watcher?: chokidar.FSWatcher; + stop: () => Promise; +}; + +const DEFAULT_IGNORE_PATTERNS = [ + "**/*.test.ts", + "**/*.test.mts", + "**/*.test.cts", + "**/*.test.js", + "**/*.test.mjs", + "**/*.test.cjs", + "**/*.spec.ts", + "**/*.spec.mts", + "**/*.spec.cts", + "**/*.spec.js", + "**/*.spec.mjs", + "**/*.spec.cjs", +]; + +export async function createEntryPointManager( + dirs: string[], + config: ResolvedConfig, + target: BuildTarget, + watch: boolean, + onEntryPointsChange?: (entryPoints: string[]) => Promise +): Promise { + // Patterns to match files + const patterns = dirs.flatMap((dir) => [`${dir}/**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}`]); + + // Patterns to ignore + const ignorePatterns = config.ignorePatterns ?? DEFAULT_IGNORE_PATTERNS; + + async function getEntryPoints() { + // Get initial entry points + const entryPoints = await glob(patterns, { + ignore: ignorePatterns, + absolute: false, + cwd: config.workingDir, + }); + + // Add required entry points + if (config.configFile) { + entryPoints.push(config.configFile); + } + + if (target === "dev") { + entryPoints.push(...devEntryPoints); + } else { + entryPoints.push(...deployEntryPoints); + } + + if (config.instrumentedPackageNames?.length ?? 0 > 0) { + entryPoints.push(telemetryEntryPoint); + } + + // Sort to ensure consistent comparison + return entryPoints.sort(); + } + + const initialEntryPoints = await getEntryPoints(); + + logger.debug("Initial entry points", { + entryPoints: initialEntryPoints, + patterns, + cwd: config.workingDir, + }); + + let currentEntryPoints = initialEntryPoints; + + // Only setup watcher if watch is true + let watcher: chokidar.FSWatcher | undefined; + + if (watch && onEntryPointsChange) { + logger.debug("Watching entry points for changes", { dirs, cwd: config.workingDir }); + // Watch the parent directories + watcher = chokidar.watch(patterns, { + ignored: ignorePatterns, + persistent: true, + ignoreInitial: true, + useFsEvents: false, + }); + + // Handle file changes + const updateEntryPoints = async (event: string, path: string) => { + logger.debug("Entry point change detected", { event, path }); + + const newEntryPoints = await getEntryPoints(); + + // Compare arrays to see if they're different + const hasChanged = + newEntryPoints.length !== currentEntryPoints.length || + newEntryPoints.some((entry, index) => entry !== currentEntryPoints[index]); + + if (hasChanged) { + logger.debug("Entry points changed", { + old: currentEntryPoints, + new: newEntryPoints, + }); + currentEntryPoints = newEntryPoints; + await onEntryPointsChange(newEntryPoints); + } + }; + + watcher + .on("add", (path) => updateEntryPoints("add", path)) + .on("addDir", (path) => updateEntryPoints("addDir", path)) + .on("unlink", (path) => updateEntryPoints("unlink", path)) + .on("unlinkDir", (path) => updateEntryPoints("unlinkDir", path)) + .on("error", (error) => logger.error("Watcher error:", error)); + } + + return { + entryPoints: initialEntryPoints, + watcher, + stop: async () => { + await watcher?.close(); + }, + }; +} diff --git a/packages/cli-v3/src/dev/devSession.ts b/packages/cli-v3/src/dev/devSession.ts index 8d80b30d7f..f314bb1e04 100644 --- a/packages/cli-v3/src/dev/devSession.ts +++ b/packages/cli-v3/src/dev/devSession.ts @@ -164,6 +164,9 @@ export async function startDevSession({ async function runBundle() { eventBus.emit("buildStarted", "dev"); + // Use glob to find initial entryPoints + // Use chokidar to watch for entryPoints changes (e.g. added or removed?) + // When there is a change, update entryPoints and start a new build with watch: true const bundleResult = await bundleWorker({ target: "dev", cwd: rawConfig.workingDir, diff --git a/packages/core/src/v3/config.ts b/packages/core/src/v3/config.ts index 42ca53c616..d565d62614 100644 --- a/packages/core/src/v3/config.ts +++ b/packages/core/src/v3/config.ts @@ -15,19 +15,70 @@ export type TriggerConfig = { * @default "node" */ runtime?: BuildRuntime; + + /** + * Specify the project ref for your trigger.dev tasks. This is the project ref that you get when you create a new project in the trigger.dev dashboard. + */ project: string; + + /** + * Specify the directories that contain your trigger.dev tasks. This is useful if you have multiple directories that contain tasks. + * + * We automatically detect directories named `trigger` to be task directories. You can override this behavior by specifying the directories here. + * + * @see @see https://trigger.dev/docs/config/config-file#dirs + */ dirs?: string[]; + + /** + * Specify glob patterns to ignore when detecting task files. By default we ignore: + * + * - *.test.ts + * - *.spec.ts + * - *.test.mts + * - *.spec.mts + * - *.test.cts + * - *.spec.cts + * - *.test.js + * - *.spec.js + * - *.test.mjs + * - *.spec.mjs + * - *.test.cjs + * - *.spec.cjs + * + */ + ignorePatterns?: string[]; + + /** + * Instrumentations to use for OpenTelemetry. This is useful if you want to add custom instrumentations to your tasks. + * + * @see https://trigger.dev/docs/config/config-file#instrumentations + */ instrumentations?: Array; + + /** + * Specify a custom path to your tsconfig file. This is useful if you have a custom tsconfig file that you want to use. + */ tsconfig?: string; + + /** + * Specify the global retry options for your tasks. You can override this on a per-task basis. + * + * @see https://trigger.dev/docs/tasks/overview#retry-options + */ retries?: { enabledInDev?: boolean; default?: RetryOptions; }; + /** * The default machine preset to use for your deployed trigger.dev tasks. You can override this on a per-task basis. * @default "small-1x" + * + * @see https://trigger.dev/docs/machines */ machine?: MachinePresetName; + /** * Set the log level for the logger. Defaults to "info", so you will see "log", "info", "warn", and "error" messages, but not "debug" messages. * @@ -43,6 +94,8 @@ export type TriggerConfig = { * Minimum value is 5 seconds * * Setting this value will effect all tasks in the project. + * + * @see https://trigger.dev/docs/tasks/overview#maxduration-option */ maxDuration?: number; @@ -50,6 +103,7 @@ export type TriggerConfig = { * Enable console logging while running the dev CLI. This will print out logs from console.log, console.warn, and console.error. By default all logs are sent to the trigger.dev backend, and not logged to the console. */ enableConsoleLogging?: boolean; + build?: { /** * Add custom conditions to the esbuild build. For example, if you are importing `ai/rsc`, you'll need to add "react-server" condition. @@ -61,8 +115,21 @@ export type TriggerConfig = { * - "node" */ conditions?: string[]; + + /** + * Add custom build extensions to the build process. + * + * @see https://trigger.dev/docs/config/config-file#extensions + */ extensions?: BuildExtension[]; + + /** + * External dependencies to exclude from the bundle. This is useful if you want to keep some dependencies as external, and not bundle them with your code. + * + * @see https://trigger.dev/docs/config/config-file#external + */ external?: string[]; + jsx?: { /** * @default "React.createElement" @@ -81,6 +148,7 @@ export type TriggerConfig = { automatic?: boolean; }; }; + deploy?: { env?: Record; }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0d1bd95a93..18d2ea1deb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1117,6 +1117,9 @@ importers: chalk: specifier: ^5.2.0 version: 5.3.0 + chokidar: + specifier: ^3.6.0 + version: 3.6.0 cli-table3: specifier: ^0.6.3 version: 0.6.3 @@ -1201,6 +1204,9 @@ importers: tinyexec: specifier: ^0.3.1 version: 0.3.1 + tinyglobby: + specifier: ^0.2.2 + version: 0.2.2 ws: specifier: ^8.18.0 version: 8.18.0 @@ -14509,7 +14515,7 @@ packages: arg: 5.0.2 cacache: 17.1.4 chalk: 4.1.2 - chokidar: 3.5.3 + chokidar: 3.6.0 dotenv: 16.4.5 esbuild: 0.17.6 esbuild-plugins-node-modules-polyfill: 1.6.1(esbuild@0.17.6) @@ -14662,7 +14668,7 @@ packages: dependencies: '@remix-run/express': 2.1.0(express@4.18.2)(typescript@5.2.2) '@remix-run/node': 2.1.0(typescript@5.2.2) - chokidar: 3.5.3 + chokidar: 3.6.0 compression: 1.7.4 express: 4.18.2 get-port: 5.1.1 @@ -19115,6 +19121,7 @@ packages: readdirp: 3.6.0 optionalDependencies: fsevents: 2.3.3 + dev: false /chokidar@3.6.0: resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} @@ -32386,7 +32393,7 @@ packages: '@esbuild-plugins/node-globals-polyfill': 0.2.3(esbuild@0.17.19) '@esbuild-plugins/node-modules-polyfill': 0.2.2(esbuild@0.17.19) blake3-wasm: 2.1.5 - chokidar: 3.5.3 + chokidar: 3.6.0 esbuild: 0.17.19 miniflare: 3.20240512.0 nanoid: 3.3.7 diff --git a/references/nextjs-realtime/batchinput.jsonl b/references/nextjs-realtime/batchinput.jsonl new file mode 100644 index 0000000000..70fd355962 --- /dev/null +++ b/references/nextjs-realtime/batchinput.jsonl @@ -0,0 +1,20 @@ +{"custom_id":"request-1","method":"POST","url":"/v1/chat/completions","body":{"model":"gpt-3.5-turbo-0125","messages":[{"role":"system","content":"You are a helpful AI assistant specializing in explaining AI and machine learning concepts."},{"role":"user","content":"What is the difference between narrow AI and general AI?"}],"max_tokens":150}} +{"custom_id":"request-2","method":"POST","url":"/v1/chat/completions","body":{"model":"gpt-3.5-turbo-0125","messages":[{"role":"system","content":"You are a helpful AI assistant specializing in explaining AI and machine learning concepts."},{"role":"user","content":"How do large language models like GPT-3 work?"}],"max_tokens":150}} +{"custom_id":"request-3","method":"POST","url":"/v1/chat/completions","body":{"model":"gpt-3.5-turbo-0125","messages":[{"role":"system","content":"You are a helpful AI assistant specializing in explaining AI and machine learning concepts."},{"role":"user","content":"What are some ethical concerns surrounding the development of AI?"}],"max_tokens":150}} +{"custom_id":"request-4","method":"POST","url":"/v1/chat/completions","body":{"model":"gpt-3.5-turbo-0125","messages":[{"role":"system","content":"You are a helpful AI assistant specializing in explaining AI and machine learning concepts."},{"role":"user","content":"Can you explain the concept of transfer learning in AI?"}],"max_tokens":150}} +{"custom_id":"request-5","method":"POST","url":"/v1/chat/completions","body":{"model":"gpt-3.5-turbo-0125","messages":[{"role":"system","content":"You are a helpful AI assistant specializing in explaining AI and machine learning concepts."},{"role":"user","content":"What is the Turing test, and is it still relevant in modern AI?"}],"max_tokens":150}} +{"custom_id":"request-6","method":"POST","url":"/v1/chat/completions","body":{"model":"gpt-3.5-turbo-0125","messages":[{"role":"system","content":"You are a helpful AI assistant specializing in explaining AI and machine learning concepts."},{"role":"user","content":"How do neural networks mimic the human brain?"}],"max_tokens":150}} +{"custom_id":"request-7","method":"POST","url":"/v1/chat/completions","body":{"model":"gpt-3.5-turbo-0125","messages":[{"role":"system","content":"You are a helpful AI assistant specializing in explaining AI and machine learning concepts."},{"role":"user","content":"What are the main challenges in natural language processing?"}],"max_tokens":150}} +{"custom_id":"request-8","method":"POST","url":"/v1/chat/completions","body":{"model":"gpt-3.5-turbo-0125","messages":[{"role":"system","content":"You are a helpful AI assistant specializing in explaining AI and machine learning concepts."},{"role":"user","content":"How does reinforcement learning differ from supervised learning?"}],"max_tokens":150}} +{"custom_id":"request-9","method":"POST","url":"/v1/chat/completions","body":{"model":"gpt-3.5-turbo-0125","messages":[{"role":"system","content":"You are a helpful AI assistant specializing in explaining AI and machine learning concepts."},{"role":"user","content":"What is the role of attention mechanisms in transformer models?"}],"max_tokens":150}} +{"custom_id":"request-10","method":"POST","url":"/v1/chat/completions","body":{"model":"gpt-3.5-turbo-0125","messages":[{"role":"system","content":"You are a helpful AI assistant specializing in explaining AI and machine learning concepts."},{"role":"user","content":"Can AI truly be creative, or is it just mimicking human creativity?"}],"max_tokens":150}} +{"custom_id":"request-11","method":"POST","url":"/v1/chat/completions","body":{"model":"gpt-3.5-turbo-0125","messages":[{"role":"system","content":"You are a helpful AI assistant specializing in explaining AI and machine learning concepts."},{"role":"user","content":"What are the potential implications of AI on the job market?"}],"max_tokens":150}} +{"custom_id":"request-12","method":"POST","url":"/v1/chat/completions","body":{"model":"gpt-3.5-turbo-0125","messages":[{"role":"system","content":"You are a helpful AI assistant specializing in explaining AI and machine learning concepts."},{"role":"user","content":"How do self-driving cars use AI to navigate and make decisions?"}],"max_tokens":150}} +{"custom_id":"request-13","method":"POST","url":"/v1/chat/completions","body":{"model":"gpt-3.5-turbo-0125","messages":[{"role":"system","content":"You are a helpful AI assistant specializing in explaining AI and machine learning concepts."},{"role":"user","content":"What is the difference between strong AI and weak AI?"}],"max_tokens":150}} +{"custom_id":"request-14","method":"POST","url":"/v1/chat/completions","body":{"model":"gpt-3.5-turbo-0125","messages":[{"role":"system","content":"You are a helpful AI assistant specializing in explaining AI and machine learning concepts."},{"role":"user","content":"How do language models handle context and maintain coherence in long texts?"}],"max_tokens":150}} +{"custom_id":"request-15","method":"POST","url":"/v1/chat/completions","body":{"model":"gpt-3.5-turbo-0125","messages":[{"role":"system","content":"You are a helpful AI assistant specializing in explaining AI and machine learning concepts."},{"role":"user","content":"What are some applications of AI in healthcare?"}],"max_tokens":150}} +{"custom_id":"request-16","method":"POST","url":"/v1/chat/completions","body":{"model":"gpt-3.5-turbo-0125","messages":[{"role":"system","content":"You are a helpful AI assistant specializing in explaining AI and machine learning concepts."},{"role":"user","content":"How does federated learning protect user privacy in AI systems?"}],"max_tokens":150}} +{"custom_id":"request-17","method":"POST","url":"/v1/chat/completions","body":{"model":"gpt-3.5-turbo-0125","messages":[{"role":"system","content":"You are a helpful AI assistant specializing in explaining AI and machine learning concepts."},{"role":"user","content":"What is the role of bias in AI, and how can it be mitigated?"}],"max_tokens":150}} +{"custom_id":"request-18","method":"POST","url":"/v1/chat/completions","body":{"model":"gpt-3.5-turbo-0125","messages":[{"role":"system","content":"You are a helpful AI assistant specializing in explaining AI and machine learning concepts."},{"role":"user","content":"Can you explain the concept of explainable AI (XAI)?"}],"max_tokens":150}} +{"custom_id":"request-19","method":"POST","url":"/v1/chat/completions","body":{"model":"gpt-3.5-turbo-0125","messages":[{"role":"system","content":"You are a helpful AI assistant specializing in explaining AI and machine learning concepts."},{"role":"user","content":"How do recommendation systems use AI to personalize content?"}],"max_tokens":150}} +{"custom_id":"request-20","method":"POST","url":"/v1/chat/completions","body":{"model":"gpt-3.5-turbo-0125","messages":[{"role":"system","content":"You are a helpful AI assistant specializing in explaining AI and machine learning concepts."},{"role":"user","content":"What are the challenges in developing AI systems that can reason like humans?"}],"max_tokens":150}} \ No newline at end of file diff --git a/references/nextjs-realtime/src/app/openai/page.tsx b/references/nextjs-realtime/src/app/openai/page.tsx new file mode 100644 index 0000000000..ddda3d218b --- /dev/null +++ b/references/nextjs-realtime/src/app/openai/page.tsx @@ -0,0 +1,12 @@ +import BatchSubmissionForm from "@/components/BatchSubmissionForm"; +import { auth } from "@trigger.dev/sdk/v3"; + +export default async function Page() { + const accessToken = await auth.createTriggerPublicToken("openai-batch"); + + return ( +
+ +
+ ); +} diff --git a/references/nextjs-realtime/src/components/BatchProgressIndicator.tsx b/references/nextjs-realtime/src/components/BatchProgressIndicator.tsx new file mode 100644 index 0000000000..a96c6d629d --- /dev/null +++ b/references/nextjs-realtime/src/components/BatchProgressIndicator.tsx @@ -0,0 +1,183 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Progress } from "@/components/ui/progress"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { AlertCircle, CheckCircle2, Clock, FileText, Loader2, RefreshCw } from "lucide-react"; + +type BatchStatus = "validating" | "in_progress" | "completed" | "failed" | "expired"; + +interface BatchInfo { + id: string; + status: BatchStatus; + totalRequests: number; + completedRequests: number; + failedRequests: number; + inputFileName: string; + outputFileName: string | null; + errorFileName: string | null; + createdAt: string; + completedAt: string | null; +} + +export default function BatchProgressIndicator() { + const [batchInfo, setBatchInfo] = useState({ + id: "batch_abc123", + status: "in_progress", + totalRequests: 1000, + completedRequests: 750, + failedRequests: 10, + inputFileName: "input.jsonl", + outputFileName: null, + errorFileName: null, + createdAt: "2023-03-15T10:30:00Z", + completedAt: null, + }); + const [lastCheckedAt, setLastCheckedAt] = useState(new Date().toISOString()); + + useEffect(() => { + // Simulate progress + const interval = setInterval(() => { + setBatchInfo((prev) => ({ + ...prev, + completedRequests: Math.min(prev.completedRequests + 10, prev.totalRequests), + status: prev.completedRequests + 10 >= prev.totalRequests ? "completed" : prev.status, + completedAt: + prev.completedRequests + 10 >= prev.totalRequests ? new Date().toISOString() : null, + outputFileName: prev.completedRequests + 10 >= prev.totalRequests ? "output.jsonl" : null, + })); + setLastCheckedAt(new Date().toISOString()); + }, 1000); + + return () => clearInterval(interval); + }, []); + + const getStatusIcon = (status: BatchStatus) => { + switch (status) { + case "validating": + case "in_progress": + return ; + case "completed": + return ; + case "failed": + return ; + case "expired": + return ; + } + }; + + const getStatusColor = (status: BatchStatus) => { + switch (status) { + case "validating": + case "in_progress": + return "bg-blue-100 text-blue-800 border-blue-300"; + case "completed": + return "bg-green-100 text-green-800 border-green-300"; + case "failed": + return "bg-red-100 text-red-800 border-red-300"; + case "expired": + return "bg-yellow-100 text-yellow-800 border-yellow-300"; + } + }; + + return ( + + + + Batch Progress: {batchInfo.id} + + {getStatusIcon(batchInfo.status)} + {batchInfo.status.replace("_", " ")} + + + + +
+ Progress + + {Math.round((batchInfo.completedRequests / batchInfo.totalRequests) * 100)}% + +
+ +
+
+

Total Requests

+

{batchInfo.totalRequests}

+
+
+

Completed

+

{batchInfo.completedRequests}

+
+
+

Failed

+

{batchInfo.failedRequests}

+
+
+

Created At

+

{new Date(batchInfo.createdAt).toLocaleString()}

+
+
+

Last Checked At

+

{new Date(lastCheckedAt).toLocaleString()}

+
+
+
+
+ + + Input: {batchInfo.inputFileName} + +
+ {batchInfo.outputFileName && ( +
+ + + Output: {batchInfo.outputFileName} + +
+ )} + {batchInfo.errorFileName && ( +
+ + + Errors: {batchInfo.errorFileName} + +
+ )} +
+
+ + +
+
+
+ ); +} diff --git a/references/nextjs-realtime/src/components/BatchSubmissionForm.tsx b/references/nextjs-realtime/src/components/BatchSubmissionForm.tsx new file mode 100644 index 0000000000..a761b182c5 --- /dev/null +++ b/references/nextjs-realtime/src/components/BatchSubmissionForm.tsx @@ -0,0 +1,102 @@ +"use client"; + +import { useState } from "react"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Textarea } from "@/components/ui/textarea"; +import { Upload, AlertCircle } from "lucide-react"; +import { Alert, AlertDescription } from "@/components/ui/alert"; +import { useRealtimeRun, useTaskTrigger } from "@trigger.dev/react-hooks"; +import type { openaiBatch } from "@/trigger/openaiBatch"; + +export default function BatchSubmissionForm({ accessToken }: { accessToken: string }) { + const trigger = useTaskTrigger("openai-batch", { + accessToken, + baseURL: process.env.NEXT_PUBLIC_TRIGGER_API_URL, + }); + + const { run } = useRealtimeRun(trigger.handle?.id, { + accessToken: trigger.handle?.publicAccessToken, + enabled: !!trigger.handle, + baseURL: process.env.NEXT_PUBLIC_TRIGGER_API_URL, + }); + + const [jsonlContent, setJsonlContent] = useState(""); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + trigger.submit({ + jsonl: jsonlContent, + }); + }; + + return ( + + + Submit Batch Job + + +
+
+ +