@mevdragon/vidfarm-devcli 0.17.0 → 0.18.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.
@@ -1,173 +1,148 @@
1
- // /reskin/chat — the AI brainstorm chat, ported into the farmville design
2
- // system as a CLEAN, CALM single-column conversation.
1
+ // /reskin/chat — the AI brainstorm chat, MIGRATED from a static mockup into a
2
+ // REAL working chat wired to the live editor-chat backend.
3
3
  //
4
- // The reskin app now has its own fixed LEFT sidebar nav (rendered by
5
- // reskinDocument, chrome:"full"), so this page NO LONGER carries its own
6
- // history sidebar or right-hand context panel those were redundant and busy.
7
- // Instead: one centered reading column (~800px) with a compact header, an
8
- // alternating message thread (ink user bubbles + soft white assistant cards),
9
- // a few starter chips, and a quiet sticky composer.
4
+ // Like settings-page.ts, this renderer accepts the SAME input object the live
5
+ // chat route already builds (`Parameters<typeof renderChatPage>[0]`). The
6
+ // `input.editorChat` boot carries the REAL streaming endpoint, the vidfarm API
7
+ // key, the template context, and the cached context the agent needs — we embed
8
+ // that boot as JSON in the page and the composer POSTs straight to it.
10
9
  //
11
- // Rich but restrained: a couple of short paragraphs, ONE bulleted hook list,
12
- // and AT MOST ONE generated-template card with Open/Fork. Gold is used sparingly
13
- // (Send button + a couple of small accents). Built with ONLY shared rk-*
14
- // primitives + a page-scoped rk-chat- block + a small self-contained script.
15
- // No app.ts imports; pure string reskinDocument(...).
10
+ // What's REAL: the composer sends the user's message to the real editor-chat
11
+ // endpoint (`boot.apiUrl`, with the `vidfarm-api-key` header + same-origin
12
+ // creds + the correct body shape) and renders the streamed assistant reply
13
+ // (SSE `text-delta` events). What's SIMPLIFIED: tool calls are rendered as
14
+ // plain status notes (no rich artifact/HTTP cards), attachments are not wired,
15
+ // and there is no thread-history sidebar (the boot carries no history). The
16
+ // design stays farmville — shared rk-* primitives + a page-scoped rk-chat-
17
+ // block + one self-contained script. No app.ts imports.
16
18
  import { reskinDocument, rkEscape } from "./document.js";
19
+ // Default keeps the current zero-arg route (`renderReskinChat()`) compiling.
20
+ // With no editorChat boot the composer degrades gracefully: it renders the
21
+ // user bubble + a friendly "endpoint not available" reply instead of throwing.
22
+ const DEFAULT_INPUT = {
23
+ userId: "",
24
+ currentAccountId: "",
25
+ name: null,
26
+ email: "",
27
+ editorChat: null
28
+ };
17
29
  // ── icons ───────────────────────────────────────────────────────────────────
18
30
  const SEND = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 12l16-8-6 16-2.5-6.5L4 12Z"/></svg>`;
19
- const CLIP = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 11.5l-8.4 8.4a5 5 0 0 1-7.1-7.1l8.5-8.5a3.3 3.3 0 0 1 4.7 4.7l-8.5 8.5a1.7 1.7 0 0 1-2.4-2.4l7.8-7.8"/></svg>`;
20
- function p(html) {
21
- return `<p class="rk-chat-p">${html}</p>`;
22
- }
23
- function hookList(items) {
24
- return `<div class="rk-chat-hooks">${items
25
- .map((h) => `<div class="rk-chat-hook">
26
- <span class="rk-chat-hook-dot" aria-hidden="true"></span>
27
- <span class="rk-chat-hook-text"><strong>${rkEscape(h.angle)}</strong> &mdash; &ldquo;${rkEscape(h.text)}&rdquo;</span>
28
- </div>`)
29
- .join("")}</div>`;
30
- }
31
- function note(label, text) {
32
- return `<p class="rk-chat-note"><strong>${rkEscape(label)}</strong> ${rkEscape(text)}</p>`;
33
- }
34
- function artifact(a) {
35
- return `<div class="rk-chat-art">
36
- <div class="rk-chat-poster">
37
- <span class="rk-chat-poster-dur">${rkEscape(a.dur)}</span>
38
- <span class="rk-chat-poster-play" aria-hidden="true">&#9654;</span>
39
- </div>
40
- <div class="rk-chat-art-main">
41
- <span class="rk-chat-art-eyebrow">${rkEscape(a.eyebrow)}</span>
42
- <h4 class="rk-chat-art-title">${rkEscape(a.title)}</h4>
43
- <p class="rk-chat-art-meta">${rkEscape(a.meta)}</p>
44
- <p class="rk-chat-art-desc">${rkEscape(a.desc)}</p>
45
- <div class="rk-chat-art-actions">
46
- <button type="button" class="rk-btn rk-btn-ink rk-btn-sm">Open in editor <span class="rk-arrow">&rarr;</span></button>
47
- <button type="button" class="rk-btn rk-btn-ghost rk-btn-sm">Fork</button>
48
- </div>
49
- </div>
50
- </div>`;
51
- }
52
- const SAMPLE = [
53
- {
54
- role: "user",
55
- time: "9:41 AM",
56
- text: "I'm launching a $39 magnesium sleep gummy for stressed founders. Give me short-form ideas that'll convert cold TikTok traffic — I keep drawing a blank."
57
- },
58
- {
59
- role: "assistant",
60
- time: "9:41 AM",
61
- body: p("Great starting point — a founder audience plus a sleep product is a hook goldmine. Here are the three angles I'd test first, each pulling a different psychological lever:") +
62
- hookList([
63
- { angle: "Problem-aware", text: "POV: it's 2am and your brain won't stop pitching investors." },
64
- { angle: "Contrarian", text: "Melatonin is why you wake up groggy. Here's what founders take instead." },
65
- { angle: "Demo", text: "I swapped my nightcap for one gummy for 14 nights. Night 12 broke me." }
66
- ]) +
67
- note("Format —", "talking-head UGC, 22–28s, hard cut every ~2s. Shoot vertical in natural light and read it like a voice memo, not an ad.")
68
- },
69
- {
70
- role: "user",
71
- time: "9:43 AM",
72
- text: "The '2am' one is 🔥. Turn it into a full UGC ad script and spin up a template I can hand to a creator."
73
- },
74
- {
75
- role: "assistant",
76
- time: "9:43 AM",
77
- body: p("Here's that hook as a ready-to-shoot 24-second cut — the emotional turn lands right before the CTA:") +
78
- artifact({
79
- dur: "0:24",
80
- eyebrow: "Proposed template",
81
- title: "The 2am Founder — UGC cut",
82
- meta: "9:16 · 24s · Talking-head · word-by-word captions",
83
- desc: "Wired to your hook and a five-beat script, with captions timed to the read. Fork it to swap the shot list or recut for a specific creator."
84
- }) +
85
- p("Want me to draft the on-screen captions and lock a trending sound? Say the word and I'll build the shot list.")
86
- }
87
- ];
31
+ // Full-sentence starter prompts clicking one sends it straight to the agent.
88
32
  const STARTERS = [
89
- { label: "Pitch 5 hooks" },
90
- { label: "Turn this into a UGC ad" },
91
- { label: "Remix a trending format" },
92
- { label: "Write scroll-stopping captions" }
33
+ "Pitch me 5 scroll-stopping hooks I can test on cold TikTok traffic.",
34
+ "Turn my best idea into a ready-to-shoot UGC ad script.",
35
+ "Remix a trending short-form format for my product.",
36
+ "Write word-by-word captions for a 24-second talking-head cut."
93
37
  ];
94
- // recent brainstorms surfaced only in the unobtrusive "History" menu now that
95
- // the standing history sidebar is gone.
96
- const RECENTS = [
97
- { title: "Magnesium sleep gummy cold TikTok", when: "2m", active: true },
98
- { title: "Founder productivity app launch week", when: "1h" },
99
- { title: "Skincare serum — before/after", when: "Yesterday" },
100
- { title: "AI notetaker demo cuts", when: "Mon" }
101
- ];
102
- // ── render ───────────────────────────────────────────────────────────────────
103
- function renderMessage(m) {
104
- if (m.role === "user") {
105
- return `<div class="rk-chat-msg is-user">
106
- <div class="rk-chat-meta">You <span class="rk-chat-time">${rkEscape(m.time)}</span></div>
107
- <div class="rk-chat-bubble rk-chat-bubble-user">${rkEscape(m.text)}</div>
108
- </div>`;
109
- }
110
- return `<div class="rk-chat-msg is-assistant">
111
- <div class="rk-chat-meta"><span class="rk-chat-avatar" aria-hidden="true">V</span>Copilot <span class="rk-chat-time">${rkEscape(m.time)}</span></div>
112
- <div class="rk-chat-bubble rk-chat-bubble-assistant">${m.body}</div>
113
- </div>`;
114
- }
115
- export function renderReskinChat() {
116
- const thread = SAMPLE.map(renderMessage).join("");
117
- const starterChips = STARTERS.map((s) => `<button type="button" class="rk-chat-chip" data-rk-prompt="${rkEscape(s.label)}">${rkEscape(s.label)}</button>`).join("");
118
- const historyItems = RECENTS.map((r) => `<button type="button" class="rk-chat-hist-item${r.active ? " is-active" : ""}" data-rk-title="${rkEscape(r.title)}">
119
- <span class="rk-chat-hist-title">${rkEscape(r.title)}</span>
120
- <span class="rk-chat-hist-when">${rkEscape(r.when)}</span>
121
- </button>`).join("");
38
+ export function renderReskinChat(input = DEFAULT_INPUT) {
39
+ const boot = input.editorChat ?? null;
40
+ // Embed the real boot as JSON. Escaping only `<` is the correct (and
41
+ // sufficient) way to keep a `<script type="application/json">` block from
42
+ // breaking out; HTML-entity escaping would corrupt JSON.parse.
43
+ const bootJson = JSON.stringify(boot).replace(/</g, "\\u003c");
44
+ const starterChips = STARTERS.map((s) => `<button type="button" class="rk-chat-chip" data-rk-prompt="${rkEscape(s)}">${rkEscape(s)}</button>`).join("");
122
45
  const body = `
123
46
  <main class="rk-chat-page">
124
- <div class="rk-chat-col">
125
- <header class="rk-chat-head">
126
- <div class="rk-chat-head-titles">
127
- <span class="rk-chat-eyebrow">Brainstorm</span>
128
- <h1 class="rk-chat-title" id="rk-chat-title">Magnesium sleep gummy — cold TikTok</h1>
129
- <p class="rk-chat-subhead">Short-form angles, hooks &amp; a ready-to-render template — brainstormed on your own AI keys.</p>
130
- </div>
131
- <div class="rk-chat-head-actions">
132
- <div class="rk-chat-history">
133
- <button type="button" class="rk-btn rk-btn-ghost rk-btn-sm" id="rk-chat-history-btn" aria-haspopup="true" aria-expanded="false">History <span class="rk-chat-caret" aria-hidden="true">▾</span></button>
134
- <div class="rk-chat-hist-menu" id="rk-chat-history-menu" hidden>
135
- <div class="rk-chat-hist-head">Recent brainstorms</div>
136
- ${historyItems}
47
+ <aside class="rk-chat-history" id="rk-chat-history" aria-label="Chat history">
48
+ <div class="rk-chat-history-head">
49
+ <span class="rk-chat-history-title">Chats</span>
50
+ <button type="button" class="rk-chat-newbtn" id="rk-chat-new"><span class="rk-chat-plus" aria-hidden="true">+</span> New</button>
51
+ </div>
52
+ <div class="rk-chat-history-list" id="rk-chat-history-list"></div>
53
+ </aside>
54
+ <div class="rk-chat-histbackdrop" id="rk-chat-histbackdrop" hidden></div>
55
+ <div class="rk-chat-main">
56
+ <div class="rk-chat-col">
57
+ <header class="rk-chat-head">
58
+ <button type="button" class="rk-chat-histtoggle" id="rk-chat-histtoggle" aria-label="Show chat history">☰</button>
59
+ <div class="rk-chat-head-titles">
60
+ <span class="rk-chat-eyebrow">Brainstorm</span>
61
+ <h1 class="rk-chat-title" id="rk-chat-title">Brainstorm studio</h1>
62
+ <p class="rk-chat-subhead">Short-form angles, hooks &amp; scripts &mdash; brainstormed live on your own AI keys, never billed by vidfarm.</p>
63
+ </div>
64
+ <div class="rk-chat-head-actions">
65
+ <label class="rk-chat-mode" aria-label="Studio mode">
66
+ <select class="rk-chat-mode-select" id="rk-chat-mode">
67
+ <option value="/chat" selected>Brainstorm</option>
68
+ <option value="/inpaint">Create Image</option>
69
+ </select>
70
+ <span class="rk-chat-mode-chev" aria-hidden="true">&#9662;</span>
71
+ </label>
72
+ </div>
73
+ </header>
74
+
75
+ <div class="rk-chat-thread" id="rk-chat-thread">
76
+ <div class="rk-chat-msg is-assistant">
77
+ <div class="rk-chat-meta"><span class="rk-chat-avatar" aria-hidden="true">V</span>Copilot</div>
78
+ <div class="rk-chat-bubble rk-chat-bubble-assistant">
79
+ <div class="rk-chat-stream">Tell me the offer, who it&rsquo;s for, and where the traffic is cold &mdash; I&rsquo;ll come back with angles, hooks, and scripts you can render. Pick a starter below or just start typing.</div>
137
80
  </div>
138
81
  </div>
139
- <button type="button" class="rk-btn rk-btn-ghost rk-btn-sm rk-chat-new" id="rk-chat-new"><span class="rk-chat-plus" aria-hidden="true">+</span> New</button>
140
82
  </div>
141
- </header>
142
-
143
- <div class="rk-chat-thread" id="rk-chat-thread">
144
- ${thread}
145
- </div>
146
83
 
147
- <div class="rk-chat-dock">
148
- <div class="rk-chat-starters" aria-label="Starter prompts">
149
- ${starterChips}
150
- </div>
151
- <div class="rk-chat-composer" id="rk-chat-composer">
152
- <button type="button" class="rk-chat-attach" aria-label="Attach a file">${CLIP}</button>
153
- <textarea id="rk-chat-input" class="rk-chat-input" rows="1"
154
- placeholder="Describe your offer, audience, or the problem to solve…"></textarea>
155
- <button type="button" class="rk-btn rk-btn-gold rk-chat-send" id="rk-chat-send">Send ${SEND}</button>
84
+ <div class="rk-chat-dock">
85
+ <div class="rk-chat-starters" aria-label="Starter prompts">
86
+ ${starterChips}
87
+ </div>
88
+ <div class="rk-chat-composer" id="rk-chat-composer">
89
+ <textarea id="rk-chat-input" class="rk-chat-input" rows="1"
90
+ placeholder="Describe your offer, audience, or the problem to solve…"></textarea>
91
+ <button type="button" class="rk-btn rk-btn-gold rk-chat-send" id="rk-chat-send">Send ${SEND}</button>
92
+ </div>
93
+ <p class="rk-chat-hint">Brainstormed on your own AI keys — never billed by vidfarm. <kbd>Enter</kbd> to send · <kbd>Shift</kbd>+<kbd>Enter</kbd> for a new line.</p>
156
94
  </div>
157
- <p class="rk-chat-hint">Brainstormed on your own AI keys — never billed by vidfarm. <kbd>Enter</kbd> to send · <kbd>Shift</kbd>+<kbd>Enter</kbd> for a new line.</p>
158
95
  </div>
159
96
  </div>
97
+ <script type="application/json" id="rk-chat-boot">${bootJson}</script>
160
98
  </main>`;
161
99
  const pageCss = `
162
- /* ── centered single-column chat ───────────────────────────────────────── */
163
- .rk-chat-page{width:100%;padding:0 var(--rk-gutter);background:var(--rk-bg)}
100
+ /* ── two-pane: history sidebar + centered chat column ─────────────────────── */
101
+ .rk-chat-page{display:flex;width:100%;background:var(--rk-bg);min-height:100vh}
102
+ .rk-chat-main{flex:1;min-width:0;display:flex;justify-content:center;padding:0 var(--rk-gutter)}
164
103
  .rk-chat-col{width:min(800px,100%);margin:0 auto;display:flex;flex-direction:column;
165
104
  min-height:100vh;padding-top:34px}
166
105
 
106
+ /* history sidebar (real editor-chat threads) */
107
+ .rk-chat-history{flex:none;width:266px;background:var(--rk-surface);border-right:1px solid var(--rk-border);
108
+ display:flex;flex-direction:column;position:sticky;top:0;height:100vh}
109
+ .rk-chat-history-head{display:flex;align-items:center;justify-content:space-between;gap:10px;
110
+ padding:20px 16px 12px;flex:none}
111
+ .rk-chat-history-title{font-family:var(--rk-font-display);font-weight:800;font-size:16px;color:var(--rk-ink);letter-spacing:-.01em}
112
+ .rk-chat-newbtn{display:inline-flex;align-items:center;gap:5px;padding:7px 13px;border-radius:var(--rk-r-full);
113
+ border:1px solid var(--rk-gold-600);background:var(--rk-gold-tint);color:var(--rk-ink);cursor:pointer;
114
+ font-family:var(--rk-font-body);font-size:12.5px;font-weight:700;transition:background var(--rk-dur) var(--rk-ease)}
115
+ .rk-chat-newbtn:hover{background:var(--rk-gold-tint2)}
116
+ .rk-chat-history-list{flex:1;min-height:0;overflow-y:auto;padding:4px 10px 16px;display:grid;gap:2px;align-content:start}
117
+ .rk-chat-hrow{display:flex;align-items:center;gap:8px;width:100%;padding:9px 10px;border-radius:var(--rk-r-lg);
118
+ border:0;background:transparent;cursor:pointer;text-align:left;font-family:inherit;min-width:0;
119
+ transition:background var(--rk-dur) var(--rk-ease),box-shadow var(--rk-dur) var(--rk-ease)}
120
+ .rk-chat-hrow:hover{background:var(--rk-n-50);box-shadow:var(--rk-shadow-xs)}
121
+ .rk-chat-hrow.is-active{background:var(--rk-gold-tint)}
122
+ .rk-chat-hrow-main{flex:1;min-width:0;display:grid;gap:1px}
123
+ .rk-chat-hrow-title{font-size:13px;font-weight:600;color:var(--rk-ink);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
124
+ .rk-chat-hrow-sub{font-size:11px;color:var(--rk-text-muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
125
+ .rk-chat-hrow-del{flex:none;border:0;background:transparent;color:var(--rk-n-400);cursor:pointer;font-size:15px;
126
+ line-height:1;padding:2px 5px;border-radius:var(--rk-r-sm);opacity:0;
127
+ transition:opacity var(--rk-dur) var(--rk-ease),color var(--rk-dur) var(--rk-ease),background var(--rk-dur) var(--rk-ease)}
128
+ .rk-chat-hrow:hover .rk-chat-hrow-del{opacity:1}
129
+ .rk-chat-hrow-del:hover{color:var(--rk-red);background:var(--rk-red-tint)}
130
+ .rk-chat-history-note{padding:16px 14px;font-size:12.5px;color:var(--rk-text-muted);line-height:1.55;display:grid;gap:9px;justify-items:start}
131
+ .rk-chat-history-note a{color:var(--rk-gold-700);font-weight:600}
132
+ .rk-chat-history-note button{font-family:inherit;font-size:12px;font-weight:600;color:var(--rk-ink);cursor:pointer;
133
+ background:#fff;border:1px solid var(--rk-border);border-radius:var(--rk-r-full);padding:5px 12px}
134
+ .rk-chat-history-note button:hover{background:var(--rk-n-100)}
135
+
136
+ /* narrow-screen history toggle + slide-over (hidden on wide) */
137
+ .rk-chat-histtoggle{display:none;flex:none;width:34px;height:34px;border-radius:var(--rk-r-lg);
138
+ border:1px solid var(--rk-border);background:#fff;color:var(--rk-n-600);cursor:pointer;font-size:15px;
139
+ align-items:center;justify-content:center;margin-right:2px;margin-top:2px}
140
+ .rk-chat-histbackdrop{display:none}
141
+
167
142
  /* compact header */
168
- .rk-chat-head{display:flex;align-items:flex-start;justify-content:space-between;gap:18px;
143
+ .rk-chat-head{display:flex;align-items:flex-start;gap:12px;
169
144
  padding-bottom:22px;margin-bottom:6px;border-bottom:1px solid var(--rk-border)}
170
- .rk-chat-head-titles{display:grid;gap:6px;min-width:0}
145
+ .rk-chat-head-titles{display:grid;gap:6px;min-width:0;flex:1}
171
146
  .rk-chat-eyebrow{display:inline-flex;align-items:center;font-size:12px;font-weight:700;
172
147
  letter-spacing:.06em;text-transform:uppercase;color:var(--rk-gold-700)}
173
148
  .rk-chat-title{font-family:var(--rk-font-display);font-weight:800;font-size:1.7rem;letter-spacing:-.025em;
@@ -175,24 +150,16 @@ export function renderReskinChat() {
175
150
  .rk-chat-subhead{font-size:14.5px;color:var(--rk-text-muted);line-height:1.5;max-width:56ch}
176
151
  .rk-chat-head-actions{display:flex;align-items:center;gap:9px;flex:none}
177
152
  .rk-chat-plus{font-size:14px;font-weight:700;line-height:1;margin-right:1px}
178
- .rk-chat-caret{font-size:11px;color:var(--rk-text-faint)}
179
-
180
- /* history popover */
181
- .rk-chat-history{position:relative}
182
- .rk-chat-hist-menu{position:absolute;top:calc(100% + 8px);right:0;z-index:20;width:min(300px,80vw);
183
- background:var(--rk-surface);border:1px solid var(--rk-border);border-radius:var(--rk-r-2xl);
184
- box-shadow:var(--rk-shadow-lg);padding:8px;display:grid;gap:2px}
185
- .rk-chat-hist-menu[hidden]{display:none}
186
- .rk-chat-hist-head{font-size:11px;font-weight:800;letter-spacing:.08em;text-transform:uppercase;
187
- color:var(--rk-text-faint);padding:8px 10px 6px}
188
- .rk-chat-hist-item{display:flex;align-items:center;justify-content:space-between;gap:10px;width:100%;
189
- text-align:left;cursor:pointer;font-family:var(--rk-font-body);background:transparent;border:1px solid transparent;
190
- border-radius:var(--rk-r-lg);padding:9px 11px;transition:background var(--rk-dur) var(--rk-ease)}
191
- .rk-chat-hist-item:hover{background:var(--rk-n-100)}
192
- .rk-chat-hist-item.is-active{background:var(--rk-gold-tint)}
193
- .rk-chat-hist-title{font-size:13px;font-weight:600;color:var(--rk-ink);min-width:0;white-space:nowrap;
194
- overflow:hidden;text-overflow:ellipsis}
195
- .rk-chat-hist-when{flex:none;font-size:11px;font-weight:600;color:var(--rk-text-faint)}
153
+
154
+ /* studio mode select (Brainstorm ↔ Create Image → /reskin/inpaint) */
155
+ .rk-chat-mode{position:relative;display:inline-flex;align-items:center}
156
+ .rk-chat-mode-select{appearance:none;-webkit-appearance:none;font-family:var(--rk-font-body);font-size:13px;font-weight:700;
157
+ color:var(--rk-ink);background:var(--rk-surface);border:1px solid var(--rk-border-strong);border-radius:var(--rk-r-full);
158
+ padding:9px 32px 9px 15px;cursor:pointer;box-shadow:var(--rk-shadow-xs);
159
+ transition:border-color var(--rk-dur) var(--rk-ease),box-shadow var(--rk-dur) var(--rk-ease)}
160
+ .rk-chat-mode-select:hover{border-color:var(--rk-gold-600)}
161
+ .rk-chat-mode-select:focus{outline:none;border-color:var(--rk-gold-600);box-shadow:var(--rk-ring-gold)}
162
+ .rk-chat-mode-chev{position:absolute;right:13px;top:50%;transform:translateY(-50%);pointer-events:none;font-size:11px;color:var(--rk-n-500)}
196
163
 
197
164
  /* thread — flex:1 pushes the dock to the bottom when the page is short */
198
165
  .rk-chat-thread{flex:1;display:grid;gap:28px;align-content:start;padding:30px 0 26px}
@@ -211,38 +178,36 @@ export function renderReskinChat() {
211
178
  border:1px solid var(--rk-n-800);box-shadow:var(--rk-shadow-sm)}
212
179
  .rk-chat-bubble-assistant{width:100%;background:var(--rk-surface);color:var(--rk-n-700);
213
180
  border:1px solid var(--rk-border);box-shadow:var(--rk-shadow-card)}
214
-
215
- /* rich (but calm) assistant content */
216
- .rk-chat-p{margin:0}
217
- .rk-chat-p + *{margin-top:14px}
218
181
  .rk-chat-bubble-assistant strong{color:var(--rk-ink);font-weight:700}
219
- .rk-chat-hooks{display:grid;gap:12px}
220
- .rk-chat-hook{display:grid;grid-template-columns:auto minmax(0,1fr);gap:11px;align-items:start}
221
- .rk-chat-hook-dot{width:7px;height:7px;border-radius:50%;background:var(--rk-gold-500);
222
- box-shadow:0 0 0 3px var(--rk-gold-tint);margin-top:8px}
223
- .rk-chat-hook-text{color:var(--rk-n-700);line-height:1.55}
224
- .rk-chat-note{margin-top:16px;font-size:13.5px;color:var(--rk-text-muted);line-height:1.6}
225
- .rk-chat-note strong{color:var(--rk-ink);font-weight:700}
226
-
227
- /* generated-template card (nested card keeps its own full hairline) */
228
- .rk-chat-art{display:flex;gap:16px;margin-top:16px;padding:15px;border-radius:var(--rk-r-2xl);
229
- background:var(--rk-n-50);border:1px solid var(--rk-border)}
230
- .rk-chat-art-main{display:grid;gap:7px;align-content:start;min-width:0}
231
- .rk-chat-art-eyebrow{font-size:11px;font-weight:800;letter-spacing:.08em;text-transform:uppercase;color:var(--rk-gold-700)}
232
- .rk-chat-art-title{font-family:var(--rk-font-display);font-weight:800;font-size:16px;color:var(--rk-ink);letter-spacing:-.01em}
233
- .rk-chat-art-meta{font-size:12.5px;color:var(--rk-text-faint);font-weight:500}
234
- .rk-chat-art-desc{font-size:13.5px;color:var(--rk-text-muted);line-height:1.55}
235
- .rk-chat-art-actions{display:flex;gap:8px;flex-wrap:wrap;margin-top:4px}
236
-
237
- /* 9:16 poster tile */
238
- .rk-chat-poster{position:relative;width:116px;aspect-ratio:9/16;flex:none;overflow:hidden;
239
- border-radius:var(--rk-r-xl);box-shadow:var(--rk-shadow-sm);
240
- background:linear-gradient(160deg,#3b2f73,#8256c9)}
241
- .rk-chat-poster-dur{position:absolute;top:8px;right:8px;font-size:10px;font-weight:700;color:#fff;
242
- background:rgba(0,0,0,.42);padding:2px 7px;border-radius:var(--rk-r-full);letter-spacing:.02em}
243
- .rk-chat-poster-play{position:absolute;inset:0;margin:auto;width:34px;height:34px;border-radius:50%;
244
- background:rgba(255,255,255,.94);color:var(--rk-ink);display:grid;place-items:center;font-size:11px;
245
- padding-left:2px;box-shadow:var(--rk-shadow-sm)}
182
+
183
+ /* streamed assistant text — pre-wrap keeps the model's own line breaks */
184
+ .rk-chat-stream{white-space:pre-wrap;overflow-wrap:anywhere;color:var(--rk-n-700);line-height:1.65}
185
+ .rk-chat-stream[hidden]{display:none}
186
+
187
+ /* thinking / status row */
188
+ .rk-chat-status{display:flex;align-items:center;gap:10px;color:var(--rk-text-faint);font-size:13.5px}
189
+ .rk-chat-status[hidden]{display:none}
190
+ .rk-chat-status-label{font-size:13px;color:var(--rk-text-muted)}
191
+
192
+ /* simplified tool-call notes (rich artifact/HTTP cards are intentionally omitted) */
193
+ .rk-chat-notes{display:grid;gap:6px;margin-bottom:10px}
194
+ .rk-chat-notes:empty{display:none}
195
+ .rk-chat-toolnote{display:inline-flex;align-items:center;gap:7px;font-size:12.5px;color:var(--rk-text-muted);
196
+ font-family:var(--rk-font-mono);background:var(--rk-n-50);border:1px solid var(--rk-border);
197
+ border-radius:var(--rk-r-lg);padding:5px 10px;width:fit-content}
198
+ .rk-chat-toolnote::before{content:"";width:6px;height:6px;border-radius:50%;background:var(--rk-gold-500);flex:none}
199
+
200
+ /* error surface */
201
+ .rk-chat-error{margin-top:2px;font-size:13.5px;color:#b42318;background:var(--rk-n-50);
202
+ border:1px solid var(--rk-border);border-radius:var(--rk-r-lg);padding:10px 12px;white-space:pre-wrap}
203
+
204
+ /* typing indicator */
205
+ .rk-chat-typing{display:inline-flex;gap:4px;align-items:center;padding:2px 0}
206
+ .rk-chat-typing span{width:7px;height:7px;border-radius:50%;background:var(--rk-n-400);
207
+ animation:rk-chat-blink 1.2s var(--rk-ease) infinite}
208
+ .rk-chat-typing span:nth-child(2){animation-delay:.18s}
209
+ .rk-chat-typing span:nth-child(3){animation-delay:.36s}
210
+ @keyframes rk-chat-blink{0%,80%,100%{opacity:.28;transform:translateY(0)}40%{opacity:1;transform:translateY(-2px)}}
246
211
 
247
212
  /* ── quiet sticky composer dock ── */
248
213
  .rk-chat-dock{position:sticky;bottom:0;z-index:10;background:var(--rk-bg);
@@ -252,70 +217,77 @@ export function renderReskinChat() {
252
217
  .rk-chat-starters{display:flex;flex-wrap:wrap;gap:9px}
253
218
  .rk-chat-chip{display:inline-flex;align-items:center;gap:7px;padding:8px 14px;border-radius:var(--rk-r-full);
254
219
  font-family:var(--rk-font-body);font-size:13px;font-weight:600;color:var(--rk-n-700);cursor:pointer;
255
- background:var(--rk-surface);border:1px solid var(--rk-border);box-shadow:var(--rk-shadow-xs);
220
+ background:var(--rk-surface);border:1px solid var(--rk-border);box-shadow:var(--rk-shadow-xs);text-align:left;
256
221
  transition:transform var(--rk-dur) var(--rk-ease),box-shadow var(--rk-dur) var(--rk-ease),border-color var(--rk-dur) var(--rk-ease),background var(--rk-dur) var(--rk-ease)}
257
222
  .rk-chat-chip:hover{border-color:var(--rk-gold-600);background:var(--rk-gold-tint);color:var(--rk-ink);
258
223
  transform:translateY(-1px);box-shadow:var(--rk-shadow-sm)}
224
+ .rk-chat-chip:disabled{opacity:.5;cursor:default;transform:none;box-shadow:var(--rk-shadow-xs)}
259
225
  .rk-chat-composer{display:flex;align-items:flex-end;gap:9px;background:var(--rk-surface);
260
- border:1px solid var(--rk-border-strong);border-radius:var(--rk-r-3xl);padding:9px 9px 9px 12px;
226
+ border:1px solid var(--rk-border-strong);border-radius:var(--rk-r-3xl);padding:9px 9px 9px 16px;
261
227
  box-shadow:var(--rk-shadow-card);transition:border-color var(--rk-dur) var(--rk-ease),box-shadow var(--rk-dur) var(--rk-ease)}
262
228
  .rk-chat-composer:focus-within{border-color:var(--rk-gold-600);box-shadow:var(--rk-ring-gold)}
263
- .rk-chat-attach{flex:none;width:40px;height:40px;border-radius:var(--rk-r-full);border:0;cursor:pointer;
264
- display:grid;place-items:center;background:var(--rk-n-100);color:var(--rk-n-600);
265
- transition:background var(--rk-dur) var(--rk-ease),color var(--rk-dur) var(--rk-ease)}
266
- .rk-chat-attach:hover{background:var(--rk-n-200);color:var(--rk-ink)}
267
- .rk-chat-attach svg{width:19px;height:19px}
268
229
  .rk-chat-input{flex:1;min-width:0;border:0;background:transparent;resize:none;font-family:var(--rk-font-body);
269
230
  font-size:15px;line-height:1.5;color:var(--rk-ink);padding:10px 2px;max-height:148px}
270
231
  .rk-chat-input:focus{outline:none;box-shadow:none}
271
232
  .rk-chat-input::placeholder{color:var(--rk-n-400)}
272
233
  .rk-chat-send{flex:none;padding:11px 18px}
273
234
  .rk-chat-send svg{width:16px;height:16px}
235
+ .rk-chat-send:disabled{opacity:.55;cursor:default}
274
236
  .rk-chat-hint{text-align:center;font-size:11.5px;color:var(--rk-text-faint);line-height:1.5}
275
237
  .rk-chat-hint kbd{font-family:var(--rk-font-mono);font-size:10.5px;background:var(--rk-n-100);
276
238
  border:1px solid var(--rk-border);border-radius:5px;padding:1px 5px;color:var(--rk-n-600)}
277
239
 
278
- /* typing indicator (appended on send) */
279
- .rk-chat-typing{display:inline-flex;gap:4px;align-items:center;padding:2px 0}
280
- .rk-chat-typing span{width:7px;height:7px;border-radius:50%;background:var(--rk-n-400);
281
- animation:rk-chat-blink 1.2s var(--rk-ease) infinite}
282
- .rk-chat-typing span:nth-child(2){animation-delay:.18s}
283
- .rk-chat-typing span:nth-child(3){animation-delay:.36s}
284
- @keyframes rk-chat-blink{0%,80%,100%{opacity:.28;transform:translateY(0)}40%{opacity:1;transform:translateY(-2px)}}
285
-
286
- /* ── narrow: bubbles go full-width, header stacks ── */
240
+ /* ── narrow: history becomes a slide-over, bubbles go full-width ── */
241
+ @media(max-width:900px){
242
+ .rk-chat-histtoggle{display:inline-flex}
243
+ .rk-chat-history{position:fixed;left:0;top:0;bottom:0;height:auto;z-index:70;width:min(300px,86vw);
244
+ transform:translateX(-100%);box-shadow:var(--rk-shadow-xl);transition:transform var(--rk-dur) var(--rk-ease)}
245
+ .rk-chat-history.is-open{transform:none}
246
+ .rk-chat-histbackdrop{display:block;position:fixed;inset:0;z-index:65;background:rgba(0,0,0,.28)}
247
+ .rk-chat-histbackdrop[hidden]{display:none}
248
+ }
287
249
  @media(max-width:720px){
288
250
  .rk-chat-col{padding-top:24px}
289
- .rk-chat-head{flex-direction:column;gap:14px}
290
- .rk-chat-head-actions{align-self:flex-start}
291
251
  .rk-chat-title{font-size:1.5rem}
292
252
  .rk-chat-bubble-user{max-width:100%}
293
- }
294
- @media(max-width:560px){
295
- .rk-chat-art{flex-direction:column}
296
- .rk-chat-poster{width:132px}
297
253
  .rk-chat-thread{gap:22px}
298
254
  }
299
255
  `;
300
256
  const script = `
301
257
  (function(){
302
258
  var root=document;
259
+
260
+ // ── read the real editor-chat boot (endpoint + auth + context) ──────────
261
+ var boot=null;
262
+ try{ var bootEl=root.getElementById('rk-chat-boot'); if(bootEl){ boot=JSON.parse(bootEl.textContent||'null'); } }catch(e){ boot=null; }
263
+ var ENDPOINT=(boot && boot.apiUrl) || '/api/v1/editor-chat';
264
+ var API_KEY=(boot && boot.vidfarmApiKey) || null;
265
+ var TEMPLATE=(boot && boot.template) || null;
266
+ var CACHED=(boot && boot.cachedContext) || null;
267
+
303
268
  var input=root.getElementById('rk-chat-input');
304
269
  var send=root.getElementById('rk-chat-send');
305
270
  var thread=root.getElementById('rk-chat-thread');
306
271
  var title=root.getElementById('rk-chat-title');
307
272
  var AVATAR='V';
308
- var REPLIES=[
309
- "On it. Give me a beat — I'll shape that into hooks, a tight script, and a template you can render.",
310
- "Good direction. I'll pull a trending format that fits and draft the on-screen captions to match.",
311
- "Love it. Let me turn that into three testable angles and flag which one is cheapest to shoot first."
312
- ];
313
- var replyTurn=0;
314
273
 
274
+ var history=[]; // [{role, text}] — the running conversation
275
+ var threadId=null; // generated on first send, reused for persistence
276
+ var busy=false;
277
+
278
+ function genId(p){ return p+'-'+Date.now().toString(36)+'-'+Math.random().toString(36).slice(2,8); }
315
279
  function autosize(){ if(!input)return; input.style.height='auto'; input.style.height=Math.min(input.scrollHeight,148)+'px'; }
316
280
  function toBottom(){ try{ window.scrollTo({top:document.body.scrollHeight,behavior:'smooth'}); }catch(e){ window.scrollTo(0,document.body.scrollHeight); } }
317
281
  function nowLabel(){ try{ return new Date().toLocaleTimeString([], {hour:'numeric', minute:'2-digit'}); }catch(e){ return ''; } }
318
282
  function el(cls){ var d=root.createElement('div'); d.className=cls; return d; }
283
+ function setBusy(v){ busy=v; if(send){ send.disabled=v; } root.querySelectorAll('[data-rk-prompt]').forEach(function(c){ c.disabled=v; }); }
284
+
285
+ function clientContext(){
286
+ var now=new Date(); var local=null, tz=null;
287
+ try{ local=new Intl.DateTimeFormat(undefined,{dateStyle:'full',timeStyle:'long'}).format(now); }catch(e){ local=now.toString(); }
288
+ try{ tz=(Intl.DateTimeFormat().resolvedOptions().timeZone)||null; }catch(e){ tz=null; }
289
+ return { currentDateTime: now.toISOString(), localDateTime: local, timeZone: tz };
290
+ }
319
291
 
320
292
  function appendUser(text){
321
293
  var msg=el('rk-chat-msg is-user');
@@ -324,87 +296,308 @@ export function renderReskinChat() {
324
296
  msg.appendChild(meta); msg.appendChild(bub);
325
297
  if(thread) thread.appendChild(msg);
326
298
  }
299
+
300
+ // Returns a small controller for the streaming assistant bubble.
327
301
  function appendAssistant(){
328
302
  var msg=el('rk-chat-msg is-assistant');
329
303
  var meta=el('rk-chat-meta');
330
304
  meta.innerHTML='<span class="rk-chat-avatar" aria-hidden="true">'+AVATAR+'</span>Copilot <span class="rk-chat-time">'+nowLabel()+'</span>';
331
305
  var bub=el('rk-chat-bubble rk-chat-bubble-assistant');
332
- bub.innerHTML='<span class="rk-chat-typing"><span></span><span></span><span></span></span>';
306
+ var status=el('rk-chat-status');
307
+ status.innerHTML='<span class="rk-chat-typing"><span></span><span></span><span></span></span><span class="rk-chat-status-label">Thinking…</span>';
308
+ var notes=el('rk-chat-notes');
309
+ var textEl=el('rk-chat-stream'); textEl.hidden=true;
310
+ bub.appendChild(status); bub.appendChild(notes); bub.appendChild(textEl);
333
311
  msg.appendChild(meta); msg.appendChild(bub);
334
312
  if(thread) thread.appendChild(msg);
335
- return bub;
313
+ return {
314
+ setStatus:function(s){ var l=status.querySelector('.rk-chat-status-label'); if(l && s){ l.textContent=s; } },
315
+ hideStatus:function(){ status.hidden=true; },
316
+ showText:function(){ status.hidden=true; textEl.hidden=false; },
317
+ setText:function(t){ textEl.textContent=t; },
318
+ addNote:function(t){ var n=el('rk-chat-toolnote'); n.textContent=t; notes.appendChild(n); },
319
+ fail:function(m){ status.hidden=true; var er=el('rk-chat-error'); er.textContent=m||'Something went wrong.'; bub.appendChild(er); }
320
+ };
321
+ }
322
+
323
+ // Minimal SSE reader — mirrors the live client's text/event-stream parsing.
324
+ function readStream(response, h){
325
+ if(!response.body || !response.body.getReader){
326
+ // No streaming reader available — fall back to reading the whole body.
327
+ return response.text().then(function(t){ parseEvents(t, h); });
328
+ }
329
+ var reader=response.body.getReader();
330
+ var decoder=new TextDecoder();
331
+ var buffer='';
332
+ function pump(){
333
+ return reader.read().then(function(res){
334
+ if(res.done){ if(buffer){ parseEvents(buffer, h); buffer=''; } return; }
335
+ buffer+=decoder.decode(res.value,{stream:true});
336
+ var events=buffer.split('\\n\\n');
337
+ buffer=events.pop()||'';
338
+ for(var i=0;i<events.length;i++){ handleEvent(events[i], h); }
339
+ return pump();
340
+ });
341
+ }
342
+ return pump();
343
+ }
344
+ function parseEvents(blob, h){ blob.split('\\n\\n').forEach(function(ev){ handleEvent(ev, h); }); }
345
+ function handleEvent(raw, h){
346
+ var lines=raw.split('\\n');
347
+ var dataLine=null;
348
+ for(var j=0;j<lines.length;j++){ var t=lines[j].trim(); if(t.indexOf('data:')===0){ dataLine=t; break; } }
349
+ if(!dataLine) return;
350
+ var payload=dataLine.slice(5).trim();
351
+ if(payload==='[DONE]' || !payload) return;
352
+ var chunk; try{ chunk=JSON.parse(payload); }catch(e){ return; }
353
+ if(!chunk || typeof chunk!=='object') return;
354
+ if(chunk.type==='text-delta' && typeof chunk.delta==='string'){ h.onDelta(chunk.delta); }
355
+ else if(chunk.type==='status'){ var s=(typeof chunk.status==='string')?chunk.status:((typeof chunk.message==='string')?chunk.message:null); if(s){ h.onStatus(s); } }
356
+ else if(chunk.type==='tool-call' && typeof chunk.toolName==='string'){ h.onToolCall(chunk.toolName); }
357
+ else if(chunk.type==='error'){ h.onError((typeof chunk.errorText==='string')?chunk.errorText:((chunk.error && chunk.error.message)||'Unable to read assistant response.')); }
336
358
  }
337
359
 
338
- function sendMessage(){
339
- if(!input) return;
340
- var text=input.value.trim();
360
+ function sendMessage(text){
361
+ if(busy) return;
362
+ text=(text||'').trim();
341
363
  if(!text) return;
364
+
365
+ history.push({role:'user', text:text});
342
366
  appendUser(text);
343
- input.value=''; autosize(); toBottom();
344
- var bub=appendAssistant(); toBottom();
345
- var line=REPLIES[replyTurn % REPLIES.length]; replyTurn++;
346
- setTimeout(function(){
347
- if(bub){ bub.innerHTML='<p class="rk-chat-p"></p>'; bub.firstChild.textContent=line; toBottom(); }
348
- }, 850);
349
- }
367
+ if(input){ input.value=''; autosize(); }
368
+ if(title && history.length===1){ title.textContent=text.slice(0,60); }
369
+ toBottom();
350
370
 
351
- if(input){ input.addEventListener('input',autosize); autosize(); }
352
- if(send){ send.addEventListener('click',sendMessage); }
353
- if(input){
354
- input.addEventListener('keydown',function(ev){
355
- if(ev.key==='Enter' && !ev.shiftKey){ ev.preventDefault(); sendMessage(); }
371
+ var view=appendAssistant();
372
+ toBottom();
373
+ setBusy(true);
374
+
375
+ if(!threadId){ threadId=genId('thread'); }
376
+ var userMsgId=genId('user');
377
+ var asstMsgId=genId('assistant');
378
+
379
+ var body={
380
+ messages: history.map(function(m){ return { role:m.role, content:[{ type:'text', text:m.text }] }; }),
381
+ thread_id: threadId,
382
+ thread_template_id: (TEMPLATE && TEMPLATE.templateId) || 'chat-brainstorm',
383
+ thread_title: text.slice(0,42),
384
+ thread_tracers: (TEMPLATE && TEMPLATE.tracers) || [],
385
+ client_context: clientContext(),
386
+ user_message: { id:userMsgId, role:'user', text:text, attachments:[] },
387
+ assistant_message: { id:asstMsgId }
388
+ };
389
+ if(TEMPLATE){ body.template=TEMPLATE; }
390
+ if(CACHED){ body.cached_context=CACHED; }
391
+
392
+ var headers={ 'content-type':'application/json' };
393
+ if(API_KEY){ headers['vidfarm-api-key']=API_KEY; }
394
+
395
+ var acc='';
396
+ var sawText=false;
397
+ var errored=false;
398
+
399
+ var handlers={
400
+ onDelta:function(d){ if(!sawText){ sawText=true; view.showText(); } acc+=d; view.setText(acc); toBottom(); },
401
+ onStatus:function(s){ if(!sawText){ view.setStatus(s); } },
402
+ onToolCall:function(name){ view.addNote('Running '+name+'…'); toBottom(); },
403
+ onError:function(m){ errored=true; view.fail(m); toBottom(); }
404
+ };
405
+
406
+ var req;
407
+ try{
408
+ req=fetch(ENDPOINT,{ method:'POST', headers:headers, credentials:'same-origin', body:JSON.stringify(body) });
409
+ }catch(err){
410
+ // Synchronous fetch failure (e.g. invalid URL) — never let it throw uncaught.
411
+ req=Promise.reject(err);
412
+ }
413
+
414
+ req.then(function(resp){
415
+ if(!resp.ok){
416
+ return resp.json().catch(function(){ return {}; }).then(function(j){
417
+ throw new Error((j && j.error) || ('Chat request failed ('+resp.status+').'));
418
+ });
419
+ }
420
+ return readStream(resp, handlers);
421
+ }).then(function(){
422
+ view.hideStatus();
423
+ if(sawText){
424
+ history.push({role:'assistant', text:acc});
425
+ } else if(!errored){
426
+ view.showText();
427
+ view.setText('The assistant finished without returning any text.');
428
+ }
429
+ setBusy(false);
430
+ if(input){ input.focus(); }
431
+ toBottom();
432
+ loadHistory();
433
+ }).catch(function(err){
434
+ var msg=(err && err.message) ? err.message : String(err);
435
+ if(!API_KEY){ msg=msg+'\\n\\nAdd an AI provider key in Settings to brainstorm on your own keys.'; }
436
+ view.fail(msg);
437
+ setBusy(false);
438
+ if(input){ input.focus(); }
439
+ toBottom();
356
440
  });
357
441
  }
358
442
 
359
- // starter chips fill + focus the composer
360
- root.querySelectorAll('[data-rk-prompt]').forEach(function(chip){
361
- chip.addEventListener('click',function(){
362
- if(!input)return;
363
- input.value=chip.getAttribute('data-rk-prompt')+': ';
364
- autosize(); input.focus();
365
- try{ input.setSelectionRange(input.value.length,input.value.length); }catch(e){}
366
- });
367
- });
443
+ // ── real chat history (editor-chat threads) ────────────────────────────
444
+ var THREADS_URL=(boot && boot.threadsUrl) || '/api/v1/editor-chat/threads';
445
+ var TEMPLATE_ID=(TEMPLATE && TEMPLATE.templateId) || 'chat-brainstorm';
446
+ var histList=root.getElementById('rk-chat-history-list');
447
+ var histAside=root.getElementById('rk-chat-history');
448
+ var histBackdrop=root.getElementById('rk-chat-histbackdrop');
449
+ var histToggle=root.getElementById('rk-chat-histtoggle');
450
+ var loadingThread=false;
368
451
 
369
- // "New" resets the thread to a warm empty state
370
- root.getElementById('rk-chat-new') && root.getElementById('rk-chat-new').addEventListener('click',function(){
452
+ function authHeaders(extra){ var h=extra||{}; if(API_KEY){ h['vidfarm-api-key']=API_KEY; } return h; }
453
+ function relTime(iso){
454
+ if(!iso) return '';
455
+ var t=Date.parse(iso); if(isNaN(t)) return '';
456
+ var s=Math.max(0,(Date.now()-t)/1000);
457
+ if(s<60) return 'just now';
458
+ if(s<3600) return Math.floor(s/60)+'m ago';
459
+ if(s<86400) return Math.floor(s/3600)+'h ago';
460
+ if(s<604800) return Math.floor(s/86400)+'d ago';
461
+ try{ return new Date(t).toLocaleDateString([], {month:'short', day:'numeric'}); }catch(e){ return ''; }
462
+ }
463
+ function histNote(text, opts){
464
+ var wrap=el('rk-chat-history-note');
465
+ var p=root.createElement('div'); p.textContent=text; wrap.appendChild(p);
466
+ if(opts && opts.href){ var a=root.createElement('a'); a.href=opts.href; a.textContent=opts.linkText||'Open'; wrap.appendChild(a); }
467
+ if(opts && opts.retry){ var b=root.createElement('button'); b.type='button'; b.textContent='Try again'; b.addEventListener('click',function(){ loadHistory(); }); wrap.appendChild(b); }
468
+ return wrap;
469
+ }
470
+ function setActiveThread(id){
471
+ threadId=id;
472
+ if(!histList) return;
473
+ var rows=histList.querySelectorAll('.rk-chat-hrow');
474
+ for(var i=0;i<rows.length;i++){ rows[i].classList.toggle('is-active', rows[i].getAttribute('data-id')===id); }
475
+ }
476
+ function renderHistory(threads){
477
+ if(!histList) return;
478
+ histList.innerHTML='';
479
+ if(!threads.length){ histList.appendChild(histNote('No conversations yet. Start one below \\u2014 it saves automatically.')); return; }
480
+ threads.forEach(function(t){
481
+ var row=root.createElement('button');
482
+ row.type='button'; row.className='rk-chat-hrow'; row.setAttribute('data-id', t.id);
483
+ if(t.id===threadId){ row.classList.add('is-active'); }
484
+ var main=el('rk-chat-hrow-main');
485
+ var ti=el('rk-chat-hrow-title'); ti.textContent=t.title || 'Untitled chat';
486
+ var sub=el('rk-chat-hrow-sub');
487
+ var count=(typeof t.messageCount==='number')?t.messageCount:0;
488
+ var when=relTime(t.lastMessageAt || t.updatedAt);
489
+ sub.textContent=(count?count+' message'+(count===1?'':'s'):'')+((count&&when)?' \\u00b7 ':'')+(when||'');
490
+ main.appendChild(ti); main.appendChild(sub);
491
+ var del=root.createElement('button');
492
+ del.type='button'; del.className='rk-chat-hrow-del'; del.setAttribute('aria-label','Delete chat'); del.textContent='\\u00d7';
493
+ del.addEventListener('click', function(ev){ ev.stopPropagation(); deleteThread(t.id, row); });
494
+ row.appendChild(main); row.appendChild(del);
495
+ row.addEventListener('click', function(){ openThread(t.id); });
496
+ histList.appendChild(row);
497
+ });
498
+ }
499
+ function loadHistory(){
500
+ if(!histList) return;
501
+ if(!API_KEY){ histList.innerHTML=''; histList.appendChild(histNote('Add an AI provider key in Settings to sync your saved chats.', {href:'/settings', linkText:'Open Settings'})); return; }
502
+ var url=THREADS_URL+'?template_id='+encodeURIComponent(TEMPLATE_ID)+'&limit=30&include_messages=false';
503
+ fetch(url, { headers:authHeaders(), credentials:'same-origin' })
504
+ .then(function(r){
505
+ if(r.status===401||r.status===403){ return {threads:[], _auth:true}; }
506
+ if(!r.ok) throw new Error('http '+r.status);
507
+ return r.json();
508
+ })
509
+ .then(function(j){
510
+ if(j && j._auth){ histList.innerHTML=''; histList.appendChild(histNote('Sign in to sync your saved chats.', {href:'/login', linkText:'Sign in'})); return; }
511
+ renderHistory((j && j.threads) || []);
512
+ })
513
+ .catch(function(){ if(histList){ histList.innerHTML=''; histList.appendChild(histNote('Couldn\\u2019t load your chats.', {retry:true})); } });
514
+ }
515
+ function appendAssistantStatic(text){ var v=appendAssistant(); v.showText(); v.setText(text||''); v.hideStatus(); return v; }
516
+ function openThread(id){
517
+ if(busy || loadingThread) return;
518
+ loadingThread=true;
519
+ closeHistoryOverlay();
520
+ fetch(THREADS_URL+'/'+encodeURIComponent(id), { headers:authHeaders(), credentials:'same-origin' })
521
+ .then(function(r){ if(!r.ok) throw new Error('http '+r.status); return r.json(); })
522
+ .then(function(j){
523
+ var t=j && j.thread; if(!t){ throw new Error('missing thread'); }
524
+ history=[];
525
+ if(thread){ thread.innerHTML=''; }
526
+ var msgs=t.messages||[];
527
+ for(var i=0;i<msgs.length;i++){
528
+ var m=msgs[i]; var role=(m.role==='assistant')?'assistant':'user'; var txt=m.text||'';
529
+ history.push({role:role, text:txt});
530
+ if(role==='user'){ appendUser(txt); } else { appendAssistantStatic(txt); }
531
+ }
532
+ if(title){ title.textContent=t.title || 'Brainstorm studio'; }
533
+ setActiveThread(id);
534
+ loadingThread=false;
535
+ toBottom();
536
+ })
537
+ .catch(function(){ loadingThread=false; });
538
+ }
539
+ function deleteThread(id, row){
540
+ if(!API_KEY) return;
541
+ fetch(THREADS_URL+'/'+encodeURIComponent(id), { method:'DELETE', headers:authHeaders(), credentials:'same-origin' })
542
+ .then(function(r){
543
+ if(!r.ok && r.status!==404) throw new Error('http '+r.status);
544
+ if(row && row.parentNode){ row.parentNode.removeChild(row); }
545
+ if(id===threadId){ resetChat(); }
546
+ if(histList && !histList.querySelector('.rk-chat-hrow')){ loadHistory(); }
547
+ })
548
+ .catch(function(){});
549
+ }
550
+ function resetChat(){
551
+ history=[]; threadId=null;
371
552
  if(thread){
372
553
  thread.innerHTML='<div class="rk-chat-msg is-assistant">'+
373
- '<div class="rk-chat-meta"><span class="rk-chat-avatar" aria-hidden="true">'+AVATAR+'</span>Copilot <span class="rk-chat-time">'+nowLabel()+'</span></div>'+
374
- '<div class="rk-chat-bubble rk-chat-bubble-assistant"><p class="rk-chat-p">Fresh page. Tell me the offer, who it\\u2019s for, and where the traffic is cold \\u2014 I\\u2019ll come back with angles, hooks, and a template you can render.</p></div>'+
554
+ '<div class="rk-chat-meta"><span class="rk-chat-avatar" aria-hidden="true">'+AVATAR+'</span>Copilot</div>'+
555
+ '<div class="rk-chat-bubble rk-chat-bubble-assistant"><div class="rk-chat-stream">Fresh page. Tell me the offer, who it\\u2019s for, and where the traffic is cold \\u2014 I\\u2019ll come back with angles, hooks, and scripts you can render.</div></div>'+
375
556
  '</div>';
376
557
  }
377
- if(title) title.textContent='New brainstorm';
378
- if(input){ input.focus(); }
558
+ if(title){ title.textContent='Brainstorm studio'; }
559
+ setActiveThread(null);
560
+ if(input){ input.value=''; autosize(); input.focus(); }
379
561
  window.scrollTo(0,0);
380
- });
562
+ }
563
+ function openHistoryOverlay(){ if(histAside){ histAside.classList.add('is-open'); } if(histBackdrop){ histBackdrop.hidden=false; } }
564
+ function closeHistoryOverlay(){ if(histAside){ histAside.classList.remove('is-open'); } if(histBackdrop){ histBackdrop.hidden=true; } }
565
+ if(histToggle){ histToggle.addEventListener('click', openHistoryOverlay); }
566
+ if(histBackdrop){ histBackdrop.addEventListener('click', closeHistoryOverlay); }
567
+ loadHistory();
381
568
 
382
- // unobtrusive history popover
383
- var histBtn=root.getElementById('rk-chat-history-btn');
384
- var histMenu=root.getElementById('rk-chat-history-menu');
385
- function closeHist(){ if(histMenu){ histMenu.hidden=true; } if(histBtn){ histBtn.setAttribute('aria-expanded','false'); } }
386
- if(histBtn && histMenu){
387
- histBtn.addEventListener('click',function(ev){
388
- ev.stopPropagation();
389
- var open=histMenu.hidden;
390
- histMenu.hidden=!open; histBtn.setAttribute('aria-expanded', open?'true':'false');
391
- });
392
- histMenu.querySelectorAll('[data-rk-title]').forEach(function(it){
393
- it.addEventListener('click',function(){
394
- histMenu.querySelectorAll('[data-rk-title]').forEach(function(x){ x.classList.toggle('is-active',x===it); });
395
- if(title){ var t=it.getAttribute('data-rk-title'); if(t) title.textContent=t; }
396
- closeHist();
397
- });
569
+ // composer wiring
570
+ if(input){ input.addEventListener('input',autosize); autosize(); }
571
+ if(send){ send.addEventListener('click',function(){ if(input){ sendMessage(input.value); } }); }
572
+ if(input){
573
+ input.addEventListener('keydown',function(ev){
574
+ if(ev.key==='Enter' && !ev.shiftKey){ ev.preventDefault(); sendMessage(input.value); }
398
575
  });
399
- root.addEventListener('click',function(ev){
400
- if(histMenu.hidden) return;
401
- if(!histMenu.contains(ev.target) && ev.target!==histBtn) closeHist();
576
+ }
577
+
578
+ // starter chips send their full prompt directly
579
+ root.querySelectorAll('[data-rk-prompt]').forEach(function(chip){
580
+ chip.addEventListener('click',function(){ if(busy) return; sendMessage(chip.getAttribute('data-rk-prompt')||''); });
581
+ });
582
+
583
+ // "New" resets the conversation to the warm empty state (shared resetChat)
584
+ var newBtn=root.getElementById('rk-chat-new');
585
+ if(newBtn){
586
+ newBtn.addEventListener('click',function(){ if(busy) return; resetChat(); });
587
+ }
588
+
589
+ // studio mode: choosing "Create Image" navigates to the inpaint workbench
590
+ var modeSel=root.getElementById('rk-chat-mode');
591
+ if(modeSel){
592
+ modeSel.addEventListener('change',function(){
593
+ var dest=modeSel.value;
594
+ if(dest && dest!=='/chat'){ window.location.href=dest; }
402
595
  });
403
596
  }
404
597
  })();`;
405
598
  return reskinDocument({
406
599
  title: "vidfarm reskin — Brainstorm",
407
- description: "Reskinned vidfarm AI brainstorm chat — a clean, calm single-column conversation with a compact header, starter chips, and a quiet composer.",
600
+ description: "Reskinned vidfarm AI brainstorm chat, wired to the live editor-chat backend — a clean single-column conversation with starter chips and a working composer.",
408
601
  activeSlug: "chat",
409
602
  pageCss,
410
603
  body,