@mevdragon/vidfarm-devcli 0.2.8 → 0.2.9
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/.env.example +9 -0
- package/GETTING_STARTED.developers.md +1 -26
- package/README.md +3 -0
- package/SKILL.developer.md +327 -4
- package/auto-create-templates/AUTO_CREATE_TEMPLATES.md +475 -0
- package/auto-create-templates/extractor-system-prompt.md +113 -0
- package/auto-create-templates/production-graph.schema.json +276 -0
- package/auto-create-templates/runbook.md +140 -0
- package/auto-create-templates/template-plan.schema.json +230 -0
- package/auto-create-templates/template-planner-prompt.md +75 -0
- package/dist/src/account-pages-legacy.js +9064 -0
- package/dist/src/account-pages.js +41 -620
- package/dist/src/app.js +6627 -244
- package/dist/src/cli.js +386 -26
- package/dist/src/config.js +12 -2
- package/dist/src/context.js +35 -35
- package/dist/src/db.js +1016 -53
- package/dist/src/dev-app-legacy.js +693 -0
- package/dist/src/dev-app.js +4 -904
- package/dist/src/domain.js +1 -1
- package/dist/src/editor-chat-history.js +72 -0
- package/dist/src/editor-chat.js +202 -0
- package/dist/src/frontend/flockposter-cache-store.js +116 -0
- package/dist/src/frontend/homepage-client.js +114 -0
- package/dist/src/frontend/homepage-shared.js +3 -0
- package/dist/src/frontend/homepage-store.js +27 -0
- package/dist/src/frontend/homepage-view.js +147 -0
- package/dist/src/frontend/page-runtime-client.js +100 -0
- package/dist/src/frontend/page-runtime-store.js +8 -0
- package/dist/src/frontend/template-editor-chat.js +2261 -0
- package/dist/src/homepage.js +695 -371
- package/dist/src/lib/template-style-options.js +95 -15
- package/dist/src/page-runtime.js +1 -0
- package/dist/src/page-shell.js +941 -0
- package/dist/src/primitive-context.js +163 -0
- package/dist/src/primitive-registry.js +340 -0
- package/dist/src/primitive-sdk.js +3 -0
- package/dist/src/primitives/remotion/html-image.js +28 -0
- package/dist/src/react-page-shell.js +34 -0
- package/dist/src/ready-post-schedule-component.js +1514 -0
- package/dist/src/registry.js +36 -17
- package/dist/src/runtime.js +6 -1
- package/dist/src/services/api-call-history.js +245 -0
- package/dist/src/services/auth.js +25 -3
- package/dist/src/services/billing.js +248 -9
- package/dist/src/services/chat-threads.js +88 -0
- package/dist/src/services/job-logs.js +14 -7
- package/dist/src/services/jobs.js +13 -2
- package/dist/src/services/providers.js +552 -133
- package/dist/src/services/rate-limits.js +236 -0
- package/dist/src/services/remotion.js +143 -16
- package/dist/src/services/storage.js +10 -8
- package/dist/src/services/template-certification.js +11 -2
- package/dist/src/services/template-loader.js +3 -1
- package/dist/src/services/template-sources.js +68 -3
- package/dist/src/template-editor-pages.js +2309 -0
- package/dist/src/template-editor-shell.js +1507 -0
- package/dist/src/worker.js +120 -22
- package/package.json +27 -4
- package/public/assets/homepage-app.js +54 -0
- package/public/assets/homepage-client-app.js +54 -0
- package/public/assets/page-runtime-client-app.js +68 -0
- package/dist/templates/vidfarm_template_0000/src/lib/images.js +0 -202
- package/dist/templates/vidfarm_template_0000/src/remotion/Root.js +0 -34
- package/dist/templates/vidfarm_template_0000/src/remotion/index.js +0 -3
- package/dist/templates/vidfarm_template_0000/src/sdk.js +0 -3
- package/dist/templates/vidfarm_template_0000/src/style-options.js +0 -51
- package/dist/templates/vidfarm_template_0000/src/template-dna.js +0 -9
- package/dist/templates/vidfarm_template_0000/src/template.js +0 -1447
- package/templates/vidfarm_template_0000/README.md +0 -100
- package/templates/vidfarm_template_0000/SKILL.md +0 -225
- package/templates/vidfarm_template_0000/assets/Abel-Regular.ttf +0 -0
- package/templates/vidfarm_template_0000/assets/DMSerifDisplay-Regular.ttf +0 -0
- package/templates/vidfarm_template_0000/assets/Montserrat[wght].ttf +0 -0
- package/templates/vidfarm_template_0000/assets/SourceCodePro[wght].ttf +0 -0
- package/templates/vidfarm_template_0000/assets/TikTokSans-SemiBold.ttf +0 -0
- package/templates/vidfarm_template_0000/assets/Yesteryear-Regular.ttf +0 -0
- package/templates/vidfarm_template_0000/composition.json +0 -11
- package/templates/vidfarm_template_0000/package.json +0 -27
- package/templates/vidfarm_template_0000/research/preview/.gitkeep +0 -1
- package/templates/vidfarm_template_0000/research/source_notes.md +0 -7
- package/templates/vidfarm_template_0000/src/lib/images.js +0 -202
- package/templates/vidfarm_template_0000/src/lib/images.ts +0 -241
- package/templates/vidfarm_template_0000/src/remotion/Root.js +0 -33
- package/templates/vidfarm_template_0000/src/remotion/Root.tsx +0 -75
- package/templates/vidfarm_template_0000/src/remotion/index.js +0 -3
- package/templates/vidfarm_template_0000/src/remotion/index.tsx +0 -4
- package/templates/vidfarm_template_0000/src/sdk.js +0 -3
- package/templates/vidfarm_template_0000/src/sdk.ts +0 -140
- package/templates/vidfarm_template_0000/src/style-options.js +0 -51
- package/templates/vidfarm_template_0000/src/style-options.ts +0 -60
- package/templates/vidfarm_template_0000/src/template-dna.js +0 -9
- package/templates/vidfarm_template_0000/src/template-dna.ts +0 -15
- package/templates/vidfarm_template_0000/src/template.js +0 -1447
- package/templates/vidfarm_template_0000/src/template.ts +0 -2049
- package/templates/vidfarm_template_0000/template.config.json +0 -21
- package/templates/vidfarm_template_0000/tmp/solobacterium-moorei-slideshow.request.json +0 -31
- package/templates/vidfarm_template_0000/tsconfig.json +0 -19
package/dist/src/domain.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export
|
|
1
|
+
export const CUSTOMER_PLAN_TIERS = ["starter", "standard", "agency"];
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
const MAX_CHAT_PAYLOAD_STRING_LENGTH = 1200;
|
|
2
|
+
const MAX_CHAT_PAYLOAD_ARRAY_ITEMS = 24;
|
|
3
|
+
const MAX_CHAT_PAYLOAD_OBJECT_KEYS = 80;
|
|
4
|
+
const DATA_URL_PREFIX_REGEX = /^data:([^;,]+)[^,]*,/i;
|
|
5
|
+
function summarizeLargeString(value) {
|
|
6
|
+
const dataUrlMatch = DATA_URL_PREFIX_REGEX.exec(value);
|
|
7
|
+
if (dataUrlMatch) {
|
|
8
|
+
return `[${dataUrlMatch[1]} data URL omitted, ${value.length.toLocaleString()} chars]`;
|
|
9
|
+
}
|
|
10
|
+
if (value.length > MAX_CHAT_PAYLOAD_STRING_LENGTH) {
|
|
11
|
+
return `${value.slice(0, MAX_CHAT_PAYLOAD_STRING_LENGTH)}... [truncated ${value.length.toLocaleString()} chars]`;
|
|
12
|
+
}
|
|
13
|
+
return value;
|
|
14
|
+
}
|
|
15
|
+
export function sanitizeChatPayload(value, depth = 0) {
|
|
16
|
+
if (value === null || value === undefined) {
|
|
17
|
+
return value;
|
|
18
|
+
}
|
|
19
|
+
if (typeof value === "string") {
|
|
20
|
+
return summarizeLargeString(value);
|
|
21
|
+
}
|
|
22
|
+
if (typeof value === "number" || typeof value === "boolean") {
|
|
23
|
+
return value;
|
|
24
|
+
}
|
|
25
|
+
if (typeof value === "bigint") {
|
|
26
|
+
return value.toString();
|
|
27
|
+
}
|
|
28
|
+
if (typeof value === "function" || typeof value === "symbol") {
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
if (depth >= 8) {
|
|
32
|
+
return "[nested payload omitted]";
|
|
33
|
+
}
|
|
34
|
+
if (Array.isArray(value)) {
|
|
35
|
+
const items = value
|
|
36
|
+
.slice(0, MAX_CHAT_PAYLOAD_ARRAY_ITEMS)
|
|
37
|
+
.map((entry) => sanitizeChatPayload(entry, depth + 1));
|
|
38
|
+
if (value.length > MAX_CHAT_PAYLOAD_ARRAY_ITEMS) {
|
|
39
|
+
items.push(`[${value.length - MAX_CHAT_PAYLOAD_ARRAY_ITEMS} more items omitted]`);
|
|
40
|
+
}
|
|
41
|
+
return items;
|
|
42
|
+
}
|
|
43
|
+
if (typeof value === "object") {
|
|
44
|
+
const output = {};
|
|
45
|
+
const entries = Object.entries(value);
|
|
46
|
+
for (const [key, entry] of entries.slice(0, MAX_CHAT_PAYLOAD_OBJECT_KEYS)) {
|
|
47
|
+
const sanitized = sanitizeChatPayload(entry, depth + 1);
|
|
48
|
+
if (sanitized !== undefined) {
|
|
49
|
+
output[key] = sanitized;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (entries.length > MAX_CHAT_PAYLOAD_OBJECT_KEYS) {
|
|
53
|
+
output.__omitted_keys = entries.length - MAX_CHAT_PAYLOAD_OBJECT_KEYS;
|
|
54
|
+
}
|
|
55
|
+
return output;
|
|
56
|
+
}
|
|
57
|
+
return String(value);
|
|
58
|
+
}
|
|
59
|
+
export function sanitizeHttpExchange(exchange) {
|
|
60
|
+
return {
|
|
61
|
+
...exchange,
|
|
62
|
+
request: {
|
|
63
|
+
...exchange.request,
|
|
64
|
+
body: sanitizeChatPayload(exchange.request.body)
|
|
65
|
+
},
|
|
66
|
+
result: {
|
|
67
|
+
...exchange.result,
|
|
68
|
+
body: sanitizeChatPayload(exchange.result.body),
|
|
69
|
+
error: exchange.result.error ? summarizeLargeString(exchange.result.error) : exchange.result.error
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
export const EDITOR_CHAT_PROVIDER_PRIORITY = [
|
|
2
|
+
"openai",
|
|
3
|
+
"gemini",
|
|
4
|
+
"openrouter",
|
|
5
|
+
"perplexity"
|
|
6
|
+
];
|
|
7
|
+
export function defaultEditorChatModel(provider) {
|
|
8
|
+
switch (provider) {
|
|
9
|
+
case "openai":
|
|
10
|
+
return "gpt-4.1-mini";
|
|
11
|
+
case "gemini":
|
|
12
|
+
return "gemini-2.5-flash";
|
|
13
|
+
case "openrouter":
|
|
14
|
+
return "qwen/qwen3.6-flash";
|
|
15
|
+
case "perplexity":
|
|
16
|
+
return "sonar";
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export function buildTemplateEditorChatSystemPrompt(input) {
|
|
20
|
+
const routeLines = input.docsRoutes
|
|
21
|
+
.map((route) => `- ${route.method} ${route.path}: ${route.summary}`)
|
|
22
|
+
.join("\n");
|
|
23
|
+
return [
|
|
24
|
+
"You are Vidfarm's template editor copilot.",
|
|
25
|
+
"Help the user understand and operate this template through its existing template and primitive REST surface.",
|
|
26
|
+
"Be concrete. Prefer exact route names, payload fields, and tracer usage over generic advice.",
|
|
27
|
+
"If you mention a route or operation, only mention routes that exist in the provided template context.",
|
|
28
|
+
"If attachments are present, reason about them as user-supplied reference assets for template operations or primitive image edits.",
|
|
29
|
+
"When the user wants to run the template, prefer doing it via REST calls instead of only describing what to do.",
|
|
30
|
+
"Execute REST calls sequentially. Make one API call, inspect the response, then decide the next step.",
|
|
31
|
+
"When chaining one operation's output into another operation, copy the exact nested payload returned by the previous response, such as result.renderVideoInput. Do not reconstruct, normalize, shorten, expand, or add job IDs to media URLs, S3 keys, durations, or slide arrays.",
|
|
32
|
+
"Never claim that you called a REST route, started rendering, concatenated slides, or created a job unless the http_request tool returned that result in this response.",
|
|
33
|
+
"Never invent or infer a job_id. Only mention a job_id that appears verbatim in an http_request tool response body or a job status response.",
|
|
34
|
+
"If you need to make an API call, call the http_request tool; do not write prose such as 'Here is the http_request call' unless the tool call has actually completed and the UI will show its exchange.",
|
|
35
|
+
"Do not fan out multiple operation calls, repeated polling bursts, or speculative retries in parallel.",
|
|
36
|
+
"Prefer the minimum number of API calls needed to complete the user's request, but if the user asks for N distinct generated outputs, make N sequential POST operation calls.",
|
|
37
|
+
"For template operations that generate one card, post, render, or asset per job, do not collapse a counted request into one job unless the user explicitly asks for one combined job.",
|
|
38
|
+
"Treat tracers as the work-organization primitive. The current chat thread's tracers are the source of truth for API context.",
|
|
39
|
+
"Reuse the default request tracer for the current chat thread unless the user explicitly asks for a separate parallel stream.",
|
|
40
|
+
"When you start or revise work with a POST template operation or primitive route, include a tracer in the JSON body.",
|
|
41
|
+
"If the user asks for each output to have its own tracer, use a different readable tracer base for each POST request.",
|
|
42
|
+
"If the user asks for each output to have its own chat thread, call frontend_action with action=create_thread for each tracer before or alongside the corresponding POST request.",
|
|
43
|
+
"If multiple tracers are available, mention which tracer you are using when it matters.",
|
|
44
|
+
"If the user asks to add, remove, switch tracers, or create a separate chat thread for a tracer, use the frontend_action tool so the page state updates directly.",
|
|
45
|
+
"If polling a job, poll gently. Prefer about every 30 seconds rather than rapid repeated checks.",
|
|
46
|
+
"If the customer already has at least one saved provider key that should work for this template, do not ask them to provide another API key.",
|
|
47
|
+
"If provider selection is unspecified, choose a supported saved provider automatically, update template config first when needed, and then continue.",
|
|
48
|
+
"If a run fails because the current template config points at an unavailable provider, switch config to a saved supported provider and retry instead of asking the user to choose.",
|
|
49
|
+
"Prefer primitive image routes when the user wants a reusable asset edit or render that is not specific to one template operation.",
|
|
50
|
+
"Use the word create for template experimentation and drafting. Use the word approve only for converting a finished output into a schedule-ready post.",
|
|
51
|
+
"When the user asks to approve, package, share, preview, or make a finished output schedule-ready, approve a post with POST /api/v1/approved/posts using only title, caption, pinned_comment when useful, media URLs, and the current tracer.",
|
|
52
|
+
"If the finished output is a rendered slideshow video, include both the MP4 and the finished slide frame image URLs in the post media array so the post page can expose Video and Slides versions. Prefer role=\"primary\" for the video and role=\"slide\" for each finished slide image.",
|
|
53
|
+
"When approving a post from chat, include tracer in the JSON body. Do not send view_password unless the user explicitly asked for a different preview password; the server defaults it to password123.",
|
|
54
|
+
"After approving a post, return the post.share_url as a Markdown link so the user can open the preview page in a new tab.",
|
|
55
|
+
"Do not claim a post was approved unless POST /api/v1/approved/posts returned a post_id and share_url in the current response.",
|
|
56
|
+
"When the user asks to schedule an approved post, use POST /api/v1/approved/posts/:postId/schedules after you know the approved post id, destination channel id, and scheduled_at ISO timestamp.",
|
|
57
|
+
"For scheduling, resolve the user's named channel against the cached scheduling channels. Use destination_type=\"email\" for email pseudo-channels and destination_type=\"flockposter\" for FlockPoster/social channels.",
|
|
58
|
+
"If the channel name or date/time is ambiguous, ask a concise clarification question instead of guessing. If the user gives a local date/time without a timezone, ask for timezone unless the timezone is already explicit in the conversation.",
|
|
59
|
+
"Do not claim a post was scheduled unless POST /api/v1/approved/posts/:postId/schedules returned a schedule object in the current response.",
|
|
60
|
+
"",
|
|
61
|
+
`Template title: ${input.templateTitle}`,
|
|
62
|
+
`Template id: ${input.templateId}`,
|
|
63
|
+
`Template slug: ${input.templateSlug}`,
|
|
64
|
+
`Editor page: ${input.page}`,
|
|
65
|
+
`Tracer id: ${input.tracerId ?? "none"}`,
|
|
66
|
+
`Available tracers: ${input.tracers.length ? input.tracers.join(", ") : "none"}`,
|
|
67
|
+
`Default request tracer: ${input.defaultRequestTracer ?? input.activeTracer ?? input.tracerId ?? "none"}`,
|
|
68
|
+
`Template description: ${input.templateDescription}`,
|
|
69
|
+
"",
|
|
70
|
+
"Documented REST routes:",
|
|
71
|
+
routeLines || "- No routes documented."
|
|
72
|
+
].join("\n");
|
|
73
|
+
}
|
|
74
|
+
export function buildEditorChatSystemInstruction(input) {
|
|
75
|
+
const sections = [];
|
|
76
|
+
if (input.cachedContext?.directorSkill?.trim()) {
|
|
77
|
+
sections.push(`Director skill context:\n\n${input.cachedContext.directorSkill.trim()}`);
|
|
78
|
+
}
|
|
79
|
+
if (input.cachedContext?.templateSkill?.trim()) {
|
|
80
|
+
sections.push(`Template skill context:\n\n${input.cachedContext.templateSkill.trim()}`);
|
|
81
|
+
}
|
|
82
|
+
sections.push([
|
|
83
|
+
`Saved providers for this customer: ${input.cachedContext?.availableProviders?.length ? input.cachedContext.availableProviders.join(", ") : "none"}.`,
|
|
84
|
+
`Current template config: ${input.cachedContext?.currentTemplateConfig ? JSON.stringify(input.cachedContext.currentTemplateConfig, null, 2) : "none saved"}.`,
|
|
85
|
+
`Cached scheduling channels${input.cachedContext?.scheduleChannelsUpdatedAt ? ` as of ${input.cachedContext.scheduleChannelsUpdatedAt}` : ""}: ${input.cachedContext?.scheduleChannels?.length ? JSON.stringify(input.cachedContext.scheduleChannels, null, 2) : "none cached"}.`,
|
|
86
|
+
`Selected library posts${input.cachedContext?.selectedPostsUpdatedAt ? ` as of ${input.cachedContext.selectedPostsUpdatedAt}` : ""}: ${input.cachedContext?.selectedPosts?.length ? JSON.stringify(input.cachedContext.selectedPosts, null, 2) : "none selected"}.`,
|
|
87
|
+
"Operational rule: if the user asks to create or run something and saved providers exist, pick one automatically and call the template config route when needed before running the operation.",
|
|
88
|
+
"Operational rule: use REST routes sequentially and avoid parallel route bursts or aggressive polling loops.",
|
|
89
|
+
"Operational rule: organize work with tracers and keep each POST run attached to the correct tracer.",
|
|
90
|
+
"Operational rule: if the user asks to schedule a post and cached scheduling channels are missing, ask them to connect or refresh channels before scheduling. If channels are present, match by id, name/title, handle, platform, or email label.",
|
|
91
|
+
"Operational rule: when selected library posts are present and the user says 'these posts' or similar, schedule exactly those post IDs unless the user narrows the set.",
|
|
92
|
+
"Tool honesty rule: job IDs, render status, concat success, and HTTP call claims must come from actual http_request tool results in the current response, never from memory or guesswork."
|
|
93
|
+
].join("\n"));
|
|
94
|
+
if (input.template) {
|
|
95
|
+
sections.push(buildTemplateEditorChatSystemPrompt(input.template));
|
|
96
|
+
}
|
|
97
|
+
if (input.currentDateTime?.trim()) {
|
|
98
|
+
sections.push([
|
|
99
|
+
"Current datetime for scheduling:",
|
|
100
|
+
input.currentDateTime.trim(),
|
|
101
|
+
"Use this timestamp to interpret relative dates like today, tomorrow, and next Friday."
|
|
102
|
+
].join("\n"));
|
|
103
|
+
}
|
|
104
|
+
return sections.filter((entry) => entry.trim().length > 0).join("\n\n");
|
|
105
|
+
}
|
|
106
|
+
export function formatEditorChatCurrentDateTime(clientContext) {
|
|
107
|
+
const lines = [];
|
|
108
|
+
const iso = clientContext?.currentDateTime?.trim();
|
|
109
|
+
const local = clientContext?.localDateTime?.trim();
|
|
110
|
+
const timeZone = clientContext?.timeZone?.trim();
|
|
111
|
+
if (iso) {
|
|
112
|
+
lines.push(`ISO timestamp: ${iso}`);
|
|
113
|
+
}
|
|
114
|
+
if (local) {
|
|
115
|
+
lines.push(`Browser local datetime: ${local}`);
|
|
116
|
+
}
|
|
117
|
+
if (timeZone) {
|
|
118
|
+
lines.push(`Browser timezone: ${timeZone}`);
|
|
119
|
+
}
|
|
120
|
+
return lines.join("\n");
|
|
121
|
+
}
|
|
122
|
+
function tryParseUrl(value) {
|
|
123
|
+
try {
|
|
124
|
+
return new URL(value);
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
function decodeDataUrl(value) {
|
|
131
|
+
const match = /^data:([^;,]+)?(?:;charset=[^;,]+)?;base64,(.+)$/i.exec(value);
|
|
132
|
+
if (!match) {
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
const mediaType = match[1]?.trim() || "application/octet-stream";
|
|
136
|
+
const data = Buffer.from(match[2], "base64");
|
|
137
|
+
return { mediaType, data };
|
|
138
|
+
}
|
|
139
|
+
export function toEditorChatModelMessages(messages) {
|
|
140
|
+
const result = [];
|
|
141
|
+
for (const entry of messages) {
|
|
142
|
+
if (!entry || typeof entry !== "object") {
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
const role = entry.role;
|
|
146
|
+
if (role !== "user" && role !== "assistant" && role !== "system" && role !== "tool") {
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
if (role === "system") {
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
const content = Array.isArray(entry.content)
|
|
153
|
+
? entry.content
|
|
154
|
+
: [];
|
|
155
|
+
if (role === "user") {
|
|
156
|
+
const parts = [];
|
|
157
|
+
for (const part of content) {
|
|
158
|
+
if (part.type === "text" && typeof part.text === "string") {
|
|
159
|
+
parts.push({ type: "text", text: part.text });
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
if (part.type !== "file" || typeof part.data !== "string") {
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
const mediaType = typeof part.mediaType === "string" && part.mediaType.trim()
|
|
166
|
+
? part.mediaType.trim()
|
|
167
|
+
: "application/octet-stream";
|
|
168
|
+
const decoded = decodeDataUrl(part.data);
|
|
169
|
+
if (decoded) {
|
|
170
|
+
parts.push({
|
|
171
|
+
type: "file",
|
|
172
|
+
data: decoded.data,
|
|
173
|
+
mediaType: mediaType || decoded.mediaType
|
|
174
|
+
});
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
const url = tryParseUrl(part.data);
|
|
178
|
+
if (!url) {
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
parts.push({
|
|
182
|
+
type: "file",
|
|
183
|
+
data: url,
|
|
184
|
+
mediaType
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
result.push({ role: "user", content: parts });
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
if (role === "assistant") {
|
|
191
|
+
const parts = [];
|
|
192
|
+
for (const part of content) {
|
|
193
|
+
if (part.type === "text" && typeof part.text === "string") {
|
|
194
|
+
parts.push({ type: "text", text: part.text });
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
result.push({ role: "assistant", content: parts });
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return result;
|
|
202
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { createStore } from "zustand/vanilla";
|
|
2
|
+
export const FLOCKPOSTER_CACHE_STORAGE_KEY = "vidfarm:flockposter-cache:v1";
|
|
3
|
+
const PUBLIC_SCOPE = "public";
|
|
4
|
+
const emptySnapshot = {
|
|
5
|
+
channels: [],
|
|
6
|
+
posts: [],
|
|
7
|
+
channelsUpdatedAt: null,
|
|
8
|
+
calendarUpdatedAt: null,
|
|
9
|
+
calendarRangeStart: null,
|
|
10
|
+
calendarRangeEnd: null
|
|
11
|
+
};
|
|
12
|
+
export function getFlockPosterCacheStorageKey(userId) {
|
|
13
|
+
return `${FLOCKPOSTER_CACHE_STORAGE_KEY}_${userId?.trim() || PUBLIC_SCOPE}`;
|
|
14
|
+
}
|
|
15
|
+
export function readFlockPosterCacheSnapshot(userId) {
|
|
16
|
+
if (typeof window === "undefined") {
|
|
17
|
+
return emptySnapshot;
|
|
18
|
+
}
|
|
19
|
+
try {
|
|
20
|
+
const raw = window.localStorage.getItem(getFlockPosterCacheStorageKey(userId));
|
|
21
|
+
if (!raw) {
|
|
22
|
+
return emptySnapshot;
|
|
23
|
+
}
|
|
24
|
+
const parsed = JSON.parse(raw);
|
|
25
|
+
if (!parsed || typeof parsed !== "object") {
|
|
26
|
+
return emptySnapshot;
|
|
27
|
+
}
|
|
28
|
+
return {
|
|
29
|
+
channels: Array.isArray(parsed.channels) ? parsed.channels : [],
|
|
30
|
+
posts: Array.isArray(parsed.posts) ? parsed.posts : [],
|
|
31
|
+
channelsUpdatedAt: typeof parsed.channelsUpdatedAt === "string" ? parsed.channelsUpdatedAt : null,
|
|
32
|
+
calendarUpdatedAt: typeof parsed.calendarUpdatedAt === "string" ? parsed.calendarUpdatedAt : null,
|
|
33
|
+
calendarRangeStart: typeof parsed.calendarRangeStart === "string" ? parsed.calendarRangeStart : null,
|
|
34
|
+
calendarRangeEnd: typeof parsed.calendarRangeEnd === "string" ? parsed.calendarRangeEnd : null
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return emptySnapshot;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function writeFlockPosterCacheSnapshot(snapshot, userId) {
|
|
42
|
+
if (typeof window === "undefined") {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
window.localStorage.setItem(getFlockPosterCacheStorageKey(userId), JSON.stringify(snapshot));
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
// Ignore storage failures and keep in-memory state only.
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
export function createFlockPosterCacheStore(userId) {
|
|
53
|
+
const initial = readFlockPosterCacheSnapshot(userId);
|
|
54
|
+
return createStore((set, get) => ({
|
|
55
|
+
...initial,
|
|
56
|
+
setChannelsData: (channels) => {
|
|
57
|
+
const next = {
|
|
58
|
+
...get(),
|
|
59
|
+
channels,
|
|
60
|
+
channelsUpdatedAt: new Date().toISOString()
|
|
61
|
+
};
|
|
62
|
+
writeFlockPosterCacheSnapshot(next, userId);
|
|
63
|
+
set(next);
|
|
64
|
+
},
|
|
65
|
+
setCalendarData: (input) => {
|
|
66
|
+
const current = get();
|
|
67
|
+
const mergedChannels = dedupeChannels([...current.channels, ...input.channels]);
|
|
68
|
+
const mergedPosts = input.replaceRange
|
|
69
|
+
? mergePostsForRange(current.posts, input.posts, input.rangeStart, input.rangeEnd)
|
|
70
|
+
: dedupePosts([...current.posts, ...input.posts]);
|
|
71
|
+
const next = {
|
|
72
|
+
...current,
|
|
73
|
+
channels: mergedChannels,
|
|
74
|
+
posts: mergedPosts,
|
|
75
|
+
channelsUpdatedAt: new Date().toISOString(),
|
|
76
|
+
calendarUpdatedAt: new Date().toISOString(),
|
|
77
|
+
calendarRangeStart: expandRangeStart(current.calendarRangeStart, input.rangeStart),
|
|
78
|
+
calendarRangeEnd: expandRangeEnd(current.calendarRangeEnd, input.rangeEnd)
|
|
79
|
+
};
|
|
80
|
+
writeFlockPosterCacheSnapshot(next, userId);
|
|
81
|
+
set(next);
|
|
82
|
+
}
|
|
83
|
+
}));
|
|
84
|
+
}
|
|
85
|
+
function dedupeChannels(channels) {
|
|
86
|
+
return [...new Map(channels.map((channel) => [channel.id, channel])).values()];
|
|
87
|
+
}
|
|
88
|
+
function dedupePosts(posts) {
|
|
89
|
+
return [...new Map(posts.map((post) => [post.id, post])).values()];
|
|
90
|
+
}
|
|
91
|
+
function mergePostsForRange(currentPosts, nextPosts, rangeStart, rangeEnd) {
|
|
92
|
+
const startTime = Date.parse(rangeStart);
|
|
93
|
+
const endTime = Date.parse(rangeEnd);
|
|
94
|
+
if (Number.isNaN(startTime) || Number.isNaN(endTime)) {
|
|
95
|
+
return dedupePosts([...currentPosts, ...nextPosts]);
|
|
96
|
+
}
|
|
97
|
+
return dedupePosts([
|
|
98
|
+
...currentPosts.filter((post) => {
|
|
99
|
+
const scheduledTime = Date.parse(post.scheduledAt);
|
|
100
|
+
return Number.isNaN(scheduledTime) || scheduledTime < startTime || scheduledTime > endTime;
|
|
101
|
+
}),
|
|
102
|
+
...nextPosts
|
|
103
|
+
]);
|
|
104
|
+
}
|
|
105
|
+
function expandRangeStart(current, next) {
|
|
106
|
+
if (!current) {
|
|
107
|
+
return next;
|
|
108
|
+
}
|
|
109
|
+
return Date.parse(next) < Date.parse(current) ? next : current;
|
|
110
|
+
}
|
|
111
|
+
function expandRangeEnd(current, next) {
|
|
112
|
+
if (!current) {
|
|
113
|
+
return next;
|
|
114
|
+
}
|
|
115
|
+
return Date.parse(next) > Date.parse(current) ? next : current;
|
|
116
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useMemo, useState } from "react";
|
|
3
|
+
import { hydrateRoot } from "react-dom/client";
|
|
4
|
+
import { useStore } from "zustand";
|
|
5
|
+
import { HomepageShell } from "./homepage-view.js";
|
|
6
|
+
import { createHomepageStore } from "./homepage-store.js";
|
|
7
|
+
const bookmarkStorageKey = "vidfarm:discover-bookmarks:v1";
|
|
8
|
+
const publicBookmarkScope = "public";
|
|
9
|
+
function readBoot() {
|
|
10
|
+
const element = document.getElementById("homepage-boot");
|
|
11
|
+
if (!element?.textContent) {
|
|
12
|
+
return {
|
|
13
|
+
templates: [],
|
|
14
|
+
account: { isLoggedIn: false },
|
|
15
|
+
loadEndpoint: "/discover/feed",
|
|
16
|
+
skillEndpointPrefix: "/discover/skills/"
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
return JSON.parse(element.textContent);
|
|
20
|
+
}
|
|
21
|
+
function resolveBookmarkStorageKey(userId) {
|
|
22
|
+
return `${bookmarkStorageKey}_${userId?.trim() || publicBookmarkScope}`;
|
|
23
|
+
}
|
|
24
|
+
function readBookmarks(userId) {
|
|
25
|
+
try {
|
|
26
|
+
const raw = window.localStorage.getItem(resolveBookmarkStorageKey(userId));
|
|
27
|
+
if (!raw) {
|
|
28
|
+
return new Set();
|
|
29
|
+
}
|
|
30
|
+
const parsed = JSON.parse(raw);
|
|
31
|
+
if (!Array.isArray(parsed)) {
|
|
32
|
+
return new Set();
|
|
33
|
+
}
|
|
34
|
+
return new Set(parsed.filter((value) => typeof value === "string"));
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return new Set();
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function writeBookmarks(bookmarks, userId) {
|
|
41
|
+
try {
|
|
42
|
+
window.localStorage.setItem(resolveBookmarkStorageKey(userId), JSON.stringify(Array.from(bookmarks)));
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
// Ignore storage failures and keep in-memory state.
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function filterTemplates(input) {
|
|
49
|
+
const query = input.searchQuery.trim().toLowerCase();
|
|
50
|
+
return input.templates.filter((template) => {
|
|
51
|
+
const matchesSearch = !query || `${template.templateId} ${template.title} ${template.slugId}`.toLowerCase().includes(query);
|
|
52
|
+
const matchesBookmarks = !input.bookmarksOnly || input.bookmarks.has(template.templateId);
|
|
53
|
+
return matchesSearch && matchesBookmarks;
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
function HomepageClientApp({ boot }) {
|
|
57
|
+
const store = useMemo(() => createHomepageStore(boot), [boot]);
|
|
58
|
+
const templates = useStore(store, (state) => state.templates);
|
|
59
|
+
const searchQuery = useStore(store, (state) => state.searchQuery);
|
|
60
|
+
const bookmarksOnly = useStore(store, (state) => state.bookmarksOnly);
|
|
61
|
+
const bookmarks = useStore(store, (state) => state.bookmarks);
|
|
62
|
+
const loading = useStore(store, (state) => state.loading);
|
|
63
|
+
const error = useStore(store, (state) => state.error);
|
|
64
|
+
const [bookmarksReady, setBookmarksReady] = useState(false);
|
|
65
|
+
const visibleTemplates = useMemo(() => filterTemplates({ templates, searchQuery, bookmarksOnly, bookmarks }), [templates, searchQuery, bookmarksOnly, bookmarks]);
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
store.getState().setBookmarks(readBookmarks(boot.account.userId));
|
|
68
|
+
setBookmarksReady(true);
|
|
69
|
+
}, [boot.account.userId, store]);
|
|
70
|
+
useEffect(() => {
|
|
71
|
+
if (!bookmarksReady) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
writeBookmarks(bookmarks, boot.account.userId);
|
|
75
|
+
}, [boot.account.userId, bookmarks, bookmarksReady]);
|
|
76
|
+
useEffect(() => {
|
|
77
|
+
if (boot.templates.length > 0) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
let cancelled = false;
|
|
81
|
+
store.getState().setLoading(true);
|
|
82
|
+
void (async () => {
|
|
83
|
+
try {
|
|
84
|
+
const response = await fetch(boot.loadEndpoint, {
|
|
85
|
+
credentials: "same-origin",
|
|
86
|
+
headers: { Accept: "application/json" }
|
|
87
|
+
});
|
|
88
|
+
const payload = await response.json().catch(() => ({}));
|
|
89
|
+
if (!response.ok) {
|
|
90
|
+
throw new Error(typeof payload.error === "string" && payload.error ? payload.error : "Unable to load formats.");
|
|
91
|
+
}
|
|
92
|
+
if (cancelled) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
store.getState().setTemplates(Array.isArray(payload.templates) ? payload.templates : []);
|
|
96
|
+
}
|
|
97
|
+
catch (fetchError) {
|
|
98
|
+
if (cancelled) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
store.getState().setError(fetchError instanceof Error ? fetchError.message : "Unable to load formats.");
|
|
102
|
+
}
|
|
103
|
+
})();
|
|
104
|
+
return () => {
|
|
105
|
+
cancelled = true;
|
|
106
|
+
};
|
|
107
|
+
}, [boot.loadEndpoint, boot.templates.length, store]);
|
|
108
|
+
return (_jsx(HomepageShell, { account: boot.account, templates: templates, visibleTemplates: visibleTemplates, searchQuery: searchQuery, bookmarksOnly: bookmarksOnly, bookmarks: bookmarks, loading: loading, error: error, skillEndpointPrefix: boot.skillEndpointPrefix, onSearchQueryChange: (value) => store.getState().setSearchQuery(value), onBookmarksOnlyChange: (value) => store.getState().setBookmarksOnly(value), onToggleBookmark: (templateId) => store.getState().toggleBookmark(templateId) }));
|
|
109
|
+
}
|
|
110
|
+
const boot = readBoot();
|
|
111
|
+
const root = document.getElementById("homepage-root");
|
|
112
|
+
if (root) {
|
|
113
|
+
hydrateRoot(root, _jsx(HomepageClientApp, { boot: boot }));
|
|
114
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { createStore } from "zustand/vanilla";
|
|
2
|
+
export function createHomepageStore(boot) {
|
|
3
|
+
return createStore((set) => ({
|
|
4
|
+
templates: boot.templates,
|
|
5
|
+
searchQuery: "",
|
|
6
|
+
bookmarksOnly: false,
|
|
7
|
+
bookmarks: new Set(),
|
|
8
|
+
loading: boot.templates.length === 0,
|
|
9
|
+
error: null,
|
|
10
|
+
setTemplates: (templates) => set({ templates, loading: false, error: null }),
|
|
11
|
+
setSearchQuery: (searchQuery) => set({ searchQuery }),
|
|
12
|
+
setBookmarksOnly: (bookmarksOnly) => set({ bookmarksOnly }),
|
|
13
|
+
setBookmarks: (bookmarks) => set({ bookmarks: new Set(bookmarks) }),
|
|
14
|
+
toggleBookmark: (templateId) => set((state) => {
|
|
15
|
+
const next = new Set(state.bookmarks);
|
|
16
|
+
if (next.has(templateId)) {
|
|
17
|
+
next.delete(templateId);
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
next.add(templateId);
|
|
21
|
+
}
|
|
22
|
+
return { bookmarks: next };
|
|
23
|
+
}),
|
|
24
|
+
setLoading: (loading) => set({ loading }),
|
|
25
|
+
setError: (error) => set({ error, loading: false })
|
|
26
|
+
}));
|
|
27
|
+
}
|