Files
gongxue-base/apps/admin/src/components/AiChat/AiChatDrawer.helpers.tsx

123 lines
3.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<AiAttachment> {
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<AiChatMessage>,
all: MessageInfo<AiChatMessage>[],
): 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 (
<div className="ai-chat-hover-actions" role="toolbar" aria-label="消息操作">
{items.map((item) => (
<Tooltip key={item.key} title={item.title}>
<span
role="button"
tabIndex={0}
className={`ai-chat-hover-action${item.danger ? ' is-danger' : ''}`}
aria-label={item.title}
onClick={item.onClick}
onKeyDown={(event) => {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
item.onClick();
}
}}
>
{item.icon}
</span>
</Tooltip>
))}
</div>
);
}
export const aiBubbleRoles: BubbleListProps['role'] = {
user: { placement: 'end', variant: 'filled', shape: 'corner' },
assistant: { placement: 'start', variant: 'borderless' },
};