86 lines
2.8 KiB
TypeScript
86 lines
2.8 KiB
TypeScript
"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>
|
||
);
|
||
}
|