# Adding video generation to a chatbot: UX patterns for long running AI tasks Chat interfaces assume a request-response loop measured in seconds: the user types, tokens stream back, the turn ends. Video generation doesn't fit that loop. A single clip can take minutes to render, it costs real money per second of output, and it can fail after the user has stopped watching. If you wire a video model into your chatbot the same way you wire in a text model, users will see a frozen typing indicator, refresh the page, and submit the same job three times. This guide covers the UX patterns that make long-running AI tasks feel normal inside a chat product. It also covers the backend plumbing behind them, using the OpenAI-compatible [RelayDance API](https://relaydance.com/docs) for Seedance video models as the working example. --- ## TL;DR - **Treat a video request as a job, not a message.** Return a task card right away and let the conversation continue. - **Show the cost before submitting.** Seedance 2.0 runs about **$0.190/second at 720p** and about **$0.470/second at 1080p** ([relaydance.com/models](https://relaydance.com/models)), so a 15-second 1080p clip costs about **$7.05**. - **Draft cheap, then finalize.** Preview with Seedance Fast (about **$0.152/second**), then re-render at 1080p or 4K once the user approves. - **Use webhooks for completion and polling as a fallback.** Push the result into the chat thread when it lands. - **Make retries safe and visible.** According to the official RelayDance docs, "Failed or errored requests are never billed", so an automatic retry on failure doesn't cost extra. --- ## 1. Why video breaks the chat mental model | Property | Text completion | Video generation | |---|---|---| | Time to first output | Under 1 second (streamed tokens) | Nothing to show until the clip finishes | | Total duration | Seconds | Often minutes per clip | | Partial results | Yes (token streaming) | No: the clip arrives complete or fails | | Cost per request | Fractions of a cent | About $0.76–$7.11+ per clip (see table below) | | Failure mode | Usually immediate | Can fail late in the job | Because there's no partial output to stream, the usual chat feedback (text appearing word by word) is missing. Your UI has to replace that feedback with explicit job state. --- ## 2. Pattern: acknowledge right away with a task card When the user asks for a video, don't block the message input. Do these three things within about 300 ms: 1. **Post an assistant message containing a task card**, not a spinner bubble. 2. **Re-enable the input box** so the user can keep chatting or queue another request. 3. **Persist the task ID** server-side, tied to the conversation, so a page refresh restores the card. A good task card shows: - The prompt (truncated) and any reference image thumbnails - Resolution, duration, and model (e.g. "Seedance 2.0 · 1080p · 10 s") - **Estimated cost**, already shown before submission (see §4) - Current state: `Queued → Generating → Ready` or `Failed` - Actions: **Cancel** (hide the card), **Retry**, **Download**, **Upscale** ### Don't fake a percentage The API reports task status, not a fraction complete. A progress bar that fills to 95% and sits there for 2 minutes hurts trust more than an honest indeterminate state. Show these instead: - Elapsed time ("Generating · 1m 12s") - A typical-duration hint based on your own measured history for that model and resolution ("usually 1–3 min") - A note that the user can leave: "We'll post it here when it's done." --- ## 3. Pattern: async job plumbing (webhook first, polling fallback) RelayDance exposes video generation as an async task API. The official docs put it this way: "Set base_url to https://relaydance.com/v1 and keep your OpenAI SDK". Authentication is a standard `Authorization: Bearer YOUR_API_KEY` header, and you create keys at [relaydance.com/console](https://relaydance.com/console). The task lifecycle has three steps: 1. `POST /v1/video/generations` with `model`, `prompt`, `seconds`, and `metadata` (`ratio`, `resolution`, `generate_audio`, `callback_url`, `content[]`). 2. Either receive a webhook at `metadata.callback_url` with the final state, **or** 3. Poll `GET /v1/video/generations/{task_id}` until `status` is `succeeded` or `failed`. The result contains the video URL. ### Submitting a job python import os import requests BASE_URL = "https://relaydance.com/v1" HEADERS = {"Authorization": f"Bearer {os.environ['RELAYDANCE_API_KEY']}"} def submit_video(prompt: str, seconds: int, resolution: str, conversation_id: str, image_urls=None): content = [{"type": "image_url", "image_url": {"url": u}} for u in (image_urls or [])] resp = requests.post( f"{BASE_URL}/video/generations", headers=HEADERS, json={ "model": "doubao-seedance-2-0", # check relaydance.com/models for current model IDs "prompt": prompt, "seconds": seconds, "metadata": { "ratio": "16:9", "resolution": resolution, "generate_audio": True, "callback_url": f"https://yourapp.example/hooks/video?conv={conversation_id}", "content": content, }, }, timeout=30, ) resp.raise_for_status() return resp.json() # store the task id against the conversation > The `content[]` item shape above is illustrative. Confirm the exact reference-media schema in the [RelayDance docs](https://relaydance.com/docs). ### Receiving the webhook python from fastapi import FastAPI, Request app = FastAPI() @app.post("/hooks/video") async def video_hook(request: Request, conv: str): task = await request.json() status = task.get("status") if status == "succeeded": await post_to_thread(conv, kind="video_ready", task=task) elif status == "failed": await post_to_thread(conv, kind="video_failed", task=task) return {"ok": True} ### Polling as a safety net Webhooks get dropped because of deploys, firewalls, or expired tunnels. Run a background sweeper that polls any task still unresolved after a threshold: python import time def poll_until_done(task_id: str, interval_s: float = 5.0, max_wait_s: float = 900): deadline = time.time() + max_wait_s while time.time() < deadline: r = requests.get(f"{BASE_URL}/video/generations/{task_id}", headers=HEADERS, timeout=30) r.raise_for_status() task = r.json() if task.get("status") in ("succeeded", "failed"): return task time.sleep(interval_s) raise TimeoutError(task_id) **UX rule:** the browser should never poll the provider directly. It subscribes to *your* backend (SSE or WebSocket), and your backend fans out the state change to every open tab in that conversation. --- ## 4. Pattern: show the price before the user commits Video is the first chatbot feature where a single message can cost several dollars. Show the estimate on the confirmation chip *before* submitting, then again on the task card. ### Reference rates Rates below are from [relaydance.com/models](https://relaydance.com/models). Check that page for live pricing. | Model / tier | Price | 5 s clip | 10 s clip | 15 s clip | |---|---|---|---|---| | Seedance Fast | ~$0.152 / s | $0.76 | $1.52 | $2.28 | | Seedance 2.0 · 720p | ~$0.190 / s | $0.95 | $1.90 | $2.85 | | Seedance 2.0 · 1080p | ~$0.470 / s | $2.35 | $4.70 | $7.05 | | Seedance 2.5 · 1080p (`doubao-seedance-2-5-1080p`, added 2026-10-10) | ~$0.711 / s | $3.56 | $7.11 | $10.67 | | Seedance native 4K | ~$4.90 per 5 s clip | $4.90 | — | — | A few takeaways for UI design: - **1080p costs about 2.5× as much as 720p** on Seedance 2.0 ($0.470 vs $0.190 per second). Make 720p the default and treat 1080p as an explicit choice. - **Duration drives cost linearly.** A duration slider with a live price readout ("10 s · ≈ $1.90") changes behavior more than a pricing page link. - **Clips max out at 15 seconds** per request on RelayDance. Enforce that in the UI so the user doesn't get an API error after confirming. ### Images are priced differently If your chatbot also generates stills (`POST /v1/images/generations`), the cost model differs. Per [relaydance.com/models](https://relaydance.com/models), for **gpt-image-2** image output is free and only input is billed. Image-to-image starts at about **CNY 0.035**, and **4K output costs the same as 1K output**. Practical implication: don't put an upsell gate on 4K stills. Default to the highest resolution, because it costs the user nothing extra. --- ## 5. Pattern: draft, then finalize Users rarely get a prompt right on the first try. Paying 1080p rates for every iteration is wasteful, so build the flow around cheap drafts: 1. **Draft:** render a 5-second preview with Seedance Fast (~$0.76) or 720p (~$0.95). 2. **Review:** the user approves, edits the prompt, or swaps a reference image. 3. **Finalize:** a one-click **"Render in 1080p"** or **"Render in 4K"** button reuses the same prompt and references. Over three draft iterations plus one final 10 s 1080p render, that's roughly 3 × $0.76 + $4.70 = **$6.98**, compared with **$14.10** for three 10 s 1080p attempts. Show that saving in the UI ("Preview first and save about 50%") to nudge the behavior. --- ## 6. Pattern: reference media as chat attachments Seedance accepts reference media in `metadata.content[]`, and the prompt cites them as `@image1` through `@imageN`. The per-request limits on RelayDance are: - **Up to 9** reference images - **Up to 3** reference videos - **Up to 3** audio tracks UX recommendations: - Render attachments as **numbered chips** (`@image1`, `@image2` …) in the composer so users can reference them by name in their prompt. - Autocomplete `@` in the composer with the attached media list. - Disable the attach button at the limit (the 10th image, 4th video, or 4th audio track) and explain why, rather than letting the API reject the request. - Carry attachments forward on "Retry" and "Render in 1080p" so users don't have to re-upload. --- ## 7. Pattern: honest, safe failure handling Late failures are the worst moment in a long-running task UX. Design for them explicitly: - **Say what happened in plain language.** "The video couldn't be generated. You weren't charged." is better than a raw error code. - **Say whether money was spent.** According to the official RelayDance docs, "Failed or errored requests are never billed". Tell the user so, because it removes the hesitation around pressing Retry. - **Offer one-click retry** that keeps the prompt, references, and settings. - **Auto-retry once for transient errors** before you show the failure state. Because failed requests aren't billed, this only costs latency, not money. - **Never auto-retry a succeeded job.** Make submission idempotent on your side (dedupe on conversation ID + prompt hash + settings within a short window) so double-clicks and refreshes don't create duplicate billed jobs. --- ## 8. Pattern: notify when the user has left A 2-minute wait is long enough for the user to switch tabs. Close the loop in these ways: - **In-thread:** replace the task card with an inline video player and a short assistant message ("Here's your 10 s clip, 1080p"). - **Tab title / favicon badge:** "(1) Video ready". - **Browser notification:** ask for permission only after the user's first video submission, when the value is obvious. - **Email or push** for jobs still pending when the session ends. Store the final video URL in your own storage if you need long-term access. Don't assume a provider-hosted URL is permanent. --- ## 9. Implementation checklist | Area | Requirement | |---|---| | Submission | Non-blocking; task card in < 300 ms; input re-enabled | | Persistence | Task ID stored per conversation; restored on refresh | | Completion | Webhook via `metadata.callback_url` + polling sweeper fallback | | Client updates | SSE/WebSocket from your backend, not provider polling | | Cost | Pre-submit estimate using per-second rates; 720p default | | Limits | 15 s max duration; 9 images / 3 videos / 3 audio references | | Iteration | Draft (Fast/720p) → finalize (1080p/4K) flow | | Failures | Plain-language message, "not charged" notice, one-click retry | | Idempotency | Dedupe duplicate submissions before calling the API | | Notifications | In-thread, tab badge, opt-in browser notifications | --- ## FAQ **Can I use the OpenAI SDK?** Yes for auth and client setup. The API is OpenAI-compatible, and the official docs say to "Set base_url to https://relaydance.com/v1 and keep your OpenAI SDK". The video task endpoints (`/v1/video/generations`) are plain REST calls you can make with any HTTP client. **Do I need a BytePlus enterprise account?** No. RelayDance provides OpenAI-compatible access to Seedance with no BytePlus enterprise account or KYC required. Payment is pay-as-you-go via USDT or Stripe card. **What happens to my bill when a generation fails?** Nothing. Per [relaydance.com](https://relaydance.com/docs), "Failed or errored requests are never billed." **Which models are available besides Seedance 2.0?** Seedance 1.5 Pro, HappyHorse video models, Grok Imagine video and image models, and Grok text models. See [relaydance.com/models](https://relaydance.com/models) for the current list and live per-model rates. --- ## Sources - RelayDance model pricing: [https://relaydance.com/models](https://relaydance.com/models). Seedance 2.0 720p (~$0.190/s), 1080p (~$0.470/s), Seedance 2.5 1080p (~$0.711/s, added 2026-10-10), Seedance Fast (~$0.152/s), native 4K (~$4.90 per 5 s clip), gpt-image-2 billing. - RelayDance API documentation: [https://relaydance.com/docs](https://relaydance.com/docs). Base URL, task endpoints, webhook and polling behavior, reference media limits, billing on failed requests.
According to the official relaydance.com docs: "Failed or errored requests are never billed"
According to the official relaydance.com/docs docs: "Set base_url to https://relaydance.com/v1 and keep your OpenAI SDK"

Key facts and figures

ItemValueSource
Seedance 2.0 720p priceabout $0.190 / secondrelaydance.com/models
Seedance 2.0 1080p priceabout $0.470 / secondrelaydance.com/models
Seedance 2.5 1080p priceabout $0.711 / second, added 2026-10-10, model doubao-seedance-2-5-1080prelaydance.com/models
Seedance Fast priceabout $0.152 / secondrelaydance.com/models
Seedance native 4K priceabout $4.90 per 5-second cliprelaydance.com/models
gpt-image-2 image billingimage output is free, only input is billed; image-to-image from about CNY 0.035; 4K and 1K output cost the samerelaydance.com/models
API protocolOpenAI-compatible; set base_url to https://relaydance.com/v1relaydance.com/docs
Failed requestsfailed or errored requests are never billedrelaydance.com/docs

Data verified 2026-06-29; live prices are on the official /models page.


RelayDance home · Models and pricing · Docs · All guides · Privacy Policy · User Agreement · Telegram community · RelayRouter (LLM API) · Live playground