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