study / web /src /components /chat /user-message.tsx
xiaolongmr
添加编辑
638cad0
Raw History Blame Contribute Delete
5.71 kB
"use client";
import { Sparkles, Trash2 } from "lucide-react";
export type UserMessageMeta = {
items: Array<{ label: string }>;
};
type UserMessageProps = {
/** 元信息行(第几轮 / 场景 / 平台 / 时间等) */
meta: string[];
/** 用户提示词文本 */
prompt: string;
/** 参考图缩略图(dataURL 或 URL) */
referenceThumbnails: string[];
/** 参考图放大用原图(缺省回退缩略图) */
referenceFullImages?: string[];
/** 是否显示操作按钮 */
showActions?: boolean;
onReusePrompt?: () => void;
onReuseConfig?: () => void;
onDeletePrompt?: () => void;
onOpenLightbox?: (images: { id: string; src: string }[], index: number) => void;
};
/**
* 用户消息(画图页 / 商品视觉页共用)
*
* 右对齐纯文本 + 元信息行 + 复用提示词/复用配置/删除 + 参考图(可点击放大)。
* 两页面结构完全一致,仅数据不同,通过 props 泛化。
*/
export function UserMessage({
meta,
prompt,
referenceThumbnails,
referenceFullImages,
showActions = true,
onReusePrompt,
onReuseConfig,
onDeletePrompt,
onOpenLightbox,
}: UserMessageProps) {
return (
<div className="flex justify-end">
<div className="max-w-[90%] px-1 py-1 text-[14px] leading-6 text-stone-900 sm:max-w-[82%] sm:text-[15px] sm:leading-7">
{meta.length > 0 && (
<div className="mb-1.5 flex flex-wrap justify-end gap-2 text-[11px] text-stone-400 sm:mb-2">
{meta.map((item, index) => (
<span key={index}>{item}</span>
))}
</div>
)}
<div className="text-right">{prompt}</div>
{showActions && (onReusePrompt || onReuseConfig || onDeletePrompt) && (
<div className="mt-2 flex flex-wrap justify-end gap-1.5">
{onReusePrompt && (
<button
type="button"
onClick={onReusePrompt}
className="inline-flex items-center gap-1 rounded-full bg-stone-100 px-2.5 py-1 text-[11px] font-medium text-stone-600 transition hover:bg-stone-200 hover:text-stone-900"
>
复用提示词
</button>
)}
{onReuseConfig && (
<button
type="button"
onClick={onReuseConfig}
className="inline-flex items-center gap-1 rounded-full bg-stone-100 px-2.5 py-1 text-[11px] font-medium text-stone-600 transition hover:bg-stone-200 hover:text-stone-900"
>
复用配置
</button>
)}
{onDeletePrompt && (
<button
type="button"
onClick={onDeletePrompt}
className="inline-flex size-6 items-center justify-center rounded-full text-stone-300 transition hover:bg-rose-50 hover:text-rose-500"
aria-label="删除提示词记录"
>
<Trash2 className="size-3" />
</button>
)}
</div>
)}
</div>
</div>
);
}
/**
* 本轮参考图(画图页 / 商品视觉页共用)
*
* 右侧缩略图列表,点击打开原图放大(Fancybox)。
*/
export function ReferenceImages({
thumbnails,
fullImages,
taskPrefix,
onOpenLightbox,
onAddToEditor,
}: {
thumbnails: string[];
fullImages?: string[];
taskPrefix: string;
onOpenLightbox: (images: { id: string; src: string }[], index: number) => void;
onAddToEditor?: (src: string, index: number) => void | Promise<void>;
}) {
if (thumbnails.length === 0) return null;
return (
<div className="flex justify-end">
<div className="flex flex-col items-end">
<div className="mb-2 text-xs font-medium text-stone-500">本轮参考图</div>
<div className="flex flex-wrap justify-end gap-3">
{thumbnails.map((src, index) => {
const fullSrc = fullImages?.[index] ?? src;
return (
<div key={index} className="flex flex-col items-end gap-2">
<button
type="button"
onClick={() => {
onOpenLightbox(
thumbnails.map((s, i) => ({
id: `${taskPrefix}-ref-${i}`,
src: fullImages?.[i] ?? s,
})),
index,
);
}}
className="group relative size-20 overflow-hidden rounded-lg border border-stone-200/80 bg-stone-100/60 text-left transition hover:border-stone-300 sm:size-24"
aria-label={`预览参考图 ${index + 1}`}
title="点击放大查看"
>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={src}
alt={`参考图 ${index + 1}`}
className="absolute inset-0 h-full w-full object-cover transition duration-200 group-hover:scale-[1.02]"
/>
</button>
{onAddToEditor ? (
<button
type="button"
onClick={() => void onAddToEditor(fullSrc, index)}
className="inline-flex items-center gap-1 rounded-full border border-stone-200 bg-white px-2.5 py-1 text-[11px] font-medium text-stone-700 transition hover:bg-stone-50"
>
<Sparkles className="size-3.5" />
加入编辑
</button>
) : null}
</div>
);
})}
</div>
</div>
</div>
);
}