2026-06-02 14:18:26 +08:00
|
|
|
import { DownOutlined } from "@ant-design/icons";
|
|
|
|
|
import type { ReactNode } from "react";
|
|
|
|
|
import type { WorkbenchOption, WorkbenchFieldGroup } from "./workbenchConstants";
|
|
|
|
|
import { getRatioOptionClassName, getSettingsGridColumnsClassName } from "./workbenchReferenceUtils";
|
|
|
|
|
|
2026-06-04 17:27:40 +08:00
|
|
|
const VIDEO_MODEL_ICON_URLS = {
|
|
|
|
|
happyHorse: "https://stringtest.oss-cn-hangzhou.aliyuncs.com/static/model-icons/HappyHorse.svg",
|
|
|
|
|
pixverse: "https://stringtest.oss-cn-hangzhou.aliyuncs.com/static/model-icons/Pixverse.svg",
|
|
|
|
|
vidu: "https://stringtest.oss-cn-hangzhou.aliyuncs.com/static/model-icons/viduQ3.svg",
|
|
|
|
|
wanxiang: "https://stringtest.oss-cn-hangzhou.aliyuncs.com/static/model-icons/wan.svg",
|
|
|
|
|
kling: "https://stringtest.oss-cn-hangzhou.aliyuncs.com/static/model-icons/kling.svg",
|
|
|
|
|
} as const;
|
|
|
|
|
|
|
|
|
|
function getVideoModelIconUrl(option: WorkbenchOption): string | null {
|
|
|
|
|
const text = `${option.value} ${option.label}`.toLowerCase();
|
|
|
|
|
if (text.includes("happyhorse")) return VIDEO_MODEL_ICON_URLS.happyHorse;
|
|
|
|
|
if (text.includes("pixverse")) return VIDEO_MODEL_ICON_URLS.pixverse;
|
|
|
|
|
if (text.includes("vidu")) return VIDEO_MODEL_ICON_URLS.vidu;
|
|
|
|
|
if (text.includes("wan") || text.includes("万相")) return VIDEO_MODEL_ICON_URLS.wanxiang;
|
|
|
|
|
if (text.includes("kling") || text.includes("可灵")) return VIDEO_MODEL_ICON_URLS.kling;
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-02 14:18:26 +08:00
|
|
|
export function SelectChip({
|
|
|
|
|
chipId,
|
|
|
|
|
value,
|
|
|
|
|
options,
|
|
|
|
|
disabled,
|
|
|
|
|
isOpen,
|
|
|
|
|
onToggle,
|
|
|
|
|
onClose,
|
|
|
|
|
onChange,
|
|
|
|
|
ariaLabel,
|
|
|
|
|
direction = "up",
|
|
|
|
|
}: {
|
|
|
|
|
chipId: string;
|
|
|
|
|
value: string;
|
|
|
|
|
options: WorkbenchOption[];
|
|
|
|
|
disabled?: boolean;
|
|
|
|
|
isOpen: boolean;
|
|
|
|
|
onToggle: () => void;
|
|
|
|
|
onClose: () => void;
|
|
|
|
|
onChange: (value: string) => void;
|
|
|
|
|
ariaLabel?: string;
|
|
|
|
|
direction?: "up" | "down";
|
|
|
|
|
}) {
|
|
|
|
|
const activeOption = options.find((option) => option.value === value);
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div
|
|
|
|
|
className={`ai-workbench-select-chip${chipId.endsWith("-model") ? " ai-workbench-select-chip--model" : ""}${disabled ? " is-disabled" : ""}${isOpen ? " is-open" : ""}`}
|
|
|
|
|
>
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
className="ai-workbench-select-chip__trigger"
|
|
|
|
|
onClick={onToggle}
|
|
|
|
|
disabled={disabled}
|
|
|
|
|
aria-label={ariaLabel}
|
|
|
|
|
aria-haspopup="listbox"
|
|
|
|
|
aria-expanded={isOpen}
|
|
|
|
|
aria-controls={`${chipId}-listbox`}
|
|
|
|
|
>
|
|
|
|
|
<span className="ai-workbench-select-chip__copy">
|
|
|
|
|
<span className="ai-workbench-select-chip__value">{activeOption?.label || value}</span>
|
|
|
|
|
</span>
|
|
|
|
|
<DownOutlined className="ai-workbench-select-chip__arrow" />
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
{isOpen ? (
|
|
|
|
|
<div
|
|
|
|
|
id={`${chipId}-listbox`}
|
|
|
|
|
className={`ai-workbench-select-chip__dropdown ai-workbench-select-chip__dropdown--${direction} is-open`}
|
|
|
|
|
role="listbox"
|
|
|
|
|
>
|
|
|
|
|
{options.map((option, index) => {
|
|
|
|
|
const active = option.value === value;
|
2026-06-04 17:27:40 +08:00
|
|
|
const iconUrl = chipId === "video-model" ? getVideoModelIconUrl(option) : null;
|
2026-06-02 14:18:26 +08:00
|
|
|
return (
|
|
|
|
|
<button
|
|
|
|
|
key={option.value}
|
|
|
|
|
type="button"
|
|
|
|
|
role="option"
|
|
|
|
|
aria-selected={active}
|
|
|
|
|
className={`ai-workbench-select-chip__option${active ? " is-active" : ""}`}
|
|
|
|
|
style={{ transitionDelay: `${index * 18}ms` }}
|
|
|
|
|
onClick={() => {
|
|
|
|
|
onChange(option.value);
|
|
|
|
|
onClose();
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<span className="ai-workbench-select-chip__option-label">
|
|
|
|
|
<span className="ai-workbench-select-chip__option-dot" aria-hidden="true" />
|
2026-06-04 17:27:40 +08:00
|
|
|
{iconUrl ? (
|
|
|
|
|
<span className="ai-workbench-select-chip__option-icon" aria-hidden="true">
|
|
|
|
|
<img src={iconUrl} alt="" loading="lazy" />
|
|
|
|
|
</span>
|
|
|
|
|
) : null}
|
2026-06-02 14:18:26 +08:00
|
|
|
<span className="ai-workbench-select-chip__option-copy">
|
|
|
|
|
<span className="ai-workbench-select-chip__option-title">
|
|
|
|
|
<span>{option.label}</span>
|
|
|
|
|
{option.badge ? (
|
|
|
|
|
<span className="ai-workbench-select-chip__option-badge">{option.badge}</span>
|
|
|
|
|
) : null}
|
|
|
|
|
</span>
|
|
|
|
|
{option.description ? (
|
|
|
|
|
<span className="ai-workbench-select-chip__option-desc">{option.description}</span>
|
|
|
|
|
) : null}
|
|
|
|
|
</span>
|
|
|
|
|
</span>
|
|
|
|
|
</button>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</div>
|
|
|
|
|
) : null}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function CompoundSelectChip({
|
|
|
|
|
chipId,
|
|
|
|
|
summary,
|
|
|
|
|
groups,
|
|
|
|
|
disabled,
|
|
|
|
|
isOpen,
|
|
|
|
|
onToggle,
|
|
|
|
|
direction = "up",
|
|
|
|
|
}: {
|
|
|
|
|
chipId: string;
|
|
|
|
|
summary: string;
|
|
|
|
|
groups: WorkbenchFieldGroup[];
|
|
|
|
|
disabled?: boolean;
|
|
|
|
|
isOpen: boolean;
|
|
|
|
|
onToggle: () => void;
|
|
|
|
|
direction?: "up" | "down";
|
|
|
|
|
}) {
|
|
|
|
|
return (
|
|
|
|
|
<div
|
|
|
|
|
className={`ai-workbench-select-chip ai-workbench-select-chip--compound${disabled ? " is-disabled" : ""}${isOpen ? " is-open" : ""}`}
|
|
|
|
|
>
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
className="ai-workbench-select-chip__trigger"
|
|
|
|
|
onClick={onToggle}
|
|
|
|
|
disabled={disabled}
|
|
|
|
|
aria-haspopup="dialog"
|
|
|
|
|
aria-expanded={isOpen}
|
|
|
|
|
aria-controls={`${chipId}-panel`}
|
|
|
|
|
>
|
|
|
|
|
<span className="ai-workbench-select-chip__copy">
|
|
|
|
|
<span className="ai-workbench-select-chip__value">{summary}</span>
|
|
|
|
|
</span>
|
|
|
|
|
<DownOutlined className="ai-workbench-select-chip__arrow" />
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
{isOpen ? (
|
|
|
|
|
<div
|
|
|
|
|
id={`${chipId}-panel`}
|
|
|
|
|
className={`ai-workbench-select-chip__dropdown ai-workbench-select-chip__dropdown--compound ai-workbench-select-chip__dropdown--${direction} is-open`}
|
|
|
|
|
role="dialog"
|
|
|
|
|
>
|
|
|
|
|
<div className="ai-workbench-settings-panel">
|
|
|
|
|
{groups.map((group) => {
|
|
|
|
|
const currentLabel =
|
|
|
|
|
group.options.find((option) => option.value === group.value)?.label || group.value;
|
|
|
|
|
const fieldKey = `${group.kind || "pill"}-${group.label}`;
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div
|
|
|
|
|
key={fieldKey}
|
|
|
|
|
className={`ai-workbench-settings-panel__field ai-workbench-settings-panel__field--${group.kind || "pill"}`}
|
|
|
|
|
>
|
|
|
|
|
<div className="ai-workbench-settings-panel__head">
|
|
|
|
|
<div className="ai-workbench-settings-panel__title-wrap">
|
|
|
|
|
{group.icon ? (
|
|
|
|
|
<span className="ai-workbench-settings-panel__title-icon">{group.icon}</span>
|
|
|
|
|
) : null}
|
|
|
|
|
<div className="ai-workbench-settings-panel__title-copy">
|
|
|
|
|
<div className="ai-workbench-settings-panel__title">{group.label}</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<span className="ai-workbench-settings-panel__current">{currentLabel}</span>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<fieldset
|
|
|
|
|
className={`ai-workbench-settings-panel__grid ai-workbench-settings-panel__grid--${group.kind || "pill"} ${getSettingsGridColumnsClassName(group.columns || 3)}`}
|
|
|
|
|
>
|
|
|
|
|
<legend className="ai-workbench-visually-hidden">{group.label}</legend>
|
|
|
|
|
{group.options.map((option) => {
|
|
|
|
|
const active = option.value === group.value;
|
|
|
|
|
return (
|
|
|
|
|
<button
|
|
|
|
|
key={`${fieldKey}-${option.value}`}
|
|
|
|
|
type="button"
|
|
|
|
|
aria-pressed={active}
|
|
|
|
|
className={`ai-workbench-settings-panel__option ai-workbench-settings-panel__option--${group.kind || "pill"}${active ? " is-active" : ""}`}
|
|
|
|
|
onClick={() => group.onChange(option.value)}
|
|
|
|
|
>
|
|
|
|
|
{group.kind === "ratio" ? (
|
|
|
|
|
<span className="ai-workbench-ratio-option">
|
|
|
|
|
<span
|
|
|
|
|
className={`ai-workbench-ratio-option__preview ${getRatioOptionClassName(option.value)}`}
|
|
|
|
|
>
|
|
|
|
|
<span className="ai-workbench-ratio-option__frame" />
|
|
|
|
|
</span>
|
|
|
|
|
<span className="ai-workbench-ratio-option__label">{option.label}</span>
|
|
|
|
|
</span>
|
|
|
|
|
) : (
|
|
|
|
|
<span>{option.label}</span>
|
|
|
|
|
)}
|
|
|
|
|
</button>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</fieldset>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
) : null}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function InlineOptionChip({
|
|
|
|
|
chipId,
|
|
|
|
|
value,
|
|
|
|
|
options,
|
|
|
|
|
icon,
|
|
|
|
|
disabled,
|
|
|
|
|
isOpen,
|
|
|
|
|
onToggle,
|
|
|
|
|
onClose,
|
|
|
|
|
onChange,
|
|
|
|
|
direction = "up",
|
|
|
|
|
}: {
|
|
|
|
|
chipId: string;
|
|
|
|
|
value: string;
|
|
|
|
|
options: WorkbenchOption[];
|
|
|
|
|
icon?: ReactNode;
|
|
|
|
|
disabled?: boolean;
|
|
|
|
|
isOpen: boolean;
|
|
|
|
|
onToggle: () => void;
|
|
|
|
|
onClose: () => void;
|
|
|
|
|
onChange: (value: string) => void;
|
|
|
|
|
direction?: "up" | "down";
|
|
|
|
|
}) {
|
|
|
|
|
const activeOption = options.find((option) => option.value === value);
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className={`wb-inline-chip${isOpen ? " is-open" : ""}${disabled ? " is-disabled" : ""}`}>
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
className="wb-inline-chip__trigger"
|
|
|
|
|
onClick={onToggle}
|
|
|
|
|
disabled={disabled}
|
|
|
|
|
aria-haspopup="listbox"
|
|
|
|
|
aria-expanded={isOpen}
|
|
|
|
|
aria-controls={`${chipId}-listbox`}
|
|
|
|
|
>
|
|
|
|
|
{icon ? <span className="wb-inline-chip__icon">{icon}</span> : null}
|
|
|
|
|
<span>{activeOption?.label || value}</span>
|
|
|
|
|
</button>
|
|
|
|
|
{isOpen ? (
|
|
|
|
|
<div id={`${chipId}-listbox`} className={`wb-inline-chip__menu wb-inline-chip__menu--${direction}`} role="listbox">
|
|
|
|
|
{options.map((option) => {
|
|
|
|
|
const active = option.value === value;
|
|
|
|
|
return (
|
|
|
|
|
<button
|
|
|
|
|
key={option.value}
|
|
|
|
|
type="button"
|
|
|
|
|
role="option"
|
|
|
|
|
aria-selected={active}
|
|
|
|
|
className={`wb-inline-chip__option${active ? " is-active" : ""}`}
|
|
|
|
|
onClick={() => {
|
|
|
|
|
onChange(option.value);
|
|
|
|
|
onClose();
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<span>{option.label}</span>
|
|
|
|
|
{active ? <span className="wb-inline-chip__check">✓</span> : null}
|
|
|
|
|
</button>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</div>
|
|
|
|
|
) : null}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
2026-06-04 17:27:40 +08:00
|
|
|
}
|