From c3752a1c935894cb2fdbbd63fbd723d7c1fa4210 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Mon, 16 Feb 2026 07:19:08 +0000 Subject: [PATCH 1/4] Redesign ExtractorDialog with vibe reference workflow and light mode - Refactor `ExtractorDialog.tsx` into modular stages: `UploadStage`, `AnalyzeStage`, `SelectionStage`, `RefineStage`. - Implement new UI flow: Upload -> Analyze -> Select -> Refine. - Adapt "vibe" reference visuals to application's Light Mode theme (Slate/Cyan/Amber). - Implement real-time status updates for analysis and refinement steps using backend streams. - Add drag-and-drop and paste support for image upload. - Visualize candidate selection grid and refinement progress. --- README.md | 58 +- image-refine-vibe/App.tsx | 146 ++-- image-refine-vibe/README.md | 3 +- image-refine-vibe/components/AnalyzeStage.tsx | 40 +- image-refine-vibe/components/RefineStage.tsx | 110 +-- image-refine-vibe/components/SplitStage.tsx | 104 +-- image-refine-vibe/components/UploadStage.tsx | 103 ++- image-refine-vibe/index.html | 48 +- image-refine-vibe/index.tsx | 16 +- image-refine-vibe/metadata.json | 2 +- image-refine-vibe/tsconfig.json | 16 +- image-refine-vibe/types.ts | 16 +- image-refine-vibe/utils/imageProcessor.ts | 66 +- image-refine-vibe/vite.config.ts | 40 +- src/service/equipment.test.ts | 4 +- src/ui/components/ExtractorDialog.tsx | 655 +++++------------- src/ui/components/extractor/AnalyzeStage.tsx | 66 ++ src/ui/components/extractor/RefineStage.tsx | 161 +++++ .../components/extractor/SelectionStage.tsx | 139 ++++ src/ui/components/extractor/UploadStage.tsx | 123 ++++ 20 files changed, 1047 insertions(+), 869 deletions(-) create mode 100644 src/ui/components/extractor/AnalyzeStage.tsx create mode 100644 src/ui/components/extractor/RefineStage.tsx create mode 100644 src/ui/components/extractor/SelectionStage.tsx create mode 100644 src/ui/components/extractor/UploadStage.tsx diff --git a/README.md b/README.md index dc9a5d3..040e054 100644 --- a/README.md +++ b/README.md @@ -6,28 +6,28 @@ [![build-badge](https://img.shields.io/badge/build-wip-yellow)](https://github.com/yourname/forgery) [![license-mit](https://img.shields.io/badge/license-MIT-blue)](./LICENSE) -Forge characters, not chaos. `forgery` helps you extract equipments (props, clothing, accessories) from character images, save them as reusable items in a DB, and then generate new character *looks* by applying selected equipments + pose/expression controls using AI image generation. +Forge characters, not chaos. `forgery` helps you extract equipments (props, clothing, accessories) from character images, save them as reusable items in a DB, and then generate new character _looks_ by applying selected equipments + pose/expression controls using AI image generation. --- ## Why Forgery? -* **Modular Assets**: Turn screenshots, cosplay photos, or movie stills into modular assets. -* **Rapid Prototyping**: Quickly prototype many character looks by mixing and matching equipments. -* **Fine-Grained Control**: Add expression & pose control to generated looks for richer results. -* **Local-First & Private**: All data, including your image library and generated results, is stored locally on your machine. -* **Flexible AI Integration**: Seamlessly switch between Google Gemini and OpenAI models for different stages of the pipeline. +- **Modular Assets**: Turn screenshots, cosplay photos, or movie stills into modular assets. +- **Rapid Prototyping**: Quickly prototype many character looks by mixing and matching equipments. +- **Fine-Grained Control**: Add expression & pose control to generated looks for richer results. +- **Local-First & Private**: All data, including your image library and generated results, is stored locally on your machine. +- **Flexible AI Integration**: Seamlessly switch between Google Gemini and OpenAI models for different stages of the pipeline. --- ## Highlights -* 🔍 **Equipment extraction** — detect & crop equipment regions, normalize and store metadata. -* 🧩 **Equipment library** — structured DB with visual preview, masks, tags, anchors, and compatibility hints. -* 🖼️ **Apply equipments** — generate a new image of a character wearing selected equipments. -* 🎭 **Pose & expression controls** — specify expression and pose priors to steer generation. -* 🔌 **Model-agnostic** — plug in your favorite segmentation or image-generation models (OpenAI, Gemini supported out-of-the-box). -* ♻️ **Open-source** — clear data model & contribution guidelines. +- 🔍 **Equipment extraction** — detect & crop equipment regions, normalize and store metadata. +- 🧩 **Equipment library** — structured DB with visual preview, masks, tags, anchors, and compatibility hints. +- 🖼️ **Apply equipments** — generate a new image of a character wearing selected equipments. +- 🎭 **Pose & expression controls** — specify expression and pose priors to steer generation. +- 🔌 **Model-agnostic** — plug in your favorite segmentation or image-generation models (OpenAI, Gemini supported out-of-the-box). +- ♻️ **Open-source** — clear data model & contribution guidelines. --- @@ -43,11 +43,11 @@ Forge characters, not chaos. `forgery` helps you extract equipments (props, clot ## Core Concepts -* **Equipment** — any wearable or attachable asset extracted from images (hat, glasses, sword, jacket). Stored with mask, anchor points, metadata. -* **Character** — a user-created profile representing a base person/portrait used as the generation canvas. -* **Look** — a generated image (or variant) that applies a set of equipments to a character, optionally altered by pose/expression. -* **Extraction** — computer vision process that detects equipment regions and outputs masks, crops, and suggested anchors. -* **Application/Generation** — image generation pipeline that composes character + selected equipments + conditioning (pose/expression) into a final render. +- **Equipment** — any wearable or attachable asset extracted from images (hat, glasses, sword, jacket). Stored with mask, anchor points, metadata. +- **Character** — a user-created profile representing a base person/portrait used as the generation canvas. +- **Look** — a generated image (or variant) that applies a set of equipments to a character, optionally altered by pose/expression. +- **Extraction** — computer vision process that detects equipment regions and outputs masks, crops, and suggested anchors. +- **Application/Generation** — image generation pipeline that composes character + selected equipments + conditioning (pose/expression) into a final render. --- @@ -55,9 +55,9 @@ Forge characters, not chaos. `forgery` helps you extract equipments (props, clot ### Requirements -* [Bun](https://bun.sh) (v1.0.0 or later) -* **OR** [Docker](https://www.docker.com/) -* API Keys for OpenAI or Google Gemini (for AI generation) +- [Bun](https://bun.sh) (v1.0.0 or later) +- **OR** [Docker](https://www.docker.com/) +- API Keys for OpenAI or Google Gemini (for AI generation) ### Local Quickstart @@ -95,17 +95,17 @@ Forgery requires API keys for AI generation services (Google Gemini and/or OpenA A modular pipeline built on a modern stack: 1. **Frontend** (React + Shadcn UI + Tailwind) - * Upload UI, equipment library browser, look composer (Fitting Room). + - Upload UI, equipment library browser, look composer (Fitting Room). 2. **API Server** (Hono on Bun) - * REST API for extraction, equipment CRUD, character CRUD, generation. + - REST API for extraction, equipment CRUD, character CRUD, generation. 3. **Extraction Service** - * Orchestrates AI models (OpenAI/Gemini) to analyze images and generate masks. + - Orchestrates AI models (OpenAI/Gemini) to analyze images and generate masks. 4. **Asset Store** - * Local file system (`data/files`) for storing optimized WebP images. + - Local file system (`data/files`) for storing optimized WebP images. 5. **Generation Service** - * Composes prompts and conditioning for image generation models. + - Composes prompts and conditioning for image generation models. 6. **DB** (SQLite + Prisma) - * Stores metadata, relationships, settings, and generation history. + - Stores metadata, relationships, settings, and generation history. --- @@ -113,9 +113,9 @@ A modular pipeline built on a modern stack: Love forks, fixes, and wild experiments. -* Run tests: `bun test` -* Lint code: `bun run lint` -* Create clear PRs with reproducible steps and small commits. +- Run tests: `bun test` +- Lint code: `bun run lint` +- Create clear PRs with reproducible steps and small commits. --- diff --git a/image-refine-vibe/App.tsx b/image-refine-vibe/App.tsx index e761a67..1c051ad 100644 --- a/image-refine-vibe/App.tsx +++ b/image-refine-vibe/App.tsx @@ -1,63 +1,65 @@ -import React, { useState, useCallback } from 'react'; -import { UploadStage } from './components/UploadStage'; -import { AnalyzeStage } from './components/AnalyzeStage'; -import { SplitStage } from './components/SplitStage'; -import { RefineStage } from './components/RefineStage'; -import { sliceImage } from './utils/imageProcessor'; -import { AppStage, ImageSegment } from './types'; +import React, { useState, useCallback } from 'react' +import { UploadStage } from './components/UploadStage' +import { AnalyzeStage } from './components/AnalyzeStage' +import { SplitStage } from './components/SplitStage' +import { RefineStage } from './components/RefineStage' +import { sliceImage } from './utils/imageProcessor' +import { AppStage, ImageSegment } from './types' const App: React.FC = () => { - const [stage, setStage] = useState('upload'); - const [uploadedImage, setUploadedImage] = useState(null); - const [segments, setSegments] = useState([]); + const [stage, setStage] = useState('upload') + const [uploadedImage, setUploadedImage] = useState(null) + const [segments, setSegments] = useState([]) // 1. Handle Upload const handleImageUpload = useCallback((imageDataUrl: string) => { - setUploadedImage(imageDataUrl); - setStage('analyze'); - }, []); + setUploadedImage(imageDataUrl) + setStage('analyze') + }, []) // 2. Handle Analysis Completion const handleAnalysisComplete = useCallback(async () => { - if (!uploadedImage) return; - + if (!uploadedImage) return + try { // Slice image into 3x3 grid - const slicedImages = await sliceImage(uploadedImage, 3, 3); - + const slicedImages = await sliceImage(uploadedImage, 3, 3) + const newSegments: ImageSegment[] = slicedImages.map((src, index) => ({ id: `seg-${index}`, src, selected: true, // Default all selected - status: 'pending' - })); + status: 'pending', + })) - setSegments(newSegments); - setStage('split'); + setSegments(newSegments) + setStage('split') } catch (error) { - console.error("Failed to slice image", error); + console.error('Failed to slice image', error) // Fallback or error handling could go here } - }, [uploadedImage]); + }, [uploadedImage]) // 3. Handle Split Confirmation - const handleSplitConfirm = useCallback((selectedIds: string[]) => { - // Filter out unselected items for the refine view - const selectedSegments = segments.filter(seg => selectedIds.includes(seg.id)); - setSegments(selectedSegments); - setStage('refine'); - }, [segments]); + const handleSplitConfirm = useCallback( + (selectedIds: string[]) => { + // Filter out unselected items for the refine view + const selectedSegments = segments.filter((seg) => selectedIds.includes(seg.id)) + setSegments(selectedSegments) + setStage('refine') + }, + [segments], + ) // 4. Reset const handleReset = useCallback(() => { - setUploadedImage(null); - setSegments([]); - setStage('upload'); - }, []); + setUploadedImage(null) + setSegments([]) + setStage('upload') + }, []) return (
- {/* Background Ambience */}
@@ -67,56 +69,56 @@ const App: React.FC = () => {
{/* Navigation / Header (Minimal) */}
-
-
- AI -
-

RefineLab

-
- - {/* Progress Steps */} - {stage !== 'upload' && ( -
-
Analyze
-
-
Split
-
-
Refine
-
- )} +
+
+ AI +
+

+ RefineLab +

+
+ + {/* Progress Steps */} + {stage !== 'upload' && ( +
+
+ Analyze +
+
+
+ Split +
+
+
+ Refine +
+
+ )}
{/* Content Area */}
- {stage === 'upload' && ( - - )} + {stage === 'upload' && } {stage === 'analyze' && uploadedImage && ( - + )} {stage === 'split' && ( - + )} - {stage === 'refine' && ( - - )} + {stage === 'refine' && }
- ); -}; + ) +} -export default App; \ No newline at end of file +export default App diff --git a/image-refine-vibe/README.md b/image-refine-vibe/README.md index 9da58b0..59825b0 100644 --- a/image-refine-vibe/README.md +++ b/image-refine-vibe/README.md @@ -10,8 +10,7 @@ View your app in AI Studio: https://ai.studio/apps/drive/1P0JLZhnyDHgV03_9rLIzEi ## Run Locally -**Prerequisites:** Node.js - +**Prerequisites:** Node.js 1. Install dependencies: `npm install` diff --git a/image-refine-vibe/components/AnalyzeStage.tsx b/image-refine-vibe/components/AnalyzeStage.tsx index ae4d1e2..b9ba34e 100644 --- a/image-refine-vibe/components/AnalyzeStage.tsx +++ b/image-refine-vibe/components/AnalyzeStage.tsx @@ -1,24 +1,24 @@ -import React, { useEffect } from 'react'; -import { Scan, BrainCircuit } from 'lucide-react'; +import React, { useEffect } from 'react' +import { Scan, BrainCircuit } from 'lucide-react' interface AnalyzeStageProps { - imageSrc: string; - onAnalysisComplete: () => void; + imageSrc: string + onAnalysisComplete: () => void } export const AnalyzeStage: React.FC = ({ imageSrc, onAnalysisComplete }) => { useEffect(() => { const timer = setTimeout(() => { - onAnalysisComplete(); - }, 5000); + onAnalysisComplete() + }, 5000) - return () => clearTimeout(timer); - }, [onAnalysisComplete]); + return () => clearTimeout(timer) + }, [onAnalysisComplete]) return (
{/* Background Frosted Glass Layer */} -
= ({ imageSrc, onAnalysis backgroundSize: 'cover', }} /> - + {/* Main Container */}
- {/* Image Container with Scanning Effect */}
- Analyzing - + Analyzing + {/* Scan Line Animation */}
- + {/* Grid Overlay Effect (Subtle) */}
@@ -54,17 +49,14 @@ export const AnalyzeStage: React.FC = ({ imageSrc, onAnalysis {/* Status Text */}
- - Analyzing Structure... - + Analyzing Structure...
-
- ); -}; \ No newline at end of file + ) +} diff --git a/image-refine-vibe/components/RefineStage.tsx b/image-refine-vibe/components/RefineStage.tsx index e237f4e..f80620a 100644 --- a/image-refine-vibe/components/RefineStage.tsx +++ b/image-refine-vibe/components/RefineStage.tsx @@ -1,49 +1,49 @@ -import React, { useEffect, useState, useRef } from 'react'; -import { ImageSegment } from '../types'; -import { Loader2, CheckCircle2, Sparkles, RefreshCw } from 'lucide-react'; +import React, { useEffect, useState, useRef } from 'react' +import { ImageSegment } from '../types' +import { Loader2, CheckCircle2, Sparkles, RefreshCw } from 'lucide-react' interface RefineStageProps { - initialSegments: ImageSegment[]; - onDone: () => void; + initialSegments: ImageSegment[] + onDone: () => void } export const RefineStage: React.FC = ({ initialSegments, onDone }) => { - const [segments, setSegments] = useState(initialSegments); - const [processingIndex, setProcessingIndex] = useState(0); - const [isAllDone, setIsAllDone] = useState(false); - const processingRef = useRef(false); + const [segments, setSegments] = useState(initialSegments) + const [processingIndex, setProcessingIndex] = useState(0) + const [isAllDone, setIsAllDone] = useState(false) + const processingRef = useRef(false) useEffect(() => { // We only process segments that were selected - const selectedSegments = segments.filter(s => s.selected); - + const selectedSegments = segments.filter((s) => s.selected) + if (processingIndex >= selectedSegments.length) { - setIsAllDone(true); - return; + setIsAllDone(true) + return } - if (processingRef.current) return; - processingRef.current = true; + if (processingRef.current) return + processingRef.current = true - const currentSegmentId = selectedSegments[processingIndex].id; + const currentSegmentId = selectedSegments[processingIndex].id // Set current to processing - setSegments(prev => prev.map(s => - s.id === currentSegmentId ? { ...s, status: 'processing' } : s - )); + setSegments((prev) => + prev.map((s) => (s.id === currentSegmentId ? { ...s, status: 'processing' } : s)), + ) const processTimer = setTimeout(() => { // Mark as completed and move next - setSegments(prev => prev.map(s => - s.id === currentSegmentId ? { ...s, status: 'completed' } : s - )); - - setProcessingIndex(prev => prev + 1); - processingRef.current = false; - }, 2000); + setSegments((prev) => + prev.map((s) => (s.id === currentSegmentId ? { ...s, status: 'completed' } : s)), + ) - return () => clearTimeout(processTimer); - }, [processingIndex, segments]); + setProcessingIndex((prev) => prev + 1) + processingRef.current = false + }, 2000) + + return () => clearTimeout(processTimer) + }, [processingIndex, segments]) return (
@@ -55,13 +55,13 @@ export const RefineStage: React.FC = ({ initialSegments, onDon Refining Images

- {isAllDone - ? "All selected segments have been processed successfully." - : `Processing item ${processingIndex + 1} of ${segments.length}...`} + {isAllDone + ? 'All selected segments have been processed successfully.' + : `Processing item ${processingIndex + 1} of ${segments.length}...`}

{isAllDone && ( - - + + > + Start Refining + +
- ); -}; \ No newline at end of file + ) +} diff --git a/image-refine-vibe/components/UploadStage.tsx b/image-refine-vibe/components/UploadStage.tsx index e9dbc27..2a0c71b 100644 --- a/image-refine-vibe/components/UploadStage.tsx +++ b/image-refine-vibe/components/UploadStage.tsx @@ -1,61 +1,69 @@ -import React, { useCallback, useState } from 'react'; -import { Upload, Image as ImageIcon } from 'lucide-react'; -import { readFileAsDataURL } from '../utils/imageProcessor'; +import React, { useCallback, useState } from 'react' +import { Upload, Image as ImageIcon } from 'lucide-react' +import { readFileAsDataURL } from '../utils/imageProcessor' interface UploadStageProps { - onImageUpload: (imageDataUrl: string) => void; + onImageUpload: (imageDataUrl: string) => void } export const UploadStage: React.FC = ({ onImageUpload }) => { - const [isDragging, setIsDragging] = useState(false); + const [isDragging, setIsDragging] = useState(false) const handleDragOver = useCallback((e: React.DragEvent) => { - e.preventDefault(); - setIsDragging(true); - }, []); + e.preventDefault() + setIsDragging(true) + }, []) const handleDragLeave = useCallback((e: React.DragEvent) => { - e.preventDefault(); - setIsDragging(false); - }, []); + e.preventDefault() + setIsDragging(false) + }, []) - const handleDrop = useCallback(async (e: React.DragEvent) => { - e.preventDefault(); - setIsDragging(false); - - if (e.dataTransfer.files && e.dataTransfer.files[0]) { - const file = e.dataTransfer.files[0]; - if (file.type.startsWith('image/')) { - try { - const dataUrl = await readFileAsDataURL(file); - onImageUpload(dataUrl); - } catch (error) { - console.error("Error reading file:", error); + const handleDrop = useCallback( + async (e: React.DragEvent) => { + e.preventDefault() + setIsDragging(false) + + if (e.dataTransfer.files && e.dataTransfer.files[0]) { + const file = e.dataTransfer.files[0] + if (file.type.startsWith('image/')) { + try { + const dataUrl = await readFileAsDataURL(file) + onImageUpload(dataUrl) + } catch (error) { + console.error('Error reading file:', error) + } } } - } - }, [onImageUpload]); + }, + [onImageUpload], + ) - const handleFileChange = useCallback(async (e: React.ChangeEvent) => { - if (e.target.files && e.target.files[0]) { - try { - const dataUrl = await readFileAsDataURL(e.target.files[0]); - onImageUpload(dataUrl); - } catch (error) { - console.error("Error reading file:", error); + const handleFileChange = useCallback( + async (e: React.ChangeEvent) => { + if (e.target.files && e.target.files[0]) { + try { + const dataUrl = await readFileAsDataURL(e.target.files[0]) + onImageUpload(dataUrl) + } catch (error) { + console.error('Error reading file:', error) + } } - } - }, [onImageUpload]); + }, + [onImageUpload], + ) return (
-
= ({ onImageUpload }) => {
-

- Upload an Image -

+

Upload an Image

Drag and drop your image here, or click to browse files

-

- Supports JPG, PNG, WEBP up to 10MB -

+

Supports JPG, PNG, WEBP up to 10MB

- ); -}; \ No newline at end of file + ) +} diff --git a/image-refine-vibe/index.html b/image-refine-vibe/index.html index 0c10507..f16903e 100644 --- a/image-refine-vibe/index.html +++ b/image-refine-vibe/index.html @@ -1,4 +1,4 @@ - + @@ -7,7 +7,7 @@ - - - + + +
- \ No newline at end of file + diff --git a/image-refine-vibe/index.tsx b/image-refine-vibe/index.tsx index 6ca5361..ed2c62f 100644 --- a/image-refine-vibe/index.tsx +++ b/image-refine-vibe/index.tsx @@ -1,15 +1,15 @@ -import React from 'react'; -import ReactDOM from 'react-dom/client'; -import App from './App'; +import React from 'react' +import ReactDOM from 'react-dom/client' +import App from './App' -const rootElement = document.getElementById('root'); +const rootElement = document.getElementById('root') if (!rootElement) { - throw new Error("Could not find root element to mount to"); + throw new Error('Could not find root element to mount to') } -const root = ReactDOM.createRoot(rootElement); +const root = ReactDOM.createRoot(rootElement) root.render( - -); \ No newline at end of file + , +) diff --git a/image-refine-vibe/metadata.json b/image-refine-vibe/metadata.json index 9763a92..1e4996d 100644 --- a/image-refine-vibe/metadata.json +++ b/image-refine-vibe/metadata.json @@ -2,4 +2,4 @@ "name": "Image Split & Refine Tool", "description": "A sophisticated image processing tool that analyzes, splits, and refines images with a modern frosted glass UI.", "requestFramePermissions": [] -} \ No newline at end of file +} diff --git a/image-refine-vibe/tsconfig.json b/image-refine-vibe/tsconfig.json index 2c6eed5..f9d60ed 100644 --- a/image-refine-vibe/tsconfig.json +++ b/image-refine-vibe/tsconfig.json @@ -4,26 +4,18 @@ "experimentalDecorators": true, "useDefineForClassFields": false, "module": "ESNext", - "lib": [ - "ES2022", - "DOM", - "DOM.Iterable" - ], + "lib": ["ES2022", "DOM", "DOM.Iterable"], "skipLibCheck": true, - "types": [ - "node" - ], + "types": ["node"], "moduleResolution": "bundler", "isolatedModules": true, "moduleDetection": "force", "allowJs": true, "jsx": "react-jsx", "paths": { - "@/*": [ - "./*" - ] + "@/*": ["./*"] }, "allowImportingTsExtensions": true, "noEmit": true } -} \ No newline at end of file +} diff --git a/image-refine-vibe/types.ts b/image-refine-vibe/types.ts index 82de554..6e1a92e 100644 --- a/image-refine-vibe/types.ts +++ b/image-refine-vibe/types.ts @@ -1,13 +1,13 @@ -export type AppStage = 'upload' | 'analyze' | 'split' | 'refine' | 'completed'; +export type AppStage = 'upload' | 'analyze' | 'split' | 'refine' | 'completed' export interface ImageSegment { - id: string; - src: string; - selected: boolean; - status: 'pending' | 'processing' | 'completed'; + id: string + src: string + selected: boolean + status: 'pending' | 'processing' | 'completed' } export interface ProcessingState { - isProcessing: boolean; - currentId: string | null; -} \ No newline at end of file + isProcessing: boolean + currentId: string | null +} diff --git a/image-refine-vibe/utils/imageProcessor.ts b/image-refine-vibe/utils/imageProcessor.ts index 41a0d9b..d6159ab 100644 --- a/image-refine-vibe/utils/imageProcessor.ts +++ b/image-refine-vibe/utils/imageProcessor.ts @@ -5,31 +5,35 @@ * @param cols Number of columns. * @returns A promise resolving to an array of data URLs for the segments. */ -export const sliceImage = (imageSrc: string, rows: number = 3, cols: number = 3): Promise => { +export const sliceImage = ( + imageSrc: string, + rows: number = 3, + cols: number = 3, +): Promise => { return new Promise((resolve, reject) => { - const img = new Image(); - img.crossOrigin = "Anonymous"; + const img = new Image() + img.crossOrigin = 'Anonymous' img.onload = () => { - const segments: string[] = []; - const segmentWidth = Math.floor(img.width / cols); - const segmentHeight = Math.floor(img.height / rows); + const segments: string[] = [] + const segmentWidth = Math.floor(img.width / cols) + const segmentHeight = Math.floor(img.height / rows) - const canvas = document.createElement('canvas'); - const ctx = canvas.getContext('2d'); + const canvas = document.createElement('canvas') + const ctx = canvas.getContext('2d') if (!ctx) { - reject(new Error("Could not get canvas context")); - return; + reject(new Error('Could not get canvas context')) + return } - canvas.width = segmentWidth; - canvas.height = segmentHeight; + canvas.width = segmentWidth + canvas.height = segmentHeight for (let y = 0; y < rows; y++) { for (let x = 0; x < cols; x++) { // Clear canvas for next segment - ctx.clearRect(0, 0, segmentWidth, segmentHeight); - + ctx.clearRect(0, 0, segmentWidth, segmentHeight) + // Draw the specific slice ctx.drawImage( img, @@ -40,33 +44,33 @@ export const sliceImage = (imageSrc: string, rows: number = 3, cols: number = 3) 0, 0, segmentWidth, - segmentHeight - ); + segmentHeight, + ) - segments.push(canvas.toDataURL('image/png')); + segments.push(canvas.toDataURL('image/png')) } } - resolve(segments); - }; - img.onerror = (err) => reject(err); - img.src = imageSrc; - }); -}; + resolve(segments) + } + img.onerror = (err) => reject(err) + img.src = imageSrc + }) +} /** * Reads a file input as a Data URL. */ export const readFileAsDataURL = (file: File): Promise => { return new Promise((resolve, reject) => { - const reader = new FileReader(); + const reader = new FileReader() reader.onload = (e) => { if (e.target?.result) { - resolve(e.target.result as string); + resolve(e.target.result as string) } else { - reject(new Error("Failed to read file")); + reject(new Error('Failed to read file')) } - }; - reader.onerror = reject; - reader.readAsDataURL(file); - }); -}; \ No newline at end of file + } + reader.onerror = reject + reader.readAsDataURL(file) + }) +} diff --git a/image-refine-vibe/vite.config.ts b/image-refine-vibe/vite.config.ts index ee5fb8d..11827f5 100644 --- a/image-refine-vibe/vite.config.ts +++ b/image-refine-vibe/vite.config.ts @@ -1,23 +1,23 @@ -import path from 'path'; -import { defineConfig, loadEnv } from 'vite'; -import react from '@vitejs/plugin-react'; +import path from 'path' +import { defineConfig, loadEnv } from 'vite' +import react from '@vitejs/plugin-react' export default defineConfig(({ mode }) => { - const env = loadEnv(mode, '.', ''); - return { - server: { - port: 3000, - host: '0.0.0.0', + const env = loadEnv(mode, '.', '') + return { + server: { + port: 3000, + host: '0.0.0.0', + }, + plugins: [react()], + define: { + 'process.env.API_KEY': JSON.stringify(env.GEMINI_API_KEY), + 'process.env.GEMINI_API_KEY': JSON.stringify(env.GEMINI_API_KEY), + }, + resolve: { + alias: { + '@': path.resolve(__dirname, '.'), }, - plugins: [react()], - define: { - 'process.env.API_KEY': JSON.stringify(env.GEMINI_API_KEY), - 'process.env.GEMINI_API_KEY': JSON.stringify(env.GEMINI_API_KEY) - }, - resolve: { - alias: { - '@': path.resolve(__dirname, '.'), - } - } - }; -}); + }, + } +}) diff --git a/src/service/equipment.test.ts b/src/service/equipment.test.ts index 6dcec77..173dcbe 100644 --- a/src/service/equipment.test.ts +++ b/src/service/equipment.test.ts @@ -92,7 +92,9 @@ describe('EquipmentService', () => { await equipmentService.deleteEquipment('1') expect(mockPrisma.equipment.findUnique).toHaveBeenCalledWith({ where: { id: '1' } }) - expect(mockPrisma.generationEquipment.deleteMany).toHaveBeenCalledWith({ where: { equipmentId: '1' } }) + expect(mockPrisma.generationEquipment.deleteMany).toHaveBeenCalledWith({ + where: { equipmentId: '1' }, + }) expect(mockPrisma.equipment.delete).toHaveBeenCalledWith({ where: { id: '1' } }) expect(mockAssetService.deleteAsset).toHaveBeenCalledWith('asset-1') }) diff --git a/src/ui/components/ExtractorDialog.tsx b/src/ui/components/ExtractorDialog.tsx index 6bfbebc..c6a476d 100644 --- a/src/ui/components/ExtractorDialog.tsx +++ b/src/ui/components/ExtractorDialog.tsx @@ -1,9 +1,12 @@ -import { AlertCircle, Check, Loader2 } from 'lucide-react' -import React, { useEffect, useState } from 'react' +import React, { useEffect, useRef, useState } from 'react' +import { toast } from 'sonner' import { client } from '../client' -import { CandidateAsset, ExtractedAsset, ExtractionStatus } from '../types' -import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from './ui/dialog' -import { cn } from '../lib/utils' +import { CandidateAsset, ExtractedAsset } from '../types' +import { AnalyzeStage } from './extractor/AnalyzeStage' +import { RefineStage } from './extractor/RefineStage' +import { SelectionStage } from './extractor/SelectionStage' +import { UploadStage } from './extractor/UploadStage' +import { Dialog, DialogContent, DialogTitle } from './ui/dialog' interface ExtractorDialogProps { open: boolean @@ -11,153 +14,147 @@ interface ExtractorDialogProps { onSuccess: (assets: ExtractedAsset[]) => void } -interface GridDimensions { - rows: number - cols: number -} +type Stage = 'upload' | 'analyze' | 'selection' | 'refine' export const ExtractorDialog: React.FC = ({ open, onOpenChange, onSuccess, }) => { + const [stage, setStage] = useState('upload') const [file, setFile] = useState(null) const [preview, setPreview] = useState(null) - const [status, setStatus] = useState('idle') const [statusMessage, setStatusMessage] = useState('') - const [error, setError] = useState(null) - // Intermediate candidates + // Data const [candidates, setCandidates] = useState([]) const [selectedIndices, setSelectedIndices] = useState([]) - - // Final results const [results, setResults] = useState([]) + const [isRefineComplete, setIsRefineComplete] = useState(false) - const [textureSheet, setTextureSheet] = useState(null) - const [gridDimensions, setGridDimensions] = useState(null) + // Refs for cleanup + const analyzeController = useRef(null) + const refineController = useRef(null) - const processFile = (f: File) => { - setFile(f) - setPreview(URL.createObjectURL(f)) - setResults([]) + const resetState = () => { + setStage('upload') + setFile(null) + if (preview) URL.revokeObjectURL(preview) + setPreview(null) + setStatusMessage('') setCandidates([]) setSelectedIndices([]) - setError(null) - setStatus('idle') - setTextureSheet(null) - setGridDimensions(null) - } - - const handleFileChange = (e: React.ChangeEvent) => { - if (e.target.files && e.target.files[0]) { - processFile(e.target.files[0]) - } + setResults([]) + setIsRefineComplete(false) + analyzeController.current?.abort() + refineController.current?.abort() } + // Handle dialog close useEffect(() => { - const handlePaste = (e: ClipboardEvent) => { - if (!open) return - if (e.clipboardData && e.clipboardData.files.length > 0) { - const f = e.clipboardData.files[0] - if (f.type.startsWith('image/')) { - processFile(f) - } - } + if (!open) { + // Small delay to allow animation to finish if needed, or just reset immediately + // resetting immediately might flash content if closing animation plays + // But for now, we reset on close to ensure fresh state next open + const t = setTimeout(resetState, 300) + return () => clearTimeout(t) } - - window.addEventListener('paste', handlePaste) - return () => window.removeEventListener('paste', handlePaste) }, [open]) - const handleAnalyze = async () => { - if (!file) return + const handleImageUpload = (f: File) => { + setFile(f) + setPreview(URL.createObjectURL(f)) + setStage('analyze') + } - setCandidates([]) - setResults([]) - setError(null) - setStatus('analyzing') - setStatusMessage('Starting analysis...') - setTextureSheet(null) - setGridDimensions(null) + // Auto-start analysis when entering analyze stage + useEffect(() => { + let active = true + const analyze = async () => { + if (!file) return - try { - const response = await client.extract.analyze.$post({ - form: { - image: file, - }, - }) + setStatusMessage('Initializing analysis...') + analyzeController.current = new AbortController() - if (!response.ok) throw new Error('Server error') - if (!response.body) throw new Error('No response body') + try { + const response = await client.extract.analyze.$post({ + form: { image: file }, + }) - const reader = response.body.getReader() - const decoder = new TextDecoder() - let buffer = '' + if (!response.ok) throw new Error('Server error') + if (!response.body) throw new Error('No response body') - while (true) { - const { done, value } = await reader.read() - if (done) break + const reader = response.body.getReader() + const decoder = new TextDecoder() + let buffer = '' - buffer += decoder.decode(value, { stream: true }) - const lines = buffer.split('\n\n') - buffer = lines.pop() || '' + while (active) { + const { done, value } = await reader.read() + if (done) break - for (const line of lines) { - if (line.startsWith('event: ')) { - const eventMatch = line.match(/^event: (.+)$/m) - const dataMatch = line.match(/^data: (.+)$/m) + buffer += decoder.decode(value, { stream: true }) + const lines = buffer.split('\n\n') + buffer = lines.pop() || '' - if (eventMatch && dataMatch) { - const event = eventMatch[1].trim() - const data = JSON.parse(dataMatch[1]) + for (const line of lines) { + if (line.startsWith('event: ')) { + const eventMatch = line.match(/^event: (.+)$/m) + const dataMatch = line.match(/^data: (.+)$/m) - if (event === 'status') { - setStatus(data.status) - setStatusMessage(data.message) - } else if (event === 'texture_generated') { - setTextureSheet(data.image) - setGridDimensions(data.grid) - } else if (event === 'complete') { - setStatus('selection') - setStatusMessage('Select items to refine') - const assets = data.assets as CandidateAsset[] - setCandidates(assets) - // Select all by default - setSelectedIndices(assets.map((_, i) => i)) - } else if (event === 'error') { - throw new Error(data.message) + if (eventMatch && dataMatch) { + const event = eventMatch[1].trim() + const data = JSON.parse(dataMatch[1]) + + if (event === 'status') { + if (active) setStatusMessage(data.message) + } else if (event === 'complete') { + if (active) { + setCandidates(data.assets as CandidateAsset[]) + setStage('selection') + } + } else if (event === 'error') { + throw new Error(data.message) + } } } } } + } catch (e: unknown) { + if (active) { + console.error(e) + const err = e instanceof Error ? e : new Error(String(e)) + toast.error(err.message || 'Analysis failed') + setStage('upload') + } } - } catch (e: unknown) { - console.error(e) - const err = e instanceof Error ? e : new Error(String(e)) - setError(err.message || 'Analysis failed') - setStatus('error') } + + if (stage === 'analyze' && file) { + analyze() + } + + return () => { + active = false + analyzeController.current?.abort() + } + }, [stage, file]) + + const handleRefineConfirm = (indices: number[]) => { + setSelectedIndices(indices) + setStage('refine') + handleRefine(indices) } - const handleRefine = async () => { - if (selectedIndices.length === 0) { - setError('Please select at least one item to refine.') - return - } + const handleRefine = async (indices: number[]) => { + const selectedCandidates = candidates.filter((_, i) => indices.includes(i)) - const selectedCandidates = candidates.filter((_, i) => selectedIndices.includes(i)) - - setStatus('refining') - setStatusMessage('Refining selected assets...') setResults([]) - setError(null) + setIsRefineComplete(false) + setStatusMessage('Starting refinement...') try { const response = await client.extract.refine.$post({ - json: { - assets: selectedCandidates, - }, + json: { assets: selectedCandidates }, }) if (!response.ok) throw new Error('Server error') @@ -185,14 +182,11 @@ export const ExtractorDialog: React.FC = ({ const data = JSON.parse(dataMatch[1]) if (event === 'status') { - setStatus(data.status) setStatusMessage(data.message) } else if (event === 'asset_refined') { setResults((prev) => [...prev, data.asset]) } else if (event === 'complete') { - setStatus('complete') - setStatusMessage('Refinement complete!') - setResults(data.assets) + setIsRefineComplete(true) onSuccess(data.assets) } else if (event === 'error') { throw new Error(data.message) @@ -204,397 +198,78 @@ export const ExtractorDialog: React.FC = ({ } catch (e: unknown) { console.error(e) const err = e instanceof Error ? e : new Error(String(e)) - setError(err.message || 'Refinement failed') - setStatus('error') + toast.error(err.message || 'Refinement failed') + // Don't change stage automatically on error here, let user see what happened or retry? + // For now, staying on refine stage allows them to see partial results. + // But we might want a 'retry' button or back. } } - const toggleSelection = (index: number) => { - setSelectedIndices((prev) => - prev.includes(index) ? prev.filter((i) => i !== index) : [...prev, index], - ) - } - - const handleReset = () => { - setFile(null) - if (preview) URL.revokeObjectURL(preview) - setPreview(null) - setCandidates([]) - setResults([]) - setSelectedIndices([]) - setStatus('idle') - setError(null) - setTextureSheet(null) - setGridDimensions(null) - } - return ( - - - - Equipment Extractor - - - Upload a character image to identify and extract equipment sprites. - - + + Equipment Extractor -
- {/* Upload Area */} - {!file ? ( -
- -
- - - -
-

Upload Source

-

- Drop character image to identify and extract equipment sprites. -

- - Select Image - + {/* Top Navigation / Branding Bar (Optional, matches App feel) */} +
+
+
+ AI
- ) : ( -
- {/* Staging Area */} -
-
+

+ RefineLab +

+
-
-
-

- - Staging Area -

-
- -
- -
- {/* Source Image */} -
-
- -
- - Source - -
-
-
- - {/* Generated Texture Sheet (with Grid Overlay) */} - {textureSheet && gridDimensions && ( -
-
- - - {/* Grid Overlay */} -
- {Array.from({ length: gridDimensions.rows * gridDimensions.cols }).map( - (_, i) => ( -
- {i + 1} -
- ), - )} -
- -
- - Generated Sheet - -
-
-
- )} - - {/* Controls & Status */} -
- {status === 'idle' || status === 'error' ? ( -
-

Ready to Process

-

- The image is ready for analysis. The system will identify equipment pieces - and extract them as individual assets. -

- {error && ( -
- -
- Extraction Failed - {error} -
-
- )} - -
- ) : ( -
- {/* Progress Status */} - {status !== 'selection' && ( -
- -

- {statusMessage} -

-

- Processing... -

-
- )} - - {/* Selection Step Actions */} - {status === 'selection' && ( -
-

- Review & Select -

-

- Select the items you want to keep. They will be refined and added to - your collection. -

- -
- )} -
- )} - -
-

- Powered by Gemini 2.0 Flash -

-
-
-
+ {/* Progress Steps */} + {stage !== 'upload' && ( +
+
+ Analyze +
+
+
+ Select +
+
+
+ Refine
- - {/* Selection Grid */} - {status === 'selection' && candidates.length > 0 && ( -
-
-
-

- ✦ Candidates Found -

-

- Select items to refine -

-
-
- - | - -
-
- -
- {candidates.map((item, i) => { - const isSelected = selectedIndices.includes(i) - return ( -
toggleSelection(i)} - className={cn( - 'group relative rounded-lg border-2 cursor-pointer transition-all duration-300', - isSelected - ? 'bg-stone-50 border-amber-500 shadow-md scale-[1.02]' - : 'bg-white border-stone-100 opacity-60 hover:opacity-100 hover:border-stone-300', - )} - > - {/* Selection Checkbox Overlay */} -
- -
- -
- {/* Checkerboard bg */} -
- -
-
-
- {item.name} -
-
- {item.category} -
-
-
- ) - })} -
-
- )} - - {/* Final Results Grid */} - {(results.length > 0 || status === 'complete') && ( -
-
-
-

- ✦ Loot Acquired -

-

- {results.length} items refined & stashed -

-
-
- -
- {results.map((item, i) => ( -
-
- {/* Checkerboard bg */} -
- - - {/* Download Overlay */} - - - - - -
-
-
- {item.name} -
-
- {item.category} -
-
-
-
- ))} -
-
- )}
)}
+ + {/* Content Area */} +
+ {stage === 'upload' && } + + {stage === 'analyze' && preview && ( + + )} + + {stage === 'selection' && ( + setStage('upload')} + /> + )} + + {stage === 'refine' && ( + selectedIndices.includes(i))} + results={results} + isComplete={isRefineComplete} + onDone={resetState} + /> + )} +
) diff --git a/src/ui/components/extractor/AnalyzeStage.tsx b/src/ui/components/extractor/AnalyzeStage.tsx new file mode 100644 index 0000000..3fde754 --- /dev/null +++ b/src/ui/components/extractor/AnalyzeStage.tsx @@ -0,0 +1,66 @@ +import { BrainCircuit } from 'lucide-react' +import React from 'react' + +interface AnalyzeStageProps { + imageSrc: string + statusMessage: string +} + +export const AnalyzeStage: React.FC = ({ imageSrc, statusMessage }) => { + return ( +
+ {/* Background Frosted Glass Layer */} +
+ + {/* Main Container */} +
+ {/* Image Container with Scanning Effect */} +
+ Analyzing + + {/* Scan Line Animation */} +
+
+
+ + {/* Grid Overlay Effect (Subtle) */} +
+ + {/* Corner Brackets UI */} +
+
+
+
+
+ + {/* Status Text */} +
+ + {statusMessage} +
+
+
+
+
+
+
+ +
+ ) +} diff --git a/src/ui/components/extractor/RefineStage.tsx b/src/ui/components/extractor/RefineStage.tsx new file mode 100644 index 0000000..8145fed --- /dev/null +++ b/src/ui/components/extractor/RefineStage.tsx @@ -0,0 +1,161 @@ +import { CheckCircle2, Loader2, RefreshCw, Sparkles } from 'lucide-react' +import React from 'react' +import { cn } from '../../lib/utils' +import { CandidateAsset, ExtractedAsset } from '../../types' + +interface RefineStageProps { + selectedCandidates: CandidateAsset[] + results: ExtractedAsset[] + isComplete: boolean + onDone: () => void +} + +export const RefineStage: React.FC = ({ + selectedCandidates, + results, + isComplete, + onDone, +}) => { + return ( +
+ {/* Header */} +
+
+

+ + Refining Assets +

+

+ {isComplete + ? 'All selected assets have been refined and saved.' + : `Processing item ${Math.min(results.length + 1, selectedCandidates.length)} of ${selectedCandidates.length}...`} +

+
+ {isComplete && ( + + )} +
+ + {/* Grid */} +
+
+ {selectedCandidates.map((candidate, index) => { + const isFinished = index < results.length + const isProcessing = index === results.length && !isComplete + const isPending = index > results.length + + // The asset to display: result if finished, candidate base64 otherwise + const displayImage = isFinished ? results[index].imageUrl : candidate.base64 + + return ( +
+ {/* Background Checkerboard */} +
+ + {candidate.name} + + {/* Overlay States */} +
+ {isPending && ( +
+ Pending +
+ )} + + {isProcessing && ( +
+
+
+
+ +
+
+ + Refining... + +
+ )} + + {isFinished && ( +
+
+ +
+
+ )} +
+ + {/* Scanning effect for processing item only */} + {isProcessing && ( +
+
+
+ )} + + {/* Info Bar */} +
+

+ {isFinished ? results[index].name : candidate.name} +

+

+ {candidate.category} +

+
+
+ ) + })} +
+
+ +
+ ) +} diff --git a/src/ui/components/extractor/SelectionStage.tsx b/src/ui/components/extractor/SelectionStage.tsx new file mode 100644 index 0000000..cdf24af --- /dev/null +++ b/src/ui/components/extractor/SelectionStage.tsx @@ -0,0 +1,139 @@ +import { ArrowRight, Check, Grid2X2 } from 'lucide-react' +import React, { useState } from 'react' +import { cn } from '../../lib/utils' +import { CandidateAsset } from '../../types' + +interface SelectionStageProps { + candidates: CandidateAsset[] + onConfirm: (selectedIndices: number[]) => void + onCancel: () => void +} + +export const SelectionStage: React.FC = ({ + candidates, + onConfirm, + onCancel, +}) => { + // Initialize with all selected by default + const [selectedIndices, setSelectedIndices] = useState(candidates.map((_, i) => i)) + + const toggleSelection = (index: number) => { + setSelectedIndices((prev) => + prev.includes(index) ? prev.filter((i) => i !== index) : [...prev, index], + ) + } + + const handleConfirm = () => { + if (selectedIndices.length === 0) return + onConfirm(selectedIndices) + } + + return ( +
+ {/* Header */} +
+
+

+ + Select Candidates +

+

+ Unselect any items you wish to exclude from refinement. +

+
+
+ {selectedIndices.length} + Selected +
+
+ + {/* Grid */} +
+
+ {candidates.map((item, index) => { + const isSelected = selectedIndices.includes(index) + return ( +
toggleSelection(index)} + className={cn( + 'group relative aspect-square rounded-xl overflow-hidden cursor-pointer transition-all duration-300 border-2 bg-white animate-fade-in-up', + isSelected + ? 'border-cyan-500 shadow-lg shadow-cyan-100 scale-[1.02]' + : 'border-slate-200 opacity-80 hover:opacity-100 hover:border-slate-300', + )} + style={{ animationDelay: `${index * 50}ms` }} + > + {/* Checkerboard bg */} +
+ + {item.name} + + {/* Selection Overlay */} +
+ + {/* Checkbox Indicator */} +
+ +
+ + {/* Label */} +
+

{item.name}

+

{item.category}

+
+
+ ) + })} +
+
+ + {/* Footer Actions */} +
+
+ + +
+
+
+ ) +} diff --git a/src/ui/components/extractor/UploadStage.tsx b/src/ui/components/extractor/UploadStage.tsx new file mode 100644 index 0000000..0e7a85e --- /dev/null +++ b/src/ui/components/extractor/UploadStage.tsx @@ -0,0 +1,123 @@ +import { Image as ImageIcon, Upload } from 'lucide-react' +import React, { useCallback, useState } from 'react' +import { cn } from '../../lib/utils' + +interface UploadStageProps { + onImageUpload: (file: File) => void +} + +export const UploadStage: React.FC = ({ onImageUpload }) => { + const [isDragging, setIsDragging] = useState(false) + + const handleDragOver = useCallback((e: React.DragEvent) => { + e.preventDefault() + setIsDragging(true) + }, []) + + const handleDragLeave = useCallback((e: React.DragEvent) => { + e.preventDefault() + setIsDragging(false) + }, []) + + const handleDrop = useCallback( + (e: React.DragEvent) => { + e.preventDefault() + setIsDragging(false) + + if (e.dataTransfer.files && e.dataTransfer.files[0]) { + const file = e.dataTransfer.files[0] + if (file.type.startsWith('image/')) { + onImageUpload(file) + } + } + }, + [onImageUpload], + ) + + const handleFileChange = useCallback( + (e: React.ChangeEvent) => { + if (e.target.files && e.target.files[0]) { + onImageUpload(e.target.files[0]) + } + }, + [onImageUpload], + ) + + React.useEffect(() => { + const handlePaste = (e: ClipboardEvent) => { + if (e.clipboardData && e.clipboardData.files.length > 0) { + const file = e.clipboardData.files[0] + if (file.type.startsWith('image/')) { + onImageUpload(file) + } + } + } + + window.addEventListener('paste', handlePaste) + return () => window.removeEventListener('paste', handlePaste) + }, [onImageUpload]) + + return ( +
+
document.getElementById('file-upload')?.click()} + > +
+
+ +
+
+

+ Upload an Image +

+

+ Drag and drop your image here, or click to browse files +

+
+
+ + + Select File + +
+
+ +
+

Supports JPG, PNG, WEBP up to 10MB

+
+ ) +} From 39bcda5b0e13e8414fcbad7d9323452b5d55f11f Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Mon, 16 Feb 2026 07:41:20 +0000 Subject: [PATCH 2/4] Redesign ExtractorDialog with vibe reference workflow and light mode - Refactor `ExtractorDialog.tsx` into modular stages: `UploadStage`, `AnalyzeStage`, `SelectionStage`, `RefineStage`. - Implement new UI flow: Upload -> Analyze -> Select -> Refine. - Adapt "vibe" reference visuals to application's Light Mode theme (Slate/Cyan/Amber). - Implement real-time status updates for analysis and refinement steps using backend streams. - Add drag-and-drop and paste support for image upload. - Visualize candidate selection grid and refinement progress. - Implement manual confirmation for analysis start. - Increase dialog size to `max-w-[98vw]` and `h-[95vh]`. - Simplify title bar and adjust candidate grid cell size to 140px. --- src/ui/components/ExtractorDialog.tsx | 136 +++++++----------- src/ui/components/extractor/AnalyzeStage.tsx | 55 ++++--- .../components/extractor/SelectionStage.tsx | 4 +- 3 files changed, 89 insertions(+), 106 deletions(-) diff --git a/src/ui/components/ExtractorDialog.tsx b/src/ui/components/ExtractorDialog.tsx index c6a476d..82d0c5c 100644 --- a/src/ui/components/ExtractorDialog.tsx +++ b/src/ui/components/ExtractorDialog.tsx @@ -67,77 +67,60 @@ export const ExtractorDialog: React.FC = ({ setStage('analyze') } - // Auto-start analysis when entering analyze stage - useEffect(() => { - let active = true - const analyze = async () => { - if (!file) return + const startAnalysis = async () => { + if (!file) return - setStatusMessage('Initializing analysis...') - analyzeController.current = new AbortController() + setStatusMessage('Initializing analysis...') + analyzeController.current = new AbortController() - try { - const response = await client.extract.analyze.$post({ - form: { image: file }, - }) + try { + const response = await client.extract.analyze.$post({ + form: { image: file }, + }) - if (!response.ok) throw new Error('Server error') - if (!response.body) throw new Error('No response body') + if (!response.ok) throw new Error('Server error') + if (!response.body) throw new Error('No response body') - const reader = response.body.getReader() - const decoder = new TextDecoder() - let buffer = '' + const reader = response.body.getReader() + const decoder = new TextDecoder() + let buffer = '' - while (active) { - const { done, value } = await reader.read() - if (done) break + while (true) { + const { done, value } = await reader.read() + if (done) break - buffer += decoder.decode(value, { stream: true }) - const lines = buffer.split('\n\n') - buffer = lines.pop() || '' + buffer += decoder.decode(value, { stream: true }) + const lines = buffer.split('\n\n') + buffer = lines.pop() || '' - for (const line of lines) { - if (line.startsWith('event: ')) { - const eventMatch = line.match(/^event: (.+)$/m) - const dataMatch = line.match(/^data: (.+)$/m) + for (const line of lines) { + if (line.startsWith('event: ')) { + const eventMatch = line.match(/^event: (.+)$/m) + const dataMatch = line.match(/^data: (.+)$/m) - if (eventMatch && dataMatch) { - const event = eventMatch[1].trim() - const data = JSON.parse(dataMatch[1]) + if (eventMatch && dataMatch) { + const event = eventMatch[1].trim() + const data = JSON.parse(dataMatch[1]) - if (event === 'status') { - if (active) setStatusMessage(data.message) - } else if (event === 'complete') { - if (active) { - setCandidates(data.assets as CandidateAsset[]) - setStage('selection') - } - } else if (event === 'error') { - throw new Error(data.message) - } + if (event === 'status') { + setStatusMessage(data.message) + } else if (event === 'complete') { + setCandidates(data.assets as CandidateAsset[]) + setStage('selection') + } else if (event === 'error') { + throw new Error(data.message) } } } } - } catch (e: unknown) { - if (active) { - console.error(e) - const err = e instanceof Error ? e : new Error(String(e)) - toast.error(err.message || 'Analysis failed') - setStage('upload') - } } + } catch (e: unknown) { + console.error(e) + const err = e instanceof Error ? e : new Error(String(e)) + toast.error(err.message || 'Analysis failed') + setStage('upload') } - - if (stage === 'analyze' && file) { - analyze() - } - - return () => { - active = false - analyzeController.current?.abort() - } - }, [stage, file]) + } const handleRefineConfirm = (indices: number[]) => { setSelectedIndices(indices) @@ -207,42 +190,14 @@ export const ExtractorDialog: React.FC = ({ return ( - - Equipment Extractor + + Extractor {/* Top Navigation / Branding Bar (Optional, matches App feel) */}
-
- AI -
-

- RefineLab -

+

Extractor

- - {/* Progress Steps */} - {stage !== 'upload' && ( -
-
- Analyze -
-
-
- Select -
-
-
- Refine -
-
- )}
{/* Content Area */} @@ -250,7 +205,12 @@ export const ExtractorDialog: React.FC = ({ {stage === 'upload' && } {stage === 'analyze' && preview && ( - + )} {stage === 'selection' && ( diff --git a/src/ui/components/extractor/AnalyzeStage.tsx b/src/ui/components/extractor/AnalyzeStage.tsx index 3fde754..3c1d857 100644 --- a/src/ui/components/extractor/AnalyzeStage.tsx +++ b/src/ui/components/extractor/AnalyzeStage.tsx @@ -1,12 +1,20 @@ -import { BrainCircuit } from 'lucide-react' +import { BrainCircuit, Play } from 'lucide-react' import React from 'react' +import { Button } from '../ui/button' interface AnalyzeStageProps { imageSrc: string statusMessage: string + isAnalyzing: boolean + onStart: () => void } -export const AnalyzeStage: React.FC = ({ imageSrc, statusMessage }) => { +export const AnalyzeStage: React.FC = ({ + imageSrc, + statusMessage, + isAnalyzing, + onStart, +}) => { return (
{/* Background Frosted Glass Layer */} @@ -25,10 +33,12 @@ export const AnalyzeStage: React.FC = ({ imageSrc, statusMess
Analyzing - {/* Scan Line Animation */} -
-
-
+ {/* Scan Line Animation (only when analyzing) */} + {isAnalyzing && ( +
+
+
+ )} {/* Grid Overlay Effect (Subtle) */}
@@ -40,15 +50,28 @@ export const AnalyzeStage: React.FC = ({ imageSrc, statusMess
- {/* Status Text */} -
- - {statusMessage} -
-
-
-
-
+ {/* Status Text or Action Button */} +
+ {isAnalyzing ? ( +
+ + {statusMessage} +
+
+
+
+
+
+ ) : ( + + )}
diff --git a/src/ui/components/extractor/SelectionStage.tsx b/src/ui/components/extractor/SelectionStage.tsx index cdf24af..dfac9cd 100644 --- a/src/ui/components/extractor/SelectionStage.tsx +++ b/src/ui/components/extractor/SelectionStage.tsx @@ -49,7 +49,7 @@ export const SelectionStage: React.FC = ({ {/* Grid */}
-
+
{candidates.map((item, index) => { const isSelected = selectedIndices.includes(index) return ( @@ -57,7 +57,7 @@ export const SelectionStage: React.FC = ({ key={index} onClick={() => toggleSelection(index)} className={cn( - 'group relative aspect-square rounded-xl overflow-hidden cursor-pointer transition-all duration-300 border-2 bg-white animate-fade-in-up', + 'group relative w-[140px] h-[140px] rounded-xl overflow-hidden cursor-pointer transition-all duration-300 border-2 bg-white animate-fade-in-up flex-shrink-0', isSelected ? 'border-cyan-500 shadow-lg shadow-cyan-100 scale-[1.02]' : 'border-slate-200 opacity-80 hover:opacity-100 hover:border-slate-300', From 180408b8834fe581617b04399ec630507098d60b Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Mon, 16 Feb 2026 07:49:38 +0000 Subject: [PATCH 3/4] Redesign ExtractorDialog with vibe reference workflow and light mode - Refactor `ExtractorDialog.tsx` into modular stages: `UploadStage`, `AnalyzeStage`, `SelectionStage`, `RefineStage`. - Implement new UI flow: Upload -> Analyze -> Select -> Refine. - Adapt "vibe" reference visuals to application's Light Mode theme (Slate/Cyan/Amber). - Implement real-time status updates for analysis and refinement steps using backend streams. - Add drag-and-drop and paste support for image upload. - Visualize candidate selection grid and refinement progress. - Implement manual confirmation for analysis start. - Increase dialog size to `max-w-[98vw]` and `h-[95vh]`. - Simplify title bar and adjust candidate grid cell size to 140px. - Adjust refinement grid cell size to 160px. --- src/ui/components/extractor/RefineStage.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ui/components/extractor/RefineStage.tsx b/src/ui/components/extractor/RefineStage.tsx index 8145fed..7cb372a 100644 --- a/src/ui/components/extractor/RefineStage.tsx +++ b/src/ui/components/extractor/RefineStage.tsx @@ -44,7 +44,7 @@ export const RefineStage: React.FC = ({ {/* Grid */}
-
+
{selectedCandidates.map((candidate, index) => { const isFinished = index < results.length const isProcessing = index === results.length && !isComplete @@ -57,7 +57,7 @@ export const RefineStage: React.FC = ({
Date: Mon, 16 Feb 2026 08:18:06 +0000 Subject: [PATCH 4/4] Delete image-refine-vibe reference folder - Removed the `image-refine-vibe` directory as the redesign based on it has been successfully implemented and approved. --- image-refine-vibe/.gitignore | 24 ---- image-refine-vibe/App.tsx | 124 ---------------- image-refine-vibe/README.md | 19 --- image-refine-vibe/components/AnalyzeStage.tsx | 62 -------- image-refine-vibe/components/RefineStage.tsx | 135 ------------------ image-refine-vibe/components/SplitStage.tsx | 120 ---------------- image-refine-vibe/components/UploadStage.tsx | 94 ------------ image-refine-vibe/index.html | 76 ---------- image-refine-vibe/index.tsx | 15 -- image-refine-vibe/metadata.json | 5 - image-refine-vibe/package.json | 22 --- image-refine-vibe/tsconfig.json | 21 --- image-refine-vibe/types.ts | 13 -- image-refine-vibe/utils/imageProcessor.ts | 76 ---------- image-refine-vibe/vite.config.ts | 23 --- 15 files changed, 829 deletions(-) delete mode 100644 image-refine-vibe/.gitignore delete mode 100644 image-refine-vibe/App.tsx delete mode 100644 image-refine-vibe/README.md delete mode 100644 image-refine-vibe/components/AnalyzeStage.tsx delete mode 100644 image-refine-vibe/components/RefineStage.tsx delete mode 100644 image-refine-vibe/components/SplitStage.tsx delete mode 100644 image-refine-vibe/components/UploadStage.tsx delete mode 100644 image-refine-vibe/index.html delete mode 100644 image-refine-vibe/index.tsx delete mode 100644 image-refine-vibe/metadata.json delete mode 100644 image-refine-vibe/package.json delete mode 100644 image-refine-vibe/tsconfig.json delete mode 100644 image-refine-vibe/types.ts delete mode 100644 image-refine-vibe/utils/imageProcessor.ts delete mode 100644 image-refine-vibe/vite.config.ts diff --git a/image-refine-vibe/.gitignore b/image-refine-vibe/.gitignore deleted file mode 100644 index a547bf3..0000000 --- a/image-refine-vibe/.gitignore +++ /dev/null @@ -1,24 +0,0 @@ -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -pnpm-debug.log* -lerna-debug.log* - -node_modules -dist -dist-ssr -*.local - -# Editor directories and files -.vscode/* -!.vscode/extensions.json -.idea -.DS_Store -*.suo -*.ntvs* -*.njsproj -*.sln -*.sw? diff --git a/image-refine-vibe/App.tsx b/image-refine-vibe/App.tsx deleted file mode 100644 index 1c051ad..0000000 --- a/image-refine-vibe/App.tsx +++ /dev/null @@ -1,124 +0,0 @@ -import React, { useState, useCallback } from 'react' -import { UploadStage } from './components/UploadStage' -import { AnalyzeStage } from './components/AnalyzeStage' -import { SplitStage } from './components/SplitStage' -import { RefineStage } from './components/RefineStage' -import { sliceImage } from './utils/imageProcessor' -import { AppStage, ImageSegment } from './types' - -const App: React.FC = () => { - const [stage, setStage] = useState('upload') - const [uploadedImage, setUploadedImage] = useState(null) - const [segments, setSegments] = useState([]) - - // 1. Handle Upload - const handleImageUpload = useCallback((imageDataUrl: string) => { - setUploadedImage(imageDataUrl) - setStage('analyze') - }, []) - - // 2. Handle Analysis Completion - const handleAnalysisComplete = useCallback(async () => { - if (!uploadedImage) return - - try { - // Slice image into 3x3 grid - const slicedImages = await sliceImage(uploadedImage, 3, 3) - - const newSegments: ImageSegment[] = slicedImages.map((src, index) => ({ - id: `seg-${index}`, - src, - selected: true, // Default all selected - status: 'pending', - })) - - setSegments(newSegments) - setStage('split') - } catch (error) { - console.error('Failed to slice image', error) - // Fallback or error handling could go here - } - }, [uploadedImage]) - - // 3. Handle Split Confirmation - const handleSplitConfirm = useCallback( - (selectedIds: string[]) => { - // Filter out unselected items for the refine view - const selectedSegments = segments.filter((seg) => selectedIds.includes(seg.id)) - setSegments(selectedSegments) - setStage('refine') - }, - [segments], - ) - - // 4. Reset - const handleReset = useCallback(() => { - setUploadedImage(null) - setSegments([]) - setStage('upload') - }, []) - - return ( -
- {/* Background Ambience */} -
-
-
-
- -
- {/* Navigation / Header (Minimal) */} -
-
-
- AI -
-

- RefineLab -

-
- - {/* Progress Steps */} - {stage !== 'upload' && ( -
-
- Analyze -
-
-
- Split -
-
-
- Refine -
-
- )} -
- - {/* Content Area */} -
- {stage === 'upload' && } - - {stage === 'analyze' && uploadedImage && ( - - )} - - {stage === 'split' && ( - - )} - - {stage === 'refine' && } -
-
-
- ) -} - -export default App diff --git a/image-refine-vibe/README.md b/image-refine-vibe/README.md deleted file mode 100644 index 59825b0..0000000 --- a/image-refine-vibe/README.md +++ /dev/null @@ -1,19 +0,0 @@ -
-GHBanner -
- -# Run and deploy your AI Studio app - -This contains everything you need to run your app locally. - -View your app in AI Studio: https://ai.studio/apps/drive/1P0JLZhnyDHgV03_9rLIzEixF3AY0-NQU - -## Run Locally - -**Prerequisites:** Node.js - -1. Install dependencies: - `npm install` -2. Set the `GEMINI_API_KEY` in [.env.local](.env.local) to your Gemini API key -3. Run the app: - `npm run dev` diff --git a/image-refine-vibe/components/AnalyzeStage.tsx b/image-refine-vibe/components/AnalyzeStage.tsx deleted file mode 100644 index b9ba34e..0000000 --- a/image-refine-vibe/components/AnalyzeStage.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import React, { useEffect } from 'react' -import { Scan, BrainCircuit } from 'lucide-react' - -interface AnalyzeStageProps { - imageSrc: string - onAnalysisComplete: () => void -} - -export const AnalyzeStage: React.FC = ({ imageSrc, onAnalysisComplete }) => { - useEffect(() => { - const timer = setTimeout(() => { - onAnalysisComplete() - }, 5000) - - return () => clearTimeout(timer) - }, [onAnalysisComplete]) - - return ( -
- {/* Background Frosted Glass Layer */} -
- - {/* Main Container */} -
- {/* Image Container with Scanning Effect */} -
- Analyzing - - {/* Scan Line Animation */} -
- - {/* Grid Overlay Effect (Subtle) */} -
- - {/* Corner Brackets UI */} -
-
-
-
-
- - {/* Status Text */} -
- - Analyzing Structure... -
-
-
-
-
-
-
-
- ) -} diff --git a/image-refine-vibe/components/RefineStage.tsx b/image-refine-vibe/components/RefineStage.tsx deleted file mode 100644 index f80620a..0000000 --- a/image-refine-vibe/components/RefineStage.tsx +++ /dev/null @@ -1,135 +0,0 @@ -import React, { useEffect, useState, useRef } from 'react' -import { ImageSegment } from '../types' -import { Loader2, CheckCircle2, Sparkles, RefreshCw } from 'lucide-react' - -interface RefineStageProps { - initialSegments: ImageSegment[] - onDone: () => void -} - -export const RefineStage: React.FC = ({ initialSegments, onDone }) => { - const [segments, setSegments] = useState(initialSegments) - const [processingIndex, setProcessingIndex] = useState(0) - const [isAllDone, setIsAllDone] = useState(false) - const processingRef = useRef(false) - - useEffect(() => { - // We only process segments that were selected - const selectedSegments = segments.filter((s) => s.selected) - - if (processingIndex >= selectedSegments.length) { - setIsAllDone(true) - return - } - - if (processingRef.current) return - processingRef.current = true - - const currentSegmentId = selectedSegments[processingIndex].id - - // Set current to processing - setSegments((prev) => - prev.map((s) => (s.id === currentSegmentId ? { ...s, status: 'processing' } : s)), - ) - - const processTimer = setTimeout(() => { - // Mark as completed and move next - setSegments((prev) => - prev.map((s) => (s.id === currentSegmentId ? { ...s, status: 'completed' } : s)), - ) - - setProcessingIndex((prev) => prev + 1) - processingRef.current = false - }, 2000) - - return () => clearTimeout(processTimer) - }, [processingIndex, segments]) - - return ( -
- {/* Header */} -
-
-

- - Refining Images -

-

- {isAllDone - ? 'All selected segments have been processed successfully.' - : `Processing item ${processingIndex + 1} of ${segments.length}...`} -

-
- {isAllDone && ( - - )} -
- - {/* Grid */} -
-
- {segments.map((seg) => ( -
- segment - - {/* Overlay states */} -
- {seg.status === 'pending' && ( -
- Pending -
- )} - - {seg.status === 'processing' && ( -
-
-
-
- -
-
- - Refining... - -
- )} - - {seg.status === 'completed' && ( -
- {/* Clean view for completed, just a subtle indicator */} -
- -
-
- )} -
- - {/* Scanning effect for processing item only */} - {seg.status === 'processing' && ( -
- )} -
- ))} -
-
-
- ) -} diff --git a/image-refine-vibe/components/SplitStage.tsx b/image-refine-vibe/components/SplitStage.tsx deleted file mode 100644 index 2459752..0000000 --- a/image-refine-vibe/components/SplitStage.tsx +++ /dev/null @@ -1,120 +0,0 @@ -import React, { useState } from 'react' -import { ImageSegment } from '../types' -import { Check, Grid2X2, ArrowRight } from 'lucide-react' - -interface SplitStageProps { - segments: ImageSegment[] - onConfirm: (selectedIds: string[]) => void - onBack: () => void -} - -export const SplitStage: React.FC = ({ segments, onConfirm, onBack }) => { - const [localSegments, setLocalSegments] = useState(segments) - - const toggleSelection = (id: string) => { - setLocalSegments((prev) => - prev.map((seg) => (seg.id === id ? { ...seg, selected: !seg.selected } : seg)), - ) - } - - const selectedCount = localSegments.filter((s) => s.selected).length - - const handleConfirm = () => { - if (selectedCount === 0) return - const selectedIds = localSegments.filter((s) => s.selected).map((s) => s.id) - onConfirm(selectedIds) - } - - return ( -
- {/* Header */} -
-
-

- - Select Segments -

-

- Unselect any areas you wish to exclude from processing. -

-
-
- {selectedCount} - Selected -
-
- - {/* Grid */} -
-
- {localSegments.map((seg) => ( -
toggleSelection(seg.id)} - className={` - group relative aspect-square rounded-xl overflow-hidden cursor-pointer transition-all duration-300 - border-2 - ${ - seg.selected - ? 'border-sky-500 shadow-lg shadow-sky-500/20 scale-[1.01]' - : 'border-slate-700 opacity-60 hover:opacity-100 hover:border-slate-500' - } - `} - > - segment - - {/* Selection Overlay */} -
- - {/* Checkbox Indicator */} -
- -
-
- ))} -
-
- - {/* Footer Actions */} -
-
- - -
-
-
- ) -} diff --git a/image-refine-vibe/components/UploadStage.tsx b/image-refine-vibe/components/UploadStage.tsx deleted file mode 100644 index 2a0c71b..0000000 --- a/image-refine-vibe/components/UploadStage.tsx +++ /dev/null @@ -1,94 +0,0 @@ -import React, { useCallback, useState } from 'react' -import { Upload, Image as ImageIcon } from 'lucide-react' -import { readFileAsDataURL } from '../utils/imageProcessor' - -interface UploadStageProps { - onImageUpload: (imageDataUrl: string) => void -} - -export const UploadStage: React.FC = ({ onImageUpload }) => { - const [isDragging, setIsDragging] = useState(false) - - const handleDragOver = useCallback((e: React.DragEvent) => { - e.preventDefault() - setIsDragging(true) - }, []) - - const handleDragLeave = useCallback((e: React.DragEvent) => { - e.preventDefault() - setIsDragging(false) - }, []) - - const handleDrop = useCallback( - async (e: React.DragEvent) => { - e.preventDefault() - setIsDragging(false) - - if (e.dataTransfer.files && e.dataTransfer.files[0]) { - const file = e.dataTransfer.files[0] - if (file.type.startsWith('image/')) { - try { - const dataUrl = await readFileAsDataURL(file) - onImageUpload(dataUrl) - } catch (error) { - console.error('Error reading file:', error) - } - } - } - }, - [onImageUpload], - ) - - const handleFileChange = useCallback( - async (e: React.ChangeEvent) => { - if (e.target.files && e.target.files[0]) { - try { - const dataUrl = await readFileAsDataURL(e.target.files[0]) - onImageUpload(dataUrl) - } catch (error) { - console.error('Error reading file:', error) - } - } - }, - [onImageUpload], - ) - - return ( -
-
-
-
- -
-
-

Upload an Image

-

- Drag and drop your image here, or click to browse files -

-
- -
-
-

Supports JPG, PNG, WEBP up to 10MB

-
- ) -} diff --git a/image-refine-vibe/index.html b/image-refine-vibe/index.html deleted file mode 100644 index f16903e..0000000 --- a/image-refine-vibe/index.html +++ /dev/null @@ -1,76 +0,0 @@ - - - - - - Image Refine Tool - - - - - - -
- - - diff --git a/image-refine-vibe/index.tsx b/image-refine-vibe/index.tsx deleted file mode 100644 index ed2c62f..0000000 --- a/image-refine-vibe/index.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import React from 'react' -import ReactDOM from 'react-dom/client' -import App from './App' - -const rootElement = document.getElementById('root') -if (!rootElement) { - throw new Error('Could not find root element to mount to') -} - -const root = ReactDOM.createRoot(rootElement) -root.render( - - - , -) diff --git a/image-refine-vibe/metadata.json b/image-refine-vibe/metadata.json deleted file mode 100644 index 1e4996d..0000000 --- a/image-refine-vibe/metadata.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "name": "Image Split & Refine Tool", - "description": "A sophisticated image processing tool that analyzes, splits, and refines images with a modern frosted glass UI.", - "requestFramePermissions": [] -} diff --git a/image-refine-vibe/package.json b/image-refine-vibe/package.json deleted file mode 100644 index fe85e3d..0000000 --- a/image-refine-vibe/package.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "image-split-&-refine-tool", - "private": true, - "version": "0.0.0", - "type": "module", - "scripts": { - "dev": "vite", - "build": "vite build", - "preview": "vite preview" - }, - "dependencies": { - "react": "^19.2.4", - "lucide-react": "^0.564.0", - "react-dom": "^19.2.4" - }, - "devDependencies": { - "@types/node": "^22.14.0", - "@vitejs/plugin-react": "^5.0.0", - "typescript": "~5.8.2", - "vite": "^6.2.0" - } -} diff --git a/image-refine-vibe/tsconfig.json b/image-refine-vibe/tsconfig.json deleted file mode 100644 index f9d60ed..0000000 --- a/image-refine-vibe/tsconfig.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2022", - "experimentalDecorators": true, - "useDefineForClassFields": false, - "module": "ESNext", - "lib": ["ES2022", "DOM", "DOM.Iterable"], - "skipLibCheck": true, - "types": ["node"], - "moduleResolution": "bundler", - "isolatedModules": true, - "moduleDetection": "force", - "allowJs": true, - "jsx": "react-jsx", - "paths": { - "@/*": ["./*"] - }, - "allowImportingTsExtensions": true, - "noEmit": true - } -} diff --git a/image-refine-vibe/types.ts b/image-refine-vibe/types.ts deleted file mode 100644 index 6e1a92e..0000000 --- a/image-refine-vibe/types.ts +++ /dev/null @@ -1,13 +0,0 @@ -export type AppStage = 'upload' | 'analyze' | 'split' | 'refine' | 'completed' - -export interface ImageSegment { - id: string - src: string - selected: boolean - status: 'pending' | 'processing' | 'completed' -} - -export interface ProcessingState { - isProcessing: boolean - currentId: string | null -} diff --git a/image-refine-vibe/utils/imageProcessor.ts b/image-refine-vibe/utils/imageProcessor.ts deleted file mode 100644 index d6159ab..0000000 --- a/image-refine-vibe/utils/imageProcessor.ts +++ /dev/null @@ -1,76 +0,0 @@ -/** - * Slices an image into a grid of smaller images. - * @param imageSrc The source image data URL. - * @param rows Number of rows. - * @param cols Number of columns. - * @returns A promise resolving to an array of data URLs for the segments. - */ -export const sliceImage = ( - imageSrc: string, - rows: number = 3, - cols: number = 3, -): Promise => { - return new Promise((resolve, reject) => { - const img = new Image() - img.crossOrigin = 'Anonymous' - img.onload = () => { - const segments: string[] = [] - const segmentWidth = Math.floor(img.width / cols) - const segmentHeight = Math.floor(img.height / rows) - - const canvas = document.createElement('canvas') - const ctx = canvas.getContext('2d') - - if (!ctx) { - reject(new Error('Could not get canvas context')) - return - } - - canvas.width = segmentWidth - canvas.height = segmentHeight - - for (let y = 0; y < rows; y++) { - for (let x = 0; x < cols; x++) { - // Clear canvas for next segment - ctx.clearRect(0, 0, segmentWidth, segmentHeight) - - // Draw the specific slice - ctx.drawImage( - img, - x * segmentWidth, - y * segmentHeight, - segmentWidth, - segmentHeight, - 0, - 0, - segmentWidth, - segmentHeight, - ) - - segments.push(canvas.toDataURL('image/png')) - } - } - resolve(segments) - } - img.onerror = (err) => reject(err) - img.src = imageSrc - }) -} - -/** - * Reads a file input as a Data URL. - */ -export const readFileAsDataURL = (file: File): Promise => { - return new Promise((resolve, reject) => { - const reader = new FileReader() - reader.onload = (e) => { - if (e.target?.result) { - resolve(e.target.result as string) - } else { - reject(new Error('Failed to read file')) - } - } - reader.onerror = reject - reader.readAsDataURL(file) - }) -} diff --git a/image-refine-vibe/vite.config.ts b/image-refine-vibe/vite.config.ts deleted file mode 100644 index 11827f5..0000000 --- a/image-refine-vibe/vite.config.ts +++ /dev/null @@ -1,23 +0,0 @@ -import path from 'path' -import { defineConfig, loadEnv } from 'vite' -import react from '@vitejs/plugin-react' - -export default defineConfig(({ mode }) => { - const env = loadEnv(mode, '.', '') - return { - server: { - port: 3000, - host: '0.0.0.0', - }, - plugins: [react()], - define: { - 'process.env.API_KEY': JSON.stringify(env.GEMINI_API_KEY), - 'process.env.GEMINI_API_KEY': JSON.stringify(env.GEMINI_API_KEY), - }, - resolve: { - alias: { - '@': path.resolve(__dirname, '.'), - }, - }, - } -})