ask personalized questions for site creation
This commit is contained in:
@@ -3,7 +3,8 @@ package com.krrishg.dto;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public record SuggestSectionsResponse(
|
public record SuggestSectionsResponse(
|
||||||
List<SectionSuggestion> sections
|
List<SectionSuggestion> sections,
|
||||||
|
List<PersonalizedQuestion> personalizedQuestions
|
||||||
) {
|
) {
|
||||||
public record SectionSuggestion(
|
public record SectionSuggestion(
|
||||||
String title,
|
String title,
|
||||||
@@ -16,4 +17,11 @@ public record SuggestSectionsResponse(
|
|||||||
String title,
|
String title,
|
||||||
String description
|
String description
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
public record PersonalizedQuestion(
|
||||||
|
String id,
|
||||||
|
String question,
|
||||||
|
String type,
|
||||||
|
List<String> options
|
||||||
|
) {}
|
||||||
}
|
}
|
||||||
@@ -617,12 +617,14 @@ public class LLMService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private SuggestSectionsResponse parseSectionSuggestions(String llmOutput) {
|
private SuggestSectionsResponse parseSectionSuggestions(String llmOutput) {
|
||||||
String json = extractJsonArray(llmOutput);
|
String json = extractJsonObject(llmOutput);
|
||||||
try {
|
try {
|
||||||
JsonNode root = objectMapper.readTree(json);
|
JsonNode root = objectMapper.readTree(json);
|
||||||
List<SuggestSectionsResponse.SectionSuggestion> sections = new ArrayList<>();
|
List<SuggestSectionsResponse.SectionSuggestion> sections = new ArrayList<>();
|
||||||
if (root.isArray()) {
|
List<SuggestSectionsResponse.PersonalizedQuestion> questions = new ArrayList<>();
|
||||||
for (JsonNode node : root) {
|
|
||||||
|
if (root.has("sections") && root.get("sections").isArray()) {
|
||||||
|
for (JsonNode node : root.get("sections")) {
|
||||||
String title = node.has("title") ? node.get("title").asText() : null;
|
String title = node.has("title") ? node.get("title").asText() : null;
|
||||||
String description = node.has("description") ? node.get("description").asText() : null;
|
String description = node.has("description") ? node.get("description").asText() : null;
|
||||||
String slug = node.has("slug") ? node.get("slug").asText() : null;
|
String slug = node.has("slug") ? node.get("slug").asText() : null;
|
||||||
@@ -641,15 +643,61 @@ public class LLMService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (root.has("personalizedQuestions") && root.get("personalizedQuestions").isArray()) {
|
||||||
|
for (JsonNode node : root.get("personalizedQuestions")) {
|
||||||
|
String id = node.has("id") ? node.get("id").asText() : null;
|
||||||
|
String question = node.has("question") ? node.get("question").asText() : null;
|
||||||
|
String type = node.has("type") ? node.get("type").asText() : "text";
|
||||||
|
if (id != null && question != null) {
|
||||||
|
List<String> options = null;
|
||||||
|
if (node.has("options") && node.get("options").isArray()) {
|
||||||
|
options = new ArrayList<>();
|
||||||
|
for (JsonNode opt : node.get("options")) {
|
||||||
|
options.add(opt.asText());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
questions.add(new SuggestSectionsResponse.PersonalizedQuestion(id, question, type, options));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (sections.isEmpty()) {
|
if (sections.isEmpty()) {
|
||||||
throw new IllegalArgumentException("LLM output must contain at least one section suggestion");
|
throw new IllegalArgumentException("LLM output must contain at least one section suggestion");
|
||||||
}
|
}
|
||||||
return new SuggestSectionsResponse(sections);
|
return new SuggestSectionsResponse(sections, questions);
|
||||||
} catch (JsonProcessingException e) {
|
} catch (JsonProcessingException e) {
|
||||||
throw new IllegalArgumentException("Failed to parse section suggestions as JSON: " + e.getMessage());
|
throw new IllegalArgumentException("Failed to parse section suggestions as JSON: " + e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String extractJsonObject(String raw) {
|
||||||
|
if (raw == null || raw.isBlank()) {
|
||||||
|
throw new IllegalArgumentException("LLM output is empty");
|
||||||
|
}
|
||||||
|
String trimmed = raw.trim();
|
||||||
|
if (trimmed.startsWith("```")) {
|
||||||
|
int start = trimmed.indexOf('\n');
|
||||||
|
int end = trimmed.lastIndexOf("```");
|
||||||
|
if (start > 0 && end > start) {
|
||||||
|
trimmed = trimmed.substring(start, end).trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!trimmed.startsWith("{")) {
|
||||||
|
int brace = trimmed.indexOf('{');
|
||||||
|
if (brace >= 0) {
|
||||||
|
trimmed = trimmed.substring(brace);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!trimmed.endsWith("}")) {
|
||||||
|
int brace = trimmed.lastIndexOf('}');
|
||||||
|
if (brace >= 0) {
|
||||||
|
trimmed = trimmed.substring(0, brace + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return trimmed;
|
||||||
|
}
|
||||||
|
|
||||||
private String extractJsonArray(String raw) {
|
private String extractJsonArray(String raw) {
|
||||||
if (raw == null || raw.isBlank()) {
|
if (raw == null || raw.isBlank()) {
|
||||||
throw new IllegalArgumentException("LLM output is empty");
|
throw new IllegalArgumentException("LLM output is empty");
|
||||||
|
|||||||
@@ -380,7 +380,8 @@ class LLMControllerTest {
|
|||||||
List.of(
|
List.of(
|
||||||
new SuggestSectionsResponse.SectionSuggestion("Home", "Hero section", "home", List.of()),
|
new SuggestSectionsResponse.SectionSuggestion("Home", "Hero section", "home", List.of()),
|
||||||
new SuggestSectionsResponse.SectionSuggestion("About", "Bio", "about", List.of())
|
new SuggestSectionsResponse.SectionSuggestion("About", "Bio", "about", List.of())
|
||||||
)
|
),
|
||||||
|
List.of()
|
||||||
);
|
);
|
||||||
when(llmService.suggestSections(any(), anyString(), anyString(), any(), any(), any())).thenReturn(response);
|
when(llmService.suggestSections(any(), anyString(), anyString(), any(), any(), any())).thenReturn(response);
|
||||||
|
|
||||||
|
|||||||
@@ -502,7 +502,8 @@ class LLMServiceTest {
|
|||||||
@Test
|
@Test
|
||||||
void suggestSectionsReturnsParsedSuggestions() {
|
void suggestSectionsReturnsParsedSuggestions() {
|
||||||
geminiProvider.withContent("""
|
geminiProvider.withContent("""
|
||||||
[
|
{
|
||||||
|
"sections": [
|
||||||
{"title": "Home", "description": "Hero section", "slug": "home",
|
{"title": "Home", "description": "Hero section", "slug": "home",
|
||||||
"patterns": [
|
"patterns": [
|
||||||
{"title": "Full-screen hero", "description": "Large image with text overlay"},
|
{"title": "Full-screen hero", "description": "Large image with text overlay"},
|
||||||
@@ -513,7 +514,12 @@ class LLMServiceTest {
|
|||||||
{"title": "Timeline", "description": "Vertical story timeline"}
|
{"title": "Timeline", "description": "Vertical story timeline"}
|
||||||
]},
|
]},
|
||||||
{"title": "Gallery", "description": "Portfolio grid", "slug": "gallery", "patterns": []}
|
{"title": "Gallery", "description": "Portfolio grid", "slug": "gallery", "patterns": []}
|
||||||
|
],
|
||||||
|
"personalizedQuestions": [
|
||||||
|
{"id": "name", "question": "What is your name?", "type": "text"},
|
||||||
|
{"id": "bio", "question": "Write a short bio", "type": "textarea"}
|
||||||
]
|
]
|
||||||
|
}
|
||||||
""");
|
""");
|
||||||
|
|
||||||
SuggestSectionsResponse response = llmService.suggestSections(
|
SuggestSectionsResponse response = llmService.suggestSections(
|
||||||
@@ -527,6 +533,10 @@ class LLMServiceTest {
|
|||||||
assertEquals("Full-screen hero", response.sections().get(0).patterns().get(0).title());
|
assertEquals("Full-screen hero", response.sections().get(0).patterns().get(0).title());
|
||||||
assertEquals("Timeline", response.sections().get(1).patterns().get(0).title());
|
assertEquals("Timeline", response.sections().get(1).patterns().get(0).title());
|
||||||
assertEquals(0, response.sections().get(2).patterns().size());
|
assertEquals(0, response.sections().get(2).patterns().size());
|
||||||
|
|
||||||
|
assertEquals(2, response.personalizedQuestions().size());
|
||||||
|
assertEquals("name", response.personalizedQuestions().get(0).id());
|
||||||
|
assertEquals("textarea", response.personalizedQuestions().get(1).type());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -540,9 +550,12 @@ class LLMServiceTest {
|
|||||||
@Test
|
@Test
|
||||||
void suggestSectionsUsesProvidedApiKey() {
|
void suggestSectionsUsesProvidedApiKey() {
|
||||||
geminiProvider.withContent("""
|
geminiProvider.withContent("""
|
||||||
[
|
{
|
||||||
|
"sections": [
|
||||||
{"title": "Home", "description": "Hero", "slug": "home", "patterns": []}
|
{"title": "Home", "description": "Hero", "slug": "home", "patterns": []}
|
||||||
]
|
],
|
||||||
|
"personalizedQuestions": []
|
||||||
|
}
|
||||||
""");
|
""");
|
||||||
|
|
||||||
SuggestSectionsResponse response = llmService.suggestSections(
|
SuggestSectionsResponse response = llmService.suggestSections(
|
||||||
|
|||||||
@@ -93,8 +93,22 @@ export interface SuggestSectionsRequest {
|
|||||||
model?: string;
|
model?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PersonalizedQuestion {
|
||||||
|
id: string;
|
||||||
|
question: string;
|
||||||
|
type: string;
|
||||||
|
options?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface QuestionAnswer {
|
||||||
|
id: string;
|
||||||
|
question: string;
|
||||||
|
answer: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface SuggestSectionsResponse {
|
export interface SuggestSectionsResponse {
|
||||||
sections: SectionSuggestion[];
|
sections: SectionSuggestion[];
|
||||||
|
personalizedQuestions?: PersonalizedQuestion[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SelectedSection {
|
export interface SelectedSection {
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import { LlmApiService } from '../core/services/llm-api.service';
|
|||||||
import { LlmConfigService } from '../core/services/llm-config.service';
|
import { LlmConfigService } from '../core/services/llm-config.service';
|
||||||
import { LlmSessionService } from './llm-session.service';
|
import { LlmSessionService } from './llm-session.service';
|
||||||
import { GenerationService } from './generation.service';
|
import { GenerationService } from './generation.service';
|
||||||
import { SiteStructure, GenerationVersion, LLMKeyStatus, SectionSuggestion } from '../core/models/llm';
|
import { SiteStructure, GenerationVersion, LLMKeyStatus, SectionSuggestion, PersonalizedQuestion } from '../core/models/llm';
|
||||||
import { SectionConfiguratorComponent, SectionConfig } from './section-configurator/section-configurator.component';
|
import { SectionConfiguratorComponent, SectionConfig } from './section-configurator/section-configurator.component';
|
||||||
import { PreviewDialogComponent } from './preview-dialog/preview-dialog.component';
|
import { PreviewDialogComponent } from './preview-dialog/preview-dialog.component';
|
||||||
import { DeleteVersionDialog } from './delete-version-dialog/delete-version-dialog';
|
import { DeleteVersionDialog } from './delete-version-dialog/delete-version-dialog';
|
||||||
@@ -123,6 +123,7 @@ type Phase = 'idle' | 'suggesting' | 'configuring' | 'generating';
|
|||||||
|
|
||||||
<app-section-configurator
|
<app-section-configurator
|
||||||
[suggestions]="sectionSuggestions"
|
[suggestions]="sectionSuggestions"
|
||||||
|
[personalizedQuestions]="personalizedQuestions"
|
||||||
(configComplete)="onConfigComplete($event)"
|
(configComplete)="onConfigComplete($event)"
|
||||||
(stepChange)="onConfigStepChange($event)"
|
(stepChange)="onConfigStepChange($event)"
|
||||||
></app-section-configurator>
|
></app-section-configurator>
|
||||||
@@ -184,7 +185,7 @@ type Phase = 'idle' | 'suggesting' | 'configuring' | 'generating';
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div *ngIf="suggestError" class="bg-red-50 border border-red-200 rounded-lg p-4 flex items-start gap-3">
|
<div *ngIf="suggestError && !showMissingKeyForm" 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>
|
<mat-icon class="text-red-500 mt-0.5">error_outline</mat-icon>
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<p class="text-sm font-medium text-red-800">Suggestion failed</p>
|
<p class="text-sm font-medium text-red-800">Suggestion failed</p>
|
||||||
@@ -193,6 +194,52 @@ type Phase = 'idle' | 'suggesting' | 'configuring' | 'generating';
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<mat-card *ngIf="showMissingKeyForm" class="p-4" appearance="outlined">
|
||||||
|
<mat-card-content class="!p-0">
|
||||||
|
<div class="flex items-start gap-3 mb-4">
|
||||||
|
<mat-icon class="text-amber-500 mt-0.5">info</mat-icon>
|
||||||
|
<div>
|
||||||
|
<p class="text-sm font-medium text-amber-800">API key required for {{ setupProvider }}</p>
|
||||||
|
<p class="text-sm text-amber-700 mt-1">{{ suggestError }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-3">
|
||||||
|
<mat-form-field appearance="outline" class="w-full">
|
||||||
|
<mat-label>Provider</mat-label>
|
||||||
|
<mat-select [(ngModel)]="setupProvider">
|
||||||
|
<mat-option value="gemini">Gemini</mat-option>
|
||||||
|
<mat-option value="openai">OpenAI / Compatible</mat-option>
|
||||||
|
</mat-select>
|
||||||
|
</mat-form-field>
|
||||||
|
<mat-form-field appearance="outline" class="w-full">
|
||||||
|
<mat-label>API Key</mat-label>
|
||||||
|
<input matInput [type]="setupKeyVisible ? 'text' : 'password'" [(ngModel)]="setupApiKey" />
|
||||||
|
<button mat-icon-button matSuffix (click)="setupKeyVisible = !setupKeyVisible" type="button"
|
||||||
|
[attr.aria-label]="setupKeyVisible ? 'Hide key' : 'Show key'">
|
||||||
|
<mat-icon>{{ setupKeyVisible ? 'visibility_off' : 'visibility' }}</mat-icon>
|
||||||
|
</button>
|
||||||
|
</mat-form-field>
|
||||||
|
<mat-form-field appearance="outline" class="w-full" *ngIf="setupProvider === 'openai'">
|
||||||
|
<mat-label>Base URL (optional)</mat-label>
|
||||||
|
<input matInput placeholder="https://api.deepseek.com/v1" [(ngModel)]="setupBaseUrl" />
|
||||||
|
<mat-hint>For OpenAI-compatible providers like DeepSeek</mat-hint>
|
||||||
|
</mat-form-field>
|
||||||
|
<mat-form-field appearance="outline" class="w-full">
|
||||||
|
<mat-label>Model (optional)</mat-label>
|
||||||
|
<input matInput placeholder="{{ setupProvider === 'gemini' ? 'gemini-2.0-flash-exp' : 'gpt-4o-mini' }}" [(ngModel)]="setupModel" />
|
||||||
|
<mat-hint>Override the default model name</mat-hint>
|
||||||
|
</mat-form-field>
|
||||||
|
<div class="flex justify-end gap-2">
|
||||||
|
<button mat-button (click)="showMissingKeyForm = false; suggestError = null">Cancel</button>
|
||||||
|
<button mat-flat-button color="primary" [disabled]="!setupApiKey.trim() || setupSaving" (click)="saveSetupKey()">
|
||||||
|
<mat-icon>save</mat-icon>
|
||||||
|
{{ setupSaving ? 'Saving...' : 'Save' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</mat-card-content>
|
||||||
|
</mat-card>
|
||||||
|
|
||||||
<div *ngIf="session.error$ | async as error" class="bg-red-50 border border-red-200 rounded-lg p-4 flex items-start gap-3">
|
<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>
|
<mat-icon class="text-red-500 mt-0.5">error_outline</mat-icon>
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
@@ -398,6 +445,7 @@ export class LlmWorkspaceComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
briefDescription = '';
|
briefDescription = '';
|
||||||
sectionSuggestions: SectionSuggestion[] = [];
|
sectionSuggestions: SectionSuggestion[] = [];
|
||||||
|
personalizedQuestions: PersonalizedQuestion[] = [];
|
||||||
sectionConfig: SectionConfig | null = null;
|
sectionConfig: SectionConfig | null = null;
|
||||||
configStepTitle = 'Customize Your Site';
|
configStepTitle = 'Customize Your Site';
|
||||||
configStepSubtitle = '';
|
configStepSubtitle = '';
|
||||||
@@ -422,6 +470,8 @@ export class LlmWorkspaceComponent implements OnInit, OnDestroy {
|
|||||||
setupSaving = false;
|
setupSaving = false;
|
||||||
setupKeyVisible = false;
|
setupKeyVisible = false;
|
||||||
|
|
||||||
|
showMissingKeyForm = false;
|
||||||
|
|
||||||
private destroy$ = new Subject<void>();
|
private destroy$ = new Subject<void>();
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
@@ -441,6 +491,7 @@ export class LlmWorkspaceComponent implements OnInit, OnDestroy {
|
|||||||
this.showRefine = false;
|
this.showRefine = false;
|
||||||
this.currentPhase = 'idle';
|
this.currentPhase = 'idle';
|
||||||
this.sectionSuggestions = [];
|
this.sectionSuggestions = [];
|
||||||
|
this.personalizedQuestions = [];
|
||||||
this.sectionConfig = null;
|
this.sectionConfig = null;
|
||||||
this.suggestError = null;
|
this.suggestError = null;
|
||||||
this.session.clearHistory();
|
this.session.clearHistory();
|
||||||
@@ -519,12 +570,19 @@ export class LlmWorkspaceComponent implements OnInit, OnDestroy {
|
|||||||
}).pipe(takeUntil(this.destroy$)).subscribe({
|
}).pipe(takeUntil(this.destroy$)).subscribe({
|
||||||
next: (response) => {
|
next: (response) => {
|
||||||
this.sectionSuggestions = response.sections;
|
this.sectionSuggestions = response.sections;
|
||||||
|
this.personalizedQuestions = response.personalizedQuestions || [];
|
||||||
this.suggesting = false;
|
this.suggesting = false;
|
||||||
this.currentPhase = 'configuring';
|
this.currentPhase = 'configuring';
|
||||||
},
|
},
|
||||||
error: (err) => {
|
error: (err) => {
|
||||||
this.suggesting = false;
|
this.suggesting = false;
|
||||||
this.suggestError = err.error?.error || err.statusText || 'Failed to get section suggestions';
|
const errorMsg = err.error?.error || err.statusText || 'Failed to get section suggestions';
|
||||||
|
this.suggestError = errorMsg;
|
||||||
|
const match = errorMsg.match(/No API key configured for (\w+)/);
|
||||||
|
if (match) {
|
||||||
|
this.showMissingKeyForm = true;
|
||||||
|
this.setupProvider = match[1];
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -535,7 +593,13 @@ export class LlmWorkspaceComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onConfigStepChange(step: string): void {
|
onConfigStepChange(step: string): void {
|
||||||
if (step.startsWith('section-')) {
|
if (step === 'questions') {
|
||||||
|
this.configStepTitle = 'Tell Us About Your Content';
|
||||||
|
this.configStepSubtitle = 'Answer a few questions so the AI can generate authentic content.';
|
||||||
|
} else if (step === 'sections') {
|
||||||
|
this.configStepTitle = 'Choose Layout Patterns';
|
||||||
|
this.configStepSubtitle = 'Pick a layout for each section, or describe your own.';
|
||||||
|
} else if (step.startsWith('section-')) {
|
||||||
const idx = parseInt(step.split('-')[1], 10);
|
const idx = parseInt(step.split('-')[1], 10);
|
||||||
const section = this.sectionSuggestions[idx];
|
const section = this.sectionSuggestions[idx];
|
||||||
this.configStepTitle = section ? `Configure ${section.title}` : 'Configure Sections';
|
this.configStepTitle = section ? `Configure ${section.title}` : 'Configure Sections';
|
||||||
@@ -552,6 +616,7 @@ export class LlmWorkspaceComponent implements OnInit, OnDestroy {
|
|||||||
backToIdle(): void {
|
backToIdle(): void {
|
||||||
this.currentPhase = 'idle';
|
this.currentPhase = 'idle';
|
||||||
this.sectionSuggestions = [];
|
this.sectionSuggestions = [];
|
||||||
|
this.personalizedQuestions = [];
|
||||||
this.sectionConfig = null;
|
this.sectionConfig = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -586,6 +651,15 @@ export class LlmWorkspaceComponent implements OnInit, OnDestroy {
|
|||||||
lines.push(`Theme direction: ${config.themeVibe}`);
|
lines.push(`Theme direction: ${config.themeVibe}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const answeredQuestions = config.questionAnswers.filter(qa => qa.answer.trim());
|
||||||
|
if (answeredQuestions.length > 0) {
|
||||||
|
lines.push('');
|
||||||
|
lines.push('Site content details:');
|
||||||
|
for (const qa of answeredQuestions) {
|
||||||
|
lines.push(`- ${qa.question}: ${qa.answer.trim()}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
lines.push('');
|
lines.push('');
|
||||||
lines.push('Pages:');
|
lines.push('Pages:');
|
||||||
|
|
||||||
@@ -711,6 +785,7 @@ export class LlmWorkspaceComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
goToSettings(): void {
|
goToSettings(): void {
|
||||||
|
if (this.configuredProviders.length === 0) return;
|
||||||
if (this.siteId) this.router.navigate(['/llm', this.siteId, 'settings']);
|
if (this.siteId) this.router.navigate(['/llm', this.siteId, 'settings']);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -741,6 +816,8 @@ export class LlmWorkspaceComponent implements OnInit, OnDestroy {
|
|||||||
this.setupApiKey = '';
|
this.setupApiKey = '';
|
||||||
this.setupBaseUrl = '';
|
this.setupBaseUrl = '';
|
||||||
this.setupModel = '';
|
this.setupModel = '';
|
||||||
|
this.showMissingKeyForm = false;
|
||||||
|
this.suggestError = null;
|
||||||
this.snackBar.open(`${this.setupProvider} API key saved`, 'Close', { duration: 2000 });
|
this.snackBar.open(`${this.setupProvider} API key saved`, 'Close', { duration: 2000 });
|
||||||
this.loadLlmConfig();
|
this.loadLlmConfig();
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -6,12 +6,23 @@ import { MatIconModule } from '@angular/material/icon';
|
|||||||
import { MatInputModule } from '@angular/material/input';
|
import { MatInputModule } from '@angular/material/input';
|
||||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||||
import { MatSelectModule } from '@angular/material/select';
|
import { MatSelectModule } from '@angular/material/select';
|
||||||
import { SectionSuggestion, SelectedSection, PatternSuggestion } from '../../core/models/llm';
|
import { SectionSuggestion, PatternSuggestion, PersonalizedQuestion, QuestionAnswer } from '../../core/models/llm';
|
||||||
|
|
||||||
|
export interface SelectedSection {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
slug: string;
|
||||||
|
selected: boolean;
|
||||||
|
customNotes: string;
|
||||||
|
selectedPattern: string | null;
|
||||||
|
customPatternDesc: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface SectionConfig {
|
export interface SectionConfig {
|
||||||
sections: SelectedSection[];
|
sections: SelectedSection[];
|
||||||
themeVibe: string;
|
themeVibe: string;
|
||||||
additionalRequirements: string;
|
additionalRequirements: string;
|
||||||
|
questionAnswers: QuestionAnswer[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const THEME_VIBES = [
|
const THEME_VIBES = [
|
||||||
@@ -47,13 +58,47 @@ const THEME_VIBES = [
|
|||||||
`],
|
`],
|
||||||
template: `
|
template: `
|
||||||
<div class="flex items-center mb-6">
|
<div class="flex items-center mb-6">
|
||||||
<div class="crumb" [class.active]="phase === 'sections'" [class.completed]="phase !== 'sections'">Sections</div>
|
<div class="crumb" [class.active]="phase === 'questions'" [class.completed]="phase !== 'questions'">Content</div>
|
||||||
<div class="crumb-line" [class.completed]="phase !== 'sections'"></div>
|
<div class="crumb-line" [class.completed]="phase !== 'questions'"></div>
|
||||||
|
<div class="crumb" [class.active]="phase === 'sections'" [class.completed]="phase === 'theme' || phase === 'details'">Layout</div>
|
||||||
|
<div class="crumb-line" [class.completed]="phase === 'theme' || phase === 'details'"></div>
|
||||||
<div class="crumb" [class.active]="phase === 'theme'" [class.completed]="phase === 'details'">Theme</div>
|
<div class="crumb" [class.active]="phase === 'theme'" [class.completed]="phase === 'details'">Theme</div>
|
||||||
<div class="crumb-line" [class.completed]="phase === 'details'"></div>
|
<div class="crumb-line" [class.completed]="phase === 'details'"></div>
|
||||||
<div class="crumb" [class.active]="phase === 'details'">Details</div>
|
<div class="crumb" [class.active]="phase === 'details'">Details</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div *ngIf="phase === 'questions'" class="space-y-5">
|
||||||
|
<div>
|
||||||
|
<h4 class="text-base font-semibold text-gray-900 mb-1">Tell us about your content</h4>
|
||||||
|
<p class="text-sm text-gray-500 mb-4">Answer a few questions so the AI can generate authentic content for your site.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div *ngFor="let q of personalizedQuestions; let qi = index" class="space-y-1.5">
|
||||||
|
<label class="text-sm font-medium text-gray-700">{{ q.question }}</label>
|
||||||
|
<mat-form-field appearance="outline" class="w-full" *ngIf="q.type === 'textarea'">
|
||||||
|
<textarea
|
||||||
|
matInput
|
||||||
|
rows="3"
|
||||||
|
[placeholder]="'Enter your ' + q.id.replace('-', ' ')"
|
||||||
|
[(ngModel)]="questionAnswers[qi].answer"
|
||||||
|
></textarea>
|
||||||
|
</mat-form-field>
|
||||||
|
<mat-form-field appearance="outline" class="w-full" *ngIf="q.type === 'select'">
|
||||||
|
<mat-select [(ngModel)]="questionAnswers[qi].answer">
|
||||||
|
<mat-option value="">Select...</mat-option>
|
||||||
|
<mat-option *ngFor="let opt of q.options" [value]="opt">{{ opt }}</mat-option>
|
||||||
|
</mat-select>
|
||||||
|
</mat-form-field>
|
||||||
|
<mat-form-field appearance="outline" class="w-full" *ngIf="q.type === 'text' || (!q.type || q.type === '')">
|
||||||
|
<input
|
||||||
|
matInput
|
||||||
|
[placeholder]="'Enter your ' + q.id.replace('-', ' ')"
|
||||||
|
[(ngModel)]="questionAnswers[qi].answer"
|
||||||
|
/>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div *ngIf="phase === 'sections' && currentSection" class="space-y-4">
|
<div *ngIf="phase === 'sections' && currentSection" class="space-y-4">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
@@ -189,7 +234,7 @@ const THEME_VIBES = [
|
|||||||
<button
|
<button
|
||||||
mat-stroked-button
|
mat-stroked-button
|
||||||
(click)="prev()"
|
(click)="prev()"
|
||||||
[disabled]="phase === 'sections' && sectionIndex === 0"
|
[disabled]="phase === 'questions'"
|
||||||
>
|
>
|
||||||
<mat-icon>arrow_back</mat-icon>
|
<mat-icon>arrow_back</mat-icon>
|
||||||
Back
|
Back
|
||||||
@@ -213,12 +258,14 @@ const THEME_VIBES = [
|
|||||||
})
|
})
|
||||||
export class SectionConfiguratorComponent implements OnInit {
|
export class SectionConfiguratorComponent implements OnInit {
|
||||||
@Input() suggestions: SectionSuggestion[] = [];
|
@Input() suggestions: SectionSuggestion[] = [];
|
||||||
|
@Input() personalizedQuestions: PersonalizedQuestion[] = [];
|
||||||
@Output() configComplete = new EventEmitter<SectionConfig>();
|
@Output() configComplete = new EventEmitter<SectionConfig>();
|
||||||
@Output() stepChange = new EventEmitter<string>();
|
@Output() stepChange = new EventEmitter<string>();
|
||||||
|
|
||||||
phase: 'sections' | 'theme' | 'details' = 'sections';
|
phase: 'questions' | 'sections' | 'theme' | 'details' = 'questions';
|
||||||
sectionIndex = 0;
|
sectionIndex = 0;
|
||||||
sections: SelectedSection[] = [];
|
sections: SelectedSection[] = [];
|
||||||
|
questionAnswers: QuestionAnswer[] = [];
|
||||||
|
|
||||||
themeVibes = THEME_VIBES;
|
themeVibes = THEME_VIBES;
|
||||||
selectedVibe = 'Any';
|
selectedVibe = 'Any';
|
||||||
@@ -235,6 +282,14 @@ export class SectionConfiguratorComponent implements OnInit {
|
|||||||
selectedPattern: s.patterns?.length ? s.patterns[0].title : null,
|
selectedPattern: s.patterns?.length ? s.patterns[0].title : null,
|
||||||
customPatternDesc: '',
|
customPatternDesc: '',
|
||||||
}));
|
}));
|
||||||
|
this.questionAnswers = this.personalizedQuestions.map(q => ({
|
||||||
|
id: q.id,
|
||||||
|
question: q.question,
|
||||||
|
answer: '',
|
||||||
|
}));
|
||||||
|
if (this.personalizedQuestions.length === 0) {
|
||||||
|
this.phase = 'sections';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
get currentSection(): SelectedSection | null {
|
get currentSection(): SelectedSection | null {
|
||||||
@@ -260,6 +315,9 @@ export class SectionConfiguratorComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get navLabel(): string {
|
get navLabel(): string {
|
||||||
|
if (this.phase === 'questions') {
|
||||||
|
return `${this.personalizedQuestions.length} question${this.personalizedQuestions.length !== 1 ? 's' : ''}`;
|
||||||
|
}
|
||||||
if (this.phase === 'sections') {
|
if (this.phase === 'sections') {
|
||||||
return `Section ${this.sectionIndex + 1} of ${this.sections.length}`;
|
return `Section ${this.sectionIndex + 1} of ${this.sections.length}`;
|
||||||
}
|
}
|
||||||
@@ -284,10 +342,14 @@ export class SectionConfiguratorComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
prev(): void {
|
prev(): void {
|
||||||
if (this.phase === 'sections') {
|
if (this.phase === 'questions') {
|
||||||
|
} else if (this.phase === 'sections') {
|
||||||
if (this.sectionIndex > 0) {
|
if (this.sectionIndex > 0) {
|
||||||
this.sectionIndex--;
|
this.sectionIndex--;
|
||||||
this.stepChange.emit(`section-${this.sectionIndex}`);
|
this.stepChange.emit(`section-${this.sectionIndex}`);
|
||||||
|
} else {
|
||||||
|
this.phase = 'questions';
|
||||||
|
this.stepChange.emit('questions');
|
||||||
}
|
}
|
||||||
} else if (this.phase === 'theme') {
|
} else if (this.phase === 'theme') {
|
||||||
this.phase = 'sections';
|
this.phase = 'sections';
|
||||||
@@ -300,7 +362,11 @@ export class SectionConfiguratorComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
next(): void {
|
next(): void {
|
||||||
if (this.phase === 'sections') {
|
if (this.phase === 'questions') {
|
||||||
|
this.phase = 'sections';
|
||||||
|
this.sectionIndex = 0;
|
||||||
|
this.stepChange.emit('sections');
|
||||||
|
} else if (this.phase === 'sections') {
|
||||||
if (this.sectionIndex < this.sections.length - 1) {
|
if (this.sectionIndex < this.sections.length - 1) {
|
||||||
this.sectionIndex++;
|
this.sectionIndex++;
|
||||||
this.stepChange.emit(`section-${this.sectionIndex}`);
|
this.stepChange.emit(`section-${this.sectionIndex}`);
|
||||||
@@ -319,6 +385,7 @@ export class SectionConfiguratorComponent implements OnInit {
|
|||||||
sections: this.sections,
|
sections: this.sections,
|
||||||
themeVibe: vibe,
|
themeVibe: vibe,
|
||||||
additionalRequirements: this.additionalRequirements,
|
additionalRequirements: this.additionalRequirements,
|
||||||
|
questionAnswers: this.questionAnswers,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user