"use client";
import type { UIMessage } from "ai";
import { useChat } from "@ai-sdk/react";
import { WorkflowChatTransport } from "@workflow/ai";
import { useState, useCallback, useMemo, useEffect } from "react";
const STORAGE_KEY = "workflow-run-id";
interface UserMessageData {
type: "user-message";
id: string;
content: string;
timestamp: number;
}
export function useMultiTurnChat() {
const [runId, setRunId] = useState<string | null>(null);
const [shouldResume, setShouldResume] = useState(false);
// Check for existing session on mount
useEffect(() => {
const storedRunId = localStorage.getItem(STORAGE_KEY);
if (storedRunId) {
setRunId(storedRunId);
setShouldResume(true);
}
}, []);
const transport = useMemo(
() =>
new WorkflowChatTransport({
api: "/api/chat",
onChatSendMessage: (response) => {
const workflowRunId = response.headers.get("x-workflow-run-id");
if (workflowRunId) {
setRunId(workflowRunId);
localStorage.setItem(STORAGE_KEY, workflowRunId);
}
},
onChatEnd: () => {
setRunId(null);
localStorage.removeItem(STORAGE_KEY);
},
prepareReconnectToStreamRequest: ({ api, ...rest }) => {
const storedRunId = localStorage.getItem(STORAGE_KEY);
if (!storedRunId) throw new Error("No active session");
return { ...rest, api: `/api/chat/${storedRunId}/stream` };
},
}),
[]
);
const { messages: rawMessages, sendMessage: baseSendMessage, status, stop, setMessages } =
useChat({ resume: shouldResume, transport });
// Reconstruct conversation order from stream markers
const messages = useMemo(() => {
const result: UIMessage[] = [];
const seenContent = new Set<string>();
for (const msg of rawMessages) {
if (msg.role === "user") {
const text = msg.parts.filter((p) => p.type === "text").map((p) => p.text).join("");
if (text) seenContent.add(text);
}
}
for (const msg of rawMessages) {
if (msg.role === "user") {
result.push(msg);
continue;
}
if (msg.role === "assistant") {
let currentParts: typeof msg.parts = [];
let partIndex = 0;
for (const part of msg.parts) {
if (part.type === "data-workflow" && "data" in part) {
const data = part.data as UserMessageData;
if (data?.type === "user-message") {
// Flush accumulated assistant parts
if (currentParts.length > 0) {
result.push({ ...msg, id: `${msg.id}-${partIndex++}`, parts: currentParts });
currentParts = [];
}
// Add user message if not duplicate
if (!seenContent.has(data.content)) {
seenContent.add(data.content);
result.push({ id: data.id, role: "user", parts: [{ type: "text", text: data.content }] });
}
continue;
}
}
currentParts.push(part);
}
if (currentParts.length > 0) {
result.push({ ...msg, id: partIndex > 0 ? `${msg.id}-${partIndex}` : msg.id, parts: currentParts });
}
}
}
return result;
}, [rawMessages]);
const sendMessage = useCallback(
async (text: string) => {
if (runId) {
// Follow-up: send via hook resumption
await fetch(`/api/chat/${runId}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ message: text }),
});
} else {
// First message: start new workflow
await baseSendMessage({ text, metadata: { createdAt: Date.now() } });
}
},
[runId, baseSendMessage]
);
const endSession = useCallback(async () => {
if (runId) {
await fetch(`/api/chat/${runId}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ message: "/done" }),
});
}
setRunId(null);
setShouldResume(false);
localStorage.removeItem(STORAGE_KEY);
setMessages([]);
}, [runId, setMessages]);
return { messages, status, runId, sendMessage, endSession, stop };
}