优化已发现问题
This commit is contained in:
@@ -360,13 +360,34 @@
|
||||
|
||||
.omni-result-card,
|
||||
.omni-process-card {
|
||||
width: min(760px, calc(100% - 44px));
|
||||
box-sizing: border-box;
|
||||
width: min(320px, calc(100% - 44px));
|
||||
max-width: 320px;
|
||||
margin: 0 0 24px 44px;
|
||||
border: 1px solid rgba(34, 42, 54, .09);
|
||||
border-radius: 15px;
|
||||
background: #fff;
|
||||
box-shadow: 0 10px 26px rgba(20, 27, 38, .06);
|
||||
animation: omniMessageIn 220ms ease both;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.omni-result-card.has-multiple,
|
||||
.omni-process-card.has-multiple {
|
||||
width: min(520px, calc(100% - 44px));
|
||||
max-width: 520px;
|
||||
}
|
||||
|
||||
.omni-result-card.has-single.is-ratio-16-9,
|
||||
.omni-process-card.has-single.is-ratio-16-9 {
|
||||
width: min(480px, calc(100% - 44px));
|
||||
max-width: 480px;
|
||||
}
|
||||
|
||||
.omni-result-card.has-single.is-ratio-1-1,
|
||||
.omni-process-card.has-single.is-ratio-1-1 {
|
||||
width: min(340px, calc(100% - 44px));
|
||||
max-width: 340px;
|
||||
}
|
||||
|
||||
.omni-strategy-card,
|
||||
@@ -1167,7 +1188,10 @@
|
||||
}
|
||||
|
||||
.omni-process-frame {
|
||||
height: 310px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
aspect-ratio: 9 / 16;
|
||||
min-height: 200px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
@@ -1178,6 +1202,14 @@
|
||||
animation: omniShimmer 1.2s ease infinite;
|
||||
}
|
||||
|
||||
.is-ratio-16-9 .omni-process-frame {
|
||||
aspect-ratio: 16 / 9;
|
||||
}
|
||||
|
||||
.is-ratio-1-1 .omni-process-frame {
|
||||
aspect-ratio: 1 / 1;
|
||||
}
|
||||
|
||||
.omni-process-frame strong {
|
||||
color: #5c6570;
|
||||
font-size: 14px;
|
||||
@@ -1211,6 +1243,8 @@
|
||||
|
||||
.omni-result-media.is-grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.omni-result-tile {
|
||||
@@ -1220,11 +1254,22 @@
|
||||
border-radius: var(--r-md, 12px);
|
||||
background: #edf0f4;
|
||||
box-shadow: 0 8px 20px rgba(20, 27, 38, .08);
|
||||
aspect-ratio: 9 / 16;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.is-ratio-16-9 .omni-result-tile {
|
||||
aspect-ratio: 16 / 9;
|
||||
}
|
||||
|
||||
.is-ratio-1-1 .omni-result-tile {
|
||||
aspect-ratio: 1 / 1;
|
||||
}
|
||||
|
||||
.omni-result-preview {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
@@ -1233,18 +1278,19 @@
|
||||
|
||||
.omni-result-tile img {
|
||||
width: 100%;
|
||||
height: 310px;
|
||||
height: 100%;
|
||||
aspect-ratio: inherit;
|
||||
display: block;
|
||||
object-fit: cover;
|
||||
background: #edf0f4;
|
||||
}
|
||||
|
||||
.omni-result-media.is-grid .omni-result-tile img {
|
||||
height: 220px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.omni-result-media.is-grid .omni-process-frame {
|
||||
height: 220px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.omni-result-play {
|
||||
@@ -1731,14 +1777,36 @@
|
||||
background: var(--klein);
|
||||
color: #fff;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 160ms ease;
|
||||
}
|
||||
|
||||
.omni-elicit-actions button.cancel-btn {
|
||||
background: var(--surface, #fff);
|
||||
color: var(--text-secondary, #606a78);
|
||||
border: 1px solid var(--border-faint, #e2e8f0);
|
||||
}
|
||||
|
||||
.omni-elicit-actions button.cancel-btn:hover:not(:disabled) {
|
||||
background: var(--background-hover, #f8fafc);
|
||||
color: var(--accent-black, #1a202c);
|
||||
border-color: var(--border-subtle, #cbd5e1);
|
||||
}
|
||||
|
||||
.omni-elicit-actions button:disabled {
|
||||
background: rgba(34, 42, 54, .18);
|
||||
color: rgba(255, 255, 255, .7);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.omni-elicit-actions button.cancel-btn:disabled {
|
||||
background: var(--surface, #fff);
|
||||
color: var(--text-tertiary, #94a3b8);
|
||||
border-color: var(--border-faint, #e2e8f0);
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
/* 已提交的追问卡变成只读回顾,不能再改 —— 重复提交后端会 409 */
|
||||
.omni-elicit-card.is-submitted .omni-elicit-options button,
|
||||
.omni-elicit-card.is-submitted .omni-elicit-assets button {
|
||||
@@ -2703,6 +2771,47 @@
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.omni-person-generate-panel {
|
||||
display: grid;
|
||||
width: 100%;
|
||||
gap: 8px;
|
||||
margin-top: 4px;
|
||||
padding: 12px;
|
||||
border-radius: var(--r-md);
|
||||
background: var(--background-lighter);
|
||||
box-shadow: inset 0 0 0 1px var(--border-faint);
|
||||
}
|
||||
|
||||
.omni-person-generate-panel label {
|
||||
color: var(--accent-black);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.omni-person-generate-panel .textarea {
|
||||
width: 100%;
|
||||
min-height: 88px;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.omni-person-generate-panel > div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.omni-person-generate-panel > div > span {
|
||||
flex: 1 1 auto;
|
||||
color: var(--black-alpha-48);
|
||||
font-size: 11px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.omni-person-generate-panel > div > button {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
/* 单选追问直接点选即提交;长方向文案纵向铺开,避免挤成难扫读的小胶囊。 */
|
||||
.omni-chat-choice-actions {
|
||||
width: 100%;
|
||||
@@ -3079,6 +3188,105 @@
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.omni-model-picker-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.omni-model-picker-card {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 8px;
|
||||
border: 0;
|
||||
border-radius: var(--r-md);
|
||||
background: var(--surface);
|
||||
color: var(--accent-black);
|
||||
box-shadow: inset 0 0 0 1px var(--border-faint);
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
transition: background var(--t-base), box-shadow var(--t-base);
|
||||
}
|
||||
|
||||
.omni-model-picker-card:hover,
|
||||
.omni-model-picker-card.is-selected {
|
||||
background: var(--background-lighter);
|
||||
box-shadow: inset 0 0 0 1px var(--heat-40);
|
||||
}
|
||||
|
||||
.omni-model-picker-card.is-selected {
|
||||
color: var(--heat);
|
||||
}
|
||||
|
||||
.omni-model-picker-media {
|
||||
display: grid;
|
||||
width: 100%;
|
||||
aspect-ratio: 3 / 4;
|
||||
place-items: center;
|
||||
overflow: hidden;
|
||||
border-radius: 6px;
|
||||
background: var(--background-base);
|
||||
color: var(--black-alpha-48);
|
||||
}
|
||||
|
||||
.omni-model-picker-media img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.omni-model-picker-media svg {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.omni-model-picker-copy {
|
||||
display: block;
|
||||
min-width: 0;
|
||||
padding: 0 2px 2px;
|
||||
}
|
||||
|
||||
.omni-model-picker-copy strong,
|
||||
.omni-model-picker-copy small {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.omni-model-picker-copy strong {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.omni-model-picker-copy small {
|
||||
margin-top: 2px;
|
||||
color: var(--black-alpha-48);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.omni-model-picker-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 12px 20px;
|
||||
}
|
||||
|
||||
.omni-model-picker-selection {
|
||||
min-width: 0;
|
||||
flex: 1 1 auto;
|
||||
overflow: hidden;
|
||||
color: var(--black-alpha-48);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 11px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@media (min-width: 1101px) {
|
||||
.omni-session-page.has-assets-panel {
|
||||
box-sizing: border-box;
|
||||
@@ -3130,4 +3338,13 @@
|
||||
.omni-assets-grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.omni-person-generate-panel > div {
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.omni-person-generate-panel > div > button {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@ import type {
|
||||
CreationMessage,
|
||||
CreationRef,
|
||||
ModelConfig,
|
||||
ModelEntity,
|
||||
} from "../types";
|
||||
import type { NavigateFn } from "./route-config";
|
||||
|
||||
@@ -219,6 +220,14 @@ function contextualReplyOptions(text: string): ReplyOption[] {
|
||||
{ label: "你来推荐", text: "你根据当前需求推荐一款" },
|
||||
];
|
||||
}
|
||||
// 角色/人物图已生成时,询问用户是否使用该角色,不再提供「由你设定角色/上传人物图/不需要人物」
|
||||
if (/是否使用这[个位]角色|角色.{0,8}(?:已生成|合适吗)|你看这[个位]角色|人物参考已生成/i.test(guidance)) {
|
||||
return [
|
||||
{ label: "使用这个角色", text: "使用这个角色继续创作" },
|
||||
{ label: "重新生成一个", text: "重新生成一个角色" },
|
||||
{ label: "上传其他人物", text: "我上传人物参考图", action: "upload" },
|
||||
];
|
||||
}
|
||||
if (/(?:哪位|哪个|什么|选择|选|换|更换|调整|修改|改).{0,12}(?:人物|角色|模特|出镜)|(?:人物|角色|模特|出镜).{0,12}(?:哪位|哪个|选择|选|换|更换|调整|修改|改)/i.test(guidance)) {
|
||||
return [
|
||||
{ label: "上传人物图", text: "我上传人物参考图", action: "upload" },
|
||||
@@ -1143,6 +1152,7 @@ function ElicitCard({
|
||||
onSubmit,
|
||||
onChatAnswer,
|
||||
onPersonSourceAction,
|
||||
onUpload,
|
||||
}: {
|
||||
message: CreationMessage;
|
||||
disabled: boolean;
|
||||
@@ -1152,7 +1162,11 @@ function ElicitCard({
|
||||
/** 普通追问直接在气泡下回答,仍走 text 链路以保留用户消息与上下文。 */
|
||||
onChatAnswer: (text: string) => void;
|
||||
/** 人物来源三选一要分别打开系统文件、模特库和平台生成流程。 */
|
||||
onPersonSourceAction: (source: "local_upload" | "model_library" | "platform_generate") => void;
|
||||
onPersonSourceAction: (
|
||||
source: "local_upload" | "model_library" | "platform_generate",
|
||||
personPrompt?: string,
|
||||
) => void;
|
||||
onUpload?: () => void;
|
||||
}) {
|
||||
const fields = ((message.payload.fields as CreationField[] | undefined) || []).filter(Boolean);
|
||||
const submitted = Boolean(message.payload.submitted);
|
||||
@@ -1164,6 +1178,8 @@ function ElicitCard({
|
||||
const [assetPicked, setAssetPicked] = useState<Record<string, CreationRef>>({});
|
||||
const [customDirection, setCustomDirection] = useState("");
|
||||
const [chatAnswer, setChatAnswer] = useState("");
|
||||
const [personPromptOpen, setPersonPromptOpen] = useState(false);
|
||||
const [personPrompt, setPersonPrompt] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
if (submitted || interaction === "chat" || interaction === "step_confirm") return;
|
||||
@@ -1210,15 +1226,21 @@ function ElicitCard({
|
||||
if (interaction === "person_source_gate") {
|
||||
const selected = String(saved.person_source || "");
|
||||
const selectedLabel = fields[0]?.options?.find((option) => option.value === selected)?.label;
|
||||
const isPet = Boolean(
|
||||
message.payload?.is_pet ||
|
||||
message.text?.includes("宠物")
|
||||
);
|
||||
const libraryOption = fields[0]?.options?.find((option) => option.value === "model_library");
|
||||
const libraryLabel = libraryOption?.label || (isPet ? "从角色库选择" : "从模特库选择");
|
||||
return (
|
||||
<div className="omni-chat-row agent">
|
||||
<span className="omni-chat-avatar">
|
||||
<Sparkles />
|
||||
</span>
|
||||
<div className="omni-chat-bubble omni-gate-bubble">
|
||||
<ChatMarkdown text={message.text || "先选定出镜人物。"} />
|
||||
<ChatMarkdown text={message.text || (isPet ? "先选定出镜宠物角色。" : "先选定出镜人物。")} />
|
||||
{!submitted ? (
|
||||
<div className="omni-gate-actions" aria-label="人物来源选择">
|
||||
<div className="omni-gate-actions" aria-label={isPet ? "宠物角色来源选择" : "人物来源选择"}>
|
||||
<button
|
||||
type="button"
|
||||
className="primary"
|
||||
@@ -1232,19 +1254,55 @@ function ElicitCard({
|
||||
disabled={disabled}
|
||||
onClick={() => onPersonSourceAction("model_library")}
|
||||
>
|
||||
从模特库选择
|
||||
{libraryLabel}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
disabled={disabled}
|
||||
onClick={() => onPersonSourceAction("platform_generate")}
|
||||
aria-expanded={personPromptOpen}
|
||||
onClick={() => setPersonPromptOpen((open) => !open)}
|
||||
>
|
||||
平台帮忙生成
|
||||
</button>
|
||||
{personPromptOpen ? (
|
||||
<div className="omni-person-generate-panel">
|
||||
<label htmlFor={`person-prompt-${message.id}`}>
|
||||
{isPet ? "描述想要的宠物外观或品种(可选)" : "描述想要的角色外观(可选)"}
|
||||
</label>
|
||||
<textarea
|
||||
id={`person-prompt-${message.id}`}
|
||||
className="textarea"
|
||||
value={personPrompt}
|
||||
disabled={disabled}
|
||||
maxLength={500}
|
||||
placeholder={
|
||||
isPet
|
||||
? "例如:呆萌可爱的金毛幼犬,毛发蓬松干净,系着小红领巾,眼神灵动"
|
||||
: "例如:25岁左右女性,短发,干练通勤风,表情自然"
|
||||
}
|
||||
onChange={(event) => setPersonPrompt(event.target.value)}
|
||||
/>
|
||||
<div>
|
||||
<span>
|
||||
{isPet
|
||||
? "不填写也可以,平台会结合当前商品和拟人主题自动设计萌宠角色。"
|
||||
: "不填写也可以,平台会结合当前商品和创作主题自动设计角色。"}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
className="primary"
|
||||
disabled={disabled}
|
||||
onClick={() => onPersonSourceAction("platform_generate", personPrompt.trim())}
|
||||
>
|
||||
{isPet ? "开始生成宠物角色" : "开始生成角色"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
) : (
|
||||
<p className="omni-gate-answered">
|
||||
{selectedLabel ? `已选择:${selectedLabel}` : "人物来源已确定"}
|
||||
{selectedLabel ? `已选择:${selectedLabel}` : isPet ? "宠物角色已确定" : "人物来源已确定"}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
@@ -1441,6 +1499,10 @@ function ElicitCard({
|
||||
className={index === 0 ? "primary" : ""}
|
||||
disabled={disabled}
|
||||
onClick={() => {
|
||||
if (option.value === "upload" || /上传/.test(option.label)) {
|
||||
onUpload?.();
|
||||
return;
|
||||
}
|
||||
if (!gateField?.key) return;
|
||||
onSubmit({ [gateField.key]: option.value }, []);
|
||||
}}
|
||||
@@ -1462,11 +1524,37 @@ function ElicitCard({
|
||||
if (interaction === "chat") {
|
||||
const question = message.text || fields[0]?.label || "这项你想怎么定?";
|
||||
const compactChoiceOnly = String(message.payload.topic || "") === "cast_relation";
|
||||
const choiceField = fields.find(
|
||||
(field) => field.type === "single" && Array.isArray(field.options) && field.options.length > 0,
|
||||
// 兼容多种 options 来源:单选、多选、或字段内直接携带 options
|
||||
const rawChoiceField = fields.find(
|
||||
(field) => Array.isArray(field.options) && field.options.length > 0,
|
||||
);
|
||||
// 如果是痛点方向追问、或文案明确提及「直接点击一个选项」,但在 fields 里未能取到 options,提供兜底选项
|
||||
const isPainPointQuestion =
|
||||
fields[0]?.key === "pain_point_direction"
|
||||
|| /痛点方向|最想拍的痛点|解决的痛点/.test(question)
|
||||
|| /直接点击一个选项/.test(question);
|
||||
const fallbackOptions = isPainPointQuestion
|
||||
? [
|
||||
{ value: "dry_makeup", label: "换季皮肤干绷紧绷,上妆卡粉斑驳浮粉" },
|
||||
{ value: "dull_rough", label: "熬夜后脸部粗糙,肤色暗沉蜡黄无光泽" },
|
||||
{ value: "poor_absorption", label: "护肤浮在表面吸收慢,黏腻厚重不透气" },
|
||||
]
|
||||
: [];
|
||||
const choiceField = rawChoiceField || (
|
||||
fallbackOptions.length > 0
|
||||
? {
|
||||
key: fields[0]?.key || "pain_point_direction",
|
||||
label: question,
|
||||
type: "single" as const,
|
||||
options: fallbackOptions,
|
||||
}
|
||||
: null
|
||||
);
|
||||
const savedChoice = choiceField ? String(saved[choiceField.key] || "") : "";
|
||||
const savedChoiceLabel = choiceField?.options?.find((option) => option.value === savedChoice)?.label;
|
||||
const savedChoiceLabel = (choiceField?.options as any[])?.find((option: any) => {
|
||||
const val = typeof option === "string" ? option : option?.value;
|
||||
return val === savedChoice;
|
||||
})?.label || (typeof choiceField?.options?.[0] === "object" ? "" : savedChoice);
|
||||
const savedChoiceText = savedChoiceLabel || savedChoice;
|
||||
const submitChatAnswer = (event: FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
@@ -1491,16 +1579,21 @@ function ElicitCard({
|
||||
className={`omni-gate-actions${compactChoiceOnly ? "" : " omni-chat-choice-actions"}`}
|
||||
aria-label={choiceField.label}
|
||||
>
|
||||
{(choiceField.options || []).map((option) => (
|
||||
<button
|
||||
type="button"
|
||||
key={option.value}
|
||||
disabled={disabled}
|
||||
onClick={() => onSubmit({ [choiceField.key]: option.value }, [])}
|
||||
>
|
||||
{option.label}
|
||||
</button>
|
||||
))}
|
||||
{(choiceField.options || []).map((option: any, index: number) => {
|
||||
const label = typeof option === "string" ? option : String(option?.label || option?.text || option?.title || option?.value || `选项 ${index + 1}`).trim();
|
||||
const value = typeof option === "string" ? option : String(option?.value || option?.label || option?.text || option?.title || `option_${index + 1}`).trim();
|
||||
if (!label) return null;
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
key={value || index}
|
||||
disabled={disabled}
|
||||
onClick={() => onSubmit({ [choiceField.key || "choice"]: value }, [])}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : null}
|
||||
{!compactChoiceOnly ? (
|
||||
@@ -1532,7 +1625,7 @@ function ElicitCard({
|
||||
<section className={`omni-elicit-card${submitted ? " is-submitted" : ""}`}>
|
||||
<header className="omni-elicit-head">
|
||||
<strong>还需要你确认</strong>
|
||||
<span>{submitted ? "已回答" : "选一下就好"}</span>
|
||||
<span>{submitted ? (saved._action === "cancel" ? "已取消" : "已回答") : "选一下就好"}</span>
|
||||
</header>
|
||||
<div className="omni-elicit-body">
|
||||
{fields.map((field) => {
|
||||
@@ -1592,7 +1685,9 @@ function ElicitCard({
|
||||
<span>{option.name}</span>
|
||||
</button>
|
||||
))}
|
||||
{(assetOptions[field.key] || []).length === 0 && <span>暂无可选素材</span>}
|
||||
{(assetOptions[field.key] || []).length === 0 && (
|
||||
<span>{submitted && saved._action === "cancel" ? "已取消选择" : "暂无可选素材"}</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -1601,6 +1696,17 @@ function ElicitCard({
|
||||
</div>
|
||||
{!submitted && (
|
||||
<div className="omni-elicit-actions">
|
||||
<button
|
||||
type="button"
|
||||
className="cancel-btn"
|
||||
disabled={disabled}
|
||||
onClick={() => {
|
||||
const primaryKey = fields[0]?.key || "choice";
|
||||
onSubmit({ _action: "cancel", [primaryKey]: "cancel" }, []);
|
||||
}}
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
disabled={disabled || !complete}
|
||||
@@ -1746,9 +1852,12 @@ function ResultCard({
|
||||
const meta = [payload.model, payload.resolution, payload.ratio].filter(Boolean).join(" · ");
|
||||
const isGeneratedVideo = first.type === "video";
|
||||
const [preview, setPreview] = useState<{ src: string; kind: "image" | "video"; name: string } | null>(null);
|
||||
const ratio = String(payload.ratio || "").trim();
|
||||
const ratioClass = ratio === "16:9" ? "is-ratio-16-9" : ratio === "1:1" ? "is-ratio-1-1" : "is-ratio-9-16";
|
||||
const multiClass = assets.length > 1 ? "has-multiple" : "has-single";
|
||||
|
||||
return (
|
||||
<section className="omni-result-card">
|
||||
<section className={`omni-result-card ${multiClass} ${ratioClass}`}>
|
||||
<div className={`omni-result-media${assets.length > 1 ? " is-grid" : ""}`}>
|
||||
{assets.map((asset, index) => {
|
||||
const cover = asset.cover || asset.url || "";
|
||||
@@ -2114,9 +2223,14 @@ function ProcessCard({
|
||||
const assets = (payload.assets as Array<Record<string, string>> | undefined) || [];
|
||||
const completedSegments = Number(payload.completed_segment_count || 0);
|
||||
const waitingSegments = isSegmentedVideo ? Math.max(1, segmentCount - completedSegments) : 1;
|
||||
const totalTiles = assets.length + waitingSegments;
|
||||
const multiClass = totalTiles > 1 || isSegmentedVideo ? "has-multiple" : "has-single";
|
||||
const ratio = String(payload.ratio || "").trim();
|
||||
const ratioClass = ratio === "16:9" ? "is-ratio-16-9" : ratio === "1:1" ? "is-ratio-1-1" : "is-ratio-9-16";
|
||||
|
||||
return (
|
||||
<section className="omni-result-card omni-process-card">
|
||||
<div className={`omni-result-media${isSegmentedVideo ? " is-grid" : ""}`}>
|
||||
<section className={`omni-result-card omni-process-card ${multiClass} ${ratioClass}`}>
|
||||
<div className={`omni-result-media${multiClass === "has-multiple" ? " is-grid" : ""}`}>
|
||||
{assets.map((asset, index) => {
|
||||
const cover = asset.cover || asset.url || "";
|
||||
const url = asset.url || cover;
|
||||
@@ -2236,6 +2350,7 @@ export function OmniSessionPage({
|
||||
const [sessionUploads, setSessionUploads] = useState<CreationRef[]>(firstUploads || []);
|
||||
const [uploading, setUploading] = useState(false);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const personFileInputRef = useRef<HTMLInputElement>(null);
|
||||
// 对话引导里的「上传人物图」要直达系统文件选择器,而不是绕回通用素材菜单。
|
||||
const quickUploadReplyRef = useRef<ReplyOption | null>(null);
|
||||
// 人物来源闸门借用全局文件选择器 / @素材面板,选完后需回到对应卡片提交。
|
||||
@@ -2262,8 +2377,12 @@ export function OmniSessionPage({
|
||||
const [stopping, setStopping] = useState(false);
|
||||
const [promptView, setPromptView] = useState<{ title: string; body: string } | null>(null);
|
||||
const [assetsOpen, setAssetsOpen] = useState(false);
|
||||
const [assetsPanelMode, setAssetsPanelMode] = useState<"session" | "model_library">("session");
|
||||
const [assetView, setAssetView] = useState<"grid" | "list">("grid");
|
||||
const [assetQuery, setAssetQuery] = useState("");
|
||||
const [modelLibraryItems, setModelLibraryItems] = useState<ModelEntity[]>([]);
|
||||
const [modelLibraryLoading, setModelLibraryLoading] = useState(false);
|
||||
const [selectedModelRef, setSelectedModelRef] = useState<CreationRef | null>(null);
|
||||
const [assetPreview, setAssetPreview] = useState<{ src: string; kind: "image" | "video"; name: string } | null>(null);
|
||||
const [pendingUserId, setPendingUserId] = useState<string | null>(() => bootLocalIdRef.current);
|
||||
// App 传进来的 onNotify 是内联箭头,**每次 App 重渲染都是新身份**。
|
||||
@@ -2285,6 +2404,26 @@ export function OmniSessionPage({
|
||||
}
|
||||
}, [notify]);
|
||||
|
||||
const openPersonModelLibrary = useCallback(async (messageId: string) => {
|
||||
personSourceRequestRef.current = { messageId, source: "model_library" };
|
||||
setMentionMenuOpen(false);
|
||||
setUploadMenuOpen(false);
|
||||
setSessionAssetModalOpen(false);
|
||||
setAssetsPanelMode("model_library");
|
||||
setAssetQuery("");
|
||||
setSelectedModelRef(null);
|
||||
setAssetsOpen(true);
|
||||
setModelLibraryLoading(true);
|
||||
try {
|
||||
const response = await api.listModels({ pageSize: 200 });
|
||||
setModelLibraryItems(response.results || []);
|
||||
} catch (error) {
|
||||
notify("error", (error as Error).message || "模特库加载失败");
|
||||
} finally {
|
||||
setModelLibraryLoading(false);
|
||||
}
|
||||
}, [notify]);
|
||||
|
||||
const editUserMessage = useCallback((message: CreationMessage) => {
|
||||
const refs = message.refs || [];
|
||||
setPrompt(message.text);
|
||||
@@ -2465,6 +2604,13 @@ export function OmniSessionPage({
|
||||
}))
|
||||
.filter((group) => group.items.length > 0);
|
||||
}, [assetQuery, sessionAssetGroups]);
|
||||
const visibleModelLibraryItems = useMemo(() => {
|
||||
const query = assetQuery.trim().toLocaleLowerCase();
|
||||
if (!query) return modelLibraryItems;
|
||||
return modelLibraryItems.filter((model) =>
|
||||
`${model.name} ${model.description || ""}`.toLocaleLowerCase().includes(query)
|
||||
);
|
||||
}, [assetQuery, modelLibraryItems]);
|
||||
messagesRef.current = messages;
|
||||
|
||||
// 出片在 worker 里跑。刷新后 GENERATING 还在库里,进来立刻拉一次再轮询,
|
||||
@@ -2639,7 +2785,7 @@ export function OmniSessionPage({
|
||||
const send = useCallback(
|
||||
async (payload: Parameters<typeof api.creationSend>[1]) => {
|
||||
// awaiting_user 下允许回答闸门;其它状态仍禁止并发发送
|
||||
if (streamingRef.current && conversation?.agent_status !== "awaiting_user") return;
|
||||
if (streamingRef.current && conversation?.agent_status !== "awaiting_user") return false;
|
||||
// 先占位用户气泡,再亮「正在整理方案」—— 顺序不能反
|
||||
const isTextTurn = payload.kind !== "elicit_answer";
|
||||
let localId: string | null = null;
|
||||
@@ -2711,7 +2857,7 @@ export function OmniSessionPage({
|
||||
/* 已 idle / 竞态忽略 */
|
||||
});
|
||||
}
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
setConversation((prev) =>
|
||||
prev
|
||||
@@ -2739,6 +2885,7 @@ export function OmniSessionPage({
|
||||
.catch(() => {
|
||||
/* agent poll 会接着拉 */
|
||||
});
|
||||
return true;
|
||||
} catch (error) {
|
||||
if (localId) {
|
||||
setMessages((prev) => prev.filter((m) => m.id !== localId));
|
||||
@@ -2754,11 +2901,37 @@ export function OmniSessionPage({
|
||||
setActiveTool("");
|
||||
const status = (error as { status?: number }).status;
|
||||
notify(status === 409 ? "info" : "error", (error as Error).message);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
[conversationId, conversation?.agent_status, mergeCreationDetail, notify]
|
||||
);
|
||||
|
||||
const submitPersonSourceChoice = useCallback(async (
|
||||
messageId: string,
|
||||
source: "local_upload" | "model_library" | "platform_generate",
|
||||
refs: CreationRef[] = [],
|
||||
personPrompt = "",
|
||||
) => {
|
||||
const answers: Record<string, string> = { person_source: source };
|
||||
if (source === "platform_generate") answers.person_prompt = personPrompt.trim();
|
||||
const succeeded = await send({
|
||||
kind: "elicit_answer",
|
||||
reply_to: messageId,
|
||||
answers,
|
||||
refs,
|
||||
});
|
||||
if (!succeeded) return false;
|
||||
setMessages((prev) =>
|
||||
prev.map((item) =>
|
||||
item.id === messageId
|
||||
? { ...item, payload: { ...item.payload, submitted: true, answers } }
|
||||
: item
|
||||
)
|
||||
);
|
||||
return true;
|
||||
}, [send]);
|
||||
|
||||
const handleStop = useCallback(async () => {
|
||||
if (stopping || !isPlanning) return;
|
||||
setStopping(true);
|
||||
@@ -2917,31 +3090,13 @@ export function OmniSessionPage({
|
||||
const insertMention = (ref: CreationRef) => {
|
||||
const personRequest = personSourceRequestRef.current;
|
||||
if (personRequest) {
|
||||
if (ref.type !== "model" && ref.type !== "character") {
|
||||
notify("info", "请选择一位模特或角色");
|
||||
if (personRequest.source !== "model_library" || ref.type !== "model") {
|
||||
notify("info", "请选择模特库中的人物");
|
||||
return;
|
||||
}
|
||||
personSourceRequestRef.current = null;
|
||||
setMentionMenuOpen(false);
|
||||
setMessages((prev) =>
|
||||
prev.map((item) =>
|
||||
item.id === personRequest.messageId
|
||||
? {
|
||||
...item,
|
||||
payload: {
|
||||
...item.payload,
|
||||
submitted: true,
|
||||
answers: { person_source: "model_library" },
|
||||
},
|
||||
}
|
||||
: item
|
||||
)
|
||||
);
|
||||
void send({
|
||||
kind: "elicit_answer",
|
||||
reply_to: personRequest.messageId,
|
||||
answers: { person_source: "model_library" },
|
||||
refs: [ref],
|
||||
void submitPersonSourceChoice(personRequest.messageId, "model_library", [ref]).then((succeeded) => {
|
||||
if (succeeded) personSourceRequestRef.current = null;
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -3008,43 +3163,22 @@ export function OmniSessionPage({
|
||||
void send({ kind: "elicit_answer", reply_to: message.id, answers, refs });
|
||||
}}
|
||||
onChatAnswer={(text) => void send({ kind: "text", text })}
|
||||
onPersonSourceAction={(source) => {
|
||||
onPersonSourceAction={(source, personPrompt = "") => {
|
||||
if (source === "local_upload") {
|
||||
personSourceRequestRef.current = { messageId: message.id, source };
|
||||
quickUploadReplyRef.current = null;
|
||||
setMentionMenuOpen(false);
|
||||
setUploadMenuOpen(false);
|
||||
fileInputRef.current?.click();
|
||||
personFileInputRef.current?.click();
|
||||
return;
|
||||
}
|
||||
if (source === "model_library") {
|
||||
personSourceRequestRef.current = { messageId: message.id, source };
|
||||
setMentionMenuOpen(false);
|
||||
setSessionAssetModalType("character");
|
||||
setSessionAssetModalOpen(true);
|
||||
void openPersonModelLibrary(message.id);
|
||||
return;
|
||||
}
|
||||
setMessages((prev) =>
|
||||
prev.map((item) =>
|
||||
item.id === message.id
|
||||
? {
|
||||
...item,
|
||||
payload: {
|
||||
...item.payload,
|
||||
submitted: true,
|
||||
answers: { person_source: source },
|
||||
},
|
||||
}
|
||||
: item
|
||||
)
|
||||
);
|
||||
void send({
|
||||
kind: "elicit_answer",
|
||||
reply_to: message.id,
|
||||
answers: { person_source: source },
|
||||
refs: [],
|
||||
});
|
||||
void submitPersonSourceChoice(message.id, source, [], personPrompt);
|
||||
}}
|
||||
onUpload={() => fileInputRef.current?.click()}
|
||||
/>
|
||||
);
|
||||
case "strategy":
|
||||
@@ -3222,7 +3356,11 @@ export function OmniSessionPage({
|
||||
aria-expanded={assetsOpen}
|
||||
aria-controls="omni-session-assets-drawer"
|
||||
title="查看对话资源"
|
||||
onClick={() => setAssetsOpen((open) => !open)}
|
||||
onClick={() => {
|
||||
setAssetsPanelMode("session");
|
||||
setAssetQuery("");
|
||||
setAssetsOpen((open) => !open || assetsPanelMode !== "session");
|
||||
}}
|
||||
>
|
||||
<List />
|
||||
<span>查看对话资源</span>
|
||||
@@ -3350,16 +3488,10 @@ export function OmniSessionPage({
|
||||
const files = Array.from(event.target.files || []);
|
||||
event.target.value = "";
|
||||
if (!files.length) return;
|
||||
const personRequest = personSourceRequestRef.current;
|
||||
const selectedImages = files.filter((file) => file.type.startsWith("image/"));
|
||||
const images = personRequest ? selectedImages.slice(0, 1) : selectedImages;
|
||||
const images = selectedImages;
|
||||
if (images.length !== files.length) {
|
||||
notify(
|
||||
"info",
|
||||
personRequest && selectedImages.length > 1
|
||||
? "人物参考每次选择一张图片"
|
||||
: "全能创作仅支持上传图片",
|
||||
);
|
||||
notify("info", "全能创作仅支持上传图片");
|
||||
}
|
||||
if (!images.length) return;
|
||||
setUploading(true);
|
||||
@@ -3372,15 +3504,14 @@ export function OmniSessionPage({
|
||||
// 后端上传时同步送审并等待通过,期间按钮保持「上传中」
|
||||
const data = await api.uploadFreeVideoRef(form);
|
||||
const ref: CreationRef = {
|
||||
// 人物闸门上传的图是身份参考,不能作为普通 asset 传给出片模型。
|
||||
type: personRequest ? "character" : "asset",
|
||||
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]));
|
||||
uploadedRefs.push(ref);
|
||||
if (!quickReply && !personRequest) {
|
||||
if (!quickReply) {
|
||||
setPendingRefs((prev) => {
|
||||
if (prev.some((item) => item.id === ref.id) || prev.length >= MENTION_REF_LIMIT) return prev;
|
||||
return [...prev, ref];
|
||||
@@ -3388,40 +3519,62 @@ export function OmniSessionPage({
|
||||
insertPromptMention(ref);
|
||||
}
|
||||
}
|
||||
notify(
|
||||
"success",
|
||||
personRequest ? "人物参考已上传并锁定" : "素材已上传并通过审核",
|
||||
);
|
||||
if (personRequest && uploadedRefs.length) {
|
||||
personSourceRequestRef.current = null;
|
||||
setMessages((prev) =>
|
||||
prev.map((item) =>
|
||||
item.id === personRequest.messageId
|
||||
? {
|
||||
...item,
|
||||
payload: {
|
||||
...item.payload,
|
||||
submitted: true,
|
||||
answers: { person_source: "local_upload" },
|
||||
},
|
||||
}
|
||||
: item
|
||||
)
|
||||
);
|
||||
void send({
|
||||
kind: "elicit_answer",
|
||||
reply_to: personRequest.messageId,
|
||||
answers: { person_source: "local_upload" },
|
||||
refs: uploadedRefs,
|
||||
});
|
||||
} else if (quickReply && uploadedRefs.length) {
|
||||
notify("success", "素材已上传并通过审核");
|
||||
if (quickReply && uploadedRefs.length) {
|
||||
quickUploadReplyRef.current = null;
|
||||
void send({ kind: "text", text: uploadReplyText(quickReply), refs: uploadedRefs });
|
||||
}
|
||||
} catch (error) {
|
||||
notify("error", (error as Error).message);
|
||||
quickUploadReplyRef.current = null;
|
||||
personSourceRequestRef.current = null;
|
||||
} finally {
|
||||
setUploading(false);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<input
|
||||
ref={personFileInputRef}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
hidden
|
||||
onChange={async (event) => {
|
||||
const file = event.target.files?.[0];
|
||||
event.target.value = "";
|
||||
const personRequest = personSourceRequestRef.current;
|
||||
if (!file || !personRequest || personRequest.source !== "local_upload") {
|
||||
if (!file) personSourceRequestRef.current = null;
|
||||
return;
|
||||
}
|
||||
const isPet = Boolean(conversation?.preset && /宠物/.test(conversation.preset));
|
||||
if (!file.type.startsWith("image/")) {
|
||||
notify("info", isPet ? "请选择一张宠物图片" : "请选择一张人物图片");
|
||||
return;
|
||||
}
|
||||
setUploading(true);
|
||||
try {
|
||||
const form = new FormData();
|
||||
form.append("file", file);
|
||||
const data = await api.uploadFreeVideoRef(form);
|
||||
const ref: CreationRef = {
|
||||
type: "character",
|
||||
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]
|
||||
);
|
||||
const succeeded = await submitPersonSourceChoice(
|
||||
personRequest.messageId,
|
||||
"local_upload",
|
||||
[ref],
|
||||
);
|
||||
if (succeeded) {
|
||||
personSourceRequestRef.current = null;
|
||||
notify("success", isPet ? "宠物参考已上传并锁定" : "人物参考已上传并锁定");
|
||||
}
|
||||
} catch (error) {
|
||||
notify("error", (error as Error).message || (isPet ? "宠物图片上传失败" : "人物图片上传失败"));
|
||||
} finally {
|
||||
setUploading(false);
|
||||
}
|
||||
@@ -3597,50 +3750,113 @@ export function OmniSessionPage({
|
||||
id="omni-session-assets-drawer"
|
||||
className="drawer omni-assets-drawer show"
|
||||
role="dialog"
|
||||
aria-label="对话资源"
|
||||
aria-label={assetsPanelMode === "model_library" ? "选择模特" : "对话资源"}
|
||||
>
|
||||
<header className="drawer-h omni-assets-drawer-head">
|
||||
<div>
|
||||
<strong>资源</strong>
|
||||
<small>{sessionAssets.length ? `本会话 · ${sessionAssets.length} 项` : "仅展示本会话素材"}</small>
|
||||
<strong>{assetsPanelMode === "model_library" ? "模特库" : "资源"}</strong>
|
||||
<small>
|
||||
{assetsPanelMode === "model_library"
|
||||
? modelLibraryLoading
|
||||
? "正在加载模特"
|
||||
: `选择一位出镜人物 · ${modelLibraryItems.length} 位`
|
||||
: sessionAssets.length
|
||||
? `本会话 · ${sessionAssets.length} 项`
|
||||
: "仅展示本会话素材"}
|
||||
</small>
|
||||
</div>
|
||||
<div className="omni-assets-panel-tools">
|
||||
<div className="view-toggle" aria-label="资源显示方式">
|
||||
<button
|
||||
type="button"
|
||||
className={assetView === "grid" ? "active" : ""}
|
||||
aria-label="网格显示"
|
||||
title="网格显示"
|
||||
onClick={() => setAssetView("grid")}
|
||||
>
|
||||
<Grid2X2 />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={assetView === "list" ? "active" : ""}
|
||||
aria-label="列表显示"
|
||||
title="列表显示"
|
||||
onClick={() => setAssetView("list")}
|
||||
>
|
||||
<List />
|
||||
</button>
|
||||
</div>
|
||||
{assetsPanelMode === "session" ? (
|
||||
<div className="view-toggle" aria-label="资源显示方式">
|
||||
<button
|
||||
type="button"
|
||||
className={assetView === "grid" ? "active" : ""}
|
||||
aria-label="网格显示"
|
||||
title="网格显示"
|
||||
onClick={() => setAssetView("grid")}
|
||||
>
|
||||
<Grid2X2 />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={assetView === "list" ? "active" : ""}
|
||||
aria-label="列表显示"
|
||||
title="列表显示"
|
||||
onClick={() => setAssetView("list")}
|
||||
>
|
||||
<List />
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
<label className="omni-assets-search">
|
||||
<Search />
|
||||
<input
|
||||
value={assetQuery}
|
||||
onChange={(event) => setAssetQuery(event.target.value)}
|
||||
placeholder="搜索资源…"
|
||||
aria-label="搜索对话资源"
|
||||
placeholder={assetsPanelMode === "model_library" ? "搜索模特…" : "搜索资源…"}
|
||||
aria-label={assetsPanelMode === "model_library" ? "搜索模特" : "搜索对话资源"}
|
||||
/>
|
||||
</label>
|
||||
<button type="button" className="x" onClick={() => setAssetsOpen(false)} aria-label="关闭资源">
|
||||
<button
|
||||
type="button"
|
||||
className="x"
|
||||
onClick={() => {
|
||||
setAssetsOpen(false);
|
||||
if (assetsPanelMode === "model_library") {
|
||||
personSourceRequestRef.current = null;
|
||||
setSelectedModelRef(null);
|
||||
}
|
||||
}}
|
||||
aria-label={assetsPanelMode === "model_library" ? "关闭模特库" : "关闭资源"}
|
||||
>
|
||||
<X />
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
<div className="drawer-b omni-assets-drawer-body">
|
||||
{sessionAssets.length === 0 ? (
|
||||
{assetsPanelMode === "model_library" ? (
|
||||
modelLibraryLoading ? (
|
||||
<div className="omni-assets-empty" role="status">
|
||||
<span className="omni-send-spinner" />
|
||||
<p>正在加载模特库</p>
|
||||
</div>
|
||||
) : visibleModelLibraryItems.length === 0 ? (
|
||||
<div className="omni-assets-empty" role="status">
|
||||
<Search />
|
||||
<p>{modelLibraryItems.length ? "没有找到相关模特" : "模特库还没有人物"}</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="omni-model-picker-grid" role="listbox" aria-label="模特列表">
|
||||
{visibleModelLibraryItems.map((model) => {
|
||||
const ref: CreationRef = {
|
||||
type: "model",
|
||||
id: model.id,
|
||||
name: model.name,
|
||||
cover: model.portrait,
|
||||
};
|
||||
const selected = selectedModelRef?.id === model.id;
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
role="option"
|
||||
aria-selected={selected}
|
||||
className={`omni-model-picker-card${selected ? " is-selected" : ""}`}
|
||||
key={model.id}
|
||||
onClick={() => setSelectedModelRef(ref)}
|
||||
>
|
||||
<span className="omni-model-picker-media">
|
||||
{model.portrait ? <img src={model.portrait} alt="" /> : <UserRound />}
|
||||
</span>
|
||||
<span className="omni-model-picker-copy">
|
||||
<strong>{model.name}</strong>
|
||||
<small>{model.is_official ? "官方模特" : "我的模特"}</small>
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
) : sessionAssets.length === 0 ? (
|
||||
<div className="omni-assets-empty" role="status">
|
||||
<FolderOpen />
|
||||
<p>本会话还没有素材</p>
|
||||
@@ -3721,6 +3937,44 @@ export function OmniSessionPage({
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
{assetsPanelMode === "model_library" ? (
|
||||
<footer className="drawer-f omni-model-picker-footer">
|
||||
<span className="omni-model-picker-selection">
|
||||
{selectedModelRef ? `已选择:${selectedModelRef.name}` : "请选择一位模特"}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
className="btn"
|
||||
onClick={() => {
|
||||
setAssetsOpen(false);
|
||||
personSourceRequestRef.current = null;
|
||||
setSelectedModelRef(null);
|
||||
}}
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-primary"
|
||||
disabled={!selectedModelRef || streaming}
|
||||
onClick={async () => {
|
||||
const request = personSourceRequestRef.current;
|
||||
if (!request || !selectedModelRef) return;
|
||||
const succeeded = await submitPersonSourceChoice(
|
||||
request.messageId,
|
||||
"model_library",
|
||||
[selectedModelRef],
|
||||
);
|
||||
if (!succeeded) return;
|
||||
personSourceRequestRef.current = null;
|
||||
setAssetsOpen(false);
|
||||
setSelectedModelRef(null);
|
||||
}}
|
||||
>
|
||||
使用该角色
|
||||
</button>
|
||||
</footer>
|
||||
) : null}
|
||||
</aside>
|
||||
) : null}
|
||||
<MediaLightbox
|
||||
@@ -3733,42 +3987,8 @@ export function OmniSessionPage({
|
||||
<AssetSelectModal
|
||||
open={sessionAssetModalOpen}
|
||||
type={sessionAssetModalType}
|
||||
onClose={() => {
|
||||
setSessionAssetModalOpen(false);
|
||||
if (personSourceRequestRef.current?.source === "model_library") {
|
||||
personSourceRequestRef.current = null;
|
||||
}
|
||||
}}
|
||||
onClose={() => setSessionAssetModalOpen(false)}
|
||||
onSelect={(newRef) => {
|
||||
const personRequest = personSourceRequestRef.current;
|
||||
if (personRequest) {
|
||||
if (newRef.type !== "model" && newRef.type !== "character") {
|
||||
notify("info", "请选择一位角色");
|
||||
return;
|
||||
}
|
||||
personSourceRequestRef.current = null;
|
||||
setMessages((prev) =>
|
||||
prev.map((item) =>
|
||||
item.id === personRequest.messageId
|
||||
? {
|
||||
...item,
|
||||
payload: {
|
||||
...item.payload,
|
||||
submitted: true,
|
||||
answers: { person_source: "model_library" },
|
||||
},
|
||||
}
|
||||
: item,
|
||||
),
|
||||
);
|
||||
void send({
|
||||
kind: "elicit_answer",
|
||||
reply_to: personRequest.messageId,
|
||||
answers: { person_source: "model_library" },
|
||||
refs: [newRef],
|
||||
});
|
||||
return;
|
||||
}
|
||||
setPendingRefs((prev) => {
|
||||
if (prev.some((item) => item.type === newRef.type && item.id === newRef.id) || prev.length >= MENTION_REF_LIMIT) {
|
||||
return prev;
|
||||
|
||||
Reference in New Issue
Block a user