community templates (#691)
This commit is contained in:
@@ -876,11 +876,11 @@ export class PageObject {
|
|||||||
await this.page.getByRole("link", { name: "Hub" }).click();
|
await this.page.getByRole("link", { name: "Hub" }).click();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async selectTemplate(templateName: string) {
|
async selectTemplate(templateName: string) {
|
||||||
await this.page.getByRole("img", { name: templateName }).click();
|
await this.page.getByRole("img", { name: templateName }).click();
|
||||||
}
|
}
|
||||||
|
|
||||||
async selectHubTemplate(templateName: "Next.js Template") {
|
async goToHubAndSelectTemplate(templateName: "Next.js Template") {
|
||||||
await this.goToHubTab();
|
await this.goToHubTab();
|
||||||
await this.selectTemplate(templateName);
|
await this.selectTemplate(templateName);
|
||||||
await this.goToAppsTab();
|
await this.goToAppsTab();
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ export default App;
|
|||||||
|
|
||||||
test("problems - manual edit (next.js)", async ({ po }) => {
|
test("problems - manual edit (next.js)", async ({ po }) => {
|
||||||
await po.setUp({ enableAutoFixProblems: true });
|
await po.setUp({ enableAutoFixProblems: true });
|
||||||
await po.selectHubTemplate("Next.js Template");
|
await po.goToHubAndSelectTemplate("Next.js Template");
|
||||||
await po.sendPrompt("tc=1");
|
await po.sendPrompt("tc=1");
|
||||||
|
|
||||||
const appPath = await po.getCurrentAppPath();
|
const appPath = await po.getCurrentAppPath();
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ testSkipIfWindows("upgrade app to select component", async ({ po }) => {
|
|||||||
testSkipIfWindows("select component next.js", async ({ po }) => {
|
testSkipIfWindows("select component next.js", async ({ po }) => {
|
||||||
await po.setUp();
|
await po.setUp();
|
||||||
|
|
||||||
await po.selectHubTemplate("Next.js Template");
|
await po.goToHubAndSelectTemplate("Next.js Template");
|
||||||
|
|
||||||
await po.sendPrompt("tc=basic");
|
await po.sendPrompt("tc=basic");
|
||||||
await po.clickTogglePreviewPanel();
|
await po.clickTogglePreviewPanel();
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
"lastShownReleaseNotesVersion": "[scrubbed]",
|
"lastShownReleaseNotesVersion": "[scrubbed]",
|
||||||
"enableProLazyEditsMode": true,
|
"enableProLazyEditsMode": true,
|
||||||
"enableProSmartFilesContextMode": true,
|
"enableProSmartFilesContextMode": true,
|
||||||
|
"selectedTemplateId": "react",
|
||||||
"selectedChatMode": "build",
|
"selectedChatMode": "build",
|
||||||
"enableAutoFixProblems": false,
|
"enableAutoFixProblems": false,
|
||||||
"enableAutoUpdate": false,
|
"enableAutoUpdate": false,
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
"lastShownReleaseNotesVersion": "[scrubbed]",
|
"lastShownReleaseNotesVersion": "[scrubbed]",
|
||||||
"enableProLazyEditsMode": true,
|
"enableProLazyEditsMode": true,
|
||||||
"enableProSmartFilesContextMode": true,
|
"enableProSmartFilesContextMode": true,
|
||||||
|
"selectedTemplateId": "react",
|
||||||
"selectedChatMode": "build",
|
"selectedChatMode": "build",
|
||||||
"enableAutoFixProblems": false,
|
"enableAutoFixProblems": false,
|
||||||
"enableAutoUpdate": true,
|
"enableAutoUpdate": true,
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
"maxChatTurnsInContext": 5,
|
"maxChatTurnsInContext": 5,
|
||||||
"enableProLazyEditsMode": true,
|
"enableProLazyEditsMode": true,
|
||||||
"enableProSmartFilesContextMode": true,
|
"enableProSmartFilesContextMode": true,
|
||||||
|
"selectedTemplateId": "react",
|
||||||
"selectedChatMode": "build",
|
"selectedChatMode": "build",
|
||||||
"enableAutoFixProblems": false,
|
"enableAutoFixProblems": false,
|
||||||
"enableAutoUpdate": true,
|
"enableAutoUpdate": true,
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
"lastShownReleaseNotesVersion": "[scrubbed]",
|
"lastShownReleaseNotesVersion": "[scrubbed]",
|
||||||
"enableProLazyEditsMode": true,
|
"enableProLazyEditsMode": true,
|
||||||
"enableProSmartFilesContextMode": true,
|
"enableProSmartFilesContextMode": true,
|
||||||
|
"selectedTemplateId": "react",
|
||||||
"selectedChatMode": "build",
|
"selectedChatMode": "build",
|
||||||
"enableAutoFixProblems": false,
|
"enableAutoFixProblems": false,
|
||||||
"enableAutoUpdate": true,
|
"enableAutoUpdate": true,
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
"lastShownReleaseNotesVersion": "[scrubbed]",
|
"lastShownReleaseNotesVersion": "[scrubbed]",
|
||||||
"enableProLazyEditsMode": true,
|
"enableProLazyEditsMode": true,
|
||||||
"enableProSmartFilesContextMode": true,
|
"enableProSmartFilesContextMode": true,
|
||||||
|
"selectedTemplateId": "react",
|
||||||
"selectedChatMode": "build",
|
"selectedChatMode": "build",
|
||||||
"enableAutoFixProblems": false,
|
"enableAutoFixProblems": false,
|
||||||
"enableAutoUpdate": true,
|
"enableAutoUpdate": true,
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
"experiments": {},
|
"experiments": {},
|
||||||
"enableProLazyEditsMode": true,
|
"enableProLazyEditsMode": true,
|
||||||
"enableProSmartFilesContextMode": true,
|
"enableProSmartFilesContextMode": true,
|
||||||
|
"selectedTemplateId": "react",
|
||||||
"selectedChatMode": "build",
|
"selectedChatMode": "build",
|
||||||
"enableAutoFixProblems": false,
|
"enableAutoFixProblems": false,
|
||||||
"enableAutoUpdate": true,
|
"enableAutoUpdate": true,
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
"lastShownReleaseNotesVersion": "[scrubbed]",
|
"lastShownReleaseNotesVersion": "[scrubbed]",
|
||||||
"enableProLazyEditsMode": true,
|
"enableProLazyEditsMode": true,
|
||||||
"enableProSmartFilesContextMode": true,
|
"enableProSmartFilesContextMode": true,
|
||||||
|
"selectedTemplateId": "react",
|
||||||
"selectedChatMode": "build",
|
"selectedChatMode": "build",
|
||||||
"enableAutoFixProblems": false,
|
"enableAutoFixProblems": false,
|
||||||
"enableAutoUpdate": true,
|
"enableAutoUpdate": true,
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
"experiments": {},
|
"experiments": {},
|
||||||
"enableProLazyEditsMode": true,
|
"enableProLazyEditsMode": true,
|
||||||
"enableProSmartFilesContextMode": true,
|
"enableProSmartFilesContextMode": true,
|
||||||
|
"selectedTemplateId": "react",
|
||||||
"selectedChatMode": "build",
|
"selectedChatMode": "build",
|
||||||
"enableAutoFixProblems": false,
|
"enableAutoFixProblems": false,
|
||||||
"enableAutoUpdate": true,
|
"enableAutoUpdate": true,
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
"lastShownReleaseNotesVersion": "[scrubbed]",
|
"lastShownReleaseNotesVersion": "[scrubbed]",
|
||||||
"enableProLazyEditsMode": true,
|
"enableProLazyEditsMode": true,
|
||||||
"enableProSmartFilesContextMode": true,
|
"enableProSmartFilesContextMode": true,
|
||||||
|
"selectedTemplateId": "react",
|
||||||
"selectedChatMode": "build",
|
"selectedChatMode": "build",
|
||||||
"enableAutoFixProblems": false,
|
"enableAutoFixProblems": false,
|
||||||
"enableAutoUpdate": true,
|
"enableAutoUpdate": true,
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
"experiments": {},
|
"experiments": {},
|
||||||
"enableProLazyEditsMode": true,
|
"enableProLazyEditsMode": true,
|
||||||
"enableProSmartFilesContextMode": true,
|
"enableProSmartFilesContextMode": true,
|
||||||
|
"selectedTemplateId": "react",
|
||||||
"selectedChatMode": "build",
|
"selectedChatMode": "build",
|
||||||
"enableAutoFixProblems": false,
|
"enableAutoFixProblems": false,
|
||||||
"enableAutoUpdate": true,
|
"enableAutoUpdate": true,
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
"lastShownReleaseNotesVersion": "[scrubbed]",
|
"lastShownReleaseNotesVersion": "[scrubbed]",
|
||||||
"enableProLazyEditsMode": true,
|
"enableProLazyEditsMode": true,
|
||||||
"enableProSmartFilesContextMode": true,
|
"enableProSmartFilesContextMode": true,
|
||||||
|
"selectedTemplateId": "react",
|
||||||
"selectedChatMode": "build",
|
"selectedChatMode": "build",
|
||||||
"enableAutoFixProblems": false,
|
"enableAutoFixProblems": false,
|
||||||
"enableAutoUpdate": true,
|
"enableAutoUpdate": true,
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"selectedModel": {
|
||||||
|
"name": "auto",
|
||||||
|
"provider": "auto"
|
||||||
|
},
|
||||||
|
"providerSettings": {},
|
||||||
|
"telemetryConsent": "unset",
|
||||||
|
"telemetryUserId": "[UUID]",
|
||||||
|
"hasRunBefore": true,
|
||||||
|
"experiments": {},
|
||||||
|
"lastShownReleaseNotesVersion": "[scrubbed]",
|
||||||
|
"enableProLazyEditsMode": true,
|
||||||
|
"enableProSmartFilesContextMode": true,
|
||||||
|
"selectedTemplateId": "react",
|
||||||
|
"selectedChatMode": "build",
|
||||||
|
"enableAutoFixProblems": false,
|
||||||
|
"enableAutoUpdate": true,
|
||||||
|
"releaseChannel": "stable",
|
||||||
|
"isTestMode": true
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"selectedModel": {
|
||||||
|
"name": "auto",
|
||||||
|
"provider": "auto"
|
||||||
|
},
|
||||||
|
"providerSettings": {},
|
||||||
|
"telemetryConsent": "unset",
|
||||||
|
"telemetryUserId": "[UUID]",
|
||||||
|
"hasRunBefore": true,
|
||||||
|
"experiments": {},
|
||||||
|
"lastShownReleaseNotesVersion": "[scrubbed]",
|
||||||
|
"enableProLazyEditsMode": true,
|
||||||
|
"enableProSmartFilesContextMode": true,
|
||||||
|
"selectedTemplateId": "jeff-kazzee/dyad-template-angular",
|
||||||
|
"selectedChatMode": "build",
|
||||||
|
"acceptedCommunityCode": true,
|
||||||
|
"enableAutoFixProblems": false,
|
||||||
|
"enableAutoUpdate": true,
|
||||||
|
"releaseChannel": "stable",
|
||||||
|
"isTestMode": true
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"selectedModel": {
|
||||||
|
"name": "test-model",
|
||||||
|
"provider": "custom::testing",
|
||||||
|
"customModelId": 1
|
||||||
|
},
|
||||||
|
"providerSettings": {},
|
||||||
|
"telemetryConsent": "unset",
|
||||||
|
"telemetryUserId": "[UUID]",
|
||||||
|
"hasRunBefore": true,
|
||||||
|
"experiments": {},
|
||||||
|
"lastShownReleaseNotesVersion": "[scrubbed]",
|
||||||
|
"enableProLazyEditsMode": true,
|
||||||
|
"enableProSmartFilesContextMode": true,
|
||||||
|
"selectedTemplateId": "next",
|
||||||
|
"selectedChatMode": "build",
|
||||||
|
"enableAutoFixProblems": false,
|
||||||
|
"enableAutoUpdate": true,
|
||||||
|
"releaseChannel": "stable",
|
||||||
|
"isTestMode": true
|
||||||
|
}
|
||||||
@@ -20,6 +20,7 @@
|
|||||||
"thinkingBudget": "low",
|
"thinkingBudget": "low",
|
||||||
"enableProLazyEditsMode": true,
|
"enableProLazyEditsMode": true,
|
||||||
"enableProSmartFilesContextMode": true,
|
"enableProSmartFilesContextMode": true,
|
||||||
|
"selectedTemplateId": "react",
|
||||||
"selectedChatMode": "build",
|
"selectedChatMode": "build",
|
||||||
"enableAutoFixProblems": false,
|
"enableAutoFixProblems": false,
|
||||||
"enableAutoUpdate": true,
|
"enableAutoUpdate": true,
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
"thinkingBudget": "medium",
|
"thinkingBudget": "medium",
|
||||||
"enableProLazyEditsMode": true,
|
"enableProLazyEditsMode": true,
|
||||||
"enableProSmartFilesContextMode": true,
|
"enableProSmartFilesContextMode": true,
|
||||||
|
"selectedTemplateId": "react",
|
||||||
"selectedChatMode": "build",
|
"selectedChatMode": "build",
|
||||||
"enableAutoFixProblems": false,
|
"enableAutoFixProblems": false,
|
||||||
"enableAutoUpdate": true,
|
"enableAutoUpdate": true,
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
"thinkingBudget": "high",
|
"thinkingBudget": "high",
|
||||||
"enableProLazyEditsMode": true,
|
"enableProLazyEditsMode": true,
|
||||||
"enableProSmartFilesContextMode": true,
|
"enableProSmartFilesContextMode": true,
|
||||||
|
"selectedTemplateId": "react",
|
||||||
"selectedChatMode": "build",
|
"selectedChatMode": "build",
|
||||||
"enableAutoFixProblems": false,
|
"enableAutoFixProblems": false,
|
||||||
"enableAutoUpdate": true,
|
"enableAutoUpdate": true,
|
||||||
|
|||||||
19
e2e-tests/template-community.spec.ts
Normal file
19
e2e-tests/template-community.spec.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { test } from "./helpers/test_helper";
|
||||||
|
|
||||||
|
test("template - community", async ({ po }) => {
|
||||||
|
await po.goToHubTab();
|
||||||
|
// This is a community template, so we should see the consent dialog
|
||||||
|
await po.selectTemplate("Angular");
|
||||||
|
await po.page.getByRole("button", { name: "Cancel" }).click();
|
||||||
|
await po.snapshotSettings();
|
||||||
|
|
||||||
|
await po.selectTemplate("Angular");
|
||||||
|
await po.page.getByRole("button", { name: "Accept" }).click();
|
||||||
|
await po.page
|
||||||
|
.locator("section")
|
||||||
|
.filter({ hasText: "Community" })
|
||||||
|
.locator("div")
|
||||||
|
.first()
|
||||||
|
.click();
|
||||||
|
await po.snapshotSettings();
|
||||||
|
});
|
||||||
@@ -3,7 +3,8 @@ import { expect } from "@playwright/test";
|
|||||||
|
|
||||||
test("create next.js app", async ({ po }) => {
|
test("create next.js app", async ({ po }) => {
|
||||||
await po.setUp();
|
await po.setUp();
|
||||||
await po.selectHubTemplate("Next.js Template");
|
await po.goToHubAndSelectTemplate("Next.js Template");
|
||||||
|
await po.snapshotSettings();
|
||||||
|
|
||||||
// Create an app
|
// Create an app
|
||||||
await po.sendPrompt("tc=edit-made-with-dyad");
|
await po.sendPrompt("tc=edit-made-with-dyad");
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import path from "node:path";
|
|||||||
import { safeStorage } from "electron";
|
import { safeStorage } from "electron";
|
||||||
import { readSettings, getSettingsFilePath } from "@/main/settings";
|
import { readSettings, getSettingsFilePath } from "@/main/settings";
|
||||||
import { getUserDataPath } from "@/paths/paths";
|
import { getUserDataPath } from "@/paths/paths";
|
||||||
|
import { UserSettings } from "@/lib/schemas";
|
||||||
|
|
||||||
// Mock dependencies
|
// Mock dependencies
|
||||||
vi.mock("node:fs");
|
vi.mock("node:fs");
|
||||||
@@ -50,23 +51,26 @@ describe("readSettings", () => {
|
|||||||
mockSettingsPath,
|
mockSettingsPath,
|
||||||
expect.stringContaining('"selectedModel"'),
|
expect.stringContaining('"selectedModel"'),
|
||||||
);
|
);
|
||||||
expect(result).toEqual({
|
expect(scrubSettings(result)).toMatchInlineSnapshot(`
|
||||||
selectedModel: {
|
{
|
||||||
name: "auto",
|
"enableAutoFixProblems": false,
|
||||||
provider: "auto",
|
"enableAutoUpdate": true,
|
||||||
},
|
"enableProLazyEditsMode": true,
|
||||||
providerSettings: {},
|
"enableProSmartFilesContextMode": true,
|
||||||
telemetryConsent: "unset",
|
"experiments": {},
|
||||||
telemetryUserId: expect.any(String),
|
"hasRunBefore": false,
|
||||||
hasRunBefore: false,
|
"providerSettings": {},
|
||||||
experiments: {},
|
"releaseChannel": "stable",
|
||||||
enableProLazyEditsMode: true,
|
"selectedChatMode": "build",
|
||||||
enableProSmartFilesContextMode: true,
|
"selectedModel": {
|
||||||
selectedChatMode: "build",
|
"name": "auto",
|
||||||
enableAutoFixProblems: false,
|
"provider": "auto",
|
||||||
enableAutoUpdate: true,
|
},
|
||||||
releaseChannel: "stable",
|
"selectedTemplateId": "react",
|
||||||
});
|
"telemetryConsent": "unset",
|
||||||
|
"telemetryUserId": "[scrubbed]",
|
||||||
|
}
|
||||||
|
`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -293,23 +297,26 @@ describe("readSettings", () => {
|
|||||||
|
|
||||||
const result = readSettings();
|
const result = readSettings();
|
||||||
|
|
||||||
expect(result).toEqual({
|
expect(scrubSettings(result)).toMatchInlineSnapshot(`
|
||||||
selectedModel: {
|
{
|
||||||
name: "auto",
|
"enableAutoFixProblems": false,
|
||||||
provider: "auto",
|
"enableAutoUpdate": true,
|
||||||
},
|
"enableProLazyEditsMode": true,
|
||||||
providerSettings: {},
|
"enableProSmartFilesContextMode": true,
|
||||||
telemetryConsent: "unset",
|
"experiments": {},
|
||||||
telemetryUserId: expect.any(String),
|
"hasRunBefore": false,
|
||||||
hasRunBefore: false,
|
"providerSettings": {},
|
||||||
experiments: {},
|
"releaseChannel": "stable",
|
||||||
enableProLazyEditsMode: true,
|
"selectedChatMode": "build",
|
||||||
enableProSmartFilesContextMode: true,
|
"selectedModel": {
|
||||||
selectedChatMode: "build",
|
"name": "auto",
|
||||||
enableAutoFixProblems: false,
|
"provider": "auto",
|
||||||
enableAutoUpdate: true,
|
},
|
||||||
releaseChannel: "stable",
|
"selectedTemplateId": "react",
|
||||||
});
|
"telemetryConsent": "unset",
|
||||||
|
"telemetryUserId": "[scrubbed]",
|
||||||
|
}
|
||||||
|
`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return default settings when JSON parsing fails", () => {
|
it("should return default settings when JSON parsing fails", () => {
|
||||||
@@ -389,3 +396,10 @@ describe("readSettings", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function scrubSettings(result: UserSettings) {
|
||||||
|
return {
|
||||||
|
...result,
|
||||||
|
telemetryUserId: "[scrubbed]",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
51
src/components/CommunityCodeConsentDialog.tsx
Normal file
51
src/components/CommunityCodeConsentDialog.tsx
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import React from "react";
|
||||||
|
import {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogAction,
|
||||||
|
AlertDialogCancel,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogTitle,
|
||||||
|
} from "@/components/ui/alert-dialog";
|
||||||
|
|
||||||
|
interface CommunityCodeConsentDialogProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onAccept: () => void;
|
||||||
|
onCancel: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CommunityCodeConsentDialog: React.FC<
|
||||||
|
CommunityCodeConsentDialogProps
|
||||||
|
> = ({ isOpen, onAccept, onCancel }) => {
|
||||||
|
return (
|
||||||
|
<AlertDialog open={isOpen} onOpenChange={(open) => !open && onCancel()}>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>Community Code Notice</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription className="space-y-3">
|
||||||
|
<p>
|
||||||
|
This code was created by a Dyad community member, not our core
|
||||||
|
team.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Community code can be very helpful, but since it's built
|
||||||
|
independently, it may have bugs, security risks, or could cause
|
||||||
|
issues with your system. We can't provide official support if
|
||||||
|
problems occur.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
We recommend reviewing the code on GitHub first. Only proceed if
|
||||||
|
you're comfortable with these risks.
|
||||||
|
</p>
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel onClick={onCancel}>Cancel</AlertDialogCancel>
|
||||||
|
<AlertDialogAction onClick={onAccept}>Accept</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
134
src/components/TemplateCard.tsx
Normal file
134
src/components/TemplateCard.tsx
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import { ArrowLeft } from "lucide-react";
|
||||||
|
import { IpcClient } from "@/ipc/ipc_client";
|
||||||
|
import { useSettings } from "@/hooks/useSettings";
|
||||||
|
import { CommunityCodeConsentDialog } from "./CommunityCodeConsentDialog";
|
||||||
|
import type { Template } from "@/shared/templates";
|
||||||
|
|
||||||
|
interface TemplateCardProps {
|
||||||
|
template: Template;
|
||||||
|
isSelected: boolean;
|
||||||
|
onSelect: (templateId: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TemplateCard: React.FC<TemplateCardProps> = ({
|
||||||
|
template,
|
||||||
|
isSelected,
|
||||||
|
onSelect,
|
||||||
|
}) => {
|
||||||
|
const { settings, updateSettings } = useSettings();
|
||||||
|
const [showConsentDialog, setShowConsentDialog] = useState(false);
|
||||||
|
|
||||||
|
const handleCardClick = () => {
|
||||||
|
// If it's a community template and user hasn't accepted community code yet, show dialog
|
||||||
|
if (!template.isOfficial && !settings?.acceptedCommunityCode) {
|
||||||
|
setShowConsentDialog(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, proceed with selection
|
||||||
|
onSelect(template.id);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleConsentAccept = () => {
|
||||||
|
// Update settings to accept community code
|
||||||
|
updateSettings({ acceptedCommunityCode: true });
|
||||||
|
|
||||||
|
// Select the template
|
||||||
|
onSelect(template.id);
|
||||||
|
|
||||||
|
// Close dialog
|
||||||
|
setShowConsentDialog(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleConsentCancel = () => {
|
||||||
|
// Just close dialog, don't update settings or select template
|
||||||
|
setShowConsentDialog(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleGithubClick = (e: React.MouseEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
if (template.githubUrl) {
|
||||||
|
IpcClient.getInstance().openExternalUrl(template.githubUrl);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
onClick={handleCardClick}
|
||||||
|
className={`
|
||||||
|
bg-white dark:bg-gray-800 rounded-xl shadow-sm overflow-hidden
|
||||||
|
transform transition-all duration-300 ease-in-out
|
||||||
|
cursor-pointer group relative
|
||||||
|
${
|
||||||
|
isSelected
|
||||||
|
? "ring-2 ring-blue-500 dark:ring-blue-400 shadow-xl"
|
||||||
|
: "hover:shadow-lg hover:-translate-y-1"
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
<div className="relative">
|
||||||
|
<img
|
||||||
|
src={template.imageUrl}
|
||||||
|
alt={template.title}
|
||||||
|
className={`w-full h-52 object-cover transition-opacity duration-300 group-hover:opacity-80 ${
|
||||||
|
isSelected ? "opacity-75" : ""
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
{isSelected && (
|
||||||
|
<span className="absolute top-3 right-3 bg-blue-600 text-white text-xs font-bold px-3 py-1.5 rounded-md shadow-lg">
|
||||||
|
Selected
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="p-4">
|
||||||
|
<div className="flex justify-between items-center mb-1.5">
|
||||||
|
<h2
|
||||||
|
className={`text-lg font-semibold ${
|
||||||
|
isSelected
|
||||||
|
? "text-blue-600 dark:text-blue-400"
|
||||||
|
: "text-gray-900 dark:text-white"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{template.title}
|
||||||
|
</h2>
|
||||||
|
{template.isOfficial && (
|
||||||
|
<span
|
||||||
|
className={`text-xs font-semibold px-2 py-0.5 rounded-full ${
|
||||||
|
isSelected
|
||||||
|
? "bg-blue-100 text-blue-700 dark:bg-blue-600 dark:text-blue-100"
|
||||||
|
: "bg-green-100 text-green-800 dark:bg-green-700 dark:text-green-200"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Official
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400 mb-3 h-8 overflow-y-auto">
|
||||||
|
{template.description}
|
||||||
|
</p>
|
||||||
|
{template.githubUrl && (
|
||||||
|
<a
|
||||||
|
className={`inline-flex items-center text-sm font-medium transition-colors duration-200 ${
|
||||||
|
isSelected
|
||||||
|
? "text-blue-500 hover:text-blue-700 dark:text-blue-300 dark:hover:text-blue-200"
|
||||||
|
: "text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300"
|
||||||
|
}`}
|
||||||
|
onClick={handleGithubClick}
|
||||||
|
>
|
||||||
|
View on GitHub{" "}
|
||||||
|
<ArrowLeft className="w-4 h-4 ml-1 transform rotate-180" />
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<CommunityCodeConsentDialog
|
||||||
|
isOpen={showConsentDialog}
|
||||||
|
onAccept={handleConsentAccept}
|
||||||
|
onCancel={handleConsentCancel}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
24
src/hooks/useTemplates.ts
Normal file
24
src/hooks/useTemplates.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { IpcClient } from "@/ipc/ipc_client";
|
||||||
|
import { localTemplatesData, type Template } from "@/shared/templates";
|
||||||
|
|
||||||
|
export function useTemplates() {
|
||||||
|
const query = useQuery({
|
||||||
|
queryKey: ["templates"],
|
||||||
|
queryFn: async (): Promise<Template[]> => {
|
||||||
|
const ipcClient = IpcClient.getInstance();
|
||||||
|
return ipcClient.getTemplates();
|
||||||
|
},
|
||||||
|
initialData: localTemplatesData,
|
||||||
|
meta: {
|
||||||
|
showErrorToast: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
templates: query.data,
|
||||||
|
isLoading: query.isLoading,
|
||||||
|
error: query.error,
|
||||||
|
refetch: query.refetch,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -5,7 +5,7 @@ import http from "isomorphic-git/http/node";
|
|||||||
import { app } from "electron";
|
import { app } from "electron";
|
||||||
import { copyDirectoryRecursive } from "../utils/file_utils";
|
import { copyDirectoryRecursive } from "../utils/file_utils";
|
||||||
import { readSettings } from "@/main/settings";
|
import { readSettings } from "@/main/settings";
|
||||||
import { DEFAULT_TEMPLATE_ID, getTemplateOrThrow } from "@/shared/templates";
|
import { getTemplateOrThrow } from "../utils/template_utils";
|
||||||
import log from "electron-log";
|
import log from "electron-log";
|
||||||
|
|
||||||
const logger = log.scope("createFromTemplate");
|
const logger = log.scope("createFromTemplate");
|
||||||
@@ -16,7 +16,7 @@ export async function createFromTemplate({
|
|||||||
fullAppPath: string;
|
fullAppPath: string;
|
||||||
}) {
|
}) {
|
||||||
const settings = readSettings();
|
const settings = readSettings();
|
||||||
const templateId = settings.selectedTemplateId ?? DEFAULT_TEMPLATE_ID;
|
const templateId = settings.selectedTemplateId;
|
||||||
|
|
||||||
if (templateId === "react") {
|
if (templateId === "react") {
|
||||||
await copyDirectoryRecursive(
|
await copyDirectoryRecursive(
|
||||||
@@ -26,7 +26,7 @@ export async function createFromTemplate({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const template = getTemplateOrThrow(templateId);
|
const template = await getTemplateOrThrow(templateId);
|
||||||
if (!template.githubUrl) {
|
if (!template.githubUrl) {
|
||||||
throw new Error(`Template ${templateId} has no GitHub URL`);
|
throw new Error(`Template ${templateId} has no GitHub URL`);
|
||||||
}
|
}
|
||||||
|
|||||||
19
src/ipc/handlers/template_handlers.ts
Normal file
19
src/ipc/handlers/template_handlers.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { createLoggedHandler } from "./safe_handle";
|
||||||
|
import log from "electron-log";
|
||||||
|
import { getAllTemplates } from "../utils/template_utils";
|
||||||
|
import { localTemplatesData, type Template } from "../../shared/templates";
|
||||||
|
|
||||||
|
const logger = log.scope("template_handlers");
|
||||||
|
const handle = createLoggedHandler(logger);
|
||||||
|
|
||||||
|
export function registerTemplateHandlers() {
|
||||||
|
handle("get-templates", async (): Promise<Template[]> => {
|
||||||
|
try {
|
||||||
|
const templates = await getAllTemplates();
|
||||||
|
return templates;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Error fetching templates:", error);
|
||||||
|
return localTemplatesData;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -52,6 +52,7 @@ import type {
|
|||||||
UpdateChatParams,
|
UpdateChatParams,
|
||||||
FileAttachment,
|
FileAttachment,
|
||||||
} from "./ipc_types";
|
} from "./ipc_types";
|
||||||
|
import type { Template } from "../shared/templates";
|
||||||
import type { AppChatContext, ProposalResult } from "@/lib/schemas";
|
import type { AppChatContext, ProposalResult } from "@/lib/schemas";
|
||||||
import { showError } from "@/lib/toast";
|
import { showError } from "@/lib/toast";
|
||||||
|
|
||||||
@@ -1024,4 +1025,9 @@ export class IpcClient {
|
|||||||
}): Promise<ProblemReport> {
|
}): Promise<ProblemReport> {
|
||||||
return this.ipcRenderer.invoke("check-problems", params);
|
return this.ipcRenderer.invoke("check-problems", params);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Template methods
|
||||||
|
public async getTemplates(): Promise<Template[]> {
|
||||||
|
return this.ipcRenderer.invoke("get-templates");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import { registerAppUpgradeHandlers } from "./handlers/app_upgrade_handlers";
|
|||||||
import { registerCapacitorHandlers } from "./handlers/capacitor_handlers";
|
import { registerCapacitorHandlers } from "./handlers/capacitor_handlers";
|
||||||
import { registerProblemsHandlers } from "./handlers/problems_handlers";
|
import { registerProblemsHandlers } from "./handlers/problems_handlers";
|
||||||
import { registerAppEnvVarsHandlers } from "./handlers/app_env_vars_handlers";
|
import { registerAppEnvVarsHandlers } from "./handlers/app_env_vars_handlers";
|
||||||
|
import { registerTemplateHandlers } from "./handlers/template_handlers";
|
||||||
|
|
||||||
export function registerIpcHandlers() {
|
export function registerIpcHandlers() {
|
||||||
// Register all IPC handlers by category
|
// Register all IPC handlers by category
|
||||||
@@ -55,4 +56,5 @@ export function registerIpcHandlers() {
|
|||||||
registerAppUpgradeHandlers();
|
registerAppUpgradeHandlers();
|
||||||
registerCapacitorHandlers();
|
registerCapacitorHandlers();
|
||||||
registerAppEnvVarsHandlers();
|
registerAppEnvVarsHandlers();
|
||||||
|
registerTemplateHandlers();
|
||||||
}
|
}
|
||||||
|
|||||||
82
src/ipc/utils/template_utils.ts
Normal file
82
src/ipc/utils/template_utils.ts
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
import {
|
||||||
|
type Template,
|
||||||
|
type ApiTemplate,
|
||||||
|
localTemplatesData,
|
||||||
|
} from "../../shared/templates";
|
||||||
|
import log from "electron-log";
|
||||||
|
|
||||||
|
const logger = log.scope("template_utils");
|
||||||
|
|
||||||
|
// In-memory cache for API templates
|
||||||
|
let apiTemplatesCache: Template[] | null = null;
|
||||||
|
let apiTemplatesFetchPromise: Promise<Template[]> | null = null;
|
||||||
|
|
||||||
|
// Convert API template to our Template interface
|
||||||
|
function convertApiTemplate(apiTemplate: ApiTemplate): Template {
|
||||||
|
return {
|
||||||
|
id: `${apiTemplate.githubOrg}/${apiTemplate.githubRepo}`,
|
||||||
|
title: apiTemplate.title,
|
||||||
|
description: apiTemplate.description,
|
||||||
|
imageUrl: apiTemplate.imageUrl,
|
||||||
|
githubUrl: `https://github.com/${apiTemplate.githubOrg}/${apiTemplate.githubRepo}`,
|
||||||
|
isOfficial: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch templates from API with caching
|
||||||
|
export async function fetchApiTemplates(): Promise<Template[]> {
|
||||||
|
// Return cached data if available
|
||||||
|
if (apiTemplatesCache) {
|
||||||
|
return apiTemplatesCache;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return existing promise if fetch is already in progress
|
||||||
|
if (apiTemplatesFetchPromise) {
|
||||||
|
return apiTemplatesFetchPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start new fetch
|
||||||
|
apiTemplatesFetchPromise = (async (): Promise<Template[]> => {
|
||||||
|
try {
|
||||||
|
const response = await fetch("https://api.dyad.sh/v1/templates");
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(
|
||||||
|
`Failed to fetch templates: ${response.status} ${response.statusText}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const apiTemplates: ApiTemplate[] = await response.json();
|
||||||
|
const convertedTemplates = apiTemplates.map(convertApiTemplate);
|
||||||
|
|
||||||
|
// Cache the result
|
||||||
|
apiTemplatesCache = convertedTemplates;
|
||||||
|
return convertedTemplates;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Failed to fetch API templates:", error);
|
||||||
|
// Reset the promise so we can retry later
|
||||||
|
apiTemplatesFetchPromise = null;
|
||||||
|
return []; // Return empty array on error
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
return apiTemplatesFetchPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all templates (local + API)
|
||||||
|
export async function getAllTemplates(): Promise<Template[]> {
|
||||||
|
const apiTemplates = await fetchApiTemplates();
|
||||||
|
return [...localTemplatesData, ...apiTemplates];
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getTemplateOrThrow(
|
||||||
|
templateId: string,
|
||||||
|
): Promise<Template> {
|
||||||
|
const allTemplates = await getAllTemplates();
|
||||||
|
const template = allTemplates.find((template) => template.id === templateId);
|
||||||
|
if (!template) {
|
||||||
|
throw new Error(
|
||||||
|
`Template ${templateId} not found. Please select a different template.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return template;
|
||||||
|
}
|
||||||
@@ -149,9 +149,10 @@ export const UserSettingsSchema = z.object({
|
|||||||
thinkingBudget: z.enum(["low", "medium", "high"]).optional(),
|
thinkingBudget: z.enum(["low", "medium", "high"]).optional(),
|
||||||
enableProLazyEditsMode: z.boolean().optional(),
|
enableProLazyEditsMode: z.boolean().optional(),
|
||||||
enableProSmartFilesContextMode: z.boolean().optional(),
|
enableProSmartFilesContextMode: z.boolean().optional(),
|
||||||
selectedTemplateId: z.string().optional(),
|
selectedTemplateId: z.string(),
|
||||||
enableSupabaseWriteSqlMigration: z.boolean().optional(),
|
enableSupabaseWriteSqlMigration: z.boolean().optional(),
|
||||||
selectedChatMode: ChatModeSchema.optional(),
|
selectedChatMode: ChatModeSchema.optional(),
|
||||||
|
acceptedCommunityCode: z.boolean().optional(),
|
||||||
|
|
||||||
enableAutoFixProblems: z.boolean().optional(),
|
enableAutoFixProblems: z.boolean().optional(),
|
||||||
enableNativeGit: z.boolean().optional(),
|
enableNativeGit: z.boolean().optional(),
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ export const showError = (message: any) => {
|
|||||||
onCopy={() => onCopy(t)}
|
onCopy={() => onCopy(t)}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
{ duration: 4000 },
|
{ duration: 8_000 },
|
||||||
);
|
);
|
||||||
|
|
||||||
return toastId;
|
return toastId;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { UserSettingsSchema, type UserSettings, Secret } from "../lib/schemas";
|
|||||||
import { safeStorage } from "electron";
|
import { safeStorage } from "electron";
|
||||||
import { v4 as uuidv4 } from "uuid";
|
import { v4 as uuidv4 } from "uuid";
|
||||||
import log from "electron-log";
|
import log from "electron-log";
|
||||||
|
import { DEFAULT_TEMPLATE_ID } from "@/shared/templates";
|
||||||
|
|
||||||
const logger = log.scope("settings");
|
const logger = log.scope("settings");
|
||||||
|
|
||||||
@@ -26,6 +27,7 @@ const DEFAULT_SETTINGS: UserSettings = {
|
|||||||
enableAutoFixProblems: false,
|
enableAutoFixProblems: false,
|
||||||
enableAutoUpdate: true,
|
enableAutoUpdate: true,
|
||||||
releaseChannel: "stable",
|
releaseChannel: "stable",
|
||||||
|
selectedTemplateId: DEFAULT_TEMPLATE_ID,
|
||||||
};
|
};
|
||||||
|
|
||||||
const SETTINGS_FILE = "user-settings.json";
|
const SETTINGS_FILE = "user-settings.json";
|
||||||
|
|||||||
@@ -3,20 +3,26 @@ import { Button } from "@/components/ui/button";
|
|||||||
import { ArrowLeft } from "lucide-react";
|
import { ArrowLeft } from "lucide-react";
|
||||||
import { useRouter } from "@tanstack/react-router";
|
import { useRouter } from "@tanstack/react-router";
|
||||||
import { useSettings } from "@/hooks/useSettings";
|
import { useSettings } from "@/hooks/useSettings";
|
||||||
import { IpcClient } from "@/ipc/ipc_client";
|
import { useTemplates } from "@/hooks/useTemplates";
|
||||||
import { DEFAULT_TEMPLATE_ID, templatesData } from "@/shared/templates";
|
import { TemplateCard } from "@/components/TemplateCard";
|
||||||
|
|
||||||
const HubPage: React.FC = () => {
|
const HubPage: React.FC = () => {
|
||||||
const { settings, updateSettings } = useSettings();
|
const { settings, updateSettings } = useSettings();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const { templates, isLoading } = useTemplates();
|
||||||
|
|
||||||
const selectedTemplateId =
|
const selectedTemplateId = settings?.selectedTemplateId;
|
||||||
settings?.selectedTemplateId || DEFAULT_TEMPLATE_ID;
|
|
||||||
|
|
||||||
const handleTemplateSelect = (templateId: string) => {
|
const handleTemplateSelect = (templateId: string) => {
|
||||||
updateSettings({ selectedTemplateId: templateId });
|
updateSettings({ selectedTemplateId: templateId });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Separate templates into official and community
|
||||||
|
const officialTemplates =
|
||||||
|
templates?.filter((template) => template.isOfficial) || [];
|
||||||
|
const communityTemplates =
|
||||||
|
templates?.filter((template) => !template.isOfficial) || [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen px-8 py-4">
|
<div className="min-h-screen px-8 py-4">
|
||||||
<div className="max-w-5xl mx-auto">
|
<div className="max-w-5xl mx-auto">
|
||||||
@@ -35,78 +41,47 @@ const HubPage: React.FC = () => {
|
|||||||
</h1>
|
</h1>
|
||||||
<p className="text-md text-gray-600 dark:text-gray-400">
|
<p className="text-md text-gray-600 dark:text-gray-400">
|
||||||
Choose a starting point for your new project.
|
Choose a starting point for your new project.
|
||||||
|
{isLoading && " Loading additional templates..."}
|
||||||
</p>
|
</p>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
{/* Official Templates Section */}
|
||||||
{templatesData.map((template) => {
|
{officialTemplates.length > 0 && (
|
||||||
const isSelected = template.id === selectedTemplateId;
|
<section className="mb-12">
|
||||||
return (
|
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-6">
|
||||||
<div
|
Official templates
|
||||||
key={template.id}
|
</h2>
|
||||||
onClick={() => handleTemplateSelect(template.id)}
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
className={`
|
{officialTemplates.map((template) => (
|
||||||
bg-white dark:bg-gray-800 rounded-xl shadow-sm overflow-hidden
|
<TemplateCard
|
||||||
transform transition-all duration-300 ease-in-out
|
key={template.id}
|
||||||
cursor-pointer group relative
|
template={template}
|
||||||
${
|
isSelected={template.id === selectedTemplateId}
|
||||||
isSelected
|
onSelect={handleTemplateSelect}
|
||||||
? "ring-2 ring-blue-500 dark:ring-blue-400 shadow-xl"
|
/>
|
||||||
: "hover:shadow-lg hover:-translate-y-1"
|
))}
|
||||||
}
|
</div>
|
||||||
`}
|
</section>
|
||||||
>
|
)}
|
||||||
<div className="relative">
|
|
||||||
<img
|
{/* Community Templates Section */}
|
||||||
src={template.imageUrl}
|
{communityTemplates.length > 0 && (
|
||||||
alt={template.title}
|
<section className="mb-12">
|
||||||
className={`w-full h-52 object-cover transition-opacity duration-300 group-hover:opacity-80 ${isSelected ? "opacity-75" : ""}`}
|
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-6">
|
||||||
/>
|
Community templates
|
||||||
{isSelected && (
|
</h2>
|
||||||
<span className="absolute top-3 right-3 bg-blue-600 text-white text-xs font-bold px-3 py-1.5 rounded-md shadow-lg">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
Selected
|
{communityTemplates.map((template) => (
|
||||||
</span>
|
<TemplateCard
|
||||||
)}
|
key={template.id}
|
||||||
</div>
|
template={template}
|
||||||
<div className="p-4">
|
isSelected={template.id === selectedTemplateId}
|
||||||
<div className="flex justify-between items-center mb-1.5">
|
onSelect={handleTemplateSelect}
|
||||||
<h2
|
/>
|
||||||
className={`text-lg font-semibold ${isSelected ? "text-blue-600 dark:text-blue-400" : "text-gray-900 dark:text-white"}`}
|
))}
|
||||||
>
|
</div>
|
||||||
{template.title}
|
</section>
|
||||||
</h2>
|
)}
|
||||||
{template.isOfficial && (
|
|
||||||
<span
|
|
||||||
className={`text-xs font-semibold px-2 py-0.5 rounded-full ${isSelected ? "bg-blue-100 text-blue-700 dark:bg-blue-600 dark:text-blue-100" : "bg-green-100 text-green-800 dark:bg-green-700 dark:text-green-200"}`}
|
|
||||||
>
|
|
||||||
Official
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-400 mb-3 h-8 overflow-y-auto">
|
|
||||||
{template.description}
|
|
||||||
</p>
|
|
||||||
{template.githubUrl && (
|
|
||||||
<a
|
|
||||||
className={`inline-flex items-center text-sm font-medium transition-colors duration-200 ${isSelected ? "text-blue-500 hover:text-blue-700 dark:text-blue-300 dark:hover:text-blue-200" : "text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300"}`}
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
if (template.githubUrl) {
|
|
||||||
IpcClient.getInstance().openExternalUrl(
|
|
||||||
template.githubUrl,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
View on GitHub{" "}
|
|
||||||
<ArrowLeft className="w-4 h-4 ml-1 transform rotate-180" />
|
|
||||||
</a>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -100,6 +100,7 @@ const validInvokeChannels = [
|
|||||||
"open-android",
|
"open-android",
|
||||||
"check-problems",
|
"check-problems",
|
||||||
"restart-dyad",
|
"restart-dyad",
|
||||||
|
"get-templates",
|
||||||
// Test-only channels
|
// Test-only channels
|
||||||
// These should ALWAYS be guarded with IS_TEST_BUILD in the main process.
|
// These should ALWAYS be guarded with IS_TEST_BUILD in the main process.
|
||||||
// We can't detect with IS_TEST_BUILD in the preload script because
|
// We can't detect with IS_TEST_BUILD in the preload script because
|
||||||
|
|||||||
@@ -7,17 +7,27 @@ export interface Template {
|
|||||||
isOfficial: boolean;
|
isOfficial: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DEFAULT_TEMPLATE_ID = "react";
|
// API Template interface from the external API
|
||||||
|
export interface ApiTemplate {
|
||||||
|
githubOrg: string;
|
||||||
|
githubRepo: string;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
imageUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
export const templatesData: Template[] = [
|
export const DEFAULT_TEMPLATE_ID = "react";
|
||||||
{
|
export const DEFAULT_TEMPLATE = {
|
||||||
id: "react",
|
id: "react",
|
||||||
title: "React.js Template",
|
title: "React.js Template",
|
||||||
description: "Uses React.js, Vite, Shadcn, Tailwind and TypeScript.",
|
description: "Uses React.js, Vite, Shadcn, Tailwind and TypeScript.",
|
||||||
imageUrl:
|
imageUrl:
|
||||||
"https://github.com/user-attachments/assets/5b700eab-b28c-498e-96de-8649b14c16d9",
|
"https://github.com/user-attachments/assets/5b700eab-b28c-498e-96de-8649b14c16d9",
|
||||||
isOfficial: true,
|
isOfficial: true,
|
||||||
},
|
};
|
||||||
|
|
||||||
|
export const localTemplatesData: Template[] = [
|
||||||
|
DEFAULT_TEMPLATE,
|
||||||
{
|
{
|
||||||
id: "next",
|
id: "next",
|
||||||
title: "Next.js Template",
|
title: "Next.js Template",
|
||||||
@@ -28,11 +38,3 @@ export const templatesData: Template[] = [
|
|||||||
isOfficial: true,
|
isOfficial: true,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export function getTemplateOrThrow(templateId: string): Template {
|
|
||||||
const template = templatesData.find((template) => template.id === templateId);
|
|
||||||
if (!template) {
|
|
||||||
throw new Error(`Template ${templateId} not found`);
|
|
||||||
}
|
|
||||||
return template;
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user