Unverified 提交 de52b9da authored 作者: Mohamed Aziz Mejri's avatar Mohamed Aziz Mejri 提交者: GitHub

Integrating web crawling in the custom theme generator (#2347)

<!-- CURSOR_SUMMARY --> > [!NOTE] > Introduces URL-based theme generation alongside image uploads, enabling prompts derived from live websites. > > - New `Website URL` input source in `AIGeneratorTab` with toggle, validation, crawl status indicators, and adjusted generate button/empty states > - Adds `useGenerateThemeFromUrl` hook and integrates it with existing generation flow and loading state > - Extends IPC types with `ThemeInputSource`, `CrawlStatus`, and `GenerateThemeFromUrlParams`; updates `templateContracts` with `generate-theme-from-url` > - Implements `generate-theme-from-url` handler: validates inputs, calls Dyad Engine `/tools/web-crawl`, selects web-crawl-specific system prompts (inspired/high-fidelity), streams model output, and returns prompt with robust error handling > - Preserves and refines image-based generation; resets state on dialog close and adds small UI polish (icons, counters, disabled states) > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit b5b1aebb277ce421953a06b148bd342fded2a64f. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> <!-- This is an auto-generated description by cubic. --> --- ## Summary by cubic Add URL-based theme generation with web crawling to the custom theme generator, alongside image uploads. Users can paste a website URL to extract a design system and generate a prompt with clear generation status, with added security and reliability improvements. - **New Features** - Source toggle: switch between “Upload Images” and “Website URL,” with state reset on dialog close. - URL input flow: validate URL and update button states/text; show generating state during processing. - New hook and types: useGenerateThemeFromUrl, ThemeInputSource, CrawlStatus. - IPC + contract: generate-theme-from-url with GenerateThemeFromUrlParams and result typing. - Backend handler: validates Dyad Pro + API key, URL, keywords; crawls via Dyad Engine (/tools/web-crawl); uses screenshot + markdown with truncation; distinct meta prompts for inspired vs high-fidelity; model mapping (Gemini 3 Pro, Claude Opus 4.5, GPT-5.2); streams prompt; returns clear errors; test mode stub output. - **Bug Fixes** - Security: restrict to HTTP/HTTPS URLs; block internal/private hosts (SSRF); sanitize crawled markdown and user keywords; Zod-validate crawl response. - Reliability: 120s crawl timeout with AbortController; clearer errors; UUID request IDs. - UI: add “generating” state; fix effect deps to avoid races; correct button disabled states. - Code health: shared model map at module scope (DRY). <sup>Written for commit b8aef2d51b1c5b7de5266f5afdfeb3174a64285b. Summary will update on new commits.</sup> <!-- End of auto-generated description by cubic. --> <!-- devin-review-badge-begin --> --- <a href="https://app.devin.ai/review/dyad-sh/dyad/pull/2347"> <picture> <source media="(prefers-color-scheme: dark)" srcset="https://static.devin.ai/assets/gh-open-in-devin-review-dark.svg?v=1"> <img src="https://static.devin.ai/assets/gh-open-in-devin-review-light.svg?v=1" alt="Open with Devin"> </picture> </a> <!-- devin-review-badge-end --> --------- Co-authored-by: 's avatarClaude <noreply@anthropic.com> Co-authored-by: 's avatarWill Chen <willchen90@gmail.com>
上级 9108f67c
......@@ -271,3 +271,68 @@ test("themes management - AI generator flow", async ({ po }) => {
await expect(po.page.getByText("AI Generated Theme")).toBeVisible();
await expect(po.page.getByText("Created via AI generator")).toBeVisible();
});
test("themes management - AI generator from website URL", async ({ po }) => {
await po.setUpDyadPro();
// Navigate to Themes page via Library sidebar
await po.goToLibraryTab();
await po.page.getByRole("link", { name: "Themes" }).click();
await expect(po.page.getByRole("heading", { name: "Themes" })).toBeVisible();
// Click New Theme button
await po.page.getByRole("button", { name: "New Theme" }).click();
// Wait for dialog to open
await expect(
po.page.getByRole("dialog").getByText("Create Custom Theme"),
).toBeVisible();
// Verify AI-Powered Generator tab is active by default
const aiTab = po.page.getByRole("tab", { name: "AI-Powered Generator" });
await expect(aiTab).toHaveAttribute("data-state", "active");
// Switch to Website URL input source
await po.page.getByRole("button", { name: "Website URL" }).click();
// Verify URL input is visible
const urlInput = po.page.getByLabel("Website URL");
await expect(urlInput).toBeVisible();
// Verify Generate button is disabled before entering URL
const generateButton = po.page.getByRole("button", {
name: "Generate Theme Prompt",
});
await expect(generateButton).toBeDisabled();
// Fill in theme details
await po.page.getByLabel("Theme Name").fill("Website Theme");
await po.page
.getByLabel("Description (optional)")
.fill("Generated from website");
// Enter a website URL
await urlInput.fill("https://example.com");
// Verify Generate button is now enabled
await expect(generateButton).toBeEnabled();
// Click Generate to get mock theme prompt (test mode returns mock response)
await generateButton.click();
// Wait for generation to complete - the generated prompt textarea should appear
await expect(po.page.locator("#ai-prompt")).toBeVisible({ timeout: 10000 });
// Verify the mock theme content is displayed (URL-specific mock)
await expect(po.page.getByText("Test Mode Theme (from URL)")).toBeVisible();
// Save the theme
await po.page.getByRole("button", { name: "Save Theme" }).click();
// Verify dialog closes and theme card appears
await expect(po.page.getByRole("dialog")).not.toBeVisible();
const themeCard = po.page.getByTestId("theme-card");
await expect(themeCard).toBeVisible();
await expect(themeCard.getByText("Website Theme")).toBeVisible();
await expect(themeCard.getByText("Generated from website")).toBeVisible();
});
......@@ -6,6 +6,7 @@ import type {
UpdateCustomThemeParams,
GenerateThemePromptParams,
GenerateThemePromptResult,
GenerateThemeFromUrlParams,
} from "@/ipc/types";
import { queryKeys } from "@/lib/queryKeys";
......@@ -92,3 +93,13 @@ export function useGenerateThemePrompt() {
},
});
}
export function useGenerateThemeFromUrl() {
return useMutation({
mutationFn: async (
params: GenerateThemeFromUrlParams,
): Promise<GenerateThemePromptResult> => {
return ipc.template.generateThemeFromUrl(params);
},
});
}
......@@ -243,8 +243,11 @@ export type {
DeleteCustomThemeParams,
ThemeGenerationMode,
ThemeGenerationModel,
ThemeInputSource,
CrawlStatus,
GenerateThemePromptParams,
GenerateThemePromptResult,
GenerateThemeFromUrlParams,
SaveThemeImageParams,
SaveThemeImageResult,
CleanupThemeImagesParams,
......
......@@ -100,6 +100,14 @@ export const ThemeGenerationModelSchema = z.enum([
]);
export type ThemeGenerationModel = z.infer<typeof ThemeGenerationModelSchema>;
// Theme input source (images or URL)
export const ThemeInputSourceSchema = z.enum(["images", "url"]);
export type ThemeInputSource = z.infer<typeof ThemeInputSourceSchema>;
// Crawl status for UI feedback
export const CrawlStatusSchema = z.enum(["crawling", "complete", "error"]);
export type CrawlStatus = z.infer<typeof CrawlStatusSchema>;
export const GenerateThemePromptParamsSchema = z.object({
imagePaths: z.array(z.string()),
keywords: z.string(),
......@@ -119,6 +127,31 @@ export type GenerateThemePromptResult = z.infer<
typeof GenerateThemePromptResultSchema
>;
// URL-based theme generation params
export const GenerateThemeFromUrlParamsSchema = z.object({
url: z
.string()
.url()
.refine(
(url) => {
try {
const parsed = new URL(url);
return parsed.protocol === "http:" || parsed.protocol === "https:";
} catch {
return false;
}
},
{ message: "Only HTTP and HTTPS URLs are supported" },
),
keywords: z.string(),
generationMode: ThemeGenerationModeSchema,
model: ThemeGenerationModelSchema,
});
export type GenerateThemeFromUrlParams = z.infer<
typeof GenerateThemeFromUrlParamsSchema
>;
export const SaveThemeImageParamsSchema = z.object({
data: z.string(),
filename: z.string(),
......@@ -201,6 +234,12 @@ export const templateContracts = {
output: GenerateThemePromptResultSchema,
}),
generateThemeFromUrl: defineContract({
channel: "generate-theme-from-url",
input: GenerateThemeFromUrlParamsSchema,
output: GenerateThemePromptResultSchema,
}),
saveThemeImage: defineContract({
channel: "save-theme-image",
input: SaveThemeImageParamsSchema,
......
......@@ -9,7 +9,7 @@ const webCrawlSchema = z.object({
url: z.string().describe("URL to crawl"),
});
const webCrawlResponseSchema = z.object({
export const webCrawlResponseSchema = z.object({
rootUrl: z.string(),
html: z.string().optional(),
markdown: z.string().optional(),
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论