From e705f240d49b9cb141130b84595bc441e51d0f85 Mon Sep 17 00:00:00 2001 From: Krrish Ghimire Date: Fri, 10 Jul 2026 15:20:25 +0545 Subject: [PATCH] optimize site generation --- .../config/GlobalExceptionHandler.java | 8 ++ .../app/llm-workspace/generation.service.ts | 93 +++++++++++++++++++ .../llm-workspace/llm-workspace.component.ts | 62 ++++--------- 3 files changed, 119 insertions(+), 44 deletions(-) create mode 100644 frontend/src/app/llm-workspace/generation.service.ts diff --git a/backend/src/main/java/com/krrishg/config/GlobalExceptionHandler.java b/backend/src/main/java/com/krrishg/config/GlobalExceptionHandler.java index 3bb5fd4..6490709 100644 --- a/backend/src/main/java/com/krrishg/config/GlobalExceptionHandler.java +++ b/backend/src/main/java/com/krrishg/config/GlobalExceptionHandler.java @@ -1,6 +1,7 @@ package com.krrishg.config; import io.jsonwebtoken.JwtException; +import org.apache.catalina.connector.ClientAbortException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.HttpStatus; @@ -35,6 +36,13 @@ public class GlobalExceptionHandler { return ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, message); } + @ExceptionHandler(ClientAbortException.class) + public void handleClientAbort(ClientAbortException ex) { + if (log.isDebugEnabled()) { + log.debug("Client disconnected: {}", ex.getMessage()); + } + } + @ExceptionHandler(Exception.class) public ProblemDetail handleGeneral(Exception ex) { log.error("Unhandled exception", ex); diff --git a/frontend/src/app/llm-workspace/generation.service.ts b/frontend/src/app/llm-workspace/generation.service.ts new file mode 100644 index 0000000..d99232f --- /dev/null +++ b/frontend/src/app/llm-workspace/generation.service.ts @@ -0,0 +1,93 @@ +import { Injectable, inject } from '@angular/core'; +import { MatSnackBar } from '@angular/material/snack-bar'; +import { BehaviorSubject, Subject, Subscription } from 'rxjs'; +import { LlmApiService } from '../core/services/llm-api.service'; +import { LlmSessionService } from './llm-session.service'; +import { LLMGenerateRequest, LLMRefineRequest, SiteStructure } from '../core/models/llm'; + +export interface GenerationState { + status: 'idle' | 'generating' | 'completed' | 'error'; + result: SiteStructure | null; + error: string | null; +} + +@Injectable({ providedIn: 'root' }) +export class GenerationService { + private api = inject(LlmApiService); + private session = inject(LlmSessionService); + private snackBar = inject(MatSnackBar); + + private generationSubscription: Subscription | null = null; + private stateSubject = new BehaviorSubject({ + status: 'idle', result: null, error: null, + }); + private refreshVersionsSubject = new Subject(); + + readonly state$ = this.stateSubject.asObservable(); + readonly refreshVersions$ = this.refreshVersionsSubject.asObservable(); + + generate(request: LLMGenerateRequest, siteId: string): void { + this.cancel(); + this.stateSubject.next({ status: 'generating', result: null, error: null }); + this.session.setGenerating(true); + this.session.clearError(); + + this.generationSubscription = this.api.generate(request).subscribe({ + next: (data) => { + this.stateSubject.next({ status: 'completed', result: data, error: null }); + this.session.setResult(data); + this.session.setCurrentVersionId(null); + this.refreshVersionsSubject.next(siteId); + this.snackBar.open('Site generated successfully!', 'Close', { duration: 5000 }); + this.generationSubscription = null; + }, + error: (err) => { + const msg = err.error?.error || err.statusText || 'Failed to connect to server'; + this.stateSubject.next({ status: 'error', result: null, error: msg }); + this.session.setError(msg); + this.snackBar.open('Generation failed: ' + msg, 'Dismiss', { duration: 8000 }); + this.generationSubscription = null; + }, + }); + } + + refine(request: LLMRefineRequest, siteId: string): void { + this.cancel(); + this.stateSubject.next({ status: 'generating', result: null, error: null }); + this.session.setGenerating(true); + this.session.clearError(); + + this.generationSubscription = this.api.refine(request).subscribe({ + next: (data) => { + this.stateSubject.next({ status: 'completed', result: data, error: null }); + this.session.setResult(data); + this.session.setCurrentVersionId(null); + this.refreshVersionsSubject.next(siteId); + this.snackBar.open('Site refined successfully!', 'Close', { duration: 5000 }); + this.generationSubscription = null; + }, + error: (err) => { + const msg = err.error?.error || err.statusText || 'Failed to connect to server'; + this.stateSubject.next({ status: 'error', result: null, error: msg }); + this.session.setError(msg); + this.snackBar.open('Refinement failed: ' + msg, 'Dismiss', { duration: 8000 }); + this.generationSubscription = null; + }, + }); + } + + cancel(): void { + if (this.generationSubscription) { + this.generationSubscription.unsubscribe(); + this.generationSubscription = null; + } + this.stateSubject.next({ status: 'idle', result: null, error: null }); + this.session.setGenerating(false); + this.session.clearError(); + } + + resetState(): void { + this.cancel(); + this.stateSubject.next({ status: 'idle', result: null, error: null }); + } +} diff --git a/frontend/src/app/llm-workspace/llm-workspace.component.ts b/frontend/src/app/llm-workspace/llm-workspace.component.ts index 90d6f00..667bbed 100644 --- a/frontend/src/app/llm-workspace/llm-workspace.component.ts +++ b/frontend/src/app/llm-workspace/llm-workspace.component.ts @@ -19,6 +19,7 @@ import { takeUntil } from 'rxjs/operators'; import { LlmApiService } from '../core/services/llm-api.service'; import { LlmConfigService } from '../core/services/llm-config.service'; import { LlmSessionService } from './llm-session.service'; +import { GenerationService } from './generation.service'; import { SiteStructure, GenerationVersion, LLMKeyStatus } from '../core/models/llm'; import { PreviewDialogComponent } from './preview-dialog/preview-dialog.component'; import { DeleteVersionDialog } from './delete-version-dialog/delete-version-dialog'; @@ -122,14 +123,14 @@ import { DeleteVersionDialog } from './delete-version-dialog/delete-version-dial

History

- {{ versions.length }} version{{ versions.length !== 1 ? 's' : '' }} + {{ (session.versions$ | async)?.length || 0 }} version{{ ((session.versions$ | async)?.length || 0) !== 1 ? 's' : '' }}

{{ truncatePrompt(v.prompt, 50) }}

-

+

No versions yet

@@ -293,13 +294,13 @@ export class LlmWorkspaceComponent implements OnInit, OnDestroy { private llmConfigService = inject(LlmConfigService); private snackBar = inject(MatSnackBar); private dialog = inject(MatDialog); + private generationService = inject(GenerationService); session = inject(LlmSessionService); prompt = ''; refinePrompt = ''; showRefine = false; siteId: string | null = null; - versions: GenerationVersion[] = []; llmKeyStatus: LLMKeyStatus = {}; llmBaseUrls: Record = {}; llmModels: Record = {}; @@ -308,6 +309,10 @@ export class LlmWorkspaceComponent implements OnInit, OnDestroy { private destroy$ = new Subject(); ngOnInit(): void { + this.generationService.refreshVersions$ + .pipe(takeUntil(this.destroy$)) + .subscribe(() => this.loadVersions()); + this.route.paramMap .pipe(takeUntil(this.destroy$)) .subscribe(params => { @@ -317,7 +322,6 @@ export class LlmWorkspaceComponent implements OnInit, OnDestroy { this.prompt = ''; this.refinePrompt = ''; this.showRefine = false; - this.versions = []; this.session.clearHistory(); this.loadVersions(); this.loadLlmConfig(); @@ -357,7 +361,6 @@ export class LlmWorkspaceComponent implements OnInit, OnDestroy { .pipe(takeUntil(this.destroy$)) .subscribe({ next: (v) => { - this.versions = v; this.session.setVersions(v); if (v.length > 0 && !this.session.currentResult) { this.api.restoreVersion(v[0].id) @@ -384,68 +387,39 @@ export class LlmWorkspaceComponent implements OnInit, OnDestroy { if (!this.prompt.trim() || !this.siteId || !this.selectedProvider) return; this.showRefine = false; this.session.setPrompt(this.prompt); - this.session.setGenerating(true); - this.session.clearError(); const baseUrl = this.llmBaseUrls[this.selectedProvider]; const model = this.llmModels[this.selectedProvider]; - this.api.generate({ + this.generationService.generate({ prompt: this.prompt, siteId: this.siteId, provider: this.selectedProvider, baseUrl, model, - }) - .pipe(takeUntil(this.destroy$)) - .subscribe({ - next: (data) => { - this.session.setResult(data); - this.session.setCurrentVersionId(null); - this.loadVersions(); - this.snackBar.open('Site generated successfully!', 'Close', { duration: 3000 }); - }, - error: (err) => { - const msg = err.error?.error || err.statusText || 'Failed to connect to server'; - this.session.setError(msg); - }, - }); + }, this.siteId); } refine(): void { const result = this.session.currentResult; if (!result || !this.refinePrompt.trim() || !this.siteId || !this.selectedProvider) return; - this.session.setGenerating(true); - this.session.clearError(); const baseUrl = this.llmBaseUrls[this.selectedProvider]; const model = this.llmModels[this.selectedProvider]; - this.api.refine({ + this.generationService.refine({ prompt: this.refinePrompt, siteId: this.siteId, provider: this.selectedProvider, baseUrl, model, currentStructure: result, - }) - .pipe(takeUntil(this.destroy$)) - .subscribe({ - next: (data) => { - this.session.setResult(data); - this.session.setCurrentVersionId(null); - this.loadVersions(); - this.refinePrompt = ''; - this.showRefine = false; - this.snackBar.open('Site refined successfully!', 'Close', { duration: 3000 }); - }, - error: (err) => { - const msg = err.error?.error || err.statusText || 'Failed to connect to server'; - this.session.setError(msg); - }, - }); + }, this.siteId); + + this.refinePrompt = ''; + this.showRefine = false; } deleteVersion(version: GenerationVersion): void { - if (this.versions.length <= 1) { + if (this.session.versions.length <= 1) { this.snackBar.open('Cannot delete the last version', 'Close', { duration: 3000 }); return; } @@ -520,7 +494,7 @@ export class LlmWorkspaceComponent implements OnInit, OnDestroy { } get currentVersionPrompt(): string | null { - const version = this.versions.find(v => v.id === this.session.currentVersionId); + const version = this.session.versions.find(v => v.id === this.session.currentVersionId); return version ? version.prompt : null; }