Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 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
4 changes: 3 additions & 1 deletion apps/studio/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,6 @@ VITE_SUPABASE_ANON_KEY=
VITE_MIXPANEL_TOKEN=

# Add your keys here to use Anthropic directly
VITE_ANTHROPIC_API_KEY=
VITE_ANTHROPIC_API_KEY=
# Add your Firecrawl API key here
VITE_FIRECRAWL_API_KEY=
1 change: 1 addition & 0 deletions apps/studio/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
"@emotion/react": "^11.13.3",
"@emotion/styled": "^11.13.0",
"@fontsource-variable/inter": "^5.1.0",
"@mendable/firecrawl-js": "^1.24.0",
"@onlook/foundation": "*",
"@onlook/supabase": "*",
"@onlook/ui": "*",
Expand Down
9 changes: 8 additions & 1 deletion apps/studio/src/lib/projects/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,9 +89,15 @@ export class CreateManager {
}
}

async sendPrompt(prompt: string, images: ImageMessageContext[], blank: boolean = false) {
async sendPrompt(
prompt: string,
images: ImageMessageContext[],
crawledContent?: string,
blank: boolean = false,
) {
sendAnalytics('prompt create project', {
prompt,
crawledContent,
blank,
});

Expand All @@ -104,6 +110,7 @@ export class CreateManager {
} else {
result = await invokeMainChannel(MainChannels.CREATE_NEW_PROJECT_PROMPT, {
prompt,
crawledContent,
images,
});
}
Expand Down
58 changes: 58 additions & 0 deletions apps/studio/src/lib/services/crawler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import FirecrawlApp from '@mendable/firecrawl-js';

export interface CrawlOptions {
limit?: number;
scrapeOptions?: {
formats?: (
| 'markdown'
| 'html'
| 'rawHtml'
| 'content'
| 'links'
| 'screenshot'
| 'screenshot@fullPage'
| 'extract'
| 'json'
| 'changeTracking'
)[];
};
}

export class CrawlerService {
private static instance: CrawlerService;

private app: FirecrawlApp;

private constructor() {
this.app = new FirecrawlApp({ apiKey: process.env.VITE_FIRECRAWL_API_KEY });
}

static getInstance(): CrawlerService {
if (!this.instance) {
this.instance = new CrawlerService();
}
return this.instance;
}

async crawlUrl(
url: string,
options: CrawlOptions = {
limit: 100,
scrapeOptions: {
formats: ['markdown', 'html'],
},
},
) {
try {
const response = await this.app.crawlUrl(url, options);

if (!response.success) {
throw new Error(`Failed to crawl: ${response.error}`);
}
return response;
} catch (error) {
console.error('Error during crawling:', error);
throw error;
}
}
}
4 changes: 4 additions & 0 deletions apps/studio/src/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,10 @@
"fileReference": "File Reference",
"submit": "Start building your site"
},
"crawl": {
"title": "Duplicate a website",
"description": "Paste a link to a website that you want to duplicate"
},
"blankStart": "Start from a blank page"
}
},
Expand Down
122 changes: 120 additions & 2 deletions apps/studio/src/routes/projects/PromptCreation/PromptingCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import { useEffect, useRef, useState } from 'react';
import useResizeObserver from 'use-resize-observer';
import { DraftImagePill } from '../../editor/EditPanel/ChatTab/ContextPills/DraftingImagePill';
import { useTranslation } from 'react-i18next';
import { CrawlerService } from '@/lib/services/crawler';
import { toast } from '@onlook/ui/use-toast';

export const PromptingCard = () => {
const projectsManager = useProjectsManager();
Expand All @@ -28,6 +30,9 @@ export const PromptingCard = () => {
const [isComposing, setIsComposing] = useState(false);
const imageRef = useRef<HTMLInputElement>(null);
const { t } = useTranslation();
const [urlInput, setUrlInput] = useState('');
const [isCrawling, setIsCrawling] = useState(false);
const [crawledValue, setCrawledValue] = useState('');

useEffect(() => {
const handleEscapeKey = (e: KeyboardEvent) => {
Expand All @@ -45,11 +50,11 @@ export const PromptingCard = () => {
console.warn('Input is too short');
return;
}
projectsManager.create.sendPrompt(inputValue, selectedImages, false);
projectsManager.create.sendPrompt(inputValue, selectedImages, crawledValue, false);
};

const handleBlankSubmit = async () => {
projectsManager.create.sendPrompt('', [], true);
projectsManager.create.sendPrompt('', [], '', true);
};

const handleDragOver = (e: React.DragEvent) => {
Expand Down Expand Up @@ -179,6 +184,62 @@ export const PromptingCard = () => {
}
};

const handleCrawlSubmit = async () => {
if (!urlInput.trim()) {
console.warn('URL input is empty');
Copy link
Contributor

Choose a reason for hiding this comment

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

For an empty URL input, consider showing a user-visible error via toast rather than just logging a warning.

Suggested change
console.warn('URL input is empty');
toast({ title: 'URL Required', description: 'Please enter a URL before submitting.', variant: 'destructive' });

return;
}

try {
const url = new URL(urlInput);
if (!['http:', 'https:'].includes(url.protocol)) {
console.warn('URL must start with http or https');
toast({
title: 'Invalid URL',
description: 'Please enter a URL that starts with http or https.',
variant: 'destructive',
});
return;
}
} catch (error) {
console.warn('Invalid URL:', urlInput);
toast({
title: 'Invalid URL',
description: 'Please enter a valid URL format.',
variant: 'destructive',
});
return;
}

setIsCrawling(true);

try {
const crawler = CrawlerService.getInstance();

const response = await crawler.crawlUrl(urlInput);

const responseData = response.data;
Copy link
Contributor

Choose a reason for hiding this comment

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

Consider validating the structure of the crawled response (e.g., ensuring response.data is an array with expected 'html' and 'markdown' properties) before using it.

const html = responseData[0]?.html || '';
const markdown = responseData[0]?.markdown || '';

setCrawledValue(`---MARKDOWN---\n${markdown}\n\n---HTML---\n${html}`);

toast({
title: 'URL Crawled',
description: `Data for ${urlInput} has been crawled successfully.`,
});
} catch (error) {
console.error('Failed to crawl URL:', error);
toast({
title: 'Failed to Crawl URL',
description: error instanceof Error ? error.message : 'An unknown error occurred',
variant: 'destructive',
});
} finally {
setIsCrawling(false);
}
};

return (
<MotionConfig transition={{ duration: 0.5, type: 'spring', bounce: 0 }}>
<div className="flex flex-col gap-4 mb-12">
Expand Down Expand Up @@ -382,6 +443,63 @@ export const PromptingCard = () => {
</CardContent>
</motion.div>
</MotionCard>
<MotionCard
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 20 }}
className="w-[600px] backdrop-blur-md bg-background/30 overflow-hidden"
>
<CardHeader>
<motion.h2
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="text-2xl text-foreground-primary"
>
{t('projects.prompt.crawl.title')}
</motion.h2>
<motion.p
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.1 }}
className="text-sm text-foreground-secondary"
>
{t('projects.prompt.crawl.description')}
</motion.p>
</CardHeader>
<CardContent className="flex flex-col gap-4">
<div className="flex flex-col gap-2">
<div className="flex flex-row gap-2">
<input
type="url"
value={urlInput}
onChange={(e) => setUrlInput(e.target.value)}
placeholder="Enter URL to crawl..."
className={cn(
'flex-1 h-9 px-3 rounded-md',
'bg-background-secondary/80 backdrop-blur-sm',
'border border-border',
'text-sm text-foreground-primary',
'placeholder:text-foreground-secondary',
'focus:outline-none focus:ring-2 focus:ring-ring',
)}
/>
<Button
variant="secondary"
className="gap-2"
disabled={!urlInput.trim() || isCrawling}
onClick={handleCrawlSubmit}
>
{isCrawling ? (
<Icons.Circle className="w-4 h-4 animate-spin" />
) : (
<Icons.ArrowRight className="w-4 h-4" />
)}
<span>{isCrawling ? 'Crawling...' : 'Crawl URL'}</span>
</Button>
</div>
</div>
</CardContent>
</MotionCard>
<Button
variant="outline"
className="w-fit mx-auto bg-background-secondary/90 text-sm border text-foreground-secondary"
Expand Down
4 changes: 4 additions & 0 deletions apps/web/client/messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,10 @@
"fileReference": "File Reference",
"submit": "Start building your site"
},
"crawl": {
"title": "Duplicate a website",
"description": "Paste a link to a website that you want to duplicate"
},
"blankStart": "Start from a blank page"
}
},
Expand Down