openvoiceui 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +104 -0
- package/Dockerfile +30 -0
- package/LICENSE +21 -0
- package/README.md +638 -0
- package/SETUP.md +360 -0
- package/app.py +232 -0
- package/auto-approve-devices.js +111 -0
- package/cli/index.js +372 -0
- package/config/__init__.py +4 -0
- package/config/default.yaml +43 -0
- package/config/flags.yaml +67 -0
- package/config/loader.py +203 -0
- package/config/providers.yaml +71 -0
- package/config/speech_normalization.yaml +182 -0
- package/config/theme.json +4 -0
- package/data/greetings.json +25 -0
- package/default-pages/ai-image-creator.html +915 -0
- package/default-pages/bulk-image-uploader.html +492 -0
- package/default-pages/desktop.html +2865 -0
- package/default-pages/file-explorer.html +854 -0
- package/default-pages/interactive-map.html +655 -0
- package/default-pages/style-guide.html +1005 -0
- package/default-pages/website-setup.html +1623 -0
- package/deploy/openclaw/Dockerfile +46 -0
- package/deploy/openvoiceui.service +30 -0
- package/deploy/setup-nginx.sh +50 -0
- package/deploy/setup-sudo.sh +306 -0
- package/deploy/skill-runner/Dockerfile +19 -0
- package/deploy/skill-runner/requirements.txt +14 -0
- package/deploy/skill-runner/server.py +269 -0
- package/deploy/supertonic/Dockerfile +22 -0
- package/deploy/supertonic/server.py +79 -0
- package/docker-compose.pinokio.yml +11 -0
- package/docker-compose.yml +59 -0
- package/greetings.json +25 -0
- package/index.html +65 -0
- package/inject-device-identity.js +142 -0
- package/package.json +82 -0
- package/profiles/default.json +114 -0
- package/profiles/manager.py +354 -0
- package/profiles/schema.json +337 -0
- package/prompts/voice-system-prompt.md +149 -0
- package/providers/__init__.py +39 -0
- package/providers/base.py +63 -0
- package/providers/llm/__init__.py +12 -0
- package/providers/llm/base.py +71 -0
- package/providers/llm/clawdbot_provider.py +112 -0
- package/providers/llm/zai_provider.py +115 -0
- package/providers/registry.py +320 -0
- package/providers/stt/__init__.py +12 -0
- package/providers/stt/base.py +58 -0
- package/providers/stt/webspeech_provider.py +49 -0
- package/providers/stt/whisper_provider.py +100 -0
- package/providers/tts/__init__.py +20 -0
- package/providers/tts/base.py +91 -0
- package/providers/tts/groq_provider.py +74 -0
- package/providers/tts/supertonic_provider.py +72 -0
- package/requirements.txt +38 -0
- package/routes/__init__.py +10 -0
- package/routes/admin.py +515 -0
- package/routes/canvas.py +1315 -0
- package/routes/chat.py +51 -0
- package/routes/conversation.py +2158 -0
- package/routes/elevenlabs_hybrid.py +306 -0
- package/routes/greetings.py +98 -0
- package/routes/icons.py +279 -0
- package/routes/image_gen.py +364 -0
- package/routes/instructions.py +190 -0
- package/routes/music.py +838 -0
- package/routes/onboarding.py +43 -0
- package/routes/pi.py +62 -0
- package/routes/profiles.py +215 -0
- package/routes/report_issue.py +68 -0
- package/routes/static_files.py +533 -0
- package/routes/suno.py +664 -0
- package/routes/theme.py +81 -0
- package/routes/transcripts.py +199 -0
- package/routes/vision.py +348 -0
- package/routes/workspace.py +288 -0
- package/server.py +1510 -0
- package/services/__init__.py +1 -0
- package/services/auth.py +143 -0
- package/services/canvas_versioning.py +239 -0
- package/services/db_pool.py +107 -0
- package/services/gateway.py +16 -0
- package/services/gateway_manager.py +333 -0
- package/services/gateways/__init__.py +12 -0
- package/services/gateways/base.py +110 -0
- package/services/gateways/compat.py +264 -0
- package/services/gateways/openclaw.py +1134 -0
- package/services/health.py +100 -0
- package/services/memory_client.py +455 -0
- package/services/paths.py +26 -0
- package/services/speech_normalizer.py +285 -0
- package/services/tts.py +270 -0
- package/setup-config.js +262 -0
- package/sounds/air_horn.mp3 +0 -0
- package/sounds/bruh.mp3 +0 -0
- package/sounds/crowd_cheer.mp3 +0 -0
- package/sounds/gunshot.mp3 +0 -0
- package/sounds/impact.mp3 +0 -0
- package/sounds/lets_go.mp3 +0 -0
- package/sounds/record_stop.mp3 +0 -0
- package/sounds/rewind.mp3 +0 -0
- package/sounds/sad_trombone.mp3 +0 -0
- package/sounds/scratch_long.mp3 +0 -0
- package/sounds/yeah.mp3 +0 -0
- package/src/adapters/ClawdBotAdapter.js +264 -0
- package/src/adapters/_template.js +133 -0
- package/src/adapters/elevenlabs-classic.js +841 -0
- package/src/adapters/elevenlabs-hybrid.js +812 -0
- package/src/adapters/hume-evi.js +676 -0
- package/src/admin.html +1339 -0
- package/src/app.js +8802 -0
- package/src/core/Config.js +173 -0
- package/src/core/EmotionEngine.js +307 -0
- package/src/core/EventBridge.js +180 -0
- package/src/core/EventBus.js +117 -0
- package/src/core/VoiceSession.js +607 -0
- package/src/face/BaseFace.js +259 -0
- package/src/face/EyeFace.js +208 -0
- package/src/face/HaloSmokeFace.js +509 -0
- package/src/face/manifest.json +27 -0
- package/src/face/previews/eyes.svg +16 -0
- package/src/face/previews/orb.svg +29 -0
- package/src/features/MusicPlayer.js +620 -0
- package/src/features/Soundboard.js +128 -0
- package/src/providers/DeepgramSTT.js +472 -0
- package/src/providers/DeepgramStreamingSTT.js +766 -0
- package/src/providers/GroqSTT.js +559 -0
- package/src/providers/TTSPlayer.js +323 -0
- package/src/providers/WebSpeechSTT.js +479 -0
- package/src/providers/tts/BaseTTSProvider.js +81 -0
- package/src/providers/tts/HumeProvider.js +77 -0
- package/src/providers/tts/SupertonicProvider.js +174 -0
- package/src/providers/tts/index.js +140 -0
- package/src/shell/adapter-registry.js +154 -0
- package/src/shell/caller-bridge.js +35 -0
- package/src/shell/camera-bridge.js +28 -0
- package/src/shell/canvas-bridge.js +32 -0
- package/src/shell/commercial-bridge.js +44 -0
- package/src/shell/face-bridge.js +44 -0
- package/src/shell/music-bridge.js +60 -0
- package/src/shell/orchestrator.js +233 -0
- package/src/shell/profile-discovery.js +303 -0
- package/src/shell/sounds-bridge.js +28 -0
- package/src/shell/transcript-bridge.js +61 -0
- package/src/shell/waveform-bridge.js +33 -0
- package/src/styles/base.css +2862 -0
- package/src/styles/face.css +417 -0
- package/src/styles/pi-overrides.css +89 -0
- package/src/styles/theme-dark.css +67 -0
- package/src/test-tts.html +175 -0
- package/src/ui/AppShell.js +544 -0
- package/src/ui/ProfileSwitcher.js +228 -0
- package/src/ui/SessionControl.js +240 -0
- package/src/ui/face/FacePicker.js +195 -0
- package/src/ui/face/FaceRenderer.js +309 -0
- package/src/ui/settings/PlaylistEditor.js +366 -0
- package/src/ui/settings/SettingsPanel.css +684 -0
- package/src/ui/settings/SettingsPanel.js +419 -0
- package/src/ui/settings/TTSVoicePreview.js +210 -0
- package/src/ui/themes/ThemeManager.js +213 -0
- package/src/ui/visualizers/BaseVisualizer.js +29 -0
- package/src/ui/visualizers/PartyFXVisualizer.css +291 -0
- package/src/ui/visualizers/PartyFXVisualizer.js +637 -0
- package/static/emulators/jsdos/js-dos.css +1 -0
- package/static/emulators/jsdos/js-dos.js +22 -0
- package/static/favicon.svg +55 -0
- package/static/icons/apple-touch-icon.png +0 -0
- package/static/icons/favicon-32.png +0 -0
- package/static/icons/icon-192.png +0 -0
- package/static/icons/icon-512.png +0 -0
- package/static/install.html +449 -0
- package/static/manifest.json +26 -0
- package/static/sw.js +21 -0
- package/tts_providers/__init__.py +136 -0
- package/tts_providers/base_provider.py +319 -0
- package/tts_providers/groq_provider.py +155 -0
- package/tts_providers/hume_provider.py +226 -0
- package/tts_providers/providers_config.json +119 -0
- package/tts_providers/qwen3_provider.py +371 -0
- package/tts_providers/resemble_provider.py +315 -0
- package/tts_providers/supertonic_provider.py +557 -0
- package/tts_providers/supertonic_tts.py +399 -0
|
@@ -0,0 +1,1623 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>Website Setup</title>
|
|
7
|
+
<style>
|
|
8
|
+
*,*::before,*::after{margin:0;padding:0;box-sizing:border-box}
|
|
9
|
+
:root{
|
|
10
|
+
--bg:#13141a;--surface:#1a1b23;--surface-2:#21222c;--surface-hover:#282936;--surface-3:#2e3040;
|
|
11
|
+
--border:#2a2b37;--border-light:#353648;--border-heavy:#42445a;
|
|
12
|
+
--text:#e8eaf0;--text-2:#9498ab;--text-3:#5f6380;
|
|
13
|
+
--brand:#3b82f6;--brand-light:#60a5fa;--brand-dim:rgba(59,130,246,.12);--brand-glow:rgba(59,130,246,.15);--brand-border:rgba(59,130,246,.25);
|
|
14
|
+
--green:#34d399;--green-dim:rgba(52,211,153,.1);--green-border:rgba(52,211,153,.2);
|
|
15
|
+
--red:#f87171;--red-dim:rgba(248,113,113,.1);
|
|
16
|
+
--amber:#fbbf24;--amber-dim:rgba(251,191,36,.1);
|
|
17
|
+
--cyan:#22d3ee;--cyan-dim:rgba(34,211,238,.1);
|
|
18
|
+
--r:14px;--r-sm:10px;--r-xs:6px;
|
|
19
|
+
}
|
|
20
|
+
html{font-size:15px}
|
|
21
|
+
body{
|
|
22
|
+
font-family:system-ui,-apple-system,'Segoe UI',Roboto,sans-serif;
|
|
23
|
+
background:var(--bg);color:var(--text-2);
|
|
24
|
+
min-height:100vh;padding:0;
|
|
25
|
+
-webkit-font-smoothing:antialiased;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/* ── Layout ── */
|
|
29
|
+
.container{max-width:840px;margin:0 auto;padding:16px;padding-bottom:80px}
|
|
30
|
+
|
|
31
|
+
/* ── Header ── */
|
|
32
|
+
.page-header{
|
|
33
|
+
position:sticky;top:0;z-index:100;
|
|
34
|
+
background:rgba(19,20,26,.92);backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);
|
|
35
|
+
border-bottom:1px solid var(--border);
|
|
36
|
+
padding:14px 16px;margin:0 -16px 20px;
|
|
37
|
+
display:flex;align-items:center;justify-content:space-between;gap:12px;
|
|
38
|
+
}
|
|
39
|
+
.hdr-left{display:flex;align-items:center;gap:12px}
|
|
40
|
+
.hdr-icon{
|
|
41
|
+
width:40px;height:40px;background:var(--brand-dim);border:1px solid var(--brand-border);
|
|
42
|
+
border-radius:10px;display:flex;align-items:center;justify-content:center;flex-shrink:0;
|
|
43
|
+
}
|
|
44
|
+
.hdr-icon svg{width:20px;height:20px;stroke:var(--brand);fill:none;stroke-width:1.8;stroke-linecap:round;stroke-linejoin:round}
|
|
45
|
+
.hdr-title{font-size:1.1rem;font-weight:700;color:var(--text);letter-spacing:-.01em}
|
|
46
|
+
.hdr-sub{font-size:.72rem;color:var(--text-3)}
|
|
47
|
+
.status-badge{
|
|
48
|
+
font-size:.68rem;font-weight:600;text-transform:uppercase;letter-spacing:.04em;
|
|
49
|
+
padding:4px 10px;border-radius:20px;white-space:nowrap;
|
|
50
|
+
}
|
|
51
|
+
.status-badge.draft{background:rgba(95,99,128,.15);color:var(--text-3)}
|
|
52
|
+
.status-badge.ready{background:var(--green-dim);color:var(--green)}
|
|
53
|
+
.status-badge.running{background:var(--brand-dim);color:var(--brand-light);animation:pulse 2s ease-in-out infinite}
|
|
54
|
+
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.5}}
|
|
55
|
+
|
|
56
|
+
/* ── Section ── */
|
|
57
|
+
.section{margin-bottom:16px}
|
|
58
|
+
.section-card{
|
|
59
|
+
background:linear-gradient(145deg,var(--surface) 0%,var(--surface-2) 100%);
|
|
60
|
+
border:1px solid var(--border);border-radius:var(--r);overflow:hidden;
|
|
61
|
+
position:relative;
|
|
62
|
+
}
|
|
63
|
+
.section-card::before{
|
|
64
|
+
content:'';position:absolute;top:0;left:0;right:0;height:1px;
|
|
65
|
+
background:linear-gradient(90deg,transparent,rgba(59,130,246,.2),transparent);
|
|
66
|
+
}
|
|
67
|
+
.section-card.essential{border-color:var(--brand-border);box-shadow:0 0 20px var(--brand-glow)}
|
|
68
|
+
.section-header{
|
|
69
|
+
display:flex;align-items:center;justify-content:space-between;
|
|
70
|
+
padding:14px 16px;cursor:pointer;user-select:none;transition:background .15s;
|
|
71
|
+
}
|
|
72
|
+
.section-header:hover{background:var(--surface-hover)}
|
|
73
|
+
.section-header h2{font-size:.82rem;font-weight:600;color:var(--text);display:flex;align-items:center;gap:8px}
|
|
74
|
+
.section-header .chevron{
|
|
75
|
+
width:16px;height:16px;stroke:var(--text-3);fill:none;stroke-width:2;
|
|
76
|
+
stroke-linecap:round;stroke-linejoin:round;transition:transform .2s;
|
|
77
|
+
}
|
|
78
|
+
.section-header.open .chevron{transform:rotate(180deg)}
|
|
79
|
+
.section-body{padding:0 16px 16px;display:none}
|
|
80
|
+
.section-body.open{display:block}
|
|
81
|
+
|
|
82
|
+
/* ── Form Fields ── */
|
|
83
|
+
.form-grid{display:grid;grid-template-columns:1fr 1fr;gap:12px}
|
|
84
|
+
.form-grid.full{grid-template-columns:1fr}
|
|
85
|
+
.form-group{display:flex;flex-direction:column;gap:4px}
|
|
86
|
+
.form-group.span-2{grid-column:1/-1}
|
|
87
|
+
label{font-size:.72rem;font-weight:600;color:var(--text-3);text-transform:uppercase;letter-spacing:.04em}
|
|
88
|
+
input[type="text"],input[type="email"],input[type="tel"],input[type="url"],textarea{
|
|
89
|
+
background:var(--surface-2);border:1px solid var(--border);border-radius:var(--r-xs);
|
|
90
|
+
padding:9px 12px;color:var(--text);font-size:.85rem;font-family:inherit;
|
|
91
|
+
outline:none;transition:border-color .15s;width:100%;
|
|
92
|
+
}
|
|
93
|
+
input:focus,textarea:focus{border-color:var(--brand-border)}
|
|
94
|
+
textarea{resize:vertical;min-height:60px}
|
|
95
|
+
.char-count{font-size:.65rem;color:var(--text-3);text-align:right}
|
|
96
|
+
|
|
97
|
+
/* ── AI Buttons ── */
|
|
98
|
+
.field-with-ai{display:flex;gap:6px;align-items:flex-start}
|
|
99
|
+
.field-with-ai input,.field-with-ai textarea{flex:1}
|
|
100
|
+
.ai-btn{
|
|
101
|
+
display:inline-flex;align-items:center;gap:4px;
|
|
102
|
+
padding:7px 10px;border-radius:var(--r-xs);border:1px solid var(--brand-border);
|
|
103
|
+
background:var(--brand-dim);color:var(--brand-light);font-size:.72rem;font-weight:600;
|
|
104
|
+
cursor:pointer;transition:all .15s;white-space:nowrap;flex-shrink:0;
|
|
105
|
+
}
|
|
106
|
+
.ai-btn:hover{background:rgba(59,130,246,.2)}
|
|
107
|
+
.ai-btn svg{width:12px;height:12px;stroke:currentColor;fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
|
|
108
|
+
|
|
109
|
+
/* ── Selectable Cards (Site Type) ── */
|
|
110
|
+
.card-select{display:grid;grid-template-columns:repeat(4,1fr);gap:8px}
|
|
111
|
+
.sel-card{
|
|
112
|
+
background:var(--surface-2);border:1.5px solid var(--border);border-radius:var(--r-sm);
|
|
113
|
+
padding:12px;cursor:pointer;transition:all .15s;text-align:center;
|
|
114
|
+
}
|
|
115
|
+
.sel-card:hover{border-color:var(--border-light);background:var(--surface-hover)}
|
|
116
|
+
.sel-card.active{border-color:var(--brand);background:var(--brand-dim);box-shadow:0 0 12px var(--brand-glow)}
|
|
117
|
+
.sel-card .ic{font-size:1.4rem;margin-bottom:4px}
|
|
118
|
+
.sel-card .nm{font-size:.78rem;font-weight:600;color:var(--text)}
|
|
119
|
+
.sel-card .desc{font-size:.65rem;color:var(--text-3);margin-top:2px}
|
|
120
|
+
|
|
121
|
+
/* ── Theme Dropdown ── */
|
|
122
|
+
.theme-select{
|
|
123
|
+
background:var(--surface-2);border:1px solid var(--border);border-radius:var(--r-xs);
|
|
124
|
+
padding:9px 12px;color:var(--text);font-size:.85rem;font-family:inherit;
|
|
125
|
+
outline:none;width:100%;cursor:pointer;appearance:none;
|
|
126
|
+
background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%239498ab' stroke-width='2'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
|
|
127
|
+
background-repeat:no-repeat;background-position:right 12px center;
|
|
128
|
+
}
|
|
129
|
+
.theme-select:focus{border-color:var(--brand-border)}
|
|
130
|
+
.swatch-bar{display:flex;height:20px;border-radius:4px;overflow:hidden;margin-top:6px}
|
|
131
|
+
.swatch-bar>div{flex:1}
|
|
132
|
+
.swatch-labels{display:flex;margin-top:2px}
|
|
133
|
+
.swatch-labels span{flex:1;font-size:.5rem;color:var(--text-3);text-align:center}
|
|
134
|
+
|
|
135
|
+
/* ── Style Builder Layout ── */
|
|
136
|
+
.style-builder{display:flex;gap:14px;margin-top:12px}
|
|
137
|
+
.style-preview-col{flex:1;min-width:0}
|
|
138
|
+
.style-controls-col{width:280px;flex-shrink:0;display:flex;flex-direction:column;gap:6px}
|
|
139
|
+
@media(max-width:700px){.style-builder{flex-direction:column}.style-controls-col{width:100%}}
|
|
140
|
+
|
|
141
|
+
/* ── Live Preview ── */
|
|
142
|
+
.preview-frame{
|
|
143
|
+
border:1px solid var(--border);border-radius:var(--r);overflow:hidden;
|
|
144
|
+
background:#fff;transition:all .3s;position:sticky;top:80px;
|
|
145
|
+
}
|
|
146
|
+
.pv-nav{display:flex;align-items:center;justify-content:space-between;padding:10px 14px;transition:all .3s}
|
|
147
|
+
.pv-logo{font-size:.8rem;font-weight:700;letter-spacing:-.02em}
|
|
148
|
+
.pv-links{display:flex;gap:10px}
|
|
149
|
+
.pv-links span{font-size:.62rem;font-weight:500;opacity:.8;cursor:default}
|
|
150
|
+
.pv-btn{font-size:.62rem;font-weight:600;padding:5px 12px;border:none;cursor:default;transition:all .3s}
|
|
151
|
+
.pv-hero{padding:22px 14px;text-align:center;transition:all .3s}
|
|
152
|
+
.pv-hero h2{font-size:.95rem;font-weight:800;letter-spacing:-.03em;margin-bottom:3px}
|
|
153
|
+
.pv-hero p{font-size:.65rem;opacity:.7;margin:0 auto 10px}
|
|
154
|
+
.pv-cards{display:grid;grid-template-columns:1fr 1fr;gap:8px;padding:0 14px 14px}
|
|
155
|
+
.pv-card{padding:10px;text-align:center;transition:all .3s}
|
|
156
|
+
.pv-card .pv-card-title{font-size:.68rem;font-weight:700;margin-bottom:2px}
|
|
157
|
+
.pv-card .pv-card-text{font-size:.55rem;opacity:.6;line-height:1.4}
|
|
158
|
+
|
|
159
|
+
/* ── Style Controls ── */
|
|
160
|
+
.ctrl-group{
|
|
161
|
+
background:var(--surface);border:1px solid var(--border);border-radius:var(--r-xs);
|
|
162
|
+
padding:8px 10px;
|
|
163
|
+
}
|
|
164
|
+
.ctrl-group-title{font-size:.62rem;font-weight:700;color:var(--brand-light);text-transform:uppercase;letter-spacing:.06em;margin-bottom:6px}
|
|
165
|
+
.ctrl-row{display:flex;gap:6px;align-items:center;margin-bottom:5px}
|
|
166
|
+
.ctrl-row:last-child{margin-bottom:0}
|
|
167
|
+
.ctrl-row label{font-size:.62rem;color:var(--text-3);width:68px;flex-shrink:0}
|
|
168
|
+
.ctrl-row select,.ctrl-row input[type="range"]{
|
|
169
|
+
flex:1;background:var(--surface-2);border:1px solid var(--border);border-radius:4px;
|
|
170
|
+
padding:4px 6px;color:var(--text);font-size:.7rem;font-family:inherit;
|
|
171
|
+
outline:none;cursor:pointer;min-width:0;
|
|
172
|
+
}
|
|
173
|
+
.ctrl-row select{
|
|
174
|
+
appearance:none;
|
|
175
|
+
background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='10' viewBox='0 0 24 24' fill='none' stroke='%239498ab' stroke-width='2'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
|
|
176
|
+
background-repeat:no-repeat;background-position:right 6px center;padding-right:20px;
|
|
177
|
+
}
|
|
178
|
+
.ctrl-row input[type="range"]{padding:2px 0;background:none;border:none}
|
|
179
|
+
.range-val{font-size:.58rem;color:var(--text-3);width:28px;text-align:right;flex-shrink:0}
|
|
180
|
+
|
|
181
|
+
/* ── Pill Buttons ── */
|
|
182
|
+
.pill-row{display:flex;flex-wrap:wrap;gap:6px}
|
|
183
|
+
.pill{
|
|
184
|
+
font-size:.75rem;font-weight:500;padding:6px 14px;border-radius:20px;
|
|
185
|
+
border:1px solid var(--border);color:var(--text-2);background:var(--surface-2);
|
|
186
|
+
cursor:pointer;transition:all .15s;
|
|
187
|
+
}
|
|
188
|
+
.pill:hover{border-color:var(--border-light);color:var(--text)}
|
|
189
|
+
.pill.active{background:var(--brand-dim);border-color:var(--brand-border);color:var(--brand-light)}
|
|
190
|
+
|
|
191
|
+
/* ── Chip Input ── */
|
|
192
|
+
.chip-input-wrap{
|
|
193
|
+
display:flex;flex-wrap:wrap;gap:6px;align-items:center;
|
|
194
|
+
background:var(--surface-2);border:1px solid var(--border);border-radius:var(--r-xs);
|
|
195
|
+
padding:6px 8px;min-height:38px;cursor:text;transition:border-color .15s;
|
|
196
|
+
}
|
|
197
|
+
.chip-input-wrap:focus-within{border-color:var(--brand-border)}
|
|
198
|
+
.chip{
|
|
199
|
+
display:inline-flex;align-items:center;gap:4px;
|
|
200
|
+
font-size:.75rem;font-weight:500;padding:3px 8px;border-radius:12px;
|
|
201
|
+
background:var(--brand-dim);border:1px solid var(--brand-border);color:var(--brand-light);
|
|
202
|
+
}
|
|
203
|
+
.chip .x{cursor:pointer;opacity:.6;font-size:.85em}
|
|
204
|
+
.chip .x:hover{opacity:1}
|
|
205
|
+
.chip-input{
|
|
206
|
+
background:none;border:none;outline:none;color:var(--text);
|
|
207
|
+
font-size:.8rem;min-width:80px;flex:1;
|
|
208
|
+
}
|
|
209
|
+
.chip-input::placeholder{color:var(--text-3)}
|
|
210
|
+
|
|
211
|
+
/* ── Toggle Chips ── */
|
|
212
|
+
.toggle-chip{
|
|
213
|
+
font-size:.75rem;font-weight:500;padding:6px 12px;border-radius:20px;
|
|
214
|
+
border:1px solid var(--border);color:var(--text-2);background:var(--surface-2);
|
|
215
|
+
cursor:pointer;transition:all .15s;user-select:none;
|
|
216
|
+
}
|
|
217
|
+
.toggle-chip:hover{border-color:var(--border-light)}
|
|
218
|
+
.toggle-chip.on{background:var(--brand-dim);border-color:var(--brand-border);color:var(--brand-light)}
|
|
219
|
+
|
|
220
|
+
/* ── List Input ── */
|
|
221
|
+
.list-items{display:flex;flex-direction:column;gap:6px;margin-bottom:8px}
|
|
222
|
+
.list-item{
|
|
223
|
+
display:flex;align-items:center;gap:8px;
|
|
224
|
+
background:var(--surface-2);border:1px solid var(--border);border-radius:var(--r-xs);padding:8px 10px;
|
|
225
|
+
}
|
|
226
|
+
.list-item input{flex:1;background:none;border:none;outline:none;color:var(--text);font-size:.8rem}
|
|
227
|
+
.list-item .rm{
|
|
228
|
+
width:20px;height:20px;display:flex;align-items:center;justify-content:center;
|
|
229
|
+
border:none;background:none;color:var(--text-3);cursor:pointer;font-size:.9rem;
|
|
230
|
+
}
|
|
231
|
+
.list-item .rm:hover{color:var(--red)}
|
|
232
|
+
.add-item-btn{
|
|
233
|
+
font-size:.75rem;color:var(--brand-light);background:none;border:1px dashed var(--brand-border);
|
|
234
|
+
border-radius:var(--r-xs);padding:7px 12px;cursor:pointer;width:100%;transition:all .15s;
|
|
235
|
+
}
|
|
236
|
+
.add-item-btn:hover{background:var(--brand-dim)}
|
|
237
|
+
|
|
238
|
+
/* ── Drop Zone (uploads) ── */
|
|
239
|
+
.drop-zone{
|
|
240
|
+
border:1.5px dashed var(--border);border-radius:var(--r-sm);
|
|
241
|
+
padding:20px;text-align:center;cursor:pointer;
|
|
242
|
+
transition:all .2s;background:var(--surface-2);position:relative;
|
|
243
|
+
}
|
|
244
|
+
.drop-zone:hover,.drop-zone.hover{border-color:var(--brand);background:var(--brand-glow)}
|
|
245
|
+
.drop-zone.small{padding:16px;display:flex;flex-direction:column;align-items:center;gap:4px}
|
|
246
|
+
.drop-zone .dz-label{font-size:.78rem;color:var(--text);font-weight:500}
|
|
247
|
+
.drop-zone .dz-hint{font-size:.65rem;color:var(--text-3)}
|
|
248
|
+
.drop-zone input[type="file"]{display:none}
|
|
249
|
+
.drop-zone.has-img{padding:0;border-style:solid;border-color:var(--brand-border);overflow:hidden}
|
|
250
|
+
.drop-zone.has-img img{width:100%;height:100%;object-fit:cover;display:block}
|
|
251
|
+
.logo-zone{width:120px;height:120px}
|
|
252
|
+
.favicon-zone{width:48px;height:48px}
|
|
253
|
+
.hero-zone{width:100%;min-height:160px}
|
|
254
|
+
.team-zone{width:200px;height:140px}
|
|
255
|
+
|
|
256
|
+
/* ── Upload Preview Grid ── */
|
|
257
|
+
.upload-grid{
|
|
258
|
+
display:grid;grid-template-columns:repeat(auto-fill,minmax(140px,1fr));
|
|
259
|
+
gap:8px;margin-top:10px;
|
|
260
|
+
}
|
|
261
|
+
.upload-card{
|
|
262
|
+
background:var(--surface-2);border:1px solid var(--border);border-radius:var(--r-xs);
|
|
263
|
+
overflow:hidden;
|
|
264
|
+
}
|
|
265
|
+
.upload-card .thumb{aspect-ratio:1;overflow:hidden;background:var(--surface-3)}
|
|
266
|
+
.upload-card .thumb img{width:100%;height:100%;object-fit:cover}
|
|
267
|
+
.upload-card .meta{padding:6px 8px}
|
|
268
|
+
.upload-card .fname{font-size:.68rem;color:var(--text);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
269
|
+
.upload-card input{
|
|
270
|
+
width:100%;background:none;border:none;border-top:1px solid var(--border);
|
|
271
|
+
padding:4px 8px;font-size:.68rem;color:var(--text-2);outline:none;
|
|
272
|
+
}
|
|
273
|
+
.upload-card .tag-row{display:flex;flex-wrap:wrap;gap:3px;padding:4px 8px}
|
|
274
|
+
.upload-card .tag{
|
|
275
|
+
font-size:.58rem;padding:2px 6px;border-radius:8px;cursor:pointer;
|
|
276
|
+
border:1px solid var(--border);color:var(--text-3);transition:all .12s;
|
|
277
|
+
}
|
|
278
|
+
.upload-card .tag.on{background:var(--brand-dim);border-color:var(--brand-border);color:var(--brand-light)}
|
|
279
|
+
|
|
280
|
+
/* ── Color Picker ── */
|
|
281
|
+
.color-row{display:flex;gap:12px;align-items:center;flex-wrap:wrap}
|
|
282
|
+
.color-pick{display:flex;align-items:center;gap:6px}
|
|
283
|
+
.color-pick label{text-transform:none;font-size:.72rem}
|
|
284
|
+
.color-pick input[type="color"]{
|
|
285
|
+
width:32px;height:32px;border:1px solid var(--border);border-radius:var(--r-xs);
|
|
286
|
+
background:none;cursor:pointer;padding:2px;
|
|
287
|
+
}
|
|
288
|
+
.reset-link{font-size:.72rem;color:var(--brand-light);cursor:pointer;text-decoration:underline}
|
|
289
|
+
|
|
290
|
+
/* ── Details toggle ── */
|
|
291
|
+
details{margin-top:8px}
|
|
292
|
+
details summary{
|
|
293
|
+
font-size:.75rem;color:var(--text-3);cursor:pointer;list-style:none;
|
|
294
|
+
display:flex;align-items:center;gap:4px;
|
|
295
|
+
}
|
|
296
|
+
details summary::-webkit-details-marker{display:none}
|
|
297
|
+
details[open] summary .arrow{transform:rotate(90deg)}
|
|
298
|
+
.arrow{display:inline-block;transition:transform .15s;font-size:.7rem}
|
|
299
|
+
|
|
300
|
+
/* ── Action Bar ── */
|
|
301
|
+
.action-bar{
|
|
302
|
+
position:fixed;bottom:0;left:0;right:0;z-index:100;
|
|
303
|
+
background:rgba(19,20,26,.95);backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);
|
|
304
|
+
border-top:1px solid var(--border);
|
|
305
|
+
padding:10px 16px;display:flex;align-items:center;justify-content:space-between;gap:12px;
|
|
306
|
+
}
|
|
307
|
+
.save-indicator{display:flex;align-items:center;gap:6px;font-size:.72rem;color:var(--text-3)}
|
|
308
|
+
.save-dot{width:6px;height:6px;border-radius:50%;background:var(--text-3);flex-shrink:0;transition:background .3s}
|
|
309
|
+
.save-dot.saving{background:var(--brand);animation:pulse 1s infinite}
|
|
310
|
+
.save-dot.saved{background:var(--green)}
|
|
311
|
+
.save-dot.error{background:var(--red)}
|
|
312
|
+
.action-buttons{display:flex;gap:8px}
|
|
313
|
+
.btn{
|
|
314
|
+
display:inline-flex;align-items:center;gap:6px;
|
|
315
|
+
padding:9px 18px;border-radius:var(--r-sm);border:1px solid var(--border);
|
|
316
|
+
background:var(--surface);color:var(--text);font-size:.8rem;font-weight:600;
|
|
317
|
+
cursor:pointer;transition:all .15s;font-family:inherit;
|
|
318
|
+
}
|
|
319
|
+
.btn:hover{border-color:var(--border-light);background:var(--surface-hover)}
|
|
320
|
+
.btn.primary{background:var(--brand);border-color:var(--brand);color:#fff;box-shadow:0 4px 12px rgba(59,130,246,.25)}
|
|
321
|
+
.btn.primary:hover{background:#2563eb;box-shadow:0 6px 20px rgba(59,130,246,.35)}
|
|
322
|
+
.btn:disabled{opacity:.4;cursor:not-allowed;transform:none;box-shadow:none}
|
|
323
|
+
|
|
324
|
+
/* ── Toast ── */
|
|
325
|
+
.toast{
|
|
326
|
+
position:fixed;bottom:76px;left:50%;transform:translateX(-50%) translateY(60px);
|
|
327
|
+
background:var(--surface-3);border:1px solid var(--border);color:var(--text);
|
|
328
|
+
padding:8px 18px;border-radius:var(--r-sm);font-size:.78rem;font-weight:500;
|
|
329
|
+
box-shadow:0 8px 24px rgba(0,0,0,.5);transition:transform .25s;z-index:300;pointer-events:none;
|
|
330
|
+
}
|
|
331
|
+
.toast.on{transform:translateX(-50%) translateY(0)}
|
|
332
|
+
|
|
333
|
+
/* ── Responsive ── */
|
|
334
|
+
@media(max-width:768px){
|
|
335
|
+
.form-grid{grid-template-columns:1fr}
|
|
336
|
+
.form-group.span-2{grid-column:auto}
|
|
337
|
+
.card-select{grid-template-columns:1fr 1fr}
|
|
338
|
+
.theme-grid{grid-template-columns:repeat(auto-fill,minmax(140px,1fr))}
|
|
339
|
+
.page-header{flex-wrap:wrap}
|
|
340
|
+
}
|
|
341
|
+
@media(max-width:480px){
|
|
342
|
+
.card-select{grid-template-columns:1fr}
|
|
343
|
+
.theme-grid{grid-template-columns:1fr 1fr}
|
|
344
|
+
.action-buttons{flex-direction:column;width:100%}
|
|
345
|
+
.action-buttons .btn{justify-content:center}
|
|
346
|
+
.action-bar{flex-direction:column;align-items:stretch}
|
|
347
|
+
}
|
|
348
|
+
</style>
|
|
349
|
+
</head>
|
|
350
|
+
<body>
|
|
351
|
+
<div class="container">
|
|
352
|
+
|
|
353
|
+
<!-- ═══════════ SECTION 1: HEADER ═══════════ -->
|
|
354
|
+
<div class="page-header">
|
|
355
|
+
<div class="hdr-left">
|
|
356
|
+
<div class="hdr-icon"><svg viewBox="0 0 24 24"><path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/></svg></div>
|
|
357
|
+
<div>
|
|
358
|
+
<div class="hdr-title">Website Setup</div>
|
|
359
|
+
<div class="hdr-sub">Fill in what you know. AI handles the rest.</div>
|
|
360
|
+
</div>
|
|
361
|
+
</div>
|
|
362
|
+
<span class="status-badge draft" id="statusBadge">Draft</span>
|
|
363
|
+
</div>
|
|
364
|
+
|
|
365
|
+
<!-- ═══════════ SECTION 2: THE ESSENTIALS ═══════════ -->
|
|
366
|
+
<div class="section">
|
|
367
|
+
<div class="section-card essential">
|
|
368
|
+
<div class="section-header open" onclick="toggleSection(this)">
|
|
369
|
+
<h2>The Essentials</h2>
|
|
370
|
+
<svg class="chevron" viewBox="0 0 24 24"><polyline points="6 9 12 15 18 9"/></svg>
|
|
371
|
+
</div>
|
|
372
|
+
<div class="section-body open">
|
|
373
|
+
<div class="form-grid">
|
|
374
|
+
<div class="form-group">
|
|
375
|
+
<label>Business Name *</label>
|
|
376
|
+
<div class="field-with-ai">
|
|
377
|
+
<input type="text" id="f-businessName" data-field="basics.businessName" placeholder="e.g. Acme Roofing">
|
|
378
|
+
<button class="ai-btn" onclick="aiEnhance('businessName',event)"><svg viewBox="0 0 24 24"><path d="M12 3v4m0 14v-4m9-5h-4M3 12h4m12.36-5.36l-2.83 2.83M6.47 17.53l-2.83 2.83m14.72 0l-2.83-2.83M6.47 6.47L3.64 3.64"/></svg>Enhance</button>
|
|
379
|
+
</div>
|
|
380
|
+
</div>
|
|
381
|
+
<div class="form-group">
|
|
382
|
+
<label>Domain</label>
|
|
383
|
+
<input type="text" id="f-domain" data-field="basics.domain" placeholder="e.g. acmeroofing.com">
|
|
384
|
+
</div>
|
|
385
|
+
<div class="form-group">
|
|
386
|
+
<label>Industry / Niche *</label>
|
|
387
|
+
<div class="field-with-ai">
|
|
388
|
+
<input type="text" id="f-industry" data-field="basics.industry" placeholder="e.g. Residential Roofing">
|
|
389
|
+
<button class="ai-btn" onclick="aiSuggest('industry',event)"><svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>Suggest</button>
|
|
390
|
+
</div>
|
|
391
|
+
</div>
|
|
392
|
+
<div class="form-group span-2">
|
|
393
|
+
<label>One-Line Description * <span class="char-count" id="descCount">0/160</span></label>
|
|
394
|
+
<div class="field-with-ai">
|
|
395
|
+
<textarea id="f-description" data-field="basics.description" rows="2" maxlength="160" placeholder="What does this business do? One sentence." oninput="updDescCount()"></textarea>
|
|
396
|
+
<div style="display:flex;flex-direction:column;gap:4px">
|
|
397
|
+
<button class="ai-btn" onclick="aiGenerate('description',event)"><svg viewBox="0 0 24 24"><path d="M12 3v4m0 14v-4m9-5h-4M3 12h4m12.36-5.36l-2.83 2.83M6.47 17.53l-2.83 2.83m14.72 0l-2.83-2.83M6.47 6.47L3.64 3.64"/></svg>Generate</button>
|
|
398
|
+
<button class="ai-btn" onclick="aiEnhance('description',event)"><svg viewBox="0 0 24 24"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg>Enhance</button>
|
|
399
|
+
</div>
|
|
400
|
+
</div>
|
|
401
|
+
</div>
|
|
402
|
+
<div class="form-group span-2">
|
|
403
|
+
<label>Site Type *</label>
|
|
404
|
+
<div class="card-select" id="siteTypeCards">
|
|
405
|
+
<div class="sel-card" data-val="local-service" onclick="selectCard(this,'siteType')">
|
|
406
|
+
<div class="ic">📞</div><div class="nm">Local Service</div><div class="desc">Contractor / local SEO</div>
|
|
407
|
+
</div>
|
|
408
|
+
<div class="sel-card" data-val="ecommerce" onclick="selectCard(this,'siteType')">
|
|
409
|
+
<div class="ic">🛒</div><div class="nm">E-Commerce</div><div class="desc">Merch / national + global SEO</div>
|
|
410
|
+
</div>
|
|
411
|
+
<div class="sel-card" data-val="portfolio" onclick="selectCard(this,'siteType')">
|
|
412
|
+
<div class="ic">👤</div><div class="nm">Portfolio</div><div class="desc">Personal / author brand SEO</div>
|
|
413
|
+
</div>
|
|
414
|
+
<div class="sel-card" data-val="webapp" onclick="selectCard(this,'siteType')">
|
|
415
|
+
<div class="ic">💻</div><div class="nm">Web App</div><div class="desc">SaaS / interactive tool</div>
|
|
416
|
+
</div>
|
|
417
|
+
</div>
|
|
418
|
+
</div>
|
|
419
|
+
</div>
|
|
420
|
+
</div>
|
|
421
|
+
</div>
|
|
422
|
+
</div>
|
|
423
|
+
|
|
424
|
+
<!-- ═══════════ SECTION 3: BRANDING & STYLE ═══════════ -->
|
|
425
|
+
<div class="section">
|
|
426
|
+
<div class="section-card">
|
|
427
|
+
<div class="section-header open" onclick="toggleSection(this)">
|
|
428
|
+
<h2>Branding & Style</h2>
|
|
429
|
+
<svg class="chevron" viewBox="0 0 24 24"><polyline points="6 9 12 15 18 9"/></svg>
|
|
430
|
+
</div>
|
|
431
|
+
<div class="section-body open">
|
|
432
|
+
|
|
433
|
+
<!-- Theme Dropdown + Color Pickers -->
|
|
434
|
+
<div class="form-grid">
|
|
435
|
+
<div class="form-group span-2">
|
|
436
|
+
<label>Color Theme</label>
|
|
437
|
+
<select class="theme-select" id="themeSelect" onchange="selectTheme(this.value)">
|
|
438
|
+
<option value="">-- Pick a theme --</option>
|
|
439
|
+
</select>
|
|
440
|
+
<div class="swatch-bar" id="swatchBar"><div></div><div></div><div></div><div></div></div>
|
|
441
|
+
<div class="swatch-labels"><span>Primary</span><span>Secondary</span><span>Accent</span><span>Background</span></div>
|
|
442
|
+
</div>
|
|
443
|
+
</div>
|
|
444
|
+
|
|
445
|
+
<!-- Color Pickers (always visible) -->
|
|
446
|
+
<div style="margin-top:10px">
|
|
447
|
+
<div class="color-row">
|
|
448
|
+
<div class="color-pick"><label>Primary</label><input type="color" id="f-colorPrimary" value="#4F46E5" oninput="setCustomColor('primary',this.value)"></div>
|
|
449
|
+
<div class="color-pick"><label>Secondary</label><input type="color" id="f-colorSecondary" value="#0F172A" oninput="setCustomColor('secondary',this.value)"></div>
|
|
450
|
+
<div class="color-pick"><label>Accent</label><input type="color" id="f-colorAccent" value="#F59E0B" oninput="setCustomColor('accent',this.value)"></div>
|
|
451
|
+
<div class="color-pick"><label>Background</label><input type="color" id="f-colorBg" value="#F8FAFC" oninput="setCustomColor('background',this.value)"></div>
|
|
452
|
+
</div>
|
|
453
|
+
</div>
|
|
454
|
+
|
|
455
|
+
<!-- Style Builder: Preview + Controls side by side -->
|
|
456
|
+
<div class="style-builder">
|
|
457
|
+
<div class="style-preview-col">
|
|
458
|
+
<div class="preview-frame" id="previewFrame">
|
|
459
|
+
<div class="pv-nav" id="pvNav">
|
|
460
|
+
<div class="pv-logo" id="pvLogo">Business Name</div>
|
|
461
|
+
<div class="pv-links"><span>Home</span><span>Services</span><span>About</span><span>Contact</span></div>
|
|
462
|
+
<div class="pv-btn" id="pvCta">Get a Quote</div>
|
|
463
|
+
</div>
|
|
464
|
+
<div class="pv-hero" id="pvHero">
|
|
465
|
+
<h2 id="pvHeadline">Your Headline Goes Here</h2>
|
|
466
|
+
<p id="pvSubtext">A short description of what you do and why customers should choose you.</p>
|
|
467
|
+
<div class="pv-btn" id="pvHeroCta" style="display:inline-block">Call to Action</div>
|
|
468
|
+
</div>
|
|
469
|
+
<div class="pv-cards" id="pvCards">
|
|
470
|
+
<div class="pv-card" id="pvCard1"><div class="pv-card-title">Service One</div><div class="pv-card-text">Brief description of this offering.</div></div>
|
|
471
|
+
<div class="pv-card" id="pvCard2"><div class="pv-card-title">Service Two</div><div class="pv-card-text">Brief description of this offering.</div></div>
|
|
472
|
+
</div>
|
|
473
|
+
</div>
|
|
474
|
+
</div>
|
|
475
|
+
<div class="style-controls-col">
|
|
476
|
+
|
|
477
|
+
<!-- Fonts -->
|
|
478
|
+
<div class="ctrl-group">
|
|
479
|
+
<div class="ctrl-group-title">Typography</div>
|
|
480
|
+
<div class="ctrl-row"><label>Headings</label><select id="f-fontHeading" onchange="setStyle('fontHeading',this.value)"><option value="system-ui, sans-serif">System Default</option><option value="'Inter', sans-serif">Inter</option><option value="'Poppins', sans-serif">Poppins</option><option value="'Montserrat', sans-serif">Montserrat</option><option value="'Playfair Display', serif">Playfair Display</option><option value="'Merriweather', serif">Merriweather</option><option value="'Roboto Slab', serif">Roboto Slab</option><option value="'Space Grotesk', sans-serif">Space Grotesk</option><option value="'DM Sans', sans-serif">DM Sans</option></select></div>
|
|
481
|
+
<div class="ctrl-row"><label>Body</label><select id="f-fontBody" onchange="setStyle('fontBody',this.value)"><option value="system-ui, sans-serif">System Default</option><option value="'Inter', sans-serif">Inter</option><option value="'Open Sans', sans-serif">Open Sans</option><option value="'Lato', sans-serif">Lato</option><option value="'Nunito', sans-serif">Nunito</option><option value="'DM Sans', sans-serif">DM Sans</option><option value="'IBM Plex Sans', sans-serif">IBM Plex Sans</option></select></div>
|
|
482
|
+
<div class="ctrl-row"><label>Weight</label><select id="f-fontWeight" onchange="setStyle('fontWeight',this.value)"><option value="normal">Normal</option><option value="bold" selected>Bold</option><option value="extra">Extra Bold</option><option value="light">Light</option></select></div>
|
|
483
|
+
<div class="ctrl-row"><label>Style</label><select id="f-fontStyle" onchange="setStyle('fontStyle',this.value)"><option value="none" selected>None</option><option value="shadow">Shadow</option><option value="outline">Outline</option><option value="glow">Glow</option></select></div>
|
|
484
|
+
</div>
|
|
485
|
+
|
|
486
|
+
<!-- Navigation -->
|
|
487
|
+
<div class="ctrl-group">
|
|
488
|
+
<div class="ctrl-group-title">Navigation</div>
|
|
489
|
+
<div class="ctrl-row"><label>Corners</label><select id="f-navRadius" onchange="setStyle('navRadius',this.value)"><option value="0">Square</option><option value="6">Slight</option><option value="12" selected>Rounded</option><option value="24">Pill</option></select></div>
|
|
490
|
+
<div class="ctrl-row"><label>Shadow</label><select id="f-navShadow" onchange="setStyle('navShadow',this.value)"><option value="none">None</option><option value="sm" selected>Subtle</option><option value="lg">Strong</option></select></div>
|
|
491
|
+
<div class="ctrl-row"><label>Glow</label><select id="f-navGlow" onchange="setStyle('navGlow',this.value)"><option value="none" selected>None</option><option value="soft">Soft</option><option value="bright">Bright</option></select></div>
|
|
492
|
+
<div class="ctrl-row"><label>Transparency</label><input type="range" id="f-navOpacity" min="70" max="100" value="100" oninput="setStyle('navOpacity',this.value)"><span class="range-val" id="navOpacityVal">100%</span></div>
|
|
493
|
+
</div>
|
|
494
|
+
|
|
495
|
+
<!-- Buttons -->
|
|
496
|
+
<div class="ctrl-group">
|
|
497
|
+
<div class="ctrl-group-title">Buttons</div>
|
|
498
|
+
<div class="ctrl-row"><label>Fill</label><select id="f-btnFill" onchange="setStyle('btnFill',this.value)"><option value="filled" selected>Filled</option><option value="outline">Outline</option><option value="ghost">Ghost</option><option value="gradient">Gradient</option></select></div>
|
|
499
|
+
<div class="ctrl-row"><label>Corners</label><select id="f-btnRadius" onchange="setStyle('btnRadius',this.value)"><option value="0">Square</option><option value="6" selected>Rounded</option><option value="12">More Rounded</option><option value="50">Pill</option></select></div>
|
|
500
|
+
<div class="ctrl-row"><label>Shadow</label><select id="f-btnShadow" onchange="setStyle('btnShadow',this.value)"><option value="none" selected>None</option><option value="sm">Subtle</option><option value="lg">Strong</option></select></div>
|
|
501
|
+
<div class="ctrl-row"><label>Glow</label><select id="f-btnGlow" onchange="setStyle('btnGlow',this.value)"><option value="none" selected>None</option><option value="soft">Soft</option><option value="bright">Bright</option></select></div>
|
|
502
|
+
<div class="ctrl-row"><label>Border</label><input type="range" id="f-btnBorder" min="0" max="4" value="0" oninput="setStyle('btnBorder',this.value)"><span class="range-val" id="btnBorderVal">0px</span></div>
|
|
503
|
+
</div>
|
|
504
|
+
|
|
505
|
+
<!-- Cards / Containers -->
|
|
506
|
+
<div class="ctrl-group">
|
|
507
|
+
<div class="ctrl-group-title">Cards / Containers</div>
|
|
508
|
+
<div class="ctrl-row"><label>Corners</label><select id="f-cardRadius" onchange="setStyle('cardRadius',this.value)"><option value="0">Square</option><option value="6">Slight</option><option value="12" selected>Rounded</option><option value="20">Very Round</option></select></div>
|
|
509
|
+
<div class="ctrl-row"><label>Shadow</label><select id="f-cardShadow" onchange="setStyle('cardShadow',this.value)"><option value="none">None</option><option value="sm" selected>Subtle</option><option value="md">Medium</option><option value="lg">Strong</option><option value="xl">Dramatic</option></select></div>
|
|
510
|
+
<div class="ctrl-row"><label>Glow</label><select id="f-cardGlow" onchange="setStyle('cardGlow',this.value)"><option value="none" selected>None</option><option value="soft">Soft</option><option value="bright">Bright</option></select></div>
|
|
511
|
+
<div class="ctrl-row"><label>Inner Glow</label><select id="f-cardInnerGlow" onchange="setStyle('cardInnerGlow',this.value)"><option value="none" selected>None</option><option value="soft">Soft</option><option value="bright">Bright</option></select></div>
|
|
512
|
+
<div class="ctrl-row"><label>Border</label><input type="range" id="f-cardBorder" min="0" max="4" value="1" oninput="setStyle('cardBorder',this.value)"><span class="range-val" id="cardBorderVal">1px</span></div>
|
|
513
|
+
<div class="ctrl-row"><label>Transparency</label><input type="range" id="f-cardOpacity" min="50" max="100" value="100" oninput="setStyle('cardOpacity',this.value)"><span class="range-val" id="cardOpacityVal">100%</span></div>
|
|
514
|
+
<div class="ctrl-row"><label>Glass</label><select id="f-cardGlass" onchange="setStyle('cardGlass',this.value)"><option value="none" selected>None</option><option value="light">Light</option><option value="heavy">Heavy</option></select></div>
|
|
515
|
+
</div>
|
|
516
|
+
|
|
517
|
+
<!-- Page / Layout -->
|
|
518
|
+
<div class="ctrl-group">
|
|
519
|
+
<div class="ctrl-group-title">Page Layout</div>
|
|
520
|
+
<div class="ctrl-row"><label>Max Width</label><select id="f-pageWidth" onchange="setStyle('pageWidth',this.value)"><option value="narrow">Narrow (960px)</option><option value="standard" selected>Standard (1200px)</option><option value="wide">Wide (1400px)</option><option value="full">Full Width</option></select></div>
|
|
521
|
+
<div class="ctrl-row"><label>Spacing</label><select id="f-spacing" onchange="setStyle('spacing',this.value)"><option value="tight">Tight</option><option value="normal" selected>Normal</option><option value="airy">Airy</option><option value="spacious">Spacious</option></select></div>
|
|
522
|
+
<div class="ctrl-row"><label>Animations</label><select id="f-animations" onchange="setStyle('animations',this.value)"><option value="none">None</option><option value="subtle" selected>Subtle</option><option value="smooth">Smooth</option><option value="playful">Playful</option></select></div>
|
|
523
|
+
</div>
|
|
524
|
+
|
|
525
|
+
</div>
|
|
526
|
+
</div>
|
|
527
|
+
|
|
528
|
+
<!-- Tone -->
|
|
529
|
+
<label style="margin:14px 0 6px;display:block">Tone</label>
|
|
530
|
+
<div class="pill-row" id="tonePills">
|
|
531
|
+
<span class="pill" data-val="professional" onclick="selectPill(this,'tone')">Professional</span>
|
|
532
|
+
<span class="pill" data-val="friendly" onclick="selectPill(this,'tone')">Friendly</span>
|
|
533
|
+
<span class="pill" data-val="bold" onclick="selectPill(this,'tone')">Bold</span>
|
|
534
|
+
<span class="pill" data-val="playful" onclick="selectPill(this,'tone')">Playful</span>
|
|
535
|
+
<span class="pill" data-val="luxury" onclick="selectPill(this,'tone')">Luxury</span>
|
|
536
|
+
<span class="pill" data-val="technical" onclick="selectPill(this,'tone')">Technical</span>
|
|
537
|
+
</div>
|
|
538
|
+
|
|
539
|
+
<!-- Logo / Favicon -->
|
|
540
|
+
<div style="display:flex;gap:16px;margin-top:14px;flex-wrap:wrap;align-items:flex-start">
|
|
541
|
+
<div>
|
|
542
|
+
<label style="margin-bottom:6px;display:block">Logo</label>
|
|
543
|
+
<div class="drop-zone small logo-zone" id="logoZone" onclick="document.getElementById('logoFile').click()">
|
|
544
|
+
<span class="dz-label">Drop logo</span><span class="dz-hint">120x120</span>
|
|
545
|
+
<input type="file" id="logoFile" accept="image/*" onchange="handleUpload('logo',this.files[0])">
|
|
546
|
+
</div>
|
|
547
|
+
</div>
|
|
548
|
+
<div>
|
|
549
|
+
<label style="margin-bottom:6px;display:block">Favicon</label>
|
|
550
|
+
<div class="drop-zone small favicon-zone" id="faviconZone" onclick="document.getElementById('faviconFile').click()">
|
|
551
|
+
<span class="dz-label">+</span>
|
|
552
|
+
<input type="file" id="faviconFile" accept="image/*" onchange="handleUpload('favicon',this.files[0])">
|
|
553
|
+
</div>
|
|
554
|
+
</div>
|
|
555
|
+
</div>
|
|
556
|
+
</div>
|
|
557
|
+
</div>
|
|
558
|
+
</div>
|
|
559
|
+
|
|
560
|
+
<!-- ═══════════ SECTION 4: KEY IMAGES ═══════════ -->
|
|
561
|
+
<div class="section">
|
|
562
|
+
<div class="section-card">
|
|
563
|
+
<div class="section-header open" onclick="toggleSection(this)">
|
|
564
|
+
<h2>Key Images</h2>
|
|
565
|
+
<svg class="chevron" viewBox="0 0 24 24"><polyline points="6 9 12 15 18 9"/></svg>
|
|
566
|
+
</div>
|
|
567
|
+
<div class="section-body open">
|
|
568
|
+
<label style="margin-bottom:6px;display:block">Hero Image</label>
|
|
569
|
+
<div class="drop-zone hero-zone" id="heroZone" onclick="document.getElementById('heroFile').click()">
|
|
570
|
+
<span class="dz-label">Drop hero image here</span><span class="dz-hint">Main banner image</span>
|
|
571
|
+
<input type="file" id="heroFile" accept="image/*" onchange="handleUpload('hero',this.files[0])">
|
|
572
|
+
</div>
|
|
573
|
+
<div class="form-group" style="margin-top:8px">
|
|
574
|
+
<div class="field-with-ai">
|
|
575
|
+
<textarea id="f-heroNotes" data-field="images.hero.notes" rows="2" placeholder="How should this image be used? Any context for the AI..."></textarea>
|
|
576
|
+
<button class="ai-btn" onclick="aiEnhance('heroNotes',event)"><svg viewBox="0 0 24 24"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg>Enhance</button>
|
|
577
|
+
</div>
|
|
578
|
+
</div>
|
|
579
|
+
|
|
580
|
+
<div style="margin-top:12px">
|
|
581
|
+
<label style="margin-bottom:6px;display:block">Team Photo (optional)</label>
|
|
582
|
+
<div class="drop-zone small team-zone" id="teamZone" onclick="document.getElementById('teamFile').click()">
|
|
583
|
+
<span class="dz-label">Drop team photo</span><span class="dz-hint">Optional</span>
|
|
584
|
+
<input type="file" id="teamFile" accept="image/*" onchange="handleUpload('team',this.files[0])">
|
|
585
|
+
</div>
|
|
586
|
+
</div>
|
|
587
|
+
|
|
588
|
+
<details style="margin-top:12px">
|
|
589
|
+
<summary><span class="arrow">▶</span> Add More Images</summary>
|
|
590
|
+
<div style="padding-top:10px">
|
|
591
|
+
<div class="drop-zone" id="bulkZone" onclick="document.getElementById('bulkFiles').click()">
|
|
592
|
+
<span class="dz-label">Drop additional images here</span>
|
|
593
|
+
<span class="dz-hint">Service photos, gallery images, backgrounds</span>
|
|
594
|
+
<input type="file" id="bulkFiles" accept="image/*" multiple onchange="handleBulkUpload(this.files)">
|
|
595
|
+
</div>
|
|
596
|
+
<div class="upload-grid" id="galleryGrid"></div>
|
|
597
|
+
</div>
|
|
598
|
+
</details>
|
|
599
|
+
</div>
|
|
600
|
+
</div>
|
|
601
|
+
</div>
|
|
602
|
+
|
|
603
|
+
<!-- ═══════════ SECTION 5: COMPANY INFO ═══════════ -->
|
|
604
|
+
<div class="section">
|
|
605
|
+
<div class="section-card">
|
|
606
|
+
<div class="section-header open" onclick="toggleSection(this)">
|
|
607
|
+
<h2>Company Info</h2>
|
|
608
|
+
<svg class="chevron" viewBox="0 0 24 24"><polyline points="6 9 12 15 18 9"/></svg>
|
|
609
|
+
</div>
|
|
610
|
+
<div class="section-body open">
|
|
611
|
+
<div class="form-grid">
|
|
612
|
+
<div class="form-group"><label>Phone</label><input type="tel" id="f-phone" data-field="basics.phone" placeholder="(555) 123-4567"></div>
|
|
613
|
+
<div class="form-group"><label>Email</label><input type="email" id="f-email" data-field="basics.email" placeholder="info@business.com"></div>
|
|
614
|
+
<div class="form-group span-2"><label>Address</label><input type="text" id="f-address" data-field="basics.address" placeholder="123 Main St, City, State ZIP"></div>
|
|
615
|
+
<div class="form-group span-2">
|
|
616
|
+
<label>About the Business</label>
|
|
617
|
+
<div class="field-with-ai">
|
|
618
|
+
<textarea id="f-aboutBusiness" data-field="basics.about" rows="3" placeholder="What does the business do? History, mission, what makes it different..."></textarea>
|
|
619
|
+
<button class="ai-btn" onclick="aiGenerate('aboutBusiness',event)"><svg viewBox="0 0 24 24"><path d="M12 3v4m0 14v-4m9-5h-4M3 12h4m12.36-5.36l-2.83 2.83M6.47 17.53l-2.83 2.83m14.72 0l-2.83-2.83M6.47 6.47L3.64 3.64"/></svg>Generate</button>
|
|
620
|
+
</div>
|
|
621
|
+
</div>
|
|
622
|
+
<div class="form-group"><label>Owner / Contact Name</label><input type="text" id="f-ownerName" data-field="basics.ownerName" placeholder="e.g. John Smith"></div>
|
|
623
|
+
<div class="form-group"><label>Years in Business</label><input type="text" id="f-yearsInBusiness" data-field="basics.yearsInBusiness" placeholder="e.g. 15"></div>
|
|
624
|
+
<div class="form-group"><label>License / Certification</label><input type="text" id="f-license" data-field="basics.license" placeholder="e.g. ROC #123456, Licensed & Insured"></div>
|
|
625
|
+
<div class="form-group"><label>Google Business Profile URL</label><input type="url" id="f-gbpUrl" data-field="basics.gbpUrl" placeholder="https://g.page/your-business"></div>
|
|
626
|
+
</div>
|
|
627
|
+
</div>
|
|
628
|
+
</div>
|
|
629
|
+
</div>
|
|
630
|
+
|
|
631
|
+
<!-- ═══════════ SECTION 5B: SERVICES, AREAS & TOPICS ═══════════ -->
|
|
632
|
+
<div class="section">
|
|
633
|
+
<div class="section-card">
|
|
634
|
+
<div class="section-header open" onclick="toggleSection(this)">
|
|
635
|
+
<h2>Services, Areas & Topics</h2>
|
|
636
|
+
<svg class="chevron" viewBox="0 0 24 24"><polyline points="6 9 12 15 18 9"/></svg>
|
|
637
|
+
</div>
|
|
638
|
+
<div class="section-body open">
|
|
639
|
+
<div class="form-group">
|
|
640
|
+
<label>Services Offered</label>
|
|
641
|
+
<div class="field-with-ai">
|
|
642
|
+
<div class="chip-input-wrap" id="servicesChips" onclick="this.querySelector('input').focus()">
|
|
643
|
+
<input type="text" class="chip-input" placeholder="Type a service + Enter" onkeydown="chipKey(event,'services')">
|
|
644
|
+
</div>
|
|
645
|
+
<button class="ai-btn" onclick="aiGenerate('services',event)"><svg viewBox="0 0 24 24"><path d="M12 3v4m0 14v-4m9-5h-4M3 12h4m12.36-5.36l-2.83 2.83M6.47 17.53l-2.83 2.83m14.72 0l-2.83-2.83M6.47 6.47L3.64 3.64"/></svg>Generate</button>
|
|
646
|
+
</div>
|
|
647
|
+
</div>
|
|
648
|
+
|
|
649
|
+
<div class="form-group" style="margin-top:10px">
|
|
650
|
+
<label>Target Areas / Cities</label>
|
|
651
|
+
<div class="field-with-ai">
|
|
652
|
+
<div class="chip-input-wrap" id="targetMarketsChips" onclick="this.querySelector('input').focus()">
|
|
653
|
+
<input type="text" class="chip-input" placeholder="City, State + Enter" onkeydown="chipKey(event,'targetMarkets')">
|
|
654
|
+
</div>
|
|
655
|
+
<button class="ai-btn" onclick="aiGenerate('targetMarkets',event)"><svg viewBox="0 0 24 24"><path d="M12 3v4m0 14v-4m9-5h-4M3 12h4m12.36-5.36l-2.83 2.83M6.47 17.53l-2.83 2.83m14.72 0l-2.83-2.83M6.47 6.47L3.64 3.64"/></svg>Generate</button>
|
|
656
|
+
</div>
|
|
657
|
+
</div>
|
|
658
|
+
|
|
659
|
+
<div class="form-group" style="margin-top:10px">
|
|
660
|
+
<label>Topics to Cover</label>
|
|
661
|
+
<div class="field-with-ai">
|
|
662
|
+
<div class="chip-input-wrap" id="topicsChips" onclick="this.querySelector('input').focus()">
|
|
663
|
+
<input type="text" class="chip-input" placeholder="Blog/content topic + Enter" onkeydown="chipKey(event,'topics')">
|
|
664
|
+
</div>
|
|
665
|
+
<button class="ai-btn" onclick="aiGenerate('topics',event)"><svg viewBox="0 0 24 24"><path d="M12 3v4m0 14v-4m9-5h-4M3 12h4m12.36-5.36l-2.83 2.83M6.47 17.53l-2.83 2.83m14.72 0l-2.83-2.83M6.47 6.47L3.64 3.64"/></svg>Generate</button>
|
|
666
|
+
</div>
|
|
667
|
+
</div>
|
|
668
|
+
|
|
669
|
+
<label style="margin:12px 0 6px;display:block">Geographic Scope</label>
|
|
670
|
+
<div class="pill-row" id="geoPills">
|
|
671
|
+
<span class="pill" data-val="local" onclick="selectPill(this,'geo')">Local</span>
|
|
672
|
+
<span class="pill" data-val="regional" onclick="selectPill(this,'geo')">Regional</span>
|
|
673
|
+
<span class="pill" data-val="statewide" onclick="selectPill(this,'geo')">Statewide</span>
|
|
674
|
+
<span class="pill" data-val="nationwide" onclick="selectPill(this,'geo')">Nationwide</span>
|
|
675
|
+
<span class="pill" data-val="worldwide" onclick="selectPill(this,'geo')">Worldwide</span>
|
|
676
|
+
</div>
|
|
677
|
+
|
|
678
|
+
<div class="form-group" style="margin-top:12px">
|
|
679
|
+
<label>Ideal Customer</label>
|
|
680
|
+
<div class="field-with-ai">
|
|
681
|
+
<textarea id="f-idealCustomer" data-field="targetAudience.idealCustomer" rows="3" placeholder="Who is the perfect customer? Age, profession, problem they're solving..."></textarea>
|
|
682
|
+
<button class="ai-btn" onclick="aiGenerate('idealCustomer',event)"><svg viewBox="0 0 24 24"><path d="M12 3v4m0 14v-4m9-5h-4M3 12h4m12.36-5.36l-2.83 2.83M6.47 17.53l-2.83 2.83m14.72 0l-2.83-2.83M6.47 6.47L3.64 3.64"/></svg>Generate</button>
|
|
683
|
+
</div>
|
|
684
|
+
</div>
|
|
685
|
+
|
|
686
|
+
<div class="form-group" style="margin-top:10px">
|
|
687
|
+
<label>Primary Call-to-Action</label>
|
|
688
|
+
<div class="field-with-ai">
|
|
689
|
+
<input type="text" id="f-primaryCTA" data-field="siteConfig.primaryCTA" placeholder="e.g. Get a Free Quote">
|
|
690
|
+
<button class="ai-btn" onclick="aiSuggest('primaryCTA',event)"><svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>Suggest</button>
|
|
691
|
+
</div>
|
|
692
|
+
</div>
|
|
693
|
+
</div>
|
|
694
|
+
</div>
|
|
695
|
+
</div>
|
|
696
|
+
|
|
697
|
+
<!-- ═══════════ SECTION 6: SEO & COMPETITION ═══════════ -->
|
|
698
|
+
<div class="section">
|
|
699
|
+
<div class="section-card">
|
|
700
|
+
<div class="section-header" onclick="toggleSection(this)">
|
|
701
|
+
<h2>SEO & Competition</h2>
|
|
702
|
+
<svg class="chevron" viewBox="0 0 24 24"><polyline points="6 9 12 15 18 9"/></svg>
|
|
703
|
+
</div>
|
|
704
|
+
<div class="section-body">
|
|
705
|
+
<div class="form-group">
|
|
706
|
+
<label>Primary Keywords</label>
|
|
707
|
+
<div class="field-with-ai">
|
|
708
|
+
<div class="chip-input-wrap" id="keywordsChips" onclick="this.querySelector('input').focus()">
|
|
709
|
+
<input type="text" class="chip-input" placeholder="Keyword + Enter" onkeydown="chipKey(event,'keywords')">
|
|
710
|
+
</div>
|
|
711
|
+
<button class="ai-btn" onclick="aiGenerate('keywords',event)"><svg viewBox="0 0 24 24"><path d="M12 3v4m0 14v-4m9-5h-4M3 12h4m12.36-5.36l-2.83 2.83M6.47 17.53l-2.83 2.83m14.72 0l-2.83-2.83M6.47 6.47L3.64 3.64"/></svg>Generate</button>
|
|
712
|
+
</div>
|
|
713
|
+
</div>
|
|
714
|
+
|
|
715
|
+
<div class="form-group" style="margin-top:10px">
|
|
716
|
+
<label>Competitor URLs</label>
|
|
717
|
+
<div class="list-items" id="competitorsList"></div>
|
|
718
|
+
<button class="add-item-btn" onclick="addListItem('competitors')">+ Add competitor URL</button>
|
|
719
|
+
</div>
|
|
720
|
+
|
|
721
|
+
<div class="form-group" style="margin-top:10px">
|
|
722
|
+
<label>Reference Sites (sites you like)</label>
|
|
723
|
+
<div class="list-items" id="referencesList"></div>
|
|
724
|
+
<button class="add-item-btn" onclick="addListItem('references')">+ Add reference site</button>
|
|
725
|
+
</div>
|
|
726
|
+
</div>
|
|
727
|
+
</div>
|
|
728
|
+
</div>
|
|
729
|
+
|
|
730
|
+
<!-- ═══════════ SECTION 7: PAGES & FEATURES ═══════════ -->
|
|
731
|
+
<div class="section">
|
|
732
|
+
<div class="section-card">
|
|
733
|
+
<div class="section-header" onclick="toggleSection(this)">
|
|
734
|
+
<h2>Pages & Features</h2>
|
|
735
|
+
<svg class="chevron" viewBox="0 0 24 24"><polyline points="6 9 12 15 18 9"/></svg>
|
|
736
|
+
</div>
|
|
737
|
+
<div class="section-body">
|
|
738
|
+
<label style="margin-bottom:6px;display:block">Pages</label>
|
|
739
|
+
<div class="pill-row" id="pagesRow"></div>
|
|
740
|
+
|
|
741
|
+
<label style="margin:14px 0 6px;display:block">Special Features</label>
|
|
742
|
+
<div class="pill-row" id="featuresRow"></div>
|
|
743
|
+
</div>
|
|
744
|
+
</div>
|
|
745
|
+
</div>
|
|
746
|
+
|
|
747
|
+
<!-- ═══════════ SECTION 8: ADDITIONAL NOTES ═══════════ -->
|
|
748
|
+
<div class="section">
|
|
749
|
+
<div class="section-card">
|
|
750
|
+
<div class="section-header open" onclick="toggleSection(this)">
|
|
751
|
+
<h2>Additional Notes</h2>
|
|
752
|
+
<svg class="chevron" viewBox="0 0 24 24"><polyline points="6 9 12 15 18 9"/></svg>
|
|
753
|
+
</div>
|
|
754
|
+
<div class="section-body open">
|
|
755
|
+
<div class="field-with-ai">
|
|
756
|
+
<textarea id="f-notes" data-field="notes" rows="6" placeholder="Anything else the AI should know? Special requirements, brand guidelines, content you already have..."></textarea>
|
|
757
|
+
<button class="ai-btn" onclick="aiEnhance('notes',event)"><svg viewBox="0 0 24 24"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg>Enhance</button>
|
|
758
|
+
</div>
|
|
759
|
+
</div>
|
|
760
|
+
</div>
|
|
761
|
+
</div>
|
|
762
|
+
|
|
763
|
+
</div><!-- /container -->
|
|
764
|
+
|
|
765
|
+
<!-- ═══════════ SECTION 9: ACTION BAR ═══════════ -->
|
|
766
|
+
<div class="action-bar">
|
|
767
|
+
<div class="save-indicator">
|
|
768
|
+
<span class="save-dot" id="saveDot"></span>
|
|
769
|
+
<span id="saveLabel">All changes saved</span>
|
|
770
|
+
</div>
|
|
771
|
+
<div class="action-buttons">
|
|
772
|
+
<button class="btn" onclick="saveNow()">Save Draft</button>
|
|
773
|
+
<button class="btn primary" id="startResearchBtn" disabled onclick="startResearch()">Start Research</button>
|
|
774
|
+
</div>
|
|
775
|
+
</div>
|
|
776
|
+
|
|
777
|
+
<!-- Toast -->
|
|
778
|
+
<div class="toast" id="toast"></div>
|
|
779
|
+
|
|
780
|
+
<script>
|
|
781
|
+
const $=id=>document.getElementById(id);
|
|
782
|
+
const esc=s=>{const d=document.createElement('div');d.textContent=s;return d.innerHTML};
|
|
783
|
+
|
|
784
|
+
// ── Theme Definitions ──
|
|
785
|
+
// Real palettes sourced from brand design systems + Tailwind/Material defaults
|
|
786
|
+
// [primary, secondary, accent, background]
|
|
787
|
+
const THEMES = [
|
|
788
|
+
{id:'stripe',name:'Stripe',sub:'SaaS, fintech, dev tools',colors:['#635BFF','#0A2540','#00D4AA','#F6F9FC']},
|
|
789
|
+
{id:'tailwind-indigo',name:'Tailwind Indigo',sub:'Dashboards, B2B apps',colors:['#4F46E5','#0F172A','#F59E0B','#F8FAFC']},
|
|
790
|
+
{id:'material-blue',name:'Material Blue',sub:'Enterprise, corporate',colors:['#1976D2','#424242','#FF9800','#FAFAFA']},
|
|
791
|
+
{id:'airbnb',name:'Airbnb Coral',sub:'Marketplaces, lifestyle',colors:['#FF5A5F','#484848','#00A699','#FFFFFF']},
|
|
792
|
+
{id:'shopify',name:'Shopify Green',sub:'E-commerce, retail',colors:['#95BF47','#5E8E3E','#F49342','#F4F6F8']},
|
|
793
|
+
{id:'supabase-dark',name:'Supabase Dark',sub:'Dev tools, dark theme',colors:['#3ECF8E','#1C1C1C','#6EE7B7','#111827']},
|
|
794
|
+
{id:'hubspot',name:'HubSpot Warm',sub:'Marketing, CRM, lead gen',colors:['#FF7A59','#33475B','#00BDA5','#F5F8FA']},
|
|
795
|
+
{id:'tailwind-emerald',name:'Tailwind Emerald',sub:'Health, finance, eco',colors:['#10B981','#1E293B','#F43F5E','#F8FAFC']},
|
|
796
|
+
{id:'calendly',name:'Calendly Blue',sub:'Scheduling, professional',colors:['#006BFF','#004796','#0AE8F0','#FFFFFF']},
|
|
797
|
+
{id:'navy-gold',name:'Navy & Gold',sub:'Law, consulting, luxury',colors:['#0A1628','#1E3A5F','#D4A843','#F8F6F2']},
|
|
798
|
+
{id:'intercom',name:'Intercom Blue',sub:'Support, chat, comms',colors:['#0057FF','#1F2D3D','#47C7F0','#FFFFFF']},
|
|
799
|
+
{id:'slack',name:'Slack Aubergine',sub:'Collaboration, community',colors:['#4A154B','#1A1D21','#E01E5A','#FFFFFF']},
|
|
800
|
+
];
|
|
801
|
+
|
|
802
|
+
// ── Data Model ──
|
|
803
|
+
let formData = {
|
|
804
|
+
basics:{businessName:'',domain:'',industry:'',description:'',phone:'',email:'',address:'',about:'',ownerName:'',yearsInBusiness:'',license:'',gbpUrl:''},
|
|
805
|
+
branding:{theme:'',customColors:{primary:'',secondary:'',accent:'',background:''},tone:'professional',logoUrl:'',faviconUrl:'',
|
|
806
|
+
style:{
|
|
807
|
+
fontHeading:'system-ui, sans-serif',fontBody:'system-ui, sans-serif',fontWeight:'bold',fontStyle:'none',
|
|
808
|
+
navRadius:'12',navShadow:'sm',navGlow:'none',navOpacity:100,
|
|
809
|
+
btnFill:'filled',btnRadius:'6',btnShadow:'none',btnGlow:'none',btnBorder:0,
|
|
810
|
+
cardRadius:'12',cardShadow:'sm',cardGlow:'none',cardInnerGlow:'none',cardBorder:1,cardOpacity:100,cardGlass:'none',
|
|
811
|
+
pageWidth:'standard',spacing:'normal',animations:'subtle'
|
|
812
|
+
}},
|
|
813
|
+
siteConfig:{siteType:'',primaryCTA:'',pages:['home','about','services','contact'],specialFeatures:[]},
|
|
814
|
+
targetAudience:{idealCustomer:'',geographicScope:'',targetMarkets:[],services:[],topics:[]},
|
|
815
|
+
seo:{primaryKeywords:[],competitors:[],referenceSites:[]},
|
|
816
|
+
images:{logo:{url:'',notes:''},hero:{url:'',notes:''},team:{url:'',notes:''},gallery:[]},
|
|
817
|
+
notes:''
|
|
818
|
+
};
|
|
819
|
+
|
|
820
|
+
let saveTimer=null, isSaving=false;
|
|
821
|
+
|
|
822
|
+
// ── Init ──
|
|
823
|
+
function init(){
|
|
824
|
+
renderThemeDropdown();
|
|
825
|
+
renderPages();
|
|
826
|
+
renderFeatures();
|
|
827
|
+
loadData();
|
|
828
|
+
setupDragDrop();
|
|
829
|
+
bindInputs();
|
|
830
|
+
// Defer preview render until after loadData populates formData
|
|
831
|
+
setTimeout(()=>{updateSwatchBar();updatePreview()},300);
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
// ── Section toggle ──
|
|
835
|
+
function toggleSection(hdr){
|
|
836
|
+
hdr.classList.toggle('open');
|
|
837
|
+
const body=hdr.nextElementSibling;
|
|
838
|
+
body.classList.toggle('open');
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
// ── Theme rendering ──
|
|
842
|
+
// ── Theme dropdown + live preview ──
|
|
843
|
+
function renderThemeDropdown(){
|
|
844
|
+
const sel=$('themeSelect');
|
|
845
|
+
sel.innerHTML='<option value="">-- Pick a theme --</option>'+
|
|
846
|
+
THEMES.map(t=>`<option value="${t.id}"${formData.branding.theme===t.id?' selected':''}>${t.name} — ${t.sub}</option>`).join('');
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
function selectTheme(id){
|
|
850
|
+
formData.branding.theme=id;
|
|
851
|
+
const t=THEMES.find(x=>x.id===id);
|
|
852
|
+
if(t){
|
|
853
|
+
$('f-colorPrimary').value=t.colors[0];
|
|
854
|
+
$('f-colorSecondary').value=t.colors[1];
|
|
855
|
+
$('f-colorAccent').value=t.colors[2];
|
|
856
|
+
$('f-colorBg').value=t.colors[3];
|
|
857
|
+
formData.branding.customColors={primary:'',secondary:'',accent:'',background:''};
|
|
858
|
+
}
|
|
859
|
+
updateSwatchBar();
|
|
860
|
+
updatePreview();
|
|
861
|
+
scheduleSave();
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
function getColors(){
|
|
865
|
+
const c=formData.branding.customColors;
|
|
866
|
+
const t=THEMES.find(x=>x.id===formData.branding.theme);
|
|
867
|
+
return{
|
|
868
|
+
primary:c.primary||t?.colors[0]||'#4F46E5',
|
|
869
|
+
secondary:c.secondary||t?.colors[1]||'#0F172A',
|
|
870
|
+
accent:c.accent||t?.colors[2]||'#F59E0B',
|
|
871
|
+
bg:c.background||t?.colors[3]||'#F8FAFC'
|
|
872
|
+
};
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
function updateSwatchBar(){
|
|
876
|
+
const cl=getColors();
|
|
877
|
+
const bar=$('swatchBar');
|
|
878
|
+
bar.children[0].style.background=cl.primary;
|
|
879
|
+
bar.children[1].style.background=cl.secondary;
|
|
880
|
+
bar.children[2].style.background=cl.accent;
|
|
881
|
+
bar.children[3].style.background=cl.bg;
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
// ── Style controls ──
|
|
885
|
+
function setStyle(prop,val){
|
|
886
|
+
const intProps=['navOpacity','cardOpacity','cardBorder','btnBorder'];
|
|
887
|
+
formData.branding.style[prop]=intProps.includes(prop)?parseInt(val):val;
|
|
888
|
+
// Update range labels
|
|
889
|
+
if(prop==='navOpacity')$('navOpacityVal').textContent=val+'%';
|
|
890
|
+
if(prop==='btnBorder')$('btnBorderVal').textContent=val+'px';
|
|
891
|
+
if(prop==='cardBorder')$('cardBorderVal').textContent=val+'px';
|
|
892
|
+
if(prop==='cardOpacity')$('cardOpacityVal').textContent=val+'%';
|
|
893
|
+
updatePreview();
|
|
894
|
+
scheduleSave();
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
// ── Helpers ──
|
|
898
|
+
function textOn(hex){
|
|
899
|
+
const c=(hex||'#ffffff').replace('#','');
|
|
900
|
+
const lum=(parseInt(c.substr(0,2),16)*299+parseInt(c.substr(2,2),16)*587+parseInt(c.substr(4,2),16)*114)/1000;
|
|
901
|
+
return lum>140?'#1a1a2e':'#ffffff';
|
|
902
|
+
}
|
|
903
|
+
function alpha(hex,a){const c=(hex||'#000000').replace('#','');return`rgba(${parseInt(c.substr(0,2),16)},${parseInt(c.substr(2,2),16)},${parseInt(c.substr(4,2),16)},${a})`}
|
|
904
|
+
function shadowCSS(level,color){
|
|
905
|
+
const c=color||'0,0,0';
|
|
906
|
+
const m={none:'none',sm:`0 1px 4px rgba(${c},.1)`,md:`0 4px 12px rgba(${c},.12)`,lg:`0 8px 24px rgba(${c},.18)`,xl:`0 16px 48px rgba(${c},.25)`};
|
|
907
|
+
return m[level]||'none';
|
|
908
|
+
}
|
|
909
|
+
function glowCSS(level,hex){
|
|
910
|
+
if(level==='none'||!hex)return'';
|
|
911
|
+
const c=hex.replace('#','');
|
|
912
|
+
const r=parseInt(c.substr(0,2),16),g=parseInt(c.substr(2,2),16),b=parseInt(c.substr(4,2),16);
|
|
913
|
+
if(level==='soft')return`0 0 15px rgba(${r},${g},${b},.25)`;
|
|
914
|
+
return`0 0 25px rgba(${r},${g},${b},.5),0 0 8px rgba(${r},${g},${b},.3)`;
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
// ── Live preview ──
|
|
918
|
+
function updatePreview(){
|
|
919
|
+
const cl=getColors();
|
|
920
|
+
const s=formData.branding.style||{};
|
|
921
|
+
const fH=s.fontHeading||'system-ui, sans-serif';
|
|
922
|
+
const fB=s.fontBody||'system-ui, sans-serif';
|
|
923
|
+
const hW=s.fontWeight==='extra'?900:s.fontWeight==='light'?300:s.fontWeight==='bold'?700:400;
|
|
924
|
+
|
|
925
|
+
const textPri=textOn(cl.bg);
|
|
926
|
+
const textSec=alpha(textPri,.6);
|
|
927
|
+
|
|
928
|
+
// Font style (heading text effects)
|
|
929
|
+
let headingExtra='';
|
|
930
|
+
if(s.fontStyle==='shadow')headingExtra='text-shadow:2px 2px 4px rgba(0,0,0,.3);';
|
|
931
|
+
else if(s.fontStyle==='outline')headingExtra=`-webkit-text-stroke:1px ${textPri};color:transparent;`;
|
|
932
|
+
else if(s.fontStyle==='glow')headingExtra=`text-shadow:0 0 10px ${cl.primary},0 0 20px ${alpha(cl.primary,.4)};`;
|
|
933
|
+
|
|
934
|
+
// ── Nav ──
|
|
935
|
+
const navR=s.navRadius||'12';
|
|
936
|
+
let navSh=[shadowCSS(s.navShadow||'sm'),glowCSS(s.navGlow||'none',cl.secondary)].filter(x=>x&&x!=='none').join(',')||'none';
|
|
937
|
+
const navAlpha=(s.navOpacity??100)/100;
|
|
938
|
+
|
|
939
|
+
const frame=$('previewFrame');
|
|
940
|
+
frame.style.cssText=`border:1px solid ${alpha(cl.secondary,.15)};border-radius:${navR}px;overflow:hidden;background:${cl.bg}`;
|
|
941
|
+
|
|
942
|
+
const nav=$('pvNav');
|
|
943
|
+
nav.style.cssText=`display:flex;align-items:center;justify-content:space-between;padding:10px 14px;background:${alpha(cl.secondary,navAlpha)};font-family:${fB};box-shadow:${navSh}`;
|
|
944
|
+
$('pvLogo').style.cssText=`font-family:${fH};color:${textOn(cl.secondary)};font-size:.8rem;font-weight:${hW}`;
|
|
945
|
+
$('pvLogo').textContent=formData.basics.businessName||'Business Name';
|
|
946
|
+
nav.querySelectorAll('.pv-links span').forEach(sp=>sp.style.color=alpha(textOn(cl.secondary),.7));
|
|
947
|
+
|
|
948
|
+
// ── Buttons ──
|
|
949
|
+
function btnCSS(bgColor){
|
|
950
|
+
const br=(s.btnRadius||'6')+'px';
|
|
951
|
+
const fill=s.btnFill||'filled';
|
|
952
|
+
const bw=(s.btnBorder||0)+'px';
|
|
953
|
+
let sh=[shadowCSS(s.btnShadow||'none'),glowCSS(s.btnGlow||'none',bgColor)].filter(x=>x&&x!=='none').join(',')||'none';
|
|
954
|
+
let css=`border-radius:${br};padding:5px 12px;box-shadow:${sh};cursor:default;`;
|
|
955
|
+
if(fill==='outline')css+=`background:transparent;color:${bgColor};border:${Math.max(parseInt(bw),2)}px solid ${bgColor};`;
|
|
956
|
+
else if(fill==='ghost')css+=`background:${alpha(bgColor,.1)};color:${bgColor};border:none;`;
|
|
957
|
+
else if(fill==='gradient')css+=`background:linear-gradient(135deg,${bgColor},${alpha(bgColor,.7)});color:${textOn(bgColor)};border:${bw} solid transparent;`;
|
|
958
|
+
else css+=`background:${bgColor};color:${textOn(bgColor)};border:${bw} solid ${alpha(bgColor,.3)};`;
|
|
959
|
+
return css;
|
|
960
|
+
}
|
|
961
|
+
$('pvCta').style.cssText=btnCSS(cl.accent)+';font-size:.62rem;font-weight:600';
|
|
962
|
+
$('pvHeroCta').style.cssText=btnCSS(cl.primary)+';font-size:.68rem;font-weight:600;display:inline-block';
|
|
963
|
+
$('pvHeroCta').textContent=formData.siteConfig.primaryCTA||'Call to Action';
|
|
964
|
+
|
|
965
|
+
// ── Hero ──
|
|
966
|
+
const hero=$('pvHero');
|
|
967
|
+
hero.style.cssText=`padding:22px 14px;text-align:center;background:${cl.bg};font-family:${fB}`;
|
|
968
|
+
$('pvHeadline').style.cssText=`font-family:${fH};color:${textPri};font-size:.95rem;font-weight:${hW};letter-spacing:-.03em;margin-bottom:3px;${headingExtra}`;
|
|
969
|
+
$('pvSubtext').style.cssText=`font-size:.65rem;color:${textSec};margin:0 auto 10px`;
|
|
970
|
+
|
|
971
|
+
// ── Cards ──
|
|
972
|
+
const cR=(s.cardRadius||'12')+'px';
|
|
973
|
+
const cBw=(s.cardBorder??1)+'px';
|
|
974
|
+
const cAlpha=(s.cardOpacity??100)/100;
|
|
975
|
+
let cSh=[shadowCSS(s.cardShadow||'sm'),glowCSS(s.cardGlow||'none',cl.primary)].filter(x=>x&&x!=='none').join(',')||'none';
|
|
976
|
+
let innerGlow='';
|
|
977
|
+
if(s.cardInnerGlow==='soft')innerGlow=`inset 0 0 12px ${alpha(cl.primary,.08)}`;
|
|
978
|
+
else if(s.cardInnerGlow==='bright')innerGlow=`inset 0 0 20px ${alpha(cl.primary,.18)}`;
|
|
979
|
+
if(innerGlow)cSh=cSh==='none'?innerGlow:cSh+','+innerGlow;
|
|
980
|
+
let glass='';
|
|
981
|
+
if(s.cardGlass==='light')glass='backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);';
|
|
982
|
+
else if(s.cardGlass==='heavy')glass='backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px);';
|
|
983
|
+
const cardBg=cAlpha<1?alpha(cl.bg,cAlpha):cl.bg;
|
|
984
|
+
|
|
985
|
+
const cards=$('pvCards');
|
|
986
|
+
cards.style.cssText=`display:grid;grid-template-columns:1fr 1fr;gap:8px;padding:0 14px 14px;background:${cl.bg}`;
|
|
987
|
+
const svcNames=(formData.targetAudience.services||[]).slice(0,2);
|
|
988
|
+
['pvCard1','pvCard2'].forEach((id,i)=>{
|
|
989
|
+
const card=$(id);
|
|
990
|
+
card.style.cssText=`border-radius:${cR};padding:10px;text-align:center;background:${cardBg};border:${cBw} solid ${alpha(cl.secondary,.12)};box-shadow:${cSh};${glass}`;
|
|
991
|
+
card.querySelector('.pv-card-title').style.cssText=`font-family:${fH};color:${textPri};font-size:.68rem;font-weight:${hW};margin-bottom:2px`;
|
|
992
|
+
card.querySelector('.pv-card-title').textContent=svcNames[i]||['Service One','Service Two'][i];
|
|
993
|
+
card.querySelector('.pv-card-text').style.cssText=`font-size:.55rem;color:${textSec};line-height:1.4`;
|
|
994
|
+
});
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
// ── Card selection (site type) ──
|
|
998
|
+
function selectCard(el,field){
|
|
999
|
+
el.parentElement.querySelectorAll('.sel-card').forEach(c=>c.classList.remove('active'));
|
|
1000
|
+
el.classList.add('active');
|
|
1001
|
+
if(field==='siteType') formData.siteConfig.siteType=el.dataset.val;
|
|
1002
|
+
scheduleSave();
|
|
1003
|
+
updateStatus();
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
// ── Pill selection ──
|
|
1007
|
+
function selectPill(el,group){
|
|
1008
|
+
el.parentElement.querySelectorAll('.pill').forEach(p=>p.classList.remove('active'));
|
|
1009
|
+
el.classList.add('active');
|
|
1010
|
+
if(group==='tone') formData.branding.tone=el.dataset.val;
|
|
1011
|
+
if(group==='geo') formData.targetAudience.geographicScope=el.dataset.val;
|
|
1012
|
+
scheduleSave();
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
// ── Pages & Features toggles ──
|
|
1016
|
+
const ALL_PAGES=['home','about','services','contact','blog','faq','portfolio','pricing','team','testimonials','service-areas'];
|
|
1017
|
+
const ALL_FEATURES=['booking-form','newsletter','e-commerce','gallery','map','live-chat','calculator','social-feed'];
|
|
1018
|
+
|
|
1019
|
+
function renderPages(){
|
|
1020
|
+
$('pagesRow').innerHTML=ALL_PAGES.map(p=>{
|
|
1021
|
+
const on=formData.siteConfig.pages.includes(p);
|
|
1022
|
+
return`<span class="toggle-chip${on?' on':''}" data-val="${p}" onclick="togglePage(this)">${p.replace(/-/g,' ')}</span>`;
|
|
1023
|
+
}).join('');
|
|
1024
|
+
}
|
|
1025
|
+
function togglePage(el){
|
|
1026
|
+
const v=el.dataset.val;
|
|
1027
|
+
el.classList.toggle('on');
|
|
1028
|
+
if(el.classList.contains('on')){if(!formData.siteConfig.pages.includes(v))formData.siteConfig.pages.push(v)}
|
|
1029
|
+
else{formData.siteConfig.pages=formData.siteConfig.pages.filter(x=>x!==v)}
|
|
1030
|
+
scheduleSave();
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
function renderFeatures(){
|
|
1034
|
+
$('featuresRow').innerHTML=ALL_FEATURES.map(f=>{
|
|
1035
|
+
const on=formData.siteConfig.specialFeatures.includes(f);
|
|
1036
|
+
return`<span class="toggle-chip${on?' on':''}" data-val="${f}" onclick="toggleFeature(this)">${f.replace(/-/g,' ')}</span>`;
|
|
1037
|
+
}).join('');
|
|
1038
|
+
}
|
|
1039
|
+
function toggleFeature(el){
|
|
1040
|
+
const v=el.dataset.val;
|
|
1041
|
+
el.classList.toggle('on');
|
|
1042
|
+
if(el.classList.contains('on')){if(!formData.siteConfig.specialFeatures.includes(v))formData.siteConfig.specialFeatures.push(v)}
|
|
1043
|
+
else{formData.siteConfig.specialFeatures=formData.siteConfig.specialFeatures.filter(x=>x!==v)}
|
|
1044
|
+
scheduleSave();
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
// ── Chip inputs ──
|
|
1048
|
+
function chipKey(e,field){
|
|
1049
|
+
if(e.key!=='Enter'||!e.target.value.trim())return;
|
|
1050
|
+
e.preventDefault();
|
|
1051
|
+
const val=e.target.value.trim();
|
|
1052
|
+
e.target.value='';
|
|
1053
|
+
let arr;
|
|
1054
|
+
if(field==='services')arr=formData.targetAudience.services;
|
|
1055
|
+
else if(field==='targetMarkets')arr=formData.targetAudience.targetMarkets;
|
|
1056
|
+
else if(field==='keywords')arr=formData.seo.primaryKeywords;
|
|
1057
|
+
else if(field==='topics')arr=formData.targetAudience.topics;
|
|
1058
|
+
else return;
|
|
1059
|
+
if(!arr.includes(val)){arr.push(val);renderChips(field);scheduleSave()}
|
|
1060
|
+
}
|
|
1061
|
+
function removeChip(field,idx){
|
|
1062
|
+
let arr;
|
|
1063
|
+
if(field==='services')arr=formData.targetAudience.services;
|
|
1064
|
+
else if(field==='targetMarkets')arr=formData.targetAudience.targetMarkets;
|
|
1065
|
+
else if(field==='keywords')arr=formData.seo.primaryKeywords;
|
|
1066
|
+
else if(field==='topics')arr=formData.targetAudience.topics;
|
|
1067
|
+
arr.splice(idx,1);renderChips(field);scheduleSave();
|
|
1068
|
+
}
|
|
1069
|
+
function renderChips(field){
|
|
1070
|
+
let arr,container;
|
|
1071
|
+
if(field==='services'){arr=formData.targetAudience.services;container=$('servicesChips')}
|
|
1072
|
+
else if(field==='targetMarkets'){arr=formData.targetAudience.targetMarkets;container=$('targetMarketsChips')}
|
|
1073
|
+
else if(field==='keywords'){arr=formData.seo.primaryKeywords;container=$('keywordsChips')}
|
|
1074
|
+
else if(field==='topics'){arr=formData.targetAudience.topics;container=$('topicsChips')}
|
|
1075
|
+
const input=container.querySelector('input');
|
|
1076
|
+
container.querySelectorAll('.chip').forEach(c=>c.remove());
|
|
1077
|
+
arr.forEach((v,i)=>{
|
|
1078
|
+
const c=document.createElement('span');c.className='chip';
|
|
1079
|
+
c.innerHTML=`${esc(v)} <span class="x" onclick="removeChip('${field}',${i})">×</span>`;
|
|
1080
|
+
container.insertBefore(c,input);
|
|
1081
|
+
});
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
// ── List inputs (competitors, references) ──
|
|
1085
|
+
function addListItem(type,url='',notes=''){
|
|
1086
|
+
const container=$(type+'List');
|
|
1087
|
+
const div=document.createElement('div');div.className='list-item';
|
|
1088
|
+
if(type==='references'){
|
|
1089
|
+
div.innerHTML=`<input type="url" placeholder="https://example.com" value="${esc(url)}" oninput="updateList('${type}')">
|
|
1090
|
+
<input type="text" placeholder="What do you like?" style="max-width:180px" value="${esc(notes)}" oninput="updateList('${type}')">
|
|
1091
|
+
<button class="rm" onclick="this.parentElement.remove();updateList('${type}')">×</button>`;
|
|
1092
|
+
}else{
|
|
1093
|
+
div.innerHTML=`<input type="url" placeholder="https://competitor.com" value="${esc(url)}" oninput="updateList('${type}')">
|
|
1094
|
+
<button class="rm" onclick="this.parentElement.remove();updateList('${type}')">×</button>`;
|
|
1095
|
+
}
|
|
1096
|
+
container.appendChild(div);
|
|
1097
|
+
if(!url)div.querySelector('input').focus();
|
|
1098
|
+
}
|
|
1099
|
+
function updateList(type){
|
|
1100
|
+
const items=$(type+'List').querySelectorAll('.list-item');
|
|
1101
|
+
if(type==='competitors'){
|
|
1102
|
+
formData.seo.competitors=Array.from(items).map(el=>el.querySelector('input').value.trim()).filter(Boolean);
|
|
1103
|
+
}else{
|
|
1104
|
+
formData.seo.referenceSites=Array.from(items).map(el=>{
|
|
1105
|
+
const inputs=el.querySelectorAll('input');
|
|
1106
|
+
return{url:inputs[0].value.trim(),notes:inputs[1]?inputs[1].value.trim():''};
|
|
1107
|
+
}).filter(r=>r.url);
|
|
1108
|
+
}
|
|
1109
|
+
scheduleSave();
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
// ── Color overrides ──
|
|
1113
|
+
function setCustomColor(which,val){
|
|
1114
|
+
formData.branding.customColors[which]=val;
|
|
1115
|
+
updateSwatchBar();updatePreview();scheduleSave();
|
|
1116
|
+
}
|
|
1117
|
+
function resetColors(){
|
|
1118
|
+
formData.branding.customColors={primary:'',secondary:'',accent:'',background:''};
|
|
1119
|
+
const t=THEMES.find(x=>x.id===formData.branding.theme);
|
|
1120
|
+
$('f-colorPrimary').value=t?.colors[0]||'#4F46E5';
|
|
1121
|
+
$('f-colorSecondary').value=t?.colors[1]||'#0F172A';
|
|
1122
|
+
$('f-colorAccent').value=t?.colors[2]||'#F59E0B';
|
|
1123
|
+
$('f-colorBg').value=t?.colors[3]||'#F8FAFC';
|
|
1124
|
+
updateSwatchBar();updatePreview();scheduleSave();
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
// ── File uploads ──
|
|
1128
|
+
async function handleUpload(type,file){
|
|
1129
|
+
if(!file)return;
|
|
1130
|
+
const fd=new FormData();fd.append('file',file);
|
|
1131
|
+
try{
|
|
1132
|
+
const resp=await authUpload('/api/upload',fd);
|
|
1133
|
+
const r=await resp.json();
|
|
1134
|
+
if(!resp.ok||r.error){toast('Upload failed: '+(r.error||''));return}
|
|
1135
|
+
const url=r.url;
|
|
1136
|
+
if(type==='logo'){formData.images.logo.url=url;showUploadPreview('logoZone',url)}
|
|
1137
|
+
else if(type==='favicon'){formData.branding.faviconUrl=url;showUploadPreview('faviconZone',url)}
|
|
1138
|
+
else if(type==='hero'){formData.images.hero.url=url;showUploadPreview('heroZone',url)}
|
|
1139
|
+
else if(type==='team'){formData.images.team.url=url;showUploadPreview('teamZone',url)}
|
|
1140
|
+
scheduleSave();
|
|
1141
|
+
toast('Uploaded: '+file.name);
|
|
1142
|
+
}catch(e){toast('Upload error: '+e.message)}
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
function showUploadPreview(zoneId,url){
|
|
1146
|
+
const zone=$(zoneId);
|
|
1147
|
+
zone.classList.add('has-img');
|
|
1148
|
+
const fileInput=zone.querySelector('input[type="file"]');
|
|
1149
|
+
zone.innerHTML='';
|
|
1150
|
+
zone.appendChild(fileInput);
|
|
1151
|
+
const img=document.createElement('img');img.src=url;
|
|
1152
|
+
zone.appendChild(img);
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
async function handleBulkUpload(files){
|
|
1156
|
+
for(const file of files){
|
|
1157
|
+
const fd=new FormData();fd.append('file',file);
|
|
1158
|
+
try{
|
|
1159
|
+
const resp=await authUpload('/api/upload',fd);
|
|
1160
|
+
const r=await resp.json();
|
|
1161
|
+
if(resp.ok&&r.url){
|
|
1162
|
+
formData.images.gallery.push({url:r.url,name:file.name,notes:'',tags:[]});
|
|
1163
|
+
renderGallery();
|
|
1164
|
+
scheduleSave();
|
|
1165
|
+
}
|
|
1166
|
+
}catch(e){}
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
const IMG_TAGS=['hero','service','team','gallery','background'];
|
|
1171
|
+
function renderGallery(){
|
|
1172
|
+
const g=$('galleryGrid');
|
|
1173
|
+
g.innerHTML=formData.images.gallery.map((img,i)=>`
|
|
1174
|
+
<div class="upload-card">
|
|
1175
|
+
<div class="thumb"><img src="${img.url}" loading="lazy"></div>
|
|
1176
|
+
<div class="meta"><div class="fname">${esc(img.name||'image')}</div></div>
|
|
1177
|
+
<input type="text" placeholder="Notes..." value="${esc(img.notes||'')}" oninput="formData.images.gallery[${i}].notes=this.value;scheduleSave()">
|
|
1178
|
+
<div class="tag-row">${IMG_TAGS.map(t=>`<span class="tag${(img.tags||[]).includes(t)?' on':''}" onclick="toggleImgTag(${i},'${t}',this)">${t}</span>`).join('')}</div>
|
|
1179
|
+
</div>
|
|
1180
|
+
`).join('');
|
|
1181
|
+
}
|
|
1182
|
+
function toggleImgTag(idx,tag,el){
|
|
1183
|
+
const img=formData.images.gallery[idx];
|
|
1184
|
+
if(!img.tags)img.tags=[];
|
|
1185
|
+
if(img.tags.includes(tag))img.tags=img.tags.filter(t=>t!==tag);
|
|
1186
|
+
else img.tags.push(tag);
|
|
1187
|
+
el.classList.toggle('on');
|
|
1188
|
+
scheduleSave();
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
// ── Drag & Drop for all zones ──
|
|
1192
|
+
function setupDragDrop(){
|
|
1193
|
+
const zones=[
|
|
1194
|
+
{id:'logoZone',type:'logo'},{id:'faviconZone',type:'favicon'},
|
|
1195
|
+
{id:'heroZone',type:'hero'},{id:'teamZone',type:'team'},{id:'bulkZone',type:'bulk'}
|
|
1196
|
+
];
|
|
1197
|
+
zones.forEach(({id,type})=>{
|
|
1198
|
+
const el=$(id);if(!el)return;
|
|
1199
|
+
el.addEventListener('dragover',e=>{e.preventDefault();el.classList.add('hover')});
|
|
1200
|
+
el.addEventListener('dragleave',()=>el.classList.remove('hover'));
|
|
1201
|
+
el.addEventListener('drop',e=>{
|
|
1202
|
+
e.preventDefault();el.classList.remove('hover');
|
|
1203
|
+
if(!e.dataTransfer.files.length)return;
|
|
1204
|
+
if(type==='bulk')handleBulkUpload(e.dataTransfer.files);
|
|
1205
|
+
else handleUpload(type,e.dataTransfer.files[0]);
|
|
1206
|
+
});
|
|
1207
|
+
});
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
// ── Input binding ──
|
|
1211
|
+
function bindInputs(){
|
|
1212
|
+
document.querySelectorAll('[data-field]').forEach(el=>{
|
|
1213
|
+
el.addEventListener('input',()=>{
|
|
1214
|
+
setNestedVal(formData,el.dataset.field,el.value);
|
|
1215
|
+
scheduleSave();
|
|
1216
|
+
updateStatus();
|
|
1217
|
+
});
|
|
1218
|
+
});
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
function setNestedVal(obj,path,val){
|
|
1222
|
+
const parts=path.split('.');
|
|
1223
|
+
let cur=obj;
|
|
1224
|
+
for(let i=0;i<parts.length-1;i++){cur=cur[parts[i]]=cur[parts[i]]||{}}
|
|
1225
|
+
cur[parts[parts.length-1]]=val;
|
|
1226
|
+
}
|
|
1227
|
+
function getNestedVal(obj,path){
|
|
1228
|
+
return path.split('.').reduce((o,k)=>o&&o[k],obj);
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
// ── Description char count ──
|
|
1232
|
+
function updDescCount(){
|
|
1233
|
+
const v=$('f-description').value;
|
|
1234
|
+
$('descCount').textContent=v.length+'/160';
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
// ── Status badge ──
|
|
1238
|
+
function updateStatus(){
|
|
1239
|
+
const b=formData.basics;
|
|
1240
|
+
const ready=b.businessName.trim()&&b.industry.trim()&&b.description.trim()&&formData.siteConfig.siteType;
|
|
1241
|
+
const badge=$('statusBadge');
|
|
1242
|
+
const btn=$('startResearchBtn');
|
|
1243
|
+
if(ready){
|
|
1244
|
+
badge.className='status-badge ready';badge.textContent='Ready';
|
|
1245
|
+
btn.disabled=false;
|
|
1246
|
+
}else{
|
|
1247
|
+
badge.className='status-badge draft';badge.textContent='Draft';
|
|
1248
|
+
btn.disabled=true;
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
// ── Auto-save ──
|
|
1253
|
+
function scheduleSave(){
|
|
1254
|
+
clearTimeout(saveTimer);
|
|
1255
|
+
$('saveDot').className='save-dot saving';
|
|
1256
|
+
$('saveLabel').textContent='Saving...';
|
|
1257
|
+
saveTimer=setTimeout(()=>doSave(),2000);
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
async function doSave(){
|
|
1261
|
+
if(isSaving)return;
|
|
1262
|
+
isSaving=true;
|
|
1263
|
+
try{
|
|
1264
|
+
const resp=await authPost('/api/canvas/data/website-setup.json',formData);
|
|
1265
|
+
if(resp.ok){
|
|
1266
|
+
$('saveDot').className='save-dot saved';
|
|
1267
|
+
$('saveLabel').textContent='All changes saved';
|
|
1268
|
+
setTimeout(()=>{if($('saveDot').classList.contains('saved'))$('saveDot').className='save-dot'},3000);
|
|
1269
|
+
}else throw new Error('save failed');
|
|
1270
|
+
}catch(e){
|
|
1271
|
+
$('saveDot').className='save-dot error';
|
|
1272
|
+
$('saveLabel').textContent='Save failed';
|
|
1273
|
+
}
|
|
1274
|
+
isSaving=false;
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
function saveNow(){clearTimeout(saveTimer);doSave()}
|
|
1278
|
+
|
|
1279
|
+
// ── Load data ──
|
|
1280
|
+
async function loadData(){
|
|
1281
|
+
try{
|
|
1282
|
+
const resp=await authGet('/api/canvas/data/website-setup.json');
|
|
1283
|
+
if(!resp.ok)return;
|
|
1284
|
+
const data=await resp.json();
|
|
1285
|
+
if(!data||!data.basics)return;
|
|
1286
|
+
formData=mergeDeep(formData,data);
|
|
1287
|
+
populateUI();
|
|
1288
|
+
}catch(e){}
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
function mergeDeep(target,source){
|
|
1292
|
+
const out={...target};
|
|
1293
|
+
for(const key in source){
|
|
1294
|
+
if(source[key]&&typeof source[key]==='object'&&!Array.isArray(source[key])){
|
|
1295
|
+
out[key]=mergeDeep(target[key]||{},source[key]);
|
|
1296
|
+
}else{out[key]=source[key]}
|
|
1297
|
+
}
|
|
1298
|
+
return out;
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
function populateUI(){
|
|
1302
|
+
// Text fields
|
|
1303
|
+
document.querySelectorAll('[data-field]').forEach(el=>{
|
|
1304
|
+
const v=getNestedVal(formData,el.dataset.field);
|
|
1305
|
+
if(v!==undefined&&v!==null)el.value=v;
|
|
1306
|
+
});
|
|
1307
|
+
updDescCount();
|
|
1308
|
+
|
|
1309
|
+
// Site type
|
|
1310
|
+
if(formData.siteConfig.siteType){
|
|
1311
|
+
document.querySelectorAll('#siteTypeCards .sel-card').forEach(c=>{
|
|
1312
|
+
c.classList.toggle('active',c.dataset.val===formData.siteConfig.siteType);
|
|
1313
|
+
});
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
// Theme dropdown
|
|
1317
|
+
renderThemeDropdown();
|
|
1318
|
+
if(formData.branding.theme)$('themeSelect').value=formData.branding.theme;
|
|
1319
|
+
|
|
1320
|
+
// Style controls — restore all values
|
|
1321
|
+
const st=formData.branding.style||{};
|
|
1322
|
+
const _sv=(id,v)=>{const el=$(id);if(el&&v!==undefined&&v!==null)el.value=v};
|
|
1323
|
+
_sv('f-fontHeading',st.fontHeading);_sv('f-fontBody',st.fontBody);
|
|
1324
|
+
_sv('f-fontWeight',st.fontWeight);_sv('f-fontStyle',st.fontStyle);
|
|
1325
|
+
_sv('f-navRadius',st.navRadius);_sv('f-navShadow',st.navShadow);_sv('f-navGlow',st.navGlow);
|
|
1326
|
+
_sv('f-navOpacity',st.navOpacity);if(st.navOpacity!==undefined)$('navOpacityVal').textContent=st.navOpacity+'%';
|
|
1327
|
+
_sv('f-btnFill',st.btnFill);_sv('f-btnRadius',st.btnRadius);_sv('f-btnShadow',st.btnShadow);_sv('f-btnGlow',st.btnGlow);
|
|
1328
|
+
_sv('f-btnBorder',st.btnBorder);if(st.btnBorder!==undefined)$('btnBorderVal').textContent=st.btnBorder+'px';
|
|
1329
|
+
_sv('f-cardRadius',st.cardRadius);_sv('f-cardShadow',st.cardShadow);_sv('f-cardGlow',st.cardGlow);_sv('f-cardInnerGlow',st.cardInnerGlow);
|
|
1330
|
+
_sv('f-cardBorder',st.cardBorder);if(st.cardBorder!==undefined)$('cardBorderVal').textContent=st.cardBorder+'px';
|
|
1331
|
+
_sv('f-cardOpacity',st.cardOpacity);if(st.cardOpacity!==undefined)$('cardOpacityVal').textContent=st.cardOpacity+'%';
|
|
1332
|
+
_sv('f-cardGlass',st.cardGlass);
|
|
1333
|
+
_sv('f-pageWidth',st.pageWidth);_sv('f-spacing',st.spacing);_sv('f-animations',st.animations);
|
|
1334
|
+
|
|
1335
|
+
// Tone
|
|
1336
|
+
document.querySelectorAll('#tonePills .pill').forEach(p=>{
|
|
1337
|
+
p.classList.toggle('active',p.dataset.val===formData.branding.tone);
|
|
1338
|
+
});
|
|
1339
|
+
|
|
1340
|
+
// Geo
|
|
1341
|
+
if(formData.targetAudience.geographicScope){
|
|
1342
|
+
document.querySelectorAll('#geoPills .pill').forEach(p=>{
|
|
1343
|
+
p.classList.toggle('active',p.dataset.val===formData.targetAudience.geographicScope);
|
|
1344
|
+
});
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
// Custom colors
|
|
1348
|
+
if(formData.branding.customColors.primary)$('f-colorPrimary').value=formData.branding.customColors.primary;
|
|
1349
|
+
if(formData.branding.customColors.secondary)$('f-colorSecondary').value=formData.branding.customColors.secondary;
|
|
1350
|
+
if(formData.branding.customColors.accent)$('f-colorAccent').value=formData.branding.customColors.accent;
|
|
1351
|
+
if(formData.branding.customColors.background)$('f-colorBg').value=formData.branding.customColors.background;
|
|
1352
|
+
|
|
1353
|
+
// Chips
|
|
1354
|
+
renderChips('services');
|
|
1355
|
+
renderChips('targetMarkets');
|
|
1356
|
+
renderChips('keywords');
|
|
1357
|
+
renderChips('topics');
|
|
1358
|
+
|
|
1359
|
+
// Pages & Features
|
|
1360
|
+
renderPages();
|
|
1361
|
+
renderFeatures();
|
|
1362
|
+
|
|
1363
|
+
// Lists
|
|
1364
|
+
formData.seo.competitors.forEach(u=>addListItem('competitors',u));
|
|
1365
|
+
(formData.seo.referenceSites||[]).forEach(r=>addListItem('references',r.url||'',r.notes||''));
|
|
1366
|
+
|
|
1367
|
+
// Images
|
|
1368
|
+
if(formData.images.logo.url)showUploadPreview('logoZone',formData.images.logo.url);
|
|
1369
|
+
if(formData.branding.faviconUrl)showUploadPreview('faviconZone',formData.branding.faviconUrl);
|
|
1370
|
+
if(formData.images.hero.url)showUploadPreview('heroZone',formData.images.hero.url);
|
|
1371
|
+
if(formData.images.team.url)showUploadPreview('teamZone',formData.images.team.url);
|
|
1372
|
+
if(formData.images.gallery&&formData.images.gallery.length)renderGallery();
|
|
1373
|
+
|
|
1374
|
+
updateStatus();
|
|
1375
|
+
updateSwatchBar();
|
|
1376
|
+
updatePreview();
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
// ── AI Engine (calls /api/chat → Groq LLM → updates field directly) ──
|
|
1380
|
+
let _aiPending=new Set();
|
|
1381
|
+
|
|
1382
|
+
function buildContext(){
|
|
1383
|
+
const b=formData.basics,t=formData.targetAudience,s=formData.siteConfig;
|
|
1384
|
+
let ctx='';
|
|
1385
|
+
if(b.businessName)ctx+=`- Business: ${b.businessName}\n`;
|
|
1386
|
+
if(b.industry)ctx+=`- Industry: ${b.industry}\n`;
|
|
1387
|
+
if(b.description)ctx+=`- Description: ${b.description}\n`;
|
|
1388
|
+
if(s.siteType)ctx+=`- Site Type: ${s.siteType}\n`;
|
|
1389
|
+
if(t.services.length)ctx+=`- Services: ${t.services.join(', ')}\n`;
|
|
1390
|
+
if(t.targetMarkets.length)ctx+=`- Target Markets: ${t.targetMarkets.join(', ')}\n`;
|
|
1391
|
+
if(t.geographicScope)ctx+=`- Geographic Scope: ${t.geographicScope}\n`;
|
|
1392
|
+
if(t.idealCustomer)ctx+=`- Ideal Customer: ${t.idealCustomer}\n`;
|
|
1393
|
+
if(formData.seo.primaryKeywords.length)ctx+=`- Keywords: ${formData.seo.primaryKeywords.join(', ')}\n`;
|
|
1394
|
+
if(b.about)ctx+=`- About: ${b.about}\n`;
|
|
1395
|
+
if(b.ownerName)ctx+=`- Owner: ${b.ownerName}\n`;
|
|
1396
|
+
if(b.yearsInBusiness)ctx+=`- Years in business: ${b.yearsInBusiness}\n`;
|
|
1397
|
+
if(b.license)ctx+=`- License: ${b.license}\n`;
|
|
1398
|
+
if(b.phone)ctx+=`- Phone: ${b.phone}\n`;
|
|
1399
|
+
if(b.address)ctx+=`- Address: ${b.address}\n`;
|
|
1400
|
+
if(t.topics&&t.topics.length)ctx+=`- Topics: ${t.topics.join(', ')}\n`;
|
|
1401
|
+
return ctx||'(No business info filled in yet)\n';
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
const PROMPTS={
|
|
1405
|
+
businessName:{enhance:c=>`Based on this business info, suggest a more professional or memorable business name variation:\n${c}Current name: ${formData.basics.businessName}\nReply with ONLY the suggested name, nothing else. No quotes, no explanation.`},
|
|
1406
|
+
industry:{suggest:c=>`Based on this business info, suggest the most specific industry/niche classification:\n${c}Reply with ONLY the industry/niche (2-4 words), nothing else. No quotes, no explanation.`},
|
|
1407
|
+
description:{
|
|
1408
|
+
generate:c=>`Based on this business info, write a concise SEO-optimized one-line description:\n${c}Requirements: Include primary service keywords naturally. Under 160 chars.\nNO marketing fluff, NO storytelling — clear, keyword-rich, factual.\nReply with ONLY the description text, nothing else. No quotes.`,
|
|
1409
|
+
enhance:c=>`Improve this business description for SEO while keeping it under 160 chars:\n${c}Current: ${formData.basics.description}\nRequirements: Better keywords, clearer value prop, still factual.\nReply with ONLY the improved description, nothing else. No quotes.`
|
|
1410
|
+
},
|
|
1411
|
+
aboutBusiness:{generate:c=>`Based on this business info, write a 2-3 sentence About section for the website:\n${c}Cover: what they do, who they serve, what makes them different.\nWrite it in third person. Professional but approachable.\nReply with ONLY the about text, nothing else.`},
|
|
1412
|
+
services:{generate:c=>`Based on this business info, suggest 8-12 specific services this business likely offers:\n${c}Reply with ONLY a comma-separated list of services, nothing else. No numbering, no bullets.`},
|
|
1413
|
+
topics:{generate:c=>`Based on this business info, suggest 10-15 blog/content topics this website should cover for SEO:\n${c}Think about: common customer questions, how-to guides, industry tips, buying guides, comparison content, seasonal topics.\nReply with ONLY a comma-separated list of topics, nothing else. No numbering, no bullets.`},
|
|
1414
|
+
targetMarkets:{generate:c=>`Based on this business info, suggest 5-8 target geographic markets (City, State format):\n${c}Reply with ONLY a comma-separated list of markets, nothing else.`},
|
|
1415
|
+
idealCustomer:{generate:c=>`Based on this business info, describe the ideal customer in 2-3 sentences:\n${c}Include: demographics, pain points, what they search for online.\nReply with ONLY the customer description, nothing else.`},
|
|
1416
|
+
primaryCTA:{suggest:c=>`Based on this business info, suggest the best primary call-to-action button text:\n${c}Reply with ONLY the CTA text (3-5 words), nothing else.`},
|
|
1417
|
+
keywords:{generate:c=>`Based on this business info, suggest 10-15 primary SEO keywords a customer would search on Google:\n${c}Include: service keywords, location keywords, long-tail phrases.\nReply with ONLY a comma-separated list of keywords, nothing else. No numbering, no bullets.`},
|
|
1418
|
+
notes:{enhance:c=>`Polish these notes into clear structured requirements for a web developer building this website:\n${c}Notes: ${formData.notes}\nReply with ONLY the structured requirements, nothing else.`},
|
|
1419
|
+
heroNotes:{enhance:c=>`Improve this hero image usage description so a web designer knows exactly how to use it:\n${c}Current: ${formData.images.hero.notes}\nReply with ONLY the improved description, nothing else.`}
|
|
1420
|
+
};
|
|
1421
|
+
|
|
1422
|
+
// Field → element ID mapping for applying AI results
|
|
1423
|
+
const FIELD_ELEMENTS={
|
|
1424
|
+
businessName:'f-businessName', industry:'f-industry', description:'f-description',
|
|
1425
|
+
aboutBusiness:'f-aboutBusiness', idealCustomer:'f-idealCustomer', primaryCTA:'f-primaryCTA',
|
|
1426
|
+
notes:'f-notes', heroNotes:'f-heroNotes'
|
|
1427
|
+
};
|
|
1428
|
+
// Fields that produce comma-separated lists → chips
|
|
1429
|
+
const CHIP_FIELDS={services:'services',targetMarkets:'targetMarkets',keywords:'keywords',topics:'topics'};
|
|
1430
|
+
|
|
1431
|
+
async function callAI(prompt){
|
|
1432
|
+
const resp=await fetch('/api/chat',{
|
|
1433
|
+
method:'POST',headers:_authHeaders({'Content-Type':'application/json'}),
|
|
1434
|
+
credentials:'same-origin',
|
|
1435
|
+
body:JSON.stringify({message:prompt})
|
|
1436
|
+
});
|
|
1437
|
+
if(!resp.ok){const e=await resp.json().catch(()=>({}));throw new Error(e.error||'AI request failed')}
|
|
1438
|
+
const data=await resp.json();
|
|
1439
|
+
return(data.response||'').trim();
|
|
1440
|
+
}
|
|
1441
|
+
function getCookie(name){
|
|
1442
|
+
const m=document.cookie.match(new RegExp('(?:^|; )'+name+'=([^;]*)'));
|
|
1443
|
+
return m?decodeURIComponent(m[1]):'';
|
|
1444
|
+
}
|
|
1445
|
+
function _authHeaders(extra){
|
|
1446
|
+
const hdrs=Object.assign({},extra||{});
|
|
1447
|
+
const token=window._canvasAuthToken||getCookie('__session');
|
|
1448
|
+
if(token)hdrs['Authorization']='Bearer '+token;
|
|
1449
|
+
return hdrs;
|
|
1450
|
+
}
|
|
1451
|
+
function authGet(url){return fetch(url,{headers:_authHeaders(),credentials:'same-origin'})}
|
|
1452
|
+
function authPost(url,data){return fetch(url,{method:'POST',headers:_authHeaders({'Content-Type':'application/json'}),credentials:'same-origin',body:JSON.stringify(data)})}
|
|
1453
|
+
function authUpload(url,formData){return fetch(url,{method:'POST',headers:_authHeaders(),credentials:'same-origin',body:formData})}
|
|
1454
|
+
|
|
1455
|
+
function setAIBtnLoading(btn,loading){
|
|
1456
|
+
if(loading){
|
|
1457
|
+
btn._origHTML=btn.innerHTML;
|
|
1458
|
+
btn.innerHTML='<span style="animation:pulse 1s infinite">...</span>';
|
|
1459
|
+
btn.disabled=true;btn.style.opacity='.6';
|
|
1460
|
+
}else{
|
|
1461
|
+
btn.innerHTML=btn._origHTML||btn.innerHTML;
|
|
1462
|
+
btn.disabled=false;btn.style.opacity='';
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
function _getBtn(e){
|
|
1467
|
+
if(!e)return null;
|
|
1468
|
+
// onclick handler: e is the Event. Find closest .ai-btn from target.
|
|
1469
|
+
const ev=e instanceof Event?e:null;
|
|
1470
|
+
if(ev)return ev.target.closest('.ai-btn');
|
|
1471
|
+
return null;
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
async function aiGenerate(field,e){
|
|
1475
|
+
const btn=_getBtn(e);
|
|
1476
|
+
if(_aiPending.has(field))return;
|
|
1477
|
+
_aiPending.add(field);
|
|
1478
|
+
if(btn)setAIBtnLoading(btn,true);
|
|
1479
|
+
try{
|
|
1480
|
+
const ctx=buildContext();
|
|
1481
|
+
const fn=PROMPTS[field]?.generate;if(!fn){toast('No generate prompt for '+field);return}
|
|
1482
|
+
const result=await callAI(fn(ctx));
|
|
1483
|
+
if(!result){toast('AI returned empty response');return}
|
|
1484
|
+
applyAIResult(field,result);
|
|
1485
|
+
scheduleSave();
|
|
1486
|
+
}catch(err){toast('AI error: '+err.message)}
|
|
1487
|
+
finally{_aiPending.delete(field);if(btn)setAIBtnLoading(btn,false)}
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
async function aiEnhance(field,e){
|
|
1491
|
+
const btn=_getBtn(e);
|
|
1492
|
+
if(_aiPending.has(field))return;
|
|
1493
|
+
_aiPending.add(field);
|
|
1494
|
+
if(btn)setAIBtnLoading(btn,true);
|
|
1495
|
+
try{
|
|
1496
|
+
const ctx=buildContext();
|
|
1497
|
+
const fn=PROMPTS[field]?.enhance;if(!fn){toast('No enhance prompt for '+field);return}
|
|
1498
|
+
const result=await callAI(fn(ctx));
|
|
1499
|
+
if(!result){toast('AI returned empty response');return}
|
|
1500
|
+
applyAIResult(field,result);
|
|
1501
|
+
scheduleSave();
|
|
1502
|
+
}catch(err){toast('AI error: '+err.message)}
|
|
1503
|
+
finally{_aiPending.delete(field);if(btn)setAIBtnLoading(btn,false)}
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
async function aiSuggest(field,e){
|
|
1507
|
+
const btn=_getBtn(e);
|
|
1508
|
+
if(_aiPending.has(field))return;
|
|
1509
|
+
_aiPending.add(field);
|
|
1510
|
+
if(btn)setAIBtnLoading(btn,true);
|
|
1511
|
+
try{
|
|
1512
|
+
const ctx=buildContext();
|
|
1513
|
+
const fn=PROMPTS[field]?.suggest;if(!fn){toast('No suggest prompt for '+field);return}
|
|
1514
|
+
const result=await callAI(fn(ctx));
|
|
1515
|
+
if(!result){toast('AI returned empty response');return}
|
|
1516
|
+
applyAIResult(field,result);
|
|
1517
|
+
scheduleSave();
|
|
1518
|
+
}catch(err){toast('AI error: '+err.message)}
|
|
1519
|
+
finally{_aiPending.delete(field);if(btn)setAIBtnLoading(btn,false)}
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
function applyAIResult(field,result){
|
|
1523
|
+
// Clean up common LLM artifacts
|
|
1524
|
+
result=result.replace(/^["']|["']$/g,'').trim();
|
|
1525
|
+
|
|
1526
|
+
// Chip fields: split comma-separated list into chips
|
|
1527
|
+
if(CHIP_FIELDS[field]){
|
|
1528
|
+
const items=result.split(',').map(s=>s.trim()).filter(Boolean);
|
|
1529
|
+
let arr;
|
|
1530
|
+
if(field==='services')arr=formData.targetAudience.services;
|
|
1531
|
+
else if(field==='targetMarkets')arr=formData.targetAudience.targetMarkets;
|
|
1532
|
+
else if(field==='keywords')arr=formData.seo.primaryKeywords;
|
|
1533
|
+
else if(field==='topics')arr=formData.targetAudience.topics;
|
|
1534
|
+
// Add only new items
|
|
1535
|
+
items.forEach(item=>{if(!arr.includes(item))arr.push(item)});
|
|
1536
|
+
renderChips(field);
|
|
1537
|
+
return;
|
|
1538
|
+
}
|
|
1539
|
+
// Text fields: set value directly
|
|
1540
|
+
const elId=FIELD_ELEMENTS[field];
|
|
1541
|
+
if(elId){
|
|
1542
|
+
const el=$(elId);if(!el)return;
|
|
1543
|
+
el.value=result;
|
|
1544
|
+
// Update the data model
|
|
1545
|
+
if(el.dataset.field)setNestedVal(formData,el.dataset.field,result);
|
|
1546
|
+
// Trigger related updates
|
|
1547
|
+
if(field==='description')updDescCount();
|
|
1548
|
+
updateStatus();
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1552
|
+
// ── Start Research ──
|
|
1553
|
+
async function startResearch(){
|
|
1554
|
+
await doSave();
|
|
1555
|
+
// Build config.json for init-website.sh
|
|
1556
|
+
const config={
|
|
1557
|
+
business:{
|
|
1558
|
+
name:formData.basics.businessName,
|
|
1559
|
+
industry:formData.basics.industry,
|
|
1560
|
+
description:formData.basics.description,
|
|
1561
|
+
about:formData.basics.about,
|
|
1562
|
+
owner:formData.basics.ownerName,
|
|
1563
|
+
years_in_business:formData.basics.yearsInBusiness,
|
|
1564
|
+
license:formData.basics.license,
|
|
1565
|
+
target_markets:formData.targetAudience.targetMarkets,
|
|
1566
|
+
services:formData.targetAudience.services,
|
|
1567
|
+
topics:formData.targetAudience.topics,
|
|
1568
|
+
competitors:formData.seo.competitors,
|
|
1569
|
+
primary_keywords:formData.seo.primaryKeywords
|
|
1570
|
+
},
|
|
1571
|
+
website:{
|
|
1572
|
+
domain:formData.basics.domain,
|
|
1573
|
+
site_type:formData.siteConfig.siteType,
|
|
1574
|
+
pages:formData.siteConfig.pages,
|
|
1575
|
+
features:formData.siteConfig.specialFeatures
|
|
1576
|
+
},
|
|
1577
|
+
branding:{
|
|
1578
|
+
theme:formData.branding.theme,
|
|
1579
|
+
tone:formData.branding.tone,
|
|
1580
|
+
colors:getColors(),
|
|
1581
|
+
style:formData.branding.style,
|
|
1582
|
+
logo:formData.images.logo.url
|
|
1583
|
+
},
|
|
1584
|
+
seo:{
|
|
1585
|
+
keywords:formData.seo.primaryKeywords,
|
|
1586
|
+
competitors:formData.seo.competitors,
|
|
1587
|
+
reference_sites:formData.seo.referenceSites.map(r=>r.url)
|
|
1588
|
+
},
|
|
1589
|
+
contact:{
|
|
1590
|
+
phone:formData.basics.phone,
|
|
1591
|
+
email:formData.basics.email,
|
|
1592
|
+
address:formData.basics.address
|
|
1593
|
+
}
|
|
1594
|
+
};
|
|
1595
|
+
// Save research config
|
|
1596
|
+
await authPost('/api/canvas/data/website-setup-config.json',config);
|
|
1597
|
+
// Tell agent
|
|
1598
|
+
const msg=`The website setup form for "${formData.basics.businessName}" has been saved to _data/website-setup.json. Config for autonomous research is at _data/website-setup-config.json. Please run the website research system: create the project folder structure, generate config.json from the saved data, and execute the 8 research tasks.`;
|
|
1599
|
+
window.parent.postMessage({type:'canvas-action',action:'speak',text:msg},'*');
|
|
1600
|
+
// Update status
|
|
1601
|
+
$('statusBadge').className='status-badge running';
|
|
1602
|
+
$('statusBadge').textContent='Research Running';
|
|
1603
|
+
toast('Research request sent to AI agent');
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1606
|
+
// ── Toast ──
|
|
1607
|
+
let toastTimer;
|
|
1608
|
+
function toast(msg){
|
|
1609
|
+
const t=$('toast');t.textContent=msg;t.classList.add('on');
|
|
1610
|
+
clearTimeout(toastTimer);toastTimer=setTimeout(()=>t.classList.remove('on'),3000);
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
// ── Boot ──
|
|
1614
|
+
if(window._canvasAuthToken){init()}else{
|
|
1615
|
+
let inited=false;
|
|
1616
|
+
window.addEventListener('message',function(e){
|
|
1617
|
+
if(e.data&&e.data.type==='auth-token'&&!inited){inited=true;init()}
|
|
1618
|
+
});
|
|
1619
|
+
setTimeout(()=>{if(!inited){inited=true;init()}},200);
|
|
1620
|
+
}
|
|
1621
|
+
</script>
|
|
1622
|
+
</body>
|
|
1623
|
+
</html>
|