@mevdragon/vidfarm-devcli 0.18.0 → 0.18.1

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.
Files changed (53) hide show
  1. package/.agents/skills/vidfarm-media/SKILL.md +41 -4
  2. package/SKILL.director.md +17 -3
  3. package/SKILL.platform.md +3 -3
  4. package/demo/dist/app.js +69 -69
  5. package/demo/dist/favicon.ico +0 -0
  6. package/dist/src/app.js +1550 -187
  7. package/dist/src/cli.js +227 -7
  8. package/dist/src/devcli/clip-store.js +29 -2
  9. package/dist/src/devcli/clips.js +52 -9
  10. package/dist/src/devcli/composition-edit.js +262 -0
  11. package/dist/src/devcli/timeline-edit.js +283 -0
  12. package/dist/src/editor-chat.js +29 -6
  13. package/dist/src/frontend/discover-client.js +130 -0
  14. package/dist/src/frontend/discover-store.js +23 -0
  15. package/dist/src/frontend/file-directory.js +744 -0
  16. package/dist/src/frontend/template-editor-chat.js +22 -19
  17. package/dist/src/landing-page.js +24 -7
  18. package/dist/src/page-shell.js +25 -1
  19. package/dist/src/reskin/agency-page.js +1 -1
  20. package/dist/src/reskin/calendar-page.js +2 -1
  21. package/dist/src/reskin/chat-page.js +319 -72
  22. package/dist/src/reskin/discover-page.js +486 -31
  23. package/dist/src/reskin/document.js +1171 -384
  24. package/dist/src/reskin/help-page.js +1 -0
  25. package/dist/src/reskin/inpaint-page.js +10 -4
  26. package/dist/src/reskin/library-page.js +918 -220
  27. package/dist/src/reskin/login-page.js +6 -6
  28. package/dist/src/reskin/pricing-page.js +2 -0
  29. package/dist/src/reskin/settings-page.js +55 -10
  30. package/dist/src/reskin/theme.js +337 -17
  31. package/dist/src/services/clip-curation/gemini.js +5 -0
  32. package/dist/src/services/clip-curation/hunt.js +79 -1
  33. package/dist/src/services/clip-curation/index.js +2 -1
  34. package/dist/src/services/clip-curation/local-agent.js +4 -3
  35. package/dist/src/services/clip-curation/media-select.js +85 -0
  36. package/dist/src/services/clip-curation/query.js +5 -1
  37. package/dist/src/services/clip-curation/refine.js +50 -20
  38. package/dist/src/services/clip-curation/scan.js +10 -3
  39. package/dist/src/services/clip-curation/taxonomy.js +3 -1
  40. package/dist/src/services/clip-curation/taxonomy.v1.json +13 -1
  41. package/dist/src/services/clip-records.js +14 -1
  42. package/dist/src/services/clip-search.js +43 -13
  43. package/dist/src/services/file-directory.js +114 -0
  44. package/dist/src/services/storage.js +24 -1
  45. package/dist/src/services/upstream.js +5 -5
  46. package/dist/src/template-editor-shell.js +16 -2
  47. package/package.json +1 -1
  48. package/public/assets/discover-client-app.js +1 -0
  49. package/public/assets/file-directory-app.js +2 -0
  50. package/public/assets/homepage-client-app.js +12 -12
  51. package/public/assets/page-runtime-client-app.js +24 -24
  52. package/src/assets/favicon.ico +0 -0
  53. package/src/assets/logo-vidfarm.png +0 -0
@@ -28,22 +28,17 @@ const DEFAULT_INPUT = {
28
28
  };
29
29
  // ── icons ───────────────────────────────────────────────────────────────────
30
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>`;
31
- // Full-sentence starter prompts clicking one sends it straight to the agent.
32
- const STARTERS = [
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."
37
- ];
38
- export function renderReskinChat(input = DEFAULT_INPUT) {
31
+ const STOP = `<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><rect x="6" y="6" width="12" height="12" rx="2.5"/></svg>`;
32
+ const CLIP = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48"/></svg>`;
33
+ export function renderReskinChat(input = DEFAULT_INPUT, opts = {}) {
39
34
  const boot = input.editorChat ?? null;
35
+ const initialThreadId = opts.initialThreadId ?? null;
40
36
  // Embed the real boot as JSON. Escaping only `<` is the correct (and
41
37
  // sufficient) way to keep a `<script type="application/json">` block from
42
38
  // breaking out; HTML-entity escaping would corrupt JSON.parse.
43
39
  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("");
45
40
  const body = `
46
- <main class="rk-chat-page">
41
+ <main class="rk-chat-page"${initialThreadId ? ` data-initial-thread="${rkEscape(initialThreadId)}"` : ""}>
47
42
  <aside class="rk-chat-history" id="rk-chat-history" aria-label="Chat history">
48
43
  <div class="rk-chat-history-head">
49
44
  <span class="rk-chat-history-title">Chats</span>
@@ -57,14 +52,12 @@ export function renderReskinChat(input = DEFAULT_INPUT) {
57
52
  <header class="rk-chat-head">
58
53
  <button type="button" class="rk-chat-histtoggle" id="rk-chat-histtoggle" aria-label="Show chat history">☰</button>
59
54
  <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>
55
+ <h1 class="rk-chat-title" id="rk-chat-title">Chat with Agent</h1>
63
56
  </div>
64
57
  <div class="rk-chat-head-actions">
65
58
  <label class="rk-chat-mode" aria-label="Studio mode">
66
59
  <select class="rk-chat-mode-select" id="rk-chat-mode">
67
- <option value="/chat" selected>Brainstorm</option>
60
+ <option value="/chat" selected>Chat</option>
68
61
  <option value="/inpaint">Create Image</option>
69
62
  </select>
70
63
  <span class="rk-chat-mode-chev" aria-hidden="true">&#9662;</span>
@@ -72,25 +65,19 @@ export function renderReskinChat(input = DEFAULT_INPUT) {
72
65
  </div>
73
66
  </header>
74
67
 
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>
80
- </div>
81
- </div>
82
- </div>
68
+ <div class="rk-chat-thread" id="rk-chat-thread"></div>
83
69
 
84
70
  <div class="rk-chat-dock">
85
- <div class="rk-chat-starters" aria-label="Starter prompts">
86
- ${starterChips}
87
- </div>
71
+ <div class="rk-chat-chips" id="rk-chat-chips" aria-label="Attached files"></div>
88
72
  <div class="rk-chat-composer" id="rk-chat-composer">
89
- <textarea id="rk-chat-input" class="rk-chat-input" rows="1"
73
+ <textarea id="rk-chat-input" class="rk-chat-input" rows="3"
90
74
  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>
75
+ <div class="rk-chat-composer-actions">
76
+ <button type="button" class="rk-chat-attach" id="rk-chat-attach" aria-label="Attach a file" title="Attach a file">${CLIP}</button>
77
+ <button type="button" class="rk-btn rk-btn-gold rk-chat-send" id="rk-chat-send">Send ${SEND}</button>
78
+ </div>
92
79
  </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>
80
+ <p class="rk-chat-hint"><kbd>Enter</kbd> to send · <kbd>Shift</kbd>+<kbd>Enter</kbd> for a new line</p>
94
81
  </div>
95
82
  </div>
96
83
  </div>
@@ -126,6 +113,8 @@ export function renderReskinChat(input = DEFAULT_INPUT) {
126
113
  line-height:1;padding:2px 5px;border-radius:var(--rk-r-sm);opacity:0;
127
114
  transition:opacity var(--rk-dur) var(--rk-ease),color var(--rk-dur) var(--rk-ease),background var(--rk-dur) var(--rk-ease)}
128
115
  .rk-chat-hrow:hover .rk-chat-hrow-del{opacity:1}
116
+ /* touch: the history panel is the primary mobile surface — keep delete visible */
117
+ @media(hover:none){.rk-chat-hrow-del{opacity:1}}
129
118
  .rk-chat-hrow-del:hover{color:var(--rk-red);background:var(--rk-red-tint)}
130
119
  .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
120
  .rk-chat-history-note a{color:var(--rk-gold-700);font-weight:600}
@@ -139,13 +128,12 @@ export function renderReskinChat(input = DEFAULT_INPUT) {
139
128
  align-items:center;justify-content:center;margin-right:2px;margin-top:2px}
140
129
  .rk-chat-histbackdrop{display:none}
141
130
 
142
- /* compact header */
131
+ /* compact header — sticky so the title stays pinned while the thread scrolls */
143
132
  .rk-chat-head{display:flex;align-items:flex-start;gap:12px;
144
- padding-bottom:22px;margin-bottom:6px;border-bottom:1px solid var(--rk-border)}
145
- .rk-chat-head-titles{display:grid;gap:6px;min-width:0;flex:1}
146
- .rk-chat-eyebrow{display:inline-flex;align-items:center;font-size:12px;font-weight:700;
147
- letter-spacing:.06em;text-transform:uppercase;color:var(--rk-gold-700)}
148
- .rk-chat-title{font-family:var(--rk-font-display);font-weight:800;font-size:1.7rem;letter-spacing:-.025em;
133
+ position:sticky;top:0;z-index:20;background:var(--rk-bg);
134
+ padding-top:18px;margin-top:-18px;padding-bottom:18px;margin-bottom:6px;border-bottom:1px solid var(--rk-border)}
135
+ .rk-chat-head-titles{display:grid;gap:5px;min-width:0;flex:1}
136
+ .rk-chat-title{font-family:var(--rk-font-display);font-weight:800;font-size:1.6rem;letter-spacing:-.025em;
149
137
  line-height:1.15;color:var(--rk-ink);min-width:0}
150
138
  .rk-chat-subhead{font-size:14.5px;color:var(--rk-text-muted);line-height:1.5;max-width:56ch}
151
139
  .rk-chat-head-actions{display:flex;align-items:center;gap:9px;flex:none}
@@ -172,16 +160,23 @@ export function renderReskinChat(input = DEFAULT_INPUT) {
172
160
  font-family:var(--rk-font-display);font-weight:800;font-size:11px;line-height:1;
173
161
  background:linear-gradient(142deg,var(--rk-gold-500),var(--rk-violet))}
174
162
 
175
- /* bubbles — uniform 1px hairline on all four sides, no colored side stripes */
176
- .rk-chat-bubble{border-radius:var(--rk-r-2xl);padding:16px 20px;font-size:15px;line-height:1.65}
163
+ /* bubbles — uniform 1px hairline on all four sides, no colored side stripes.
164
+ overflow-wrap keeps a long pasted URL/token in a USER bubble from forcing
165
+ horizontal page scroll on phones (assistant text already wraps via -stream). */
166
+ .rk-chat-bubble{border-radius:var(--rk-r-2xl);padding:16px 20px;font-size:15px;line-height:1.65;overflow-wrap:anywhere}
177
167
  .rk-chat-bubble-user{max-width:min(78%,560px);background:var(--rk-ink);color:#f4f4f5;
178
168
  border:1px solid var(--rk-n-800);box-shadow:var(--rk-shadow-sm)}
179
169
  .rk-chat-bubble-assistant{width:100%;background:var(--rk-surface);color:var(--rk-n-700);
180
170
  border:1px solid var(--rk-border);box-shadow:var(--rk-shadow-card)}
181
171
  .rk-chat-bubble-assistant strong{color:var(--rk-ink);font-weight:700}
172
+ .rk-chat-bubble-assistant em{font-style:italic}
173
+ .rk-chat-bubble-assistant a{color:var(--rk-gold-700);font-weight:600;text-decoration:underline;text-underline-offset:2px;overflow-wrap:anywhere}
182
174
 
183
175
  /* streamed assistant text — pre-wrap keeps the model's own line breaks */
184
176
  .rk-chat-stream{white-space:pre-wrap;overflow-wrap:anywhere;color:var(--rk-n-700);line-height:1.65}
177
+ .rk-chat-stream a{color:var(--rk-gold-700);font-weight:600;text-decoration:underline;text-underline-offset:2px;overflow-wrap:anywhere}
178
+ .rk-chat-stream strong{color:var(--rk-ink);font-weight:700}
179
+ .rk-chat-stream em{font-style:italic}
185
180
  .rk-chat-stream[hidden]{display:none}
186
181
 
187
182
  /* thinking / status row */
@@ -222,12 +217,46 @@ export function renderReskinChat(input = DEFAULT_INPUT) {
222
217
  .rk-chat-chip:hover{border-color:var(--rk-gold-600);background:var(--rk-gold-tint);color:var(--rk-ink);
223
218
  transform:translateY(-1px);box-shadow:var(--rk-shadow-sm)}
224
219
  .rk-chat-chip:disabled{opacity:.5;cursor:default;transform:none;box-shadow:var(--rk-shadow-xs)}
225
- .rk-chat-composer{display:flex;align-items:flex-end;gap:9px;background:var(--rk-surface);
226
- border:1px solid var(--rk-border-strong);border-radius:var(--rk-r-3xl);padding:9px 9px 9px 16px;
220
+
221
+ /* empty new-chat state no greeting, just a few subtle suggestions */
222
+ .rk-chat-empty{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:14px;
223
+ min-height:min(46vh,360px);text-align:center;padding:24px 0}
224
+ .rk-chat-empty-hint{font-size:13px;font-weight:600;letter-spacing:.02em;text-transform:uppercase;color:var(--rk-text-faint)}
225
+ .rk-chat-suggests{display:flex;flex-direction:column;gap:8px;width:min(420px,100%)}
226
+ .rk-chat-suggest{display:block;width:100%;text-align:left;padding:12px 15px;border-radius:var(--rk-r-xl);
227
+ border:1px solid var(--rk-border);background:var(--rk-surface);color:var(--rk-n-600);cursor:pointer;
228
+ font-family:var(--rk-font-body);font-size:13.5px;font-weight:500;line-height:1.4;box-shadow:var(--rk-shadow-xs);
229
+ 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),color var(--rk-dur) var(--rk-ease)}
230
+ .rk-chat-suggest:hover{border-color:var(--rk-gold-600);background:var(--rk-gold-tint);color:var(--rk-ink);
231
+ transform:translateY(-1px);box-shadow:var(--rk-shadow-sm)}
232
+ /* composer = one bordered "text area" box: textarea fills the top, the attach +
233
+ send controls sit inside it along the bottom row */
234
+ .rk-chat-composer{display:flex;flex-direction:column;gap:9px;background:var(--rk-surface);
235
+ border:1px solid var(--rk-border-strong);border-radius:var(--rk-r-2xl);padding:11px 12px 10px;
227
236
  box-shadow:var(--rk-shadow-card);transition:border-color var(--rk-dur) var(--rk-ease),box-shadow var(--rk-dur) var(--rk-ease)}
228
237
  .rk-chat-composer:focus-within{border-color:var(--rk-gold-600);box-shadow:var(--rk-ring-gold)}
229
- .rk-chat-input{flex:1;min-width:0;border:0;background:transparent;resize:none;font-family:var(--rk-font-body);
230
- font-size:15px;line-height:1.5;color:var(--rk-ink);padding:10px 2px;max-height:148px}
238
+ .rk-chat-composer-actions{display:flex;align-items:center;gap:9px}
239
+ .rk-chat-composer-actions .rk-chat-send{margin-left:auto}
240
+
241
+ /* attach button — opens the file-explorer drawer (chrome script wires it) */
242
+ .rk-chat-attach{flex:none;width:38px;height:38px;border-radius:var(--rk-r-full);border:1px solid var(--rk-border);
243
+ background:#fff;color:var(--rk-n-500);cursor:pointer;display:grid;place-items:center;
244
+ transition:background var(--rk-dur) var(--rk-ease),border-color var(--rk-dur) var(--rk-ease),color var(--rk-dur) var(--rk-ease)}
245
+ .rk-chat-attach:hover{border-color:var(--rk-gold-600);background:var(--rk-gold-tint);color:var(--rk-ink)}
246
+ .rk-chat-attach svg{width:18px;height:18px}
247
+
248
+ /* attached-file chips (populated when a file is picked from the drawer) */
249
+ .rk-chat-chips{display:flex;flex-wrap:wrap;gap:7px}
250
+ .rk-chat-chips:empty{display:none}
251
+ .rk-chat-attach-chip{display:inline-flex;align-items:center;gap:5px;padding:5px 8px 5px 12px;border-radius:var(--rk-r-full);
252
+ background:var(--rk-gold-tint2);color:var(--rk-gold-700);font-size:12.5px;font-weight:600;max-width:230px}
253
+ .rk-chat-attach-chip span{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
254
+ .rk-chat-attach-chip button{border:0;background:transparent;color:inherit;cursor:pointer;font-size:15px;line-height:1;padding:0 2px}
255
+ .rk-chat-attach-chip .rk-chat-attach-preview{font-family:inherit;font-size:12.5px;font-weight:600;padding:0;max-width:200px;
256
+ white-space:nowrap;overflow:hidden;text-overflow:ellipsis;text-decoration:underline;text-underline-offset:2px;text-decoration-color:transparent}
257
+ .rk-chat-attach-chip .rk-chat-attach-preview:hover{text-decoration-color:currentColor}
258
+ .rk-chat-input{width:100%;min-width:0;min-height:76px;border:0;background:transparent;resize:none;font-family:var(--rk-font-body);
259
+ font-size:15px;line-height:1.5;color:var(--rk-ink);padding:4px 4px 0;max-height:210px}
231
260
  .rk-chat-input:focus{outline:none;box-shadow:none}
232
261
  .rk-chat-input::placeholder{color:var(--rk-n-400)}
233
262
  .rk-chat-send{flex:none;padding:11px 18px}
@@ -267,6 +296,8 @@ export function renderReskinChat(input = DEFAULT_INPUT) {
267
296
 
268
297
  var input=root.getElementById('rk-chat-input');
269
298
  var send=root.getElementById('rk-chat-send');
299
+ var SEND_HTML=${JSON.stringify('Send ' + SEND)};
300
+ var STOP_HTML=${JSON.stringify('Stop ' + STOP)};
270
301
  var thread=root.getElementById('rk-chat-thread');
271
302
  var title=root.getElementById('rk-chat-title');
272
303
  var AVATAR='V';
@@ -274,13 +305,174 @@ export function renderReskinChat(input = DEFAULT_INPUT) {
274
305
  var history=[]; // [{role, text}] — the running conversation
275
306
  var threadId=null; // generated on first send, reused for persistence
276
307
  var busy=false;
308
+ var pendingAbort=null; // AbortController for the in-flight reply (Stop button)
309
+ var attachments=[]; // files picked from the file-explorer drawer
310
+ var chipsEl=root.getElementById('rk-chat-chips');
311
+
312
+ // Render the pending-attachment chips above the composer.
313
+ function renderAttachChips(){
314
+ if(!chipsEl) return;
315
+ chipsEl.innerHTML='';
316
+ attachments.forEach(function(f, i){
317
+ var chip=el('rk-chat-attach-chip');
318
+ var label=root.createElement('button'); label.type='button'; label.className='rk-chat-attach-preview';
319
+ label.title='Preview '+f.name; label.textContent=f.name;
320
+ if(f.viewUrl){ label.addEventListener('click',function(){ openFilePreview(attachments, i); }); }
321
+ chip.appendChild(label);
322
+ var x=root.createElement('button'); x.type='button'; x.setAttribute('aria-label','Remove '+f.name); x.textContent='\\u00d7';
323
+ x.addEventListener('click',function(){ attachments=attachments.filter(function(a){ return a.id!==f.id; }); renderAttachChips(); });
324
+ chip.appendChild(x); chipsEl.appendChild(chip);
325
+ });
326
+ }
327
+ // The file-explorer drawer (chrome script) hands picked files over via a DOM
328
+ // event — it lives in a separate script, so a CustomEvent is the bridge.
329
+ document.addEventListener('rk-chat-attach-file',function(ev){
330
+ var d=(ev && ev.detail)||{};
331
+ if(!d.id || !d.viewUrl) return;
332
+ if(attachments.some(function(a){ return a.id===String(d.id); })) return;
333
+ attachments.push({ id:String(d.id), name:d.name||'file', contentType:d.contentType||'application/octet-stream', viewUrl:d.viewUrl });
334
+ renderAttachChips();
335
+ if(input) input.focus();
336
+ });
277
337
 
338
+ // Each conversation has its own URL (/chat/<id>); a fresh page is /chat.
339
+ var pageEl=root.querySelector('.rk-chat-page');
340
+ var INITIAL_THREAD=(pageEl && pageEl.getAttribute('data-initial-thread')) || null;
341
+ function chatScopePrefix(){ var m=(location.pathname||'').match(/^(\\/u\\/[^/]+)\\/chat/); return m ? m[1] : ''; } // preserve account scope
342
+ function chatUrlFor(id){ return chatScopePrefix()+(id ? '/chat/'+encodeURIComponent(id) : '/chat'); }
343
+ function threadFromLocation(){ var m=(location.pathname||'').match(/\\/chat\\/(.+)$/); return m ? decodeURIComponent(m[1]) : null; }
344
+ function setChatUrl(id, replace){
345
+ // NB: the local "history" var is the message list — use window.history here.
346
+ var h=window.history; if(!h || (!h.pushState && !h.replaceState)) return;
347
+ var url=chatUrlFor(id);
348
+ try{ if(replace){ h.replaceState(null,'',url); } else { h.pushState(null,'',url); } }catch(e){}
349
+ }
278
350
  function genId(p){ return p+'-'+Date.now().toString(36)+'-'+Math.random().toString(36).slice(2,8); }
279
- function autosize(){ if(!input)return; input.style.height='auto'; input.style.height=Math.min(input.scrollHeight,148)+'px'; }
351
+ function autosize(){ if(!input)return; input.style.height='auto'; input.style.height=Math.min(input.scrollHeight,210)+'px'; }
280
352
  function toBottom(){ try{ window.scrollTo({top:document.body.scrollHeight,behavior:'smooth'}); }catch(e){ window.scrollTo(0,document.body.scrollHeight); } }
281
353
  function nowLabel(){ try{ return new Date().toLocaleTimeString([], {hour:'numeric', minute:'2-digit'}); }catch(e){ return ''; } }
282
354
  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; }); }
355
+ // While a reply streams, the Send button becomes an interruptive Stop (stays
356
+ // clickable so the user can abort); it flips back to Send when the reply ends.
357
+ function setBusy(v){ busy=v; if(send){ send.innerHTML=v?STOP_HTML:SEND_HTML; send.classList.toggle('is-stop',v); } root.querySelectorAll('[data-rk-prompt]').forEach(function(c){ c.disabled=v; }); }
358
+
359
+ // Inline markdown → HTML (escape first, then bold/italic/links). pre-wrap on
360
+ // the container keeps the model's own line breaks, so no block handling needed.
361
+ function escapeHtml(s){ return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); }
362
+ function mdToHtml(text){
363
+ var s=escapeHtml(text);
364
+ s=s.replace(/\\*\\*([^*]+)\\*\\*/g,'<strong>$1</strong>');
365
+ s=s.replace(/(^|[^\\w*])\\*([^*\\n]+)\\*/g,'$1<em>$2</em>');
366
+ s=s.replace(/(^|[^\\w_])_([^_\\n]+)_/g,'$1<em>$2</em>');
367
+ s=s.replace(/\\[([^\\]]+)\\]\\((https?:\\/\\/[^\\s)]+)\\)/g,'<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>');
368
+ s=s.replace(/(^|[\\s(])(https?:\\/\\/[^\\s<]+[^<.,:;"')\\]\\s])/g,'$1<a href="$2" target="_blank" rel="noopener noreferrer">$2</a>');
369
+ return s;
370
+ }
371
+
372
+ // ── file preview modal (shares the rk-aichat-preview styling from theme.ts):
373
+ // images carousel, video, audio, pdf, and text (json pretty-print, md render,
374
+ // txt/csv/srt/vtt as <pre>) ──
375
+ function fileKind(name, ct){
376
+ ct=(ct||'').toLowerCase();
377
+ var ext=((name||'').split('.').pop()||'').toLowerCase();
378
+ if(ct.indexOf('image/')===0 || /^(png|jpe?g|gif|webp|svg|avif|bmp|heic|ico)$/.test(ext)) return 'image';
379
+ if(ct.indexOf('video/')===0 || /^(mp4|mov|webm|m4v|mkv|ogv)$/.test(ext)) return 'video';
380
+ if(ct.indexOf('audio/')===0 || /^(mp3|wav|m4a|aac|ogg|oga|flac|opus)$/.test(ext)) return 'audio';
381
+ if(ct.indexOf('application/pdf')===0 || ext==='pdf') return 'pdf';
382
+ if(ct.indexOf('json')>=0 || ext==='json') return 'json';
383
+ if(ext==='md' || ext==='markdown' || ct.indexOf('markdown')>=0) return 'markdown';
384
+ if(ct.indexOf('text/')===0 || /^(txt|csv|tsv|srt|vtt|log|xml|yaml|yml|ini|conf|html?)$/.test(ext)) return 'text';
385
+ return 'other';
386
+ }
387
+ // Block-level markdown for .md previews (headings, lists, quotes, rules, code,
388
+ // plus the inline bold/italic/link handling from mdToHtml).
389
+ function renderMarkdownBlock(src){
390
+ var BT=String.fromCharCode(96), FENCE=BT+BT+BT;
391
+ function inline(s){ return mdToHtml(s).replace(new RegExp(BT+'([^'+BT+']+)'+BT,'g'),'<code>$1</code>'); }
392
+ var lines=String(src).replace(/\\r\\n?/g,'\\n').split('\\n');
393
+ var out=[], inCode=false, code=[], listType=null, listItems=[];
394
+ function flushList(){ if(!listType)return; out.push('<'+listType+'>'+listItems.join('')+'</'+listType+'>'); listType=null; listItems=[]; }
395
+ function flushCode(){ out.push('<pre class="rk-aichat-preview-code"><code>'+escapeHtml(code.join('\\n'))+'</code></pre>'); code=[]; }
396
+ for(var i=0;i<lines.length;i++){
397
+ var ln=lines[i];
398
+ if(ln.trim().indexOf(FENCE)===0){ if(inCode){ flushCode(); inCode=false; } else { flushList(); inCode=true; } continue; }
399
+ if(inCode){ code.push(ln); continue; }
400
+ var h=ln.match(/^(#{1,6})\\s+(.*)$/);
401
+ if(h){ flushList(); var lvl=h[1].length; out.push('<h'+lvl+'>'+inline(h[2])+'</h'+lvl+'>'); continue; }
402
+ if(/^\\s*([-*+])\\s+/.test(ln)){ if(listType!=='ul'){ flushList(); listType='ul'; } listItems.push('<li>'+inline(ln.replace(/^\\s*[-*+]\\s+/,''))+'</li>'); continue; }
403
+ var om=ln.match(/^\\s*\\d+\\.\\s+(.*)$/);
404
+ if(om){ if(listType!=='ol'){ flushList(); listType='ol'; } listItems.push('<li>'+inline(om[1])+'</li>'); continue; }
405
+ if(/^\\s*>\\s?/.test(ln)){ flushList(); out.push('<blockquote>'+inline(ln.replace(/^\\s*>\\s?/,''))+'</blockquote>'); continue; }
406
+ if(/^\\s*(-{3,}|\\*{3,}|_{3,})\\s*$/.test(ln)){ flushList(); out.push('<hr>'); continue; }
407
+ if(!ln.trim()){ flushList(); continue; }
408
+ flushList(); out.push('<p>'+inline(ln)+'</p>');
409
+ }
410
+ if(inCode) flushCode();
411
+ flushList();
412
+ return out.join('');
413
+ }
414
+ function openFilePreview(items, index){
415
+ items=(items||[]).filter(function(f){ return f && f.viewUrl; });
416
+ if(!items.length) return;
417
+ var idx=Math.max(0,Math.min(index||0,items.length-1));
418
+ var back=el('rk-aichat-modal-back');
419
+ var modal=el('rk-aichat-modal rk-aichat-preview');
420
+ var head=el('rk-aichat-modal-head');
421
+ var nameEl=root.createElement('span'); nameEl.className='rk-aichat-http-url rk-aichat-preview-name';
422
+ var openLink=root.createElement('a'); openLink.className='rk-aichat-preview-open'; openLink.target='_blank'; openLink.rel='noopener noreferrer'; openLink.textContent='Open \\u2197';
423
+ var xBtn=root.createElement('button'); xBtn.type='button'; xBtn.className='rk-aichat-modal-x'; xBtn.setAttribute('aria-label','Close'); xBtn.innerHTML='&times;';
424
+ head.appendChild(nameEl); head.appendChild(openLink); head.appendChild(xBtn);
425
+ var body=el('rk-aichat-modal-body rk-aichat-preview-body');
426
+ var nav=el('rk-aichat-preview-nav');
427
+ var prev=root.createElement('button'); prev.type='button'; prev.className='rk-aichat-preview-btn'; prev.innerHTML='\\u2039 Prev';
428
+ var counter=root.createElement('span'); counter.className='rk-aichat-preview-count';
429
+ var next=root.createElement('button'); next.type='button'; next.className='rk-aichat-preview-btn'; next.innerHTML='Next \\u203a';
430
+ nav.appendChild(prev); nav.appendChild(counter); nav.appendChild(next);
431
+ function renderUnknown(container, it, msg){
432
+ var wrap=el('rk-aichat-preview-empty');
433
+ var p=root.createElement('div'); p.textContent=msg||'No inline preview for this file type.'; wrap.appendChild(p);
434
+ var a=root.createElement('a'); a.href=it.viewUrl; a.target='_blank'; a.rel='noopener noreferrer'; a.className='rk-aichat-preview-dl'; a.textContent='Open / download \\u2197'; wrap.appendChild(a);
435
+ container.appendChild(wrap);
436
+ }
437
+ function renderText(container, it, kind){
438
+ var loading=el('rk-aichat-preview-loading'); loading.textContent='Loading\\u2026'; container.appendChild(loading);
439
+ fetch(it.viewUrl,{ credentials:'same-origin' })
440
+ .then(function(r){ if(!r.ok) throw new Error('http '+r.status); return r.text(); })
441
+ .then(function(txt){
442
+ container.innerHTML='';
443
+ if(kind==='json'){ var pretty=txt; try{ pretty=JSON.stringify(JSON.parse(txt),null,2); }catch(e){} var pj=root.createElement('pre'); pj.className='rk-aichat-http-pre rk-aichat-preview-text'; pj.textContent=pretty; container.appendChild(pj); return; }
444
+ if(kind==='markdown'){ var md=el('rk-aichat-preview-md'); md.innerHTML=renderMarkdownBlock(txt); container.appendChild(md); return; }
445
+ var pre=root.createElement('pre'); pre.className='rk-aichat-http-pre rk-aichat-preview-text'; pre.textContent=txt; container.appendChild(pre);
446
+ })
447
+ .catch(function(){ container.innerHTML=''; renderUnknown(container, it, 'Couldn\\u2019t load this file.'); });
448
+ }
449
+ function render(){
450
+ var it=items[idx];
451
+ nameEl.textContent=it.name||'file'; nameEl.title=it.name||'';
452
+ openLink.href=it.viewUrl;
453
+ body.innerHTML='';
454
+ var kind=fileKind(it.name, it.contentType);
455
+ if(kind==='image'){ var im=root.createElement('img'); im.className='rk-aichat-preview-img'; im.src=it.viewUrl; im.alt=it.name||''; body.appendChild(im); }
456
+ else if(kind==='video'){ var v=root.createElement('video'); v.className='rk-aichat-preview-media'; v.src=it.viewUrl; v.controls=true; v.playsInline=true; body.appendChild(v); }
457
+ else if(kind==='audio'){ var wrap=el('rk-aichat-preview-audiowrap'); var a=root.createElement('audio'); a.className='rk-aichat-preview-audio'; a.src=it.viewUrl; a.controls=true; wrap.appendChild(a); body.appendChild(wrap); }
458
+ else if(kind==='pdf'){ var f=root.createElement('iframe'); f.className='rk-aichat-preview-frame'; f.src=it.viewUrl; body.appendChild(f); }
459
+ else if(kind==='json'||kind==='markdown'||kind==='text'){ renderText(body, it, kind); }
460
+ else { renderUnknown(body, it); }
461
+ counter.textContent=(idx+1)+' / '+items.length;
462
+ prev.disabled=idx<=0; next.disabled=idx>=items.length-1;
463
+ nav.hidden=items.length<2;
464
+ }
465
+ prev.addEventListener('click',function(){ if(idx>0){ idx--; render(); } });
466
+ next.addEventListener('click',function(){ if(idx<items.length-1){ idx++; render(); } });
467
+ modal.appendChild(head); modal.appendChild(body); modal.appendChild(nav); back.appendChild(modal);
468
+ function close(){ if(back.parentNode) back.parentNode.removeChild(back); document.removeEventListener('keydown',onKey); }
469
+ function onKey(e){ if(e.key==='Escape'){ e.stopPropagation(); close(); } else if(e.key==='ArrowLeft'){ if(idx>0){ idx--; render(); } } else if(e.key==='ArrowRight'){ if(idx<items.length-1){ idx++; render(); } } }
470
+ back.addEventListener('click',function(e){ if(e.target===back) close(); });
471
+ xBtn.addEventListener('click',close);
472
+ document.addEventListener('keydown',onKey);
473
+ document.body.appendChild(back);
474
+ render();
475
+ }
284
476
 
285
477
  function clientContext(){
286
478
  var now=new Date(); var local=null, tz=null;
@@ -301,7 +493,7 @@ export function renderReskinChat(input = DEFAULT_INPUT) {
301
493
  function appendAssistant(){
302
494
  var msg=el('rk-chat-msg is-assistant');
303
495
  var meta=el('rk-chat-meta');
304
- meta.innerHTML='<span class="rk-chat-avatar" aria-hidden="true">'+AVATAR+'</span>Copilot <span class="rk-chat-time">'+nowLabel()+'</span>';
496
+ meta.innerHTML='Agent <span class="rk-chat-time">'+nowLabel()+'</span>';
305
497
  var bub=el('rk-chat-bubble rk-chat-bubble-assistant');
306
498
  var status=el('rk-chat-status');
307
499
  status.innerHTML='<span class="rk-chat-typing"><span></span><span></span><span></span></span><span class="rk-chat-status-label">Thinking…</span>';
@@ -310,11 +502,14 @@ export function renderReskinChat(input = DEFAULT_INPUT) {
310
502
  bub.appendChild(status); bub.appendChild(notes); bub.appendChild(textEl);
311
503
  msg.appendChild(meta); msg.appendChild(bub);
312
504
  if(thread) thread.appendChild(msg);
505
+ var lastText='';
313
506
  return {
314
507
  setStatus:function(s){ var l=status.querySelector('.rk-chat-status-label'); if(l && s){ l.textContent=s; } },
315
508
  hideStatus:function(){ status.hidden=true; },
316
509
  showText:function(){ status.hidden=true; textEl.hidden=false; },
317
- setText:function(t){ textEl.textContent=t; },
510
+ setText:function(t){ lastText=t; textEl.textContent=t; },
511
+ // After streaming completes, re-render as markdown (bold/italic/links).
512
+ renderMd:function(){ if(lastText){ textEl.innerHTML=mdToHtml(lastText); } },
318
513
  addNote:function(t){ var n=el('rk-chat-toolnote'); n.textContent=t; notes.appendChild(n); },
319
514
  fail:function(m){ status.hidden=true; var er=el('rk-chat-error'); er.textContent=m||'Something went wrong.'; bub.appendChild(er); }
320
515
  };
@@ -360,19 +555,26 @@ export function renderReskinChat(input = DEFAULT_INPUT) {
360
555
  function sendMessage(text){
361
556
  if(busy) return;
362
557
  text=(text||'').trim();
363
- if(!text) return;
558
+ // Attachments picked from the drawer travel in user_message.attachments;
559
+ // sending is allowed with only files (no text) too.
560
+ var atts=attachments.map(function(f){ return { id:String(f.id), fileName:f.name, contentType:f.contentType, viewUrl:f.viewUrl }; });
561
+ if(!text && !atts.length) return;
364
562
 
365
- history.push({role:'user', text:text});
366
- appendUser(text);
563
+ var line=text;
564
+ if(attachments.length){ line=(text?text+' ':'')+'(attached: '+attachments.map(function(f){ return f.name; }).join(', ')+')'; }
565
+
566
+ clearEmptyState();
567
+ history.push({role:'user', text:text||line});
568
+ appendUser(line);
367
569
  if(input){ input.value=''; autosize(); }
368
- if(title && history.length===1){ title.textContent=text.slice(0,60); }
570
+ attachments=[]; renderAttachChips();
369
571
  toBottom();
370
572
 
371
573
  var view=appendAssistant();
372
574
  toBottom();
373
575
  setBusy(true);
374
576
 
375
- if(!threadId){ threadId=genId('thread'); }
577
+ if(!threadId){ threadId=genId('thread'); setChatUrl(threadId, true); }
376
578
  var userMsgId=genId('user');
377
579
  var asstMsgId=genId('assistant');
378
580
 
@@ -380,10 +582,10 @@ export function renderReskinChat(input = DEFAULT_INPUT) {
380
582
  messages: history.map(function(m){ return { role:m.role, content:[{ type:'text', text:m.text }] }; }),
381
583
  thread_id: threadId,
382
584
  thread_template_id: (TEMPLATE && TEMPLATE.templateId) || 'chat-brainstorm',
383
- thread_title: text.slice(0,42),
585
+ thread_title: (text||line).slice(0,42),
384
586
  thread_tracers: (TEMPLATE && TEMPLATE.tracers) || [],
385
587
  client_context: clientContext(),
386
- user_message: { id:userMsgId, role:'user', text:text, attachments:[] },
588
+ user_message: { id:userMsgId, role:'user', text:text||line, attachments:atts },
387
589
  assistant_message: { id:asstMsgId }
388
590
  };
389
591
  if(TEMPLATE){ body.template=TEMPLATE; }
@@ -403,9 +605,11 @@ export function renderReskinChat(input = DEFAULT_INPUT) {
403
605
  onError:function(m){ errored=true; view.fail(m); toBottom(); }
404
606
  };
405
607
 
608
+ var controller=(typeof AbortController!=='undefined') ? new AbortController() : null;
609
+ pendingAbort=controller;
406
610
  var req;
407
611
  try{
408
- req=fetch(ENDPOINT,{ method:'POST', headers:headers, credentials:'same-origin', body:JSON.stringify(body) });
612
+ req=fetch(ENDPOINT,{ method:'POST', headers:headers, credentials:'same-origin', body:JSON.stringify(body), signal: controller?controller.signal:undefined });
409
613
  }catch(err){
410
614
  // Synchronous fetch failure (e.g. invalid URL) — never let it throw uncaught.
411
615
  req=Promise.reject(err);
@@ -419,9 +623,11 @@ export function renderReskinChat(input = DEFAULT_INPUT) {
419
623
  }
420
624
  return readStream(resp, handlers);
421
625
  }).then(function(){
626
+ pendingAbort=null;
422
627
  view.hideStatus();
423
628
  if(sawText){
424
629
  history.push({role:'assistant', text:acc});
630
+ view.renderMd();
425
631
  } else if(!errored){
426
632
  view.showText();
427
633
  view.setText('The assistant finished without returning any text.');
@@ -431,6 +637,17 @@ export function renderReskinChat(input = DEFAULT_INPUT) {
431
637
  toBottom();
432
638
  loadHistory();
433
639
  }).catch(function(err){
640
+ pendingAbort=null;
641
+ // User hit Stop mid-reply — keep whatever streamed so far, no error.
642
+ if(err && (err.name==='AbortError' || (controller && controller.signal && controller.signal.aborted))){
643
+ view.hideStatus();
644
+ if(sawText){ history.push({role:'assistant', text:acc}); view.renderMd(); }
645
+ else{ view.showText(); view.setText('Stopped.'); }
646
+ setBusy(false);
647
+ if(input){ input.focus(); }
648
+ toBottom();
649
+ return;
650
+ }
434
651
  var msg=(err && err.message) ? err.message : String(err);
435
652
  if(!API_KEY){ msg=msg+'\\n\\nAdd an AI provider key in Settings to brainstorm on your own keys.'; }
436
653
  view.fail(msg);
@@ -512,9 +729,10 @@ export function renderReskinChat(input = DEFAULT_INPUT) {
512
729
  })
513
730
  .catch(function(){ if(histList){ histList.innerHTML=''; histList.appendChild(histNote('Couldn\\u2019t load your chats.', {retry:true})); } });
514
731
  }
515
- function appendAssistantStatic(text){ var v=appendAssistant(); v.showText(); v.setText(text||''); v.hideStatus(); return v; }
516
- function openThread(id){
732
+ function appendAssistantStatic(text){ var v=appendAssistant(); v.showText(); v.setText(text||''); v.hideStatus(); v.renderMd(); return v; }
733
+ function openThread(id, opts){
517
734
  if(busy || loadingThread) return;
735
+ if(!(opts && opts.skipUrl)) setChatUrl(id, false);
518
736
  loadingThread=true;
519
737
  closeHistoryOverlay();
520
738
  fetch(THREADS_URL+'/'+encodeURIComponent(id), { headers:authHeaders(), credentials:'same-origin' })
@@ -529,7 +747,6 @@ export function renderReskinChat(input = DEFAULT_INPUT) {
529
747
  history.push({role:role, text:txt});
530
748
  if(role==='user'){ appendUser(txt); } else { appendAssistantStatic(txt); }
531
749
  }
532
- if(title){ title.textContent=t.title || 'Brainstorm studio'; }
533
750
  setActiveThread(id);
534
751
  loadingThread=false;
535
752
  toBottom();
@@ -547,15 +764,37 @@ export function renderReskinChat(input = DEFAULT_INPUT) {
547
764
  })
548
765
  .catch(function(){});
549
766
  }
550
- function resetChat(){
767
+ // A fresh chat opens empty — no greeting message — with a few subtle
768
+ // suggestion pills the user can tap to start (or just type their own).
769
+ function renderEmptyState(){
770
+ if(!thread) return;
771
+ thread.innerHTML='';
772
+ var suggestions=[
773
+ 'Give me 10 ad hooks for my offer',
774
+ 'What angle works best for cold traffic?',
775
+ 'Help me plan a cold-start strategy'
776
+ ];
777
+ var wrap=el('rk-chat-empty');
778
+ var hint=el('rk-chat-empty-hint'); hint.textContent='Ask anything, or try one of these';
779
+ var list=el('rk-chat-suggests');
780
+ suggestions.forEach(function(s){
781
+ var b=root.createElement('button');
782
+ b.type='button'; b.className='rk-chat-suggest'; b.textContent=s;
783
+ b.addEventListener('click',function(){ if(busy) return; sendMessage(s); });
784
+ list.appendChild(b);
785
+ });
786
+ wrap.appendChild(hint); wrap.appendChild(list);
787
+ thread.appendChild(wrap);
788
+ }
789
+ function clearEmptyState(){
790
+ if(!thread) return;
791
+ var e=thread.querySelector('.rk-chat-empty');
792
+ if(e && e.parentNode){ e.parentNode.removeChild(e); }
793
+ }
794
+ function resetChat(opts){
551
795
  history=[]; threadId=null;
552
- if(thread){
553
- thread.innerHTML='<div class="rk-chat-msg is-assistant">'+
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>'+
556
- '</div>';
557
- }
558
- if(title){ title.textContent='Brainstorm studio'; }
796
+ if(!(opts && opts.skipUrl)) setChatUrl(null, false);
797
+ renderEmptyState();
559
798
  setActiveThread(null);
560
799
  if(input){ input.value=''; autosize(); input.focus(); }
561
800
  window.scrollTo(0,0);
@@ -566,20 +805,27 @@ export function renderReskinChat(input = DEFAULT_INPUT) {
566
805
  if(histBackdrop){ histBackdrop.addEventListener('click', closeHistoryOverlay); }
567
806
  loadHistory();
568
807
 
808
+ // Deep-link: /chat/<id> opens that saved thread on load; otherwise the page
809
+ // starts on the empty new-chat state with subtle suggestions.
810
+ if(INITIAL_THREAD){ openThread(INITIAL_THREAD, {skipUrl:true}); }
811
+ else{ renderEmptyState(); }
812
+ // Back/forward between conversations — sync the visible thread to the URL.
813
+ window.addEventListener('popstate',function(){
814
+ if(busy || loadingThread) return;
815
+ var id=threadFromLocation();
816
+ if(id){ if(id!==threadId){ openThread(id, {skipUrl:true}); } }
817
+ else if(threadId){ resetChat({skipUrl:true}); }
818
+ });
819
+
569
820
  // composer wiring
570
821
  if(input){ input.addEventListener('input',autosize); autosize(); }
571
- if(send){ send.addEventListener('click',function(){ if(input){ sendMessage(input.value); } }); }
822
+ if(send){ send.addEventListener('click',function(){ if(busy){ if(pendingAbort){ pendingAbort.abort(); } return; } if(input){ sendMessage(input.value); } }); }
572
823
  if(input){
573
824
  input.addEventListener('keydown',function(ev){
574
- if(ev.key==='Enter' && !ev.shiftKey){ ev.preventDefault(); sendMessage(input.value); }
825
+ if(ev.key==='Enter' && !ev.shiftKey){ ev.preventDefault(); if(busy){ if(pendingAbort){ pendingAbort.abort(); } return; } sendMessage(input.value); }
575
826
  });
576
827
  }
577
828
 
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
829
  // "New" resets the conversation to the warm empty state (shared resetChat)
584
830
  var newBtn=root.getElementById('rk-chat-new');
585
831
  if(newBtn){
@@ -596,9 +842,10 @@ export function renderReskinChat(input = DEFAULT_INPUT) {
596
842
  }
597
843
  })();`;
598
844
  return reskinDocument({
599
- title: "vidfarm reskin Brainstorm",
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.",
845
+ title: "vidfarm — Chat with Agent",
846
+ description: "Vidfarm AI agent chat, wired to the live editor-chat backend — a clean single-column conversation that opens empty with subtle suggestions and a working composer.",
601
847
  activeSlug: "chat",
848
+ account: { name: input.name, email: input.email },
602
849
  pageCss,
603
850
  body,
604
851
  script