update site generation flow
This commit is contained in:
@@ -20,10 +20,13 @@ 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 { SiteStructure, GenerationVersion, LLMKeyStatus, SectionSuggestion } from '../core/models/llm';
|
||||
import { SectionConfiguratorComponent, SectionConfig } from './section-configurator/section-configurator.component';
|
||||
import { PreviewDialogComponent } from './preview-dialog/preview-dialog.component';
|
||||
import { DeleteVersionDialog } from './delete-version-dialog/delete-version-dialog';
|
||||
|
||||
type Phase = 'idle' | 'suggesting' | 'configuring' | 'generating';
|
||||
|
||||
@Component({
|
||||
selector: 'app-llm-workspace',
|
||||
standalone: true,
|
||||
@@ -33,6 +36,7 @@ import { DeleteVersionDialog } from './delete-version-dialog/delete-version-dial
|
||||
MatInputModule, MatFormFieldModule, MatProgressSpinnerModule, MatSelectModule,
|
||||
MatSnackBarModule, MatCardModule,
|
||||
MatDividerModule, MatExpansionModule, MatDialogModule,
|
||||
SectionConfiguratorComponent,
|
||||
],
|
||||
styles: [`
|
||||
.provider-select { width: 140px; }
|
||||
@@ -52,26 +56,24 @@ import { DeleteVersionDialog } from './delete-version-dialog/delete-version-dial
|
||||
</button>
|
||||
</mat-toolbar>
|
||||
|
||||
|
||||
|
||||
<div class="flex-1 overflow-y-auto">
|
||||
<div class="px-6 py-6 space-y-6">
|
||||
|
||||
<mat-card class="p-4">
|
||||
<mat-card class="p-4" *ngIf="currentPhase === 'idle'">
|
||||
<mat-card-content class="!p-0">
|
||||
<mat-form-field appearance="outline" class="w-full">
|
||||
<mat-label>Describe your website</mat-label>
|
||||
<mat-label>Describe your website idea</mat-label>
|
||||
<textarea
|
||||
matInput
|
||||
rows="5"
|
||||
placeholder="e.g. A personal portfolio for a photographer with a gallery page, about section, and contact form. Dark theme with accent gold."
|
||||
[(ngModel)]="prompt"
|
||||
[disabled]="(session.generating$ | async) === true"
|
||||
rows="4"
|
||||
placeholder="e.g. A photography portfolio for Alex Chen"
|
||||
[(ngModel)]="briefDescription"
|
||||
[disabled]="suggesting"
|
||||
></textarea>
|
||||
</mat-form-field>
|
||||
<div class="flex items-center justify-between mt-2">
|
||||
<span class="text-xs text-gray-400">
|
||||
Describe the pages, layout, theme, and content you want.
|
||||
Briefly describe the type of site you want. The AI will suggest sections you can customize.
|
||||
</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<mat-form-field appearance="outline" class="!mb-0 provider-select" *ngIf="configuredProviders.length > 1">
|
||||
@@ -84,19 +86,51 @@ import { DeleteVersionDialog } from './delete-version-dialog/delete-version-dial
|
||||
<button
|
||||
mat-raised-button
|
||||
color="primary"
|
||||
[disabled]="!prompt.trim() || (session.generating$ | async) || configuredProviders.length === 0"
|
||||
(click)="generate()"
|
||||
[disabled]="!briefDescription.trim() || suggesting || configuredProviders.length === 0"
|
||||
(click)="suggestSections()"
|
||||
>
|
||||
<mat-icon>auto_awesome</mat-icon>
|
||||
Generate
|
||||
Plan My Site
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
|
||||
<mat-card *ngIf="configuredProviders.length === 0 && !(session.generating$ | async)" class="p-4" appearance="outlined">
|
||||
<div *ngIf="suggesting" class="flex flex-col items-center justify-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">Analyzing your idea...</p>
|
||||
<p class="text-sm text-gray-400">Finding the right sections for your site</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Phase 3: Section Configurator -->
|
||||
<div *ngIf="currentPhase === 'configuring'" class="space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold text-gray-900">
|
||||
{{ configStepTitle }}
|
||||
</h3>
|
||||
<p class="text-sm text-gray-500">
|
||||
{{ configStepSubtitle }}
|
||||
</p>
|
||||
</div>
|
||||
<button mat-stroked-button (click)="backToIdle()">
|
||||
<mat-icon>arrow_back</mat-icon>
|
||||
Back
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<app-section-configurator
|
||||
[suggestions]="sectionSuggestions"
|
||||
(configComplete)="onConfigComplete($event)"
|
||||
(stepChange)="onConfigStepChange($event)"
|
||||
></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>
|
||||
<p class="text-sm text-gray-600 mb-3">
|
||||
@@ -146,6 +180,15 @@ import { DeleteVersionDialog } from './delete-version-dialog/delete-version-dial
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div *ngIf="suggestError" class="bg-red-50 border border-red-200 rounded-lg p-4 flex items-start gap-3">
|
||||
<mat-icon class="text-red-500 mt-0.5">error_outline</mat-icon>
|
||||
<div class="flex-1">
|
||||
<p class="text-sm font-medium text-red-800">Suggestion failed</p>
|
||||
<p class="text-sm text-red-600 mt-1">{{ suggestError }}</p>
|
||||
<button mat-button color="warn" class="mt-2" (click)="suggestError = null">Dismiss</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div *ngIf="session.error$ | async as error" class="bg-red-50 border border-red-200 rounded-lg p-4 flex items-start gap-3">
|
||||
<mat-icon class="text-red-500 mt-0.5">error_outline</mat-icon>
|
||||
<div class="flex-1">
|
||||
@@ -325,11 +368,11 @@ import { DeleteVersionDialog } from './delete-version-dialog/delete-version-dial
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div *ngIf="configuredProviders.length > 0 && !(session.generating$ | async) && !(session.currentResult$ | async) && !(session.error$ | async)" class="flex flex-col items-center justify-center py-16 text-gray-400">
|
||||
<div *ngIf="configuredProviders.length > 0 && currentPhase === 'idle' && !(session.currentResult$ | async) && !(session.error$ | async)" class="flex flex-col items-center justify-center py-16 text-gray-400">
|
||||
<mat-icon class="text-6xl mb-4" style="width: auto; height: auto; font-size: 4rem;">auto_awesome</mat-icon>
|
||||
<h3 class="text-xl font-medium text-gray-500 mb-2">Describe your dream site</h3>
|
||||
<p class="text-sm text-gray-400 text-center max-w-md">
|
||||
Tell the AI what kind of website you want — pages, style, content — and it will generate a complete site structure ready for you to customize.
|
||||
Tell the AI what kind of website you want. It will suggest pages and sections that you can customize before generating.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -347,6 +390,17 @@ export class LlmWorkspaceComponent implements OnInit, OnDestroy {
|
||||
private generationService = inject(GenerationService);
|
||||
session = inject(LlmSessionService);
|
||||
|
||||
currentPhase: Phase = 'idle';
|
||||
|
||||
briefDescription = '';
|
||||
sectionSuggestions: SectionSuggestion[] = [];
|
||||
sectionConfig: SectionConfig | null = null;
|
||||
configStepTitle = 'Customize Your Site';
|
||||
configStepSubtitle = '';
|
||||
|
||||
suggestError: string | null = null;
|
||||
suggesting = false;
|
||||
|
||||
prompt = '';
|
||||
refinePrompt = '';
|
||||
showRefine = false;
|
||||
@@ -377,9 +431,14 @@ export class LlmWorkspaceComponent implements OnInit, OnDestroy {
|
||||
const siteId = params.get('siteId');
|
||||
if (siteId !== this.siteId) {
|
||||
this.siteId = siteId;
|
||||
this.briefDescription = '';
|
||||
this.prompt = '';
|
||||
this.refinePrompt = '';
|
||||
this.showRefine = false;
|
||||
this.currentPhase = 'idle';
|
||||
this.sectionSuggestions = [];
|
||||
this.sectionConfig = null;
|
||||
this.suggestError = null;
|
||||
this.session.clearHistory();
|
||||
this.loadVersions();
|
||||
this.loadLlmConfig();
|
||||
@@ -439,17 +498,72 @@ export class LlmWorkspaceComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
}
|
||||
|
||||
suggestSections(): void {
|
||||
if (!this.briefDescription.trim() || !this.siteId || !this.selectedProvider) return;
|
||||
|
||||
this.suggesting = true;
|
||||
this.suggestError = null;
|
||||
|
||||
const baseUrl = this.llmBaseUrls[this.selectedProvider];
|
||||
const model = this.llmModels[this.selectedProvider];
|
||||
|
||||
this.api.suggestSections({
|
||||
briefDescription: this.briefDescription,
|
||||
provider: this.selectedProvider,
|
||||
baseUrl,
|
||||
model,
|
||||
}).pipe(takeUntil(this.destroy$)).subscribe({
|
||||
next: (response) => {
|
||||
this.sectionSuggestions = response.sections;
|
||||
this.suggesting = false;
|
||||
this.currentPhase = 'configuring';
|
||||
},
|
||||
error: (err) => {
|
||||
this.suggesting = false;
|
||||
this.suggestError = err.error?.error || err.statusText || 'Failed to get section suggestions';
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
onConfigComplete(config: SectionConfig): void {
|
||||
this.sectionConfig = config;
|
||||
this.generate();
|
||||
}
|
||||
|
||||
onConfigStepChange(step: string): void {
|
||||
if (step.startsWith('section-')) {
|
||||
const idx = parseInt(step.split('-')[1], 10);
|
||||
const section = this.sectionSuggestions[idx];
|
||||
this.configStepTitle = section ? `Configure ${section.title}` : 'Configure Sections';
|
||||
this.configStepSubtitle = 'Choose a layout pattern or describe your own.';
|
||||
} else if (step === 'theme') {
|
||||
this.configStepTitle = 'Choose Theme Direction';
|
||||
this.configStepSubtitle = 'Pick a general aesthetic for your site, or describe your own.';
|
||||
} else if (step === 'details') {
|
||||
this.configStepTitle = 'Additional Details';
|
||||
this.configStepSubtitle = 'Any final preferences for the generated site.';
|
||||
}
|
||||
}
|
||||
|
||||
backToIdle(): void {
|
||||
this.currentPhase = 'idle';
|
||||
this.sectionSuggestions = [];
|
||||
this.sectionConfig = null;
|
||||
}
|
||||
|
||||
generate(): void {
|
||||
if (!this.prompt.trim() || !this.siteId || !this.selectedProvider) return;
|
||||
if (!this.sectionConfig || !this.siteId || !this.selectedProvider) return;
|
||||
|
||||
const compiledPrompt = this.buildPrompt();
|
||||
this.prompt = compiledPrompt;
|
||||
this.currentPhase = 'generating';
|
||||
this.showRefine = false;
|
||||
this.session.setPrompt(this.prompt);
|
||||
this.session.setPrompt(compiledPrompt);
|
||||
|
||||
const baseUrl = this.llmBaseUrls[this.selectedProvider];
|
||||
const model = this.llmModels[this.selectedProvider];
|
||||
this.generationService.generate({
|
||||
prompt: this.prompt,
|
||||
prompt: compiledPrompt,
|
||||
siteId: this.siteId,
|
||||
provider: this.selectedProvider,
|
||||
baseUrl,
|
||||
@@ -457,6 +571,38 @@ export class LlmWorkspaceComponent implements OnInit, OnDestroy {
|
||||
}, this.siteId);
|
||||
}
|
||||
|
||||
private buildPrompt(): string {
|
||||
const config = this.sectionConfig;
|
||||
if (!config) return '';
|
||||
|
||||
const lines: string[] = [];
|
||||
lines.push(`Site type: ${this.briefDescription}`);
|
||||
|
||||
if (config.themeVibe !== 'Any') {
|
||||
lines.push(`Theme direction: ${config.themeVibe}`);
|
||||
}
|
||||
|
||||
lines.push('');
|
||||
lines.push('Pages:');
|
||||
|
||||
const selectedSections = config.sections.filter(s => s.selected);
|
||||
for (const section of selectedSections) {
|
||||
let desc = section.description;
|
||||
if (section.customNotes.trim()) {
|
||||
desc += ' ' + section.customNotes.trim();
|
||||
}
|
||||
lines.push(`- ${section.title}: ${desc}`);
|
||||
}
|
||||
|
||||
if (config.additionalRequirements.trim()) {
|
||||
lines.push('');
|
||||
lines.push('Additional requirements:');
|
||||
lines.push(config.additionalRequirements.trim());
|
||||
}
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
refine(): void {
|
||||
const result = this.session.currentResult;
|
||||
if (!result || !this.refinePrompt.trim() || !this.siteId || !this.selectedProvider) return;
|
||||
|
||||
Reference in New Issue
Block a user