Files
shopify-ai-backup/chat/public/builder.html

2650 lines
96 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;
}
.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);
}
.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; }
}
</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() {
try {
const response = await fetch('/chat/shopify-builder-prompt.txt');
if (response.ok) {
builderState.pluginPrompt = await response.text();
} else {
console.warn('Could not load plugin prompt file, using default');
builderState.pluginPrompt = getDefaultPrompt();
}
} catch (error) {
console.warn('Error loading plugin prompt file:', error);
builderState.pluginPrompt = getDefaultPrompt();
}
// Load subsequent prompt for follow-up messages
try {
const response = await fetch('/chat/shopify-builder-subsequent-prompt.txt');
if (response.ok) {
builderState.subsequentPrompt = await response.text();
} else {
console.warn('Could not load subsequent prompt file, using default');
builderState.subsequentPrompt = getDefaultSubsequentPrompt();
}
} catch (error) {
console.warn('Error loading subsequent prompt file:', error);
builderState.subsequentPrompt = getDefaultSubsequentPrompt();
}
}
function getDefaultPrompt() {
return `You are an expert WordPress plugin developer. When asked to build a WordPress plugin, you must create a COMPLETE, FULLY FUNCTIONAL PHP plugin with all necessary files and WordPress integrations.
Here is the user's request:
{{USER_REQUEST}}
IMPORTANT REQUIREMENTS:
1. Create a complete WordPress plugin in PHP following WordPress coding standards
2. Include plugin header comment block and proper folder structure
3. Register activation/deactivation/uninstall hooks and any required DB migrations
4. Provide admin UI using WordPress admin pages, Settings API, or custom blocks as requested
5. Add shortcodes or Gutenberg blocks for frontend features where applicable
6. Include REST API endpoints or AJAX handlers if the plugin exposes APIs
7. Ensure capability checks, nonce protection, sanitization, and escaping for security
8. Provide a clear README with installation, activation, and usage instructions
STRUCTURE TO CREATE:
- plugin-name.php (main plugin bootstrap file with header)
- includes/ for helper classes and functions
- admin/ for admin pages, settings, and menu registration
- public/ for frontend templates, shortcodes, and assets
- uninstall.php for cleanup
- readme.txt and README.md with usage and installation steps
- composer.json or package.json only if needed for build tooling
When building, explain each major step and create all files needed for a production-ready plugin. The plugin should be installable via the WordPress admin and follow WP security best practices.
`;
}
function getDefaultSubsequentPrompt() {
return `You are continuing to help build a WordPress plugin. Follow the same strict standards as before.
User's request: {{USER_REQUEST}}
REMEMBER THESE CRITICAL REQUIREMENTS:
1. PHP SYNTAX CHECKING - MUST use \`php -l filename.php\` for every PHP file you create or modify
2. After generating code, run \`./scripts/validate-wordpress-plugin.sh <plugin-root-directory>\` to verify all PHP files
3. Use {{PLUGIN_SLUG}} prefix for all functions, classes, and CSS classes
4. Main plugin file must include WordPress.org update prevention filter
5. Plugin header: Plugin URI and Update URI as specified, Author: Plugin Compass
6. Complete admin styling with WordPress admin classes (.wrap, .card, .notice, .button, .widefat)
7. Complete public styling with responsive design, mobile-first, WCAG 2.1 AA compliance
8. Enqueue styles/scripts properly with dependencies
9. Security: capability checks, nonce protection, sanitization, escaping
10. Compatibility with latest WordPress and WooCommerce
STYLING REQUIREMENTS:
- Admin: WordPress color schemes, proper icons (Dashicons/SVG), responsive design
- Public: Modern responsive design, hover states, transitions, high contrast
- Both: Clear hierarchy, consistent spacing, accessible
Never edit files outside the workspace. Be concise and provide complete, production-ready code with full CSS.
`;
}
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;
const originalLabel = miniBtn ? miniBtn.textContent : '';
if (miniBtn) {
miniBtn.disabled = true;
miniBtn.innerHTML = '<svg width="20" height="20" 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><polyline points="12 6 12 12 16 14"></polyline></svg>'; // Clock icon for planning
}
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 {
if (miniBtn) {
miniBtn.disabled = false;
// Restore original SVG icon
miniBtn.innerHTML = `<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>`;
}
}
}
// 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
document.getElementById('mini-send-btn').addEventListener('click', handleSend, 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>