refactor git provider and site structures
This commit is contained in:
@@ -1,76 +0,0 @@
|
||||
export type ComponentType =
|
||||
| 'HEADING' | 'TEXT' | 'IMAGE' | 'BUTTON' | 'DIVIDER'
|
||||
| 'GALLERY' | 'VIDEO' | 'CONTACT_FORM' | 'MAP' | 'CUSTOM_HTML';
|
||||
|
||||
export interface ComponentItem {
|
||||
id: string;
|
||||
pageId?: string;
|
||||
type: ComponentType;
|
||||
config: Record<string, unknown>;
|
||||
styles: Record<string, unknown>;
|
||||
orderIndex: number;
|
||||
}
|
||||
|
||||
export function getDefaultConfig(type: ComponentType): Record<string, unknown> {
|
||||
switch (type) {
|
||||
case 'HEADING': return { text: 'Heading', level: 'h2' };
|
||||
case 'TEXT': return { content: 'Text content here...' };
|
||||
case 'IMAGE': return { src: '', alt: 'Image', linkTo: '' };
|
||||
case 'BUTTON': return { text: 'Click Me', link: '#', variant: 'filled' };
|
||||
case 'DIVIDER': return { style: 'solid' };
|
||||
case 'GALLERY': return { images: [] };
|
||||
case 'VIDEO': return { src: '', autoplay: false, controls: true };
|
||||
case 'CONTACT_FORM': return { fields: ['name', 'email', 'message'], submitText: 'Send', emailTo: '' };
|
||||
case 'MAP': return { address: 'New York, NY', zoom: 14 };
|
||||
case 'CUSTOM_HTML': return { html: '<p>Custom HTML</p>' };
|
||||
}
|
||||
}
|
||||
|
||||
export function getDefaultStyles(type: ComponentType): Record<string, unknown> {
|
||||
switch (type) {
|
||||
case 'HEADING': return { color: '#1a1a1a', fontSize: '2rem', fontWeight: '700', textAlign: 'left', margin: '0 0 1rem' };
|
||||
case 'TEXT': return { color: '#4a4a4a', fontSize: '1rem', lineHeight: '1.6', textAlign: 'left' };
|
||||
case 'IMAGE': return { width: '100%', height: 'auto', borderRadius: '0', objectFit: 'cover' };
|
||||
case 'BUTTON': return { backgroundColor: '#1976d2', color: '#ffffff', borderRadius: '4px', padding: '0.75rem 1.5rem' };
|
||||
case 'DIVIDER': return { color: '#e0e0e0', thickness: '1px', margin: '1.5rem 0' };
|
||||
case 'GALLERY': return { columns: '3', gap: '8px', borderRadius: '4px' };
|
||||
case 'VIDEO': return { width: '100%', maxWidth: '800px', borderRadius: '4px' };
|
||||
case 'CONTACT_FORM': return { backgroundColor: '#f5f5f5', padding: '2rem', borderRadius: '8px' };
|
||||
case 'MAP': return { width: '100%', height: '400px', borderRadius: '4px' };
|
||||
case 'CUSTOM_HTML': return { padding: '0', margin: '0' };
|
||||
}
|
||||
}
|
||||
|
||||
const LABELS: Record<string, string> = {
|
||||
HEADING: 'Heading',
|
||||
TEXT: 'Text',
|
||||
IMAGE: 'Image',
|
||||
BUTTON: 'Button',
|
||||
DIVIDER: 'Divider',
|
||||
GALLERY: 'Gallery',
|
||||
VIDEO: 'Video',
|
||||
CONTACT_FORM: 'Contact Form',
|
||||
MAP: 'Map',
|
||||
CUSTOM_HTML: 'Custom HTML',
|
||||
};
|
||||
|
||||
const ICONS: Record<string, string> = {
|
||||
HEADING: 'title',
|
||||
TEXT: 'text_fields',
|
||||
IMAGE: 'image',
|
||||
BUTTON: 'smart_button',
|
||||
DIVIDER: 'horizontal_rule',
|
||||
GALLERY: 'photo_library',
|
||||
VIDEO: 'videocam',
|
||||
CONTACT_FORM: 'contact_mail',
|
||||
MAP: 'map',
|
||||
CUSTOM_HTML: 'code',
|
||||
};
|
||||
|
||||
export function getComponentLabel(type: string): string {
|
||||
return LABELS[type] || LABELS[type.toUpperCase()] || type;
|
||||
}
|
||||
|
||||
export function getComponentIcon(type: string): string {
|
||||
return ICONS[type] || ICONS[type.toUpperCase()] || 'code';
|
||||
}
|
||||
@@ -1,5 +1,3 @@
|
||||
import { ComponentType } from './component';
|
||||
|
||||
export interface LLMGenerateRequest {
|
||||
prompt: string;
|
||||
siteId: string;
|
||||
@@ -37,15 +35,7 @@ export interface PageStructure {
|
||||
seoTitle?: string;
|
||||
seoDescription?: string;
|
||||
orderIndex: number;
|
||||
components: ComponentStructure[];
|
||||
}
|
||||
|
||||
export interface ComponentStructure {
|
||||
id?: string;
|
||||
type: ComponentType;
|
||||
orderIndex: number;
|
||||
config: Record<string, unknown>;
|
||||
styles: Record<string, unknown>;
|
||||
rawHtml?: string;
|
||||
}
|
||||
|
||||
export interface TokenUsage {
|
||||
|
||||
@@ -18,7 +18,6 @@ import { takeUntil } from 'rxjs/operators';
|
||||
import { LlmApiService } from '../core/services/llm-api.service';
|
||||
import { LlmSessionService } from './llm-session.service';
|
||||
import { SiteStructure, GenerationVersion } from '../core/models/llm';
|
||||
import { getComponentLabel } from '../core/models/component';
|
||||
import { PreviewDialogComponent } from './preview-dialog/preview-dialog.component';
|
||||
import { DeleteVersionDialog } from './delete-version-dialog/delete-version-dialog';
|
||||
|
||||
@@ -146,8 +145,6 @@ import { DeleteVersionDialog } from './delete-version-dialog/delete-version-dial
|
||||
<h3 class="text-lg font-semibold text-gray-900">Generated Site Structure</h3>
|
||||
<p class="text-sm text-gray-500">
|
||||
{{ result.pages.length }} page{{ result.pages.length !== 1 ? 's' : '' }}
|
||||
·
|
||||
{{ totalComponents(result) }} component{{ totalComponents(result) !== 1 ? 's' : '' }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
@@ -236,14 +233,12 @@ import { DeleteVersionDialog } from './delete-version-dialog/delete-version-dial
|
||||
<mat-panel-description class="text-xs text-gray-400">
|
||||
/{{ page.slug }}
|
||||
·
|
||||
{{ page.components.length }} component{{ page.components.length !== 1 ? 's' : '' }}
|
||||
Raw HTML
|
||||
</mat-panel-description>
|
||||
</mat-expansion-panel-header>
|
||||
<div class="space-y-1">
|
||||
<div *ngFor="let comp of page.components" class="flex items-center gap-2 py-1">
|
||||
<mat-icon class="text-gray-400 text-lg">circle</mat-icon>
|
||||
<span class="text-sm text-gray-700">{{ getLabel(comp.type) }}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 py-1">
|
||||
<mat-icon class="text-gray-400 text-lg">code</mat-icon>
|
||||
<span class="text-sm text-gray-400 italic">Raw HTML content</span>
|
||||
</div>
|
||||
</mat-expansion-panel>
|
||||
</mat-accordion>
|
||||
@@ -446,14 +441,6 @@ export class LlmWorkspaceComponent implements OnInit, OnDestroy {
|
||||
if (this.siteId) this.router.navigate(['/llm', this.siteId, 'settings']);
|
||||
}
|
||||
|
||||
totalComponents(structure: SiteStructure): number {
|
||||
return structure.pages.reduce((sum, p) => sum + p.components.length, 0);
|
||||
}
|
||||
|
||||
getLabel(type: string): string {
|
||||
return getComponentLabel(type);
|
||||
}
|
||||
|
||||
isCurrentVersion(versionId: string): boolean {
|
||||
return this.session.currentVersionId === versionId;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatToolbarModule } from '@angular/material/toolbar';
|
||||
import { NgIf, NgFor, AsyncPipe } from '@angular/common';
|
||||
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
|
||||
import { SiteStructure, PageStructure, ComponentStructure } from '../../core/models/llm';
|
||||
import { SiteStructure } from '../../core/models/llm';
|
||||
|
||||
type Device = 'desktop' | 'tablet' | 'mobile';
|
||||
|
||||
@@ -93,23 +93,6 @@ export class PreviewDialogComponent {
|
||||
+ Object.entries(fonts).map(([k, v]) => ` --font-${k}: ${v};`).join('\n')
|
||||
+ Object.entries(spacing).map(([k, v]) => ` --spacing-${k}: ${v};`).join('\n');
|
||||
|
||||
const sections: { bg: string; indices: number[] }[] = [];
|
||||
for (let i = 0; i < page.components.length; i++) {
|
||||
const c = page.components[i];
|
||||
const bg = String((c.styles && c.styles['backgroundColor']) || '');
|
||||
if (bg || i === 0) {
|
||||
sections.push({ bg, indices: [i] });
|
||||
} else {
|
||||
sections[sections.length - 1].indices.push(i);
|
||||
}
|
||||
}
|
||||
const lastIndices = new Set(sections.map(s => s.indices[s.indices.length - 1]));
|
||||
const sectionsHtml = sections.map(s => {
|
||||
const bgStyle = s.bg ? ` style="background:${s.bg}"` : '';
|
||||
const inner = s.indices.map(idx => this.renderComponent(page.components[idx], idx)).join('\n');
|
||||
return `<section${bgStyle}>\n <div class="container">\n${inner}\n </div>\n</section>`;
|
||||
}).join('\n');
|
||||
|
||||
return `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
@@ -129,128 +112,11 @@ ${cssVars}
|
||||
line-height: 1.6;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
.container { max-width: var(--spacing-containerWidth, 1100px); margin: 0 auto; padding: 0 1.5rem; }
|
||||
section { padding: var(--spacing-sectionPadding, 3rem) 0; }
|
||||
${this.buildPageStyles(page.components, lastIndices)}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
${sectionsHtml}
|
||||
${page.rawHtml || ''}
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
|
||||
private buildPageStyles(components: ComponentStructure[], lastIndices: Set<number>): string {
|
||||
return components.map((c, i) => {
|
||||
const s = c.styles || {};
|
||||
let entries = Object.entries(s).filter(([k]) => k !== 'backgroundColor');
|
||||
if (lastIndices.has(i)) {
|
||||
entries = entries.filter(([k]) => k !== 'marginBottom');
|
||||
}
|
||||
if (entries.length === 0) return '';
|
||||
return `.comp-${i} {\n${entries.map(([k, v]) => ` ${this.cssProperty(k)}: ${v};`).join('\n')}\n}`;
|
||||
}).filter(Boolean).join('\n');
|
||||
}
|
||||
|
||||
private renderComponent(c: ComponentStructure, i?: number): string {
|
||||
const idx = i ?? 0;
|
||||
const config = c.config || {};
|
||||
const type = (c.type || '').toUpperCase();
|
||||
const cls = `comp-${idx}`;
|
||||
|
||||
switch (type) {
|
||||
case 'HEADING': {
|
||||
const level = (config['level'] as string) || 'h2';
|
||||
const text = (config['text'] as string) || 'Heading';
|
||||
const tag = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(level) ? level : 'h2';
|
||||
return `<${tag} class="${cls}">${this.esc(text)}</${tag}>`;
|
||||
}
|
||||
case 'TEXT': {
|
||||
const content = (config['content'] as string) || '';
|
||||
return `<div class="${cls}">${content}</div>`;
|
||||
}
|
||||
case 'IMAGE': {
|
||||
const src = (config['src'] as string) || '';
|
||||
const alt = (config['alt'] as string) || '';
|
||||
const caption = (config['caption'] as string) || '';
|
||||
const linkTo = (config['linkTo'] as string) || '';
|
||||
const img = `<img src="${this.esc(src)}" alt="${this.esc(alt)}" class="${cls}" style="max-width:100%;height:auto">`;
|
||||
const imgHtml = linkTo ? `<a href="${this.esc(linkTo)}">${img}</a>` : img;
|
||||
return caption ? `<figure style="margin:0">${imgHtml}<figcaption style="text-align:center;font-size:0.875rem;color:#6b7280;margin-top:0.5rem">${this.esc(caption)}</figcaption></figure>` : imgHtml;
|
||||
}
|
||||
case 'BUTTON': {
|
||||
const text = (config['text'] as string) || 'Button';
|
||||
const link = (config['link'] as string) || '#';
|
||||
const variant = (config['variant'] as string) || 'primary';
|
||||
let btnStyle = 'display:inline-block;text-decoration:none;font-weight:500;font-size:1rem;padding:0.75rem 2rem;border-radius:8px;transition:all 0.2s;cursor:pointer;border:2px solid transparent';
|
||||
if (variant === 'outline') {
|
||||
btnStyle += `;background:transparent;color:var(--color-primary,#4f46e5);border-color:var(--color-primary,#4f46e5)`;
|
||||
} else if (variant === 'secondary') {
|
||||
btnStyle += `;background:var(--color-secondary,#6366f1);color:#ffffff`;
|
||||
} else {
|
||||
btnStyle += `;background:var(--color-primary,#4f46e5);color:#ffffff`;
|
||||
}
|
||||
return `<a href="${this.esc(link)}" class="${cls}" style="${btnStyle}">${this.esc(text)}</a>`;
|
||||
}
|
||||
case 'DIVIDER': {
|
||||
const style = (config['style'] as string) || 'solid';
|
||||
const color = (config['color'] as string) || 'var(--color-text, #d1d5db)';
|
||||
return `<hr class="${cls}" style="border:none;border-top:1px ${style} ${color}">`;
|
||||
}
|
||||
case 'GALLERY': {
|
||||
const images = (config['images'] as string[]) || [];
|
||||
const columns = Math.min(Math.max((config['columns'] as number) || 3, 1), 6);
|
||||
if (images.length === 0) return `<div class="${cls}">Gallery (no images)</div>`;
|
||||
return `<div class="${cls}" style="display:grid;grid-template-columns:repeat(${columns},1fr);gap:1rem">
|
||||
${images.map(src => `<img src="${this.esc(src)}" style="width:100%;height:250px;object-fit:cover;border-radius:8px">`).join('\n')}
|
||||
</div>`;
|
||||
}
|
||||
case 'VIDEO': {
|
||||
const src = (config['src'] as string) || '';
|
||||
const controls = config['controls'] !== false;
|
||||
const autoplay = config['autoplay'] === true;
|
||||
return `<video src="${this.esc(src)}" class="${cls}" ${controls ? 'controls' : ''} ${autoplay ? 'autoplay' : ''} muted style="max-width:100%"></video>`;
|
||||
}
|
||||
case 'CONTACT_FORM': {
|
||||
const fields = (config['fields'] as string[]) || ['name', 'email', 'message'];
|
||||
const submitText = (config['submitText'] as string) || 'Send';
|
||||
return `<form class="${cls}">
|
||||
${fields.map(f => `
|
||||
<div style="margin-bottom:1rem">
|
||||
<label style="display:block;font-size:0.875rem;font-weight:600;margin-bottom:0.375rem;color:var(--color-text,#374151)">${this.esc(this.capitalize(f))}</label>
|
||||
${f === 'message' ? `<textarea rows="4" style="width:100%;padding:0.75rem;border:1px solid #d1d5db;border-radius:8px;font-family:inherit;font-size:0.95rem;transition:border-color 0.2s;outline:none" onfocus="this.style.borderColor='var(--color-primary,#4f46e5)'" onblur="this.style.borderColor='#d1d5db'"></textarea>`
|
||||
: `<input type="${f === 'email' ? 'email' : 'text'}" style="width:100%;padding:0.75rem;border:1px solid #d1d5db;border-radius:8px;font-family:inherit;font-size:0.95rem;transition:border-color 0.2s;outline:none" onfocus="this.style.borderColor='var(--color-primary,#4f46e5)'" onblur="this.style.borderColor='#d1d5db'">`}
|
||||
</div>
|
||||
`).join('')}
|
||||
<button type="submit" style="padding:0.75rem 2rem;background:var(--color-primary,#4f46e5);color:#fff;border:none;border-radius:8px;cursor:pointer;font-size:1rem;font-weight:500;transition:opacity 0.2s" onmouseover="this.style.opacity='0.9'" onmouseout="this.style.opacity='1'">${this.esc(submitText)}</button>
|
||||
</form>`;
|
||||
}
|
||||
case 'MAP': {
|
||||
const address = (config['address'] as string) || 'New York, NY';
|
||||
return `<div class="${cls}" style="display:flex;align-items:center;justify-content:center;background:#f3f4f6">
|
||||
<span style="color:#9ca3af">Map: ${this.esc(address)}</span>
|
||||
</div>`;
|
||||
}
|
||||
case 'CUSTOM_HTML': {
|
||||
const html = (config['html'] as string) || '';
|
||||
return `<div class="${cls}">${html}</div>`;
|
||||
}
|
||||
default:
|
||||
return `<div class="${cls}">[${this.esc(type)}]</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
private cssProperty(key: string): string {
|
||||
return key.replace(/([A-Z])/g, '-$1').toLowerCase();
|
||||
}
|
||||
|
||||
private esc(s: string): string {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = s;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
private capitalize(s: string): string {
|
||||
return s.charAt(0).toUpperCase() + s.slice(1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,11 +98,11 @@ import { SiteResponse } from '../../core/models/site';
|
||||
Your site is live at <a [href]="liveUrl" target="_blank" class="text-blue-600 underline">{{ liveUrl }}</a>
|
||||
</p>
|
||||
<div class="flex gap-3" *ngIf="!publishing">
|
||||
<button mat-flat-button color="primary" *ngIf="siteStatus === 'DRAFT'" [disabled]="publishing" (click)="publishSite()">
|
||||
<button mat-flat-button color="primary" (click)="publishSite()">
|
||||
<mat-icon>publish</mat-icon>
|
||||
Publish
|
||||
{{ siteStatus === 'PUBLISHED' ? 'Update' : 'Publish' }}
|
||||
</button>
|
||||
<button mat-stroked-button color="warn" *ngIf="siteStatus === 'PUBLISHED'" [disabled]="publishing" (click)="unpublishSite()">
|
||||
<button mat-stroked-button color="warn" *ngIf="siteStatus === 'PUBLISHED'" (click)="unpublishSite()">
|
||||
<mat-icon>unpublish</mat-icon>
|
||||
Unpublish
|
||||
</button>
|
||||
|
||||
Reference in New Issue
Block a user