Skip to content

feat(channel): add HappyHorse (百炼快乐马) DashScope video generation channel + playground UI - #5124

Open
tears710 wants to merge 6 commits into
QuantumNous:mainfrom
tears710:feat/happyhorse-channel
Open

feat(channel): add HappyHorse (百炼快乐马) DashScope video generation channel + playground UI#5124
tears710 wants to merge 6 commits into
QuantumNous:mainfrom
tears710:feat/happyhorse-channel

Conversation

@tears710

@tears710 tears710 commented May 26, 2026

Copy link
Copy Markdown

📝 变更描述 / Description

新增 HappyHorse(百炼快乐马) 渠道,对接阿里云百炼 DashScope 异步视频合成 API(Channel Type 58),并配套前端 Playground 视频任务 UI 与单元测试。

后端(Task adaptor)

  • 新增 relay/channel/task/happyhorse/ 完整适配器:支持 happyhorse-1.0-t2v / i2v / r2v / video-edit 四个模型映射到 DashScope wanx2.1-t2v-turbo / wanx2.1-i2v-turbo / wanx2.1-kf2v-plus / wanx2.1-vace-plus,遵循 Task Adaptor 抽象 (ValidateRequestAndSetAction / BuildRequestURL / BuildRequestHeader / ParseTaskResult)。
  • 请求体走 X-DashScope-Async: enable,按官方规范组装 input.prompt + input.media[]、parameters.size/duration/...,对 media[] 为空、type 非法等情况做严格校验。
  • ParseTaskResult 透传 DashScope code/message,方便上游调度看到失败原因。
  • controller/model.go:channelId2Models 初始化时把 Task adaptor 的模型也注册进去,修复"填入相关模型"按钮在该渠道下为空的 bug。
  • web/default 渠道前端按渠道类型展示对应模型列表。

前端(Playground 视频任务 UI)

  • 在 web/default/src/features/playground 下新增视频任务相关组件:video-input-form / video-player / video-task-item / video-task-queue,以及 use-video-task hook 驱动「提交 → 轮询 → 重试 / 取消」生命周期。
  • 复用现有 playground entry / api.ts / constants / types,不影响 chat / image 已有形态。

测试

  • relay/channel/task/happyhorse/adaptor_test.go:覆盖四个模型的 Validate / BuildRequestURL / BuildRequestHeader / ParseTaskResult 路径。

🚀 变更类型 / Type of change

  • ✨ 新功能 (New feature) - HappyHorse 渠道 + Playground 视频任务 UI
  • 🐛 Bug 修复 (Bug fix) - channelId2Models 缺失 Task adaptor 模型;前端"填入相关模型"按钮按渠道类型展示

🔗 关联任务 / Related Issue

  • 暂无

✅ 提交前检查项 / Checklist

  • 人工确认
  • 非重复提交
  • Bug fix 说明
  • 变更理解
  • 范围聚焦(不包含 vendor/ / Dockerfile.dev / .qoder/ 等基础设施)
  • 本地验证:go build -mod=mod ./... ✅ / go test -mod=mod ./relay/channel/task/happyhorse/... ✅ (ok 0.859s) / go vet ✅
  • 安全合规:无硬编码凭据

📸 运行证明 / Proof of Work

$ go test -mod=mod ./relay/channel/task/happyhorse/...
ok  	one-api/relay/channel/task/happyhorse	0.859s

$ go build -mod=mod ./...
(无输出,构建通过)

$ go vet -mod=mod ./relay/channel/task/happyhorse/...
(无输出)

提交列表(5 commits ahead of upstream/main):

9d56d1922 feat(channel): add HappyHorse (百炼快乐马) video generation channel (#58)
e4d066fcd fix(model): register Task adaptor models in channelId2Models init
577180151 fix(frontend): use channel-specific models for '填入相关模型' button
2180071be test(channel/happyhorse): add unit tests for HappyHorse adaptor
8ff103e2f feat(playground): add video task UI for HappyHorse and async video channels

Summary by CodeRabbit

  • New Features
    • HappyHorse video-generation channel added and listed in channel selector (with icon)
    • New Video tab in Playground: model selection, API key/token choice, submit video tasks
    • VideoInputForm, MediaDropZone, VideoTaskQueue, VideoTaskItem, and VideoPlayer UI components
    • Persistent task queue with automatic polling, previews, progress, and clear/remove actions
    • Advanced video options (seed, watermark, prompt extension) and localized UI strings
  • API
    • Client APIs to submit/poll video tasks and manage user tokens

Review Change Stack

tears710 added 5 commits May 26, 2026 21:22
…mNous#58)

- Add ChannelTypeHappyHorse = 58 with DashScope base URL
- Implement TaskAdaptor: supports t2v/i2v/r2v/video-edit models
- Use media[] array input format (differs from Ali's flat img_url fields)
- video-edit model uses video_url type; others use image_url type
- Default params: resolution=720P, duration=5s, prompt_extend=true
- Billing via seconds multiplier on base model ratio
- Add default model prices for 4 happyhorse-1.0-* models
- Register adaptor in GetTaskAdaptor switch
- Add frontend channel type 58 with Tongyi icon

🤖 Generated with [Qoder][https://qoder.com]
Task-type channel adaptors (e.g. HappyHorse, Kling) are not mapped to
a standard APIType, so their GetModelList() was never called during
init(), leaving them absent from the channel preset model selector.

Now when ChannelType2APIType returns !success, fall back to
GetTaskAdaptor and register its models into both channelId2Models and
openAIModels so the frontend "填入相关模型" shortcut works correctly.

🤖 Generated with [Qoder][https://qoder.com]
Add getModelsByChannelType() API call to fetch /api/models map, and
update basicModels useMemo to prefer channel-type-specific models
when available, so HappyHorse (type 58) only fills its 4 models
instead of all global models.

🤖 Generated with [Qoder][https://qoder.com]
- Add adaptor_test.go covering ValidateRequestAndSetAction, BuildRequestURL,
  BuildRequestHeader, and ParseTaskResult for happyhorse-1.0-{t2v,i2v,r2v,video-edit}.
- Tighten adaptor request validation: reject empty media[], invalid types,
  and surface DashScope error code/message via ParseTaskResult.

Verified via: go test ./relay/channel/task/happyhorse/...
…annels

- Add video task queue components (video-input-form, video-player,
  video-task-item, video-task-queue) for submitting and tracking
  asynchronous video generation requests.
- Add use-video-task hook to drive task lifecycle (submit, poll, retry,
  cancel) and surface DashScope-style status/error to the UI.
- Wire playground entry, api.ts, constants, hooks index, and types to
  expose the video task surface alongside existing image/chat modes.

Targets HappyHorse (channel type 58, happyhorse-1.0-{t2v,i2v,r2v,video-edit})
and other Task adaptors using the async video pattern.
@coderabbitai

coderabbitai Bot commented May 26, 2026

Copy link
Copy Markdown
Contributor

Walkthrough

Adds HappyHorse video-generation channel (type 58): backend channel registration, a full HappyHorse TaskAdaptor with tests and model discovery changes, pricing entries, and a complete frontend playground (APIs, types, hook, components, i18n) enabling video submission, polling, queueing, and preview.

Changes

Backend: Channel Registration & Task Adaptor

Layer / File(s) Summary
Channel type registration and wiring
constant/channel.go, relay/relay_adaptor.go
Adds ChannelTypeHappyHorse = 58, base URL and display name; imports and returns HappyHorse TaskAdaptor in adaptor factory.
Model pricing & discovery integration
setting/ratio_setting/model_ratio.go, controller/model.go
Adds default per-second pricing entries for HappyHorse models and extends model initialization to load models from task adaptors by channel ID.
HappyHorse adaptor types & constants
relay/channel/task/happyhorse/constants.go, relay/channel/task/happyhorse/adaptor.go (types section)
Defines HappyHorse request/response DTOs, exported ModelList and ChannelName constants.
HappyHorse adaptor implementation
relay/channel/task/happyhorse/adaptor.go (implementation sections)
Implements TaskAdaptor: init/validation, request URL/header/body construction, billing estimation, DoRequest/DoResponse, FetchTask, ParseTaskResult, and ConvertToOpenAIVideo including status mapping.
HappyHorse adaptor tests
relay/channel/task/happyhorse/adaptor_test.go
Adds comprehensive unit tests covering request conversion, metadata overrides, status mapping, parsing, response handling, fetch validation, and OpenAI-compatible conversion.

Web Frontend: Channels & Playground

Layer / File(s) Summary
Web channel type and model management
web/default/src/features/channels/constants.ts, web/default/src/features/channels/lib/channel-utils.ts, web/default/src/features/channels/api.ts, web/default/src/features/channels/components/drawers/channel-mutate-drawer.tsx
Registers frontend channel type 58 and icon mapping, adds getModelsByChannelType() API, and updates drawer to prefer server-provided model lists for a channel.
Playground types and constants
web/default/src/features/playground/types.ts, web/default/src/features/playground/constants.ts
Adds video task types, request/response/item interfaces, model config types, API endpoint constants, status values, model configurations, polling interval, and storage keys.
Playground API helpers (tokens & video tasks)
web/default/src/features/playground/api.ts
Adds helpers to list user tokens, fetch unmasked token key, submit video generation with Bearer auth, and poll video task status; uses skip-error overrides for video endpoints.
Video task hook (localStorage + polling)
web/default/src/features/playground/hooks/use-video-task.ts, web/default/src/features/playground/hooks/index.ts
Implements useVideoTask to persist task queue, submit tasks, manage per-task polling with key restoration, and expose submit/clear/remove actions.
Video UI components
web/default/src/features/playground/components/media-drop-zone.tsx, video-input-form.tsx, video-player.tsx, video-task-item.tsx, video-task-queue.tsx
Adds MediaDropZone, VideoInputForm (token/model/prompt/media/advanced), VideoPlayer (preview/copy/fullscreen), VideoTaskItem (status/progress), and VideoTaskQueue (filtering, in-progress/completed lists).
Playground integration with Video tab
web/default/src/features/playground/index.tsx
Conditionally renders a Video tab when video models exist; wires useVideoTask, VideoInputForm, VideoPlayer, and VideoTaskQueue, with toast-based error handling.
i18n locales
web/default/src/i18n/locales/*
Adds translation keys for API key errors, restoration failure, generation/submission failures, media-to-video labels, task statuses, and related strings across multiple locales.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant VideoInputForm
  participant API
  participant useVideoTask
  participant HappyHorseBackend

  User->>VideoInputForm: choose model, prompt, media, token
  VideoInputForm->>API: fetchTokenKey(tokenId)
  API-->>VideoInputForm: unmasked API key
  VideoInputForm->>useVideoTask: submitTask(request, apiKey)
  useVideoTask->>API: submitVideoGeneration(request, apiKey)
  API->>HappyHorseBackend: POST /tasks (Bearer)
  HappyHorseBackend-->>API: { task_id, ... }
  API-->>useVideoTask: VideoTaskResponse
  useVideoTask->>useVideoTask: create task, store apiKey, start polling
  loop polling
    useVideoTask->>API: fetchVideoTaskStatus(taskId, apiKey)
    API->>HappyHorseBackend: GET /tasks/{task_id}
    HappyHorseBackend-->>API: status/progress/(videoUrl)
    API-->>useVideoTask: VideoTaskResponse
    useVideoTask->>useVideoTask: updateTask(status, progress, videoUrl)
  end
  useVideoTask-->>VideoInputForm: task completed (videoUrl available)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

Suggested reviewers

  • Calcium-Ion
  • seefs001
  • creamlike1024

Poem

🐰 A HappyHorse leaps into the code,
Prompt to pixels down the upload road,
Tasks queue, polls, and then they play—
Videos bloom, hip-hip-hooray! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.40% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically describes the main changes: adding HappyHorse channel support with video generation and playground UI.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@tears710 tears710 changed the title feat(channel): add HappyHorse (百炼乘风) DashScope video generation channel + playground UI feat(channel): add HappyHorse (百炼快乐马) DashScope video generation channel + playground UI May 26, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 13

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@controller/model.go`:
- Around line 100-119: The code appends task-adaptor models into openAIModels
and channelId2Models after openAIModelsMap is initialized, so lookups using
openAIModelsMap (e.g., RetrieveModel) will miss these new entries; update the
code that handles task adaptor models (taskAdaptor.Init,
taskAdaptor.GetModelList, channelId2Models population and openAIModels appends)
to also insert or refresh entries in openAIModelsMap (the map created at
initialization) for each modelName (use the same model object shape used when
building openAIModelsMap) so subsequent calls like RetrieveModel see the new
task-adaptor models.

In `@relay/channel/task/happyhorse/adaptor.go`:
- Around line 110-156: Validate duration and resolution locally inside
ValidateRequestAndSetAction: inspect taskReq (returned by
relaycommon.GetTaskRequest) for duration and resolution fields (e.g., in
taskReq.Metadata or explicit fields) and verify they fall within allowed
ranges/formats (e.g., numeric, positive, and within agreed min/max values); if
invalid, return a dto.TaskError with an appropriate Code (e.g.,
"invalid_duration" / "invalid_resolution"), Message and http.StatusBadRequest
instead of forwarding to upstream; ensure the same validation logic covers the
code paths for image/video models (those checked by strings.Contains(model,
...)) so both the video-edit and i2v/r2v branches reject out-of-range values
locally.
- Around line 302-313: EstimateBilling currently reads taskReq.Duration directly
(via relaycommon.GetTaskRequest) and ignores the metadata-based override applied
during request assembly; update TaskAdaptor.EstimateBilling to compute the
effective duration the same way the request assembly does (i.e., apply the
metadata override logic used elsewhere when building the final task request) and
use that effective duration (falling back to the existing default of 5 seconds)
when returning the "seconds" billing map so billing matches the assembled
request.

In
`@web/default/src/features/channels/components/drawers/channel-mutate-drawer.tsx`:
- Around line 417-424: The early-return for empty allModelsList in the useMemo
for basicModels prevents using server-provided channel-type-specific models;
reorder the logic in the useMemo so you first check channelTypeModelsData
(channelTypeMap) and return specific = channelTypeMap[String(currentType)] if
present and non-empty, and only then fall back to return [] when allModelsList
is empty; apply the same reorder/fix to the other similar guard occurrence that
mirrors this pattern (the other useMemo/guard around related-model fill).

In `@web/default/src/features/playground/components/video-input-form.tsx`:
- Around line 202-207: The placeholder currently uses a nested ternary
(isLoadingTokens ? ... : tokens.length === 0 ? ... : ...) which violates the
nested-ternary rule; replace it with a small helper function or an if-else
inside the VideoInputForm component (e.g., add a computePlaceholder or
getPlaceholderText function) that uses isLoadingTokens, tokens (tokens.length),
and t to return the correct string ("Loading...", "No API keys available", or
"Select API key"), then use that helper for the placeholder prop.
- Around line 70-72: selectedModel can stay set to a fallback value that isn't
present in availableModels when models arrive asynchronously; add a useEffect
that watches availableModels and resets selectedModel via setSelectedModel
whenever the current selectedModel is not found in availableModels (set it to
availableModels[0].model if present, otherwise to HAPPYHORSE_MODELS[0].model).
Implement the same pattern for the other similar useState initialized around the
107–112 region so both state values stay in sync with async model availability.
- Around line 157-171: The submit flow allows requests for models with
modelConfig.requiresVideo or modelConfig.requiresImage to proceed when videoUrl
or imageUrls are empty; add explicit client-side validation in the submit
handler and form state so the submit is blocked and the submit button disabled
when required references are missing. Concretely, check
modelConfig.requiresVideo && videoUrl.trim() and modelConfig.requiresImage &&
imageUrls.filter(u => u.trim() !== '').length > 0 before building req (the block
around req.input_reference / req.images) and return/early-fail the submit if
these conditions are not met; also set the submit button's disabled prop (and
any form-level isValid flag) based on the same checks so users cannot click
submit (apply the same validation where the other related code exists around the
image/video handling at the other occurrence referenced by lines 416-418).
Ensure error state messages are set so the UI can show why submit is disabled.

In `@web/default/src/features/playground/components/video-player.tsx`:
- Around line 64-100: The three icon-only Button components (the copy button
using copied/handleCopy with CopyIcon/CheckIcon, the fullscreen button using
handleFullscreen with Maximize2Icon, and the close button using onClose with
XIcon) rely only on title for accessible names; add explicit aria-label
attributes to each Button (e.g., aria-label="Copy URL" on the copy button,
aria-label="Fullscreen" on the fullscreen button, aria-label="Close" on the
close button) so screen readers receive a reliable name while preserving
existing title, size, variant and event handlers.

In `@web/default/src/features/playground/components/video-task-queue.tsx`:
- Around line 145-157: The icon-only clear button (the <Button> with props
title={t('Clear finished tasks')} and onClick calling onClearFinished, rendering
<Trash2Icon>) needs an explicit accessible name: add an aria-label prop (e.g.,
aria-label={t('Clear finished tasks')}) to the Button so screen readers get a
clear name instead of relying on title; keep the existing title if desired but
ensure aria-label is present for accessibility.

In `@web/default/src/features/playground/constants.ts`:
- Around line 110-154: HAPPYHORSE_MODELS currently contains hardcoded
user-facing labels (label fields) and VIDEO_MODEL_TYPE_LABELS maps types to
hardcoded strings; change those label values to i18n keys (e.g.
'playground.model.text_to_video', 'playground.model.label.t2v', etc.) and update
VIDEO_MODEL_TYPE_LABELS to return i18n keys instead of plain text, then ensure
consumers render with the i18n function (e.g. use t(config.label) and
t(VIDEO_MODEL_TYPE_LABELS[type])). Update references to the symbols
HAPPYHORSE_MODELS, VIDEO_MODEL_TYPE_LABELS and any usages that render
config.label to call t(...) so all user-facing strings go through i18n.

In `@web/default/src/features/playground/hooks/use-video-task.ts`:
- Around line 103-106: Replace hardcoded English toast strings with i18next
translations: import { t } from 'i18next' at the top of use-video-task.ts, then
change calls like toast.success('Video generation completed') and
toast.error(errorMsg ?? 'Video generation failed') to use t('...') keys (and
fall back to errorMsg when appropriate), and update the other hardcoded
instances noted (around lines 137-147, 173, 193) to use t(...) as well; ensure
keys are descriptive (e.g., 'video.generationCompleted',
'video.generationFailed') and preserve the conditional use of errorMsg when
present.
- Around line 81-116: The poll function can start overlapping
fetchVideoTaskStatus calls; add an in-flight guard (e.g., a per-id boolean like
pollingInFlight.current[id]) to skip starting a new poll while a previous one is
running in poll inside use-video-task.ts: before calling fetchVideoTaskStatus
check and set the guard, and clear it in finally (or after stopPolling) so no
concurrent requests run; ensure stopPolling clears both
pollingTimers.current[id] and the in-flight flag and keep existing updateTask,
toast, and stopPolling logic unchanged.

In `@web/default/src/features/playground/index.tsx`:
- Around line 279-283: The previewTask state can point to a task that gets
deleted or cleared, leaving VideoPlayer showing stale content; update the
task-deletion and "clear finished" handlers (the functions that remove tasks and
the handler that clears finished tasks) to call setPreviewTask(null) when the
affected task matches previewTask (e.g., compare IDs) or when clearing finished
tasks remove the currently previewed task; ensure this logic runs inside the
existing delete/clear handlers so previewTask is unset and VideoPlayer is closed
when its task is removed.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 87780884-7436-4b77-957e-f8f5991b4b4b

📥 Commits

Reviewing files that changed from the base of the PR and between 74985fa and 8ff103e.

📒 Files selected for processing (21)
  • constant/channel.go
  • controller/model.go
  • relay/channel/task/happyhorse/adaptor.go
  • relay/channel/task/happyhorse/adaptor_test.go
  • relay/channel/task/happyhorse/constants.go
  • relay/relay_adaptor.go
  • setting/ratio_setting/model_ratio.go
  • web/default/src/features/channels/api.ts
  • web/default/src/features/channels/components/drawers/channel-mutate-drawer.tsx
  • web/default/src/features/channels/constants.ts
  • web/default/src/features/channels/lib/channel-utils.ts
  • web/default/src/features/playground/api.ts
  • web/default/src/features/playground/components/video-input-form.tsx
  • web/default/src/features/playground/components/video-player.tsx
  • web/default/src/features/playground/components/video-task-item.tsx
  • web/default/src/features/playground/components/video-task-queue.tsx
  • web/default/src/features/playground/constants.ts
  • web/default/src/features/playground/hooks/index.ts
  • web/default/src/features/playground/hooks/use-video-task.ts
  • web/default/src/features/playground/index.tsx
  • web/default/src/features/playground/types.ts

Comment thread controller/model.go
Comment thread relay/channel/task/happyhorse/adaptor.go
Comment thread relay/channel/task/happyhorse/adaptor.go
Comment thread web/default/src/features/playground/components/video-input-form.tsx
Comment thread web/default/src/features/playground/components/video-task-queue.tsx
Comment on lines +110 to +154
export const HAPPYHORSE_MODELS: VideoModelConfig[] = [
{
model: 'happyhorse-1.0-t2v',
label: '文生视频 (Text-to-Video)',
type: 'text-to-video',
requiresImage: false,
requiresVideo: false,
supportedSizes: ['720P', '1080P'],
durationRange: [2, 15],
},
{
model: 'happyhorse-1.0-i2v',
label: '图生视频 (Image-to-Video)',
type: 'image-to-video',
requiresImage: true,
requiresVideo: false,
supportedSizes: ['720P', '1080P'],
durationRange: [2, 15],
},
{
model: 'happyhorse-1.0-r2v',
label: '首尾帧生视频 (Reference-to-Video)',
type: 'reference-to-video',
requiresImage: true,
requiresVideo: false,
supportedSizes: ['720P', '1080P'],
durationRange: [2, 15],
},
{
model: 'happyhorse-1.0-video-edit',
label: '视频编辑 (Video Edit)',
type: 'video-edit',
requiresImage: false,
requiresVideo: true,
supportedSizes: ['720P', '1080P'],
durationRange: [2, 15],
},
]

export const VIDEO_MODEL_TYPE_LABELS: Record<VideoModelType, string> = {
'text-to-video': 'T2V',
'image-to-video': 'I2V',
'reference-to-video': 'R2V',
'video-edit': 'Edit',
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

New video labels are hardcoded instead of i18n keys.

Lines 113, 122, 131, 140 and Lines 150-153 introduce user-facing text directly in constants. This bypasses translation flow and makes localization inconsistent.

Suggested direction
 export const HAPPYHORSE_MODELS: VideoModelConfig[] = [
   {
     model: 'happyhorse-1.0-t2v',
-    label: '文生视频 (Text-to-Video)',
+    label: 'playground.video.model.text_to_video',
...
 export const VIDEO_MODEL_TYPE_LABELS: Record<VideoModelType, string> = {
-  'text-to-video': 'T2V',
+  'text-to-video': 'playground.video.type.t2v',

Then render with t(config.label) / t(VIDEO_MODEL_TYPE_LABELS[type]).

As per coding guidelines, “All user-facing text content must support i18n using the t() function” and in constants.ts labels should be i18n keys rendered with t(...).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/default/src/features/playground/constants.ts` around lines 110 - 154,
HAPPYHORSE_MODELS currently contains hardcoded user-facing labels (label fields)
and VIDEO_MODEL_TYPE_LABELS maps types to hardcoded strings; change those label
values to i18n keys (e.g. 'playground.model.text_to_video',
'playground.model.label.t2v', etc.) and update VIDEO_MODEL_TYPE_LABELS to return
i18n keys instead of plain text, then ensure consumers render with the i18n
function (e.g. use t(config.label) and t(VIDEO_MODEL_TYPE_LABELS[type])). Update
references to the symbols HAPPYHORSE_MODELS, VIDEO_MODEL_TYPE_LABELS and any
usages that render config.label to call t(...) so all user-facing strings go
through i18n.

Comment thread web/default/src/features/playground/hooks/use-video-task.ts
Comment thread web/default/src/features/playground/hooks/use-video-task.ts Outdated
Comment thread web/default/src/features/playground/index.tsx
- Move openAIModelsMap rebuild after UniqBy so task-adaptor models are findable
- Add duration (2-15s) and resolution (720P/1080P) local validation in adaptor
- Add metadata duration validation to prevent billing override abuse
- Normalize resolution value back to taskReq.Size after validation
- Align EstimateBilling with metadata override precedence
- Fix basicModels useMemo: check channelTypeModelsData before allModelsList guard
- Sync selectedModel with async availableModels via useEffect
- Add isSubmitting guard to handleSubmit to prevent double submission
- Add hasRequiredMedia client-side validation before submit
- Add aria-label to all icon-only Buttons (video-player, video-task-queue)
- Fix stale previewTask when task is removed or cleared
- Add in-flight guard to polling loop to prevent concurrent requests
- Convert all hardcoded toast strings to i18n t() calls
- Convert HAPPYHORSE_MODELS labels to i18n keys
- Add media-drop-zone.tsx component (drag/paste/URL upload)
- Fix URL.createObjectURL memory leak with prevUrlRef + cleanup
- Add 15 new i18n keys to all 6 locale files (en/zh/fr/ja/ru/vi)

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
web/default/src/features/playground/components/video-player.tsx (1)

77-80: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add aria-hidden='true' to decorative icons inside labeled buttons.

The button icons are decorative because each button already has an aria-label. Screen readers should skip these icons to avoid redundant announcements.

Add aria-hidden to all button icons
           {copied ? (
-            <CheckIcon className='size-3.5' />
+            <CheckIcon className='size-3.5' aria-hidden='true' />
           ) : (
-            <CopyIcon className='size-3.5' />
+            <CopyIcon className='size-3.5' aria-hidden='true' />
           )}

Apply the same to Maximize2Icon (line 91) and XIcon (line 102):

-          <Maximize2Icon className='size-3.5' />
+          <Maximize2Icon className='size-3.5' aria-hidden='true' />
-          <XIcon className='size-3.5' />
+          <XIcon className='size-3.5' aria-hidden='true' />

As per coding guidelines: "add aria-hidden='true' to decorative icons."

Also applies to: 91-91, 102-102

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/default/src/features/playground/components/video-player.tsx` around lines
77 - 80, The button icons are decorative and should be hidden from screen
readers; update the JSX for the icon components used inside labeled
buttons—CheckIcon, CopyIcon, Maximize2Icon, and XIcon—to include
aria-hidden='true' (e.g., <CheckIcon aria-hidden='true' />) so screen readers do
not redundantly announce them while preserving the existing aria-label on the
buttons.
🧹 Nitpick comments (2)
web/default/src/features/playground/components/media-drop-zone.tsx (1)

44-54: ⚡ Quick win

Consider validating file type matches the accept prop.

When users drop or paste files, the component does not verify that the file's MIME type matches the accept prop ('image' or 'video'). A user could drop a video file when accept='image' is set, leading to unexpected behavior or errors downstream.

Add file type validation
   const handleFile = useCallback(
     (file: File) => {
+      const expectedType = isImage ? 'image' : 'video'
+      if (!file.type.startsWith(`${expectedType}/`)) {
+        return // or show an error message
+      }
       if (prevUrlRef.current?.startsWith('blob:')) {
         URL.revokeObjectURL(prevUrlRef.current)
       }

Also applies to: 65-73, 75-96

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/default/src/features/playground/components/media-drop-zone.tsx` around
lines 44 - 54, The file handlers (e.g., handleFile) currently create object URLs
for any dropped/pasted File without checking the component's accept prop; add
MIME validation so the File's type matches accept ('image' =>
type.startsWith('image/'), 'video' => type.startsWith('video/')) before creating
the URL and calling onChange. If the file does not match, revoke any created
URL, do not call onChange, and surface the rejection (e.g., call an
onError/onInvalid callback or return early) in the handlers that process user
input (handleFile and the corresponding drop/paste handlers) so only accepted
file types are accepted.
web/default/src/i18n/locales/en.json (1)

2013-2013: ⚡ Quick win

Avoid introducing parallel key variants for the same concept.

Line 2013 (Image-to-Video), Line 3222 (Reference-to-Video), and Line 3906 (Text-to-Video) introduce hyphenated labels while non-hyphen variants already exist, which increases translation drift and lookup inconsistency. Please standardize on one canonical key pattern for each label.

As per coding guidelines: “Use hierarchical and semantically clear translation key names such as dashboard.overview.title and maintain naming consistency.”

Also applies to: 3222-3222, 3906-3906

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/default/src/i18n/locales/en.json` at line 2013, The JSON introduces
hyphenated duplicate keys ("Image-to-Video", "Reference-to-Video",
"Text-to-Video"); remove these hyphenated entries and consolidate to the
existing canonical keys (the non-hyphen variants already present) so there is a
single translation key per concept, then update any code or lookup references
that use "Image-to-Video", "Reference-to-Video", or "Text-to-Video" to use the
canonical non-hyphen keys and run your i18n validation/extraction to ensure no
missing keys remain.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@relay/channel/task/happyhorse/adaptor.go`:
- Around line 129-166: The metadata resolution (HappyHorseMetadata.Resolution)
is not validated, allowing overrides to bypass local checks in
convertToHappyHorseRequest; after unmarshaling metadata with
taskcommon.UnmarshalMetadata(&meta) in the validation block, normalize
meta.Resolution (strings.TrimSpace + strings.ToUpper, append "P" if missing) and
ensure it equals "720P" or "1080P"; if invalid, return a dto.TaskError with Code
"invalid_resolution" and the same StatusCode/LocalError semantics used for
taskReq.Size, and update the metadata/resolution in taskReq or meta so
downstream sees the normalized value.

In `@web/default/src/features/playground/components/media-drop-zone.tsx`:
- Line 164: The decorative icons XIcon, ImageIcon, and VideoIcon are missing
aria-hidden and should be ignored by screen readers; update the JSX where XIcon,
ImageIcon, and VideoIcon are rendered in media-drop-zone.tsx to include
aria-hidden="true" on each icon element (ensure the attribute is added to the
icon props/element so screen readers skip these decorative icons while leaving
surrounding button/label text unchanged).
- Line 180: Remove the unsafe "as any" assertion on the ref usage (ref={inputRef
as any}) and pass the properly typed ref to the Input component (use
ref={inputRef}); if TypeScript reports an incompatibility, update the Input
component's ref prop type to accept React.RefObject<HTMLInputElement> (or
appropriate element) or switch to a callback ref that forwards the inputRef
value, ensuring inputRef remains typed as React.RefObject<HTMLInputElement>.
- Around line 126-140: The drop zone is focusable (role='button', tabIndex={0})
but missing keyboard handlers; add an onKeyDown on the same element to listen
for Enter and Space and call inputRef.current?.click() to open the file picker
(for Space also preventDefault to avoid page scroll), and keep existing
onClick/onDrop behavior; use the existing identifiers inputRef, setIsDragging,
handleDrop and value when locating the element to update.

In `@web/default/src/i18n/locales/fr.json`:
- Around line 3867-3868: The French translations for the keys "Task failed: API
Key invalid" and "Task failed: cannot restore key" include an undeclared
interpolation `{{prompt}}...` which can render unresolved placeholders; update
the values for those keys in fr.json to remove the `{{prompt}}...` fragment so
they match the source keys (e.g., change "Tâche \"{{prompt}}...\" échouée : clé
API invalide" and "Tâche \"{{prompt}}...\" échouée : impossible de restaurer la
clé" to versions without `{{prompt}}...`), ensuring no undeclared placeholders
remain.

In `@web/default/src/i18n/locales/ru.json`:
- Around line 3867-3868: The translations for the keys "Task failed: API Key
invalid" and "Task failed: cannot restore key" include an unresolved
interpolation placeholder `{{prompt}}...`; remove that placeholder and replace
the values with plain static Russian messages (e.g., 'Задача не выполнена:
недействительный ключ API' and 'Задача не выполнена: не удалось восстановить
ключ') so no raw interpolation tokens remain in the ru.json entries for those
keys.

In `@web/default/src/i18n/locales/vi.json`:
- Around line 3867-3868: Remove the unmatched interpolation token from the
Vietnamese translations for the keys "Task failed: API Key invalid" and "Task
failed: cannot restore key" in vi.json: replace the values that include
'{{prompt}}...' with plain messages that match the source (no interpolation
placeholders) so the locale stays parity with other languages and no raw
placeholders render in the UI.

---

Outside diff comments:
In `@web/default/src/features/playground/components/video-player.tsx`:
- Around line 77-80: The button icons are decorative and should be hidden from
screen readers; update the JSX for the icon components used inside labeled
buttons—CheckIcon, CopyIcon, Maximize2Icon, and XIcon—to include
aria-hidden='true' (e.g., <CheckIcon aria-hidden='true' />) so screen readers do
not redundantly announce them while preserving the existing aria-label on the
buttons.

---

Nitpick comments:
In `@web/default/src/features/playground/components/media-drop-zone.tsx`:
- Around line 44-54: The file handlers (e.g., handleFile) currently create
object URLs for any dropped/pasted File without checking the component's accept
prop; add MIME validation so the File's type matches accept ('image' =>
type.startsWith('image/'), 'video' => type.startsWith('video/')) before creating
the URL and calling onChange. If the file does not match, revoke any created
URL, do not call onChange, and surface the rejection (e.g., call an
onError/onInvalid callback or return early) in the handlers that process user
input (handleFile and the corresponding drop/paste handlers) so only accepted
file types are accepted.

In `@web/default/src/i18n/locales/en.json`:
- Line 2013: The JSON introduces hyphenated duplicate keys ("Image-to-Video",
"Reference-to-Video", "Text-to-Video"); remove these hyphenated entries and
consolidate to the existing canonical keys (the non-hyphen variants already
present) so there is a single translation key per concept, then update any code
or lookup references that use "Image-to-Video", "Reference-to-Video", or
"Text-to-Video" to use the canonical non-hyphen keys and run your i18n
validation/extraction to ensure no missing keys remain.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a490422d-73bf-4ca6-a004-3f66eda260d2

📥 Commits

Reviewing files that changed from the base of the PR and between 8ff103e and 091d9b3.

⛔ Files ignored due to path filters (1)
  • web/default/bun.lock is excluded by !**/*.lock
📒 Files selected for processing (16)
  • controller/model.go
  • relay/channel/task/happyhorse/adaptor.go
  • web/default/src/features/channels/components/drawers/channel-mutate-drawer.tsx
  • web/default/src/features/playground/components/media-drop-zone.tsx
  • web/default/src/features/playground/components/video-input-form.tsx
  • web/default/src/features/playground/components/video-player.tsx
  • web/default/src/features/playground/components/video-task-queue.tsx
  • web/default/src/features/playground/constants.ts
  • web/default/src/features/playground/hooks/use-video-task.ts
  • web/default/src/features/playground/index.tsx
  • web/default/src/i18n/locales/en.json
  • web/default/src/i18n/locales/fr.json
  • web/default/src/i18n/locales/ja.json
  • web/default/src/i18n/locales/ru.json
  • web/default/src/i18n/locales/vi.json
  • web/default/src/i18n/locales/zh.json
✅ Files skipped from review due to trivial changes (1)
  • web/default/src/i18n/locales/ja.json
🚧 Files skipped from review as they are similar to previous changes (7)
  • web/default/src/features/playground/constants.ts
  • web/default/src/features/channels/components/drawers/channel-mutate-drawer.tsx
  • controller/model.go
  • web/default/src/features/playground/index.tsx
  • web/default/src/features/playground/components/video-input-form.tsx
  • web/default/src/features/playground/hooks/use-video-task.ts
  • web/default/src/features/playground/components/video-task-queue.tsx

Comment on lines +129 to +166
// Validate duration and resolution locally before forwarding to upstream
if taskReq.Duration > 0 && (taskReq.Duration < 2 || taskReq.Duration > 15) {
return &dto.TaskError{
Code: "invalid_duration",
Message: "duration must be between 2 and 15 seconds",
StatusCode: http.StatusBadRequest,
LocalError: true,
}
}
if taskReq.Size != "" {
resolution := strings.ToUpper(strings.TrimSpace(taskReq.Size))
if !strings.HasSuffix(resolution, "P") {
resolution += "P"
}
if resolution != "720P" && resolution != "1080P" {
return &dto.TaskError{
Code: "invalid_resolution",
Message: "resolution must be 720P or 1080P",
StatusCode: http.StatusBadRequest,
LocalError: true,
}
}
taskReq.Size = resolution // normalize for downstream consumers
}
// Validate metadata duration (same override path as EstimateBilling / request assembly)
if taskReq.Metadata != nil {
var meta HappyHorseMetadata
if err := taskcommon.UnmarshalMetadata(taskReq.Metadata, &meta); err == nil {
if meta.Duration != nil && *meta.Duration > 0 && (*meta.Duration < 2 || *meta.Duration > 15) {
return &dto.TaskError{
Code: "invalid_duration",
Message: "metadata duration must be between 2 and 15 seconds",
StatusCode: http.StatusBadRequest,
LocalError: true,
}
}
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Missing validation for meta.Resolution in metadata.

The validation logic validates taskReq.Size (lines 138-152) and meta.Duration (lines 157-164), but meta.Resolution is not validated here. However, convertToHappyHorseRequest (lines 317-323) applies meta.Resolution as an override. This allows invalid resolutions like "480P" to bypass local validation when passed via metadata.

For consistency with the stated goal of local validation before forwarding to upstream:

🛡️ Proposed fix to validate metadata resolution
 	// Validate metadata duration (same override path as EstimateBilling / request assembly)
 	if taskReq.Metadata != nil {
 		var meta HappyHorseMetadata
 		if err := taskcommon.UnmarshalMetadata(taskReq.Metadata, &meta); err == nil {
+			if meta.Resolution != nil {
+				resolution := strings.ToUpper(strings.TrimSpace(*meta.Resolution))
+				if !strings.HasSuffix(resolution, "P") {
+					resolution += "P"
+				}
+				if resolution != "720P" && resolution != "1080P" {
+					return &dto.TaskError{
+						Code:       "invalid_resolution",
+						Message:    "metadata resolution must be 720P or 1080P",
+						StatusCode: http.StatusBadRequest,
+						LocalError: true,
+					}
+				}
+			}
 			if meta.Duration != nil && *meta.Duration > 0 && (*meta.Duration < 2 || *meta.Duration > 15) {
 				return &dto.TaskError{
 					Code:       "invalid_duration",
 					Message:    "metadata duration must be between 2 and 15 seconds",
 					StatusCode: http.StatusBadRequest,
 					LocalError: true,
 				}
 			}
 		}
 	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Validate duration and resolution locally before forwarding to upstream
if taskReq.Duration > 0 && (taskReq.Duration < 2 || taskReq.Duration > 15) {
return &dto.TaskError{
Code: "invalid_duration",
Message: "duration must be between 2 and 15 seconds",
StatusCode: http.StatusBadRequest,
LocalError: true,
}
}
if taskReq.Size != "" {
resolution := strings.ToUpper(strings.TrimSpace(taskReq.Size))
if !strings.HasSuffix(resolution, "P") {
resolution += "P"
}
if resolution != "720P" && resolution != "1080P" {
return &dto.TaskError{
Code: "invalid_resolution",
Message: "resolution must be 720P or 1080P",
StatusCode: http.StatusBadRequest,
LocalError: true,
}
}
taskReq.Size = resolution // normalize for downstream consumers
}
// Validate metadata duration (same override path as EstimateBilling / request assembly)
if taskReq.Metadata != nil {
var meta HappyHorseMetadata
if err := taskcommon.UnmarshalMetadata(taskReq.Metadata, &meta); err == nil {
if meta.Duration != nil && *meta.Duration > 0 && (*meta.Duration < 2 || *meta.Duration > 15) {
return &dto.TaskError{
Code: "invalid_duration",
Message: "metadata duration must be between 2 and 15 seconds",
StatusCode: http.StatusBadRequest,
LocalError: true,
}
}
}
}
// Validate duration and resolution locally before forwarding to upstream
if taskReq.Duration > 0 && (taskReq.Duration < 2 || taskReq.Duration > 15) {
return &dto.TaskError{
Code: "invalid_duration",
Message: "duration must be between 2 and 15 seconds",
StatusCode: http.StatusBadRequest,
LocalError: true,
}
}
if taskReq.Size != "" {
resolution := strings.ToUpper(strings.TrimSpace(taskReq.Size))
if !strings.HasSuffix(resolution, "P") {
resolution += "P"
}
if resolution != "720P" && resolution != "1080P" {
return &dto.TaskError{
Code: "invalid_resolution",
Message: "resolution must be 720P or 1080P",
StatusCode: http.StatusBadRequest,
LocalError: true,
}
}
taskReq.Size = resolution // normalize for downstream consumers
}
// Validate metadata duration (same override path as EstimateBilling / request assembly)
if taskReq.Metadata != nil {
var meta HappyHorseMetadata
if err := taskcommon.UnmarshalMetadata(taskReq.Metadata, &meta); err == nil {
if meta.Resolution != nil {
resolution := strings.ToUpper(strings.TrimSpace(*meta.Resolution))
if !strings.HasSuffix(resolution, "P") {
resolution += "P"
}
if resolution != "720P" && resolution != "1080P" {
return &dto.TaskError{
Code: "invalid_resolution",
Message: "metadata resolution must be 720P or 1080P",
StatusCode: http.StatusBadRequest,
LocalError: true,
}
}
}
if meta.Duration != nil && *meta.Duration > 0 && (*meta.Duration < 2 || *meta.Duration > 15) {
return &dto.TaskError{
Code: "invalid_duration",
Message: "metadata duration must be between 2 and 15 seconds",
StatusCode: http.StatusBadRequest,
LocalError: true,
}
}
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@relay/channel/task/happyhorse/adaptor.go` around lines 129 - 166, The
metadata resolution (HappyHorseMetadata.Resolution) is not validated, allowing
overrides to bypass local checks in convertToHappyHorseRequest; after
unmarshaling metadata with taskcommon.UnmarshalMetadata(&meta) in the validation
block, normalize meta.Resolution (strings.TrimSpace + strings.ToUpper, append
"P" if missing) and ensure it equals "720P" or "1080P"; if invalid, return a
dto.TaskError with Code "invalid_resolution" and the same StatusCode/LocalError
semantics used for taskReq.Size, and update the metadata/resolution in taskReq
or meta so downstream sees the normalized value.

Comment on lines +126 to +140
role='button'
tabIndex={0}
onClick={() => inputRef.current?.click()}
onDragOver={(e) => {
e.preventDefault()
setIsDragging(true)
}}
onDragLeave={() => setIsDragging(false)}
onDrop={handleDrop}
className={cn(
'border-muted flex cursor-pointer flex-col items-center justify-center gap-1.5 rounded-md border border-dashed p-4 transition-colors',
isDragging && 'border-primary bg-primary/5',
!value && 'hover:bg-muted/50',
)}
>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add keyboard event handlers for the drop zone.

The drop zone uses role='button' and tabIndex={0} to make it focusable, but it lacks keyboard event handlers. Users navigating via keyboard cannot activate the file picker with Enter or Space keys.

Add onKeyDown handler
       <div
         role='button'
         tabIndex={0}
         onClick={() => inputRef.current?.click()}
+        onKeyDown={(e) => {
+          if (e.key === 'Enter' || e.key === ' ') {
+            e.preventDefault()
+            inputRef.current?.click()
+          }
+        }}
         onDragOver={(e) => {

As per coding guidelines: "Ensure keyboard operability and logical focus order; use ARIA attributes when necessary."

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
role='button'
tabIndex={0}
onClick={() => inputRef.current?.click()}
onDragOver={(e) => {
e.preventDefault()
setIsDragging(true)
}}
onDragLeave={() => setIsDragging(false)}
onDrop={handleDrop}
className={cn(
'border-muted flex cursor-pointer flex-col items-center justify-center gap-1.5 rounded-md border border-dashed p-4 transition-colors',
isDragging && 'border-primary bg-primary/5',
!value && 'hover:bg-muted/50',
)}
>
role='button'
tabIndex={0}
onClick={() => inputRef.current?.click()}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault()
inputRef.current?.click()
}
}}
onDragOver={(e) => {
e.preventDefault()
setIsDragging(true)
}}
onDragLeave={() => setIsDragging(false)}
onDrop={handleDrop}
className={cn(
'border-muted flex cursor-pointer flex-col items-center justify-center gap-1.5 rounded-md border border-dashed p-4 transition-colors',
isDragging && 'border-primary bg-primary/5',
!value && 'hover:bg-muted/50',
)}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/default/src/features/playground/components/media-drop-zone.tsx` around
lines 126 - 140, The drop zone is focusable (role='button', tabIndex={0}) but
missing keyboard handlers; add an onKeyDown on the same element to listen for
Enter and Space and call inputRef.current?.click() to open the file picker (for
Space also preventDefault to avoid page scroll), and keep existing
onClick/onDrop behavior; use the existing identifiers inputRef, setIsDragging,
handleDrop and value when locating the element to update.

onClick={clearValue}
className='bg-destructive text-destructive-foreground absolute top-1 right-1 flex size-5 items-center justify-center rounded-full text-xs'
>
<XIcon className='size-3' />

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add aria-hidden='true' to decorative icons.

The icons (XIcon, ImageIcon, VideoIcon) are decorative because their context is already conveyed by surrounding text or button labels. Screen readers should skip them.

Add aria-hidden to icons
             <button
               type='button'
               onClick={clearValue}
               className='bg-destructive text-destructive-foreground absolute top-1 right-1 flex size-5 items-center justify-center rounded-full text-xs'
             >
-              <XIcon className='size-3' />
+              <XIcon className='size-3' aria-hidden='true' />
             </button>

Apply the same change to ImageIcon (line 170) and VideoIcon (line 172):

             {isImage ? (
-              <ImageIcon className='text-muted-foreground size-6' />
+              <ImageIcon className='text-muted-foreground size-6' aria-hidden='true' />
             ) : (
-              <VideoIcon className='text-muted-foreground size-6' />
+              <VideoIcon className='text-muted-foreground size-6' aria-hidden='true' />
             )}

As per coding guidelines: "add aria-hidden='true' to decorative icons."

Also applies to: 170-170, 172-172

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/default/src/features/playground/components/media-drop-zone.tsx` at line
164, The decorative icons XIcon, ImageIcon, and VideoIcon are missing
aria-hidden and should be ignored by screen readers; update the JSX where XIcon,
ImageIcon, and VideoIcon are rendered in media-drop-zone.tsx to include
aria-hidden="true" on each icon element (ensure the attribute is added to the
icon props/element so screen readers skip these decorative icons while leaving
surrounding button/label text unchanged).

</div>

<Input
ref={inputRef as any}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Avoid using any type for ref.

The as any type assertion bypasses TypeScript's type checking. The Input component likely expects a compatible ref type for HTMLInputElement, which inputRef already provides.

Fix by removing the type assertion
       <Input
-        ref={inputRef as any}
+        ref={inputRef}
         type='file'
         accept={acceptAttr}

If TypeScript reports an incompatibility, explicitly type the Input component's ref prop or use a callback ref instead.

As per coding guidelines: "Avoid any type in TypeScript; prefer specific types or unknown; explicitly annotate parameter and return value types."

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
ref={inputRef as any}
<Input
ref={inputRef}
type='file'
accept={acceptAttr}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/default/src/features/playground/components/media-drop-zone.tsx` at line
180, Remove the unsafe "as any" assertion on the ref usage (ref={inputRef as
any}) and pass the properly typed ref to the Input component (use
ref={inputRef}); if TypeScript reports an incompatibility, update the Input
component's ref prop type to accept React.RefObject<HTMLInputElement> (or
appropriate element) or switch to a callback ref that forwards the inputRef
value, ensuring inputRef remains typed as React.RefObject<HTMLInputElement>.

Comment on lines +3867 to +3868
"Task failed: API Key invalid": "Tâche \"{{prompt}}...\" échouée : clé API invalide",
"Task failed: cannot restore key": "Tâche \"{{prompt}}...\" échouée : impossible de restaurer la clé",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Remove undeclared {{prompt}} interpolation from task-failure translations.

Line 3867 and Line 3868 add {{prompt}}... in FR values even though the source keys don’t contain that variable. This can surface unresolved placeholders in UI when prompt isn’t provided.

💡 Suggested fix
-    "Task failed: API Key invalid": "Tâche \"{{prompt}}...\" échouée : clé API invalide",
-    "Task failed: cannot restore key": "Tâche \"{{prompt}}...\" échouée : impossible de restaurer la clé",
+    "Task failed: API Key invalid": "Échec de la tâche : clé API invalide",
+    "Task failed: cannot restore key": "Échec de la tâche : impossible de restaurer la clé",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"Task failed: API Key invalid": "Tâche \"{{prompt}}...\" échouée : clé API invalide",
"Task failed: cannot restore key": "Tâche \"{{prompt}}...\" échouée : impossible de restaurer la clé",
"Task failed: API Key invalid": "Échec de la tâche : clé API invalide",
"Task failed: cannot restore key": "Échec de la tâche : impossible de restaurer la clé",
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/default/src/i18n/locales/fr.json` around lines 3867 - 3868, The French
translations for the keys "Task failed: API Key invalid" and "Task failed:
cannot restore key" include an undeclared interpolation `{{prompt}}...` which
can render unresolved placeholders; update the values for those keys in fr.json
to remove the `{{prompt}}...` fragment so they match the source keys (e.g.,
change "Tâche \"{{prompt}}...\" échouée : clé API invalide" and "Tâche
\"{{prompt}}...\" échouée : impossible de restaurer la clé" to versions without
`{{prompt}}...`), ensuring no undeclared placeholders remain.

Comment on lines +3867 to +3868
"Task failed: API Key invalid": "Задача \"{{prompt}}...\" не выполнена: недействительный ключ API",
"Task failed: cannot restore key": "Задача \"{{prompt}}...\" не выполнена: не удалось восстановить ключ",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Remove unresolved interpolation placeholders from task-failure messages.

These translations inject {{prompt}}..., but the key patterns here don’t indicate interpolation input. This can render raw placeholders to users.

💡 Suggested fix
-    "Task failed: API Key invalid": "Задача \"{{prompt}}...\" не выполнена: недействительный ключ API",
-    "Task failed: cannot restore key": "Задача \"{{prompt}}...\" не выполнена: не удалось восстановить ключ",
+    "Task failed: API Key invalid": "Задача не выполнена: недействительный ключ API",
+    "Task failed: cannot restore key": "Задача не выполнена: не удалось восстановить ключ",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"Task failed: API Key invalid": "Задача \"{{prompt}}...\" не выполнена: недействительный ключ API",
"Task failed: cannot restore key": "Задача \"{{prompt}}...\" не выполнена: не удалось восстановить ключ",
"Task failed: API Key invalid": "Задача не выполнена: недействительный ключ API",
"Task failed: cannot restore key": "Задача не выполнена: не удалось восстановить ключ",
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/default/src/i18n/locales/ru.json` around lines 3867 - 3868, The
translations for the keys "Task failed: API Key invalid" and "Task failed:
cannot restore key" include an unresolved interpolation placeholder
`{{prompt}}...`; remove that placeholder and replace the values with plain
static Russian messages (e.g., 'Задача не выполнена: недействительный ключ API'
and 'Задача не выполнена: не удалось восстановить ключ') so no raw interpolation
tokens remain in the ru.json entries for those keys.

Comment on lines +3867 to +3868
"Task failed: API Key invalid": "Nhiệm vụ \"{{prompt}}...\" thất bại: khóa API không hợp lệ",
"Task failed: cannot restore key": "Nhiệm vụ \"{{prompt}}...\" thất bại: không thể khôi phục khóa",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Remove unmatched interpolation placeholder from task-failure translations.

These Vietnamese values introduce {{prompt}}..., but the source keys have no interpolation token. This can render raw placeholders in UI and break message parity across locales.

💡 Suggested fix
-    "Task failed: API Key invalid": "Nhiệm vụ \"{{prompt}}...\" thất bại: khóa API không hợp lệ",
-    "Task failed: cannot restore key": "Nhiệm vụ \"{{prompt}}...\" thất bại: không thể khôi phục khóa",
+    "Task failed: API Key invalid": "Nhiệm vụ thất bại: khóa API không hợp lệ",
+    "Task failed: cannot restore key": "Nhiệm vụ thất bại: không thể khôi phục khóa",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"Task failed: API Key invalid": "Nhiệm vụ \"{{prompt}}...\" thất bại: khóa API không hợp lệ",
"Task failed: cannot restore key": "Nhiệm vụ \"{{prompt}}...\" thất bại: không thể khôi phục khóa",
"Task failed: API Key invalid": "Nhiệm vụ thất bại: khóa API không hợp lệ",
"Task failed: cannot restore key": "Nhiệm vụ thất bại: không thể khôi phục khóa",
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/default/src/i18n/locales/vi.json` around lines 3867 - 3868, Remove the
unmatched interpolation token from the Vietnamese translations for the keys
"Task failed: API Key invalid" and "Task failed: cannot restore key" in vi.json:
replace the values that include '{{prompt}}...' with plain messages that match
the source (no interpolation placeholders) so the locale stays parity with other
languages and no raw placeholders render in the UI.

aaaajl added a commit to aaaajl/WinGrid-api that referenced this pull request Jul 15, 2026
…yground UI

Assign HappyHorse channel type 59 so it does not collide with Advanced Custom (58).

Co-authored-by: Cursor <cursoragent@cursor.com>
JacksonsY added a commit to JacksonsY/new-api that referenced this pull request Jul 19, 2026
移植上游 PR QuantumNous#5124 的 playground 视频 UI 部分。

只有存在 happyhorse-* 模型时才出现 Chat/Video 分页,否则原样早返回
单栏聊天布局,现有 PlaygroundChat/PlaygroundInput 的 props 一字未改。
左栏为参数表单(模型类型、分辨率、时长、媒体输入、高级设置),右栏为
任务队列与播放器,提交后 5 秒轮询、任务列表持久化到 localStorage。

未采纳该 PR 的独立渠道方案:它的上游地址与既有 ali 渠道完全相同
(DashScope video-synthesis),happyhorse 已由 QuantumNous#4810 作为 ali 渠道下的
模型接入,再开一个渠道号只是重复——渠道号不可回收。前端改用模型名
前缀 happyhorse- 识别,同时覆盖仓库里的 1.0 与 1.1 两个系列。

在上游实现上修正三处:

1. 拖拽上传实际不可用
   原实现对拖入文件调 URL.createObjectURL 生成 blob: URL 直接当
   input_reference 提交。blob: 是仅在本标签页有效的句柄,上游服务器
   取不到,而本地预览却能正常显示,用户要到任务失败才发现。改为只接受
   http(s) URL(text/uri-list 或 text/plain)。真正的文件上传需要后端
   对象存储接口,不在本次范围。

2. 明文 API Key 的多余留存
   删除了写入后从未被读取的 taskApiKeys 引用与 TokenOption.key 字段。
   密钥现仅在提交前按需拉取,存活于轮询闭包内。仍存在的残留面:未完成
   任务会在每次进入 playground 时重新拉取明文密钥,根治需要后端会话
   鉴权代理,已记录为后续项。

3. localStorage 无上限
   任务历史无限追加,最终撑爆配额后静默吞掉 QuotaExceededError。现限制
   50 条,写入与读取两侧都截断;并为 loadTasksFromStorage 补 Array
   守卫——原实现遇到非数组值会整页崩溃。

补齐 32 个 i18n key × 7 种语言(上游 patch 只登记了其代码实际调用的
一部分,其余回退显示英文原串,且完全没有 zh-TW)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
JacksonsY added a commit to JacksonsY/new-api that referenced this pull request Jul 19, 2026
上一批移植(QuantumNous#4810 + QuantumNous#5124)引入的三处缺陷,合起来使 happyhorse 图生视频
端到端不可用。三处单看都不像问题,是组合出来的。

1. input_reference 被静默丢弃
   QuantumNous#4810 把 ali 的校验入口从 ValidateMultipartDirect 换成
   ValidateBasicTaskRequest,而后者没有 InputReference→Images 的归一化。
   旧模型与 wan2.7-i2v 因为走 firstTaskImage 兜底而幸免,happyhorse 全系
   与 wan2.7-r2v/videoedit 的 isNewFormatModel 分支只读 req.Images,于是
   收不到任何媒体——而 QuantumNous#5124 的新前端发的正是 input_reference。
   归一化补在通用校验器里:input_reference 是 OpenAI 视频接口的标准字段,
   其余用该校验器的渠道(xai/gemini/vertex)此前同样收不到它。

2. 刚提交的任务返回非协议状态值 "unknown"
   QuantumNous#4810 把状态源从 convertAliStatus(存档响应)换成 task.ToOpenAIVideo()
   (DB 行),而 InitTask 建行时是 NOT_START,ToVideoStatus 没有该分支,落到
   default 返回 "unknown"。轮询 worker 15 秒才接手,这期间下游按四态解析
   会整条落空——前端表现为任务提交后从队列里消失十几秒。
   NOT_START 语义就是排队中,补进 queued 分支。

3. 首尾帧语义丢失
   imageMediaType 对整个数组只返回一种类型,happyhorse-i2v 传两张图会被
   打成两个 first_frame。改为按模型分型:参考类(r2v/videoedit)同型追加,
   图生视频首张 first_frame、次张 last_frame,与 wan2.7-i2v 的
   normalizeWan27I2VInput 对齐。

另修两处既有缺陷:

- 时长兜底原本在 metadata 合并之前执行,而 metadata 能把 Duration 写回 0。
  0 一头被 omitempty 从上游请求里抹掉(阿里改用自己的默认时长照常出片),
  另一头让 seconds 倍率在 AddOtherRatio 的 ratio>0 守卫处被静默丢弃(按 1
  秒计价)——净效果是付 1 秒的钱拿到默认时长的视频,稳定可复现。兜底移到
  metadata 合并之后,使计费口径与上游下单口径一致。
- 双图走首尾帧字段的分支原本对所有旧模型生效,但只有 kf2v 系列真正支持
  首尾帧,其余旧版图生视频只认 img_url,发 first_frame_url/last_frame_url
  会被上游拒绝。收窄到 kf2v。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
JacksonsY added a commit to JacksonsY/new-api that referenced this pull request Jul 19, 2026
review 查出,均为 QuantumNous#5124 移植带入。

1. 切换分页销毁输入
   TabsContent 默认不保留挂载,切走再切回会清掉聊天草稿与滚动位置,视频侧的
   提示词/分辨率/时长/图片 URL 也全部重置,且每次重挂都重新拉一遍密钥列表。
   两个面板都加 keepMounted。

2. 轮询无上限
   状态查询的异常被整体吞掉并注明继续重试,只有 completed/failed 才停表。
   上游任务被清理(404)或密钥被删(401)时,只要标签页开着就以 12 次/分钟
   永远轮询下去,任务也永远停在进行中。加 240 次(20 分钟)上限,超时置失败。

3. 非四态状态导致任务从队列消失
   后端在任务刚入库时可能返回四态之外的值,原样存下来会让任务既不算进行中
   也不算已完成,在队列里整条消失。加归一化兜底(后端侧的根因已另行修复)。

4. 取密钥失败时完全静默
   fetchTokenKey 走 skipErrorHandler,全局拦截器不弹提示,而 handleSubmit
   没有 try/catch——密钥被删或无权限时点「生成视频」毫无反应,只在控制台
   留一条未捕获拒绝。

5. 提交失败仍清空提示词
   onSubmit 捕获异常后不再抛出,await 正常返回,于是失败也会执行 setPrompt('')
   ——用户刚写的长提示词随报错一起消失且找不回。onSubmit 改为返回是否成功,
   仅成功时清空。

另加多标签页同步:任务列表是整体覆写持久化,不监听 storage 事件的话,本标签页
的下一次写入会抹掉另一个标签页刚提交的任务(已付费任务失联)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant