Files
shopify-ai-backup/chat/public/builder.html
southseact-3d c30d2ba715 Fix scroll-to-bottom in builder page
- Simplified scrollChatToBottom() with more reliable techniques
- Added scrollIntoView on last message as fallback
- Added additional scroll with direct scrollTop assignment after delay
- Added CSS optimizations for scrolling behavior (-webkit-overflow-scrolling, transform, will-change)
- Added more robust scroll attempts in renderMessages after DOM updates
2026-02-08 17:16:07 +00:00

2695 lines
95 KiB
HTML

<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Plugin Compass - WordPress Plugin AI Builder</title>
<link rel="icon" type="image/png" href="/assets/Plugin.png">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link
href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600&family=Inter:wght@400;600&display=swap"
rel="stylesheet">
<link rel="stylesheet" href="/chat/styles.css">
<style>
:root {
--shopify-green: #008060;
--shopify-green-dark: #004c3f;
--shopify-green-light: #e3f5ef;
--accent: #008060;
--accent-2: #004c3f;
--ink: #0f172a;
}
body {
background: radial-gradient(circle at 10% 20%, rgba(0, 128, 96, 0.06), transparent 25%),
radial-gradient(circle at 90% 10%, rgba(0, 76, 63, 0.05), transparent 28%),
linear-gradient(135deg, #f7f9fb 0%, #edf1f5 100%);
color: var(--ink);
font-family: 'Space Grotesk', 'Inter', system-ui, -apple-system, sans-serif;
}
/* Builder page must override the global 2-col .app-shell grid */
.app-shell.builder-single {
max-width: 1180px;
margin: 0 auto;
padding: 18px 16px 28px;
display: flex !important;
flex-direction: column !important;
gap: 16px;
min-height: 100vh;
grid-template-columns: none !important;
grid-auto-flow: initial !important;
}
.chat-wrap {
display: flex;
flex: 1;
flex-direction: column;
min-height: 0;
overflow: hidden;
}
.chat-area {
flex: 1;
overflow-y: auto;
min-height: 0;
-webkit-overflow-scrolling: touch;
scroll-behavior: auto;
transform: translateZ(0);
will-change: scroll-position;
}
.app-shell.builder-single>* {
width: 100%;
}
.builder-header {
background: rgba(255, 255, 255, 0.98);
backdrop-filter: blur(10px);
border: 1px solid var(--border);
border-radius: 16px;
padding: 12px 18px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.08);
flex-wrap: wrap;
margin-bottom: 16px;
}
.top-left-actions {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 12px;
}
.header-left {
display: flex;
align-items: center;
gap: 20px;
flex-wrap: wrap;
}
.main-actions {
display: flex;
align-items: center;
gap: 8px;
}
.brand-pack {
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
.brand-mark {
background: linear-gradient(135deg, var(--shopify-green), var(--shopify-green-dark)) !important;
font-size: 18px !important;
color: #fff;
width: 40px;
height: 40px;
border-radius: 12px;
display: grid;
place-items: center;
font-weight: 700;
letter-spacing: 0.5px;
}
.brand-copy {
display: flex;
flex-direction: column;
gap: 2px;
}
.brand-title {
font-size: 18px;
font-weight: 700;
letter-spacing: -0.01em;
}
.brand-sub {
color: var(--muted);
font-size: 13px;
}
.header-actions {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.user-badge {
display: inline-flex;
align-items: center;
gap: 10px;
padding: 8px 10px;
border: 1px solid var(--border);
border-radius: 12px;
background: #fff;
text-decoration: none;
color: inherit;
min-width: 0;
cursor: pointer;
transition: all 0.2s;
}
.user-badge:hover {
border-color: var(--shopify-green);
background: #f8f9fa;
}
.user-menu-container {
position: relative;
display: inline-block;
}
.user-menu-popup {
position: absolute;
top: calc(100% + 8px);
right: 0;
background: #fff;
border: 1px solid #e9ecef;
border-radius: 12px;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
z-index: 1000;
min-width: 180px;
display: none;
padding: 6px;
animation: menuFadeIn 0.2s ease-out;
}
@keyframes menuFadeIn {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.user-menu-popup.active {
display: block;
}
.user-menu-item {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 12px;
text-decoration: none;
color: #495057;
font-size: 14px;
font-weight: 500;
border-radius: 8px;
transition: all 0.2s;
}
.user-menu-item:hover {
background: #f8f9fa;
color: var(--shopify-green);
}
.user-menu-item svg {
color: #6c757d;
}
.user-menu-item:hover svg {
color: var(--shopify-green);
}
.user-avatar {
width: 36px;
height: 36px;
border-radius: 999px;
background: var(--shopify-green-light);
color: var(--shopify-green);
display: grid;
place-items: center;
font-weight: 700;
letter-spacing: 0.02em;
text-transform: uppercase;
flex-shrink: 0;
}
.user-meta {
display: flex;
flex-direction: column;
gap: 2px;
min-width: 0;
}
.user-status {
font-size: 11px;
color: var(--muted);
text-transform: uppercase;
letter-spacing: 0.04em;
}
.user-email {
font-weight: 700;
font-size: 13px;
color: var(--ink);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 180px;
}
.queue-indicator {
background: var(--shopify-green-light);
color: var(--shopify-green);
border: 1px solid rgba(0, 128, 96, 0.25);
padding: 8px 12px;
border-radius: 10px;
font-weight: 600;
letter-spacing: 0.01em;
}
.action-link {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 10px 12px;
border-radius: 10px;
font-weight: 600;
border: 1px solid var(--border);
background: #fff;
color: var(--ink);
text-decoration: none;
transition: transform 0.15s ease, box-shadow 0.2s ease, border-color 0.2s ease;
}
.action-link:hover {
transform: translateY(-1px);
border-color: rgba(0, 128, 96, 0.4);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.04);
}
.primary {
background: linear-gradient(135deg, var(--shopify-green), var(--shopify-green-dark));
color: #fff;
border: none;
}
.primary:hover {
box-shadow: 0 10px 25px rgba(0, 128, 96, 0.25);
}
/* Cancel mode for send button */
.primary.cancel-mode {
background: linear-gradient(135deg, #ef4444, #dc2626);
color: #fff;
}
.primary.cancel-mode:hover {
box-shadow: 0 10px 25px rgba(239, 68, 68, 0.25);
}
/* Cancelled message styling */
.message.assistant.cancelled {
opacity: 0.7;
}
.message.assistant.cancelled .body {
background: #fef2f2;
border: 1px solid #fecaca;
}
.back-home {
color: var(--muted);
text-decoration: none;
font-weight: 600;
font-size: 13px;
padding: 6px 10px;
border-radius: 10px;
border: 1px dashed var(--border);
transition: color 0.2s ease, border-color 0.2s ease;
}
.back-home:hover {
color: var(--shopify-green);
border-color: rgba(0, 128, 96, 0.4);
}
.info-row {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
gap: 12px;
}
.mode-card {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 12px;
padding: 14px 16px;
border-radius: 14px;
border: 1px solid rgba(0, 128, 96, 0.28);
background: linear-gradient(135deg, rgba(0, 128, 96, 0.1), rgba(0, 76, 63, 0.06));
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.05);
}
.mode-main {
display: flex;
align-items: center;
gap: 8px;
font-weight: 700;
color: var(--shopify-green);
letter-spacing: 0.02em;
}
.pill {
display: inline-flex;
align-items: center;
gap: 6px;
background: rgba(0, 128, 96, 0.12);
color: var(--shopify-green);
padding: 6px 10px;
border-radius: 999px;
font-size: 12px;
font-weight: 700;
letter-spacing: 0.01em;
}
.mode-badge {
align-self: flex-start;
background: #fff;
border: 1px dashed rgba(0, 128, 96, 0.3);
color: var(--shopify-green);
padding: 6px 10px;
border-radius: 10px;
font-weight: 600;
}
.panel.meta-panel {
background: #fff;
border-radius: 14px;
border: 1px solid var(--border);
padding: 12px 14px;
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.05);
}
.meta-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
gap: 10px 12px;
}
.section-title {
font-size: 18px;
font-weight: 700;
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 10px;
}
.shopify-badge {
background: var(--shopify-green-light);
color: var(--shopify-green);
padding: 4px 10px;
border-radius: 6px;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.02em;
}
.chat-area {
background: #fff;
border-radius: 14px;
border: 1px solid var(--border);
box-shadow: 0 15px 40px rgba(0, 0, 0, 0.06);
}
.composer {
background: #fff;
border-radius: 14px;
border: 1px solid var(--border);
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.05);
padding: 14px 16px;
position: sticky;
bottom: 0;
z-index: 5;
margin-bottom: 20px;
}
.prompt-templates {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 10px;
}
.prompt-template {
background: #f5faf8;
border: 1px solid rgba(0, 128, 96, 0.22);
color: var(--shopify-green);
padding: 6px 12px;
border-radius: 10px;
font-size: 12px;
cursor: pointer;
transition: all 0.2s ease;
font-weight: 600;
}
.prompt-template:hover {
background: rgba(0, 128, 96, 0.16);
border-color: rgba(0, 128, 96, 0.5);
transform: translateY(-1px);
box-shadow: 0 2px 10px rgba(0, 128, 96, 0.15);
}
/* Model selector styles to match the page */
.model-select {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 8px 10px;
border-radius: 10px;
background: #fff;
border: 1px solid var(--border);
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.04);
cursor: pointer;
transition: all 0.15s ease;
}
.model-select:hover {
transform: translateY(-1px);
border-color: rgba(0, 128, 96, 0.18);
box-shadow: 0 12px 28px rgba(0, 0, 0, 0.06);
}
/* Wrapper for inline model selector placed next to the usage meter */
.model-select-wrap {
margin-left: auto;
position: relative;
z-index: 2000; /* ensure dropdown overlays the composer */
}
.model-select-input {
-webkit-appearance: none;
appearance: none;
background: transparent;
border: none;
padding: 6px 8px;
font-weight: 700;
color: var(--ink);
}
.model-select-caret {
opacity: 0.8;
pointer-events: none;
}
/* Custom model dropdown styles */
.model-select-btn {
display: flex;
align-items: center;
width: 100%;
}
.model-select-btn:hover {
background: rgba(0, 128, 96, 0.05);
}
.model-select-dropdown {
position: absolute;
top: calc(100% + 8px);
left: 0;
background: #fff;
border: 1px solid var(--border);
border-radius: 10px;
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.12);
padding: 8px;
z-index: 1000;
min-width: 280px;
max-width: 350px;
max-height: 300px;
overflow-y: auto;
}
.model-select-options {
display: flex;
flex-direction: column;
gap: 4px;
}
.model-option {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 12px;
border-radius: 8px;
cursor: pointer;
transition: all 0.15s ease;
border: 1px solid transparent;
}
.model-option:hover {
background: rgba(0, 128, 96, 0.08);
border-color: rgba(0, 128, 96, 0.2);
}
.model-option.selected {
background: rgba(0, 128, 96, 0.12);
border-color: rgba(0, 128, 96, 0.3);
}
.model-option img {
width: 20px;
height: 20px;
border-radius: 4px;
flex-shrink: 0;
object-fit: contain;
}
.model-option-text {
flex: 1;
font-weight: 600;
color: var(--ink);
min-width: 0;
}
.model-option.disabled {
opacity: 0.5;
cursor: not-allowed;
pointer-events: none;
}
/* Inline blurred dropdown for starter plan */
.model-preview-dropdown {
position: absolute;
top: calc(100% + 8px);
left: 0;
background: #fff;
border: 1px solid var(--border);
border-radius: 10px;
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.12);
padding: 12px;
z-index: 1000;
min-width: 260px;
}
.model-preview-item {
padding: 10px 12px;
border-radius: 8px;
border: 1px solid #e5e7eb;
filter: blur(4px);
pointer-events: none;
color: var(--muted);
margin-bottom: 8px;
}
.model-preview-item:last-child {
margin-bottom: 0;
}
.usage-meter {
flex: 1;
min-width: 220px;
max-width: 520px;
display: flex;
flex-direction: column;
gap: 6px;
padding: 8px 10px;
border-radius: 12px;
border: 1px solid rgba(0, 128, 96, 0.18);
background: #f8fffc;
}
.usage-meter-actions {
display: flex;
align-items: center;
gap: 12px;
margin-top: 8px;
padding-top: 8px;
border-top: 1px solid rgba(0, 128, 96, 0.08);
}
.usage-hint {
font-size: 11px;
color: var(--muted);
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.02em;
}
.usage-link {
font-size: 12px;
font-weight: 700;
color: var(--shopify-green);
text-decoration: none;
transition: all 0.2s;
display: flex;
align-items: center;
gap: 4px;
}
.usage-link:hover {
color: var(--shopify-green-dark);
transform: translateY(-1px);
}
.usage-link svg {
transition: transform 0.2s;
}
.usage-link:hover svg {
transform: translateX(2px);
}
.usage-meter-header {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 10px;
}
.usage-meter-title {
font-size: 12px;
color: var(--muted);
font-weight: 700;
letter-spacing: 0.02em;
}
.usage-meter-percent {
font-size: 12px;
font-weight: 700;
color: var(--shopify-green);
white-space: nowrap;
line-height: 1.2;
text-align: right;
}
.usage-meter-track {
height: 8px;
background: rgba(0, 128, 96, 0.12);
border-radius: 999px;
overflow: hidden;
}
.usage-meter-fill {
height: 100%;
width: 0%;
background: linear-gradient(135deg, var(--shopify-green), var(--shopify-green-dark));
border-radius: 999px;
transition: width 0.25s ease;
}
.input-row {
display: flex;
gap: 10px;
}
#message-input {
font-size: 16px;
}
#session-list {
display: none !important;
}
#new-chat,
#history-btn {
display: inline-flex;
align-items: center;
gap: 8px;
}
#history-btn {
background: #fff;
}
.history-modal-content {
max-width: 560px;
}
.history-list {
display: flex;
flex-direction: column;
gap: 10px;
max-height: 420px;
overflow: auto;
margin-top: 12px;
}
.history-item {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 12px 14px;
border-radius: 12px;
border: 1px solid var(--border);
background: #fff;
cursor: pointer;
transition: all 0.15s ease;
}
.history-item:hover {
border-color: rgba(0, 128, 96, 0.4);
box-shadow: 0 8px 22px rgba(0, 0, 0, 0.06);
transform: translateY(-1px);
}
.history-item.active {
border-color: var(--shopify-green);
background: rgba(0, 128, 96, 0.06);
}
.history-title {
font-weight: 700;
color: var(--ink);
font-size: 14px;
margin-bottom: 4px;
}
.history-preview {
color: var(--muted);
font-size: 12px;
line-height: 1.4;
max-height: 34px;
overflow: hidden;
}
.history-meta {
font-size: 11px;
color: var(--muted);
white-space: nowrap;
}
.history-empty {
color: var(--muted);
text-align: center;
padding: 24px 12px;
border: 1px dashed var(--border);
border-radius: 12px;
}
.status-line {
margin-top: 8px;
color: var(--muted);
font-size: 13px;
}
.export-btn {
background: linear-gradient(135deg, #10b981, #059669);
color: white;
border: none;
border-radius: 10px;
padding: 10px 14px;
font-weight: 600;
cursor: pointer;
width: 100%;
transition: all 0.2s ease;
}
.export-btn:hover {
transform: translateY(-1px);
box-shadow: 0 8px 20px rgba(16, 185, 129, 0.25);
}
.terminal-link {
display: inline-flex;
align-items: center;
gap: 6px;
color: var(--muted);
font-size: 12px;
text-decoration: none;
padding: 6px 10px;
border-radius: 6px;
border: 1px dashed var(--border);
transition: all 0.2s ease;
}
.terminal-link:hover {
color: var(--shopify-green);
border-color: var(--shopify-green);
}
.modal {
display: none;
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.4);
backdrop-filter: blur(4px);
align-items: center;
justify-content: center;
z-index: 10000;
}
.modal-content {
background: #fff;
padding: 24px;
border-radius: 16px;
width: 100%;
max-width: 440px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.15);
border: 1px solid var(--border);
position: relative;
}
@media (max-width: 900px) {
.app-shell {
padding: 12px 10px 18px;
}
.builder-header {
flex-direction: column;
}
.header-actions {
width: 100%;
justify-content: flex-start;
}
.composer {
position: static;
margin-bottom: 60px;
}
}
@media (max-width: 640px) {
.top-left-actions {
position: sticky;
top: 0;
z-index: 1000;
background: rgba(247, 249, 251, 0.8);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
padding: 10px 16px;
margin-bottom: 0;
width: 100%;
box-sizing: border-box;
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
}
.app-shell.builder-single {
padding-top: 8px;
}
.input-row {
flex-direction: row !important;
flex-wrap: nowrap !important;
align-items: flex-end;
gap: 8px;
}
#message-input {
flex: 1;
min-height: 48px;
min-width: 0;
}
#mini-send-btn {
height: 48px;
width: 48px;
padding: 0;
flex-shrink: 0;
}
.composer {
margin-bottom: 60px;
padding: 12px;
}
.prompt-templates {
flex-direction: column;
align-items: stretch !important;
}
.composer-top-actions {
width: 100%;
justify-content: space-between;
margin-left: 0 !important;
margin-top: 8px;
}
.model-select-wrap {
flex: 1;
}
#upload-media-btn {
flex-shrink: 0;
}
/* Model selector option styles */
.model-select-dropdown { max-height: 360px; overflow: auto; }
.model-option { display:flex; align-items:center; gap:8px; padding:8px 12px; cursor:pointer; border-radius:8px; }
.model-option.disabled { opacity: 0.6; cursor:default; }
.model-option img { width:20px; height:20px; border-radius:4px; flex-shrink:0; }
.model-option-text { font-weight:700; color:var(--ink); }
.model-option-multiplier { margin-left:auto; background:#f5f7f9; color:var(--muted); padding:4px 8px; border-radius:999px; font-weight:700; font-size:12px; }
#model-select-multiplier { margin-left:8px; padding:2px 8px; background:#f5f7f9; border-radius:999px; font-weight:700; font-size:12px; color:var(--muted); display:none; }
/* Todo container styles */
.todo-container {
margin: 16px 0;
padding: 16px;
background: #f8fffc;
border: 1px solid rgba(0, 128, 96, 0.2);
border-radius: 12px;
}
.todo-container .todo-item {
display: flex;
align-items: flex-start;
gap: 10px;
padding: 10px 12px;
margin-bottom: 8px;
border-radius: 8px;
background: #fff;
border: 1px solid var(--border);
transition: all 0.2s ease;
}
.todo-container .todo-item:last-child {
margin-bottom: 0;
}
.todo-container .todo-status-icon {
flex-shrink: 0;
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
}
.todo-container .todo-content {
flex: 1;
font-size: 14px;
line-height: 1.4;
color: var(--ink);
}
/* Status-specific styles */
.todo-status-completed {
border-color: rgba(16, 185, 129, 0.3) !important;
background: rgba(16, 185, 129, 0.05) !important;
}
.todo-status-completed .todo-status-icon {
color: #10b981;
}
.todo-status-in_progress {
border-color: rgba(245, 158, 11, 0.3) !important;
background: rgba(245, 158, 11, 0.05) !important;
}
.todo-status-in_progress .todo-status-icon {
color: #f59e0b;
}
.todo-status-cancelled {
opacity: 0.6;
text-decoration: line-through;
}
.todo-status-cancelled .todo-status-icon {
color: #6b7280;
}
.todo-status-pending .todo-status-icon {
color: #9ca3af;
}
/* Priority badges */
.todo-priority-high {
background: #fee2e2;
color: #dc2626;
}
.todo-priority-low {
background: #dbeafe;
color: #2563eb;
}
}
</style>
<!-- PostHog Analytics -->
<script src="/posthog.js"></script>
</head>
<body>
<div class="top-left-actions">
<button id="new-chat" class="primary action-link" title="Start a new chat">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round">
<line x1="12" y1="5" x2="12" y2="19"></line>
<line x1="5" y1="12" x2="19" y2="12"></line>
</svg>
New chat
</button>
<button id="history-btn" class="action-link" title="Chat history">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round">
<path d="M3 12a9 9 0 1 0 3-6.7"></path>
<polyline points="3 4 3 10 9 10"></polyline>
<path d="M12 7v5l4 2"></path>
</svg>
History
</button>
</div>
<div class="app-shell builder-single">
<header class="builder-header">
<div class="header-left">
<div class="brand-pack">
<img src="/assets/Plugin.png" alt="WP" style="width: 40px; height: 40px; border-radius: 12px;">
<div class="brand-copy">
<div class="brand-title">Plugin Compass</div>
<div class="brand-sub">Plan, build, and ship in one view</div>
</div>
<a href="/apps" class="back-home">Back to My Plugins</a>
</div>
</div>
<div class="header-actions">
<button id="upgrade-header-btn" class="primary action-link" title="Upgrade your plan">
Upgrade
</button>
<div class="user-menu-container">
<div class="user-badge" id="user-badge" title="Account & settings">
<div class="user-avatar" id="user-avatar">?</div>
<div class="user-meta">
<div class="user-status" id="user-plan">Checking plan…</div>
<div class="user-email" id="user-email">Checking account…</div>
</div>
</div>
<div class="user-menu-popup" id="user-menu-popup">
<a href="/settings" class="user-menu-item">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="3"></circle>
<path
d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z">
</path>
</svg>
Settings
</a>
<a href="/topup" class="user-menu-item">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10"></circle>
<path d="M16 8h-6a2 2 0 1 0 0 4h4a2 2 0 1 1 0 4H8"></path>
<path d="M12 18V6"></path>
</svg>
Tokens Top-up
</a>
<div style="margin: 4px 0; border-top: 1px solid #f1f3f5;"></div>
<a href="#" class="user-menu-item" id="logout-link">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round">
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"></path>
<polyline points="16 17 21 12 16 7"></polyline>
<line x1="21" y1="12" x2="9" y2="12"></line>
</svg>
Logout
</a>
</div>
</div>
<button class="primary action-link" id="export-zip-btn" title="Download as ZIP">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"></path>
<polyline points="7,10 12,15 17,10"></polyline>
<line x1="12" y1="15" x2="12" y2="3"></line>
</svg>
Download ZIP
</button>
</div>
</header>
<section class="chat-wrap">
<div class="section-title" id="chat-title">New Project<span class="shopify-badge">WordPress</span></div>
<section class="chat-area" id="chat-area"></section>
</section>
<div class="composer">
<div class="prompt-templates" style="align-items:center; display:flex; gap:12px; flex-wrap: wrap;">
<div id="usage-meter" class="usage-meter" aria-label="Usage this month">
<div class="usage-meter-header">
<span id="usage-meter-title" class="usage-meter-title">Usage</span>
<span id="usage-meter-percent" class="usage-meter-percent"></span>
</div>
<div id="usage-meter-track" class="usage-meter-track" role="progressbar" aria-valuemin="0" aria-valuemax="100"
aria-valuenow="0">
<div id="usage-meter-fill" class="usage-meter-fill" style="width:0%;"></div>
</div>
<div class="usage-meter-actions">
<span class="usage-hint">Want more credits?</span>
<a href="/topup" class="usage-link">
Purchase more
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3"
stroke-linecap="round" stroke-linejoin="round">
<polyline points="9 18 15 12 9 6"></polyline>
</svg>
</a>
<a href="/upgrade?source=usage_limit" class="usage-link">
Upgrade
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3"
stroke-linecap="round" stroke-linejoin="round">
<polyline points="9 18 15 12 9 6"></polyline>
</svg>
</a>
</div>
</div>
<div class="composer-top-actions" style="display: flex; align-items: center; gap: 8px; margin-left: auto;">
<!-- Model Selector -->
<div id="model-select-wrap" class="model-select-wrap" style="position: relative; display: inline-flex; align-items: center; gap: 8px;">
<!-- Hidden select for backward compatibility -->
<select id="model-select" style="display: none;">
<option value="auto">Auto (admin managed)</option>
</select>
<!-- Custom dropdown button -->
<div id="model-select-btn" class="model-select" role="button" aria-haspopup="listbox" aria-expanded="false" style="display: flex; align-items: center; gap: 8px; padding: 8px 10px; border-radius: 10px; background: #fff; border: 1px solid var(--border); box-shadow: 0 8px 20px rgba(0, 0, 0, 0.04); cursor: pointer; transition: all 0.15s ease;">
<img id="model-icon" src="" alt="" style="width: 20px; height: 20px; display: none;" />
<span id="model-select-text">Select model</span>
<span id="model-select-multiplier" style="font-size: 12px; font-weight: 700; color: var(--shopify-green); display: none;"></span>
<svg class="model-select-caret" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="opacity: 0.8; pointer-events: none;">
<polyline points="6 9 12 15 18 9"></polyline>
</svg>
</div>
<!-- Custom dropdown (opens downward to overlay the composer) -->
<div id="model-select-dropdown" class="model-select-dropdown" style="position: absolute; top: calc(100% + 8px); left: 0; background: #fff; border: 1px solid var(--border); border-radius: 10px; box-shadow: 0 12px 40px rgba(0, 0, 0, 0.12); padding: 8px; z-index: 2000; min-width: 280px; max-width: 350px; max-height: 400px; overflow-y: auto; display: none; pointer-events: auto;">
<div id="model-select-options" class="model-select-options" style="display: flex; flex-direction: column; gap: 4px;">
<!-- Options will be populated by JavaScript -->
</div>
</div>
</div>
<button id="upload-media-btn" class="ghost" title="Attach images" style="align-items: center; justify-content: center; width: 40px; height: 40px; padding: 0; border-radius: 8px; border: 1px solid var(--border); background: #fff;">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48"></path>
</svg>
</button>
<input id="upload-media-input" type="file" accept="image/*" multiple style="display:none" />
</div>
</div>
<div class="input-row" style="display: flex; align-items: flex-end; gap: 10px;">
<textarea id="message-input" rows="1" style="flex: 1; min-height: 48px;"
placeholder="Describe the WordPress plugin you want to build..."></textarea>
<button id="mini-send-btn" class="primary" title="Send message"
style="padding: 12px; border-radius: 12px; height: 48px; width: 48px; display: flex; align-items: center; justify-content: center; flex-shrink: 0;">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round">
<line x1="22" y1="2" x2="11" y2="13"></line>
<polygon points="22 2 15 22 11 13 2 9 22 2"></polygon>
</svg>
</button>
</div>
<div id="attachment-preview" class="attachment-preview" style="display:none"></div>
<div id="status-line" class="status-line" style="display:none"></div>
<div id="status-line-admin" class="status-line" style="display:none"></div>
</div>
</div>
<!-- Chat History Modal -->
<div id="history-modal" class="modal">
<div class="modal-content history-modal-content">
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:12px;">
<div style="display:flex; align-items:center; gap:10px;">
<div style="width:32px; height:32px; border-radius:10px; background:linear-gradient(135deg, var(--shopify-green), var(--shopify-green-dark)); display:flex; align-items:center; justify-content:center;">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M3 12a9 9 0 1 0 3-6.7"></path>
<polyline points="3 4 3 10 9 10"></polyline>
<path d="M12 7v5l4 2"></path>
</svg>
</div>
<strong style="color:var(--ink); font-size:20px; font-weight:700;">Chat history</strong>
</div>
<button id="history-close" style="border:none; background:transparent; color:var(--muted); cursor:pointer; font-size:20px; display:flex; align-items:center; justify-content:center; width:32px; height:32px; border-radius:8px; transition:all 0.2s ease;">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</div>
<p style="color:var(--muted); margin-bottom:12px; line-height:1.6; font-size:14px;">Switch between previous chats or start a new one.</p>
<div id="history-list" class="history-list"></div>
<div id="history-empty" class="history-empty" style="display:none;">No previous chats yet.</div>
</div>
</div>
<!-- Export Modal -->
<div id="export-modal" class="modal"
style="display:none; position:fixed; inset:0; background:rgba(0,0,0,0.4); align-items:center; justify-content:center; z-index:10000;">
<div
style="background:#fff; padding:28px; border-radius:16px; width:100%; max-width:480px; box-shadow:0 20px 60px rgba(0,0,0,0.15); border:1px solid var(--border); position:relative;">
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:20px;">
<div style="display:flex; align-items:center; gap:10px;">
<div
style="width:32px; height:32px; border-radius:10px; background:linear-gradient(135deg, var(--shopify-green), var(--shopify-green-dark)); display:flex; align-items:center; justify-content:center;">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round">
<path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"></path>
<polyline points="7,10 12,15 17,10"></polyline>
<line x1="12" y1="15" x2="12" y2="3"></line>
</svg>
</div>
<strong style="color:var(--ink); font-size:20px; font-weight:700;">Export Your WordPress Plugin</strong>
</div>
<button id="export-close"
style="border:none; background:transparent; color:var(--muted); cursor:pointer; font-size:20px; display:flex; align-items:center; justify-content:center; width:32px; height:32px; border-radius:8px; transition:all 0.2s ease;">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</div>
<p style="color:var(--muted); margin-bottom:24px; line-height:1.6; font-size:15px;">Your WordPress plugin is ready
to be exported. Click the button below to download it as a ZIP file.</p>
<div style="display:flex; flex-direction:column; gap:16px;">
<button id="download-zip" class="export-btn"
style="display:flex; align-items:center; justify-content:center; gap:10px; padding:14px 20px; font-size:16px; font-weight:700; border-radius:12px; transition:all 0.2s ease;">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round">
<path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"></path>
<polyline points="7,10 12,15 17,10"></polyline>
<line x1="12" y1="15" x2="12" y2="3"></line>
</svg>
Download as ZIP
</button>
</div>
<div id="export-status"
style="margin-top:20px; padding:16px; background:var(--shopify-green-light); border-radius:12px; display:none; border:1px solid rgba(0, 128, 96, 0.25);">
<div style="color:var(--shopify-green); font-weight:600; display:flex; align-items:center; gap:8px;"
id="export-status-text">
<div
style="width:16px; height:16px; border:2px solid var(--shopify-green); border-top:2px solid transparent; border-radius:50%; animation:spin 1s linear infinite;">
</div>
Preparing export...
</div>
</div>
</div>
</div>
<style>
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
#export-close:hover {
background: rgba(0, 128, 96, 0.1);
color: var(--shopify-green);
}
#history-close:hover {
background: rgba(0, 128, 96, 0.1);
color: var(--shopify-green);
}
#download-zip {
background: linear-gradient(135deg, var(--shopify-green), var(--shopify-green-dark));
color: white;
border: none;
font-weight: 700;
}
#download-zip:hover {
transform: translateY(-1px);
box-shadow: 0 10px 25px rgba(0, 128, 96, 0.25);
}
#download-zip:active {
transform: translateY(0);
}
#download-zip:disabled {
opacity: 0.7;
cursor: not-allowed;
transform: none;
box-shadow: none;
}
/* Upgrade Modal Styles */
#upgrade-close:hover {
background: rgba(0, 128, 96, 0.1);
color: var(--shopify-green);
}
#upgrade-btn {
background: linear-gradient(135deg, var(--shopify-green), var(--shopify-green-dark));
color: white;
border: none;
font-weight: 700;
}
#upgrade-btn:hover {
transform: translateY(-1px);
box-shadow: 0 10px 25px rgba(0, 128, 96, 0.25);
}
#upgrade-btn:active {
transform: translateY(0);
}
</style>
<!-- Onboarding Modal -->
<div id="onboarding-modal" class="onboarding-modal"
style="display:none; position:fixed; inset:0; background:transparent; z-index:10001; transition: all 0.3s ease;">
<div id="onboarding-container" class="onboarding-container"
style="background:#fff; border-radius:12px; width:280px; box-shadow:0 20px 50px rgba(0,0,0,0.2); position:fixed; overflow:visible; display:flex; flex-direction:column; transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);">
<!-- Tooltip Pointer -->
<div id="onboarding-pointer" style="position:absolute; width:16px; height:16px; background:#fff; transform:rotate(45deg); z-index:-1; display:none;"></div>
<button id="onboarding-exit"
style="position:absolute; top:8px; right:8px; border:none; background:rgba(255,255,255,0.8); color:#9ca3af; cursor:pointer; font-size:14px; width:24px; height:24px; border-radius:50%; display:flex; align-items:center; justify-content:center; z-index:100; transition:all 0.2s ease;">
</button>
<div id="onboarding-content" class="onboarding-content" style="padding:16px 18px 12px;">
<div id="onboarding-step-1" class="onboarding-step">
<h2 style="font-size:15px; font-weight:700; color:#0f172a; margin-bottom:6px;">Welcome!</h2>
<p style="color:#64748b; font-size:13px; line-height:1.4; margin-bottom:0;">
Build custom WordPress plugins with AI. Let's take a quick tour of your new builder.
</p>
</div>
<div id="onboarding-step-2" class="onboarding-step" style="display:none;">
<h2 style="font-size:15px; font-weight:700; color:#0f172a; margin-bottom:6px;">Chat with AI</h2>
<p style="color:#64748b; font-size:13px; line-height:1.4; margin-bottom:0;">
Describe the plugin you want to build. Our AI understands WordPress and creates complete code.
</p>
</div>
<div id="onboarding-step-3" class="onboarding-step" style="display:none;">
<h2 style="font-size:15px; font-weight:700; color:#0f172a; margin-bottom:6px;">Select Model</h2>
<p style="color:#64748b; font-size:13px; line-height:1.4; margin-bottom:0;">
Choose between different AI models. Each has unique strengths for specific types of logic.
</p>
</div>
<div id="onboarding-step-4" class="onboarding-step" style="display:none;">
<h2 style="font-size:15px; font-weight:700; color:#0f172a; margin-bottom:6px;">Images & Assets</h2>
<p style="color:#64748b; font-size:13px; line-height:1.4; margin-bottom:0;">
Upload UI mockups or screenshots. The AI can "see" your designs and help implement them.
</p>
</div>
<div id="onboarding-step-5" class="onboarding-step" style="display:none;">
<h2 style="font-size:15px; font-weight:700; color:#0f172a; margin-bottom:6px;">Ready to Build?</h2>
<p style="color:#64748b; font-size:13px; line-height:1.4; margin-bottom:0;">
Once you approves a plan, the builder will generate the full plugin for you to download.
</p>
</div>
</div>
<div class="onboarding-footer" style="padding:0 18px 14px; display:flex; align-items:center; justify-content:space-between;">
<button id="onboarding-back"
style="padding:6px 0; font-size:13px; font-weight:600; border:none; background:transparent; color:#94a3b8; cursor:pointer; display:none;">
Back
</button>
<div id="onboarding-step-counter" style="font-size:12px; font-weight:600; color:#94a3b8; flex:1; text-align:center;">
1 of 5
</div>
<button id="onboarding-next"
style="padding:6px 12px; font-size:13px; font-weight:700; border-radius:8px; border:none; background:#008060; color:#fff; cursor:pointer; transition:all 0.2s ease;">
Next
</button>
</div>
</div>
</div>
<!-- Confirm Build Modal -->
<div id="confirm-build-modal" class="modal">
<div class="modal-content">
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:16px;">
<strong style="color:var(--ink); font-size:20px;">Proceed with Build?</strong>
<button id="confirm-build-close"
style="border:none; background:transparent; color:var(--muted); cursor:pointer; font-size:20px; display:flex; align-items:center; justify-content:center; width:24px; height:24px;">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</div>
<p style="color:var(--muted); margin-bottom:24px; line-height: 1.6; font-size: 15px;">
Are you sure you want to proceed with this plan? This will start the build process and generate code based on
the approved plan.
</p>
<div style="display:flex; gap:12px;">
<button id="confirm-build-cancel" class="action-link" style="flex:1; justify-content:center;">Cancel</button>
<button id="confirm-build-proceed" class="primary action-link" style="flex:1; justify-content:center;">Yes,
Proceed</button>
</div>
</div>
</div>
<!-- Upgrade Modal for Free Plan Upload Media -->
<div id="upgrade-modal" class="modal"
style="display:none; position:fixed; inset:0; background:rgba(0,0,0,0.4); align-items:center; justify-content:center; z-index:10000;">
<div
style="background:#fff; padding:28px; border-radius:16px; width:100%; max-width:480px; box-shadow:0 20px 60px rgba(0,0,0,0.15); border:1px solid var(--border); position:relative;">
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:20px;">
<div style="display:flex; align-items:center; gap:10px;">
<div
style="width:32px; height:32px; border-radius:10px; background:linear-gradient(135deg, var(--shopify-green), var(--shopify-green-dark)); display:flex; align-items:center; justify-content:center;">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round">
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z">
</path>
</svg>
</div>
<strong style="color:var(--ink); font-size:20px; font-weight:700;">Premium Feature</strong>
</div>
<button id="upgrade-close"
style="border:none; background:transparent; color:var(--muted); cursor:pointer; font-size:20px; display:flex; align-items:center; justify-content:center; width:32px; height:32px; border-radius:8px; transition:all 0.2s ease;">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</div>
<div style="text-align:center; margin-bottom:24px;">
<div style="font-size:48px; margin-bottom:16px;">📷</div>
<h3 style="font-size:24px; font-weight:700; color:var(--ink); margin-bottom:8px;">Upload Media</h3>
<p style="color:var(--muted); margin-bottom:24px; line-height:1.6; font-size:15px;">Upload and attach images to
your conversations with AI. This feature is available on our Professional and Enterprise plans.</p>
</div>
<div
style="background:linear-gradient(135deg, rgba(0, 128, 96, 0.1), rgba(0, 76, 63, 0.06)); border:1px solid rgba(0, 128, 96, 0.2); border-radius:12px; padding:16px; margin-bottom:24px;">
<h4 style="font-weight:700; color:var(--shopify-green); margin-bottom:8px;">What's included:</h4>
<ul style="color:var(--muted); font-size:14px; line-height:1.5; margin:0; padding-left:16px;">
<li>Upload multiple images at once</li>
<li>Drag & drop interface</li>
<li>Automatic image optimization</li>
<li>Paste images directly into chat</li>
</ul>
</div>
<div style="display:flex; flex-direction:column; gap:12px;">
<button id="upgrade-btn" class="upgrade-btn"
style="display:flex; align-items:center; justify-content:center; gap:10px; padding:14px 20px; font-size:16px; font-weight:700; border-radius:12px; transition:all 0.2s ease; background:linear-gradient(135deg, var(--shopify-green), var(--shopify-green-dark)); color:white; border:none;">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round">
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z">
</path>
</svg>
Upgrade Now
</button>
<button id="upgrade-later" class="action-link" style="flex:1; justify-content:center;">Maybe Later</button>
</div>
</div>
</div>
<!-- Token Limit Modal -->
<div id="token-limit-modal" class="modal"
style="display:none; position:fixed; inset:0; background:rgba(0,0,0,0.4); align-items:center; justify-content:center; z-index:10000;">
<div
style="background:#fff; padding:28px; border-radius:16px; width:100%; max-width:480px; box-shadow:0 20px 60px rgba(0,0,0,0.15); border:1px solid var(--border); position:relative;">
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:20px;">
<div style="display:flex; align-items:center; gap:10px;">
<div
style="width:32px; height:32px; border-radius:10px; background:linear-gradient(135deg, #f59e0b, #d97706); display:flex; align-items:center; justify-content:center;">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round">
<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"></path>
<line x1="12" y1="9" x2="12" y2="13"></line>
<line x1="12" y1="17" x2="12.01" y2="17"></line>
</svg>
</div>
<strong style="color:var(--ink); font-size:20px; font-weight:700;">Low Token Balance</strong>
</div>
<button id="token-limit-close"
style="border:none; background:transparent; color:var(--muted); cursor:pointer; font-size:20px; display:flex; align-items:center; justify-content:center; width:32px; height:32px; border-radius:8px; transition:all 0.2s ease;">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</div>
<div style="text-align:center; margin-bottom:24px;">
<div style="font-size:48px; margin-bottom:16px;">⚠️</div>
<h3 style="font-size:24px; font-weight:700; color:var(--ink); margin-bottom:8px;">Running Low on Tokens</h3>
<p style="color:var(--muted); margin-bottom:24px; line-height:1.6; font-size:15px;">You have only 5,000 tokens remaining. To continue building, please purchase more tokens or upgrade your plan.</p>
</div>
<div style="display:flex; flex-direction:column; gap:12px;">
<a id="token-limit-upgrade" href="/upgrade?source=token_limit" class="upgrade-btn"
style="display:flex; align-items:center; justify-content:center; gap:10px; padding:14px 20px; font-size:16px; font-weight:700; border-radius:12px; transition:all 0.2s ease; background:linear-gradient(135deg, var(--shopify-green), var(--shopify-green-dark)); color:white; border:none; text-decoration:none;">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round">
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z">
</path>
</svg>
Upgrade Plan
</a>
<a id="token-limit-topup" href="/topup?source=token_limit" class="action-link"
style="display:flex; align-items:center; justify-content:center; gap:10px; padding:14px 20px; font-size:16px; font-weight:700; border-radius:12px; transition:all 0.2s ease; border:1px solid var(--border); background:#fff; color:var(--ink); text-decoration:none;">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
<polyline points="7,10 12,15 17,10"></polyline>
<line x1="12" y1="15" x2="12" y2="3"></line>
</svg>
Buy Top-up
</a>
</div>
</div>
</div>
<script src="/chat/builder.js"></script>
<script>
// Plugin Builder specific functionality
// Note: builderState and updateBuildModeUI are now defined in builder.js
const userBadge = document.getElementById('user-badge');
const userBadgePlan = document.getElementById('user-plan');
const userBadgeEmail = document.getElementById('user-email');
const userBadgeAvatar = document.getElementById('user-avatar');
const userMenuPopup = document.getElementById('user-menu-popup');
const logoutLink = document.getElementById('logout-link');
if (userBadge && userMenuPopup) {
userBadge.addEventListener('click', (e) => {
e.stopPropagation();
userMenuPopup.classList.toggle('active');
});
document.addEventListener('click', (e) => {
if (!userMenuPopup.contains(e.target) && !userBadge.contains(e.target)) {
userMenuPopup.classList.remove('active');
}
});
}
if (logoutLink) {
logoutLink.addEventListener('click', async (e) => {
e.preventDefault();
try {
const response = await fetch('/api/logout', { method: 'POST' });
if (response.ok) {
localStorage.removeItem('shopify_ai_user');
localStorage.removeItem('wordpress_plugin_ai_user');
localStorage.removeItem('plugin_compass_onboarding_completed');
window.location.href = '/login';
}
} catch (err) {
console.error('Logout failed:', err);
window.location.href = '/login';
}
});
}
const MOBILE_HEADER_MQ = window.matchMedia('(max-width: 640px) and (orientation: portrait)');
function updateUserBadgeForMobile(isMobile) {
if (!userBadge) return;
const emailEl = userBadgeEmail;
const metaEl = userBadge.querySelector('.user-meta');
if (emailEl) emailEl.setAttribute('aria-hidden', isMobile ? 'true' : 'false');
if (metaEl) metaEl.setAttribute('aria-hidden', isMobile ? 'true' : 'false');
if (isMobile) {
userBadge.setAttribute('aria-label', 'Account');
document.body.classList.add('mobile-portrait');
} else {
const email = (userBadgeEmail && userBadgeEmail.textContent) || readLocalEmail();
userBadge.setAttribute('aria-label', email ? `Signed in as ${email}` : 'Account settings');
document.body.classList.remove('mobile-portrait');
}
try {
if (window.dataLayer && typeof window.dataLayer.push === 'function') {
window.dataLayer.push({ event: 'mobilePortraitHeader', mobilePortrait: isMobile });
}
fetch('/api/track', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ event: 'mobilePortraitHeader', mobilePortrait: isMobile }),
keepalive: true,
}).catch(() => { });
} catch (_) { }
}
function setUserBadgeEmail(email) {
const safeEmail = (email || '').trim();
if (userBadgeEmail) userBadgeEmail.textContent = safeEmail || 'Guest';
if (userBadgeAvatar) userBadgeAvatar.textContent = safeEmail ? safeEmail.charAt(0).toUpperCase() : '?';
try {
updateUserBadgeForMobile(MOBILE_HEADER_MQ.matches);
} catch (_) {
if (userBadge) {
userBadge.setAttribute('aria-label', safeEmail ? `Signed in as ${safeEmail}` : 'Account settings');
}
}
}
if (MOBILE_HEADER_MQ.addEventListener) {
MOBILE_HEADER_MQ.addEventListener('change', (e) => updateUserBadgeForMobile(e.matches));
} else if (MOBILE_HEADER_MQ.addListener) {
MOBILE_HEADER_MQ.addListener((e) => updateUserBadgeForMobile(e.matches));
}
updateUserBadgeForMobile(MOBILE_HEADER_MQ.matches);
function readLocalEmail() {
const keys = ['wordpress_plugin_ai_user', 'shopify_ai_user'];
for (const key of keys) {
try {
const raw = localStorage.getItem(key);
if (!raw) continue;
const parsed = JSON.parse(raw);
if (parsed?.email) return parsed.email;
} catch (_) { /* ignore */ }
}
return '';
}
async function loadUserBadge() {
console.log('[BUILDER-BADGE] loadUserBadge called');
let email = '';
let planFetched = false;
const startTime = Date.now();
try {
// Wait for builder.js to be fully loaded and provide getAccountInfo
let getAccountInfoFn = window.getAccountInfo;
// If getAccountInfo is not available yet, wait up to 1 second for it
if (typeof getAccountInfoFn !== 'function') {
await new Promise((resolve) => {
let attempts = 0;
const checkInterval = setInterval(() => {
if (typeof window.getAccountInfo === 'function' || attempts++ > 10) {
clearInterval(checkInterval);
getAccountInfoFn = window.getAccountInfo;
resolve();
}
}, 100);
});
console.log('[BUILDER-BADGE] Waited for getAccountInfo:', Date.now() - startTime, 'ms');
}
// Use shared getAccountInfo() when available, but fall back to local fetch
const accountPromise = (typeof getAccountInfoFn === 'function')
? getAccountInfoFn()
: fetch('/api/account', { credentials: 'same-origin' }).then(r => r.ok ? r.json() : null);
// Race the account fetch against a timeout to prevent hanging
const timeoutMs = 10000; // 10 seconds - increased for reliability
const timeoutPromise = new Promise((resolve) => setTimeout(() => {
console.log('[BUILDER-BADGE] Timeout reached after', timeoutMs, 'ms');
resolve(null);
}, timeoutMs));
const data = await Promise.race([accountPromise, timeoutPromise]);
console.log('[BUILDER-BADGE] Account fetch completed:', {
hasData: !!data,
hasPlan: !!data?.account?.plan,
hasEmail: !!data?.account?.email,
duration: Date.now() - startTime
});
if (data) {
email = data?.account?.email || '';
if (data?.account?.plan) {
planFetched = true;
try {
if (typeof applyAccountPlan === 'function') {
applyAccountPlan(data.account.plan);
console.log('[BUILDER-BADGE] Applied plan via applyAccountPlan:', data.account.plan);
} else if (userBadge) {
userBadge.dataset.plan = data.account.plan;
console.log('[BUILDER-BADGE] Applied plan via userBadge:', data.account.plan);
}
if (userBadgePlan && typeof formatPlanLabel === 'function') {
userBadgePlan.textContent = formatPlanLabel(data.account.plan);
}
} catch (e) {
console.warn('[BUILDER-BADGE] Failed to apply plan:', e.message);
}
}
}
// If timeout occurred (no data), apply default and continue fetching in background
if (!data) {
console.log('[BUILDER-BADGE] Data was null, applying default plan');
// Apply default plan immediately so UI doesn't hang
if (userBadgePlan && typeof formatPlanLabel === 'function') {
userBadgePlan.textContent = formatPlanLabel('hobby');
}
// Continue fetching in background and update when ready
accountPromise.then((fullData) => {
console.log('[BUILDER-BADGE] Background fetch completed:', {
hasPlan: !!fullData?.account?.plan,
hasEmail: !!fullData?.account?.email
});
if (fullData && fullData.account && fullData.account.plan) {
if (typeof applyAccountPlan === 'function') {
applyAccountPlan(fullData.account.plan);
} else if (userBadge) {
userBadge.dataset.plan = fullData.account.plan;
}
if (userBadgePlan && typeof formatPlanLabel === 'function') {
userBadgePlan.textContent = formatPlanLabel(fullData.account.plan);
}
}
if (fullData && fullData.account && fullData.account.email && !email) {
email = fullData.account.email;
setUserBadgeEmail(email);
if (userBadge) {
userBadge.setAttribute('aria-label', email ? `Signed in as ${email}` : 'Account settings');
}
}
}).catch((e) => {
console.warn('[BUILDER-BADGE] Background fetch failed:', e.message);
});
}
} catch (e) {
console.warn('[BUILDER-BADGE] Error in loadUserBadge:', e.message);
// On error, apply default plan
if (!planFetched && userBadgePlan && typeof formatPlanLabel === 'function') {
userBadgePlan.textContent = formatPlanLabel('hobby');
}
}
if (!email) {
email = readLocalEmail();
console.log('[BUILDER-BADGE] Using local email:', email ? 'yes' : 'no');
}
setUserBadgeEmail(email);
if (userBadge) {
userBadge.setAttribute('aria-label', email ? `Signed in as ${email}` : 'Account settings');
}
console.log('[BUILDER-BADGE] loadUserBadge complete, duration:', Date.now() - startTime, 'ms');
}
// Wait for builder.js to load before initializing user badge
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', loadUserBadge);
} else {
loadUserBadge();
}
const BUILDER_ENTRY_KEY = 'apps_to_builder';
const BUILDER_ENTRY_MAX_AGE_MS = 30 * 60 * 1000; // allow refreshes for 30 minutes
function redirectToApps() {
const next = encodeURIComponent(window.location.pathname + window.location.search);
window.location.replace(`/apps?next=${next}`);
}
function readBuilderEntry() {
try {
const raw = sessionStorage.getItem(BUILDER_ENTRY_KEY);
if (!raw) return null;
const parsed = JSON.parse(raw);
if (!parsed || !parsed.ts) return null;
if (Date.now() - parsed.ts > BUILDER_ENTRY_MAX_AGE_MS) return null;
return parsed;
} catch (_) {
return null;
}
}
function waitForSessionsLoaded() {
return new Promise((resolve) => {
const checkState = () => {
if (typeof state !== 'undefined' && state.sessionsLoaded) {
resolve();
} else {
setTimeout(checkState, 100);
}
};
checkState();
});
}
function slugifyName(name) {
const base = (name || '').trim().toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/-+/g, '-')
.replace(/^-+|-+$/g, '');
const suffix = Math.random().toString(36).slice(2, 6);
return `${base || 'app'}-${suffix}`;
}
// Load plugin builder prompt from file
async function loadPluginPrompt() {
// Since WordPress-specific instructions are now handled by OpenCode system prompts,
// we load empty prompts to ensure only the user's text/plan is sent to OpenCode
builderState.pluginPrompt = '';
builderState.subsequentPrompt = '';
console.log('Using OpenCode system prompts for WordPress plugin development');
}
function latestPlanMessage() {
const session = state.sessions.find(s => s.id === state.currentSessionId);
if (!session || !Array.isArray(session.messages)) return null;
const plans = session.messages.filter((m) => m.phase === 'plan');
if (!plans.length) return null;
return plans[plans.length - 1];
}
function resolveLatestPlanText(planMessage) {
const message = planMessage || latestPlanMessage();
const planReply = message?.reply || message?.partialOutput || '';
return builderState.lastPlanText || planReply || '';
}
async function ensureSessionExists() {
if (!state.currentSessionId) {
try {
// Check if we have any existing sessions that we can use
if (state.sessions && state.sessions.length > 0) {
console.log('[BUILDER] ensureSessionExists: using existing session:', state.sessions[0].id);
if (typeof selectSession === 'function') {
await selectSession(state.sessions[0].id);
}
} else if (typeof createSession === 'function') {
console.log('[BUILDER] ensureSessionExists: creating new session');
await createSession();
}
} catch (error) {
console.error('[BUILDER] ensureSessionExists failed:', error);
}
}
}
async function sendPlanMessage(content) {
if (!content) return;
// Prevent duplicate sends: check if this exact message was recently sent
try {
const raw = localStorage.getItem('builder_last_sent_message');
if (raw) {
const data = JSON.parse(raw);
if (data && data.content === content && (Date.now() - data.timestamp) < 5000) {
console.log('[BUILDER] Ignoring duplicate plan message send attempt');
const input = document.getElementById('message-input');
if (input) input.value = '';
return;
}
}
} catch (e) {
// Ignore localStorage errors
}
await ensureSessionExists();
// Double-check that we have a valid session
if (!state.currentSessionId) {
console.error('[BUILDER] sendPlanMessage: no session available after ensureSessionExists');
setStatus('Unable to establish session. Please refresh and try again.');
return;
}
// Show user message immediately (optimistic rendering)
const tempMessageId = 'temp-' + Date.now();
const session = state.sessions.find(s => s.id === state.currentSessionId);
if (session) {
session.messages = session.messages || [];
session.messages.push({
id: tempMessageId,
content: content,
displayContent: content,
model: 'openrouter',
cli: 'openrouter',
phase: 'plan',
status: 'queued',
createdAt: new Date().toISOString()
});
renderMessages(session);
}
// Keep the draft in the input until the plan request is accepted by the server.
const input = document.getElementById('message-input');
const payload = { sessionId: state.currentSessionId, content, displayContent: content };
setStatus('Planning with OpenRouter...');
// Show loading indicator with "planning" text
if (typeof showLoadingIndicator === 'function') {
showLoadingIndicator('planning');
}
const miniBtn = el.miniSendBtn;
// Set sending state for cancellation support
if (typeof state !== 'undefined') {
state.isSending = true;
state.currentSendingMessageId = tempMessageId;
}
if (typeof updateSendButtonState === 'function') {
updateSendButtonState();
}
try {
const response = await api('/api/plan', { method: 'POST', body: JSON.stringify(payload) });
// Track this message as sent to prevent duplicates
try {
localStorage.setItem('builder_last_sent_message', JSON.stringify({
content: content,
timestamp: Date.now()
}));
} catch (e) {
// Ignore localStorage errors
}
// Clear input only after the server successfully accepts the plan request.
if (input) {
input.value = '';
input.style.height = 'auto';
try { localStorage.removeItem('builder_message_input'); } catch (_) { }
}
const planSummary = response?.planSummary;
const planReply = response?.message?.reply;
builderState.lastPlanText = planSummary || planReply || '';
builderState.lastUserRequest = builderState.lastUserRequest || content;
builderState.planApproved = false;
builderState.mode = 'plan';
updateBuildModeUI();
await loadUsageSummary();
// Hide loading indicator after plan is received
if (typeof hideLoadingIndicator === 'function') {
hideLoadingIndicator();
}
await refreshCurrentSession();
} catch (error) {
setStatus('Planning failed: ' + error.message);
// Hide loading indicator on error
if (typeof hideLoadingIndicator === 'function') {
hideLoadingIndicator();
}
// Remove the temp message on error and refresh
if (session) {
session.messages = session.messages.filter(m => m.id !== tempMessageId);
renderMessages(session);
}
} finally {
// Reset sending state
if (typeof state !== 'undefined') {
state.isSending = false;
state.currentSendingMessageId = null;
}
if (typeof updateSendButtonState === 'function') {
updateSendButtonState();
}
}
}
// Note: updateBuildModeUI is now defined in builder.js
// Load prompt on page load
loadPluginPrompt().then(() => {
console.log('Plugin prompt loaded successfully');
}).catch(err => {
console.warn('Failed to load plugin prompt:', err);
});
// Override init function to handle session from URL with improved reliability
window.addEventListener('DOMContentLoaded', async () => {
console.log('[BUILDER-INIT] ===========================================');
console.log('[BUILDER-INIT] DOMContentLoaded - starting initialization');
console.log('[BUILDER-INIT] Timestamp:', new Date().toISOString());
console.log('[BUILDER-INIT] User agent:', navigator.userAgent);
console.log('[BUILDER-INIT] Page URL:', window.location.href);
console.log('[BUILDER-INIT] ===========================================');
const urlParams = new URLSearchParams(window.location.search);
const sessionParam = urlParams.get('session');
const entry = readBuilderEntry();
console.log('[BUILDER-INIT] URL params:', {
sessionParam,
entry: entry ? { appId: entry.appId, ts: entry.ts, ageMs: Date.now() - entry.ts } : null,
timestamp: new Date().toISOString()
});
// Builder requires a target session/app id, but should remain accessible on refresh/bookmark.
// Only redirect immediately if there's no session param AND no recent builder entry
if (!sessionParam && !entry) {
console.log('[BUILDER-INIT] No session param AND no builder entry found, redirecting to apps');
redirectToApps();
return;
}
// If we have a session param but no builder entry, create one for refresh support
if (sessionParam && !entry) {
try {
sessionStorage.setItem(BUILDER_ENTRY_KEY, JSON.stringify({ appId: sessionParam, ts: Date.now() }));
console.log('[BUILDER-INIT] Created new sessionStorage entry for appId:', sessionParam);
} catch (e) {
console.warn('[BUILDER-INIT] Failed to set sessionStorage entry:', e.message);
}
}
// Accept either a session id or an appId (slug) here — apps page sometimes stores
// the appId and sometimes the session id depending on the action. We'll validate
// the actual existence of a matching session after sessions are loaded below.
// Wait for builder.js to be loaded and provide necessary functions
let attempts = 0;
const maxWaitAttempts = 100; // Wait up to 10 seconds
console.log('[BUILDER-INIT] Waiting for builder.js functions...');
while ((typeof window.loadSessions !== 'function' || typeof window.state === 'undefined') && attempts < maxWaitAttempts) {
await new Promise(resolve => setTimeout(resolve, 100));
attempts++;
}
console.log('[BUILDER-INIT] Waited', attempts * 100, 'ms for builder.js functions');
console.log('[BUILDER-INIT] loadSessions available:', typeof window.loadSessions === 'function');
console.log('[BUILDER-INIT] state available:', typeof window.state !== 'undefined');
console.log('[BUILDER-INIT] api available:', typeof window.api === 'function');
if (typeof window.loadSessions !== 'function' || typeof window.state === 'undefined') {
console.error('[BUILDER-INIT] Builder.js functions not available after waiting');
console.error('[BUILDER-INIT] Redirecting to apps due to initialization failure');
redirectToApps();
return;
}
console.log('[BUILDER-INIT] Builder.js functions loaded, loading sessions...');
// Ensure sessions are loaded for the builder UI with retry
try {
const sessionResult = await window.loadSessions();
console.log('[BUILDER-INIT] Sessions load result:', {
success: sessionResult?.success,
sessionCount: sessionResult?.sessionCount,
timestamp: new Date().toISOString()
});
} catch (e) {
console.error('[BUILDER-INIT] Session loading failed:', e.message);
}
console.log('[BUILDER-INIT] Sessions loaded:', window.state.sessions?.length || 0, 'sessions');
console.log('[BUILDER-INIT] Current session ID:', window.state.currentSessionId);
// Allow matching the session either by its id or its appId (slug). The apps page
// may pass a slug (appId) instead of the internal session id.
let matched = null;
// First try to match from already loaded sessions
if (sessionParam) {
matched = window.state.sessions.find(s => s.id === sessionParam || s.appId === sessionParam);
console.log('[BUILDER-INIT] Initial session match attempt:', {
matched: !!matched,
sessionParam,
matchedId: matched?.id,
matchedAppId: matched?.appId,
totalSessions: window.state.sessions.length,
timestamp: new Date().toISOString()
});
}
// If the list endpoint didn't include the session (or the list failed), try a direct fetch by id.
// This works for both UUID session IDs and non-UUID appId values
if (!matched && sessionParam && typeof window.api === 'function') {
try {
console.log('[BUILDER-INIT] Trying direct fetch by session/app ID:', sessionParam);
const direct = await window.api(`/api/sessions/${encodeURIComponent(sessionParam)}`);
if (direct && direct.session && direct.session.id) {
matched = direct.session;
window.state.sessions = window.state.sessions || [];
// Avoid duplicates
if (!window.state.sessions.find(s => s.id === matched.id)) {
window.state.sessions.push(direct.session);
}
console.log('[BUILDER-INIT] Direct fetch succeeded:', {
id: matched.id,
appId: matched.appId,
timestamp: new Date().toISOString()
});
} else {
console.log('[BUILDER-INIT] Direct fetch returned no session');
}
} catch (e) {
console.warn('[BUILDER-INIT] Direct fetch failed:', e.message);
}
}
// If still not matched and we have a non-UUID appId, try searching by appId
if (!matched && sessionParam && typeof window.api === 'function') {
// Only try appId lookup if sessionParam doesn't look like a UUID
const looksLikeUuid = /^[a-f0-9\-]{36}$/i.test(sessionParam);
if (!looksLikeUuid) {
try {
console.log('[BUILDER-INIT] Trying appId lookup:', sessionParam);
const byApp = await window.api(`/api/sessions?appId=${encodeURIComponent(sessionParam)}`);
if (byApp && Array.isArray(byApp.sessions) && byApp.sessions.length) {
matched = byApp.sessions[0];
window.state.sessions = window.state.sessions || [];
if (!window.state.sessions.find(s => s.id === matched.id)) {
window.state.sessions.push(matched);
}
console.log('[BUILDER-INIT] AppId lookup succeeded:', {
matchedId: matched.id,
matchedAppId: matched.appId,
timestamp: new Date().toISOString()
});
} else {
console.log('[BUILDER-INIT] AppId lookup returned no sessions');
}
} catch (e) {
console.warn('[BUILDER-INIT] AppId lookup failed:', e.message);
}
} else {
console.log('[BUILDER-INIT] Skipping appId lookup - sessionParam looks like UUID');
}
}
if (matched) {
console.log('[BUILDER-INIT] Session matched, selecting session:', {
id: matched.id,
appId: matched.appId,
messageCount: matched.messages?.length || 0,
timestamp: new Date().toISOString()
});
try {
const session = await window.selectSession(matched.id);
console.log('[BUILDER-INIT] Session selection response:', {
sessionId: session?.id,
entryMode: session?.entryMode,
messageCount: session?.messages?.length || 0,
timestamp: new Date().toISOString()
});
// Mode is already set correctly by applyEntryMode in selectSession
// Just update UI to reflect current mode
console.log('[BUILDER-INIT] Mode already set by applyEntryMode:', builderState.mode);
updateBuildModeUI(builderState.mode);
console.log('[BUILDER-INIT] Session selection complete - initialization successful');
// Auto-scroll to bottom of latest message on page load
// Use multiple delays to ensure scroll works even if content takes time to render
if (typeof window.scrollChatToBottom === 'function') {
const scrollToBottomWithRetry = () => {
window.scrollChatToBottom();
setTimeout(() => window.scrollChatToBottom(), 200);
setTimeout(() => window.scrollChatToBottom(), 500);
setTimeout(() => window.scrollChatToBottom(), 1000);
};
scrollToBottomWithRetry();
}
// Load ALL sessions for this app to ensure history list shows all previous chats
if (matched.appId && typeof window.loadSessions === 'function') {
console.log('[BUILDER-INIT] Loading all sessions for appId:', matched.appId);
const allSessionsResult = await window.loadSessions();
console.log('[BUILDER-INIT] All sessions loaded:', allSessionsResult?.sessionCount);
}
// Load usage summary on initial page load
if (typeof window.loadUsageSummary === 'function') {
window.loadUsageSummary().catch(err => {
console.warn('[USAGE] Initial loadUsageSummary failed:', err.message);
});
}
} catch (e) {
console.error('[BUILDER-INIT] Session selection failed:', e.message);
console.error('[BUILDER-INIT] Stack:', e.stack);
// Don't redirect on selection error - the session exists, just failed to load
redirectToApps();
}
} else {
console.error('[BUILDER-INIT] No matching session found after all attempts');
console.error('[BUILDER-INIT] Session param:', sessionParam);
console.error('[BUILDER-INIT] Total sessions loaded:', window.state.sessions.length);
console.error('[BUILDER-INIT] Sessions:', window.state.sessions.map(s => ({ id: s.id, appId: s.appId })));
// No matching session or app was found — send user back to apps overview
redirectToApps();
}
});
// Override the sendMessage function to prepend plugin builder context
const originalSendMessage = window.sendMessage;
if (typeof sendMessage === 'function') {
// The app.js defines sendMessage, we need to intercept it
}
// Intercept message sending to route PLAN to OpenRouter and BUILD to OpenCode
const handleSend = async (e) => {
const input = document.getElementById('message-input');
const content = input.value.trim();
if (!content) return;
if (builderState.mode === 'plan') {
e.preventDefault();
e.stopPropagation();
// Prevent any other send handlers from firing (builder.js also attaches one)
if (typeof e.stopImmediatePropagation === 'function') e.stopImmediatePropagation();
// Don't clear input here - let sendPlanMessage handle it after ensuring session exists
builderState.lastUserRequest = builderState.lastUserRequest || content;
await sendPlanMessage(content);
return;
}
// Build mode: ensure a session exists, otherwise create one before sending
if (!state.currentSessionId) {
e.preventDefault();
e.stopPropagation();
try {
// Check if we have any existing sessions that we can use
if (state.sessions && state.sessions.length > 0) {
console.log('[BUILDER] handleSend: using existing session:', state.sessions[0].id);
if (typeof selectSession === 'function') {
await selectSession(state.sessions[0].id);
}
} else if (typeof createSession === 'function') {
console.log('[BUILDER] handleSend: creating new session');
await createSession();
}
} catch (error) {
console.error('Failed to establish session:', error);
setStatus('Failed to establish session: ' + error.message);
return;
}
}
// Always call sendMessage in build mode
if (typeof sendMessage === 'function') {
await sendMessage();
// sendMessage handles clearing the input itself
}
};
// document.getElementById('send-btn') removed
// Use handleSendButtonClick which handles both send and cancel modes
document.getElementById('mini-send-btn').addEventListener('click', (e) => {
if (typeof handleSendButtonClick === 'function') {
handleSendButtonClick(e);
} else {
// Fallback to old behavior if function not loaded yet
handleSend(e);
}
}, true);
// Initialize UI
updateBuildModeUI();
// Mobile menu toggle
const menuToggle = document.getElementById('menu-toggle') || document.createElement('button');
const sidebar = document.querySelector('.sidebar');
if (menuToggle && sidebar) {
// Set up menu toggle button if it doesn't exist
if (!menuToggle.id) {
menuToggle.id = 'menu-toggle';
menuToggle.innerHTML = '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="3" y1="6" x2="21" y2="6"></line><line x1="3" y1="12" x2="21" y2="12"></line><line x1="3" y1="18" x2="21" y2="18"></line></svg>';
menuToggle.style.cssText = 'border:none; background:transparent; color:var(--muted); cursor:pointer; padding:8px; border-radius:6px; display:none;';
document.querySelector('.main').insertBefore(menuToggle, document.querySelector('.main').firstChild);
}
menuToggle.addEventListener('click', () => {
sidebar.classList.toggle('active');
// Update icon based on state
if (sidebar.classList.contains('active')) {
menuToggle.innerHTML = '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>';
} else {
menuToggle.innerHTML = '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="3" y1="6" x2="21" y2="6"></line><line x1="3" y1="12" x2="21" y2="12"></line><line x1="3" y1="18" x2="21" y2="18"></line></svg>';
}
});
// Close sidebar when clicking outside on mobile
sidebar.addEventListener('click', (e) => {
if (e.target === sidebar) {
sidebar.classList.remove('active');
menuToggle.innerHTML = '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="3" y1="6" x2="21" y2="6"></line><line x1="3" y1="12" x2="21" y2="12"></line><line x1="3" y1="18" x2="21" y2="18"></line></svg>';
}
});
}
// Set up terminal link
const terminalLink = document.getElementById('terminal-link');
if (terminalLink) {
const host = window.location.hostname;
terminalLink.href = `http://${host}:4001`;
}
// Upgrade Modal functionality
function showUpgradeModal() {
if (typeof state !== 'undefined' && state.accountPlan === 'enterprise') {
alert('You are already on the Enterprise plan with full access.');
return;
}
const modal = document.getElementById('upgrade-modal');
if (modal) {
modal.style.display = 'flex';
}
}
function hideUpgradeModal() {
const modal = document.getElementById('upgrade-modal');
if (modal) {
modal.style.display = 'none';
}
}
// Event listeners for upgrade modal
const upgradeClose = document.getElementById('upgrade-close');
const upgradeBtn = document.getElementById('upgrade-btn');
const upgradeLater = document.getElementById('upgrade-later');
const upgradeModal = document.getElementById('upgrade-modal');
if (upgradeClose) {
upgradeClose.addEventListener('click', hideUpgradeModal);
}
if (upgradeBtn) {
upgradeBtn.addEventListener('click', () => {
hideUpgradeModal();
window.location.href = '/select-plan';
});
}
if (upgradeLater) {
upgradeLater.addEventListener('click', hideUpgradeModal);
}
if (upgradeModal) {
upgradeModal.addEventListener('click', (e) => {
if (e.target === upgradeModal) {
hideUpgradeModal();
}
});
}
</script>
<script>
// Onboarding Modal functionality for builder.html
(function() {
const ONBOARDING_COMPLETED_KEY = 'plugin_compass_onboarding_completed';
let currentStep = 1;
const totalSteps = 5;
let onboardingStatus = null;
async function isOnboardingCompleted() {
if (onboardingStatus !== null) {
return onboardingStatus;
}
try {
const res = await fetch('/api/onboarding');
if (res.ok) {
const data = await res.json();
onboardingStatus = data.completed;
return onboardingStatus;
}
} catch (e) {
}
return false;
}
async function markOnboardingCompleted() {
try {
const res = await fetch('/api/onboarding', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ completed: true })
});
if (res.ok) {
onboardingStatus = true;
}
} catch (e) {
console.warn('Failed to save onboarding completion status');
}
}
const tourSteps = [
{ target: null, color: '#008060' },
{ target: '#message-input', color: '#5A31F4' },
{ target: '#model-select-btn', color: '#F59E0B' },
{ target: '#upload-media-btn', color: '#10B981' },
{ target: null, color: '#EC4899' }
];
function showStep(step) {
const totalSteps = tourSteps.length;
const config = tourSteps[step - 1];
// Update content visibility
for (let i = 1; i <= totalSteps; i++) {
const stepEl = document.getElementById('onboarding-step-' + i);
if (stepEl) stepEl.style.display = i === step ? 'block' : 'none';
}
// Update counter
const counter = document.getElementById('onboarding-step-counter');
if (counter) counter.textContent = `${step} of ${totalSteps}`;
const backBtn = document.getElementById('onboarding-back');
const nextBtn = document.getElementById('onboarding-next');
if (backBtn) backBtn.style.display = step === 1 ? 'none' : 'block';
if (nextBtn) {
nextBtn.textContent = step === totalSteps ? 'Get Started' : 'Next';
nextBtn.style.background = step === totalSteps ? '#10B981' : '#008060';
}
// Position the box
positionTourBox(config.target);
}
function positionTourBox(targetSelector) {
const container = document.getElementById('onboarding-container');
const pointer = document.getElementById('onboarding-pointer');
const modal = document.getElementById('onboarding-modal');
if (!container) return;
const isMobile = window.innerWidth < 480;
const boxWidth = isMobile ? Math.min(280, window.innerWidth - 32) : 280;
if (!targetSelector) {
// Center position
container.style.top = '50%';
container.style.left = '50%';
container.style.transform = 'translate(-50%, -50%)';
if (pointer) pointer.style.display = 'none';
modal.style.background = 'transparent';
return;
}
const target = document.querySelector(targetSelector);
if (!target) {
// Fallback to center if target missing
positionTourBox(null);
return;
}
modal.style.background = 'transparent'; // Lighter backdrop for tour
const rect = target.getBoundingClientRect();
// Default: Position above the element
let top = rect.top - container.offsetHeight - 16;
let left = rect.left + (rect.width / 2) - (boxWidth / 2);
// Bounds checking
if (top < 16) {
// Position below instead
top = rect.bottom + 16;
if (pointer) {
pointer.style.display = 'block';
pointer.style.top = '-8px';
pointer.style.bottom = 'auto';
pointer.style.left = '50%';
pointer.style.marginLeft = '-8px';
}
} else {
if (pointer) {
pointer.style.display = 'block';
pointer.style.bottom = '-8px';
pointer.style.top = 'auto';
pointer.style.left = '50%';
pointer.style.marginLeft = '-8px';
}
}
// Keep horizontal within viewport
const padding = 16;
left = Math.max(padding, Math.min(left, window.innerWidth - boxWidth - padding));
container.style.top = top + 'px';
container.style.left = left + 'px';
container.style.transform = 'none';
}
function nextStep() {
if (currentStep < tourSteps.length) {
currentStep++;
showStep(currentStep);
} else {
completeOnboarding();
}
}
function prevStep() {
if (currentStep > 1) {
currentStep--;
showStep(currentStep);
}
}
function goToStep(step) {
if (step >= 1 && step <= tourSteps.length) {
currentStep = step;
showStep(currentStep);
}
}
function completeOnboarding() {
markOnboardingCompleted();
hideOnboarding();
if (typeof posthog !== 'undefined') {
posthog.capture('onboarding_completed');
}
}
function hideOnboarding() {
const modal = document.getElementById('onboarding-modal');
if (modal) {
modal.style.display = 'none';
}
}
function showOnboarding() {
const modal = document.getElementById('onboarding-modal');
if (modal) {
currentStep = 1;
modal.style.display = 'block'; // Changed from flex to allow absolute children positioning
showStep(currentStep);
}
}
window.showOnboarding = showOnboarding;
window.hideOnboarding = hideOnboarding;
function initOnboarding() {
const exitBtn = document.getElementById('onboarding-exit');
const backBtn = document.getElementById('onboarding-back');
const nextBtn = document.getElementById('onboarding-next');
const modal = document.getElementById('onboarding-modal');
if (exitBtn) {
exitBtn.addEventListener('click', completeOnboarding);
}
if (backBtn) {
backBtn.addEventListener('click', prevStep);
}
if (nextBtn) {
nextBtn.addEventListener('click', nextStep);
}
if (modal) {
modal.addEventListener('click', function(e) {
if (e.target === modal) {
completeOnboarding();
}
});
}
window.addEventListener('resize', () => {
if (modal.style.display !== 'none') {
const config = tourSteps[currentStep - 1];
positionTourBox(config.target);
}
});
if (typeof document !== 'undefined') {
document.addEventListener('keydown', function(e) {
const onboardingModal = document.getElementById('onboarding-modal');
if (!onboardingModal || onboardingModal.style.display === 'none') return;
if (e.key === 'Escape') {
completeOnboarding();
} else if (e.key === 'ArrowRight') {
nextStep();
} else if (e.key === 'ArrowLeft') {
prevStep();
}
});
}
}
document.addEventListener('DOMContentLoaded', async function() {
initOnboarding();
if (!(await isOnboardingCompleted())) {
setTimeout(showOnboarding, 800);
}
});
})();
</script>
<style>
/* Onboarding Modal Styles */
#onboarding-exit:hover {
background: #f3f4f6;
color: #374151;
}
.onboarding-btn-primary:hover {
transform: translateY(-1px);
box-shadow: 0 8px 20px rgba(0, 128, 96, 0.3);
}
.onboarding-btn-primary:active {
transform: translateY(0);
}
.onboarding-btn-secondary:hover {
background: #f9fafb;
border-color: #d1d5db;
}
.onboarding-dot.active {
background: #008060;
transform: scale(1.2);
}
.onboarding-dot:hover {
background: #008060;
opacity: 0.7;
}
</style>
<footer
style="margin-top: auto; padding: 40px 24px 20px; border-top: 1px solid #e1e3e5; text-align: center; color: #6d7175; font-size: 0.875rem;">
<p>&copy; 2026 Plugin Compass. All rights reserved.</p>
<div style="margin-top: 12px; display: flex; justify-content: center; gap: 16px;">
<a href="/terms" style="color: #008060; text-decoration: none;">Terms</a>
<a href="/privacy" style="color: #008060; text-decoration: none;">Privacy</a>
<a href="/contact" style="color: #008060; text-decoration: none;">Contact Us</a>
</div>
</footer>
</body>
</html>