import React from 'react'; import type { BubbleListProps } from '@ant-design/x'; import type { Attachment } from '@ant-design/x/es/attachments'; import type { MessageInfo } from '@ant-design/x-sdk'; import { Tooltip } from 'antd'; import type { AiAttachment, AiChatMessage, AiConversation } from './types'; export interface ConversationData extends AiConversation { key: string; label: string; } export type ConversationRunStatus = 'running' | 'done' | 'error' | 'stopped'; export function conversationStatusMeta(status: ConversationRunStatus): { label: string; color: string; } { if (status === 'running') return { label: '生成中', color: 'processing' }; if (status === 'done') return { label: '已完成', color: 'success' }; if (status === 'error') return { label: '失败', color: 'error' }; return { label: '已停止', color: 'default' }; } export function sortConversations(items: AiConversation[]): AiConversation[] { return [...items].sort((a, b) => { const aTime = new Date(a.lastMessageAt || a.updatedAt).getTime(); const bTime = new Date(b.lastMessageAt || b.updatedAt).getTime(); return bTime - aTime; }); } export function toConversationData(item: AiConversation): ConversationData { return { ...item, key: String(item.id), label: item.title }; } export function toUploadFile(attachment: AiAttachment): Attachment { return { uid: String(attachment.id), name: attachment.name, size: attachment.size, status: attachment.status === 'ready' ? 'done' : attachment.status === 'failed' ? 'error' : 'uploading', url: attachment.url, response: attachment, description: attachment.error || undefined, cardType: attachment.mimeType.startsWith('image/') ? 'image' : 'file', }; } export function emptyAssistant(): AiChatMessage { return { role: 'assistant', content: '', reasoningContent: '', toolRuns: [], attachments: [], }; } /** * 当前会话内新发送的用户消息还没有服务端数字 ID(本地为 msg_N 临时 key), * 但紧随其后的 AI 回答会携带 replyToMessageId,可据此反推用户消息 ID。 */ export function resolveUserMessageId( info: MessageInfo, all: MessageInfo[], ): number | null { if (typeof info.message.id === 'number') return info.message.id; const index = all.findIndex((item) => item.id === info.id); if (index === -1) return null; for (const item of all.slice(index + 1)) { if (typeof item.message.replyToMessageId === 'number') { return item.message.replyToMessageId; } } return null; } export interface HoverActionItem { key: string; title: string; icon: React.ReactNode; danger?: boolean; onClick: () => void; } /** Codex Desktop 风格:hover 消息时在气泡外显示的纯图标操作,不包裹 Button */ export function MessageHoverActions({ items }: { items: HoverActionItem[] }) { return (
{items.map((item) => ( { if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); item.onClick(); } }} > {item.icon} ))}
); } export const aiBubbleRoles: BubbleListProps['role'] = { user: { placement: 'end', variant: 'filled', shape: 'corner' }, assistant: { placement: 'start', variant: 'borderless' }, };