diff --git a/packages/trigger-sdk/src/v3/ai.ts b/packages/trigger-sdk/src/v3/ai.ts index ab2c12122..3bb0db9dc 100644 --- a/packages/trigger-sdk/src/v3/ai.ts +++ b/packages/trigger-sdk/src/v3/ai.ts @@ -379,6 +379,47 @@ async function pipeChat( * emits a control chunk and suspends via `messagesInput.wait()`. The frontend * transport resumes the same run by sending the next message via input streams. */ +/** + * Event passed to the `onChatStart` callback. + */ +export type ChatStartEvent = { + /** The unique identifier for the chat session. */ + chatId: string; + /** The initial model-ready messages for this conversation. */ + messages: ModelMessage[]; + /** Custom data from the frontend (passed via `metadata` on `sendMessage()` or the transport). */ + clientData: unknown; +}; + +/** + * Event passed to the `onTurnComplete` callback. + */ +export type TurnCompleteEvent = { + /** The unique identifier for the chat session. */ + chatId: string; + /** The full accumulated conversation in model format (all turns so far). */ + messages: ModelMessage[]; + /** + * The full accumulated conversation in UI format (all turns so far). + * This is the format expected by `useChat` — store this for persistence. + */ + uiMessages: UIMessage[]; + /** + * Only the new model messages from this turn (user message(s) + assistant response). + * Useful for appending to an existing conversation record. + */ + newMessages: ModelMessage[]; + /** + * Only the new UI messages from this turn (user message(s) + assistant response). + * Useful for inserting individual message records instead of overwriting the full history. + */ + newUIMessages: UIMessage[]; + /** The assistant's response for this turn (undefined if `pipeChat` was used manually). */ + responseMessage: UIMessage | undefined; + /** The turn number (0-indexed). */ + turn: number; +}; + export type ChatTaskOptions = Omit< TaskOptions, "run" @@ -394,6 +435,35 @@ export type ChatTaskOptions = Omit< */ run: (payload: ChatTaskRunPayload) => Promise; + /** + * Called on the first turn (turn 0) of a new run, before the `run` function executes. + * + * Use this to create the chat record in your database when a new conversation starts. + * + * @example + * ```ts + * onChatStart: async ({ chatId, messages, clientData }) => { + * await db.chat.create({ data: { id: chatId, userId: clientData.userId } }); + * } + * ``` + */ + onChatStart?: (event: ChatStartEvent) => Promise | void; + + /** + * Called after each turn completes (after the response is captured, before waiting + * for the next message). Also fires on the final turn. + * + * Use this to persist the conversation to your database after each assistant response. + * + * @example + * ```ts + * onTurnComplete: async ({ chatId, messages }) => { + * await db.chat.update({ where: { id: chatId }, data: { messages } }); + * } + * ``` + */ + onTurnComplete?: (event: TurnCompleteEvent) => Promise | void; + /** * Maximum number of conversational turns (message round-trips) a single run * will handle before ending. After this many turns the run completes @@ -456,6 +526,8 @@ function chatTask( ): Task { const { run: userRun, + onChatStart, + onTurnComplete, maxTurns = 100, turnTimeout = "1h", warmTimeoutInSeconds = 30, @@ -478,6 +550,10 @@ function chatTask( // user message(s) and the captured assistant response. let accumulatedMessages: ModelMessage[] = []; + // Accumulated UI messages for persistence. Mirrors the model accumulator + // but in frontend-friendly UIMessage format (with parts, id, etc.). + let accumulatedUIMessages: UIMessage[] = []; + // Mutable reference to the current turn's stop controller so the // stop input stream listener (registered once) can abort the right turn. let currentStopController: AbortController | undefined; @@ -549,15 +625,52 @@ function chatTask( // Turn 2+: only the new message(s) → appended to the accumulator. const incomingModelMessages = await convertToModelMessages(uiMessages); + // Track new messages for this turn (user input + assistant response). + const turnNewModelMessages: ModelMessage[] = []; + const turnNewUIMessages: UIMessage[] = []; + if (turn === 0) { accumulatedMessages = incomingModelMessages; + accumulatedUIMessages = [...uiMessages]; + // On first turn, the "new" messages are just the last user message + // (the rest is history). We'll add the response after streaming. + if (uiMessages.length > 0) { + turnNewUIMessages.push(uiMessages[uiMessages.length - 1]!); + const lastModel = incomingModelMessages[incomingModelMessages.length - 1]; + if (lastModel) turnNewModelMessages.push(lastModel); + } } else if (currentWirePayload.trigger === "regenerate-message") { // Regenerate: frontend sent full history with last assistant message // removed. Reset the accumulator to match. accumulatedMessages = incomingModelMessages; + accumulatedUIMessages = [...uiMessages]; + // No new user messages for regenerate — just the response (added below) } else { // Submit: frontend sent only the new user message(s). Append to accumulator. accumulatedMessages.push(...incomingModelMessages); + accumulatedUIMessages.push(...uiMessages); + turnNewModelMessages.push(...incomingModelMessages); + turnNewUIMessages.push(...uiMessages); + } + + // Fire onChatStart on the first turn + if (turn === 0 && onChatStart) { + await tracer.startActiveSpan( + "onChatStart()", + async () => { + await onChatStart({ + chatId: currentWirePayload.chatId, + messages: accumulatedMessages, + clientData: wireMetadata, + }); + }, + { + attributes: { + [SemanticInternalAttributes.STYLE_ICON]: "task-hook-onStart", + [SemanticInternalAttributes.COLLAPSED]: true, + }, + } + ); } // Captured by the onFinish callback below — works even on abort/stop. @@ -602,11 +715,14 @@ function chatTask( // The onFinish callback fires even on abort/stop, so partial responses // from stopped generation are captured correctly. if (capturedResponseMessage) { + accumulatedUIMessages.push(capturedResponseMessage); + turnNewUIMessages.push(capturedResponseMessage); try { const responseModelMessages = await convertToModelMessages([ stripProviderMetadata(capturedResponseMessage), ]); accumulatedMessages.push(...responseModelMessages); + turnNewModelMessages.push(...responseModelMessages); } catch { // Conversion failed — skip accumulation for this turn } @@ -618,6 +734,30 @@ function chatTask( if (runSignal.aborted) return "exit"; + // Fire onTurnComplete after response capture + if (onTurnComplete) { + await tracer.startActiveSpan( + "onTurnComplete()", + async () => { + await onTurnComplete({ + chatId: currentWirePayload.chatId, + messages: accumulatedMessages, + uiMessages: accumulatedUIMessages, + newMessages: turnNewModelMessages, + newUIMessages: turnNewUIMessages, + responseMessage: capturedResponseMessage, + turn, + }); + }, + { + attributes: { + [SemanticInternalAttributes.STYLE_ICON]: "task-hook-onComplete", + [SemanticInternalAttributes.COLLAPSED]: true, + }, + } + ); + } + // Write turn-complete control chunk so frontend closes its stream await writeTurnCompleteChunk(currentWirePayload.chatId); @@ -629,9 +769,12 @@ function chatTask( // Phase 1: Keep the run warm for quick response to the next message. // The run stays active (using compute) during this window. - if (warmTimeoutInSeconds > 0) { + const effectiveWarmTimeout = + (metadata.get(WARM_TIMEOUT_METADATA_KEY) as number | undefined) ?? warmTimeoutInSeconds; + + if (effectiveWarmTimeout > 0) { const warm = await messagesInput.once({ - timeoutMs: warmTimeoutInSeconds * 1000, + timeoutMs: effectiveWarmTimeout * 1000, spanName: "waiting (warm)", }); @@ -643,8 +786,11 @@ function chatTask( } // Phase 2: Suspend the task (frees compute) until the next message arrives + const effectiveTurnTimeout = + (metadata.get(TURN_TIMEOUT_METADATA_KEY) as string | undefined) ?? turnTimeout; + const next = await messagesInput.wait({ - timeout: turnTimeout, + timeout: effectiveTurnTimeout, spanName: "waiting (suspended)", }); @@ -693,6 +839,74 @@ function chatTask( * const token = await chat.createAccessToken("my-chat"); * ``` */ +// --------------------------------------------------------------------------- +// Runtime configuration helpers +// --------------------------------------------------------------------------- + +const TURN_TIMEOUT_METADATA_KEY = "chat.turnTimeout"; +const WARM_TIMEOUT_METADATA_KEY = "chat.warmTimeout"; + +/** + * Override the turn timeout for subsequent turns in the current run. + * + * The turn timeout controls how long the run stays suspended (freeing compute) + * waiting for the next user message. When it expires, the run completes + * gracefully and the next message starts a fresh run. + * + * Call from inside a `chatTask` run function to adjust based on context. + * + * @param duration - A duration string (e.g. `"5m"`, `"1h"`, `"30s"`) + * + * @example + * ```ts + * run: async ({ messages, signal }) => { + * chat.setTurnTimeout("2h"); + * return streamText({ model, messages, abortSignal: signal }); + * } + * ``` + */ +function setTurnTimeout(duration: string): void { + metadata.set(TURN_TIMEOUT_METADATA_KEY, duration); +} + +/** + * Override the turn timeout in seconds for subsequent turns in the current run. + * + * @param seconds - Number of seconds to wait for the next message before ending the run + * + * @example + * ```ts + * run: async ({ messages, signal }) => { + * chat.setTurnTimeoutInSeconds(3600); // 1 hour + * return streamText({ model, messages, abortSignal: signal }); + * } + * ``` + */ +function setTurnTimeoutInSeconds(seconds: number): void { + metadata.set(TURN_TIMEOUT_METADATA_KEY, `${seconds}s`); +} + +/** + * Override the warm timeout for subsequent turns in the current run. + * + * The warm timeout controls how long the run stays active (using compute) + * after each turn, waiting for the next message. During this window, + * responses are instant. After it expires, the run suspends. + * + * @param seconds - Number of seconds to stay warm (0 to suspend immediately) + * + * @example + * ```ts + * run: async ({ messages, signal }) => { + * chat.setWarmTimeoutInSeconds(60); + * return streamText({ model, messages, abortSignal: signal }); + * } + * ``` + */ +function setWarmTimeoutInSeconds(seconds: number): void { + metadata.set(WARM_TIMEOUT_METADATA_KEY, seconds); +} + export const chat = { /** Create a chat task. See {@link chatTask}. */ task: chatTask, @@ -700,6 +914,12 @@ export const chat = { pipe: pipeChat, /** Create a public access token for a chat task. See {@link createChatAccessToken}. */ createAccessToken: createChatAccessToken, + /** Override the turn timeout at runtime (duration string). See {@link setTurnTimeout}. */ + setTurnTimeout, + /** Override the turn timeout at runtime (seconds). See {@link setTurnTimeoutInSeconds}. */ + setTurnTimeoutInSeconds, + /** Override the warm timeout at runtime. See {@link setWarmTimeoutInSeconds}. */ + setWarmTimeoutInSeconds, }; /** diff --git a/packages/trigger-sdk/src/v3/chat-react.ts b/packages/trigger-sdk/src/v3/chat-react.ts index e37e2e8e5..1ee48a4b2 100644 --- a/packages/trigger-sdk/src/v3/chat-react.ts +++ b/packages/trigger-sdk/src/v3/chat-react.ts @@ -23,7 +23,7 @@ * ``` */ -import { useRef } from "react"; +import { useEffect, useRef } from "react"; import { TriggerChatTransport, type TriggerChatTransportOptions, @@ -57,6 +57,9 @@ export type UseTriggerChatTransportOptions = Om * For dynamic access tokens, pass a function — it will be called on each * request without needing to recreate the transport. * + * The `onSessionChange` callback is kept in a ref so the transport always + * calls the latest version without needing to be recreated. + * * @example * ```tsx * import { useChat } from "@ai-sdk/react"; @@ -80,5 +83,12 @@ export function useTriggerChatTransport( if (ref.current === null) { ref.current = new TriggerChatTransport(options); } + + // Keep onSessionChange up to date without recreating the transport + const { onSessionChange } = options; + useEffect(() => { + ref.current?.setOnSessionChange(onSessionChange); + }, [onSessionChange]); + return ref.current; } diff --git a/packages/trigger-sdk/src/v3/chat.test.ts b/packages/trigger-sdk/src/v3/chat.test.ts index af39b6c96..50138b572 100644 --- a/packages/trigger-sdk/src/v3/chat.test.ts +++ b/packages/trigger-sdk/src/v3/chat.test.ts @@ -1727,4 +1727,187 @@ describe("TriggerChatTransport", () => { expect(triggerCallCount).toBe(2); }); }); + + describe("onSessionChange", () => { + it("should fire when a new session is created", async () => { + const onSessionChange = vi.fn(); + const triggerRunId = "run_session_new"; + const publicToken = "pub_session_new"; + + global.fetch = vi.fn().mockImplementation(async (url: string | URL) => { + const urlStr = typeof url === "string" ? url : url.toString(); + + if (urlStr.includes("/trigger")) { + return new Response(JSON.stringify({ id: triggerRunId }), { + status: 200, + headers: { + "content-type": "application/json", + "x-trigger-jwt": publicToken, + }, + }); + } + + if (urlStr.includes("/realtime/v1/streams/")) { + const chunks = [ + ...sampleChunks, + { type: "__trigger_turn_complete" }, + ]; + return new Response(createSSEStream(sseEncode(chunks)), { + status: 200, + headers: { + "content-type": "text/event-stream", + "X-Stream-Version": "v1", + }, + }); + } + + throw new Error(`Unexpected fetch URL: ${urlStr}`); + }); + + const transport = new TriggerChatTransport({ + task: "my-task", + accessToken: "token", + baseURL: "https://api.test.trigger.dev", + onSessionChange, + }); + + const stream = await transport.sendMessages({ + trigger: "submit-message", + chatId: "chat-1", + messageId: undefined, + messages: [createUserMessage("Hello")], + abortSignal: undefined, + }); + + // Session created notification should have fired + expect(onSessionChange).toHaveBeenCalledWith("chat-1", { + runId: triggerRunId, + publicAccessToken: publicToken, + lastEventId: undefined, + }); + + // Consume stream + const reader = stream.getReader(); + while (!(await reader.read()).done) {} + + // Should also fire with updated lastEventId on turn complete + const lastCall = onSessionChange.mock.calls[onSessionChange.mock.calls.length - 1]!; + expect(lastCall![0]).toBe("chat-1"); + expect(lastCall![1]).not.toBeNull(); + expect(lastCall![1].lastEventId).toBeDefined(); + }); + + it("should fire with null when session is deleted (stream ends naturally)", async () => { + const onSessionChange = vi.fn(); + + global.fetch = vi.fn().mockImplementation(async (url: string | URL) => { + const urlStr = typeof url === "string" ? url : url.toString(); + + if (urlStr.includes("/trigger")) { + return new Response(JSON.stringify({ id: "run_end" }), { + status: 200, + headers: { + "content-type": "application/json", + "x-trigger-jwt": "pub_end", + }, + }); + } + + if (urlStr.includes("/realtime/v1/streams/")) { + // No turn-complete chunk — stream ends naturally (run completed) + return new Response(createSSEStream(sseEncode(sampleChunks)), { + status: 200, + headers: { + "content-type": "text/event-stream", + "X-Stream-Version": "v1", + }, + }); + } + + throw new Error(`Unexpected fetch URL: ${urlStr}`); + }); + + const transport = new TriggerChatTransport({ + task: "my-task", + accessToken: "token", + baseURL: "https://api.test.trigger.dev", + onSessionChange, + }); + + const stream = await transport.sendMessages({ + trigger: "submit-message", + chatId: "chat-end", + messageId: undefined, + messages: [createUserMessage("Hello")], + abortSignal: undefined, + }); + + // Consume the stream fully + const reader = stream.getReader(); + while (!(await reader.read()).done) {} + + // Session should have been created then deleted + expect(onSessionChange).toHaveBeenCalledWith("chat-end", expect.objectContaining({ + runId: "run_end", + })); + expect(onSessionChange).toHaveBeenCalledWith("chat-end", null); + }); + + it("should be updatable via setOnSessionChange", async () => { + const onSessionChange1 = vi.fn(); + const onSessionChange2 = vi.fn(); + + global.fetch = vi.fn().mockImplementation(async (url: string | URL) => { + const urlStr = typeof url === "string" ? url : url.toString(); + + if (urlStr.includes("/trigger")) { + return new Response(JSON.stringify({ id: "run_update" }), { + status: 200, + headers: { + "content-type": "application/json", + "x-trigger-jwt": "pub_update", + }, + }); + } + + if (urlStr.includes("/realtime/v1/streams/")) { + const chunks = [ + ...sampleChunks, + { type: "__trigger_turn_complete" }, + ]; + return new Response(createSSEStream(sseEncode(chunks)), { + status: 200, + headers: { + "content-type": "text/event-stream", + "X-Stream-Version": "v1", + }, + }); + } + + throw new Error(`Unexpected fetch URL: ${urlStr}`); + }); + + const transport = new TriggerChatTransport({ + task: "my-task", + accessToken: "token", + baseURL: "https://api.test.trigger.dev", + onSessionChange: onSessionChange1, + }); + + // Update the callback before sending + transport.setOnSessionChange(onSessionChange2); + + await transport.sendMessages({ + trigger: "submit-message", + chatId: "chat-update", + messageId: undefined, + messages: [createUserMessage("Hello")], + abortSignal: undefined, + }); + + // Only onSessionChange2 should have been called + expect(onSessionChange1).not.toHaveBeenCalled(); + expect(onSessionChange2).toHaveBeenCalled(); + }); + }); }); diff --git a/packages/trigger-sdk/src/v3/chat.ts b/packages/trigger-sdk/src/v3/chat.ts index ed7530b4b..eb0aae7cc 100644 --- a/packages/trigger-sdk/src/v3/chat.ts +++ b/packages/trigger-sdk/src/v3/chat.ts @@ -100,6 +100,57 @@ export type TriggerChatTransportOptions = { * ``` */ metadata?: Record; + + /** + * Restore active chat sessions from external storage (e.g. localStorage). + * + * After a page refresh, pass previously persisted sessions here so the + * transport can reconnect to existing runs instead of starting new ones. + * Use `getSession()` to retrieve session state for persistence. + * + * @example + * ```ts + * new TriggerChatTransport({ + * task: "my-chat", + * accessToken, + * sessions: { + * "chat-abc": { runId: "run_123", publicAccessToken: "...", lastEventId: "42" }, + * }, + * }); + * ``` + */ + sessions?: Record; + + /** + * Called whenever a chat session's state changes. + * + * Fires when: + * - A new session is created (after triggering a task) + * - A turn completes (lastEventId updated) + * - A session is removed (run ended or input stream send failed) — `session` will be `null` + * + * Use this to persist session state for reconnection after page refreshes, + * without needing to call `getSession()` manually. + * + * @example + * ```ts + * new TriggerChatTransport({ + * task: "my-chat", + * accessToken, + * onSessionChange: (chatId, session) => { + * if (session) { + * localStorage.setItem(`session:${chatId}`, JSON.stringify(session)); + * } else { + * localStorage.removeItem(`session:${chatId}`); + * } + * }, + * }); + * ``` + */ + onSessionChange?: ( + chatId: string, + session: { runId: string; publicAccessToken: string; lastEventId?: string } | null + ) => void; }; /** @@ -151,6 +202,12 @@ export class TriggerChatTransport implements ChatTransport { private readonly extraHeaders: Record; private readonly streamTimeoutSeconds: number; private readonly defaultMetadata: Record | undefined; + private _onSessionChange: + | (( + chatId: string, + session: { runId: string; publicAccessToken: string; lastEventId?: string } | null + ) => void) + | undefined; private sessions: Map = new Map(); @@ -165,6 +222,18 @@ export class TriggerChatTransport implements ChatTransport { this.extraHeaders = options.headers ?? {}; this.streamTimeoutSeconds = options.streamTimeoutSeconds ?? DEFAULT_STREAM_TIMEOUT_SECONDS; this.defaultMetadata = options.metadata; + this._onSessionChange = options.onSessionChange; + + // Restore sessions from external storage + if (options.sessions) { + for (const [chatId, session] of Object.entries(options.sessions)) { + this.sessions.set(chatId, { + runId: session.runId, + publicAccessToken: session.publicAccessToken, + lastEventId: session.lastEventId, + }); + } + } } sendMessages = async ( @@ -216,6 +285,7 @@ export class TriggerChatTransport implements ChatTransport { } catch { // If sending fails (run died, etc.), fall through to trigger a new run. this.sessions.delete(chatId); + this.notifySessionChange(chatId, null); } } @@ -236,10 +306,12 @@ export class TriggerChatTransport implements ChatTransport { ? (triggerResponse as { publicAccessToken?: string }).publicAccessToken : undefined; - this.sessions.set(chatId, { + const newSession: ChatSessionState = { runId, publicAccessToken: publicAccessToken ?? currentToken, - }); + }; + this.sessions.set(chatId, newSession); + this.notifySessionChange(chatId, newSession); return this.subscribeToStream( runId, publicAccessToken ?? currentToken, @@ -261,6 +333,62 @@ export class TriggerChatTransport implements ChatTransport { return this.subscribeToStream(session.runId, session.publicAccessToken, undefined, options.chatId); }; + /** + * Get the current session state for a chat, suitable for external persistence. + * + * Returns `undefined` if no active session exists for this chatId. + * Persist the returned value to localStorage so it can be restored + * after a page refresh via `restoreSession()`. + * + * @example + * ```ts + * const session = transport.getSession(chatId); + * if (session) { + * localStorage.setItem(`session:${chatId}`, JSON.stringify(session)); + * } + * ``` + */ + getSession = (chatId: string): { runId: string; publicAccessToken: string; lastEventId?: string } | undefined => { + const session = this.sessions.get(chatId); + if (!session) return undefined; + return { + runId: session.runId, + publicAccessToken: session.publicAccessToken, + lastEventId: session.lastEventId, + }; + }; + + /** + * Update the `onSessionChange` callback. + * Useful for React hooks that need to update the callback without recreating the transport. + */ + setOnSessionChange( + callback: + | (( + chatId: string, + session: { runId: string; publicAccessToken: string; lastEventId?: string } | null + ) => void) + | undefined + ): void { + this._onSessionChange = callback; + } + + private notifySessionChange( + chatId: string, + session: ChatSessionState | null + ): void { + if (!this._onSessionChange) return; + if (session) { + this._onSessionChange(chatId, { + runId: session.runId, + publicAccessToken: session.publicAccessToken, + lastEventId: session.lastEventId, + }); + } else { + this._onSessionChange(chatId, null); + } + } + private subscribeToStream( runId: string, accessToken: string, @@ -331,6 +459,7 @@ export class TriggerChatTransport implements ChatTransport { // the next message via input streams. if (chatId && !combinedSignal.aborted) { this.sessions.delete(chatId); + this.notifySessionChange(chatId, null); } controller.close(); return; @@ -348,6 +477,7 @@ export class TriggerChatTransport implements ChatTransport { session.lastEventId = value.id; } + // Guard against heartbeat or malformed SSE events if (value.chunk != null && typeof value.chunk === "object") { const chunk = value.chunk as Record; @@ -363,6 +493,10 @@ export class TriggerChatTransport implements ChatTransport { } if (chunk.type === "__trigger_turn_complete" && chatId) { + // Notify with updated lastEventId before closing + if (session) { + this.notifySessionChange(chatId, session); + } internalAbort.abort(); try { controller.close(); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 82f1780ca..51cd5b8d9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2181,6 +2181,12 @@ importers: '@ai-sdk/react': specifier: ^3.0.0 version: 3.0.170(react@19.1.0)(zod@3.25.76) + '@prisma/adapter-pg': + specifier: ^7.4.2 + version: 7.4.2 + '@prisma/client': + specifier: ^7.4.2 + version: 7.4.2(prisma@7.4.2(@types/react@19.0.12)(better-sqlite3@11.10.0)(magicast@0.3.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.5.4))(typescript@5.5.4) '@trigger.dev/sdk': specifier: workspace:* version: link:../../packages/trigger-sdk @@ -2190,6 +2196,9 @@ importers: next: specifier: 15.3.3 version: 15.3.3(@opentelemetry/api@1.9.0)(@playwright/test@1.37.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + pg: + specifier: ^8.16.3 + version: 8.16.3 react: specifier: ^19.0.0 version: 19.1.0 @@ -2218,6 +2227,9 @@ importers: '@types/react-dom': specifier: ^19 version: 19.0.4(@types/react@19.2.14) + prisma: + specifier: ^7.4.2 + version: 7.4.2(@types/react@19.0.12)(better-sqlite3@11.10.0)(magicast@0.3.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.5.4) tailwindcss: specifier: ^4 version: 4.0.17 @@ -4157,18 +4169,30 @@ packages: '@changesets/write@0.2.3': resolution: {integrity: sha512-Dbamr7AIMvslKnNYsLFafaVORx4H0pvCA2MHqgtNCySMe1blImEyAEOzDmcgKAkgz4+uwoLz7demIrX+JBr/Xw==} + '@chevrotain/cst-dts-gen@10.5.0': + resolution: {integrity: sha512-lhmC/FyqQ2o7pGK4Om+hzuDrm9rhFYIJ/AXoQBeongmn870Xeb0L6oGEiuR8nohFNL5sMaQEJWCxr1oIVIVXrw==} + '@chevrotain/cst-dts-gen@11.0.3': resolution: {integrity: sha512-BvIKpRLeS/8UbfxXxgC33xOumsacaeCKAjAeLyOn7Pcp95HiRbrpl14S+9vaZLolnbssPIUuiUd8IvgkRyt6NQ==} + '@chevrotain/gast@10.5.0': + resolution: {integrity: sha512-pXdMJ9XeDAbgOWKuD1Fldz4ieCs6+nLNmyVhe2gZVqoO7v8HXuHYs5OV2EzUtbuai37TlOAQHrTDvxMnvMJz3A==} + '@chevrotain/gast@11.0.3': resolution: {integrity: sha512-+qNfcoNk70PyS/uxmj3li5NiECO+2YKZZQMbmjTqRI3Qchu8Hig/Q9vgkHpI3alNjr7M+a2St5pw5w5F6NL5/Q==} '@chevrotain/regexp-to-ast@11.0.3': resolution: {integrity: sha512-1fMHaBZxLFvWI067AVbGJav1eRY7N8DDvYCTwGBiE/ytKBgP8azTdgyrKyWZ9Mfh09eHWb5PgTSO8wi7U824RA==} + '@chevrotain/types@10.5.0': + resolution: {integrity: sha512-f1MAia0x/pAVPWH/T73BJVyO2XU5tI4/iE7cnxb7tqdNTNhQI3Uq3XkqcoteTmD4t1aM0LbHCJOhgIDn07kl2A==} + '@chevrotain/types@11.0.3': resolution: {integrity: sha512-gsiM3G8b58kZC2HaWR50gu6Y1440cHiJ+i3JUvcp/35JchYejb2+5MVeJK0iKThYpAa/P2PYFV4hoi44HD+aHQ==} + '@chevrotain/utils@10.5.0': + resolution: {integrity: sha512-hBzuU5+JjB2cqNZyszkDHZgOSrUUT8V3dhgRl8Q9Gp6dAj/H5+KILGjbhDpc3Iy9qmqlm/akuOI2ut9VUtzJxQ==} + '@chevrotain/utils@11.0.3': resolution: {integrity: sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ==} @@ -4362,6 +4386,20 @@ packages: '@electric-sql/client@1.0.14': resolution: {integrity: sha512-LtPAfeMxXRiYS0hyDQ5hue2PjljUiK9stvzsVyVb4nwxWQxfOWTSF42bHTs/o5i3x1T4kAQ7mwHpxa4A+f8X7Q==} + '@electric-sql/pglite-socket@0.0.20': + resolution: {integrity: sha512-J5nLGsicnD9wJHnno9r+DGxfcZWh+YJMCe0q/aCgtG6XOm9Z7fKeite8IZSNXgZeGltSigM9U/vAWZQWdgcSFg==} + hasBin: true + peerDependencies: + '@electric-sql/pglite': 0.3.15 + + '@electric-sql/pglite-tools@0.2.20': + resolution: {integrity: sha512-BK50ZnYa3IG7ztXhtgYf0Q7zijV32Iw1cYS8C+ThdQlwx12V5VZ9KRJ42y82Hyb4PkTxZQklVQA9JHyUlex33A==} + peerDependencies: + '@electric-sql/pglite': 0.3.15 + + '@electric-sql/pglite@0.3.15': + resolution: {integrity: sha512-Cj++n1Mekf9ETfdc16TlDi+cDDQF0W7EcbyRHYOAeZdsAe8M/FJg18itDTSwyHfar2WIezawM9o0EKaRGVKygQ==} + '@electric-sql/react@0.3.5': resolution: {integrity: sha512-qPrlF3BsRg5L8zAn1sLGzc3pkswfEHyQI3lNOu7Xllv1DBx85RvHR1zgGGPAUfC8iwyWupQu9pFPE63GdbeuhA==} peerDependencies: @@ -5910,6 +5948,10 @@ packages: '@cfworker/json-schema': optional: true + '@mrleebo/prisma-ast@0.13.1': + resolution: {integrity: sha512-XyroGQXcHrZdvmrGJvsA9KNeOOgGMg1Vg9OlheUsBOSKznLMDl+YChxbkboRHvtFYJEMRYmlV3uoo/njCw05iw==} + engines: {node: '>=16'} + '@msgpack/msgpack@3.0.0-beta2': resolution: {integrity: sha512-y+l1PNV0XDyY8sM3YtuMLK5vE3/hkfId+Do8pLo/OPxfxuFAUwcGz3oiiUuV46/aBpwTzZ+mRWVMtlSKbradhw==} engines: {node: '>= 14'} @@ -6925,9 +6967,15 @@ packages: '@prisma/adapter-pg@6.20.0-integration-next.8': resolution: {integrity: sha512-5+ZjSPMzyfDYMmWLH1IaQIOQGa8eJrqEz5A9V4vS4+b6LV6qvCOHjqlnbRQ5IKSNCwFP055SJ54RsPES+0jOyA==} + '@prisma/adapter-pg@7.4.2': + resolution: {integrity: sha512-oUo2Zhe9Tf6YwVL8kLPuOLTK1Z2pwi/Ua77t2PuGyBan2w7shRKqHvYK+3XXmRH9RWhPJ4SMtHZKpNo6Ax/4bQ==} + '@prisma/client-runtime-utils@6.20.0-integration-next.8': resolution: {integrity: sha512-prENLjPislFvRWDHNgXmg9yzixQYsFPVQGtDv5zIMs4pV2KPdNc5pCiZ3n77hAinvqGJVafASa+eU4TfpVphdA==} + '@prisma/client-runtime-utils@7.4.2': + resolution: {integrity: sha512-cID+rzOEb38VyMsx5LwJMEY4NGIrWCNpKu/0ImbeooQ2Px7TI+kOt7cm0NelxUzF2V41UVVXAmYjANZQtCu1/Q==} + '@prisma/client@4.9.0': resolution: {integrity: sha512-bz6QARw54sWcbyR1lLnF2QHvRW5R/Jxnbbmwh3u+969vUKXtBkXgSgjDA85nji31ZBlf7+FrHDy5x+5ydGyQDg==} engines: {node: '>=14.17'} @@ -6985,6 +7033,18 @@ packages: typescript: optional: true + '@prisma/client@7.4.2': + resolution: {integrity: sha512-ts2mu+cQHriAhSxngO3StcYubBGTWDtu/4juZhXCUKOwgh26l+s4KD3vT2kMUzFyrYnll9u/3qWrtzRv9CGWzA==} + engines: {node: ^20.19 || ^22.12 || >=24.0} + peerDependencies: + prisma: '*' + typescript: 5.5.4 + peerDependenciesMeta: + prisma: + optional: true + typescript: + optional: true + '@prisma/config@6.14.0': resolution: {integrity: sha512-IwC7o5KNNGhmblLs23swnfBjADkacBb7wvyDXUWLwuvUQciKJZqyecU0jw0d7JRkswrj+XTL8fdr0y2/VerKQQ==} @@ -6997,6 +7057,9 @@ packages: '@prisma/config@6.20.0-integration-next.8': resolution: {integrity: sha512-nwf+tczfiGSn0tnuHmBpnK+wmaYzcC20sn9Zt8BSoJVCewJxf8ASHPxZEGgvFLl05zbCfFtq3rMc6ZnAiYjowg==} + '@prisma/config@7.4.2': + resolution: {integrity: sha512-CftBjWxav99lzY1Z4oDgomdb1gh9BJFAOmWF6P2v1xRfXqQb56DfBub+QKcERRdNoAzCb3HXy3Zii8Vb4AsXhg==} + '@prisma/debug@4.16.2': resolution: {integrity: sha512-7L7WbG0qNNZYgLpsVB8rCHCXEyHFyIycRlRDNwkVfjQmACC2OW6AWCYCbfdjQhkF/t7+S3njj8wAWAocSs+Brw==} @@ -7012,12 +7075,24 @@ packages: '@prisma/debug@6.20.0-integration-next.8': resolution: {integrity: sha512-PqUUFXf8MDoIrsKMzpF4NYqA3gHE8l/CUWVnYa4hNIbynCcEhvk7iT+6ve0u9w1TiGVUFnIVMuqFGEb2aHCuFw==} + '@prisma/debug@7.2.0': + resolution: {integrity: sha512-YSGTiSlBAVJPzX4ONZmMotL+ozJwQjRmZweQNIq/ER0tQJKJynNkRB3kyvt37eOfsbMCXk3gnLF6J9OJ4QWftw==} + + '@prisma/debug@7.4.2': + resolution: {integrity: sha512-aP7qzu+g/JnbF6U69LMwHoUkELiserKmWsE2shYuEpNUJ4GrtxBCvZwCyCBHFSH2kLTF2l1goBlBh4wuvRq62w==} + + '@prisma/dev@0.20.0': + resolution: {integrity: sha512-ovlBYwWor0OzG+yH4J3Ot+AneD818BttLA+Ii7wjbcLHUrnC4tbUPVGyNd3c/+71KETPKZfjhkTSpdS15dmXNQ==} + '@prisma/driver-adapter-utils@6.16.0': resolution: {integrity: sha512-dsRHvEnifJ3xqpMKGBy1jRwR8yc+7Ko4TcHrdTQJIfq6NYN2gNoOf0k91hcbzs5AH19wDxjuHXCveklWq5AJdA==} '@prisma/driver-adapter-utils@6.20.0-integration-next.8': resolution: {integrity: sha512-TXpFugr3sCl2bHechoG3p9mvlq2Z3GgA0Cp73lUOEWQyUuoG8NW/4UA56Ax1r5fBUAs9hKbr20Ld6wKCZhnz8Q==} + '@prisma/driver-adapter-utils@7.4.2': + resolution: {integrity: sha512-REdjFpT/ye9KdDs+CXAXPIbMQkVLhne9G5Pe97sNY4Ovx4r2DAbWM9hOFvvB1Oq8H8bOCdu0Ri3AoGALquQqVw==} + '@prisma/engines-version@4.9.0-42.ceb5c99003b99c9ee2c1d2e618e359c14aef2ea5': resolution: {integrity: sha512-M16aibbxi/FhW7z1sJCX8u+0DriyQYY5AyeTH7plQm9MLnURoiyn3CZBqAyIoQ+Z1pS77usCIibYJWSgleBMBA==} @@ -7033,6 +7108,9 @@ packages: '@prisma/engines-version@6.20.0-11.next-80ee0a44bf5668992b0c909c946a755b86b56c95': resolution: {integrity: sha512-DqrQqRIgeocvWpgN7t9PymiJdV8ISSSrZCuilAtpKEaKIt4JUGIxsAdWNMRSHk188hYA2W1YFG5KvWUYBaCO1A==} + '@prisma/engines-version@7.5.0-10.94a226be1cf2967af2541cca5529f0f7ba866919': + resolution: {integrity: sha512-5FIKY3KoYQlBuZC2yc16EXfVRQ8HY+fLqgxkYfWCtKhRb3ajCRzP/rPeoSx11+NueJDANdh4hjY36mdmrTcGSg==} + '@prisma/engines@6.14.0': resolution: {integrity: sha512-LhJjqsALFEcoAtF07nSaOkVguaxw/ZsgfROIYZ8bAZDobe7y8Wy+PkYQaPOK1iLSsFgV2MhCO/eNrI1gdSOj6w==} @@ -7045,6 +7123,9 @@ packages: '@prisma/engines@6.20.0-integration-next.8': resolution: {integrity: sha512-XdzTxN0PFLIW2DcprG9xlMy39FrsjxW5J2qtHQ58FBtbllHSZGD0pK2nzATw5dRh7nGhmX+uNA02cqHv5oND3A==} + '@prisma/engines@7.4.2': + resolution: {integrity: sha512-B+ZZhI4rXlzjVqRw/93AothEKOU5/x4oVyJFGo9RpHPnBwaPwk4Pi0Q4iGXipKxeXPs/dqljgNBjK0m8nocOJA==} + '@prisma/fetch-engine@6.14.0': resolution: {integrity: sha512-MPzYPOKMENYOaY3AcAbaKrfvXVlvTc6iHmTXsp9RiwCX+bPyfDMqMFVUSVXPYrXnrvEzhGHfyiFy0PRLHPysNg==} @@ -7057,6 +7138,9 @@ packages: '@prisma/fetch-engine@6.20.0-integration-next.8': resolution: {integrity: sha512-zVNM5Q1hFclpqD1y7wujDzyc3l01S8ZMuP0Zddzuda4LOA7/F2enjro48VcD2/fxkBgzkkmO/quLOGnbQDKO7g==} + '@prisma/fetch-engine@7.4.2': + resolution: {integrity: sha512-f/c/MwYpdJO7taLETU8rahEstLeXfYgQGlz5fycG7Fbmva3iPdzGmjiSWHeSWIgNnlXnelUdCJqyZnFocurZuA==} + '@prisma/generator-helper@4.16.2': resolution: {integrity: sha512-bMOH7y73Ui7gpQrioFeavMQA+Tf8ksaVf8Nhs9rQNzuSg8SSV6E9baczob0L5KGZTSgYoqnrRxuo03kVJYrnIg==} @@ -7072,6 +7156,12 @@ packages: '@prisma/get-platform@6.20.0-integration-next.8': resolution: {integrity: sha512-21jEfhFpC8FuvPD7JEf1Qu02engBCBa3+1il3UiyHKcKS3Kbp9IgR+DVqqrqSWIGJg8+1oTfF/3AgbjunaQ1Ag==} + '@prisma/get-platform@7.2.0': + resolution: {integrity: sha512-k1V0l0Td1732EHpAfi2eySTezyllok9dXb6UQanajkJQzPUGi3vO2z7jdkz67SypFTdmbnyGYxvEvYZdZsMAVA==} + + '@prisma/get-platform@7.4.2': + resolution: {integrity: sha512-UTnChXRwiauzl/8wT4hhe7Xmixja9WE28oCnGpBtRejaHhvekx5kudr3R4Y9mLSA0kqGnAMeyTiKwDVMjaEVsw==} + '@prisma/instrumentation@6.11.1': resolution: {integrity: sha512-mrZOev24EDhnefmnZX7WVVT7v+r9LttPRqf54ONvj6re4XMF7wFTpK2tLJi4XHB7fFp/6xhYbgRel8YV7gQiyA==} peerDependencies: @@ -7082,6 +7172,9 @@ packages: peerDependencies: '@opentelemetry/api': ^1.8 + '@prisma/query-plan-executor@7.2.0': + resolution: {integrity: sha512-EOZmNzcV8uJ0mae3DhTsiHgoNCuu1J9mULQpGCh62zN3PxPTd+qI9tJvk5jOst8WHKQNwJWR3b39t0XvfBB0WQ==} + '@prisma/studio-core-licensed@0.6.0': resolution: {integrity: sha512-LNC8ohLosuWz6n9oKNqfR5Ep/JYiPavk4RxrU6inOS4LEvMQts8N+Vtt7NAB9i06BaiIRKnPsg1Hcaao5pRjSw==} peerDependencies: @@ -7089,6 +7182,13 @@ packages: react: ^18.0.0 || ^19.0.0 react-dom: ^18.0.0 || ^19.0.0 + '@prisma/studio-core@0.13.1': + resolution: {integrity: sha512-agdqaPEePRHcQ7CexEfkX1RvSH9uWDb6pXrZnhCRykhDFAV0/0P3d07WtfiY8hZWb7oRU4v+NkT4cGFHkQJIPg==} + peerDependencies: + '@types/react': ^18.0.0 || ^19.0.0 + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + '@protobuf-ts/runtime@2.11.1': resolution: {integrity: sha512-KuDaT1IfHkugM2pyz+FwiY80ejWrkH1pAtOBOZFuR6SXEFTsnb/jiQWQ1rCIrcKx2BtyxnxW6BWwsVSA/Ie+WQ==} @@ -11867,6 +11967,10 @@ packages: aws-sign2@0.7.0: resolution: {integrity: sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==} + aws-ssl-profiles@1.1.2: + resolution: {integrity: sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==} + engines: {node: '>= 6.0.0'} + aws4@1.12.0: resolution: {integrity: sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg==} @@ -12240,6 +12344,9 @@ packages: peerDependencies: chevrotain: ^11.0.0 + chevrotain@10.5.0: + resolution: {integrity: sha512-Pkv5rBY3+CsHOYfV5g/Vs5JY9WTHHDEKOlohI2XeygaZhUeqhAlldZ8Hz9cRmxu709bvS08YzxHdTPHhffc13A==} + chevrotain@11.0.3: resolution: {integrity: sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw==} @@ -14131,6 +14238,10 @@ packages: resolution: {integrity: sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==} engines: {node: '>=14'} + foreground-child@3.3.1: + resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} + engines: {node: '>=14'} + forever-agent@0.6.1: resolution: {integrity: sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==} @@ -14254,6 +14365,9 @@ packages: functions-have-names@1.2.3: resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + generate-function@2.3.1: + resolution: {integrity: sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==} + generic-names@4.0.0: resolution: {integrity: sha512-ySFolZQfw9FoDb3ed9d80Cm9f0+r7qj+HJkWjeD9RBfpxEVTlVhol+gvaQB/78WbwYfbnNh8nWHHBSlg072y6A==} @@ -14273,6 +14387,9 @@ packages: resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==} engines: {node: '>=6'} + get-port-please@3.2.0: + resolution: {integrity: sha512-I9QVvBw5U/hw3RmWpYKRumUeaDgxTPd401x364rLmWBJcOQ753eov1eTgzDqRG9bqFIfDc7gfzcQEWrUri3o1A==} + get-port@5.1.1: resolution: {integrity: sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ==} engines: {node: '>=8'} @@ -14422,6 +14539,9 @@ packages: resolution: {integrity: sha512-rEDCuqUQ4tbD78TpzsMtt5OIf0cBCSDWSJtUDaF6JsAh+k0v9r++NzxNEG87oDZx9ZwGhD8DaezR2L/yrw0Jdw==} engines: {node: '>=10'} + grammex@3.1.12: + resolution: {integrity: sha512-6ufJOsSA7LcQehIJNCO7HIBykfM7DXQual0Ny780/DEcJIpBlHRvcqEBWGPYd7hrXL2GJ3oJI1MIhaXjWmLQOQ==} + grapheme-splitter@1.0.4: resolution: {integrity: sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==} @@ -14434,6 +14554,9 @@ packages: engines: {node: '>=14.0.0'} hasBin: true + graphmatch@1.1.1: + resolution: {integrity: sha512-5ykVn/EXM1hF0XCaWh05VbYvEiOL2lY1kBxZtaYsyvjp7cmWOU1XsAdfQBwClraEofXDT197lFbXOEVMHpvQOg==} + graphql@16.6.0: resolution: {integrity: sha512-KPIBPDlW7NxrbT/eh4qPXz5FiFdL5UbaA0XUNz2Rp3Z3hqBSkbj0GVjwFDztsWVauZUWsbKHgMg++sk8UX0bkw==} engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} @@ -14562,6 +14685,10 @@ packages: hoist-non-react-statics@3.3.2: resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} + hono@4.11.4: + resolution: {integrity: sha512-U7tt8JsyrxSRKspfhtLET79pU8K+tInj5QZXs1jSugO1Vq5dFj3kmZsRldo29mTBfcjDRVRXrEZ6LS63Cog9ZA==} + engines: {node: '>=16.9.0'} + hono@4.11.8: resolution: {integrity: sha512-eVkB/CYCCei7K2WElZW9yYQFWssG0DhaDhVvr7wy5jJ22K+ck8fWW0EsLpB0sITUTvPnc97+rrbQqIr5iqiy9Q==} engines: {node: '>=16.9.0'} @@ -14616,6 +14743,9 @@ packages: resolution: {integrity: sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==} engines: {node: '>=0.8', npm: '>=1.3.7'} + http-status-codes@2.3.0: + resolution: {integrity: sha512-RJ8XvFvpPM/Dmc5SV+dC4y5PCeOhT3x1Hq0NU3rjGeg5a/CqlhZ7uudknPwZFz4aeAXDcbAyaeP7GAo9lvngtA==} + https-proxy-agent@5.0.1: resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} engines: {node: '>= 6'} @@ -14947,6 +15077,9 @@ packages: is-promise@4.0.0: resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + is-property@1.0.2: + resolution: {integrity: sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==} + is-reference@3.0.3: resolution: {integrity: sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==} @@ -15621,6 +15754,10 @@ packages: resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==} engines: {node: '>=12'} + lru.min@1.1.4: + resolution: {integrity: sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA==} + engines: {bun: '>=1.0.0', deno: '>=1.30.0', node: '>=8.0.0'} + lucide-react@0.229.0: resolution: {integrity: sha512-b0/KSFXhPi++vUbnYEDUgP8Z8Rw9MQpRfBr+dRZNPMT3FD1HrVgMHXhSpkm9ZrrEtuqIfHf/O+tAGmw4WOmIog==} peerDependencies: @@ -16263,9 +16400,17 @@ packages: resolution: {integrity: sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==} hasBin: true + mysql2@3.15.3: + resolution: {integrity: sha512-FBrGau0IXmuqg4haEZRBfHNWB5mUARw6hNwPDXXGg0XzVJ50mr/9hb267lvpVMnhZ1FON3qNd4Xfcez1rbFwSg==} + engines: {node: '>= 8.0'} + mz@2.7.0: resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + named-placeholders@1.1.6: + resolution: {integrity: sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==} + engines: {node: '>=8.0.0'} + nan@2.23.1: resolution: {integrity: sha512-r7bBUGKzlqk8oPBDYxt6Z0aEdF1G1rwlMcLk8LCOMbOzf0mG+JUfUzG4fIMWwHWP0iyaLWEQZJmtB7nOHEm/qw==} @@ -17545,6 +17690,19 @@ packages: typescript: optional: true + prisma@7.4.2: + resolution: {integrity: sha512-2bP8Ruww3Q95Z2eH4Yqh4KAENRsj/SxbdknIVBfd6DmjPwmpsC4OVFMLOeHt6tM3Amh8ebjvstrUz3V/hOe1dA==} + engines: {node: ^20.19 || ^22.12 || >=24.0} + hasBin: true + peerDependencies: + better-sqlite3: '>=9.0.0' + typescript: 5.5.4 + peerDependenciesMeta: + better-sqlite3: + optional: true + typescript: + optional: true + prismjs@1.29.0: resolution: {integrity: sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==} engines: {node: '>=6'} @@ -18031,6 +18189,9 @@ packages: regex@6.0.1: resolution: {integrity: sha512-uorlqlzAKjKQZ5P+kTJr3eeJGSVroLKoHmquUj4zHWuR+hEyNqlXsSKlYYF5F4NI6nl7tWCs0apKJ0lmfsXAPA==} + regexp-to-ast@0.5.0: + resolution: {integrity: sha512-tlbJqcMHnPKI9zSrystikWKwHkBqu2a/Sgw01h3zFjvYrMxEDYHzzoMZnUrbIfpTFEsoRnnviOXNCzFiSc54Qw==} + regexp.prototype.flags@1.4.3: resolution: {integrity: sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==} engines: {node: '>= 0.4'} @@ -18103,6 +18264,9 @@ packages: remark-stringify@11.0.0: resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==} + remeda@2.33.4: + resolution: {integrity: sha512-ygHswjlc/opg2VrtiYvUOPLjxjtdKvjGz1/plDhkG66hjNjFr1xmfrs2ClNFo/E6TyUFiwYNh53bKV26oBoMGQ==} + remend@1.2.1: resolution: {integrity: sha512-4wC12bgXsfKAjF1ewwkNIQz5sqewz/z1xgIgjEMb3r1pEytQ37F0Cm6i+OhbTWEvguJD7lhOUJhK5fSasw9f0w==} @@ -18445,6 +18609,9 @@ packages: resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} engines: {node: '>= 18'} + seq-queue@0.0.5: + resolution: {integrity: sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==} + serialize-javascript@6.0.1: resolution: {integrity: sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==} @@ -18717,6 +18884,10 @@ packages: resolution: {integrity: sha512-mkpF+RG402P66VMsnQkWewTRzDBWfu9iLbOfxaW/nAKOS/2A9MheQmcU5cmX0D0At9azrorZwpvcBRNNBozACQ==} hasBin: true + sqlstring@2.3.3: + resolution: {integrity: sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==} + engines: {node: '>= 0.6'} + sqs-consumer@7.5.0: resolution: {integrity: sha512-aY3akgMjuK1aj4E7ZVAURUUnC8aNgUBES+b4SN+6ccMmJhi37MamWl7g1JbPow8sjIp1fBPz1bXCCDJmtjOTAg==} engines: {node: '>=18.0.0'} @@ -18769,6 +18940,9 @@ packages: resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} engines: {node: '>= 0.8'} + std-env@3.10.0: + resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + std-env@3.7.0: resolution: {integrity: sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==} @@ -19870,8 +20044,8 @@ packages: typescript: optional: true - valibot@1.3.1: - resolution: {integrity: sha512-sfdRir/QFM0JaF22hqTroPc5xy4DimuGQVKFrzF1YfGwaS1nJot3Y8VqMdLO2Lg27fMzat2yD3pY5PbAYO39Gg==} + valibot@1.2.0: + resolution: {integrity: sha512-mm1rxUsmOxzrwnX5arGS+U4T25RdvpPjPN4yR0u9pUBov9+zGVtO84tif1eY4r6zWxVxu3KzIyknJy3rxfRZZg==} peerDependencies: typescript: 5.5.4 peerDependenciesMeta: @@ -20357,6 +20531,9 @@ packages: yup@1.7.0: resolution: {integrity: sha512-VJce62dBd+JQvoc+fCVq+KZfPHr+hXaxCcVgotfwWvlR0Ja3ffYKaJBT8rptPOSKOGJDCUnW2C2JWpud7aRP6Q==} + zeptomatch@2.1.0: + resolution: {integrity: sha512-KiGErG2J0G82LSpniV0CtIzjlJ10E04j02VOudJsPyPwNZgGnRKQy7I1R7GMyg/QswnE4l7ohSGrQbQbjXPPDA==} + zip-stream@6.0.1: resolution: {integrity: sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==} engines: {node: '>= 14'} @@ -23070,12 +23247,23 @@ snapshots: human-id: 1.0.2 prettier: 2.8.8 + '@chevrotain/cst-dts-gen@10.5.0': + dependencies: + '@chevrotain/gast': 10.5.0 + '@chevrotain/types': 10.5.0 + lodash: 4.17.23 + '@chevrotain/cst-dts-gen@11.0.3': dependencies: '@chevrotain/gast': 11.0.3 '@chevrotain/types': 11.0.3 lodash-es: 4.18.1 + '@chevrotain/gast@10.5.0': + dependencies: + '@chevrotain/types': 10.5.0 + lodash: 4.17.23 + '@chevrotain/gast@11.0.3': dependencies: '@chevrotain/types': 11.0.3 @@ -23083,8 +23271,12 @@ snapshots: '@chevrotain/regexp-to-ast@11.0.3': {} + '@chevrotain/types@10.5.0': {} + '@chevrotain/types@11.0.3': {} + '@chevrotain/utils@10.5.0': {} + '@chevrotain/utils@11.0.3': {} '@clack/core@0.5.0': @@ -23295,6 +23487,16 @@ snapshots: optionalDependencies: '@rollup/rollup-darwin-arm64': 4.53.2 + '@electric-sql/pglite-socket@0.0.20(@electric-sql/pglite@0.3.15)': + dependencies: + '@electric-sql/pglite': 0.3.15 + + '@electric-sql/pglite-tools@0.2.20(@electric-sql/pglite@0.3.15)': + dependencies: + '@electric-sql/pglite': 0.3.15 + + '@electric-sql/pglite@0.3.15': {} + '@electric-sql/react@0.3.5(react@18.2.0)': dependencies: '@electric-sql/client': 0.4.0 @@ -24002,6 +24204,10 @@ snapshots: dependencies: hono: 4.5.11 + '@hono/node-server@1.19.9(hono@4.11.4)': + dependencies: + hono: 4.11.4 + '@hono/node-server@1.19.9(hono@4.11.8)': dependencies: hono: 4.11.8 @@ -24516,6 +24722,11 @@ snapshots: transitivePeerDependencies: - supports-color + '@mrleebo/prisma-ast@0.13.1': + dependencies: + chevrotain: 10.5.0 + lilconfig: 2.1.0 + '@msgpack/msgpack@3.0.0-beta2': {} '@neondatabase/serverless@0.9.5': @@ -25576,8 +25787,18 @@ snapshots: transitivePeerDependencies: - pg-native + '@prisma/adapter-pg@7.4.2': + dependencies: + '@prisma/driver-adapter-utils': 7.4.2 + pg: 8.16.3 + postgres-array: 3.0.4 + transitivePeerDependencies: + - pg-native + '@prisma/client-runtime-utils@6.20.0-integration-next.8': {} + '@prisma/client-runtime-utils@7.4.2': {} + '@prisma/client@4.9.0(prisma@6.14.0(magicast@0.3.5)(typescript@5.5.4))': dependencies: '@prisma/engines-version': 4.9.0-42.ceb5c99003b99c9ee2c1d2e618e359c14aef2ea5 @@ -25606,6 +25827,13 @@ snapshots: prisma: 6.20.0-integration-next.8(@types/react@19.2.14)(magicast@0.3.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.5.4) typescript: 5.5.4 + '@prisma/client@7.4.2(prisma@7.4.2(@types/react@19.0.12)(better-sqlite3@11.10.0)(magicast@0.3.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.5.4))(typescript@5.5.4)': + dependencies: + '@prisma/client-runtime-utils': 7.4.2 + optionalDependencies: + prisma: 7.4.2(@types/react@19.0.12)(better-sqlite3@11.10.0)(magicast@0.3.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.5.4) + typescript: 5.5.4 + '@prisma/config@6.14.0(magicast@0.3.5)': dependencies: c12: 3.1.0(magicast@0.3.5) @@ -25642,6 +25870,15 @@ snapshots: transitivePeerDependencies: - magicast + '@prisma/config@7.4.2(magicast@0.3.5)': + dependencies: + c12: 3.1.0(magicast@0.3.5) + deepmerge-ts: 7.1.5 + effect: 3.18.4 + empathic: 2.0.0 + transitivePeerDependencies: + - magicast + '@prisma/debug@4.16.2': dependencies: '@types/debug': 4.1.8 @@ -25658,6 +25895,32 @@ snapshots: '@prisma/debug@6.20.0-integration-next.8': {} + '@prisma/debug@7.2.0': {} + + '@prisma/debug@7.4.2': {} + + '@prisma/dev@0.20.0(typescript@5.5.4)': + dependencies: + '@electric-sql/pglite': 0.3.15 + '@electric-sql/pglite-socket': 0.0.20(@electric-sql/pglite@0.3.15) + '@electric-sql/pglite-tools': 0.2.20(@electric-sql/pglite@0.3.15) + '@hono/node-server': 1.19.9(hono@4.11.4) + '@mrleebo/prisma-ast': 0.13.1 + '@prisma/get-platform': 7.2.0 + '@prisma/query-plan-executor': 7.2.0 + foreground-child: 3.3.1 + get-port-please: 3.2.0 + hono: 4.11.4 + http-status-codes: 2.3.0 + pathe: 2.0.3 + proper-lockfile: 4.1.2 + remeda: 2.33.4 + std-env: 3.10.0 + valibot: 1.2.0(typescript@5.5.4) + zeptomatch: 2.1.0 + transitivePeerDependencies: + - typescript + '@prisma/driver-adapter-utils@6.16.0': dependencies: '@prisma/debug': 6.16.0 @@ -25666,6 +25929,10 @@ snapshots: dependencies: '@prisma/debug': 6.20.0-integration-next.8 + '@prisma/driver-adapter-utils@7.4.2': + dependencies: + '@prisma/debug': 7.4.2 + '@prisma/engines-version@4.9.0-42.ceb5c99003b99c9ee2c1d2e618e359c14aef2ea5': {} '@prisma/engines-version@6.14.0-25.717184b7b35ea05dfa71a3236b7af656013e1e49': {} @@ -25676,6 +25943,8 @@ snapshots: '@prisma/engines-version@6.20.0-11.next-80ee0a44bf5668992b0c909c946a755b86b56c95': {} + '@prisma/engines-version@7.5.0-10.94a226be1cf2967af2541cca5529f0f7ba866919': {} + '@prisma/engines@6.14.0': dependencies: '@prisma/debug': 6.14.0 @@ -25704,6 +25973,13 @@ snapshots: '@prisma/fetch-engine': 6.20.0-integration-next.8 '@prisma/get-platform': 6.20.0-integration-next.8 + '@prisma/engines@7.4.2': + dependencies: + '@prisma/debug': 7.4.2 + '@prisma/engines-version': 7.5.0-10.94a226be1cf2967af2541cca5529f0f7ba866919 + '@prisma/fetch-engine': 7.4.2 + '@prisma/get-platform': 7.4.2 + '@prisma/fetch-engine@6.14.0': dependencies: '@prisma/debug': 6.14.0 @@ -25728,6 +26004,12 @@ snapshots: '@prisma/engines-version': 6.20.0-11.next-80ee0a44bf5668992b0c909c946a755b86b56c95 '@prisma/get-platform': 6.20.0-integration-next.8 + '@prisma/fetch-engine@7.4.2': + dependencies: + '@prisma/debug': 7.4.2 + '@prisma/engines-version': 7.5.0-10.94a226be1cf2967af2541cca5529f0f7ba866919 + '@prisma/get-platform': 7.4.2 + '@prisma/generator-helper@4.16.2': dependencies: '@prisma/debug': 4.16.2 @@ -25753,6 +26035,14 @@ snapshots: dependencies: '@prisma/debug': 6.20.0-integration-next.8 + '@prisma/get-platform@7.2.0': + dependencies: + '@prisma/debug': 7.2.0 + + '@prisma/get-platform@7.4.2': + dependencies: + '@prisma/debug': 7.4.2 + '@prisma/instrumentation@6.11.1(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 @@ -25767,12 +26057,20 @@ snapshots: transitivePeerDependencies: - supports-color + '@prisma/query-plan-executor@7.2.0': {} + '@prisma/studio-core-licensed@0.6.0(@types/react@19.2.14)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: '@types/react': 19.2.14 react: 19.1.0 react-dom: 19.1.0(react@19.1.0) + '@prisma/studio-core@0.13.1(@types/react@19.0.12)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@types/react': 19.0.12 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + '@protobuf-ts/runtime@2.11.1': {} '@protobufjs/aspromise@1.1.2': {} @@ -32472,6 +32770,8 @@ snapshots: aws-sign2@0.7.0: {} + aws-ssl-profiles@1.1.2: {} + aws4@1.12.0: {} aws4fetch@1.0.18: {} @@ -32893,6 +33193,15 @@ snapshots: chevrotain: 11.0.3 lodash-es: 4.18.1 + chevrotain@10.5.0: + dependencies: + '@chevrotain/cst-dts-gen': 10.5.0 + '@chevrotain/gast': 10.5.0 + '@chevrotain/types': 10.5.0 + '@chevrotain/utils': 10.5.0 + lodash: 4.17.23 + regexp-to-ast: 0.5.0 + chevrotain@11.0.3: dependencies: '@chevrotain/cst-dts-gen': 11.0.3 @@ -35198,6 +35507,11 @@ snapshots: cross-spawn: 7.0.6 signal-exit: 4.1.0 + foreground-child@3.3.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + forever-agent@0.6.1: {} form-data-encoder@1.7.2: {} @@ -35322,6 +35636,10 @@ snapshots: functions-have-names@1.2.3: {} + generate-function@2.3.1: + dependencies: + is-property: 1.0.2 + generic-names@4.0.0: dependencies: loader-utils: 3.2.1 @@ -35345,6 +35663,8 @@ snapshots: get-nonce@1.0.1: {} + get-port-please@3.2.0: {} + get-port@5.1.1: {} get-port@7.2.0: {} @@ -35537,6 +35857,8 @@ snapshots: chalk: 4.1.2 tinygradient: 1.1.5 + grammex@3.1.12: {} + grapheme-splitter@1.0.4: {} graphile-config@0.0.1-beta.8: @@ -35569,6 +35891,8 @@ snapshots: - supports-color - typescript + graphmatch@1.1.1: {} + graphql@16.6.0: {} gunzip-maybe@1.4.2: @@ -35781,6 +36105,8 @@ snapshots: dependencies: react-is: 16.13.1 + hono@4.11.4: {} + hono@4.11.8: {} hono@4.5.11: {} @@ -35845,6 +36171,8 @@ snapshots: jsprim: 1.4.2 sshpk: 1.18.0 + http-status-codes@2.3.0: {} + https-proxy-agent@5.0.1: dependencies: agent-base: 6.0.2 @@ -36131,6 +36459,8 @@ snapshots: is-promise@4.0.0: {} + is-property@1.0.2: {} + is-reference@3.0.3: dependencies: '@types/estree': 1.0.8 @@ -36723,6 +37053,8 @@ snapshots: lru-cache@7.18.3: {} + lru.min@1.1.4: {} + lucide-react@0.229.0(react@18.2.0): dependencies: react: 18.2.0 @@ -37766,12 +38098,28 @@ snapshots: mustache@4.2.0: {} + mysql2@3.15.3: + dependencies: + aws-ssl-profiles: 1.1.2 + denque: 2.1.0 + generate-function: 2.3.1 + iconv-lite: 0.7.2 + long: 5.2.3 + lru.min: 1.1.4 + named-placeholders: 1.1.6 + seq-queue: 0.0.5 + sqlstring: 2.3.3 + mz@2.7.0: dependencies: any-promise: 1.3.0 object-assign: 4.1.1 thenify-all: 1.6.0 + named-placeholders@1.1.6: + dependencies: + lru.min: 1.1.4 + nan@2.23.1: optional: true @@ -39060,6 +39408,23 @@ snapshots: - react - react-dom + prisma@7.4.2(@types/react@19.0.12)(better-sqlite3@11.10.0)(magicast@0.3.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.5.4): + dependencies: + '@prisma/config': 7.4.2(magicast@0.3.5) + '@prisma/dev': 0.20.0(typescript@5.5.4) + '@prisma/engines': 7.4.2 + '@prisma/studio-core': 0.13.1(@types/react@19.0.12)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + mysql2: 3.15.3 + postgres: 3.4.7 + optionalDependencies: + better-sqlite3: 11.10.0 + typescript: 5.5.4 + transitivePeerDependencies: + - '@types/react' + - magicast + - react + - react-dom + prismjs@1.29.0: {} prismjs@1.30.0: {} @@ -39881,6 +40246,8 @@ snapshots: dependencies: regex-utilities: 2.3.0 + regexp-to-ast@0.5.0: {} + regexp.prototype.flags@1.4.3: dependencies: call-bind: 1.0.8 @@ -40020,7 +40387,11 @@ snapshots: mdast-util-to-markdown: 2.1.2 unified: 11.0.5 - remix-auth-email-link@2.0.2(@remix-run/server-runtime@2.17.4(typescript@5.5.4))(remix-auth@3.6.0(@remix-run/react@2.17.4(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.5.4))(@remix-run/server-runtime@2.17.4(typescript@5.5.4))): + remeda@2.33.4: {} + + remend@1.2.1: {} + + remix-auth-email-link@2.0.2(@remix-run/server-runtime@2.1.0(typescript@5.5.4))(remix-auth@3.6.0(@remix-run/react@2.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.5.4))(@remix-run/server-runtime@2.1.0(typescript@5.5.4))): dependencies: '@remix-run/server-runtime': 2.17.4(typescript@5.5.4) crypto-js: 4.1.1 @@ -40425,6 +40796,8 @@ snapshots: transitivePeerDependencies: - supports-color + seq-queue@0.0.5: {} + serialize-javascript@6.0.1: dependencies: randombytes: 2.1.0 @@ -40820,6 +41193,8 @@ snapshots: argparse: 2.0.1 nearley: 2.20.1 + sqlstring@2.3.3: {} + sqs-consumer@7.5.0(@aws-sdk/client-sqs@3.454.0): dependencies: '@aws-sdk/client-sqs': 3.454.0 @@ -40885,6 +41260,8 @@ snapshots: statuses@2.0.2: {} + std-env@3.10.0: {} + std-env@3.7.0: {} std-env@3.8.1: {} @@ -42209,7 +42586,7 @@ snapshots: optionalDependencies: typescript: 5.5.4 - valibot@1.3.1(typescript@5.5.4): + valibot@1.2.0(typescript@5.5.4): optionalDependencies: typescript: 5.5.4 @@ -42825,6 +43202,11 @@ snapshots: toposort: 2.0.2 type-fest: 2.19.0 + zeptomatch@2.1.0: + dependencies: + grammex: 3.1.12 + graphmatch: 1.1.1 + zip-stream@6.0.1: dependencies: archiver-utils: 5.0.2 diff --git a/references/ai-chat/.gitignore b/references/ai-chat/.gitignore new file mode 100644 index 000000000..30838110e --- /dev/null +++ b/references/ai-chat/.gitignore @@ -0,0 +1 @@ +lib/generated/ diff --git a/references/ai-chat/package.json b/references/ai-chat/package.json index 89ccc3688..a1d7f1848 100644 --- a/references/ai-chat/package.json +++ b/references/ai-chat/package.json @@ -6,15 +6,22 @@ "dev": "next dev --turbopack", "build": "next build", "start": "next start", - "dev:trigger": "trigger dev" + "dev:trigger": "trigger dev", + "db:migrate": "prisma migrate dev", + "db:push": "prisma db push", + "db:generate": "prisma generate", + "postinstall": "prisma generate" }, "dependencies": { "@ai-sdk/anthropic": "^3.0.0", "@ai-sdk/openai": "^3.0.0", "@ai-sdk/react": "^3.0.0", + "@prisma/adapter-pg": "^7.4.2", + "@prisma/client": "^7.4.2", "@trigger.dev/sdk": "workspace:*", "ai": "^6.0.0", "next": "15.3.3", + "pg": "^8.16.3", "react": "^19.0.0", "react-dom": "^19.0.0", "streamdown": "^2.3.0", @@ -27,6 +34,7 @@ "@types/react": "^19", "@types/react-dom": "^19", "tailwindcss": "^4", + "prisma": "^7.4.2", "trigger.dev": "workspace:*", "typescript": "^5" } diff --git a/references/ai-chat/prisma.config.ts b/references/ai-chat/prisma.config.ts new file mode 100644 index 000000000..d73df7b31 --- /dev/null +++ b/references/ai-chat/prisma.config.ts @@ -0,0 +1,12 @@ +import "dotenv/config"; +import { defineConfig, env } from "prisma/config"; + +export default defineConfig({ + schema: "prisma/schema.prisma", + migrations: { + path: "prisma/migrations", + }, + datasource: { + url: env("DATABASE_URL"), + }, +}); diff --git a/references/ai-chat/prisma/migrations/20260305112427_init/migration.sql b/references/ai-chat/prisma/migrations/20260305112427_init/migration.sql new file mode 100644 index 000000000..951cd33d9 --- /dev/null +++ b/references/ai-chat/prisma/migrations/20260305112427_init/migration.sql @@ -0,0 +1,20 @@ +-- CreateTable +CREATE TABLE "Chat" ( + "id" TEXT NOT NULL, + "title" TEXT NOT NULL, + "messages" JSONB NOT NULL DEFAULT '[]', + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Chat_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "ChatSession" ( + "id" TEXT NOT NULL, + "runId" TEXT NOT NULL, + "publicAccessToken" TEXT NOT NULL, + "lastEventId" TEXT, + + CONSTRAINT "ChatSession_pkey" PRIMARY KEY ("id") +); diff --git a/references/ai-chat/prisma/migrations/migration_lock.toml b/references/ai-chat/prisma/migrations/migration_lock.toml new file mode 100644 index 000000000..044d57cdb --- /dev/null +++ b/references/ai-chat/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (e.g., Git) +provider = "postgresql" diff --git a/references/ai-chat/prisma/schema.prisma b/references/ai-chat/prisma/schema.prisma new file mode 100644 index 000000000..4899b46f7 --- /dev/null +++ b/references/ai-chat/prisma/schema.prisma @@ -0,0 +1,23 @@ +generator client { + provider = "prisma-client" + output = "../lib/generated/prisma" +} + +datasource db { + provider = "postgresql" +} + +model Chat { + id String @id + title String + messages Json @default("[]") + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +model ChatSession { + id String @id // chatId + runId String + publicAccessToken String + lastEventId String? +} diff --git a/references/ai-chat/src/app/actions.ts b/references/ai-chat/src/app/actions.ts index 08657dd1a..ccf6319f6 100644 --- a/references/ai-chat/src/app/actions.ts +++ b/references/ai-chat/src/app/actions.ts @@ -2,5 +2,79 @@ import { chat } from "@trigger.dev/sdk/ai"; import type { aiChat } from "@/trigger/chat"; +import { prisma } from "@/lib/prisma"; export const getChatToken = async () => chat.createAccessToken("ai-chat"); + +export async function getChatList() { + const chats = await prisma.chat.findMany({ + select: { id: true, title: true, createdAt: true, updatedAt: true }, + orderBy: { updatedAt: "desc" }, + }); + return chats.map((c) => ({ + id: c.id, + title: c.title, + createdAt: c.createdAt.getTime(), + updatedAt: c.updatedAt.getTime(), + })); +} + +export async function getChatMessages(chatId: string) { + const found = await prisma.chat.findUnique({ where: { id: chatId } }); + if (!found) return []; + return found.messages as any[]; +} + +export async function saveChatMessages(chatId: string, messages: unknown[]) { + await prisma.chat.update({ + where: { id: chatId }, + data: { messages: messages as any }, + }).catch(() => {}); +} + +export async function deleteChat(chatId: string) { + await prisma.chat.delete({ where: { id: chatId } }).catch(() => {}); + await prisma.chatSession.delete({ where: { id: chatId } }).catch(() => {}); +} + +export async function updateChatTitle(chatId: string, title: string) { + await prisma.chat.update({ where: { id: chatId }, data: { title } }).catch(() => {}); +} + +export async function saveSessionAction( + chatId: string, + session: { runId: string; publicAccessToken: string; lastEventId?: string } +) { + await prisma.chatSession.upsert({ + where: { id: chatId }, + create: { + id: chatId, + runId: session.runId, + publicAccessToken: session.publicAccessToken, + lastEventId: session.lastEventId, + }, + update: { + runId: session.runId, + publicAccessToken: session.publicAccessToken, + lastEventId: session.lastEventId, + }, + }); +} + +export async function deleteSessionAction(chatId: string) { + await prisma.chatSession.delete({ where: { id: chatId } }).catch(() => {}); +} + +export async function getAllSessions() { + const sessions = await prisma.chatSession.findMany(); + const result: Record = + {}; + for (const s of sessions) { + result[s.id] = { + runId: s.runId, + publicAccessToken: s.publicAccessToken, + lastEventId: s.lastEventId ?? undefined, + }; + } + return result; +} diff --git a/references/ai-chat/src/app/page.tsx b/references/ai-chat/src/app/page.tsx index 1a003392f..cd2eac83c 100644 --- a/references/ai-chat/src/app/page.tsx +++ b/references/ai-chat/src/app/page.tsx @@ -1,9 +1,58 @@ -import { Chat } from "@/components/chat"; +"use client"; + +import type { UIMessage } from "ai"; +import { useEffect, useState } from "react"; +import { ChatApp } from "@/components/chat-app"; +import { + getChatList, + getChatMessages, + getAllSessions, +} from "@/app/actions"; + +type ChatMeta = { + id: string; + title: string; + createdAt: number; + updatedAt: number; +}; export default function Home() { + const [chatList, setChatList] = useState([]); + const [activeChatId, setActiveChatId] = useState(null); + const [initialMessages, setInitialMessages] = useState([]); + const [initialSessions, setInitialSessions] = useState< + Record + >({}); + const [loaded, setLoaded] = useState(false); + + useEffect(() => { + async function load() { + const [list, sessions] = await Promise.all([getChatList(), getAllSessions()]); + setChatList(list); + setInitialSessions(sessions); + + let firstChatId: string | null = null; + let firstMessages: UIMessage[] = []; + if (list.length > 0) { + firstChatId = list[0]!.id; + firstMessages = await getChatMessages(firstChatId); + } + + setActiveChatId(firstChatId); + setInitialMessages(firstMessages); + setLoaded(true); + } + load(); + }, []); + + if (!loaded) return null; + return ( -
- -
+ ); } diff --git a/references/ai-chat/src/components/chat-app.tsx b/references/ai-chat/src/components/chat-app.tsx new file mode 100644 index 000000000..9a8f4642a --- /dev/null +++ b/references/ai-chat/src/components/chat-app.tsx @@ -0,0 +1,155 @@ +"use client"; + +import type { UIMessage } from "ai"; +import { generateId } from "ai"; +import { useTriggerChatTransport } from "@trigger.dev/sdk/chat/react"; +import { useCallback, useEffect, useState } from "react"; +import { Chat } from "@/components/chat"; +import { ChatSidebar } from "@/components/chat-sidebar"; +import { + getChatToken, + getChatList, + getChatMessages, + deleteChat as deleteChatAction, + updateChatTitle, + saveSessionAction, + deleteSessionAction, + saveChatMessages, +} from "@/app/actions"; + +type ChatMeta = { + id: string; + title: string; + createdAt: number; + updatedAt: number; +}; + +type ChatAppProps = { + initialChatList: ChatMeta[]; + initialActiveChatId: string | null; + initialMessages: UIMessage[]; + initialSessions: Record< + string, + { runId: string; publicAccessToken: string; lastEventId?: string } + >; +}; + +export function ChatApp({ + initialChatList, + initialActiveChatId, + initialMessages, + initialSessions, +}: ChatAppProps) { + const [chatList, setChatList] = useState(initialChatList); + const [activeChatId, setActiveChatId] = useState(initialActiveChatId); + const [messages, setMessages] = useState(initialMessages); + + const handleSessionChange = useCallback( + ( + chatId: string, + session: { runId: string; publicAccessToken: string; lastEventId?: string } | null + ) => { + if (session) { + saveSessionAction(chatId, session); + } else { + deleteSessionAction(chatId); + } + }, + [] + ); + + const transport = useTriggerChatTransport({ + task: "ai-chat", + accessToken: getChatToken, + baseURL: process.env.NEXT_PUBLIC_TRIGGER_API_URL, + sessions: initialSessions, + onSessionChange: handleSessionChange, + }); + + // Load messages when active chat changes + useEffect(() => { + if (!activeChatId) { + setMessages([]); + return; + } + // Don't reload if we already have the initial messages for the initial chat + if (activeChatId === initialActiveChatId && messages === initialMessages) { + return; + } + getChatMessages(activeChatId).then(setMessages); + }, [activeChatId]); + + function handleNewChat() { + const id = generateId(); + setActiveChatId(id); + setMessages([]); + } + + function handleSelectChat(id: string) { + setActiveChatId(id); + } + + async function handleDeleteChat(id: string) { + await deleteChatAction(id); + const list = await getChatList(); + setChatList(list); + if (activeChatId === id) { + if (list.length > 0) { + setActiveChatId(list[0]!.id); + } else { + setActiveChatId(null); + } + } + } + + const handleFirstMessage = useCallback(async (chatId: string, text: string) => { + const title = text.slice(0, 40).trim() || "New chat"; + await updateChatTitle(chatId, title); + const list = await getChatList(); + setChatList(list); + }, []); + + const handleMessagesChange = useCallback(async (_chatId: string, _messages: UIMessage[]) => { + // Messages are persisted server-side via onTurnComplete. + // Refresh the chat list to update timestamps. + const list = await getChatList(); + setChatList(list); + }, []); + + return ( +
+ +
+ {activeChatId ? ( + + ) : ( +
+
+

No conversation selected

+ +
+
+ )} +
+
+ ); +} diff --git a/references/ai-chat/src/components/chat-sidebar.tsx b/references/ai-chat/src/components/chat-sidebar.tsx new file mode 100644 index 000000000..bb688e99b --- /dev/null +++ b/references/ai-chat/src/components/chat-sidebar.tsx @@ -0,0 +1,82 @@ +"use client"; + +type ChatMeta = { + id: string; + title: string; + createdAt: number; + updatedAt: number; +}; + +function timeAgo(ts: number): string { + const seconds = Math.floor((Date.now() - ts) / 1000); + if (seconds < 60) return "just now"; + const minutes = Math.floor(seconds / 60); + if (minutes < 60) return `${minutes}m ago`; + const hours = Math.floor(minutes / 60); + if (hours < 24) return `${hours}h ago`; + const days = Math.floor(hours / 24); + return `${days}d ago`; +} + +type ChatSidebarProps = { + chats: ChatMeta[]; + activeChatId: string | null; + onSelectChat: (id: string) => void; + onNewChat: () => void; + onDeleteChat: (id: string) => void; +}; + +export function ChatSidebar({ + chats, + activeChatId, + onSelectChat, + onNewChat, + onDeleteChat, +}: ChatSidebarProps) { + const sorted = [...chats].sort((a, b) => b.updatedAt - a.updatedAt); + + return ( +
+
+ +
+ +
+ {sorted.length === 0 && ( +

No conversations yet

+ )} + + {sorted.map((chat) => ( + + ))} +
+
+ ); +} diff --git a/references/ai-chat/src/components/chat.tsx b/references/ai-chat/src/components/chat.tsx index 7e9d7a266..e85361b4d 100644 --- a/references/ai-chat/src/components/chat.tsx +++ b/references/ai-chat/src/components/chat.tsx @@ -1,12 +1,11 @@ "use client"; +import type { UIMessage } from "ai"; import { useChat } from "@ai-sdk/react"; -import { useTriggerChatTransport } from "@trigger.dev/sdk/chat/react"; +import type { TriggerChatTransport } from "@trigger.dev/sdk/chat"; import { useEffect, useRef, useState } from "react"; import { Streamdown } from "streamdown"; -import { getChatToken } from "@/app/actions"; -import { MODEL_OPTIONS, DEFAULT_MODEL } from "@/trigger/chat"; -import type { aiChat } from "@/trigger/chat"; +import { MODEL_OPTIONS, DEFAULT_MODEL } from "@/lib/models"; function ToolInvocation({ part }: { part: any }) { const [expanded, setExpanded] = useState(false); @@ -71,41 +70,73 @@ function ToolInvocation({ part }: { part: any }) { ); } -export function Chat() { +type ChatProps = { + chatId: string; + initialMessages: UIMessage[]; + transport: TriggerChatTransport; + onFirstMessage?: (chatId: string, text: string) => void; + onMessagesChange?: (chatId: string, messages: UIMessage[]) => void; +}; + +export function Chat({ + chatId, + initialMessages, + transport, + onFirstMessage, + onMessagesChange, +}: ChatProps) { const [input, setInput] = useState(""); const [model, setModel] = useState(DEFAULT_MODEL); - // Track which model was used for each assistant message (keyed by the preceding user message ID) const modelByUserMsgId = useRef>(new Map()); - - const transport = useTriggerChatTransport({ - task: "ai-chat", - accessToken: getChatToken, - baseURL: process.env.NEXT_PUBLIC_TRIGGER_API_URL, - }); + const hasCalledFirstMessage = useRef(false); const { messages, sendMessage, stop, status, error } = useChat({ + id: chatId, + messages: initialMessages, transport, }); + // Notify parent of first user message (for chat metadata creation) + useEffect(() => { + if (hasCalledFirstMessage.current) return; + const firstUser = messages.find((m) => m.role === "user"); + if (firstUser) { + hasCalledFirstMessage.current = true; + const text = firstUser.parts + .filter((p: any) => p.type === "text") + .map((p: any) => p.text) + .join(" "); + onFirstMessage?.(chatId, text); + } + }, [messages, chatId, onFirstMessage]); + // Pending message to send after the current turn completes const [pendingMessage, setPendingMessage] = useState<{ text: string; model: string } | null>(null); - // Auto-send the pending message when the turn completes + // Handle turn completion: persist messages and auto-send pending message const prevStatus = useRef(status); useEffect(() => { - if (prevStatus.current === "streaming" && status === "ready" && pendingMessage) { + const turnCompleted = prevStatus.current === "streaming" && status === "ready"; + prevStatus.current = status; + + if (!turnCompleted) return; + + // Persist messages when a turn completes — this ensures the final assistant + // message content is saved (not the empty placeholder from mid-stream). + if (messages.length > 0) { + onMessagesChange?.(chatId, messages); + } + + // Auto-send the pending message + if (pendingMessage) { const { text, model: pendingMsgModel } = pendingMessage; setPendingMessage(null); pendingModel.current = pendingMsgModel; sendMessage({ text }, { metadata: { model: pendingMsgModel } }); } - prevStatus.current = status; - }, [status, sendMessage, pendingMessage]); + }, [status, messages, chatId, onMessagesChange, sendMessage, pendingMessage]); - // Build a map of assistant message index -> model used - // Each assistant message follows a user message, so we track by position function getModelForAssistantAt(index: number): string | undefined { - // Walk backwards to find the preceding user message for (let i = index - 1; i >= 0; i--) { if (messages[i]?.role === "user") { return modelByUserMsgId.current.get(messages[i].id); @@ -114,16 +145,13 @@ export function Chat() { return undefined; } - // When sending, record which model is selected for this user message const originalSendMessage = sendMessage; function trackedSendMessage(msg: Parameters[0], opts?: Parameters[1]) { - // We'll track it after the message appears — use a ref to store the pending model pendingModel.current = model; originalSendMessage(msg, opts); } const pendingModel = useRef(model); - // Track model for new user messages as they appear const trackedUserIds = useRef>(new Set()); for (const msg of messages) { if (msg.role === "user" && !trackedUserIds.current.has(msg.id)) { @@ -146,7 +174,6 @@ export function Chat() { className={`flex ${message.role === "user" ? "justify-end" : "justify-start"}`} >
- {/* Model badge for assistant messages */} {message.role === "assistant" && (
@@ -199,7 +226,6 @@ export function Chat() {
)} - {/* Queued message indicator */} {pendingMessage && (
@@ -214,20 +240,17 @@ export function Chat() { )}
- {/* Error */} {error && (
{error.message}
)} - {/* Input */}
{ e.preventDefault(); if (!input.trim()) return; if (status === "streaming") { - // Buffer the message — it will be sent when the current turn completes setPendingMessage({ text: input, model }); } else { trackedSendMessage({ text: input }, { metadata: { model } }); diff --git a/references/ai-chat/src/lib/models.ts b/references/ai-chat/src/lib/models.ts new file mode 100644 index 000000000..5261a800a --- /dev/null +++ b/references/ai-chat/src/lib/models.ts @@ -0,0 +1,8 @@ +export const MODEL_OPTIONS = [ + "gpt-4o-mini", + "gpt-4o", + "claude-sonnet-4-6", + "claude-opus-4-6", +]; + +export const DEFAULT_MODEL = "gpt-4o-mini"; diff --git a/references/ai-chat/src/lib/prisma.ts b/references/ai-chat/src/lib/prisma.ts new file mode 100644 index 000000000..5e78334aa --- /dev/null +++ b/references/ai-chat/src/lib/prisma.ts @@ -0,0 +1,15 @@ +import { PrismaPg } from "@prisma/adapter-pg"; +import { PrismaClient } from "../../lib/generated/prisma/client"; + +const globalForPrisma = globalThis as unknown as { prisma: PrismaClient | undefined }; + +function createClient() { + const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL! }); + return new PrismaClient({ adapter }); +} + +export const prisma = globalForPrisma.prisma ?? createClient(); + +if (process.env.NODE_ENV !== "production") { + globalForPrisma.prisma = prisma; +} diff --git a/references/ai-chat/src/trigger/chat.ts b/references/ai-chat/src/trigger/chat.ts index bd7069f1a..3eb6d4402 100644 --- a/references/ai-chat/src/trigger/chat.ts +++ b/references/ai-chat/src/trigger/chat.ts @@ -5,6 +5,13 @@ import { openai } from "@ai-sdk/openai"; import { anthropic } from "@ai-sdk/anthropic"; import { z } from "zod"; import os from "node:os"; +import { PrismaPg } from "@prisma/adapter-pg"; +import { PrismaClient } from "../../lib/generated/prisma/client"; + +const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL! }); +const prisma = new PrismaClient({ adapter }); + +import { DEFAULT_MODEL } from "@/lib/models"; const MODELS: Record LanguageModel> = { "gpt-4o-mini": () => openai("gpt-4o-mini"), @@ -13,9 +20,6 @@ const MODELS: Record LanguageModel> = { "claude-opus-4-6": () => anthropic("claude-opus-4-6"), }; -export const MODEL_OPTIONS = Object.keys(MODELS); -export const DEFAULT_MODEL = "gpt-4o-mini"; - function getModel(modelId?: string): LanguageModel { const factory = MODELS[modelId ?? DEFAULT_MODEL]; if (!factory) return MODELS[DEFAULT_MODEL]!(); @@ -83,6 +87,19 @@ declare const Deno: unknown; export const aiChat = chat.task({ id: "ai-chat", warmTimeoutInSeconds: 10, + onChatStart: async ({ chatId }) => { + await prisma.chat.upsert({ + where: { id: chatId }, + create: { id: chatId, title: "New chat" }, + update: {}, + }); + }, + onTurnComplete: async ({ chatId, uiMessages }) => { + await prisma.chat.update({ + where: { id: chatId }, + data: { messages: uiMessages as any }, + }); + }, run: async ({ messages, clientData, stopSignal }) => { const { model: modelId } = z .object({ model: z.string().optional() }) diff --git a/references/ai-chat/trigger.config.ts b/references/ai-chat/trigger.config.ts index 4412bfc93..334df4bc1 100644 --- a/references/ai-chat/trigger.config.ts +++ b/references/ai-chat/trigger.config.ts @@ -1,7 +1,15 @@ import { defineConfig } from "@trigger.dev/sdk"; +import { prismaExtension } from "@trigger.dev/build/extensions/prisma"; export default defineConfig({ project: process.env.TRIGGER_PROJECT_REF!, dirs: ["./src/trigger"], maxDuration: 300, + build: { + extensions: [ + prismaExtension({ + mode: "modern", + }), + ], + }, });