text-img-editor 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,88 @@
1
+ # Text & Image Editor
2
+
3
+ Block-based editor for creating LLM prompts with text, annotated images, and variables. Features Slate.js rich text with @mentions and #variables.
4
+
5
+ ## Quick Start
6
+
7
+ ```bash
8
+ # Install dependencies
9
+ npm install
10
+
11
+ # Start upload server (for image uploads)
12
+ cd server && npm install && npm start
13
+
14
+ # Run development server (new terminal)
15
+ npm run dev
16
+ ```
17
+
18
+ Open http://localhost:5173
19
+
20
+ ## Features
21
+
22
+ ### Rich Text Editor (Slate.js)
23
+ - **@mentions** - Reference bounding boxes on images (`@BOX_NAME`)
24
+ - **#variables** - Insert string or boolean variables (`#VAR_NAME`)
25
+ - Click on tokens to see details (box preview / variable info)
26
+
27
+ ### Image Blocks
28
+ - Drag & drop images into the document
29
+ - Draw bounding boxes with mouse
30
+ - Name boxes in SCREAMING_SNAKE_CASE for @mention references
31
+ - Add descriptions to boxes
32
+
33
+ ### LLM-Ready Output
34
+ Generates OpenAI Vision API compatible format:
35
+
36
+ ```json
37
+ {
38
+ "content": [
39
+ { "type": "text", "text": "Analyze [box_id - abc123]" },
40
+ { "type": "image", "image": "https://..." },
41
+ { "type": "text", "text": "{\"image_id\":\"...\",\"image_size\":[1920,1080],\"boxes\":[...]}" }
42
+ ]
43
+ }
44
+ ```
45
+
46
+ ## Usage
47
+
48
+ 1. **Text**: Click to type, use `@` for box mentions, `#` for variables
49
+ 2. **Images**: Drag image files into the editor
50
+ 3. **Boxes**: Click and drag on images to draw bounding boxes
51
+ 4. **Box Names**: Click a box to set its SCREAMING_SNAKE_CASE name
52
+ 5. **Mentions**: Type `@` to reference named boxes in text
53
+ 6. **Variables**: Type `#` to insert or create variables
54
+
55
+ ## Token Format
56
+
57
+ | Token | Example | Output |
58
+ |-------|---------|--------|
59
+ | @mention | `@MACHINE` | `[box_id - xyz123]` |
60
+ | #variable (string) | `#USER_NAME` | Replaced with value |
61
+ | #variable (boolean) | `#IS_ACTIVE` | `[CONDITION: if "..." then ... else ...]` |
62
+
63
+ ## Box Metadata Format
64
+
65
+ ```json
66
+ {
67
+ "image_id": "abc123",
68
+ "image_size": [1920, 1080],
69
+ "bbox_format": "xyxy",
70
+ "bbox_units": "pixels",
71
+ "boxes": [
72
+ {
73
+ "box_id": "xyz789",
74
+ "label": "MACHINE",
75
+ "semantic": "CNC machine",
76
+ "bbox": [100, 200, 500, 600]
77
+ }
78
+ ]
79
+ }
80
+ ```
81
+
82
+ ## Tech Stack
83
+
84
+ - React 18
85
+ - TypeScript
86
+ - Vite
87
+ - Slate.js (rich text)
88
+ - Express (upload server)
package/dist/index.cjs ADDED
@@ -0,0 +1 @@
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const D=require("react/jsx-runtime"),i=require("react");function S(){return Math.random().toString(36).substring(2,9)}const V=new Map;function W(e){const s=[];for(const n of V.values()){const o=new RegExp(n.parsePattern.source,"g");let t;for(;(t=o.exec(e))!==null;){const a=n.parseMatch(t);a.start=t.index,a.end=t.index+t[0].length,s.push(a)}}return s.sort((n,o)=>n.start-o.start)}function _(e,s){const n=W(e);for(let o=n.length-1;o>=0;o--){const t=n[o];if(t.type==="mention"){const a=`[box_id - ${t.data.boxId}]`;e=e.substring(0,t.start)+a+e.substring(t.end)}else if(t.type==="variable"){const a=s[t.data.name];if(a)if(a.type==="string")e=e.substring(0,t.start)+a.value+e.substring(t.end);else{const r=`[CONDITION: if "${a.condition.if}" then ${a.condition.then} else ${a.condition.else}]`;e=e.substring(0,t.start)+r+e.substring(t.end)}}}return e}function q(e,s){const n=[];for(const o of e)if(o.type==="text"){const a=_(o.content,s);a.trim()&&n.push({type:"text",text:a})}else if(o.type==="image"){const t=o;if(n.push({type:"image",image:t.src}),t.width&&t.height){const a=t.width,r=t.height,p={image_id:t.id,image_size:[a,r],bbox_format:"[x, y, width, height]",bbox_units:"pixels",boxes:t.boxes.map(c=>({box_id:c.id,label:c.name||"",semantic:c.description||"",bbox_format:"[x, y, width, height]",bbox:[Math.round(c.x*a),Math.round(c.y*r),Math.round(c.width*a),Math.round(c.height*r)]}))};n.push({type:"text",text:JSON.stringify(p,null,2)})}}return{content:n}}const A={name:"json-v1",version:"1.0",serialize(e,s={},n){const o=e.map(a=>a.type==="text"?{type:"text",content:a.content}:{type:"image",src:a.src,width:a.width,height:a.height,boxes:a.boxes.map(p=>({id:p.id,name:p.name||"",x:p.x,y:p.y,width:p.width,height:p.height,description:p.description}))}),t=q(e,s);return{version:1,blocks:o,variables:Object.keys(s).length>0?s:void 0,metadata:n??{updatedAt:new Date().toISOString()},llm_value:t}},deserialize(e){return{blocks:e.blocks.map(n=>n.type==="text"?{id:S(),type:"text",content:n.content}:{id:S(),type:"image",src:n.src,width:n.width,height:n.height,boxes:n.boxes.map(t=>({id:t.id,name:t.name||"",x:t.x,y:t.y,width:t.width,height:t.height,description:t.description}))}),variables:e.variables||{}}},validate(e){const s=[];if(!e||typeof e!="object")return{valid:!1,errors:[{path:"",message:"Data must be an object"}]};const n=e;return n.version!==1&&s.push({path:"version",message:"Version must be 1"}),Array.isArray(n.blocks)?(n.blocks.forEach((o,t)=>{if(!o||typeof o!="object"){s.push({path:`blocks[${t}]`,message:"Block must be an object"});return}const a=o;a.type==="text"?typeof a.content!="string"&&s.push({path:`blocks[${t}].content`,message:"Content must be a string"}):a.type==="image"?(typeof a.src!="string"&&s.push({path:`blocks[${t}].src`,message:"Src must be a string"}),Array.isArray(a.boxes)||s.push({path:`blocks[${t}].boxes`,message:"Boxes must be an array"})):s.push({path:`blocks[${t}].type`,message:'Type must be "text" or "image"'})}),n.variables!==void 0&&(typeof n.variables!="object"||n.variables===null)&&s.push({path:"variables",message:"Variables must be an object"}),{valid:s.length===0,errors:s}):(s.push({path:"blocks",message:"Blocks must be an array"}),{valid:!1,errors:s})}},J=e=>e.startsWith("data:image/");function H({initialData:e,onChange:s}={}){const o=(()=>{if(e){const f=A.validate(e);if(f.valid)return A.deserialize(e);console.error("Invalid initialData:",f.errors)}return{blocks:[],variables:{}}})(),[t,a]=i.useState(o.blocks),[r,p]=i.useState(o.variables),c=i.useRef(!0),d=i.useRef(s);d.current=s,i.useEffect(()=>{if(c.current){c.current=!1;return}if(!t.some(u=>u.type==="image"&&J(u.src))&&d.current){const u=A.serialize(t,r);d.current(u)}},[t,r]);const m=i.useCallback(f=>{const u={id:S(),type:"text",content:""};a(l=>f!==void 0?[...l.slice(0,f),u,...l.slice(f)]:[...l,u])},[]),v=i.useCallback((f,u)=>{const l=S(),b={id:l,type:"image",src:f,boxes:[]};return a(k=>u!==void 0?[...k.slice(0,u),b,...k.slice(u)]:[...k,b]),l},[]),B=i.useCallback((f,u)=>{a(l=>l.map(b=>b.id===f?{...b,...u}:b))},[]),w=i.useCallback(f=>{a(u=>u.filter(l=>l.id!==f))},[]),x=i.useCallback((f,u)=>{p(l=>({...l,[f]:u}))},[]);return{blocks:t,variables:r,addTextBlock:m,addImageBlock:v,updateBlock:B,deleteBlock:w,addVariable:x}}const F=e=>e.startsWith("data:image/");function G({onUploadImage:e,onUpdateBlock:s}){const[n,o]=i.useState(new Map),t=i.useCallback((c,d)=>F(d)&&!n.has(c),[n]),a=i.useCallback((c,d)=>{!e||!F(d)||(o(m=>{const v=new Map(m);return v.delete(c),v}),e(d).then(m=>{const v=new Image;v.onload=()=>{s(c,{src:m})},v.onerror=()=>{o(B=>new Map(B).set(c,"Failed to load image"))},v.src=m}).catch(m=>{console.error("Failed to upload image:",m);const v=m instanceof Error?m.message:"Upload failed";o(B=>new Map(B).set(c,v))}))},[e,s]),r=i.useCallback((c,d)=>{F(d)&&a(c,d)},[a]),p=i.useCallback(c=>c.some(d=>d.type==="image"&&F(d.src)),[]);return{uploadErrors:n,isUploading:t,uploadImageInBackground:a,retryUpload:r,hasBase64Images:p}}function K({blocksCount:e,focusedBlockIndex:s,onAddImageBlock:n,onAddTextBlock:o,onSetFocusedBlockIndex:t}){const[a,r]=i.useState(!1),[p,c]=i.useState(null),d=i.useRef(0),m=i.useCallback(l=>{l.preventDefault(),d.current++,l.dataTransfer.types.includes("Files")&&r(!0)},[]),v=i.useCallback(l=>{l.preventDefault(),l.dataTransfer.dropEffect="copy"},[]),B=i.useCallback(l=>{l.preventDefault(),d.current--,d.current===0&&(r(!1),c(null))},[]),w=i.useCallback((l,b)=>{const k=new FileReader;k.onload=y=>{var M;const E=(M=y.target)==null?void 0:M.result;n(E,b),setTimeout(()=>{o(b+1),t(b+1)},50)},k.readAsDataURL(l)},[n,o,t]),x=i.useCallback(l=>{if(l.defaultPrevented)return;l.preventDefault(),d.current=0,r(!1),c(null);const k=Array.from(l.dataTransfer.files).find(y=>y.type.startsWith("image/"));k&&w(k,e)},[e,w]),f=i.useCallback((l,b)=>{l.preventDefault(),l.stopPropagation(),d.current=0,r(!1),c(null);const y=Array.from(l.dataTransfer.files).find(E=>E.type.startsWith("image/"));y&&w(y,b)},[w]),u=i.useCallback(l=>{const k=Array.from(l.clipboardData.items).find(y=>y.type.startsWith("image/"));if(k){l.preventDefault();const y=k.getAsFile();if(y){const E=s+1;w(y,E)}}},[s,w]);return{isDraggingFile:a,activeDropZone:p,setActiveDropZone:c,handleDragEnter:m,handleDragOver:v,handleDragLeave:B,handleEditorDrop:x,handleDropOnZone:f,handlePaste:u}}function Q({isFirst:e,visible:s,isActive:n,onDragEnter:o,onDragLeave:t,onDrop:a}){return D.jsx("div",{className:`drop-zone ${s?"drop-zone--visible":""} ${n?"drop-zone--active":""} ${e?"drop-zone--first":""}`,onDragEnter:o,onDragLeave:t,onDragOver:r=>r.preventDefault(),onDrop:a,children:D.jsx("div",{className:"drop-zone-line"})})}const L=i.memo(Q,(e,s)=>e.visible===s.visible&&e.isActive===s.isActive&&e.isFirst===s.isFirst),X=new Map;function Y(e){return X.get(e)}function ee({block:e,blockIndex:s,focusedBlockIndex:n,allBlocks:o,variables:t,onUpdate:a,onDelete:r,onVariableCreate:p,isUploading:c,getUploadError:d,onRetryUpload:m,onFocus:v}){const B=Y(e.type);if(!B)return null;const w=B.Component,x=i.useMemo(()=>({blockIndex:s,focusedBlockIndex:n,allBlocks:o,variables:t,onVariableCreate:p,isUploading:c,getUploadError:d,onRetryUpload:m}),[s,n,o,t,p,c,d,m]),f=i.useMemo(()=>B.getProps(e,x),[B,e,x]);return D.jsx("div",{className:"block-wrapper",draggable:!1,onFocus:v,onDragStart:u=>u.preventDefault(),children:D.jsx(w,{block:e,onUpdate:a,onDelete:r,...f})})}function te(e,s){if(e.block!==s.block)return!1;const n=e.blockIndex===e.focusedBlockIndex,o=s.blockIndex===s.focusedBlockIndex;if(n!==o||s.block.type==="text"&&(e.variables!==s.variables||e.allBlocks!==s.allBlocks))return!1;if(s.block.type==="image"){const t=e.isUploading(e.block.id,e.block.src),a=s.isUploading(s.block.id,s.block.src);if(t!==a)return!1;const r=e.getUploadError(e.block.id),p=s.getUploadError(s.block.id);if(r!==p)return!1}return!0}const se=i.memo(ee,te);function ae({initialData:e,onChange:s,onUploadImage:n}){const[o,t]=i.useState(0),a=i.useRef(null),{blocks:r,variables:p,addTextBlock:c,addImageBlock:d,updateBlock:m,deleteBlock:v,addVariable:B}=H({initialData:e,onChange:s}),{uploadErrors:w,isUploading:x,uploadImageInBackground:f,retryUpload:u}=G({onUploadImage:n,onUpdateBlock:m}),l=i.useCallback((g,h)=>{const C=d(g,h);f(C,g)},[d,f]),{isDraggingFile:b,activeDropZone:k,setActiveDropZone:y,handleDragEnter:E,handleDragOver:M,handleDragLeave:$,handleEditorDrop:N,handleDropOnZone:U,handlePaste:O}=K({blocksCount:r.length,focusedBlockIndex:o,onAddImageBlock:l,onAddTextBlock:c,onSetFocusedBlockIndex:t}),T=i.useCallback((g,h)=>{B(g,h)},[B]),z=i.useCallback(g=>{const h=r.find(C=>C.id===g);h&&h.type==="image"&&u(g,h.src)},[r,u]),P=i.useCallback(g=>w.get(g),[w]),Z=i.useMemo(()=>{const g=new Map;return r.forEach((h,C)=>{g.set(h.id,{onUpdate:j=>m(h.id,j),onDelete:()=>v(h.id),onFocus:()=>t(C)})}),g},[r,m,v]),R=i.useCallback(g=>{k===g&&y(null)},[k,y]),I=i.useMemo(()=>{const g=new Map;for(let h=0;h<=r.length;h++){const C=h;g.set(C,{onDragEnter:()=>y(C),onDragLeave:()=>R(C),onDrop:j=>U(j,C)})}return g},[r.length,y,R,U]);return r.length===0?D.jsxs("div",{className:"editor editor--empty",onDragEnter:E,onDragOver:M,onDragLeave:$,onDrop:g=>U(g,0),children:[D.jsx("div",{className:"editor-placeholder",onClick:()=>c(),children:D.jsx("span",{children:"Start typing or drag an image here..."})}),b&&D.jsxs("div",{className:"editor-drop-overlay",children:[D.jsx("div",{className:"drop-icon",children:"📷"}),D.jsx("p",{children:"Drop image to add"})]})]}):D.jsx("div",{ref:a,className:`editor ${b?"editor--dragging":""}`,onDragEnter:E,onDragOver:M,onDragLeave:$,onDrop:N,onPaste:O,children:D.jsxs("div",{className:"editor-document",children:[r.map((g,h)=>{const C=Z.get(g.id),j=I.get(h);return!C||!j?null:D.jsxs("div",{className:"block-with-dropzone",children:[D.jsx(L,{index:h,isFirst:h===0,visible:b,isActive:k===h,onDragEnter:j.onDragEnter,onDragLeave:j.onDragLeave,onDrop:j.onDrop}),D.jsx(se,{block:g,blockIndex:h,focusedBlockIndex:o,allBlocks:r,variables:p,onUpdate:C.onUpdate,onDelete:C.onDelete,onVariableCreate:T,isUploading:x,getUploadError:P,onRetryUpload:z,onFocus:C.onFocus})]},g.id)}),I.get(r.length)&&D.jsx(L,{index:r.length,visible:b,isActive:k===r.length,onDragEnter:I.get(r.length).onDragEnter,onDragLeave:I.get(r.length).onDragLeave,onDrop:I.get(r.length).onDrop})]})})}exports.Editor=ae;exports.JsonV1Adapter=A;
@@ -0,0 +1,180 @@
1
+ import { JSX as JSX_2 } from 'react/jsx-runtime';
2
+
3
+ export declare type Block = TextBlock | ImageBlock;
4
+
5
+ declare type BlockV1 = TextBlockV1 | ImageBlockV1;
6
+
7
+ declare interface BooleanVariable {
8
+ type: 'boolean';
9
+ condition: {
10
+ if: string;
11
+ then: boolean;
12
+ else: boolean;
13
+ };
14
+ }
15
+
16
+ export declare interface BoundingBox {
17
+ id: string;
18
+ name: string;
19
+ x: number;
20
+ y: number;
21
+ width: number;
22
+ height: number;
23
+ description: string;
24
+ }
25
+
26
+ declare interface BoxV1 {
27
+ id: string;
28
+ name: string;
29
+ x: number;
30
+ y: number;
31
+ width: number;
32
+ height: number;
33
+ description: string;
34
+ }
35
+
36
+ declare interface DocumentMetadata {
37
+ createdAt?: string;
38
+ updatedAt?: string;
39
+ title?: string;
40
+ }
41
+
42
+ export declare interface DocumentV1 {
43
+ version: 1;
44
+ blocks: BlockV1[];
45
+ variables?: VariablesMap;
46
+ metadata?: DocumentMetadata;
47
+ llm_value?: LLMValue;
48
+ }
49
+
50
+ export declare function Editor({ initialData, onChange, onUploadImage }: EditorProps_2): JSX_2.Element;
51
+
52
+ export declare interface EditorProps {
53
+ initialData?: DocumentV1 | null;
54
+ onChange?: (data: DocumentV1) => void;
55
+ onUploadImage?: (base64: string) => Promise<string>;
56
+ }
57
+
58
+ declare interface EditorProps_2 {
59
+ initialData?: DocumentV1 | null;
60
+ onChange?: (data: DocumentV1) => void;
61
+ onUploadImage?: (base64: string) => Promise<string>;
62
+ }
63
+
64
+ export declare interface ImageBlock {
65
+ id: string;
66
+ type: 'image';
67
+ src: string;
68
+ width?: number;
69
+ height?: number;
70
+ boxes: BoundingBox[];
71
+ }
72
+
73
+ declare interface ImageBlockV1 {
74
+ type: 'image';
75
+ src: string;
76
+ width?: number;
77
+ height?: number;
78
+ boxes: BoxV1[];
79
+ }
80
+
81
+ /**
82
+ * JSON V1 Serializer Adapter
83
+ *
84
+ * Converts between internal Block[] format and external DocumentV1 format.
85
+ *
86
+ * Key differences:
87
+ * - External format has version and metadata
88
+ * - External TextBlock/ImageBlock don't have id (internal only)
89
+ * - External BoxV1 keeps id (for referencing in text)
90
+ */
91
+ export declare const JsonV1Adapter: SerializerAdapter<DocumentV1>;
92
+
93
+ export declare interface LLMBox {
94
+ box_id: string;
95
+ label: string;
96
+ semantic: string;
97
+ bbox: [number, number, number, number];
98
+ }
99
+
100
+ export declare interface LLMBoxMetadata {
101
+ image_id: string;
102
+ image_size: [number, number];
103
+ bbox_format: '[x, y, width, height]';
104
+ bbox_units: 'pixels';
105
+ boxes: LLMBox[];
106
+ }
107
+
108
+ export declare type LLMContentItem = LLMTextContent | LLMImageContent;
109
+
110
+ declare interface LLMImageContent {
111
+ type: 'image';
112
+ image: string;
113
+ }
114
+
115
+ declare interface LLMTextContent {
116
+ type: 'text';
117
+ text: string;
118
+ }
119
+
120
+ export declare interface LLMValue {
121
+ content: LLMContentItem[];
122
+ }
123
+
124
+ declare interface SerializerAdapter<T = unknown> {
125
+ /** Adapter name for identification */
126
+ name: string;
127
+ /** Version of this adapter */
128
+ version: string;
129
+ /** Convert internal blocks to external format */
130
+ serialize(blocks: Block[], variables?: VariablesMap, metadata?: DocumentMetadata): T;
131
+ /** Convert external format to internal blocks */
132
+ deserialize(data: T): {
133
+ blocks: Block[];
134
+ variables: VariablesMap;
135
+ };
136
+ /** Validate external data before deserializing */
137
+ validate(data: unknown): ValidationResult;
138
+ }
139
+
140
+ declare interface StringVariable {
141
+ type: 'string';
142
+ value: string;
143
+ }
144
+
145
+ export declare interface TextBlock {
146
+ id: string;
147
+ type: 'text';
148
+ content: string;
149
+ }
150
+
151
+ declare interface TextBlockV1 {
152
+ type: 'text';
153
+ content: string;
154
+ }
155
+
156
+ declare interface ValidationError {
157
+ path: string;
158
+ message: string;
159
+ }
160
+
161
+ declare interface ValidationResult {
162
+ valid: boolean;
163
+ errors: ValidationError[];
164
+ }
165
+
166
+ export declare type Variable = StringVariable | BooleanVariable;
167
+
168
+ export declare type VariablesMap = Record<string, Variable>;
169
+
170
+ export { }
171
+
172
+
173
+ declare module 'slate' {
174
+ interface CustomTypes {
175
+ Editor: BaseEditor & ReactEditor & HistoryEditor;
176
+ Element: CustomElement;
177
+ Text: CustomText;
178
+ }
179
+ }
180
+
package/dist/index.js ADDED
@@ -0,0 +1,545 @@
1
+ import { jsx as E, jsxs as x } from "react/jsx-runtime";
2
+ import { useState as A, useRef as N, useEffect as q, useCallback as h, memo as Z, useMemo as z } from "react";
3
+ function C() {
4
+ return Math.random().toString(36).substring(2, 9);
5
+ }
6
+ const G = /* @__PURE__ */ new Map();
7
+ function K(e) {
8
+ const n = [];
9
+ for (const r of G.values()) {
10
+ const o = new RegExp(r.parsePattern.source, "g");
11
+ let t;
12
+ for (; (t = o.exec(e)) !== null; ) {
13
+ const s = r.parseMatch(t);
14
+ s.start = t.index, s.end = t.index + t[0].length, n.push(s);
15
+ }
16
+ }
17
+ return n.sort((r, o) => r.start - o.start);
18
+ }
19
+ function Q(e, n) {
20
+ const r = K(e);
21
+ for (let o = r.length - 1; o >= 0; o--) {
22
+ const t = r[o];
23
+ if (t.type === "mention") {
24
+ const s = `[box_id - ${t.data.boxId}]`;
25
+ e = e.substring(0, t.start) + s + e.substring(t.end);
26
+ } else if (t.type === "variable") {
27
+ const s = n[t.data.name];
28
+ if (s)
29
+ if (s.type === "string")
30
+ e = e.substring(0, t.start) + s.value + e.substring(t.end);
31
+ else {
32
+ const a = `[CONDITION: if "${s.condition.if}" then ${s.condition.then} else ${s.condition.else}]`;
33
+ e = e.substring(0, t.start) + a + e.substring(t.end);
34
+ }
35
+ }
36
+ }
37
+ return e;
38
+ }
39
+ function X(e, n) {
40
+ const r = [];
41
+ for (const o of e)
42
+ if (o.type === "text") {
43
+ const s = Q(o.content, n);
44
+ s.trim() && r.push({
45
+ type: "text",
46
+ text: s
47
+ });
48
+ } else if (o.type === "image") {
49
+ const t = o;
50
+ if (r.push({
51
+ type: "image",
52
+ image: t.src
53
+ }), t.width && t.height) {
54
+ const s = t.width, a = t.height, u = {
55
+ image_id: t.id,
56
+ image_size: [s, a],
57
+ bbox_format: "[x, y, width, height]",
58
+ bbox_units: "pixels",
59
+ boxes: t.boxes.map((c) => ({
60
+ box_id: c.id,
61
+ label: c.name || "",
62
+ semantic: c.description || "",
63
+ bbox_format: "[x, y, width, height]",
64
+ // Convert normalized (0-1) to pixels [x, y, width, height]
65
+ bbox: [
66
+ Math.round(c.x * s),
67
+ Math.round(c.y * a),
68
+ Math.round(c.width * s),
69
+ Math.round(c.height * a)
70
+ ]
71
+ }))
72
+ };
73
+ r.push({
74
+ type: "text",
75
+ text: JSON.stringify(u, null, 2)
76
+ });
77
+ }
78
+ }
79
+ return { content: r };
80
+ }
81
+ const T = {
82
+ name: "json-v1",
83
+ version: "1.0",
84
+ serialize(e, n = {}, r) {
85
+ const o = e.map((s) => s.type === "text" ? {
86
+ type: "text",
87
+ content: s.content
88
+ } : {
89
+ type: "image",
90
+ src: s.src,
91
+ width: s.width,
92
+ height: s.height,
93
+ boxes: s.boxes.map((u) => ({
94
+ id: u.id,
95
+ name: u.name || "",
96
+ x: u.x,
97
+ y: u.y,
98
+ width: u.width,
99
+ height: u.height,
100
+ description: u.description
101
+ }))
102
+ }), t = X(e, n);
103
+ return {
104
+ version: 1,
105
+ blocks: o,
106
+ variables: Object.keys(n).length > 0 ? n : void 0,
107
+ metadata: r ?? {
108
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
109
+ },
110
+ llm_value: t
111
+ };
112
+ },
113
+ deserialize(e) {
114
+ return {
115
+ blocks: e.blocks.map((r) => r.type === "text" ? {
116
+ id: C(),
117
+ type: "text",
118
+ content: r.content
119
+ } : {
120
+ id: C(),
121
+ type: "image",
122
+ src: r.src,
123
+ width: r.width,
124
+ height: r.height,
125
+ boxes: r.boxes.map((t) => ({
126
+ id: t.id,
127
+ name: t.name || "",
128
+ x: t.x,
129
+ y: t.y,
130
+ width: t.width,
131
+ height: t.height,
132
+ description: t.description
133
+ }))
134
+ }),
135
+ variables: e.variables || {}
136
+ };
137
+ },
138
+ validate(e) {
139
+ const n = [];
140
+ if (!e || typeof e != "object")
141
+ return { valid: !1, errors: [{ path: "", message: "Data must be an object" }] };
142
+ const r = e;
143
+ return r.version !== 1 && n.push({ path: "version", message: "Version must be 1" }), Array.isArray(r.blocks) ? (r.blocks.forEach((o, t) => {
144
+ if (!o || typeof o != "object") {
145
+ n.push({ path: `blocks[${t}]`, message: "Block must be an object" });
146
+ return;
147
+ }
148
+ const s = o;
149
+ s.type === "text" ? typeof s.content != "string" && n.push({ path: `blocks[${t}].content`, message: "Content must be a string" }) : s.type === "image" ? (typeof s.src != "string" && n.push({ path: `blocks[${t}].src`, message: "Src must be a string" }), Array.isArray(s.boxes) || n.push({ path: `blocks[${t}].boxes`, message: "Boxes must be an array" })) : n.push({ path: `blocks[${t}].type`, message: 'Type must be "text" or "image"' });
150
+ }), r.variables !== void 0 && (typeof r.variables != "object" || r.variables === null) && n.push({ path: "variables", message: "Variables must be an object" }), { valid: n.length === 0, errors: n }) : (n.push({ path: "blocks", message: "Blocks must be an array" }), { valid: !1, errors: n });
151
+ }
152
+ }, Y = (e) => e.startsWith("data:image/");
153
+ function ee({
154
+ initialData: e,
155
+ onChange: n
156
+ } = {}) {
157
+ const o = (() => {
158
+ if (e) {
159
+ const p = T.validate(e);
160
+ if (p.valid)
161
+ return T.deserialize(e);
162
+ console.error("Invalid initialData:", p.errors);
163
+ }
164
+ return { blocks: [], variables: {} };
165
+ })(), [t, s] = A(o.blocks), [a, u] = A(o.variables), c = N(!0), l = N(n);
166
+ l.current = n, q(() => {
167
+ if (c.current) {
168
+ c.current = !1;
169
+ return;
170
+ }
171
+ if (!t.some(
172
+ (d) => d.type === "image" && Y(d.src)
173
+ ) && l.current) {
174
+ const d = T.serialize(t, a);
175
+ l.current(d);
176
+ }
177
+ }, [t, a]);
178
+ const m = h((p) => {
179
+ const d = {
180
+ id: C(),
181
+ type: "text",
182
+ content: ""
183
+ };
184
+ s(
185
+ (i) => p !== void 0 ? [...i.slice(0, p), d, ...i.slice(p)] : [...i, d]
186
+ );
187
+ }, []), D = h((p, d) => {
188
+ const i = C(), v = {
189
+ id: i,
190
+ type: "image",
191
+ src: p,
192
+ boxes: []
193
+ };
194
+ return s(
195
+ (b) => d !== void 0 ? [...b.slice(0, d), v, ...b.slice(d)] : [...b, v]
196
+ ), i;
197
+ }, []), k = h((p, d) => {
198
+ s(
199
+ (i) => i.map(
200
+ (v) => v.id === p ? { ...v, ...d } : v
201
+ )
202
+ );
203
+ }, []), w = h((p) => {
204
+ s((d) => d.filter((i) => i.id !== p));
205
+ }, []), I = h((p, d) => {
206
+ u((i) => ({
207
+ ...i,
208
+ [p]: d
209
+ }));
210
+ }, []);
211
+ return {
212
+ blocks: t,
213
+ variables: a,
214
+ addTextBlock: m,
215
+ addImageBlock: D,
216
+ updateBlock: k,
217
+ deleteBlock: w,
218
+ addVariable: I
219
+ };
220
+ }
221
+ const L = (e) => e.startsWith("data:image/");
222
+ function te({
223
+ onUploadImage: e,
224
+ onUpdateBlock: n
225
+ }) {
226
+ const [r, o] = A(/* @__PURE__ */ new Map()), t = h((c, l) => L(l) && !r.has(c), [r]), s = h((c, l) => {
227
+ !e || !L(l) || (o((m) => {
228
+ const D = new Map(m);
229
+ return D.delete(c), D;
230
+ }), e(l).then((m) => {
231
+ const D = new Image();
232
+ D.onload = () => {
233
+ n(c, { src: m });
234
+ }, D.onerror = () => {
235
+ o((k) => new Map(k).set(c, "Failed to load image"));
236
+ }, D.src = m;
237
+ }).catch((m) => {
238
+ console.error("Failed to upload image:", m);
239
+ const D = m instanceof Error ? m.message : "Upload failed";
240
+ o((k) => new Map(k).set(c, D));
241
+ }));
242
+ }, [e, n]), a = h((c, l) => {
243
+ L(l) && s(c, l);
244
+ }, [s]), u = h((c) => c.some(
245
+ (l) => l.type === "image" && L(l.src)
246
+ ), []);
247
+ return {
248
+ uploadErrors: r,
249
+ isUploading: t,
250
+ uploadImageInBackground: s,
251
+ retryUpload: a,
252
+ hasBase64Images: u
253
+ };
254
+ }
255
+ function ne({
256
+ blocksCount: e,
257
+ focusedBlockIndex: n,
258
+ onAddImageBlock: r,
259
+ onAddTextBlock: o,
260
+ onSetFocusedBlockIndex: t
261
+ }) {
262
+ const [s, a] = A(!1), [u, c] = A(null), l = N(0), m = h((i) => {
263
+ i.preventDefault(), l.current++, i.dataTransfer.types.includes("Files") && a(!0);
264
+ }, []), D = h((i) => {
265
+ i.preventDefault(), i.dataTransfer.dropEffect = "copy";
266
+ }, []), k = h((i) => {
267
+ i.preventDefault(), l.current--, l.current === 0 && (a(!1), c(null));
268
+ }, []), w = h((i, v) => {
269
+ const b = new FileReader();
270
+ b.onload = (y) => {
271
+ var U;
272
+ const F = (U = y.target) == null ? void 0 : U.result;
273
+ r(F, v), setTimeout(() => {
274
+ o(v + 1), t(v + 1);
275
+ }, 50);
276
+ }, b.readAsDataURL(i);
277
+ }, [r, o, t]), I = h((i) => {
278
+ if (i.defaultPrevented) return;
279
+ i.preventDefault(), l.current = 0, a(!1), c(null);
280
+ const b = Array.from(i.dataTransfer.files).find((y) => y.type.startsWith("image/"));
281
+ b && w(b, e);
282
+ }, [e, w]), p = h((i, v) => {
283
+ i.preventDefault(), i.stopPropagation(), l.current = 0, a(!1), c(null);
284
+ const y = Array.from(i.dataTransfer.files).find((F) => F.type.startsWith("image/"));
285
+ y && w(y, v);
286
+ }, [w]), d = h((i) => {
287
+ const b = Array.from(i.clipboardData.items).find((y) => y.type.startsWith("image/"));
288
+ if (b) {
289
+ i.preventDefault();
290
+ const y = b.getAsFile();
291
+ if (y) {
292
+ const F = n + 1;
293
+ w(y, F);
294
+ }
295
+ }
296
+ }, [n, w]);
297
+ return {
298
+ isDraggingFile: s,
299
+ activeDropZone: u,
300
+ setActiveDropZone: c,
301
+ handleDragEnter: m,
302
+ handleDragOver: D,
303
+ handleDragLeave: k,
304
+ handleEditorDrop: I,
305
+ handleDropOnZone: p,
306
+ handlePaste: d
307
+ };
308
+ }
309
+ function se({
310
+ isFirst: e,
311
+ visible: n,
312
+ isActive: r,
313
+ onDragEnter: o,
314
+ onDragLeave: t,
315
+ onDrop: s
316
+ }) {
317
+ return /* @__PURE__ */ E(
318
+ "div",
319
+ {
320
+ className: `drop-zone ${n ? "drop-zone--visible" : ""} ${r ? "drop-zone--active" : ""} ${e ? "drop-zone--first" : ""}`,
321
+ onDragEnter: o,
322
+ onDragLeave: t,
323
+ onDragOver: (a) => a.preventDefault(),
324
+ onDrop: s,
325
+ children: /* @__PURE__ */ E("div", { className: "drop-zone-line" })
326
+ }
327
+ );
328
+ }
329
+ const S = Z(se, (e, n) => e.visible === n.visible && e.isActive === n.isActive && e.isFirst === n.isFirst), re = /* @__PURE__ */ new Map();
330
+ function ae(e) {
331
+ return re.get(e);
332
+ }
333
+ function oe({
334
+ block: e,
335
+ blockIndex: n,
336
+ focusedBlockIndex: r,
337
+ allBlocks: o,
338
+ variables: t,
339
+ onUpdate: s,
340
+ onDelete: a,
341
+ onVariableCreate: u,
342
+ isUploading: c,
343
+ getUploadError: l,
344
+ onRetryUpload: m,
345
+ onFocus: D
346
+ }) {
347
+ const k = ae(e.type);
348
+ if (!k) return null;
349
+ const w = k.Component, I = z(() => ({
350
+ blockIndex: n,
351
+ focusedBlockIndex: r,
352
+ allBlocks: o,
353
+ variables: t,
354
+ onVariableCreate: u,
355
+ isUploading: c,
356
+ getUploadError: l,
357
+ onRetryUpload: m
358
+ }), [n, r, o, t, u, c, l, m]), p = z(
359
+ () => k.getProps(e, I),
360
+ [k, e, I]
361
+ );
362
+ return /* @__PURE__ */ E(
363
+ "div",
364
+ {
365
+ className: "block-wrapper",
366
+ draggable: !1,
367
+ onFocus: D,
368
+ onDragStart: (d) => d.preventDefault(),
369
+ children: /* @__PURE__ */ E(
370
+ w,
371
+ {
372
+ block: e,
373
+ onUpdate: s,
374
+ onDelete: a,
375
+ ...p
376
+ }
377
+ )
378
+ }
379
+ );
380
+ }
381
+ function ie(e, n) {
382
+ if (e.block !== n.block) return !1;
383
+ const r = e.blockIndex === e.focusedBlockIndex, o = n.blockIndex === n.focusedBlockIndex;
384
+ if (r !== o || n.block.type === "text" && (e.variables !== n.variables || e.allBlocks !== n.allBlocks))
385
+ return !1;
386
+ if (n.block.type === "image") {
387
+ const t = e.isUploading(e.block.id, e.block.src), s = n.isUploading(n.block.id, n.block.src);
388
+ if (t !== s) return !1;
389
+ const a = e.getUploadError(e.block.id), u = n.getUploadError(n.block.id);
390
+ if (a !== u) return !1;
391
+ }
392
+ return !0;
393
+ }
394
+ const ce = Z(oe, ie);
395
+ function ge({
396
+ initialData: e,
397
+ onChange: n,
398
+ onUploadImage: r
399
+ }) {
400
+ const [o, t] = A(0), s = N(null), {
401
+ blocks: a,
402
+ variables: u,
403
+ addTextBlock: c,
404
+ addImageBlock: l,
405
+ updateBlock: m,
406
+ deleteBlock: D,
407
+ addVariable: k
408
+ } = ee({
409
+ initialData: e,
410
+ onChange: n
411
+ }), {
412
+ uploadErrors: w,
413
+ isUploading: I,
414
+ uploadImageInBackground: p,
415
+ retryUpload: d
416
+ } = te({
417
+ onUploadImage: r,
418
+ onUpdateBlock: m
419
+ }), i = h((g, f) => {
420
+ const B = l(g, f);
421
+ p(B, g);
422
+ }, [l, p]), {
423
+ isDraggingFile: v,
424
+ activeDropZone: b,
425
+ setActiveDropZone: y,
426
+ handleDragEnter: F,
427
+ handleDragOver: U,
428
+ handleDragLeave: j,
429
+ handleEditorDrop: R,
430
+ handleDropOnZone: O,
431
+ handlePaste: V
432
+ } = ne({
433
+ blocksCount: a.length,
434
+ focusedBlockIndex: o,
435
+ onAddImageBlock: i,
436
+ onAddTextBlock: c,
437
+ onSetFocusedBlockIndex: t
438
+ }), W = h((g, f) => {
439
+ k(g, f);
440
+ }, [k]), _ = h((g) => {
441
+ const f = a.find((B) => B.id === g);
442
+ f && f.type === "image" && d(g, f.src);
443
+ }, [a, d]), H = h((g) => w.get(g), [w]), J = z(() => {
444
+ const g = /* @__PURE__ */ new Map();
445
+ return a.forEach((f, B) => {
446
+ g.set(f.id, {
447
+ onUpdate: (M) => m(f.id, M),
448
+ onDelete: () => D(f.id),
449
+ onFocus: () => t(B)
450
+ });
451
+ }), g;
452
+ }, [a, m, D]), P = h((g) => {
453
+ b === g && y(null);
454
+ }, [b, y]), $ = z(() => {
455
+ const g = /* @__PURE__ */ new Map();
456
+ for (let f = 0; f <= a.length; f++) {
457
+ const B = f;
458
+ g.set(B, {
459
+ onDragEnter: () => y(B),
460
+ onDragLeave: () => P(B),
461
+ onDrop: (M) => O(M, B)
462
+ });
463
+ }
464
+ return g;
465
+ }, [a.length, y, P, O]);
466
+ return a.length === 0 ? /* @__PURE__ */ x(
467
+ "div",
468
+ {
469
+ className: "editor editor--empty",
470
+ onDragEnter: F,
471
+ onDragOver: U,
472
+ onDragLeave: j,
473
+ onDrop: (g) => O(g, 0),
474
+ children: [
475
+ /* @__PURE__ */ E("div", { className: "editor-placeholder", onClick: () => c(), children: /* @__PURE__ */ E("span", { children: "Start typing or drag an image here..." }) }),
476
+ v && /* @__PURE__ */ x("div", { className: "editor-drop-overlay", children: [
477
+ /* @__PURE__ */ E("div", { className: "drop-icon", children: "📷" }),
478
+ /* @__PURE__ */ E("p", { children: "Drop image to add" })
479
+ ] })
480
+ ]
481
+ }
482
+ ) : /* @__PURE__ */ E(
483
+ "div",
484
+ {
485
+ ref: s,
486
+ className: `editor ${v ? "editor--dragging" : ""}`,
487
+ onDragEnter: F,
488
+ onDragOver: U,
489
+ onDragLeave: j,
490
+ onDrop: R,
491
+ onPaste: V,
492
+ children: /* @__PURE__ */ x("div", { className: "editor-document", children: [
493
+ a.map((g, f) => {
494
+ const B = J.get(g.id), M = $.get(f);
495
+ return !B || !M ? null : /* @__PURE__ */ x("div", { className: "block-with-dropzone", children: [
496
+ /* @__PURE__ */ E(
497
+ S,
498
+ {
499
+ index: f,
500
+ isFirst: f === 0,
501
+ visible: v,
502
+ isActive: b === f,
503
+ onDragEnter: M.onDragEnter,
504
+ onDragLeave: M.onDragLeave,
505
+ onDrop: M.onDrop
506
+ }
507
+ ),
508
+ /* @__PURE__ */ E(
509
+ ce,
510
+ {
511
+ block: g,
512
+ blockIndex: f,
513
+ focusedBlockIndex: o,
514
+ allBlocks: a,
515
+ variables: u,
516
+ onUpdate: B.onUpdate,
517
+ onDelete: B.onDelete,
518
+ onVariableCreate: W,
519
+ isUploading: I,
520
+ getUploadError: H,
521
+ onRetryUpload: _,
522
+ onFocus: B.onFocus
523
+ }
524
+ )
525
+ ] }, g.id);
526
+ }),
527
+ $.get(a.length) && /* @__PURE__ */ E(
528
+ S,
529
+ {
530
+ index: a.length,
531
+ visible: v,
532
+ isActive: b === a.length,
533
+ onDragEnter: $.get(a.length).onDragEnter,
534
+ onDragLeave: $.get(a.length).onDragLeave,
535
+ onDrop: $.get(a.length).onDrop
536
+ }
537
+ )
538
+ ] })
539
+ }
540
+ );
541
+ }
542
+ export {
543
+ ge as Editor,
544
+ T as JsonV1Adapter
545
+ };
package/dist/style.css ADDED
@@ -0,0 +1 @@
1
+ .editor{background:#fff;border:1px solid #e0e0e0;border-radius:8px;min-height:500px;position:relative;padding:40px 60px;box-shadow:0 1px 3px #00000014}.editor--dragging{border-color:#2196f3;background:linear-gradient(to bottom,#e3f2fd,#fff 100px)}.editor--empty{display:flex;align-items:flex-start;justify-content:flex-start}.editor-placeholder{color:#9e9e9e;font-size:16px;cursor:text;padding:4px 0;width:100%}.editor-placeholder:hover{color:#757575}.editor-document{display:flex;flex-direction:column;padding-bottom:48px;position:relative}.block-wrapper{position:relative;-webkit-user-drag:none;user-drag:none}.block-with-dropzone{position:relative}.drop-zone{position:absolute;left:0;right:0;top:-12px;height:24px;z-index:50;display:flex;align-items:center;justify-content:center;cursor:copy;opacity:0;pointer-events:none}.drop-zone--visible{opacity:1;pointer-events:auto}.drop-zone--first{top:0}.drop-zone-line{width:100%;height:4px;background:#90caf9;border-radius:2px;pointer-events:none}.drop-zone--active .drop-zone-line{background:#2196f3}.editor-document>.drop-zone{position:absolute;bottom:8px;top:auto;height:32px}.editor-drop-overlay{position:absolute;top:0;right:0;bottom:0;left:0;background:#2196f3f2;display:flex;flex-direction:column;align-items:center;justify-content:center;border-radius:8px;z-index:100;pointer-events:none}.drop-icon{font-size:64px;margin-bottom:16px}.editor-drop-overlay p{font-size:18px;font-weight:500;color:#fff}
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "text-img-editor",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "main": "./dist/index.cjs",
6
+ "module": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/index.js",
11
+ "require": "./dist/index.cjs",
12
+ "types": "./dist/index.d.ts"
13
+ },
14
+ "./styles.css": "./dist/style.css"
15
+ },
16
+ "files": [
17
+ "dist"
18
+ ],
19
+ "sideEffects": [
20
+ "**/*.css"
21
+ ],
22
+ "scripts": {
23
+ "dev": "vite",
24
+ "build": "tsc && vite build",
25
+ "build:lib": "vite build",
26
+ "preview": "vite preview",
27
+ "prepublishOnly": "npm run build:lib"
28
+ },
29
+ "peerDependencies": {
30
+ "react": "^18.0.0",
31
+ "react-dom": "^18.0.0"
32
+ },
33
+ "dependencies": {
34
+ "slate": "^0.120.0",
35
+ "slate-history": "^0.113.1",
36
+ "slate-react": "^0.120.0"
37
+ },
38
+ "devDependencies": {
39
+ "@types/node": "^20.0.0",
40
+ "@types/react": "^18.2.0",
41
+ "@types/react-dom": "^18.2.0",
42
+ "@vitejs/plugin-react": "^4.2.0",
43
+ "react": "^18.2.0",
44
+ "react-dom": "^18.2.0",
45
+ "typescript": "^5.3.0",
46
+ "vite": "^5.0.0",
47
+ "vite-plugin-dts": "^3.9.0"
48
+ },
49
+ "repository": {
50
+ "type": "git",
51
+ "url": "https://github.com/yourorg/text-img-editor"
52
+ },
53
+ "keywords": [
54
+ "react",
55
+ "editor",
56
+ "llm",
57
+ "bounding-box",
58
+ "image-annotation"
59
+ ],
60
+ "license": "MIT"
61
+ }