class EmbedPreview {
constructor() {
this.params = this.parseURLParams();
this.config = this.getInitialConfig();
this.init();
}
parseURLParams() {
const urlParams = new URLSearchParams(window.location.search);
const params = {};
for (const [key, value] of urlParams.entries()) {
params[key] = decodeURIComponent(value);
}
return params;
}
getInitialConfig() {
return {
prompt: this.params.prompt || '',
context: this.params.context ? this.params.context.split(',').map(c => c.trim()) : [],
model: this.params.model || 'gpt-4o',
mode: this.params.agentMode || 'chat',
thinking: this.params.thinking === 'true',
max: this.params.max === 'true',
lightColor: this.params.lightColor || '#3b82f6',
darkColor: this.params.darkColor || '#60a5fa',
themeMode: this.params.themeMode || 'auto'
};
}
init() {
this.setupColors();
this.setupElements();
this.render();
}
setupColors() {
const root = document.documentElement;
// Determine if we should use dark mode
let isDarkMode;
if (this.config.themeMode === 'light') {
isDarkMode = false;
} else if (this.config.themeMode === 'dark') {
isDarkMode = true;
} else {
// Auto mode - use system preference
isDarkMode = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
}
const baseColor = isDarkMode ? this.config.darkColor : this.config.lightColor;
// Convert hex to RGB
const hexToRgb = (hex) => {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result ? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16)
} : null;
};
// Convert RGB to HSL
const rgbToHsl = (r, g, b) => {
r /= 255;
g /= 255;
b /= 255;
const max = Math.max(r, g, b);
const min = Math.min(r, g, b);
let h, s, l = (max + min) / 2;
if (max === min) {
h = s = 0;
} else {
const d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch (max) {
case r: h = ((g - b) / d + (g < b ? 6 : 0)) / 6; break;
case g: h = ((b - r) / d + 2) / 6; break;
case b: h = ((r - g) / d + 4) / 6; break;
}
}
return { h: h * 360, s: s * 100, l: l * 100 };
};
// Convert HSL to RGB
const hslToRgb = (h, s, l) => {
h /= 360;
s /= 100;
l /= 100;
let r, g, b;
if (s === 0) {
r = g = b = l;
} else {
const hue2rgb = (p, q, t) => {
if (t < 0) t += 1;
if (t > 1) t -= 1;
if (t < 1/6) return p + (q - p) * 6 * t;
if (t < 1/2) return q;
if (t < 2/3) return p + (q - p) * (2/3 - t) * 6;
return p;
};
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
const p = 2 * l - q;
r = hue2rgb(p, q, h + 1/3);
g = hue2rgb(p, q, h);
b = hue2rgb(p, q, h - 1/3);
}
return {
r: Math.round(r * 255),
g: Math.round(g * 255),
b: Math.round(b * 255)
};
};
const rgb = hexToRgb(baseColor);
if (rgb) {
// Get HSL values for color manipulation
const hsl = rgbToHsl(rgb.r, rgb.g, rgb.b);
// Set primary color
root.style.setProperty('--primary', `${rgb.r} ${rgb.g} ${rgb.b}`);
// Generate color scheme based on primary color
if (isDarkMode) {
// Dark mode: darker backgrounds, lighter text
const bgHsl = { ...hsl, s: Math.min(hsl.s * 0.15, 20), l: 8 };
const bg = hslToRgb(bgHsl.h, bgHsl.s, bgHsl.l);
const mutedHsl = { ...hsl, s: Math.min(hsl.s * 0.2, 25), l: 15 };
const muted = hslToRgb(mutedHsl.h, mutedHsl.s, mutedHsl.l);
const borderHsl = { ...hsl, s: Math.min(hsl.s * 0.25, 30), l: 20 };
const border = hslToRgb(borderHsl.h, borderHsl.s, borderHsl.l);
root.style.setProperty('--background', `${bg.r} ${bg.g} ${bg.b}`);
root.style.setProperty('--foreground', '248 250 252');
root.style.setProperty('--muted', `${muted.r} ${muted.g} ${muted.b}`);
root.style.setProperty('--muted-foreground', '148 163 184');
root.style.setProperty('--border', `${border.r} ${border.g} ${border.b}`);
} else {
// Light mode: lighter backgrounds, darker text
const bgHsl = { ...hsl, s: Math.min(hsl.s * 0.05, 5), l: 99 };
const bg = hslToRgb(bgHsl.h, bgHsl.s, bgHsl.l);
const mutedHsl = { ...hsl, s: Math.min(hsl.s * 0.1, 10), l: 97 };
const muted = hslToRgb(mutedHsl.h, mutedHsl.s, mutedHsl.l);
const borderHsl = { ...hsl, s: Math.min(hsl.s * 0.15, 15), l: 92 };
const border = hslToRgb(borderHsl.h, borderHsl.s, borderHsl.l);
root.style.setProperty('--background', `${bg.r} ${bg.g} ${bg.b}`);
root.style.setProperty('--foreground', '15 23 42');
root.style.setProperty('--muted', `${muted.r} ${muted.g} ${muted.b}`);
root.style.setProperty('--muted-foreground', '100 116 139');
root.style.setProperty('--border', `${border.r} ${border.g} ${border.b}`);
}
// Set accent (slightly different from primary)
const accentHsl = { ...hsl, l: Math.min(hsl.l + 10, 90) };
const accent = hslToRgb(accentHsl.h, accentHsl.s, accentHsl.l);
root.style.setProperty('--accent', `${accent.r} ${accent.g} ${accent.b}`);
}
this.isDarkMode = isDarkMode;
}
setupElements() {
const copyButton = document.getElementById('copy-button');
if (copyButton) {
copyButton.addEventListener('click', () => this.handleCopy());
}
const downloadButton = document.getElementById('download-button');
if (downloadButton) {
downloadButton.addEventListener('click', () => this.handleDownload());
}
}
render() {
this.renderContextPills();
this.renderPromptText();
this.renderSettingsPills();
}
renderContextPills() {
const container = document.getElementById('context-pills');
if (!container) return;
container.innerHTML = '';
this.config.context.forEach(context => {
const pill = this.createContextPill(context);
container.appendChild(pill);
});
}
createContextPill(context) {
const pill = document.createElement('div');
pill.className = 'px-2 py-0.5 rounded-lg text-[0.65rem] font-medium animate-slide-in flex items-center gap-1';
let icon = '';
// Use dynamic color classes for all pills
if (this.isDarkMode) {
pill.className += ' bg-dynamic-primary/10 text-dynamic-foreground border border-dynamic-primary/30';
} else {
pill.className += ' bg-dynamic-primary/0 text-dynamic-foreground border border-dynamic-primary/20';
}
if (context.startsWith('@')) {
// @mentions - just show the text
pill.innerHTML = '' + context + '';
} else if (context.startsWith('http://') || context.startsWith('https://')) {
// Web URLs show world icon
icon = '';
pill.innerHTML = icon + '' + context + '';
} else if (context.startsWith('#')) {
// Any hashtag context shows image icon
icon = '';
// Remove hash from display
pill.innerHTML = icon + '' + context.substring(1) + '';
} else if (context.includes('.')) {
// File context (contains a dot)
icon = '';
pill.innerHTML = icon + '' + context + '';
} else {
// Generic context
pill.innerHTML = '' + context + '';
}
return pill;
}
renderPromptText() {
const promptText = document.getElementById('prompt-text');
const placeholder = document.getElementById('prompt-placeholder');
if (!promptText || !placeholder) return;
if (this.config.prompt) {
promptText.innerHTML = this.highlightMentions(this.config.prompt);
placeholder.style.display = 'none';
} else {
promptText.innerHTML = '';
placeholder.style.display = 'block';
}
}
renderSettingsPills() {
const container = document.getElementById('settings-pills');
if (!container) return;
container.innerHTML = '';
// Mode pill (first)
const modePill = this.createSettingPill(this.capitalizeFirst(this.config.mode), 'mode');
container.appendChild(modePill);
// Model pill (second)
const modelPill = this.createSettingPill(this.config.model, 'model');
container.appendChild(modelPill);
// Thinking pill (if enabled)
if (this.config.thinking) {
const thinkingPill = this.createSettingPill('Thinking', 'thinking');
container.appendChild(thinkingPill);
}
// MAX pill (if enabled)
if (this.config.max) {
const maxPill = this.createSettingPill('MAX', 'max');
container.appendChild(maxPill);
}
}
createSettingPill(text, type) {
const pill = document.createElement('div');
pill.className = 'rounded-full text-xs font-medium flex items-center gap-1.5';
let icon = '';
// Use different styling based on pill type
if (type === 'mode') {
// Mode pill keeps the background
if (this.isDarkMode) {
pill.className += ' px-3 py-2 bg-dynamic-primary/20 text-dynamic-foreground border border-dynamic-primary/30';
} else {
pill.className += ' px-3 py-2 bg-dynamic-primary/10 text-dynamic-foreground border border-dynamic-primary/20';
}
} else {
// Model, thinking, and max pills only have text color
pill.className += ' pl-1 text-dynamic-primary';
}
switch (type) {
case 'model':
pill.innerHTML = '' + text + '';
break;
case 'mode':
// Add icon based on mode type
const modeType = text.toLowerCase();
switch (modeType) {
case 'agent':
icon = '';
break;
case 'chat':
icon = '';
break;
case 'manual':
icon = '';
break;
case 'cloud':
icon = '';
break;
default:
icon = '';
}
pill.innerHTML = icon + '' + text + '';
break;
case 'thinking':
// Brain icon for thinking mode
icon = '';
pill.innerHTML = icon + '' + text + '';
break;
case 'max':
// Lightning bolt icon for MAX mode
icon = '';
pill.innerHTML = icon + '' + text + '';
break;
}
return pill;
}
highlightMentions(text) {
return text.replace(/@(\w+)/g, '@$1');
}
capitalizeFirst(str) {
return str.charAt(0).toUpperCase() + str.slice(1);
}
async handleCopy() {
if (!this.config.prompt) {
this.showNotification('No prompt to copy');
return;
}
await this.copyToClipboard(this.config.prompt);
this.showNotification('Prompt copied to clipboard!');
}
async copyToClipboard(text) {
if (navigator.clipboard && window.isSecureContext) {
await navigator.clipboard.writeText(text);
} else {
// Fallback
const textArea = document.createElement('textarea');
textArea.value = text;
textArea.style.position = 'fixed';
textArea.style.left = '-999999px';
textArea.style.top = '-999999px';
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
document.execCommand('copy');
textArea.remove();
}
}
async handleDownload() {
try {
// Get computed styles
const computedStyles = getComputedStyle(document.documentElement);
const bgColor = `rgb(${computedStyles.getPropertyValue('--background').trim()})`;
const fgColor = `rgb(${computedStyles.getPropertyValue('--foreground').trim()})`;
const mutedColor = `rgb(${computedStyles.getPropertyValue('--muted').trim()})`;
const mutedFgColor = `rgb(${computedStyles.getPropertyValue('--muted-foreground').trim()})`;
const primaryColor = `rgb(${computedStyles.getPropertyValue('--primary').trim()})`;
const borderColor = `rgb(${computedStyles.getPropertyValue('--border').trim()})`;
// Create SVG representation
const { svg, scale } = this.createSVG({
bgColor,
fgColor,
mutedColor,
mutedFgColor,
primaryColor,
borderColor
});
// Convert SVG to PNG and download (scale will make it retina-compatible)
await this.downloadSVGasPNG(svg, 'prompt-preview.png', scale);
this.showNotification('Downloaded as PNG!');
} catch (error) {
console.error('Download failed:', error);
this.showNotification('Download failed');
}
}
createSVG(colors) {
const scale = 2; // 2x for retina displays
const width = 800 * scale;
const padding = 16 * scale; // Match sm:p-4 (1rem = 16px)
const mobileGap = 4 * scale; // Match gap-1 on mobile
const desktopGap = 8 * scale; // Match sm:gap-2
// Start calculating height dynamically
let yOffset = padding;
let svgContent = '';
// Context pills area with min-height of 25px
const contextPills = document.getElementById('context-pills');
const contextPillsHeight = 25 * scale; // min-h-[25px]
const contextPillsMargin = 8 * scale; // mb-0 sm:mb-2 (using desktop margin)
if (contextPills && contextPills.children.length > 0) {
let xOffset = padding;
const pillHeight = 20 * scale; // Smaller pills in context area
const pillPadding = 8 * scale;
const fontSize = 10.4 * scale;
for (const pill of contextPills.children) {
const pillText = pill.textContent.trim();
const pillWidth = Math.max(60 * scale, pillText.length * 7 * scale + pillPadding * 2);
// Pill background with proper opacity
const pillY = yOffset + (contextPillsHeight - pillHeight) / 2;
svgContent += ``;
// Pill text - fix vertical alignment using dominant-baseline
const textY = pillY + pillHeight / 2;
svgContent += `${this.escapeXML(pillText)}`;
xOffset += pillWidth + desktopGap;
}
}
yOffset += contextPillsHeight + contextPillsMargin;
// Main prompt container - calculate needed height
const promptText = this.config.prompt || '← Enter your prompt on designer...';
const isPlaceholder = !this.config.prompt;
const containerPadding = 24 * scale; // p-3 sm:p-6 (using desktop padding)
const lineHeight = 22 * scale; // leading-relaxed
const fontSize = 16 * scale; // text-sm sm:text-base (using desktop size)
// Calculate wrapped text lines
const maxTextWidth = width - padding * 2 - containerPadding * 2;
const lines = this.wrapText(promptText, maxTextWidth / scale, 16); // Use unscaled values for text wrapping
const textHeight = Math.max(100 * scale, lines.length * lineHeight + containerPadding * 2);
// Prompt container background
svgContent += ``;
// Prompt text
const textColor = isPlaceholder ? colors.mutedFgColor : colors.fgColor;
const fontStyle = isPlaceholder ? 'italic' : 'normal';
let textY = yOffset + containerPadding + fontSize;
for (const line of lines) {
svgContent += `${this.escapeXML(line)}`;
textY += lineHeight;
}
yOffset += textHeight + desktopGap; // mt-0 sm:mt-2
// Bottom bar with settings pills and buttons
const bottomBarHeight = 40 * scale; // h-10 for buttons
const settingsY = yOffset + (bottomBarHeight - 28 * scale) / 2; // Center pills vertically
let settingsX = padding;
// Mode pill (with background)
if (this.config.mode) {
const modeText = this.capitalizeFirst(this.config.mode);
const modePadding = 12 * scale; // px-3
const modeHeight = 32 * scale; // py-2
const modeWidth = modeText.length * 8 * scale + modePadding * 2;
const modeFontSize = 12 * scale;
svgContent += ``;
svgContent += `${modeText}`;
settingsX += modeWidth + desktopGap;
}
// Model pill (text only)
if (this.config.model) {
const modelText = this.config.model;
const modelFontSize = 12 * scale;
svgContent += `${modelText}`;
settingsX += modelText.length * 7 * scale + 12 * scale;
}
// Thinking pill
if (this.config.thinking) {
const thinkingFontSize = 12 * scale;
svgContent += `🧠 Thinking`;
settingsX += 70 * scale;
}
// MAX pill
if (this.config.max) {
const maxFontSize = 12 * scale;
svgContent += `⚡ MAX`;
}
// Watermark on the right
const watermarkText = "prompts.chat/embed";
const watermarkFontSize = 14 * scale;
const watermarkX = width - padding - watermarkText.length * 7 * scale;
const watermarkY = yOffset + bottomBarHeight / 2;
// Add watermark text with subtle styling
svgContent += `${watermarkText}`;
// Calculate final height
const totalHeight = yOffset + bottomBarHeight + padding;
// Create SVG with calculated height
let svg = `';
return { svg, scale };
}
wrapText(text, maxWidth, fontSize) {
const words = text.split(' ');
const lines = [];
let currentLine = '';
// More accurate character width calculation based on font size
const avgCharWidth = fontSize * 0.55; // Adjusted for typical character width
for (const word of words) {
const testLine = currentLine ? currentLine + ' ' + word : word;
const testWidth = testLine.length * avgCharWidth;
if (testWidth > maxWidth && currentLine) {
lines.push(currentLine);
currentLine = word;
} else {
currentLine = testLine;
}
}
if (currentLine) {
lines.push(currentLine);
}
return lines;
}
escapeXML(text) {
return text
.replace(/&/g, '&')
.replace(//g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
async downloadSVGasPNG(svgString, filename, scale = 1) {
// Create a blob from the SVG string
const blob = new Blob([svgString], { type: 'image/svg+xml' });
const url = URL.createObjectURL(blob);
// Create an image element
const img = new Image();
return new Promise((resolve, reject) => {
img.onload = () => {
// Create canvas with the actual pixel dimensions
const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
const ctx = canvas.getContext('2d');
// Enable high quality rendering for retina
ctx.imageSmoothingEnabled = true;
ctx.imageSmoothingQuality = 'high';
// Draw the image at full resolution
ctx.drawImage(img, 0, 0);
// Convert to PNG and download
canvas.toBlob((blob) => {
if (blob) {
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(a.href);
}
URL.revokeObjectURL(url);
resolve();
}, 'image/png', 1.0); // Maximum quality
};
img.onerror = () => {
URL.revokeObjectURL(url);
reject(new Error('Failed to load SVG'));
};
img.src = url;
});
}
showNotification(message) {
const notification = document.getElementById('notification');
if (!notification) return;
notification.textContent = message;
notification.classList.remove('opacity-0');
notification.classList.add('opacity-100');
setTimeout(() => {
notification.classList.remove('opacity-100');
notification.classList.add('opacity-0');
}, 2000);
}
}
// Initialize when DOM is ready
document.addEventListener('DOMContentLoaded', () => {
window.embedPreview = new EmbedPreview();
});
// Expose API for external usage
window.EmbedPreviewAPI = {
getPrompt: () => window.embedPreview?.config.prompt,
setPrompt: (prompt) => {
if (window.embedPreview) {
window.embedPreview.config.prompt = prompt;
window.embedPreview.renderPromptText();
}
},
updateConfig: (config) => {
if (window.embedPreview) {
Object.assign(window.embedPreview.config, config);
window.embedPreview.render();
}
}
};