@mevdragon/vidfarm-devcli 0.18.0 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/editor-capabilities/SKILL.md +166 -0
- package/.agents/skills/editor-capabilities/references/re-theme-walkthrough.md +58 -0
- package/.agents/skills/vidfarm-media/SKILL.md +43 -4
- package/SKILL.director.md +190 -18
- package/SKILL.platform.md +8 -4
- package/demo/dist/app.css +1 -1
- package/demo/dist/app.js +77 -75
- package/demo/dist/favicon.ico +0 -0
- package/dist/src/app.js +3031 -300
- package/dist/src/cli.js +1549 -69
- package/dist/src/config.js +7 -0
- package/dist/src/devcli/clip-store.js +29 -2
- package/dist/src/devcli/clips.js +55 -9
- package/dist/src/devcli/composition-edit.js +658 -8
- package/dist/src/devcli/local-backend.js +123 -0
- package/dist/src/devcli/migrate-local.js +140 -0
- package/dist/src/devcli/sync.js +311 -0
- package/dist/src/devcli/timeline-edit.js +490 -0
- package/dist/src/editor-chat.js +56 -17
- package/dist/src/frontend/discover-client.js +130 -0
- package/dist/src/frontend/discover-store.js +23 -0
- package/dist/src/frontend/file-directory.js +995 -0
- package/dist/src/frontend/homepage-view.js +3 -3
- package/dist/src/frontend/template-editor-chat.js +28 -22
- package/dist/src/landing-page.js +24 -7
- package/dist/src/page-shell.js +26 -2
- package/dist/src/primitive-registry.js +409 -4
- package/dist/src/reskin/agency-page.js +1 -1
- package/dist/src/reskin/calendar-page.js +2 -1
- package/dist/src/reskin/chat-page.js +420 -85
- package/dist/src/reskin/discover-page.js +731 -39
- package/dist/src/reskin/document.js +1311 -387
- package/dist/src/reskin/help-page.js +1 -0
- package/dist/src/reskin/inpaint-clipper-page.js +649 -0
- package/dist/src/reskin/inpaint-page.js +2459 -446
- package/dist/src/reskin/inpaint-video-page.js +1339 -0
- package/dist/src/reskin/library-page.js +1168 -228
- package/dist/src/reskin/login-page.js +6 -6
- package/dist/src/reskin/pricing-page.js +2 -0
- package/dist/src/reskin/settings-page.js +55 -10
- package/dist/src/reskin/theme.js +365 -20
- package/dist/src/services/billing.js +4 -0
- package/dist/src/services/clip-curation/gemini.js +5 -0
- package/dist/src/services/clip-curation/hunt.js +81 -1
- package/dist/src/services/clip-curation/index.js +2 -1
- package/dist/src/services/clip-curation/local-agent.js +4 -3
- package/dist/src/services/clip-curation/media-select.js +85 -0
- package/dist/src/services/clip-curation/query.js +5 -1
- package/dist/src/services/clip-curation/refine.js +50 -20
- package/dist/src/services/clip-curation/scan.js +10 -3
- package/dist/src/services/clip-curation/taxonomy.js +3 -1
- package/dist/src/services/clip-curation/taxonomy.v1.json +13 -1
- package/dist/src/services/clip-records.js +42 -1
- package/dist/src/services/clip-search.js +43 -13
- package/dist/src/services/file-directory.js +117 -0
- package/dist/src/services/hyperframes.js +283 -3
- package/dist/src/services/serverless-records.js +43 -0
- package/dist/src/services/storage.js +47 -2
- package/dist/src/services/upstream.js +5 -5
- package/dist/src/template-editor-shell.js +16 -2
- package/package.json +1 -1
- package/public/assets/discover-client-app.js +1 -0
- package/public/assets/file-directory-app.js +2 -0
- package/public/assets/homepage-client-app.js +12 -12
- package/public/assets/page-runtime-client-app.js +24 -24
- package/public/assets/placeholders/scene-placeholder.png +0 -0
- package/src/assets/favicon.ico +0 -0
- package/src/assets/logo-vidfarm.png +0 -0
|
@@ -28,22 +28,17 @@ const DEFAULT_INPUT = {
|
|
|
28
28
|
};
|
|
29
29
|
// ── icons ───────────────────────────────────────────────────────────────────
|
|
30
30
|
const SEND = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 12l16-8-6 16-2.5-6.5L4 12Z"/></svg>`;
|
|
31
|
-
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
"Turn my best idea into a ready-to-shoot UGC ad script.",
|
|
35
|
-
"Remix a trending short-form format for my product.",
|
|
36
|
-
"Write word-by-word captions for a 24-second talking-head cut."
|
|
37
|
-
];
|
|
38
|
-
export function renderReskinChat(input = DEFAULT_INPUT) {
|
|
31
|
+
const STOP = `<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><rect x="6" y="6" width="12" height="12" rx="2.5"/></svg>`;
|
|
32
|
+
const CLIP = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48"/></svg>`;
|
|
33
|
+
export function renderReskinChat(input = DEFAULT_INPUT, opts = {}) {
|
|
39
34
|
const boot = input.editorChat ?? null;
|
|
35
|
+
const initialThreadId = opts.initialThreadId ?? null;
|
|
40
36
|
// Embed the real boot as JSON. Escaping only `<` is the correct (and
|
|
41
37
|
// sufficient) way to keep a `<script type="application/json">` block from
|
|
42
38
|
// breaking out; HTML-entity escaping would corrupt JSON.parse.
|
|
43
39
|
const bootJson = JSON.stringify(boot).replace(/</g, "\\u003c");
|
|
44
|
-
const starterChips = STARTERS.map((s) => `<button type="button" class="rk-chat-chip" data-rk-prompt="${rkEscape(s)}">${rkEscape(s)}</button>`).join("");
|
|
45
40
|
const body = `
|
|
46
|
-
<main class="rk-chat-page">
|
|
41
|
+
<main class="rk-chat-page"${initialThreadId ? ` data-initial-thread="${rkEscape(initialThreadId)}"` : ""}>
|
|
47
42
|
<aside class="rk-chat-history" id="rk-chat-history" aria-label="Chat history">
|
|
48
43
|
<div class="rk-chat-history-head">
|
|
49
44
|
<span class="rk-chat-history-title">Chats</span>
|
|
@@ -57,40 +52,34 @@ export function renderReskinChat(input = DEFAULT_INPUT) {
|
|
|
57
52
|
<header class="rk-chat-head">
|
|
58
53
|
<button type="button" class="rk-chat-histtoggle" id="rk-chat-histtoggle" aria-label="Show chat history">☰</button>
|
|
59
54
|
<div class="rk-chat-head-titles">
|
|
60
|
-
<
|
|
61
|
-
<h1 class="rk-chat-title" id="rk-chat-title">Brainstorm studio</h1>
|
|
62
|
-
<p class="rk-chat-subhead">Short-form angles, hooks & scripts — brainstormed live on your own AI keys, never billed by vidfarm.</p>
|
|
55
|
+
<h1 class="rk-chat-title" id="rk-chat-title">Chat with Agent</h1>
|
|
63
56
|
</div>
|
|
64
57
|
<div class="rk-chat-head-actions">
|
|
65
58
|
<label class="rk-chat-mode" aria-label="Studio mode">
|
|
66
59
|
<select class="rk-chat-mode-select" id="rk-chat-mode">
|
|
67
|
-
<option value="/chat" selected>
|
|
68
|
-
<option value="/
|
|
60
|
+
<option value="/chat" selected>Chat</option>
|
|
61
|
+
<option value="/tools/image">Create Image</option>
|
|
62
|
+
<option value="/tools/clipper">Clip a Video</option>
|
|
63
|
+
<option value="/editor/original/fork/new">Create Video</option>
|
|
69
64
|
</select>
|
|
70
65
|
<span class="rk-chat-mode-chev" aria-hidden="true">▾</span>
|
|
71
66
|
</label>
|
|
72
67
|
</div>
|
|
73
68
|
</header>
|
|
74
69
|
|
|
75
|
-
<div class="rk-chat-thread" id="rk-chat-thread">
|
|
76
|
-
<div class="rk-chat-msg is-assistant">
|
|
77
|
-
<div class="rk-chat-meta"><span class="rk-chat-avatar" aria-hidden="true">V</span>Copilot</div>
|
|
78
|
-
<div class="rk-chat-bubble rk-chat-bubble-assistant">
|
|
79
|
-
<div class="rk-chat-stream">Tell me the offer, who it’s for, and where the traffic is cold — I’ll come back with angles, hooks, and scripts you can render. Pick a starter below or just start typing.</div>
|
|
80
|
-
</div>
|
|
81
|
-
</div>
|
|
82
|
-
</div>
|
|
70
|
+
<div class="rk-chat-thread" id="rk-chat-thread"></div>
|
|
83
71
|
|
|
84
72
|
<div class="rk-chat-dock">
|
|
85
|
-
<div class="rk-chat-
|
|
86
|
-
${starterChips}
|
|
87
|
-
</div>
|
|
73
|
+
<div class="rk-chat-chips" id="rk-chat-chips" aria-label="Attached files"></div>
|
|
88
74
|
<div class="rk-chat-composer" id="rk-chat-composer">
|
|
89
|
-
<textarea id="rk-chat-input" class="rk-chat-input" rows="
|
|
75
|
+
<textarea id="rk-chat-input" class="rk-chat-input" rows="3"
|
|
90
76
|
placeholder="Describe your offer, audience, or the problem to solve…"></textarea>
|
|
91
|
-
<
|
|
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>
|
|
92
81
|
</div>
|
|
93
|
-
<p class="rk-chat-hint"
|
|
82
|
+
<p class="rk-chat-hint"><kbd>Enter</kbd> to send · <kbd>Shift</kbd>+<kbd>Enter</kbd> for a new line</p>
|
|
94
83
|
</div>
|
|
95
84
|
</div>
|
|
96
85
|
</div>
|
|
@@ -126,6 +115,8 @@ export function renderReskinChat(input = DEFAULT_INPUT) {
|
|
|
126
115
|
line-height:1;padding:2px 5px;border-radius:var(--rk-r-sm);opacity:0;
|
|
127
116
|
transition:opacity var(--rk-dur) var(--rk-ease),color var(--rk-dur) var(--rk-ease),background var(--rk-dur) var(--rk-ease)}
|
|
128
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}}
|
|
129
120
|
.rk-chat-hrow-del:hover{color:var(--rk-red);background:var(--rk-red-tint)}
|
|
130
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}
|
|
131
122
|
.rk-chat-history-note a{color:var(--rk-gold-700);font-weight:600}
|
|
@@ -139,27 +130,29 @@ export function renderReskinChat(input = DEFAULT_INPUT) {
|
|
|
139
130
|
align-items:center;justify-content:center;margin-right:2px;margin-top:2px}
|
|
140
131
|
.rk-chat-histbackdrop{display:none}
|
|
141
132
|
|
|
142
|
-
/* compact header */
|
|
133
|
+
/* compact header — sticky so the title stays pinned while the thread scrolls */
|
|
143
134
|
.rk-chat-head{display:flex;align-items:flex-start;gap:12px;
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
.rk-chat-
|
|
147
|
-
|
|
148
|
-
.rk-chat-title{font-family:var(--rk-font-display);font-weight:800;font-size:1.7rem;letter-spacing:-.025em;
|
|
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;
|
|
149
139
|
line-height:1.15;color:var(--rk-ink);min-width:0}
|
|
150
140
|
.rk-chat-subhead{font-size:14.5px;color:var(--rk-text-muted);line-height:1.5;max-width:56ch}
|
|
151
141
|
.rk-chat-head-actions{display:flex;align-items:center;gap:9px;flex:none}
|
|
152
142
|
.rk-chat-plus{font-size:14px;font-weight:700;line-height:1;margin-right:1px}
|
|
153
143
|
|
|
154
|
-
/* studio mode select (
|
|
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. */
|
|
155
147
|
.rk-chat-mode{position:relative;display:inline-flex;align-items:center}
|
|
156
|
-
.rk-chat-mode-select{appearance:none;-webkit-appearance:none;font-family:var(--rk-font-body);font-size:
|
|
157
|
-
color:var(--rk-ink);background:var(--rk-
|
|
158
|
-
padding:
|
|
159
|
-
transition:border-color var(--rk-dur) var(--rk-ease),box-shadow var(--rk-dur) var(--rk-ease)}
|
|
160
|
-
.rk-chat-mode-select:hover{border-color:var(--rk-gold-
|
|
161
|
-
.rk-chat-mode-select:focus{outline:none;border-color:var(--rk-gold-
|
|
162
|
-
.rk-chat-mode-chev{position:absolute;right:
|
|
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)}
|
|
163
156
|
|
|
164
157
|
/* thread — flex:1 pushes the dock to the bottom when the page is short */
|
|
165
158
|
.rk-chat-thread{flex:1;display:grid;gap:28px;align-content:start;padding:30px 0 26px}
|
|
@@ -172,16 +165,23 @@ export function renderReskinChat(input = DEFAULT_INPUT) {
|
|
|
172
165
|
font-family:var(--rk-font-display);font-weight:800;font-size:11px;line-height:1;
|
|
173
166
|
background:linear-gradient(142deg,var(--rk-gold-500),var(--rk-violet))}
|
|
174
167
|
|
|
175
|
-
/* bubbles — uniform 1px hairline on all four sides, no colored side stripes
|
|
176
|
-
|
|
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}
|
|
177
172
|
.rk-chat-bubble-user{max-width:min(78%,560px);background:var(--rk-ink);color:#f4f4f5;
|
|
178
173
|
border:1px solid var(--rk-n-800);box-shadow:var(--rk-shadow-sm)}
|
|
179
174
|
.rk-chat-bubble-assistant{width:100%;background:var(--rk-surface);color:var(--rk-n-700);
|
|
180
175
|
border:1px solid var(--rk-border);box-shadow:var(--rk-shadow-card)}
|
|
181
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}
|
|
182
179
|
|
|
183
180
|
/* streamed assistant text — pre-wrap keeps the model's own line breaks */
|
|
184
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
185
|
.rk-chat-stream[hidden]{display:none}
|
|
186
186
|
|
|
187
187
|
/* thinking / status row */
|
|
@@ -222,12 +222,48 @@ export function renderReskinChat(input = DEFAULT_INPUT) {
|
|
|
222
222
|
.rk-chat-chip:hover{border-color:var(--rk-gold-600);background:var(--rk-gold-tint);color:var(--rk-ink);
|
|
223
223
|
transform:translateY(-1px);box-shadow:var(--rk-shadow-sm)}
|
|
224
224
|
.rk-chat-chip:disabled{opacity:.5;cursor:default;transform:none;box-shadow:var(--rk-shadow-xs)}
|
|
225
|
-
|
|
226
|
-
|
|
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;
|
|
227
241
|
box-shadow:var(--rk-shadow-card);transition:border-color var(--rk-dur) var(--rk-ease),box-shadow var(--rk-dur) var(--rk-ease)}
|
|
228
242
|
.rk-chat-composer:focus-within{border-color:var(--rk-gold-600);box-shadow:var(--rk-ring-gold)}
|
|
229
|
-
.rk-chat-
|
|
230
|
-
|
|
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}
|
|
231
267
|
.rk-chat-input:focus{outline:none;box-shadow:none}
|
|
232
268
|
.rk-chat-input::placeholder{color:var(--rk-n-400)}
|
|
233
269
|
.rk-chat-send{flex:none;padding:11px 18px}
|
|
@@ -267,6 +303,8 @@ export function renderReskinChat(input = DEFAULT_INPUT) {
|
|
|
267
303
|
|
|
268
304
|
var input=root.getElementById('rk-chat-input');
|
|
269
305
|
var send=root.getElementById('rk-chat-send');
|
|
306
|
+
var SEND_HTML=${JSON.stringify('Send ' + SEND)};
|
|
307
|
+
var STOP_HTML=${JSON.stringify('Stop ' + STOP)};
|
|
270
308
|
var thread=root.getElementById('rk-chat-thread');
|
|
271
309
|
var title=root.getElementById('rk-chat-title');
|
|
272
310
|
var AVATAR='V';
|
|
@@ -274,13 +312,215 @@ export function renderReskinChat(input = DEFAULT_INPUT) {
|
|
|
274
312
|
var history=[]; // [{role, text}] — the running conversation
|
|
275
313
|
var threadId=null; // generated on first send, reused for persistence
|
|
276
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
|
+
});
|
|
277
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
|
+
}
|
|
278
398
|
function genId(p){ return p+'-'+Date.now().toString(36)+'-'+Math.random().toString(36).slice(2,8); }
|
|
279
|
-
function autosize(){ if(!input)return; input.style.height='auto'; input.style.height=Math.min(input.scrollHeight,
|
|
399
|
+
function autosize(){ if(!input)return; input.style.height='auto'; input.style.height=Math.min(input.scrollHeight,210)+'px'; }
|
|
280
400
|
function toBottom(){ try{ window.scrollTo({top:document.body.scrollHeight,behavior:'smooth'}); }catch(e){ window.scrollTo(0,document.body.scrollHeight); } }
|
|
281
401
|
function nowLabel(){ try{ return new Date().toLocaleTimeString([], {hour:'numeric', minute:'2-digit'}); }catch(e){ return ''; } }
|
|
282
402
|
function el(cls){ var d=root.createElement('div'); d.className=cls; return d; }
|
|
283
|
-
|
|
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,'&').replace(/</g,'<').replace(/>/g,'>'); }
|
|
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='×';
|
|
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
|
+
}
|
|
284
524
|
|
|
285
525
|
function clientContext(){
|
|
286
526
|
var now=new Date(); var local=null, tz=null;
|
|
@@ -301,7 +541,7 @@ export function renderReskinChat(input = DEFAULT_INPUT) {
|
|
|
301
541
|
function appendAssistant(){
|
|
302
542
|
var msg=el('rk-chat-msg is-assistant');
|
|
303
543
|
var meta=el('rk-chat-meta');
|
|
304
|
-
meta.innerHTML='
|
|
544
|
+
meta.innerHTML='Agent <span class="rk-chat-time">'+nowLabel()+'</span>';
|
|
305
545
|
var bub=el('rk-chat-bubble rk-chat-bubble-assistant');
|
|
306
546
|
var status=el('rk-chat-status');
|
|
307
547
|
status.innerHTML='<span class="rk-chat-typing"><span></span><span></span><span></span></span><span class="rk-chat-status-label">Thinking…</span>';
|
|
@@ -310,11 +550,14 @@ export function renderReskinChat(input = DEFAULT_INPUT) {
|
|
|
310
550
|
bub.appendChild(status); bub.appendChild(notes); bub.appendChild(textEl);
|
|
311
551
|
msg.appendChild(meta); msg.appendChild(bub);
|
|
312
552
|
if(thread) thread.appendChild(msg);
|
|
553
|
+
var lastText='';
|
|
313
554
|
return {
|
|
314
555
|
setStatus:function(s){ var l=status.querySelector('.rk-chat-status-label'); if(l && s){ l.textContent=s; } },
|
|
315
556
|
hideStatus:function(){ status.hidden=true; },
|
|
316
557
|
showText:function(){ status.hidden=true; textEl.hidden=false; },
|
|
317
|
-
setText:function(t){ textEl.textContent=t; },
|
|
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); } },
|
|
318
561
|
addNote:function(t){ var n=el('rk-chat-toolnote'); n.textContent=t; notes.appendChild(n); },
|
|
319
562
|
fail:function(m){ status.hidden=true; var er=el('rk-chat-error'); er.textContent=m||'Something went wrong.'; bub.appendChild(er); }
|
|
320
563
|
};
|
|
@@ -357,33 +600,62 @@ export function renderReskinChat(input = DEFAULT_INPUT) {
|
|
|
357
600
|
else if(chunk.type==='error'){ h.onError((typeof chunk.errorText==='string')?chunk.errorText:((chunk.error && chunk.error.message)||'Unable to read assistant response.')); }
|
|
358
601
|
}
|
|
359
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
|
+
|
|
360
612
|
function sendMessage(text){
|
|
361
613
|
if(busy) return;
|
|
362
614
|
text=(text||'').trim();
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
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);
|
|
367
628
|
if(input){ input.value=''; autosize(); }
|
|
368
|
-
|
|
629
|
+
attachments=[]; renderAttachChips();
|
|
369
630
|
toBottom();
|
|
370
631
|
|
|
371
632
|
var view=appendAssistant();
|
|
372
633
|
toBottom();
|
|
373
634
|
setBusy(true);
|
|
374
635
|
|
|
375
|
-
if(!threadId){ threadId=genId('thread'); }
|
|
636
|
+
if(!threadId){ threadId=genId('thread'); setChatUrl(threadId, true); }
|
|
376
637
|
var userMsgId=genId('user');
|
|
377
638
|
var asstMsgId=genId('assistant');
|
|
378
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) : '';
|
|
379
644
|
var body={
|
|
380
|
-
messages: history.map(function(m
|
|
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
|
+
}),
|
|
381
653
|
thread_id: threadId,
|
|
382
654
|
thread_template_id: (TEMPLATE && TEMPLATE.templateId) || 'chat-brainstorm',
|
|
383
|
-
thread_title: text.slice(0,42),
|
|
655
|
+
thread_title: (text||line).slice(0,42),
|
|
384
656
|
thread_tracers: (TEMPLATE && TEMPLATE.tracers) || [],
|
|
385
657
|
client_context: clientContext(),
|
|
386
|
-
user_message: { id:userMsgId, role:'user', text:text, attachments:
|
|
658
|
+
user_message: { id:userMsgId, role:'user', text:text||line, attachments:atts },
|
|
387
659
|
assistant_message: { id:asstMsgId }
|
|
388
660
|
};
|
|
389
661
|
if(TEMPLATE){ body.template=TEMPLATE; }
|
|
@@ -403,9 +675,11 @@ export function renderReskinChat(input = DEFAULT_INPUT) {
|
|
|
403
675
|
onError:function(m){ errored=true; view.fail(m); toBottom(); }
|
|
404
676
|
};
|
|
405
677
|
|
|
678
|
+
var controller=(typeof AbortController!=='undefined') ? new AbortController() : null;
|
|
679
|
+
pendingAbort=controller;
|
|
406
680
|
var req;
|
|
407
681
|
try{
|
|
408
|
-
req=fetch(ENDPOINT,{ method:'POST', headers:headers, credentials:'same-origin', body:JSON.stringify(body) });
|
|
682
|
+
req=fetch(ENDPOINT,{ method:'POST', headers:headers, credentials:'same-origin', body:JSON.stringify(body), signal: controller?controller.signal:undefined });
|
|
409
683
|
}catch(err){
|
|
410
684
|
// Synchronous fetch failure (e.g. invalid URL) — never let it throw uncaught.
|
|
411
685
|
req=Promise.reject(err);
|
|
@@ -419,9 +693,11 @@ export function renderReskinChat(input = DEFAULT_INPUT) {
|
|
|
419
693
|
}
|
|
420
694
|
return readStream(resp, handlers);
|
|
421
695
|
}).then(function(){
|
|
696
|
+
pendingAbort=null;
|
|
422
697
|
view.hideStatus();
|
|
423
698
|
if(sawText){
|
|
424
699
|
history.push({role:'assistant', text:acc});
|
|
700
|
+
view.renderMd();
|
|
425
701
|
} else if(!errored){
|
|
426
702
|
view.showText();
|
|
427
703
|
view.setText('The assistant finished without returning any text.');
|
|
@@ -431,6 +707,17 @@ export function renderReskinChat(input = DEFAULT_INPUT) {
|
|
|
431
707
|
toBottom();
|
|
432
708
|
loadHistory();
|
|
433
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
|
+
}
|
|
434
721
|
var msg=(err && err.message) ? err.message : String(err);
|
|
435
722
|
if(!API_KEY){ msg=msg+'\\n\\nAdd an AI provider key in Settings to brainstorm on your own keys.'; }
|
|
436
723
|
view.fail(msg);
|
|
@@ -512,9 +799,10 @@ export function renderReskinChat(input = DEFAULT_INPUT) {
|
|
|
512
799
|
})
|
|
513
800
|
.catch(function(){ if(histList){ histList.innerHTML=''; histList.appendChild(histNote('Couldn\\u2019t load your chats.', {retry:true})); } });
|
|
514
801
|
}
|
|
515
|
-
function appendAssistantStatic(text){ var v=appendAssistant(); v.showText(); v.setText(text||''); v.hideStatus(); return v; }
|
|
516
|
-
function openThread(id){
|
|
802
|
+
function appendAssistantStatic(text){ var v=appendAssistant(); v.showText(); v.setText(text||''); v.hideStatus(); v.renderMd(); return v; }
|
|
803
|
+
function openThread(id, opts){
|
|
517
804
|
if(busy || loadingThread) return;
|
|
805
|
+
if(!(opts && opts.skipUrl)) setChatUrl(id, false);
|
|
518
806
|
loadingThread=true;
|
|
519
807
|
closeHistoryOverlay();
|
|
520
808
|
fetch(THREADS_URL+'/'+encodeURIComponent(id), { headers:authHeaders(), credentials:'same-origin' })
|
|
@@ -529,7 +817,6 @@ export function renderReskinChat(input = DEFAULT_INPUT) {
|
|
|
529
817
|
history.push({role:role, text:txt});
|
|
530
818
|
if(role==='user'){ appendUser(txt); } else { appendAssistantStatic(txt); }
|
|
531
819
|
}
|
|
532
|
-
if(title){ title.textContent=t.title || 'Brainstorm studio'; }
|
|
533
820
|
setActiveThread(id);
|
|
534
821
|
loadingThread=false;
|
|
535
822
|
toBottom();
|
|
@@ -547,15 +834,37 @@ export function renderReskinChat(input = DEFAULT_INPUT) {
|
|
|
547
834
|
})
|
|
548
835
|
.catch(function(){});
|
|
549
836
|
}
|
|
550
|
-
|
|
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){
|
|
551
865
|
history=[]; threadId=null;
|
|
552
|
-
if(
|
|
553
|
-
|
|
554
|
-
'<div class="rk-chat-meta"><span class="rk-chat-avatar" aria-hidden="true">'+AVATAR+'</span>Copilot</div>'+
|
|
555
|
-
'<div class="rk-chat-bubble rk-chat-bubble-assistant"><div class="rk-chat-stream">Fresh page. Tell me the offer, who it\\u2019s for, and where the traffic is cold \\u2014 I\\u2019ll come back with angles, hooks, and scripts you can render.</div></div>'+
|
|
556
|
-
'</div>';
|
|
557
|
-
}
|
|
558
|
-
if(title){ title.textContent='Brainstorm studio'; }
|
|
866
|
+
if(!(opts && opts.skipUrl)) setChatUrl(null, false);
|
|
867
|
+
renderEmptyState();
|
|
559
868
|
setActiveThread(null);
|
|
560
869
|
if(input){ input.value=''; autosize(); input.focus(); }
|
|
561
870
|
window.scrollTo(0,0);
|
|
@@ -566,39 +875,65 @@ export function renderReskinChat(input = DEFAULT_INPUT) {
|
|
|
566
875
|
if(histBackdrop){ histBackdrop.addEventListener('click', closeHistoryOverlay); }
|
|
567
876
|
loadHistory();
|
|
568
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
|
+
|
|
569
890
|
// composer wiring
|
|
570
891
|
if(input){ input.addEventListener('input',autosize); autosize(); }
|
|
571
|
-
if(send){ send.addEventListener('click',function(){ if(input){ sendMessage(input.value); } }); }
|
|
892
|
+
if(send){ send.addEventListener('click',function(){ if(busy){ if(pendingAbort){ pendingAbort.abort(); } return; } if(input){ sendMessage(input.value); } }); }
|
|
572
893
|
if(input){
|
|
573
894
|
input.addEventListener('keydown',function(ev){
|
|
574
|
-
if(ev.key==='Enter' && !ev.shiftKey){ ev.preventDefault(); sendMessage(input.value); }
|
|
895
|
+
if(ev.key==='Enter' && !ev.shiftKey){ ev.preventDefault(); if(busy){ if(pendingAbort){ pendingAbort.abort(); } return; } sendMessage(input.value); }
|
|
575
896
|
});
|
|
576
897
|
}
|
|
577
898
|
|
|
578
|
-
// starter chips send their full prompt directly
|
|
579
|
-
root.querySelectorAll('[data-rk-prompt]').forEach(function(chip){
|
|
580
|
-
chip.addEventListener('click',function(){ if(busy) return; sendMessage(chip.getAttribute('data-rk-prompt')||''); });
|
|
581
|
-
});
|
|
582
|
-
|
|
583
899
|
// "New" resets the conversation to the warm empty state (shared resetChat)
|
|
584
900
|
var newBtn=root.getElementById('rk-chat-new');
|
|
585
901
|
if(newBtn){
|
|
586
902
|
newBtn.addEventListener('click',function(){ if(busy) return; resetChat(); });
|
|
587
903
|
}
|
|
588
904
|
|
|
589
|
-
//
|
|
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.
|
|
590
924
|
var modeSel=root.getElementById('rk-chat-mode');
|
|
591
925
|
if(modeSel){
|
|
592
926
|
modeSel.addEventListener('change',function(){
|
|
593
927
|
var dest=modeSel.value;
|
|
594
|
-
if(dest && dest!=='/chat'){
|
|
928
|
+
if(dest && dest!=='/chat'){ navigateWithChat(dest); }
|
|
595
929
|
});
|
|
596
930
|
}
|
|
597
931
|
})();`;
|
|
598
932
|
return reskinDocument({
|
|
599
|
-
title: "vidfarm
|
|
600
|
-
description: "
|
|
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.",
|
|
601
935
|
activeSlug: "chat",
|
|
936
|
+
account: { name: input.name, email: input.email },
|
|
602
937
|
pageCss,
|
|
603
938
|
body,
|
|
604
939
|
script
|