- 版本目标 → 收录版本(前后端用户可见字符串、能力描述、给模型的提示) - Agent 上游 / 上游 → 模型服务;界面上的 Agent / Agent 会话 → 智能体 / 智能体会话 - 身份歧义 / 识别有歧义 / 无法确定作品 → 没识别出作品;待识别(池) → 未识别 - 建档 → 加入媒体库;资产 → 文件;库根 → 媒体库目录;纳管 → 本工具 / 外部任务; 哨兵 → 标记文件 / 检查目录;站点快照 → 抓取;Feed / RSS 订阅 / 来源健康 → RSS - 代码标识符、API 字段名、迁移文件不改;glossary.md 同步;smoke 选择器跟随 Co-Authored-By: Claude Opus 5.5 <[email protected]>
156 lines
5.1 KiB
TypeScript
156 lines
5.1 KiB
TypeScript
// Adapted from agentui (https://github.com/ashish200729/agentui), components/agents/message.tsx + message-context.tsx
|
|
// Copyright (c) 2026 Saurabh Chauhan. MIT License, see ./LICENSE-agentui.md
|
|
// 改造:motion.article 的弹簧入场换成 tw-animate 的 animate-in(duration-base / ease-pop,motion-reduce 关闭);
|
|
// 头像底色、字号(text-label / text-meta)、圆角换语义 token;新增 from="system" 行(居中提示,用于压缩/中止等系统消息);
|
|
// MessageTyping 三点动画用 animate-pulse + 具名时长的 animation-delay。
|
|
import { type ComponentPropsWithRef, createContext, useContext } from "react";
|
|
import { cn } from "~/lib/utils.ts";
|
|
|
|
export type MessageFrom = "user" | "assistant" | "system";
|
|
export type MessageSide = "start" | "end";
|
|
|
|
export const MessageSideContext = createContext<MessageSide | undefined>(undefined);
|
|
const MessageContext = createContext<{ from: MessageFrom }>({ from: "assistant" });
|
|
|
|
export interface MessageProps extends ComponentPropsWithRef<"article"> {
|
|
from: MessageFrom;
|
|
/** 挂载时播一次入场(新到达的消息用;回放历史时不播) */
|
|
animateIn?: boolean;
|
|
}
|
|
|
|
const FROM_LABEL: Record<MessageFrom, string> = { user: "用户消息", assistant: "智能体消息", system: "系统消息" };
|
|
|
|
export function Message({ from, animateIn = false, className, children, ...props }: MessageProps) {
|
|
return (
|
|
<MessageSideContext.Provider value={from === "user" ? "end" : "start"}>
|
|
<MessageContext.Provider value={{ from }}>
|
|
<article
|
|
data-slot="message"
|
|
data-from={from}
|
|
aria-label={props["aria-label"] ?? FROM_LABEL[from]}
|
|
{...props}
|
|
className={cn(
|
|
"group/message flex w-full min-w-0 items-start gap-2.5",
|
|
from === "user" ? "flex-row-reverse" : "flex-row",
|
|
animateIn &&
|
|
"animate-in fade-in-0 slide-in-from-bottom-1 duration-base ease-pop motion-reduce:animate-none",
|
|
className,
|
|
)}
|
|
>
|
|
{children}
|
|
</article>
|
|
</MessageContext.Provider>
|
|
</MessageSideContext.Provider>
|
|
);
|
|
}
|
|
|
|
export function MessageGroup({
|
|
spacing = "default",
|
|
className,
|
|
...props
|
|
}: ComponentPropsWithRef<"div"> & { spacing?: "compact" | "default" }) {
|
|
return (
|
|
<div
|
|
data-slot="message-group"
|
|
className={cn("flex w-full flex-col", spacing === "compact" ? "gap-1.5" : "gap-4", className)}
|
|
{...props}
|
|
/>
|
|
);
|
|
}
|
|
|
|
export function MessageAvatar({
|
|
placeholder = false,
|
|
className,
|
|
...props
|
|
}: ComponentPropsWithRef<"div"> & { placeholder?: boolean }) {
|
|
return (
|
|
<div
|
|
data-slot="message-avatar"
|
|
aria-hidden
|
|
className={cn(
|
|
"mt-0.5 grid size-7 shrink-0 place-items-center overflow-hidden rounded-full bg-foreground/[0.06] text-meta font-medium text-muted-foreground [&_svg]:size-3.5",
|
|
placeholder && "invisible",
|
|
className,
|
|
)}
|
|
{...props}
|
|
/>
|
|
);
|
|
}
|
|
|
|
export function MessageContent({ className, ...props }: ComponentPropsWithRef<"div">) {
|
|
const { from } = useContext(MessageContext);
|
|
return (
|
|
<div
|
|
data-slot="message-content"
|
|
className={cn("flex min-w-0 flex-1 flex-col gap-1.5", from === "user" ? "items-end" : "items-start", className)}
|
|
{...props}
|
|
/>
|
|
);
|
|
}
|
|
|
|
export function MessageHeader({ className, ...props }: ComponentPropsWithRef<"div">) {
|
|
const { from } = useContext(MessageContext);
|
|
return (
|
|
<div
|
|
data-slot="message-header"
|
|
className={cn(
|
|
"flex items-center gap-1.5 px-1 text-label leading-none text-muted-foreground",
|
|
from === "user" ? "justify-end" : "justify-start",
|
|
className,
|
|
)}
|
|
{...props}
|
|
/>
|
|
);
|
|
}
|
|
|
|
export function MessageFooter({ className, ...props }: ComponentPropsWithRef<"div">) {
|
|
const { from } = useContext(MessageContext);
|
|
return (
|
|
<div
|
|
data-slot="message-footer"
|
|
className={cn(
|
|
"flex min-h-5 items-center gap-1 px-1 text-label text-muted-foreground",
|
|
from === "user" ? "justify-end" : "justify-start",
|
|
className,
|
|
)}
|
|
{...props}
|
|
/>
|
|
);
|
|
}
|
|
|
|
/** 居中的系统提示条(上下文压缩、续跑等) */
|
|
export function MessageMarker({ className, ...props }: ComponentPropsWithRef<"div">) {
|
|
return (
|
|
<div
|
|
data-slot="message-marker"
|
|
role="note"
|
|
className={cn(
|
|
"mx-auto flex w-fit max-w-[88%] items-center gap-1.5 rounded-full border border-border/60 bg-foreground/[0.04] px-2.5 py-1 text-center text-meta text-muted-foreground",
|
|
className,
|
|
)}
|
|
{...props}
|
|
/>
|
|
);
|
|
}
|
|
|
|
const DOT_DELAY = ["", "[animation-delay:var(--duration-micro)]", "[animation-delay:var(--duration-layout)]"];
|
|
|
|
export function MessageTyping({
|
|
label = "正在回复",
|
|
className,
|
|
...props
|
|
}: ComponentPropsWithRef<"span"> & { label?: string }) {
|
|
return (
|
|
<span data-slot="message-typing" className={cn("inline-flex h-5 items-center gap-1", className)} {...props}>
|
|
<span className="sr-only">{label}</span>
|
|
{DOT_DELAY.map((d, i) => (
|
|
<span
|
|
key={i}
|
|
aria-hidden
|
|
className={cn("size-1 rounded-full bg-current opacity-60 animate-pulse motion-reduce:animate-none", d)}
|
|
/>
|
|
))}
|
|
</span>
|
|
);
|
|
}
|