diff --git a/config/routes.ts b/config/routes.ts index 8a50de9d..5885a09a 100644 --- a/config/routes.ts +++ b/config/routes.ts @@ -25,6 +25,14 @@ export default [ icon: 'Comment', component: './playground/index' }, + { + name: 'speech', + title: 'Speech', + path: '/playground/speech', + key: 'speech', + icon: 'Comment', + component: './playground/speech' + }, { name: 'embedding', title: 'embedding', diff --git a/src/locales/en-US/menu.ts b/src/locales/en-US/menu.ts index c9ee5112..4e48291f 100644 --- a/src/locales/en-US/menu.ts +++ b/src/locales/en-US/menu.ts @@ -2,7 +2,9 @@ export default { 'menu.dashboard': 'Dashboard', 'menu.playground': 'Playground', 'menu.playground.rerank': 'Rerank', + 'menu.playground.embedding': 'Embedding', 'menu.playground.chat': 'Chat', + 'menu.playground.speech': 'Speech', 'menu.compare': 'Compare', 'menu.models': 'Models', 'menu.resources': 'Resources', diff --git a/src/locales/zh-CN/menu.ts b/src/locales/zh-CN/menu.ts index ef313f7a..a9141b61 100644 --- a/src/locales/zh-CN/menu.ts +++ b/src/locales/zh-CN/menu.ts @@ -4,6 +4,7 @@ export default { 'menu.playground.rerank': '重排', 'menu.playground.embedding': '文本嵌入', 'menu.playground.chat': '对话', + 'menu.playground.speech': '语音', 'menu.compare': '多模型对比', 'menu.models': '模型', 'menu.resources': '资源', diff --git a/src/pages/llmodels/config/llama-config.ts b/src/pages/llmodels/config/llama-config.ts index 2af59f07..f4497545 100644 --- a/src/pages/llmodels/config/llama-config.ts +++ b/src/pages/llmodels/config/llama-config.ts @@ -23,6 +23,10 @@ const options = [ { label: '--ubatch-size', value: '--ubatch-size' + }, + { + label: '--images', + value: '--images' } ]; diff --git a/src/pages/playground/components/ground-stt.tsx b/src/pages/playground/components/ground-stt.tsx new file mode 100644 index 00000000..9a120a61 --- /dev/null +++ b/src/pages/playground/components/ground-stt.tsx @@ -0,0 +1,7 @@ +import React from 'react'; + +const GroundStt = () => { + return
STT
; +}; + +export default React.memo(GroundStt); diff --git a/src/pages/playground/components/ground-tts.tsx b/src/pages/playground/components/ground-tts.tsx new file mode 100644 index 00000000..bce84aa1 --- /dev/null +++ b/src/pages/playground/components/ground-tts.tsx @@ -0,0 +1,353 @@ +import useOverlayScroller from '@/hooks/use-overlay-scroller'; +import { fetchChunkedData, readStreamData } from '@/utils/fetch-chunk-data'; +import { useIntl, useSearchParams } from '@umijs/max'; +import { Spin } from 'antd'; +import classNames from 'classnames'; +import _ from 'lodash'; +import 'overlayscrollbars/overlayscrollbars.css'; +import { + forwardRef, + memo, + useEffect, + useImperativeHandle, + useMemo, + useRef, + useState +} from 'react'; +import { CHAT_API } from '../apis'; +import { Roles, generateMessages } from '../config'; +import { TTSParamsConfig as paramsConfig } from '../config/params-config'; +import { MessageItem } from '../config/types'; +import '../style/ground-left.less'; +import '../style/system-message-wrap.less'; +import MessageInput from './message-input'; +import MessageContent from './multiple-chat/message-content'; +import SystemMessage from './multiple-chat/system-message'; +import ReferenceParams from './reference-params'; +import RerankerParams from './reranker-params'; +import ViewCodeModal from './view-code-modal'; + +interface MessageProps { + modelList: Global.BaseOption[]; + loaded?: boolean; + ref?: any; +} + +const GroundLeft: React.FC = forwardRef((props, ref) => { + const { modelList } = props; + const messageId = useRef(0); + const [messageList, setMessageList] = useState([]); + + const intl = useIntl(); + const [searchParams] = useSearchParams(); + const selectModel = searchParams.get('model') || ''; + const [parameters, setParams] = useState({}); + const [systemMessage, setSystemMessage] = useState(''); + const [show, setShow] = useState(false); + const [loading, setLoading] = useState(false); + const [tokenResult, setTokenResult] = useState(null); + const [collapse, setCollapse] = useState(false); + const contentRef = useRef(''); + const controllerRef = useRef(null); + const scroller = useRef(null); + const currentMessageRef = useRef(null); + const paramsRef = useRef(null); + const messageListLengthCache = useRef(0); + + const { initialize, updateScrollerPosition } = useOverlayScroller(); + const { initialize: innitializeParams } = useOverlayScroller(); + + const initialValues = { + voice: 'Alloy', + response_format: 'mp3', + speed: 1 + }; + + useImperativeHandle(ref, () => { + return { + viewCode() { + setShow(true); + }, + setCollapse() { + setCollapse(!collapse); + }, + collapse: collapse + }; + }); + + const viewCodeMessage = useMemo(() => { + return generateMessages([ + { role: Roles.System, content: systemMessage }, + ...messageList + ]); + }, [messageList, systemMessage]); + + const setMessageId = () => { + messageId.current = messageId.current + 1; + }; + + const handleNewMessage = (message?: { role: string; content: string }) => { + const newMessage = message || { + role: + _.last(messageList)?.role === Roles.User ? Roles.Assistant : Roles.User, + content: '' + }; + messageList.push({ + ...newMessage, + uid: messageId.current + 1 + }); + setMessageId(); + setMessageList([...messageList]); + }; + + const joinMessage = (chunk: any) => { + setTokenResult({ + ...(chunk?.usage ?? {}) + }); + + if (!chunk || !_.get(chunk, 'choices', []).length) { + return; + } + contentRef.current = + contentRef.current + _.get(chunk, 'choices.0.delta.content', ''); + setMessageList([ + ...messageList, + ...currentMessageRef.current, + { + role: Roles.Assistant, + content: contentRef.current, + uid: messageId.current + } + ]); + }; + const handleStopConversation = () => { + controllerRef.current?.abort?.(); + setLoading(false); + }; + + const submitMessage = async (current?: { role: string; content: string }) => { + if (!parameters.model) return; + try { + setLoading(true); + setMessageId(); + setTokenResult(null); + + controllerRef.current?.abort?.(); + controllerRef.current = new AbortController(); + const signal = controllerRef.current.signal; + currentMessageRef.current = current + ? [ + { + ...current, + uid: messageId.current + } + ] + : []; + + contentRef.current = ''; + setMessageList((pre) => { + return [...pre, ...currentMessageRef.current]; + }); + + const messageParams = [ + { role: Roles.System, content: systemMessage }, + ...messageList, + ...currentMessageRef.current + ]; + + const messages = generateMessages(messageParams); + + const chatParams = { + messages: messages, + ...parameters, + stream: true, + stream_options: { + include_usage: true + } + }; + const result: any = await fetchChunkedData({ + data: chatParams, + url: CHAT_API, + signal + }); + + if (result?.error) { + setTokenResult({ + error: true, + errorMessage: + result?.data?.error?.message || result?.data?.message || '' + }); + return; + } + setMessageId(); + const { reader, decoder } = result; + await readStreamData(reader, decoder, (chunk: any) => { + if (chunk?.error) { + setTokenResult({ + error: true, + errorMessage: chunk?.error?.message || chunk?.message || '' + }); + return; + } + joinMessage(chunk); + }); + } catch (error) { + // console.log('error:', error); + } finally { + setLoading(false); + } + }; + const handleClear = () => { + if (!messageList.length) { + return; + } + setMessageId(); + setMessageList([]); + setTokenResult(null); + }; + + const handleSendMessage = (message: Omit) => { + console.log('message:', message); + const currentMessage = + message.content || message.imgs?.length ? message : undefined; + submitMessage(currentMessage); + }; + + const handleCloseViewCode = () => { + setShow(false); + }; + + const handleSelectModel = () => {}; + + const handlePresetPrompt = (list: { role: string; content: string }[]) => { + const sysMsg = list.filter((item) => item.role === 'system'); + const userMsg = list + .filter((item) => item.role === 'user') + .map((item) => { + setMessageId(); + return { + ...item, + uid: messageId.current + }; + }); + setSystemMessage(sysMsg[0]?.content || ''); + setMessageList(userMsg); + }; + + useEffect(() => { + if (scroller.current) { + initialize(scroller.current); + } + }, [scroller.current, initialize]); + + useEffect(() => { + if (paramsRef.current) { + innitializeParams(paramsRef.current); + } + }, [paramsRef.current, innitializeParams]); + + useEffect(() => { + if (loading) { + updateScrollerPosition(); + } + }, [messageList, loading]); + + useEffect(() => { + if (messageList.length > messageListLengthCache.current) { + updateScrollerPosition(); + } + messageListLengthCache.current = messageList.length; + }, [messageList.length]); + + return ( +
+
+
+ <> +
+ +
+ +
+ + {loading && ( + +
+
+ )} +
+ +
+ {tokenResult && ( +
+ +
+ )} +
+ +
+
+
+
+ +
+
+ + +
+ ); +}); + +export default memo(GroundLeft); diff --git a/src/pages/playground/components/reranker-params.tsx b/src/pages/playground/components/reranker-params.tsx index c06e53f0..5ec7ff4f 100644 --- a/src/pages/playground/components/reranker-params.tsx +++ b/src/pages/playground/components/reranker-params.tsx @@ -1,9 +1,10 @@ +import FieldWrapper from '@/components/seal-form/field-wrapper'; import SealInput from '@/components/seal-form/seal-input'; import SealSelect from '@/components/seal-form/seal-select'; import { INPUT_WIDTH } from '@/constants'; import { InfoCircleOutlined } from '@ant-design/icons'; import { useIntl } from '@umijs/max'; -import { Form, InputNumber, Tooltip } from 'antd'; +import { Form, InputNumber, Slider, Tooltip } from 'antd'; import _ from 'lodash'; import { memo, useCallback, useEffect, useId } from 'react'; import { ParamsSchema } from '../config/types'; @@ -104,45 +105,6 @@ const ParamsSettings: React.FC = ({ form.setFieldsValue(globalParams); }, [globalParams]); - const renderFields = useCallback(() => { - console.log('paramsConfig:', paramsConfig); - if (!paramsConfig?.length) { - return null; - } - return paramsConfig.map((item: ParamsSchema) => { - if (item.type === 'InputNumber') { - return ( - - - - ); - } - if (item.type === 'Select') { - return ( - - - - ); - } - return null; - }); - }, [paramsConfig]); - const renderLabel = (args: { field: string; label: string; @@ -178,6 +140,73 @@ const ParamsSettings: React.FC = ({ ); }; + const renderFields = useCallback(() => { + console.log('paramsConfig:', paramsConfig); + if (!paramsConfig?.length) { + return null; + } + return paramsConfig.map((item: ParamsSchema) => { + if (item.type === 'InputNumber') { + return ( + + + + ); + } + if (item.type === 'Select') { + return ( + + + + ); + } + if (item.type === 'Slider') { + return ( + + + handleFieldValueChange(val, item.name)} + > + + + ); + } + return null; + }); + }, [paramsConfig]); + return (
[]; + options?: Global.BaseOption[]; value?: string | number | boolean | string[]; min?: number; max?: number; step?: number; disabled?: boolean; defaultValue?: string | number | boolean; - rules: { required: boolean; message: string }[]; + rules: { required: boolean; message?: string }[]; placeholder?: string; attrs?: Record; + description?: { + text: string; + isLocalized?: boolean; + }; } diff --git a/src/pages/playground/speech.tsx b/src/pages/playground/speech.tsx new file mode 100644 index 00000000..476160d0 --- /dev/null +++ b/src/pages/playground/speech.tsx @@ -0,0 +1,189 @@ +import IconFont from '@/components/icon-font'; +import breakpoints from '@/config/breakpoints'; +import HotKeys from '@/config/hotkeys'; +import useWindowResize from '@/hooks/use-window-resize'; +import { AudioOutlined } from '@ant-design/icons'; +import { PageContainer } from '@ant-design/pro-components'; +import { useIntl } from '@umijs/max'; +import { Button, Segmented, Space, Tabs, TabsProps } from 'antd'; +import classNames from 'classnames'; +import _ from 'lodash'; +import { useCallback, useEffect, useRef, useState } from 'react'; +import { useHotkeys } from 'react-hotkeys-hook'; +import { queryModelsList } from './apis'; +import GroundSTT from './components/ground-stt'; +import GroundTTS from './components/ground-tts'; +import './style/play-ground.less'; + +const TabsValueMap = { + Tab1: 'tts', + Tab2: 'stt' +}; + +const Playground: React.FC = () => { + const intl = useIntl(); + const { size } = useWindowResize(); + const [activeKey, setActiveKey] = useState(TabsValueMap.Tab1); + const groundTabRef1 = useRef(null); + const groundTabRef2 = useRef(null); + const [modelList, setModelList] = useState[]>([]); + const [loaded, setLoaded] = useState(false); + const optionsList = [ + { + label: 'TTS', + value: TabsValueMap.Tab1, + icon: + }, + { + label: 'Realtime', + value: TabsValueMap.Tab2, + icon: + } + ]; + + const handleViewCode = useCallback(() => { + if (activeKey === TabsValueMap.Tab1) { + groundTabRef1.current?.viewCode?.(); + } else if (activeKey === TabsValueMap.Tab2) { + groundTabRef2.current?.viewCode?.(); + } + }, [activeKey]); + + const handleToggleCollapse = useCallback(() => { + if (activeKey === TabsValueMap.Tab1) { + groundTabRef1.current?.setCollapse?.(); + return; + } + groundTabRef2.current?.setCollapse?.(); + }, [activeKey]); + + const items: TabsProps['items'] = [ + { + key: 'tts', + label: 'TTS', + children: ( + + ) + }, + { + key: 'stt', + label: 'Realtime', + children: ( + + ) + } + ]; + + useEffect(() => { + if (size.width < breakpoints.lg) { + if (!groundTabRef1.current?.collapse) { + groundTabRef1.current?.setCollapse?.(); + } + } + }, [size.width]); + + useEffect(() => { + const getModelList = async () => { + try { + const params = { + embedding_only: false + }; + const res = await queryModelsList(params); + const list = _.map(res.data || [], (item: any) => { + return { + value: item.id, + label: item.id + }; + }) as Global.BaseOption[]; + return list; + } catch (error) { + console.error(error); + return []; + } + }; + + const fetchData = async () => { + try { + const modelist = await getModelList(); + setModelList(modelist); + } catch (error) { + setLoaded(true); + } + }; + fetchData(); + }, []); + + const renderExtra = () => { + if (activeKey === 'compare') { + return false; + } + return ( + + + + + ); + }; + + useHotkeys( + HotKeys.RIGHT.join(','), + () => { + groundTabRef1.current?.setCollapse?.(); + }, + { + preventDefault: true + } + ); + + return ( + + + {intl.formatMessage({ id: 'menu.playground.speech' })} + + { + setActiveKey(key)} + > + } + + ), + breadcrumb: {} + }} + extra={renderExtra()} + className={classNames('playground-container', { + compare: activeKey === 'compare', + chat: activeKey !== 'compare' + })} + > +
+
+ +
+
+
+ ); +}; + +export default Playground;