From 874eb3367809cd15631259b7ee7844109892fe7f Mon Sep 17 00:00:00 2001 From: Krrish Ghimire Date: Sun, 5 Jul 2026 23:40:37 +0545 Subject: [PATCH] refactor git provider and site structures --- .../java/com/krrishg/config/GitConfig.java | 9 + .../java/com/krrishg/dto/SiteStructure.java | 13 +- .../krrishg/service/GitHubPagesProvider.java | 38 ++ .../krrishg/service/GitLabPagesProvider.java | 47 +++ .../java/com/krrishg/service/GitService.java | 174 +++++---- .../krrishg/service/LLMResponseParser.java | 39 +- .../com/krrishg/service/PagesProvider.java | 24 ++ .../com/krrishg/service/RenderService.java | 236 +----------- .../krrishg/controller/LLMControllerTest.java | 20 +- .../com/krrishg/service/GitServiceTest.java | 364 ++++++++++++++++++ .../service/LLMResponseParserTest.java | 75 ++-- .../com/krrishg/service/LLMServiceTest.java | 18 +- frontend/src/app/core/models/component.ts | 76 ---- frontend/src/app/core/models/llm.ts | 12 +- .../llm-workspace/llm-workspace.component.ts | 21 +- .../preview-dialog.component.ts | 138 +------ .../settings/settings.component.ts | 6 +- 17 files changed, 631 insertions(+), 679 deletions(-) create mode 100644 backend/src/main/java/com/krrishg/service/GitHubPagesProvider.java create mode 100644 backend/src/main/java/com/krrishg/service/GitLabPagesProvider.java create mode 100644 backend/src/main/java/com/krrishg/service/PagesProvider.java create mode 100644 backend/src/test/java/com/krrishg/service/GitServiceTest.java delete mode 100644 frontend/src/app/core/models/component.ts diff --git a/backend/src/main/java/com/krrishg/config/GitConfig.java b/backend/src/main/java/com/krrishg/config/GitConfig.java index b5bea5e..53ff790 100644 --- a/backend/src/main/java/com/krrishg/config/GitConfig.java +++ b/backend/src/main/java/com/krrishg/config/GitConfig.java @@ -23,12 +23,21 @@ public class GitConfig { @PostConstruct public void init() throws IOException { + if (reposPath == null) return; Path path = Paths.get(reposPath); if (!Files.exists(path)) { Files.createDirectories(path); } } + public void setAuthorName(String authorName) { + this.authorName = authorName; + } + + public void setAuthorEmail(String authorEmail) { + this.authorEmail = authorEmail; + } + public Path getReposPath() { return Paths.get(reposPath); } diff --git a/backend/src/main/java/com/krrishg/dto/SiteStructure.java b/backend/src/main/java/com/krrishg/dto/SiteStructure.java index 804c10e..2fd4a38 100644 --- a/backend/src/main/java/com/krrishg/dto/SiteStructure.java +++ b/backend/src/main/java/com/krrishg/dto/SiteStructure.java @@ -34,17 +34,6 @@ public class SiteStructure { private String seoTitle; private String seoDescription; private int orderIndex; - private List components; - } - - @Data - @AllArgsConstructor - @Builder - public static class ComponentStructure { - private String id; - private String type; - private int orderIndex; - private Map config; - private Map styles; + private String rawHtml; } } diff --git a/backend/src/main/java/com/krrishg/service/GitHubPagesProvider.java b/backend/src/main/java/com/krrishg/service/GitHubPagesProvider.java new file mode 100644 index 0000000..d369435 --- /dev/null +++ b/backend/src/main/java/com/krrishg/service/GitHubPagesProvider.java @@ -0,0 +1,38 @@ +package com.krrishg.service; + +import com.krrishg.model.GitRemote; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +class GitHubPagesProvider implements PagesProvider { + + @Override + public String branchName() { + return "gh-pages"; + } + + @Override + public Path contentRoot(Path tempDir) { + return tempDir; + } + + @Override + public void writeProviderFiles(Path tempDir) throws IOException { + Files.writeString(tempDir.resolve(".nojekyll"), ""); + } + + @Override + public String buildPagesUrl(GitRemote remote) { + String url = GitService.normalizeHttpsUrl(remote.getRemoteUrl()); + String path = url.substring(url.indexOf("github.com") + "github.com".length() + 1); + if (path.endsWith(".git")) path = path.substring(0, path.length() - 4); + if (path.endsWith("/")) path = path.substring(0, path.length() - 1); + String[] parts = path.split("/", 2); + if (parts.length == 2) { + return "https://" + parts[0] + ".github.io/" + parts[1] + "/"; + } + return url; + } +} diff --git a/backend/src/main/java/com/krrishg/service/GitLabPagesProvider.java b/backend/src/main/java/com/krrishg/service/GitLabPagesProvider.java new file mode 100644 index 0000000..5f6bc9a --- /dev/null +++ b/backend/src/main/java/com/krrishg/service/GitLabPagesProvider.java @@ -0,0 +1,47 @@ +package com.krrishg.service; + +import com.krrishg.model.GitRemote; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +class GitLabPagesProvider implements PagesProvider { + + @Override + public String branchName() { + return "gl-pages"; + } + + @Override + public Path contentRoot(Path tempDir) { + return tempDir.resolve("public"); + } + + @Override + public void writeProviderFiles(Path tempDir) throws IOException { + Files.writeString(tempDir.resolve(".gitlab-ci.yml"), + "pages:\n" + + " stage: deploy\n" + + " script:\n" + + " - echo \"Deploying GitLab Pages\"\n" + + " artifacts:\n" + + " paths:\n" + + " - public\n" + + " rules:\n" + + " - if: '$CI_COMMIT_BRANCH == \"gl-pages\"'\n"); + } + + @Override + public String buildPagesUrl(GitRemote remote) { + String url = GitService.normalizeHttpsUrl(remote.getRemoteUrl()); + String path = url.substring(url.indexOf("gitlab.com") + "gitlab.com".length() + 1); + if (path.endsWith(".git")) path = path.substring(0, path.length() - 4); + if (path.endsWith("/")) path = path.substring(0, path.length() - 1); + String[] parts = path.split("/", 2); + if (parts.length == 2) { + return "https://" + parts[0] + ".gitlab.io/" + parts[1] + "/"; + } + return url; + } +} diff --git a/backend/src/main/java/com/krrishg/service/GitService.java b/backend/src/main/java/com/krrishg/service/GitService.java index b7dfb77..b548085 100644 --- a/backend/src/main/java/com/krrishg/service/GitService.java +++ b/backend/src/main/java/com/krrishg/service/GitService.java @@ -18,6 +18,7 @@ import org.springframework.stereotype.Service; import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.util.*; @@ -194,7 +195,7 @@ public class GitService { public record CommitInfo(String hash, String message, String timestamp, String author) {} - private String normalizeHttpsUrl(String url) { + static String normalizeHttpsUrl(String url) { if (url.startsWith("https://")) { return url; } @@ -218,38 +219,15 @@ public class GitService { String authUrl = httpsUrl.replace("https://", "https://git:" + remote.getEncryptedToken() + "@"); - boolean isGitLab = remote.getRemoteUrl().contains("gitlab.com"); - String branch = isGitLab ? "gl-pages" : "gh-pages"; - + PagesProvider provider = PagesProvider.forRemote(remote); Path tempDir = Files.createTempDirectory("indie-pages-" + siteId); try { - Path contentRoot = isGitLab ? tempDir.resolve("public") : tempDir; + Path contentRoot = provider.contentRoot(tempDir); Files.createDirectories(contentRoot); + pushPagesBranch(tempDir, authUrl, provider.branchName(), contentRoot, provider, files, "Deploy site"); - for (Map.Entry entry : files.entrySet()) { - Path filePath = contentRoot.resolve(entry.getKey()); - Files.createDirectories(filePath.getParent()); - Files.write(filePath, entry.getValue()); - } - - if (isGitLab) { - Files.writeString(tempDir.resolve(".gitlab-ci.yml"), - "pages:\n" + - " stage: deploy\n" + - " script:\n" + - " - echo \"Deploying GitLab Pages\"\n" + - " artifacts:\n" + - " paths:\n" + - " - public\n" + - " rules:\n" + - " - if: '$CI_COMMIT_BRANCH == \"gl-pages\"'\n"); - } else { - Files.writeString(tempDir.resolve(".nojekyll"), ""); - } - pushOrphanBranch(tempDir, authUrl, branch); - - log.info("Pushed site {} to {} branch {}", siteId, remote.getRemoteUrl(), branch); - return buildPagesUrl(remote); + log.info("Pushed site {} to {} branch {}", siteId, remote.getRemoteUrl(), provider.branchName()); + return provider.buildPagesUrl(remote); } finally { deleteDirectory(tempDir); } @@ -258,19 +236,82 @@ public class GitService { } } + void pushPagesBranch(Path tempDir, String authUrl, String branch, + Path contentRoot, PagesProvider provider, Map files, + String commitMessage) throws Exception { + String authorConfig = "-c user.name='" + gitConfig.getAuthorName() + + "' -c user.email='" + gitConfig.getAuthorEmail() + "'"; + runGit(tempDir, "git init", "git remote add origin " + authUrl); - private void pushOrphanBranch(Path tempDir, String authUrl, String branch) throws Exception { - String authorConfig = "-c user.name='" + gitConfig.getAuthorName() + "' -c user.email='" + gitConfig.getAuthorEmail() + "'"; - runGit(tempDir, - "git init", - "git " + authorConfig + " commit --allow-empty -m 'Initial commit'", - "git push --force " + authUrl + " HEAD:main", - "git checkout --orphan " + branch, - "git add -A", - "git " + authorConfig + " commit -m 'Deploy site'", - "git push --force " + authUrl + " HEAD:" + branch - ); + if (branchExistsOnRemote(authUrl, branch)) { + runGit(tempDir, + "git fetch origin " + branch, + "git checkout -b " + branch + " origin/" + branch, + "git rm -rf ." + ); + writeSiteFilesTo(contentRoot, files); + provider.writeProviderFiles(tempDir); + runGit(tempDir, + "git add -A", + "git " + authorConfig + " commit -m '" + commitMessage + "'", + "git fetch origin " + branch, + "git merge origin/" + branch + " -X ours", + "git push origin " + branch + ); + } else { + runGit(tempDir, "git checkout --orphan " + branch); + writeSiteFilesTo(contentRoot, files); + provider.writeProviderFiles(tempDir); + runGit(tempDir, + "git add -A", + "git " + authorConfig + " commit -m '" + commitMessage + "'", + "git push origin " + branch + ); + } + } + + private void writeSiteFilesTo(Path contentRoot, Map files) throws IOException { + for (Map.Entry entry : files.entrySet()) { + Path filePath = contentRoot.resolve(entry.getKey()); + Files.createDirectories(filePath.getParent()); + Files.write(filePath, entry.getValue()); + } + } + + private boolean branchExistsOnRemote(String authUrl, String branch) throws Exception { + ProcessBuilder pb = new ProcessBuilder("sh", "-c", + "git ls-remote --exit-code " + authUrl + " " + branch); + Process process = pb.start(); + return process.waitFor() == 0; + } + + private Map createRemovalPage() { + String html = """ + + + + + + Site Removed + + + +
+

This site has been unpublished

+

The owner has removed this site.

+
+ + + """; + return Map.of("index.html", html.getBytes(StandardCharsets.UTF_8)); } private void runGit(Path workDir, String... commands) throws Exception { @@ -287,36 +328,6 @@ public class GitService { } } - private String buildPagesUrl(GitRemote remote) { - String url = normalizeHttpsUrl(remote.getRemoteUrl()); - String owner = ""; - String repo = ""; - - if (url.contains("github.com")) { - String path = url.substring(url.indexOf("github.com") + "github.com".length() + 1); - if (path.endsWith(".git")) path = path.substring(0, path.length() - 4); - if (path.endsWith("/")) path = path.substring(0, path.length() - 1); - String[] parts = path.split("/", 2); - if (parts.length == 2) { - owner = parts[0]; - repo = parts[1]; - } - return "https://" + owner + ".github.io/" + repo + "/"; - } else if (url.contains("gitlab.com")) { - String path = url.substring(url.indexOf("gitlab.com") + "gitlab.com".length() + 1); - if (path.endsWith(".git")) path = path.substring(0, path.length() - 4); - if (path.endsWith("/")) path = path.substring(0, path.length() - 1); - String[] parts = path.split("/", 2); - if (parts.length == 2) { - owner = parts[0]; - repo = parts[1]; - } - return "https://" + owner + ".gitlab.io/" + repo + "/"; - } - - return url; - } - public void removePages(String siteId) { GitRemote remote = gitRemoteRepository.findBySiteId(UUID.fromString(siteId)) .orElse(null); @@ -330,20 +341,21 @@ public class GitService { String authUrl = httpsUrl.replace("https://", "https://git:" + remote.getEncryptedToken() + "@"); - boolean isGitLab = remote.getRemoteUrl().contains("gitlab.com"); - String branch = isGitLab ? "gl-pages" : "gh-pages"; + PagesProvider provider = PagesProvider.forRemote(remote); + String branch = provider.branchName(); + + if (!branchExistsOnRemote(authUrl, branch)) { + log.info("No {} branch found for site {}, nothing to remove", branch, siteId); + return; + } Path tempDir = Files.createTempDirectory("indie-pages-remove-" + siteId); try { - String authorConfig = "-c user.name='" + gitConfig.getAuthorName() + "' -c user.email='" + gitConfig.getAuthorEmail() + "'"; - runGit(tempDir, - "git init", - "git " + authorConfig + " commit --allow-empty -m 'Remove site'", - "git checkout --orphan " + branch, - "git " + authorConfig + " commit --allow-empty -m 'Remove site'", - "git push --force " + authUrl + " HEAD:" + branch - ); - log.info("Removed pages for site {} from {} branch {}", siteId, remote.getProvider(), branch); + Path contentRoot = provider.contentRoot(tempDir); + Files.createDirectories(contentRoot); + Map removalFiles = createRemovalPage(); + pushPagesBranch(tempDir, authUrl, branch, contentRoot, provider, removalFiles, "Unpublish site"); + log.info("Unpublished site {} from {} branch {}", siteId, remote.getProvider(), branch); } finally { deleteDirectory(tempDir); } diff --git a/backend/src/main/java/com/krrishg/service/LLMResponseParser.java b/backend/src/main/java/com/krrishg/service/LLMResponseParser.java index 00c6966..82db3f9 100644 --- a/backend/src/main/java/com/krrishg/service/LLMResponseParser.java +++ b/backend/src/main/java/com/krrishg/service/LLMResponseParser.java @@ -142,14 +142,9 @@ public class LLMResponseParser { if (pageNode.has("orderIndex")) { builder.orderIndex(pageNode.get("orderIndex").asInt()); } - - List components = new ArrayList<>(); - if (pageNode.has("components") && pageNode.get("components").isArray()) { - for (JsonNode compNode : pageNode.get("components")) { - components.add(parseComponent(compNode)); - } + if (pageNode.has("rawHtml")) { + builder.rawHtml(pageNode.get("rawHtml").asText()); } - builder.components(components); SiteStructure.PageStructure page = builder.build(); if (page.getTitle() == null || page.getSlug() == null) { @@ -158,34 +153,4 @@ public class LLMResponseParser { return page; } - - private SiteStructure.ComponentStructure parseComponent(JsonNode compNode) { - SiteStructure.ComponentStructure.ComponentStructureBuilder builder = SiteStructure.ComponentStructure.builder(); - - if (compNode.has("id")) { - builder.id(compNode.get("id").asText()); - } else { - builder.id(UUID.randomUUID().toString()); - } - - if (compNode.has("type")) { - builder.type(compNode.get("type").asText()); - } - if (compNode.has("orderIndex")) { - builder.orderIndex(compNode.get("orderIndex").asInt()); - } - if (compNode.has("config")) { - builder.config(objectMapper.convertValue(compNode.get("config"), Map.class)); - } - if (compNode.has("styles")) { - builder.styles(objectMapper.convertValue(compNode.get("styles"), Map.class)); - } - - SiteStructure.ComponentStructure component = builder.build(); - if (component.getType() == null) { - throw new IllegalArgumentException("Component must have a type"); - } - - return component; - } } diff --git a/backend/src/main/java/com/krrishg/service/PagesProvider.java b/backend/src/main/java/com/krrishg/service/PagesProvider.java new file mode 100644 index 0000000..93cd782 --- /dev/null +++ b/backend/src/main/java/com/krrishg/service/PagesProvider.java @@ -0,0 +1,24 @@ +package com.krrishg.service; + +import com.krrishg.model.GitRemote; + +import java.io.IOException; +import java.nio.file.Path; + +interface PagesProvider { + + String branchName(); + + Path contentRoot(Path tempDir); + + void writeProviderFiles(Path tempDir) throws IOException; + + String buildPagesUrl(GitRemote remote); + + static PagesProvider forRemote(GitRemote remote) { + if (remote.getRemoteUrl().contains("gitlab.com")) { + return new GitLabPagesProvider(); + } + return new GitHubPagesProvider(); + } +} diff --git a/backend/src/main/java/com/krrishg/service/RenderService.java b/backend/src/main/java/com/krrishg/service/RenderService.java index 6d79f5e..bc8e85e 100644 --- a/backend/src/main/java/com/krrishg/service/RenderService.java +++ b/backend/src/main/java/com/krrishg/service/RenderService.java @@ -7,8 +7,8 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; -import java.util.*; -import java.util.stream.Collectors; +import java.util.LinkedHashMap; +import java.util.Map; @Service public class RenderService { @@ -58,52 +58,16 @@ public class RenderService { } } - List
sections = buildSections(page.getComponents()); - Set lastIndices = sections.stream() - .map(s -> s.indices.get(s.indices.size() - 1)) - .collect(Collectors.toSet()); - - StringBuilder sectionsHtml = new StringBuilder(); - for (Section section : sections) { - String bgStyle = section.bg != null && !section.bg.isEmpty() - ? " style=\"background:" + section.bg + "\"" : ""; - sectionsHtml.append("\n
\n"); - for (int idx : section.indices) { - SiteStructure.ComponentStructure comp = page.getComponents().get(idx); - sectionsHtml.append(" ").append(renderComponent(comp, idx)).append("\n"); - } - sectionsHtml.append("
\n
\n"); - } - - StringBuilder compStyles = new StringBuilder(); - for (int i = 0; i < page.getComponents().size(); i++) { - SiteStructure.ComponentStructure c = page.getComponents().get(i); - if (c.getStyles() == null || c.getStyles().isEmpty()) continue; - List> entries = c.getStyles().entrySet().stream() - .filter(e -> !"backgroundColor".equals(e.getKey())) - .collect(Collectors.toList()); - if (lastIndices.contains(i)) { - entries = entries.stream() - .filter(e -> !"marginBottom".equals(e.getKey())) - .collect(Collectors.toList()); - } - if (entries.isEmpty()) continue; - compStyles.append(".comp-").append(i).append(" {\n"); - for (Map.Entry e : entries) { - compStyles.append(" ").append(cssProperty(e.getKey())).append(": ").append(e.getValue()).append(";\n"); - } - compStyles.append("}\n"); - } - String title = page.getSeoTitle() != null && !page.getSeoTitle().isEmpty() ? page.getSeoTitle() : page.getTitle(); + String bodyContent = page.getRawHtml() != null ? page.getRawHtml() : ""; return "\n" + "\n" + "\n" + " \n" + " \n" - + " " + esc(title) + "\n" + + " " + title + "\n" + " \n" + " \n" + " \n" + "\n" + "\n" - + sectionsHtml.toString() - + "\n" + + bodyContent + + "\n\n" + ""; } - private String renderComponent(SiteStructure.ComponentStructure c, int index) { - Map config = c.getConfig() != null ? c.getConfig() : new HashMap<>(); - String type = (c.getType() != null ? c.getType() : "").toUpperCase(); - String cls = "comp-" + index; - - return switch (type) { - case "HEADING" -> renderHeading(config, cls); - case "TEXT" -> renderText(config, cls); - case "IMAGE" -> renderImage(config, cls); - case "BUTTON" -> renderButton(config, cls); - case "DIVIDER" -> renderDivider(config, cls); - case "GALLERY" -> renderGallery(config, cls); - case "VIDEO" -> renderVideo(config, cls); - case "CONTACT_FORM" -> renderContactForm(config, cls); - case "MAP" -> renderMap(config, cls); - case "CUSTOM_HTML" -> renderCustomHtml(config, cls); - default -> "
[" + esc(type) + "]
"; - }; - } - - private String renderHeading(Map config, String cls) { - String level = str(config.get("level"), "h2"); - String text = str(config.get("text"), "Heading"); - String tag = List.of("h1", "h2", "h3", "h4", "h5", "h6").contains(level) ? level : "h2"; - return "<" + tag + " class=\"" + cls + "\">" + esc(text) + ""; - } - - private String renderText(Map config, String cls) { - String content = str(config.get("content"), ""); - return "
" + content + "
"; - } - - private String renderImage(Map config, String cls) { - String src = str(config.get("src"), ""); - String alt = str(config.get("alt"), ""); - String caption = str(config.get("caption"), ""); - String linkTo = str(config.get("linkTo"), ""); - String img = "\"""; - String imgHtml = linkTo.isEmpty() ? img : "" + img + ""; - if (!caption.isEmpty()) { - return "
" + imgHtml - + "
" - + esc(caption) + "
"; - } - return imgHtml; - } - - private String renderButton(Map config, String cls) { - String text = str(config.get("text"), "Button"); - String link = str(config.get("link"), "#"); - String variant = str(config.get("variant"), "primary"); - String btnStyle = "display:inline-block;text-decoration:none;font-weight:500;font-size:1rem;padding:0.75rem 2rem;border-radius:8px;transition:all 0.2s;cursor:pointer;border:2px solid transparent"; - btnStyle += switch (variant) { - case "outline" -> ";background:transparent;color:var(--color-primary,#4f46e5);border-color:var(--color-primary,#4f46e5)"; - case "secondary" -> ";background:var(--color-secondary,#6366f1);color:#ffffff"; - default -> ";background:var(--color-primary,#4f46e5);color:#ffffff"; - }; - return "" + esc(text) + ""; - } - - private String renderDivider(Map config, String cls) { - String style = str(config.get("style"), "solid"); - String color = str(config.get("color"), "var(--color-text, #d1d5db)"); - return "
"; - } - - @SuppressWarnings("unchecked") - private String renderGallery(Map config, String cls) { - List images = (List) config.getOrDefault("images", List.of()); - int columns = Math.min(Math.max(toInt(config.get("columns"), 3), 1), 6); - if (images.isEmpty()) { - return "
Gallery (no images)
"; - } - StringBuilder sb = new StringBuilder(); - sb.append("
\n"); - for (String src : images) { - sb.append(" \n"); - } - sb.append("
"); - return sb.toString(); - } - - private String renderVideo(Map config, String cls) { - String src = str(config.get("src"), ""); - boolean controls = config.get("controls") != Boolean.FALSE; - boolean autoplay = config.get("autoplay") == Boolean.TRUE; - StringBuilder sb = new StringBuilder(); - sb.append(""); - return sb.toString(); - } - - @SuppressWarnings("unchecked") - private String renderContactForm(Map config, String cls) { - List fields = (List) config.getOrDefault("fields", List.of("name", "email", "message")); - String submitText = str(config.get("submitText"), "Send"); - StringBuilder sb = new StringBuilder(); - sb.append("
\n"); - for (String field : fields) { - sb.append("
\n"); - sb.append(" \n"); - if ("message".equals(field)) { - sb.append(" \n"); - } else { - String inputType = "email".equals(field) ? "email" : "text"; - sb.append(" \n"); - } - sb.append("
\n"); - } - sb.append(" \n"); - sb.append("
"); - return sb.toString(); - } - - private String renderMap(Map config, String cls) { - String address = str(config.get("address"), "New York, NY"); - return "
\n" - + " Map: " + esc(address) + "\n" - + "
"; - } - - private String renderCustomHtml(Map config, String cls) { - String html = str(config.get("html"), ""); - return "
" + html + "
"; - } - - private List
buildSections(List components) { - List
sections = new ArrayList<>(); - if (components == null) return sections; - for (int i = 0; i < components.size(); i++) { - SiteStructure.ComponentStructure c = components.get(i); - String bg = ""; - if (c.getStyles() != null && c.getStyles().get("backgroundColor") != null) { - bg = String.valueOf(c.getStyles().get("backgroundColor")); - } - if ((bg != null && !bg.isEmpty()) || i == 0) { - sections.add(new Section(bg, new ArrayList<>())); - } - sections.get(sections.size() - 1).indices.add(i); - } - return sections; - } - - private String cssProperty(String key) { - return key.replaceAll("([a-z])([A-Z])", "$1-$2").toLowerCase(); - } - - private String esc(String s) { - if (s == null) return ""; - return s.replace("&", "&") - .replace("<", "<") - .replace(">", ">") - .replace("\"", """) - .replace("'", "'"); - } - - private String capitalize(String s) { - if (s == null || s.isEmpty()) return s; - return Character.toUpperCase(s.charAt(0)) + s.substring(1); - } - - private String str(Object value, String defaultValue) { - return value != null ? String.valueOf(value) : defaultValue; - } - - private int toInt(Object value, int defaultValue) { - if (value instanceof Number n) return n.intValue(); - if (value instanceof String s) { - try { return Integer.parseInt(s); } catch (NumberFormatException e) { return defaultValue; } - } - return defaultValue; - } - - private record Section(String bg, List indices) {} - public record SiteOutput(String css, String js, Map pages) { public Map asFileMap() { Map files = new LinkedHashMap<>(); @@ -321,7 +99,7 @@ public class RenderService { for (Map.Entry entry : pages.entrySet()) { String slug = entry.getKey(); String html = entry.getValue(); - if (slug == null || slug.isEmpty() || "home".equals(slug)) { + if (slug == null || slug.isEmpty() || "home".equals(slug) || "index".equals(slug)) { files.put("index.html", html.getBytes()); } else { files.put(slug + "/index.html", html.getBytes()); diff --git a/backend/src/test/java/com/krrishg/controller/LLMControllerTest.java b/backend/src/test/java/com/krrishg/controller/LLMControllerTest.java index e5a01e6..7434bf9 100644 --- a/backend/src/test/java/com/krrishg/controller/LLMControllerTest.java +++ b/backend/src/test/java/com/krrishg/controller/LLMControllerTest.java @@ -72,9 +72,7 @@ class LLMControllerTest { .pages(List.of( SiteStructure.PageStructure.builder() .title("Home").slug("home") - .components(List.of( - SiteStructure.ComponentStructure.builder().type("hero").build() - )) + .rawHtml("

Home

") .build() )) .build(); @@ -101,9 +99,7 @@ class LLMControllerTest { .pages(List.of( SiteStructure.PageStructure.builder() .title("Home").slug("home") - .components(List.of( - SiteStructure.ComponentStructure.builder().type("hero").build() - )) + .rawHtml("

Home

") .build() )) .build(); @@ -215,9 +211,7 @@ class LLMControllerTest { .pages(List.of( SiteStructure.PageStructure.builder() .title("Old").slug("old") - .components(List.of( - SiteStructure.ComponentStructure.builder().type("text").build() - )) + .rawHtml("

Old

") .build() )) .build(); @@ -225,9 +219,7 @@ class LLMControllerTest { .pages(List.of( SiteStructure.PageStructure.builder() .title("Refined").slug("refined") - .components(List.of( - SiteStructure.ComponentStructure.builder().type("hero").build() - )) + .rawHtml("

Refined

") .build() )) .build(); @@ -354,9 +346,7 @@ class LLMControllerTest { .pages(List.of( SiteStructure.PageStructure.builder() .title("Restored").slug("restored") - .components(List.of( - SiteStructure.ComponentStructure.builder().type("hero").build() - )) + .rawHtml("

Restored

") .build() )) .build(); diff --git a/backend/src/test/java/com/krrishg/service/GitServiceTest.java b/backend/src/test/java/com/krrishg/service/GitServiceTest.java new file mode 100644 index 0000000..7515469 --- /dev/null +++ b/backend/src/test/java/com/krrishg/service/GitServiceTest.java @@ -0,0 +1,364 @@ +package com.krrishg.service; + +import com.krrishg.config.GitConfig; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; + +class GitServiceTest { + + @TempDir + Path tempDir; + + private Path remoteDir; + private GitService gitService; + + @BeforeEach + void setUp() throws Exception { + remoteDir = tempDir.resolve("remote"); + Files.createDirectories(remoteDir); + + GitConfig config = new GitConfig(); + config.setAuthorName("Test Author"); + config.setAuthorEmail("test@example.com"); + gitService = new GitService(config, null, null, null); + } + + @Test + void initialPublishCreatesBranchOnRemote() throws Exception { + initRemote(); + Path workDir = tempDir.resolve("work"); + Files.createDirectories(workDir); + Path contentRoot = workDir; + + Map files = new HashMap<>(); + files.put("index.html", "

Hello

".getBytes()); + files.put("style.css", "body { color: red; }".getBytes()); + + gitService.pushPagesBranch(workDir, "file://" + remoteDir.toString(), "gh-pages", + contentRoot, new GitHubPagesProvider(), files, "Deploy site"); + + assertBranchExists("gh-pages"); + assertRemoteFileContent("gh-pages", "index.html", "

Hello

"); + assertRemoteFileContent("gh-pages", "style.css", "body { color: red; }"); + assertRemoteHasNojekyll("gh-pages"); + } + + @Test + void initialPublishForGitLabCreatesPublicDirectory() throws Exception { + initRemote(); + Path workDir = tempDir.resolve("work"); + Files.createDirectories(workDir); + Path contentRoot = workDir.resolve("public"); + Files.createDirectories(contentRoot); + + Map files = new HashMap<>(); + files.put("index.html", "

GitLab

".getBytes()); + + gitService.pushPagesBranch(workDir, "file://" + remoteDir.toString(), "gl-pages", + contentRoot, new GitLabPagesProvider(), files, "Deploy site"); + + assertBranchExists("gl-pages"); + assertRemoteFileContent("gl-pages", "public/index.html", "

GitLab

"); + assertRemoteHasGitlabCi("gl-pages"); + } + + @Test + void secondPublishPreservesHistory() throws Exception { + initRemote(); + Path workDir = tempDir.resolve("work"); + Files.createDirectories(workDir); + Path contentRoot = workDir; + + Map firstFiles = new HashMap<>(); + firstFiles.put("index.html", "

First

".getBytes()); + + gitService.pushPagesBranch(workDir, "file://" + remoteDir.toString(), "gh-pages", + contentRoot, new GitHubPagesProvider(), firstFiles, "Deploy site"); + + Path workDir2 = tempDir.resolve("work2"); + Files.createDirectories(workDir2); + Path contentRoot2 = workDir2; + + Map secondFiles = new HashMap<>(); + secondFiles.put("index.html", "

Second

".getBytes()); + + gitService.pushPagesBranch(workDir2, "file://" + remoteDir.toString(), "gh-pages", + contentRoot2, new GitHubPagesProvider(), secondFiles, "Deploy site"); + + assertRemoteFileContent("gh-pages", "index.html", "

Second

"); + + int commitCount = countCommits("gh-pages"); + assertEquals(2, commitCount); + } + + @Test + void divergenceUsesLocalContentOnConflict() throws Exception { + initRemote(); + Path workDir = tempDir.resolve("work"); + Files.createDirectories(workDir); + Path contentRoot = workDir; + + Map initialFiles = new HashMap<>(); + initialFiles.put("index.html", "

Original

".getBytes()); + gitService.pushPagesBranch(workDir, "file://" + remoteDir.toString(), "gh-pages", + contentRoot, new GitHubPagesProvider(), initialFiles, "Deploy site"); + + addFileToRemote("gh-pages", "index.html", "

Remote change

"); + addFileToRemote("gh-pages", "remote-only.html", "

Remote only

"); + + Path workDir2 = tempDir.resolve("work2"); + Files.createDirectories(workDir2); + Path contentRoot2 = workDir2; + + Map localFiles = new HashMap<>(); + localFiles.put("index.html", "

Local new version

".getBytes()); + localFiles.put("local-only.html", "

Local only

".getBytes()); + + gitService.pushPagesBranch(workDir2, "file://" + remoteDir.toString(), "gh-pages", + contentRoot2, new GitHubPagesProvider(), localFiles, "Deploy site"); + + assertRemoteFileContent("gh-pages", "index.html", "

Local new version

"); + assertRemoteFileContent("gh-pages", "local-only.html", "

Local only

"); + int commitCount = countCommits("gh-pages"); + assertEquals(4, commitCount, "Should preserve history from both sides"); + } + + @Test + void publishWithoutNojekyllForGitHub() throws Exception { + initRemote(); + Path workDir = tempDir.resolve("work"); + Files.createDirectories(workDir); + Path contentRoot = workDir; + + Map files = new HashMap<>(); + files.put("index.html", "

Test

".getBytes()); + + gitService.pushPagesBranch(workDir, "file://" + remoteDir.toString(), "gh-pages", + contentRoot, new GitHubPagesProvider(), files, "Deploy site"); + + assertRemoteFileContent("gh-pages", "index.html", "

Test

"); + } + + @Test + void unpublishReplacesContentWithRemovalPage() throws Exception { + initRemote(); + Path workDir = tempDir.resolve("work"); + Files.createDirectories(workDir); + Path contentRoot = workDir; + + Map siteFiles = new HashMap<>(); + siteFiles.put("index.html", "

My Site

".getBytes()); + siteFiles.put("about.html", "

About

".getBytes()); + gitService.pushPagesBranch(workDir, "file://" + remoteDir.toString(), "gh-pages", + contentRoot, new GitHubPagesProvider(), siteFiles, "Deploy site"); + + Path workDir2 = tempDir.resolve("work2"); + Files.createDirectories(workDir2); + Path contentRoot2 = workDir2; + + Map removalFiles = new HashMap<>(); + removalFiles.put("index.html", "".getBytes()); + gitService.pushPagesBranch(workDir2, "file://" + remoteDir.toString(), "gh-pages", + contentRoot2, new GitHubPagesProvider(), removalFiles, "Unpublish site"); + + assertRemoteFileContentStart("gh-pages", "index.html", ""); + assertRemoteFileMissing("gh-pages", "about.html"); + } + + @Test + void unpublishPreservesHistory() throws Exception { + initRemote(); + Path workDir = tempDir.resolve("work"); + Files.createDirectories(workDir); + Path contentRoot = workDir; + + Map siteFiles = new HashMap<>(); + siteFiles.put("index.html", "

My Site

".getBytes()); + gitService.pushPagesBranch(workDir, "file://" + remoteDir.toString(), "gh-pages", + contentRoot, new GitHubPagesProvider(), siteFiles, "Deploy site"); + + Path workDir2 = tempDir.resolve("work2"); + Files.createDirectories(workDir2); + Path contentRoot2 = workDir2; + + Map removalFiles = new HashMap<>(); + removalFiles.put("index.html", "".getBytes()); + gitService.pushPagesBranch(workDir2, "file://" + remoteDir.toString(), "gh-pages", + contentRoot2, new GitHubPagesProvider(), removalFiles, "Unpublish site"); + + int commitCount = countCommits("gh-pages"); + assertEquals(2, commitCount, "Unpublish should add a commit, not replace history"); + } + + @Test + void unpublishForGitLabUsesPublicDirectory() throws Exception { + initRemote(); + Path workDir = tempDir.resolve("work"); + Files.createDirectories(workDir); + Path contentRoot = workDir.resolve("public"); + Files.createDirectories(contentRoot); + + Map siteFiles = new HashMap<>(); + siteFiles.put("index.html", "

GitLab Site

".getBytes()); + gitService.pushPagesBranch(workDir, "file://" + remoteDir.toString(), "gl-pages", + contentRoot, new GitLabPagesProvider(), siteFiles, "Deploy site"); + + Path workDir2 = tempDir.resolve("work2"); + Files.createDirectories(workDir2); + Path contentRoot2 = workDir2.resolve("public"); + Files.createDirectories(contentRoot2); + + Map removalFiles = new HashMap<>(); + removalFiles.put("index.html", "".getBytes()); + gitService.pushPagesBranch(workDir2, "file://" + remoteDir.toString(), "gl-pages", + contentRoot2, new GitLabPagesProvider(), removalFiles, "Unpublish site"); + + assertRemoteFileContentStart("gl-pages", "public/index.html", ""); + assertRemoteHasGitlabCi("gl-pages"); + } + + private void assertRemoteFileContentStart(String branch, String path, String expectedPrefix) throws Exception { + Path cloneDir = tempDir.resolve("prefix-" + path.hashCode()); + Files.createDirectories(cloneDir); + runGit(cloneDir, + "git init", + "git remote add origin file://" + remoteDir.toString(), + "git fetch origin " + branch, + "git checkout -b " + branch + " origin/" + branch + ); + Path filePath = cloneDir.resolve(path); + assertTrue(Files.exists(filePath), "File " + path + " should exist in " + branch); + String actual = Files.readString(filePath); + assertTrue(actual.startsWith(expectedPrefix), + "Expected content starting with \"" + expectedPrefix + "\" but got: " + actual); + } + + private void assertRemoteFileMissing(String branch, String path) throws Exception { + Path cloneDir = tempDir.resolve("missing-" + path.hashCode()); + Files.createDirectories(cloneDir); + runGit(cloneDir, + "git init", + "git remote add origin file://" + remoteDir.toString(), + "git fetch origin " + branch, + "git checkout -b " + branch + " origin/" + branch + ); + Path filePath = cloneDir.resolve(path); + assertFalse(Files.exists(filePath), "File " + path + " should NOT exist in " + branch); + } + + private void initRemote() throws Exception { + runGit(remoteDir, "git init --bare"); + } + + private void assertBranchExists(String branch) throws Exception { + ProcessBuilder pb = new ProcessBuilder("sh", "-c", + "git ls-remote --exit-code file://" + remoteDir.toString() + " " + branch); + pb.directory(tempDir.toFile()); + assertEquals(0, pb.start().waitFor(), "Branch " + branch + " should exist on remote"); + } + + private void assertRemoteFileContent(String branch, String path, String expectedContent) throws Exception { + Path cloneDir = tempDir.resolve("assert-" + path.hashCode()); + Files.createDirectories(cloneDir); + runGit(cloneDir, + "git init", + "git remote add origin file://" + remoteDir.toString(), + "git fetch origin " + branch, + "git checkout -b " + branch + " origin/" + branch + ); + Path filePath = cloneDir.resolve(path); + assertTrue(Files.exists(filePath), "File " + path + " should exist in " + branch); + String actual = Files.readString(filePath); + assertEquals(expectedContent, actual); + } + + private void assertRemoteHasNojekyll(String branch) throws Exception { + try { + assertRemoteFileContent(branch, ".nojekyll", ""); + } catch (AssertionError e) { + Path cloneDir = tempDir.resolve("nojekyll-check"); + Files.createDirectories(cloneDir); + runGit(cloneDir, + "git init", + "git remote add origin file://" + remoteDir.toString(), + "git fetch origin " + branch, + "git checkout -b " + branch + " origin/" + branch + ); + assertTrue(Files.exists(cloneDir.resolve(".nojekyll")), + ".nojekyll file should exist on " + branch); + } + } + + private void assertRemoteHasGitlabCi(String branch) throws Exception { + assertRemoteFileContent(branch, ".gitlab-ci.yml", + "pages:\n" + + " stage: deploy\n" + + " script:\n" + + " - echo \"Deploying GitLab Pages\"\n" + + " artifacts:\n" + + " paths:\n" + + " - public\n" + + " rules:\n" + + " - if: '$CI_COMMIT_BRANCH == \"gl-pages\"'\n"); + } + + private void addFileToRemote(String branch, String path, String content) throws Exception { + Path cloneDir = tempDir.resolve("remote-add-" + path.hashCode()); + Files.createDirectories(cloneDir); + runGit(cloneDir, + "git init", + "git remote add origin file://" + remoteDir.toString(), + "git fetch origin " + branch, + "git checkout -b " + branch + " origin/" + branch + ); + Path filePath = cloneDir.resolve(path); + Files.createDirectories(filePath.getParent()); + Files.writeString(filePath, content); + runGit(cloneDir, + "git add -A", + "git -c user.name='Remote' -c user.email='remote@example.com' commit -m 'Remote change: " + path + "'", + "git push origin " + branch + ); + } + + private int countCommits(String branch) throws Exception { + Path cloneDir = tempDir.resolve("count-commits"); + Files.createDirectories(cloneDir); + runGit(cloneDir, + "git init", + "git remote add origin file://" + remoteDir.toString(), + "git fetch origin " + branch + ); + ProcessBuilder pb = new ProcessBuilder("sh", "-c", + "git rev-list --count origin/" + branch); + pb.directory(cloneDir.toFile()); + pb.redirectErrorStream(true); + Process process = pb.start(); + String output = new String(process.getInputStream().readAllBytes()).trim(); + int exitCode = process.waitFor(); + if (exitCode != 0) return 0; + return Integer.parseInt(output); + } + + private void runGit(Path workDir, String... commands) throws Exception { + for (String cmd : commands) { + ProcessBuilder pb = new ProcessBuilder("sh", "-c", cmd); + pb.directory(workDir.toFile()); + pb.redirectErrorStream(true); + Process process = pb.start(); + int exitCode = process.waitFor(); + if (exitCode != 0) { + String output = new String(process.getInputStream().readAllBytes()); + throw new RuntimeException("git command failed (exit " + exitCode + "): " + cmd + "\n" + output); + } + } + } +} diff --git a/backend/src/test/java/com/krrishg/service/LLMResponseParserTest.java b/backend/src/test/java/com/krrishg/service/LLMResponseParserTest.java index 74104e6..ab03e1d 100644 --- a/backend/src/test/java/com/krrishg/service/LLMResponseParserTest.java +++ b/backend/src/test/java/com/krrishg/service/LLMResponseParserTest.java @@ -95,9 +95,7 @@ class LLMResponseParserTest { { "title": "Home", "slug": "home", - "components": [ - {"type": "hero", "orderIndex": 0, "config": {"title": "Welcome"}} - ] + "rawHtml": "

Welcome

" } ] } @@ -111,8 +109,7 @@ class LLMResponseParserTest { assertEquals(1, structure.getPages().size()); assertEquals("Home", structure.getPages().get(0).getTitle()); assertEquals("home", structure.getPages().get(0).getSlug()); - assertEquals(1, structure.getPages().get(0).getComponents().size()); - assertEquals("hero", structure.getPages().get(0).getComponents().get(0).getType()); + assertEquals("

Welcome

", structure.getPages().get(0).getRawHtml()); } @Test @@ -120,7 +117,7 @@ class LLMResponseParserTest { String json = """ { "pages": [ - {"title": "Home", "slug": "home", "components": [{"type": "text"}]} + {"title": "Home", "slug": "home", "rawHtml": "

Hello

"} ] } """; @@ -147,7 +144,7 @@ class LLMResponseParserTest { String json = """ { "pages": [ - {"slug": "home", "components": [{"type": "hero"}]} + {"slug": "home", "rawHtml": "

test

"} ] } """; @@ -159,19 +156,7 @@ class LLMResponseParserTest { String json = """ { "pages": [ - {"title": "Home", "components": [{"type": "hero"}]} - ] - } - """; - assertThrows(IllegalArgumentException.class, () -> parser.parse(json)); - } - - @Test - void throwsOnComponentWithoutType() { - String json = """ - { - "pages": [ - {"title": "Home", "slug": "home", "components": [{"orderIndex": 0}]} + {"title": "Home", "rawHtml": "

test

"} ] } """; @@ -183,7 +168,7 @@ class LLMResponseParserTest { String json = """ { "pages": [ - {"title": "Home", "slug": "home", "components": [{"type": "hero"}]} + {"title": "Home", "slug": "home", "rawHtml": "

Hello

"} ] } """; @@ -192,20 +177,6 @@ class LLMResponseParserTest { assertNotNull(structure.getPages().get(0).getId()); } - @Test - void assignsGeneratedIdWhenComponentIdMissing() { - String json = """ - { - "pages": [ - {"title": "Home", "slug": "home", "components": [{"type": "hero"}]} - ] - } - """; - - SiteStructure structure = parser.parse(json); - assertNotNull(structure.getPages().get(0).getComponents().get(0).getId()); - } - @Test void parsesPageWithAllOptionalFields() { String json = """ @@ -218,15 +189,7 @@ class LLMResponseParserTest { "seoTitle": "About Us", "seoDescription": "Learn about our company", "orderIndex": 1, - "components": [ - { - "id": "comp-1", - "type": "text", - "orderIndex": 0, - "config": {"body": "Hello"}, - "styles": {"color": "red"} - } - ] + "rawHtml": "

About

Our story

" } ] } @@ -240,13 +203,7 @@ class LLMResponseParserTest { assertEquals("About Us", page.getSeoTitle()); assertEquals("Learn about our company", page.getSeoDescription()); assertEquals(1, page.getOrderIndex()); - - SiteStructure.ComponentStructure comp = page.getComponents().get(0); - assertEquals("comp-1", comp.getId()); - assertEquals("text", comp.getType()); - assertEquals(0, comp.getOrderIndex()); - assertEquals("Hello", comp.getConfig().get("body")); - assertEquals("red", comp.getStyles().get("color")); + assertEquals("

About

Our story

", page.getRawHtml()); } @Test @@ -255,7 +212,7 @@ class LLMResponseParserTest { { "unknownField": true, "pages": [ - {"title": "Home", "slug": "home", "components": [{"type": "hero", "extra": 1}]} + {"title": "Home", "slug": "home", "rawHtml": "

test

", "extra": 1} ] } """; @@ -268,5 +225,19 @@ class LLMResponseParserTest { String json = "{invalid json here}"; assertThrows(IllegalArgumentException.class, () -> parser.parse(json)); } + + @Test + void parsesPageWithoutRawHtml() { + String json = """ + { + "pages": [ + {"title": "Empty", "slug": "empty"} + ] + } + """; + + SiteStructure structure = parser.parse(json); + assertNull(structure.getPages().get(0).getRawHtml()); + } } } diff --git a/backend/src/test/java/com/krrishg/service/LLMServiceTest.java b/backend/src/test/java/com/krrishg/service/LLMServiceTest.java index f60b277..b154ecd 100644 --- a/backend/src/test/java/com/krrishg/service/LLMServiceTest.java +++ b/backend/src/test/java/com/krrishg/service/LLMServiceTest.java @@ -78,7 +78,7 @@ class LLMServiceTest { activeProvider.withContent(""" { "pages": [ - {"title": "Home", "slug": "home", "components": [{"type": "hero"}]} + {"title": "Home", "slug": "home", "rawHtml": "

Welcome

"} ] } """); @@ -97,7 +97,7 @@ class LLMServiceTest { activeProvider.withContent(""" { "pages": [ - {"title": "About", "slug": "about", "components": [{"type": "text"}]} + {"title": "About", "slug": "about", "rawHtml": "

About

"} ] } """); @@ -122,7 +122,7 @@ class LLMServiceTest { activeProvider.withContent(""" { "pages": [ - {"title": "Home", "slug": "home", "components": [{"type": "hero"}]} + {"title": "Home", "slug": "home", "rawHtml": "

Hero

"} ] } """); @@ -147,7 +147,7 @@ class LLMServiceTest { activeProvider.withContent(""" { "pages": [ - {"title": "Home", "slug": "home", "components": [{"type": "hero"}]} + {"title": "Home", "slug": "home", "rawHtml": "

Hero

"} ] } """); @@ -174,7 +174,7 @@ class LLMServiceTest { fallbackProvider.withContent(""" { "pages": [ - {"title": "Fallback", "slug": "fallback", "components": [{"type": "hero"}]} + {"title": "Fallback", "slug": "fallback", "rawHtml": "

Hero

"} ] } """); @@ -204,9 +204,7 @@ class LLMServiceTest { SiteStructure.PageStructure.builder() .title("Old") .slug("old") - .components(List.of( - SiteStructure.ComponentStructure.builder().type("text").build() - )) + .rawHtml("

Old content

") .build() )) .build(); @@ -214,7 +212,7 @@ class LLMServiceTest { activeProvider.withContent(""" { "pages": [ - {"title": "Refined", "slug": "refined", "components": [{"type": "hero"}]} + {"title": "Refined", "slug": "refined", "rawHtml": "

Hero

"} ] } """); @@ -258,7 +256,7 @@ class LLMServiceTest { GenerationVersion version = GenerationVersion.builder() .id("v1") .fullStructure(""" - {"pages": [{"title": "Restored", "slug": "restored", "components": [{"type": "hero"}]}]} + {"pages": [{"title": "Restored", "slug": "restored", "rawHtml": "

Hero

"}]} """) .build(); diff --git a/frontend/src/app/core/models/component.ts b/frontend/src/app/core/models/component.ts deleted file mode 100644 index 3e05b1e..0000000 --- a/frontend/src/app/core/models/component.ts +++ /dev/null @@ -1,76 +0,0 @@ -export type ComponentType = - | 'HEADING' | 'TEXT' | 'IMAGE' | 'BUTTON' | 'DIVIDER' - | 'GALLERY' | 'VIDEO' | 'CONTACT_FORM' | 'MAP' | 'CUSTOM_HTML'; - -export interface ComponentItem { - id: string; - pageId?: string; - type: ComponentType; - config: Record; - styles: Record; - orderIndex: number; -} - -export function getDefaultConfig(type: ComponentType): Record { - switch (type) { - case 'HEADING': return { text: 'Heading', level: 'h2' }; - case 'TEXT': return { content: 'Text content here...' }; - case 'IMAGE': return { src: '', alt: 'Image', linkTo: '' }; - case 'BUTTON': return { text: 'Click Me', link: '#', variant: 'filled' }; - case 'DIVIDER': return { style: 'solid' }; - case 'GALLERY': return { images: [] }; - case 'VIDEO': return { src: '', autoplay: false, controls: true }; - case 'CONTACT_FORM': return { fields: ['name', 'email', 'message'], submitText: 'Send', emailTo: '' }; - case 'MAP': return { address: 'New York, NY', zoom: 14 }; - case 'CUSTOM_HTML': return { html: '

Custom HTML

' }; - } -} - -export function getDefaultStyles(type: ComponentType): Record { - switch (type) { - case 'HEADING': return { color: '#1a1a1a', fontSize: '2rem', fontWeight: '700', textAlign: 'left', margin: '0 0 1rem' }; - case 'TEXT': return { color: '#4a4a4a', fontSize: '1rem', lineHeight: '1.6', textAlign: 'left' }; - case 'IMAGE': return { width: '100%', height: 'auto', borderRadius: '0', objectFit: 'cover' }; - case 'BUTTON': return { backgroundColor: '#1976d2', color: '#ffffff', borderRadius: '4px', padding: '0.75rem 1.5rem' }; - case 'DIVIDER': return { color: '#e0e0e0', thickness: '1px', margin: '1.5rem 0' }; - case 'GALLERY': return { columns: '3', gap: '8px', borderRadius: '4px' }; - case 'VIDEO': return { width: '100%', maxWidth: '800px', borderRadius: '4px' }; - case 'CONTACT_FORM': return { backgroundColor: '#f5f5f5', padding: '2rem', borderRadius: '8px' }; - case 'MAP': return { width: '100%', height: '400px', borderRadius: '4px' }; - case 'CUSTOM_HTML': return { padding: '0', margin: '0' }; - } -} - -const LABELS: Record = { - HEADING: 'Heading', - TEXT: 'Text', - IMAGE: 'Image', - BUTTON: 'Button', - DIVIDER: 'Divider', - GALLERY: 'Gallery', - VIDEO: 'Video', - CONTACT_FORM: 'Contact Form', - MAP: 'Map', - CUSTOM_HTML: 'Custom HTML', -}; - -const ICONS: Record = { - HEADING: 'title', - TEXT: 'text_fields', - IMAGE: 'image', - BUTTON: 'smart_button', - DIVIDER: 'horizontal_rule', - GALLERY: 'photo_library', - VIDEO: 'videocam', - CONTACT_FORM: 'contact_mail', - MAP: 'map', - CUSTOM_HTML: 'code', -}; - -export function getComponentLabel(type: string): string { - return LABELS[type] || LABELS[type.toUpperCase()] || type; -} - -export function getComponentIcon(type: string): string { - return ICONS[type] || ICONS[type.toUpperCase()] || 'code'; -} diff --git a/frontend/src/app/core/models/llm.ts b/frontend/src/app/core/models/llm.ts index 6db5663..450e607 100644 --- a/frontend/src/app/core/models/llm.ts +++ b/frontend/src/app/core/models/llm.ts @@ -1,5 +1,3 @@ -import { ComponentType } from './component'; - export interface LLMGenerateRequest { prompt: string; siteId: string; @@ -37,15 +35,7 @@ export interface PageStructure { seoTitle?: string; seoDescription?: string; orderIndex: number; - components: ComponentStructure[]; -} - -export interface ComponentStructure { - id?: string; - type: ComponentType; - orderIndex: number; - config: Record; - styles: Record; + rawHtml?: string; } export interface TokenUsage { diff --git a/frontend/src/app/llm-workspace/llm-workspace.component.ts b/frontend/src/app/llm-workspace/llm-workspace.component.ts index 239c185..18ee410 100644 --- a/frontend/src/app/llm-workspace/llm-workspace.component.ts +++ b/frontend/src/app/llm-workspace/llm-workspace.component.ts @@ -18,7 +18,6 @@ import { takeUntil } from 'rxjs/operators'; import { LlmApiService } from '../core/services/llm-api.service'; import { LlmSessionService } from './llm-session.service'; import { SiteStructure, GenerationVersion } from '../core/models/llm'; -import { getComponentLabel } from '../core/models/component'; import { PreviewDialogComponent } from './preview-dialog/preview-dialog.component'; import { DeleteVersionDialog } from './delete-version-dialog/delete-version-dialog'; @@ -146,8 +145,6 @@ import { DeleteVersionDialog } from './delete-version-dialog/delete-version-dial

Generated Site Structure

{{ result.pages.length }} page{{ result.pages.length !== 1 ? 's' : '' }} - · - {{ totalComponents(result) }} component{{ totalComponents(result) !== 1 ? 's' : '' }}

@@ -236,14 +233,12 @@ import { DeleteVersionDialog } from './delete-version-dialog/delete-version-dial /{{ page.slug }} · - {{ page.components.length }} component{{ page.components.length !== 1 ? 's' : '' }} + Raw HTML -
-
- circle - {{ getLabel(comp.type) }} -
+
+ code + Raw HTML content
@@ -446,14 +441,6 @@ export class LlmWorkspaceComponent implements OnInit, OnDestroy { if (this.siteId) this.router.navigate(['/llm', this.siteId, 'settings']); } - totalComponents(structure: SiteStructure): number { - return structure.pages.reduce((sum, p) => sum + p.components.length, 0); - } - - getLabel(type: string): string { - return getComponentLabel(type); - } - isCurrentVersion(versionId: string): boolean { return this.session.currentVersionId === versionId; } diff --git a/frontend/src/app/llm-workspace/preview-dialog/preview-dialog.component.ts b/frontend/src/app/llm-workspace/preview-dialog/preview-dialog.component.ts index 82d590c..5d0f14d 100644 --- a/frontend/src/app/llm-workspace/preview-dialog/preview-dialog.component.ts +++ b/frontend/src/app/llm-workspace/preview-dialog/preview-dialog.component.ts @@ -5,7 +5,7 @@ import { MatIconModule } from '@angular/material/icon'; import { MatToolbarModule } from '@angular/material/toolbar'; import { NgIf, NgFor, AsyncPipe } from '@angular/common'; import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; -import { SiteStructure, PageStructure, ComponentStructure } from '../../core/models/llm'; +import { SiteStructure } from '../../core/models/llm'; type Device = 'desktop' | 'tablet' | 'mobile'; @@ -93,23 +93,6 @@ export class PreviewDialogComponent { + Object.entries(fonts).map(([k, v]) => ` --font-${k}: ${v};`).join('\n') + Object.entries(spacing).map(([k, v]) => ` --spacing-${k}: ${v};`).join('\n'); - const sections: { bg: string; indices: number[] }[] = []; - for (let i = 0; i < page.components.length; i++) { - const c = page.components[i]; - const bg = String((c.styles && c.styles['backgroundColor']) || ''); - if (bg || i === 0) { - sections.push({ bg, indices: [i] }); - } else { - sections[sections.length - 1].indices.push(i); - } - } - const lastIndices = new Set(sections.map(s => s.indices[s.indices.length - 1])); - const sectionsHtml = sections.map(s => { - const bgStyle = s.bg ? ` style="background:${s.bg}"` : ''; - const inner = s.indices.map(idx => this.renderComponent(page.components[idx], idx)).join('\n'); - return `\n
\n${inner}\n
\n
`; - }).join('\n'); - return ` @@ -129,128 +112,11 @@ ${cssVars} line-height: 1.6; -webkit-font-smoothing: antialiased; } - .container { max-width: var(--spacing-containerWidth, 1100px); margin: 0 auto; padding: 0 1.5rem; } - section { padding: var(--spacing-sectionPadding, 3rem) 0; } - ${this.buildPageStyles(page.components, lastIndices)} - ${sectionsHtml} + ${page.rawHtml || ''} `; } - - private buildPageStyles(components: ComponentStructure[], lastIndices: Set): string { - return components.map((c, i) => { - const s = c.styles || {}; - let entries = Object.entries(s).filter(([k]) => k !== 'backgroundColor'); - if (lastIndices.has(i)) { - entries = entries.filter(([k]) => k !== 'marginBottom'); - } - if (entries.length === 0) return ''; - return `.comp-${i} {\n${entries.map(([k, v]) => ` ${this.cssProperty(k)}: ${v};`).join('\n')}\n}`; - }).filter(Boolean).join('\n'); - } - - private renderComponent(c: ComponentStructure, i?: number): string { - const idx = i ?? 0; - const config = c.config || {}; - const type = (c.type || '').toUpperCase(); - const cls = `comp-${idx}`; - - switch (type) { - case 'HEADING': { - const level = (config['level'] as string) || 'h2'; - const text = (config['text'] as string) || 'Heading'; - const tag = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(level) ? level : 'h2'; - return `<${tag} class="${cls}">${this.esc(text)}`; - } - case 'TEXT': { - const content = (config['content'] as string) || ''; - return `
${content}
`; - } - case 'IMAGE': { - const src = (config['src'] as string) || ''; - const alt = (config['alt'] as string) || ''; - const caption = (config['caption'] as string) || ''; - const linkTo = (config['linkTo'] as string) || ''; - const img = `${this.esc(alt)}`; - const imgHtml = linkTo ? `${img}` : img; - return caption ? `
${imgHtml}
${this.esc(caption)}
` : imgHtml; - } - case 'BUTTON': { - const text = (config['text'] as string) || 'Button'; - const link = (config['link'] as string) || '#'; - const variant = (config['variant'] as string) || 'primary'; - let btnStyle = 'display:inline-block;text-decoration:none;font-weight:500;font-size:1rem;padding:0.75rem 2rem;border-radius:8px;transition:all 0.2s;cursor:pointer;border:2px solid transparent'; - if (variant === 'outline') { - btnStyle += `;background:transparent;color:var(--color-primary,#4f46e5);border-color:var(--color-primary,#4f46e5)`; - } else if (variant === 'secondary') { - btnStyle += `;background:var(--color-secondary,#6366f1);color:#ffffff`; - } else { - btnStyle += `;background:var(--color-primary,#4f46e5);color:#ffffff`; - } - return `${this.esc(text)}`; - } - case 'DIVIDER': { - const style = (config['style'] as string) || 'solid'; - const color = (config['color'] as string) || 'var(--color-text, #d1d5db)'; - return `
`; - } - case 'GALLERY': { - const images = (config['images'] as string[]) || []; - const columns = Math.min(Math.max((config['columns'] as number) || 3, 1), 6); - if (images.length === 0) return `
Gallery (no images)
`; - return `
- ${images.map(src => ``).join('\n')} -
`; - } - case 'VIDEO': { - const src = (config['src'] as string) || ''; - const controls = config['controls'] !== false; - const autoplay = config['autoplay'] === true; - return ``; - } - case 'CONTACT_FORM': { - const fields = (config['fields'] as string[]) || ['name', 'email', 'message']; - const submitText = (config['submitText'] as string) || 'Send'; - return `
- ${fields.map(f => ` -
- - ${f === 'message' ? `` - : ``} -
- `).join('')} - -
`; - } - case 'MAP': { - const address = (config['address'] as string) || 'New York, NY'; - return `
- Map: ${this.esc(address)} -
`; - } - case 'CUSTOM_HTML': { - const html = (config['html'] as string) || ''; - return `
${html}
`; - } - default: - return `
[${this.esc(type)}]
`; - } - } - - private cssProperty(key: string): string { - return key.replace(/([A-Z])/g, '-$1').toLowerCase(); - } - - private esc(s: string): string { - const div = document.createElement('div'); - div.textContent = s; - return div.innerHTML; - } - - private capitalize(s: string): string { - return s.charAt(0).toUpperCase() + s.slice(1); - } } diff --git a/frontend/src/app/llm-workspace/settings/settings.component.ts b/frontend/src/app/llm-workspace/settings/settings.component.ts index 591d239..f254a9b 100644 --- a/frontend/src/app/llm-workspace/settings/settings.component.ts +++ b/frontend/src/app/llm-workspace/settings/settings.component.ts @@ -98,11 +98,11 @@ import { SiteResponse } from '../../core/models/site'; Your site is live at {{ liveUrl }}

- -