Files
hunjing/frontend/src/components/chat/MessageList.tsx
T
2026-08-21 16:00:13 +08:00

86 lines
2.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { useEffect, useRef } from "react";
import { EmptyState } from "@/components/shared/StateViews";
import { MessageBubble } from "./MessageBubble";
import { useChatStore } from "@/lib/stores/chat-store";
/** 工具确认回调签名:传 run_id / confirmation_id / tool_call_id 供端点调用 */
export type ToolConfirmationHandler = (
runId: string,
confirmationId: string,
toolCallId: string,
) => void;
/**
* 消息列表:自管理滚动到底。
*
* 计划 todo(SessionPlanCard)暂不挂载——当前阶段(implementation-status.md §8)不实现
* 自动长期任务规划,AI 单次回复内的推演已由回复气泡的「思考过程」承担。待后端
* plan.* 运行事件落地(planner-design.md §5)后,再于「AI 实际产出计划」时挂载,
* 无计划则不渲染(而非用 mock 常驻显示)。
*/
export function MessageList({
onApproveToolCall,
onDenyToolCall,
onRegenerate,
characterName,
}: {
onApproveToolCall?: ToolConfirmationHandler;
onDenyToolCall?: ToolConfirmationHandler;
onRegenerate?: () => void;
characterName?: string;
} = {}) {
const messages = useChatStore((s) => s.messages);
const sending = useChatStore((s) => s.sending);
const bottomRef = useRef<HTMLDivElement>(null);
useEffect(() => {
bottomRef.current?.scrollIntoView({ behavior: "smooth" });
}, [messages]);
// 空会话:不展示计划 todo,仅空状态引导
if (messages.length === 0) {
return (
<div className="flex flex-1 items-center justify-center p-6">
<EmptyState
title="镜面空明"
description="发送一条文本消息,开启与当前人物的对话。"
/>
</div>
);
}
// 找到最后一条 assistant 消息的索引:用于在该气泡下显示「重新回答」按钮。
// 发送中(sending)时正在生成新的助手消息,此时不显示该按钮——由 MessageBubble
// 内部根据 isLastAssistant && !sending 综合判定。
let lastAssistantIdx = -1;
for (let i = messages.length - 1; i >= 0; i--) {
if (messages[i].role === "assistant") {
lastAssistantIdx = i;
break;
}
}
return (
<div className="flex-1 overflow-y-auto px-4 py-6 sm:px-6">
<div className="space-y-5">
{messages.map((message, idx) => (
<MessageBubble
key={message.message_id}
message={message}
isLastAssistant={idx === lastAssistantIdx}
canRegenerate={idx === lastAssistantIdx && !sending}
onApproveToolCall={onApproveToolCall}
onDenyToolCall={onDenyToolCall}
onRegenerate={onRegenerate}
characterName={characterName}
/>
))}
<div ref={bottomRef} />
</div>
</div>
);
}