live update while generating site
This commit is contained in:
@@ -1,24 +1,40 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Injectable, inject } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Observable } from 'rxjs';
|
||||
import { AuthService } from './auth.service';
|
||||
import { LLMGenerateRequest, LLMRefineRequest, SiteStructure, GenerationVersion, SuggestSectionsRequest, SuggestSectionsResponse } from '../models/llm';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class LlmApiService {
|
||||
private authService = inject(AuthService);
|
||||
|
||||
constructor(private http: HttpClient) {}
|
||||
|
||||
generate(request: LLMGenerateRequest): Observable<SiteStructure> {
|
||||
return this.http.post<SiteStructure>('/api/llm/generate', request);
|
||||
private withToken(url: string): string {
|
||||
const token = this.authService.getAccessToken();
|
||||
return token ? `${url}?token=${encodeURIComponent(token)}` : url;
|
||||
}
|
||||
|
||||
refine(request: LLMRefineRequest): Observable<SiteStructure> {
|
||||
return this.http.post<SiteStructure>('/api/llm/refine', request);
|
||||
generate(request: LLMGenerateRequest): Observable<{siteId: string}> {
|
||||
return this.http.post<{siteId: string}>('/api/llm/generate', request);
|
||||
}
|
||||
|
||||
refine(request: LLMRefineRequest): Observable<{siteId: string}> {
|
||||
return this.http.post<{siteId: string}>('/api/llm/refine', request);
|
||||
}
|
||||
|
||||
suggestSections(request: SuggestSectionsRequest): Observable<SuggestSectionsResponse> {
|
||||
return this.http.post<SuggestSectionsResponse>('/api/llm/suggest-sections', request);
|
||||
}
|
||||
|
||||
getGenerationEvents(siteId: string): EventSource {
|
||||
return new EventSource(this.withToken(`/api/llm/generate/${siteId}/events`));
|
||||
}
|
||||
|
||||
getRefineEvents(siteId: string): EventSource {
|
||||
return new EventSource(this.withToken(`/api/llm/refine/${siteId}/events`));
|
||||
}
|
||||
|
||||
getVersions(siteId: string): Observable<GenerationVersion[]> {
|
||||
return this.http.get<GenerationVersion[]>(`/api/llm/versions/${siteId}`);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Injectable, inject } from '@angular/core';
|
||||
import { Injectable, NgZone, 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';
|
||||
@@ -16,8 +16,10 @@ export class GenerationService {
|
||||
private api = inject(LlmApiService);
|
||||
private session = inject(LlmSessionService);
|
||||
private snackBar = inject(MatSnackBar);
|
||||
private zone = inject(NgZone);
|
||||
|
||||
private generationSubscription: Subscription | null = null;
|
||||
private eventSource: EventSource | null = null;
|
||||
private stateSubject = new BehaviorSubject<GenerationState>({
|
||||
status: 'idle', result: null, error: null,
|
||||
});
|
||||
@@ -31,17 +33,17 @@ export class GenerationService {
|
||||
this.stateSubject.next({ status: 'generating', result: null, error: null });
|
||||
this.session.setGenerating(true);
|
||||
this.session.clearError();
|
||||
this.session.clearProgressMessages();
|
||||
this.session.clearStreamingContent();
|
||||
|
||||
this.openGenerationEventSource(siteId);
|
||||
|
||||
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 });
|
||||
next: () => {
|
||||
this.generationSubscription = null;
|
||||
},
|
||||
error: (err) => {
|
||||
this.closeEventSource();
|
||||
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);
|
||||
@@ -56,17 +58,17 @@ export class GenerationService {
|
||||
this.stateSubject.next({ status: 'generating', result: null, error: null });
|
||||
this.session.setGenerating(true);
|
||||
this.session.clearError();
|
||||
this.session.clearProgressMessages();
|
||||
this.session.clearStreamingContent();
|
||||
|
||||
this.openRefineEventSource(siteId);
|
||||
|
||||
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 });
|
||||
next: () => {
|
||||
this.generationSubscription = null;
|
||||
},
|
||||
error: (err) => {
|
||||
this.closeEventSource();
|
||||
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);
|
||||
@@ -76,14 +78,123 @@ export class GenerationService {
|
||||
});
|
||||
}
|
||||
|
||||
private openGenerationEventSource(siteId: string): void {
|
||||
this.closeEventSource();
|
||||
this.eventSource = this.api.getGenerationEvents(siteId);
|
||||
|
||||
this.eventSource.addEventListener('progress', (event: MessageEvent) => {
|
||||
const data = JSON.parse(event.data);
|
||||
this.zone.run(() => {
|
||||
if (data.stage === 'generating_section') {
|
||||
this.session.setCurrentSection(data.message);
|
||||
} else {
|
||||
this.session.addProgressMessage({ stage: data.stage, message: data.message });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
this.eventSource.addEventListener('streaming', (event: MessageEvent) => {
|
||||
const data = JSON.parse(event.data);
|
||||
if (data.chunk) {
|
||||
this.zone.run(() => this.session.appendStreamingContent(data.chunk));
|
||||
}
|
||||
});
|
||||
|
||||
this.eventSource.addEventListener('complete', (event: MessageEvent) => {
|
||||
const data = JSON.parse(event.data);
|
||||
this.zone.run(() => {
|
||||
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.closeEventSource();
|
||||
});
|
||||
|
||||
this.eventSource.addEventListener('error', (event: MessageEvent) => {
|
||||
let msg = 'Generation failed';
|
||||
try {
|
||||
const data = JSON.parse((event as any).data);
|
||||
msg = data.error || msg;
|
||||
} catch {}
|
||||
this.zone.run(() => {
|
||||
this.stateSubject.next({ status: 'error', result: null, error: msg });
|
||||
this.session.setError(msg);
|
||||
this.snackBar.open('Generation failed: ' + msg, 'Dismiss', { duration: 8000 });
|
||||
});
|
||||
this.closeEventSource();
|
||||
});
|
||||
}
|
||||
|
||||
private openRefineEventSource(siteId: string): void {
|
||||
this.closeEventSource();
|
||||
this.eventSource = this.api.getRefineEvents(siteId);
|
||||
|
||||
this.eventSource.addEventListener('progress', (event: MessageEvent) => {
|
||||
const data = JSON.parse(event.data);
|
||||
this.zone.run(() => {
|
||||
if (data.stage === 'generating_section') {
|
||||
this.session.setCurrentSection(data.message);
|
||||
} else {
|
||||
this.session.addProgressMessage({ stage: data.stage, message: data.message });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
this.eventSource.addEventListener('streaming', (event: MessageEvent) => {
|
||||
const data = JSON.parse(event.data);
|
||||
if (data.chunk) {
|
||||
this.zone.run(() => this.session.appendStreamingContent(data.chunk));
|
||||
}
|
||||
});
|
||||
|
||||
this.eventSource.addEventListener('complete', (event: MessageEvent) => {
|
||||
const data = JSON.parse(event.data);
|
||||
this.zone.run(() => {
|
||||
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.closeEventSource();
|
||||
});
|
||||
|
||||
this.eventSource.addEventListener('error', (event: MessageEvent) => {
|
||||
let msg = 'Refinement failed';
|
||||
try {
|
||||
const data = JSON.parse((event as any).data);
|
||||
msg = data.error || msg;
|
||||
} catch {}
|
||||
this.zone.run(() => {
|
||||
this.stateSubject.next({ status: 'error', result: null, error: msg });
|
||||
this.session.setError(msg);
|
||||
this.snackBar.open('Refinement failed: ' + msg, 'Dismiss', { duration: 8000 });
|
||||
});
|
||||
this.closeEventSource();
|
||||
});
|
||||
}
|
||||
|
||||
private closeEventSource(): void {
|
||||
if (this.eventSource) {
|
||||
this.eventSource.close();
|
||||
this.eventSource = null;
|
||||
}
|
||||
}
|
||||
|
||||
cancel(): void {
|
||||
if (this.generationSubscription) {
|
||||
this.generationSubscription.unsubscribe();
|
||||
this.generationSubscription = null;
|
||||
}
|
||||
this.closeEventSource();
|
||||
this.stateSubject.next({ status: 'idle', result: null, error: null });
|
||||
this.session.setGenerating(false);
|
||||
this.session.clearError();
|
||||
this.session.clearProgressMessages();
|
||||
this.session.clearStreamingContent();
|
||||
this.session.setCurrentSection('');
|
||||
}
|
||||
|
||||
resetState(): void {
|
||||
|
||||
@@ -2,6 +2,11 @@ import { Injectable } from '@angular/core';
|
||||
import { BehaviorSubject, Observable } from 'rxjs';
|
||||
import { SiteStructure, GenerationVersion } from '../core/models/llm';
|
||||
|
||||
export interface ProgressMessage {
|
||||
stage: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface PromptEntry {
|
||||
originalPrompt: string;
|
||||
refinedPrompt: string | null;
|
||||
@@ -19,6 +24,9 @@ export class LlmSessionService {
|
||||
private versionsSubject = new BehaviorSubject<GenerationVersion[]>([]);
|
||||
private currentVersionIdSubject = new BehaviorSubject<string | null>(null);
|
||||
private loadingVersionsSubject = new BehaviorSubject<boolean>(false);
|
||||
private progressMessagesSubject = new BehaviorSubject<ProgressMessage[]>([]);
|
||||
private streamingContentSubject = new BehaviorSubject<string>('');
|
||||
private currentSectionSubject = new BehaviorSubject<string>('');
|
||||
|
||||
currentPrompt$ = this.currentPromptSubject.asObservable();
|
||||
currentResult$ = this.currentResultSubject.asObservable();
|
||||
@@ -28,6 +36,9 @@ export class LlmSessionService {
|
||||
versions$ = this.versionsSubject.asObservable();
|
||||
currentVersionId$ = this.currentVersionIdSubject.asObservable();
|
||||
loadingVersions$ = this.loadingVersionsSubject.asObservable();
|
||||
progressMessages$ = this.progressMessagesSubject.asObservable();
|
||||
streamingContent$ = this.streamingContentSubject.asObservable();
|
||||
currentSection$ = this.currentSectionSubject.asObservable();
|
||||
|
||||
get currentPrompt(): string { return this.currentPromptSubject.value; }
|
||||
get currentResult(): SiteStructure | null { return this.currentResultSubject.value; }
|
||||
@@ -35,6 +46,9 @@ export class LlmSessionService {
|
||||
get error(): string | null { return this.errorSubject.value; }
|
||||
get versions(): GenerationVersion[] { return this.versionsSubject.value; }
|
||||
get currentVersionId(): string | null { return this.currentVersionIdSubject.value; }
|
||||
get progressMessages(): ProgressMessage[] { return this.progressMessagesSubject.value; }
|
||||
get streamingContent(): string { return this.streamingContentSubject.value; }
|
||||
get currentSection(): string { return this.currentSectionSubject.value; }
|
||||
|
||||
setPrompt(prompt: string): void {
|
||||
this.currentPromptSubject.next(prompt);
|
||||
@@ -72,12 +86,37 @@ export class LlmSessionService {
|
||||
this.errorSubject.next(null);
|
||||
}
|
||||
|
||||
addProgressMessage(msg: ProgressMessage): void {
|
||||
const msgs = this.progressMessagesSubject.value;
|
||||
this.progressMessagesSubject.next([...msgs, msg]);
|
||||
}
|
||||
|
||||
clearProgressMessages(): void {
|
||||
this.progressMessagesSubject.next([]);
|
||||
this.currentSectionSubject.next('');
|
||||
}
|
||||
|
||||
setCurrentSection(section: string): void {
|
||||
this.currentSectionSubject.next(section);
|
||||
}
|
||||
|
||||
appendStreamingContent(chunk: string): void {
|
||||
this.streamingContentSubject.next(this.streamingContentSubject.value + chunk);
|
||||
}
|
||||
|
||||
clearStreamingContent(): void {
|
||||
this.streamingContentSubject.next('');
|
||||
}
|
||||
|
||||
clearHistory(): void {
|
||||
this.historySubject.next([]);
|
||||
this.currentResultSubject.next(null);
|
||||
this.currentPromptSubject.next('');
|
||||
this.currentVersionIdSubject.next(null);
|
||||
this.errorSubject.next(null);
|
||||
this.progressMessagesSubject.next([]);
|
||||
this.streamingContentSubject.next('');
|
||||
this.currentSectionSubject.next('');
|
||||
}
|
||||
|
||||
setVersions(versions: GenerationVersion[]): void {
|
||||
|
||||
@@ -33,7 +33,7 @@ type Phase = 'idle' | 'suggesting' | 'configuring' | 'generating';
|
||||
imports: [
|
||||
NgIf, NgFor, AsyncPipe, KeyValuePipe, DatePipe, FormsModule, RouterModule,
|
||||
MatToolbarModule, MatButtonModule, MatIconModule,
|
||||
MatInputModule, MatFormFieldModule, MatProgressSpinnerModule, MatSelectModule,
|
||||
MatInputModule, MatFormFieldModule, MatProgressSpinnerModule, MatSelectModule,
|
||||
MatSnackBarModule, MatCardModule,
|
||||
MatDividerModule, MatExpansionModule, MatDialogModule,
|
||||
SectionConfiguratorComponent,
|
||||
@@ -105,7 +105,6 @@ type Phase = 'idle' | 'suggesting' | 'configuring' | 'generating';
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Phase 3: Section Configurator -->
|
||||
<div *ngIf="currentPhase === 'configuring'" class="space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
@@ -129,7 +128,6 @@ type Phase = 'idle' | 'suggesting' | 'configuring' | 'generating';
|
||||
></app-section-configurator>
|
||||
</div>
|
||||
|
||||
<!-- API key setup (shown when no providers configured) -->
|
||||
<mat-card *ngIf="configuredProviders.length === 0 && !(session.generating$ | async) && currentPhase !== 'generating'" class="p-4" appearance="outlined">
|
||||
<mat-card-content class="!p-0">
|
||||
<h4 class="text-sm font-semibold text-gray-500 uppercase tracking-wide mb-4">Configure AI Provider</h4>
|
||||
@@ -172,11 +170,17 @@ type Phase = 'idle' | 'suggesting' | 'configuring' | 'generating';
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
|
||||
<div *ngIf="session.generating$ | async" class="flex flex-col items-center justify-center py-12 space-y-4">
|
||||
<div *ngIf="session.generating$ | async" class="flex flex-col items-center py-12 space-y-4">
|
||||
<mat-spinner diameter="48"></mat-spinner>
|
||||
<div class="text-center">
|
||||
<p class="text-lg font-medium text-gray-700">Generating your site...</p>
|
||||
<p class="text-sm text-gray-400">This usually takes 15-30 seconds</p>
|
||||
<div class="space-y-2 w-80">
|
||||
<div *ngFor="let msg of session.progressMessages$ | async" class="flex items-center gap-2 text-sm">
|
||||
<mat-icon class="text-green-500 text-lg shrink-0">check_circle</mat-icon>
|
||||
<span class="text-gray-600">{{ msg.message }}</span>
|
||||
</div>
|
||||
<div *ngIf="session.currentSection$ | async as section" class="flex items-center gap-2 text-sm pt-1">
|
||||
<mat-icon class="text-blue-400 text-lg shrink-0">arrow_forward</mat-icon>
|
||||
<span class="text-blue-600 font-medium">{{ section }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user