From 7d0fc122b5877e44df6851dfca2243380ca8755e Mon Sep 17 00:00:00 2001 From: Dandelion <3301085697@qq.com> Date: Wed, 5 Aug 2026 09:55:03 +0800 Subject: [PATCH 1/3] fix:fix the problem that frontend can not corectly render after ml-backend return rles --- .../ImageView/SuggestionControls.jsx | 40 ++++--------------- web/libs/editor/src/regions/RegionWrapper.jsx | 9 +---- 2 files changed, 9 insertions(+), 40 deletions(-) diff --git a/web/libs/editor/src/components/ImageView/SuggestionControls.jsx b/web/libs/editor/src/components/ImageView/SuggestionControls.jsx index 49eab76bb0df..71ebaffc9e36 100644 --- a/web/libs/editor/src/components/ImageView/SuggestionControls.jsx +++ b/web/libs/editor/src/components/ImageView/SuggestionControls.jsx @@ -1,5 +1,5 @@ import { useCallback, useEffect, useState } from "react"; -import { Circle, Group, Image, Layer, Rect } from "react-konva"; +import { Circle, Group, Image, Rect } from "react-konva"; import { IconCheck, IconCross } from "@humansignal/icons"; import Konva from "konva"; import chroma from "chroma-js"; @@ -28,7 +28,7 @@ const getItemPosition = (item) => { }; }; -export const SuggestionControls = observer(({ item, useLayer }) => { +export const SuggestionControls = observer(({ item }) => { const position = getItemPosition(item); const [hovered, setHovered] = useState(false); const scale = 1 / item.parent.zoomScale; @@ -39,33 +39,13 @@ export const SuggestionControls = observer(({ item, useLayer }) => { height: 32, }; - const groupPosition = useLayer - ? { - x: 0, - y: 0, - scaleX: 1, - scaleY: 1, - } - : { - x: position.x, - y: position.y, - scaleX: scale, - scaleY: scale, - }; - - const layerPosition = useLayer - ? { - x: position.x, - y: position.y, - scaleX: scale, - scaleY: scale, - } - : {}; - const content = ( setHovered(true)} onMouseLeave={() => setHovered(false)} @@ -87,13 +67,7 @@ export const SuggestionControls = observer(({ item, useLayer }) => { ); - return useLayer ? ( - - {content} - - ) : ( - content - ); + return content; } return null; }); diff --git a/web/libs/editor/src/regions/RegionWrapper.jsx b/web/libs/editor/src/regions/RegionWrapper.jsx index 0a8c07437030..f751b435fb9a 100644 --- a/web/libs/editor/src/regions/RegionWrapper.jsx +++ b/web/libs/editor/src/regions/RegionWrapper.jsx @@ -1,15 +1,10 @@ import { observer } from "mobx-react"; -import { Fragment, useContext } from "react"; -import { ImageViewContext } from "../components/ImageView/ImageViewContext"; -import { SuggestionControls } from "../components/ImageView/SuggestionControls"; - -export const RegionWrapper = observer(({ item, children }) => { - const { suggestion } = useContext(ImageViewContext) ?? {}; +import { Fragment } from "react"; +export const RegionWrapper = observer(({ children }) => { return ( {children} - {suggestion && } ); }); From 8ed615858bb37f65afbdd870afb411befb729b71 Mon Sep 17 00:00:00 2001 From: Dandelion <3301085697@qq.com> Date: Wed, 5 Aug 2026 17:40:41 +0800 Subject: [PATCH 2/3] Fix Data Manager assignee refresh --- web/libs/datamanager/src/stores/Assignee.js | 33 ++++++++------------- web/webpack.config.js | 3 +- 2 files changed, 14 insertions(+), 22 deletions(-) diff --git a/web/libs/datamanager/src/stores/Assignee.js b/web/libs/datamanager/src/stores/Assignee.js index d34f6ad8586a..e9f441b2d314 100644 --- a/web/libs/datamanager/src/stores/Assignee.js +++ b/web/libs/datamanager/src/stores/Assignee.js @@ -1,28 +1,22 @@ import { types } from "mobx-state-tree"; import { User } from "./Users"; import { StringOrNumberID } from "./types"; -import { FF_DISABLE_GLOBAL_USER_FETCHING, isFF } from "../utils/feature-flags"; -// Create a union type that can handle both user references and direct user objects -const UserOrReference = types.union({ - dispatcher: (snapshot) => { - // If it's a full user object (has firstName, email, etc.), use User model - if (snapshot && typeof snapshot === "object" && (snapshot.firstName || snapshot.email || snapshot.username)) { - return User; - } - // Otherwise, it's a reference to a user ID - return types.reference(User); - }, - cases: { - [User.name]: User, - reference: types.reference(User), - }, +const userSnapshot = (id, user = {}) => ({ + id, + firstName: "", + lastName: "", + username: "", + email: "", + lastActivity: "", + initials: "", + ...user, }); export const Assignee = types .model("Assignee", { id: StringOrNumberID, - user: types.late(() => UserOrReference), + user: User, review: types.maybeNull(types.enumeration(["accepted", "rejected", "fixed"])), reviewed: types.maybeNull(types.boolean), annotated: types.maybeNull(types.boolean), @@ -59,7 +53,7 @@ export const Assignee = types if (typeof sn === "number") { result = { id: sn, - user: sn, + user: userSnapshot(sn), annotated: true, review: null, reviewed: false, @@ -68,12 +62,9 @@ export const Assignee = types const { user_id, annotated, review, reviewed, ...user } = sn; const id = user_id ?? sn.id; - // When global user fetching is disabled, always create user objects, otherwise use references via user id - // If we only have user_id and no other user properties, just use the user_id as reference - const hasUserProperties = Object.keys(user).length > 0; result = { id, - user: isFF(FF_DISABLE_GLOBAL_USER_FETCHING) && hasUserProperties ? { id, ...user } : id, // Use user_id as reference + user: userSnapshot(id, user), annotated, review, reviewed, diff --git a/web/webpack.config.js b/web/webpack.config.js index 30691561a8ea..19220f1b6ad6 100644 --- a/web/webpack.config.js +++ b/web/webpack.config.js @@ -97,7 +97,8 @@ module.exports = composePlugins( config.output = { ...config.output, uniqueName: "labelstudio", - publicPath: + chunkFilename: isDevelopment ? "[name].js" : "[name].[contenthash].js", + publicPath: isDevelopment && FRONTEND_HOSTNAME ? `${FRONTEND_HOSTNAME}/react-app/` : process.env.MODE === "standalone-playground" From 0e0e807a8db5c0fdb54614558a99074ec8dbbd52 Mon Sep 17 00:00:00 2001 From: Dandelion <3301085697@qq.com> Date: Fri, 7 Aug 2026 15:19:47 +0800 Subject: [PATCH 3/3] Fix multi-rectangle interactive context --- docker-compose.yml | 7 +- .../src/pages/DataManager/DataManager.jsx | 5 +- .../pages/DataManager/interactive-context.js | 25 ++++++ .../DataManager/interactive-context.test.js | 90 +++++++++++++++++++ 4 files changed, 123 insertions(+), 4 deletions(-) create mode 100644 web/apps/labelstudio/src/pages/DataManager/interactive-context.js create mode 100644 web/apps/labelstudio/src/pages/DataManager/interactive-context.test.js diff --git a/docker-compose.yml b/docker-compose.yml index 8f2d0072db4a..7d60e3d6edd4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,8 +4,7 @@ services: image: heartexlabs/label-studio:latest restart: unless-stopped ports: - - "8080:8085" - - "8081:8086" + - "8085:8085" depends_on: - app environment: @@ -19,6 +18,7 @@ services: # - NGINX_SSL_CERT_KEY=/certs/cert.key volumes: - ./mydata:/label-studio/data:rw + - E:/data:/label-studio/data/external:ro - ./deploy/nginx/certs:/certs:ro # Optional: Override nginx default conf # - ./deploy/my.conf:/etc/nginx/nginx.conf @@ -43,9 +43,12 @@ services: - POSTGRE_HOST=db - LABEL_STUDIO_HOST=${LABEL_STUDIO_HOST:-} - JSON_LOG=1 + - LABEL_STUDIO_LOCAL_FILES_DOCUMENT_ROOT=/label-studio/data + - LABEL_STUDIO_LOCAL_FILES_SERVING_ENABLED=true # - LOG_LEVEL=DEBUG volumes: - ./mydata:/label-studio/data:rw + - E:/data:/label-studio/data/external:ro command: label-studio-uwsgi db: diff --git a/web/apps/labelstudio/src/pages/DataManager/DataManager.jsx b/web/apps/labelstudio/src/pages/DataManager/DataManager.jsx index 9b90c920be5b..91df3ecd7d68 100644 --- a/web/apps/labelstudio/src/pages/DataManager/DataManager.jsx +++ b/web/apps/labelstudio/src/pages/DataManager/DataManager.jsx @@ -14,6 +14,7 @@ import { isDefined } from "../../utils/helpers"; import { ImportModal } from "../CreateProject/Import/ImportModal"; import { ExportPage } from "../ExportPage/ExportPage"; import { APIConfig } from "./api-config"; +import { getInteractiveContextResult } from "./interactive-context"; import "./DataManager.prefix.css"; @@ -157,8 +158,8 @@ export const DataManagerPage = ({ ...props }) => { if (interactiveBacked) { dataManager.on("lsf:regionFinishedDrawing", (reg, group) => { const { lsf, task, currentAnnotation: annotation } = dataManager.lsf; - const ids = group.map((r) => r.cleanId); - const result = annotation.serializeAnnotation().filter((res) => ids.includes(res.id)); + const serializedAnnotation = annotation.serializeAnnotation(); + const result = getInteractiveContextResult(serializedAnnotation, reg, group); const suggestionsRequest = api.callApi("mlInteractive", { params: { pk: interactiveBacked.id }, diff --git a/web/apps/labelstudio/src/pages/DataManager/interactive-context.js b/web/apps/labelstudio/src/pages/DataManager/interactive-context.js new file mode 100644 index 000000000000..13e31765f794 --- /dev/null +++ b/web/apps/labelstudio/src/pages/DataManager/interactive-context.js @@ -0,0 +1,25 @@ +export const getInteractiveContextResult = (serializedAnnotation, region, group) => { + const control = region.results?.[0]; + const fromName = control?.from_name?.name ?? control?.from_name; + const toName = control?.to_name?.name ?? control?.to_name; + + if (region.type === "rectangleregion" && fromName && toName) { + const currentRegion = region.serialize?.(); + const annotationResults = currentRegion + ? serializedAnnotation.some((result) => result.id === currentRegion.id) + ? serializedAnnotation + : [...serializedAnnotation, currentRegion] + : serializedAnnotation; + + return annotationResults.filter( + (result) => + result.type === "rectanglelabels" && + result.from_name === fromName && + result.to_name === toName, + ); + } + + const ids = group.map((item) => item.cleanId); + + return serializedAnnotation.filter((result) => ids.includes(result.id)); +}; \ No newline at end of file diff --git a/web/apps/labelstudio/src/pages/DataManager/interactive-context.test.js b/web/apps/labelstudio/src/pages/DataManager/interactive-context.test.js new file mode 100644 index 000000000000..af18ba39bc31 --- /dev/null +++ b/web/apps/labelstudio/src/pages/DataManager/interactive-context.test.js @@ -0,0 +1,90 @@ +import { getInteractiveContextResult } from "./interactive-context"; + +const smartRectangle = { + type: "rectangleregion", + results: [{ from_name: { name: "box_target" }, to_name: { name: "image" } }], +}; + +describe("getInteractiveContextResult", () => { + it("includes every current rectangle for the smart rectangle control", () => { + const firstBox = { + id: "first", + type: "rectanglelabels", + from_name: "box_target", + to_name: "image", + }; + const secondBox = { + id: "second", + type: "rectanglelabels", + from_name: "box_target", + to_name: "image", + }; + + const result = getInteractiveContextResult( + [firstBox, secondBox], + smartRectangle, + [{ cleanId: secondBox.id }], + ); + + expect(result).toEqual([firstBox, secondBox]); + }); + + it("excludes deleted and unrelated results from the smart rectangle context", () => { + const currentBox = { + id: "current", + type: "rectanglelabels", + from_name: "box_target", + to_name: "image", + }; + const otherControlBox = { + id: "other-control", + type: "rectanglelabels", + from_name: "other_box_target", + to_name: "image", + }; + const brushResult = { + id: "brush", + type: "brushlabels", + from_name: "brush_target", + to_name: "image", + }; + + const result = getInteractiveContextResult( + [currentBox, otherControlBox, brushResult], + smartRectangle, + [{ cleanId: currentBox.id }], + ); + + expect(result).toEqual([currentBox]); + }); + + it("includes the event region before annotation serialization catches up", () => { + const currentBox = { + id: "current", + type: "rectanglelabels", + from_name: "box_target", + to_name: "image", + }; + const region = { + ...smartRectangle, + serialize: () => currentBox, + }; + + const result = getInteractiveContextResult([], region, [{ cleanId: "current" }]); + + expect(result).toEqual([currentBox]); + }); + + it("preserves group-based behavior for non-rectangle smart regions", () => { + const point = { id: "point", type: "keypointlabels" }; + const unrelatedPoint = { id: "other-point", type: "keypointlabels" }; + + const result = getInteractiveContextResult( + [point, unrelatedPoint], + { type: "keypointregion", results: [{}] }, + [{ cleanId: point.id }], + ); + + expect(result).toEqual([point]); + }); +}); \ No newline at end of file