初始化提交

This commit is contained in:
chenxin committed 2026-08-21 16:00:13 +08:00
commit 887c1f449e
963 files changed
+166869

No files matched your search

@@ -0,0 +1,85 @@
"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>
);
}