@mevdragon/vidfarm-devcli 0.17.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 (55) hide show
  1. package/.agents/skills/vidfarm-media/SKILL.md +43 -6
  2. package/SKILL.director.md +37 -23
  3. package/SKILL.platform.md +6 -6
  4. package/demo/dist/app.js +69 -69
  5. package/demo/dist/favicon.ico +0 -0
  6. package/dist/src/app.js +1832 -213
  7. package/dist/src/cli.js +238 -17
  8. package/dist/src/devcli/clip-store.js +29 -2
  9. package/dist/src/devcli/clips.js +116 -73
  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 +214 -170
  20. package/dist/src/reskin/calendar-page.js +503 -187
  21. package/dist/src/reskin/chat-page.js +739 -299
  22. package/dist/src/reskin/discover-page.js +1450 -279
  23. package/dist/src/reskin/document.js +1392 -16
  24. package/dist/src/reskin/help-page.js +139 -100
  25. package/dist/src/reskin/index-page.js +1 -1
  26. package/dist/src/reskin/inpaint-page.js +547 -0
  27. package/dist/src/reskin/job-runs-page.js +355 -127
  28. package/dist/src/reskin/library-page.js +1188 -317
  29. package/dist/src/reskin/login-page.js +124 -87
  30. package/dist/src/reskin/pricing-page.js +197 -166
  31. package/dist/src/reskin/settings-page.js +566 -187
  32. package/dist/src/reskin/theme.js +434 -17
  33. package/dist/src/services/clip-curation/gemini.js +5 -0
  34. package/dist/src/services/clip-curation/hunt.js +79 -1
  35. package/dist/src/services/clip-curation/index.js +2 -1
  36. package/dist/src/services/clip-curation/local-agent.js +4 -3
  37. package/dist/src/services/clip-curation/media-select.js +85 -0
  38. package/dist/src/services/clip-curation/query.js +5 -1
  39. package/dist/src/services/clip-curation/refine.js +50 -20
  40. package/dist/src/services/clip-curation/scan.js +10 -3
  41. package/dist/src/services/clip-curation/taxonomy.js +3 -1
  42. package/dist/src/services/clip-curation/taxonomy.v1.json +13 -1
  43. package/dist/src/services/clip-records.js +14 -1
  44. package/dist/src/services/clip-search.js +43 -13
  45. package/dist/src/services/file-directory.js +114 -0
  46. package/dist/src/services/storage.js +24 -1
  47. package/dist/src/services/upstream.js +5 -5
  48. package/dist/src/template-editor-shell.js +16 -2
  49. package/package.json +1 -1
  50. package/public/assets/discover-client-app.js +1 -0
  51. package/public/assets/file-directory-app.js +2 -0
  52. package/public/assets/homepage-client-app.js +12 -12
  53. package/public/assets/page-runtime-client-app.js +24 -24
  54. package/src/assets/favicon.ico +0 -0
  55. package/src/assets/logo-vidfarm.png +0 -0
@@ -1,198 +1,153 @@
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
- ];
88
- 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" }
93
- ];
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("");
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 = {}) {
34
+ const boot = input.editorChat ?? null;
35
+ const initialThreadId = opts.initialThreadId ?? null;
36
+ // Embed the real boot as JSON. Escaping only `<` is the correct (and
37
+ // sufficient) way to keep a `<script type="application/json">` block from
38
+ // breaking out; HTML-entity escaping would corrupt JSON.parse.
39
+ const bootJson = JSON.stringify(boot).replace(/</g, "\\u003c");
122
40
  const body = `
123
- <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}
137
- </div>
41
+ <main class="rk-chat-page"${initialThreadId ? ` data-initial-thread="${rkEscape(initialThreadId)}"` : ""}>
42
+ <aside class="rk-chat-history" id="rk-chat-history" aria-label="Chat history">
43
+ <div class="rk-chat-history-head">
44
+ <span class="rk-chat-history-title">Chats</span>
45
+ <button type="button" class="rk-chat-newbtn" id="rk-chat-new"><span class="rk-chat-plus" aria-hidden="true">+</span> New</button>
46
+ </div>
47
+ <div class="rk-chat-history-list" id="rk-chat-history-list"></div>
48
+ </aside>
49
+ <div class="rk-chat-histbackdrop" id="rk-chat-histbackdrop" hidden></div>
50
+ <div class="rk-chat-main">
51
+ <div class="rk-chat-col">
52
+ <header class="rk-chat-head">
53
+ <button type="button" class="rk-chat-histtoggle" id="rk-chat-histtoggle" aria-label="Show chat history">☰</button>
54
+ <div class="rk-chat-head-titles">
55
+ <h1 class="rk-chat-title" id="rk-chat-title">Chat with Agent</h1>
138
56
  </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
- </div>
141
- </header>
57
+ <div class="rk-chat-head-actions">
58
+ <label class="rk-chat-mode" aria-label="Studio mode">
59
+ <select class="rk-chat-mode-select" id="rk-chat-mode">
60
+ <option value="/chat" selected>Chat</option>
61
+ <option value="/inpaint">Create Image</option>
62
+ </select>
63
+ <span class="rk-chat-mode-chev" aria-hidden="true">&#9662;</span>
64
+ </label>
65
+ </div>
66
+ </header>
142
67
 
143
- <div class="rk-chat-thread" id="rk-chat-thread">
144
- ${thread}
145
- </div>
68
+ <div class="rk-chat-thread" id="rk-chat-thread"></div>
146
69
 
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>
70
+ <div class="rk-chat-dock">
71
+ <div class="rk-chat-chips" id="rk-chat-chips" aria-label="Attached files"></div>
72
+ <div class="rk-chat-composer" id="rk-chat-composer">
73
+ <textarea id="rk-chat-input" class="rk-chat-input" rows="3"
74
+ placeholder="Describe your offer, audience, or the problem to solve…"></textarea>
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>
79
+ </div>
80
+ <p class="rk-chat-hint"><kbd>Enter</kbd> to send · <kbd>Shift</kbd>+<kbd>Enter</kbd> for a new line</p>
156
81
  </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
82
  </div>
159
83
  </div>
84
+ <script type="application/json" id="rk-chat-boot">${bootJson}</script>
160
85
  </main>`;
161
86
  const pageCss = `
162
- /* ── centered single-column chat ───────────────────────────────────────── */
163
- .rk-chat-page{width:100%;padding:0 var(--rk-gutter);background:var(--rk-bg)}
87
+ /* ── two-pane: history sidebar + centered chat column ─────────────────────── */
88
+ .rk-chat-page{display:flex;width:100%;background:var(--rk-bg);min-height:100vh}
89
+ .rk-chat-main{flex:1;min-width:0;display:flex;justify-content:center;padding:0 var(--rk-gutter)}
164
90
  .rk-chat-col{width:min(800px,100%);margin:0 auto;display:flex;flex-direction:column;
165
91
  min-height:100vh;padding-top:34px}
166
92
 
167
- /* compact header */
168
- .rk-chat-head{display:flex;align-items:flex-start;justify-content:space-between;gap:18px;
169
- 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}
171
- .rk-chat-eyebrow{display:inline-flex;align-items:center;font-size:12px;font-weight:700;
172
- letter-spacing:.06em;text-transform:uppercase;color:var(--rk-gold-700)}
173
- .rk-chat-title{font-family:var(--rk-font-display);font-weight:800;font-size:1.7rem;letter-spacing:-.025em;
93
+ /* history sidebar (real editor-chat threads) */
94
+ .rk-chat-history{flex:none;width:266px;background:var(--rk-surface);border-right:1px solid var(--rk-border);
95
+ display:flex;flex-direction:column;position:sticky;top:0;height:100vh}
96
+ .rk-chat-history-head{display:flex;align-items:center;justify-content:space-between;gap:10px;
97
+ padding:20px 16px 12px;flex:none}
98
+ .rk-chat-history-title{font-family:var(--rk-font-display);font-weight:800;font-size:16px;color:var(--rk-ink);letter-spacing:-.01em}
99
+ .rk-chat-newbtn{display:inline-flex;align-items:center;gap:5px;padding:7px 13px;border-radius:var(--rk-r-full);
100
+ border:1px solid var(--rk-gold-600);background:var(--rk-gold-tint);color:var(--rk-ink);cursor:pointer;
101
+ font-family:var(--rk-font-body);font-size:12.5px;font-weight:700;transition:background var(--rk-dur) var(--rk-ease)}
102
+ .rk-chat-newbtn:hover{background:var(--rk-gold-tint2)}
103
+ .rk-chat-history-list{flex:1;min-height:0;overflow-y:auto;padding:4px 10px 16px;display:grid;gap:2px;align-content:start}
104
+ .rk-chat-hrow{display:flex;align-items:center;gap:8px;width:100%;padding:9px 10px;border-radius:var(--rk-r-lg);
105
+ border:0;background:transparent;cursor:pointer;text-align:left;font-family:inherit;min-width:0;
106
+ transition:background var(--rk-dur) var(--rk-ease),box-shadow var(--rk-dur) var(--rk-ease)}
107
+ .rk-chat-hrow:hover{background:var(--rk-n-50);box-shadow:var(--rk-shadow-xs)}
108
+ .rk-chat-hrow.is-active{background:var(--rk-gold-tint)}
109
+ .rk-chat-hrow-main{flex:1;min-width:0;display:grid;gap:1px}
110
+ .rk-chat-hrow-title{font-size:13px;font-weight:600;color:var(--rk-ink);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
111
+ .rk-chat-hrow-sub{font-size:11px;color:var(--rk-text-muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
112
+ .rk-chat-hrow-del{flex:none;border:0;background:transparent;color:var(--rk-n-400);cursor:pointer;font-size:15px;
113
+ line-height:1;padding:2px 5px;border-radius:var(--rk-r-sm);opacity:0;
114
+ transition:opacity var(--rk-dur) var(--rk-ease),color var(--rk-dur) var(--rk-ease),background var(--rk-dur) var(--rk-ease)}
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}}
118
+ .rk-chat-hrow-del:hover{color:var(--rk-red);background:var(--rk-red-tint)}
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}
120
+ .rk-chat-history-note a{color:var(--rk-gold-700);font-weight:600}
121
+ .rk-chat-history-note button{font-family:inherit;font-size:12px;font-weight:600;color:var(--rk-ink);cursor:pointer;
122
+ background:#fff;border:1px solid var(--rk-border);border-radius:var(--rk-r-full);padding:5px 12px}
123
+ .rk-chat-history-note button:hover{background:var(--rk-n-100)}
124
+
125
+ /* narrow-screen history toggle + slide-over (hidden on wide) */
126
+ .rk-chat-histtoggle{display:none;flex:none;width:34px;height:34px;border-radius:var(--rk-r-lg);
127
+ border:1px solid var(--rk-border);background:#fff;color:var(--rk-n-600);cursor:pointer;font-size:15px;
128
+ align-items:center;justify-content:center;margin-right:2px;margin-top:2px}
129
+ .rk-chat-histbackdrop{display:none}
130
+
131
+ /* compact header — sticky so the title stays pinned while the thread scrolls */
132
+ .rk-chat-head{display:flex;align-items:flex-start;gap:12px;
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;
174
137
  line-height:1.15;color:var(--rk-ink);min-width:0}
175
138
  .rk-chat-subhead{font-size:14.5px;color:var(--rk-text-muted);line-height:1.5;max-width:56ch}
176
139
  .rk-chat-head-actions{display:flex;align-items:center;gap:9px;flex:none}
177
140
  .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)}
141
+
142
+ /* studio mode select (Brainstorm ↔ Create Image → /reskin/inpaint) */
143
+ .rk-chat-mode{position:relative;display:inline-flex;align-items:center}
144
+ .rk-chat-mode-select{appearance:none;-webkit-appearance:none;font-family:var(--rk-font-body);font-size:13px;font-weight:700;
145
+ color:var(--rk-ink);background:var(--rk-surface);border:1px solid var(--rk-border-strong);border-radius:var(--rk-r-full);
146
+ padding:9px 32px 9px 15px;cursor:pointer;box-shadow:var(--rk-shadow-xs);
147
+ transition:border-color var(--rk-dur) var(--rk-ease),box-shadow var(--rk-dur) var(--rk-ease)}
148
+ .rk-chat-mode-select:hover{border-color:var(--rk-gold-600)}
149
+ .rk-chat-mode-select:focus{outline:none;border-color:var(--rk-gold-600);box-shadow:var(--rk-ring-gold)}
150
+ .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
151
 
197
152
  /* thread — flex:1 pushes the dock to the bottom when the page is short */
198
153
  .rk-chat-thread{flex:1;display:grid;gap:28px;align-content:start;padding:30px 0 26px}
@@ -205,44 +160,49 @@ export function renderReskinChat() {
205
160
  font-family:var(--rk-font-display);font-weight:800;font-size:11px;line-height:1;
206
161
  background:linear-gradient(142deg,var(--rk-gold-500),var(--rk-violet))}
207
162
 
208
- /* bubbles — uniform 1px hairline on all four sides, no colored side stripes */
209
- .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}
210
167
  .rk-chat-bubble-user{max-width:min(78%,560px);background:var(--rk-ink);color:#f4f4f5;
211
168
  border:1px solid var(--rk-n-800);box-shadow:var(--rk-shadow-sm)}
212
169
  .rk-chat-bubble-assistant{width:100%;background:var(--rk-surface);color:var(--rk-n-700);
213
170
  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
171
  .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)}
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}
174
+
175
+ /* streamed assistant text — pre-wrap keeps the model's own line breaks */
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}
180
+ .rk-chat-stream[hidden]{display:none}
181
+
182
+ /* thinking / status row */
183
+ .rk-chat-status{display:flex;align-items:center;gap:10px;color:var(--rk-text-faint);font-size:13.5px}
184
+ .rk-chat-status[hidden]{display:none}
185
+ .rk-chat-status-label{font-size:13px;color:var(--rk-text-muted)}
186
+
187
+ /* simplified tool-call notes (rich artifact/HTTP cards are intentionally omitted) */
188
+ .rk-chat-notes{display:grid;gap:6px;margin-bottom:10px}
189
+ .rk-chat-notes:empty{display:none}
190
+ .rk-chat-toolnote{display:inline-flex;align-items:center;gap:7px;font-size:12.5px;color:var(--rk-text-muted);
191
+ font-family:var(--rk-font-mono);background:var(--rk-n-50);border:1px solid var(--rk-border);
192
+ border-radius:var(--rk-r-lg);padding:5px 10px;width:fit-content}
193
+ .rk-chat-toolnote::before{content:"";width:6px;height:6px;border-radius:50%;background:var(--rk-gold-500);flex:none}
194
+
195
+ /* error surface */
196
+ .rk-chat-error{margin-top:2px;font-size:13.5px;color:#b42318;background:var(--rk-n-50);
197
+ border:1px solid var(--rk-border);border-radius:var(--rk-r-lg);padding:10px 12px;white-space:pre-wrap}
198
+
199
+ /* typing indicator */
200
+ .rk-chat-typing{display:inline-flex;gap:4px;align-items:center;padding:2px 0}
201
+ .rk-chat-typing span{width:7px;height:7px;border-radius:50%;background:var(--rk-n-400);
202
+ animation:rk-chat-blink 1.2s var(--rk-ease) infinite}
203
+ .rk-chat-typing span:nth-child(2){animation-delay:.18s}
204
+ .rk-chat-typing span:nth-child(3){animation-delay:.36s}
205
+ @keyframes rk-chat-blink{0%,80%,100%{opacity:.28;transform:translateY(0)}40%{opacity:1;transform:translateY(-2px)}}
246
206
 
247
207
  /* ── quiet sticky composer dock ── */
248
208
  .rk-chat-dock{position:sticky;bottom:0;z-index:10;background:var(--rk-bg);
@@ -252,70 +212,274 @@ export function renderReskinChat() {
252
212
  .rk-chat-starters{display:flex;flex-wrap:wrap;gap:9px}
253
213
  .rk-chat-chip{display:inline-flex;align-items:center;gap:7px;padding:8px 14px;border-radius:var(--rk-r-full);
254
214
  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);
215
+ background:var(--rk-surface);border:1px solid var(--rk-border);box-shadow:var(--rk-shadow-xs);text-align:left;
256
216
  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
217
  .rk-chat-chip:hover{border-color:var(--rk-gold-600);background:var(--rk-gold-tint);color:var(--rk-ink);
258
218
  transform:translateY(-1px);box-shadow:var(--rk-shadow-sm)}
259
- .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;
219
+ .rk-chat-chip:disabled{opacity:.5;cursor:default;transform:none;box-shadow:var(--rk-shadow-xs)}
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;
261
236
  box-shadow:var(--rk-shadow-card);transition:border-color var(--rk-dur) var(--rk-ease),box-shadow var(--rk-dur) var(--rk-ease)}
262
237
  .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
- .rk-chat-input{flex:1;min-width:0;border:0;background:transparent;resize:none;font-family:var(--rk-font-body);
269
- 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}
270
260
  .rk-chat-input:focus{outline:none;box-shadow:none}
271
261
  .rk-chat-input::placeholder{color:var(--rk-n-400)}
272
262
  .rk-chat-send{flex:none;padding:11px 18px}
273
263
  .rk-chat-send svg{width:16px;height:16px}
264
+ .rk-chat-send:disabled{opacity:.55;cursor:default}
274
265
  .rk-chat-hint{text-align:center;font-size:11.5px;color:var(--rk-text-faint);line-height:1.5}
275
266
  .rk-chat-hint kbd{font-family:var(--rk-font-mono);font-size:10.5px;background:var(--rk-n-100);
276
267
  border:1px solid var(--rk-border);border-radius:5px;padding:1px 5px;color:var(--rk-n-600)}
277
268
 
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 ── */
269
+ /* ── narrow: history becomes a slide-over, bubbles go full-width ── */
270
+ @media(max-width:900px){
271
+ .rk-chat-histtoggle{display:inline-flex}
272
+ .rk-chat-history{position:fixed;left:0;top:0;bottom:0;height:auto;z-index:70;width:min(300px,86vw);
273
+ transform:translateX(-100%);box-shadow:var(--rk-shadow-xl);transition:transform var(--rk-dur) var(--rk-ease)}
274
+ .rk-chat-history.is-open{transform:none}
275
+ .rk-chat-histbackdrop{display:block;position:fixed;inset:0;z-index:65;background:rgba(0,0,0,.28)}
276
+ .rk-chat-histbackdrop[hidden]{display:none}
277
+ }
287
278
  @media(max-width:720px){
288
279
  .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
280
  .rk-chat-title{font-size:1.5rem}
292
281
  .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
282
  .rk-chat-thread{gap:22px}
298
283
  }
299
284
  `;
300
285
  const script = `
301
286
  (function(){
302
287
  var root=document;
288
+
289
+ // ── read the real editor-chat boot (endpoint + auth + context) ──────────
290
+ var boot=null;
291
+ try{ var bootEl=root.getElementById('rk-chat-boot'); if(bootEl){ boot=JSON.parse(bootEl.textContent||'null'); } }catch(e){ boot=null; }
292
+ var ENDPOINT=(boot && boot.apiUrl) || '/api/v1/editor-chat';
293
+ var API_KEY=(boot && boot.vidfarmApiKey) || null;
294
+ var TEMPLATE=(boot && boot.template) || null;
295
+ var CACHED=(boot && boot.cachedContext) || null;
296
+
303
297
  var input=root.getElementById('rk-chat-input');
304
298
  var send=root.getElementById('rk-chat-send');
299
+ var SEND_HTML=${JSON.stringify('Send ' + SEND)};
300
+ var STOP_HTML=${JSON.stringify('Stop ' + STOP)};
305
301
  var thread=root.getElementById('rk-chat-thread');
306
302
  var title=root.getElementById('rk-chat-title');
307
303
  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
-
315
- function autosize(){ if(!input)return; input.style.height='auto'; input.style.height=Math.min(input.scrollHeight,148)+'px'; }
304
+
305
+ var history=[]; // [{role, text}]the running conversation
306
+ var threadId=null; // generated on first send, reused for persistence
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
+ });
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
+ }
350
+ function genId(p){ return p+'-'+Date.now().toString(36)+'-'+Math.random().toString(36).slice(2,8); }
351
+ function autosize(){ if(!input)return; input.style.height='auto'; input.style.height=Math.min(input.scrollHeight,210)+'px'; }
316
352
  function toBottom(){ try{ window.scrollTo({top:document.body.scrollHeight,behavior:'smooth'}); }catch(e){ window.scrollTo(0,document.body.scrollHeight); } }
317
353
  function nowLabel(){ try{ return new Date().toLocaleTimeString([], {hour:'numeric', minute:'2-digit'}); }catch(e){ return ''; } }
318
354
  function el(cls){ var d=root.createElement('div'); d.className=cls; return d; }
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
+ }
476
+
477
+ function clientContext(){
478
+ var now=new Date(); var local=null, tz=null;
479
+ try{ local=new Intl.DateTimeFormat(undefined,{dateStyle:'full',timeStyle:'long'}).format(now); }catch(e){ local=now.toString(); }
480
+ try{ tz=(Intl.DateTimeFormat().resolvedOptions().timeZone)||null; }catch(e){ tz=null; }
481
+ return { currentDateTime: now.toISOString(), localDateTime: local, timeZone: tz };
482
+ }
319
483
 
320
484
  function appendUser(text){
321
485
  var msg=el('rk-chat-msg is-user');
@@ -324,88 +488,364 @@ export function renderReskinChat() {
324
488
  msg.appendChild(meta); msg.appendChild(bub);
325
489
  if(thread) thread.appendChild(msg);
326
490
  }
491
+
492
+ // Returns a small controller for the streaming assistant bubble.
327
493
  function appendAssistant(){
328
494
  var msg=el('rk-chat-msg is-assistant');
329
495
  var meta=el('rk-chat-meta');
330
- 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>';
331
497
  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>';
498
+ var status=el('rk-chat-status');
499
+ status.innerHTML='<span class="rk-chat-typing"><span></span><span></span><span></span></span><span class="rk-chat-status-label">Thinking…</span>';
500
+ var notes=el('rk-chat-notes');
501
+ var textEl=el('rk-chat-stream'); textEl.hidden=true;
502
+ bub.appendChild(status); bub.appendChild(notes); bub.appendChild(textEl);
333
503
  msg.appendChild(meta); msg.appendChild(bub);
334
504
  if(thread) thread.appendChild(msg);
335
- return bub;
505
+ var lastText='';
506
+ return {
507
+ setStatus:function(s){ var l=status.querySelector('.rk-chat-status-label'); if(l && s){ l.textContent=s; } },
508
+ hideStatus:function(){ status.hidden=true; },
509
+ showText:function(){ status.hidden=true; textEl.hidden=false; },
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); } },
513
+ addNote:function(t){ var n=el('rk-chat-toolnote'); n.textContent=t; notes.appendChild(n); },
514
+ fail:function(m){ status.hidden=true; var er=el('rk-chat-error'); er.textContent=m||'Something went wrong.'; bub.appendChild(er); }
515
+ };
336
516
  }
337
517
 
338
- function sendMessage(){
339
- if(!input) return;
340
- var text=input.value.trim();
341
- if(!text) return;
342
- 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);
518
+ // Minimal SSE reader — mirrors the live client's text/event-stream parsing.
519
+ function readStream(response, h){
520
+ if(!response.body || !response.body.getReader){
521
+ // No streaming reader available — fall back to reading the whole body.
522
+ return response.text().then(function(t){ parseEvents(t, h); });
523
+ }
524
+ var reader=response.body.getReader();
525
+ var decoder=new TextDecoder();
526
+ var buffer='';
527
+ function pump(){
528
+ return reader.read().then(function(res){
529
+ if(res.done){ if(buffer){ parseEvents(buffer, h); buffer=''; } return; }
530
+ buffer+=decoder.decode(res.value,{stream:true});
531
+ var events=buffer.split('\\n\\n');
532
+ buffer=events.pop()||'';
533
+ for(var i=0;i<events.length;i++){ handleEvent(events[i], h); }
534
+ return pump();
535
+ });
536
+ }
537
+ return pump();
538
+ }
539
+ function parseEvents(blob, h){ blob.split('\\n\\n').forEach(function(ev){ handleEvent(ev, h); }); }
540
+ function handleEvent(raw, h){
541
+ var lines=raw.split('\\n');
542
+ var dataLine=null;
543
+ for(var j=0;j<lines.length;j++){ var t=lines[j].trim(); if(t.indexOf('data:')===0){ dataLine=t; break; } }
544
+ if(!dataLine) return;
545
+ var payload=dataLine.slice(5).trim();
546
+ if(payload==='[DONE]' || !payload) return;
547
+ var chunk; try{ chunk=JSON.parse(payload); }catch(e){ return; }
548
+ if(!chunk || typeof chunk!=='object') return;
549
+ if(chunk.type==='text-delta' && typeof chunk.delta==='string'){ h.onDelta(chunk.delta); }
550
+ 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); } }
551
+ else if(chunk.type==='tool-call' && typeof chunk.toolName==='string'){ h.onToolCall(chunk.toolName); }
552
+ else if(chunk.type==='error'){ h.onError((typeof chunk.errorText==='string')?chunk.errorText:((chunk.error && chunk.error.message)||'Unable to read assistant response.')); }
349
553
  }
350
554
 
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(); }
555
+ function sendMessage(text){
556
+ if(busy) return;
557
+ text=(text||'').trim();
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;
562
+
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);
569
+ if(input){ input.value=''; autosize(); }
570
+ attachments=[]; renderAttachChips();
571
+ toBottom();
572
+
573
+ var view=appendAssistant();
574
+ toBottom();
575
+ setBusy(true);
576
+
577
+ if(!threadId){ threadId=genId('thread'); setChatUrl(threadId, true); }
578
+ var userMsgId=genId('user');
579
+ var asstMsgId=genId('assistant');
580
+
581
+ var body={
582
+ messages: history.map(function(m){ return { role:m.role, content:[{ type:'text', text:m.text }] }; }),
583
+ thread_id: threadId,
584
+ thread_template_id: (TEMPLATE && TEMPLATE.templateId) || 'chat-brainstorm',
585
+ thread_title: (text||line).slice(0,42),
586
+ thread_tracers: (TEMPLATE && TEMPLATE.tracers) || [],
587
+ client_context: clientContext(),
588
+ user_message: { id:userMsgId, role:'user', text:text||line, attachments:atts },
589
+ assistant_message: { id:asstMsgId }
590
+ };
591
+ if(TEMPLATE){ body.template=TEMPLATE; }
592
+ if(CACHED){ body.cached_context=CACHED; }
593
+
594
+ var headers={ 'content-type':'application/json' };
595
+ if(API_KEY){ headers['vidfarm-api-key']=API_KEY; }
596
+
597
+ var acc='';
598
+ var sawText=false;
599
+ var errored=false;
600
+
601
+ var handlers={
602
+ onDelta:function(d){ if(!sawText){ sawText=true; view.showText(); } acc+=d; view.setText(acc); toBottom(); },
603
+ onStatus:function(s){ if(!sawText){ view.setStatus(s); } },
604
+ onToolCall:function(name){ view.addNote('Running '+name+'…'); toBottom(); },
605
+ onError:function(m){ errored=true; view.fail(m); toBottom(); }
606
+ };
607
+
608
+ var controller=(typeof AbortController!=='undefined') ? new AbortController() : null;
609
+ pendingAbort=controller;
610
+ var req;
611
+ try{
612
+ req=fetch(ENDPOINT,{ method:'POST', headers:headers, credentials:'same-origin', body:JSON.stringify(body), signal: controller?controller.signal:undefined });
613
+ }catch(err){
614
+ // Synchronous fetch failure (e.g. invalid URL) — never let it throw uncaught.
615
+ req=Promise.reject(err);
616
+ }
617
+
618
+ req.then(function(resp){
619
+ if(!resp.ok){
620
+ return resp.json().catch(function(){ return {}; }).then(function(j){
621
+ throw new Error((j && j.error) || ('Chat request failed ('+resp.status+').'));
622
+ });
623
+ }
624
+ return readStream(resp, handlers);
625
+ }).then(function(){
626
+ pendingAbort=null;
627
+ view.hideStatus();
628
+ if(sawText){
629
+ history.push({role:'assistant', text:acc});
630
+ view.renderMd();
631
+ } else if(!errored){
632
+ view.showText();
633
+ view.setText('The assistant finished without returning any text.');
634
+ }
635
+ setBusy(false);
636
+ if(input){ input.focus(); }
637
+ toBottom();
638
+ loadHistory();
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
+ }
651
+ var msg=(err && err.message) ? err.message : String(err);
652
+ if(!API_KEY){ msg=msg+'\\n\\nAdd an AI provider key in Settings to brainstorm on your own keys.'; }
653
+ view.fail(msg);
654
+ setBusy(false);
655
+ if(input){ input.focus(); }
656
+ toBottom();
356
657
  });
357
658
  }
358
659
 
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
- });
660
+ // ── real chat history (editor-chat threads) ────────────────────────────
661
+ var THREADS_URL=(boot && boot.threadsUrl) || '/api/v1/editor-chat/threads';
662
+ var TEMPLATE_ID=(TEMPLATE && TEMPLATE.templateId) || 'chat-brainstorm';
663
+ var histList=root.getElementById('rk-chat-history-list');
664
+ var histAside=root.getElementById('rk-chat-history');
665
+ var histBackdrop=root.getElementById('rk-chat-histbackdrop');
666
+ var histToggle=root.getElementById('rk-chat-histtoggle');
667
+ var loadingThread=false;
368
668
 
369
- // "New" resets the thread to a warm empty state
370
- root.getElementById('rk-chat-new') && root.getElementById('rk-chat-new').addEventListener('click',function(){
371
- if(thread){
372
- 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>'+
375
- '</div>';
376
- }
377
- if(title) title.textContent='New brainstorm';
378
- if(input){ input.focus(); }
669
+ function authHeaders(extra){ var h=extra||{}; if(API_KEY){ h['vidfarm-api-key']=API_KEY; } return h; }
670
+ function relTime(iso){
671
+ if(!iso) return '';
672
+ var t=Date.parse(iso); if(isNaN(t)) return '';
673
+ var s=Math.max(0,(Date.now()-t)/1000);
674
+ if(s<60) return 'just now';
675
+ if(s<3600) return Math.floor(s/60)+'m ago';
676
+ if(s<86400) return Math.floor(s/3600)+'h ago';
677
+ if(s<604800) return Math.floor(s/86400)+'d ago';
678
+ try{ return new Date(t).toLocaleDateString([], {month:'short', day:'numeric'}); }catch(e){ return ''; }
679
+ }
680
+ function histNote(text, opts){
681
+ var wrap=el('rk-chat-history-note');
682
+ var p=root.createElement('div'); p.textContent=text; wrap.appendChild(p);
683
+ if(opts && opts.href){ var a=root.createElement('a'); a.href=opts.href; a.textContent=opts.linkText||'Open'; wrap.appendChild(a); }
684
+ if(opts && opts.retry){ var b=root.createElement('button'); b.type='button'; b.textContent='Try again'; b.addEventListener('click',function(){ loadHistory(); }); wrap.appendChild(b); }
685
+ return wrap;
686
+ }
687
+ function setActiveThread(id){
688
+ threadId=id;
689
+ if(!histList) return;
690
+ var rows=histList.querySelectorAll('.rk-chat-hrow');
691
+ for(var i=0;i<rows.length;i++){ rows[i].classList.toggle('is-active', rows[i].getAttribute('data-id')===id); }
692
+ }
693
+ function renderHistory(threads){
694
+ if(!histList) return;
695
+ histList.innerHTML='';
696
+ if(!threads.length){ histList.appendChild(histNote('No conversations yet. Start one below \\u2014 it saves automatically.')); return; }
697
+ threads.forEach(function(t){
698
+ var row=root.createElement('button');
699
+ row.type='button'; row.className='rk-chat-hrow'; row.setAttribute('data-id', t.id);
700
+ if(t.id===threadId){ row.classList.add('is-active'); }
701
+ var main=el('rk-chat-hrow-main');
702
+ var ti=el('rk-chat-hrow-title'); ti.textContent=t.title || 'Untitled chat';
703
+ var sub=el('rk-chat-hrow-sub');
704
+ var count=(typeof t.messageCount==='number')?t.messageCount:0;
705
+ var when=relTime(t.lastMessageAt || t.updatedAt);
706
+ sub.textContent=(count?count+' message'+(count===1?'':'s'):'')+((count&&when)?' \\u00b7 ':'')+(when||'');
707
+ main.appendChild(ti); main.appendChild(sub);
708
+ var del=root.createElement('button');
709
+ del.type='button'; del.className='rk-chat-hrow-del'; del.setAttribute('aria-label','Delete chat'); del.textContent='\\u00d7';
710
+ del.addEventListener('click', function(ev){ ev.stopPropagation(); deleteThread(t.id, row); });
711
+ row.appendChild(main); row.appendChild(del);
712
+ row.addEventListener('click', function(){ openThread(t.id); });
713
+ histList.appendChild(row);
714
+ });
715
+ }
716
+ function loadHistory(){
717
+ if(!histList) return;
718
+ 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; }
719
+ var url=THREADS_URL+'?template_id='+encodeURIComponent(TEMPLATE_ID)+'&limit=30&include_messages=false';
720
+ fetch(url, { headers:authHeaders(), credentials:'same-origin' })
721
+ .then(function(r){
722
+ if(r.status===401||r.status===403){ return {threads:[], _auth:true}; }
723
+ if(!r.ok) throw new Error('http '+r.status);
724
+ return r.json();
725
+ })
726
+ .then(function(j){
727
+ if(j && j._auth){ histList.innerHTML=''; histList.appendChild(histNote('Sign in to sync your saved chats.', {href:'/login', linkText:'Sign in'})); return; }
728
+ renderHistory((j && j.threads) || []);
729
+ })
730
+ .catch(function(){ if(histList){ histList.innerHTML=''; histList.appendChild(histNote('Couldn\\u2019t load your chats.', {retry:true})); } });
731
+ }
732
+ function appendAssistantStatic(text){ var v=appendAssistant(); v.showText(); v.setText(text||''); v.hideStatus(); v.renderMd(); return v; }
733
+ function openThread(id, opts){
734
+ if(busy || loadingThread) return;
735
+ if(!(opts && opts.skipUrl)) setChatUrl(id, false);
736
+ loadingThread=true;
737
+ closeHistoryOverlay();
738
+ fetch(THREADS_URL+'/'+encodeURIComponent(id), { headers:authHeaders(), credentials:'same-origin' })
739
+ .then(function(r){ if(!r.ok) throw new Error('http '+r.status); return r.json(); })
740
+ .then(function(j){
741
+ var t=j && j.thread; if(!t){ throw new Error('missing thread'); }
742
+ history=[];
743
+ if(thread){ thread.innerHTML=''; }
744
+ var msgs=t.messages||[];
745
+ for(var i=0;i<msgs.length;i++){
746
+ var m=msgs[i]; var role=(m.role==='assistant')?'assistant':'user'; var txt=m.text||'';
747
+ history.push({role:role, text:txt});
748
+ if(role==='user'){ appendUser(txt); } else { appendAssistantStatic(txt); }
749
+ }
750
+ setActiveThread(id);
751
+ loadingThread=false;
752
+ toBottom();
753
+ })
754
+ .catch(function(){ loadingThread=false; });
755
+ }
756
+ function deleteThread(id, row){
757
+ if(!API_KEY) return;
758
+ fetch(THREADS_URL+'/'+encodeURIComponent(id), { method:'DELETE', headers:authHeaders(), credentials:'same-origin' })
759
+ .then(function(r){
760
+ if(!r.ok && r.status!==404) throw new Error('http '+r.status);
761
+ if(row && row.parentNode){ row.parentNode.removeChild(row); }
762
+ if(id===threadId){ resetChat(); }
763
+ if(histList && !histList.querySelector('.rk-chat-hrow')){ loadHistory(); }
764
+ })
765
+ .catch(function(){});
766
+ }
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){
795
+ history=[]; threadId=null;
796
+ if(!(opts && opts.skipUrl)) setChatUrl(null, false);
797
+ renderEmptyState();
798
+ setActiveThread(null);
799
+ if(input){ input.value=''; autosize(); input.focus(); }
379
800
  window.scrollTo(0,0);
801
+ }
802
+ function openHistoryOverlay(){ if(histAside){ histAside.classList.add('is-open'); } if(histBackdrop){ histBackdrop.hidden=false; } }
803
+ function closeHistoryOverlay(){ if(histAside){ histAside.classList.remove('is-open'); } if(histBackdrop){ histBackdrop.hidden=true; } }
804
+ if(histToggle){ histToggle.addEventListener('click', openHistoryOverlay); }
805
+ if(histBackdrop){ histBackdrop.addEventListener('click', closeHistoryOverlay); }
806
+ loadHistory();
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}); }
380
818
  });
381
819
 
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
- });
820
+ // composer wiring
821
+ if(input){ input.addEventListener('input',autosize); autosize(); }
822
+ if(send){ send.addEventListener('click',function(){ if(busy){ if(pendingAbort){ pendingAbort.abort(); } return; } if(input){ sendMessage(input.value); } }); }
823
+ if(input){
824
+ input.addEventListener('keydown',function(ev){
825
+ if(ev.key==='Enter' && !ev.shiftKey){ ev.preventDefault(); if(busy){ if(pendingAbort){ pendingAbort.abort(); } return; } sendMessage(input.value); }
398
826
  });
399
- root.addEventListener('click',function(ev){
400
- if(histMenu.hidden) return;
401
- if(!histMenu.contains(ev.target) && ev.target!==histBtn) closeHist();
827
+ }
828
+
829
+ // "New" resets the conversation to the warm empty state (shared resetChat)
830
+ var newBtn=root.getElementById('rk-chat-new');
831
+ if(newBtn){
832
+ newBtn.addEventListener('click',function(){ if(busy) return; resetChat(); });
833
+ }
834
+
835
+ // studio mode: choosing "Create Image" navigates to the inpaint workbench
836
+ var modeSel=root.getElementById('rk-chat-mode');
837
+ if(modeSel){
838
+ modeSel.addEventListener('change',function(){
839
+ var dest=modeSel.value;
840
+ if(dest && dest!=='/chat'){ window.location.href=dest; }
402
841
  });
403
842
  }
404
843
  })();`;
405
844
  return reskinDocument({
406
- 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.",
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.",
408
847
  activeSlug: "chat",
848
+ account: { name: input.name, email: input.email },
409
849
  pageCss,
410
850
  body,
411
851
  script