From 39a71d51cf994fda95995817134558aa9f596e62 Mon Sep 17 00:00:00 2001 From: jialin Date: Mon, 6 Jan 2025 20:44:34 +0800 Subject: [PATCH] fix: image edit ux --- config/proxy.ts | 1 + src/components/image-editor/index.less | 1 + src/components/image-editor/index.tsx | 109 +++++++++++++----- src/hooks/use-update-chunk-list.ts | 14 ++- src/locales/en-US/models.ts | 2 +- src/pages/api-keys/index.tsx | 3 - src/pages/llmodels/catalog.tsx | 5 +- .../components/deploy-builtin-modal.tsx | 34 +++++- src/pages/llmodels/components/table-list.tsx | 7 +- src/pages/llmodels/index.tsx | 32 ++--- .../playground/components/image-edit.tsx | 26 +++-- src/utils/fetch-chunk-data.ts | 4 +- 12 files changed, 165 insertions(+), 73 deletions(-) diff --git a/config/proxy.ts b/config/proxy.ts index 6d13e9e0..41048ffa 100644 --- a/config/proxy.ts +++ b/config/proxy.ts @@ -18,6 +18,7 @@ export default function createProxyTable(target?: string) { changeOrigin: true, secure: false, ws: true, + log: 'debug', pathRewrite: (pth: string) => pth.replace(`/^/${api}`, `/${api}`), // onProxyRes: (proxyRes: any, req: any, res: any) => { // console.log('headers=========', { diff --git a/src/components/image-editor/index.less b/src/components/image-editor/index.less index 9f97046a..ca41eea9 100644 --- a/src/components/image-editor/index.less +++ b/src/components/image-editor/index.less @@ -14,5 +14,6 @@ display: flex; justify-content: center; align-items: center; + overflow: hidden; } } diff --git a/src/components/image-editor/index.tsx b/src/components/image-editor/index.tsx index 3f0a681d..f23cab8a 100644 --- a/src/components/image-editor/index.tsx +++ b/src/components/image-editor/index.tsx @@ -11,7 +11,7 @@ import React, { useCallback, useEffect, useRef, useState } from 'react'; import IconFont from '../icon-font'; import './index.less'; -type Point = { x: number; y: number }; +type Point = { x: number; y: number; lineWidth: number }; type Stroke = Point[]; type CanvasImageEditorProps = { @@ -48,6 +48,13 @@ const CanvasImageEditor: React.FC = ({ const cursorRef = useRef(null); const [imgLoaded, setImgLoaded] = useState(false); + let scale = 1; + let offsetX = 0; + let offsetY = 0; + + const MIN_SCALE = 0.5; + const MAX_SCALE = 5; + const getTransformedPoint = (event: React.MouseEvent) => { const overlayCanvas = overlayCanvasRef.current!; const rect = overlayCanvas.getBoundingClientRect(); @@ -187,13 +194,12 @@ const CanvasImageEditor: React.FC = ({ ctx: CanvasRenderingContext2D, stroke: Stroke | Point[], options: { - lineWidth: number; + lineWidth?: number; color: string; compositeOperation: 'source-over' | 'destination-out'; } ) => { - const { lineWidth, color, compositeOperation } = options; - ctx.lineWidth = lineWidth; + const { color, compositeOperation } = options; ctx.lineCap = 'round'; ctx.lineJoin = 'round'; ctx.globalCompositeOperation = compositeOperation; @@ -201,6 +207,7 @@ const CanvasImageEditor: React.FC = ({ ctx.beginPath(); stroke.forEach((point, i) => { + ctx.lineWidth = point.lineWidth; if (i === 0) { ctx.moveTo(point.x, point.y); } else { @@ -248,7 +255,8 @@ const CanvasImageEditor: React.FC = ({ console.log('Drawing:', e.nativeEvent, { x, y }); currentStroke.current.push({ x, - y + y, + lineWidth }); const ctx = overlayCanvasRef.current!.getContext('2d'); @@ -257,12 +265,12 @@ const CanvasImageEditor: React.FC = ({ drawLine( ctx!, - { x, y }, + { x, y, lineWidth }, { lineWidth, color: COLOR, compositeOperation: 'destination-out' } ); drawLine( ctx!, - { x, y }, + { x, y, lineWidth }, { lineWidth, color: COLOR, compositeOperation: 'source-over' } ); @@ -276,7 +284,8 @@ const CanvasImageEditor: React.FC = ({ const { x, y } = getTransformedPoint(e); currentStroke.current.push({ x, - y + y, + lineWidth }); const ctx = overlayCanvasRef.current!.getContext('2d'); @@ -367,20 +376,18 @@ const CanvasImageEditor: React.FC = ({ strokes?.forEach((stroke: Point[], index) => { overlayCtx.save(); drawStroke(overlayCtx, stroke, { - lineWidth, color: COLOR, compositeOperation: 'destination-out' }); drawStroke(overlayCtx, stroke, { - lineWidth, color: COLOR, compositeOperation: 'source-over' }); overlayCtx.restore(); }); }, - [lineWidth, drawStroke] + [drawStroke] ); const undo = () => { @@ -408,7 +415,8 @@ const CanvasImageEditor: React.FC = ({ return stroke.map((point) => { return { x: point.x * scale, - y: point.y * scale + y: point.y * scale, + lineWidth: point.lineWidth }; }); }); @@ -466,7 +474,6 @@ const CanvasImageEditor: React.FC = ({ const contentRect = entries[0].contentRect; if (!contentRect.width || !contentRect.height || !imgLoaded) return; await drawImage(); - console.log('Image Loaded:', imageStatus, strokesRef.current); if (imageStatus.isOriginal) { redrawStrokes(strokesRef.current, 'resize'); } @@ -486,24 +493,70 @@ const CanvasImageEditor: React.FC = ({ } }, [drawImage, onReset, redrawStrokes, imageStatus]); + const calcTransformedPoint = (event: React.MouseEvent) => { + const overlayCanvas = overlayCanvasRef.current!; + const rect = overlayCanvas.getBoundingClientRect(); + + // 获取鼠标在画布上的原始坐标 + const x = event.clientX - rect.left; + const y = event.clientY - rect.top; + + // 考虑缩放比例和偏移量 + const transformedX = (x - offsetX) / scale; + const transformedY = (y - offsetY) / scale; + + return { x: transformedX, y: transformedY }; + }; + + const handleOnWheel = (event: WheelEvent) => { + event.preventDefault(); + + const zoomFactor = event.deltaY < 0 ? 1.1 : 0.9; + const newScale = Math.min( + MAX_SCALE, + Math.max(MIN_SCALE, scale * zoomFactor) + ); + + const rect = canvasRef.current!.getBoundingClientRect(); + const mouseX = event.clientX - rect.left; + const mouseY = event.clientY - rect.top; + + // 计算新的偏移量 + offsetX = mouseX - (mouseX - offsetX) * (newScale / scale); + offsetY = mouseY - (mouseY - offsetY) * (newScale / scale); + + // 更新缩放比例 + scale = newScale; + + // 设置画布的变换 + const overlayCtx = overlayCanvasRef.current!.getContext('2d')!; + overlayCtx.setTransform(scale, 0, 0, scale, offsetX, offsetY); + const canvasCtx = canvasRef.current!.getContext('2d')!; + canvasCtx.setTransform(scale, 0, 0, scale, offsetX, offsetY); + overlayCanvasRef.current!.style.transform = `scale(${scale})`; + canvasRef.current!.style.transform = `scale(${scale})`; + + console.log('Zoom:', scale, offsetX, offsetY); + }; + useEffect(() => { initializeImage(); }, [initializeImage]); - useEffect(() => { - const container = containerRef.current; - if (!container) return; - if (container) { - resizeObserver.current = new ResizeObserver( - _.throttle(handleResize, 100) - ); - resizeObserver.current.observe(container); - } - - return () => { - resizeObserver.current?.disconnect(); - }; - }, [handleResize, containerRef.current]); + // useEffect(() => { + // const container = containerRef.current; + // if (!container) return; + // if (container) { + // resizeObserver.current = new ResizeObserver( + // _.throttle(handleResize, 100) + // ); + // resizeObserver.current.observe(container); + // } + + // return () => { + // resizeObserver.current?.disconnect(); + // }; + // }, [handleResize, containerRef.current]); useEffect(() => { createOffscreenCanvas(); @@ -551,7 +604,7 @@ const CanvasImageEditor: React.FC = ({ style={{ marginBlock: '4px 6px', marginLeft: 0, flex: 1 }} vertical={false} defaultValue={lineWidth} - min={1} + min={10} max={60} onChange={(value) => setLineWidth(value)} /> diff --git a/src/hooks/use-update-chunk-list.ts b/src/hooks/use-update-chunk-list.ts index 73becf24..0e55431b 100644 --- a/src/hooks/use-update-chunk-list.ts +++ b/src/hooks/use-update-chunk-list.ts @@ -7,8 +7,12 @@ interface ChunkedCollection { collection: any[]; type: string | number; } + +type EventsType = 'CREATE' | 'UPDATE' | 'DELETE' | 'INSERT'; + // Only used to update lists without nested state export function useUpdateChunkedList(options: { + events?: EventsType[]; dataList?: any[]; limit?: number; setDataList: (args: any, opts?: any) => void; @@ -17,10 +21,10 @@ export function useUpdateChunkedList(options: { mapFun?: (args: any) => any; computedID?: (d: object) => string; }) { + const { events = ['CREATE', 'DELETE', 'UPDATE', 'INSERT'] } = options; const deletedIdsRef = useRef>(new Set()); const cacheDataListRef = useRef(options.dataList || []); const timerRef = useRef(null); - const countRef = useRef(0); const limit = options.limit || 10; useEffect(() => { @@ -57,7 +61,7 @@ export function useUpdateChunkedList(options: { } const ids: any[] = data?.ids || []; // CREATE - if (data?.type === WatchEventType.CREATE) { + if (data?.type === WatchEventType.CREATE && events.includes('CREATE')) { const newDataList = collections.reduce((acc: any[], item: any) => { const updateIndex = cacheDataListRef.current?.findIndex( (sItem: any) => sItem.id === item.id @@ -77,7 +81,7 @@ export function useUpdateChunkedList(options: { ].slice(0, limit); } // DELETE - if (data?.type === WatchEventType.DELETE) { + if (data?.type === WatchEventType.DELETE && events.includes('DELETE')) { cacheDataListRef.current = cacheDataListRef.current?.filter( (item: any) => { return !ids?.includes(item.id); @@ -89,7 +93,7 @@ export function useUpdateChunkedList(options: { }); } // UPDATE - if (data?.type === WatchEventType.UPDATE) { + if (data?.type === WatchEventType.UPDATE && events.includes('UPDATE')) { collections?.forEach((item: any) => { const updateIndex = cacheDataListRef.current?.findIndex( (sItem: any) => sItem.id === item.id @@ -97,7 +101,7 @@ export function useUpdateChunkedList(options: { const updateItem = { ...item }; if (updateIndex > -1) { cacheDataListRef.current[updateIndex] = updateItem; - } else if (updateIndex === -1) { + } else if (updateIndex === -1 && events.includes('INSERT')) { cacheDataListRef.current = [ updateItem, ...cacheDataListRef.current.slice(0, limit - 1) diff --git a/src/locales/en-US/models.ts b/src/locales/en-US/models.ts index 5e2bce96..f1d3655f 100644 --- a/src/locales/en-US/models.ts +++ b/src/locales/en-US/models.ts @@ -88,5 +88,5 @@ export default { 'models.form.search.gguftips': 'If using macOS or Windows as a worker, check GGUF (uncheck for audio models).', 'models.form.button.addlabel': 'Add Label', - 'models.filter.category': 'Filter by Category' + 'models.filter.category': 'Filter by category' }; diff --git a/src/pages/api-keys/index.tsx b/src/pages/api-keys/index.tsx index ba8a18ca..011e058e 100644 --- a/src/pages/api-keys/index.tsx +++ b/src/pages/api-keys/index.tsx @@ -22,7 +22,6 @@ import { ListItem } from './config/types'; const { Column } = Table; const APIKeys: React.FC = () => { - console.log('APIKeys========'); const rowSelection = useTableRowSelection(); const { sortOrder, setSortOrder } = useTableSort({ defaultSortOrder: 'descend' @@ -47,7 +46,6 @@ const APIKeys: React.FC = () => { }); const handlePageChange = (page: number, pageSize: number) => { - console.log('handlePageChange====', page, pageSize); setQueryParams({ ...queryParams, page: page, @@ -56,7 +54,6 @@ const APIKeys: React.FC = () => { }; const handleTableChange = (pagination: any, filters: any, sorter: any) => { - console.log('handleTableChange=======', pagination, filters, sorter); setSortOrder(sorter.order); }; diff --git a/src/pages/llmodels/catalog.tsx b/src/pages/llmodels/catalog.tsx index 173f057c..3cbaf4e1 100644 --- a/src/pages/llmodels/catalog.tsx +++ b/src/pages/llmodels/catalog.tsx @@ -30,6 +30,7 @@ const Catalog: React.FC = () => { const navigate = useNavigate(); const [span, setSpan] = React.useState(8); const [activeId, setActiveId] = React.useState(-1); + const [isFirst, setIsFirst] = React.useState(true); const [dataSource, setDataSource] = useState<{ dataList: CatalogItemType[]; loading: boolean; @@ -102,6 +103,8 @@ const Catalog: React.FC = () => { total: dataSource.total }); console.log('error', error); + } finally { + setIsFirst(false); } }, [queryParams]); @@ -297,7 +300,7 @@ const Catalog: React.FC = () => { style={{ width: '100%' }} wrapperClassName="skelton-wrapper" > - + {isFirst && } )} diff --git a/src/pages/llmodels/components/deploy-builtin-modal.tsx b/src/pages/llmodels/components/deploy-builtin-modal.tsx index 98c85953..ee7d76e3 100644 --- a/src/pages/llmodels/components/deploy-builtin-modal.tsx +++ b/src/pages/llmodels/components/deploy-builtin-modal.tsx @@ -7,7 +7,12 @@ import { Button, Drawer } from 'antd'; import _ from 'lodash'; import { memo, useCallback, useEffect, useRef, useState } from 'react'; import { queryCatalogItemSpec } from '../apis'; -import { backendOptionsMap, modelSourceMap, sourceOptions } from '../config'; +import { + backendOptionsMap, + modelCategoriesMap, + modelSourceMap, + sourceOptions +} from '../config'; import { CatalogSpec, FormData, ListItem } from '../config/types'; import ColumnWrapper from './column-wrapper'; import DataForm from './data-form'; @@ -40,6 +45,7 @@ const backendOptions = [ ]; const defaultQuant = ['Q4_K_M']; +const EmbeddingRerankFirstQuant = ['FP16']; const AddModal: React.FC = (props) => { const { title, @@ -51,7 +57,6 @@ const AddModal: React.FC = (props) => { current, width = 600 } = props || {}; - const SEARCH_SOURCE = []; const form = useRef({}); const intl = useIntl(); @@ -69,6 +74,16 @@ const AddModal: React.FC = (props) => { form.current?.submit?.(); }; + const getDefaultQuant = (data: { category: string; quantOption: string }) => { + if ( + data.category === modelCategoriesMap.embedding || + data.category === modelCategoriesMap.reranker + ) { + return EmbeddingRerankFirstQuant.includes(data.quantOption); + } + return defaultQuant.includes(data.quantOption); + }; + const getModelFile = (spec: CatalogSpec) => { let modelInfo = {}; if (spec.source === modelSourceMap.huggingface_value) { @@ -234,7 +249,10 @@ const AddModal: React.FC = (props) => { size: _.get(sizeList, '0.value', 0), quantization: _.find(quantizaList, (item: { label: string; value: string }) => - defaultQuant.includes(item.value) + getDefaultQuant({ + category: _.get(current, 'categories.0', ''), + quantOption: item.value + }) )?.value || _.get(quantizaList, '0.value', '') }); @@ -264,7 +282,10 @@ const AddModal: React.FC = (props) => { const source = _.get(sources, '0.value', ''); const defaultSpec = _.find(groupList[source], (item: CatalogSpec) => { - return defaultQuant.includes(item.quantization); + return getDefaultQuant({ + category: _.get(current, 'categories.0', ''), + quantOption: item.quantization + }); }) || _.get(groupList, `${source}.0`, {}); setSourceList(sources); @@ -320,7 +341,10 @@ const AddModal: React.FC = (props) => { size: val, quantization: _.find(list, (item: { label: string; value: string }) => - defaultQuant.includes(item.value) + getDefaultQuant({ + category: _.get(current, 'categories.0', ''), + quantOption: item.value + }) )?.value || _.get(list, '0.value', '') }); diff --git a/src/pages/llmodels/components/table-list.tsx b/src/pages/llmodels/components/table-list.tsx index d119a5da..cb5cd149 100644 --- a/src/pages/llmodels/components/table-list.tsx +++ b/src/pages/llmodels/components/table-list.tsx @@ -56,7 +56,7 @@ import UpdateModel from './update-modal'; import ViewLogsModal from './view-logs-modal'; interface ModelsProps { - handleSearch: (e: any) => void; + handleSearch: () => void; handleNameChange: (e: any) => void; handleShowSizeChange?: (page: number, size: number) => void; handlePageChange: (page: number, pageSize: number | undefined) => void; @@ -337,7 +337,6 @@ const Models: React.FC = ({ const handleModalOk = useCallback( async (data: FormData) => { try { - console.log('data:', data, openDeployModal); const result = getSourceRepoConfigValue(currentData?.source, data); await updateModel({ data: { @@ -348,6 +347,7 @@ const Models: React.FC = ({ }); setOpenAddModal(false); message.success(intl.formatMessage({ id: 'common.message.success' })); + handleSearch(); } catch (error) {} }, [currentData] @@ -385,6 +385,7 @@ const Models: React.FC = ({ updateExpandedRowKeys([modelData.id, ...expandedRowKeys]); }, 300); message.success(intl.formatMessage({ id: 'common.message.success' })); + handleSearch?.(); } catch (error) {} }, [openDeployModal] @@ -405,6 +406,7 @@ const Models: React.FC = ({ removeExpandedRowKey([row.id]); rowSelection.removeSelectedKey(row.id); handleDeleteSuccess(); + handleSearch(); } }); }; @@ -419,6 +421,7 @@ const Models: React.FC = ({ rowSelection.clearSelections(); removeExpandedRowKey(rowSelection.selectedRowKeys); handleDeleteSuccess(); + handleSearch(); } }); }; diff --git a/src/pages/llmodels/index.tsx b/src/pages/llmodels/index.tsx index b5d4c4a0..ef460956 100644 --- a/src/pages/llmodels/index.tsx +++ b/src/pages/llmodels/index.tsx @@ -45,6 +45,7 @@ const Models: React.FC = () => { const { updateChunkedList, cacheDataListRef, deletedIdsRef } = useUpdateChunkedList({ + events: ['UPDATE'], dataList: dataSource.dataList, setDataList(list, opts?: any) { setDataSource((pre) => { @@ -118,8 +119,6 @@ const Models: React.FC = () => { _.each(list, (data: any) => { updateChunkedList(data); }); - - console.log('deletedIdsRef=======', deletedIdsRef.current); }; const updateInstanceHandler = (list: any) => { @@ -129,14 +128,19 @@ const Models: React.FC = () => { const createModelsChunkRequest = useCallback(async () => { chunkRequedtRef.current?.current?.cancel?.(); try { + const query = { + search: queryParams.search, + categories: queryParams.categories + }; chunkRequedtRef.current = setChunkRequest({ - url: `${MODELS_API}?${qs.stringify(_.pickBy(queryParams, (val: any) => !!val))}`, + url: `${MODELS_API}?${qs.stringify(_.pickBy(query, (val: any) => !!val))}`, handler: updateHandler }); } catch (error) { // ignore } - }, [queryParams]); + }, [queryParams.categories, queryParams.search]); + const createModelsInstanceChunkRequest = useCallback(async () => { chunkInstanceRequedtRef.current?.current?.cancel?.(); try { @@ -150,11 +154,6 @@ const Models: React.FC = () => { } }, []); - const getList = async () => { - await fetchData(); - await createModelsChunkRequest(); - }; - const handleOnViewLogs = useCallback(() => { isPageHidden.current = true; chunkRequedtRef.current?.current?.cancel?.(); @@ -173,12 +172,9 @@ const Models: React.FC = () => { }, 100); }, [fetchData, createModelsChunkRequest, createModelsInstanceChunkRequest]); - const handleSearch = useCallback( - async (e: any) => { - await fetchData(); - }, - [fetchData] - ); + const handleSearch = useCallback(async () => { + await fetchData(); + }, [fetchData]); const debounceUpdateFilter = _.debounce((e: any) => { setQueryParams({ @@ -202,12 +198,16 @@ const Models: React.FC = () => { ); useEffect(() => { - getList(); + fetchData(); return () => { axiosToken?.cancel?.(); }; }, [queryParams]); + useEffect(() => { + createModelsChunkRequest(); + }, [createModelsChunkRequest]); + useEffect(() => { getWorkerList(); createModelsInstanceChunkRequest(); diff --git a/src/pages/playground/components/image-edit.tsx b/src/pages/playground/components/image-edit.tsx index 8a762641..6ae911cc 100644 --- a/src/pages/playground/components/image-edit.tsx +++ b/src/pages/playground/components/image-edit.tsx @@ -63,7 +63,7 @@ const METAKEYS = [ ]; const advancedFieldsDefaultValus = { - seed: 1, + seed: null, sample_method: 'euler_a', cfg_scale: 4.5, guidance: 3.5, @@ -278,6 +278,14 @@ const GroundImages: React.FC = forwardRef((props, ref) => { setMessageId(); setTokenResult(null); setCurrentPrompt(current?.content || ''); + setUploadList((pre) => { + return pre.map((item) => { + return { + ...item, + dataUrl: image + }; + }); + }); setRouteCache(routeCachekey['/playground/text-to-image'], true); const imgSize = _.split(finalParameters.size, 'x').map((item: string) => @@ -338,8 +346,8 @@ const GroundImages: React.FC = forwardRef((props, ref) => { const result: any = await fetchChunkedData({ data: params, - // url: `http:///v1/images/edits?t=${Date.now()}`, - url: `${EDIT_IMAGE_API}?t=${Date.now()}`, + // url: 'http://192.168.50.174:40053/v1/images/edits', + url: EDIT_IMAGE_API, signal: requestToken.current.signal }); @@ -372,6 +380,7 @@ const GroundImages: React.FC = forwardRef((props, ref) => { imgItem.dataUrl = `data:image/png;base64,${item.b64_json}`; } const progress = _.round(item.progress, 0); + console.log('progress:', item, progress); newImageList[item.index] = { dataUrl: imgItem.dataUrl, height: imgSize[1], @@ -381,7 +390,7 @@ const GroundImages: React.FC = forwardRef((props, ref) => { uid: imgItem.uid, span: imgItem.span, loading: stream_options.chunk_results ? progress < 100 : false, - preview: progress >= 100, + preview: false, progress: progress }; }); @@ -588,6 +597,7 @@ const GroundImages: React.FC = forwardRef((props, ref) => { ); const handleUpdateImageList = useCallback((base64List: any) => { + console.log('updateimagelist=========', base64List); const img = _.get(base64List, '[0].dataUrl', ''); setUploadList(base64List); setImage(img); @@ -667,7 +677,7 @@ const GroundImages: React.FC = forwardRef((props, ref) => { {...uploadList[0]} height={125} maxHeight={125} - preview={true} + preview={false} loading={false} autoSize={false} editable={false} @@ -728,11 +738,7 @@ const GroundImages: React.FC = forwardRef((props, ref) => { return (
-
+
<>
{ diff --git a/src/utils/fetch-chunk-data.ts b/src/utils/fetch-chunk-data.ts index fc20dc4e..db6ff41c 100644 --- a/src/utils/fetch-chunk-data.ts +++ b/src/utils/fetch-chunk-data.ts @@ -74,8 +74,8 @@ const createFormData = (data: any): FormData => { formData.append(key, value); } else if (typeof value === 'object' && value !== null) { formData.append(key, JSON.stringify(value)); - } else { - formData.append(key, String(value)); + } else if (value !== undefined && value !== null) { + formData.append(key, value); } };