Skip to content

Commit 4bc0028

Browse files
committed
feat(frontend): add screenshot API and update project photo mutation
1 parent b4b9918 commit 4bc0028

File tree

11 files changed

+17792
-13762
lines changed

11 files changed

+17792
-13762
lines changed

backend/src/upload/upload.service.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ export class UploadService {
9999
// Get the appropriate URL for the uploaded file
100100
const bucketUrl = this.getBucketUrl();
101101

102-
return { url: `${bucketUrl}/${key}`, key };
102+
return { url: path.join(bucketUrl, key), key };
103103
} else {
104104
// Upload to local storage from buffer
105105
const directory = path.join(this.mediaDir, subdirectory);
@@ -135,8 +135,7 @@ export class UploadService {
135135

136136
// Get the appropriate URL for the uploaded file
137137
const bucketUrl = this.getBucketUrl();
138-
139-
return { url: `${bucketUrl}/${key}`, key };
138+
return { url: path.join(bucketUrl, key), key };
140139
} else {
141140
// Upload to local storage using stream
142141
const directory = path.join(this.mediaDir, subdirectory);

frontend/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
"motion": "^12.4.7",
5252
"next": "^14.2.13",
5353
"next-themes": "^0.3.0",
54+
"puppeteer": "^24.3.1",
5455
"react": "^18.3.1",
5556
"react-activity-calendar": "^2.7.8",
5657
"react-code-blocks": "^0.1.6",

frontend/src/app/api/runProject/route.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@ import { exec } from 'child_process';
33
import * as path from 'path';
44
import * as net from 'net';
55
import { getProjectPath } from 'codefox-common';
6+
import puppetter from 'puppeteer';
7+
import { useMutation } from '@apollo/client/react/hooks/useMutation';
8+
import { toast } from 'sonner';
9+
import { UPDATE_PROJECT_PHOTO_URL } from '@/graphql/request';
610

711
const runningContainers = new Map<
812
string,
@@ -294,6 +298,7 @@ export async function GET(req: Request) {
294298

295299
try {
296300
const { domain, containerId } = await buildAndRunDocker(projectPath);
301+
297302
return NextResponse.json({
298303
message: 'Docker container started',
299304
domain,
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { randomUUID } from 'crypto';
2+
import { NextResponse } from 'next/server';
3+
import puppeteer from 'puppeteer';
4+
5+
export async function GET(req: Request) {
6+
const { searchParams } = new URL(req.url);
7+
const url = searchParams.get('url');
8+
9+
if (!url) {
10+
return NextResponse.json(
11+
{ error: 'URL parameter is required' },
12+
{ status: 400 }
13+
);
14+
}
15+
16+
try {
17+
const browser = await puppeteer.launch({
18+
headless: true,
19+
});
20+
const page = await browser.newPage();
21+
22+
// Set viewport to a reasonable size
23+
await page.setViewport({
24+
width: 1280,
25+
height: 720,
26+
});
27+
28+
await page.goto(url, {
29+
waitUntil: 'networkidle0',
30+
timeout: 30000,
31+
});
32+
33+
// Take screenshot
34+
const screenshot = await page.screenshot({
35+
path: `dsadas.png`,
36+
type: 'png',
37+
fullPage: true,
38+
});
39+
40+
await browser.close();
41+
42+
// Return the screenshot as a PNG image
43+
return new Response(screenshot, {
44+
headers: {
45+
'Content-Type': 'image/png',
46+
'Cache-Control': 's-maxage=3600',
47+
},
48+
});
49+
} catch (error: any) {
50+
console.error('Screenshot error:', error);
51+
return NextResponse.json(
52+
{ error: error.message || 'Failed to capture screenshot' },
53+
{ status: 500 }
54+
);
55+
}
56+
}

frontend/src/components/chat/code-engine/project-context.tsx

Lines changed: 150 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
GET_CHAT_DETAILS,
1515
GET_USER_PROJECTS,
1616
UPDATE_PROJECT_PUBLIC_STATUS,
17+
UPDATE_PROJECT_PHOTO_URL,
1718
} from '@/graphql/request';
1819
import { Project } from '../project-modal';
1920
import { useRouter } from 'next/navigation';
@@ -40,12 +41,31 @@ export interface ProjectContextType {
4041
) => Promise<void>;
4142
pollChatProject: (chatId: string) => Promise<Project | null>;
4243
isLoading: boolean;
44+
getWebUrl: (
45+
projectPath: string
46+
) => Promise<{ domain: string; containerId: string }>;
47+
takeProjectScreenshot: (projectId: string, url: string) => Promise<void>;
4348
}
4449

4550
export const ProjectContext = createContext<ProjectContextType | undefined>(
4651
undefined
4752
);
48-
53+
const checkUrlStatus = async (url: string) => {
54+
let status = 0;
55+
while (status !== 200) {
56+
try {
57+
const res = await fetch(url, { method: 'HEAD' });
58+
status = res.status;
59+
if (status !== 200) {
60+
console.log(`URL status: ${status}. Retrying...`);
61+
await new Promise((resolve) => setTimeout(resolve, 1000));
62+
}
63+
} catch (err) {
64+
console.error('Error checking URL status:', err);
65+
await new Promise((resolve) => setTimeout(resolve, 1000));
66+
}
67+
}
68+
};
4969
export function ProjectProvider({ children }: { children: ReactNode }) {
5070
const router = useRouter();
5171

@@ -173,6 +193,106 @@ export function ProjectProvider({ children }: { children: ReactNode }) {
173193
}
174194
);
175195

196+
const [updateProjectPhotoMutation] = useMutation(UPDATE_PROJECT_PHOTO_URL, {
197+
onCompleted: (data) => {
198+
// Update projects list
199+
setProjects((prev) =>
200+
prev.map((project) =>
201+
project.id === data.updateProjectPhoto.id
202+
? {
203+
...project,
204+
photoUrl: data.updateProjectPhoto.photoUrl,
205+
}
206+
: project
207+
)
208+
);
209+
210+
// Update current project if it's the one being modified
211+
if (curProject?.id === data.updateProjectPhoto.id) {
212+
setCurProject((prev) =>
213+
prev
214+
? {
215+
...prev,
216+
photoUrl: data.updateProjectPhoto.photoUrl,
217+
}
218+
: prev
219+
);
220+
}
221+
},
222+
onError: (error) => {
223+
toast.error(`Failed to update project photo: ${error.message}`);
224+
},
225+
});
226+
227+
const takeProjectScreenshot = useCallback(
228+
async (projectId: string, url: string) => {
229+
try {
230+
await checkUrlStatus(url);
231+
232+
const screenshotResponse = await fetch(
233+
`/api/screenshot?url=${encodeURIComponent(url)}`
234+
);
235+
236+
if (!screenshotResponse.ok) {
237+
throw new Error('Failed to capture screenshot');
238+
}
239+
240+
const arrayBuffer = await screenshotResponse.arrayBuffer();
241+
const blob = new Blob([arrayBuffer], { type: 'image/png' });
242+
243+
const file = new File([blob], 'screenshot.png', { type: 'image/png' });
244+
245+
await updateProjectPhotoMutation({
246+
variables: {
247+
input: {
248+
projectId,
249+
file,
250+
},
251+
},
252+
});
253+
} catch (error) {
254+
console.error('Error:', error);
255+
}
256+
},
257+
[updateProjectPhotoMutation]
258+
);
259+
260+
const getWebUrl = useCallback(
261+
async (projectPath: string) => {
262+
try {
263+
const response = await fetch(
264+
`/api/runProject?projectPath=${encodeURIComponent(projectPath)}`,
265+
{
266+
method: 'GET',
267+
headers: {
268+
'Content-Type': 'application/json',
269+
},
270+
}
271+
);
272+
273+
if (!response.ok) {
274+
throw new Error('Failed to get web URL');
275+
}
276+
277+
const data = await response.json();
278+
const baseUrl = `http://${data.domain}`;
279+
const project = projects.find((p) => p.projectPath === projectPath);
280+
if (project) {
281+
await takeProjectScreenshot(project.id, baseUrl);
282+
}
283+
284+
return {
285+
domain: data.domain,
286+
containerId: data.containerId,
287+
};
288+
} catch (error) {
289+
console.error('Error getting web URL:', error);
290+
throw error;
291+
}
292+
},
293+
[projects, updateProjectPhotoMutation]
294+
);
295+
176296
const [getChatDetail] = useLazyQuery(GET_CHAT_DETAILS, {
177297
fetchPolicy: 'network-only',
178298
});
@@ -300,8 +420,31 @@ export function ProjectProvider({ children }: { children: ReactNode }) {
300420
const { data } = await getChatDetail({ variables: { chatId } });
301421

302422
if (data?.getChatDetails?.project) {
303-
chatProjectCache.current.set(chatId, data.getChatDetails.project);
304-
return data.getChatDetails.project;
423+
const project = data.getChatDetails.project;
424+
chatProjectCache.current.set(chatId, project);
425+
426+
try {
427+
// Get web URL and wait for it to be ready
428+
const response = await fetch(
429+
`/api/runProject?projectPath=${encodeURIComponent(project.projectPath)}`,
430+
{
431+
method: 'GET',
432+
headers: {
433+
'Content-Type': 'application/json',
434+
},
435+
}
436+
);
437+
438+
if (response.ok) {
439+
const data = await response.json();
440+
const baseUrl = `http://${data.domain}`;
441+
await takeProjectScreenshot(project.id, baseUrl);
442+
}
443+
} catch (error) {
444+
console.error('Error capturing project screenshot:', error);
445+
}
446+
447+
return project;
305448
}
306449
} catch (error) {
307450
console.error('Error polling chat:', error);
@@ -314,7 +457,7 @@ export function ProjectProvider({ children }: { children: ReactNode }) {
314457
chatProjectCache.current.set(chatId, null);
315458
return null;
316459
},
317-
[getChatDetail]
460+
[getChatDetail, updateProjectPhotoMutation]
318461
);
319462

320463
const contextValue = useMemo(
@@ -331,6 +474,8 @@ export function ProjectProvider({ children }: { children: ReactNode }) {
331474
setProjectPublicStatus,
332475
pollChatProject,
333476
isLoading,
477+
getWebUrl,
478+
takeProjectScreenshot,
334479
}),
335480
[
336481
projects,
@@ -342,6 +487,7 @@ export function ProjectProvider({ children }: { children: ReactNode }) {
342487
setProjectPublicStatus,
343488
pollChatProject,
344489
isLoading,
490+
getWebUrl,
345491
]
346492
);
347493

frontend/src/components/chat/code-engine/web-view.tsx

Lines changed: 13 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,13 @@ import {
1111
ZoomIn,
1212
ZoomOut,
1313
} from 'lucide-react';
14+
import puppeteer from 'puppeteer';
1415

1516
export default function WebPreview() {
16-
const { curProject } = useContext(ProjectContext);
17+
const { curProject, getWebUrl } = useContext(ProjectContext);
18+
if (!curProject || !getWebUrl) {
19+
throw new Error('ProjectContext not properly initialized');
20+
}
1721
const [baseUrl, setBaseUrl] = useState('');
1822
const [displayPath, setDisplayPath] = useState('/');
1923
const [history, setHistory] = useState<string[]>(['/']);
@@ -26,7 +30,7 @@ export default function WebPreview() {
2630
const lastProjectPathRef = useRef<string | null>(null);
2731

2832
useEffect(() => {
29-
const getWebUrl = async () => {
33+
const initWebUrl = async () => {
3034
if (!curProject) return;
3135
const projectPath = curProject.projectPath;
3236

@@ -42,53 +46,23 @@ export default function WebPreview() {
4246
}
4347

4448
try {
45-
const response = await fetch(
46-
`/api/runProject?projectPath=${encodeURIComponent(projectPath)}`,
47-
{
48-
method: 'GET',
49-
headers: {
50-
'Content-Type': 'application/json',
51-
},
52-
}
53-
);
54-
const json = await response.json();
55-
56-
await new Promise((resolve) => setTimeout(resolve, 100));
57-
49+
const { domain } = await getWebUrl(projectPath);
5850
containerRef.current = {
5951
projectPath,
60-
domain: json.domain,
52+
domain,
6153
};
6254

63-
const checkUrlStatus = async (url: string) => {
64-
let status = 0;
65-
while (status !== 200) {
66-
try {
67-
const res = await fetch(url, { method: 'HEAD' });
68-
status = res.status;
69-
if (status !== 200) {
70-
console.log(`URL status: ${status}. Retrying...`);
71-
await new Promise((resolve) => setTimeout(resolve, 1000));
72-
}
73-
} catch (err) {
74-
console.error('Error checking URL status:', err);
75-
await new Promise((resolve) => setTimeout(resolve, 1000));
76-
}
77-
}
78-
};
79-
80-
const baseUrl = `http://${json.domain}`;
81-
await checkUrlStatus(baseUrl);
82-
55+
const baseUrl = `http://${domain}`;
56+
console.log('baseUrl:', baseUrl);
8357
setBaseUrl(baseUrl);
8458
setDisplayPath('/');
8559
} catch (error) {
86-
console.error('fetching url error:', error);
60+
console.error('Error getting web URL:', error);
8761
}
8862
};
8963

90-
getWebUrl();
91-
}, [curProject]);
64+
initWebUrl();
65+
}, [curProject, getWebUrl]);
9266

9367
useEffect(() => {
9468
if (iframeRef.current && baseUrl) {

0 commit comments

Comments
 (0)