diff --git a/src/components/seal-form/seal-textarea.tsx b/src/components/seal-form/seal-textarea.tsx index b55b105c..e12b4d0f 100644 --- a/src/components/seal-form/seal-textarea.tsx +++ b/src/components/seal-form/seal-textarea.tsx @@ -19,6 +19,7 @@ const SealTextArea: React.FC = (props) => { variant, extra, addAfter, + trim, ...rest } = props; const [isFocus, setIsFocus] = useState(false); diff --git a/src/pages/playground/components/ground-left.tsx b/src/pages/playground/components/ground-left.tsx index 9c01035e..5e0f6bfa 100644 --- a/src/pages/playground/components/ground-left.tsx +++ b/src/pages/playground/components/ground-left.tsx @@ -16,7 +16,7 @@ import { } from 'react'; import { CHAT_API } from '../apis'; import { OpenAIViewCode, Roles, generateMessages } from '../config'; -import { MessageItem } from '../config/types'; +import { MessageItem, MessageItemAction } from '../config/types'; import '../style/ground-left.less'; import '../style/system-message-wrap.less'; import MessageInput from './message-input'; @@ -53,6 +53,11 @@ const GroundLeft: React.FC = forwardRef((props, ref) => { const paramsRef = useRef(null); const messageListLengthCache = useRef(0); const reasonContentRef = useRef(''); + const [actions, setActions] = useState([ + 'upload', + 'delete', + 'copy' + ]); const { initialize, updateScrollerPosition } = useOverlayScroller(); const { initialize: innitializeParams } = useOverlayScroller(); @@ -249,6 +254,15 @@ const GroundLeft: React.FC = forwardRef((props, ref) => { setMessageList(userMsg); }; + const handleOnCheck = (e: any) => { + const checked = e.target.checked; + if (checked) { + setActions(['upload', 'delete', 'copy', 'markdown']); + } else { + setActions(['upload', 'delete', 'copy']); + } + }; + const throttleUpdatePosition = _.throttle(updateScrollerPosition, 100); useEffect(() => { @@ -303,6 +317,7 @@ const GroundLeft: React.FC = forwardRef((props, ref) => { setMessageList={setMessageList} editable={true} loading={loading} + actions={actions} /> {loading && ( @@ -323,6 +338,18 @@ const GroundLeft: React.FC = forwardRef((props, ref) => { minRows: 5, maxRows: 5 }} + actions={[ + 'clear', + 'layout', + 'role', + 'upload', + 'add', + 'paste', + 'check' + ]} + defaultChecked={false} + checkLabel="Markdown" + onCheck={handleOnCheck} loading={loading} disabled={!parameters.model} isEmpty={!messageList.length} diff --git a/src/pages/playground/components/message-input.tsx b/src/pages/playground/components/message-input.tsx index f06fb66d..7f151e58 100644 --- a/src/pages/playground/components/message-input.tsx +++ b/src/pages/playground/components/message-input.tsx @@ -96,6 +96,7 @@ interface MessageInputProps { style?: React.CSSProperties; actions?: ActionType[]; checkLabel?: React.ReactNode; + defaultChecked?: boolean; defaultSize?: { minRows: number; maxRows: number }; } @@ -116,6 +117,7 @@ const MessageInput: React.FC = forwardRef( isEmpty, submitIcon, placeholer, + defaultChecked = true, tools, style, checkLabel, @@ -356,35 +358,31 @@ const MessageInput: React.FC = forwardRef(
{title} - { + {actions.includes('role') && ( <> - {actions.includes('role') && ( - <> - - - - )} - {actions.includes('upload') && message.role === Roles.User && ( - - )} + + - } - {tools} + )} {actions.includes('check') && ( - + {checkLabel} )} + {actions.includes('upload') && message.role === Roles.User && ( + + )} + {tools} {actions.includes('clear') && ( = ({ actions = ['upload', 'delete', 'copy'] }) => { const intl = useIntl(); - const inputRef = useRef(null); - const imgCountRef = useRef(0); - - const handleMessageChange = (e: any) => { - updateMessage?.({ - imgs: data.imgs || [], - role: data.role, - content: e.target.value, - uid: data.uid - }); - }; - - const handleToggleRole = () => { - updateMessage?.({ - imgs: data.imgs || [], - role: data.role === Roles.User ? Roles.Assistant : Roles.User, - content: data.content, - uid: data.uid - }); - }; - - const getPasteContent = useCallback( - async (event: any) => { - const clipboardData = event.clipboardData || window.clipboardData; - const items = clipboardData.items; - const imgPromises: Promise[] = []; - - 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((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] - ); - - const handleOnPaste = (e: any) => { - const text = e.clipboardData.getData('text'); - if (!text) { - e.preventDefault(); - getPasteContent(e); - } - }; - - const handleUpdateImgList = useCallback( - (list: { uid: number | string; dataUrl: string }[]) => { - updateMessage?.({ - role: data.role, - content: data.content, - uid: data.uid, - imgs: [...(data.imgs || []), ...list] - }); - }, - [data, updateMessage] - ); - - 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 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) => { - console.log('e===========', e); - e.stopPropagation(); - e.preventDefault(); - inputRef.current.focus(); - }; return (
@@ -181,71 +38,20 @@ const ContentItem: React.FC = ({ intl.formatMessage({ id: `playground.${data.role}` })}
)} -
- {actions.includes('upload') && data.role === Roles.User && ( - - )} - {data.content && actions.includes('copy') && ( - - )} - {actions.includes('delete') && ( - - - - )} -
+
- {editable ? ( -
- - -
- ) : ( -
- - {data.content &&
{data.content}
} -
- )} +
); }; diff --git a/src/pages/playground/components/multiple-chat/message-actions.tsx b/src/pages/playground/components/multiple-chat/message-actions.tsx new file mode 100644 index 00000000..e4a2c296 --- /dev/null +++ b/src/pages/playground/components/multiple-chat/message-actions.tsx @@ -0,0 +1,82 @@ +import CopyButton from '@/components/copy-button'; +import { + FileMarkdownOutlined, + FileTextOutlined, + MinusCircleOutlined +} from '@ant-design/icons'; +import { useIntl } from '@umijs/max'; +import { Button, Tooltip } from 'antd'; +import React, { useCallback } from 'react'; +import { Roles } from '../../config'; +import { MessageItem, MessageItemAction } from '../../config/types'; +import UploadImg from '../upload-img'; + +interface MessageActionsProps { + data: MessageItem; + actions: MessageItemAction[]; + renderMode?: string; + renderModeChange?: (value: string) => void; + updateMessage?: (message: MessageItem) => void; + onDelete?: () => void; +} + +const renderOptions = [ + { label: 'Markdown', value: 'markdown', icon: }, + { label: 'Plain', value: 'plain', icon: } +]; + +const MessageActions: React.FC = ({ + actions, + data, + renderMode, + renderModeChange, + updateMessage, + onDelete +}) => { + const intl = useIntl(); + + const handleUpdateImgList = useCallback( + (list: { uid: number | string; dataUrl: string }[]) => { + updateMessage?.({ + role: data.role, + content: data.content, + uid: data.uid, + imgs: [...(data.imgs || []), ...list] + }); + }, + [data, updateMessage] + ); + + return ( + <> + {actions.length > 1 ? ( +
+ {actions.includes('upload') && data.role === Roles.User && ( + + )} + {data.content && actions.includes('copy') && ( + + )} + {actions.includes('delete') && ( + +
+ ) : null} + + ); +}; + +export default MessageActions; diff --git a/src/pages/playground/components/multiple-chat/message-body.tsx b/src/pages/playground/components/multiple-chat/message-body.tsx new file mode 100644 index 00000000..98269e61 --- /dev/null +++ b/src/pages/playground/components/multiple-chat/message-body.tsx @@ -0,0 +1,233 @@ +import MarkdownViewer from '@/components/markdown-viewer'; +import { Input } from 'antd'; +import classNames from 'classnames'; +import _ from 'lodash'; +import React, { useCallback, useMemo, 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 { + data: MessageItem; + editable?: boolean; + loading?: boolean; + showTitle?: boolean; + actions?: MessageItemAction[]; + updateMessage?: (message: MessageItem) => void; + onDelete?: () => void; +} + +const MessageBody: React.FC = ({ + editable, + data, + loading, + actions, + updateMessage +}) => { + const inputRef = useRef(null); + const imgCountRef = useRef(0); + const thinkerRef = useRef(null); + + const content = useMemo(() => { + if (!thinkerRef.current && actions?.includes('markdown')) { + thinkerRef.current = new ThinkParser(); + } + if (actions?.includes('markdown')) { + return thinkerRef.current.parse(data.content); + } + 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[] = []; + + 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((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] + ); + + 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 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(); + }; + + if (!editable) { + return ( +
+ + {data.content &&
{data.content}
} +
+ ); + } + + return ( +
+ + <> + {data.role === Roles.User ? ( + + ) : ( + <> + {actions?.includes('markdown') ? ( + <> + +
+ +
+ + ) : ( + + )} + + )} + +
+ ); +}; + +export default MessageBody; diff --git a/src/pages/playground/components/multiple-chat/think-content.tsx b/src/pages/playground/components/multiple-chat/think-content.tsx new file mode 100644 index 00000000..2e70d17e --- /dev/null +++ b/src/pages/playground/components/multiple-chat/think-content.tsx @@ -0,0 +1,26 @@ +import IconFont from '@/components/icon-font'; +import { Button } from 'antd'; +import React from 'react'; +import '../../style/think-content.less'; + +interface ThinkContentProps { + content: string; +} + +const ThinkContent: React.FC = ({ content }) => { + return ( + <> + {content ? ( +
+ + +
{content}
+
+ ) : null} + + ); +}; + +export default ThinkContent; diff --git a/src/pages/playground/components/multiple-chat/think-parser.ts b/src/pages/playground/components/multiple-chat/think-parser.ts new file mode 100644 index 00000000..15a231d3 --- /dev/null +++ b/src/pages/playground/components/multiple-chat/think-parser.ts @@ -0,0 +1,59 @@ +class ThinkParser { + thought: string; + result: string; + collecting: boolean; + lastCheckedIndex: number; + + constructor() { + this.thought = ''; + this.result = ''; + this.collecting = false; + this.lastCheckedIndex = 0; + } + + parse(chunk: string) { + if (this.lastCheckedIndex < chunk.length) { + const currentChunk = chunk.substring(this.lastCheckedIndex); + + if (!this.collecting) { + // find tag + let startIndex = currentChunk.indexOf(''); + if (startIndex !== -1) { + // handle text before tag + this.result += currentChunk.substring(0, startIndex); + // handle thought part + this.thought += currentChunk.substring(startIndex + 7); + this.collecting = true; + } else { + // if no tag found, just append the whole chunk to result + this.result += currentChunk; + } + } else { + // find tag + let endIndex = currentChunk.indexOf(''); + if (endIndex !== -1) { + // handle text before tag + this.thought += currentChunk.substring(0, endIndex); + // handle result part + this.result += currentChunk.substring(endIndex + 8); + + this.collecting = false; + } else { + this.thought += currentChunk; + } + this.lastCheckedIndex = chunk.length; + } + } + + return { thought: this.thought.trim(), result: this.result }; + } + + reset() { + this.thought = ''; + this.result = ''; + this.collecting = false; + this.lastCheckedIndex = 0; + } +} + +export default ThinkParser; diff --git a/src/pages/playground/config/types.ts b/src/pages/playground/config/types.ts index 33c3f348..be94461b 100644 --- a/src/pages/playground/config/types.ts +++ b/src/pages/playground/config/types.ts @@ -6,7 +6,7 @@ export interface ModelSelectionItem extends Global.BaseOption { type?: string; } -export type MessageItemAction = 'upload' | 'delete' | 'copy'; +export type MessageItemAction = 'upload' | 'delete' | 'copy' | 'markdown'; export interface MessageItem { content: string; diff --git a/src/pages/playground/style/content-item.less b/src/pages/playground/style/content-item.less index 9adf5d85..d80535f7 100644 --- a/src/pages/playground/style/content-item.less +++ b/src/pages/playground/style/content-item.less @@ -20,6 +20,18 @@ display: none; } + .actions .mrkd-switch { + border-radius: 12px; + + .ant-segmented-thumb, + .ant-segmented-item-selected, + .ant-segmented-item, + .ant-segmented-item::after { + border-radius: 10px; + overflow: hidden; + } + } + &:hover { .actions { display: flex; diff --git a/src/pages/playground/style/think-content.less b/src/pages/playground/style/think-content.less new file mode 100644 index 00000000..a435475f --- /dev/null +++ b/src/pages/playground/style/think-content.less @@ -0,0 +1,13 @@ +.think-wrapper { + margin-bottom: 16px; + color: var(--ant-color-text-tertiary); + border-left: 2px solid var(--ant-color-fill-secondary); + + .think-content { + white-space: pre-wrap; + padding: 6px 4px; + background-color: var(--ant-color-fill-tertiary); + color: var(--ant-color-text-tertiary); + border-radius: var(--border-radius-base); + } +} diff --git a/src/utils/fetch-chunk-data.ts b/src/utils/fetch-chunk-data.ts index d0522abb..d23d1036 100644 --- a/src/utils/fetch-chunk-data.ts +++ b/src/utils/fetch-chunk-data.ts @@ -27,6 +27,7 @@ const extractJSON = (dataStr: string) => { const errorHandler = async (res: any) => { try { const data = await res.json(); + console.log('errorHandler:', data); return { error: true, data: data