import { api, apiBlob, del, patch, post, postForm, put, qs, uploadFile } from "./client"; import type { Artifact, ArtifactType, BillingStatus, CalendarItem, ChatMessage, ChatSummary, Doc, DocDetail, Job, LibraryVoice, Notebook, NotebookSummary, WorkspaceChat, Page, Platform, Quota, ResurfaceItem, Settings, SocialAccounts, Source, TopicDetail, TopicMap, Usage, Voice, Delivery, VoiceDesignInput, VoiceDesignPreview, VoiceProfileData, VoiceState, ExtractedTheme, LegacyVideo, MyTemplate, ScriptPreview, ScriptScene, TemplateGeneration, TemplatesResponse, VideoCatalog, VideoConfig, VideoCreateBody, VideoFocusSource, VideoFocusTopic, VideoCustomVoice, VideoDesignedVoice, VideoJobState, VideoListItem, VideoOptions, VideoProject, VideoProjectData, VideoQuota, VideoSavedVoice, VideoScene, VideoStatus, VideoStyleItem, VideoVoicesResponse, } from "./types"; type SourceJob = { source: Source; job: Job }; export const sourcesApi = { list: () => api("/api/sources"), connect: (url: string) => post("/api/sources", { url }), importUrl: (url: string) => post("/api/sources/url", { url }), importFile: async (file: File) => { const { upload_id } = await uploadFile(file); return post("/api/sources/upload", { upload_id }); }, sync: (id: string) => post(`/api/sources/${id}/sync`), remove: (id: string) => del<{ ok: true; removed_posts: number }>(`/api/sources/${id}`), }; export const docsApi = { list: (p: { q?: string; source_id?: string; limit?: number; offset?: number } = {}) => api>(`/api/documents${qs(p)}`), get: (id: string) => api(`/api/documents/${id}`), remove: (id: string) => del(`/api/documents/${id}`), }; export const notebooksApi = { list: () => api("/api/notebooks"), get: (id: string) => api(`/api/notebooks/${id}`), create: (title: string, document_ids: string[] = [], description?: string) => post<{ id: string; title: string; added: number }>("/api/notebooks", { title, document_ids, description }), /** The "All posts" notebook, created on first use. */ archive: () => post<{ id: string; title: string }>("/api/notebooks/archive"), fromTopic: (topicId: string) => post<{ id: string; title: string }>(`/api/notebooks/from-topic/${topicId}`), update: (id: string, body: { title?: string; description?: string }) => patch(`/api/notebooks/${id}`, body), remove: (id: string) => del(`/api/notebooks/${id}`), addDocs: (id: string, document_ids: string[]) => post<{ added: number }>(`/api/notebooks/${id}/documents`, { document_ids }), removeDoc: (id: string, docId: string) => del(`/api/notebooks/${id}/documents/${docId}`), artifacts: (id: string) => api(`/api/notebooks/${id}/artifacts`), chats: (id: string) => api(`/api/notebooks/${id}/chats`), /** Every answered chat in the workspace, newest first. */ allChats: () => api("/api/notebooks/chats"), messages: (chatId: string) => api(`/api/notebooks/chats/${chatId}/messages`), removeChat: (chatId: string) => del(`/api/notebooks/chats/${chatId}`), }; export type GenerateBody = { type: ArtifactType; notebook_id?: string; document_id?: string; /** No notebook or post: use the "All posts" notebook. */ archive?: boolean; format?: "deep_dive" | "brief" | "debate"; minutes?: number; style?: "audiogram"; audio_artifact_id?: string; slides?: { heading: string; body: string }[]; parent_id?: string; }; export const artifactsApi = { list: (p: { type?: string; notebook_id?: string; document_id?: string; status?: string; q?: string; limit?: number; offset?: number } = {}) => api>(`/api/artifacts${qs(p)}`), get: (id: string) => api(`/api/artifacts/${id}`), generate: (body: GenerateBody) => post("/api/artifacts/generate", body), update: (id: string, body: { title?: string; content?: Record }) => patch(`/api/artifacts/${id}`, body), remove: (id: string) => del(`/api/artifacts/${id}`), retry: (id: string) => post(`/api/artifacts/${id}/retry`), }; export const jobsApi = { active: () => api("/api/jobs?active=1"), recent: (kind?: string) => api(`/api/jobs${qs({ kind, limit: 10 })}`), get: (id: string) => api(`/api/jobs/${id}`), }; export const topicsApi = { map: () => api("/api/topics"), get: (id: string) => api(`/api/topics/${id}`), rebuild: (full = false) => post(`/api/topics/rebuild${qs({ full })}`), }; export const voiceApi = { get: () => api("/api/voice"), update: (body: { profile?: VoiceProfileData; host_voices?: Partial<{ host_a: string; host_b: string }>; delivery?: Partial<{ host_a: Delivery; host_b: Delivery }>; }) => put("/api/voice", body), build: (document_ids: string[]) => post("/api/voice/build", { document_ids }), voices: () => api("/api/voice/voices"), consent: async (files: File[], consent_text: string, remove_background_noise: boolean) => { const upload_ids = []; for (const f of files) upload_ids.push((await uploadFile(f)).upload_id); return post("/api/voice/consent", { upload_ids, agreed: true, consent_text, remove_background_noise }); }, library: (p: { search?: string; gender?: string; age?: string; accent?: string; language?: string; use_case?: string; page?: number }) => api<{ voices: LibraryVoice[]; has_more: boolean }>(`/api/voice/library${qs(p)}`), addFromLibrary: (v: LibraryVoice, use_as?: "host_a" | "host_b") => post("/api/voice/library/add", { public_owner_id: v.public_owner_id, voice_id: v.voice_id, name: v.name, use_as, }), preview: (voice_id: string, opts: { text?: string; host?: "host_a" | "host_b"; delivery?: Delivery } = {}) => post<{ url: string; seconds: number }>("/api/voice/preview", { voice_id, ...opts }), readingScript: () => api<{ title: string; text: string; words: number }>("/api/voice/reading-script"), quota: () => api("/api/voice/quota"), revoke: () => del("/api/voice/consent"), design: (input: VoiceDesignInput) => post<{ description: string; previews: VoiceDesignPreview[] }>("/api/voice/design", input), saveDesign: (body: { generated_voice_id: string; name: string; description: string; use_as?: "host_a" | "host_b" }) => post("/api/voice/design/save", body), }; export type ItemBody = { platform: Platform; scheduled_at: string; content: string; thread?: string[]; artifact_id?: string | null; document_id?: string | null; social_account_id?: string | null; remind_by_email?: boolean; draft?: boolean; }; export const launchpadApi = { items: (p: { start?: string; end?: string; status?: string } = {}) => api(`/api/calendar${qs(p)}`), create: (body: ItemBody) => post("/api/calendar", body), update: (id: string, body: Partial & { status?: "scheduled" | "draft" }) => patch(`/api/calendar/${id}`, body), remove: (id: string) => del(`/api/calendar/${id}`), publishNow: (id: string) => post(`/api/calendar/${id}/publish`), accounts: () => api("/api/social/accounts"), connectBluesky: (handle: string, app_password: string) => post("/api/social/bluesky", { handle, app_password }), startOAuth: (platform: "x" | "linkedin") => post<{ url: string }>(`/api/social/${platform}/start`), completeOAuth: (ticket: string) => post("/api/social/complete", { ticket }), disconnect: (id: string) => del(`/api/social/accounts/${id}`), }; export const resurfaceApi = { get: () => api<{ evergreen: ResurfaceItem[]; on_this_day: ResurfaceItem[]; unscored: number; total_posts: number }>( "/api/resurface", ), scan: () => post("/api/resurface/scan"), }; export type SettingsPatch = { name?: string; workspace_name?: string; brand_name?: string; brand_accent?: string; logo_upload_id?: string; training_opt_in?: boolean; email_unsubscribed?: boolean; }; export const settingsApi = { get: () => api("/api/settings"), update: (body: SettingsPatch) => patch("/api/settings", body), usage: () => api("/api/settings/usage"), }; export type BillingCycle = "monthly" | "annual"; export const billingApi = { status: () => api("/api/billing/status"), checkout: (plan: string, cycle: BillingCycle) => post<{ url: string }>("/api/billing/checkout", { plan, cycle }), portal: () => post<{ url: string }>("/api/billing/portal"), confirm: (sessionId: string) => post("/api/billing/confirm", { session_id: sessionId }), }; function form(fields: Record): FormData { const f = new FormData(); for (const [k, v] of Object.entries(fields)) { if (v === undefined || v === null) continue; f.append(k, v instanceof Blob ? v : String(v)); } return f; } export type ScriptDraft = { title: string; display_text: string; narration_text: string; revision: number; draft_scenes: { id: number; title: string; display_text: string | null; narration_text: string }[] }; /** Videos are made by blog2video; the backend proxies every call. Ids here are our artifact ids. */ export const videosApi = { config: () => api("/api/videos/config"), quota: () => api("/api/videos/quota"), catalog: () => api("/api/videos/catalog"), list: () => api("/api/videos"), create: (body: VideoCreateBody) => post("/api/videos", body), /** Three topics the AI suggests a video from this source could focus on. */ focusTopics: (body: VideoFocusSource) => post<{ topics: VideoFocusTopic[] }>("/api/videos/focus-topics", body), /** Multi-link: one video per link. Items with `error` say where it stopped (usually the allowance ran out). */ createBatch: (urls: string[], options: VideoOptions) => post<(Artifact | { error: string; url: string })[]>("/api/videos/batch", { ...options, urls }), /** Up to 5 documents; blog2video reads them itself. */ createFromFiles: (files: File[], options: VideoOptions) => { const f = new FormData(); files.forEach((file) => f.append("files", file)); f.append("options", JSON.stringify(options)); return postForm("/api/videos/upload", f); }, get: (id: string) => api(`/api/videos/${id}`), status: (id: string) => api(`/api/videos/${id}/status`), remove: (id: string) => del(`/api/videos/${id}`), // Script review script: (id: string) => api<{ status: string; scenes: ScriptScene[] }>(`/api/videos/${id}/script`), narrationPreview: (id: string, sceneId: number, body: ScriptDraft) => post(`/api/videos/${id}/script/scenes/${sceneId}/narration-preview`, body), aiPreview: (id: string, sceneId: number, body: ScriptDraft & { instruction: string }) => post(`/api/videos/${id}/script/scenes/${sceneId}/ai-preview`, body), approveScript: (id: string, scenes: (ScriptScene & { source_fingerprint?: string; accepted_ai_instructions?: string[] })[]) => post(`/api/videos/${id}/script/approve`, { scenes }), }; const P = (id: string, path: string) => `/api/videos/${id}/p/${path}`; export type LayoutSchema = Record; fields?: Record[] }>; /** The template's layouts, from blog2video's GET .../layouts. Variants ("news_headline__v2") are visual styles of a base * layout: layout_variants maps a base to its variant ids, layout_variant_labels names each ("Broadsheet"). */ export type LayoutInfo = { layouts: string[]; layout_names?: Record; layout_prop_schema?: LayoutSchema; layout_variants?: Record; layout_variant_labels?: Record; }; export type StockClip = { provider: string; id: string; preview_url: string; thumbnail_url: string; download_url: string; width: number; height: number; duration: number; author: string; page_url: string }; /** The editor: blog2video's project endpoints, through our allowlisted proxy. */ export const videoEditApi = { project: (id: string) => api(P(id, "")), updateProject: (id: string, body: Partial) => patch(P(id, "update-project"), body), // Scenes updateScene: (id: string, sceneId: number, body: Partial>) => put(P(id, `scenes/${sceneId}`), body), deleteScene: (id: string, sceneId: number) => del(P(id, `scenes/${sceneId}`)), reorderScenes: (id: string, scene_orders: { scene_id: number; order: number }[]) => post(P(id, "scenes/reorder"), { scene_orders }), regenerateScene: (id: string, sceneId: number, body: { description?: string; narration_text?: string; regenerate_voiceover?: boolean; voiceover_verbatim?: boolean; layout?: string; image?: File }) => postForm(P(id, `scenes/${sceneId}/regenerate`), form(body)), addScene: (id: string, prompt: string, position?: number) => post(P(id, "scenes/add"), { prompt, position }), addSceneStatus: (id: string) => api(P(id, "scenes/add-status")), // Images uploadImage: (id: string, sceneId: number, image: File) => postForm(P(id, `scenes/${sceneId}/image`), form({ image })), generateImage: (id: string, sceneId: number, image_description: string) => // Not saved on the scene: the image comes back for the user to keep (uploadImage) or discard. post<{ image_base64: string; refined_prompt: string }>(P(id, `scenes/${sceneId}/generate-image`), { image_description }), imageFocus: (id: string, sceneId: number, body: { image_focus_x: number; image_focus_y: number; image_zoom?: number }) => patch(P(id, `scenes/${sceneId}/image-focus`), body), moveImage: (id: string, from_scene_id: number, to_scene_id: number) => post(P(id, "images/move"), { from_scene_id, to_scene_id }), swapImages: (id: string, first_scene_id: number, second_scene_id: number) => post(P(id, "images/swap"), { first_scene_id, second_scene_id }), duplicateImage: (id: string, source_scene_id: number, target_scene_id: number) => post(P(id, "images/duplicate"), { source_scene_id, target_scene_id }), /** Deletes the image or clip from the video for good (files too) and takes it off any scene. */ /** The template's layouts; layout_prop_schema[layout].defaults holds its default font sizes (meta.json). */ layouts: (id: string) => api(P(id, "layouts")), deleteAsset: (id: string, assetId: number) => del(P(id, `assets/${assetId}`)), assignImage: (id: string, scene_id: number, asset_id: number) => post(P(id, "images/assign-existing"), { scene_id, asset_id }), // Stock footage searchStock: (id: string, q: string) => api<{ clips: StockClip[] }>(P(id, `stock-footage/search${qs({ q })}`)), /** Downloads the clip into the project (a new asset). It is not on the scene until linkStock. */ useStock: (id: string, sceneId: number, c: StockClip) => post<{ asset_id: number; filename: string }>(P(id, `scenes/${sceneId}/stock-footage`), { provider: c.provider, clip_id: c.id, download_url: c.download_url, width: c.width, height: c.height, duration: c.duration, author: c.author, page_url: c.page_url }), /** Put a clip that is already in the project on a scene (assignedVideo). */ linkStock: (id: string, scene_id: number, filename: string) => post(P(id, "stock-footage/link"), { scene_id, filename }), pendingStock: (id: string) => api<{ scenes?: unknown[]; [k: string]: unknown }>(P(id, "stock-footage/pending")), approveStock: (id: string) => post(P(id, "stock-footage/approve")), rejectStock: (id: string) => post(P(id, "stock-footage/reject")), // Voice, voiceovers, language, template uploadVoiceover: (id: string, sceneId: number, audio: File) => postForm(P(id, `scenes/${sceneId}/voiceover`), form({ audio })), deleteVoiceover: (id: string) => post(P(id, "delete-voiceover")), changeVoice: (id: string, body: { voice_gender?: string; voice_accent?: string; custom_voice_id?: string | null; voice_emotion?: string | null }) => post(P(id, "change-voice"), body), voiceStatus: (id: string) => api(P(id, "voice-change-status")), changeLanguage: (id: string, content_language: string) => post(P(id, "change-language"), { content_language }), languageStatus: (id: string) => api(P(id, "language-change-status")), changeTemplate: (id: string, template: string) => post(P(id, "change-template-regenerate-layouts"), { template }), templateStatus: (id: string) => api(P(id, "template-change-status")), // Refresh the script (keeps narration and voiceovers, rewrites titles and layouts) refreshScript: (id: string, user_instruction?: string) => post(P(id, "regenerate-script"), { user_instruction }), refreshStatus: (id: string) => api(P(id, "regenerate-script-status")), refreshPreview: (id: string) => api>(P(id, "regenerate-script/preview")), refreshVerify: (id: string) => post(P(id, "regenerate-script/verify")), refreshRetry: (id: string, user_instruction?: string) => post(P(id, "regenerate-script/regenerate"), { user_instruction }), // AI chat editing ★ chat: (id: string, message: string, conversation_id?: number) => post<{ reply?: string; message?: string; conversation_id?: number; [k: string]: unknown }>(P(id, "chat"), { message, conversation_id }), chatHistory: (id: string) => api(P(id, "chat/history")), // Avatars ★ authorizeAvatars: (id: string, scene_ids: number[], avatar_preset?: string, avatar_motion_style?: string) => post(P(id, "avatar-batch/authorize"), { scene_ids, avatar_preset, avatar_motion_style }), makeAvatar: (id: string, sceneId: number, avatar_preset?: string) => postForm(P(id, `scenes/${sceneId}/avatar`), form({ avatar_preset })), avatarProgress: (id: string) => api>(P(id, "avatar-progress")), deleteAvatar: (id: string, sceneId: number) => del(P(id, `scenes/${sceneId}/avatar`)), uploadPortrait: (id: string, file: File) => postForm(P(id, "avatar-portrait"), form({ file })), deletePortrait: (id: string) => del(P(id, "avatar-portrait")), avatarAppearance: (id: string, sceneId: number, body: { avatar_shape?: string; avatar_size?: number; avatar_position?: string; avatar_opacity?: number }) => patch(P(id, `scenes/${sceneId}/avatar-appearance`), body), // Render, download, frames /** force renders again even when an MP4 is already on R2 (blog2video otherwise just returns the old one). */ render: (id: string, { force = false, resolution = "1080p" }: { force?: boolean; resolution?: string } = {}) => post(P(id, `render${qs({ resolution, force_render: force || undefined })}`)), renderStatus: (id: string) => api(P(id, "render-status")), cancelRender: (id: string) => post(P(id, "cancel-render")), downloadUrl: (id: string) => api<{ url: string }>(P(id, "download-url")), downloadStudio: (id: string) => apiBlob(P(id, "download-studio")), still: (id: string, frame: number) => apiBlob(P(id, `render-still${qs({ frame })}`)), }; export const videoVoicesApi = { list: () => api("/api/video-voices"), save: (voice_id: string) => post("/api/video-voices/saved", { voice_id }), unsave: (voice_id: string) => del(`/api/video-voices/saved/${encodeURIComponent(voice_id)}`), designFromPrompt: (prompt: string) => post<{ previews: VideoDesignedVoice[] }>("/api/video-voices/design/prompt", { prompt }), designFromPreset: (body: { gender: string; age: string; persona: string; speed: string; accent: string }) => post<{ previews: VideoDesignedVoice[] }>("/api/video-voices/design/preset", body), keep: (body: { generated_voice_id: string; source: "prompt" | "preset"; name: string; prompt_text?: string }) => post("/api/video-voices/custom", body), clone: (name: string, file: File, remove_background_noise = true) => postForm("/api/video-voices/clone", form({ name, file, remove_background_noise })), customPreview: (customId: number) => api<{ preview_url: string | null; ready: boolean }>(`/api/video-voices/custom/${customId}/preview`), removeCustom: (customId: number) => del(`/api/video-voices/custom/${customId}`), /** ★ A few seconds of the voice with the chosen tuning (audio/mpeg). */ sample: (body: { voice_gender: string; voice_accent: string; custom_voice_id?: string; voice_emotion?: string; video_style?: string }) => apiBlob("/api/video-voices/sample", { method: "POST", body: JSON.stringify(body) }), }; export const videoStylesApi = { list: () => api("/api/video-styles"), aiDraft: (prompt: string) => post<{ name: string; guidance: string }>("/api/video-styles/ai-draft", { prompt }), create: (body: { name: string; guidance: string; creation_method: "manual" | "ai"; source_prompt?: string }) => post("/api/video-styles", body), update: (styleId: number, body: { name: string; guidance: string; version: number }) => patch(`/api/video-styles/${styleId}`, body), remove: (styleId: number) => del(`/api/video-styles/${styleId}`), }; const T = (id: number, path: string) => `/api/video-templates/${id}/p/${path}`; export const videoTemplatesApi = { list: () => api("/api/video-templates"), extractFromUrl: (url: string) => post("/api/video-templates/extract/url", { url }), extractFromPrompt: (prompt: string, name?: string) => post("/api/video-templates/extract/prompt", { prompt, name }), extractFromDoc: (file: File, name: string) => postForm("/api/video-templates/extract/doc", form({ file, name })), create: (body: { name: string; theme: Record; source_url?: string; logo_urls?: string[]; og_image?: string; screenshot_url?: string }) => post("/api/video-templates", body), generate: (id: number) => post(`/api/video-templates/${id}/generate`), generationStatus: (id: number) => api(`/api/video-templates/${id}/generation-status`), remove: (id: number) => del(`/api/video-templates/${id}`), // Editing get: (id: number) => api>(T(id, "")), update: (id: number, body: { name?: string; theme?: Record }) => put>(T(id, ""), body), uploadLogo: (id: number, file: File) => postForm(T(id, "upload-logo"), form({ file })), aiEdit: (id: number, sceneKey: string, prompt: string) => post(T(id, `scenes/${sceneKey}/ai-edit`), { prompt }), aiEditStatus: (id: number, sceneKey: string) => api(T(id, `scenes/${sceneKey}/ai-edit/status`)), drafts: (id: number) => api(T(id, "scene-drafts")), applyDraft: (id: number, sceneKey: string) => post(T(id, `scenes/${sceneKey}/draft/apply`)), discardDraft: (id: number, sceneKey: string) => post(T(id, `scenes/${sceneKey}/draft/discard`)), fontDefaults: (id: number, body: Record, sceneKey?: string) => patch(T(id, sceneKey ? `scenes/${sceneKey}/font-defaults` : "scenes/font-defaults"), body), chart: (id: number, sceneKey: string, body: Record) => patch(T(id, `scenes/${sceneKey}/chart`), body), versions: (id: number) => api(T(id, "versions")), rollback: (id: number, versionId: number) => post(T(id, `versions/${versionId}/rollback`)), regenerate: (id: number) => post(T(id, "regenerate-code")), resume: (id: number) => post(T(id, "resume-generation")), rate: (id: number, body: Record) => post(T(id, "rating"), body), };