@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,732 +0,0 @@
1
- // /reskin/settings — the flagship reskin, now MIGRATED to real production data.
2
- //
3
- // This is the reference for the /reskin/* UX migration: the renderer accepts the
4
- // SAME input object the live settings route already builds (via
5
- // `Parameters<typeof renderSettingsPage>[0]`), so the route wiring is just "fetch
6
- // real data + call this instead of the legacy renderer". Every form posts to the
7
- // REAL /settings/* endpoints with the correct account + ids, so profile save,
8
- // AI-key add/remove, FlockPoster connect, channel resend/delete, attachment
9
- // upload/delete, and wallet top-up all actually work. Design stays farmville
10
- // (shared rk-* primitives, isolated CSS); only the data + actions became real.
11
- import { reskinDocument, rkEscape } from "./document.js";
12
- const EYE = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7Z"/><circle cx="12" cy="12" r="3"/></svg>`;
13
- const SEARCH_ICON = `<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5"/></svg>`;
14
- function money(n) {
15
- const abs = Math.abs(n).toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
16
- return `${n < 0 ? "−" : ""}$${abs}`;
17
- }
18
- function fmtBytes(bytes) {
19
- if (!bytes || bytes < 1024)
20
- return `${bytes || 0} B`;
21
- const units = ["KB", "MB", "GB"];
22
- let v = bytes / 1024;
23
- let i = 0;
24
- while (v >= 1024 && i < units.length - 1) {
25
- v /= 1024;
26
- i++;
27
- }
28
- return `${v.toFixed(v >= 10 ? 0 : 1)} ${units[i]}`;
29
- }
30
- function fmtWhen(ms) {
31
- if (!ms || ms <= 0)
32
- return "";
33
- try {
34
- return new Date(ms).toLocaleString("en-US", { dateStyle: "medium", timeStyle: "short" });
35
- }
36
- catch {
37
- return "";
38
- }
39
- }
40
- function fmtDate(iso) {
41
- const t = Date.parse(iso);
42
- if (Number.isNaN(t))
43
- return iso;
44
- return new Date(t).toLocaleDateString("en-US", { month: "short", day: "numeric" });
45
- }
46
- function walletEventTitle(e) {
47
- if (e.entity_type === "wallet_funding_event")
48
- return "Wallet top-up";
49
- if (e.cost_center_slug === "hyperframes_lambda" || e.cost_center_slug === "remotion_lambda")
50
- return "Timeline render usage";
51
- if (e.cost_center_slug === "wallet_funding")
52
- return "Wallet funding";
53
- if (e.cost_center_slug) {
54
- return e.cost_center_slug.split(/[_-]+/g).filter(Boolean)
55
- .map((p) => p.charAt(0).toUpperCase() + p.slice(1)).join(" ");
56
- }
57
- return "Usage charge";
58
- }
59
- export function renderReskinSettings(input) {
60
- const acct = `<input type="hidden" name="account" value="${rkEscape(input.currentAccountId)}" />`;
61
- const displayName = input.name ?? "";
62
- const activeTab = input.activeTab || "profile";
63
- const flash = input.error
64
- ? `<div class="rk-notice rk-notice-err rk-set-flash">${rkEscape(input.error)}</div>`
65
- : input.notice
66
- ? `<div class="rk-notice rk-notice-ok rk-set-flash">${rkEscape(input.notice)}</div>`
67
- : "";
68
- // ---- Profile ----
69
- const attachmentCards = input.attachments.length
70
- ? input.attachments.map((a) => {
71
- const ext = (a.fileName.split(".").pop() || a.contentType.split("/").pop() || "file").toUpperCase().slice(0, 4);
72
- return `
73
- <div class="rk-item">
74
- <span class="rk-tile rk-tile-txt rk-tile-sky">${rkEscape(ext)}</span>
75
- <div class="rk-item-main">
76
- <div class="rk-item-title">${rkEscape(a.fileName)}</div>
77
- <div class="rk-item-sub">${rkEscape(fmtBytes(a.sizeBytes))} · added ${rkEscape(fmtDate(a.createdAt))}</div>
78
- </div>
79
- <a class="rk-btn rk-btn-ghost rk-btn-sm" href="${rkEscape(a.viewUrl)}" target="_blank" rel="noreferrer">Open</a>
80
- <form method="post" action="/settings/attachments/${encodeURIComponent(a.id)}/delete" data-rk-confirm="Delete ${rkEscape(a.fileName)}?" style="display:inline">
81
- ${acct}
82
- <button class="rk-btn rk-btn-danger rk-btn-sm" type="submit">Remove</button>
83
- </form>
84
- </div>`;
85
- }).join("")
86
- : `<p class="rk-muted rk-set-empty">No brand assets yet — upload logos, fonts, VO, or footage to reuse across projects.</p>`;
87
- const profile = `
88
- <div class="rk-card rk-set-panel-card">
89
- <div class="rk-card-head">
90
- <span class="rk-eyebrow">Profile</span>
91
- <h2>Profile, notes &amp; attachments</h2>
92
- <p class="rk-muted">The product description powers Swipe mode's auto-recut. Attachments are your reusable brand assets.</p>
93
- </div>
94
- <form class="rk-stack" method="post" action="/settings/profile">
95
- ${acct}
96
- <input type="hidden" name="groupchat_url" value="${rkEscape(input.groupchatUrl ?? "")}" />
97
- <div class="rk-field">
98
- <label class="rk-label" for="rk-name">Display name</label>
99
- <input class="rk-input" id="rk-name" type="text" name="display_name" value="${rkEscape(displayName)}" />
100
- </div>
101
- <div class="rk-field">
102
- <label class="rk-label" for="rk-about">Product description</label>
103
- <textarea class="rk-textarea" id="rk-about" name="about" placeholder="What it is, who it's for, the promise…">${rkEscape(input.about ?? "")}</textarea>
104
- <span class="rk-hint">Swipe mode reads this to auto-recut templates into ads for you.</span>
105
- </div>
106
- <div class="rk-row">
107
- <button class="rk-btn rk-btn-ink" type="submit">Save changes</button>
108
- <span class="rk-hint">Signed in as ${rkEscape(input.email)}</span>
109
- </div>
110
- </form>
111
- </div>
112
- <div class="rk-card rk-set-panel-card rk-set-myfiles">
113
- <div class="rk-card-head">
114
- <span class="rk-eyebrow">My Files</span>
115
- <h2>Your files</h2>
116
- <p class="rk-muted">Your private library — logos, fonts, VO, footage, and notes. Open <b>My Files</b> or <b>Temp</b> below and browse into folders.</p>
117
- </div>
118
- <form method="post" action="/settings/attachments" enctype="multipart/form-data" class="rk-stack rk-set-upload" data-rk-upload>
119
- ${acct}
120
- <label class="rk-set-drop" for="rk-files">
121
- <div><strong>Choose files</strong> to upload — logos, fonts, VO, footage.</div>
122
- <input class="rk-set-file" id="rk-files" type="file" name="files" multiple />
123
- </label>
124
- <div class="rk-row"><button class="rk-btn rk-btn-gold rk-btn-sm" type="submit" data-rk-upload-btn>Upload files</button></div>
125
- </form>
126
- <div class="rk-set-explorer" data-rk-fx>
127
- <div class="rk-aichat-crumbs rk-set-fx-crumbs" data-rk-fx-crumbs aria-label="Current folder"></div>
128
- <div class="rk-set-fx-body" data-rk-fx-body></div>
129
- </div>
130
- </div>`;
131
- // ---- Wallet ----
132
- const walletBalance = input.wallet.unavailableReason
133
- ? `<div class="rk-notice rk-notice-err">${rkEscape(input.wallet.unavailableReason)}</div>`
134
- : `<div class="rk-stat-num rk-set-balance-num">${money(input.wallet.balanceUsd)}</div>
135
- <p class="rk-muted">Available balance${input.wallet.updatedAtMs > 0 ? ` · updated ${rkEscape(fmtWhen(input.wallet.updatedAtMs))}` : ""}</p>`;
136
- const historyRows = input.walletEvents.unavailableReason
137
- ? `<div class="rk-notice rk-notice-err">${rkEscape(input.walletEvents.unavailableReason)}</div>`
138
- : input.walletEvents.events.length
139
- ? input.walletEvents.events.map((e) => {
140
- const credit = e.entity_type === "wallet_funding_event" || e.amount_usd > 0;
141
- const sub = [e.cost_center_slug || e.entity_type, e.job_id, e.tracer].filter(Boolean).join(" · ");
142
- return `
143
- <div class="rk-item">
144
- <span class="rk-tile rk-tile-sm rk-tile-txt ${credit ? "rk-tile-green" : "rk-tile-ink"}">${credit ? "+" : "−"}</span>
145
- <div class="rk-item-main">
146
- <div class="rk-item-title">${rkEscape(walletEventTitle(e))}</div>
147
- <div class="rk-item-sub">${rkEscape(sub)} · ${rkEscape(fmtWhen(e.occurred_at_ms))}</div>
148
- </div>
149
- <div class="rk-set-amount ${credit ? "is-credit" : ""}">${money(e.amount_usd)}</div>
150
- </div>`;
151
- }).join("")
152
- : `<p class="rk-muted rk-set-empty">No wallet activity yet.</p>`;
153
- const wallet = `
154
- <div class="rk-stack rk-set-panel">
155
- <div class="rk-card rk-set-balance">
156
- <span class="rk-eyebrow">Wallet</span>
157
- ${walletBalance}
158
- <div class="rk-row rk-wrap rk-set-balance-cta">
159
- <button class="rk-btn rk-btn-gold" type="button" data-rk-topup>Top up wallet <span class="rk-arrow">→</span></button>
160
- </div>
161
- <div class="rk-hint rk-set-topup-status" data-rk-topup-status hidden></div>
162
- </div>
163
- <div class="rk-card">
164
- <div class="rk-card-head">
165
- <span class="rk-eyebrow">Ledger</span>
166
- <h2>Transparent billing history</h2>
167
- <p class="rk-muted">Every render and top-up, itemized. You're only charged for compute — BYO AI keys are never billed.</p>
168
- </div>
169
- <div class="rk-list">${historyRows}</div>
170
- </div>
171
- </div>`;
172
- // ---- Channels ----
173
- // Mirrors the live /settings#channels ordering: topline heading + Connect
174
- // button + "…" menu (Add Email Channel), then a channel search, then the
175
- // channel list. The FlockPoster key + email channel forms live in dialogs.
176
- const emailCards = input.emailChannels.map((ch) => {
177
- const status = ch.status || "Pending";
178
- const statusPill = /connect|active|verified/i.test(status) ? "rk-pill-green" : "rk-pill-gold";
179
- const hay = [ch.title, ch.platform || "email", ch.handle].filter(Boolean).join(" ").toLowerCase();
180
- return `<div class="rk-item" data-rk-channel-hay="${rkEscape(hay)}">
181
- <span class="rk-tile rk-tile-txt rk-tile-sky">${rkEscape((ch.platform || "E").charAt(0).toUpperCase())}</span>
182
- <div class="rk-item-main">
183
- <div class="rk-item-title">${rkEscape(ch.title)}</div>
184
- <div class="rk-item-sub">${rkEscape(ch.platform || "Email")} · ${rkEscape(ch.handle || "")}</div>
185
- </div>
186
- <span class="rk-pill ${statusPill}">${rkEscape(status)}</span>
187
- <form method="post" action="/settings/channels/email/${encodeURIComponent(ch.rawId ?? "")}/resend" style="display:inline">${acct}<button class="rk-btn rk-btn-ghost rk-btn-sm" type="submit">Resend</button></form>
188
- <form method="post" action="/settings/channels/email/${encodeURIComponent(ch.rawId ?? "")}/delete" data-rk-confirm="Remove this channel?" style="display:inline">${acct}<button class="rk-btn rk-btn-danger rk-btn-sm" type="submit">Remove</button></form>
189
- </div>`;
190
- }).join("");
191
- const flockCards = input.flockposterChannels.map((ch) => {
192
- const status = ch.status || "Connected";
193
- const tint = /tiktok/i.test(ch.platform || "") ? "rk-tile-ink" : /insta/i.test(ch.platform || "") ? "rk-tile-coral" : "rk-tile-sky";
194
- const hay = [ch.title, ch.platform, ch.handle].filter(Boolean).join(" ").toLowerCase();
195
- return `<div class="rk-item" data-rk-channel-hay="${rkEscape(hay)}">
196
- <span class="rk-tile rk-tile-txt ${tint}">${rkEscape((ch.platform || "F").charAt(0).toUpperCase())}</span>
197
- <div class="rk-item-main">
198
- <div class="rk-item-title">${rkEscape(ch.title)}</div>
199
- <div class="rk-item-sub">${rkEscape(ch.platform || "")} · ${rkEscape(ch.handle || "")}</div>
200
- </div>
201
- <span class="rk-pill rk-pill-green">${rkEscape(status)}</span>
202
- </div>`;
203
- }).join("");
204
- const channelCards = flockCards + emailCards;
205
- const channelCount = input.emailChannels.length + input.flockposterChannels.length;
206
- const hasFlockKey = Boolean(input.flockposterApiKey);
207
- const channels = `
208
- <div class="rk-stack rk-set-panel">
209
- ${input.flockposterChannelsError ? `<div class="rk-notice rk-notice-err">${rkEscape(input.flockposterChannelsError)}</div>` : ""}
210
- <div class="rk-card">
211
- <div class="rk-spread rk-set-channels-topline">
212
- <div class="rk-card-head rk-set-channels-headcopy">
213
- <span class="rk-eyebrow">Channels</span>
214
- <h2>${channelCount} connected channel${channelCount === 1 ? "" : "s"}</h2>
215
- <p class="rk-muted">Where approved cuts get published. Connect via FlockPoster, or add an email destination.</p>
216
- </div>
217
- <div class="rk-row rk-set-channels-actions">
218
- <button class="rk-btn rk-btn-gold rk-btn-sm" type="button" data-rk-open-flock>Connect</button>
219
- <div class="rk-set-menu-wrap">
220
- <button class="rk-btn rk-btn-ghost rk-btn-sm rk-set-menu-btn" type="button" data-rk-channel-menu aria-expanded="false" aria-label="More channel actions">…</button>
221
- <div class="rk-set-menu" data-rk-channel-menu-pop hidden>
222
- <button type="button" data-rk-open-email>Add Email Channel</button>
223
- </div>
224
- </div>
225
- </div>
226
- </div>
227
- <label class="rk-set-chsearch">
228
- <span class="rk-set-chsearch-ic" aria-hidden="true">${SEARCH_ICON}</span>
229
- <input class="rk-input" data-rk-channel-search type="search"
230
- placeholder="${channelCount ? "Search channels by name, handle, or platform" : "Add an email channel or connect FlockPoster to search channels"}"
231
- autocomplete="off" spellcheck="false" aria-label="Search channels"${channelCount ? "" : " disabled"} />
232
- </label>
233
- <div class="rk-list" data-rk-channel-list>${channelCards || `<p class="rk-muted rk-set-empty">No channels connected yet.</p>`}</div>
234
- <p class="rk-muted rk-set-empty" data-rk-channel-noresults hidden>No channels match your search.</p>
235
- </div>
236
- </div>
237
- <dialog class="rk-set-dialog" data-rk-flock-dialog${input.openFlockPosterDialog ? ` data-rk-autoopen="1"` : ""}>
238
- <form class="rk-set-dialog-form" method="post" action="/settings/channels/flockposter">
239
- ${acct}
240
- <div class="rk-card-head">
241
- <span class="rk-eyebrow">FlockPoster settings</span>
242
- <h3>FlockPoster API key</h3>
243
- <p class="rk-muted">${hasFlockKey ? "Update the stored key and retest the connection before saving." : "Paste a FlockPoster API key to connect this workspace and load its channels."}</p>
244
- </div>
245
- <div class="rk-field">
246
- <label class="rk-label" for="rk-fp">API key</label>
247
- <div class="rk-input-group">
248
- <input class="rk-input" id="rk-fp" type="password" name="flockposter_api_key" placeholder="${hasFlockKey ? "•••••••• (stored) — paste to replace" : "fp_live_…"}" autocomplete="off" />
249
- <button class="rk-btn rk-btn-ghost rk-btn-sm" type="button" data-rk-reveal="rk-fp" aria-label="Show API key">${EYE}</button>
250
- </div>
251
- <span class="rk-hint">${hasFlockKey ? "The key stays hidden by default — changing it triggers a connection test on save." : "We test the key before storing it. If the connection succeeds, this tab loads your channels."}</span>
252
- </div>
253
- <div class="rk-set-dialog-actions">
254
- <a class="rk-btn rk-btn-ghost rk-btn-sm" href="https://flockposter.com" target="_blank" rel="noreferrer">Visit App</a>
255
- <button type="button" class="rk-btn rk-btn-ghost" data-rk-dialog-close>Close</button>
256
- <button type="submit" class="rk-btn rk-btn-gold">${hasFlockKey ? "Save" : "Connect"} &amp; test</button>
257
- </div>
258
- </form>
259
- </dialog>
260
- <dialog class="rk-set-dialog" data-rk-email-dialog>
261
- <form class="rk-set-dialog-form" method="post" action="/settings/channels/email">
262
- ${acct}
263
- <div class="rk-card-head">
264
- <span class="rk-eyebrow">Email channel</span>
265
- <h3>Add Email Channel</h3>
266
- <p class="rk-muted">We'll send a consent confirmation before this email can be used for scheduled delivery.</p>
267
- </div>
268
- <div class="rk-field"><label class="rk-label" for="rk-em-title">Title</label><input class="rk-input" id="rk-em-title" name="title" required maxlength="160" placeholder="Client newsletter" /></div>
269
- <div class="rk-field"><label class="rk-label" for="rk-em-addr">Email</label><input class="rk-input" id="rk-em-addr" name="email" type="email" required maxlength="320" placeholder="name@example.com" /></div>
270
- <div class="rk-field"><label class="rk-label" for="rk-em-note">Note</label><textarea class="rk-textarea rk-set-email-note" id="rk-em-note" name="note" maxlength="2000" placeholder="Internal notes for team"></textarea></div>
271
- <div class="rk-set-dialog-actions">
272
- <button type="button" class="rk-btn rk-btn-ghost" data-rk-dialog-close>Close</button>
273
- <button type="submit" class="rk-btn rk-btn-ink">Send confirmation</button>
274
- </div>
275
- </form>
276
- </dialog>`;
277
- // ---- Developer ----
278
- const keyRows = input.providerKeys.length
279
- ? input.providerKeys.map((k, i) => {
280
- const id = `rk-pk-${i}`;
281
- const tint = k.provider === "openai" ? "rk-tile-green" : k.provider === "gemini" ? "rk-tile-sky" : k.provider === "elevenlabs" ? "rk-tile-amber" : "rk-tile-violet";
282
- const status = k.status || "active";
283
- const lastUsed = k.last_used_at ? `used ${rkEscape(fmtDate(k.last_used_at))}` : "never used";
284
- return `<div class="rk-item">
285
- <span class="rk-tile rk-tile-txt ${tint}">${rkEscape((k.provider || "?").charAt(0).toUpperCase())}</span>
286
- <div class="rk-item-main">
287
- <div class="rk-item-title">${rkEscape(k.label || `${k.provider} key`)} <span class="rk-pill rk-pill-green">${rkEscape(status)}</span></div>
288
- <div class="rk-item-sub"><span class="rk-mono" id="${id}" data-rk-secret="${rkEscape(k.secret)}">${"•".repeat(18)}</span> · ${rkEscape(k.provider)} · ${lastUsed}</div>
289
- </div>
290
- <button class="rk-btn rk-btn-ghost rk-btn-sm" type="button" data-rk-reveal-text="${id}" aria-label="Show key">${EYE}</button>
291
- <form method="post" action="/settings/provider-keys/${encodeURIComponent(k.id)}/delete" data-rk-confirm="Remove this AI key?" style="display:inline">
292
- ${acct}
293
- <button class="rk-btn rk-btn-danger rk-btn-sm" type="submit">Remove</button>
294
- </form>
295
- </div>`;
296
- }).join("")
297
- : `<p class="rk-muted rk-set-empty">No AI keys yet — add one to enable clip tagging, recaption, and Swipe.</p>`;
298
- const developer = `
299
- <div class="rk-stack rk-set-panel">
300
- <div class="rk-card">
301
- <div class="rk-card-head">
302
- <span class="rk-eyebrow">Developer</span>
303
- <h2>Your vidfarm API key</h2>
304
- <p class="rk-muted">Authenticates the <code>vidfarm</code> CLI and REST calls. Keep it secret.</p>
305
- </div>
306
- <div class="rk-field">
307
- <div class="rk-input-group">
308
- <input class="rk-input rk-mono" id="rk-vfkey" type="password" value="${rkEscape(input.vidfarmApiKey)}" readonly />
309
- <button class="rk-btn rk-btn-ghost rk-btn-sm" type="button" data-rk-reveal="rk-vfkey" aria-label="Show key">${EYE}</button>
310
- <button class="rk-btn rk-btn-ink rk-btn-sm" type="button" data-rk-copy="rk-vfkey">Copy</button>
311
- </div>
312
- </div>
313
- <div class="rk-row rk-wrap rk-set-skillrow">
314
- <button class="rk-btn rk-btn-ghost rk-btn-sm" type="button" data-rk-skill-copy data-rk-skill-content="${rkEscape(input.directorSkill || "")}">Copy Director SKILL.md</button>
315
- <span class="rk-hint">The full REST + CLI playbook for driving vidfarm from an agent — one click, paste into your agent.</span>
316
- </div>
317
- </div>
318
- <div class="rk-card">
319
- <div class="rk-card-head">
320
- <span class="rk-eyebrow">AI keys</span>
321
- <h2>Bring your own AI Keys</h2>
322
- <p class="rk-muted">Stored per workspace. OpenAI, Gemini, or OpenRouter power clip tagging, recaption, image/video generation, and Swipe — BYOK, never billed by vidfarm. <b>ElevenLabs is audio-only</b> (music, voiceover, transcription) and does <b>not</b> count as a qualified AI key — you still need one of the others for the core features. With no ElevenLabs key, music &amp; speech run on vidfarm's key and bill your wallet (<code>use_wallet_credits</code>).</p>
323
- </div>
324
- <div class="rk-list rk-set-keys">${keyRows}</div>
325
- <form class="rk-set-addkey" method="post" action="/settings/provider-keys">
326
- ${acct}
327
- <div class="rk-grid rk-grid-3 rk-set-addkey-grid">
328
- <div class="rk-field">
329
- <label class="rk-label" for="rk-prov">Provider</label>
330
- <select class="rk-select" id="rk-prov" name="provider">
331
- <option value="openai">OpenAI</option><option value="gemini">Gemini</option><option value="openrouter">OpenRouter</option><option value="elevenlabs">ElevenLabs (audio only)</option>
332
- </select>
333
- </div>
334
- <div class="rk-field">
335
- <label class="rk-label" for="rk-prov-label">Label</label>
336
- <input class="rk-input" id="rk-prov-label" name="label" placeholder="Optional" />
337
- </div>
338
- <div class="rk-field">
339
- <label class="rk-label" for="rk-prov-secret">API key</label>
340
- <input class="rk-input" id="rk-prov-secret" type="password" name="secret" placeholder="sk-…" />
341
- </div>
342
- </div>
343
- <button class="rk-btn rk-btn-gold" type="submit">Add AI key</button>
344
- </form>
345
- </div>
346
- </div>`;
347
- // ---- Log out ----
348
- const logout = `
349
- <div class="rk-card rk-set-panel-card rk-set-logout">
350
- <div class="rk-card-head">
351
- <span class="rk-eyebrow">Session</span>
352
- <h2>Log out</h2>
353
- <p class="rk-muted">End this browser session. You'll need to sign back in as <b>${rkEscape(input.email)}</b> to return to your studio.</p>
354
- </div>
355
- <form method="post" action="/logout" class="rk-row rk-wrap rk-set-logout-actions">
356
- ${acct}
357
- <button class="rk-btn rk-btn-danger" type="submit">Log out</button>
358
- <button class="rk-btn rk-btn-ghost" type="button" data-rk-tab="profile">Cancel</button>
359
- </form>
360
- </div>`;
361
- const tab = (slug, label, danger = false) => `<button type="button" class="rk-side-item${danger ? " rk-side-item-danger" : ""}${slug === activeTab ? " is-active" : ""}" data-rk-tab="${slug}">${label}</button>`;
362
- const body = `
363
- <main class="rk-container rk-page">
364
- <div class="rk-page-head">
365
- <h1 class="rk-h1">Settings</h1>
366
- <p class="rk-sub">Manage your profile, wallet, publishing channels, and developer access.</p>
367
- </div>
368
- ${flash}
369
- <div class="rk-shell">
370
- <nav class="rk-side" aria-label="Settings sections">
371
- ${tab("profile", "Profile")}
372
- ${tab("wallet", "Wallet")}
373
- ${tab("channels", "Channels")}
374
- ${tab("developer", "Developer")}
375
- <div class="rk-side-sep"></div>
376
- <a class="rk-side-item" href="https://billing.stripe.com/p/login/fZecQubkxcwOgDuaEE" target="_blank" rel="noopener noreferrer">Subscription</a>
377
- ${tab("logout", "Log out", true)}
378
- </nav>
379
- <div class="rk-set-panels">
380
- <section class="rk-reveal" data-rk-panel="profile"${activeTab === "profile" ? "" : " hidden"}>${profile}</section>
381
- <section class="rk-reveal" data-rk-panel="wallet"${activeTab === "wallet" ? "" : " hidden"}>${wallet}</section>
382
- <section class="rk-reveal" data-rk-panel="channels"${activeTab === "channels" ? "" : " hidden"}>${channels}</section>
383
- <section class="rk-reveal" data-rk-panel="developer"${activeTab === "developer" ? "" : " hidden"}>${developer}</section>
384
- <section class="rk-reveal" data-rk-panel="logout" hidden>${logout}</section>
385
- </div>
386
- </div>
387
- </main>`;
388
- const pageCss = `
389
- .rk-tile-txt{font-family:var(--rk-font-display);font-weight:700;font-size:14px;letter-spacing:.01em;line-height:1}
390
- .rk-tile-txt.rk-tile-sm{font-size:15px}
391
- .rk-set-flash{max-width:760px;margin-bottom:20px}
392
- .rk-set-panel-card,.rk-set-panel{max-width:760px}
393
- .rk-set-panel-card + .rk-set-panel-card{margin-top:20px}
394
- .rk-set-empty{padding:8px 2px}
395
- .rk-set-drop{display:flex;align-items:center;gap:14px;padding:20px;border:1.5px dashed var(--rk-border-strong);border-radius:var(--rk-r-2xl);background:var(--rk-n-50);font-size:14px;color:var(--rk-text-muted);cursor:pointer}
396
- .rk-set-drop:hover{border-color:var(--rk-gold-600);background:var(--rk-gold-tint)}
397
- .rk-set-file{margin-left:auto;font-size:13px;max-width:220px}
398
- .rk-set-attach{margin-top:4px}
399
- .rk-set-balance{display:grid;gap:10px;align-content:start}
400
- .rk-set-balance-num{font-size:var(--rk-text-5xl)}
401
- .rk-set-balance-cta{margin-top:6px}
402
- .rk-set-topup-status{margin-top:4px}
403
- .rk-set-amount{font-family:var(--rk-font-display);font-weight:700;font-size:15px;color:var(--rk-n-600);white-space:nowrap}
404
- .rk-set-amount.is-credit{color:#16a34a}
405
- .rk-set-keys{margin-bottom:20px}
406
- .rk-set-addkey{border-top:1px solid var(--rk-border);padding-top:20px;display:grid;gap:16px}
407
- .rk-set-addkey-grid{gap:14px}
408
- .rk-set-skillrow{margin-top:14px;padding-top:14px;border-top:1px solid var(--rk-border)}
409
- .rk-set-skillrow .rk-hint{flex:1;min-width:200px}
410
-
411
- /* log-out tab — danger-styled side item + a hairline separator above it */
412
- .rk-side-sep{height:1px;background:var(--rk-border);margin:8px 6px}
413
- @media(max-width:860px){.rk-side-sep{display:none}}
414
- .rk-side-item-danger{color:var(--rk-red)}
415
- .rk-side-item-danger:hover{background:var(--rk-red-tint);color:#b91c1c}
416
- .rk-side-item-danger.is-active{background:var(--rk-red-tint);color:#b91c1c}
417
- .rk-set-logout{max-width:760px}
418
- .rk-set-logout-actions{margin-top:2px}
419
-
420
- /* channels topline: heading left, Connect + "…" menu right (mirrors live) */
421
- .rk-set-channels-topline{align-items:flex-start;gap:16px}
422
- .rk-set-channels-headcopy{margin-bottom:14px;flex:1;min-width:0}
423
- .rk-set-channels-actions{flex:none;gap:8px}
424
- .rk-set-menu-wrap{position:relative}
425
- .rk-set-menu-btn{width:34px;padding:7px 0;font-weight:800;letter-spacing:.04em}
426
- .rk-set-menu{position:absolute;right:0;top:calc(100% + 6px);z-index:20;min-width:190px;background:#fff;border:1px solid var(--rk-border);border-radius:var(--rk-r-xl);box-shadow:var(--rk-shadow-lg);padding:6px;display:grid}
427
- .rk-set-menu[hidden]{display:none}
428
- .rk-set-menu button{display:block;width:100%;text-align:left;padding:9px 12px;border:0;background:transparent;border-radius:var(--rk-r-md);font-family:var(--rk-font-body);font-size:13.5px;font-weight:600;color:var(--rk-n-700);cursor:pointer}
429
- .rk-set-menu button:hover{background:var(--rk-n-100);color:var(--rk-ink)}
430
- .rk-set-chsearch{position:relative;display:block;margin-bottom:14px}
431
- .rk-set-chsearch-ic{position:absolute;left:13px;top:50%;transform:translateY(-50%);color:var(--rk-n-400);pointer-events:none;display:flex}
432
- .rk-set-chsearch .rk-input{padding-left:38px;border-radius:var(--rk-r-full)}
433
- .rk-set-chsearch .rk-input:disabled{background:var(--rk-n-100);cursor:not-allowed}
434
- [data-rk-channel-hay][hidden]{display:none}
435
-
436
- /* shared dialog chrome (FlockPoster connect + email channel) */
437
- .rk-set-dialog{width:min(30rem,calc(100vw - 2rem));padding:0;border:1px solid var(--rk-border);border-radius:var(--rk-r-3xl);background:var(--rk-surface);box-shadow:var(--rk-shadow-xl)}
438
- .rk-set-dialog::backdrop{background:rgba(23,23,23,.34);backdrop-filter:blur(6px)}
439
- .rk-set-dialog-form{display:grid;gap:16px;margin:0;padding:26px}
440
- .rk-set-dialog-actions{display:flex;justify-content:flex-end;align-items:center;gap:10px;margin-top:2px}
441
- .rk-set-dialog-actions .rk-btn:first-child{margin-right:auto}
442
- .rk-set-email-note{min-height:76px}
443
-
444
- /* My Files — inline, always-open file explorer (reuses the chat panel's
445
- rk-aichat-f* directory-nav visuals) with My Files + Temp as root folders */
446
- .rk-set-myfiles{display:grid;gap:16px}
447
- .rk-set-upload{flex:none}
448
- .rk-set-explorer{border:1px solid var(--rk-border);border-radius:var(--rk-r-2xl);background:var(--rk-n-50);overflow:hidden}
449
- .rk-set-fx-crumbs{padding:12px 14px 8px}
450
- .rk-set-fx-body{max-height:360px;min-height:120px;overflow-y:auto;padding:2px 10px 12px;display:grid;gap:2px;align-content:start}
451
- /* a revealed API key lives in .rk-item-sub (nowrap+ellipsis), so it truncates to
452
- a few chars + "…". When shown (data-rk-shown=1), let that row wrap in full. */
453
- .rk-set-panel .rk-item-sub:has([data-rk-shown="1"]){white-space:normal;overflow:visible;overflow-wrap:anywhere}
454
- /* phone portrait: the channel / key rows pack tile + status pill + Resend +
455
- Remove into one non-wrapping flex row that overflows the padded card and
456
- forces horizontal page scroll. Let the actions wrap under the title. */
457
- @media(max-width:620px){
458
- .rk-set-panel .rk-item{flex-wrap:wrap}
459
- .rk-set-panel .rk-item .rk-item-main{flex:1 1 100%;order:-1}
460
- }
461
- /* very narrow: let the dialog footer buttons wrap instead of cramping */
462
- @media(max-width:420px){
463
- .rk-set-dialog-actions{flex-wrap:wrap;gap:8px}
464
- .rk-set-dialog-actions .rk-btn:first-child{margin-right:0}
465
- }
466
- `;
467
- const script = `
468
- (function(){
469
- var root=document;
470
- // Each panel that has its own canonical URL (/settings/<tab>). "logout" is a
471
- // client-only confirm panel, so it never rewrites the address bar.
472
- var URL_TABS={profile:1,wallet:1,channels:1,developer:1};
473
- function setUrl(tab){
474
- if(!URL_TABS[tab]||!history.pushState)return;
475
- try{
476
- var u=new URL(window.location.href);
477
- var m=u.pathname.match(/^(\\/u\\/[^/]+)\\/settings/); // preserve account scope
478
- u.pathname=(m?m[1]:'')+'/settings/'+tab; u.hash='';
479
- history.pushState(null,'',u.pathname+u.search);
480
- }catch(e){ history.pushState(null,'','/settings/'+tab); }
481
- }
482
- function show(tab,opts){
483
- root.querySelectorAll('[data-rk-tab]').forEach(function(b){b.classList.toggle('is-active',b.getAttribute('data-rk-tab')===tab)});
484
- root.querySelectorAll('[data-rk-panel]').forEach(function(p){p.hidden=p.getAttribute('data-rk-panel')!==tab});
485
- if(!opts||!opts.skipUrl)setUrl(tab);
486
- }
487
- root.querySelectorAll('[data-rk-tab]').forEach(function(btn){
488
- btn.addEventListener('click',function(){show(btn.getAttribute('data-rk-tab'));});
489
- });
490
- function tabFromLocation(){
491
- var m=(location.pathname||'').match(/\\/settings\\/([a-z]+)/);
492
- if(m&&URL_TABS[m[1]])return m[1];
493
- var hash=(location.hash||'').replace('#','');
494
- return (hash&&URL_TABS[hash])?hash:null;
495
- }
496
- var initial=tabFromLocation();
497
- if(initial&&root.querySelector('[data-rk-tab="'+initial+'"]'))show(initial,{skipUrl:true});
498
- // Back/forward between settings tabs — reflect the path without pushing again.
499
- window.addEventListener('popstate',function(){ show(tabFromLocation()||'profile',{skipUrl:true}); });
500
- // reveal password inputs
501
- root.querySelectorAll('[data-rk-reveal]').forEach(function(btn){
502
- btn.addEventListener('click',function(){var el=root.getElementById(btn.getAttribute('data-rk-reveal'));if(el)el.type=el.type==='password'?'text':'password';});
503
- });
504
- // reveal masked secret text
505
- root.querySelectorAll('[data-rk-reveal-text]').forEach(function(btn){
506
- btn.addEventListener('click',function(){var el=root.getElementById(btn.getAttribute('data-rk-reveal-text'));if(!el)return;var real=el.getAttribute('data-rk-secret');var shown=el.getAttribute('data-rk-shown')==='1';el.textContent=shown?'\\u2022'.repeat(18):real;el.setAttribute('data-rk-shown',shown?'0':'1');});
507
- });
508
- // copy
509
- root.querySelectorAll('[data-rk-copy]').forEach(function(btn){
510
- btn.addEventListener('click',function(){var el=root.getElementById(btn.getAttribute('data-rk-copy'));if(!el||!navigator.clipboard)return;navigator.clipboard.writeText(el.value||el.textContent||'').then(function(){var t=btn.textContent;btn.textContent='Copied!';setTimeout(function(){btn.textContent=t;},1200);});});
511
- });
512
- // destructive form confirmations
513
- root.querySelectorAll('form[data-rk-confirm]').forEach(function(f){
514
- f.addEventListener('submit',function(e){if(!window.confirm(f.getAttribute('data-rk-confirm')||'Are you sure?'))e.preventDefault();});
515
- });
516
- // director skill — 1-click copy (content travels in the attribute, like live)
517
- var skillCopy=root.querySelector('[data-rk-skill-copy]');
518
- if(skillCopy){
519
- skillCopy.addEventListener('click',function(){
520
- var text=skillCopy.getAttribute('data-rk-skill-content')||'';
521
- if(!navigator.clipboard)return;
522
- navigator.clipboard.writeText(text).then(function(){
523
- var t=skillCopy.textContent;skillCopy.textContent='Copied!';
524
- setTimeout(function(){skillCopy.textContent=t;},1200);
525
- });
526
- });
527
- }
528
- // My Files — inline, always-open file explorer. Same directory-nav model as
529
- // the chat pop panel: a single virtual root whose top-level entries are the
530
- // "My Files" and "Temp" folders; navigate into them natively. Files open in
531
- // a new tab (view URL); no drawer, no source tabs.
532
- (function(){
533
- var fx=root.querySelector('[data-rk-fx]');
534
- if(!fx) return;
535
- var crumbsEl=fx.querySelector('[data-rk-fx-crumbs]');
536
- var bodyEl=fx.querySelector('[data-rk-fx-body]');
537
- var ROOTS={
538
- my_files:{url:'/api/v1/user/me/attachments',key:'attachments',label:'My Files'},
539
- temp:{url:'/api/v1/user/me/temporary-files',key:'files',label:'Temp'},
540
- raws:{url:'/raws/feed',key:'clips',label:'Raws'}
541
- };
542
- var VROOTS=[{key:'my_files',label:'My Files'},{key:'temp',label:'Temp'},{key:'raws',label:'Raws'}];
543
- var st={root:'',path:{},data:{},status:{}};
544
- VROOTS.forEach(function(v){ st.path[v.key]=''; st.data[v.key]=null; st.status[v.key]='idle'; });
545
-
546
- function normFolder(v){ return String(v||'').split('/').map(function(s){return s.trim();}).filter(Boolean).join('/'); }
547
- function normItem(raw){
548
- // Handles attachments, temp files, AND raws/clips (which use clip_id +
549
- // source_filename/description and carry no content_type/folder).
550
- var id=raw.id||raw.attachment_id||raw.file_id||raw.clip_id||'';
551
- var name=raw.fileName||raw.file_name||raw.source_filename||raw.title||raw.description||'';
552
- var viewUrl=raw.viewUrl||raw.view_url||'';
553
- if(!id||(!name&&!viewUrl)) return null;
554
- if(!name) name=id;
555
- var ct=raw.contentType||raw.content_type||'';
556
- if(!ct&&(raw.clip_id||raw.file_path)) ct='video/mp4';
557
- return {id:id,name:name,contentType:ct,viewUrl:viewUrl,
558
- sizeBytes:typeof raw.sizeBytes==='number'?raw.sizeBytes:raw.size_bytes,folderPath:normFolder(raw.folderPath||raw.folder_path||'')};
559
- }
560
- function fmtBytes(b){ if(!b||b<=0) return ''; var u=['B','KB','MB','GB'],v=b,i=0; while(v>=1024&&i<u.length-1){v/=1024;i++;} return (v>=10||i===0?Math.round(v):v.toFixed(1))+' '+u[i]; }
561
- function immediateFolders(data,current){
562
- var cur=normFolder(current); var prefix=cur?cur+'/':''; var names={};
563
- function consider(fp){ var n=normFolder(fp); if(!n||(cur&&n===cur)||n.indexOf(prefix)!==0) return; var child=n.slice(prefix.length).split('/')[0]; if(child) names[child]=prefix+child; }
564
- (data.folders||[]).forEach(consider);
565
- (data.items||[]).forEach(function(it){ consider(it.folderPath); });
566
- return Object.keys(names).sort().map(function(n){ return {name:n,path:names[n]}; });
567
- }
568
- function loadRoot(rootKey,force){
569
- if(!ROOTS[rootKey]) return;
570
- if(!force&&st.status[rootKey]==='ready'){ render(); return; }
571
- st.status[rootKey]='loading'; render();
572
- fetch(ROOTS[rootKey].url,{credentials:'same-origin',headers:{accept:'application/json'}})
573
- .then(function(r){ if(r.status===401||r.status===403){ st.status[rootKey]='auth'; return null; } if(!r.ok) throw new Error('http '+r.status); return r.json(); })
574
- .then(function(j){ if(!j){ render(); return; } var raw=j[ROOTS[rootKey].key]||[]; var items=[]; for(var i=0;i<raw.length;i++){ var n=normItem(raw[i]); if(n) items.push(n); } st.data[rootKey]={items:items,folders:j.folders||[]}; st.status[rootKey]='ready'; render(); })
575
- .catch(function(){ st.status[rootKey]='error'; render(); });
576
- }
577
- function stateNode(text,opts){
578
- var wrap=document.createElement('div'); wrap.className='rk-aichat-fstate';
579
- var p=document.createElement('div'); p.textContent=text; wrap.appendChild(p);
580
- if(opts&&opts.href){ var a=document.createElement('a'); a.href=opts.href; a.textContent=opts.linkText||'Open'; wrap.appendChild(a); }
581
- if(opts&&opts.retry){ var b=document.createElement('button'); b.type='button'; b.textContent='Try again'; b.addEventListener('click',function(){ loadRoot(st.root,true); }); wrap.appendChild(b); }
582
- return wrap;
583
- }
584
- function folderRow(label,onOpen,sub){
585
- var b=document.createElement('button'); b.type='button'; b.className='rk-aichat-frow';
586
- var ic=document.createElement('span'); ic.className='rk-aichat-fic rk-aichat-fic-folder'; ic.textContent='/';
587
- var main=document.createElement('span'); main.className='rk-aichat-fmain';
588
- var name=document.createElement('span'); name.className='rk-aichat-fname'; name.textContent=label; main.appendChild(name);
589
- if(sub){ var meta=document.createElement('span'); meta.className='rk-aichat-fmeta'; meta.textContent=sub; main.appendChild(meta); }
590
- var chev=document.createElement('span'); chev.className='rk-aichat-fadd'; chev.textContent='Open \\u203a';
591
- b.appendChild(ic); b.appendChild(main); b.appendChild(chev);
592
- b.addEventListener('click',onOpen);
593
- return b;
594
- }
595
- function vrootRow(v){
596
- var sub=''; var s=st.status[v.key]; var d=st.data[v.key];
597
- if(s==='ready'&&d) sub=d.items.length+(d.items.length===1?' file':' files');
598
- else if(s==='loading') sub='Loading\\u2026'; else if(s==='auth') sub='Sign in to view';
599
- return folderRow(v.label,function(){ enterRoot(v.key); },sub);
600
- }
601
- function fileRow(it){
602
- var open=it.viewUrl?document.createElement('a'):document.createElement('div');
603
- open.className='rk-aichat-frow';
604
- if(it.viewUrl){ open.href=it.viewUrl; open.target='_blank'; open.rel='noopener'; }
605
- var ic=document.createElement('span'); ic.className='rk-aichat-fic rk-aichat-fic-file';
606
- if(it.viewUrl&&it.contentType.indexOf('image/')===0){ var img=document.createElement('img'); img.src=it.viewUrl; img.alt=''; img.loading='lazy'; ic.appendChild(img); }
607
- else if(it.viewUrl&&it.contentType.indexOf('video/')===0){ var vid=document.createElement('video'); vid.src=it.viewUrl; vid.muted=true; vid.playsInline=true; ic.appendChild(vid); }
608
- else { ic.textContent=(it.name.split('.').pop()||it.name.slice(0,1)).slice(0,3).toUpperCase(); }
609
- var main=document.createElement('span'); main.className='rk-aichat-fmain';
610
- var name=document.createElement('span'); name.className='rk-aichat-fname'; name.textContent=it.name;
611
- var meta=document.createElement('span'); meta.className='rk-aichat-fmeta'; meta.textContent=fmtBytes(it.sizeBytes)||it.contentType||'file';
612
- main.appendChild(name); main.appendChild(meta);
613
- var add=document.createElement('span'); add.className='rk-aichat-fadd'; add.textContent=it.viewUrl?'Open \\u2197':'';
614
- open.appendChild(ic); open.appendChild(main); open.appendChild(add);
615
- return open;
616
- }
617
- function crumb(label,current,onClick){
618
- var b=document.createElement('button'); b.type='button'; b.className='rk-aichat-crumb'+(current?' is-current':''); b.textContent=label; b.addEventListener('click',onClick); return b;
619
- }
620
- function crumbSep(){ var s=document.createElement('span'); s.className='rk-aichat-crumb-sep'; s.textContent='/'; return s; }
621
- function renderCrumbs(){
622
- crumbsEl.innerHTML='';
623
- var atVirtual=!st.root;
624
- crumbsEl.appendChild(crumb('Files',atVirtual,goVirtualRoot));
625
- if(atVirtual) return;
626
- if(st.status[st.root]!=='ready') return;
627
- var cur=normFolder(st.path[st.root]);
628
- crumbsEl.appendChild(crumbSep());
629
- crumbsEl.appendChild(crumb(ROOTS[st.root].label,!cur,function(){ goPath(''); }));
630
- var parts=cur.split('/').filter(Boolean); var acc='';
631
- parts.forEach(function(part,idx){ crumbsEl.appendChild(crumbSep()); acc=acc?acc+'/'+part:part; var pth=acc; crumbsEl.appendChild(crumb(part,idx===parts.length-1,function(){ goPath(pth); })); });
632
- }
633
- var rendering=false;
634
- function render(){ if(rendering) return; rendering=true; try{ renderInner(); } finally{ rendering=false; } }
635
- function renderInner(){
636
- renderCrumbs(); bodyEl.innerHTML='';
637
- if(!st.root){
638
- VROOTS.forEach(function(v){ if(st.status[v.key]==='idle') loadRoot(v.key,false); });
639
- VROOTS.forEach(function(v){ bodyEl.appendChild(vrootRow(v)); });
640
- return;
641
- }
642
- var s=st.status[st.root];
643
- if(s==='idle'){ loadRoot(st.root,false); return; }
644
- if(s==='loading'){ bodyEl.appendChild(stateNode('Loading your files\\u2026')); return; }
645
- if(s==='auth'){ bodyEl.appendChild(stateNode('Sign in to browse your files.',{href:'/login',linkText:'Sign in'})); return; }
646
- if(s==='error'){ bodyEl.appendChild(stateNode('Couldn\\u2019t reach your files.',{retry:true})); return; }
647
- var data=st.data[st.root]||{items:[],folders:[]};
648
- var cur=normFolder(st.path[st.root]);
649
- var folders=immediateFolders(data,cur);
650
- var here=data.items.filter(function(it){ return normFolder(it.folderPath)===cur; });
651
- if(!folders.length&&!here.length){ bodyEl.appendChild(stateNode(cur?'This folder is empty.':'No files in '+ROOTS[st.root].label+' yet.')); return; }
652
- folders.forEach(function(f){ bodyEl.appendChild(folderRow(f.name,function(){ goPath(f.path); })); });
653
- here.forEach(function(it){ bodyEl.appendChild(fileRow(it)); });
654
- }
655
- function goPath(p){ st.path[st.root]=normFolder(p); render(); }
656
- function goVirtualRoot(){ st.root=''; render(); }
657
- function enterRoot(rootKey){ if(!ROOTS[rootKey]) return; st.root=rootKey; st.path[rootKey]=''; loadRoot(rootKey,false); render(); }
658
- render();
659
- })();
660
- // channels: quick search filter
661
- var chSearch=root.querySelector('[data-rk-channel-search]');
662
- var chList=root.querySelector('[data-rk-channel-list]');
663
- var chNone=root.querySelector('[data-rk-channel-noresults]');
664
- if(chSearch&&chList){
665
- chSearch.addEventListener('input',function(){
666
- var q=(chSearch.value||'').trim().toLowerCase();
667
- var shown=0;
668
- chList.querySelectorAll('[data-rk-channel-hay]').forEach(function(row){
669
- var ok=!q||(row.getAttribute('data-rk-channel-hay')||'').indexOf(q)>-1;
670
- row.hidden=!ok;if(ok)shown++;
671
- });
672
- if(chNone)chNone.hidden=!(q&&shown===0);
673
- });
674
- }
675
- // channels: "…" actions menu
676
- var menuBtn=root.querySelector('[data-rk-channel-menu]');
677
- var menuPop=root.querySelector('[data-rk-channel-menu-pop]');
678
- if(menuBtn&&menuPop){
679
- menuBtn.addEventListener('click',function(e){
680
- e.stopPropagation();
681
- var open=menuPop.hidden;
682
- menuPop.hidden=!open;
683
- menuBtn.setAttribute('aria-expanded',open?'true':'false');
684
- });
685
- root.addEventListener('click',function(e){
686
- if(!menuPop.hidden&&!menuPop.contains(e.target)&&e.target!==menuBtn){menuPop.hidden=true;menuBtn.setAttribute('aria-expanded','false');}
687
- });
688
- }
689
- // channel dialogs (FlockPoster connect + email channel)
690
- function wireDialog(dlgSel,openSel){
691
- var dlg=root.querySelector(dlgSel);
692
- if(!dlg)return null;
693
- root.querySelectorAll(openSel).forEach(function(btn){
694
- btn.addEventListener('click',function(){
695
- if(menuPop)menuPop.hidden=true;
696
- if(typeof dlg.showModal==='function'&&!dlg.open)dlg.showModal();
697
- });
698
- });
699
- dlg.addEventListener('click',function(e){
700
- var r=dlg.getBoundingClientRect();
701
- if(e.clientX<r.left||e.clientX>r.right||e.clientY<r.top||e.clientY>r.bottom)dlg.close();
702
- });
703
- dlg.querySelectorAll('[data-rk-dialog-close]').forEach(function(btn){btn.addEventListener('click',function(){dlg.close();});});
704
- return dlg;
705
- }
706
- var flockDlg=wireDialog('[data-rk-flock-dialog]','[data-rk-open-flock]');
707
- wireDialog('[data-rk-email-dialog]','[data-rk-open-email]');
708
- if(flockDlg&&flockDlg.getAttribute('data-rk-autoopen')==='1'&&typeof flockDlg.showModal==='function'&&!flockDlg.open){flockDlg.showModal();}
709
- // wallet top-up → real funding link
710
- var topBtn=root.querySelector('[data-rk-topup]');
711
- var topStatus=root.querySelector('[data-rk-topup-status]');
712
- if(topBtn){
713
- topBtn.addEventListener('click',function(){
714
- topBtn.disabled=true;if(topStatus){topStatus.hidden=false;topStatus.textContent='Starting secure checkout…';}
715
- fetch('/settings/wallet/funding-link',{method:'POST',headers:{'content-type':'application/json'},credentials:'same-origin',body:'{}'})
716
- .then(function(r){return r.json();})
717
- .then(function(j){if(j&&j.checkout_url){window.location.href=j.checkout_url;}else{throw new Error(j&&j.error||'Unable to start funding');}})
718
- .catch(function(err){topBtn.disabled=false;if(topStatus){topStatus.hidden=false;topStatus.textContent=String(err.message||err);}});
719
- });
720
- }
721
- })();`;
722
- return reskinDocument({
723
- title: "vidfarm reskin — Settings",
724
- description: "Vidfarm settings — profile, wallet, channels, developer.",
725
- activeSlug: "settings",
726
- account: { name: input.name, email: input.email },
727
- pageCss,
728
- body,
729
- script
730
- });
731
- }
732
- //# sourceMappingURL=settings-page.js.map