diff --git a/src/atoms/models.ts b/src/atoms/models.ts new file mode 100644 index 00000000..8c1ad242 --- /dev/null +++ b/src/atoms/models.ts @@ -0,0 +1,4 @@ +import { atom } from 'jotai'; + +// models expand keys: create, update , delete, +export const modelsExpandKeysAtom = atom([]); diff --git a/src/components/auto-image/index.tsx b/src/components/auto-image/index.tsx index 06750c87..b209a5e3 100644 --- a/src/components/auto-image/index.tsx +++ b/src/components/auto-image/index.tsx @@ -71,6 +71,9 @@ const AutoImage: React.FC< }, [props.onLoad]); const handleOnError = () => { + console.log('error===img=========', { + src: props.src + }); setIsError(true); }; diff --git a/src/components/image-editor/index.tsx b/src/components/image-editor/index.tsx index 3e3a0f28..573126f2 100644 --- a/src/components/image-editor/index.tsx +++ b/src/components/image-editor/index.tsx @@ -496,6 +496,11 @@ const CanvasImageEditor: React.FC = ({ overlayCtx!.resetTransform(); }, []); + const updateCursorSize = () => { + cursorRef.current!.style.width = `${lineWidth * autoScale.current}px`; + cursorRef.current!.style.height = `${lineWidth * autoScale.current}px`; + }; + const initializeImage = useCallback(async () => { if (imguid === preImguid.current) { return; @@ -518,6 +523,7 @@ const CanvasImageEditor: React.FC = ({ if (strokesRef.current.length) { redrawStrokes(strokesRef.current); } + updateCursorSize(); }, [drawImage, onReset, redrawStrokes, imguid]); const updateZoom = (scaleChange: number, mouseX: number, mouseY: number) => { @@ -557,11 +563,6 @@ const CanvasImageEditor: React.FC = ({ canvasRef.current!.style.transform = `scale(${autoScale.current})`; }; - const updateCursorSize = () => { - cursorRef.current!.style.width = `${lineWidth * autoScale.current}px`; - cursorRef.current!.style.height = `${lineWidth * autoScale.current}px`; - }; - const handleOnWheel = (event: any) => { handleZoom(event); updateCursorSize(); diff --git a/src/hooks/use-expanded-row-keys.ts b/src/hooks/use-expanded-row-keys.ts index 2ea1b66f..e92c960c 100644 --- a/src/hooks/use-expanded-row-keys.ts +++ b/src/hooks/use-expanded-row-keys.ts @@ -1,7 +1,8 @@ import { useCallback, useState } from 'react'; -export default function useExpandedRowKeys() { - const [expandedRowKeys, setExpandedRowKeys] = useState([]); +export default function useExpandedRowKeys(defaultKeys: React.Key[] = []) { + const [expandedRowKeys, setExpandedRowKeys] = + useState(defaultKeys); const handleExpandChange = useCallback( (expanded: boolean, record: any, rowKey: any) => { diff --git a/src/locales/zh-CN/menu.ts b/src/locales/zh-CN/menu.ts index 7ac59d79..57492aae 100644 --- a/src/locales/zh-CN/menu.ts +++ b/src/locales/zh-CN/menu.ts @@ -5,7 +5,7 @@ export default { 'menu.playground.embedding': '文本嵌入', 'menu.playground.chat': '对话', 'menu.playground.speech': '语音', - 'menu.playground.text2images': '文生图', + 'menu.playground.text2images': '图像', 'menu.compare': '多模型对比', 'menu.models': '模型', 'menu.models.modelList': '部署与管理', diff --git a/src/locales/zh-CN/playground.ts b/src/locales/zh-CN/playground.ts index 8bb21b99..1e824a73 100644 --- a/src/locales/zh-CN/playground.ts +++ b/src/locales/zh-CN/playground.ts @@ -4,7 +4,7 @@ export default { 'playground.system': '系统', 'playground.systemMessage': '系统消息', 'playground.user': '用户', - 'playground.assistant': '小助手', + 'playground.assistant': 'AI助手', 'playground.newMessage': '新消息', 'playground.viewcode': '查看代码', 'playground.model': '模型', diff --git a/src/pages/llmodels/catalog.tsx b/src/pages/llmodels/catalog.tsx index 7063414f..ac99fb9c 100644 --- a/src/pages/llmodels/catalog.tsx +++ b/src/pages/llmodels/catalog.tsx @@ -1,9 +1,11 @@ +import { modelsExpandKeysAtom } from '@/atoms/models'; import PageTools from '@/components/page-tools'; import { PageAction } from '@/config'; import { SyncOutlined } from '@ant-design/icons'; import { PageContainer } from '@ant-design/pro-components'; import { useIntl, useNavigate } from '@umijs/max'; import { Button, Input, Pagination, Select, Space, message } from 'antd'; +import { useAtom } from 'jotai'; import _ from 'lodash'; import React, { useCallback, useEffect, useState } from 'react'; import { createModel, queryCatalogList } from './apis'; @@ -38,6 +40,7 @@ const Catalog: React.FC = () => { current: {}, source: modelSourceMap.huggingface_value }); + const [modelsExpandKeys, setModelsExpandKeys] = useAtom(modelsExpandKeysAtom); const cacheData = React.useRef([]); const categoryOptions = [...modelCategories.filter((item) => item.value)]; @@ -53,7 +56,7 @@ const Catalog: React.FC = () => { ); } if (search) { - return _.toLower(item.name).includes(search); + return _.toLower(item.name).includes(_.toLower(search)); } if (categories.length > 0) { return categories.some((category) => @@ -134,6 +137,7 @@ const Catalog: React.FC = () => { show: false }); message.success(intl.formatMessage({ id: 'common.message.success' })); + setModelsExpandKeys([modelData.id]); navigate('/models/list'); } catch (error) {} }, diff --git a/src/pages/llmodels/components/table-list.tsx b/src/pages/llmodels/components/table-list.tsx index fcae76f4..e8164179 100644 --- a/src/pages/llmodels/components/table-list.tsx +++ b/src/pages/llmodels/components/table-list.tsx @@ -1,3 +1,4 @@ +import { modelsExpandKeysAtom } from '@/atoms/models'; import AutoTooltip from '@/components/auto-tooltip'; import DeleteModal from '@/components/delete-modal'; import DropdownButtons from '@/components/drop-down-buttons'; @@ -30,6 +31,7 @@ import { PageContainer } from '@ant-design/pro-components'; import { Access, useAccess, useIntl, useNavigate } from '@umijs/max'; import { Button, Dropdown, Input, Select, Space, Tag, message } from 'antd'; import dayjs from 'dayjs'; +import { useAtom } from 'jotai'; import _ from 'lodash'; import { memo, useCallback, useEffect, useRef, useState } from 'react'; import { useHotkeys } from 'react-hotkeys-hook'; @@ -125,6 +127,7 @@ const Models: React.FC = ({ loading, total }) => { + const [expandAtom] = useAtom(modelsExpandKeysAtom); const access = useAccess(); const intl = useIntl(); const navigate = useNavigate(); @@ -134,7 +137,7 @@ const Models: React.FC = ({ updateExpandedRowKeys, removeExpandedRowKey, expandedRowKeys - } = useExpandedRowKeys(); + } = useExpandedRowKeys(expandAtom); const { sortOrder, setSortOrder } = useTableSort({ defaultSortOrder: 'descend' }); @@ -309,6 +312,7 @@ const Models: React.FC = ({ } }); message.success(intl.formatMessage({ id: 'common.message.success' })); + updateExpandedRowKeys([row.id, ...expandedRowKeys]); } catch (error) { // ingore } @@ -329,6 +333,7 @@ const Models: React.FC = ({ replicas: 0 } }); + removeExpandedRowKey([row.id]); } catch (error) { // ingore } diff --git a/src/pages/playground/components/ground-images.tsx b/src/pages/playground/components/ground-images.tsx index 2e81a4a6..1ade5daa 100644 --- a/src/pages/playground/components/ground-images.tsx +++ b/src/pages/playground/components/ground-images.tsx @@ -352,6 +352,7 @@ const GroundImages: React.FC = forwardRef((props, ref) => { if (item.b64_json && stream_options.chunk_results) { imgItem.dataUrl += item.b64_json; } else if (item.b64_json) { + console.log('item.b64_json:', item.b64_json, item.index); imgItem.dataUrl = `data:image/png;base64,${item.b64_json}`; } const progress = _.round(item.progress, 0); diff --git a/src/utils/fetch-chunk-data.ts b/src/utils/fetch-chunk-data.ts index 2e17b31e..346de4da 100644 --- a/src/utils/fetch-chunk-data.ts +++ b/src/utils/fetch-chunk-data.ts @@ -199,20 +199,32 @@ export const readStreamData = async ( }; // Process the remainder of the buffer -const processBuffer = (buffer: string, callback: (data: any) => void) => { +const processBuffer = async (buffer: string, callback: (data: any) => void) => { + if (!buffer) return; + const lines = buffer.split('\n'); for (const line of lines) { - if (line.startsWith('data: ')) { - const jsonStr = line.slice(6).trim(); + const trimmedLine = line.trim(); + + if (trimmedLine.startsWith('data: ')) { + const jsonStr = trimmedLine.slice(6).trim(); try { - const jsonData = JSON.parse(jsonStr); - callback(jsonData); + if (jsonStr !== '[DONE]') { + console.log('jsonStr>>>>>>>>>>>>>done:', jsonStr); + const jsonData = JSON.parse(jsonStr); + callback(jsonData); + } } catch (e) { - console.error( - 'Failed to parse JSON from remaining buffer:', - jsonStr, - e - ); + console.error('Failed to parse JSON from line:', jsonStr, e); + } + } else if (trimmedLine.startsWith('error:')) { + const errorStr = trimmedLine.slice(7).trim(); + console.log('jsonStr>>>>>>>>>>>>>error:', errorStr); + try { + const jsonData = JSON.parse(errorStr); + callback({ error: jsonData }); + } catch (e) { + console.error('Failed to parse error JSON from line:', errorStr, e); } } } @@ -221,47 +233,89 @@ const processBuffer = (buffer: string, callback: (data: any) => void) => { export const readLargeStreamData = async ( reader: any, decoder: TextDecoder, - callback: (data: any) => void + callback: (data: any) => void, + throttleDelay = 200 ) => { let buffer = ''; // cache incomplete line + class BufferManager { + private buffer: any[] = []; + private failed: boolean = false; + private isFlushing: boolean = false; + private callback: (data: any) => void; + + constructor(callback: (data: any) => void) { + this.callback = callback; + } + + public add(data: any) { + this.buffer.push(data); + } + + public async flush() { + if (this.buffer.length === 0 || this.isFlushing) { + return; + } + this.failed = false; + this.isFlushing = true; + + while (this.buffer.length > 0) { + const data = this.buffer.shift(); + + try { + processBuffer(data, this.callback); + } catch (error) { + console.error('Error processing buffer:', error); + this.failed = true; + this.buffer.unshift(data); + break; + } + } + this.isFlushing = false; + } + + public getBuffer() { + return this.buffer; + } + } + + const bufferManager = new BufferManager(callback); + + const throttledCallback = throttle(async () => { + bufferManager.flush(); + }, throttleDelay); + + let isReading = true; + while (true) { const { done, value } = await reader?.read?.(); if (done) { + isReading = false; // Process remaining buffered data - if (buffer.trim()) { - processBuffer(buffer, callback); + if (buffer) { + bufferManager.add(buffer); } + bufferManager.flush(); break; } - // Decode new chunk of data and append to buffer - buffer += decoder.decode(value, { stream: true }); - - // Try to process the complete line in the buffer - const lines = buffer.split('\n'); - buffer = lines.pop() || ''; // Keep last line (may be incomplete) + try { + // Decode new chunk of data and append to buffer + buffer += decoder.decode(value, { stream: true }); - for (const line of lines) { - if (line.startsWith('data: ')) { - const jsonStr = line.slice(6).trim(); + // Try to process the complete line in the buffer + const lines = buffer.split('\n'); + buffer = lines.pop() || ''; // Keep last line (may be incomplete) - try { - if (jsonStr !== '[DONE]') { - const jsonData = JSON.parse(jsonStr); - callback(jsonData); - } - } catch (e) { - console.error('Failed to parse JSON:', jsonStr, e); - } + for (const line of lines) { + bufferManager.add(line); } - if (line.startsWith('error:')) { - const errorStr = line.slice(7).trim(); - const jsonData = JSON.parse(errorStr); - callback({ error: jsonData }); - } + throttledCallback(); + } catch (error) { + console.log('Error reading stream data:', error); + // do nothing } } }; diff --git a/src/utils/index.ts b/src/utils/index.ts index ed1daa50..17b31df2 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -149,35 +149,46 @@ export const generateRandomNumber = () => { }; function base64ToBlob(base64: string, contentType = '', sliceSize = 512) { - const base64Content = base64.replace(/^data:image\/(png|jpg);base64,/, ''); - const byteCharacters = atob(base64Content); - const byteArrays = []; + try { + const base64Content = base64.replace(/^data:image\/(png|jpg);base64,/, ''); + const byteCharacters = atob(base64Content); + const byteArrays = []; - for (let offset = 0; offset < byteCharacters.length; offset += sliceSize) { - const slice = byteCharacters.slice(offset, offset + sliceSize); + for (let offset = 0; offset < byteCharacters.length; offset += sliceSize) { + const slice = byteCharacters.slice(offset, offset + sliceSize); - const byteNumbers = new Array(slice.length); - for (let i = 0; i < slice.length; i++) { - byteNumbers[i] = slice.charCodeAt(i); + const byteNumbers = new Array(slice.length); + for (let i = 0; i < slice.length; i++) { + byteNumbers[i] = slice.charCodeAt(i); + } + + const byteArray = new Uint8Array(byteNumbers); + byteArrays.push(byteArray); } - const byteArray = new Uint8Array(byteNumbers); - byteArrays.push(byteArray); + return new Blob(byteArrays, { type: contentType }); + } catch (error) { + return null; } - - return new Blob(byteArrays, { type: contentType }); } export const base64ToFile = (base64String: string, fileName: string) => { - if (!base64String) { + try { + if (!base64String) { + return null; + } + console.log('base64String:', base64String); + const match = base64String.match(/data:(.*?);base64,/); + if (!match) { + throw new Error('Invalid base64 string'); + } + const contentType = match[1]; + const blob = base64ToBlob(base64String, contentType); + if (!blob) { + throw new Error('Failed to convert base64 to blob'); + } + return new File([blob], fileName || contentType, { type: contentType }); + } catch (error) { return null; } - console.log('base64String:', base64String); - const match = base64String.match(/data:(.*?);base64,/); - if (!match) { - throw new Error('Invalid base64 string'); - } - const contentType = match[1]; - const blob = base64ToBlob(base64String, contentType); - return new File([blob], fileName || contentType, { type: contentType }); };