Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/routes/home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export default function Home() {
navigate(`/visualizer/${newId}`, {
state: {
initialImage: saved.sourceImage,
initialRendered: saved.renderedImage || null,
initialRender: saved.renderedImage || null,
name
}
});
Expand Down
125 changes: 112 additions & 13 deletions app/routes/visualizer.$id.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,122 @@
import React from 'react';
import {useLocation} from "react-router";
import React, {useEffect, useRef, useState} from 'react';
import {useLocation, useNavigate} from "react-router";
import {generate3DView} from "../../lib/ai.action";
import {Box, Download, RefreshCcw, Share2, X} from "lucide-react";
import Button from "../../components/ui/Button";

function VisualizerId(props) {
function VisualizerId() {

const navigate = useNavigate();
const location = useLocation();

const { initialImage, name } = location.state || {};
return (
<section>
<h1>{name || 'Untitled Project'}</h1>
const { initialImage, initialRender, name } = location.state || {};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

name is destructured but never used; hardcoded "Untitled Project" displayed instead.

Line 12 extracts name from location state but Line 73 ignores it and displays 'Untitled Project'.

Proposed fix
-                                <h2>{'Untitled Project'}</h2>
+                                <h2>{name || 'Untitled Project'}</h2>

Also applies to: 73-73

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/routes/visualizer`.$id.tsx at line 12, The component destructures name
from location.state but the JSX still renders a hardcoded "Untitled Project";
update the render to use the destructured name (fallback to "Untitled Project"
if name is falsy) so the project title uses the passed-in value — locate the
destructuring of location.state (const { initialImage, initialRender, name } =
location.state || {}) and replace the hardcoded 'Untitled Project' in the JSX
with name || 'Untitled Project' (or name ?? 'Untitled Project').


const hasInitialGenerated = useRef(false);

const [isProcessing, setIsProcessing] = useState(false);
const [isMounted, setIsMounted] = useState(false);
const [currentImage, setCurrentImage] = useState<string | null>(initialRender || null);

useEffect(() => {
setIsMounted(true);
}, []);

const handleBack = () => navigate('/');

const runGeneration = async () => {
if(!initialImage) return;

try {
setIsProcessing(true);
const result = await generate3DView({ sourceImage: initialImage})

if(result.renderedImage) {
setCurrentImage(result.renderedImage);

// update the project with the rendered image.
}
} catch(e) {
console.error(`Generation failed: `, e)
} finally {
setIsProcessing(false);
}
}

useEffect(() => {
if(!initialImage || hasInitialGenerated.current) return;

hasInitialGenerated.current = true;
if(initialRender) {
setCurrentImage(initialRender);
} else {
runGeneration();
}

}, [initialImage, initialRender])

return (
<div className="visualizer">
{initialImage && (
<div className="image-container">
<h2>Source Image</h2>
<img src={initialImage} alt="source"/>
<nav className="topbar">
<div className="brand">
<Box className="logo"/>
<span className="name">Planly AI</span>
</div>
<Button variant="ghost" size="sm" onClick={handleBack} className="exit">
<X className="icon"/>Exit Editor
</Button>
</nav>
<section className="content">
<div className="panel">
<div className="panel-header">
<div className="panel-meta">
<p>Project</p>
<h2>{'Untitled Project'}</h2>
<p className="note">Created by You</p>
</div>

<div className="panel-actions">
<Button
size="sm"
onClick={() => {}}
className="export"
disabled={!isMounted || !currentImage}>
<Download className="w-4 h-4 mr-2"/> Export
</Button>
<Button size="sm" onClick={() => {}} className="share">
<Share2 className="w-4 h-4 mr-2"/> Share
</Button>
</div>
</div>

<div className={`render-area ${isProcessing ? 'processing' : ''}`}>
{isMounted && (
<>
{currentImage ? (
<img src={currentImage} alt="Rendered Image" className="render-image"/>
) : (
<div className="render-placeholder">
{initialImage && (
<img src={initialImage} alt="Original"
className="render-fallback"/>
)}
</div>
)}
{isProcessing && (
<div className="render-overlay">
<div className="rendering-card">
<RefreshCcw className="spinner"/>
<span className="title">Rendering...</span>
<span className="subtitle">Generating your 3D visualization</span>
</div>
</div>
)}
</>
)}
</div>

</div>
)}
</section>
</div>
</section>
);
}

Expand Down
Binary file added assets/example-floor-plan-1.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
42 changes: 42 additions & 0 deletions lib/ai.action.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import {getImageExtension} from "./utils";
import puter from "@heyputer/puter.js";
import {PLANLY_AI_RENDER_PROMPT} from "./constants";

export const fetchaDataUrl = async (url: string): Promise<string> => {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Failed to fetch image: ${response.statusText}`);
}
const blob = await response.blob();
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result as string);
reader.onerror = () => reject(reader.error);
reader.readAsDataURL(blob);
});
}

export const generate3DView = async ({sourceImage}: Generate3DViewParams) => {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Missing type definition for Generate3DViewParams.

The type is used but never defined or imported, causing a TypeScript compilation error.

Proposed fix: Add the type definition
 import {getImageExtension} from "./utils";
 import puter from "`@heyputer/puter.js`";
 import {PLANLY_AI_RENDER_PROMPT} from "./constants";
+
+interface Generate3DViewParams {
+    sourceImage: string;
+}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/ai.action.ts` at line 19, The generate3DView function signature uses an
undefined type Generate3DViewParams; add or import a proper type definition for
Generate3DViewParams (e.g., an interface with sourceImage: string | Buffer |
File depending on how images are passed) and export/import it where types are
centralized so TypeScript compiles; update the file to reference the new/renamed
type and ensure any callers match the property name sourceImage used by
generate3DView.

const dataUrl = sourceImage.startsWith('data:') ? sourceImage : await fetchaDataUrl(sourceImage);

const base64Data = dataUrl.split(`,`)[1];
const mimeType = dataUrl.split(';')[0].split(':')[1];

if(!mimeType || !base64Data) throw new Error("Invalid image data");

const response = await puter.ai.txt2img(PLANLY_AI_RENDER_PROMPT, {
provider: 'gemini',
model: 'gemini-2.5-flash-image-preview',
input_image: base64Data,
input_image_mime_type: mimeType,
ratio: { w: 1024, h: 1024}
})

const rawImageUrl = (response as HTMLImageElement).src ?? null;

if (!rawImageUrl) return { renderedImage: null, renderedPath: undefined};

const renderedImage = rawImageUrl.startsWith('data:image') ? rawImageUrl : await fetchaDataUrl(rawImageUrl);

return { renderedImage, renderedPath: undefined};
}
Empty file removed lib/ai.actions.ts
Empty file.
8 changes: 4 additions & 4 deletions lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ export const PUTER_WORKER_URL = import.meta.env.VITE_PUTER_WORKER_URL || "";

// Storage Paths
export const STORAGE_PATHS = {
ROOT: "roomify",
SOURCES: "roomify/sources",
RENDERS: "roomify/renders",
ROOT: "planly",
SOURCES: "planly/sources",
RENDERS: "planly/renders",
} as const;

// Timing Constants (in milliseconds)
Expand All @@ -29,7 +29,7 @@ export const MAX_UPLOAD_SIZE = 50 * 1024 * 1024; // 50MB
export const ALLOWED_FILE_TYPES = ['image/jpeg', 'image/jpg', 'image/png'];
export const ALLOWED_FILE_EXTENSIONS = ['.jpg', '.jpeg', '.png'];

export const ROOMIFY_RENDER_PROMPT = `
export const PLANLY_AI_RENDER_PROMPT = `
TASK: Convert the input 2D floor plan into a **photorealistic, top‑down 3D architectural render**.

STRICT REQUIREMENTS (do not violate):
Expand Down