From cf9f1816b2236d1c93715055cdb217f46b0a70a1 Mon Sep 17 00:00:00 2001 From: Arpit Gupta Date: Tue, 7 Apr 2026 00:42:58 +0530 Subject: [PATCH 1/3] fix: finalize artist handoff after chat completion --- .../__tests__/handleChatCompletion.test.ts | 57 +++++++++++++++- lib/chat/__tests__/handleChatStream.test.ts | 65 +++++++++++++++++++ lib/chat/handleChatCompletion.ts | 65 ++++++++++++++++++- lib/chat/handleChatStream.ts | 53 ++++++++++----- .../registerCreateNewArtistTool.test.ts | 19 +++--- .../artists/registerCreateNewArtistTool.ts | 11 +--- 6 files changed, 231 insertions(+), 39 deletions(-) diff --git a/lib/chat/__tests__/handleChatCompletion.test.ts b/lib/chat/__tests__/handleChatCompletion.test.ts index aab50328..cd7c827d 100644 --- a/lib/chat/__tests__/handleChatCompletion.test.ts +++ b/lib/chat/__tests__/handleChatCompletion.test.ts @@ -9,6 +9,8 @@ import { sendNewConversationNotification } from "@/lib/telegram/sendNewConversat import { generateChatTitle } from "@/lib/chat/generateChatTitle"; import { handleSendEmailToolOutputs } from "@/lib/emails/handleSendEmailToolOutputs"; import { sendErrorNotification } from "@/lib/telegram/sendErrorNotification"; +import { copyRoom } from "@/lib/rooms/copyRoom"; +import { copyChatMessages } from "@/lib/chats/copyChatMessages"; import { handleChatCompletion } from "../handleChatCompletion"; import type { ChatRequestBody } from "../validateChatRequest"; @@ -45,6 +47,14 @@ vi.mock("@/lib/telegram/sendErrorNotification", () => ({ sendErrorNotification: vi.fn(), })); +vi.mock("@/lib/rooms/copyRoom", () => ({ + copyRoom: vi.fn(), +})); + +vi.mock("@/lib/chats/copyChatMessages", () => ({ + copyChatMessages: vi.fn(), +})); + const mockSelectAccountEmails = vi.mocked(selectAccountEmails); const mockSelectRoom = vi.mocked(selectRoom); const mockUpsertRoom = vi.mocked(upsertRoom); @@ -53,6 +63,8 @@ const mockSendNewConversationNotification = vi.mocked(sendNewConversationNotific const mockGenerateChatTitle = vi.mocked(generateChatTitle); const mockHandleSendEmailToolOutputs = vi.mocked(handleSendEmailToolOutputs); const mockSendErrorNotification = vi.mocked(sendErrorNotification); +const mockCopyRoom = vi.mocked(copyRoom); +const mockCopyChatMessages = vi.mocked(copyChatMessages); // Helper to create mock UIMessage /** @@ -92,6 +104,12 @@ describe("handleChatCompletion", () => { mockSelectRoom.mockResolvedValue({ id: "room-456" }); mockUpsertMemory.mockResolvedValue(null); mockHandleSendEmailToolOutputs.mockResolvedValue(); + mockCopyRoom.mockResolvedValue("new-room-123"); + mockCopyChatMessages.mockResolvedValue({ + success: true, + copiedCount: 2, + clearedExisting: true, + }); }); afterEach(() => { @@ -267,7 +285,7 @@ describe("handleChatCompletion", () => { const responseMessages = [createMockUIMessage("resp-1", "assistant", "Hi!")]; // Should not throw - await expect(handleChatCompletion(body, responseMessages)).resolves.toBeUndefined(); + await expect(handleChatCompletion(body, responseMessages)).resolves.toEqual({}); }); }); @@ -317,5 +335,42 @@ describe("handleChatCompletion", () => { }), ); }); + + it("returns redirect path after creating and copying the final artist room", async () => { + const body = createMockBody(); + const responseMessages: UIMessage[] = [ + { + id: "resp-1", + role: "assistant", + parts: [ + { + type: "tool-create_new_artist", + toolCallId: "tool-1", + state: "output-available", + input: {}, + output: { + artist: { + account_id: "artist-123", + name: "Test Artist", + }, + artistAccountId: "artist-123", + message: "ok", + }, + } as any, + ], + createdAt: new Date(), + }, + ]; + + const result = await handleChatCompletion(body, responseMessages); + + expect(mockCopyRoom).toHaveBeenCalledWith("room-456", "artist-123"); + expect(mockCopyChatMessages).toHaveBeenCalledWith({ + sourceChatId: "room-456", + targetChatId: "new-room-123", + clearExisting: true, + }); + expect(result).toEqual({ redirectPath: "/chat/new-room-123" }); + }); }); }); diff --git a/lib/chat/__tests__/handleChatStream.test.ts b/lib/chat/__tests__/handleChatStream.test.ts index ab9b9e79..ef2fae86 100644 --- a/lib/chat/__tests__/handleChatStream.test.ts +++ b/lib/chat/__tests__/handleChatStream.test.ts @@ -5,6 +5,7 @@ import { getApiKeyAccountId } from "@/lib/auth/getApiKeyAccountId"; import { validateOverrideAccountId } from "@/lib/accounts/validateOverrideAccountId"; import { setupChatRequest } from "@/lib/chat/setupChatRequest"; import { setupConversation } from "@/lib/chat/setupConversation"; +import { handleChatCompletion } from "@/lib/chat/handleChatCompletion"; import { createUIMessageStream, createUIMessageStreamResponse } from "ai"; import { handleChatStream } from "../handleChatStream"; @@ -71,6 +72,7 @@ const mockGetApiKeyAccountId = vi.mocked(getApiKeyAccountId); const mockValidateOverrideAccountId = vi.mocked(validateOverrideAccountId); const mockSetupConversation = vi.mocked(setupConversation); const mockSetupChatRequest = vi.mocked(setupChatRequest); +const mockHandleChatCompletion = vi.mocked(handleChatCompletion); const mockCreateUIMessageStream = vi.mocked(createUIMessageStream); const mockCreateUIMessageStreamResponse = vi.mocked(createUIMessageStreamResponse); @@ -99,6 +101,7 @@ describe("handleChatStream", () => { roomId: roomId || "mock-room-id", memoryId: "mock-memory-id", })); + mockHandleChatCompletion.mockResolvedValue({}); }); afterEach(() => { @@ -247,6 +250,68 @@ describe("handleChatStream", () => { }), ); }); + + it("uses sendFinish false and emits redirect data after completion", async () => { + mockGetApiKeyAccountId.mockResolvedValue("account-123"); + mockHandleChatCompletion.mockResolvedValue({ redirectPath: "/chat/new-room-123" }); + + const toUIMessageStream = vi.fn().mockReturnValue(new ReadableStream()); + const mockAgent = { + stream: vi.fn().mockResolvedValue({ + toUIMessageStream, + usage: Promise.resolve({ inputTokens: 100, outputTokens: 50 }), + }), + tools: {}, + }; + + mockSetupChatRequest.mockResolvedValue({ + agent: mockAgent, + messages: [], + } as any); + + const mockStream = new ReadableStream(); + mockCreateUIMessageStream.mockReturnValue(mockStream); + mockCreateUIMessageStreamResponse.mockReturnValue(new Response(mockStream)); + + const request = createMockRequest({ prompt: "Hello" }, { "x-api-key": "valid-key" }); + + await handleChatStream(request as any); + + const execute = mockCreateUIMessageStream.mock.calls[0][0].execute; + const writer = { + merge: vi.fn(), + write: vi.fn(), + onError: undefined, + }; + + await execute({ writer } as any); + + expect(toUIMessageStream).toHaveBeenCalledWith( + expect.objectContaining({ + sendFinish: false, + onFinish: expect.any(Function), + }), + ); + + const onFinish = toUIMessageStream.mock.calls[0][0].onFinish; + await onFinish({ + isAborted: false, + finishReason: "stop", + messages: [{ id: "resp-1", role: "assistant", parts: [] }], + responseMessage: { id: "resp-1", role: "assistant", parts: [] }, + isContinuation: false, + }); + + expect(writer.write).toHaveBeenCalledWith({ + type: "data-redirect", + data: { path: "/chat/new-room-123" }, + transient: true, + }); + expect(writer.write).toHaveBeenCalledWith({ + type: "finish", + finishReason: "stop", + }); + }); }); describe("error handling", () => { diff --git a/lib/chat/handleChatCompletion.ts b/lib/chat/handleChatCompletion.ts index 81dedabb..f5231080 100644 --- a/lib/chat/handleChatCompletion.ts +++ b/lib/chat/handleChatCompletion.ts @@ -1,4 +1,8 @@ -import type { UIMessage } from "ai"; +import { + getToolOrDynamicToolName, + isToolOrDynamicToolUIPart, + type UIMessage, +} from "ai"; import selectAccountEmails from "@/lib/supabase/account_emails/selectAccountEmails"; import selectRoom from "@/lib/supabase/rooms/selectRoom"; import { upsertRoom } from "@/lib/supabase/rooms/upsertRoom"; @@ -11,6 +15,38 @@ import { handleSendEmailToolOutputs } from "@/lib/emails/handleSendEmailToolOutp import { sendErrorNotification } from "@/lib/telegram/sendErrorNotification"; import { serializeError } from "@/lib/errors/serializeError"; import type { ChatRequestBody } from "./validateChatRequest"; +import { copyRoom } from "@/lib/rooms/copyRoom"; +import { copyChatMessages } from "@/lib/chats/copyChatMessages"; +import type { CreateNewArtistResult } from "@/lib/mcp/tools/artists/registerCreateNewArtistTool"; + +export interface ChatCompletionResult { + redirectPath?: string; +} + +function getCreateArtistResult(responseMessages: UIMessage[]): CreateNewArtistResult | null { + for (const message of responseMessages) { + for (const part of message.parts) { + if (!isToolOrDynamicToolUIPart(part)) continue; + if (getToolOrDynamicToolName(part) !== "create_new_artist") continue; + if (part.state !== "output-available") continue; + + if (part.type === "dynamic-tool") { + const text = part.output?.content?.[0]?.text; + if (!text) continue; + + try { + return JSON.parse(text) as CreateNewArtistResult; + } catch { + continue; + } + } + + return part.output as CreateNewArtistResult; + } + } + + return null; +} /** * Handles post-chat-completion tasks: @@ -28,7 +64,7 @@ import type { ChatRequestBody } from "./validateChatRequest"; export async function handleChatCompletion( body: ChatRequestBody, responseMessages: UIMessage[], -): Promise { +): Promise { try { const { messages, roomId = "", accountId, artistId } = body; @@ -81,8 +117,32 @@ export async function handleChatCompletion( content: filterMessageContentForMemories(responseMessages[responseMessages.length - 1]), }); + let redirectPath: string | undefined; + const createArtistResult = getCreateArtistResult(responseMessages); + if (createArtistResult?.artistAccountId) { + const newRoomId = await copyRoom(roomId, createArtistResult.artistAccountId); + + if (newRoomId) { + const copyResult = await copyChatMessages({ + sourceChatId: roomId, + targetChatId: newRoomId, + clearExisting: true, + }); + + if (copyResult.success) { + redirectPath = `/chat/${newRoomId}`; + } else { + console.error("Failed to copy final artist conversation:", copyResult.error); + } + } else { + console.error("Failed to create final artist conversation room"); + } + } + // Process any email tool outputs await handleSendEmailToolOutputs(responseMessages); + + return { redirectPath }; } catch (error) { sendErrorNotification({ ...body, @@ -90,5 +150,6 @@ export async function handleChatCompletion( error: serializeError(error), }); console.error("Failed to save chat", error); + return {}; } } diff --git a/lib/chat/handleChatStream.ts b/lib/chat/handleChatStream.ts index 7319b2cb..aef681c8 100644 --- a/lib/chat/handleChatStream.ts +++ b/lib/chat/handleChatStream.ts @@ -38,23 +38,42 @@ export async function handleChatStream(request: NextRequest): Promise execute: async options => { const { writer } = options; streamResult = await agent.stream(chatConfig); - writer.merge(streamResult.toUIMessageStream()); - }, - onFinish: async event => { - if (event.isAborted) { - return; - } - const assistantMessages = event.messages.filter(message => message.role === "assistant"); - const responseMessages = - assistantMessages.length > 0 ? assistantMessages : [event.responseMessage]; - await handleChatCompletion(body, responseMessages); - if (streamResult) { - await handleChatCredits({ - usage: await streamResult.usage, - model: body.model ?? DEFAULT_MODEL, - accountId: body.accountId, - }); - } + writer.merge( + streamResult.toUIMessageStream({ + sendFinish: false, + onFinish: async event => { + if (event.isAborted) { + return; + } + + const assistantMessages = event.messages.filter( + message => message.role === "assistant", + ); + const responseMessages = + assistantMessages.length > 0 ? assistantMessages : [event.responseMessage]; + const { redirectPath } = await handleChatCompletion(body, responseMessages); + + if (redirectPath) { + writer.write({ + type: "data-redirect", + data: { path: redirectPath }, + transient: true, + }); + } + + writer.write({ + type: "finish", + finishReason: event.finishReason, + }); + + await handleChatCredits({ + usage: await streamResult!.usage, + model: body.model ?? DEFAULT_MODEL, + accountId: body.accountId, + }); + }, + }), + ); }, onError: e => { console.error("/api/chat onError:", e); diff --git a/lib/mcp/tools/artists/__tests__/registerCreateNewArtistTool.test.ts b/lib/mcp/tools/artists/__tests__/registerCreateNewArtistTool.test.ts index 47e912af..c5523245 100644 --- a/lib/mcp/tools/artists/__tests__/registerCreateNewArtistTool.test.ts +++ b/lib/mcp/tools/artists/__tests__/registerCreateNewArtistTool.test.ts @@ -6,17 +6,12 @@ import type { ServerRequest, ServerNotification } from "@modelcontextprotocol/sd import { registerCreateNewArtistTool } from "../registerCreateNewArtistTool"; const mockCreateArtistInDb = vi.fn(); -const mockCopyRoom = vi.fn(); const mockCanAccessAccount = vi.fn(); vi.mock("@/lib/artists/createArtistInDb", () => ({ createArtistInDb: (...args: unknown[]) => mockCreateArtistInDb(...args), })); -vi.mock("@/lib/rooms/copyRoom", () => ({ - copyRoom: (...args: unknown[]) => mockCopyRoom(...args), -})); - vi.mock("@/lib/organizations/canAccessAccount", () => ({ canAccessAccount: (...args: unknown[]) => mockCanAccessAccount(...args), })); @@ -132,7 +127,7 @@ describe("registerCreateNewArtistTool", () => { }); }); - it("copies room when active_conversation_id is provided", async () => { + it("does not create a room when active_conversation_id is provided", async () => { const mockArtist = { id: "artist-123", account_id: "artist-123", @@ -141,7 +136,6 @@ describe("registerCreateNewArtistTool", () => { account_socials: [], }; mockCreateArtistInDb.mockResolvedValue(mockArtist); - mockCopyRoom.mockResolvedValue("new-room-789"); const result = await registeredHandler( { @@ -152,12 +146,19 @@ describe("registerCreateNewArtistTool", () => { createMockExtra(), ); - expect(mockCopyRoom).toHaveBeenCalledWith("source-room-111", "artist-123"); expect(result).toEqual({ content: [ { type: "text", - text: expect.stringContaining("new-room-789"), + text: expect.stringContaining('"artistAccountId":"artist-123"'), + }, + ], + }); + expect(result).toEqual({ + content: [ + { + type: "text", + text: expect.not.stringContaining("newRoomId"), }, ], }); diff --git a/lib/mcp/tools/artists/registerCreateNewArtistTool.ts b/lib/mcp/tools/artists/registerCreateNewArtistTool.ts index 7495bcbf..2b1ed4a4 100644 --- a/lib/mcp/tools/artists/registerCreateNewArtistTool.ts +++ b/lib/mcp/tools/artists/registerCreateNewArtistTool.ts @@ -5,7 +5,6 @@ import { z } from "zod"; import type { McpAuthInfo } from "@/lib/mcp/verifyApiKey"; import { resolveAccountId } from "@/lib/mcp/resolveAccountId"; import { createArtistInDb, type CreateArtistResult } from "@/lib/artists/createArtistInDb"; -import { copyRoom } from "@/lib/rooms/copyRoom"; import { getToolResultSuccess } from "@/lib/mcp/getToolResultSuccess"; import { getToolResultError } from "@/lib/mcp/getToolResultError"; @@ -44,7 +43,6 @@ export type CreateNewArtistResult = { artistAccountId?: string; message: string; error?: string; - newRoomId?: string | null; }; /** @@ -71,7 +69,7 @@ export function registerCreateNewArtistTool(server: McpServer): void { extra: RequestHandlerExtra, ) => { try { - const { name, account_id, active_conversation_id, organization_id } = args; + const { name, account_id, organization_id } = args; // Resolve accountId from auth or use provided account_id const authInfo = extra.authInfo as McpAuthInfo | undefined; @@ -99,12 +97,6 @@ export function registerCreateNewArtistTool(server: McpServer): void { return getToolResultError("Failed to create artist"); } - // Copy the conversation to the new artist if requested - let newRoomId: string | null = null; - if (active_conversation_id) { - newRoomId = await copyRoom(active_conversation_id, artist.account_id); - } - const result: CreateNewArtistResult = { artist: { account_id: artist.account_id, @@ -113,7 +105,6 @@ export function registerCreateNewArtistTool(server: McpServer): void { }, artistAccountId: artist.account_id, message: `Successfully created artist "${name}". Now searching Spotify for this artist to connect their profile...`, - newRoomId, }; return getToolResultSuccess(result); From dd9204a99792454e4b95096b3634ec11197a14c2 Mon Sep 17 00:00:00 2001 From: Arpit Gupta Date: Tue, 7 Apr 2026 01:08:34 +0530 Subject: [PATCH 2/3] fix: reuse shared chat copy helper --- .../__tests__/handleChatCompletion.test.ts | 4 +- lib/chat/handleChatCompletion.ts | 15 ++-- .../__tests__/copyChatMessagesHandler.test.ts | 78 +++++++------------ lib/chats/copyChatMessages.ts | 54 +++++++++++++ lib/chats/copyChatMessagesHandler.ts | 39 +++------- 5 files changed, 101 insertions(+), 89 deletions(-) create mode 100644 lib/chats/copyChatMessages.ts diff --git a/lib/chat/__tests__/handleChatCompletion.test.ts b/lib/chat/__tests__/handleChatCompletion.test.ts index cd7c827d..8348a927 100644 --- a/lib/chat/__tests__/handleChatCompletion.test.ts +++ b/lib/chat/__tests__/handleChatCompletion.test.ts @@ -106,8 +106,8 @@ describe("handleChatCompletion", () => { mockHandleSendEmailToolOutputs.mockResolvedValue(); mockCopyRoom.mockResolvedValue("new-room-123"); mockCopyChatMessages.mockResolvedValue({ - success: true, - copiedCount: 2, + status: "success", + copiedCount: 1, clearedExisting: true, }); }); diff --git a/lib/chat/handleChatCompletion.ts b/lib/chat/handleChatCompletion.ts index f5231080..8ac78b7c 100644 --- a/lib/chat/handleChatCompletion.ts +++ b/lib/chat/handleChatCompletion.ts @@ -1,8 +1,4 @@ -import { - getToolOrDynamicToolName, - isToolOrDynamicToolUIPart, - type UIMessage, -} from "ai"; +import { getToolOrDynamicToolName, isToolOrDynamicToolUIPart, type UIMessage } from "ai"; import selectAccountEmails from "@/lib/supabase/account_emails/selectAccountEmails"; import selectRoom from "@/lib/supabase/rooms/selectRoom"; import { upsertRoom } from "@/lib/supabase/rooms/upsertRoom"; @@ -16,8 +12,8 @@ import { sendErrorNotification } from "@/lib/telegram/sendErrorNotification"; import { serializeError } from "@/lib/errors/serializeError"; import type { ChatRequestBody } from "./validateChatRequest"; import { copyRoom } from "@/lib/rooms/copyRoom"; -import { copyChatMessages } from "@/lib/chats/copyChatMessages"; import type { CreateNewArtistResult } from "@/lib/mcp/tools/artists/registerCreateNewArtistTool"; +import { copyChatMessages } from "@/lib/chats/copyChatMessages"; export interface ChatCompletionResult { redirectPath?: string; @@ -31,7 +27,8 @@ function getCreateArtistResult(responseMessages: UIMessage[]): CreateNewArtistRe if (part.state !== "output-available") continue; if (part.type === "dynamic-tool") { - const text = part.output?.content?.[0]?.text; + const text = (part.output as { content?: Array<{ text?: string }> } | undefined) + ?.content?.[0]?.text; if (!text) continue; try { @@ -129,10 +126,10 @@ export async function handleChatCompletion( clearExisting: true, }); - if (copyResult.success) { + if (copyResult.status === "success") { redirectPath = `/chat/${newRoomId}`; } else { - console.error("Failed to copy final artist conversation:", copyResult.error); + console.error(copyResult.error); } } else { console.error("Failed to create final artist conversation room"); diff --git a/lib/chats/__tests__/copyChatMessagesHandler.test.ts b/lib/chats/__tests__/copyChatMessagesHandler.test.ts index 77c72a49..f6ca9c4d 100644 --- a/lib/chats/__tests__/copyChatMessagesHandler.test.ts +++ b/lib/chats/__tests__/copyChatMessagesHandler.test.ts @@ -3,10 +3,7 @@ import { NextRequest, NextResponse } from "next/server"; import { copyChatMessagesHandler } from "@/lib/chats/copyChatMessagesHandler"; import { validateCopyChatMessagesBody } from "@/lib/chats/validateCopyChatMessagesBody"; import { validateChatAccess } from "@/lib/chats/validateChatAccess"; -import selectMemories from "@/lib/supabase/memories/selectMemories"; -import deleteMemories from "@/lib/supabase/memories/deleteMemories"; -import insertMemories from "@/lib/supabase/memories/insertMemories"; -import { generateUUID } from "@/lib/uuid/generateUUID"; +import { copyChatMessages } from "@/lib/chats/copyChatMessages"; vi.mock("@/lib/networking/getCorsHeaders", () => ({ getCorsHeaders: vi.fn(() => ({ "Access-Control-Allow-Origin": "*" })), @@ -20,20 +17,8 @@ vi.mock("@/lib/chats/validateChatAccess", () => ({ validateChatAccess: vi.fn(), })); -vi.mock("@/lib/supabase/memories/selectMemories", () => ({ - default: vi.fn(), -})); - -vi.mock("@/lib/supabase/memories/deleteMemories", () => ({ - default: vi.fn(), -})); - -vi.mock("@/lib/supabase/memories/insertMemories", () => ({ - default: vi.fn(), -})); - -vi.mock("@/lib/uuid/generateUUID", () => ({ - generateUUID: vi.fn(), +vi.mock("@/lib/chats/copyChatMessages", () => ({ + copyChatMessages: vi.fn(), })); const sourceChatId = "123e4567-e89b-42d3-a456-426614174000"; @@ -71,7 +56,7 @@ describe("copyChatMessagesHandler", () => { expect(response.status).toBe(400); expect(body.error).toBe("Invalid body"); - expect(selectMemories).not.toHaveBeenCalled(); + expect(copyChatMessages).not.toHaveBeenCalled(); }); it("returns 500 when source messages cannot be loaded", async () => { @@ -81,7 +66,10 @@ describe("copyChatMessagesHandler", () => { }); vi.mocked(validateChatAccess).mockResolvedValueOnce(accessRoom(sourceChatId)); vi.mocked(validateChatAccess).mockResolvedValueOnce(accessRoom(targetChatId)); - vi.mocked(selectMemories).mockResolvedValue(null); + vi.mocked(copyChatMessages).mockResolvedValue({ + status: "error", + error: "Failed to load source chat messages", + }); const response = await copyChatMessagesHandler(request, sourceChatId); const body = await response.json(); @@ -97,32 +85,21 @@ describe("copyChatMessagesHandler", () => { }); vi.mocked(validateChatAccess).mockResolvedValueOnce(accessRoom(sourceChatId)); vi.mocked(validateChatAccess).mockResolvedValueOnce(accessRoom(targetChatId)); - vi.mocked(selectMemories).mockResolvedValue([ - { - id: "mem-1", - room_id: sourceChatId, - content: { role: "user", content: "hello" }, - updated_at: "2026-03-31T00:00:00.000Z", - created_at: "2026-03-31T00:00:00.000Z", - }, - ]); - vi.mocked(deleteMemories).mockResolvedValue(true); - vi.mocked(generateUUID).mockReturnValue("new-mem-1"); - vi.mocked(insertMemories).mockResolvedValue(1); + vi.mocked(copyChatMessages).mockResolvedValue({ + status: "success", + copiedCount: 1, + clearedExisting: true, + }); const response = await copyChatMessagesHandler(request, sourceChatId); const body = await response.json(); expect(response.status).toBe(200); - expect(deleteMemories).toHaveBeenCalledWith(targetChatId); - expect(insertMemories).toHaveBeenCalledWith([ - { - id: "new-mem-1", - room_id: targetChatId, - content: { role: "user", content: "hello" }, - updated_at: "2026-03-31T00:00:00.000Z", - }, - ]); + expect(copyChatMessages).toHaveBeenCalledWith({ + sourceChatId, + targetChatId, + clearExisting: true, + }); expect(body).toEqual({ status: "success", source_chat_id: sourceChatId, @@ -139,15 +116,16 @@ describe("copyChatMessagesHandler", () => { }); vi.mocked(validateChatAccess).mockResolvedValueOnce(accessRoom(sourceChatId)); vi.mocked(validateChatAccess).mockResolvedValueOnce(accessRoom(targetChatId)); - vi.mocked(selectMemories).mockResolvedValue([]); - vi.mocked(deleteMemories).mockResolvedValue(false); + vi.mocked(copyChatMessages).mockResolvedValue({ + status: "error", + error: "Failed to clear target chat messages", + }); const response = await copyChatMessagesHandler(request, sourceChatId); const body = await response.json(); expect(response.status).toBe(500); expect(body.error).toBe("Failed to clear target chat messages"); - expect(insertMemories).not.toHaveBeenCalled(); }); it("skips clear when clearExisting is false", async () => { @@ -157,14 +135,16 @@ describe("copyChatMessagesHandler", () => { }); vi.mocked(validateChatAccess).mockResolvedValueOnce(accessRoom(sourceChatId)); vi.mocked(validateChatAccess).mockResolvedValueOnce(accessRoom(targetChatId)); - vi.mocked(selectMemories).mockResolvedValue([]); - vi.mocked(insertMemories).mockResolvedValue(0); + vi.mocked(copyChatMessages).mockResolvedValue({ + status: "success", + copiedCount: 0, + clearedExisting: false, + }); const response = await copyChatMessagesHandler(request, sourceChatId); const body = await response.json(); expect(response.status).toBe(200); - expect(deleteMemories).not.toHaveBeenCalled(); expect(body.copied_count).toBe(0); expect(body.cleared_existing).toBe(false); }); @@ -194,7 +174,7 @@ describe("copyChatMessagesHandler", () => { const response = await copyChatMessagesHandler(request, sourceChatId); expect(response.status).toBe(401); - expect(selectMemories).not.toHaveBeenCalled(); + expect(copyChatMessages).not.toHaveBeenCalled(); }); it("passes through target chat access errors", async () => { @@ -210,6 +190,6 @@ describe("copyChatMessagesHandler", () => { const response = await copyChatMessagesHandler(request, sourceChatId); expect(response.status).toBe(403); - expect(selectMemories).not.toHaveBeenCalled(); + expect(copyChatMessages).not.toHaveBeenCalled(); }); }); diff --git a/lib/chats/copyChatMessages.ts b/lib/chats/copyChatMessages.ts new file mode 100644 index 00000000..40000d5f --- /dev/null +++ b/lib/chats/copyChatMessages.ts @@ -0,0 +1,54 @@ +import selectMemories from "@/lib/supabase/memories/selectMemories"; +import deleteMemories from "@/lib/supabase/memories/deleteMemories"; +import insertMemories from "@/lib/supabase/memories/insertMemories"; +import { generateUUID } from "@/lib/uuid/generateUUID"; + +type CopyChatMessagesParams = { + sourceChatId: string; + targetChatId: string; + clearExisting?: boolean; +}; + +export type CopyChatMessagesResult = + | { + status: "success"; + copiedCount: number; + clearedExisting: boolean; + } + | { + status: "error"; + error: string; + }; + +export async function copyChatMessages({ + sourceChatId, + targetChatId, + clearExisting = true, +}: CopyChatMessagesParams): Promise { + const sourceMemories = await selectMemories(sourceChatId, { ascending: true }); + if (!sourceMemories) { + return { status: "error", error: "Failed to load source chat messages" }; + } + + if (clearExisting) { + const deleted = await deleteMemories(targetChatId); + if (!deleted) { + return { status: "error", error: "Failed to clear target chat messages" }; + } + } + + const copiedCount = await insertMemories( + sourceMemories.map(memory => ({ + id: generateUUID(), + room_id: targetChatId, + content: memory.content, + updated_at: memory.updated_at, + })), + ); + + return { + status: "success", + copiedCount, + clearedExisting: clearExisting, + }; +} diff --git a/lib/chats/copyChatMessagesHandler.ts b/lib/chats/copyChatMessagesHandler.ts index 0fa3fd43..2dc0489e 100644 --- a/lib/chats/copyChatMessagesHandler.ts +++ b/lib/chats/copyChatMessagesHandler.ts @@ -1,12 +1,9 @@ import type { NextRequest } from "next/server"; import { NextResponse } from "next/server"; import { getCorsHeaders } from "@/lib/networking/getCorsHeaders"; -import { generateUUID } from "@/lib/uuid/generateUUID"; -import selectMemories from "@/lib/supabase/memories/selectMemories"; -import insertMemories from "@/lib/supabase/memories/insertMemories"; -import deleteMemories from "@/lib/supabase/memories/deleteMemories"; import { validateCopyChatMessagesBody } from "@/lib/chats/validateCopyChatMessagesBody"; import { validateChatAccess } from "@/lib/chats/validateChatAccess"; +import { copyChatMessages } from "@/lib/chats/copyChatMessages"; /** * Handles POST /api/chats/[id]/messages/copy. @@ -37,42 +34,26 @@ export async function copyChatMessagesHandler( const accessibleSourceChatId = sourceAccess.roomId; const accessibleTargetChatId = targetAccess.roomId; - const { clearExisting } = validated; + const copyResult = await copyChatMessages({ + sourceChatId: accessibleSourceChatId, + targetChatId: accessibleTargetChatId, + clearExisting: validated.clearExisting, + }); - const sourceMemories = await selectMemories(accessibleSourceChatId, { ascending: true }); - if (!sourceMemories) { + if (copyResult.status === "error") { return NextResponse.json( - { status: "error", error: "Failed to load source chat messages" }, + { status: "error", error: copyResult.error }, { status: 500, headers: getCorsHeaders() }, ); } - if (clearExisting) { - const deleted = await deleteMemories(accessibleTargetChatId); - if (!deleted) { - return NextResponse.json( - { status: "error", error: "Failed to clear target chat messages" }, - { status: 500, headers: getCorsHeaders() }, - ); - } - } - - const copiedCount = await insertMemories( - sourceMemories.map(memory => ({ - id: generateUUID(), - room_id: accessibleTargetChatId, - content: memory.content, - updated_at: memory.updated_at, - })), - ); - return NextResponse.json( { status: "success", source_chat_id: accessibleSourceChatId, target_chat_id: accessibleTargetChatId, - copied_count: copiedCount, - cleared_existing: clearExisting, + copied_count: copyResult.copiedCount, + cleared_existing: copyResult.clearedExisting, }, { status: 200, headers: getCorsHeaders() }, ); From e3d8ab6ba5da2a21a8249cb5fcd1d2479f31f141 Mon Sep 17 00:00:00 2001 From: Arpit Gupta Date: Tue, 7 Apr 2026 01:16:22 +0530 Subject: [PATCH 3/3] refactor: isolate artist chat handoff --- app/api/chat/route.ts | 14 ++- .../__tests__/handleChatCompletion.test.ts | 30 +------ lib/chat/__tests__/handleChatStream.test.ts | 26 +++++- .../handleCreateArtistRedirect.test.ts | 86 +++++++++++++++++++ lib/chat/handleChatCompletion.ts | 62 +------------ lib/chat/handleChatStream.ts | 26 +++--- lib/chat/handleCreateArtistRedirect.ts | 59 +++++++++++++ 7 files changed, 201 insertions(+), 102 deletions(-) create mode 100644 lib/chat/__tests__/handleCreateArtistRedirect.test.ts create mode 100644 lib/chat/handleCreateArtistRedirect.ts diff --git a/app/api/chat/route.ts b/app/api/chat/route.ts index 973166f0..b6e0f467 100644 --- a/app/api/chat/route.ts +++ b/app/api/chat/route.ts @@ -2,6 +2,7 @@ import type { NextRequest } from "next/server"; import { NextResponse } from "next/server"; import { getCorsHeaders } from "@/lib/networking/getCorsHeaders"; import { handleChatStream } from "@/lib/chat/handleChatStream"; +import { handleCreateArtistRedirect } from "@/lib/chat/handleCreateArtistRedirect"; /** * OPTIONS handler for CORS preflight requests. @@ -37,5 +38,16 @@ export async function OPTIONS() { * @returns A streaming response or error */ export async function POST(request: NextRequest): Promise { - return handleChatStream(request); + return handleChatStream(request, async ({ body, responseMessages, writer }) => { + const redirectPath = await handleCreateArtistRedirect(body, responseMessages); + if (!redirectPath) { + return; + } + + writer.write({ + type: "data-redirect", + data: { path: redirectPath }, + transient: true, + }); + }); } diff --git a/lib/chat/__tests__/handleChatCompletion.test.ts b/lib/chat/__tests__/handleChatCompletion.test.ts index 8348a927..6da3ee4b 100644 --- a/lib/chat/__tests__/handleChatCompletion.test.ts +++ b/lib/chat/__tests__/handleChatCompletion.test.ts @@ -9,8 +9,6 @@ import { sendNewConversationNotification } from "@/lib/telegram/sendNewConversat import { generateChatTitle } from "@/lib/chat/generateChatTitle"; import { handleSendEmailToolOutputs } from "@/lib/emails/handleSendEmailToolOutputs"; import { sendErrorNotification } from "@/lib/telegram/sendErrorNotification"; -import { copyRoom } from "@/lib/rooms/copyRoom"; -import { copyChatMessages } from "@/lib/chats/copyChatMessages"; import { handleChatCompletion } from "../handleChatCompletion"; import type { ChatRequestBody } from "../validateChatRequest"; @@ -47,14 +45,6 @@ vi.mock("@/lib/telegram/sendErrorNotification", () => ({ sendErrorNotification: vi.fn(), })); -vi.mock("@/lib/rooms/copyRoom", () => ({ - copyRoom: vi.fn(), -})); - -vi.mock("@/lib/chats/copyChatMessages", () => ({ - copyChatMessages: vi.fn(), -})); - const mockSelectAccountEmails = vi.mocked(selectAccountEmails); const mockSelectRoom = vi.mocked(selectRoom); const mockUpsertRoom = vi.mocked(upsertRoom); @@ -63,8 +53,6 @@ const mockSendNewConversationNotification = vi.mocked(sendNewConversationNotific const mockGenerateChatTitle = vi.mocked(generateChatTitle); const mockHandleSendEmailToolOutputs = vi.mocked(handleSendEmailToolOutputs); const mockSendErrorNotification = vi.mocked(sendErrorNotification); -const mockCopyRoom = vi.mocked(copyRoom); -const mockCopyChatMessages = vi.mocked(copyChatMessages); // Helper to create mock UIMessage /** @@ -104,12 +92,6 @@ describe("handleChatCompletion", () => { mockSelectRoom.mockResolvedValue({ id: "room-456" }); mockUpsertMemory.mockResolvedValue(null); mockHandleSendEmailToolOutputs.mockResolvedValue(); - mockCopyRoom.mockResolvedValue("new-room-123"); - mockCopyChatMessages.mockResolvedValue({ - status: "success", - copiedCount: 1, - clearedExisting: true, - }); }); afterEach(() => { @@ -285,7 +267,7 @@ describe("handleChatCompletion", () => { const responseMessages = [createMockUIMessage("resp-1", "assistant", "Hi!")]; // Should not throw - await expect(handleChatCompletion(body, responseMessages)).resolves.toEqual({}); + await expect(handleChatCompletion(body, responseMessages)).resolves.toBeUndefined(); }); }); @@ -362,15 +344,9 @@ describe("handleChatCompletion", () => { }, ]; - const result = await handleChatCompletion(body, responseMessages); + await handleChatCompletion(body, responseMessages); - expect(mockCopyRoom).toHaveBeenCalledWith("room-456", "artist-123"); - expect(mockCopyChatMessages).toHaveBeenCalledWith({ - sourceChatId: "room-456", - targetChatId: "new-room-123", - clearExisting: true, - }); - expect(result).toEqual({ redirectPath: "/chat/new-room-123" }); + expect(mockHandleSendEmailToolOutputs).toHaveBeenCalledWith(responseMessages); }); }); }); diff --git a/lib/chat/__tests__/handleChatStream.test.ts b/lib/chat/__tests__/handleChatStream.test.ts index ef2fae86..3f805eba 100644 --- a/lib/chat/__tests__/handleChatStream.test.ts +++ b/lib/chat/__tests__/handleChatStream.test.ts @@ -6,6 +6,7 @@ import { validateOverrideAccountId } from "@/lib/accounts/validateOverrideAccoun import { setupChatRequest } from "@/lib/chat/setupChatRequest"; import { setupConversation } from "@/lib/chat/setupConversation"; import { handleChatCompletion } from "@/lib/chat/handleChatCompletion"; +import { handleCreateArtistRedirect } from "@/lib/chat/handleCreateArtistRedirect"; import { createUIMessageStream, createUIMessageStreamResponse } from "ai"; import { handleChatStream } from "../handleChatStream"; @@ -55,6 +56,10 @@ vi.mock("@/lib/chat/handleChatCompletion", () => ({ handleChatCompletion: vi.fn(), })); +vi.mock("@/lib/chat/handleCreateArtistRedirect", () => ({ + handleCreateArtistRedirect: vi.fn(), +})); + vi.mock("@/lib/credits/handleChatCredits", () => ({ handleChatCredits: vi.fn(), })); @@ -73,6 +78,7 @@ const mockValidateOverrideAccountId = vi.mocked(validateOverrideAccountId); const mockSetupConversation = vi.mocked(setupConversation); const mockSetupChatRequest = vi.mocked(setupChatRequest); const mockHandleChatCompletion = vi.mocked(handleChatCompletion); +const mockHandleCreateArtistRedirect = vi.mocked(handleCreateArtistRedirect); const mockCreateUIMessageStream = vi.mocked(createUIMessageStream); const mockCreateUIMessageStreamResponse = vi.mocked(createUIMessageStreamResponse); @@ -101,7 +107,8 @@ describe("handleChatStream", () => { roomId: roomId || "mock-room-id", memoryId: "mock-memory-id", })); - mockHandleChatCompletion.mockResolvedValue({}); + mockHandleChatCompletion.mockResolvedValue(); + mockHandleCreateArtistRedirect.mockResolvedValue(undefined); }); afterEach(() => { @@ -253,8 +260,6 @@ describe("handleChatStream", () => { it("uses sendFinish false and emits redirect data after completion", async () => { mockGetApiKeyAccountId.mockResolvedValue("account-123"); - mockHandleChatCompletion.mockResolvedValue({ redirectPath: "/chat/new-room-123" }); - const toUIMessageStream = vi.fn().mockReturnValue(new ReadableStream()); const mockAgent = { stream: vi.fn().mockResolvedValue({ @@ -275,7 +280,20 @@ describe("handleChatStream", () => { const request = createMockRequest({ prompt: "Hello" }, { "x-api-key": "valid-key" }); - await handleChatStream(request as any); + await handleChatStream(request as any, async ({ body, responseMessages, writer }) => { + const redirectPath = await handleCreateArtistRedirect(body, responseMessages); + if (!redirectPath) { + return; + } + + writer.write({ + type: "data-redirect", + data: { path: redirectPath }, + transient: true, + }); + }); + + mockHandleCreateArtistRedirect.mockResolvedValue("/chat/new-room-123"); const execute = mockCreateUIMessageStream.mock.calls[0][0].execute; const writer = { diff --git a/lib/chat/__tests__/handleCreateArtistRedirect.test.ts b/lib/chat/__tests__/handleCreateArtistRedirect.test.ts new file mode 100644 index 00000000..0e5e326a --- /dev/null +++ b/lib/chat/__tests__/handleCreateArtistRedirect.test.ts @@ -0,0 +1,86 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import type { UIMessage } from "ai"; +import { copyRoom } from "@/lib/rooms/copyRoom"; +import { copyChatMessages } from "@/lib/chats/copyChatMessages"; +import { handleCreateArtistRedirect } from "../handleCreateArtistRedirect"; + +vi.mock("@/lib/rooms/copyRoom", () => ({ + copyRoom: vi.fn(), +})); + +vi.mock("@/lib/chats/copyChatMessages", () => ({ + copyChatMessages: vi.fn(), +})); + +const mockCopyRoom = vi.mocked(copyRoom); +const mockCopyChatMessages = vi.mocked(copyChatMessages); + +describe("handleCreateArtistRedirect", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("returns redirect path after creating and copying the final artist room", async () => { + mockCopyRoom.mockResolvedValue("new-room-123"); + mockCopyChatMessages.mockResolvedValue({ + status: "success", + copiedCount: 1, + clearedExisting: true, + }); + + const responseMessages: UIMessage[] = [ + { + id: "resp-1", + role: "assistant", + parts: [ + { + type: "tool-create_new_artist", + toolCallId: "tool-1", + state: "output-available", + input: {}, + output: { + artist: { + account_id: "artist-123", + name: "Test Artist", + }, + artistAccountId: "artist-123", + message: "ok", + }, + } as any, + ], + createdAt: new Date(), + }, + ]; + + const result = await handleCreateArtistRedirect( + { + accountId: "account-123", + messages: [], + roomId: "room-456", + } as any, + responseMessages, + ); + + expect(mockCopyRoom).toHaveBeenCalledWith("room-456", "artist-123"); + expect(mockCopyChatMessages).toHaveBeenCalledWith({ + sourceChatId: "room-456", + targetChatId: "new-room-123", + clearExisting: true, + }); + expect(result).toBe("/chat/new-room-123"); + }); + + it("returns undefined when no create artist result exists", async () => { + const result = await handleCreateArtistRedirect( + { + accountId: "account-123", + messages: [], + roomId: "room-456", + } as any, + [{ id: "resp-1", role: "assistant", parts: [], createdAt: new Date() }], + ); + + expect(result).toBeUndefined(); + expect(mockCopyRoom).not.toHaveBeenCalled(); + }); +}); diff --git a/lib/chat/handleChatCompletion.ts b/lib/chat/handleChatCompletion.ts index 8ac78b7c..81dedabb 100644 --- a/lib/chat/handleChatCompletion.ts +++ b/lib/chat/handleChatCompletion.ts @@ -1,4 +1,4 @@ -import { getToolOrDynamicToolName, isToolOrDynamicToolUIPart, type UIMessage } from "ai"; +import type { UIMessage } from "ai"; import selectAccountEmails from "@/lib/supabase/account_emails/selectAccountEmails"; import selectRoom from "@/lib/supabase/rooms/selectRoom"; import { upsertRoom } from "@/lib/supabase/rooms/upsertRoom"; @@ -11,39 +11,6 @@ import { handleSendEmailToolOutputs } from "@/lib/emails/handleSendEmailToolOutp import { sendErrorNotification } from "@/lib/telegram/sendErrorNotification"; import { serializeError } from "@/lib/errors/serializeError"; import type { ChatRequestBody } from "./validateChatRequest"; -import { copyRoom } from "@/lib/rooms/copyRoom"; -import type { CreateNewArtistResult } from "@/lib/mcp/tools/artists/registerCreateNewArtistTool"; -import { copyChatMessages } from "@/lib/chats/copyChatMessages"; - -export interface ChatCompletionResult { - redirectPath?: string; -} - -function getCreateArtistResult(responseMessages: UIMessage[]): CreateNewArtistResult | null { - for (const message of responseMessages) { - for (const part of message.parts) { - if (!isToolOrDynamicToolUIPart(part)) continue; - if (getToolOrDynamicToolName(part) !== "create_new_artist") continue; - if (part.state !== "output-available") continue; - - if (part.type === "dynamic-tool") { - const text = (part.output as { content?: Array<{ text?: string }> } | undefined) - ?.content?.[0]?.text; - if (!text) continue; - - try { - return JSON.parse(text) as CreateNewArtistResult; - } catch { - continue; - } - } - - return part.output as CreateNewArtistResult; - } - } - - return null; -} /** * Handles post-chat-completion tasks: @@ -61,7 +28,7 @@ function getCreateArtistResult(responseMessages: UIMessage[]): CreateNewArtistRe export async function handleChatCompletion( body: ChatRequestBody, responseMessages: UIMessage[], -): Promise { +): Promise { try { const { messages, roomId = "", accountId, artistId } = body; @@ -114,32 +81,8 @@ export async function handleChatCompletion( content: filterMessageContentForMemories(responseMessages[responseMessages.length - 1]), }); - let redirectPath: string | undefined; - const createArtistResult = getCreateArtistResult(responseMessages); - if (createArtistResult?.artistAccountId) { - const newRoomId = await copyRoom(roomId, createArtistResult.artistAccountId); - - if (newRoomId) { - const copyResult = await copyChatMessages({ - sourceChatId: roomId, - targetChatId: newRoomId, - clearExisting: true, - }); - - if (copyResult.status === "success") { - redirectPath = `/chat/${newRoomId}`; - } else { - console.error(copyResult.error); - } - } else { - console.error("Failed to create final artist conversation room"); - } - } - // Process any email tool outputs await handleSendEmailToolOutputs(responseMessages); - - return { redirectPath }; } catch (error) { sendErrorNotification({ ...body, @@ -147,6 +90,5 @@ export async function handleChatCompletion( error: serializeError(error), }); console.error("Failed to save chat", error); - return {}; } } diff --git a/lib/chat/handleChatStream.ts b/lib/chat/handleChatStream.ts index aef681c8..aca1381b 100644 --- a/lib/chat/handleChatStream.ts +++ b/lib/chat/handleChatStream.ts @@ -7,6 +7,16 @@ import { setupChatRequest } from "./setupChatRequest"; import { getCorsHeaders } from "@/lib/networking/getCorsHeaders"; import generateUUID from "@/lib/uuid/generateUUID"; import { DEFAULT_MODEL } from "@/lib/const"; +import type { ChatRequestBody } from "./validateChatRequest"; +import type { UIMessage } from "ai"; + +type ChatStreamFinishHandler = (params: { + body: ChatRequestBody; + responseMessages: UIMessage[]; + writer: { + write: (chunk: unknown) => void; + }; +}) => Promise; /** * Handles a streaming chat request. @@ -19,7 +29,10 @@ import { DEFAULT_MODEL } from "@/lib/const"; * @param request - The incoming NextRequest * @returns A streaming response or error NextResponse */ -export async function handleChatStream(request: NextRequest): Promise { +export async function handleChatStream( + request: NextRequest, + onFinish?: ChatStreamFinishHandler, +): Promise { const validatedBodyOrError = await validateChatRequest(request); if (validatedBodyOrError instanceof NextResponse) { return validatedBodyOrError; @@ -51,15 +64,8 @@ export async function handleChatStream(request: NextRequest): Promise ); const responseMessages = assistantMessages.length > 0 ? assistantMessages : [event.responseMessage]; - const { redirectPath } = await handleChatCompletion(body, responseMessages); - - if (redirectPath) { - writer.write({ - type: "data-redirect", - data: { path: redirectPath }, - transient: true, - }); - } + await handleChatCompletion(body, responseMessages); + await onFinish?.({ body, responseMessages, writer }); writer.write({ type: "finish", diff --git a/lib/chat/handleCreateArtistRedirect.ts b/lib/chat/handleCreateArtistRedirect.ts new file mode 100644 index 00000000..7fbd24f6 --- /dev/null +++ b/lib/chat/handleCreateArtistRedirect.ts @@ -0,0 +1,59 @@ +import { getToolOrDynamicToolName, isToolOrDynamicToolUIPart, type UIMessage } from "ai"; +import { copyChatMessages } from "@/lib/chats/copyChatMessages"; +import { copyRoom } from "@/lib/rooms/copyRoom"; +import type { ChatRequestBody } from "./validateChatRequest"; +import type { CreateNewArtistResult } from "@/lib/mcp/tools/artists/registerCreateNewArtistTool"; + +function getCreateArtistResult(responseMessages: UIMessage[]): CreateNewArtistResult | null { + for (const message of responseMessages) { + for (const part of message.parts) { + if (!isToolOrDynamicToolUIPart(part)) continue; + if (getToolOrDynamicToolName(part) !== "create_new_artist") continue; + if (part.state !== "output-available") continue; + + if (part.type === "dynamic-tool") { + const text = (part.output as { content?: Array<{ text?: string }> } | undefined) + ?.content?.[0]?.text; + if (!text) continue; + + try { + return JSON.parse(text) as CreateNewArtistResult; + } catch { + continue; + } + } + + return part.output as CreateNewArtistResult; + } + } + + return null; +} + +export async function handleCreateArtistRedirect( + body: ChatRequestBody, + responseMessages: UIMessage[], +): Promise { + const createArtistResult = getCreateArtistResult(responseMessages); + if (!createArtistResult?.artistAccountId) { + return undefined; + } + + const newRoomId = await copyRoom(body.roomId ?? "", createArtistResult.artistAccountId); + if (!newRoomId) { + console.error("Failed to create final artist conversation room"); + return undefined; + } + + const copyResult = await copyChatMessages({ + sourceChatId: body.roomId ?? "", + targetChatId: newRoomId, + clearExisting: true, + }); + if (copyResult.status === "error") { + console.error(copyResult.error); + return undefined; + } + + return `/chat/${newRoomId}`; +}