build full example with persisting messages, adding necessary hooks, and documenting it all
This commit is contained in:
@@ -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<TIdentifier extends string> = Omit<
|
||||
TaskOptions<TIdentifier, ChatTaskWirePayload, unknown>,
|
||||
"run"
|
||||
@@ -394,6 +435,35 @@ export type ChatTaskOptions<TIdentifier extends string> = Omit<
|
||||
*/
|
||||
run: (payload: ChatTaskRunPayload) => Promise<unknown>;
|
||||
|
||||
/**
|
||||
* 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> | 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> | 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<TIdentifier extends string>(
|
||||
): Task<TIdentifier, ChatTaskWirePayload, unknown> {
|
||||
const {
|
||||
run: userRun,
|
||||
onChatStart,
|
||||
onTurnComplete,
|
||||
maxTurns = 100,
|
||||
turnTimeout = "1h",
|
||||
warmTimeoutInSeconds = 30,
|
||||
@@ -478,6 +550,10 @@ function chatTask<TIdentifier extends string>(
|
||||
// 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<TIdentifier extends string>(
|
||||
// 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<TIdentifier extends string>(
|
||||
// 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<TIdentifier extends string>(
|
||||
|
||||
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<TIdentifier extends string>(
|
||||
|
||||
// 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<TIdentifier extends string>(
|
||||
}
|
||||
|
||||
// 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<TIdentifier extends string>(
|
||||
* 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,
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
* ```
|
||||
*/
|
||||
|
||||
import { useRef } from "react";
|
||||
import { useEffect, useRef } from "react";
|
||||
import {
|
||||
TriggerChatTransport,
|
||||
type TriggerChatTransportOptions,
|
||||
@@ -57,6 +57,9 @@ export type UseTriggerChatTransportOptions<TTask extends AnyTask = AnyTask> = 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<TTask extends AnyTask = AnyTask>(
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -100,6 +100,57 @@ export type TriggerChatTransportOptions = {
|
||||
* ```
|
||||
*/
|
||||
metadata?: Record<string, unknown>;
|
||||
|
||||
/**
|
||||
* 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<string, { runId: string; publicAccessToken: string; lastEventId?: string }>;
|
||||
|
||||
/**
|
||||
* 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<UIMessage> {
|
||||
private readonly extraHeaders: Record<string, string>;
|
||||
private readonly streamTimeoutSeconds: number;
|
||||
private readonly defaultMetadata: Record<string, unknown> | undefined;
|
||||
private _onSessionChange:
|
||||
| ((
|
||||
chatId: string,
|
||||
session: { runId: string; publicAccessToken: string; lastEventId?: string } | null
|
||||
) => void)
|
||||
| undefined;
|
||||
|
||||
private sessions: Map<string, ChatSessionState> = new Map();
|
||||
|
||||
@@ -165,6 +222,18 @@ export class TriggerChatTransport implements ChatTransport<UIMessage> {
|
||||
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<UIMessage> {
|
||||
} 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<UIMessage> {
|
||||
? (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<UIMessage> {
|
||||
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<UIMessage> {
|
||||
// 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<UIMessage> {
|
||||
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<string, unknown>;
|
||||
@@ -363,6 +493,10 @@ export class TriggerChatTransport implements ChatTransport<UIMessage> {
|
||||
}
|
||||
|
||||
if (chunk.type === "__trigger_turn_complete" && chatId) {
|
||||
// Notify with updated lastEventId before closing
|
||||
if (session) {
|
||||
this.notifySessionChange(chatId, session);
|
||||
}
|
||||
internalAbort.abort();
|
||||
try {
|
||||
controller.close();
|
||||
|
||||
Generated
+386
-4
@@ -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
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
lib/generated/
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"),
|
||||
},
|
||||
});
|
||||
@@ -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")
|
||||
);
|
||||
@@ -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"
|
||||
@@ -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?
|
||||
}
|
||||
@@ -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<typeof aiChat>("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<string, { runId: string; publicAccessToken: string; lastEventId?: string }> =
|
||||
{};
|
||||
for (const s of sessions) {
|
||||
result[s.id] = {
|
||||
runId: s.runId,
|
||||
publicAccessToken: s.publicAccessToken,
|
||||
lastEventId: s.lastEventId ?? undefined,
|
||||
};
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -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<ChatMeta[]>([]);
|
||||
const [activeChatId, setActiveChatId] = useState<string | null>(null);
|
||||
const [initialMessages, setInitialMessages] = useState<UIMessage[]>([]);
|
||||
const [initialSessions, setInitialSessions] = useState<
|
||||
Record<string, { runId: string; publicAccessToken: string; lastEventId?: string }>
|
||||
>({});
|
||||
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 (
|
||||
<main className="flex h-screen flex-col">
|
||||
<Chat />
|
||||
</main>
|
||||
<ChatApp
|
||||
initialChatList={chatList}
|
||||
initialActiveChatId={activeChatId}
|
||||
initialMessages={initialMessages}
|
||||
initialSessions={initialSessions}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<ChatMeta[]>(initialChatList);
|
||||
const [activeChatId, setActiveChatId] = useState<string | null>(initialActiveChatId);
|
||||
const [messages, setMessages] = useState<UIMessage[]>(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 (
|
||||
<main className="flex h-screen">
|
||||
<ChatSidebar
|
||||
chats={chatList}
|
||||
activeChatId={activeChatId}
|
||||
onSelectChat={handleSelectChat}
|
||||
onNewChat={handleNewChat}
|
||||
onDeleteChat={handleDeleteChat}
|
||||
/>
|
||||
<div className="flex-1">
|
||||
{activeChatId ? (
|
||||
<Chat
|
||||
key={activeChatId}
|
||||
chatId={activeChatId}
|
||||
initialMessages={messages}
|
||||
transport={transport}
|
||||
onFirstMessage={handleFirstMessage}
|
||||
onMessagesChange={handleMessagesChange}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<div className="text-center">
|
||||
<p className="text-sm text-gray-400">No conversation selected</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleNewChat}
|
||||
className="mt-3 rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700"
|
||||
>
|
||||
Start a new chat
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
<div className="flex h-full w-64 shrink-0 flex-col border-r border-gray-200 bg-gray-50">
|
||||
<div className="p-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onNewChat}
|
||||
className="w-full rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm font-medium text-gray-700 hover:bg-gray-100"
|
||||
>
|
||||
+ New Chat
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
{sorted.length === 0 && (
|
||||
<p className="px-3 py-8 text-center text-xs text-gray-400">No conversations yet</p>
|
||||
)}
|
||||
|
||||
{sorted.map((chat) => (
|
||||
<button
|
||||
key={chat.id}
|
||||
type="button"
|
||||
onClick={() => onSelectChat(chat.id)}
|
||||
className={`group flex w-full items-start gap-2 px-3 py-2.5 text-left text-sm hover:bg-gray-100 ${
|
||||
activeChatId === chat.id ? "bg-white" : ""
|
||||
}`}
|
||||
>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="truncate font-medium text-gray-800">{chat.title}</div>
|
||||
<div className="text-[10px] text-gray-400">{timeAgo(chat.updatedAt)}</div>
|
||||
</div>
|
||||
<span
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onDeleteChat(chat.id);
|
||||
}}
|
||||
className="mt-0.5 hidden shrink-0 rounded p-0.5 text-xs text-gray-400 hover:bg-red-100 hover:text-red-600 group-hover:inline-block"
|
||||
>
|
||||
×
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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<Map<string, string>>(new Map());
|
||||
|
||||
const transport = useTriggerChatTransport<typeof aiChat>({
|
||||
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<typeof sendMessage>[0], opts?: Parameters<typeof sendMessage>[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<string>(model);
|
||||
|
||||
// Track model for new user messages as they appear
|
||||
const trackedUserIds = useRef<Set<string>>(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"}`}
|
||||
>
|
||||
<div className={`max-w-[80%] ${message.role === "user" ? "" : "w-full"}`}>
|
||||
{/* Model badge for assistant messages */}
|
||||
{message.role === "assistant" && (
|
||||
<div className="mb-1 flex items-center gap-2 text-[10px] text-gray-400">
|
||||
<span className="rounded bg-gray-200 px-1.5 py-0.5 font-medium text-gray-500">
|
||||
@@ -199,7 +226,6 @@ export function Chat() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Queued message indicator */}
|
||||
{pendingMessage && (
|
||||
<div className="flex justify-end">
|
||||
<div className="max-w-[80%]">
|
||||
@@ -214,20 +240,17 @@ export function Chat() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Error */}
|
||||
{error && (
|
||||
<div className="shrink-0 border-t border-red-100 bg-red-50 px-4 py-2 text-sm text-red-600">
|
||||
{error.message}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Input */}
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
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 } });
|
||||
|
||||
@@ -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";
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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<string, () => LanguageModel> = {
|
||||
"gpt-4o-mini": () => openai("gpt-4o-mini"),
|
||||
@@ -13,9 +20,6 @@ const MODELS: Record<string, () => 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() })
|
||||
|
||||
@@ -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",
|
||||
}),
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user