"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.AIStudioChat = AIStudioChat;
const tslib_1 = require("tslib");
const jsx_runtime_1 = require("react/jsx-runtime");
const react_1 = require("react");
const uuid_1 = require("uuid");
const openai_1 = require("../../../adapters/openai/index.js");
const types_1 = require("../../../types/index.js");
const messageTypeRegistry_1 = require("../../../utils/messageTypeRegistry.js");
const InputContext_1 = require("../../molecules/InputContext/index.js");
const ChatContainer_1 = require("../ChatContainer/index.js");
const McpToolRenderer_1 = require("./McpToolRenderer.js");
const transforms_1 = require("./transforms/index.js");
function isFileAttachment(value) {
    if (!value || typeof value !== 'object')
        return false;
    const v = value;
    return typeof v.id === 'string' && typeof v.name === 'string';
}
function resolveFileAttachments(attachments, fileIds) {
    if (attachments && attachments.length > 0)
        return attachments;
    if (fileIds && fileIds.length > 0)
        return fileIds.map((id) => ({ id, name: id }));
    return undefined;
}
/**
 * Extracts plain text content from any TChatMessage variant.
 */
function getMessageTextContent(message) {
    var _a;
    if (message.role === 'user') {
        return typeof message.content === 'string' ? message.content : '';
    }
    const { content } = message;
    if (typeof content === 'string') {
        return content;
    }
    if (Array.isArray(content)) {
        return content
            .map((item) => {
            var _a;
            if (typeof item === 'string')
                return item;
            if (item.type === 'text' && ((_a = item.data) === null || _a === void 0 ? void 0 : _a.text))
                return item.data.text;
            return '';
        })
            .join('\n');
    }
    if (content && typeof content === 'object' && 'type' in content) {
        if (content.type === 'text' && ((_a = content.data) === null || _a === void 0 ? void 0 : _a.text)) {
            return content.data.text;
        }
    }
    return '';
}
/**
 * Derives a human-readable chat name from the first user message.
 */
function deriveChatName(content) {
    const trimmed = content.trim();
    return trimmed.length > 40 ? `${trimmed.slice(0, 40)}...` : trimmed || 'New chat';
}
/**
 * AIStudioChat - a ready-to-use chat component with built-in OpenAI streaming support.
 *
 * Wraps ChatContainer and handles all state internally: streaming, message history,
 * multi-chat management, and cancellation. Requires only an `apiUrl` to start working.
 *
 * @param props - component props
 * @returns React component
 */
function AIStudioChat(props) {
    const { fileUpload, fileDialogTitle } = props, rest = tslib_1.__rest(props, ["fileUpload", "fileDialogTitle"]);
    return ((0, jsx_runtime_1.jsx)(InputContext_1.InputContextProvider, { fileUpload: fileUpload, fileDialogTitle: fileDialogTitle, children: (0, jsx_runtime_1.jsx)(AIStudioChatInner, Object.assign({}, rest)) }));
}
function AIStudioChatInner(props) {
    var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
    const { apiUrl, initialMessages = [], showHistory = false, showNewChat = showHistory, systemPrompt, requestInit, extraRequestParams, trackTokenUsage = true, onBeforeSend } = props, chatContainerProps = tslib_1.__rest(props, ["apiUrl", "initialMessages", "showHistory", "showNewChat", "systemPrompt", "requestInit", "extraRequestParams", "trackTokenUsage", "onBeforeSend"]);
    const { prepareFilesForSend, reset, contextItems, attachmentContent } = (0, InputContext_1.useInputContext)();
    // Current chat messages
    const [messages, setMessages] = (0, react_1.useState)(initialMessages);
    // Multi-chat state (used when showHistory=true)
    const [chats, setChats] = (0, react_1.useState)([]);
    const [activeChat, setActiveChat] = (0, react_1.useState)(null);
    // Per-chat message store (ref to avoid unnecessary re-renders on switch)
    const chatMessagesRef = (0, react_1.useRef)({});
    // Always-current reference to activeChat for use inside async callbacks
    // (intentionally mutable ref — not a forgotten dependency)
    const activeChatRef = (0, react_1.useRef)(null);
    activeChatRef.current = activeChat;
    // Tracks the last response id for conversation continuity (previous_response_id)
    const responseIdRef = (0, react_1.useRef)(null);
    // Streaming state
    const [controller, setController] = (0, react_1.useState)(null);
    const [isFetching, setIsFetching] = (0, react_1.useState)(false);
    const [streamSource, setStreamSource] = (0, react_1.useState)(null);
    const [streamOptions, setStreamOptions] = (0, react_1.useState)(null);
    const handleStreamEnd = (0, react_1.useCallback)((finalMessages) => {
        const committed = finalMessages.filter((msg) => msg.role !== 'assistant' || getMessageTextContent(msg) !== '');
        setMessages(committed);
        setStreamSource(null);
        setStreamOptions(null);
        // Persist messages and update the last message preview in history
        const chat = activeChatRef.current;
        if (chat) {
            chatMessagesRef.current[chat.id] = committed;
            const lastMsg = committed[committed.length - 1];
            if (lastMsg) {
                setChats((prev) => prev.map((c) => c.id === chat.id ? Object.assign(Object.assign({}, c), { lastMessage: getMessageTextContent(lastMsg) }) : c));
            }
        }
    }, []);
    const streamResult = (0, openai_1.useOpenAIStreamAdapter)(streamSource, {
        initialMessages: (_a = streamOptions === null || streamOptions === void 0 ? void 0 : streamOptions.initialMessages) !== null && _a !== void 0 ? _a : [],
        assistantMessageId: (_b = streamOptions === null || streamOptions === void 0 ? void 0 : streamOptions.assistantMessageId) !== null && _b !== void 0 ? _b : 'assistant-idle',
        onStreamEnd: handleStreamEnd,
        trackTokenUsage,
    });
    // Sync latest responseId to ref so next request can use it as previousResponseId
    (0, react_1.useEffect)(() => {
        if (streamResult.responseId) {
            responseIdRef.current = streamResult.responseId;
        }
    }, [streamResult.responseId]);
    const hasSource = Boolean(streamSource);
    const displayMessages = hasSource && streamResult.messages.length > 0 ? streamResult.messages : messages;
    const status = (0, react_1.useMemo)(() => {
        if (!hasSource) {
            return isFetching ? 'submitted' : 'ready';
        }
        if (streamResult.status === 'streaming')
            return 'streaming';
        if (streamResult.status === 'error')
            return 'error';
        return 'ready';
    }, [hasSource, isFetching, streamResult.status]);
    /**
     * Core send function. Takes an already-resolved set of previous messages and
     * pre-converted base64 images so it can be called both from handleSendMessage
     * (which converts File objects) and from handleRetry (which reuses stored images).
     */
    const performSend = (0, react_1.useCallback)(async ({ previousMessages, content, base64Images = [], perSendParams = {}, }) => {
        // Auto-create a chat when history is enabled and no chat is active yet
        if (showHistory && !activeChatRef.current) {
            const newChat = {
                id: (0, uuid_1.v4)(),
                name: deriveChatName(content),
                createTime: new Date().toISOString(),
            };
            chatMessagesRef.current[newChat.id] = [];
            activeChatRef.current = newChat;
            setActiveChat(newChat);
            setChats((prev) => [newChat, ...prev]);
        }
        const rawFileIds = Array.isArray(perSendParams.fileIds)
            ? perSendParams.fileIds
            : undefined;
        const rawFileAttachments = Array.isArray(perSendParams.fileAttachments)
            ? perSendParams.fileAttachments.filter(isFileAttachment)
            : undefined;
        const userMessage = {
            id: (0, uuid_1.v4)(),
            role: 'user',
            content,
            images: base64Images.length > 0 ? base64Images : undefined,
            fileAttachments: resolveFileAttachments(rawFileAttachments, rawFileIds),
        };
        const messagesWithUser = [...previousMessages, userMessage];
        const assistantMessageId = (0, uuid_1.v4)();
        setMessages(messagesWithUser);
        setIsFetching(true);
        const abortController = new AbortController();
        setController(abortController);
        try {
            // Last user message: plain text or multipart (text + images)
            const lastUserContent = base64Images.length > 0
                ? [
                    { type: 'text', text: content },
                    ...base64Images.map((url) => ({
                        type: 'image_url',
                        image_url: { url },
                    })),
                ]
                : content;
            const apiMessages = [
                ...(systemPrompt ? [{ role: 'system', content: systemPrompt }] : []),
                ...previousMessages
                    .filter((msg) => msg.role !== 'assistant' || getMessageTextContent(msg) !== '')
                    .map((msg) => ({
                    role: msg.role,
                    content: getMessageTextContent(msg),
                })),
                { role: 'user', content: lastUserContent },
            ];
            const response = await fetch(apiUrl, Object.assign(Object.assign({}, requestInit), { method: 'POST', headers: Object.assign({ 'Content-Type': 'application/json', Accept: 'text/event-stream' }, requestInit === null || requestInit === void 0 ? void 0 : requestInit.headers), body: JSON.stringify(Object.assign(Object.assign(Object.assign({ messages: apiMessages }, extraRequestParams), perSendParams), (responseIdRef.current
                    ? { previousResponseId: responseIdRef.current }
                    : {}))), signal: abortController.signal }));
            if (!response.ok) {
                throw new Error(`API error: ${response.status} ${response.statusText}`);
            }
            setStreamSource((0, transforms_1.normalizeMcpCallIds)((0, transforms_1.omitMcpListToolsEvents)((0, openai_1.fetchResponseToStreamEvents)(response))));
            setStreamOptions({ initialMessages: messagesWithUser, assistantMessageId });
        }
        catch (error) {
            if (error.name !== 'AbortError') {
                const errorMessage = {
                    id: (0, uuid_1.v4)(),
                    role: 'assistant',
                    content: error.message,
                };
                setMessages((prev) => {
                    const filtered = prev.filter((msg) => msg.role !== 'assistant' || getMessageTextContent(msg) !== '');
                    return [...filtered, errorMessage];
                });
            }
            setStreamSource(null);
            setStreamOptions(null);
        }
        finally {
            setIsFetching(false);
            setController(null);
        }
    }, [showHistory, systemPrompt, requestInit, apiUrl, extraRequestParams]);
    const handleSendMessage = (0, react_1.useCallback)(async (data) => {
        // Call onBeforeSend hook — allows caller to inject per-request params or cancel send
        let perSendParams = {};
        if (onBeforeSend) {
            const result = await onBeforeSend({ content: data.content });
            if (result === false || result === null)
                return;
            if (result)
                perSendParams = result;
        }
        const { base64Images, perSendFileParams } = await prepareFilesForSend(data.attachments);
        await performSend({
            previousMessages: messages,
            content: data.content,
            base64Images,
            perSendParams: Object.assign(Object.assign({}, perSendParams), perSendFileParams),
        });
        reset();
    }, [messages, onBeforeSend, performSend, prepareFilesForSend, reset]);
    const handleCancel = (0, react_1.useCallback)(async () => {
        controller === null || controller === void 0 ? void 0 : controller.abort();
        setStreamSource(null);
        setStreamOptions(null);
    }, [controller]);
    const handleRetry = (0, react_1.useCallback)(async () => {
        var _a;
        const lastUserMsg = [...messages]
            .reverse()
            .find((m) => m.role === 'user');
        if (!lastUserMsg)
            return;
        const lastUserIndex = messages.lastIndexOf(lastUserMsg);
        const trimmedMessages = messages.slice(0, lastUserIndex);
        await performSend({
            previousMessages: trimmedMessages,
            content: lastUserMsg.content,
            // Reuse images stored on the original message — no FileReader needed
            base64Images: (_a = lastUserMsg.images) !== null && _a !== void 0 ? _a : [],
        });
    }, [messages, performSend]);
    const handleCreateChat = (0, react_1.useCallback)(() => {
        const current = activeChatRef.current;
        if (current) {
            chatMessagesRef.current[current.id] = messages;
        }
        responseIdRef.current = null;
        const newChat = {
            id: (0, uuid_1.v4)(),
            name: 'New chat',
            createTime: new Date().toISOString(),
        };
        chatMessagesRef.current[newChat.id] = [];
        setChats((prev) => [newChat, ...prev]);
        setActiveChat(newChat);
        setMessages([]);
        reset();
    }, [messages, reset]);
    const handleSelectChat = (0, react_1.useCallback)((chat) => {
        var _a;
        const current = activeChatRef.current;
        if (current) {
            chatMessagesRef.current[current.id] = messages;
        }
        responseIdRef.current = null;
        setActiveChat(chat);
        setMessages((_a = chatMessagesRef.current[chat.id]) !== null && _a !== void 0 ? _a : []);
        reset();
    }, [messages, reset]);
    const handleDeleteChat = (0, react_1.useCallback)(async (chat) => {
        var _a;
        delete chatMessagesRef.current[chat.id];
        setChats((prev) => prev.filter((c) => c.id !== chat.id));
        if (((_a = activeChatRef.current) === null || _a === void 0 ? void 0 : _a.id) === chat.id) {
            responseIdRef.current = null;
            setActiveChat(null);
            setMessages([]);
            reset();
        }
    }, [reset]);
    /**
     * Update `userRating` on an assistant message inside the internal store.
     * Mirrors the change into the per-chat history map so it survives chat switches.
     */
    const setUserRating = (0, react_1.useCallback)((messageId, rating) => {
        const updateRating = (msgs) => msgs.map((msg) => msg.role === 'assistant' && msg.id === messageId
            ? Object.assign(Object.assign({}, msg), { userRating: rating }) : msg);
        setMessages(updateRating);
        const chat = activeChatRef.current;
        if (chat && chatMessagesRef.current[chat.id]) {
            chatMessagesRef.current[chat.id] = updateRating(chatMessagesRef.current[chat.id]);
        }
    }, []);
    /**
     * Build the final messageListConfig:
     * - Wrap consumer-provided Like/Dislike actions so the library toggles `userRating`
     *   automatically before delegating to the original `onClick`. Other actions and
     *   non-default (ReactNode) entries pass through unchanged.
     * - Inject a custom 'tool' renderer that renders MCP request/response sections when
     *   `mcpRequest`/`mcpResponse` are present in the tool content. A consumer-provided
     *   `messageRendererRegistry` wins on overlap.
     */
    const messageListConfig = (0, react_1.useMemo)(() => {
        var _a;
        const original = chatContainerProps.messageListConfig;
        const baseRegistry = (0, messageTypeRegistry_1.createMessageRendererRegistry)();
        (0, messageTypeRegistry_1.registerMessageRenderer)(baseRegistry, 'tool', {
            component: McpToolRenderer_1.McpToolRenderer,
        });
        const messageRendererRegistry = (0, messageTypeRegistry_1.mergeMessageRendererRegistries)(baseRegistry, (_a = original === null || original === void 0 ? void 0 : original.messageRendererRegistry) !== null && _a !== void 0 ? _a : {});
        const originalAssistantActions = original === null || original === void 0 ? void 0 : original.assistantActions;
        if (!originalAssistantActions) {
            return Object.assign(Object.assign({}, original), { messageRendererRegistry });
        }
        const wrappedAssistantActions = originalAssistantActions.map((action) => {
            const typed = action;
            const isLike = typed.type === types_1.BaseMessageActionType.Like;
            const isDislike = typed.type === types_1.BaseMessageActionType.Dislike;
            if (!isLike && !isDislike)
                return action;
            const targetRating = isLike ? 'like' : 'dislike';
            const originalOnClick = typed.onClick;
            return Object.assign(Object.assign({}, typed), { onClick: (message) => {
                    if (message.id) {
                        const nextRating = message.userRating === targetRating ? undefined : targetRating;
                        setUserRating(message.id, nextRating);
                    }
                    originalOnClick(message);
                } });
        });
        return Object.assign(Object.assign({}, original), { assistantActions: wrappedAssistantActions, messageRendererRegistry });
    }, [chatContainerProps.messageListConfig, setUserRating]);
    return ((0, jsx_runtime_1.jsx)(ChatContainer_1.ChatContainer, Object.assign({}, chatContainerProps, { messageListConfig: messageListConfig, messages: displayMessages, status: status, error: streamResult.error, onSendMessage: handleSendMessage, onCancel: handleCancel, onRetry: handleRetry, chats: showHistory ? chats : undefined, activeChat: showHistory ? activeChat : undefined, onSelectChat: showHistory ? handleSelectChat : undefined, onCreateChat: showHistory ? handleCreateChat : undefined, onDeleteChat: showHistory ? handleDeleteChat : undefined, showHistory: showHistory, showNewChat: showNewChat, promptInputProps: Object.assign(Object.assign({}, chatContainerProps.promptInputProps), { view: 'full', headerProps: Object.assign(Object.assign({}, (_c = chatContainerProps.promptInputProps) === null || _c === void 0 ? void 0 : _c.headerProps), { contextItems: (_f = (_e = (_d = chatContainerProps.promptInputProps) === null || _d === void 0 ? void 0 : _d.headerProps) === null || _e === void 0 ? void 0 : _e.contextItems) !== null && _f !== void 0 ? _f : contextItems }), footerProps: Object.assign(Object.assign({}, (_g = chatContainerProps.promptInputProps) === null || _g === void 0 ? void 0 : _g.footerProps), { attachmentContent: (_k = (_j = (_h = chatContainerProps.promptInputProps) === null || _h === void 0 ? void 0 : _h.footerProps) === null || _j === void 0 ? void 0 : _j.attachmentContent) !== null && _k !== void 0 ? _k : attachmentContent }) }) })));
}
//# sourceMappingURL=AIStudioChat.js.map
