Files
rootandClaude Opus 5.5 4480fc346d refactor(copy): 术语统一——收录版本、模型服务、智能体等(DEAI-2)
- 版本目标 → 收录版本(前后端用户可见字符串、能力描述、给模型的提示)
- Agent 上游 / 上游 → 模型服务;界面上的 Agent / Agent 会话 → 智能体 / 智能体会话
- 身份歧义 / 识别有歧义 / 无法确定作品 → 没识别出作品;待识别(池) → 未识别
- 建档 → 加入媒体库;资产 → 文件;库根 → 媒体库目录;纳管 → 本工具 / 外部任务;
  哨兵 → 标记文件 / 检查目录;站点快照 → 抓取;Feed / RSS 订阅 / 来源健康 → RSS
- 代码标识符、API 字段名、迁移文件不改;glossary.md 同步;smoke 选择器跟随

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-29 20:35:43 +08:00

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>
);
}