style: chat message render markdown by default

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