Skip to content

Commit 549bf6a

Browse files
move function to server side
1 parent 140aab6 commit 549bf6a

File tree

5 files changed

+105
-89
lines changed

5 files changed

+105
-89
lines changed

apps/web/client/src/app/project/[id]/_components/top-bar/project-breadcrumb.tsx

Lines changed: 7 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,6 @@ import { useStateManager } from '@/components/store/state';
33
import { transKeys } from '@/i18n/keys';
44
import { api } from '@/trpc/react';
55
import { Routes } from '@/utils/constants';
6-
import { uploadBlobToStorage } from '@/utils/supabase/client';
7-
import { STORAGE_BUCKETS } from '@onlook/constants';
8-
import { fromPreviewImg } from '@onlook/db';
96
import { Button } from '@onlook/ui/button';
107
import {
118
DropdownMenu,
@@ -17,22 +14,20 @@ import {
1714
import { Icons } from '@onlook/ui/icons';
1815
import { toast } from '@onlook/ui/sonner';
1916
import { cn } from '@onlook/ui/utils';
20-
import { getScreenshotPath, getValidUrl } from '@onlook/utility';
2117
import { observer } from 'mobx-react-lite';
2218
import { useTranslations } from 'next-intl';
2319
import { redirect, useRouter } from 'next/navigation';
2420
import { usePostHog } from 'posthog-js/react';
2521
import { useRef, useState } from 'react';
2622
import { RecentProjectsMenu } from './recent-projects';
27-
import { handleScrapeUrlTool } from '@/components/tools/handlers/web';
2823

2924
export const ProjectBreadcrumb = observer(() => {
3025
const editorEngine = useEditorEngine();
3126
const stateManager = useStateManager();
3227
const posthog = usePostHog();
3328

3429
const { data: project } = api.project.get.useQuery({ projectId: editorEngine.projectId });
35-
const { mutateAsync: updateProject } = api.project.update.useMutation();
30+
const { mutate: captureScreenshot } = api.project.captureScreenshot.useMutation();
3631
const t = useTranslations();
3732
const closeTimeoutRef = useRef<Timer | null>(null);
3833
const router = useRouter();
@@ -44,7 +39,7 @@ export const ProjectBreadcrumb = observer(() => {
4439
try {
4540
setIsClosingProject(true);
4641

47-
await captureProjectScreenshot();
42+
captureProjectScreenshot();
4843
} catch (error) {
4944
console.error('Failed to take screenshots:', error);
5045
} finally {
@@ -55,71 +50,15 @@ export const ProjectBreadcrumb = observer(() => {
5550
}
5651
}
5752

58-
async function captureProjectScreenshot() {
59-
if (!project?.sandbox?.url) {
60-
console.error('No sandbox URL found');
61-
return;
62-
}
63-
64-
if (!project.id) {
53+
function captureProjectScreenshot() {
54+
if (!project?.id) {
6555
console.error('No project ID found');
6656
return;
6757
}
68-
69-
const screenShot = await handleScrapeUrlTool({
70-
url: project.sandbox.url,
71-
formats: ['screenshot'],
72-
actions: [
73-
{
74-
type: 'screenshot',
75-
fullPage: true,
76-
},
77-
],
78-
onlyMainContent: true,
79-
});
80-
81-
const validUrl = getValidUrl(screenShot);
82-
83-
if (!validUrl) {
84-
console.error('Invalid screenshot URL');
85-
return;
86-
}
87-
8858
try {
89-
const response = await fetch(screenShot, { mode: 'cors' });
90-
if (response.ok) {
91-
const blob = await response.blob();
92-
const mimeType = blob.type || 'image/png';
93-
const uploaded = await uploadBlobToStorage(
94-
STORAGE_BUCKETS.PREVIEW_IMAGES,
95-
getScreenshotPath(project.id, mimeType),
96-
blob,
97-
{ contentType: mimeType },
98-
);
99-
100-
if (uploaded) {
101-
const dbPreviewImg = fromPreviewImg({
102-
type: 'storage',
103-
storagePath: {
104-
bucket: STORAGE_BUCKETS.PREVIEW_IMAGES,
105-
path: uploaded.path,
106-
},
107-
});
108-
if (project?.metadata) {
109-
await updateProject({
110-
id: project.id,
111-
project: {
112-
...dbPreviewImg,
113-
},
114-
});
115-
}
116-
return;
117-
}
118-
} else {
119-
console.error('Failed to fetch screenshot URL:', response.statusText);
120-
}
121-
} catch (err) {
122-
console.error('Error handling screenshot URL upload:', err);
59+
captureScreenshot({ projectId: project.id });
60+
} catch (error) {
61+
console.error('Failed to capture screenshot on server:', error);
12362
}
12463
}
12564

apps/web/client/src/components/tools/handlers/web.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ export async function handleScrapeUrlTool(
1717
includeTags: args.includeTags,
1818
excludeTags: args.excludeTags,
1919
waitFor: args.waitFor,
20-
actions: args.actions,
2120
});
2221

2322
if (!result.result) {

apps/web/client/src/server/api/routers/code.ts

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -43,11 +43,7 @@ export const codeRouter = createTRPCRouter({
4343
scrapeUrl: protectedProcedure
4444
.input(z.object({
4545
url: z.string().url(),
46-
formats: z.array(z.enum(['markdown', 'html', 'json', 'screenshot'])).default(['markdown']),
47-
actions: z.array(z.object({
48-
type: z.enum(['screenshot']),
49-
fullPage: z.boolean().default(false).optional(),
50-
})).optional(),
46+
formats: z.array(z.enum(['markdown', 'html', 'json'])).default(['markdown']),
5147
onlyMainContent: z.boolean().default(true),
5248
includeTags: z.array(z.string()).optional(),
5349
excludeTags: z.array(z.string()).optional(),
@@ -64,7 +60,6 @@ export const codeRouter = createTRPCRouter({
6460
const result = await app.scrapeUrl(input.url, {
6561
formats: input.formats,
6662
onlyMainContent: input.onlyMainContent,
67-
...(input.actions && { actions: input.actions }),
6863
...(input.includeTags && { includeTags: input.includeTags }),
6964
...(input.excludeTags && { excludeTags: input.excludeTags }),
7065
...(input.waitFor !== undefined && { waitFor: input.waitFor }),
@@ -76,11 +71,7 @@ export const codeRouter = createTRPCRouter({
7671

7772
// Return the primary content format (markdown by default)
7873
// or the first available format if markdown isn't available
79-
const markdown = result.markdown ?? result.html ?? JSON.stringify(result.json, null, 2);
80-
// Return the screenshot if it exists
81-
const screenshot = result.screenshot;
82-
83-
const content = markdown ?? screenshot;
74+
const content = result.markdown ?? result.html ?? JSON.stringify(result.json, null, 2);
8475

8576
if (!content) {
8677
throw new Error('No content was scraped from the URL');

apps/web/client/src/server/api/routers/project/project.ts

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
import { env } from '@/env';
12
import { trackEvent } from '@/utils/analytics/server';
3+
import FirecrawlApp from '@mendable/firecrawl-js';
24
import { initModel } from '@onlook/ai';
35
import {
46
canvases,
@@ -25,9 +27,102 @@ import { and, eq, ne } from 'drizzle-orm';
2527
import { z } from 'zod';
2628
import { createTRPCRouter, protectedProcedure } from '../../trpc';
2729
import { projectCreateRequestRouter } from './createRequest';
30+
import { STORAGE_BUCKETS } from '@onlook/constants';
31+
import { fromPreviewImg } from '@onlook/db';
32+
import { getScreenshotPath, getValidUrl } from '@onlook/utility';
2833

2934
export const projectRouter = createTRPCRouter({
3035
createRequest: projectCreateRequestRouter,
36+
captureScreenshot: protectedProcedure
37+
.input(z.object({ projectId: z.string() }))
38+
.mutation(async ({ ctx, input }) => {
39+
try {
40+
if (!env.FIRECRAWL_API_KEY) {
41+
throw new Error('FIRECRAWL_API_KEY is not configured');
42+
}
43+
44+
const project = await ctx.db.query.projects.findFirst({
45+
where: eq(projects.id, input.projectId),
46+
});
47+
48+
if (!project) {
49+
throw new Error('Project not found');
50+
}
51+
52+
if (!project.sandboxUrl) {
53+
throw new Error('No sandbox URL found');
54+
}
55+
56+
const app = new FirecrawlApp({ apiKey: env.FIRECRAWL_API_KEY });
57+
58+
const result = await app.scrapeUrl(project.sandboxUrl, {
59+
formats: ['screenshot'],
60+
onlyMainContent: true,
61+
actions: [
62+
{
63+
type: 'screenshot',
64+
fullPage: true,
65+
},
66+
],
67+
});
68+
69+
if (!result.success) {
70+
throw new Error(`Failed to scrape URL: ${result.error || 'Unknown error'}`);
71+
}
72+
73+
const screenshotUrlRaw = result.screenshot;
74+
const screenshotUrl = screenshotUrlRaw ? getValidUrl(screenshotUrlRaw) : null;
75+
76+
if (!screenshotUrl) {
77+
throw new Error('Invalid screenshot URL');
78+
}
79+
80+
const response = await fetch(screenshotUrl);
81+
if (!response.ok) {
82+
throw new Error(`Failed to fetch screenshot: ${response.status} ${response.statusText}`);
83+
}
84+
85+
const arrayBuffer = await response.arrayBuffer();
86+
const mimeType = response.headers.get('content-type') ?? 'image/png';
87+
const path = getScreenshotPath(project.id, mimeType);
88+
89+
const { data, error } = await ctx.supabase.storage
90+
.from(STORAGE_BUCKETS.PREVIEW_IMAGES)
91+
.upload(path, arrayBuffer, {
92+
contentType: mimeType,
93+
});
94+
95+
if (error) {
96+
throw new Error(`Supabase upload error: ${error.message}`);
97+
}
98+
99+
if (!data) {
100+
throw new Error('No data returned from storage upload');
101+
}
102+
103+
const dbPreviewImg = fromPreviewImg({
104+
type: 'storage',
105+
storagePath: {
106+
bucket: STORAGE_BUCKETS.PREVIEW_IMAGES,
107+
path: data.path,
108+
},
109+
});
110+
111+
await ctx.db.update(projects)
112+
.set({
113+
previewImgUrl: dbPreviewImg.previewImgUrl,
114+
previewImgPath: dbPreviewImg.previewImgPath,
115+
previewImgBucket: dbPreviewImg.previewImgBucket,
116+
updatedAt: new Date(),
117+
})
118+
.where(eq(projects.id, project.id));
119+
120+
return { success: true, path: data.path };
121+
} catch (error) {
122+
console.error('Error capturing project screenshot:', error);
123+
return { success: false, error: error instanceof Error ? error.message : 'Unknown error' };
124+
}
125+
}),
31126
list: protectedProcedure
32127
.input(z.object({
33128
limit: z.number().optional(),

packages/ai/src/tools/web.ts

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ export const SCRAPE_URL_TOOL_NAME = 'scrape_url';
55
export const SCRAPE_URL_TOOL_PARAMETERS = z.object({
66
url: z.string().url().describe('The URL to scrape. Must be a valid HTTP or HTTPS URL.'),
77
formats: z
8-
.array(z.enum(['markdown', 'html', 'json', 'screenshot']))
8+
.array(z.enum(['markdown', 'html', 'json']))
99
.default(['markdown'])
1010
.describe('The formats to return the scraped content in. Defaults to markdown.'),
1111
onlyMainContent: z
@@ -26,14 +26,6 @@ export const SCRAPE_URL_TOOL_PARAMETERS = z.object({
2626
.number()
2727
.optional()
2828
.describe('Time in milliseconds to wait for the page to load before scraping.'),
29-
actions: z
30-
.array(
31-
z.object({
32-
type: z.enum(['screenshot']),
33-
fullPage: z.boolean().default(false).optional(),
34-
}),
35-
)
36-
.optional(),
3729
});
3830

3931
export const scrapeUrlTool = tool({

0 commit comments

Comments
 (0)