feat: AI 对话支持 A2UI 表单/审查/图表与 Agent 工具
This commit is contained in:
122
apps/admin/src/components/AiChat/AiChatDrawer.helpers.tsx
Normal file
122
apps/admin/src/components/AiChat/AiChatDrawer.helpers.tsx
Normal file
@@ -0,0 +1,122 @@
|
||||
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' },
|
||||
};
|
||||
Reference in New Issue
Block a user