@mevdragon/vidfarm-devcli 0.20.5 → 0.20.8

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 (154) hide show
  1. package/.agents/skills/editor-capabilities/SKILL.md +15 -1
  2. package/.agents/skills/vidfarm-director/SKILL.md +106 -0
  3. package/.agents/skills/vidfarm-director/recipes/find-and-fork-template.md +16 -0
  4. package/.agents/skills/vidfarm-director/recipes/local-edit-render-approve.md +13 -0
  5. package/.agents/skills/vidfarm-director/recipes/onboard-a-new-director.md +13 -0
  6. package/.agents/skills/vidfarm-director/recipes/retheme-template.md +17 -0
  7. package/.agents/skills/vidfarm-director/references/assets-and-sourcing.md +111 -0
  8. package/.agents/skills/vidfarm-director/references/automation-and-local-dev.md +243 -0
  9. package/.agents/skills/vidfarm-director/references/core-workflows.md +273 -0
  10. package/.agents/skills/vidfarm-director/references/editor-workflows.md +365 -0
  11. package/.agents/skills/vidfarm-director/references/onboarding.md +28 -0
  12. package/.agents/skills/vidfarm-director/references/primitives.md +265 -0
  13. package/SKILL.director.md +521 -261
  14. package/dist/src/cli.js +404 -96
  15. package/dist/src/devcli/clips.js +15 -10
  16. package/dist/src/devcli/doctor.js +2 -2
  17. package/dist/src/devcli/local-backend.js +46 -2
  18. package/dist/src/services/clip-curation/cost.js +5 -1
  19. package/dist/src/services/clip-curation/gemini.js +10 -0
  20. package/package.json +30 -10
  21. package/SKILL.platform.md +0 -432
  22. package/demo/README.md +0 -28
  23. package/demo/dist/app.css +0 -1
  24. package/demo/dist/app.js +0 -1850
  25. package/demo/dist/chunks/chunk-DXB73IDG.js +0 -1
  26. package/demo/dist/chunks/chunk-S7OWAJDS.js +0 -36
  27. package/demo/dist/chunks/chunk-VTIBZ6AN.js +0 -1
  28. package/demo/dist/chunks/dist-ADSJKBVE.js +0 -332
  29. package/demo/dist/chunks/domEditingLayers-VZMLL4AP-SGHWPND4.js +0 -1
  30. package/demo/dist/chunks/hyperframes-player-XB65TCD6.js +0 -425
  31. package/demo/dist/chunks/lib-XAQ37YOE.js +0 -1
  32. package/demo/dist/chunks/src-TJ2QYA4U.js +0 -207
  33. package/demo/dist/favicon.ico +0 -0
  34. package/demo/dist/icons/timeline/audio.svg +0 -7
  35. package/demo/dist/icons/timeline/captions.svg +0 -5
  36. package/demo/dist/icons/timeline/composition.svg +0 -12
  37. package/demo/dist/icons/timeline/image.svg +0 -18
  38. package/demo/dist/icons/timeline/music.svg +0 -10
  39. package/demo/dist/icons/timeline/text.svg +0 -3
  40. package/demo/dist/index.html +0 -15
  41. package/dist/src/account-pages-legacy.js +0 -9396
  42. package/dist/src/account-pages.js +0 -61
  43. package/dist/src/app.js +0 -21602
  44. package/dist/src/composition-runtime.js +0 -1053
  45. package/dist/src/config.js +0 -217
  46. package/dist/src/context.js +0 -447
  47. package/dist/src/dev-app-legacy.js +0 -739
  48. package/dist/src/dev-app.js +0 -6
  49. package/dist/src/devcli/migrate-local.js +0 -140
  50. package/dist/src/devcli/sync.js +0 -368
  51. package/dist/src/domain.js +0 -5
  52. package/dist/src/editor-chat-history.js +0 -82
  53. package/dist/src/editor-chat.js +0 -756
  54. package/dist/src/editor-dark-theme.js +0 -1128
  55. package/dist/src/frontend/debug.js +0 -71
  56. package/dist/src/frontend/discover-client.js +0 -130
  57. package/dist/src/frontend/discover-store.js +0 -23
  58. package/dist/src/frontend/file-directory.js +0 -1018
  59. package/dist/src/frontend/flockposter-cache-store.js +0 -124
  60. package/dist/src/frontend/homepage-client.js +0 -446
  61. package/dist/src/frontend/homepage-shared.js +0 -201
  62. package/dist/src/frontend/homepage-store.js +0 -66
  63. package/dist/src/frontend/homepage-view.js +0 -705
  64. package/dist/src/frontend/page-runtime-client.js +0 -132
  65. package/dist/src/frontend/page-runtime-store.js +0 -9
  66. package/dist/src/frontend/sentry.js +0 -42
  67. package/dist/src/frontend/template-editor-chat.js +0 -4181
  68. package/dist/src/help-page.js +0 -346
  69. package/dist/src/homepage.js +0 -1458
  70. package/dist/src/index.js +0 -16
  71. package/dist/src/instrument.js +0 -30
  72. package/dist/src/landing-page.js +0 -384
  73. package/dist/src/page-runtime.js +0 -2
  74. package/dist/src/page-shell.js +0 -1452
  75. package/dist/src/primitive-context.js +0 -416
  76. package/dist/src/primitive-registry.js +0 -3940
  77. package/dist/src/primitive-sdk.js +0 -4
  78. package/dist/src/primitives/hyperframes-media.js +0 -108
  79. package/dist/src/react-page-shell.js +0 -35
  80. package/dist/src/ready-post-schedule-component.js +0 -1540
  81. package/dist/src/registry.js +0 -296
  82. package/dist/src/reskin/agency-page.js +0 -299
  83. package/dist/src/reskin/calendar-page.js +0 -568
  84. package/dist/src/reskin/chat-page.js +0 -942
  85. package/dist/src/reskin/discover-page.js +0 -1788
  86. package/dist/src/reskin/document.js +0 -1587
  87. package/dist/src/reskin/help-page.js +0 -357
  88. package/dist/src/reskin/index-page.js +0 -62
  89. package/dist/src/reskin/inpaint-clipper-page.js +0 -890
  90. package/dist/src/reskin/inpaint-page.js +0 -2554
  91. package/dist/src/reskin/inpaint-video-page.js +0 -1339
  92. package/dist/src/reskin/job-runs-page.js +0 -477
  93. package/dist/src/reskin/library-page.js +0 -1634
  94. package/dist/src/reskin/login-page.js +0 -262
  95. package/dist/src/reskin/portfolio-page.js +0 -687
  96. package/dist/src/reskin/pricing-page.js +0 -390
  97. package/dist/src/reskin/settings-page.js +0 -732
  98. package/dist/src/reskin/theme.js +0 -711
  99. package/dist/src/runtime.js +0 -35
  100. package/dist/src/services/api-call-history.js +0 -249
  101. package/dist/src/services/auth.js +0 -152
  102. package/dist/src/services/billing-pricing.js +0 -39
  103. package/dist/src/services/billing.js +0 -241
  104. package/dist/src/services/cast.js +0 -127
  105. package/dist/src/services/chat-threads.js +0 -92
  106. package/dist/src/services/clip-records.js +0 -250
  107. package/dist/src/services/clip-search.js +0 -77
  108. package/dist/src/services/clip-vectors.js +0 -125
  109. package/dist/src/services/composition-sanitize.js +0 -124
  110. package/dist/src/services/composition-watch.js +0 -79
  111. package/dist/src/services/elevenlabs.js +0 -222
  112. package/dist/src/services/file-directory.js +0 -117
  113. package/dist/src/services/fork-access.js +0 -93
  114. package/dist/src/services/fork-manifest.js +0 -42
  115. package/dist/src/services/ghostcut.js +0 -179
  116. package/dist/src/services/hyperframes.js +0 -3517
  117. package/dist/src/services/job-capacity.js +0 -14
  118. package/dist/src/services/job-logs.js +0 -197
  119. package/dist/src/services/jobs.js +0 -136
  120. package/dist/src/services/local-dynamo.js +0 -0
  121. package/dist/src/services/media-processing.js +0 -766
  122. package/dist/src/services/primitive-media-lambda.js +0 -280
  123. package/dist/src/services/providers.js +0 -2748
  124. package/dist/src/services/rate-limits.js +0 -262
  125. package/dist/src/services/scene-annotations.js +0 -32
  126. package/dist/src/services/serverless-auth.js +0 -382
  127. package/dist/src/services/serverless-jobs.js +0 -1084
  128. package/dist/src/services/serverless-provider-keys.js +0 -409
  129. package/dist/src/services/serverless-records.js +0 -1515
  130. package/dist/src/services/serverless-template-configs.js +0 -75
  131. package/dist/src/services/storage.js +0 -461
  132. package/dist/src/services/swipe-customize.js +0 -437
  133. package/dist/src/services/template-certification.js +0 -413
  134. package/dist/src/services/template-loader.js +0 -99
  135. package/dist/src/services/template-runtime-bundles.js +0 -217
  136. package/dist/src/services/template-sources.js +0 -1017
  137. package/dist/src/services/upstream.js +0 -248
  138. package/dist/src/services/video-normalization.js +0 -2
  139. package/dist/src/services/webhooks.js +0 -62
  140. package/dist/src/template-editor-pages.js +0 -2576
  141. package/dist/src/template-editor-shell.js +0 -2893
  142. package/dist/src/template-sdk.js +0 -4
  143. package/dist/src/worker.js +0 -17
  144. package/public/assets/discover-client-app.js +0 -1
  145. package/public/assets/file-directory-app.js +0 -3
  146. package/public/assets/homepage-app.js +0 -54
  147. package/public/assets/homepage-client-app.js +0 -80
  148. package/public/assets/page-runtime-client-app.js +0 -94
  149. package/public/assets/placeholders/scene-placeholder.png +0 -0
  150. package/src/assets/SELLING_AWARENESS_STAGES.md +0 -579
  151. package/src/assets/SELLING_WITH_HOOKS.md +0 -377
  152. package/src/assets/SELLING_WITH_VSLS.md +0 -606
  153. package/src/assets/favicon.ico +0 -0
  154. package/src/assets/logo-vidfarm.png +0 -0
@@ -1,942 +0,0 @@
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
- //
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.
9
- //
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.
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
- };
29
- // ── icons ───────────────────────────────────────────────────────────────────
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
- 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");
40
- const body = `
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>
56
- </div>
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="/tools/image">Create Image</option>
62
- <option value="/editor/original/fork/new">Create Video</option>
63
- <option value="/tools/clipper">Clip Media</option>
64
- </select>
65
- <span class="rk-chat-mode-chev" aria-hidden="true">&#9662;</span>
66
- </label>
67
- </div>
68
- </header>
69
-
70
- <div class="rk-chat-thread" id="rk-chat-thread"></div>
71
-
72
- <div class="rk-chat-dock">
73
- <div class="rk-chat-chips" id="rk-chat-chips" aria-label="Attached files"></div>
74
- <div class="rk-chat-composer" id="rk-chat-composer">
75
- <textarea id="rk-chat-input" class="rk-chat-input" rows="3"
76
- placeholder="Describe your offer, audience, or the problem to solve…"></textarea>
77
- <div class="rk-chat-composer-actions">
78
- <button type="button" class="rk-chat-attach" id="rk-chat-attach" aria-label="Attach a file" title="Attach a file">${CLIP}</button>
79
- <button type="button" class="rk-btn rk-btn-gold rk-chat-send" id="rk-chat-send">Send ${SEND}</button>
80
- </div>
81
- </div>
82
- <p class="rk-chat-hint"><kbd>Enter</kbd> to send · <kbd>Shift</kbd>+<kbd>Enter</kbd> for a new line</p>
83
- </div>
84
- </div>
85
- </div>
86
- <script type="application/json" id="rk-chat-boot">${bootJson}</script>
87
- </main>`;
88
- const pageCss = `
89
- /* ── two-pane: history sidebar + centered chat column ─────────────────────── */
90
- .rk-chat-page{display:flex;width:100%;background:var(--rk-bg);min-height:100vh}
91
- .rk-chat-main{flex:1;min-width:0;display:flex;justify-content:center;padding:0 var(--rk-gutter)}
92
- .rk-chat-col{width:min(800px,100%);margin:0 auto;display:flex;flex-direction:column;
93
- min-height:100vh;padding-top:34px}
94
-
95
- /* history sidebar (real editor-chat threads) */
96
- .rk-chat-history{flex:none;width:266px;background:var(--rk-surface);border-right:1px solid var(--rk-border);
97
- display:flex;flex-direction:column;position:sticky;top:0;height:100vh}
98
- .rk-chat-history-head{display:flex;align-items:center;justify-content:space-between;gap:10px;
99
- padding:20px 16px 12px;flex:none}
100
- .rk-chat-history-title{font-family:var(--rk-font-display);font-weight:800;font-size:16px;color:var(--rk-ink);letter-spacing:-.01em}
101
- .rk-chat-newbtn{display:inline-flex;align-items:center;gap:5px;padding:7px 13px;border-radius:var(--rk-r-full);
102
- border:1px solid var(--rk-gold-600);background:var(--rk-gold-tint);color:var(--rk-ink);cursor:pointer;
103
- font-family:var(--rk-font-body);font-size:12.5px;font-weight:700;transition:background var(--rk-dur) var(--rk-ease)}
104
- .rk-chat-newbtn:hover{background:var(--rk-gold-tint2)}
105
- .rk-chat-history-list{flex:1;min-height:0;overflow-y:auto;padding:4px 10px 16px;display:grid;gap:2px;align-content:start}
106
- .rk-chat-hrow{display:flex;align-items:center;gap:8px;width:100%;padding:9px 10px;border-radius:var(--rk-r-lg);
107
- border:0;background:transparent;cursor:pointer;text-align:left;font-family:inherit;min-width:0;
108
- transition:background var(--rk-dur) var(--rk-ease),box-shadow var(--rk-dur) var(--rk-ease)}
109
- .rk-chat-hrow:hover{background:var(--rk-n-50);box-shadow:var(--rk-shadow-xs)}
110
- .rk-chat-hrow.is-active{background:var(--rk-gold-tint)}
111
- .rk-chat-hrow-main{flex:1;min-width:0;display:grid;gap:1px}
112
- .rk-chat-hrow-title{font-size:13px;font-weight:600;color:var(--rk-ink);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
113
- .rk-chat-hrow-sub{font-size:11px;color:var(--rk-text-muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
114
- .rk-chat-hrow-del{flex:none;border:0;background:transparent;color:var(--rk-n-400);cursor:pointer;font-size:15px;
115
- line-height:1;padding:2px 5px;border-radius:var(--rk-r-sm);opacity:0;
116
- transition:opacity var(--rk-dur) var(--rk-ease),color var(--rk-dur) var(--rk-ease),background var(--rk-dur) var(--rk-ease)}
117
- .rk-chat-hrow:hover .rk-chat-hrow-del{opacity:1}
118
- /* touch: the history panel is the primary mobile surface — keep delete visible */
119
- @media(hover:none){.rk-chat-hrow-del{opacity:1}}
120
- .rk-chat-hrow-del:hover{color:var(--rk-red);background:var(--rk-red-tint)}
121
- .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}
122
- .rk-chat-history-note a{color:var(--rk-gold-700);font-weight:600}
123
- .rk-chat-history-note button{font-family:inherit;font-size:12px;font-weight:600;color:var(--rk-ink);cursor:pointer;
124
- background:#fff;border:1px solid var(--rk-border);border-radius:var(--rk-r-full);padding:5px 12px}
125
- .rk-chat-history-note button:hover{background:var(--rk-n-100)}
126
-
127
- /* narrow-screen history toggle + slide-over (hidden on wide) */
128
- .rk-chat-histtoggle{display:none;flex:none;width:34px;height:34px;border-radius:var(--rk-r-lg);
129
- border:1px solid var(--rk-border);background:#fff;color:var(--rk-n-600);cursor:pointer;font-size:15px;
130
- align-items:center;justify-content:center;margin-right:2px;margin-top:2px}
131
- .rk-chat-histbackdrop{display:none}
132
-
133
- /* compact header — sticky so the title stays pinned while the thread scrolls */
134
- .rk-chat-head{display:flex;align-items:flex-start;gap:12px;
135
- position:sticky;top:0;z-index:20;background:var(--rk-bg);
136
- padding-top:18px;margin-top:-18px;padding-bottom:18px;margin-bottom:6px;border-bottom:1px solid var(--rk-border)}
137
- .rk-chat-head-titles{display:grid;gap:5px;min-width:0;flex:1}
138
- .rk-chat-title{font-family:var(--rk-font-display);font-weight:800;font-size:1.6rem;letter-spacing:-.025em;
139
- line-height:1.15;color:var(--rk-ink);min-width:0}
140
- .rk-chat-subhead{font-size:14.5px;color:var(--rk-text-muted);line-height:1.5;max-width:56ch}
141
- .rk-chat-head-actions{display:flex;align-items:center;gap:9px;flex:none}
142
- .rk-chat-plus{font-size:14px;font-weight:700;line-height:1;margin-right:1px}
143
-
144
- /* studio mode select (Chat ↔ Create Image → /tools/image ↔ Create Video → /editor).
145
- A prominent gold pill so it reads clearly as the "what do you want to make?"
146
- switcher, not a faint afterthought. */
147
- .rk-chat-mode{position:relative;display:inline-flex;align-items:center}
148
- .rk-chat-mode-select{appearance:none;-webkit-appearance:none;font-family:var(--rk-font-body);font-size:13.5px;font-weight:800;
149
- letter-spacing:-.01em;color:var(--rk-ink);background:var(--rk-gold-tint);border:1.5px solid var(--rk-gold-600);
150
- border-radius:var(--rk-r-full);padding:10px 38px 10px 17px;cursor:pointer;box-shadow:var(--rk-shadow-sm);
151
- transition:border-color var(--rk-dur) var(--rk-ease),background var(--rk-dur) var(--rk-ease),box-shadow var(--rk-dur) var(--rk-ease)}
152
- .rk-chat-mode-select:hover{background:var(--rk-gold-tint2);border-color:var(--rk-gold-700);box-shadow:var(--rk-shadow-md,var(--rk-shadow-sm))}
153
- .rk-chat-mode-select:focus{outline:none;border-color:var(--rk-gold-700);box-shadow:var(--rk-ring-gold)}
154
- .rk-chat-mode-chev{position:absolute;right:15px;top:50%;transform:translateY(-50%);pointer-events:none;font-size:12px;
155
- font-weight:900;color:var(--rk-gold-700)}
156
-
157
- /* thread — flex:1 pushes the dock to the bottom when the page is short */
158
- .rk-chat-thread{flex:1;display:grid;gap:28px;align-content:start;padding:30px 0 26px}
159
- .rk-chat-msg{display:grid;gap:8px;min-width:0}
160
- .rk-chat-msg.is-user{justify-items:end}
161
- .rk-chat-meta{display:flex;align-items:center;gap:7px;font-size:11.5px;font-weight:700;letter-spacing:.05em;
162
- text-transform:uppercase;color:var(--rk-text-faint);padding:0 4px}
163
- .rk-chat-time{font-weight:600;letter-spacing:.02em;color:var(--rk-n-300);text-transform:none;font-size:11px}
164
- .rk-chat-avatar{width:20px;height:20px;border-radius:6px;flex:none;display:grid;place-items:center;color:#fff;
165
- font-family:var(--rk-font-display);font-weight:800;font-size:11px;line-height:1;
166
- background:linear-gradient(142deg,var(--rk-gold-500),var(--rk-violet))}
167
-
168
- /* bubbles — uniform 1px hairline on all four sides, no colored side stripes.
169
- overflow-wrap keeps a long pasted URL/token in a USER bubble from forcing
170
- horizontal page scroll on phones (assistant text already wraps via -stream). */
171
- .rk-chat-bubble{border-radius:var(--rk-r-2xl);padding:16px 20px;font-size:15px;line-height:1.65;overflow-wrap:anywhere}
172
- .rk-chat-bubble-user{max-width:min(78%,560px);background:var(--rk-ink);color:#f4f4f5;
173
- border:1px solid var(--rk-n-800);box-shadow:var(--rk-shadow-sm)}
174
- .rk-chat-bubble-assistant{width:100%;background:var(--rk-surface);color:var(--rk-n-700);
175
- border:1px solid var(--rk-border);box-shadow:var(--rk-shadow-card)}
176
- .rk-chat-bubble-assistant strong{color:var(--rk-ink);font-weight:700}
177
- .rk-chat-bubble-assistant em{font-style:italic}
178
- .rk-chat-bubble-assistant a{color:var(--rk-gold-700);font-weight:600;text-decoration:underline;text-underline-offset:2px;overflow-wrap:anywhere}
179
-
180
- /* streamed assistant text — pre-wrap keeps the model's own line breaks */
181
- .rk-chat-stream{white-space:pre-wrap;overflow-wrap:anywhere;color:var(--rk-n-700);line-height:1.65}
182
- .rk-chat-stream a{color:var(--rk-gold-700);font-weight:600;text-decoration:underline;text-underline-offset:2px;overflow-wrap:anywhere}
183
- .rk-chat-stream strong{color:var(--rk-ink);font-weight:700}
184
- .rk-chat-stream em{font-style:italic}
185
- .rk-chat-stream[hidden]{display:none}
186
-
187
- /* thinking / status row */
188
- .rk-chat-status{display:flex;align-items:center;gap:10px;color:var(--rk-text-faint);font-size:13.5px}
189
- .rk-chat-status[hidden]{display:none}
190
- .rk-chat-status-label{font-size:13px;color:var(--rk-text-muted)}
191
-
192
- /* simplified tool-call notes (rich artifact/HTTP cards are intentionally omitted) */
193
- .rk-chat-notes{display:grid;gap:6px;margin-bottom:10px}
194
- .rk-chat-notes:empty{display:none}
195
- .rk-chat-toolnote{display:inline-flex;align-items:center;gap:7px;font-size:12.5px;color:var(--rk-text-muted);
196
- font-family:var(--rk-font-mono);background:var(--rk-n-50);border:1px solid var(--rk-border);
197
- border-radius:var(--rk-r-lg);padding:5px 10px;width:fit-content}
198
- .rk-chat-toolnote::before{content:"";width:6px;height:6px;border-radius:50%;background:var(--rk-gold-500);flex:none}
199
-
200
- /* error surface */
201
- .rk-chat-error{margin-top:2px;font-size:13.5px;color:#b42318;background:var(--rk-n-50);
202
- border:1px solid var(--rk-border);border-radius:var(--rk-r-lg);padding:10px 12px;white-space:pre-wrap}
203
-
204
- /* typing indicator */
205
- .rk-chat-typing{display:inline-flex;gap:4px;align-items:center;padding:2px 0}
206
- .rk-chat-typing span{width:7px;height:7px;border-radius:50%;background:var(--rk-n-400);
207
- animation:rk-chat-blink 1.2s var(--rk-ease) infinite}
208
- .rk-chat-typing span:nth-child(2){animation-delay:.18s}
209
- .rk-chat-typing span:nth-child(3){animation-delay:.36s}
210
- @keyframes rk-chat-blink{0%,80%,100%{opacity:.28;transform:translateY(0)}40%{opacity:1;transform:translateY(-2px)}}
211
-
212
- /* ── quiet sticky composer dock ── */
213
- .rk-chat-dock{position:sticky;bottom:0;z-index:10;background:var(--rk-bg);
214
- padding:16px 0 22px;display:grid;gap:12px}
215
- .rk-chat-dock::before{content:"";position:absolute;left:0;right:0;top:-26px;height:26px;pointer-events:none;
216
- background:linear-gradient(to top,var(--rk-bg),transparent)}
217
- .rk-chat-starters{display:flex;flex-wrap:wrap;gap:9px}
218
- .rk-chat-chip{display:inline-flex;align-items:center;gap:7px;padding:8px 14px;border-radius:var(--rk-r-full);
219
- font-family:var(--rk-font-body);font-size:13px;font-weight:600;color:var(--rk-n-700);cursor:pointer;
220
- background:var(--rk-surface);border:1px solid var(--rk-border);box-shadow:var(--rk-shadow-xs);text-align:left;
221
- transition:transform var(--rk-dur) var(--rk-ease),box-shadow var(--rk-dur) var(--rk-ease),border-color var(--rk-dur) var(--rk-ease),background var(--rk-dur) var(--rk-ease)}
222
- .rk-chat-chip:hover{border-color:var(--rk-gold-600);background:var(--rk-gold-tint);color:var(--rk-ink);
223
- transform:translateY(-1px);box-shadow:var(--rk-shadow-sm)}
224
- .rk-chat-chip:disabled{opacity:.5;cursor:default;transform:none;box-shadow:var(--rk-shadow-xs)}
225
-
226
- /* empty new-chat state — no greeting, just a few subtle suggestions */
227
- .rk-chat-empty{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:14px;
228
- min-height:min(46vh,360px);text-align:center;padding:24px 0}
229
- .rk-chat-empty-hint{font-size:13px;font-weight:600;letter-spacing:.02em;text-transform:uppercase;color:var(--rk-text-faint)}
230
- .rk-chat-suggests{display:flex;flex-direction:column;gap:8px;width:min(420px,100%)}
231
- .rk-chat-suggest{display:block;width:100%;text-align:left;padding:12px 15px;border-radius:var(--rk-r-xl);
232
- border:1px solid var(--rk-border);background:var(--rk-surface);color:var(--rk-n-600);cursor:pointer;
233
- font-family:var(--rk-font-body);font-size:13.5px;font-weight:500;line-height:1.4;box-shadow:var(--rk-shadow-xs);
234
- 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)}
235
- .rk-chat-suggest:hover{border-color:var(--rk-gold-600);background:var(--rk-gold-tint);color:var(--rk-ink);
236
- transform:translateY(-1px);box-shadow:var(--rk-shadow-sm)}
237
- /* composer = one bordered "text area" box: textarea fills the top, the attach +
238
- send controls sit inside it along the bottom row */
239
- .rk-chat-composer{display:flex;flex-direction:column;gap:9px;background:var(--rk-surface);
240
- border:1px solid var(--rk-border-strong);border-radius:var(--rk-r-2xl);padding:11px 12px 10px;
241
- box-shadow:var(--rk-shadow-card);transition:border-color var(--rk-dur) var(--rk-ease),box-shadow var(--rk-dur) var(--rk-ease)}
242
- .rk-chat-composer:focus-within{border-color:var(--rk-gold-600);box-shadow:var(--rk-ring-gold)}
243
- .rk-chat-composer-actions{display:flex;align-items:center;gap:9px}
244
- .rk-chat-composer-actions .rk-chat-send{margin-left:auto}
245
-
246
- /* attach button — opens the file-explorer drawer (chrome script wires it) */
247
- .rk-chat-attach{flex:none;width:38px;height:38px;border-radius:var(--rk-r-full);border:1px solid var(--rk-border);
248
- background:#fff;color:var(--rk-n-500);cursor:pointer;display:grid;place-items:center;
249
- transition:background var(--rk-dur) var(--rk-ease),border-color var(--rk-dur) var(--rk-ease),color var(--rk-dur) var(--rk-ease)}
250
- .rk-chat-attach:hover{border-color:var(--rk-gold-600);background:var(--rk-gold-tint);color:var(--rk-ink)}
251
- .rk-chat-attach svg{width:18px;height:18px}
252
-
253
- /* attached-file chips (populated when a file is picked from the drawer) */
254
- .rk-chat-chips{display:flex;flex-wrap:wrap;gap:7px}
255
- .rk-chat-chips:empty{display:none}
256
- .rk-chat-attach-chip{display:inline-flex;align-items:center;gap:5px;padding:5px 8px 5px 12px;border-radius:var(--rk-r-full);
257
- background:var(--rk-gold-tint2);color:var(--rk-gold-700);font-size:12.5px;font-weight:600;max-width:230px}
258
- .rk-chat-attach-chip span{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
259
- .rk-chat-attach-chip button{border:0;background:transparent;color:inherit;cursor:pointer;font-size:15px;line-height:1;padding:0 2px}
260
- .rk-chat-attach-chip .rk-chat-attach-preview{font-family:inherit;font-size:12.5px;font-weight:600;padding:0;max-width:200px;
261
- white-space:nowrap;overflow:hidden;text-overflow:ellipsis;text-decoration:underline;text-underline-offset:2px;text-decoration-color:transparent}
262
- .rk-chat-attach-chip .rk-chat-attach-preview:hover{text-decoration-color:currentColor}
263
- .rk-chat-attach-chip.is-pending{opacity:.75}
264
- .rk-chat-attach-chip.is-pending .rk-chat-attach-preview{text-decoration:none;cursor:default}
265
- .rk-chat-input{width:100%;min-width:0;min-height:76px;border:0;background:transparent;resize:none;font-family:var(--rk-font-body);
266
- font-size:15px;line-height:1.5;color:var(--rk-ink);padding:4px 4px 0;max-height:210px}
267
- .rk-chat-input:focus{outline:none;box-shadow:none}
268
- .rk-chat-input::placeholder{color:var(--rk-n-400)}
269
- .rk-chat-send{flex:none;padding:11px 18px}
270
- .rk-chat-send svg{width:16px;height:16px}
271
- .rk-chat-send:disabled{opacity:.55;cursor:default}
272
- .rk-chat-hint{text-align:center;font-size:11.5px;color:var(--rk-text-faint);line-height:1.5}
273
- .rk-chat-hint kbd{font-family:var(--rk-font-mono);font-size:10.5px;background:var(--rk-n-100);
274
- border:1px solid var(--rk-border);border-radius:5px;padding:1px 5px;color:var(--rk-n-600)}
275
-
276
- /* ── narrow: history becomes a slide-over, bubbles go full-width ── */
277
- @media(max-width:900px){
278
- .rk-chat-histtoggle{display:inline-flex}
279
- .rk-chat-history{position:fixed;left:0;top:0;bottom:0;height:auto;z-index:70;width:min(300px,86vw);
280
- transform:translateX(-100%);box-shadow:var(--rk-shadow-xl);transition:transform var(--rk-dur) var(--rk-ease)}
281
- .rk-chat-history.is-open{transform:none}
282
- .rk-chat-histbackdrop{display:block;position:fixed;inset:0;z-index:65;background:rgba(0,0,0,.28)}
283
- .rk-chat-histbackdrop[hidden]{display:none}
284
- }
285
- @media(max-width:720px){
286
- .rk-chat-col{padding-top:24px}
287
- .rk-chat-title{font-size:1.5rem}
288
- .rk-chat-bubble-user{max-width:100%}
289
- .rk-chat-thread{gap:22px}
290
- }
291
- `;
292
- const script = `
293
- (function(){
294
- var root=document;
295
-
296
- // ── read the real editor-chat boot (endpoint + auth + context) ──────────
297
- var boot=null;
298
- try{ var bootEl=root.getElementById('rk-chat-boot'); if(bootEl){ boot=JSON.parse(bootEl.textContent||'null'); } }catch(e){ boot=null; }
299
- var ENDPOINT=(boot && boot.apiUrl) || '/api/v1/editor-chat';
300
- var API_KEY=(boot && boot.vidfarmApiKey) || null;
301
- var TEMPLATE=(boot && boot.template) || null;
302
- var CACHED=(boot && boot.cachedContext) || null;
303
-
304
- var input=root.getElementById('rk-chat-input');
305
- var send=root.getElementById('rk-chat-send');
306
- var SEND_HTML=${JSON.stringify('Send ' + SEND)};
307
- var STOP_HTML=${JSON.stringify('Stop ' + STOP)};
308
- var thread=root.getElementById('rk-chat-thread');
309
- var title=root.getElementById('rk-chat-title');
310
- var AVATAR='V';
311
-
312
- var history=[]; // [{role, text}] — the running conversation
313
- var threadId=null; // generated on first send, reused for persistence
314
- var busy=false;
315
- var pendingAbort=null; // AbortController for the in-flight reply (Stop button)
316
- var attachments=[]; // files picked from the file-explorer drawer
317
- var chipsEl=root.getElementById('rk-chat-chips');
318
-
319
- // Render the pending-attachment chips above the composer.
320
- function renderAttachChips(){
321
- if(!chipsEl) return;
322
- chipsEl.innerHTML='';
323
- attachments.forEach(function(f, i){
324
- var chip=el('rk-chat-attach-chip'+(f.pending?' is-pending':''));
325
- if(f.pending){ var sp=root.createElement('span'); sp.className='rk-aichat-chip-spin'; sp.setAttribute('aria-hidden','true'); chip.appendChild(sp); }
326
- var label=root.createElement('button'); label.type='button'; label.className='rk-chat-attach-preview';
327
- label.title=f.pending?('Uploading '+f.name+'\\u2026'):('Preview '+f.name); label.textContent=f.name;
328
- if(f.viewUrl && !f.pending){ label.addEventListener('click',function(){ var vs=attachments.filter(function(a){return !a.pending;}); openFilePreview(vs, vs.indexOf(f)); }); }
329
- chip.appendChild(label);
330
- var x=root.createElement('button'); x.type='button'; x.setAttribute('aria-label','Remove '+f.name); x.textContent='\\u00d7';
331
- x.addEventListener('click',function(){ attachments=attachments.filter(function(a){ return a.id!==f.id; }); renderAttachChips(); });
332
- chip.appendChild(x); chipsEl.appendChild(chip);
333
- });
334
- }
335
- // The file-explorer drawer (chrome script) hands picked files over via a DOM
336
- // event — it lives in a separate script, so a CustomEvent is the bridge.
337
- document.addEventListener('rk-chat-attach-file',function(ev){
338
- var d=(ev && ev.detail)||{};
339
- if(!d.id || !d.viewUrl) return;
340
- if(attachments.some(function(a){ return a.id===String(d.id); })) return;
341
- attachments.push({ id:String(d.id), name:d.name||'file', contentType:d.contentType||'application/octet-stream', viewUrl:d.viewUrl });
342
- renderAttachChips();
343
- if(input) input.focus();
344
- });
345
-
346
- // ─── paste-to-attach: a copied image/file dropped into the composer gets
347
- // auto-uploaded to the user's /temp folder, then attached like a picked file.
348
- function pasteExtName(blob, idx){
349
- var ct=(blob && blob.type)||'';
350
- var base=ct.indexOf('image/')===0?'pasted-image':ct.indexOf('video/')===0?'pasted-video':ct.indexOf('audio/')===0?'pasted-audio':'pasted-file';
351
- var ext=ct && ct.indexOf('/')>-1?ct.split('/')[1].split('+')[0].split(';')[0]:'bin';
352
- if(ext==='jpeg') ext='jpg';
353
- return base+'-'+Date.now().toString(36)+(idx?'-'+idx:'')+'.'+ext;
354
- }
355
- function uploadPasted(blob, fileName, pendingId){
356
- var fd=new FormData(); fd.append('file', blob, fileName);
357
- var h={}; if(API_KEY) h['vidfarm-api-key']=API_KEY;
358
- fetch(window.location.origin+'/api/v1/user/me/temporary-files/upload', { method:'POST', credentials:'same-origin', headers:h, body:fd })
359
- .then(function(r){ return r.json().catch(function(){ return {}; }).then(function(j){ if(!r.ok) throw new Error((j&&j.error)||('upload failed ('+r.status+')')); return j; }); })
360
- .then(function(j){
361
- var f=j&&j.file; if(!f||!f.viewUrl) throw new Error('upload returned no file');
362
- var idx=attachments.map(function(a){ return a.id; }).indexOf(pendingId);
363
- var item={ id:String(f.id), name:f.fileName||fileName, contentType:f.contentType||(blob&&blob.type)||'application/octet-stream', viewUrl:f.viewUrl };
364
- if(idx>-1) attachments[idx]=item; else attachments.push(item);
365
- renderAttachChips();
366
- })
367
- .catch(function(){ attachments=attachments.filter(function(a){ return a.id!==pendingId; }); renderAttachChips(); });
368
- }
369
- if(input) input.addEventListener('paste', function(e){
370
- var dt=e.clipboardData; if(!dt) return;
371
- var blobs=[], items=dt.items;
372
- if(items && items.length){ for(var i=0;i<items.length;i++){ if(items[i] && items[i].kind==='file'){ var b=items[i].getAsFile(); if(b) blobs.push(b); } } }
373
- if(!blobs.length && dt.files && dt.files.length){ for(var k=0;k<dt.files.length;k++) blobs.push(dt.files[k]); }
374
- if(!blobs.length) return; // plain-text paste → leave it to the textarea
375
- e.preventDefault();
376
- blobs.forEach(function(blob, idx){
377
- var name=(blob && blob.name)||pasteExtName(blob, idx);
378
- var pendingId='paste-'+Date.now().toString(36)+'-'+idx+'-'+Math.random().toString(36).slice(2,6);
379
- attachments.push({ id:pendingId, name:name, contentType:(blob&&blob.type)||'application/octet-stream', pending:true });
380
- renderAttachChips();
381
- uploadPasted(blob, name, pendingId);
382
- });
383
- if(input) input.focus();
384
- });
385
-
386
- // Each conversation has its own URL (/chat/<id>); a fresh page is /chat.
387
- var pageEl=root.querySelector('.rk-chat-page');
388
- var INITIAL_THREAD=(pageEl && pageEl.getAttribute('data-initial-thread')) || null;
389
- function chatScopePrefix(){ var m=(location.pathname||'').match(/^(\\/u\\/[^/]+)\\/chat/); return m ? m[1] : ''; } // preserve account scope
390
- function chatUrlFor(id){ return chatScopePrefix()+(id ? '/chat/'+encodeURIComponent(id) : '/chat'); }
391
- function threadFromLocation(){ var m=(location.pathname||'').match(/\\/chat\\/(.+)$/); return m ? decodeURIComponent(m[1]) : null; }
392
- function setChatUrl(id, replace){
393
- // NB: the local "history" var is the message list — use window.history here.
394
- var h=window.history; if(!h || (!h.pushState && !h.replaceState)) return;
395
- var url=chatUrlFor(id);
396
- try{ if(replace){ h.replaceState(null,'',url); } else { h.pushState(null,'',url); } }catch(e){}
397
- }
398
- function genId(p){ return p+'-'+Date.now().toString(36)+'-'+Math.random().toString(36).slice(2,8); }
399
- function autosize(){ if(!input)return; input.style.height='auto'; input.style.height=Math.min(input.scrollHeight,210)+'px'; }
400
- function toBottom(){ try{ window.scrollTo({top:document.body.scrollHeight,behavior:'smooth'}); }catch(e){ window.scrollTo(0,document.body.scrollHeight); } }
401
- function nowLabel(){ try{ return new Date().toLocaleTimeString([], {hour:'numeric', minute:'2-digit'}); }catch(e){ return ''; } }
402
- function el(cls){ var d=root.createElement('div'); d.className=cls; return d; }
403
- // While a reply streams, the Send button becomes an interruptive Stop (stays
404
- // clickable so the user can abort); it flips back to Send when the reply ends.
405
- 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; }); }
406
-
407
- // Inline markdown → HTML (escape first, then bold/italic/links). pre-wrap on
408
- // the container keeps the model's own line breaks, so no block handling needed.
409
- function escapeHtml(s){ return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); }
410
- function mdToHtml(text){
411
- var s=escapeHtml(text);
412
- s=s.replace(/\\*\\*([^*]+)\\*\\*/g,'<strong>$1</strong>');
413
- s=s.replace(/(^|[^\\w*])\\*([^*\\n]+)\\*/g,'$1<em>$2</em>');
414
- s=s.replace(/(^|[^\\w_])_([^_\\n]+)_/g,'$1<em>$2</em>');
415
- s=s.replace(/\\[([^\\]]+)\\]\\((https?:\\/\\/[^\\s)]+)\\)/g,'<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>');
416
- s=s.replace(/(^|[\\s(])(https?:\\/\\/[^\\s<]+[^<.,:;"')\\]\\s])/g,'$1<a href="$2" target="_blank" rel="noopener noreferrer">$2</a>');
417
- return s;
418
- }
419
-
420
- // ── file preview modal (shares the rk-aichat-preview styling from theme.ts):
421
- // images carousel, video, audio, pdf, and text (json pretty-print, md render,
422
- // txt/csv/srt/vtt as <pre>) ──
423
- function fileKind(name, ct){
424
- ct=(ct||'').toLowerCase();
425
- var ext=((name||'').split('.').pop()||'').toLowerCase();
426
- if(ct.indexOf('image/')===0 || /^(png|jpe?g|gif|webp|svg|avif|bmp|heic|ico)$/.test(ext)) return 'image';
427
- if(ct.indexOf('video/')===0 || /^(mp4|mov|webm|m4v|mkv|ogv)$/.test(ext)) return 'video';
428
- if(ct.indexOf('audio/')===0 || /^(mp3|wav|m4a|aac|ogg|oga|flac|opus)$/.test(ext)) return 'audio';
429
- if(ct.indexOf('application/pdf')===0 || ext==='pdf') return 'pdf';
430
- if(ct.indexOf('json')>=0 || ext==='json') return 'json';
431
- if(ext==='md' || ext==='markdown' || ct.indexOf('markdown')>=0) return 'markdown';
432
- if(ct.indexOf('text/')===0 || /^(txt|csv|tsv|srt|vtt|log|xml|yaml|yml|ini|conf|html?)$/.test(ext)) return 'text';
433
- return 'other';
434
- }
435
- // Block-level markdown for .md previews (headings, lists, quotes, rules, code,
436
- // plus the inline bold/italic/link handling from mdToHtml).
437
- function renderMarkdownBlock(src){
438
- var BT=String.fromCharCode(96), FENCE=BT+BT+BT;
439
- function inline(s){ return mdToHtml(s).replace(new RegExp(BT+'([^'+BT+']+)'+BT,'g'),'<code>$1</code>'); }
440
- var lines=String(src).replace(/\\r\\n?/g,'\\n').split('\\n');
441
- var out=[], inCode=false, code=[], listType=null, listItems=[];
442
- function flushList(){ if(!listType)return; out.push('<'+listType+'>'+listItems.join('')+'</'+listType+'>'); listType=null; listItems=[]; }
443
- function flushCode(){ out.push('<pre class="rk-aichat-preview-code"><code>'+escapeHtml(code.join('\\n'))+'</code></pre>'); code=[]; }
444
- for(var i=0;i<lines.length;i++){
445
- var ln=lines[i];
446
- if(ln.trim().indexOf(FENCE)===0){ if(inCode){ flushCode(); inCode=false; } else { flushList(); inCode=true; } continue; }
447
- if(inCode){ code.push(ln); continue; }
448
- var h=ln.match(/^(#{1,6})\\s+(.*)$/);
449
- if(h){ flushList(); var lvl=h[1].length; out.push('<h'+lvl+'>'+inline(h[2])+'</h'+lvl+'>'); continue; }
450
- if(/^\\s*([-*+])\\s+/.test(ln)){ if(listType!=='ul'){ flushList(); listType='ul'; } listItems.push('<li>'+inline(ln.replace(/^\\s*[-*+]\\s+/,''))+'</li>'); continue; }
451
- var om=ln.match(/^\\s*\\d+\\.\\s+(.*)$/);
452
- if(om){ if(listType!=='ol'){ flushList(); listType='ol'; } listItems.push('<li>'+inline(om[1])+'</li>'); continue; }
453
- if(/^\\s*>\\s?/.test(ln)){ flushList(); out.push('<blockquote>'+inline(ln.replace(/^\\s*>\\s?/,''))+'</blockquote>'); continue; }
454
- if(/^\\s*(-{3,}|\\*{3,}|_{3,})\\s*$/.test(ln)){ flushList(); out.push('<hr>'); continue; }
455
- if(!ln.trim()){ flushList(); continue; }
456
- flushList(); out.push('<p>'+inline(ln)+'</p>');
457
- }
458
- if(inCode) flushCode();
459
- flushList();
460
- return out.join('');
461
- }
462
- function openFilePreview(items, index){
463
- items=(items||[]).filter(function(f){ return f && f.viewUrl; });
464
- if(!items.length) return;
465
- var idx=Math.max(0,Math.min(index||0,items.length-1));
466
- var back=el('rk-aichat-modal-back');
467
- var modal=el('rk-aichat-modal rk-aichat-preview');
468
- var head=el('rk-aichat-modal-head');
469
- var nameEl=root.createElement('span'); nameEl.className='rk-aichat-http-url rk-aichat-preview-name';
470
- var openLink=root.createElement('a'); openLink.className='rk-aichat-preview-open'; openLink.target='_blank'; openLink.rel='noopener noreferrer'; openLink.textContent='Open \\u2197';
471
- var xBtn=root.createElement('button'); xBtn.type='button'; xBtn.className='rk-aichat-modal-x'; xBtn.setAttribute('aria-label','Close'); xBtn.innerHTML='&times;';
472
- head.appendChild(nameEl); head.appendChild(openLink); head.appendChild(xBtn);
473
- var body=el('rk-aichat-modal-body rk-aichat-preview-body');
474
- var nav=el('rk-aichat-preview-nav');
475
- var prev=root.createElement('button'); prev.type='button'; prev.className='rk-aichat-preview-btn'; prev.innerHTML='\\u2039 Prev';
476
- var counter=root.createElement('span'); counter.className='rk-aichat-preview-count';
477
- var next=root.createElement('button'); next.type='button'; next.className='rk-aichat-preview-btn'; next.innerHTML='Next \\u203a';
478
- nav.appendChild(prev); nav.appendChild(counter); nav.appendChild(next);
479
- function renderUnknown(container, it, msg){
480
- var wrap=el('rk-aichat-preview-empty');
481
- var p=root.createElement('div'); p.textContent=msg||'No inline preview for this file type.'; wrap.appendChild(p);
482
- 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);
483
- container.appendChild(wrap);
484
- }
485
- function renderText(container, it, kind){
486
- var loading=el('rk-aichat-preview-loading'); loading.textContent='Loading\\u2026'; container.appendChild(loading);
487
- fetch(it.viewUrl,{ credentials:'same-origin' })
488
- .then(function(r){ if(!r.ok) throw new Error('http '+r.status); return r.text(); })
489
- .then(function(txt){
490
- container.innerHTML='';
491
- 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; }
492
- if(kind==='markdown'){ var md=el('rk-aichat-preview-md'); md.innerHTML=renderMarkdownBlock(txt); container.appendChild(md); return; }
493
- var pre=root.createElement('pre'); pre.className='rk-aichat-http-pre rk-aichat-preview-text'; pre.textContent=txt; container.appendChild(pre);
494
- })
495
- .catch(function(){ container.innerHTML=''; renderUnknown(container, it, 'Couldn\\u2019t load this file.'); });
496
- }
497
- function render(){
498
- var it=items[idx];
499
- nameEl.textContent=it.name||'file'; nameEl.title=it.name||'';
500
- openLink.href=it.viewUrl;
501
- body.innerHTML='';
502
- var kind=fileKind(it.name, it.contentType);
503
- 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); }
504
- 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); }
505
- 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); }
506
- else if(kind==='pdf'){ var f=root.createElement('iframe'); f.className='rk-aichat-preview-frame'; f.src=it.viewUrl; body.appendChild(f); }
507
- else if(kind==='json'||kind==='markdown'||kind==='text'){ renderText(body, it, kind); }
508
- else { renderUnknown(body, it); }
509
- counter.textContent=(idx+1)+' / '+items.length;
510
- prev.disabled=idx<=0; next.disabled=idx>=items.length-1;
511
- nav.hidden=items.length<2;
512
- }
513
- prev.addEventListener('click',function(){ if(idx>0){ idx--; render(); } });
514
- next.addEventListener('click',function(){ if(idx<items.length-1){ idx++; render(); } });
515
- modal.appendChild(head); modal.appendChild(body); modal.appendChild(nav); back.appendChild(modal);
516
- function close(){ if(back.parentNode) back.parentNode.removeChild(back); document.removeEventListener('keydown',onKey); }
517
- 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(); } } }
518
- back.addEventListener('click',function(e){ if(e.target===back) close(); });
519
- xBtn.addEventListener('click',close);
520
- document.addEventListener('keydown',onKey);
521
- document.body.appendChild(back);
522
- render();
523
- }
524
-
525
- function clientContext(){
526
- var now=new Date(); var local=null, tz=null;
527
- try{ local=new Intl.DateTimeFormat(undefined,{dateStyle:'full',timeStyle:'long'}).format(now); }catch(e){ local=now.toString(); }
528
- try{ tz=(Intl.DateTimeFormat().resolvedOptions().timeZone)||null; }catch(e){ tz=null; }
529
- return { currentDateTime: now.toISOString(), localDateTime: local, timeZone: tz };
530
- }
531
-
532
- function appendUser(text){
533
- var msg=el('rk-chat-msg is-user');
534
- var meta=el('rk-chat-meta'); meta.innerHTML='You <span class="rk-chat-time">'+nowLabel()+'</span>';
535
- var bub=el('rk-chat-bubble rk-chat-bubble-user'); bub.textContent=text;
536
- msg.appendChild(meta); msg.appendChild(bub);
537
- if(thread) thread.appendChild(msg);
538
- }
539
-
540
- // Returns a small controller for the streaming assistant bubble.
541
- function appendAssistant(){
542
- var msg=el('rk-chat-msg is-assistant');
543
- var meta=el('rk-chat-meta');
544
- meta.innerHTML='Agent <span class="rk-chat-time">'+nowLabel()+'</span>';
545
- var bub=el('rk-chat-bubble rk-chat-bubble-assistant');
546
- var status=el('rk-chat-status');
547
- status.innerHTML='<span class="rk-chat-typing"><span></span><span></span><span></span></span><span class="rk-chat-status-label">Thinking…</span>';
548
- var notes=el('rk-chat-notes');
549
- var textEl=el('rk-chat-stream'); textEl.hidden=true;
550
- bub.appendChild(status); bub.appendChild(notes); bub.appendChild(textEl);
551
- msg.appendChild(meta); msg.appendChild(bub);
552
- if(thread) thread.appendChild(msg);
553
- var lastText='';
554
- return {
555
- setStatus:function(s){ var l=status.querySelector('.rk-chat-status-label'); if(l && s){ l.textContent=s; } },
556
- hideStatus:function(){ status.hidden=true; },
557
- showText:function(){ status.hidden=true; textEl.hidden=false; },
558
- setText:function(t){ lastText=t; textEl.textContent=t; },
559
- // After streaming completes, re-render as markdown (bold/italic/links).
560
- renderMd:function(){ if(lastText){ textEl.innerHTML=mdToHtml(lastText); } },
561
- addNote:function(t){ var n=el('rk-chat-toolnote'); n.textContent=t; notes.appendChild(n); },
562
- fail:function(m){ status.hidden=true; var er=el('rk-chat-error'); er.textContent=m||'Something went wrong.'; bub.appendChild(er); }
563
- };
564
- }
565
-
566
- // Minimal SSE reader — mirrors the live client's text/event-stream parsing.
567
- function readStream(response, h){
568
- if(!response.body || !response.body.getReader){
569
- // No streaming reader available — fall back to reading the whole body.
570
- return response.text().then(function(t){ parseEvents(t, h); });
571
- }
572
- var reader=response.body.getReader();
573
- var decoder=new TextDecoder();
574
- var buffer='';
575
- function pump(){
576
- return reader.read().then(function(res){
577
- if(res.done){ if(buffer){ parseEvents(buffer, h); buffer=''; } return; }
578
- buffer+=decoder.decode(res.value,{stream:true});
579
- var events=buffer.split('\\n\\n');
580
- buffer=events.pop()||'';
581
- for(var i=0;i<events.length;i++){ handleEvent(events[i], h); }
582
- return pump();
583
- });
584
- }
585
- return pump();
586
- }
587
- function parseEvents(blob, h){ blob.split('\\n\\n').forEach(function(ev){ handleEvent(ev, h); }); }
588
- function handleEvent(raw, h){
589
- var lines=raw.split('\\n');
590
- var dataLine=null;
591
- for(var j=0;j<lines.length;j++){ var t=lines[j].trim(); if(t.indexOf('data:')===0){ dataLine=t; break; } }
592
- if(!dataLine) return;
593
- var payload=dataLine.slice(5).trim();
594
- if(payload==='[DONE]' || !payload) return;
595
- var chunk; try{ chunk=JSON.parse(payload); }catch(e){ return; }
596
- if(!chunk || typeof chunk!=='object') return;
597
- if(chunk.type==='text-delta' && typeof chunk.delta==='string'){ h.onDelta(chunk.delta); }
598
- 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); } }
599
- else if(chunk.type==='tool-call' && typeof chunk.toolName==='string'){ h.onToolCall(chunk.toolName); }
600
- else if(chunk.type==='error'){ h.onError((typeof chunk.errorText==='string')?chunk.errorText:((chunk.error && chunk.error.message)||'Unable to read assistant response.')); }
601
- }
602
-
603
- // Attachment URL line the model can quote back as REST payload URLs, alongside
604
- // the binary file content parts. Mirrors the /editor React dock. Double-
605
- // backslash-n = a browser-side newline (this function lives in a template literal).
606
- function buildAttachmentUrlText(atts){
607
- if(!atts || !atts.length) return '';
608
- var lines=atts.map(function(a){ return '- '+(a.fileName||'file')+' ('+(a.contentType||'')+'): '+a.viewUrl; });
609
- return '\\n\\nAttached file URLs for REST payloads:\\n'+lines.join('\\n')+'\\nUse these exact URLs as prompt_attachments for image generation, source_image_url for image edits, media_url for video slides, or reference attachment URLs when calling template routes.';
610
- }
611
-
612
- function sendMessage(text){
613
- if(busy) return;
614
- text=(text||'').trim();
615
- // Attachments picked from the drawer travel in user_message.attachments;
616
- // sending is allowed with only files (no text) too.
617
- // A pasted file may still be uploading to /temp — wait rather than drop it.
618
- if(attachments.some(function(f){ return f && f.pending; })){ renderAttachChips(); return; }
619
- var atts=attachments.map(function(f){ return { id:String(f.id), fileName:f.name, contentType:f.contentType, viewUrl:f.viewUrl }; });
620
- if(!text && !atts.length) return;
621
-
622
- var line=text;
623
- if(attachments.length){ line=(text?text+' ':'')+'(attached: '+attachments.map(function(f){ return f.name; }).join(', ')+')'; }
624
-
625
- clearEmptyState();
626
- history.push({role:'user', text:text||line});
627
- appendUser(line);
628
- if(input){ input.value=''; autosize(); }
629
- attachments=[]; renderAttachChips();
630
- toBottom();
631
-
632
- var view=appendAssistant();
633
- toBottom();
634
- setBusy(true);
635
-
636
- if(!threadId){ threadId=genId('thread'); setChatUrl(threadId, true); }
637
- var userMsgId=genId('user');
638
- var asstMsgId=genId('assistant');
639
-
640
- // Attachments must ride in messages[].content as file parts + a URL text
641
- // line — the backend feeds the model messages[].content, not the persistence-
642
- // only user_message.attachments. Append to the latest user turn (= last entry).
643
- var attUrlText = atts.length ? buildAttachmentUrlText(atts) : '';
644
- var body={
645
- messages: history.map(function(m, i){
646
- var isLastUser = (i === history.length - 1) && m.role === 'user';
647
- var t = m.text;
648
- if(isLastUser && attUrlText) t = t + attUrlText;
649
- var content = [{ type:'text', text:t }];
650
- if(isLastUser && atts.length){ atts.forEach(function(a){ content.push({ type:'file', data:a.viewUrl, mediaType:a.contentType||'application/octet-stream' }); }); }
651
- return { role:m.role, content:content };
652
- }),
653
- thread_id: threadId,
654
- thread_template_id: (TEMPLATE && TEMPLATE.templateId) || 'chat-brainstorm',
655
- thread_title: (text||line).slice(0,42),
656
- thread_tracers: (TEMPLATE && TEMPLATE.tracers) || [],
657
- client_context: clientContext(),
658
- user_message: { id:userMsgId, role:'user', text:text||line, attachments:atts },
659
- assistant_message: { id:asstMsgId }
660
- };
661
- if(TEMPLATE){ body.template=TEMPLATE; }
662
- if(CACHED){ body.cached_context=CACHED; }
663
-
664
- var headers={ 'content-type':'application/json' };
665
- if(API_KEY){ headers['vidfarm-api-key']=API_KEY; }
666
-
667
- var acc='';
668
- var sawText=false;
669
- var errored=false;
670
-
671
- var handlers={
672
- onDelta:function(d){ if(!sawText){ sawText=true; view.showText(); } acc+=d; view.setText(acc); toBottom(); },
673
- onStatus:function(s){ if(!sawText){ view.setStatus(s); } },
674
- onToolCall:function(name){ view.addNote('Running '+name+'…'); toBottom(); },
675
- onError:function(m){ errored=true; view.fail(m); toBottom(); }
676
- };
677
-
678
- var controller=(typeof AbortController!=='undefined') ? new AbortController() : null;
679
- pendingAbort=controller;
680
- var req;
681
- try{
682
- req=fetch(ENDPOINT,{ method:'POST', headers:headers, credentials:'same-origin', body:JSON.stringify(body), signal: controller?controller.signal:undefined });
683
- }catch(err){
684
- // Synchronous fetch failure (e.g. invalid URL) — never let it throw uncaught.
685
- req=Promise.reject(err);
686
- }
687
-
688
- req.then(function(resp){
689
- if(!resp.ok){
690
- return resp.json().catch(function(){ return {}; }).then(function(j){
691
- throw new Error((j && j.error) || ('Chat request failed ('+resp.status+').'));
692
- });
693
- }
694
- return readStream(resp, handlers);
695
- }).then(function(){
696
- pendingAbort=null;
697
- view.hideStatus();
698
- if(sawText){
699
- history.push({role:'assistant', text:acc});
700
- view.renderMd();
701
- } else if(!errored){
702
- view.showText();
703
- view.setText('The assistant finished without returning any text.');
704
- }
705
- setBusy(false);
706
- if(input){ input.focus(); }
707
- toBottom();
708
- loadHistory();
709
- }).catch(function(err){
710
- pendingAbort=null;
711
- // User hit Stop mid-reply — keep whatever streamed so far, no error.
712
- if(err && (err.name==='AbortError' || (controller && controller.signal && controller.signal.aborted))){
713
- view.hideStatus();
714
- if(sawText){ history.push({role:'assistant', text:acc}); view.renderMd(); }
715
- else{ view.showText(); view.setText('Stopped.'); }
716
- setBusy(false);
717
- if(input){ input.focus(); }
718
- toBottom();
719
- return;
720
- }
721
- var msg=(err && err.message) ? err.message : String(err);
722
- if(!API_KEY){ msg=msg+'\\n\\nAdd an AI provider key in Settings to brainstorm on your own keys.'; }
723
- view.fail(msg);
724
- setBusy(false);
725
- if(input){ input.focus(); }
726
- toBottom();
727
- });
728
- }
729
-
730
- // ── real chat history (editor-chat threads) ────────────────────────────
731
- var THREADS_URL=(boot && boot.threadsUrl) || '/api/v1/editor-chat/threads';
732
- var TEMPLATE_ID=(TEMPLATE && TEMPLATE.templateId) || 'chat-brainstorm';
733
- var histList=root.getElementById('rk-chat-history-list');
734
- var histAside=root.getElementById('rk-chat-history');
735
- var histBackdrop=root.getElementById('rk-chat-histbackdrop');
736
- var histToggle=root.getElementById('rk-chat-histtoggle');
737
- var loadingThread=false;
738
-
739
- function authHeaders(extra){ var h=extra||{}; if(API_KEY){ h['vidfarm-api-key']=API_KEY; } return h; }
740
- function relTime(iso){
741
- if(!iso) return '';
742
- var t=Date.parse(iso); if(isNaN(t)) return '';
743
- var s=Math.max(0,(Date.now()-t)/1000);
744
- if(s<60) return 'just now';
745
- if(s<3600) return Math.floor(s/60)+'m ago';
746
- if(s<86400) return Math.floor(s/3600)+'h ago';
747
- if(s<604800) return Math.floor(s/86400)+'d ago';
748
- try{ return new Date(t).toLocaleDateString([], {month:'short', day:'numeric'}); }catch(e){ return ''; }
749
- }
750
- function histNote(text, opts){
751
- var wrap=el('rk-chat-history-note');
752
- var p=root.createElement('div'); p.textContent=text; wrap.appendChild(p);
753
- if(opts && opts.href){ var a=root.createElement('a'); a.href=opts.href; a.textContent=opts.linkText||'Open'; wrap.appendChild(a); }
754
- if(opts && opts.retry){ var b=root.createElement('button'); b.type='button'; b.textContent='Try again'; b.addEventListener('click',function(){ loadHistory(); }); wrap.appendChild(b); }
755
- return wrap;
756
- }
757
- function setActiveThread(id){
758
- threadId=id;
759
- if(!histList) return;
760
- var rows=histList.querySelectorAll('.rk-chat-hrow');
761
- for(var i=0;i<rows.length;i++){ rows[i].classList.toggle('is-active', rows[i].getAttribute('data-id')===id); }
762
- }
763
- function renderHistory(threads){
764
- if(!histList) return;
765
- histList.innerHTML='';
766
- if(!threads.length){ histList.appendChild(histNote('No conversations yet. Start one below \\u2014 it saves automatically.')); return; }
767
- threads.forEach(function(t){
768
- var row=root.createElement('button');
769
- row.type='button'; row.className='rk-chat-hrow'; row.setAttribute('data-id', t.id);
770
- if(t.id===threadId){ row.classList.add('is-active'); }
771
- var main=el('rk-chat-hrow-main');
772
- var ti=el('rk-chat-hrow-title'); ti.textContent=t.title || 'Untitled chat';
773
- var sub=el('rk-chat-hrow-sub');
774
- var count=(typeof t.messageCount==='number')?t.messageCount:0;
775
- var when=relTime(t.lastMessageAt || t.updatedAt);
776
- sub.textContent=(count?count+' message'+(count===1?'':'s'):'')+((count&&when)?' \\u00b7 ':'')+(when||'');
777
- main.appendChild(ti); main.appendChild(sub);
778
- var del=root.createElement('button');
779
- del.type='button'; del.className='rk-chat-hrow-del'; del.setAttribute('aria-label','Delete chat'); del.textContent='\\u00d7';
780
- del.addEventListener('click', function(ev){ ev.stopPropagation(); deleteThread(t.id, row); });
781
- row.appendChild(main); row.appendChild(del);
782
- row.addEventListener('click', function(){ openThread(t.id); });
783
- histList.appendChild(row);
784
- });
785
- }
786
- function loadHistory(){
787
- if(!histList) return;
788
- 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; }
789
- var url=THREADS_URL+'?template_id='+encodeURIComponent(TEMPLATE_ID)+'&limit=30&include_messages=false';
790
- fetch(url, { headers:authHeaders(), credentials:'same-origin' })
791
- .then(function(r){
792
- if(r.status===401||r.status===403){ return {threads:[], _auth:true}; }
793
- if(!r.ok) throw new Error('http '+r.status);
794
- return r.json();
795
- })
796
- .then(function(j){
797
- if(j && j._auth){ histList.innerHTML=''; histList.appendChild(histNote('Sign in to sync your saved chats.', {href:'/login', linkText:'Sign in'})); return; }
798
- renderHistory((j && j.threads) || []);
799
- })
800
- .catch(function(){ if(histList){ histList.innerHTML=''; histList.appendChild(histNote('Couldn\\u2019t load your chats.', {retry:true})); } });
801
- }
802
- function appendAssistantStatic(text){ var v=appendAssistant(); v.showText(); v.setText(text||''); v.hideStatus(); v.renderMd(); return v; }
803
- function openThread(id, opts){
804
- if(busy || loadingThread) return;
805
- if(!(opts && opts.skipUrl)) setChatUrl(id, false);
806
- loadingThread=true;
807
- closeHistoryOverlay();
808
- fetch(THREADS_URL+'/'+encodeURIComponent(id), { headers:authHeaders(), credentials:'same-origin' })
809
- .then(function(r){ if(!r.ok) throw new Error('http '+r.status); return r.json(); })
810
- .then(function(j){
811
- var t=j && j.thread; if(!t){ throw new Error('missing thread'); }
812
- history=[];
813
- if(thread){ thread.innerHTML=''; }
814
- var msgs=t.messages||[];
815
- for(var i=0;i<msgs.length;i++){
816
- var m=msgs[i]; var role=(m.role==='assistant')?'assistant':'user'; var txt=m.text||'';
817
- history.push({role:role, text:txt});
818
- if(role==='user'){ appendUser(txt); } else { appendAssistantStatic(txt); }
819
- }
820
- setActiveThread(id);
821
- loadingThread=false;
822
- toBottom();
823
- })
824
- .catch(function(){ loadingThread=false; });
825
- }
826
- function deleteThread(id, row){
827
- if(!API_KEY) return;
828
- fetch(THREADS_URL+'/'+encodeURIComponent(id), { method:'DELETE', headers:authHeaders(), credentials:'same-origin' })
829
- .then(function(r){
830
- if(!r.ok && r.status!==404) throw new Error('http '+r.status);
831
- if(row && row.parentNode){ row.parentNode.removeChild(row); }
832
- if(id===threadId){ resetChat(); }
833
- if(histList && !histList.querySelector('.rk-chat-hrow')){ loadHistory(); }
834
- })
835
- .catch(function(){});
836
- }
837
- // A fresh chat opens empty — no greeting message — with a few subtle
838
- // suggestion pills the user can tap to start (or just type their own).
839
- function renderEmptyState(){
840
- if(!thread) return;
841
- thread.innerHTML='';
842
- var suggestions=[
843
- 'Give me 10 ad hooks for my offer',
844
- 'What angle works best for cold traffic?',
845
- 'Help me plan a cold-start strategy'
846
- ];
847
- var wrap=el('rk-chat-empty');
848
- var hint=el('rk-chat-empty-hint'); hint.textContent='Ask anything, or try one of these';
849
- var list=el('rk-chat-suggests');
850
- suggestions.forEach(function(s){
851
- var b=root.createElement('button');
852
- b.type='button'; b.className='rk-chat-suggest'; b.textContent=s;
853
- b.addEventListener('click',function(){ if(busy) return; sendMessage(s); });
854
- list.appendChild(b);
855
- });
856
- wrap.appendChild(hint); wrap.appendChild(list);
857
- thread.appendChild(wrap);
858
- }
859
- function clearEmptyState(){
860
- if(!thread) return;
861
- var e=thread.querySelector('.rk-chat-empty');
862
- if(e && e.parentNode){ e.parentNode.removeChild(e); }
863
- }
864
- function resetChat(opts){
865
- history=[]; threadId=null;
866
- if(!(opts && opts.skipUrl)) setChatUrl(null, false);
867
- renderEmptyState();
868
- setActiveThread(null);
869
- if(input){ input.value=''; autosize(); input.focus(); }
870
- window.scrollTo(0,0);
871
- }
872
- function openHistoryOverlay(){ if(histAside){ histAside.classList.add('is-open'); } if(histBackdrop){ histBackdrop.hidden=false; } }
873
- function closeHistoryOverlay(){ if(histAside){ histAside.classList.remove('is-open'); } if(histBackdrop){ histBackdrop.hidden=true; } }
874
- if(histToggle){ histToggle.addEventListener('click', openHistoryOverlay); }
875
- if(histBackdrop){ histBackdrop.addEventListener('click', closeHistoryOverlay); }
876
- loadHistory();
877
-
878
- // Deep-link: /chat/<id> opens that saved thread on load; otherwise the page
879
- // starts on the empty new-chat state with subtle suggestions.
880
- if(INITIAL_THREAD){ openThread(INITIAL_THREAD, {skipUrl:true}); }
881
- else{ renderEmptyState(); }
882
- // Back/forward between conversations — sync the visible thread to the URL.
883
- window.addEventListener('popstate',function(){
884
- if(busy || loadingThread) return;
885
- var id=threadFromLocation();
886
- if(id){ if(id!==threadId){ openThread(id, {skipUrl:true}); } }
887
- else if(threadId){ resetChat({skipUrl:true}); }
888
- });
889
-
890
- // composer wiring
891
- if(input){ input.addEventListener('input',autosize); autosize(); }
892
- if(send){ send.addEventListener('click',function(){ if(busy){ if(pendingAbort){ pendingAbort.abort(); } return; } if(input){ sendMessage(input.value); } }); }
893
- if(input){
894
- input.addEventListener('keydown',function(ev){
895
- if(ev.key==='Enter' && !ev.shiftKey){ ev.preventDefault(); if(busy){ if(pendingAbort){ pendingAbort.abort(); } return; } sendMessage(input.value); }
896
- });
897
- }
898
-
899
- // "New" resets the conversation to the warm empty state (shared resetChat)
900
- var newBtn=root.getElementById('rk-chat-new');
901
- if(newBtn){
902
- newBtn.addEventListener('click',function(){ if(busy) return; resetChat(); });
903
- }
904
-
905
- // Navigate to another surface (Create Image → /tools/image, Create Video → a new
906
- // /editor project, or any AI-driven page nav) while HANDING OFF the live
907
- // conversation: append ?rk_chat=<threadId> so the destination's AI pop-panel
908
- // (document.ts) auto-opens and reloads this exact thread — a seamless jump that
909
- // never loses the chat context. Uses a global URL param so any page's dock can
910
- // honor it. No active thread → just navigate.
911
- function navigateWithChat(dest){
912
- if(!dest) return;
913
- try{
914
- if(threadId){
915
- var u=new URL(dest, window.location.origin);
916
- u.searchParams.set('rk_chat', threadId);
917
- dest=u.pathname + u.search + u.hash;
918
- }
919
- }catch(e){}
920
- window.location.href=dest;
921
- }
922
- // studio mode: choosing "Create Image" / "Create Video" navigates to that
923
- // workbench, carrying the current chat thread along for the ride.
924
- var modeSel=root.getElementById('rk-chat-mode');
925
- if(modeSel){
926
- modeSel.addEventListener('change',function(){
927
- var dest=modeSel.value;
928
- if(dest && dest!=='/chat'){ navigateWithChat(dest); }
929
- });
930
- }
931
- })();`;
932
- return reskinDocument({
933
- title: "vidfarm — Chat with Agent",
934
- 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.",
935
- activeSlug: "chat",
936
- account: { name: input.name, email: input.email },
937
- pageCss,
938
- body,
939
- script
940
- });
941
- }
942
- //# sourceMappingURL=chat-page.js.map