From 4c3dddc486ae7f4b079e663ac59d9f68f3d843d3 Mon Sep 17 00:00:00 2001 From: jialin Date: Mon, 14 Oct 2024 10:23:11 +0800 Subject: [PATCH] feat: playground rerank --- package.json | 4 +- pnpm-lock.yaml | 86 +++++ src/components/seal-form/row-textarea.tsx | 115 ++++++ .../seal-form/styles/row-textarea.less | 60 +++ src/global.less | 1 + src/locales/en-US/playground.ts | 1 + src/locales/zh-CN/playground.ts | 1 + src/pages/llmodels/components/table-list.tsx | 52 +-- .../llmodels/components/update-modal.tsx | 39 +- src/pages/llmodels/config/types.ts | 1 + src/pages/playground/apis/index.ts | 18 + src/pages/playground/components/file-list.tsx | 49 +++ .../playground/components/ground-reranker.tsx | 354 ++++++++++++++++++ .../playground/components/input-list.tsx | 109 ++++++ .../playground/components/message-input.tsx | 138 ++++--- .../components/multiple-chat/content-item.tsx | 21 +- .../components/multiple-chat/index.tsx | 1 + .../multiple-chat/message-content.tsx | 8 +- .../components/reference-params.tsx | 43 ++- .../playground/components/rerank-message.tsx | 56 +++ .../playground/components/reranker-params.tsx | 188 ++++++++++ .../playground/components/upload-file.tsx | 118 ++++++ .../playground/components/upload-img.tsx | 8 +- src/pages/playground/config/types.ts | 1 + src/pages/playground/index.tsx | 70 +++- src/pages/playground/style/content-item.less | 2 +- src/pages/playground/style/file-list.less | 38 ++ src/pages/playground/style/ground-left.less | 8 + src/pages/playground/style/input-list.less | 39 ++ .../playground/style/rerank-message.less | 45 +++ src/utils/index.ts | 45 +++ 31 files changed, 1579 insertions(+), 140 deletions(-) create mode 100644 src/components/seal-form/row-textarea.tsx create mode 100644 src/components/seal-form/styles/row-textarea.less create mode 100644 src/pages/playground/components/file-list.tsx create mode 100644 src/pages/playground/components/ground-reranker.tsx create mode 100644 src/pages/playground/components/input-list.tsx create mode 100644 src/pages/playground/components/rerank-message.tsx create mode 100644 src/pages/playground/components/reranker-params.tsx create mode 100644 src/pages/playground/components/upload-file.tsx create mode 100644 src/pages/playground/style/file-list.less create mode 100644 src/pages/playground/style/input-list.less create mode 100644 src/pages/playground/style/rerank-message.less diff --git a/package.json b/package.json index f627ef53..15f0e0d2 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "jotai": "^2.8.4", "localforage": "^1.10.0", "lodash": "^4.17.21", + "mammoth": "^1.8.0", "marked": "^14.1.0", "numeral": "^2.0.6", "overlayscrollbars": "^2.10.0", @@ -48,7 +49,8 @@ "react-dom": "^18.2.0", "react-hotkeys-hook": "^4.5.0", "simplebar-react": "^3.2.6", - "umi-presets-pro": "^2.0.3" + "umi-presets-pro": "^2.0.3", + "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz" }, "devDependencies": { "@types/marked": "^6.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3b0a4ae1..956c3821 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -83,6 +83,9 @@ dependencies: lodash: specifier: ^4.17.21 version: 4.17.21 + mammoth: + specifier: ^1.8.0 + version: 1.8.0 marked: specifier: ^14.1.0 version: 14.1.0 @@ -113,6 +116,9 @@ dependencies: umi-presets-pro: specifier: ^2.0.3 version: 2.0.3(@babel/core@7.25.7)(@types/react-dom@18.3.0)(@types/react@18.3.1)(antd@5.20.6)(dva@2.5.0-beta.2)(rc-field-form@2.4.0)(react-dom@18.2.0)(react@18.2.0)(umi@4.3.24) + xlsx: + specifier: https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz + version: '@cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz' devDependencies: '@types/marked': @@ -7066,6 +7072,11 @@ packages: '@webassemblyjs/ast': 1.12.1 '@xtuc/long': 4.2.2 + /@xmldom/xmldom@0.8.10: + resolution: {integrity: sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==, tarball: https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz} + engines: {node: '>=10.0.0'} + dev: false + /@xterm/addon-fit@0.10.0(@xterm/xterm@5.5.0): resolution: {integrity: sha512-UFYkDm4HUahf2lnEyHvio51TNGiLK66mqP2JoATy7hRZeXaGMRDr00JiSF7m63vR5WKATF605yEggJKsw0JpMQ==, tarball: https://registry.npmjs.org/@xterm/addon-fit/-/addon-fit-0.10.0.tgz} peerDependencies: @@ -7866,6 +7877,10 @@ packages: engines: {node: '>=8'} dev: false + /bluebird@3.4.7: + resolution: {integrity: sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==, tarball: https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz} + dev: false + /bn.js@4.12.0: resolution: {integrity: sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==, tarball: https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz} dev: false @@ -9014,6 +9029,10 @@ packages: randombytes: 2.1.0 dev: false + /dingbat-to-unicode@1.0.1: + resolution: {integrity: sha512-98l0sW87ZT58pU4i61wa2OHwxbiYSbuxsCBozaVnYX2iCnr3bLM3fIes1/ej7h1YdOKuKt/MLs706TVnALA65w==, tarball: https://registry.npmjs.org/dingbat-to-unicode/-/dingbat-to-unicode-1.0.1.tgz} + dev: false + /dir-glob@3.0.1: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} @@ -9110,6 +9129,12 @@ packages: tslib: 2.6.2 dev: false + /duck@0.1.12: + resolution: {integrity: sha512-wkctla1O6VfP89gQ+J/yDesM0S7B7XLXjKGzXxMDVFg7uEn706niAtyYovKbyq1oT9YwDcly721/iUWoc8MVRg==, tarball: https://registry.npmjs.org/duck/-/duck-0.1.12.tgz} + dependencies: + underscore: 1.13.7 + dev: false + /duplexer2@0.1.4: resolution: {integrity: sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==} dependencies: @@ -11928,6 +11953,15 @@ packages: object.values: 1.2.0 dev: false + /jszip@3.10.1: + resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==, tarball: https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz} + dependencies: + lie: 3.3.0 + pako: 1.0.11 + readable-stream: 2.3.8 + setimmediate: 1.0.5 + dev: false + /keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} dependencies: @@ -12028,6 +12062,12 @@ packages: immediate: 3.0.6 dev: false + /lie@3.3.0: + resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==, tarball: https://registry.npmjs.org/lie/-/lie-3.3.0.tgz} + dependencies: + immediate: 3.0.6 + dev: false + /lightningcss-darwin-arm64@1.22.1: resolution: {integrity: sha512-ldvElu+R0QimNTjsKpaZkUv3zf+uefzLy/R1R19jtgOfSRM+zjUCUgDhfEDRmVqJtMwYsdhMI2aJtJChPC6Osg==, tarball: https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.22.1.tgz} engines: {node: '>= 12.0.0'} @@ -12262,6 +12302,14 @@ packages: js-tokens: 4.0.0 dev: false + /lop@0.4.2: + resolution: {integrity: sha512-RefILVDQ4DKoRZsJ4Pj22TxE3omDO47yFpkIBoDKzkqPRISs5U1cnAdg/5583YPkWPaLIYHOKRMQSvjFsO26cw==, tarball: https://registry.npmjs.org/lop/-/lop-0.4.2.tgz} + dependencies: + duck: 0.1.12 + option: 0.2.4 + underscore: 1.13.7 + dev: false + /lower-case@2.0.2: resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==, tarball: https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz} dependencies: @@ -12306,6 +12354,23 @@ packages: tmpl: 1.0.5 dev: false + /mammoth@1.8.0: + resolution: {integrity: sha512-pJNfxSk9IEGVpau+tsZFz22ofjUsl2mnA5eT8PjPs2n0BP+rhVte4Nez6FdgEuxv3IGI3afiV46ImKqTGDVlbA==, tarball: https://registry.npmjs.org/mammoth/-/mammoth-1.8.0.tgz} + engines: {node: '>=12.0.0'} + hasBin: true + dependencies: + '@xmldom/xmldom': 0.8.10 + argparse: 1.0.10 + base64-js: 1.5.1 + bluebird: 3.4.7 + dingbat-to-unicode: 1.0.1 + jszip: 3.10.1 + lop: 0.4.2 + path-is-absolute: 1.0.1 + underscore: 1.13.7 + xmlbuilder: 10.1.1 + dev: false + /map-age-cleaner@0.1.3: resolution: {integrity: sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==, tarball: https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz} engines: {node: '>=6'} @@ -13023,6 +13088,10 @@ packages: yaml: 1.10.2 dev: false + /option@0.2.4: + resolution: {integrity: sha512-pkEqbDyl8ou5cpq+VsnQbe/WlEy5qS7xPzMS1U55OCG9KPvwFD46zDbxQIj3egJSFc3D+XhYOPUzz49zQAVy7A==, tarball: https://registry.npmjs.org/option/-/option-0.2.4.tgz} + dev: false + /optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} @@ -18023,6 +18092,10 @@ packages: which-boxed-primitive: 1.0.2 dev: false + /underscore@1.13.7: + resolution: {integrity: sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g==, tarball: https://registry.npmjs.org/underscore/-/underscore-1.13.7.tgz} + dev: false + /undici-types@5.26.5: resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} dev: false @@ -18592,6 +18665,11 @@ packages: signal-exit: 3.0.7 dev: false + /xmlbuilder@10.1.1: + resolution: {integrity: sha512-OyzrcFLL/nb6fMGHbiRDuPup9ljBycsdCypwuyg5AAHvyWzGfChJpCXMG88AGTIMFhGZ9RccFN1e6lhg3hkwKg==, tarball: https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-10.1.1.tgz} + engines: {node: '>=4.0'} + dev: false + /xtend@2.1.2: resolution: {integrity: sha512-vMNKzr2rHP9Dp/e1NQFnLQlwlhp9L/LfvnsVdHxN1f+uggyVI3i08uD14GPvCToPkdsRfyPqIyYGmIk58V98ZQ==} engines: {node: '>=0.4'} @@ -18681,3 +18759,11 @@ packages: /zwitch@1.0.5: resolution: {integrity: sha512-V50KMwwzqJV0NpZIZFwfOD5/lyny3WlSzRiXgA0G7VUnRlqttta1L6UQIHzd6EuBY/cHGfwTIck7w1yH6Q5zUw==, tarball: https://registry.npmjs.org/zwitch/-/zwitch-1.0.5.tgz} dev: false + + '@cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz': + resolution: {tarball: https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz} + name: xlsx + version: 0.20.3 + engines: {node: '>=0.8'} + hasBin: true + dev: false diff --git a/src/components/seal-form/row-textarea.tsx b/src/components/seal-form/row-textarea.tsx new file mode 100644 index 00000000..e4dd54c8 --- /dev/null +++ b/src/components/seal-form/row-textarea.tsx @@ -0,0 +1,115 @@ +import { CloseOutlined } from '@ant-design/icons'; +import { useIntl } from '@umijs/max'; +import { Button, Input, Tooltip } from 'antd'; +import classNames from 'classnames'; +import React, { useState } from 'react'; +import './styles/row-textarea.less'; + +interface SystemMessageProps { + style?: React.CSSProperties; + value: string; + placeholder?: string; + label?: React.ReactNode; + onChange: (e: any) => void; +} + +const RowTextarea: React.FC = (props) => { + const { value, onChange, style, label, placeholder } = props; + const intl = useIntl(); + const rowTextAreaRef = React.useRef(null); + const [autoSize, setAutoSize] = useState<{ + minRows: number; + maxRows: number; + focus: boolean; + }>({ minRows: 1, maxRows: 1, focus: false }); + + const handleFocus = () => { + setAutoSize({ + minRows: 3, + maxRows: 3, + focus: true + }); + setTimeout(() => { + rowTextAreaRef.current?.focus?.({ + cursor: 'end' + }); + }, 50); + }; + + const handleBlur = (e: any) => { + setAutoSize({ + minRows: 2, + maxRows: 2, + focus: false + }); + }; + + const handleOnChange = (e: any) => { + onChange?.(e); + }; + + const handleClear = () => { + onChange?.({ target: { value: '' } }); + }; + + return ( +
+ { +
+ {label} + +
+ } + {!autoSize.focus && ( +
+
+ {label} + {value || ( + + {placeholder} + + )} +
+ {value && ( + + + + )} +
+ )} +
+ ); +}; + +export default React.memo(RowTextarea); diff --git a/src/components/seal-form/styles/row-textarea.less b/src/components/seal-form/styles/row-textarea.less new file mode 100644 index 00000000..77c70e32 --- /dev/null +++ b/src/components/seal-form/styles/row-textarea.less @@ -0,0 +1,60 @@ +.row-textarea { + position: relative; + + &.focus { + padding-top: 9px; + } + + .content-wrap { + position: relative; + display: flex; + align-items: center; + justify-content: space-between; + padding-right: 20px; + cursor: pointer; + + &:hover { + .clear-btn { + display: block; + } + } + } + + textarea.ant-input { + background-color: transparent; + box-shadow: none; + } + + .clear-btn { + display: none; + position: absolute; + right: 6px; + top: 6px; + } + + .textarea-label { + position: relative; + top: 1px; + font-weight: var(--font-weight-bold); + padding-left: 14px; + } + + .content { + flex: 1; + width: 100px; + height: 40px; + line-height: 24px; + padding: 8px 14px; + padding-right: 4px; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + color: var(--ant-color-text-secondary); + + .title { + font-weight: var(--font-weight-normal); + padding-right: 10px; + color: var(--ant-color-text); + } + } +} diff --git a/src/global.less b/src/global.less index 2f8277ed..f4d0bd71 100644 --- a/src/global.less +++ b/src/global.less @@ -64,6 +64,7 @@ html { --seal-transition-func: cubic-bezier(0, 0, 1, 1); --color-progress-green: rgba(84, 204, 152, 100%); --color-border-1: rgba(217, 217, 217, 100%); + --ant-rate-star-color: #fadb14; // ======== input ============ --ant-input-active-shadow: 0 0 0 2px rgba(5, 255, 105, 6%); --ant-input-active-border-color: #007bff; diff --git a/src/locales/en-US/playground.ts b/src/locales/en-US/playground.ts index f64f91a4..5ca57e4f 100644 --- a/src/locales/en-US/playground.ts +++ b/src/locales/en-US/playground.ts @@ -42,6 +42,7 @@ export default { 'playground.toolbar.compare4Model': '4-Model Comparison', 'playground.toolbar.compare6Model': '6-Model Comparison', 'playground.input.holder': 'Type / to input message', + 'playground.input.keyword.holder': 'Type / to input your query', 'playground.compare.apply': 'Apply', 'playground.compare.applytoall': 'Apply to all models', 'playground.model.noavailable': 'No available models', diff --git a/src/locales/zh-CN/playground.ts b/src/locales/zh-CN/playground.ts index 94f7a3fc..51f9608c 100644 --- a/src/locales/zh-CN/playground.ts +++ b/src/locales/zh-CN/playground.ts @@ -42,6 +42,7 @@ export default { 'playground.toolbar.compare4Model': '4 模型对比', 'playground.toolbar.compare6Model': '6 模型对比', 'playground.input.holder': '按 / 开始输入', + 'playground.input.keyword.holder': '按 / 输入你的查询', 'playground.compare.apply': '应用', 'playground.compare.applytoall': '应用到所有模型', 'playground.model.noavailable': '无可用模型', diff --git a/src/pages/llmodels/components/table-list.tsx b/src/pages/llmodels/components/table-list.tsx index 167b4d42..7c618bf8 100644 --- a/src/pages/llmodels/components/table-list.tsx +++ b/src/pages/llmodels/components/table-list.tsx @@ -367,26 +367,29 @@ const Models: React.FC = ({ setOpenAddModal(true); }; - const handleSelect = useCallback((val: any, row: ListItem) => { - if (val === 'edit') { - handleEdit(row); - } - if (val === 'chat') { - handleOpenPlayGround(row); - } - if (val === 'delete') { - handleDelete(row); - } - if (val === 'embedding') { - setEmbeddingParams({ - params: { - input: 'Your text string goes here', - model: row.name - }, - show: true - }); - } - }, []); + const handleSelect = useCallback( + (val: any, row: ListItem) => { + if (val === 'edit') { + handleEdit(row); + } + if (val === 'chat') { + handleOpenPlayGround(row); + } + if (val === 'delete') { + handleDelete(row); + } + if (val === 'embedding') { + setEmbeddingParams({ + params: { + input: 'Your text string goes here', + model: row.name + }, + show: true + }); + } + }, + [handleEdit, handleOpenPlayGround, handleDelete, setEmbeddingParams] + ); const handleChildSelect = useCallback( (val: any, row: ModelInstanceListItem, list: ModelInstanceListItem[]) => { @@ -524,7 +527,14 @@ const Models: React.FC = ({ {text} - {record.embedding_only && ( + {record.reranker && ( + + + Reranker + + + )} + {record.embedding_only && !record.reranker && ( Embedding Only diff --git a/src/pages/llmodels/components/update-modal.tsx b/src/pages/llmodels/components/update-modal.tsx index a21b89f1..636328f5 100644 --- a/src/pages/llmodels/components/update-modal.tsx +++ b/src/pages/llmodels/components/update-modal.tsx @@ -72,21 +72,24 @@ const UpdateModal: React.FC = (props) => { setGpuOptions(list); }; - const initFormValue = useMemo(() => { - const result = setSourceRepoConfigValue( - props.data?.source || '', - props.data - ); + useEffect(() => { + if (action === PageAction.EDIT && open) { + const result = setSourceRepoConfigValue( + props.data?.source || '', + props.data + ); - return { - ...result.values, - ..._.omit(props.data, result.omits), - scheduleType: props.data?.gpu_selector ? 'manual' : 'auto', - gpu_selector: props.data?.gpu_selector - ? `${props.data?.gpu_selector.worker_name}-${props.data?.gpu_selector.gpu_name}-${props.data?.gpu_selector.gpu_index}` - : null - }; - }, [props.data]); + const formData = { + ...result.values, + ..._.omit(props.data, result.omits), + scheduleType: props.data?.gpu_selector ? 'manual' : 'auto', + gpu_selector: props.data?.gpu_selector + ? `${props.data?.gpu_selector.worker_name}-${props.data?.gpu_selector.gpu_name}-${props.data?.gpu_selector.gpu_index}` + : null + }; + form.setFieldsValue(formData); + } + }, [open]); useEffect(() => { setIsGGUF(props.data?.backend === backendOptionsMap.llamaBox); @@ -245,6 +248,10 @@ const UpdateModal: React.FC = (props) => { } }; + const handleOnClose = () => { + onCancel?.(); + }; + useEffect(() => { getGPUList(); }, []); @@ -256,6 +263,7 @@ const UpdateModal: React.FC = (props) => { centered={true} onOk={handleSumit} onCancel={onCancel} + onClose={handleOnClose} destroyOnClose={true} closeIcon={true} maskClosable={false} @@ -294,9 +302,6 @@ const UpdateModal: React.FC = (props) => { padding: 'var(--ant-modal-content-padding)', paddingBlock: 0 }} - initialValues={{ - ...initFormValue - }} > name="name" diff --git a/src/pages/llmodels/config/types.ts b/src/pages/llmodels/config/types.ts index 6c48b5a4..19045a54 100644 --- a/src/pages/llmodels/config/types.ts +++ b/src/pages/llmodels/config/types.ts @@ -1,6 +1,7 @@ export interface ListItem { source: string; backend: string; + reranker: boolean; huggingface_repo_id: string; huggingface_file_name: string; huggingface_filename: string; diff --git a/src/pages/playground/apis/index.ts b/src/pages/playground/apis/index.ts index 90b92039..c2c2d3ee 100644 --- a/src/pages/playground/apis/index.ts +++ b/src/pages/playground/apis/index.ts @@ -4,6 +4,8 @@ export const CHAT_API = '/v1-openai/chat/completions'; export const OPENAI_MODELS = '/v1-openai/models'; +export const RERANKER_API = '/rerank'; + export async function execChatCompletions(params: any) { return request(`${CHAT_API}`, { method: 'POST', @@ -17,3 +19,19 @@ export const queryModelsList = async (params: any) => { params }); }; + +export const rerankerQuery = async ( + params: { + model: string; + query: string; + top_n: number; + documents: string[]; + }, + options?: any +) => { + return request(`${RERANKER_API}`, { + method: 'POST', + data: params, + cancelToken: options?.cancelToken + }); +}; diff --git a/src/pages/playground/components/file-list.tsx b/src/pages/playground/components/file-list.tsx new file mode 100644 index 00000000..08db044b --- /dev/null +++ b/src/pages/playground/components/file-list.tsx @@ -0,0 +1,49 @@ +import AutoTooltip from '@/components/auto-tooltip'; +import { DeleteOutlined, PaperClipOutlined } from '@ant-design/icons'; +import { useIntl } from '@umijs/max'; +import { Button, Tooltip } from 'antd'; +import classNames from 'classnames'; +import React from 'react'; +import '../style/file-list.less'; + +interface FileListProps { + fileList: { text: string; name: string; uid: number | string }[]; + ghost?: boolean; + showIcon?: boolean; + onDelete?: (uid: number | string) => void; +} + +const FileList: React.FC = (props) => { + const { fileList, ghost, showIcon = true, onDelete } = props; + const intl = useIntl(); + return ( +
+ {fileList.map((file) => { + return ( +
+ + {showIcon && } + {file.name} + + {onDelete && ( + + + + )} +
+ ); + })} +
+ ); +}; + +export default React.memo(FileList); diff --git a/src/pages/playground/components/ground-reranker.tsx b/src/pages/playground/components/ground-reranker.tsx new file mode 100644 index 00000000..fdbe91d7 --- /dev/null +++ b/src/pages/playground/components/ground-reranker.tsx @@ -0,0 +1,354 @@ +import useOverlayScroller from '@/hooks/use-overlay-scroller'; +import useRequestToken from '@/hooks/use-request-token'; +import { ClearOutlined, InboxOutlined } from '@ant-design/icons'; +import { useIntl, useSearchParams } from '@umijs/max'; +import { Button, Spin } from 'antd'; +import classNames from 'classnames'; +import 'overlayscrollbars/overlayscrollbars.css'; +import { + forwardRef, + memo, + useEffect, + useImperativeHandle, + useRef, + useState +} from 'react'; +import { rerankerQuery } from '../apis'; +import { MessageItem } from '../config/types'; +import '../style/ground-left.less'; +import '../style/system-message-wrap.less'; +import FileList from './file-list'; +import InputList from './input-list'; +import MessageInput from './message-input'; +import ReferenceParams from './reference-params'; +import RerankMessage from './rerank-message'; +import RerankerParams from './reranker-params'; +import UploadFile from './upload-file'; +import ViewCodeModal from './view-code-modal'; + +interface MessageProps { + modelList: Global.BaseOption[]; + loaded?: boolean; + ref?: any; +} + +const GroundReranker: React.FC = forwardRef((props, ref) => { + const { modelList } = props; + const messageId = useRef(0); + const [messageList, setMessageList] = useState([]); + + const intl = useIntl(); + const requestSource = useRequestToken(); + 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 requestToken = useRef(null); + const [fileList, setFileList] = useState< + { text: string; name: string; uid: number | string }[] + >([]); + + const [textList, setTextList] = useState< + { text: string; uid: number | string; name: string }[] + >([]); + + const { initialize, updateScrollerPosition } = useOverlayScroller(); + const { initialize: innitializeParams } = useOverlayScroller(); + + useImperativeHandle(ref, () => { + return { + viewCode() { + setShow(true); + }, + setCollapse() { + setCollapse(!collapse); + } + }; + }); + + const setMessageId = () => { + messageId.current = messageId.current + 1; + }; + + const handleStopConversation = () => { + requestToken.current?.cancel?.(); + setLoading(false); + }; + + const submitMessage = async (current?: { role: string; content: string }) => { + if (!parameters.model) return; + try { + setLoading(true); + setMessageId(); + setTokenResult(null); + + requestToken.current?.cancel?.(); + requestToken.current = requestSource(); + + controllerRef.current?.abort?.(); + controllerRef.current = new AbortController(); + const signal = controllerRef.current.signal; + + currentMessageRef.current = current + ? [ + { + content: current.content, + title: 'Query', + uid: messageId.current + } + ] + : []; + + contentRef.current = ''; + setMessageList((pre) => { + return [...currentMessageRef.current]; + }); + + const documentList: any[] = [...textList, ...fileList]; + console.log('documentList:', documentList); + const result: any = await rerankerQuery( + { + model: parameters.model, + top_n: parameters.top_n, + query: current?.content || '', + documents: [ + ...textList.map((item) => item.text), + ...fileList.map((item) => item.text) + ] + }, + { + token: requestToken.current.token + } + ); + console.log('result:', result); + + setMessageId(); + setTokenResult(result.usage); + setMessageList([ + ...currentMessageRef.current, + { + title: 'Result', + content: result.results?.map((item: any) => { + return { + uid: item.index, + text: `${item.document?.text?.slice(0, 100) || ''}`, + docIndex: item.index, + title: documentList[item.index]?.name || '', + score: item.relevance_score + }; + }), + uid: messageId.current + } + ]); + } catch (error: any) { + setTokenResult({ + error: true, + errorMessage: error.response?.data?.error?.message + }); + } finally { + setLoading(false); + } + }; + const handleClear = () => { + if (!messageList.length) { + return; + } + setMessageId(); + setMessageList([]); + setTokenResult(null); + }; + + const handleSendMessage = (message: Omit) => { + const currentMessage = + message.content || message.imgs?.length ? message : undefined; + submitMessage(currentMessage); + }; + + const handleCloseViewCode = () => { + setShow(false); + }; + + const handleUpdateFileList = ( + files: { text: string; name: string; uid: number | string }[] + ) => { + console.log('files:', files); + setFileList((preList) => { + return [...preList, ...files]; + }); + }; + + const handleDeleteFile = (uid: number | string) => { + setFileList((preList) => { + return preList.filter((item) => item.uid !== uid); + }); + }; + + const handleTextListChange = ( + list: { text: string; uid: number | string; name: string }[] + ) => { + setTextList(list); + }; + + const handleClearDocuments = () => { + setTextList([]); + setFileList([]); + }; + + useEffect(() => { + setMessageId(); + setMessageList([]); + setTokenResult(null); + }, [parameters.model]); + + 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 && ( +
+ +
+ )} +
+ +
+
+
+
+ +

+ Documents + +

+ +
+ +
+
+ +
+ +
+ + + Click or drag file to this area to upload + +
+ + support .txt, .doc, .docx, .xls, .xlsx + +
+
+
+ + +
+ ); +}); + +export default memo(GroundReranker); diff --git a/src/pages/playground/components/input-list.tsx b/src/pages/playground/components/input-list.tsx new file mode 100644 index 00000000..22caceff --- /dev/null +++ b/src/pages/playground/components/input-list.tsx @@ -0,0 +1,109 @@ +import RowTextarea from '@/components/seal-form/row-textarea'; +import { + MinusCircleOutlined, + PlusCircleOutlined, + PlusOutlined +} from '@ant-design/icons'; +import { useIntl } from '@umijs/max'; +import { Button, Tooltip } from 'antd'; +import React, { useRef } from 'react'; +import '../style/input-list.less'; + +interface InputListProps { + textList: { text: string; uid: number | string; name: string }[]; + onChange?: ( + textList: { text: string; uid: number | string; name: string }[] + ) => void; +} + +const InputList: React.FC = ({ textList, onChange }) => { + const intl = useIntl(); + const messageId = useRef(0); + + const setMessageId = () => { + messageId.current = messageId.current + 1; + }; + + const handleAdd = () => { + setMessageId(); + const dataList = [...textList]; + dataList.push({ + text: '', + uid: messageId.current, + name: `Text ${dataList.length + 1}` + }); + onChange?.(dataList); + }; + + const handleDelete = (text: { text: string; uid: number | string }) => { + const dataList = [...textList]; + const index = dataList.findIndex((item) => item.uid === text.uid); + dataList.splice(index, 1); + onChange?.(dataList); + }; + + const handleTextChange = ( + value: string, + text: { text: string; uid: number | string } + ) => { + const dataList = [...textList]; + const index = dataList.findIndex((item) => item.uid === text.uid); + dataList[index].text = value; + onChange?.(dataList); + }; + + return ( +
+ {textList.length === 0 && ( + + )} + {textList.map((text, index) => { + return ( +
+
+ handleTextChange(e.target.value, text)} + > +
+ + + + + {index === textList.length - 1 && ( + + + + )} + +
+ ); + })} +
+ ); +}; + +export default React.memo(InputList); diff --git a/src/pages/playground/components/message-input.tsx b/src/pages/playground/components/message-input.tsx index a42d5ca8..018f4164 100644 --- a/src/pages/playground/components/message-input.tsx +++ b/src/pages/playground/components/message-input.tsx @@ -1,6 +1,5 @@ import IconFont from '@/components/icon-font'; import HotKeys, { KeyMap } from '@/config/hotkeys'; -import { platformCall } from '@/utils'; import { ClearOutlined, SendOutlined, SwapOutlined } from '@ant-design/icons'; import { useIntl } from '@umijs/max'; import { Button, Divider, Input, Select, Tooltip } from 'antd'; @@ -61,18 +60,19 @@ interface MessageInputProps { handleAbortFetch: () => void; updateLayout?: (value: { span: number; count: number }) => void; clearAll: () => void; - setModelSelections: ( + setModelSelections?: ( modelList: (Global.BaseOption & { instanceId: symbol; })[] ) => void; - presetPrompt: (list: CurrentMessage[]) => void; - addMessage: (message: CurrentMessage) => void; + presetPrompt?: (list: CurrentMessage[]) => void; + addMessage?: (message: CurrentMessage) => void; loading: boolean; showModelSelection?: boolean; disabled: boolean; isEmpty?: boolean; scope: string; + placeholer?: string; } const MessageInput: React.FC = ({ @@ -88,12 +88,11 @@ const MessageInput: React.FC = ({ showModelSelection, disabled, isEmpty, - scope + scope, + placeholer }) => { const { TextArea } = Input; const intl = useIntl(); - const platform = platformCall(); - // const [disabled, setDisabled] = useState(false); const [open, setOpen] = useState(false); const [focused, setFocused] = useState(false); const [message, setMessage] = useState({ @@ -130,11 +129,9 @@ const MessageInput: React.FC = ({ resetMessage(); }; const onStop = () => { - // setDisabled(false); handleAbortFetch(); }; const handleLayoutChange = (value: { span: number; count: number }) => { - console.log('layout change:', value); updateLayout?.(value); }; @@ -151,7 +148,6 @@ const MessageInput: React.FC = ({ }; const handleUpdateModelSelections = (value: string[]) => { - console.log('update model selections:', value); const list = value?.map?.((val) => { return { value: val, @@ -159,7 +155,7 @@ const MessageInput: React.FC = ({ instanceId: Symbol(val) }; }); - setModelSelections(list); + setModelSelections?.(list); }; const handleOpenPrompt = () => { @@ -167,8 +163,7 @@ const MessageInput: React.FC = ({ }; const handleAddMessage = () => { - console.log('add message=====1'); - addMessage({ ...message }); + addMessage?.({ ...message }); resetMessage(); }; @@ -249,9 +244,14 @@ const MessageInput: React.FC = ({ e.preventDefault(); const text = e.clipboardData.getData('text'); if (text) { + const startPos = e.target.selectionStart; + const endPos = e.target.selectionEnd; setMessage?.({ ...message, - content: message.content + text + content: + message.content.slice(0, startPos) + + text + + message.content.slice(endPos) }); } else { getPasteContent(e); @@ -284,6 +284,10 @@ const MessageInput: React.FC = ({ [message, handleDeleteLastImage] ); + const handleSelectPrompt = (list: CurrentMessage[]) => { + presetPrompt?.(list); + }; + useHotkeys( HotKeys.SUBMIT, (e: any) => { @@ -325,16 +329,23 @@ const MessageInput: React.FC = ({
- - - + {scope !== 'reranker' && ( + <> + + + + + )} @@ -345,17 +356,6 @@ const MessageInput: React.FC = ({ onClick={handleClearAll} > - - {/* - - */} {updateLayout && ( <> @@ -391,18 +391,20 @@ const MessageInput: React.FC = ({ > )} - - [{KeyMap.ADD.textKeybinding}]{' '} + {scope !== 'reranker' && ( + + [{KeyMap.ADD.textKeybinding}]{' '} + {intl.formatMessage({ id: 'common.button.add' })} + + } + > + - + + + )} {!loading ? ( = ({ onDelete={handleDeleteImg} >
- + {scope !== 'reranker' ? ( + + ) : ( + + )} {!message.content && !focused && ( )} @@ -464,7 +482,7 @@ const MessageInput: React.FC = ({ setOpen(false)} - onSelect={presetPrompt} + onSelect={handleSelectPrompt} >
); diff --git a/src/pages/playground/components/multiple-chat/content-item.tsx b/src/pages/playground/components/multiple-chat/content-item.tsx index fe36f9e3..f1a108b3 100644 --- a/src/pages/playground/components/multiple-chat/content-item.tsx +++ b/src/pages/playground/components/multiple-chat/content-item.tsx @@ -15,6 +15,7 @@ interface MessageItemProps { data: MessageItem; editable?: boolean; loading?: boolean; + actions?: string[]; updateMessage?: (message: MessageItem) => void; onDelete?: () => void; } @@ -24,7 +25,8 @@ const ContentItem: React.FC = ({ onDelete, loading, data, - editable + editable, + actions = ['upload', 'delete', 'copy'] }) => { const intl = useIntl(); const inputRef = useRef(null); @@ -107,9 +109,12 @@ const ContentItem: React.FC = ({ e.preventDefault(); const text = e.clipboardData.getData('text'); if (text) { + const startPos = e.target.selectionStart; + const endPos = e.target.selectionEnd; updateMessage?.({ role: data.role, - content: data.content + text, + content: + data.content.slice(0, startPos) + text + data.content.slice(endPos), uid: data.uid }); } else { @@ -176,13 +181,13 @@ const ContentItem: React.FC = ({
- {intl.formatMessage({ id: `playground.${data.role}` })} + {data.title ?? intl.formatMessage({ id: `playground.${data.role}` })}
- {editable && ( + {actions.includes('upload') && ( )} - {data.content && ( + {data.content && actions.includes('copy') && ( = ({ fontSize="12px" > )} - {editable && ( + {actions.includes('delete') && (
) : ( -
- {data.content} -
+
{data.content}
)}
); diff --git a/src/pages/playground/components/multiple-chat/index.tsx b/src/pages/playground/components/multiple-chat/index.tsx index a900f923..8de3c1f2 100644 --- a/src/pages/playground/components/multiple-chat/index.tsx +++ b/src/pages/playground/components/multiple-chat/index.tsx @@ -29,6 +29,7 @@ const MultiCompare: React.FC = ({ modelList, loaded }) => { stop: null, temperature: 1, top_p: 1, + top_n: 0, max_tokens: 1024 }); const [spans, setSpans] = useState<{ diff --git a/src/pages/playground/components/multiple-chat/message-content.tsx b/src/pages/playground/components/multiple-chat/message-content.tsx index 1ee00eec..2f80d7f9 100644 --- a/src/pages/playground/components/multiple-chat/message-content.tsx +++ b/src/pages/playground/components/multiple-chat/message-content.tsx @@ -5,10 +5,11 @@ import ContentItem from './content-item'; interface MessageContentProps { loading?: boolean; - spans: { + spans?: { span: number; count: number; }; + actions?: string[]; editable?: boolean; messageList: MessageItem[]; setMessageList?: (list: any) => void; @@ -17,8 +18,8 @@ interface MessageContentProps { const MessageContent: React.FC = ({ setMessageList, messageList, - spans, - editable + editable, + actions = ['upload', 'delete', 'copy'] }) => { const updateMessage = (index: number, message: MessageItem) => { const newMessageList = [...messageList]; @@ -41,6 +42,7 @@ const MessageContent: React.FC = ({ key={item.uid} data={item} editable={editable} + actions={actions} onDelete={() => handleDelete(index)} updateMessage={(data) => updateMessage(index, data)} /> diff --git a/src/pages/playground/components/reference-params.tsx b/src/pages/playground/components/reference-params.tsx index 31009800..13f4a055 100644 --- a/src/pages/playground/components/reference-params.tsx +++ b/src/pages/playground/components/reference-params.tsx @@ -5,6 +5,7 @@ import _ from 'lodash'; import '../style/reference-params.less'; interface ReferenceParamsProps { + showOutput?: boolean; usage: { error?: boolean; errorMessage?: string; @@ -19,7 +20,7 @@ interface ReferenceParamsProps { const ReferenceParams = (props: ReferenceParamsProps) => { const intl = useIntl(); - const { usage } = props; + const { usage, showOutput = true } = props; if (!usage || _.isEmpty(usage)) { return null; } @@ -63,25 +64,27 @@ const ReferenceParams = (props: ReferenceParamsProps) => { - - - - TPOT: {_.round(usage.time_per_output_token_ms, 2) || 0} ms - - - TTFT: {_.round(usage.time_to_first_token_ms, 2) || 0} ms - - - } - > - - {intl.formatMessage({ id: 'playground.tokenoutput' })}:{' '} - {_.round(usage.tokens_per_second, 2) || 0} Tokens/s - - - + {showOutput && ( + + + + TPOT: {_.round(usage.time_per_output_token_ms, 2) || 0} ms + + + TTFT: {_.round(usage.time_to_first_token_ms, 2) || 0} ms + + + } + > + + {intl.formatMessage({ id: 'playground.tokenoutput' })}:{' '} + {_.round(usage.tokens_per_second, 2) || 0} Tokens/s + + + + )}
); }; diff --git a/src/pages/playground/components/rerank-message.tsx b/src/pages/playground/components/rerank-message.tsx new file mode 100644 index 00000000..85d8b8f0 --- /dev/null +++ b/src/pages/playground/components/rerank-message.tsx @@ -0,0 +1,56 @@ +import { StarFilled } from '@ant-design/icons'; +import { Tooltip } from 'antd'; +import _ from 'lodash'; +import React from 'react'; +import '../style/content-item.less'; +import '../style/rerank-message.less'; + +interface RerankMessageProps { + dataList: { title?: string; content: any; uid: number | string }[]; +} +const RerankMessage: React.FC = ({ dataList }) => { + return ( +
+ {dataList.map((item) => { + return ( +
+
+ {item.title} +
+
+ {Array.isArray(item.content) ? ( +
+ {item.content.map((sItem, sIndex) => { + return ( +
+
+ [{sItem.docIndex + 1}] + + Score: {_.round(sItem.score, 2)} + } + > + + {_.round(sItem.score, 2)} + + +
+
{sItem.text}
+
《{sItem.title}》
+
+ ); + })} +
+ ) : ( +
{item.content}
+ )} +
+
+ ); + })} +
+ ); +}; + +export default React.memo(RerankMessage); diff --git a/src/pages/playground/components/reranker-params.tsx b/src/pages/playground/components/reranker-params.tsx new file mode 100644 index 00000000..be49a912 --- /dev/null +++ b/src/pages/playground/components/reranker-params.tsx @@ -0,0 +1,188 @@ +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 _ from 'lodash'; +import { memo, useCallback, useEffect, useId } from 'react'; +import CustomLabelStyles from '../style/custom-label.less'; + +type ParamsSettingsFormProps = { + top_n?: number; + model?: string; +}; + +type ParamsSettingsProps = { + selectedModel?: string; + showModelSelector?: boolean; + params?: ParamsSettingsFormProps; + model?: string; + modelList: Global.BaseOption[]; + onValuesChange?: (changeValues: any, value: Record) => void; + setParams: (params: any) => void; + globalParams?: ParamsSettingsFormProps; +}; + +const ParamsSettings: React.FC = ({ + selectedModel, + setParams, + globalParams, + onValuesChange, + modelList, + showModelSelector = true +}) => { + const intl = useIntl(); + const initialValues = { + top_n: 1 + }; + const [form] = Form.useForm(); + const formId = useId(); + + useEffect(() => { + if (showModelSelector) { + form.setFieldsValue({ + model: selectedModel || _.get(modelList, '[0].value'), + ...initialValues + }); + setParams({ + model: selectedModel || _.get(modelList, '[0].value'), + ...initialValues + }); + } else { + form.setFieldsValue({ + model: selectedModel || '', + ...initialValues + }); + setParams({ + model: selectedModel || '', + ...initialValues + }); + } + }, [modelList, showModelSelector, selectedModel]); + + const handleOnFinish = (values: any) => { + console.log('handleOnFinish', values); + }; + + const handleOnFinishFailed = (errorInfo: any) => { + console.log('handleOnFinishFailed', errorInfo); + }; + + const handleValuesChange = useCallback( + (changedValues: any, allValues: any) => { + setParams?.(allValues); + onValuesChange?.(changedValues, allValues); + }, + [onValuesChange, setParams] + ); + const handleFieldValueChange = useCallback( + (val: any, field: string) => { + const values = form.getFieldsValue(); + form.setFieldsValue({ + ...values, + [field]: val + }); + setParams({ + ...values, + [field]: val + }); + onValuesChange?.( + { [field]: val }, + { + ...values, + [field]: val + } + ); + }, + [form, setParams, onValuesChange] + ); + + useEffect(() => { + form.setFieldsValue(globalParams); + }, [globalParams]); + + const renderLabel = (args: { + field: string; + label: string; + description: string; + }) => { + return ( + + + {args.description ? ( + + {args.label} + + + + + ) : ( + {args.label} + )} + + + handleFieldValueChange(val, args.field)} + > + + ); + }; + + return ( +
+
+ { + <> + + name="model" + rules={[ + { + required: true, + message: intl.formatMessage( + { + id: 'common.form.rule.select' + }, + { name: intl.formatMessage({ id: 'playground.model' }) } + ) + } + ]} + > + + + + } + + name="top_n" + rules={[{ required: true }]} + > + + +
+
+ ); +}; + +export default memo(ParamsSettings); diff --git a/src/pages/playground/components/upload-file.tsx b/src/pages/playground/components/upload-file.tsx new file mode 100644 index 00000000..d5fc85f4 --- /dev/null +++ b/src/pages/playground/components/upload-file.tsx @@ -0,0 +1,118 @@ +import { readBlob, readExcelContent, readWordContent } from '@/utils'; +import { PaperClipOutlined } from '@ant-design/icons'; +import { useIntl } from '@umijs/max'; +import { Button, Tooltip, Upload } from 'antd'; +import type { UploadFile } from 'antd/es/upload'; +import { RcFile } from 'antd/es/upload'; +import { debounce } from 'lodash'; +import React, { useCallback, useRef } from 'react'; + +interface UploadImgProps { + size?: 'small' | 'middle' | 'large'; + accept?: string; + children?: React.ReactNode; + handleUpdateFileList: ( + fileList: { text: string; name: string; uid: number | string }[] + ) => void; +} + +const UploadImg: React.FC = ({ + handleUpdateFileList, + size = 'small', + accept = '.txt,.doc,.docx', + children +}) => { + const intl = useIntl(); + const uploadRef = useRef(null); + + const wordReg = /\.(doc|docx)$/; + const excelReg = /\.(xls|xlsx)$/; + + const getBase64 = useCallback((file: RcFile): Promise => { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.readAsDataURL(file); + reader.onload = () => resolve(reader.result as string); + reader.onerror = (error) => reject(error); + }); + }, []); + + const debouncedUpdate = useCallback( + debounce( + (files: { text: string; name: string; uid: number | string }[]) => { + handleUpdateFileList(files); + }, + 300 + ), + [handleUpdateFileList, intl] + ); + + const handleChange = useCallback( + async (info: any) => { + try { + const { fileList } = info; + + const newFileList = await Promise.all( + fileList.map(async (item: UploadFile) => { + if (wordReg.test(item.name)) { + const context = await readWordContent( + item.originFileObj as RcFile + ); + item.url = context; + } else if (excelReg.test(item.name)) { + const context = await readExcelContent( + item.originFileObj as RcFile + ); + item.url = context; + } else { + const context = await readBlob(item.originFileObj as RcFile); + item.url = context; + } + return item; + }) + ); + + if (newFileList.length > 0) { + const files = newFileList + .filter((sitem) => sitem.url) + .map((item: UploadFile) => { + return { + text: item.url as string, + name: item.name as string, + uid: item.uid + }; + }); + + debouncedUpdate(files); + } + } catch (error) { + // console.log('error', error); + } + }, + [debouncedUpdate, getBase64] + ); + + return ( + <> + false} + onChange={handleChange} + > + {children ? ( + children + ) : ( + + + diff --git a/src/pages/playground/config/types.ts b/src/pages/playground/config/types.ts index 5e6ef0a1..6a69db73 100644 --- a/src/pages/playground/config/types.ts +++ b/src/pages/playground/config/types.ts @@ -7,6 +7,7 @@ export interface ModelSelectionItem extends Global.BaseOption { export interface MessageItem { role: string; content: string; + title?: string; imgs?: { uid: string | number; dataUrl: string }[]; uid: number; } diff --git a/src/pages/playground/index.tsx b/src/pages/playground/index.tsx index 65f3ac7a..e159c03b 100644 --- a/src/pages/playground/index.tsx +++ b/src/pages/playground/index.tsx @@ -1,6 +1,10 @@ import IconFont from '@/components/icon-font'; import HotKeys from '@/config/hotkeys'; -import { MessageOutlined, OneToOneOutlined } from '@ant-design/icons'; +import { + FileSearchOutlined, + MessageOutlined, + OneToOneOutlined +} from '@ant-design/icons'; import { PageContainer } from '@ant-design/pro-components'; import { useIntl } from '@umijs/max'; import { Button, Segmented, Space, Tabs, TabsProps } from 'antd'; @@ -10,6 +14,7 @@ import { useCallback, useEffect, useRef, useState } from 'react'; import { useHotkeys } from 'react-hotkeys-hook'; import { queryModelsList } from './apis'; import GroundLeft from './components/ground-left'; +import GroundReranker from './components/ground-reranker'; import MultipleChat from './components/multiple-chat'; import './style/play-ground.less'; @@ -17,7 +22,11 @@ const Playground: React.FC = () => { const intl = useIntl(); const [activeKey, setActiveKey] = useState('chat'); const groundLeftRef = useRef(null); + const groundRerankerRef = useRef(null); const [modelList, setModelList] = useState[]>([]); + const [rerankerModelList, setRerankerModelList] = useState< + Global.BaseOption[] + >([]); const [loaded, setLoaded] = useState(false); const optionsList = [ { @@ -29,6 +38,11 @@ const Playground: React.FC = () => { label: intl.formatMessage({ id: 'menu.compare' }), value: 'compare', icon: + }, + { + label: 'Rerank', + value: 'reranker', + icon: } ]; @@ -37,8 +51,12 @@ const Playground: React.FC = () => { }, [groundLeftRef]); const handleToggleCollapse = useCallback(() => { + if (activeKey === 'reranker') { + groundRerankerRef.current?.setCollapse?.(); + return; + } groundLeftRef.current?.setCollapse?.(); - }, [groundLeftRef]); + }, [groundLeftRef, groundRerankerRef, activeKey]); const items: TabsProps['items'] = [ { @@ -52,6 +70,17 @@ const Playground: React.FC = () => { key: 'compare', label: 'Compare', children: + }, + { + key: 'reranker', + label: 'Reranker', + children: ( + + ) } ]; @@ -68,14 +97,43 @@ const Playground: React.FC = () => { label: item.id }; }) as Global.BaseOption[]; - setModelList(list); + return list; } catch (error) { console.error(error); - } finally { + return []; + } + }; + const getModelListByReranker = async () => { + try { + const params = { + reranker: true + }; + 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, rerankerModelList] = await Promise.all([ + getModelList(), + getModelListByReranker() + ]); + setModelList(modelist); + setRerankerModelList(rerankerModelList); + } catch (error) { setLoaded(true); } }; - getModelList(); + fetchData(); }, []); const renderExtra = () => { @@ -136,7 +194,7 @@ const Playground: React.FC = () => { extra={renderExtra()} className={classNames('playground-container', { compare: activeKey === 'compare', - chat: activeKey === 'chat' + chat: activeKey !== 'compare' })} >
diff --git a/src/pages/playground/style/content-item.less b/src/pages/playground/style/content-item.less index 81993696..3c08768b 100644 --- a/src/pages/playground/style/content-item.less +++ b/src/pages/playground/style/content-item.less @@ -31,7 +31,7 @@ &-content { word-break: break-word; - padding: 8px; + padding: 8px 14px; min-height: 38px; border-radius: var(--border-radius-mini); background-color: var(--ant-color-fill-tertiary); diff --git a/src/pages/playground/style/file-list.less b/src/pages/playground/style/file-list.less new file mode 100644 index 00000000..a261c2f5 --- /dev/null +++ b/src/pages/playground/style/file-list.less @@ -0,0 +1,38 @@ +.file-list { + display: flex; + flex-direction: column; + gap: 8px; + + .file-item { + display: flex; + align-items: center; + justify-content: space-between; + border-bottom: 1px solid var(--ant-color-split); + padding: 8px 14px; + padding-right: 6px; + cursor: pointer; + height: 41px; + transition: background-color 0.3s ease; + + .delete-btn { + display: none; + } + + &:hover { + background-color: var(--ant-color-fill-tertiary); + + .delete-btn { + display: block; + } + } + + &.ghost { + background-color: transparent; + } + + .title { + display: flex; + align-items: center; + } + } +} diff --git a/src/pages/playground/style/ground-left.less b/src/pages/playground/style/ground-left.less index 62cd94a7..130f1f44 100644 --- a/src/pages/playground/style/ground-left.less +++ b/src/pages/playground/style/ground-left.less @@ -2,6 +2,7 @@ display: flex; .params-wrapper { + position: relative; overflow-x: hidden; width: 390px; border-left: 1px solid var(--ant-color-split); @@ -50,4 +51,11 @@ } } } + + .ant-upload-wrapper .ant-upload-drag { + background-color: var(--color-fill-sider); + display: flex; + flex-direction: column; + justify-content: center; + } } diff --git a/src/pages/playground/style/input-list.less b/src/pages/playground/style/input-list.less new file mode 100644 index 00000000..4459c9d6 --- /dev/null +++ b/src/pages/playground/style/input-list.less @@ -0,0 +1,39 @@ +.input-list { + display: flex; + flex-direction: column; + gap: 8px; + + .input-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0; + cursor: pointer; + transition: background-color 0.3s ease; + border-bottom: 1px solid var(--ant-color-split); + + &:focus-within { + background-color: transparent; + } + + .input-wrap { + flex: 1; + } + + .btn-group { + width: 60px; + margin-left: 8px; + display: flex; + gap: 8px; + } + + &:hover { + background-color: var(--ant-color-fill-tertiary); + + .btn-group { + display: flex; + gap: 8px; + } + } + } +} diff --git a/src/pages/playground/style/rerank-message.less b/src/pages/playground/style/rerank-message.less new file mode 100644 index 00000000..d8bc605d --- /dev/null +++ b/src/pages/playground/style/rerank-message.less @@ -0,0 +1,45 @@ +.rerank-message { + display: flex; + flex-direction: column; + + .rank { + margin-right: 10px; + max-width: 75px; + min-width: 55px; + display: flex; + align-items: center; + justify-content: space-between; + + .score { + margin-left: 5px; + color: var(--ant-rate-star-color); + } + } + + .result { + display: flex; + flex-direction: column; + gap: 10px; + } + + .content-item-text { + margin-bottom: 0; + display: flex; + align-items: center; + } + + .text { + flex: 1; + width: 300px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + margin-bottom: 0; + } + + .doc-name { + margin-bottom: 0; + font-weight: var(--font-weight-bold); + font-style: italic; + } +} diff --git a/src/utils/index.ts b/src/utils/index.ts index 3e1d97da..3ea136e6 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,4 +1,7 @@ import _ from 'lodash'; +import mammoth from 'mammoth'; +import XLSX from 'xlsx'; + export const isNotEmptyValue = (value: any) => { if (Array.isArray(value)) { return value.length > 0; @@ -183,3 +186,45 @@ export function loadLanguageConfig(language: string) { return languageConfig; } + +export function readBlob(blob: Blob): Promise { + return new Promise((resolve) => { + const reader = new FileReader(); + reader.onload = function (e: any) { + resolve(e.target.result); + }; + reader.readAsText(blob, 'utf-8'); + }); +} + +export function readWordContent(file: File): Promise { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = function (e: any) { + const arrayBuffer = e.target.result; + mammoth + .extractRawText({ arrayBuffer }) + .then((result) => { + resolve(result.value); + }) + .catch((error) => reject(error)); + }; + reader.onerror = (error) => reject(error); + reader.readAsArrayBuffer(file); + }); +} + +export function readExcelContent(file: File): Promise { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = function (e: any) { + const arrayBuffer = e.target.result; + const workbook = XLSX.read(arrayBuffer, { type: 'string' }); + const ws = workbook.Sheets[workbook.SheetNames[0]]; // get the first worksheet + const data = XLSX.utils.sheet_to_json(ws); + resolve(JSON.stringify(data)); + }; + reader.onerror = (error) => reject(error); + reader.readAsArrayBuffer(file); + }); +}