community templates (#691)

This commit is contained in:
Will Chen
2025-07-23 10:11:16 -07:00
committed by GitHub
parent 9edd0fa80f
commit e947eede7a
37 changed files with 544 additions and 135 deletions

View File

@@ -5,7 +5,7 @@ import http from "isomorphic-git/http/node";
import { app } from "electron";
import { copyDirectoryRecursive } from "../utils/file_utils";
import { readSettings } from "@/main/settings";
import { DEFAULT_TEMPLATE_ID, getTemplateOrThrow } from "@/shared/templates";
import { getTemplateOrThrow } from "../utils/template_utils";
import log from "electron-log";
const logger = log.scope("createFromTemplate");
@@ -16,7 +16,7 @@ export async function createFromTemplate({
fullAppPath: string;
}) {
const settings = readSettings();
const templateId = settings.selectedTemplateId ?? DEFAULT_TEMPLATE_ID;
const templateId = settings.selectedTemplateId;
if (templateId === "react") {
await copyDirectoryRecursive(
@@ -26,7 +26,7 @@ export async function createFromTemplate({
return;
}
const template = getTemplateOrThrow(templateId);
const template = await getTemplateOrThrow(templateId);
if (!template.githubUrl) {
throw new Error(`Template ${templateId} has no GitHub URL`);
}

View 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;
}
});
}

View File

@@ -52,6 +52,7 @@ import type {
UpdateChatParams,
FileAttachment,
} from "./ipc_types";
import type { Template } from "../shared/templates";
import type { AppChatContext, ProposalResult } from "@/lib/schemas";
import { showError } from "@/lib/toast";
@@ -1024,4 +1025,9 @@ export class IpcClient {
}): Promise<ProblemReport> {
return this.ipcRenderer.invoke("check-problems", params);
}
// Template methods
public async getTemplates(): Promise<Template[]> {
return this.ipcRenderer.invoke("get-templates");
}
}

View File

@@ -25,6 +25,7 @@ import { registerAppUpgradeHandlers } from "./handlers/app_upgrade_handlers";
import { registerCapacitorHandlers } from "./handlers/capacitor_handlers";
import { registerProblemsHandlers } from "./handlers/problems_handlers";
import { registerAppEnvVarsHandlers } from "./handlers/app_env_vars_handlers";
import { registerTemplateHandlers } from "./handlers/template_handlers";
export function registerIpcHandlers() {
// Register all IPC handlers by category
@@ -55,4 +56,5 @@ export function registerIpcHandlers() {
registerAppUpgradeHandlers();
registerCapacitorHandlers();
registerAppEnvVarsHandlers();
registerTemplateHandlers();
}

View 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;
}