diff --git a/src/components/logs-viewer/index.tsx b/src/components/logs-viewer/index.tsx index ee05ce86..517bbcb5 100644 --- a/src/components/logs-viewer/index.tsx +++ b/src/components/logs-viewer/index.tsx @@ -4,7 +4,14 @@ import { Terminal } from '@xterm/xterm'; import '@xterm/xterm/css/xterm.css'; import classNames from 'classnames'; import _ from 'lodash'; -import { memo, useCallback, useEffect, useRef, useState } from 'react'; +import { + memo, + useCallback, + useEffect, + useLayoutEffect, + useRef, + useState +} from 'react'; import './index.less'; import useSize from './use-size'; @@ -22,7 +29,7 @@ const LogsViewer: React.FC = (props) => { const termRef = useRef({}); const termwrapRef = useRef({}); const fitAddonRef = useRef({}); - const cacheDataRef = useRef(null); + const cacheDataRef = useRef(''); const [logs, setLogs] = useState(''); const size = useSize(scroller); @@ -99,7 +106,7 @@ const LogsViewer: React.FC = (props) => { }; }, [termwrapRef.current]); - useEffect(() => { + useLayoutEffect(() => { if (size) { handleResize(); } diff --git a/src/components/logs-viewer/old.less b/src/components/logs-viewer/old.less new file mode 100644 index 00000000..d54d3d72 --- /dev/null +++ b/src/components/logs-viewer/old.less @@ -0,0 +1,22 @@ +.logs-viewer-wrap-w2 { + .wrap { + padding: 5px 0 5px 10px; + overflow: auto; + background-color: var(--color-logs-bg); + border-radius: var(--border-radius-mini); + + .content { + word-wrap: break-word; + + &.line-break { + word-wrap: break-word; + } + + color: var(--color-logs-text); + font-size: var(--font-size-small); + line-height: 22px; + white-space: pre-wrap; + background-color: var(--color-logs-bg); + } + } +} diff --git a/src/components/logs-viewer/old.tsx b/src/components/logs-viewer/old.tsx new file mode 100644 index 00000000..76d10076 --- /dev/null +++ b/src/components/logs-viewer/old.tsx @@ -0,0 +1,159 @@ +import useSetChunkFetch from '@/hooks/use-chunk-fetch'; +import useSetChunkRequest from '@/hooks/use-chunk-request'; +import useContainerScroll from '@/hooks/use-container-scorll'; +import Convert from 'ansi-to-html'; +import classNames from 'classnames'; +import { memo, useCallback, useEffect, useRef, useState } from 'react'; +import './old.less'; + +interface LogsViewerProps { + height: number; + content?: string; + url: string; + params?: object; +} +const LogsViewer: React.FC = (props) => { + const { height, content, url } = props; + const [nowrap, setNowrap] = useState(false); + const [logsContent, setLogsContent] = useState([]); + const { setChunkRequest } = useSetChunkRequest(); + const { setChunkFetch } = useSetChunkFetch(); + const chunkRequedtRef = useRef(null); + const scroller = useRef(null); + const cacheDataRef = useRef(''); + const { updateScrollerPosition, handleContentWheel } = useContainerScroll( + scroller, + { toBottom: true } + ); + + const convert = new Convert({ + newline: true, + escapeXML: true, + stream: false + }); + + useEffect(() => { + updateScrollerPosition(); + }, [logsContent]); + + const ansiEscapeRegex = /(\x1B\[A)+$/; + + const endsWithAnsiEscapeSequence = useCallback((str: string) => { + return ansiEscapeRegex.test(str); + }, []); + const getCursorUpLines = useCallback((str: string) => { + const matches = str.match(/(?:\x1B\[A)+$/); + + return matches ? matches[0].length / 3 : 0; + }, []); + + const removeDot = useCallback((str: string) => { + return str.replace(/^\(.*?\)/, ''); + }, []); + const replaceAnsiEscapeSequence = useCallback((str: string) => { + const res = str.replace(ansiEscapeRegex, ''); + return removeDot(res); + }, []); + + const handleRControl = useCallback((str: string) => { + if (str.includes('\r')) { + const parts = str.split('\r'); + const lastLine = parts[parts.length - 1]; + return lastLine; + } + return str; + }, []); + + const parseHtmlStr = useCallback((logStr: string) => { + cacheDataRef.current += logStr.replace('\r\n', '\n'); + console.log('data>>>>>>>>>>>', { + data: logStr, + cacheDataRef: cacheDataRef.current + }); + const result: string[] = []; + const lines = cacheDataRef.current + .split('\n') + .filter((line: string) => line.trim() !== ''); + // const lines = text; + lines.forEach((line: string, index: number) => { + const upCount = getCursorUpLines(line); + console.log('line=========1', { + line, + upCount, + result + }); + if (endsWithAnsiEscapeSequence(line)) { + const newLine = handleRControl(line); + const val = removeDot(newLine); + if (result.length < upCount) { + result.push(''); + } + if (upCount) { + console.log('line=========0', { + line, + upCount, + result + }); + const placeIndex = result.length - upCount; + result[placeIndex] = replaceAnsiEscapeSequence(val); + } else { + result.push(val); + } + } else { + const val = handleRControl(line); + result.push(val); + } + }); + return result.map((item) => { + return convert.toHtml(item); + }); + }, []); + + const updateContent = (newVal: string) => { + const list = parseHtmlStr(newVal); + setLogsContent(list); + }; + + const createChunkConnection = async () => { + chunkRequedtRef.current?.current?.abort?.(); + chunkRequedtRef.current = setChunkFetch({ + url, + params: { + ...props.params, + watch: true + }, + contentType: 'text', + handler: updateContent + }); + }; + + useEffect(() => { + createChunkConnection(); + return () => { + chunkRequedtRef.current?.current?.abort?.(); + }; + }, [url, props.params]); + + return ( +
+
+
+
+ {logsContent.map((item, index) => { + return ( +
+ ); + })} +
+
+
+
+ ); +}; + +export default memo(LogsViewer); diff --git a/src/components/logs-viewer/test.ts b/src/components/logs-viewer/test.ts new file mode 100644 index 00000000..22ebaa60 --- /dev/null +++ b/src/components/logs-viewer/test.ts @@ -0,0 +1,186 @@ +export default [ + '2024-09-11T20:09:42+08:00 - gpustack.worker.downloaders - INFO - Downloading model leafspark/Reflection-Llama-3.1-70B-GGUF/Reflection-Llama-3.1-70B.fp16*.gguf', + '\rFetching 4 files: 0%| | 0/4 [00:00 = ({ 'list_item', 'br', 'html', - 'escape' + 'escape', + 'del', + 'blockquote', + 'checkbox' ]; - renderer.link = ({ href, title, text }) => { - return `${text}`; - }; + // renderer.link = ({ href, title, text }) => { + // return `${text}`; + // }; const isValidURL = useCallback((url: string) => { const pattern = /^(https?:\/\/|\/\/)([^\s/$.?#].[^\s]*)$/; @@ -101,6 +104,18 @@ const MarkdownViewer: React.FC = ({ htmlstr =
  • {text}
  • ; } + if (token.type === 'del') { + htmlstr = {text}; + } + + if (token.type === 'blockquote') { + htmlstr =
    {text}
    ; + } + + if (token.type === 'checkbox') { + htmlstr = ; + } + if (token.type === 'br') { htmlstr =
    ; } diff --git a/src/components/seal-table/index.tsx b/src/components/seal-table/index.tsx index b4097ca5..ba9ae19e 100644 --- a/src/components/seal-table/index.tsx +++ b/src/components/seal-table/index.tsx @@ -46,7 +46,10 @@ const SealTable: React.FC = ( if (selectedRowKeys?.length === 0) { setSelectAll(false); setIndeterminate(false); - } else if (selectedRowKeys?.length === props.dataSource.length) { + } else if ( + selectedRowKeys?.length === props.dataSource.length && + selectedRowKeys.length > 0 + ) { setSelectAll(true); setIndeterminate(false); } else { @@ -54,7 +57,7 @@ const SealTable: React.FC = ( setIndeterminate(true); } } - }, [rowSelection]); + }, [rowSelection, props.dataSource]); const handleSelectAllChange = (e: any) => { if (e.target.checked) { diff --git a/src/hooks/use-chunk-fetch.ts b/src/hooks/use-chunk-fetch.ts new file mode 100644 index 00000000..bf1c219f --- /dev/null +++ b/src/hooks/use-chunk-fetch.ts @@ -0,0 +1,92 @@ +import qs from 'query-string'; +import { useEffect, useRef } from 'react'; + +interface RequestConfig { + url: string; + handler: (data: any) => any; + beforeReconnect?: () => void; + params?: object; + contentType?: 'json' | 'text'; +} + +const useSetChunkFetch = () => { + const axiosToken = useRef(null); + const requestConfig = useRef({}); + + const readTextEventStreamData = async ( + reader: any, + decoder: TextDecoder, + callback: (data: any) => void + ) => { + const { done, value } = await reader.read(); + + if (done) { + return; + } + + let chunk = decoder.decode(value, { stream: true }); + callback(chunk); + await readTextEventStreamData(reader, decoder, callback); + }; + + const fetchChunkRequest = async ({ + url, + handler, + params = {} + }: RequestConfig) => { + axiosToken.current?.abort?.(); + axiosToken.current = new AbortController(); + try { + const response = await fetch( + `v1${url}?${qs.stringify({ + ...params, + watch: true + })}`, + { + method: 'GET', + body: null, + headers: { + 'Content-Type': 'application/octet-stream' + }, + signal: axiosToken.current.signal + } + ); + console.log('response============', response); + if (!response.ok) { + return; + } + const reader = response?.body?.getReader(); + const decoder = new TextDecoder('utf-8'); + await readTextEventStreamData(reader, decoder, handler); + } catch (error) { + // handle error + } + + return axiosToken.current; + }; + + const setChunkFetch = (config: RequestConfig) => { + requestConfig.current = { ...config }; + fetchChunkRequest(requestConfig.current); + return axiosToken; + }; + + useEffect(() => { + const handleUnload = () => { + axiosToken.current?.abort?.(); + }; + + window.addEventListener('beforeunload', handleUnload); + + return () => { + axiosToken.current?.abort?.(); + window.removeEventListener('beforeunload', handleUnload); + }; + }, []); + + return { + setChunkFetch + }; +}; + +export default useSetChunkFetch; diff --git a/src/hooks/use-chunk-request.ts b/src/hooks/use-chunk-request.ts index 52dcf53b..3a515182 100644 --- a/src/hooks/use-chunk-request.ts +++ b/src/hooks/use-chunk-request.ts @@ -172,7 +172,7 @@ const useSetChunkRequest = () => { let result = response; let cres = ''; - console.log('chunkrequest============e==', e); + console.log('chunkrequest============e==', result); if (contentType === 'json') { const currentRes = sliceData(response, e.loaded, loadedSize); result = parseData(currentRes); @@ -194,7 +194,6 @@ const useSetChunkRequest = () => { retryCount.current -= 1; } } catch (error) { - console.log('error=============', error); if (!axios.isCancel(error)) { setRequestReadyState(4); if (retryCount.current > 0) { diff --git a/src/hooks/use-table-row-selection.ts b/src/hooks/use-table-row-selection.ts index 8ef3d9cc..9276c84e 100644 --- a/src/hooks/use-table-row-selection.ts +++ b/src/hooks/use-table-row-selection.ts @@ -11,10 +11,15 @@ export default function useTableRowSelection() { setSelectedRowKeys([]); }; + const removeSelectedKey = (rowKey: React.Key) => { + setSelectedRowKeys((keys) => keys.filter((key) => key !== rowKey)); + }; + const rowSelection = { selectedRowKeys, clearSelections, - onChange: onSelectChange + onChange: onSelectChange, + removeSelectedKey }; return rowSelection; diff --git a/src/pages/llmodels/components/table-list.tsx b/src/pages/llmodels/components/table-list.tsx index 44fd1ade..e5a300c6 100644 --- a/src/pages/llmodels/components/table-list.tsx +++ b/src/pages/llmodels/components/table-list.tsx @@ -311,6 +311,7 @@ const Models: React.FC = ({ async onOk() { await deleteModel(row.id); updateExpandedRowKeys([row.id]); + rowSelection.removeSelectedKey(row.id); } }); }; diff --git a/src/pages/llmodels/components/view-logs-modal.tsx b/src/pages/llmodels/components/view-logs-modal.tsx index 58403729..a59ae5a6 100644 --- a/src/pages/llmodels/components/view-logs-modal.tsx +++ b/src/pages/llmodels/components/view-logs-modal.tsx @@ -1,4 +1,4 @@ -import LogsViewer from '@/components/logs-viewer'; +import LogsViewer from '@/components/logs-viewer/index'; import { useIntl } from '@umijs/max'; import { Modal } from 'antd'; import React, { useEffect, useState } from 'react'; diff --git a/src/pages/playground/components/multiple-chat/content-item.tsx b/src/pages/playground/components/multiple-chat/content-item.tsx index f1a108b3..6ed0c8b1 100644 --- a/src/pages/playground/components/multiple-chat/content-item.tsx +++ b/src/pages/playground/components/multiple-chat/content-item.tsx @@ -58,7 +58,6 @@ const ContentItem: React.FC = ({ for (let i = 0; i < items.length; i++) { let item = items[i]; - console.log('item===========', item); if (item.kind === 'file' && item.type.indexOf('image') !== -1) { const file = item.getAsFile(); @@ -102,7 +101,7 @@ const ContentItem: React.FC = ({ console.error('Error processing images:', error); } }, - [data] + [data, updateMessage] ); const handleOnPaste = (e: any) => { @@ -131,7 +130,7 @@ const ContentItem: React.FC = ({ imgs: [...(data.imgs || []), ...list] }); }, - [data] + [data, updateMessage] ); const handleDeleteImg = (uid: number | string) => {