diff --git a/web/src/lib/api.ts b/web/src/lib/api.ts index ab83d29..fbc1df0 100644 --- a/web/src/lib/api.ts +++ b/web/src/lib/api.ts @@ -254,6 +254,29 @@ export const createSchedule = (data: { body: JSON.stringify(data), }); +export const createThreadBundle = (data: { + account_id: string; + scheduled_at?: string; + auto_approve?: boolean; + posts: Array<{ + media_type?: string; + caption?: string; + media_urls?: string[]; + reply_to?: number | string; + }>; +}): Promise<{ + data: { + bundle_id: string; + items: Array<{ id: string; seq: number; status: string }>; + failures: unknown[]; + mode: string; + }; +}> => + request('/threads', { + method: 'POST', + body: JSON.stringify(data), + }); + export const updateSchedule = (id: string, data: { scheduled_at?: string; status?: string; diff --git a/web/src/routes/admin/schedules/+page.svelte b/web/src/routes/admin/schedules/+page.svelte index 8a5ccdb..1757420 100644 --- a/web/src/routes/admin/schedules/+page.svelte +++ b/web/src/routes/admin/schedules/+page.svelte @@ -8,6 +8,7 @@ import { listSchedulesPaged, createSchedule, + createThreadBundle, deleteSchedule, patchSchedule, approveSchedule, @@ -202,6 +203,8 @@ function openCreateModal() { modalAccountId = accounts.length > 0 ? accounts[0].id : ''; + bundleMode = false; + bundleItems = [{ mediaType: 'TEXT', caption: '', mediaUrl: '' }]; modalScheduledAt = ''; modalPublishNow = false; modalCaption = ''; @@ -227,12 +230,84 @@ } } + // ── Bundle mode state (thread bundles) ── + let bundleMode = $state(false); + type BundleItemDraft = { + mediaType: string; + caption: string; + mediaUrl: string; + }; + let bundleItems = $state>([ + { mediaType: 'TEXT', caption: '', mediaUrl: '' } + ]); + + function addBundleItem() { + if (bundleItems.length >= 10) { + toast('Bundle is limited to 10 items', 'error'); + return; + } + bundleItems = [...bundleItems, { mediaType: 'TEXT', caption: '', mediaUrl: '' }]; + } + + function removeBundleItem(idx: number) { + bundleItems = bundleItems.filter((_, k) => k !== idx); + } + + async function handleCreate() { if (!modalAccountId || (!modalPublishNow && !modalScheduledAt)) { toast('Account and scheduled time are required', 'error'); return; } + // ── Bundle mode: root + chained replies via POST /api/threads ── + if (bundleMode) { + const items = bundleItems + .map((it, idx) => { + const out: Record = {}; + const mt = it.mediaType || 'TEXT'; + if (mt !== 'TEXT' || idx > 0) out.media_type = mt; + if (idx === 0) out.media_type = mt; + out.caption = it.caption; + if (mt !== 'TEXT') { + const urls = [it.mediaUrl.trim()].filter(Boolean); + if (urls.length === 0) return null; + out.media_urls = urls; + } + if (idx > 0) out.reply_to = 0; // chained to root by default + return Object.keys(out).length > 0 ? out : null; + }) + .filter(Boolean); + if (items.length === 0) { + toast('Add at least one item', 'error'); + return; + } + creating = true; + try { + const payload: Record = { + account_id: modalAccountId, + posts: items + }; + if (!modalPublishNow && modalScheduledAt) { + payload.scheduled_at = new Date(modalScheduledAt).toISOString(); + } + payload.auto_approve = modalPublishNow || undefined; + const res = await createThreadBundle(payload as any); + toast( + `Thread bundle created (${res.data.items.length} items, ${res.data.mode})`, + 'success' + ); + closeCreateModal(); + await loadData(); + } catch (e: any) { + toast(e.message || 'Failed to create thread bundle', 'error'); + } finally { + creating = false; + } + return; + } + + // Validate media URLs based on type let mediaType = 'TEXT'; let mediaUrlList: string[] = []; @@ -855,6 +930,56 @@ {/if} +
+ +
+ + {#if bundleMode} +
+ + {#each bundleItems as item, idx} +
+
+ + {idx === 0 ? 'Root post' : `Reply → root post`} + + {#if idx > 0} + + {/if} +
+ + + {#if item.mediaType === 'IMAGE'} + + {/if} +
+ {/each} + {#if bundleItems.length < 10} + + {/if} +
+ {/if} + + {#if !bundleMode}
+ {/if}
@@ -881,7 +1007,7 @@ onclick={handleCreate} disabled={creating} > - {creating ? 'Creating…' : 'Create as Draft'} + {creating ? 'Creating…' : bundleMode ? 'Create Bundle' : 'Create as Draft'}