From 90a9556e89656c24845fe3eb316cbe52ce64f323 Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Sat, 7 Mar 2026 15:37:57 +0000 Subject: [PATCH] Some improvements to the example ai-chat --- references/ai-chat/prisma/schema.prisma | 1 + references/ai-chat/src/app/actions.ts | 3 +- .../ai-chat/src/components/chat-app.tsx | 47 +++- references/ai-chat/src/components/chat.tsx | 209 +++++++++++++----- references/ai-chat/src/trigger/chat.ts | 9 +- 5 files changed, 198 insertions(+), 71 deletions(-) diff --git a/references/ai-chat/prisma/schema.prisma b/references/ai-chat/prisma/schema.prisma index 5b58955c2..d3941b750 100644 --- a/references/ai-chat/prisma/schema.prisma +++ b/references/ai-chat/prisma/schema.prisma @@ -21,6 +21,7 @@ model User { model Chat { id String @id title String + model String @default("gpt-4o-mini") messages Json @default("[]") userId String? user User? @relation(fields: [userId], references: [id]) diff --git a/references/ai-chat/src/app/actions.ts b/references/ai-chat/src/app/actions.ts index 3b6c55e71..56398c9c8 100644 --- a/references/ai-chat/src/app/actions.ts +++ b/references/ai-chat/src/app/actions.ts @@ -8,12 +8,13 @@ export const getChatToken = async () => chat.createAccessToken("a export async function getChatList() { const chats = await prisma.chat.findMany({ - select: { id: true, title: true, createdAt: true, updatedAt: true }, + select: { id: true, title: true, model: true, createdAt: true, updatedAt: true }, orderBy: { updatedAt: "desc" }, }); return chats.map((c) => ({ id: c.id, title: c.title, + model: c.model, createdAt: c.createdAt.getTime(), updatedAt: c.updatedAt.getTime(), })); diff --git a/references/ai-chat/src/components/chat-app.tsx b/references/ai-chat/src/components/chat-app.tsx index a00695ec4..c1008e2fe 100644 --- a/references/ai-chat/src/components/chat-app.tsx +++ b/references/ai-chat/src/components/chat-app.tsx @@ -7,6 +7,7 @@ import type { aiChat } from "@/trigger/chat"; import { useCallback, useEffect, useState } from "react"; import { Chat } from "@/components/chat"; import { ChatSidebar } from "@/components/chat-sidebar"; +import { DEFAULT_MODEL } from "@/lib/models"; import { getChatToken, getChatList, @@ -19,18 +20,22 @@ import { type ChatMeta = { id: string; title: string; + model: string; createdAt: number; updatedAt: number; }; +type SessionInfo = { + runId: string; + publicAccessToken: string; + lastEventId?: string; +}; + type ChatAppProps = { initialChatList: ChatMeta[]; initialActiveChatId: string | null; initialMessages: UIMessage[]; - initialSessions: Record< - string, - { runId: string; publicAccessToken: string; lastEventId?: string } - >; + initialSessions: Record; }; export function ChatApp({ @@ -42,15 +47,21 @@ export function ChatApp({ const [chatList, setChatList] = useState(initialChatList); const [activeChatId, setActiveChatId] = useState(initialActiveChatId); const [messages, setMessages] = useState(initialMessages); + const [sessions, setSessions] = useState>(initialSessions); + + // Model for new chats (before first message is sent) + const [newChatModel, setNewChatModel] = useState(DEFAULT_MODEL); const handleSessionChange = useCallback( - ( - chatId: string, - session: { runId: string; publicAccessToken: string; lastEventId?: string } | null - ) => { - // Session creation and token updates are handled server-side via onChatStart/onTurnComplete. - // We only need to clean up when the run ends (session = null). - if (!session) { + (chatId: string, session: SessionInfo | null) => { + if (session) { + setSessions((prev) => ({ ...prev, [chatId]: session })); + } else { + setSessions((prev) => { + const next = { ...prev }; + delete next[chatId]; + return next; + }); deleteSessionAction(chatId); } }, @@ -86,6 +97,7 @@ export function ChatApp({ const id = generateId(); setActiveChatId(id); setMessages([]); + setNewChatModel(DEFAULT_MODEL); } function handleSelectChat(id: string) { @@ -119,6 +131,14 @@ export function ChatApp({ setChatList(list); }, []); + // Determine the model for the active chat + const activeChatMeta = chatList.find((c) => c.id === activeChatId); + const isNewChat = activeChatId != null && !activeChatMeta; + const activeModel = isNewChat ? newChatModel : (activeChatMeta?.model ?? DEFAULT_MODEL); + + // Get session for the active chat + const activeSession = activeChatId ? sessions[activeChatId] : undefined; + return (
0} + model={activeModel} + isNewChat={isNewChat} + onModelChange={isNewChat ? setNewChatModel : undefined} + session={activeSession} + dashboardUrl={process.env.NEXT_PUBLIC_TRIGGER_DASHBOARD_URL} onFirstMessage={handleFirstMessage} onMessagesChange={handleMessagesChange} /> diff --git a/references/ai-chat/src/components/chat.tsx b/references/ai-chat/src/components/chat.tsx index 13abe0df6..f6e1916b5 100644 --- a/references/ai-chat/src/components/chat.tsx +++ b/references/ai-chat/src/components/chat.tsx @@ -5,7 +5,7 @@ import { useChat } from "@ai-sdk/react"; import type { TriggerChatTransport } from "@trigger.dev/sdk/chat"; import { useEffect, useRef, useState } from "react"; import { Streamdown } from "streamdown"; -import { MODEL_OPTIONS, DEFAULT_MODEL } from "@/lib/models"; +import { MODEL_OPTIONS } from "@/lib/models"; function ToolInvocation({ part }: { part: any }) { const [expanded, setExpanded] = useState(false); @@ -70,11 +70,112 @@ function ToolInvocation({ part }: { part: any }) { ); } +function DebugPanel({ + chatId, + model, + status, + session, + dashboardUrl, + messageCount, +}: { + chatId: string; + model: string; + status: string; + session?: { runId: string; publicAccessToken: string; lastEventId?: string }; + dashboardUrl?: string; + messageCount: number; +}) { + const [open, setOpen] = useState(false); + + const runUrl = + session?.runId && dashboardUrl + ? `${dashboardUrl}/runs/${session.runId}` + : undefined; + + return ( +
+ + + {open && ( +
+ + + + + {session ? ( + <> + + + + ) : ( + + )} +
+ )} +
+ ); +} + +function Row({ + label, + value, + mono, + link, +}: { + label: string; + value: string; + mono?: boolean; + link?: string; +}) { + return ( +
+ {label} + {link ? ( + + {value} + + ) : ( + {value} + )} +
+ ); +} + type ChatProps = { chatId: string; initialMessages: UIMessage[]; transport: TriggerChatTransport; resume?: boolean; + model: string; + isNewChat: boolean; + onModelChange?: (model: string) => void; + session?: { runId: string; publicAccessToken: string; lastEventId?: string }; + dashboardUrl?: string; onFirstMessage?: (chatId: string, text: string) => void; onMessagesChange?: (chatId: string, messages: UIMessage[]) => void; }; @@ -84,12 +185,15 @@ export function Chat({ initialMessages, transport, resume: resumeProp, + model, + isNewChat, + onModelChange, + session, + dashboardUrl, onFirstMessage, onMessagesChange, }: ChatProps) { const [input, setInput] = useState(""); - const [model, setModel] = useState(DEFAULT_MODEL); - const modelByUserMsgId = useRef>(new Map()); const hasCalledFirstMessage = useRef(false); const { messages, sendMessage, stop, status, error } = useChat({ @@ -114,7 +218,7 @@ export function Chat({ }, [messages, chatId, onFirstMessage]); // Pending message to send after the current turn completes - const [pendingMessage, setPendingMessage] = useState<{ text: string; model: string } | null>(null); + const [pendingMessage, setPendingMessage] = useState(null); // Handle turn completion: persist messages and auto-send pending message const prevStatus = useRef(status); @@ -124,47 +228,48 @@ export function Chat({ 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). + // Persist messages when a turn completes if (messages.length > 0) { onMessagesChange?.(chatId, messages); } // Auto-send the pending message if (pendingMessage) { - const { text, model: pendingMsgModel } = pendingMessage; + const text = pendingMessage; setPendingMessage(null); - pendingModel.current = pendingMsgModel; - sendMessage({ text }, { metadata: { model: pendingMsgModel } }); + sendMessage({ text }, { metadata: { model } }); } - }, [status, messages, chatId, onMessagesChange, sendMessage, pendingMessage]); - - function getModelForAssistantAt(index: number): string | undefined { - for (let i = index - 1; i >= 0; i--) { - if (messages[i]?.role === "user") { - return modelByUserMsgId.current.get(messages[i].id); - } - } - return undefined; - } - - const originalSendMessage = sendMessage; - function trackedSendMessage(msg: Parameters[0], opts?: Parameters[1]) { - pendingModel.current = model; - originalSendMessage(msg, opts); - } - const pendingModel = useRef(model); - - const trackedUserIds = useRef>(new Set()); - for (const msg of messages) { - if (msg.role === "user" && !trackedUserIds.current.has(msg.id)) { - trackedUserIds.current.add(msg.id); - modelByUserMsgId.current.set(msg.id, pendingModel.current); - } - } + }, [status, messages, chatId, onMessagesChange, sendMessage, pendingMessage, model]); return (
+ {/* Model selector for new chats */} + {isNewChat && messages.length === 0 && onModelChange && ( +
+ Model: + +
+ )} + + {/* Model badge for existing chats */} + {(!isNewChat || messages.length > 0) && ( +
+ + {model} + +
+ )} + {/* Messages */}
{messages.length === 0 && ( @@ -177,13 +282,6 @@ export function Chat({ className={`flex ${message.role === "user" ? "justify-end" : "justify-start"}`} >
- {message.role === "assistant" && ( -
- - {getModelForAssistantAt(messageIndex) ?? DEFAULT_MODEL} - -
- )}
- {pendingMessage.text} + {pendingMessage}
Queued — will send when current response finishes @@ -262,14 +360,24 @@ export function Chat({
)} + {/* Debug panel */} + +
{ e.preventDefault(); if (!input.trim()) return; if (status === "streaming") { - setPendingMessage({ text: input, model }); + setPendingMessage(input); } else { - trackedSendMessage({ text: input }, { metadata: { model } }); + sendMessage({ text: input }, { metadata: { model } }); } setInput(""); }} @@ -300,19 +408,6 @@ export function Chat({ )}
-
- -
); diff --git a/references/ai-chat/src/trigger/chat.ts b/references/ai-chat/src/trigger/chat.ts index d8e171c31..226e6bcad 100644 --- a/references/ai-chat/src/trigger/chat.ts +++ b/references/ai-chat/src/trigger/chat.ts @@ -156,10 +156,15 @@ export const aiChat = chat.task({ }); if (!continuation) { - // Brand new chat — create the record + // Brand new chat — create the record with the selected model await prisma.chat.upsert({ where: { id: chatId }, - create: { id: chatId, title: "New chat", userId: user.id }, + create: { + id: chatId, + title: "New chat", + userId: user.id, + model: clientData.model ?? DEFAULT_MODEL, + }, update: {}, }); }