修改全能创作发现问题
This commit is contained in:
@@ -567,7 +567,7 @@ export function App() {
|
||||
}
|
||||
setRoute({
|
||||
page: next, authMode, productId, projectId, conversationId, hash,
|
||||
tab: options.tab, firstMessage: options.firstMessage, firstRefs: options.firstRefs,
|
||||
tab: options.tab, firstMessage: options.firstMessage, firstRefs: options.firstRefs, firstUploads: options.firstUploads,
|
||||
});
|
||||
const arriving: NavHistoryState = {
|
||||
airshelf: 1,
|
||||
@@ -1088,6 +1088,7 @@ export function App() {
|
||||
conversationId={route.conversationId}
|
||||
firstMessage={route.firstMessage}
|
||||
firstRefs={route.firstRefs}
|
||||
firstUploads={route.firstUploads}
|
||||
navigate={navigate}
|
||||
onNotify={(type, text) => setNotice({ type, text })}
|
||||
/>
|
||||
|
||||
@@ -593,10 +593,13 @@ export const api = {
|
||||
* video_prompt 直接提交,同步返回「生成中」消息,之后靠轮询转成结果。
|
||||
*/
|
||||
confirmCreationPlan(id: string, replyTo: string, params?: Record<string, string>) {
|
||||
return request<{ message: CreationMessage }>(`/api/ai/creations/${id}/send/`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ kind: "confirm", reply_to: replyTo, params })
|
||||
});
|
||||
return request<{ message?: CreationMessage; regenerate?: boolean; params?: Record<string, string> }>(
|
||||
`/api/ai/creations/${id}/send/`,
|
||||
{
|
||||
method: "POST",
|
||||
body: JSON.stringify({ kind: "confirm", reply_to: replyTo, params })
|
||||
}
|
||||
);
|
||||
},
|
||||
/**
|
||||
* 发一条消息 → SSE 流。事件:tool / reasoning / delta / message / task / credits / done / error。
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { ChevronDown, SlidersHorizontal, Sparkles } from "lucide-react";
|
||||
import { CustomSelect } from "./custom-select";
|
||||
|
||||
export const OMNI_VIDEO_MODELS = ["Seedance 2.5", "Seedance 2.0", "Seedance 2.0 Fast", "Seedance 2.0 Mini"];
|
||||
export const OMNI_IMAGE_MODELS = ["Seedream5.0", "YQ image2"];
|
||||
export const OMNI_RESOLUTIONS = ["1080p", "720p", "480p"];
|
||||
export const OMNI_RATIOS = ["9:16", "16:9", "1:1", "4:3", "3:4"];
|
||||
export const OMNI_VIDEO_DURATIONS = [
|
||||
"4 秒", "5 秒", "6 秒", "7 秒", "8 秒", "9 秒", "10 秒",
|
||||
"11 秒", "12 秒", "13 秒", "14 秒", "15 秒", "30 秒",
|
||||
];
|
||||
export const OMNI_IMAGE_COUNTS = ["1 张", "2 张", "4 张", "8 张"];
|
||||
|
||||
function toOptions(values: string[]) {
|
||||
return values.map((value) => ({ value, label: value }));
|
||||
}
|
||||
|
||||
function withCurrent(values: string[], current: string) {
|
||||
return current && !values.includes(current) ? [current, ...values] : values;
|
||||
}
|
||||
|
||||
export function OmniParamBar({
|
||||
isVideo,
|
||||
disabled,
|
||||
model,
|
||||
resolution,
|
||||
ratio,
|
||||
duration,
|
||||
onModel,
|
||||
onResolution,
|
||||
onRatio,
|
||||
onDuration,
|
||||
}: {
|
||||
isVideo: boolean;
|
||||
disabled?: boolean;
|
||||
model: string;
|
||||
resolution: string;
|
||||
ratio: string;
|
||||
duration: string;
|
||||
onModel: (value: string) => void;
|
||||
onResolution: (value: string) => void;
|
||||
onRatio: (value: string) => void;
|
||||
onDuration: (value: string) => void;
|
||||
}) {
|
||||
const [menuOpen, setMenuOpen] = useState(false);
|
||||
const [customOn, setCustomOn] = useState(isVideo ? duration !== "智能时长" : true);
|
||||
const wrapRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
setCustomOn(isVideo ? duration !== "智能时长" : true);
|
||||
}, [isVideo, duration]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!menuOpen) return;
|
||||
const onDown = (event: MouseEvent) => {
|
||||
if (!wrapRef.current?.contains(event.target as Node)) setMenuOpen(false);
|
||||
};
|
||||
document.addEventListener("mousedown", onDown);
|
||||
return () => document.removeEventListener("mousedown", onDown);
|
||||
}, [menuOpen]);
|
||||
|
||||
const models = withCurrent(isVideo ? OMNI_VIDEO_MODELS : OMNI_IMAGE_MODELS, model);
|
||||
const durations = isVideo ? OMNI_VIDEO_DURATIONS : OMNI_IMAGE_COUNTS;
|
||||
|
||||
return (
|
||||
<>
|
||||
<label className="omni-parameter omni-parameter-model">
|
||||
<CustomSelect
|
||||
fill
|
||||
size="sm"
|
||||
aria-label={isVideo ? "视频模型" : "图片模型"}
|
||||
disabled={disabled}
|
||||
value={model}
|
||||
onChange={onModel}
|
||||
options={toOptions(models)}
|
||||
/>
|
||||
</label>
|
||||
<label className={`omni-parameter${isVideo ? "" : " is-hidden"}`}>
|
||||
<CustomSelect
|
||||
fill
|
||||
size="sm"
|
||||
aria-label="分辨率"
|
||||
disabled={disabled}
|
||||
value={resolution}
|
||||
onChange={onResolution}
|
||||
options={toOptions(withCurrent(OMNI_RESOLUTIONS, resolution))}
|
||||
/>
|
||||
</label>
|
||||
<label className="omni-parameter">
|
||||
<CustomSelect
|
||||
fill
|
||||
size="sm"
|
||||
aria-label="画面比例"
|
||||
disabled={disabled}
|
||||
value={ratio}
|
||||
onChange={onRatio}
|
||||
options={toOptions(withCurrent(OMNI_RATIOS, ratio))}
|
||||
/>
|
||||
</label>
|
||||
<div className={`omni-duration-control${isVideo ? "" : " is-image-count"}`} ref={wrapRef}>
|
||||
<button
|
||||
type="button"
|
||||
className="omni-duration-trigger"
|
||||
aria-expanded={menuOpen}
|
||||
disabled={disabled}
|
||||
onClick={() => setMenuOpen((open) => !open)}
|
||||
>
|
||||
<span>{duration}</span>
|
||||
<ChevronDown />
|
||||
</button>
|
||||
<div className="omni-duration-menu" hidden={!menuOpen}>
|
||||
<span className="omni-duration-title">{isVideo ? "时长" : "生成张数"}</span>
|
||||
{isVideo ? (
|
||||
<div className="omni-duration-modes">
|
||||
<button
|
||||
type="button"
|
||||
className={!customOn ? "active" : ""}
|
||||
onClick={() => {
|
||||
setCustomOn(false);
|
||||
onDuration("智能时长");
|
||||
setMenuOpen(false);
|
||||
}}
|
||||
>
|
||||
<Sparkles />
|
||||
<span>智能时长</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={customOn ? "active" : ""}
|
||||
onClick={() => setCustomOn(true)}
|
||||
>
|
||||
<SlidersHorizontal />
|
||||
<span>自定义时长</span>
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="omni-duration-values" hidden={isVideo && !customOn}>
|
||||
{durations.map((value) => (
|
||||
<button
|
||||
type="button"
|
||||
key={value}
|
||||
className={duration === value ? "active" : ""}
|
||||
onClick={() => {
|
||||
onDuration(value);
|
||||
setCustomOn(true);
|
||||
setMenuOpen(false);
|
||||
}}
|
||||
>
|
||||
{value}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -5,6 +5,7 @@
|
||||
--black: #101012;
|
||||
--text: #17181a;
|
||||
--muted: #6f747c;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
@keyframes omniMessageIn {
|
||||
@@ -33,24 +34,31 @@
|
||||
|
||||
.omni-home-history {
|
||||
position: absolute;
|
||||
top: 24px;
|
||||
top: 20px;
|
||||
right: 0;
|
||||
height: 36px;
|
||||
height: 40px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
padding: 0 12px;
|
||||
border: 1px solid rgba(34, 42, 54, .10);
|
||||
border-radius: 10px;
|
||||
color: #414750;
|
||||
gap: 8px;
|
||||
padding: 0 14px;
|
||||
border: 1.5px solid rgba(16, 16, 18, .22);
|
||||
border-radius: 12px;
|
||||
color: var(--black);
|
||||
background: #fff;
|
||||
font-size: 11px;
|
||||
font-size: 13px;
|
||||
font-weight: 650;
|
||||
box-shadow: 0 4px 12px rgba(16, 16, 18, .08);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.omni-home-history:hover {
|
||||
border-color: var(--black);
|
||||
background: #f7f7f8;
|
||||
}
|
||||
|
||||
.omni-home-history svg {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.omni-home-kicker {
|
||||
@@ -59,7 +67,7 @@
|
||||
gap: 7px;
|
||||
margin-bottom: 12px;
|
||||
color: var(--klein);
|
||||
font-size: 10px;
|
||||
font-size: 12px;
|
||||
font-weight: 750;
|
||||
letter-spacing: .13em;
|
||||
}
|
||||
@@ -81,7 +89,7 @@
|
||||
.omni-home-hero > p {
|
||||
margin: 10px 0 17px;
|
||||
color: #747b86;
|
||||
font-size: 13px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.omni-output-switch {
|
||||
@@ -100,7 +108,7 @@
|
||||
border-radius: 999px;
|
||||
color: #6d737c;
|
||||
background: transparent;
|
||||
font-size: 11px;
|
||||
font-size: 12px;
|
||||
font-weight: 650;
|
||||
cursor: pointer;
|
||||
}
|
||||
@@ -152,7 +160,7 @@
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
color: var(--klein);
|
||||
font-size: 11px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.omni-selected-case svg,
|
||||
@@ -199,7 +207,7 @@
|
||||
border-radius: 8px;
|
||||
color: #344159;
|
||||
background: rgba(0, 47, 167, .045);
|
||||
font-size: 11px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
@@ -255,7 +263,7 @@
|
||||
outline: 0;
|
||||
color: var(--text);
|
||||
background: transparent;
|
||||
font-size: 14px;
|
||||
font-size: 15px;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
@@ -290,7 +298,7 @@
|
||||
border-radius: 9px;
|
||||
color: #353b45;
|
||||
background: #f8f9fa;
|
||||
font-size: 14px;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
transition: color 150ms ease, border-color 150ms ease, background-color 150ms ease;
|
||||
@@ -336,7 +344,7 @@
|
||||
display: block;
|
||||
padding: 5px 8px 7px;
|
||||
color: #8b919a;
|
||||
font-size: 9px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.omni-upload-menu button {
|
||||
@@ -373,14 +381,14 @@
|
||||
}
|
||||
|
||||
.omni-upload-menu button span {
|
||||
font-size: 11px;
|
||||
font-size: 12px;
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
.omni-upload-menu button small {
|
||||
margin-top: 3px;
|
||||
color: #9298a1;
|
||||
font-size: 9px;
|
||||
font-size: 11px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
@@ -405,7 +413,7 @@
|
||||
display: block;
|
||||
padding: 5px 8px 7px;
|
||||
color: #8b919a;
|
||||
font-size: 9px;
|
||||
font-size: 11px;
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
@@ -419,7 +427,7 @@
|
||||
display: block;
|
||||
padding: 5px 8px 7px;
|
||||
color: #8b919a;
|
||||
font-size: 9px;
|
||||
font-size: 11px;
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
@@ -466,14 +474,14 @@
|
||||
}
|
||||
|
||||
.omni-mention-menu button span {
|
||||
font-size: 11px;
|
||||
font-size: 12px;
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
.omni-mention-menu button small {
|
||||
margin-top: 2px;
|
||||
color: #8b919a;
|
||||
font-size: 9px;
|
||||
font-size: 11px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
@@ -521,7 +529,7 @@
|
||||
border-color: rgba(34, 42, 54, .11);
|
||||
border-radius: 10px;
|
||||
background: #f8f9fa;
|
||||
font-size: 11px;
|
||||
font-size: 12px;
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
@@ -547,7 +555,7 @@
|
||||
|
||||
.omni-parameter .custom-select-option {
|
||||
min-height: 34px;
|
||||
font-size: 11px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.omni-duration-control {
|
||||
@@ -572,11 +580,16 @@
|
||||
border-radius: 10px;
|
||||
color: #373d46;
|
||||
background: #f8f9fa;
|
||||
font-size: 11px;
|
||||
font-size: 12px;
|
||||
font-weight: 650;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.omni-duration-trigger:disabled {
|
||||
opacity: .55;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.omni-duration-trigger:hover,
|
||||
.omni-duration-trigger[aria-expanded="true"] {
|
||||
border-color: rgba(0, 47, 167, .36);
|
||||
@@ -616,7 +629,7 @@
|
||||
display: block;
|
||||
margin-bottom: 10px;
|
||||
color: #7d838d;
|
||||
font-size: 11px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.omni-duration-modes {
|
||||
@@ -635,7 +648,7 @@
|
||||
border-radius: 10px;
|
||||
color: #626974;
|
||||
background: #f7f8fa;
|
||||
font-size: 11px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@@ -674,7 +687,7 @@
|
||||
border-radius: 8px;
|
||||
color: #575e69;
|
||||
background: #fff;
|
||||
font-size: 11px;
|
||||
font-size: 12px;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
@@ -708,7 +721,7 @@
|
||||
border-radius: 10px;
|
||||
color: #fff;
|
||||
background: var(--klein);
|
||||
font-size: 11px;
|
||||
font-size: 12px;
|
||||
font-weight: 650;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 9px 18px rgba(0, 47, 167, .17);
|
||||
@@ -753,7 +766,7 @@
|
||||
border-radius: 8px;
|
||||
color: #707681;
|
||||
background: transparent;
|
||||
font-size: 12px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
}
|
||||
@@ -861,7 +874,7 @@
|
||||
color: #fff;
|
||||
background: #111318;
|
||||
box-shadow: 0 8px 18px rgba(0, 0, 0, .18);
|
||||
font-size: 10px;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
@@ -1001,7 +1014,7 @@
|
||||
border-radius: 999px;
|
||||
color: var(--klein);
|
||||
background: #edf5ff;
|
||||
font-size: 10px;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
@@ -1013,7 +1026,7 @@
|
||||
.omni-preset-detail > p {
|
||||
margin: 0;
|
||||
color: #737a84;
|
||||
font-size: 13px;
|
||||
font-size: 14px;
|
||||
line-height: 1.85;
|
||||
}
|
||||
|
||||
@@ -1026,13 +1039,13 @@
|
||||
|
||||
.omni-preset-default span {
|
||||
color: #8a9099;
|
||||
font-size: 10px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.omni-preset-default p {
|
||||
margin: 7px 0 0;
|
||||
color: #3e4653;
|
||||
font-size: 12px;
|
||||
font-size: 13px;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
@@ -1050,7 +1063,7 @@
|
||||
border-radius: 10px;
|
||||
color: #616873;
|
||||
background: #fff;
|
||||
font-size: 12px;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@@ -1073,14 +1086,14 @@
|
||||
|
||||
.omni-case-copy strong {
|
||||
margin-bottom: 6px;
|
||||
font-size: 13px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.omni-case-copy small {
|
||||
display: -webkit-box;
|
||||
overflow: hidden;
|
||||
color: #858b94;
|
||||
font-size: 10px;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
@@ -1107,7 +1120,7 @@
|
||||
|
||||
.omni-history-heading > span {
|
||||
color: var(--klein);
|
||||
font-size: 9px;
|
||||
font-size: 11px;
|
||||
font-weight: 750;
|
||||
letter-spacing: .13em;
|
||||
}
|
||||
@@ -1144,7 +1157,7 @@
|
||||
.omni-history-header p {
|
||||
margin-top: 7px;
|
||||
color: #858b94;
|
||||
font-size: 11px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.omni-history-tools {
|
||||
@@ -1174,7 +1187,7 @@
|
||||
border-radius: 12px;
|
||||
color: #fff;
|
||||
background: var(--klein);
|
||||
font-size: 12px;
|
||||
font-size: 13px;
|
||||
font-weight: 650;
|
||||
cursor: pointer;
|
||||
}
|
||||
@@ -1191,7 +1204,7 @@
|
||||
border-radius: 9px;
|
||||
color: #707681;
|
||||
background: transparent;
|
||||
font-size: 12px;
|
||||
font-size: 13px;
|
||||
font-weight: 650;
|
||||
cursor: pointer;
|
||||
}
|
||||
@@ -1259,7 +1272,7 @@
|
||||
.omni-history-item p,
|
||||
.omni-history-item small {
|
||||
color: #858b94;
|
||||
font-size: 12.5px;
|
||||
font-size: 14px;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
@@ -1359,7 +1372,7 @@
|
||||
.omni-delete-dialog p {
|
||||
margin: 8px 0 20px;
|
||||
color: #7d838d;
|
||||
font-size: 11px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.omni-delete-dialog > div {
|
||||
@@ -1374,7 +1387,7 @@
|
||||
border-radius: 9px;
|
||||
color: #5e6570;
|
||||
background: #fff;
|
||||
font-size: 10px;
|
||||
font-size: 12px;
|
||||
font-weight: 650;
|
||||
cursor: pointer;
|
||||
}
|
||||
@@ -1390,7 +1403,7 @@
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
color: #a86a00;
|
||||
font-size: 11.5px;
|
||||
font-size: 13px;
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
@@ -1422,7 +1435,7 @@
|
||||
border-color: rgba(34, 42, 54, .11);
|
||||
border-radius: 10px;
|
||||
background: #f8f9fa;
|
||||
font-size: 11px;
|
||||
font-size: 12px;
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
@@ -1454,7 +1467,7 @@
|
||||
|
||||
.omni-history-empty p {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.omni-history-empty button {
|
||||
@@ -1466,7 +1479,7 @@
|
||||
border-radius: 10px;
|
||||
color: #fff;
|
||||
background: var(--klein);
|
||||
font-size: 12px;
|
||||
font-size: 13px;
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
.omni-session-page {
|
||||
min-height: calc(100vh - var(--topbar-height));
|
||||
background: transparent;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* 进会话后顶栏换成对话信息,滚下去也还在 */
|
||||
@@ -73,7 +74,7 @@
|
||||
border-radius: 10px;
|
||||
color: #414750;
|
||||
background: #fff;
|
||||
font-size: 11px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@@ -92,7 +93,7 @@
|
||||
|
||||
.omni-session-meta span {
|
||||
color: #7d838c;
|
||||
font-size: 9px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.omni-session-meta span:not(:last-child)::after {
|
||||
@@ -149,7 +150,7 @@
|
||||
border-radius: 14px;
|
||||
color: #323945;
|
||||
background: #f7f8fa;
|
||||
font-size: 12px;
|
||||
font-size: 13px;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
@@ -218,7 +219,7 @@
|
||||
|
||||
.omni-strategy-head strong,
|
||||
.omni-video-plan-head strong {
|
||||
font-size: 13px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.omni-strategy-head span,
|
||||
@@ -227,7 +228,7 @@
|
||||
border-radius: 999px;
|
||||
color: var(--klein);
|
||||
background: #edf5ff;
|
||||
font-size: 9px;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
@@ -252,13 +253,13 @@
|
||||
|
||||
.omni-strategy-grid span {
|
||||
color: #858b94;
|
||||
font-size: 9px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.omni-strategy-grid strong {
|
||||
margin-top: 6px;
|
||||
color: #272d37;
|
||||
font-size: 11px;
|
||||
font-size: 12px;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
@@ -272,7 +273,7 @@
|
||||
border-radius: 10px;
|
||||
color: #39404b;
|
||||
background: #f5f9ff;
|
||||
font-size: 10px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.omni-strategy-direction b {
|
||||
@@ -301,7 +302,7 @@
|
||||
.omni-plan-section > strong {
|
||||
display: block;
|
||||
margin-bottom: 10px;
|
||||
font-size: 11px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.omni-plan-points {
|
||||
@@ -315,7 +316,7 @@
|
||||
border-radius: 9px;
|
||||
color: #525965;
|
||||
background: #f6f8fa;
|
||||
font-size: 10px;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
@@ -323,7 +324,7 @@
|
||||
display: block;
|
||||
margin-bottom: 4px;
|
||||
color: #252b35;
|
||||
font-size: 9px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.omni-plan-points i,
|
||||
@@ -345,7 +346,7 @@
|
||||
border-radius: 10px;
|
||||
color: #5b626d;
|
||||
background: #fff;
|
||||
font-size: 9px;
|
||||
font-size: 11px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
@@ -353,7 +354,7 @@
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
color: var(--klein);
|
||||
font-size: 10px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.omni-plan-matrix {
|
||||
@@ -369,7 +370,7 @@
|
||||
border-radius: 7px;
|
||||
color: #6d747e;
|
||||
background: #f6f8fa;
|
||||
font-size: 9px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
}
|
||||
@@ -395,7 +396,7 @@
|
||||
padding: 13px 17px;
|
||||
color: #444b56;
|
||||
background: #f6f8fb;
|
||||
font-size: 10px;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
@@ -433,13 +434,13 @@
|
||||
}
|
||||
|
||||
.omni-prompt-file-copy strong {
|
||||
font-size: 12px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.omni-prompt-file-copy small {
|
||||
margin-top: 4px;
|
||||
color: #858b94;
|
||||
font-size: 9px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.omni-prompt-file-card button {
|
||||
@@ -449,7 +450,7 @@
|
||||
border-radius: 9px;
|
||||
color: #333944;
|
||||
background: #fff;
|
||||
font-size: 10px;
|
||||
font-size: 12px;
|
||||
font-weight: 650;
|
||||
cursor: pointer;
|
||||
}
|
||||
@@ -472,7 +473,7 @@
|
||||
border-radius: 9px;
|
||||
color: var(--klein);
|
||||
background: #fff;
|
||||
font-size: 11px;
|
||||
font-size: 12px;
|
||||
font-weight: 650;
|
||||
cursor: pointer;
|
||||
}
|
||||
@@ -505,7 +506,7 @@
|
||||
outline: 0;
|
||||
color: var(--text);
|
||||
background: transparent;
|
||||
font-size: 12px;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
@@ -557,7 +558,7 @@
|
||||
border-radius: 10px;
|
||||
color: #373d46;
|
||||
background: #f8f9fa;
|
||||
font-size: 10.5px;
|
||||
font-size: 12px;
|
||||
font-weight: 650;
|
||||
cursor: pointer;
|
||||
}
|
||||
@@ -619,12 +620,12 @@
|
||||
outline: 0;
|
||||
color: #272d36;
|
||||
background: transparent;
|
||||
font-size: 12px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.omni-session-preset-search span {
|
||||
color: #7d838c;
|
||||
font-size: 11px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.omni-session-preset-body {
|
||||
@@ -659,7 +660,7 @@
|
||||
.omni-session-preset-categories button {
|
||||
min-height: 36px;
|
||||
padding: 0 10px;
|
||||
font-size: 11px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.omni-session-preset-categories button.active {
|
||||
@@ -691,13 +692,13 @@
|
||||
}
|
||||
|
||||
.omni-session-preset-list strong {
|
||||
font-size: 12px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.omni-session-preset-list small {
|
||||
overflow: hidden;
|
||||
color: #949aa3;
|
||||
font-size: 9px;
|
||||
font-size: 11px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
@@ -729,7 +730,7 @@
|
||||
overflow: hidden;
|
||||
margin: 0;
|
||||
color: #737a85;
|
||||
font-size: 10px;
|
||||
font-size: 12px;
|
||||
line-height: 1.55;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 3;
|
||||
@@ -738,7 +739,7 @@
|
||||
.omni-session-preset-preview small {
|
||||
margin-top: 8px;
|
||||
color: #8d939c;
|
||||
font-size: 9px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.omni-session-preset-preview button {
|
||||
@@ -747,7 +748,7 @@
|
||||
border-radius: 9px;
|
||||
color: #fff;
|
||||
background: var(--klein);
|
||||
font-size: 11px;
|
||||
font-size: 12px;
|
||||
font-weight: 650;
|
||||
cursor: pointer;
|
||||
}
|
||||
@@ -755,7 +756,7 @@
|
||||
.omni-session-parameter .custom-select-trigger {
|
||||
height: 34px;
|
||||
padding: 0 9px;
|
||||
font-size: 10.5px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.omni-session-parameter .custom-select-menu {
|
||||
@@ -816,7 +817,7 @@
|
||||
border-radius: 8px;
|
||||
background: transparent;
|
||||
color: #5b616a;
|
||||
font-size: 12px;
|
||||
font-size: 13px;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
@@ -852,7 +853,7 @@
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
color: #8b919a;
|
||||
font-size: 12px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
}
|
||||
@@ -864,7 +865,7 @@
|
||||
gap: 8px;
|
||||
height: 100%;
|
||||
color: #8b919a;
|
||||
font-size: 12px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.omni-at-loading .omni-send-spinner {
|
||||
@@ -882,7 +883,7 @@
|
||||
border-radius: 8px;
|
||||
color: #303641;
|
||||
background: transparent;
|
||||
font-size: 12px;
|
||||
font-size: 13px;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
@@ -908,7 +909,7 @@
|
||||
|
||||
.omni-at-list button small {
|
||||
color: #8b919a;
|
||||
font-size: 10px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.omni-session-send {
|
||||
@@ -945,7 +946,7 @@
|
||||
|
||||
.omni-process-frame strong {
|
||||
color: #5c6570;
|
||||
font-size: 13px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
@@ -1073,13 +1074,13 @@
|
||||
}
|
||||
|
||||
.omni-result-info strong {
|
||||
font-size: 12px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.omni-result-info small {
|
||||
margin-top: 3px;
|
||||
color: #858b94;
|
||||
font-size: 9px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.omni-result-info button {
|
||||
@@ -1091,7 +1092,7 @@
|
||||
border-radius: 9px;
|
||||
color: #fff;
|
||||
background: var(--black);
|
||||
font-size: 10px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@@ -1166,7 +1167,7 @@
|
||||
display: block;
|
||||
margin-bottom: 9px;
|
||||
color: var(--black);
|
||||
font-size: 12px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
@@ -1182,7 +1183,7 @@
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
color: #575d66;
|
||||
font-size: 12px;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
transition: border-color .15s ease, color .15s ease, background .15s ease;
|
||||
}
|
||||
@@ -1205,7 +1206,7 @@
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
color: var(--black);
|
||||
font-size: 12px;
|
||||
font-size: 13px;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
@@ -1248,7 +1249,7 @@
|
||||
.omni-elicit-assets span {
|
||||
overflow: hidden;
|
||||
color: #575d66;
|
||||
font-size: 10px;
|
||||
font-size: 12px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
@@ -1266,7 +1267,7 @@
|
||||
border-radius: 8px;
|
||||
background: var(--klein);
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@@ -1287,19 +1288,61 @@
|
||||
.omni-confirm-card {
|
||||
width: min(760px, 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 14px;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: 12px;
|
||||
margin: 0 0 24px 44px;
|
||||
padding: 13px 16px;
|
||||
padding: 14px 16px;
|
||||
border: 1px solid rgba(34, 42, 54, .10);
|
||||
border-radius: 14px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.omni-confirm-card > span {
|
||||
.omni-confirm-copy {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.omni-confirm-copy strong {
|
||||
color: #22262e;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.omni-confirm-copy span,
|
||||
.omni-confirm-foot > span {
|
||||
color: #6f747c;
|
||||
font-size: 11px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.omni-confirm-params {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.omni-confirm-params .omni-duration-control {
|
||||
position: relative;
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
.omni-confirm-hint {
|
||||
margin: 0;
|
||||
padding: 8px 10px;
|
||||
border-radius: 8px;
|
||||
background: #fff7ed;
|
||||
color: #9a3412;
|
||||
font-size: 12px;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.omni-confirm-foot {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.omni-confirm-card button {
|
||||
@@ -1311,7 +1354,7 @@
|
||||
border-radius: 8px;
|
||||
background: var(--klein);
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@@ -1322,7 +1365,7 @@
|
||||
|
||||
.omni-confirm-card button i {
|
||||
color: rgba(255, 255, 255, .72);
|
||||
font-size: 11px;
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@@ -1352,7 +1395,7 @@
|
||||
max-width: 220px;
|
||||
padding: 3px 8px 3px 4px;
|
||||
border-radius: 999px;
|
||||
font-size: 10px;
|
||||
font-size: 12px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
@@ -1361,7 +1404,7 @@
|
||||
padding: 2px 6px;
|
||||
border-radius: 999px;
|
||||
font-style: normal;
|
||||
font-size: 9px;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
@@ -1424,7 +1467,7 @@
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
color: rgba(255, 255, 255, .72);
|
||||
font-size: 10px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.omni-send-spinner,
|
||||
@@ -1564,7 +1607,7 @@
|
||||
|
||||
.omni-prompt-drawer header strong {
|
||||
overflow: hidden;
|
||||
font-size: 14px;
|
||||
font-size: 15px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
@@ -1572,7 +1615,7 @@
|
||||
.omni-prompt-drawer header small {
|
||||
margin-top: 2px;
|
||||
color: #8b919a;
|
||||
font-size: 11px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.omni-prompt-drawer header button {
|
||||
@@ -1616,14 +1659,14 @@
|
||||
.omni-prompt-block h3 {
|
||||
margin: 0 0 8px;
|
||||
color: var(--klein);
|
||||
font-size: 12px;
|
||||
font-size: 13px;
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
.omni-prompt-block p {
|
||||
margin: 0;
|
||||
color: #2b3240;
|
||||
font-size: 13px;
|
||||
font-size: 14px;
|
||||
line-height: 1.7;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
|
||||
@@ -2,14 +2,12 @@ import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import {
|
||||
ArrowLeft,
|
||||
Box,
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
FolderOpen,
|
||||
History,
|
||||
Image as ImageIcon,
|
||||
Play,
|
||||
Plus,
|
||||
SlidersHorizontal,
|
||||
Sparkles,
|
||||
Trash2,
|
||||
Upload,
|
||||
@@ -20,7 +18,7 @@ import {
|
||||
X,
|
||||
} from "lucide-react";
|
||||
import { api } from "../api";
|
||||
import { CustomSelect } from "../components/custom-select";
|
||||
import { OmniParamBar } from "../components/omni-param-bar";
|
||||
import { ConfirmModal } from "../components/overlays";
|
||||
import type { CreationConversation, CreationRef } from "../types";
|
||||
import type { NavigateFn } from "./route-config";
|
||||
@@ -37,8 +35,6 @@ type PresetItem = {
|
||||
cover: string;
|
||||
};
|
||||
|
||||
type Attachment = { name: string; type: string; url?: string; source: "local" | "library" };
|
||||
|
||||
const VIDEO_PRESETS: PresetItem[] = [
|
||||
{ name: "剧情反转带货", category: "story", mode: "video", title: "剧情反转带货", desc: "用人物冲突与意外反转建立记忆点,商品承担剧情中的关键作用。", starter: "创作一条有前后反转的剧情带货视频,让商品自然成为解决问题的关键。", cover: "/assets/prototype/photo-1529139574466-a303027c1d8b.jpg" },
|
||||
{ name: "商品拟人广告", category: "commerce", mode: "video", title: "商品拟人广告", desc: "让商品成为故事主角,通过个性、动作和情绪完成趣味表达。", starter: "把商品塑造成有性格的拟人角色,创作一条轻快、有趣的短广告。", cover: "/assets/prototype/photo-1608248543803-ba4f8c70ae0b.jpg" },
|
||||
@@ -75,12 +71,6 @@ const IMAGE_FILTERS = [
|
||||
{ key: "style", label: "风格" },
|
||||
];
|
||||
|
||||
const VIDEO_MODELS = ["Seedance 2.5", "Seedance 2.0", "Seedance 2.0 Fast", "Seedance 2.0 Mini"];
|
||||
const IMAGE_MODELS = ["Seedream5.0", "YQ image2"];
|
||||
const VIDEO_DURATIONS = ["4 秒", "5 秒", "6 秒", "7 秒", "8 秒", "9 秒", "10 秒", "11 秒", "12 秒", "13 秒", "14 秒", "15 秒", "30 秒"];
|
||||
const IMAGE_COUNTS = ["1 张", "2 张", "4 张", "8 张"];
|
||||
const RATIOS = ["9:16", "16:9", "1:1", "4:3", "3:4"];
|
||||
|
||||
const MENTION_REF_LIMIT = 5;
|
||||
|
||||
const MENTION_TABS: Array<{ type: CreationRef["type"]; label: string; Icon: typeof ImageIcon }> = [
|
||||
@@ -91,6 +81,14 @@ const MENTION_TABS: Array<{ type: CreationRef["type"]; label: string; Icon: type
|
||||
{ type: "scene", label: "场景", Icon: FolderOpen },
|
||||
];
|
||||
|
||||
function mergeSessionUploads(results: CreationRef[], uploads: CreationRef[], query: string, type: CreationRef["type"]) {
|
||||
if (type !== "asset") return results;
|
||||
const q = query.trim().toLowerCase();
|
||||
const extras = uploads.filter((item) => !q || item.name.toLowerCase().includes(q));
|
||||
const seen = new Set(extras.map((item) => item.id));
|
||||
return [...extras, ...results.filter((item) => !seen.has(item.id))];
|
||||
}
|
||||
|
||||
function formatRelativeTime(iso: string) {
|
||||
const then = new Date(iso).getTime();
|
||||
if (Number.isNaN(then)) return "";
|
||||
@@ -103,10 +101,6 @@ function formatRelativeTime(iso: string) {
|
||||
return days < 30 ? `${days} 天前` : new Date(then).toLocaleDateString("zh-CN");
|
||||
}
|
||||
|
||||
function toOptions(values: string[]) {
|
||||
return values.map((value) => ({ value, label: value }));
|
||||
}
|
||||
|
||||
export function OmniCreatePage({
|
||||
navigate,
|
||||
onNotify,
|
||||
@@ -117,13 +111,12 @@ export function OmniCreatePage({
|
||||
const [outputMode, setOutputMode] = useState<OutputMode>("video");
|
||||
const [selectedCase, setSelectedCase] = useState<PresetItem | null>(null);
|
||||
const [prompt, setPrompt] = useState("");
|
||||
const [attachments, setAttachments] = useState<Attachment[]>([]);
|
||||
const [sessionUploads, setSessionUploads] = useState<CreationRef[]>([]);
|
||||
const [uploading, setUploading] = useState(false);
|
||||
const [model, setModel] = useState("Seedance 2.5");
|
||||
const [resolution, setResolution] = useState("1080p");
|
||||
const [ratio, setRatio] = useState("9:16");
|
||||
const [duration, setDuration] = useState("智能时长");
|
||||
const [customDurationOn, setCustomDurationOn] = useState(false);
|
||||
const [durationMenuOpen, setDurationMenuOpen] = useState(false);
|
||||
const [uploadMenuOpen, setUploadMenuOpen] = useState(false);
|
||||
const [mentionMenuOpen, setMentionMenuOpen] = useState(false);
|
||||
const [mentionTab, setMentionTab] = useState<CreationRef["type"]>("asset");
|
||||
@@ -143,16 +136,13 @@ export function OmniCreatePage({
|
||||
setResolution("模型默认");
|
||||
setRatio("1:1");
|
||||
setDuration("1 张");
|
||||
setCustomDurationOn(true);
|
||||
} else {
|
||||
setModel("Seedance 2.5");
|
||||
setResolution("1080p");
|
||||
setRatio("9:16");
|
||||
setDuration("智能时长");
|
||||
setCustomDurationOn(false);
|
||||
}
|
||||
setActiveCategory("all");
|
||||
setDurationMenuOpen(false);
|
||||
setSelectedCase((prev) => (prev && prev.mode !== outputMode ? null : prev));
|
||||
}, [outputMode]);
|
||||
|
||||
@@ -162,8 +152,7 @@ export function OmniCreatePage({
|
||||
if (toolsRef.current?.contains(target)) return;
|
||||
setUploadMenuOpen(false);
|
||||
setMentionMenuOpen(false);
|
||||
setDurationMenuOpen(false);
|
||||
};
|
||||
};
|
||||
document.addEventListener("mousedown", onDown);
|
||||
return () => document.removeEventListener("mousedown", onDown);
|
||||
}, []);
|
||||
@@ -186,12 +175,11 @@ export function OmniCreatePage({
|
||||
setMentionTab(type);
|
||||
setMentionMenuOpen(true);
|
||||
setUploadMenuOpen(false);
|
||||
setDurationMenuOpen(false);
|
||||
setMentionLoading(true);
|
||||
setMentionResults([]);
|
||||
try {
|
||||
const res = await api.searchMentions({ q: query, types: [type], limit: 12 });
|
||||
setMentionResults(res.results);
|
||||
setMentionResults(mergeSessionUploads(res.results, sessionUploads, query, type));
|
||||
setTypeLabels(res.type_labels);
|
||||
} catch (error) {
|
||||
onNotify?.("error", (error as Error).message);
|
||||
@@ -215,20 +203,39 @@ export function OmniCreatePage({
|
||||
setMentionMenuOpen(false);
|
||||
};
|
||||
|
||||
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const handleFileChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const files = Array.from(event.target.files || []);
|
||||
if (!files.length) return;
|
||||
setAttachments((prev) => [
|
||||
...prev,
|
||||
...files.map((file) => ({
|
||||
name: file.name,
|
||||
type: file.type,
|
||||
url: URL.createObjectURL(file),
|
||||
source: "local" as const,
|
||||
})),
|
||||
]);
|
||||
setUploadMenuOpen(false);
|
||||
event.target.value = "";
|
||||
setUploadMenuOpen(false);
|
||||
if (!files.length) return;
|
||||
const images = files.filter((file) => file.type.startsWith("image/"));
|
||||
if (images.length !== files.length) {
|
||||
onNotify?.("info", "全能创作仅支持上传图片");
|
||||
}
|
||||
if (!images.length) return;
|
||||
setUploading(true);
|
||||
try {
|
||||
for (const file of images) {
|
||||
const form = new FormData();
|
||||
form.append("file", file);
|
||||
const data = await api.uploadFreeVideoRef(form);
|
||||
const ref: CreationRef = {
|
||||
type: "asset",
|
||||
id: data.asset_id,
|
||||
name: data.name || file.name,
|
||||
cover: data.thumb_url || data.url,
|
||||
};
|
||||
setSessionUploads((prev) => (prev.some((item) => item.id === ref.id) ? prev : [...prev, ref]));
|
||||
setPendingRefs((prev) => {
|
||||
if (prev.some((item) => item.id === ref.id) || prev.length >= MENTION_REF_LIMIT) return prev;
|
||||
return [...prev, ref];
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
onNotify?.("error", (error as Error).message);
|
||||
} finally {
|
||||
setUploading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -273,24 +280,11 @@ export function OmniCreatePage({
|
||||
<X />
|
||||
</button>
|
||||
</span>
|
||||
))}{attachments.map((file, index) => (
|
||||
<span className="omni-attachment-chip" key={`${file.name}-${index}`}>
|
||||
{file.type.startsWith("video") ? <Video /> : <ImageIcon />}
|
||||
<span>{file.name}</span>
|
||||
<button
|
||||
type="button"
|
||||
className="omni-attachment-remove"
|
||||
aria-label={`删除 ${file.name}`}
|
||||
onClick={() => setAttachments((prev) => prev.filter((_, i) => i !== index))}
|
||||
>
|
||||
<X />
|
||||
</button>
|
||||
</span>
|
||||
))}</div>
|
||||
<textarea
|
||||
id="omniStartPrompt"
|
||||
rows={3}
|
||||
placeholder="描述你想制作的内容,@ 可引用商品、模特、场景或已有素材……"
|
||||
placeholder="描述你想制作的内容,@ 可引用商品、模特、场景、素材或刚上传的图片……"
|
||||
value={prompt}
|
||||
onChange={(event) => {
|
||||
const value = event.target.value;
|
||||
@@ -309,7 +303,6 @@ export function OmniCreatePage({
|
||||
onClick={() => {
|
||||
setUploadMenuOpen((open) => !open);
|
||||
setMentionMenuOpen(false);
|
||||
setDurationMenuOpen(false);
|
||||
}}
|
||||
>
|
||||
<Plus />
|
||||
@@ -328,14 +321,14 @@ export function OmniCreatePage({
|
||||
</button>
|
||||
<button type="button" onClick={() => fileInputRef.current?.click()}>
|
||||
<Upload />
|
||||
<span>本地上传<small>添加电脑中的图片或视频</small></span>
|
||||
<span>本地上传<small>添加电脑中的图片</small></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept="image/*,video/*"
|
||||
accept="image/*"
|
||||
multiple
|
||||
hidden
|
||||
onChange={handleFileChange}
|
||||
@@ -349,7 +342,6 @@ export function OmniCreatePage({
|
||||
if (mentionMenuOpen) setMentionMenuOpen(false);
|
||||
else void openMentions("", mentionTab);
|
||||
setUploadMenuOpen(false);
|
||||
setDurationMenuOpen(false);
|
||||
}}
|
||||
>
|
||||
@
|
||||
@@ -393,100 +385,25 @@ export function OmniCreatePage({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label className="omni-parameter omni-parameter-model">
|
||||
<CustomSelect
|
||||
fill
|
||||
size="sm"
|
||||
aria-label={outputMode === "video" ? "视频模型" : "图片模型"}
|
||||
value={model}
|
||||
onChange={setModel}
|
||||
options={toOptions(outputMode === "video" ? VIDEO_MODELS : IMAGE_MODELS)}
|
||||
/>
|
||||
</label>
|
||||
<label className={`omni-parameter${outputMode === "image" ? " is-hidden" : ""}`}>
|
||||
<CustomSelect
|
||||
fill
|
||||
size="sm"
|
||||
aria-label="分辨率"
|
||||
value={resolution}
|
||||
onChange={setResolution}
|
||||
options={toOptions(["1080p", "720p", "480p"])}
|
||||
/>
|
||||
</label>
|
||||
<label className="omni-parameter">
|
||||
<CustomSelect
|
||||
fill
|
||||
size="sm"
|
||||
aria-label="画面比例"
|
||||
value={ratio}
|
||||
onChange={setRatio}
|
||||
options={toOptions(RATIOS)}
|
||||
/>
|
||||
</label>
|
||||
<div className={`omni-duration-control${outputMode === "image" ? " is-image-count" : ""}`}>
|
||||
<button
|
||||
type="button"
|
||||
className="omni-duration-trigger"
|
||||
aria-expanded={durationMenuOpen}
|
||||
onClick={() => {
|
||||
setDurationMenuOpen((open) => !open);
|
||||
setUploadMenuOpen(false);
|
||||
setMentionMenuOpen(false);
|
||||
}}
|
||||
>
|
||||
<span>{duration}</span>
|
||||
<ChevronDown />
|
||||
</button>
|
||||
<div className="omni-duration-menu" hidden={!durationMenuOpen}>
|
||||
<span className="omni-duration-title">{outputMode === "image" ? "生成张数" : "时长"}</span>
|
||||
<div className="omni-duration-modes">
|
||||
<button
|
||||
type="button"
|
||||
className={!customDurationOn ? "active" : ""}
|
||||
onClick={() => {
|
||||
setCustomDurationOn(false);
|
||||
setDuration("智能时长");
|
||||
setDurationMenuOpen(false);
|
||||
}}
|
||||
>
|
||||
<Sparkles />
|
||||
<span>智能时长</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={customDurationOn ? "active" : ""}
|
||||
onClick={() => setCustomDurationOn(true)}
|
||||
>
|
||||
<SlidersHorizontal />
|
||||
<span>自定义时长</span>
|
||||
</button>
|
||||
</div>
|
||||
<div className="omni-duration-values" hidden={outputMode === "video" && !customDurationOn}>
|
||||
{(outputMode === "image" ? IMAGE_COUNTS : VIDEO_DURATIONS).map((value) => (
|
||||
<button
|
||||
type="button"
|
||||
key={value}
|
||||
className={duration === value ? "active" : ""}
|
||||
onClick={() => {
|
||||
setDuration(value);
|
||||
setCustomDurationOn(true);
|
||||
setDurationMenuOpen(false);
|
||||
}}
|
||||
>
|
||||
{value}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<OmniParamBar
|
||||
isVideo={outputMode === "video"}
|
||||
model={model}
|
||||
resolution={resolution}
|
||||
ratio={ratio}
|
||||
duration={duration}
|
||||
onModel={setModel}
|
||||
onResolution={setResolution}
|
||||
onRatio={setRatio}
|
||||
onDuration={setDuration}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="omni-start-generate"
|
||||
disabled={starting}
|
||||
disabled={starting || uploading}
|
||||
onClick={() => {
|
||||
const text = prompt.trim();
|
||||
if (!text && !selectedCase && attachments.length === 0) {
|
||||
if (!text && !selectedCase && pendingRefs.length === 0) {
|
||||
onNotify?.("info", "请先输入你想制作的内容,或选择一个创作预设");
|
||||
return;
|
||||
}
|
||||
@@ -508,7 +425,7 @@ export function OmniCreatePage({
|
||||
})
|
||||
.then((conversation) => {
|
||||
// 首条消息交给对话页发,避免这里再复制一份 SSE 消费逻辑
|
||||
navigate("omniSession", { conversationId: conversation.id, firstMessage: text, firstRefs: pendingRefs });
|
||||
navigate("omniSession", { conversationId: conversation.id, firstMessage: text, firstRefs: pendingRefs, firstUploads: sessionUploads });
|
||||
})
|
||||
.catch((error) => {
|
||||
onNotify?.("error", (error as Error).message);
|
||||
@@ -516,7 +433,7 @@ export function OmniCreatePage({
|
||||
});
|
||||
}}
|
||||
>
|
||||
{starting ? "正在创建…" : "开始创作"}
|
||||
{uploading ? "图片上传中…" : starting ? "正在创建…" : "开始创作"}
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
@@ -678,10 +595,6 @@ export function OmniHistoryPage({
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<button type="button" className="omni-history-new" onClick={() => navigate("omniCreate")}>
|
||||
<Plus />
|
||||
新建会话
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
<div className="omni-history-list">
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
X,
|
||||
} from "lucide-react";
|
||||
import { api } from "../api";
|
||||
import { OmniParamBar } from "../components/omni-param-bar";
|
||||
import { MediaLightbox } from "../components/overlays";
|
||||
import type {
|
||||
CreationConversationDetail,
|
||||
@@ -149,14 +150,10 @@ function StrategyCard({ payload }: { payload: Record<string, unknown> }) {
|
||||
}
|
||||
|
||||
type PlanTimelineItem = { start: number; end: number; stage: string; desc?: string };
|
||||
type PlanMatrixRow = { point: string; hits: number[] };
|
||||
|
||||
function PlanCard({ payload }: { payload: Record<string, unknown> }) {
|
||||
const points = (payload.points as string[] | undefined) || [];
|
||||
const timeline = (payload.timeline as PlanTimelineItem[] | undefined) || [];
|
||||
const matrix = (payload.matrix as { shots?: number; rows?: PlanMatrixRow[] } | undefined) || {};
|
||||
const shots = matrix.shots || 4;
|
||||
const rows = matrix.rows || [];
|
||||
const voice = (payload.voice_chars as number[] | undefined) || [];
|
||||
const format = (value: number) => (Number.isInteger(value) ? String(value) : value.toFixed(1));
|
||||
|
||||
@@ -196,33 +193,6 @@ function PlanCard({ payload }: { payload: Record<string, unknown> }) {
|
||||
</div>
|
||||
</section>
|
||||
) : null}
|
||||
{rows.length > 0 ? (
|
||||
<section className="omni-plan-section">
|
||||
<strong>卖点覆盖矩阵</strong>
|
||||
<table className="omni-plan-matrix">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>卖点</th>
|
||||
{Array.from({ length: shots }, (_, i) => (
|
||||
<th key={i}>镜头 {i + 1}</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows.map((row) => (
|
||||
<tr key={row.point}>
|
||||
<td>{row.point}</td>
|
||||
{Array.from({ length: shots }, (_, i) => (
|
||||
<td key={i} className={row.hits?.includes(i + 1) ? "hit" : undefined}>
|
||||
{row.hits?.includes(i + 1) ? "✓" : ""}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
) : null}
|
||||
<div className="omni-plan-summary">
|
||||
{voice.length === 2 ? (
|
||||
<span>
|
||||
@@ -426,24 +396,83 @@ function ElicitCard({
|
||||
* 确认闸门:方案卡下面那条「开始生成 · 约 N 积分」。
|
||||
* 点一次就锁住 —— 连点两下后端会 409,但前端也不该让它发生第二次。
|
||||
*/
|
||||
function asStringMap(value: unknown): Record<string, string> {
|
||||
if (!value || typeof value !== "object") return {};
|
||||
const out: Record<string, string> = {};
|
||||
for (const [key, item] of Object.entries(value as Record<string, unknown>)) {
|
||||
if (item != null && item !== "") out[key] = String(item);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function paramLine(params: Record<string, string>, isVideo: boolean) {
|
||||
|
||||
return [
|
||||
params.model,
|
||||
isVideo ? params.resolution : "",
|
||||
params.ratio,
|
||||
isVideo ? params.duration : params.count,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" · ");
|
||||
}
|
||||
|
||||
function ConfirmCard({
|
||||
message,
|
||||
sessionParams,
|
||||
isVideo,
|
||||
disabled,
|
||||
onConfirm,
|
||||
}: {
|
||||
message: CreationMessage;
|
||||
sessionParams: Record<string, string>;
|
||||
isVideo: boolean;
|
||||
disabled: boolean;
|
||||
onConfirm: () => void;
|
||||
onConfirm: (params: Record<string, string>) => void;
|
||||
}) {
|
||||
const submitted = Boolean(message.payload.submitted);
|
||||
const credits = Number(message.payload.estimated_credits || 0);
|
||||
const payloadParams = asStringMap(message.payload.params);
|
||||
const snapshot = { ...sessionParams, ...payloadParams };
|
||||
const [draft, setDraft] = useState(snapshot);
|
||||
const cardIsVideo = message.payload.kind !== "image" && isVideo;
|
||||
const durationChanged =
|
||||
cardIsVideo && Boolean(draft.duration) && Boolean(snapshot.duration) && draft.duration !== snapshot.duration;
|
||||
const setField = (key: string, value: string) => setDraft((prev) => ({ ...prev, [key]: value }));
|
||||
const summary = paramLine(draft, cardIsVideo) || "当前参数";
|
||||
|
||||
return (
|
||||
<section className="omni-confirm-card">
|
||||
<span>{submitted ? "已确认,正在出片" : "方案确认后即可出片,中途不再打断"}</span>
|
||||
<button type="button" disabled={disabled || submitted} onClick={onConfirm}>
|
||||
{String(message.payload.label || "开始生成")}
|
||||
{credits > 0 ? <i>约 {credits} 积分</i> : null}
|
||||
</button>
|
||||
<div className="omni-confirm-copy">
|
||||
<strong>{submitted ? "已确认" : `即将用 ${summary} 生成`}</strong>
|
||||
<span>{submitted ? "正在提交生成" : "确认前可以改参数。改时长会按新时长重写脚本。"}</span>
|
||||
</div>
|
||||
{submitted ? null : (
|
||||
<div className="omni-confirm-params">
|
||||
<OmniParamBar
|
||||
isVideo={cardIsVideo}
|
||||
disabled={disabled}
|
||||
model={draft.model || ""}
|
||||
resolution={draft.resolution || ""}
|
||||
ratio={draft.ratio || ""}
|
||||
duration={cardIsVideo ? (draft.duration || "") : (draft.count || "")}
|
||||
onModel={(value) => setField("model", value)}
|
||||
onResolution={(value) => setField("resolution", value)}
|
||||
onRatio={(value) => setField("ratio", value)}
|
||||
onDuration={(value) => setField(cardIsVideo ? "duration" : "count", value)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{durationChanged ? (
|
||||
<p className="omni-confirm-hint">改时长会重新生成脚本。确认后先重写方案,不会直接出片。</p>
|
||||
) : null}
|
||||
<div className="omni-confirm-foot">
|
||||
<span>{submitted ? "已确认,正在出片" : "点确认后才会提交生成"}</span>
|
||||
<button type="button" disabled={disabled || submitted} onClick={() => onConfirm(draft)}>
|
||||
{durationChanged ? "确认并重写脚本" : String(message.payload.label || "开始生成")}
|
||||
{!durationChanged && credits > 0 ? <i>约 {credits} 积分</i> : null}
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -592,12 +621,21 @@ const MENTION_TABS: Array<{ type: CreationRef["type"]; label: string; Icon: type
|
||||
{ type: "scene", label: "场景", Icon: FolderOpen },
|
||||
];
|
||||
|
||||
function mergeSessionUploads(results: CreationRef[], uploads: CreationRef[], query: string, type: CreationRef["type"]) {
|
||||
if (type !== "asset") return results;
|
||||
const q = query.trim().toLowerCase();
|
||||
const extras = uploads.filter((item) => !q || item.name.toLowerCase().includes(q));
|
||||
const seen = new Set(extras.map((item) => item.id));
|
||||
return [...extras, ...results.filter((item) => !seen.has(item.id))];
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────── 页面
|
||||
|
||||
export function OmniSessionPage({
|
||||
conversationId,
|
||||
firstMessage,
|
||||
firstRefs,
|
||||
firstUploads,
|
||||
navigate,
|
||||
onNotify,
|
||||
}: {
|
||||
@@ -605,6 +643,7 @@ export function OmniSessionPage({
|
||||
/** 首页「开始创作」带过来的第一句话。只在刚进页面时发一次,刷新后不重发(它已经在库里)。 */
|
||||
firstMessage?: string;
|
||||
firstRefs?: CreationRef[];
|
||||
firstUploads?: CreationRef[];
|
||||
navigate: NavigateFn;
|
||||
onNotify?: (type: "success" | "error" | "info", text: string) => void;
|
||||
}) {
|
||||
@@ -612,6 +651,9 @@ export function OmniSessionPage({
|
||||
const [messages, setMessages] = useState<CreationMessage[]>([]);
|
||||
const [prompt, setPrompt] = useState("");
|
||||
const [pendingRefs, setPendingRefs] = useState<CreationRef[]>([]);
|
||||
const [sessionUploads, setSessionUploads] = useState<CreationRef[]>(firstUploads || []);
|
||||
const [uploading, setUploading] = useState(false);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const [streaming, setStreaming] = useState(false);
|
||||
const [liveText, setLiveText] = useState("");
|
||||
const [activeTool, setActiveTool] = useState("");
|
||||
@@ -708,17 +750,29 @@ export function OmniSessionPage({
|
||||
// firstSentRef 挡住 StrictMode 的双次挂载,否则会重复发一条。
|
||||
useEffect(() => {
|
||||
if (!conversation || firstSentRef.current) return;
|
||||
if (!firstMessage?.trim() || conversation.messages.length > 0) {
|
||||
const text = firstMessage?.trim() || "";
|
||||
const refs = firstRefs || [];
|
||||
if ((!text && refs.length === 0) || conversation.messages.length > 0) {
|
||||
firstSentRef.current = true;
|
||||
return;
|
||||
}
|
||||
firstSentRef.current = true;
|
||||
void send({ kind: "text", text: firstMessage.trim(), refs: firstRefs || [] });
|
||||
void send({ kind: "text", text, refs });
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [conversation, firstMessage]);
|
||||
|
||||
useEffect(() => () => abortRef.current?.abort(), []);
|
||||
|
||||
useEffect(() => {
|
||||
const fromHistory = messages.flatMap((message) => message.refs || []).filter((ref) => ref.type === "asset");
|
||||
if (!fromHistory.length) return;
|
||||
setSessionUploads((prev) => {
|
||||
const seen = new Set(prev.map((item) => item.id));
|
||||
const extra = fromHistory.filter((item) => !seen.has(item.id));
|
||||
return extra.length ? [...prev, ...extra] : prev;
|
||||
});
|
||||
}, [messages]);
|
||||
|
||||
const hasGenerating = useMemo(
|
||||
() => messages.some((message) => message.kind === "generating"),
|
||||
[messages]
|
||||
@@ -863,18 +917,32 @@ export function OmniSessionPage({
|
||||
[conversationId, applyEvent, notify]
|
||||
);
|
||||
|
||||
const handleConfirm = async (message: CreationMessage) => {
|
||||
const handleConfirm = async (message: CreationMessage, nextParams: Record<string, string>) => {
|
||||
if (confirming) return;
|
||||
setConfirming(true);
|
||||
try {
|
||||
const { message: generating } = await api.confirmCreationPlan(conversationId, message.id);
|
||||
// 闸门置灰 + 追加「生成中」卡;结果由轮询回填
|
||||
setMessages((prev) => [
|
||||
...prev.map((m) =>
|
||||
m.id === message.id ? { ...m, payload: { ...m.payload, submitted: true } } : m
|
||||
),
|
||||
generating,
|
||||
]);
|
||||
const result = await api.confirmCreationPlan(conversationId, message.id, nextParams);
|
||||
setMessages((prev) =>
|
||||
prev.map((m) =>
|
||||
m.id === message.id
|
||||
? { ...m, payload: { ...m.payload, submitted: true, params: nextParams } }
|
||||
: m
|
||||
)
|
||||
);
|
||||
setConversation((prev) =>
|
||||
prev ? { ...prev, params: { ...prev.params, ...nextParams } } : prev
|
||||
);
|
||||
if (result.regenerate) {
|
||||
notify("info", `时长已改为 ${nextParams.duration || ""},正在按新时长重写脚本`);
|
||||
void send({
|
||||
kind: "text",
|
||||
text: `时长改成了${nextParams.duration},请按新参数重新写方案,旧方案作废`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (result.message) {
|
||||
setMessages((prev) => [...prev, result.message as CreationMessage]);
|
||||
}
|
||||
} catch (error) {
|
||||
notify("error", (error as Error).message);
|
||||
} finally {
|
||||
@@ -885,7 +953,7 @@ export function OmniSessionPage({
|
||||
const handleSend = () => {
|
||||
// 顺序要紧:先判 streaming 再清输入框。反过来的话,流式期间敲一次回车
|
||||
// 会把已经打好的内容清空,消息却没发出去。
|
||||
if (streaming) return;
|
||||
if (streaming || uploading) return;
|
||||
const text = prompt.trim();
|
||||
if (!text && pendingRefs.length === 0) return;
|
||||
setPrompt("");
|
||||
@@ -902,7 +970,7 @@ export function OmniSessionPage({
|
||||
setMentionResults([]);
|
||||
try {
|
||||
const res = await api.searchMentions({ q: query, types: [type], limit: 12 });
|
||||
setMentionResults(res.results);
|
||||
setMentionResults(mergeSessionUploads(res.results, sessionUploads, query, type));
|
||||
setTypeLabels(res.type_labels);
|
||||
} catch (error) {
|
||||
notify("error", (error as Error).message);
|
||||
@@ -971,8 +1039,10 @@ export function OmniSessionPage({
|
||||
<ConfirmCard
|
||||
key={message.id}
|
||||
message={message}
|
||||
sessionParams={params}
|
||||
isVideo={isVideo}
|
||||
disabled={streaming || confirming}
|
||||
onConfirm={() => void handleConfirm(message)}
|
||||
onConfirm={(nextParams) => void handleConfirm(message, nextParams)}
|
||||
/>
|
||||
);
|
||||
case "generating":
|
||||
@@ -1086,9 +1156,9 @@ export function OmniSessionPage({
|
||||
<textarea
|
||||
id="omniSessionPrompt"
|
||||
rows={2}
|
||||
placeholder="回复创作助手,也可以继续补充图片、视频或要求……"
|
||||
placeholder="回复创作助手,也可以继续补充图片或要求……"
|
||||
value={prompt}
|
||||
disabled={streaming}
|
||||
disabled={streaming || uploading}
|
||||
onChange={(event) => {
|
||||
const value = event.target.value;
|
||||
setPrompt(value);
|
||||
@@ -1134,15 +1204,55 @@ export function OmniSessionPage({
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setUploadMenuOpen(false);
|
||||
notify("info", "本地上传稍后接入");
|
||||
fileInputRef.current?.click();
|
||||
}}
|
||||
>
|
||||
<Upload />
|
||||
<span>
|
||||
本地上传<small>添加电脑中的图片或视频</small>
|
||||
本地上传<small>添加电脑中的图片</small>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
multiple
|
||||
hidden
|
||||
onChange={async (event) => {
|
||||
const files = Array.from(event.target.files || []);
|
||||
event.target.value = "";
|
||||
if (!files.length) return;
|
||||
const images = files.filter((file) => file.type.startsWith("image/"));
|
||||
if (images.length !== files.length) {
|
||||
notify("info", "全能创作仅支持上传图片");
|
||||
}
|
||||
if (!images.length) return;
|
||||
setUploading(true);
|
||||
try {
|
||||
for (const file of images) {
|
||||
const form = new FormData();
|
||||
form.append("file", file);
|
||||
const data = await api.uploadFreeVideoRef(form);
|
||||
const ref: CreationRef = {
|
||||
type: "asset",
|
||||
id: data.asset_id,
|
||||
name: data.name || file.name,
|
||||
cover: data.thumb_url || data.url,
|
||||
};
|
||||
setSessionUploads((prev) => (prev.some((item) => item.id === ref.id) ? prev : [...prev, ref]));
|
||||
setPendingRefs((prev) => {
|
||||
if (prev.some((item) => item.id === ref.id) || prev.length >= MENTION_REF_LIMIT) return prev;
|
||||
return [...prev, ref];
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
notify("error", (error as Error).message);
|
||||
} finally {
|
||||
setUploading(false);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="omni-session-mention-wrap" ref={mentionWrapRef}>
|
||||
<button
|
||||
@@ -1199,7 +1309,7 @@ export function OmniSessionPage({
|
||||
type="button"
|
||||
className="omni-session-send"
|
||||
aria-label="发送"
|
||||
disabled={streaming}
|
||||
disabled={streaming || uploading}
|
||||
onClick={handleSend}
|
||||
>
|
||||
<ArrowUp />
|
||||
|
||||
@@ -55,6 +55,7 @@ export type ResolvedRoute = {
|
||||
// 刷新后它已经在库里了,再发一遍会重复。
|
||||
firstMessage?: string;
|
||||
firstRefs?: import("../types").CreationRef[];
|
||||
firstUploads?: import("../types").CreationRef[];
|
||||
hash?: string;
|
||||
// 视频项目页初始 tab(all/wip/done/fail):由 navigate 透传,刷新/前进后退不持久(回默认 all)。
|
||||
tab?: string;
|
||||
@@ -67,6 +68,7 @@ export type NavigateOptions = {
|
||||
conversationId?: string;
|
||||
firstMessage?: string;
|
||||
firstRefs?: import("../types").CreationRef[];
|
||||
firstUploads?: import("../types").CreationRef[];
|
||||
replace?: boolean;
|
||||
hash?: string;
|
||||
// 视频项目页初始 tab(all/wip/done/fail):仅经 route state 透传给 ProjectsPage,不进 URL path。
|
||||
|
||||
Reference in New Issue
Block a user