style: chat message render markdown by default
This commit is contained in:
@@ -2,7 +2,7 @@ import { createFromIconfontCN } from '@ant-design/icons';
|
||||
// import './iconfont/iconfont.js';
|
||||
|
||||
const IconFont = createFromIconfontCN({
|
||||
scriptUrl: '//at.alicdn.com/t/c/font_4613488_yikpvmcl8ag.js'
|
||||
scriptUrl: '//at.alicdn.com/t/c/font_4613488_s7szfcrvqw.js'
|
||||
});
|
||||
|
||||
export default IconFont;
|
||||
|
||||
@@ -9,6 +9,13 @@
|
||||
border-color: var(--ant-color-split);
|
||||
}
|
||||
|
||||
hr {
|
||||
border: none;
|
||||
height: 1px;
|
||||
background-color: var(--ant-color-split);
|
||||
border-color: var(--ant-color-split);
|
||||
}
|
||||
|
||||
p {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
@@ -140,6 +140,7 @@ export default {
|
||||
'playground.image.edit': 'Edit',
|
||||
'playground.image.fitview': 'Fit View',
|
||||
'playground.chat.aithought': 'CoT',
|
||||
'playground.chat.thinking': 'Thinking...',
|
||||
'playground.image.mask.uploaded': 'Mask Uploaded',
|
||||
'playground.image.mask.upload':
|
||||
'Upload Mask: No additional drawing allowed after upload.',
|
||||
|
||||
@@ -137,6 +137,7 @@ export default {
|
||||
'playground.image.edit': 'Редактировать',
|
||||
'playground.image.fitview': 'Подогнать размер',
|
||||
'playground.chat.aithought': 'Рассуждение (CoT)',
|
||||
'playground.chat.thinking': 'TODO: Translate key "playground.chat.thinking"',
|
||||
'playground.image.mask.uploaded': 'Маска загружена',
|
||||
'playground.image.mask.upload':
|
||||
'TODO: Translate key "playground.image.mask.upload"',
|
||||
|
||||
@@ -135,6 +135,7 @@ export default {
|
||||
'playground.image.edit': '编辑图片',
|
||||
'playground.image.fitview': '适应视图',
|
||||
'playground.chat.aithought': '思考过程',
|
||||
'playground.chat.thinking': '思考中...',
|
||||
'playground.image.mask.uploaded': '遮罩已上传',
|
||||
'playground.image.mask.upload': '上传遮罩:上传后将不可再绘制',
|
||||
'playground.params.frequency_penalty.tips': `数值介于 -2.0 和 2.0 之间。正值会根据新词在文本中已出现的频率对其进行惩罚,从而降低模型逐字重复相同内容的可能性。`,
|
||||
|
||||
@@ -40,7 +40,8 @@ const GroundLeft: React.FC<MessageProps> = forwardRef((props, ref) => {
|
||||
const [actions, setActions] = useState<MessageItemAction[]>([
|
||||
'upload',
|
||||
'delete',
|
||||
'copy'
|
||||
'copy',
|
||||
'edit'
|
||||
]);
|
||||
|
||||
const {
|
||||
@@ -107,15 +108,6 @@ const GroundLeft: React.FC<MessageProps> = forwardRef((props, ref) => {
|
||||
setShow(false);
|
||||
};
|
||||
|
||||
const handleOnCheck = (e: any) => {
|
||||
const checked = e.target.checked;
|
||||
if (checked) {
|
||||
setActions(['upload', 'delete', 'copy', 'markdown']);
|
||||
} else {
|
||||
setActions(['upload', 'delete', 'copy']);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="ground-left-wrapper">
|
||||
<div className="ground-left">
|
||||
@@ -163,18 +155,8 @@ const GroundLeft: React.FC<MessageProps> = forwardRef((props, ref) => {
|
||||
minRows: 5,
|
||||
maxRows: 5
|
||||
}}
|
||||
actions={[
|
||||
'clear',
|
||||
'layout',
|
||||
'role',
|
||||
'upload',
|
||||
'add',
|
||||
'paste',
|
||||
'check'
|
||||
]}
|
||||
actions={['clear', 'layout', 'role', 'upload', 'add', 'paste']}
|
||||
defaultChecked={false}
|
||||
checkLabel="Markdown"
|
||||
onCheck={handleOnCheck}
|
||||
loading={loading}
|
||||
disabled={!parameters.model}
|
||||
isEmpty={!messageList.length}
|
||||
|
||||
@@ -285,7 +285,9 @@ const MessageInput: React.FC<MessageInputProps> = forwardRef(
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
handleSendMessage();
|
||||
if (!loading) {
|
||||
handleSendMessage();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
import IconFont from '@/components/icon-font';
|
||||
import { StopOutlined } from '@ant-design/icons';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import React from 'react';
|
||||
import { Button } from 'antd';
|
||||
import React, { useMemo } from 'react';
|
||||
import { Roles } from '../../config';
|
||||
import { MessageItem, MessageItemAction } from '../../config/types';
|
||||
import '../../style/content-item.less';
|
||||
import MessageActions from './message-actions';
|
||||
import MessageBody from './message-body';
|
||||
import ThinkParser from './think-parser';
|
||||
|
||||
interface MessageItemProps {
|
||||
data: MessageItem;
|
||||
@@ -15,10 +20,81 @@ interface MessageItemProps {
|
||||
onDelete?: () => void;
|
||||
}
|
||||
|
||||
const contentRenderOptions = [
|
||||
{ label: 'Markdown', value: 'markdown' },
|
||||
{ label: 'Plain Text', value: 'plain' }
|
||||
];
|
||||
interface SaveActionsProps {
|
||||
handleSave: () => void;
|
||||
handleCancel: () => void;
|
||||
}
|
||||
|
||||
interface ThoughtStatusProps {
|
||||
collapsed: boolean;
|
||||
setCollapsed: (collapsed: boolean) => void;
|
||||
isThinking: boolean;
|
||||
}
|
||||
|
||||
const SaveActions: React.FC<SaveActionsProps> = ({
|
||||
handleSave,
|
||||
handleCancel
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
return (
|
||||
<div className="save-actions">
|
||||
<div className="actions-wrap gap-8">
|
||||
<Button
|
||||
size="small"
|
||||
icon={<StopOutlined />}
|
||||
onClick={handleCancel}
|
||||
style={{ borderRadius: 'var(--border-radius-base)' }}
|
||||
>
|
||||
{intl.formatMessage({ id: 'common.button.cancel' })}
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
type="primary"
|
||||
icon={<IconFont type="icon-save3" />}
|
||||
onClick={handleSave}
|
||||
style={{ borderRadius: 'var(--border-radius-base)' }}
|
||||
>
|
||||
{intl.formatMessage({ id: 'common.button.save' })}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const ThoughtStatus: React.FC<ThoughtStatusProps> = ({
|
||||
collapsed,
|
||||
setCollapsed,
|
||||
isThinking
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
return (
|
||||
<div
|
||||
className="flex-center"
|
||||
style={{
|
||||
marginLeft: 10,
|
||||
borderRadius: 'var(--border-radius-base)',
|
||||
width: 'max-content',
|
||||
overflow: 'hidden'
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
size="small"
|
||||
type="text"
|
||||
className="flex-center"
|
||||
variant="filled"
|
||||
color="default"
|
||||
onClick={() => setCollapsed(!collapsed)}
|
||||
>
|
||||
<span>
|
||||
{isThinking
|
||||
? intl.formatMessage({ id: 'playground.chat.thinking' })
|
||||
: intl.formatMessage({ id: 'playground.chat.aithought' })}
|
||||
</span>
|
||||
<IconFont type="icon-down" rotate={collapsed ? 0 : 180} />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const ContentItem: React.FC<MessageItemProps> = ({
|
||||
updateMessage,
|
||||
@@ -27,35 +103,141 @@ const ContentItem: React.FC<MessageItemProps> = ({
|
||||
data,
|
||||
editable,
|
||||
showTitle = true,
|
||||
actions = ['upload', 'delete', 'copy']
|
||||
actions = ['upload', 'delete', 'copy', 'edit']
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const [showMarkdown, setShowMarkdown] = React.useState(true);
|
||||
const messageRef = React.useRef<any>(null);
|
||||
const thinkerRef = React.useRef<any>(null);
|
||||
const [collapsed, setCollapsed] = React.useState(false);
|
||||
|
||||
const handleOnEdit = () => {
|
||||
setShowMarkdown(false);
|
||||
messageRef.current?.setEditContent(data.content);
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
messageRef.current?.handleSaveEdit();
|
||||
setShowMarkdown(true);
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
messageRef.current?.handleCancelEdit();
|
||||
setShowMarkdown(true);
|
||||
};
|
||||
|
||||
const handleUpdateMessage = (message: MessageItem) => {
|
||||
updateMessage?.(message);
|
||||
if (data.role === Roles.Assistant) {
|
||||
thinkerRef.current?.reset();
|
||||
}
|
||||
};
|
||||
|
||||
const reasoningContent = useMemo(() => {
|
||||
if (data.role === Roles.User) {
|
||||
return {
|
||||
thought: '',
|
||||
isThinking: false,
|
||||
result: ''
|
||||
};
|
||||
}
|
||||
if (!thinkerRef.current && showMarkdown) {
|
||||
thinkerRef.current = new ThinkParser();
|
||||
}
|
||||
if (showMarkdown) {
|
||||
const res = thinkerRef.current.parse(data.content);
|
||||
return res;
|
||||
}
|
||||
return {
|
||||
thought: '',
|
||||
isThinking: false,
|
||||
result: data.content
|
||||
};
|
||||
}, [data.content, showMarkdown, data.role]);
|
||||
|
||||
const renderTitle = useMemo(() => {
|
||||
if (!showTitle) {
|
||||
return null;
|
||||
}
|
||||
let roleTitle: React.ReactNode = intl.formatMessage({
|
||||
id: `playground.${data.role}`
|
||||
});
|
||||
let avatar = (
|
||||
<IconFont
|
||||
type="icon-user-filled"
|
||||
className="font-size-16 m-r-5 text-tertiary"
|
||||
/>
|
||||
);
|
||||
|
||||
if (data.role === Roles.Assistant) {
|
||||
avatar = (
|
||||
<IconFont
|
||||
type="icon-assistant-filled"
|
||||
className="font-size-16 m-r-5"
|
||||
style={{ color: 'var(--ant-blue-4)' }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (data.title) {
|
||||
roleTitle = data.title;
|
||||
}
|
||||
return (
|
||||
<div className="role flex-center">
|
||||
{avatar}
|
||||
{roleTitle}
|
||||
{data.role === Roles.Assistant && reasoningContent.thought && (
|
||||
<ThoughtStatus
|
||||
isThinking={reasoningContent.isThinking}
|
||||
collapsed={collapsed}
|
||||
setCollapsed={setCollapsed}
|
||||
></ThoughtStatus>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}, [
|
||||
data.role,
|
||||
data.title,
|
||||
intl,
|
||||
showTitle,
|
||||
reasoningContent.thought,
|
||||
reasoningContent.isThinking,
|
||||
collapsed
|
||||
]);
|
||||
|
||||
return (
|
||||
<div className="content-item">
|
||||
<div className="content-item-title">{showTitle && renderTitle}</div>
|
||||
<div
|
||||
className="content-item-role"
|
||||
style={{ display: !showTitle && !actions.length ? 'none' : 'flex' }}
|
||||
style={{ display: !actions.length ? 'none' : 'flex' }}
|
||||
>
|
||||
{showTitle && (
|
||||
<div className="role">
|
||||
{data.title ??
|
||||
intl.formatMessage({ id: `playground.${data.role}` })}
|
||||
</div>
|
||||
{showMarkdown ? (
|
||||
<MessageActions
|
||||
data={data}
|
||||
actions={actions}
|
||||
loading={loading}
|
||||
onDelete={onDelete}
|
||||
onEdit={handleOnEdit}
|
||||
updateMessage={updateMessage}
|
||||
></MessageActions>
|
||||
) : (
|
||||
<SaveActions
|
||||
handleSave={handleSave}
|
||||
handleCancel={handleCancel}
|
||||
></SaveActions>
|
||||
)}
|
||||
<MessageActions
|
||||
data={data}
|
||||
actions={actions}
|
||||
onDelete={onDelete}
|
||||
updateMessage={updateMessage}
|
||||
></MessageActions>
|
||||
</div>
|
||||
<MessageBody
|
||||
ref={messageRef}
|
||||
editable={editable}
|
||||
showMarkdown={showMarkdown}
|
||||
data={data}
|
||||
loading={loading}
|
||||
actions={actions}
|
||||
updateMessage={updateMessage}
|
||||
collapsed={collapsed}
|
||||
reasoningContent={reasoningContent}
|
||||
updateMessage={handleUpdateMessage}
|
||||
></MessageBody>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -43,7 +43,8 @@ const MultiCompare: React.FC<MultiCompareProps> = ({ modelList, loaded }) => {
|
||||
const [actions, setActions] = useState<MessageItemAction[]>([
|
||||
'upload',
|
||||
'delete',
|
||||
'copy'
|
||||
'copy',
|
||||
'edit'
|
||||
]);
|
||||
|
||||
const isLoading = useMemo(() => {
|
||||
@@ -222,15 +223,6 @@ const MultiCompare: React.FC<MultiCompareProps> = ({ modelList, loaded }) => {
|
||||
handleUpdateModelList(value);
|
||||
};
|
||||
|
||||
const handleOnCheck = (e: any) => {
|
||||
const checked = e.target.checked;
|
||||
if (checked) {
|
||||
setActions(['upload', 'delete', 'copy', 'markdown']);
|
||||
} else {
|
||||
setActions(['upload', 'delete', 'copy']);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
modelRefs.current = {};
|
||||
let list = _.take(modelList, spans.count);
|
||||
@@ -305,18 +297,8 @@ const MultiCompare: React.FC<MultiCompareProps> = ({ modelList, loaded }) => {
|
||||
clearAll={handleClearAll}
|
||||
updateLayout={updateLayout}
|
||||
setModelSelections={handleUpdateModelSelections}
|
||||
actions={[
|
||||
'clear',
|
||||
'layout',
|
||||
'role',
|
||||
'upload',
|
||||
'add',
|
||||
'paste',
|
||||
'check'
|
||||
]}
|
||||
actions={['clear', 'layout', 'role', 'upload', 'add', 'paste']}
|
||||
defaultChecked={false}
|
||||
checkLabel="Markdown"
|
||||
onCheck={handleOnCheck}
|
||||
defaultSize={{
|
||||
minRows: 5,
|
||||
maxRows: 5
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
import CopyButton from '@/components/copy-button';
|
||||
import {
|
||||
FileMarkdownOutlined,
|
||||
FileTextOutlined,
|
||||
MinusCircleOutlined
|
||||
} from '@ant-design/icons';
|
||||
import { EditOutlined, MinusCircleOutlined } from '@ant-design/icons';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { Button, Tooltip } from 'antd';
|
||||
import React, { useCallback } from 'react';
|
||||
@@ -14,24 +10,19 @@ import UploadImg from '../upload-img';
|
||||
interface MessageActionsProps {
|
||||
data: MessageItem;
|
||||
actions: MessageItemAction[];
|
||||
renderMode?: string;
|
||||
renderModeChange?: (value: string) => void;
|
||||
loading?: boolean;
|
||||
updateMessage?: (message: MessageItem) => void;
|
||||
onDelete?: () => void;
|
||||
onEdit?: () => void;
|
||||
}
|
||||
|
||||
const renderOptions = [
|
||||
{ label: 'Markdown', value: 'markdown', icon: <FileMarkdownOutlined /> },
|
||||
{ label: 'Plain', value: 'plain', icon: <FileTextOutlined /> }
|
||||
];
|
||||
|
||||
const MessageActions: React.FC<MessageActionsProps> = ({
|
||||
actions,
|
||||
data,
|
||||
renderMode,
|
||||
renderModeChange,
|
||||
loading,
|
||||
updateMessage,
|
||||
onDelete
|
||||
onDelete,
|
||||
onEdit
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
|
||||
@@ -49,30 +40,44 @@ const MessageActions: React.FC<MessageActionsProps> = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
{actions.length > 1 ? (
|
||||
{actions.length > 1 && !loading ? (
|
||||
<div className="actions">
|
||||
{actions.includes('upload') && data.role === Roles.User && (
|
||||
<UploadImg handleUpdateImgList={handleUpdateImgList} />
|
||||
)}
|
||||
{data.content && actions.includes('copy') && (
|
||||
<CopyButton
|
||||
text={data.content}
|
||||
size="small"
|
||||
shape="default"
|
||||
type="text"
|
||||
fontSize="12px"
|
||||
/>
|
||||
)}
|
||||
{actions.includes('delete') && (
|
||||
<Tooltip title={intl.formatMessage({ id: 'common.button.delete' })}>
|
||||
<Button
|
||||
<div className="actions-wrap gap-5">
|
||||
{actions.includes('upload') && data.role === Roles.User && (
|
||||
<UploadImg handleUpdateImgList={handleUpdateImgList} />
|
||||
)}
|
||||
{actions.includes('edit') && data.role === Roles.Assistant && (
|
||||
<Tooltip title={intl.formatMessage({ id: 'common.button.edit' })}>
|
||||
<Button
|
||||
size="small"
|
||||
type="text"
|
||||
icon={<EditOutlined />}
|
||||
onClick={onEdit}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
{data.content && actions.includes('copy') && (
|
||||
<CopyButton
|
||||
text={data.content}
|
||||
size="small"
|
||||
shape="default"
|
||||
type="text"
|
||||
onClick={onDelete}
|
||||
icon={<MinusCircleOutlined />}
|
||||
fontSize="12px"
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
)}
|
||||
{actions.includes('delete') && (
|
||||
<Tooltip
|
||||
title={intl.formatMessage({ id: 'common.button.delete' })}
|
||||
>
|
||||
<Button
|
||||
size="small"
|
||||
type="text"
|
||||
onClick={onDelete}
|
||||
icon={<MinusCircleOutlined />}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
|
||||
@@ -2,217 +2,214 @@ import FullMarkdown from '@/components/markdown-viewer/full-markdown';
|
||||
import { Input } from 'antd';
|
||||
import classNames from 'classnames';
|
||||
import _ from 'lodash';
|
||||
import React, { useCallback, useMemo, useRef } from 'react';
|
||||
import React, {
|
||||
forwardRef,
|
||||
useCallback,
|
||||
useImperativeHandle,
|
||||
useRef
|
||||
} from 'react';
|
||||
import { Roles } from '../../config';
|
||||
import { MessageItem, MessageItemAction } from '../../config/types';
|
||||
import ThumbImg from '../thumb-img';
|
||||
import ThinkContent from './think-content';
|
||||
import ThinkParser from './think-parser';
|
||||
|
||||
interface MessageBodyProps {
|
||||
ref?: any;
|
||||
data: MessageItem;
|
||||
editable?: boolean;
|
||||
loading?: boolean;
|
||||
showTitle?: boolean;
|
||||
actions?: MessageItemAction[];
|
||||
showMarkdown?: boolean;
|
||||
reasoningContent: {
|
||||
thought: string;
|
||||
isThinking: boolean;
|
||||
result: string;
|
||||
};
|
||||
collapsed?: boolean;
|
||||
updateMessage?: (message: MessageItem) => void;
|
||||
onDelete?: () => void;
|
||||
}
|
||||
|
||||
const MessageBody: React.FC<MessageBodyProps> = ({
|
||||
editable,
|
||||
data,
|
||||
loading,
|
||||
actions,
|
||||
updateMessage
|
||||
}) => {
|
||||
const inputRef = useRef<any>(null);
|
||||
const imgCountRef = useRef(0);
|
||||
const thinkerRef = useRef<any>(null);
|
||||
|
||||
const content = useMemo(() => {
|
||||
if (!thinkerRef.current && actions?.includes('markdown')) {
|
||||
thinkerRef.current = new ThinkParser();
|
||||
}
|
||||
if (actions?.includes('markdown')) {
|
||||
const res = thinkerRef.current.parse(data.content);
|
||||
console.log('markdown parse:', res);
|
||||
return res;
|
||||
}
|
||||
return {
|
||||
thought: '',
|
||||
result: data.content
|
||||
};
|
||||
}, [data.content, actions]);
|
||||
|
||||
const getPasteContent = useCallback(
|
||||
async (event: any) => {
|
||||
// @ts-ignore
|
||||
const clipboardData = event.clipboardData || window.clipboardData;
|
||||
const items = clipboardData.items;
|
||||
const imgPromises: Promise<string>[] = [];
|
||||
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
let item = items[i];
|
||||
|
||||
if (item.kind === 'file' && item.type.indexOf('image') !== -1) {
|
||||
const file = item.getAsFile();
|
||||
const imgPromise = new Promise<string>((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = function (event) {
|
||||
const base64String = event.target?.result as string;
|
||||
if (base64String) {
|
||||
resolve(base64String);
|
||||
} else {
|
||||
reject('Failed to convert image to base64');
|
||||
}
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
imgPromises.push(imgPromise);
|
||||
} else if (item.kind === 'string') {
|
||||
// string
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const imgs = await Promise.all(imgPromises);
|
||||
if (imgs.length) {
|
||||
const list = _.map(imgs, (img: string) => {
|
||||
imgCountRef.current += 1;
|
||||
return {
|
||||
uid: imgCountRef.current,
|
||||
dataUrl: img
|
||||
};
|
||||
});
|
||||
|
||||
updateMessage?.({
|
||||
role: data.role,
|
||||
content: data.content,
|
||||
uid: data.uid,
|
||||
imgs: [...(data.imgs || []), ...list]
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error processing images:', error);
|
||||
}
|
||||
const MessageBody: React.FC<MessageBodyProps> = forwardRef(
|
||||
(
|
||||
{
|
||||
editable,
|
||||
data,
|
||||
loading,
|
||||
reasoningContent,
|
||||
showMarkdown,
|
||||
collapsed,
|
||||
updateMessage
|
||||
},
|
||||
[data, updateMessage]
|
||||
);
|
||||
ref
|
||||
) => {
|
||||
const inputRef = useRef<any>(null);
|
||||
const imgCountRef = useRef(0);
|
||||
const [editContent, setEditContent] = React.useState(data.content);
|
||||
|
||||
const handleOnPaste = (e: any) => {
|
||||
const text = e.clipboardData.getData('text');
|
||||
if (!text) {
|
||||
e.preventDefault();
|
||||
getPasteContent(e);
|
||||
}
|
||||
};
|
||||
const getPasteContent = useCallback(
|
||||
async (event: any) => {
|
||||
// @ts-ignore
|
||||
const clipboardData = event.clipboardData || window.clipboardData;
|
||||
const items = clipboardData.items;
|
||||
const imgPromises: Promise<string>[] = [];
|
||||
|
||||
const handleDeleteImg = (uid: number | string) => {
|
||||
const list = _.filter(data.imgs, (item: MessageItem) => item.uid !== uid);
|
||||
updateMessage?.({
|
||||
role: data.role,
|
||||
content: data.content,
|
||||
uid: data.uid,
|
||||
imgs: list
|
||||
});
|
||||
};
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
let item = items[i];
|
||||
|
||||
const handleMessageChange = (e: any) => {
|
||||
updateMessage?.({
|
||||
imgs: data.imgs || [],
|
||||
role: data.role,
|
||||
content: e.target.value,
|
||||
uid: data.uid
|
||||
});
|
||||
if (data.role === Roles.Assistant) {
|
||||
thinkerRef.current?.reset();
|
||||
}
|
||||
};
|
||||
if (item.kind === 'file' && item.type.indexOf('image') !== -1) {
|
||||
const file = item.getAsFile();
|
||||
const imgPromise = new Promise<string>((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = function (event) {
|
||||
const base64String = event.target?.result as string;
|
||||
if (base64String) {
|
||||
resolve(base64String);
|
||||
} else {
|
||||
reject('Failed to convert image to base64');
|
||||
}
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
imgPromises.push(imgPromise);
|
||||
} else if (item.kind === 'string') {
|
||||
// string
|
||||
}
|
||||
}
|
||||
|
||||
const handleDeleteLastImage = useCallback(() => {
|
||||
if (data.imgs && data.imgs?.length > 0) {
|
||||
const newImgList = [...(data.imgs || [])];
|
||||
const lastImage = newImgList.pop();
|
||||
if (lastImage) {
|
||||
handleDeleteImg(lastImage.uid);
|
||||
}
|
||||
}
|
||||
}, [data.imgs, handleDeleteImg]);
|
||||
try {
|
||||
const imgs = await Promise.all(imgPromises);
|
||||
if (imgs.length) {
|
||||
const list = _.map(imgs, (img: string) => {
|
||||
imgCountRef.current += 1;
|
||||
return {
|
||||
uid: imgCountRef.current,
|
||||
dataUrl: img
|
||||
};
|
||||
});
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(event: any) => {
|
||||
if (
|
||||
event.key === 'Backspace' &&
|
||||
data.content === '' &&
|
||||
data.imgs &&
|
||||
data.imgs?.length > 0
|
||||
) {
|
||||
// inputref blur
|
||||
event.preventDefault();
|
||||
handleDeleteLastImage();
|
||||
}
|
||||
},
|
||||
[data, handleDeleteLastImage]
|
||||
);
|
||||
const handleClickWrapper = (e: any) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
inputRef.current?.focus();
|
||||
};
|
||||
|
||||
if (!editable) {
|
||||
return (
|
||||
<div
|
||||
className={classNames('content-item-content', {
|
||||
'has-img': data.imgs?.length
|
||||
})}
|
||||
>
|
||||
<ThumbImg
|
||||
editable={editable}
|
||||
dataList={data.imgs || []}
|
||||
onDelete={handleDeleteImg}
|
||||
/>
|
||||
{data.content && <div className="text">{data.content}</div>}
|
||||
</div>
|
||||
updateMessage?.({
|
||||
role: data.role,
|
||||
content: data.content,
|
||||
uid: data.uid,
|
||||
imgs: [...(data.imgs || []), ...list]
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error processing images:', error);
|
||||
}
|
||||
},
|
||||
[data, updateMessage]
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames('message-content-input', {
|
||||
'has-img': data.imgs?.length
|
||||
})}
|
||||
onClick={handleClickWrapper}
|
||||
>
|
||||
<ThumbImg
|
||||
editable={editable}
|
||||
dataList={data.imgs || []}
|
||||
onDelete={handleDeleteImg}
|
||||
/>
|
||||
<>
|
||||
{data.role === Roles.User ? (
|
||||
<Input.TextArea
|
||||
ref={inputRef}
|
||||
value={data.content}
|
||||
variant="filled"
|
||||
autoSize={{ minRows: 1 }}
|
||||
style={{ borderRadius: 'var(--border-radius-mini)' }}
|
||||
readOnly={loading}
|
||||
onKeyDown={handleKeyDown}
|
||||
onChange={handleMessageChange}
|
||||
onPaste={handleOnPaste}
|
||||
const handleOnPaste = (e: any) => {
|
||||
const text = e.clipboardData.getData('text');
|
||||
if (!text) {
|
||||
e.preventDefault();
|
||||
getPasteContent(e);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteImg = (uid: number | string) => {
|
||||
const list = _.filter(data.imgs, (item: MessageItem) => item.uid !== uid);
|
||||
updateMessage?.({
|
||||
role: data.role,
|
||||
content: data.content,
|
||||
uid: data.uid,
|
||||
imgs: list
|
||||
});
|
||||
};
|
||||
|
||||
const handleMessageChange = (e: any) => {
|
||||
updateMessage?.({
|
||||
imgs: data.imgs || [],
|
||||
role: data.role,
|
||||
content: e.target.value,
|
||||
uid: data.uid
|
||||
});
|
||||
};
|
||||
|
||||
const handleEdit = (e: any) => {
|
||||
setEditContent(e.target.value);
|
||||
};
|
||||
|
||||
const handleCancelEdit = () => {
|
||||
setEditContent(data.content);
|
||||
};
|
||||
|
||||
const handleSaveEdit = () => {
|
||||
updateMessage?.({
|
||||
role: data.role,
|
||||
content: editContent,
|
||||
uid: data.uid,
|
||||
imgs: data.imgs
|
||||
});
|
||||
};
|
||||
|
||||
const handleDeleteLastImage = useCallback(() => {
|
||||
if (data.imgs && data.imgs?.length > 0) {
|
||||
const newImgList = [...(data.imgs || [])];
|
||||
const lastImage = newImgList.pop();
|
||||
if (lastImage) {
|
||||
handleDeleteImg(lastImage.uid);
|
||||
}
|
||||
}
|
||||
}, [data.imgs, handleDeleteImg]);
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(event: any) => {
|
||||
if (
|
||||
event.key === 'Backspace' &&
|
||||
data.content === '' &&
|
||||
data.imgs &&
|
||||
data.imgs?.length > 0
|
||||
) {
|
||||
// inputref blur
|
||||
event.preventDefault();
|
||||
handleDeleteLastImage();
|
||||
}
|
||||
},
|
||||
[data, handleDeleteLastImage]
|
||||
);
|
||||
const handleClickWrapper = (e: any) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
inputRef.current?.focus();
|
||||
};
|
||||
|
||||
const renderReadOnlyMode = () => {
|
||||
return (
|
||||
<div
|
||||
className={classNames('content-item-content', {
|
||||
'has-img': data.imgs?.length
|
||||
})}
|
||||
>
|
||||
<ThumbImg
|
||||
editable={editable}
|
||||
dataList={data.imgs || []}
|
||||
onDelete={handleDeleteImg}
|
||||
/>
|
||||
{data.content && <div className="text">{data.content}</div>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderEditMode = () => {
|
||||
return (
|
||||
<div
|
||||
className={classNames('message-content-input', {
|
||||
'has-img': data.imgs?.length
|
||||
})}
|
||||
onClick={handleClickWrapper}
|
||||
>
|
||||
<ThumbImg
|
||||
editable={editable}
|
||||
dataList={data.imgs || []}
|
||||
onDelete={handleDeleteImg}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
{actions?.includes('markdown') ? (
|
||||
<>
|
||||
<ThinkContent content={content.thought}></ThinkContent>
|
||||
<FullMarkdown
|
||||
content={`${content.result || ''}`}
|
||||
></FullMarkdown>
|
||||
</>
|
||||
) : (
|
||||
{data.role === Roles.User ? (
|
||||
<Input.TextArea
|
||||
ref={inputRef}
|
||||
value={data.content}
|
||||
@@ -224,12 +221,49 @@ const MessageBody: React.FC<MessageBodyProps> = ({
|
||||
onChange={handleMessageChange}
|
||||
onPaste={handleOnPaste}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
{showMarkdown ? (
|
||||
<>
|
||||
<ThinkContent
|
||||
collapsed={collapsed}
|
||||
content={reasoningContent.thought}
|
||||
isThinking={reasoningContent.isThinking}
|
||||
></ThinkContent>
|
||||
<div style={{ paddingInline: 4 }}>
|
||||
<FullMarkdown
|
||||
content={`${reasoningContent.result || ''}`}
|
||||
></FullMarkdown>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<Input.TextArea
|
||||
ref={inputRef}
|
||||
value={editContent}
|
||||
defaultValue={data.content}
|
||||
variant="filled"
|
||||
autoSize={{ minRows: 1 }}
|
||||
style={{ borderRadius: 'var(--border-radius-mini)' }}
|
||||
onKeyDown={handleKeyDown}
|
||||
onChange={handleEdit}
|
||||
onPaste={handleOnPaste}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
handleSaveEdit,
|
||||
handleCancelEdit,
|
||||
setEditContent
|
||||
}));
|
||||
|
||||
return <>{editable ? renderEditMode() : renderReadOnlyMode()}</>;
|
||||
}
|
||||
);
|
||||
|
||||
export default MessageBody;
|
||||
|
||||
@@ -17,6 +17,7 @@ const MessageContent: React.FC<MessageContentProps> = ({
|
||||
messageList,
|
||||
editable,
|
||||
showTitle = true,
|
||||
loading,
|
||||
actions = ['upload', 'delete', 'copy']
|
||||
}) => {
|
||||
const updateMessage = (index: number, message: MessageItem) => {
|
||||
@@ -37,6 +38,7 @@ const MessageContent: React.FC<MessageContentProps> = ({
|
||||
<div className="message-content-list">
|
||||
{messageList.map((item, index) => (
|
||||
<ContentItem
|
||||
loading={loading}
|
||||
key={item.uid}
|
||||
data={item}
|
||||
editable={editable}
|
||||
|
||||
@@ -1,38 +1,23 @@
|
||||
import IconFont from '@/components/icon-font';
|
||||
import { DownOutlined } from '@ant-design/icons';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { Button } from 'antd';
|
||||
import React, { useState } from 'react';
|
||||
import FullMarkdown from '@/components/markdown-viewer/full-markdown';
|
||||
import React from 'react';
|
||||
import '../../style/think-content.less';
|
||||
|
||||
interface ThinkContentProps {
|
||||
content: string;
|
||||
isThinking?: boolean;
|
||||
collapsed?: boolean;
|
||||
}
|
||||
|
||||
const ThinkContent: React.FC<ThinkContentProps> = ({ content }) => {
|
||||
const intl = useIntl();
|
||||
const [collapsed, setCollapsed] = useState(false);
|
||||
const ThinkContent: React.FC<ThinkContentProps> = ({ content, collapsed }) => {
|
||||
return (
|
||||
<>
|
||||
{content ? (
|
||||
<div className="think-wrapper">
|
||||
<div className="btn-collapse">
|
||||
<Button
|
||||
size="small"
|
||||
type="text"
|
||||
className="flex-center"
|
||||
variant={collapsed ? 'filled' : undefined}
|
||||
color="default"
|
||||
onClick={() => setCollapsed(!collapsed)}
|
||||
>
|
||||
<IconFont type="icon-AIzhineng" />
|
||||
<span>
|
||||
{intl.formatMessage({ id: 'playground.chat.aithought' })}
|
||||
</span>
|
||||
<DownOutlined rotate={collapsed ? 0 : 180} className="m-l-10" />
|
||||
</Button>
|
||||
</div>
|
||||
{!collapsed && <div className="think-content">{content}</div>}
|
||||
{!collapsed && (
|
||||
<div className="think-content">
|
||||
<FullMarkdown content={content}></FullMarkdown>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
|
||||
@@ -49,7 +49,11 @@ class ThinkParser {
|
||||
}
|
||||
}
|
||||
|
||||
return { thought: this.thought.trim(), result: this.result };
|
||||
return {
|
||||
thought: this.thought.trim(),
|
||||
result: this.result,
|
||||
isThinking: this.collecting
|
||||
};
|
||||
}
|
||||
|
||||
reset() {
|
||||
|
||||
@@ -6,7 +6,12 @@ export interface ModelSelectionItem extends Global.BaseOption<string> {
|
||||
type?: string;
|
||||
}
|
||||
|
||||
export type MessageItemAction = 'upload' | 'delete' | 'copy' | 'markdown';
|
||||
export type MessageItemAction =
|
||||
| 'upload'
|
||||
| 'delete'
|
||||
| 'copy'
|
||||
| 'markdown'
|
||||
| 'edit';
|
||||
|
||||
export interface MessageItem {
|
||||
content: string;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
.content-item {
|
||||
margin-bottom: 12px;
|
||||
margin-bottom: 24px;
|
||||
position: relative;
|
||||
|
||||
.markdown-viewer {
|
||||
h4 {
|
||||
@@ -12,10 +13,19 @@
|
||||
&-role {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
justify-content: flex-end;
|
||||
font-weight: var(--font-weight-bold);
|
||||
margin-bottom: 8px;
|
||||
height: 24px;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.content-item-title {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.role {
|
||||
@@ -26,6 +36,15 @@
|
||||
|
||||
.actions {
|
||||
display: none;
|
||||
border: 1px solid var(--ant-color-border);
|
||||
border-radius: var(--border-radius-base);
|
||||
background-color: var(--color-white-1);
|
||||
}
|
||||
|
||||
.actions-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.actions .mrkd-switch {
|
||||
@@ -43,9 +62,6 @@
|
||||
&:hover {
|
||||
.actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
.think-wrapper {
|
||||
margin-bottom: 16px;
|
||||
color: var(--ant-color-text-tertiary);
|
||||
|
||||
.btn-collapse {
|
||||
@@ -10,10 +9,11 @@
|
||||
}
|
||||
|
||||
.think-content {
|
||||
white-space: pre-wrap;
|
||||
margin-bottom: 8px;
|
||||
padding: 12px;
|
||||
border-left: 2px solid var(--ant-color-fill-secondary);
|
||||
background-color: var(--ant-color-fill-tertiary);
|
||||
color: var(--ant-color-text-tertiary);
|
||||
border-radius: var(--border-radius-base);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user