2714 lines
97 KiB
HTML
2714 lines
97 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;
|
|
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;
|
|
width: 100%;
|
|
box-sizing: border-box;
|
|
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
|
|
}
|
|
|
|
.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: 10000;
|
|
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 {
|
|
margin-bottom: 0;
|
|
}
|
|
|
|
.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" style="margin-right:8px;">
|
|
<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 style="display:none; align-items:center; gap:8px;">
|
|
<label style="display:inline-flex; align-items:center; gap:8px; font-weight:600; font-size:13px;">
|
|
<input type="checkbox" id="external-testing-toggle" style="width:18px; height:18px;" />
|
|
<span id="external-testing-label" style="font-weight:600; font-size:13px;">External WP Tests</span>
|
|
</label>
|
|
<button id="external-testing-info" class="action-link" title="Run the plugin through CLI tests on an external WP site" style="padding:6px 8px; border-radius:8px;">i</button>
|
|
<div id="external-testing-usage" style="font-size:12px; color:var(--muted); min-width:140px; text-align:right;">—</div>
|
|
</div>
|
|
</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 -->
|
|
|
|
<!-- External Testing Limit Modal -->
|
|
<div id="external-testing-limit-modal" class="modal" style="display:none;">
|
|
<div class="modal-card" style="max-width:520px;">
|
|
<div style="display:flex; justify-content:space-between; align-items:center;">
|
|
<strong style="color:var(--ink); font-size:20px;">External Testing Limit Reached</strong>
|
|
<button id="external-testing-limit-close" style="background:none; border:none; font-size:22px;">×</button>
|
|
</div>
|
|
<p style="color:var(--muted); margin-top:12px;">Your current plan limits the number of external WP CLI tests you can run per month. To continue using external testing, upgrade your plan or purchase additional test credits.</p>
|
|
<div class="admin-actions" style="margin-top:18px; justify-content:flex-end; gap:8px;">
|
|
<button id="external-testing-limit-upgrade" class="primary">Upgrade Plan</button>
|
|
<button id="external-testing-limit-cancel" class="action-link">Cancel</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<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>© 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> |