This commit is contained in:
Fatih Kadir Akın 2025-06-18 16:21:53 +03:00
parent e06e0e7e61
commit 5f12aab0a5
11 changed files with 587 additions and 167 deletions

4
.gitignore vendored
View File

@ -0,0 +1,4 @@
.idea/
.vscode/
.DS_Store
_site/

View File

@ -39,14 +39,14 @@
</div>
<!-- Main Content -->
<div class="flex-1 flex flex-col p-2 sm:p-4">
<div class="flex-1 flex flex-col p-2 sm:p-4 min-h-0">
<!-- Top Bar with Context Pills and Edit Button -->
<div class="flex justify-between items-start gap-2 mb-0 sm:mb-2">
<div class="flex justify-between items-start gap-2 mb-1 sm:mb-2 max-w-full flex-shrink-0">
<!-- Context Pills -->
<div id="context-pills" class="flex overflow-x-auto gap-2 empty:hidden pb-1 scrollbar-hide flex-1"></div>
<div id="context-pills" class="flex flex-wrap gap-1.5 empty:hidden flex-1 min-w-0 pr-2"></div>
<!-- Edit Button -->
<button id="edit-button" class="p-1 text-dynamic-muted-foreground hover:text-dynamic-foreground transition-colors" title="Edit in designer">
<button id="edit-button" class="p-1 text-dynamic-muted-foreground hover:text-dynamic-foreground transition-colors flex-shrink-0" title="Edit in designer">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
@ -55,15 +55,15 @@
</div>
<!-- Main Prompt Interface - Full Height -->
<div class="flex-1 flex flex-col">
<div id="prompt-container" class="flex-1 bg-dynamic-muted border border-dynamic-border rounded-xl p-3 relative focus-within:border-dynamic-primary transition-colors flex flex-col">
<div class="flex-1 flex flex-col min-h-0">
<div id="prompt-container" class="flex-1 bg-dynamic-muted border border-dynamic-border rounded-xl p-3 relative focus-within:border-dynamic-primary transition-colors flex flex-col min-h-0">
<div id="prompt-text" class="flex-1 text-dynamic-foreground leading-relaxed whitespace-pre-wrap overflow-y-auto custom-scrollbar text-sm sm:text-base"></div>
<div id="prompt-placeholder" class="text-dynamic-muted-foreground italic absolute top-3 sm:top-6 left-3 sm:left-6 pointer-events-none text-sm sm:text-base">← Enter your prompt on designer...</div>
<div id="prompt-placeholder" class="text-dynamic-muted-foreground italic absolute top-3 left-3 pointer-events-none text-sm sm:text-base">← Enter your prompt on designer...</div>
</div>
</div>
<!-- Bottom Bar -->
<div class="flex justify-between items-center gap-2 mt-0 sm:mt-2">
<div class="flex justify-between items-center gap-2 mt-1 sm:mt-2 flex-shrink-0">
<!-- Settings Pills -->
<div id="settings-pills" class="flex gap-1 sm:gap-2 flex-wrap flex-1 min-w-0"></div>

View File

@ -65,9 +65,14 @@
class="w-full p-3 bg-dynamic-background border border-dynamic-border rounded-lg text-sm resize-none focus-ring custom-scrollbar touch-target"
rows="8"
placeholder="Enter your prompt..."></textarea>
<button type="button" id="load-example" class="text-xs text-dynamic-primary hover:text-opacity-80 transition-opacity touch-target py-1">
Load example →
</button>
<div class="flex gap-3">
<button type="button" id="vibe-example" class="text-xs text-dynamic-primary hover:text-opacity-80 transition-opacity touch-target py-1">
Vibe coding example →
</button>
<button type="button" id="chat-example" class="text-xs text-dynamic-primary hover:text-opacity-80 transition-opacity touch-target py-1">
Chat example →
</button>
</div>
</div>
<!-- Model & Mode -->
@ -229,7 +234,13 @@
<!-- File Tree -->
<div class="space-y-2">
<label class="text-sm font-medium text-dynamic-muted-foreground">File Tree</label>
<div class="flex items-center justify-between">
<label class="text-sm font-medium text-dynamic-muted-foreground">File Tree</label>
<label class="flex items-center space-x-1.5 text-xs">
<input type="checkbox" id="designer-show-filetree" class="rounded border-dynamic-border w-3.5 h-3.5" checked>
<span class="text-dynamic-muted-foreground">Show in preview</span>
</label>
</div>
<textarea id="designer-filetree"
class="w-full p-3 bg-dynamic-background border border-dynamic-border rounded-lg text-sm resize-none focus-ring custom-scrollbar touch-target font-mono"
rows="6"

View File

@ -2,6 +2,7 @@ class EmbedPreview {
constructor() {
this.params = this.parseURLParams();
this.config = this.getInitialConfig();
this.selectedFiles = new Set(); // Track selected files
this.init();
}
@ -198,17 +199,27 @@ class EmbedPreview {
container.innerHTML = '';
// Render initial context from config
this.config.context.forEach(context => {
const pill = this.createContextPill(context);
const pill = this.createContextPill(context, false);
container.appendChild(pill);
});
// Render selected files and folders
this.selectedFiles.forEach(filePath => {
const fileName = this.getFileName(filePath);
const pill = this.createContextPill(fileName, true, filePath);
pill.title = filePath; // Show full path on hover
container.appendChild(pill);
});
}
createContextPill(context) {
createContextPill(context, isRemovable, fullPath = null) {
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';
pill.className = 'px-2 py-0.5 rounded-lg text-[0.65rem] font-medium animate-slide-in flex items-center gap-1 flex-shrink-0 whitespace-nowrap';
let icon = '';
let content = '';
// Use dynamic color classes for all pills
if (this.isDarkMode) {
@ -219,24 +230,44 @@ class EmbedPreview {
if (context.startsWith('@')) {
// @mentions - just show the text
pill.innerHTML = '<span>' + context + '</span>';
content = '<span>' + context + '</span>';
} else if (context.startsWith('http://') || context.startsWith('https://')) {
// Web URLs show world icon
icon = '<svg class="w-3 h-3" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM4.332 8.027a6.012 6.012 0 011.912-2.706C6.512 5.73 6.974 6 7.5 6A1.5 1.5 0 019 7.5V8a2 2 0 004 0 2 2 0 011.523-1.943A5.977 5.977 0 0116 10c0 .34-.028.675-.083 1H15a2 2 0 00-2 2v2.197A5.973 5.973 0 0110 16v-2a2 2 0 00-2-2 2 2 0 01-2-2 2 2 0 00-1.668-1.973z" clip-rule="evenodd"/></svg>';
pill.innerHTML = icon + '<span>' + context + '</span>';
content = icon + '<span>' + context + '</span>';
} else if (context.startsWith('#')) {
// Any hashtag context shows image icon
icon = '<svg class="w-3 h-3" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M4 3a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V5a2 2 0 00-2-2H4zm12 12H4l4-8 3 6 2-4 3 6z" clip-rule="evenodd"/></svg>';
// Remove hash from display
pill.innerHTML = icon + '<span>' + context.substring(1) + '</span>';
} else if (context.includes('.')) {
// File context (contains a dot)
content = icon + '<span>' + context.substring(1) + '</span>';
} else if (context.endsWith('/')) {
// Folder context (ends with /)
icon = '<svg class="w-3 h-3" viewBox="0 0 20 20" fill="currentColor"><path d="M2 6a2 2 0 012-2h5l2 2h5a2 2 0 012 2v6a2 2 0 01-2 2H4a2 2 0 01-2-2V6z"/></svg>';
// Keep trailing slash for display
content = icon + '<span>' + context + '</span>';
} else if (context.includes('.') || isRemovable) {
// File context (contains a dot) or removable file from sidebar
icon = '<svg class="w-3 h-3" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M4 2a2 2 0 00-2 2v12a2 2 0 002 2h12a2 2 0 002-2V7.414A2 2 0 0017.414 6L14 2.586A2 2 0 0012.586 2H4zm2 4a1 1 0 011-1h4a1 1 0 110 2H7a1 1 0 01-1-1zm1 3a1 1 0 100 2h6a1 1 0 100-2H7zm-1 5a1 1 0 011-1h6a1 1 0 110 2H7a1 1 0 01-1-1z" clip-rule="evenodd"/></svg>';
pill.innerHTML = icon + '<span>' + context + '</span>';
content = icon + '<span>' + context + '</span>';
} else {
// Generic context
pill.innerHTML = '<span>' + context + '</span>';
content = '<span>' + context + '</span>';
}
pill.innerHTML = content;
// Add remove button for removable pills
if (isRemovable && fullPath) {
const removeBtn = document.createElement('button');
removeBtn.className = 'ml-0.5 text-dynamic-muted-foreground hover:text-dynamic-foreground transition-colors';
removeBtn.innerHTML = '<svg class="w-2.5 h-2.5" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"/></svg>';
removeBtn.addEventListener('click', (e) => {
e.stopPropagation();
this.removeFileFromContext(fullPath);
});
pill.appendChild(removeBtn);
}
return pill;
}
@ -343,7 +374,7 @@ class EmbedPreview {
}
highlightMentions(text) {
return text.replace(/@([\w\.\/\:\-\&\=\?]+)/g, '<span class="mention">@$1</span>');
return text.replace(/@(\w+)/g, '<span class="mention">@$1</span>');
}
capitalizeFirst(str) {
@ -416,6 +447,36 @@ class EmbedPreview {
}, 2000);
}
addFileToContext(filePath) {
if (!this.selectedFiles.has(filePath)) {
this.selectedFiles.add(filePath);
this.renderContextPills();
this.renderFileTree(); // Re-render to show selection
}
}
getFileName(filePath) {
// Extract just the filename or folder name from the path
const cleanPath = filePath.endsWith('/') ? filePath.slice(0, -1) : filePath;
const parts = cleanPath.split('/');
const name = parts[parts.length - 1];
// Add trailing slash back for folders to maintain context
return filePath.endsWith('/') ? name + '/' : name;
}
removeFileFromContext(filePath) {
if (this.selectedFiles.has(filePath)) {
this.selectedFiles.delete(filePath);
this.renderContextPills();
this.renderFileTree(); // Re-render to update selection
}
}
getFullPath(node, currentPath = '') {
// Helper to get the full path from a node
return currentPath ? `${currentPath}/${node.name}` : node.name;
}
buildFileTree(paths) {
const tree = {};
@ -425,19 +486,26 @@ class EmbedPreview {
// Remove asterisk if present
const cleanPath = isHighlighted ? path.slice(0, -1) : path;
const parts = cleanPath.split('/');
// Check if it's a folder (ends with /)
const isFolder = cleanPath.endsWith('/');
// Remove trailing slash for processing
const processPath = isFolder ? cleanPath.slice(0, -1) : cleanPath;
const parts = processPath.split('/').filter(p => p !== ''); // Filter out empty strings
let current = tree;
parts.forEach((part, index) => {
if (!current[part]) {
const isLastPart = index === parts.length - 1;
current[part] = {
name: part,
isFile: index === parts.length - 1,
isHighlighted: index === parts.length - 1 && isHighlighted,
isFile: isLastPart && !isFolder,
isHighlighted: isLastPart && isHighlighted,
children: {}
};
}
if (index < parts.length - 1) {
if (index < parts.length - 1 || (index === parts.length - 1 && !current[part].isFile)) {
current = current[part].children;
}
});
@ -463,13 +531,13 @@ class EmbedPreview {
treeContainer.innerHTML = '';
// Render tree
this.renderTreeNode(tree, treeContainer, 0);
this.renderTreeNode(tree, treeContainer, 0, '');
} else {
sidebar.classList.add('hidden');
}
}
renderTreeNode(node, container, level) {
renderTreeNode(node, container, level, parentPath) {
const sortedKeys = Object.keys(node).sort((a, b) => {
// Folders first, then files
const aIsFile = node[a].isFile;
@ -480,10 +548,17 @@ class EmbedPreview {
sortedKeys.forEach(key => {
const item = node[key];
const fullPath = parentPath ? `${parentPath}/${item.name}` : item.name;
const itemElement = document.createElement('div');
// Add highlighting class if the file is marked
// Check if file or folder is selected
const contextPath = item.isFile ? fullPath : fullPath + '/';
const isSelected = this.selectedFiles.has(contextPath);
// Add highlighting class if the file is marked or selected
if (item.isHighlighted) {
itemElement.className = 'flex items-center gap-1 py-0.5 px-1.5 bg-dynamic-primary/20 rounded text-xs text-dynamic-foreground font-medium transition-all';
} else if (isSelected) {
itemElement.className = 'flex items-center gap-1 py-0.5 px-1.5 bg-dynamic-primary/20 rounded cursor-pointer text-xs text-dynamic-foreground font-medium transition-all hover:bg-dynamic-primary/30';
} else {
itemElement.className = 'flex items-center gap-1 py-0.5 px-1.5 hover:bg-dynamic-primary/10 rounded cursor-pointer text-xs text-dynamic-foreground/80 hover:text-dynamic-foreground transition-colors';
@ -491,6 +566,23 @@ class EmbedPreview {
itemElement.style.paddingLeft = `${level * 12 + 6}px`;
// Add click handler for files and folders (but not starred items)
if (!item.isHighlighted) {
itemElement.addEventListener('click', () => {
const contextPath = item.isFile ? fullPath : fullPath + '/';
if (isSelected) {
this.removeFileFromContext(contextPath);
} else {
this.addFileToContext(contextPath);
}
});
itemElement.title = isSelected ? 'Click to remove from context' : 'Click to add to context';
itemElement.style.cursor = 'pointer';
} else {
itemElement.style.cursor = 'default';
itemElement.title = 'Starred items cannot be selected';
}
// Add icon
const icon = document.createElement('span');
icon.className = 'flex-shrink-0';
@ -500,8 +592,8 @@ class EmbedPreview {
const ext = key.split('.').pop().toLowerCase();
let iconColor = 'text-dynamic-muted-foreground';
// If highlighted, use primary color for icon
if (item.isHighlighted) {
// If highlighted or selected, use primary color for icon
if (item.isHighlighted || isSelected) {
iconColor = 'text-dynamic-primary';
} else {
// Color code common file types
@ -536,19 +628,35 @@ class EmbedPreview {
itemElement.appendChild(icon);
itemElement.appendChild(nameSpan);
// Add a star indicator for highlighted files
if (item.isHighlighted) {
const starIcon = document.createElement('span');
starIcon.className = 'ml-auto text-dynamic-primary';
starIcon.innerHTML = '<svg class="w-2.5 h-2.5" viewBox="0 0 20 20" fill="currentColor"><path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"/></svg>';
itemElement.appendChild(starIcon);
// Add indicators for highlighted or selected items
if (item.isHighlighted || isSelected) {
const indicatorContainer = document.createElement('span');
indicatorContainer.className = 'ml-auto flex items-center gap-1';
// Add star for highlighted items
if (item.isHighlighted) {
const starIcon = document.createElement('span');
starIcon.className = 'text-dynamic-primary';
starIcon.innerHTML = '<svg class="w-2.5 h-2.5" viewBox="0 0 20 20" fill="currentColor"><path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"/></svg>';
indicatorContainer.appendChild(starIcon);
}
// Add checkmark for selected items (not highlighted)
if (isSelected && !item.isHighlighted) {
const checkIcon = document.createElement('span');
checkIcon.className = 'text-dynamic-primary';
checkIcon.innerHTML = '<svg class="w-2.5 h-2.5" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"/></svg>';
indicatorContainer.appendChild(checkIcon);
}
itemElement.appendChild(indicatorContainer);
}
container.appendChild(itemElement);
// Recursively render children
if (!item.isFile && Object.keys(item.children).length > 0) {
this.renderTreeNode(item.children, container, level + 1);
this.renderTreeNode(item.children, container, level + 1, fullPath);
}
});
}

View File

@ -90,6 +90,8 @@
display: none; /* Safari and Chrome */
}
/* Touch target improvements for mobile */
.touch-target {
min-height: 44px;
@ -101,16 +103,15 @@
/* Fixed height for prompt container to enable scrolling */
#prompt-container {
height: calc(100vh - 140px); /* Fixed height minus header and footer space */
min-height: 100px;
max-height: calc(100vh - 120px);
/* max-height: 100vh; */
flex: 1;
display: flex;
flex-direction: column;
}
@media (max-width: 640px) {
#prompt-container {
height: calc(100vh - 90px); /* Smaller on mobile */
max-height: calc(100vh - 90px);
min-height: 80px;
}
}
@ -123,11 +124,10 @@
/* Responsive improvements */
@media (max-width: 640px) {
/* Ensure context pills don't wrap and scroll horizontally */
/* Context pills can wrap on mobile too */
#context-pills {
flex-wrap: nowrap;
scrollbar-width: none;
-ms-overflow-style: none;
flex-wrap: wrap !important;
max-height: 80px; /* Smaller on mobile */
}
/* More compact pill spacing on mobile */
@ -192,6 +192,43 @@
}
}
/* Context pills container improvements */
#context-pills {
max-width: calc(100% - 40px); /* Reserve space for edit button */
display: flex;
flex-wrap: wrap;
max-height: 120px; /* Limit context pills height */
overflow-y: auto;
overflow-x: hidden;
}
/* Custom scrollbar for context pills */
#context-pills {
scrollbar-width: thin; /* Firefox */
}
#context-pills::-webkit-scrollbar {
width: 4px;
}
#context-pills::-webkit-scrollbar-track {
background: transparent;
}
#context-pills::-webkit-scrollbar-thumb {
background: rgb(var(--border) / 0.5);
border-radius: 2px;
}
#context-pills::-webkit-scrollbar-thumb:hover {
background: rgb(var(--border));
}
/* Ensure pills maintain size */
#context-pills > div {
flex-shrink: 0;
}
/* Ensure proper touch behavior */
@media (hover: none) and (pointer: coarse) {
/* All interactive elements get proper touch targets */

View File

@ -39,14 +39,14 @@
</div>
<!-- Main Content -->
<div class="flex-1 flex flex-col p-2 sm:p-4">
<div class="flex-1 flex flex-col p-2 sm:p-4 min-h-0">
<!-- Top Bar with Context Pills and Edit Button -->
<div class="flex justify-between items-start gap-2 mb-0 sm:mb-2">
<div class="flex justify-between items-start gap-2 mb-1 sm:mb-2 max-w-full flex-shrink-0">
<!-- Context Pills -->
<div id="context-pills" class="flex overflow-x-auto gap-2 empty:hidden pb-1 scrollbar-hide flex-1"></div>
<div id="context-pills" class="flex flex-wrap gap-1.5 empty:hidden flex-1 min-w-0 pr-2"></div>
<!-- Edit Button -->
<button id="edit-button" class="p-1 text-dynamic-muted-foreground hover:text-dynamic-foreground transition-colors" title="Edit in designer">
<button id="edit-button" class="p-1 text-dynamic-muted-foreground hover:text-dynamic-foreground transition-colors flex-shrink-0" title="Edit in designer">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
@ -55,15 +55,15 @@
</div>
<!-- Main Prompt Interface - Full Height -->
<div class="flex-1 flex flex-col">
<div id="prompt-container" class="flex-1 bg-dynamic-muted border border-dynamic-border rounded-xl p-3 relative focus-within:border-dynamic-primary transition-colors flex flex-col">
<div class="flex-1 flex flex-col min-h-0">
<div id="prompt-container" class="flex-1 bg-dynamic-muted border border-dynamic-border rounded-xl p-3 relative focus-within:border-dynamic-primary transition-colors flex flex-col min-h-0">
<div id="prompt-text" class="flex-1 text-dynamic-foreground leading-relaxed whitespace-pre-wrap overflow-y-auto custom-scrollbar text-sm sm:text-base"></div>
<div id="prompt-placeholder" class="text-dynamic-muted-foreground italic absolute top-3 sm:top-6 left-3 sm:left-6 pointer-events-none text-sm sm:text-base">← Enter your prompt on designer...</div>
<div id="prompt-placeholder" class="text-dynamic-muted-foreground italic absolute top-3 left-3 pointer-events-none text-sm sm:text-base">← Enter your prompt on designer...</div>
</div>
</div>
<!-- Bottom Bar -->
<div class="flex justify-between items-center gap-2 mt-0 sm:mt-2">
<div class="flex justify-between items-center gap-2 mt-1 sm:mt-2 flex-shrink-0">
<!-- Settings Pills -->
<div id="settings-pills" class="flex gap-1 sm:gap-2 flex-wrap flex-1 min-w-0"></div>

View File

@ -33,7 +33,8 @@ class EmbedDesigner {
darkColor: this.params.darkColor || savedConfig.darkColor || '#60a5fa',
height: this.params.height || savedConfig.height || '400',
themeMode: this.params.themeMode || savedConfig.themeMode || 'auto',
filetree: this.params.filetree ? decodeURIComponent(this.params.filetree).split('\n').filter(f => f.trim()) : (savedConfig.filetree || [])
filetree: this.params.filetree ? decodeURIComponent(this.params.filetree).split('\n').filter(f => f.trim()) : (savedConfig.filetree || []),
showFiletree: savedConfig.showFiletree !== undefined ? savedConfig.showFiletree : true
};
}
@ -55,7 +56,8 @@ class EmbedDesigner {
darkColor: '#60a5fa',
height: '400',
themeMode: 'auto',
filetree: []
filetree: [],
showFiletree: true
};
}
@ -78,7 +80,8 @@ class EmbedDesigner {
darkColor: config.darkColor || '#60a5fa',
height: config.height || '400',
themeMode: config.themeMode || 'auto',
filetree: config.filetree || []
filetree: config.filetree || [],
showFiletree: config.showFiletree !== undefined ? config.showFiletree : true
};
}
}
@ -141,6 +144,12 @@ class EmbedDesigner {
document.getElementById('designer-thinking').checked = this.config.thinking;
document.getElementById('designer-max').checked = this.config.max;
// Set show filetree checkbox (default to true if not set)
const showFiletreeCheckbox = document.getElementById('designer-show-filetree');
if (showFiletreeCheckbox) {
showFiletreeCheckbox.checked = this.config.showFiletree !== false;
}
// Set height slider
const heightSlider = document.getElementById('designer-height');
const heightValue = document.getElementById('height-value');
@ -186,10 +195,12 @@ class EmbedDesigner {
setupDesignerEvents() {
// Form changes update preview
['designer-prompt', 'designer-context', 'designer-mode-select', 'designer-thinking', 'designer-max', 'designer-filetree'].forEach(id => {
['designer-prompt', 'designer-context', 'designer-mode-select', 'designer-thinking', 'designer-max', 'designer-filetree', 'designer-show-filetree'].forEach(id => {
const element = document.getElementById(id);
element.addEventListener('input', () => this.updateConfigFromForm());
element.addEventListener('change', () => this.updateConfigFromForm());
if (element) {
element.addEventListener('input', () => this.updateConfigFromForm());
element.addEventListener('change', () => this.updateConfigFromForm());
}
});
// Theme mode buttons
@ -317,7 +328,8 @@ class EmbedDesigner {
darkColor: '#60a5fa',
height: '400',
themeMode: 'auto',
filetree: []
filetree: [],
showFiletree: true
};
// Update UI to reflect defaults
this.setupDesignerElements();
@ -326,9 +338,13 @@ class EmbedDesigner {
}
});
// Load example button
document.getElementById('load-example').addEventListener('click', () => {
this.loadExample();
// Example buttons
document.getElementById('vibe-example').addEventListener('click', () => {
this.loadVibeExample();
});
document.getElementById('chat-example').addEventListener('click', () => {
this.loadChatExample();
});
// Modal events
@ -366,7 +382,8 @@ class EmbedDesigner {
darkColor: darkColorText ? darkColorText.value : '#60a5fa',
height: heightSlider ? heightSlider.value : '400',
themeMode: this.config.themeMode || 'auto',
filetree: document.getElementById('designer-filetree').value.split('\n').map(f => f.trim()).filter(f => f)
filetree: document.getElementById('designer-filetree').value.split('\n').map(f => f.trim()).filter(f => f),
showFiletree: document.getElementById('designer-show-filetree').checked
};
this.updatePreview();
@ -439,7 +456,7 @@ class EmbedDesigner {
if (this.config.lightColor !== '#3b82f6') params.set('lightColor', this.config.lightColor);
if (this.config.darkColor !== '#60a5fa') params.set('darkColor', this.config.darkColor);
if (this.config.themeMode !== 'auto') params.set('themeMode', this.config.themeMode);
if (this.config.filetree && this.config.filetree.length > 0) params.set('filetree', encodeURIComponent(this.config.filetree.join('\n')));
if (this.config.showFiletree && this.config.filetree && this.config.filetree.length > 0) params.set('filetree', encodeURIComponent(this.config.filetree.join('\n')));
params.set('preview', 'true');
return `/embed-preview/?${params.toString()}`;
@ -456,7 +473,7 @@ class EmbedDesigner {
if (this.config.lightColor !== '#3b82f6') params.set('lightColor', this.config.lightColor);
if (this.config.darkColor !== '#60a5fa') params.set('darkColor', this.config.darkColor);
if (this.config.themeMode !== 'auto') params.set('themeMode', this.config.themeMode);
if (this.config.filetree && this.config.filetree.length > 0) params.set('filetree', encodeURIComponent(this.config.filetree.join('\n')));
if (this.config.showFiletree && this.config.filetree && this.config.filetree.length > 0) params.set('filetree', encodeURIComponent(this.config.filetree.join('\n')));
return `${window.location.origin}/embed-preview/?${params.toString()}`;
}
@ -537,42 +554,77 @@ class EmbedDesigner {
}, duration);
}
loadExample() {
// Set example values
loadVibeExample() {
// Set vibe coding example values
document.getElementById('designer-prompt').value =
`You are a senior React developer. I need help building a modern e-commerce product listing component.
`I'm working on a React e-commerce app. The current ProductList component is getting messy with too much logic.
Requirements:
- Use React hooks and functional components
- Implement product filtering by category and price range
- Add smooth animations for product cards
- Make it fully responsive with a grid layout
- Include loading states and error handling
Can you help me refactor it to:
1. Extract the filtering logic into a custom hook
2. Split the large component into smaller, reusable pieces
3. Add proper TypeScript types
4. Implement virtualization for better performance with large product lists
The component should fetch data from a REST API and display products with images, titles, prices, and ratings. Please provide clean, well-commented code following React best practices.`;
The component currently handles products display, filtering by category/price, sorting, and pagination all in one file. I want a cleaner architecture.`;
document.getElementById('designer-context').value = '@codebase, ProductList.jsx';
document.getElementById('designer-context').value = '@codebase, ProductList.tsx, components/, hooks/';
document.getElementById('designer-filetree').value =
`src/components/ProductList.jsx*
src/components/ProductCard.jsx
src/components/Filters.jsx
src/hooks/useProducts.js
src/api/products.js
src/styles/products.css
src/utils/formatters.js
public/index.html
`src/
src/components/
src/components/ProductList.tsx*
src/components/ProductCard.tsx
src/components/ProductGrid.tsx
src/components/Filters/
src/components/Filters/CategoryFilter.tsx
src/components/Filters/PriceRangeFilter.tsx
src/components/Filters/index.tsx
src/hooks/
src/hooks/useProducts.ts
src/hooks/useFilters.ts
src/hooks/useInfiniteScroll.ts
src/types/
src/types/product.ts
src/api/
src/api/products.ts
src/utils/
src/utils/formatters.ts
package.json
README.md`;
tsconfig.json`;
// Set some example settings
document.getElementById('designer-model').value = 'Claude 3.7 Sonnet';
// Set vibe coding settings
document.getElementById('designer-model').value = 'Claude 4 Opus';
document.getElementById('designer-mode-select').value = 'agent';
document.getElementById('designer-thinking').checked = true;
document.getElementById('designer-max').checked = true;
document.getElementById('designer-show-filetree').checked = true;
// Update config from form
this.updateConfigFromForm();
this.showNotification('Example loaded!');
this.showNotification('Vibe coding example loaded!');
}
loadChatExample() {
// Set chat example values
document.getElementById('designer-prompt').value =
`I'm planning a dinner party for 8 people this weekend. One person is vegetarian, another is gluten-free, and I want to make something impressive but not too complicated.
Can you suggest a menu with appetizers, main course, and dessert that would work for everyone? I have about 4 hours to prepare everything and a moderate cooking skill level.`;
document.getElementById('designer-context').value = '#Screenshot Kitchen Layout.png, Recipes Collection.pdf';
document.getElementById('designer-filetree').value = '';
// Set chat settings
document.getElementById('designer-model').value = 'GPT 4o';
document.getElementById('designer-mode-select').value = 'chat';
document.getElementById('designer-thinking').checked = false;
document.getElementById('designer-max').checked = false;
document.getElementById('designer-show-filetree').checked = false;
// Update config from form
this.updateConfigFromForm();
this.showNotification('Chat example loaded!');
}
}

View File

@ -65,9 +65,14 @@
class="w-full p-3 bg-dynamic-background border border-dynamic-border rounded-lg text-sm resize-none focus-ring custom-scrollbar touch-target"
rows="8"
placeholder="Enter your prompt..."></textarea>
<button type="button" id="load-example" class="text-xs text-dynamic-primary hover:text-opacity-80 transition-opacity touch-target py-1">
Load example →
</button>
<div class="flex gap-3">
<button type="button" id="vibe-example" class="text-xs text-dynamic-primary hover:text-opacity-80 transition-opacity touch-target py-1">
Vibe coding example →
</button>
<button type="button" id="chat-example" class="text-xs text-dynamic-primary hover:text-opacity-80 transition-opacity touch-target py-1">
Chat example →
</button>
</div>
</div>
<!-- Model & Mode -->
@ -229,7 +234,13 @@
<!-- File Tree -->
<div class="space-y-2">
<label class="text-sm font-medium text-dynamic-muted-foreground">File Tree</label>
<div class="flex items-center justify-between">
<label class="text-sm font-medium text-dynamic-muted-foreground">File Tree</label>
<label class="flex items-center space-x-1.5 text-xs">
<input type="checkbox" id="designer-show-filetree" class="rounded border-dynamic-border w-3.5 h-3.5" checked>
<span class="text-dynamic-muted-foreground">Show in preview</span>
</label>
</div>
<textarea id="designer-filetree"
class="w-full p-3 bg-dynamic-background border border-dynamic-border rounded-lg text-sm resize-none focus-ring custom-scrollbar touch-target font-mono"
rows="6"

View File

@ -2,6 +2,7 @@ class EmbedPreview {
constructor() {
this.params = this.parseURLParams();
this.config = this.getInitialConfig();
this.selectedFiles = new Set(); // Track selected files
this.init();
}
@ -198,17 +199,27 @@ class EmbedPreview {
container.innerHTML = '';
// Render initial context from config
this.config.context.forEach(context => {
const pill = this.createContextPill(context);
const pill = this.createContextPill(context, false);
container.appendChild(pill);
});
// Render selected files and folders
this.selectedFiles.forEach(filePath => {
const fileName = this.getFileName(filePath);
const pill = this.createContextPill(fileName, true, filePath);
pill.title = filePath; // Show full path on hover
container.appendChild(pill);
});
}
createContextPill(context) {
createContextPill(context, isRemovable, fullPath = null) {
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';
pill.className = 'px-2 py-0.5 rounded-lg text-[0.65rem] font-medium animate-slide-in flex items-center gap-1 flex-shrink-0 whitespace-nowrap';
let icon = '';
let content = '';
// Use dynamic color classes for all pills
if (this.isDarkMode) {
@ -219,24 +230,44 @@ class EmbedPreview {
if (context.startsWith('@')) {
// @mentions - just show the text
pill.innerHTML = '<span>' + context + '</span>';
content = '<span>' + context + '</span>';
} else if (context.startsWith('http://') || context.startsWith('https://')) {
// Web URLs show world icon
icon = '<svg class="w-3 h-3" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM4.332 8.027a6.012 6.012 0 011.912-2.706C6.512 5.73 6.974 6 7.5 6A1.5 1.5 0 019 7.5V8a2 2 0 004 0 2 2 0 011.523-1.943A5.977 5.977 0 0116 10c0 .34-.028.675-.083 1H15a2 2 0 00-2 2v2.197A5.973 5.973 0 0110 16v-2a2 2 0 00-2-2 2 2 0 01-2-2 2 2 0 00-1.668-1.973z" clip-rule="evenodd"/></svg>';
pill.innerHTML = icon + '<span>' + context + '</span>';
content = icon + '<span>' + context + '</span>';
} else if (context.startsWith('#')) {
// Any hashtag context shows image icon
icon = '<svg class="w-3 h-3" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M4 3a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V5a2 2 0 00-2-2H4zm12 12H4l4-8 3 6 2-4 3 6z" clip-rule="evenodd"/></svg>';
// Remove hash from display
pill.innerHTML = icon + '<span>' + context.substring(1) + '</span>';
} else if (context.includes('.')) {
// File context (contains a dot)
content = icon + '<span>' + context.substring(1) + '</span>';
} else if (context.endsWith('/')) {
// Folder context (ends with /)
icon = '<svg class="w-3 h-3" viewBox="0 0 20 20" fill="currentColor"><path d="M2 6a2 2 0 012-2h5l2 2h5a2 2 0 012 2v6a2 2 0 01-2 2H4a2 2 0 01-2-2V6z"/></svg>';
// Keep trailing slash for display
content = icon + '<span>' + context + '</span>';
} else if (context.includes('.') || isRemovable) {
// File context (contains a dot) or removable file from sidebar
icon = '<svg class="w-3 h-3" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M4 2a2 2 0 00-2 2v12a2 2 0 002 2h12a2 2 0 002-2V7.414A2 2 0 0017.414 6L14 2.586A2 2 0 0012.586 2H4zm2 4a1 1 0 011-1h4a1 1 0 110 2H7a1 1 0 01-1-1zm1 3a1 1 0 100 2h6a1 1 0 100-2H7zm-1 5a1 1 0 011-1h6a1 1 0 110 2H7a1 1 0 01-1-1z" clip-rule="evenodd"/></svg>';
pill.innerHTML = icon + '<span>' + context + '</span>';
content = icon + '<span>' + context + '</span>';
} else {
// Generic context
pill.innerHTML = '<span>' + context + '</span>';
content = '<span>' + context + '</span>';
}
pill.innerHTML = content;
// Add remove button for removable pills
if (isRemovable && fullPath) {
const removeBtn = document.createElement('button');
removeBtn.className = 'ml-0.5 text-dynamic-muted-foreground hover:text-dynamic-foreground transition-colors';
removeBtn.innerHTML = '<svg class="w-2.5 h-2.5" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"/></svg>';
removeBtn.addEventListener('click', (e) => {
e.stopPropagation();
this.removeFileFromContext(fullPath);
});
pill.appendChild(removeBtn);
}
return pill;
}
@ -416,6 +447,36 @@ class EmbedPreview {
}, 2000);
}
addFileToContext(filePath) {
if (!this.selectedFiles.has(filePath)) {
this.selectedFiles.add(filePath);
this.renderContextPills();
this.renderFileTree(); // Re-render to show selection
}
}
getFileName(filePath) {
// Extract just the filename or folder name from the path
const cleanPath = filePath.endsWith('/') ? filePath.slice(0, -1) : filePath;
const parts = cleanPath.split('/');
const name = parts[parts.length - 1];
// Add trailing slash back for folders to maintain context
return filePath.endsWith('/') ? name + '/' : name;
}
removeFileFromContext(filePath) {
if (this.selectedFiles.has(filePath)) {
this.selectedFiles.delete(filePath);
this.renderContextPills();
this.renderFileTree(); // Re-render to update selection
}
}
getFullPath(node, currentPath = '') {
// Helper to get the full path from a node
return currentPath ? `${currentPath}/${node.name}` : node.name;
}
buildFileTree(paths) {
const tree = {};
@ -425,19 +486,26 @@ class EmbedPreview {
// Remove asterisk if present
const cleanPath = isHighlighted ? path.slice(0, -1) : path;
const parts = cleanPath.split('/');
// Check if it's a folder (ends with /)
const isFolder = cleanPath.endsWith('/');
// Remove trailing slash for processing
const processPath = isFolder ? cleanPath.slice(0, -1) : cleanPath;
const parts = processPath.split('/').filter(p => p !== ''); // Filter out empty strings
let current = tree;
parts.forEach((part, index) => {
if (!current[part]) {
const isLastPart = index === parts.length - 1;
current[part] = {
name: part,
isFile: index === parts.length - 1,
isHighlighted: index === parts.length - 1 && isHighlighted,
isFile: isLastPart && !isFolder,
isHighlighted: isLastPart && isHighlighted,
children: {}
};
}
if (index < parts.length - 1) {
if (index < parts.length - 1 || (index === parts.length - 1 && !current[part].isFile)) {
current = current[part].children;
}
});
@ -463,13 +531,13 @@ class EmbedPreview {
treeContainer.innerHTML = '';
// Render tree
this.renderTreeNode(tree, treeContainer, 0);
this.renderTreeNode(tree, treeContainer, 0, '');
} else {
sidebar.classList.add('hidden');
}
}
renderTreeNode(node, container, level) {
renderTreeNode(node, container, level, parentPath) {
const sortedKeys = Object.keys(node).sort((a, b) => {
// Folders first, then files
const aIsFile = node[a].isFile;
@ -480,10 +548,17 @@ class EmbedPreview {
sortedKeys.forEach(key => {
const item = node[key];
const fullPath = parentPath ? `${parentPath}/${item.name}` : item.name;
const itemElement = document.createElement('div');
// Add highlighting class if the file is marked
// Check if file or folder is selected
const contextPath = item.isFile ? fullPath : fullPath + '/';
const isSelected = this.selectedFiles.has(contextPath);
// Add highlighting class if the file is marked or selected
if (item.isHighlighted) {
itemElement.className = 'flex items-center gap-1 py-0.5 px-1.5 bg-dynamic-primary/20 rounded text-xs text-dynamic-foreground font-medium transition-all';
} else if (isSelected) {
itemElement.className = 'flex items-center gap-1 py-0.5 px-1.5 bg-dynamic-primary/20 rounded cursor-pointer text-xs text-dynamic-foreground font-medium transition-all hover:bg-dynamic-primary/30';
} else {
itemElement.className = 'flex items-center gap-1 py-0.5 px-1.5 hover:bg-dynamic-primary/10 rounded cursor-pointer text-xs text-dynamic-foreground/80 hover:text-dynamic-foreground transition-colors';
@ -491,6 +566,23 @@ class EmbedPreview {
itemElement.style.paddingLeft = `${level * 12 + 6}px`;
// Add click handler for files and folders (but not starred items)
if (!item.isHighlighted) {
itemElement.addEventListener('click', () => {
const contextPath = item.isFile ? fullPath : fullPath + '/';
if (isSelected) {
this.removeFileFromContext(contextPath);
} else {
this.addFileToContext(contextPath);
}
});
itemElement.title = isSelected ? 'Click to remove from context' : 'Click to add to context';
itemElement.style.cursor = 'pointer';
} else {
itemElement.style.cursor = 'default';
itemElement.title = 'Starred items cannot be selected';
}
// Add icon
const icon = document.createElement('span');
icon.className = 'flex-shrink-0';
@ -500,8 +592,8 @@ class EmbedPreview {
const ext = key.split('.').pop().toLowerCase();
let iconColor = 'text-dynamic-muted-foreground';
// If highlighted, use primary color for icon
if (item.isHighlighted) {
// If highlighted or selected, use primary color for icon
if (item.isHighlighted || isSelected) {
iconColor = 'text-dynamic-primary';
} else {
// Color code common file types
@ -536,19 +628,35 @@ class EmbedPreview {
itemElement.appendChild(icon);
itemElement.appendChild(nameSpan);
// Add a star indicator for highlighted files
if (item.isHighlighted) {
const starIcon = document.createElement('span');
starIcon.className = 'ml-auto text-dynamic-primary';
starIcon.innerHTML = '<svg class="w-2.5 h-2.5" viewBox="0 0 20 20" fill="currentColor"><path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"/></svg>';
itemElement.appendChild(starIcon);
// Add indicators for highlighted or selected items
if (item.isHighlighted || isSelected) {
const indicatorContainer = document.createElement('span');
indicatorContainer.className = 'ml-auto flex items-center gap-1';
// Add star for highlighted items
if (item.isHighlighted) {
const starIcon = document.createElement('span');
starIcon.className = 'text-dynamic-primary';
starIcon.innerHTML = '<svg class="w-2.5 h-2.5" viewBox="0 0 20 20" fill="currentColor"><path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"/></svg>';
indicatorContainer.appendChild(starIcon);
}
// Add checkmark for selected items (not highlighted)
if (isSelected && !item.isHighlighted) {
const checkIcon = document.createElement('span');
checkIcon.className = 'text-dynamic-primary';
checkIcon.innerHTML = '<svg class="w-2.5 h-2.5" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"/></svg>';
indicatorContainer.appendChild(checkIcon);
}
itemElement.appendChild(indicatorContainer);
}
container.appendChild(itemElement);
// Recursively render children
if (!item.isFile && Object.keys(item.children).length > 0) {
this.renderTreeNode(item.children, container, level + 1);
this.renderTreeNode(item.children, container, level + 1, fullPath);
}
});
}

View File

@ -90,6 +90,8 @@
display: none; /* Safari and Chrome */
}
/* Touch target improvements for mobile */
.touch-target {
min-height: 44px;
@ -101,16 +103,15 @@
/* Fixed height for prompt container to enable scrolling */
#prompt-container {
height: calc(100vh - 140px); /* Fixed height minus header and footer space */
min-height: 100px;
max-height: calc(100vh - 120px);
/* max-height: 100vh; */
flex: 1;
display: flex;
flex-direction: column;
}
@media (max-width: 640px) {
#prompt-container {
height: calc(100vh - 90px); /* Smaller on mobile */
max-height: calc(100vh - 90px);
min-height: 80px;
}
}
@ -123,11 +124,10 @@
/* Responsive improvements */
@media (max-width: 640px) {
/* Ensure context pills don't wrap and scroll horizontally */
/* Context pills can wrap on mobile too */
#context-pills {
flex-wrap: nowrap;
scrollbar-width: none;
-ms-overflow-style: none;
flex-wrap: wrap !important;
max-height: 80px; /* Smaller on mobile */
}
/* More compact pill spacing on mobile */
@ -192,6 +192,43 @@
}
}
/* Context pills container improvements */
#context-pills {
max-width: calc(100% - 40px); /* Reserve space for edit button */
display: flex;
flex-wrap: wrap;
max-height: 120px; /* Limit context pills height */
overflow-y: auto;
overflow-x: hidden;
}
/* Custom scrollbar for context pills */
#context-pills {
scrollbar-width: thin; /* Firefox */
}
#context-pills::-webkit-scrollbar {
width: 4px;
}
#context-pills::-webkit-scrollbar-track {
background: transparent;
}
#context-pills::-webkit-scrollbar-thumb {
background: rgb(var(--border) / 0.5);
border-radius: 2px;
}
#context-pills::-webkit-scrollbar-thumb:hover {
background: rgb(var(--border));
}
/* Ensure pills maintain size */
#context-pills > div {
flex-shrink: 0;
}
/* Ensure proper touch behavior */
@media (hover: none) and (pointer: coarse) {
/* All interactive elements get proper touch targets */

View File

@ -33,7 +33,8 @@ class EmbedDesigner {
darkColor: this.params.darkColor || savedConfig.darkColor || '#60a5fa',
height: this.params.height || savedConfig.height || '400',
themeMode: this.params.themeMode || savedConfig.themeMode || 'auto',
filetree: this.params.filetree ? decodeURIComponent(this.params.filetree).split('\n').filter(f => f.trim()) : (savedConfig.filetree || [])
filetree: this.params.filetree ? decodeURIComponent(this.params.filetree).split('\n').filter(f => f.trim()) : (savedConfig.filetree || []),
showFiletree: savedConfig.showFiletree !== undefined ? savedConfig.showFiletree : true
};
}
@ -55,7 +56,8 @@ class EmbedDesigner {
darkColor: '#60a5fa',
height: '400',
themeMode: 'auto',
filetree: []
filetree: [],
showFiletree: true
};
}
@ -78,7 +80,8 @@ class EmbedDesigner {
darkColor: config.darkColor || '#60a5fa',
height: config.height || '400',
themeMode: config.themeMode || 'auto',
filetree: config.filetree || []
filetree: config.filetree || [],
showFiletree: config.showFiletree !== undefined ? config.showFiletree : true
};
}
}
@ -141,6 +144,12 @@ class EmbedDesigner {
document.getElementById('designer-thinking').checked = this.config.thinking;
document.getElementById('designer-max').checked = this.config.max;
// Set show filetree checkbox (default to true if not set)
const showFiletreeCheckbox = document.getElementById('designer-show-filetree');
if (showFiletreeCheckbox) {
showFiletreeCheckbox.checked = this.config.showFiletree !== false;
}
// Set height slider
const heightSlider = document.getElementById('designer-height');
const heightValue = document.getElementById('height-value');
@ -186,10 +195,12 @@ class EmbedDesigner {
setupDesignerEvents() {
// Form changes update preview
['designer-prompt', 'designer-context', 'designer-mode-select', 'designer-thinking', 'designer-max', 'designer-filetree'].forEach(id => {
['designer-prompt', 'designer-context', 'designer-mode-select', 'designer-thinking', 'designer-max', 'designer-filetree', 'designer-show-filetree'].forEach(id => {
const element = document.getElementById(id);
element.addEventListener('input', () => this.updateConfigFromForm());
element.addEventListener('change', () => this.updateConfigFromForm());
if (element) {
element.addEventListener('input', () => this.updateConfigFromForm());
element.addEventListener('change', () => this.updateConfigFromForm());
}
});
// Theme mode buttons
@ -317,7 +328,8 @@ class EmbedDesigner {
darkColor: '#60a5fa',
height: '400',
themeMode: 'auto',
filetree: []
filetree: [],
showFiletree: true
};
// Update UI to reflect defaults
this.setupDesignerElements();
@ -326,9 +338,13 @@ class EmbedDesigner {
}
});
// Load example button
document.getElementById('load-example').addEventListener('click', () => {
this.loadExample();
// Example buttons
document.getElementById('vibe-example').addEventListener('click', () => {
this.loadVibeExample();
});
document.getElementById('chat-example').addEventListener('click', () => {
this.loadChatExample();
});
// Modal events
@ -366,7 +382,8 @@ class EmbedDesigner {
darkColor: darkColorText ? darkColorText.value : '#60a5fa',
height: heightSlider ? heightSlider.value : '400',
themeMode: this.config.themeMode || 'auto',
filetree: document.getElementById('designer-filetree').value.split('\n').map(f => f.trim()).filter(f => f)
filetree: document.getElementById('designer-filetree').value.split('\n').map(f => f.trim()).filter(f => f),
showFiletree: document.getElementById('designer-show-filetree').checked
};
this.updatePreview();
@ -439,7 +456,7 @@ class EmbedDesigner {
if (this.config.lightColor !== '#3b82f6') params.set('lightColor', this.config.lightColor);
if (this.config.darkColor !== '#60a5fa') params.set('darkColor', this.config.darkColor);
if (this.config.themeMode !== 'auto') params.set('themeMode', this.config.themeMode);
if (this.config.filetree && this.config.filetree.length > 0) params.set('filetree', encodeURIComponent(this.config.filetree.join('\n')));
if (this.config.showFiletree && this.config.filetree && this.config.filetree.length > 0) params.set('filetree', encodeURIComponent(this.config.filetree.join('\n')));
params.set('preview', 'true');
return `/embed-preview/?${params.toString()}`;
@ -456,7 +473,7 @@ class EmbedDesigner {
if (this.config.lightColor !== '#3b82f6') params.set('lightColor', this.config.lightColor);
if (this.config.darkColor !== '#60a5fa') params.set('darkColor', this.config.darkColor);
if (this.config.themeMode !== 'auto') params.set('themeMode', this.config.themeMode);
if (this.config.filetree && this.config.filetree.length > 0) params.set('filetree', encodeURIComponent(this.config.filetree.join('\n')));
if (this.config.showFiletree && this.config.filetree && this.config.filetree.length > 0) params.set('filetree', encodeURIComponent(this.config.filetree.join('\n')));
return `${window.location.origin}/embed-preview/?${params.toString()}`;
}
@ -537,42 +554,77 @@ class EmbedDesigner {
}, duration);
}
loadExample() {
// Set example values
loadVibeExample() {
// Set vibe coding example values
document.getElementById('designer-prompt').value =
`You are a senior React developer. I need help building a modern e-commerce product listing component.
`I'm working on a React e-commerce app. The current ProductList component is getting messy with too much logic.
Requirements:
- Use React hooks and functional components
- Implement product filtering by category and price range
- Add smooth animations for product cards
- Make it fully responsive with a grid layout
- Include loading states and error handling
Can you help me refactor it to:
1. Extract the filtering logic into a custom hook
2. Split the large component into smaller, reusable pieces
3. Add proper TypeScript types
4. Implement virtualization for better performance with large product lists
The component should fetch data from a REST API and display products with images, titles, prices, and ratings. Please provide clean, well-commented code following React best practices.`;
The component currently handles products display, filtering by category/price, sorting, and pagination all in one file. I want a cleaner architecture.`;
document.getElementById('designer-context').value = '@codebase, ProductList.jsx';
document.getElementById('designer-context').value = '@codebase, ProductList.tsx, components/, hooks/';
document.getElementById('designer-filetree').value =
`src/components/ProductList.jsx*
src/components/ProductCard.jsx
src/components/Filters.jsx
src/hooks/useProducts.js
src/api/products.js
src/styles/products.css
src/utils/formatters.js
public/index.html
`src/
src/components/
src/components/ProductList.tsx*
src/components/ProductCard.tsx
src/components/ProductGrid.tsx
src/components/Filters/
src/components/Filters/CategoryFilter.tsx
src/components/Filters/PriceRangeFilter.tsx
src/components/Filters/index.tsx
src/hooks/
src/hooks/useProducts.ts
src/hooks/useFilters.ts
src/hooks/useInfiniteScroll.ts
src/types/
src/types/product.ts
src/api/
src/api/products.ts
src/utils/
src/utils/formatters.ts
package.json
README.md`;
tsconfig.json`;
// Set some example settings
document.getElementById('designer-model').value = 'Claude 3.7 Sonnet';
// Set vibe coding settings
document.getElementById('designer-model').value = 'Claude 4 Opus';
document.getElementById('designer-mode-select').value = 'agent';
document.getElementById('designer-thinking').checked = true;
document.getElementById('designer-max').checked = true;
document.getElementById('designer-show-filetree').checked = true;
// Update config from form
this.updateConfigFromForm();
this.showNotification('Example loaded!');
this.showNotification('Vibe coding example loaded!');
}
loadChatExample() {
// Set chat example values
document.getElementById('designer-prompt').value =
`I'm planning a dinner party for 8 people this weekend. One person is vegetarian, another is gluten-free, and I want to make something impressive but not too complicated.
Can you suggest a menu with appetizers, main course, and dessert that would work for everyone? I have about 4 hours to prepare everything and a moderate cooking skill level.`;
document.getElementById('designer-context').value = '#Screenshot Kitchen Layout.png, Recipes Collection.pdf';
document.getElementById('designer-filetree').value = '';
// Set chat settings
document.getElementById('designer-model').value = 'GPT 4o';
document.getElementById('designer-mode-select').value = 'chat';
document.getElementById('designer-thinking').checked = false;
document.getElementById('designer-max').checked = false;
document.getElementById('designer-show-filetree').checked = false;
// Update config from form
this.updateConfigFromForm();
this.showNotification('Chat example loaded!');
}
}