Skip to content

Layer copy pasting #1791

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 8 commits into from
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 to use Firecrawl directly
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 @@ -40,6 +40,7 @@
"@emotion/react": "^11.13.3",
"@emotion/styled": "^11.13.0",
"@fontsource-variable/inter": "^5.1.0",
"@mendable/firecrawl-js": "^1.23.9",
"@onlook/foundation": "*",
"@onlook/supabase": "*",
"@onlook/ui": "*",
Expand Down
20 changes: 16 additions & 4 deletions apps/studio/src/lib/editor/engine/code/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,22 @@ export class CodeManager {
console.error('Failed to get code block. No oid found.');
return null;
}
return invokeMainChannel(MainChannels.GET_CODE_BLOCK, {
oid,
stripIds,
});
try {
const codeBlock = (await invokeMainChannel(MainChannels.GET_CODE_BLOCK, {
oid,
stripIds,
})) as string;

if (!codeBlock) {
console.error('No code block found for oid:', oid);
return null;
}

return codeBlock;
} catch (error) {
console.error('Error getting code block:', error);
return null;
}
}

async getFileContent(filePath: string, stripIds: boolean): Promise<string | null> {
Expand Down
26 changes: 23 additions & 3 deletions apps/studio/src/lib/editor/engine/copy/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { EditorAttributes } from '@onlook/models/constants';
import type { DomElement } from '@onlook/models/element';
import { makeAutoObservable } from 'mobx';
import type { EditorEngine } from '..';
import { toast } from '@onlook/ui/use-toast';

export class CopyManager {
copied: {
Expand Down Expand Up @@ -157,9 +158,28 @@ export class CopyManager {

async duplicate() {
const savedCopied = this.copied;
await this.copy();
await this.paste();
this.copied = savedCopied;
try {
await this.copy();
if (!this.copied) {
console.error('Nothing to duplicate, Failed to copy element');
return;
}
await this.paste();

const selectedWebview = this.editorEngine.webviews.selected[0];
if (selectedWebview) {
await this.editorEngine.elements.refreshSelectedElements(selectedWebview);
}
} catch (error) {
console.error('Failed to duplicate element', error);
toast({
title: 'Duplication failed',
description: 'Failed to duplicate element. Please try again.',
variant: 'destructive',
});
} finally {
this.copied = savedCopied;
}
}

clear() {
Expand Down
94 changes: 56 additions & 38 deletions apps/studio/src/lib/editor/engine/element/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,48 +137,66 @@ export class ElementManager {
}

for (const selectedEl of selected) {
const webviewId = selectedEl.webviewId;
const webview = this.editorEngine.webviews.getWebview(webviewId);
if (!webview) {
return;
}

const { shouldDelete, error } = await this.shouldDelete(selectedEl, webview);

if (!shouldDelete) {
try {
const webviewId = selectedEl.webviewId;
const webview = this.editorEngine.webviews.getWebview(webviewId);
if (!webview) {
return;
}

const { shouldDelete, error } = await this.shouldDelete(selectedEl, webview);

if (!shouldDelete) {
toast({
title: 'Cannot delete element',
description: error,
variant: 'destructive',
});
return;
}
const removeAction = (await webview.executeJavaScript(
`window.api?.getRemoveActionFromDomId('${selectedEl.domId}', '${webviewId}')`,
)) as RemoveElementAction | null;
if (!removeAction) {
console.error('Remove action not found');
toast({
title: 'Cannot delete element',
description: 'Remove action not found. Try refreshing the page.',
variant: 'destructive',
});
return;
}
let codeBlock;
if (selectedEl.instanceId) {
codeBlock = await this.editorEngine.code.getCodeBlock(selectedEl.instanceId);
}

if (!codeBlock) {
codeBlock = await this.editorEngine.code.getCodeBlock(selectedEl.oid);
}

if (!codeBlock) {
toast({
title: 'Cannot delete element',
description: 'Code block not found. Try refreshing the page.',
variant: 'destructive',
});
return;
}

removeAction.codeBlock = codeBlock;
this.editorEngine.action.run(removeAction);

this.clearSelectedElements();
this.editorEngine.overlay.state.removeClickRects();
} catch (error) {
console.error('Failed to delete element:', error);
toast({
title: 'Cannot delete element',
description: error,
title: 'Deletion failed',
description: 'An unexpected error occurred while deleting the element.',
variant: 'destructive',
});
return;
}

const removeAction = (await webview.executeJavaScript(
`window.api?.getRemoveActionFromDomId('${selectedEl.domId}', '${webviewId}')`,
)) as RemoveElementAction | null;
if (!removeAction) {
console.error('Remove action not found');
toast({
title: 'Cannot delete element',
description: 'Remove action not found. Try refreshing the page.',
variant: 'destructive',
});
return;
}
const oid = selectedEl.instanceId || selectedEl.oid;
const codeBlock = await this.editorEngine.code.getCodeBlock(oid);
if (!codeBlock) {
toast({
title: 'Cannot delete element',
description: 'Code block not found. Try refreshing the page.',
variant: 'destructive',
});
return;
}

removeAction.codeBlock = codeBlock;
this.editorEngine.action.run(removeAction);
}
}

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 @@ -105,6 +111,7 @@ export class CreateManager {
result = await invokeMainChannel(MainChannels.CREATE_NEW_PROJECT_PROMPT, {
prompt,
images,
crawledContent,
});
}

Expand Down
60 changes: 60 additions & 0 deletions apps/studio/src/lib/services/crawler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
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 (!CrawlerService.instance) {
CrawlerService.instance = new CrawlerService();
}
return CrawlerService.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('Crawl error:', error);
throw error;
}
}
}
6 changes: 5 additions & 1 deletion apps/studio/src/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,11 +68,15 @@
"title": "What kind of website do you want to make?",
"description": "Tell us a bit about your project. Be as detailed as possible.",
"input": {
"placeholder": "Paste a reference screenshot, write a novel, get creative...",
"placeholder": "Paste a reference screenshot, write a novel, get creative...\nlink (www.*****.com or anything with an https://) won't work.",
"imageUpload": "Upload Image Reference",
"fileReference": "File Reference",
"submit": "Start building your site"
},
"crawl": {
"title": "Crawl a website",
"description": "Paste a link to a website you want to duplicate"
},
"blankStart": "Start from a blank page"
}
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,13 @@ const PageTreeNode: React.FC<PageTreeNodeProps> = ({ node, style }) => {

const handleDuplicate = async () => {
try {
await editorEngine.pages.duplicatePage(node.data.path, node.data.path);
const basePath = node.data.path;
const newPath = basePath.replace(/(\/[^/]+)$/, (match) => {
const baseName = getBaseName(match);
const newName = `${baseName}1`;
return `/${newName}`;
});
await editorEngine.pages.duplicatePage(basePath, newPath);

toast({
title: 'Page duplicated',
Expand Down
Loading