refactor git provider and site structures

This commit is contained in:
Krrish Ghimire
2026-07-05 23:40:37 +05:45
parent bfc61ed660
commit 874eb33678
17 changed files with 631 additions and 679 deletions

View File

@@ -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';
}

View File

@@ -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 {

View File

@@ -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' : '' }}
&middot;
{{ 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 }}
&middot;
{{ 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;
}

View File

@@ -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);
}
}

View File

@@ -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>