From c362219a400f807c99e1ec30d77bc11eb354d21b Mon Sep 17 00:00:00 2001 From: Krrish Ghimire Date: Sat, 11 Jul 2026 01:28:09 +0545 Subject: [PATCH] prune old sites --- .../com/krrishg/controller/LLMController.java | 13 ++ .../java/com/krrishg/dto/SiteStructure.java | 1 + .../krrishg/service/LLMResponseParser.java | 25 ++- .../java/com/krrishg/service/LLMService.java | 155 +++++++++++++- backend/src/main/resources/application.yml | 4 + .../com/krrishg/service/LLMServiceTest.java | 195 +++++++++++++++++- .../src/app/core/services/llm-api.service.ts | 4 + .../llm-workspace/llm-workspace.component.ts | 111 +++++++++- 8 files changed, 485 insertions(+), 23 deletions(-) diff --git a/backend/src/main/java/com/krrishg/controller/LLMController.java b/backend/src/main/java/com/krrishg/controller/LLMController.java index 3217775..a9a7cc3 100644 --- a/backend/src/main/java/com/krrishg/controller/LLMController.java +++ b/backend/src/main/java/com/krrishg/controller/LLMController.java @@ -162,4 +162,17 @@ public class LLMController { return ResponseEntity.notFound().build(); } } + + @PostMapping("/versions/{siteId}/prune") + public ResponseEntity pruneVersions( + @AuthenticationPrincipal UserPrincipal principal, + @PathVariable UUID siteId, + @RequestBody(required = false) Map request) { + int keep = 10; + if (request != null && request.containsKey("keep")) { + keep = ((Number) request.get("keep")).intValue(); + } + int deleted = llmService.pruneVersions(siteId, keep); + return ResponseEntity.ok(Map.of("deleted", deleted)); + } } diff --git a/backend/src/main/java/com/krrishg/dto/SiteStructure.java b/backend/src/main/java/com/krrishg/dto/SiteStructure.java index 2fd4a38..e5a05c9 100644 --- a/backend/src/main/java/com/krrishg/dto/SiteStructure.java +++ b/backend/src/main/java/com/krrishg/dto/SiteStructure.java @@ -35,5 +35,6 @@ public class SiteStructure { private String seoDescription; private int orderIndex; private String rawHtml; + private boolean deleted; } } diff --git a/backend/src/main/java/com/krrishg/service/LLMResponseParser.java b/backend/src/main/java/com/krrishg/service/LLMResponseParser.java index 82db3f9..24d757b 100644 --- a/backend/src/main/java/com/krrishg/service/LLMResponseParser.java +++ b/backend/src/main/java/com/krrishg/service/LLMResponseParser.java @@ -31,7 +31,17 @@ public class LLMResponseParser { String json = extractJson(llmOutput); try { JsonNode root = objectMapper.readTree(json); - return parseSiteStructure(root); + return parseSiteStructure(root, true); + } catch (JsonProcessingException e) { + throw new IllegalArgumentException("Failed to parse LLM output as JSON: " + e.getMessage()); + } + } + + public SiteStructure parseLenient(String llmOutput) { + String json = extractJson(llmOutput); + try { + JsonNode root = objectMapper.readTree(json); + return parseSiteStructure(root, false); } catch (JsonProcessingException e) { throw new IllegalArgumentException("Failed to parse LLM output as JSON: " + e.getMessage()); } @@ -69,7 +79,7 @@ public class LLMResponseParser { return trimmed; } - private SiteStructure parseSiteStructure(JsonNode root) { + private SiteStructure parseSiteStructure(JsonNode root, boolean requirePages) { SiteStructure.SiteStructureBuilder builder = SiteStructure.builder(); if (root.has("theme")) { @@ -79,13 +89,13 @@ public class LLMResponseParser { List pages = new ArrayList<>(); if (root.has("pages") && root.get("pages").isArray()) { for (JsonNode pageNode : root.get("pages")) { - pages.add(parsePage(pageNode)); + pages.add(parsePage(pageNode, !requirePages)); } } builder.pages(pages); SiteStructure structure = builder.build(); - if (structure.getPages() == null || structure.getPages().isEmpty()) { + if (requirePages && (structure.getPages() == null || structure.getPages().isEmpty())) { throw new IllegalArgumentException("LLM output must contain at least one page"); } @@ -118,7 +128,7 @@ public class LLMResponseParser { return map; } - private SiteStructure.PageStructure parsePage(JsonNode pageNode) { + private SiteStructure.PageStructure parsePage(JsonNode pageNode, boolean lenient) { SiteStructure.PageStructure.PageStructureBuilder builder = SiteStructure.PageStructure.builder(); if (pageNode.has("id")) { @@ -145,9 +155,12 @@ public class LLMResponseParser { if (pageNode.has("rawHtml")) { builder.rawHtml(pageNode.get("rawHtml").asText()); } + if (pageNode.has("deleted")) { + builder.deleted(pageNode.get("deleted").asBoolean()); + } SiteStructure.PageStructure page = builder.build(); - if (page.getTitle() == null || page.getSlug() == null) { + if (!lenient && !page.isDeleted() && (page.getTitle() == null || page.getSlug() == null)) { throw new IllegalArgumentException("Page must have title and slug"); } diff --git a/backend/src/main/java/com/krrishg/service/LLMService.java b/backend/src/main/java/com/krrishg/service/LLMService.java index 903e23d..1b981dc 100644 --- a/backend/src/main/java/com/krrishg/service/LLMService.java +++ b/backend/src/main/java/com/krrishg/service/LLMService.java @@ -29,8 +29,12 @@ import java.io.IOException; import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.UUID; import java.util.stream.Collectors; @@ -38,7 +42,6 @@ import java.util.stream.Collectors; public class LLMService { private static final Logger log = LoggerFactory.getLogger(LLMService.class); - private static final int MAX_VERSIONS_PER_SITE = 100; private final ProviderFactory providerFactory; private final LLMResponseParser responseParser; @@ -48,6 +51,8 @@ public class LLMService { private final EncryptionService encryptionService; private final String systemPrompt; private final String refinerPrompt; + private final int maxVersionsPerSite; + private final double refineTemperature; public LLMService(ProviderFactory providerFactory, LLMResponseParser responseParser, @@ -57,7 +62,9 @@ public class LLMService { EncryptionService encryptionService, ResourceLoader resourceLoader, @Value("${indie.llm.system-prompt-path:classpath:prompts/site-generator.txt}") String promptPath, - @Value("${indie.llm.refiner-prompt-path:classpath:prompts/site-refiner.txt}") String refinerPath) { + @Value("${indie.llm.refiner-prompt-path:classpath:prompts/site-refiner.txt}") String refinerPath, + @Value("${indie.llm.max-versions-per-site:10}") int maxVersionsPerSite, + @Value("${indie.llm.refine.temperature:0.3}") double refineTemperature) { this.providerFactory = providerFactory; this.responseParser = responseParser; this.referenceService = referenceService; @@ -66,6 +73,8 @@ public class LLMService { this.encryptionService = encryptionService; this.systemPrompt = loadSystemPrompt(resourceLoader, promptPath); this.refinerPrompt = loadSystemPrompt(resourceLoader, refinerPath); + this.maxVersionsPerSite = maxVersionsPerSite; + this.refineTemperature = refineTemperature; } public SiteStructure generateSite(UUID userId, UUID siteId, String prompt, @@ -121,29 +130,54 @@ public class LLMService { String combinedPrompt = "Current site structure:\n" + currentJson + "\n\nRefinement request: " + refinementPrompt + referenceContext - + "\n\nOutput the complete updated JSON structure with the requested changes applied."; + + "\n\nOutput only the fields that changed. Fields you omit will remain unchanged." + + " Include the \"id\" field for any page you modify or add." + + " To delete a page, include it with \"id\" and \"deleted\": true." + + " Pages not included in your output will stay as-is."; LLMOptions options = LLMOptions.builder() - .temperature(0.5) + .temperature(refineTemperature) .build(); LLMResult result = callLLM(provider, resolvedKey, resolvedBaseUrl, resolvedModel, refinerPrompt, combinedPrompt, references, options); log.info("LLM refinement completed: model={}, latency={}ms, tokens={}+{}", result.getModel(), result.getLatencyMs(), result.getPromptTokens(), result.getCompletionTokens()); - SiteStructure structure = responseParser.parse(result.getContent()); - saveVersion(userId, siteId, refinementPrompt, references, result.getContent()); + SiteStructure patch = responseParser.parseLenient(result.getContent()); + SiteStructure merged = mergePatches(currentStructure, patch); + saveVersion(userId, siteId, refinementPrompt, references, serializeStructure(merged)); - return structure; + return merged; } public List getVersions(UUID siteId) { Query query = new Query(Criteria.where("siteId").is(siteId)) .with(Sort.by(Sort.Direction.DESC, "versionNumber")) - .limit(MAX_VERSIONS_PER_SITE); + .limit(maxVersionsPerSite); return mongoTemplate.find(query, GenerationVersion.class); } + public int pruneVersions(UUID siteId) { + return pruneVersions(siteId, maxVersionsPerSite); + } + + public int pruneVersions(UUID siteId, int keep) { + Query countQuery = new Query(Criteria.where("siteId").is(siteId)); + long total = mongoTemplate.count(countQuery, GenerationVersion.class); + if (total <= keep) { + return 0; + } + long toDelete = total - keep; + Query findQuery = new Query(Criteria.where("siteId").is(siteId)) + .with(Sort.by(Sort.Direction.ASC, "versionNumber")) + .limit((int) toDelete); + List oldVersions = mongoTemplate.find(findQuery, GenerationVersion.class); + for (GenerationVersion v : oldVersions) { + mongoTemplate.remove(v); + } + return oldVersions.size(); + } + public SiteStructure restoreVersion(String versionId) { GenerationVersion version = mongoTemplate.findById(versionId, GenerationVersion.class); if (version == null) { @@ -160,6 +194,110 @@ public class LLMService { mongoTemplate.remove(version); } + private SiteStructure mergePatches(SiteStructure current, SiteStructure patch) { + SiteStructure.SiteStructureBuilder builder = SiteStructure.builder(); + + if (patch.getTheme() != null) { + builder.theme(mergeTheme(current.getTheme(), patch.getTheme())); + } else { + builder.theme(current.getTheme()); + } + + if (patch.getPages() != null && !patch.getPages().isEmpty()) { + Map currentPageMap = new HashMap<>(); + for (SiteStructure.PageStructure page : current.getPages()) { + if (page.getId() != null) { + currentPageMap.put(page.getId(), page); + } + } + + Set deletedIds = new HashSet<>(); + for (SiteStructure.PageStructure patchPage : patch.getPages()) { + if (patchPage.isDeleted() && patchPage.getId() != null) { + deletedIds.add(patchPage.getId()); + } + } + + List mergedPages = new ArrayList<>(); + for (SiteStructure.PageStructure currentPage : current.getPages()) { + if (currentPage.getId() != null && deletedIds.contains(currentPage.getId())) { + continue; + } + SiteStructure.PageStructure matchingPatch = null; + if (currentPage.getId() != null) { + for (SiteStructure.PageStructure pp : patch.getPages()) { + if (currentPage.getId().equals(pp.getId())) { + matchingPatch = pp; + break; + } + } + } + if (matchingPatch != null && !matchingPatch.isDeleted()) { + mergedPages.add(mergePage(currentPage, matchingPatch)); + } else { + mergedPages.add(currentPage); + } + } + for (SiteStructure.PageStructure patchPage : patch.getPages()) { + if (patchPage.isDeleted()) continue; + String pid = patchPage.getId(); + if (pid == null || !currentPageMap.containsKey(pid)) { + mergedPages.add(patchPage); + } + } + builder.pages(mergedPages); + } else { + builder.pages(current.getPages()); + } + + return builder.build(); + } + + private SiteStructure.ThemeConfig mergeTheme(SiteStructure.ThemeConfig current, SiteStructure.ThemeConfig patch) { + SiteStructure.ThemeConfig.ThemeConfigBuilder builder = SiteStructure.ThemeConfig.builder(); + + if (patch.getColors() != null) { + Map mergedColors = new HashMap<>(current.getColors() != null ? current.getColors() : Map.of()); + mergedColors.putAll(patch.getColors()); + builder.colors(mergedColors); + } else { + builder.colors(current.getColors()); + } + + if (patch.getFonts() != null) { + Map mergedFonts = new HashMap<>(current.getFonts() != null ? current.getFonts() : Map.of()); + mergedFonts.putAll(patch.getFonts()); + builder.fonts(mergedFonts); + } else { + builder.fonts(current.getFonts()); + } + + if (patch.getSpacing() != null) { + Map mergedSpacing = new HashMap<>(current.getSpacing() != null ? current.getSpacing() : Map.of()); + mergedSpacing.putAll(patch.getSpacing()); + builder.spacing(mergedSpacing); + } else { + builder.spacing(current.getSpacing()); + } + + return builder.build(); + } + + private SiteStructure.PageStructure mergePage(SiteStructure.PageStructure current, SiteStructure.PageStructure patch) { + SiteStructure.PageStructure.PageStructureBuilder builder = SiteStructure.PageStructure.builder(); + + builder.id(patch.getId() != null ? patch.getId() : current.getId()); + builder.title(patch.getTitle() != null ? patch.getTitle() : current.getTitle()); + builder.slug(patch.getSlug() != null ? patch.getSlug() : current.getSlug()); + builder.seoTitle(patch.getSeoTitle() != null ? patch.getSeoTitle() : current.getSeoTitle()); + builder.seoDescription(patch.getSeoDescription() != null ? patch.getSeoDescription() : current.getSeoDescription()); + builder.orderIndex(patch.getOrderIndex() != 0 ? patch.getOrderIndex() : current.getOrderIndex()); + builder.rawHtml(patch.getRawHtml() != null ? patch.getRawHtml() : current.getRawHtml()); + builder.deleted(patch.isDeleted()); + + return builder.build(); + } + private String resolveApiKey(UUID userId, String provider, String apiKey) { if (apiKey != null && !apiKey.isBlank()) { return apiKey; @@ -247,6 +385,7 @@ public class LLMService { .createdAt(LocalDateTime.now()) .build(); mongoTemplate.save(version); + pruneVersions(siteId); } private int getNextVersionNumber(UUID siteId) { diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index 41529b6..015bf11 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -73,6 +73,10 @@ indie: llm: read-timeout: ${INDIE_LLM_READ_TIMEOUT:180s} + max-versions-per-site: ${INDIE_LLM_MAX_VERSIONS:10} + refine: + temperature: ${INDIE_LLM_REFINE_TEMPERATURE:0.3} + read-timeout: ${INDIE_LLM_REFINE_READ_TIMEOUT:300s} rate-limit: max-requests-per-hour: ${INDIE_LLM_RATE_LIMIT:10} providers: diff --git a/backend/src/test/java/com/krrishg/service/LLMServiceTest.java b/backend/src/test/java/com/krrishg/service/LLMServiceTest.java index 4dbed5a..2cc7686 100644 --- a/backend/src/test/java/com/krrishg/service/LLMServiceTest.java +++ b/backend/src/test/java/com/krrishg/service/LLMServiceTest.java @@ -82,7 +82,8 @@ class LLMServiceTest { llmService = new LLMService(providerFactory, responseParser, referenceService, mongoTemplate, userRepository, encryptionService, resourceLoader, "classpath:prompts/site-generator.txt", - "classpath:prompts/site-refiner.txt"); + "classpath:prompts/site-refiner.txt", + 10, 0.3); } @Test @@ -210,9 +211,11 @@ class LLMServiceTest { @Test void refineSiteReturnsRefinedStructure() { + String pageId = UUID.randomUUID().toString(); SiteStructure current = SiteStructure.builder() .pages(List.of( SiteStructure.PageStructure.builder() + .id(pageId) .title("Old") .slug("old") .rawHtml("

Old content

") @@ -223,16 +226,202 @@ class LLMServiceTest { geminiProvider.withContent(""" { "pages": [ - {"title": "Refined", "slug": "refined", "rawHtml": "

Hero

"} + {"id": "__PAGE_ID__", "title": "Refined", "slug": "refined", "rawHtml": "

Updated

"} ] } - """); + """.replace("__PAGE_ID__", pageId)); when(mongoTemplate.findOne(any(Query.class), eq(GenerationVersion.class))).thenReturn(null); SiteStructure result = llmService.refineSite(userId, siteId, current, "Make it better", null, "gemini", null); assertEquals("Refined", result.getPages().get(0).getTitle()); + assertEquals("

Updated

", result.getPages().get(0).getRawHtml()); + verify(mongoTemplate).save(any(GenerationVersion.class)); + } + + @Test + void refineSitePreservesUnchangedPages() { + String pageId1 = UUID.randomUUID().toString(); + String pageId2 = UUID.randomUUID().toString(); + SiteStructure current = SiteStructure.builder() + .theme(SiteStructure.ThemeConfig.builder() + .colors(Map.of("primary", "#000", "background", "#fff")) + .build()) + .pages(List.of( + SiteStructure.PageStructure.builder() + .id(pageId1) + .title("Home") + .slug("home") + .rawHtml("

Home content

") + .build(), + SiteStructure.PageStructure.builder() + .id(pageId2) + .title("About") + .slug("about") + .rawHtml("

About content

") + .build() + )) + .build(); + + geminiProvider.withContent(""" + { + "theme": { + "colors": { + "primary": "#1a1a2e", + "accent": "#e94560" + } + }, + "pages": [ + {"id": "__PAGE_ID__", "rawHtml": "

Updated home

"} + ] + } + """.replace("__PAGE_ID__", pageId1)); + + when(mongoTemplate.findOne(any(Query.class), eq(GenerationVersion.class))).thenReturn(null); + + SiteStructure result = llmService.refineSite(userId, siteId, current, "Change primary color", null, "gemini", null); + + assertEquals("#1a1a2e", result.getTheme().getColors().get("primary")); + assertEquals("#fff", result.getTheme().getColors().get("background")); + assertNotNull(result.getTheme().getColors().get("accent")); + + assertEquals(2, result.getPages().size()); + assertEquals("

Updated home

", result.getPages().get(0).getRawHtml()); + assertEquals("About", result.getPages().get(1).getTitle()); + assertEquals("

About content

", result.getPages().get(1).getRawHtml()); + verify(mongoTemplate).save(any(GenerationVersion.class)); + } + + @Test + void refineSiteDeletesPage() { + String pageId1 = UUID.randomUUID().toString(); + String pageId2 = UUID.randomUUID().toString(); + SiteStructure current = SiteStructure.builder() + .pages(List.of( + SiteStructure.PageStructure.builder() + .id(pageId1) + .title("Home") + .slug("home") + .rawHtml("

Home

") + .build(), + SiteStructure.PageStructure.builder() + .id(pageId2) + .title("About") + .slug("about") + .rawHtml("

About

") + .build() + )) + .build(); + + geminiProvider.withContent(""" + { + "pages": [ + {"id": "__PAGE_ID__", "deleted": true} + ] + } + """.replace("__PAGE_ID__", pageId1)); + + when(mongoTemplate.findOne(any(Query.class), eq(GenerationVersion.class))).thenReturn(null); + + SiteStructure result = llmService.refineSite(userId, siteId, current, "Remove Home page", null, "gemini", null); + + assertEquals(1, result.getPages().size()); + assertEquals("About", result.getPages().get(0).getTitle()); + verify(mongoTemplate).save(any(GenerationVersion.class)); + } + + @Test + void refineSiteAddsNewPage() { + String existingId = UUID.randomUUID().toString(); + SiteStructure current = SiteStructure.builder() + .pages(List.of( + SiteStructure.PageStructure.builder() + .id(existingId) + .title("Home") + .slug("home") + .rawHtml("

Home

") + .build() + )) + .build(); + + geminiProvider.withContent(""" + { + "pages": [ + {"title": "Contact", "slug": "contact", "rawHtml": "

Contact form

"} + ] + } + """); + + when(mongoTemplate.findOne(any(Query.class), eq(GenerationVersion.class))).thenReturn(null); + + SiteStructure result = llmService.refineSite(userId, siteId, current, "Add Contact page", null, "gemini", null); + + assertEquals(2, result.getPages().size()); + assertEquals("Contact", result.getPages().get(1).getTitle()); + verify(mongoTemplate).save(any(GenerationVersion.class)); + } + + @Test + void pruneVersionsDeletesOldestVersions() { + lenient().when(mongoTemplate.count(any(Query.class), eq(GenerationVersion.class))).thenReturn(15L); + List oldVersions = List.of( + GenerationVersion.builder().id("v1").siteId(siteId).versionNumber(1).build(), + GenerationVersion.builder().id("v2").siteId(siteId).versionNumber(2).build(), + GenerationVersion.builder().id("v3").siteId(siteId).versionNumber(3).build(), + GenerationVersion.builder().id("v4").siteId(siteId).versionNumber(4).build(), + GenerationVersion.builder().id("v5").siteId(siteId).versionNumber(5).build() + ); + lenient().when(mongoTemplate.find(any(Query.class), eq(GenerationVersion.class))).thenReturn(oldVersions); + + int deleted = llmService.pruneVersions(siteId, 10); + + assertEquals(5, deleted); + } + + @Test + void pruneVersionsDoesNothingWhenUnderLimit() { + lenient().when(mongoTemplate.count(any(Query.class), eq(GenerationVersion.class))).thenReturn(8L); + + int deleted = llmService.pruneVersions(siteId, 10); + + assertEquals(0, deleted); + } + + @Test + void saveVersionTriggersAutoCleanup() { + String pageId = UUID.randomUUID().toString(); + when(mongoTemplate.findOne(any(Query.class), eq(GenerationVersion.class))).thenReturn(null); + lenient().when(mongoTemplate.count(any(Query.class), eq(GenerationVersion.class))).thenReturn(15L); + lenient().when(mongoTemplate.find(any(Query.class), eq(GenerationVersion.class))).thenReturn(List.of( + GenerationVersion.builder().id("old").siteId(siteId).versionNumber(1).build(), + GenerationVersion.builder().id("older").siteId(siteId).versionNumber(2).build(), + GenerationVersion.builder().id("oldest").siteId(siteId).versionNumber(3).build(), + GenerationVersion.builder().id("ancient").siteId(siteId).versionNumber(4).build(), + GenerationVersion.builder().id("prehistoric").siteId(siteId).versionNumber(5).build() + )); + + geminiProvider.withContent(""" + { + "pages": [ + {"id": "__PAGE_ID__", "title": "Home", "slug": "home", "rawHtml": "

Test

"} + ] + } + """.replace("__PAGE_ID__", pageId)); + + SiteStructure current = SiteStructure.builder() + .pages(List.of( + SiteStructure.PageStructure.builder() + .id(pageId) + .title("Home") + .slug("home") + .rawHtml("

Old

") + .build() + )) + .build(); + + llmService.refineSite(userId, siteId, current, "Update", null, "gemini", null); + verify(mongoTemplate).save(any(GenerationVersion.class)); } diff --git a/frontend/src/app/core/services/llm-api.service.ts b/frontend/src/app/core/services/llm-api.service.ts index bb772f1..bdc5422 100644 --- a/frontend/src/app/core/services/llm-api.service.ts +++ b/frontend/src/app/core/services/llm-api.service.ts @@ -26,4 +26,8 @@ export class LlmApiService { deleteVersion(versionId: string): Observable { return this.http.delete(`/api/llm/versions/${versionId}`); } + + pruneVersions(siteId: string, keep: number = 10): Observable<{deleted: number}> { + return this.http.post<{deleted: number}>(`/api/llm/versions/${siteId}/prune`, { keep }); + } } diff --git a/frontend/src/app/llm-workspace/llm-workspace.component.ts b/frontend/src/app/llm-workspace/llm-workspace.component.ts index 667bbed..1be4fe1 100644 --- a/frontend/src/app/llm-workspace/llm-workspace.component.ts +++ b/frontend/src/app/llm-workspace/llm-workspace.component.ts @@ -91,10 +91,48 @@ import { DeleteVersionDialog } from './delete-version-dialog/delete-version-dial Generate -
-

- No API keys configured. Go to Settings to add an API key for an AI provider. -

+ +
+ + + + + +

Configure AI Provider

+

+ Add an API key to enable AI site generation. Keys are encrypted at rest. +

+
+ + Provider + + Gemini + OpenAI / Compatible + + + + API Key + + + + + Base URL (optional) + + For OpenAI-compatible providers like DeepSeek + + + Model (optional) + + Override the default model name + +
+
@@ -123,7 +161,19 @@ import { DeleteVersionDialog } from './delete-version-dialog/delete-version-dial

History

- {{ (session.versions$ | async)?.length || 0 }} version{{ ((session.versions$ | async)?.length || 0) !== 1 ? 's' : '' }} +
+ + {{ (session.versions$ | async)?.length || 0 }} version{{ ((session.versions$ | async)?.length || 0) !== 1 ? 's' : '' }} +
@@ -275,7 +325,7 @@ import { DeleteVersionDialog } from './delete-version-dialog/delete-version-dial
-
+
auto_awesome

Describe your dream site

@@ -306,6 +356,14 @@ export class LlmWorkspaceComponent implements OnInit, OnDestroy { llmModels: Record = {}; configuredProviders: string[] = []; selectedProvider = ''; + + setupProvider = 'gemini'; + setupApiKey = ''; + setupBaseUrl = ''; + setupModel = ''; + setupSaving = false; + setupKeyVisible = false; + private destroy$ = new Subject(); ngOnInit(): void { @@ -446,6 +504,23 @@ export class LlmWorkspaceComponent implements OnInit, OnDestroy { }); } + pruneOldVersions(): void { + if (!this.siteId) return; + this.api.pruneVersions(this.siteId) + .pipe(takeUntil(this.destroy$)) + .subscribe({ + next: (result) => { + this.loadVersions(); + if (result.deleted > 0) { + this.snackBar.open(`Cleaned up ${result.deleted} old versions`, 'Close', { duration: 3000 }); + } + }, + error: () => { + this.snackBar.open('Failed to clean up versions', 'Close', { duration: 3000 }); + }, + }); + } + switchToVersion(version: GenerationVersion): void { if (version.id === this.session.currentVersionId) return; this.session.setGenerating(true); @@ -502,6 +577,30 @@ export class LlmWorkspaceComponent implements OnInit, OnDestroy { return prompt.length > maxLen ? prompt.slice(0, maxLen) + '...' : prompt; } + saveSetupKey(): void { + const key = this.setupApiKey?.trim(); + const baseUrl = this.setupBaseUrl?.trim() || undefined; + const model = this.setupModel?.trim() || undefined; + if (!key) return; + this.setupSaving = true; + this.llmConfigService.saveKey(this.setupProvider, key, baseUrl, model) + .pipe(takeUntil(this.destroy$)) + .subscribe({ + next: () => { + this.setupSaving = false; + this.setupApiKey = ''; + this.setupBaseUrl = ''; + this.setupModel = ''; + this.snackBar.open(`${this.setupProvider} API key saved`, 'Close', { duration: 2000 }); + this.loadLlmConfig(); + }, + error: () => { + this.setupSaving = false; + this.snackBar.open('Failed to save API key', 'Close', { duration: 3000 }); + }, + }); + } + ngOnDestroy(): void { this.destroy$.next(); this.destroy$.complete();