----
"use client";

import { useChat } from "@/hooks/chat";
import { useChatStore } from "@/stores";
import { useSearchParams } from "next/navigation";
import { useEffect, useRef, useState } from "react";
import { AskInput } from "./ask-input";
import { role } from "../../generated";
import MessagesList from "./messages-list";
import { StarterQuestionsList } from "./starter-questions";

const useAutoScroll = (ref: React.RefObject<HTMLDivElement>) => {
  const { messages } = useChatStore();

  useEffect(() => {
    if (messages.at(-1)?.role === role.USER) {
      ref.current?.scrollIntoView({
        behavior: "smooth",
        block: "end",
      });
    }
  }, [messages, ref]);
};

const useAutoResizeInput = (
  ref: React.RefObject<HTMLDivElement>,
  setWidth: (width: number) => void,
) => {
  const { messages } = useChatStore();

  useEffect(() => {
    const updatePosition = () => {
      if (ref.current) {
        setWidth(ref.current.scrollWidth);
      }
    };
    updatePosition();
    window.addEventListener("resize", updatePosition);
    return () => {
      window.removeEventListener("resize", updatePosition);
    };
  }, [messages, ref, setWidth]);
};

const useAutoFocus = (ref: React.RefObject<HTMLTextAreaElement>) => {
  useEffect(() => {
    ref.current?.focus();
  }, [ref]);
};

export const ChatPanel = ({ threadId }: { threadId?: number }) => {
  const searchParams = useSearchParams();
  const queryMessage = searchParams.get("q");
  const hasRun = useRef(false);

  const {
    handleSend,
    streamingMessage,
    isStreamingMessage,
    isStreamingProSearch,
  } = useChat();
  const { messages, setThreadId } = useChatStore();

  const [width, setWidth] = useState(0);
  const messagesRef = useRef<HTMLDivElement | null>(null);
  const messageBottomRef = useRef<HTMLDivElement | null>(null);
  const inputRef = useRef<HTMLTextAreaElement | null>(null);

  useAutoScroll(messageBottomRef);
  useAutoResizeInput(messagesRef, setWidth);
  useAutoFocus(inputRef);

  useEffect(() => {
    if (queryMessage && !hasRun.current) {
      hasRun.current = true; // 先设置为 true，防止 handleSend 中的状态变化重新触发
      setThreadId(null);
      handleSend(queryMessage);
    }
    // 添加 queryMessage 和 handleSend 作为依赖项
  }, [queryMessage, handleSend, setThreadId]);

  return (
    <>
      {messages.length > 0 || threadId ? (
        // 移除 isLoading 判断，直接渲染消息列表
        <div ref={messagesRef} className="pt-10 w-full relative">
          <MessagesList
            messages={messages}
            streamingMessage={streamingMessage}
            isStreamingMessage={isStreamingMessage}
            isStreamingProSearch={isStreamingProSearch}
            onRelatedQuestionSelect={handleSend}
          />
          <div ref={messageBottomRef} className="h-0" />
          <div
            className="bottom-12 fixed px-2 max-w-screen-md justify-center items-center md:px-2"
            style={{ width: `${width}px` }}
          >
            <AskInput isFollowingUp sendMessage={handleSend} />
          </div>
        </div>
      ) : (
        <div className="w-full flex flex-col justify-center items-center">
          <div className="flex items-center justify-center mb-8">
            <span className="text-3xl">Mindra Search - 思索</span>
          </div>
          <AskInput sendMessage={handleSend} />
          <div className="w-full flex flex-row px-3 justify-between space-y-2 pt-1">
            <StarterQuestionsList handleSend={handleSend} />
          </div>
        </div>
      )}
    </>
  );
};----
---
import { AssistantMessageContent } from "./assistant-message";
import { Separator } from "./ui/separator";
import { UserMessageContent } from "./user-message";
import { memo } from "react";
import {
  ChatState,
  role,
} from "../../generated";
import { ProSearchRender } from "./pro-search-render";

const MessagesList = ({
  messages,
  streamingMessage,
  isStreamingMessage,
  isStreamingProSearch,
  onRelatedQuestionSelect,
}: {
  messages: ChatState[];
  streamingMessage: ChatState | null;
  isStreamingMessage: boolean;
  isStreamingProSearch: boolean;
  onRelatedQuestionSelect: (question: string) => void;
}) => {
  const streamingProResponse = streamingMessage?.pro_response;
  return (
    <div className="flex flex-col pb-28">
      {messages.map((message, index) =>
        message.role === role.USER ? (
          <UserMessageContent key={index} message={message} />
        ) : (
          <>
            {message.pro_response && (
              <ProSearchRender streamingProResponse={message.pro_response} />
            )}
            <AssistantMessageContent
              key={index}
              message={message}
              onRelatedQuestionSelect={onRelatedQuestionSelect}
            />
            {index !== messages.length - 1 && <Separator />}
          </>
        ),
      )}
      {isStreamingProSearch && (
        <div className="mb-4">
          <ProSearchRender
            streamingProResponse={streamingProResponse ?? null}
            isStreamingProSearch={isStreamingProSearch}
          />
        </div>
      )}
      {streamingMessage && isStreamingMessage && (
        <AssistantMessageContent
          message={streamingMessage}
          isStreaming={true}
          onRelatedQuestionSelect={onRelatedQuestionSelect}
        />
      )}
    </div>
  );
};

export default memo(MessagesList);
------
import { useMutation } from "@tanstack/react-query";
import {
  SearchStreamEvent,
  SearchBeginStream,
  SearchEndStream,
  SearchResultStream,
  SearchTextChunkStream,
  SearchRelatedQueriesStream,
  ProSearchPlanStream,
  ProSearchQueryStream,
  ProSearchReadResultsStream,
  SearchErrorStream,
  ChatSearchResponseEvent,
  ProSearchStep,
  ProSearchStepStatus,
  ChatRequest,
  ChatMessage,
  role,
  ChatState,
} from "../../generated";
import Error from "next/error";
import {
  fetchEventSource,
  FetchEventSourceInit,
} from "@microsoft/fetch-event-source";
import { useState } from "react";
import { useConfigStore, useChatStore } from "@/stores";
import { env } from "../env.mjs";
import { useRouter } from "next/navigation";

const BASE_URL = env.NEXT_PUBLIC_API_URL;

const streamChat = async ({
  request,
  onMessage,
}: {
  request: ChatRequest;
  onMessage?: FetchEventSourceInit["onmessage"];
}): Promise<void> => {
  return await fetchEventSource(`${BASE_URL}/mindra/chatSearch`, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
    },
    keepalive: true,
    openWhenHidden: true,
    body: JSON.stringify({ ...request }),
    onmessage: onMessage,
    onerror: (error) => { },
  });
};

const convertToChatRequest = (query: string, history: ChatState[], requestId: (string | number)) => {
  const newHistory: ChatMessage[] = history.map((message) => ({
    role:
      message.role === role.USER
        ? role.USER
        : role.ASSISTANT,
    content: message.content,
  }));
  return { query, history: newHistory, requestId };
};

export const useChat = () => {
  const { addMessage, messages, threadId, setThreadId } = useChatStore();
  const { proMode } = useConfigStore();

  const [streamingMessage, setStreamingMessage] = useState<ChatState | null>(
    null,
  );
  const [isStreamingProSearch, setIsStreamingProSearch] = useState(false);
  const [isStreamingMessage, setIsStreamingMessage] = useState(false);

  let steps_details: ProSearchStep[] = [];

  const handleEvent = (eventItem: ChatSearchResponseEvent, state: ChatState) => {
    switch (eventItem.event) {
      case SearchStreamEvent.STREAM_BEGIN:
        setIsStreamingMessage(true);
        setStreamingMessage({
          ...state,
          role: role.ASSISTANT,
          content: "",
          related_queries: [],
          sources: [],
          images: [],
        });
        break;
      case SearchStreamEvent.SEARCH_RESULTS:
        const data = eventItem.data as SearchResultStream;
        state.sources = data.results ?? [];
        state.images = data.images ?? [];
        break;
      case SearchStreamEvent.TEXT_CHUNK:
        state.content += (eventItem.data as SearchTextChunkStream).text;

        if (!state.pro_response) {
          break;
        }
        // Hide the pro search once we start streaming
        steps_details = steps_details.map((step) => ({
          ...step,
          status: ProSearchStepStatus.DONE,
        }));
        state.pro_response = {
          steps_details: steps_details,
        };

        break;
      case SearchStreamEvent.RELATED_QUERIES:
        state.related_queries =
          (eventItem.data as SearchRelatedQueriesStream).related_queries ?? [];
        break;
      case SearchStreamEvent.STREAM_END:
        const endData = eventItem.data as SearchEndStream;
        addMessage({ ...state });
        setStreamingMessage(null);
        setIsStreamingMessage(false);
        setIsStreamingProSearch(false);

      // Only if the backend is using the DB
      // if (endData.thread_id) {
      //   setThreadId(endData.thread_id);
      //   window.history.pushState({}, "", `/search/${endData.thread_id}`);
      // }
      // return;
      case SearchStreamEvent.PRO_SEARCH_PLAN:
        const { steps } = eventItem.data as ProSearchPlanStream;
        steps_details =
          steps?.map((step, index) => ({
            step: step,
            queries: [],
            results: [],
            status: ProSearchStepStatus.DEFAULT,
            step_number: index,
          })) ?? [];

        steps_details[0].status = ProSearchStepStatus.CURRENT;
        state.pro_response = {
          steps_details: steps_details,
        };
        break;
      case SearchStreamEvent.PRO_SEARCH_QUERIES:
        const { queries, step_number: queryStepNumber } =
          eventItem.data as ProSearchQueryStream;
        steps_details[queryStepNumber].queries = queries;
        steps_details[queryStepNumber].status = ProSearchStepStatus.CURRENT;
        if (queryStepNumber !== 0) {
          steps_details[queryStepNumber - 1].status =
            ProSearchStepStatus.DONE;
        }
        state.pro_response = {
          steps_details: steps_details,
        };
        break;
      case SearchStreamEvent.PRO_SEARCH_READ_RESULTS:
        const { results, step_number: resultsStepNumber } =
          eventItem.data as ProSearchReadResultsStream;
        steps_details[resultsStepNumber].results = results;

        break;
      case SearchStreamEvent.PRO_SEARCH_DONE:
        break;
      case SearchStreamEvent.ERROR:
        const errorData = eventItem.data as SearchErrorStream;
        addMessage({
          role: role.ASSISTANT,
          content: errorData.detail,
          related_queries: [],
          sources: [],
          images: [],
          pro_response: state.pro_response,
          is_error_message: true,
        });
        setStreamingMessage(null);
        setIsStreamingMessage(false);
        setIsStreamingProSearch(false);
        return;
    }
    setStreamingMessage({
      role: role.ASSISTANT,
      content: state.content,
      related_queries: state.related_queries,
      sources: state.sources,
      images: state.images,
      pro_response:
        state.pro_response !== null
          ? {
            steps: steps_details.map((step) => step.step),
            steps_details: steps_details,
          }
          : null,
    });
  };

  const { mutateAsync: chat } = useMutation<void, Error, ChatRequest>({
    retry: false,
    mutationFn: async (request) => {
      const state: ChatState = {
        role: role.ASSISTANT,
        content: "",
        sources: [],
        related_queries: [],
        images: [],
        pro_response: null,
      };
      addMessage({ role: role.USER, content: request.query });
      setIsStreamingProSearch(proMode);

      const req = {
        ...request,
        requestId: (threadId || '').toString(),
        proSearch: proMode,
      };

      await streamChat({
        request: req,
        onMessage: (event) => {
          // Handles keep-alive events
          if (!event.data) return;

          const eventItem: ChatSearchResponseEvent = JSON.parse(event.data);
          handleEvent(eventItem, state);
        },
      });
    },
  });

  const handleSend = async (query: string) => {
    await chat(convertToChatRequest(query, messages, (threadId || '').toString()));
  };

  return {
    handleSend,
    streamingMessage,
    isStreamingMessage,
    isStreamingProSearch,
  };
};
------BUG/ERROR： 发送一个问题，但是会循环请求