修改全能创作
This commit is contained in:
@@ -461,6 +461,7 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
margin-top: 2px;
|
||||
border: 0;
|
||||
border-radius: 11px;
|
||||
color: #fff;
|
||||
@@ -515,60 +516,6 @@
|
||||
.login-page .login-row a:hover,
|
||||
.login-page .login-switch a:hover { text-decoration: underline; }
|
||||
|
||||
/* 三入口操作条:同宽收进卡片,小屏改纵向避免撑破 */
|
||||
.login-page .login-actions {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(0, 1.4fr) minmax(0, 1fr);
|
||||
align-items: stretch;
|
||||
gap: 0;
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
border-radius: 11px;
|
||||
background: linear-gradient(135deg, #002d9f, #001d70 76%);
|
||||
box-shadow: 0 14px 30px rgba(0, 25, 102, 0.32);
|
||||
}
|
||||
.login-page .login-actions .login-submit {
|
||||
margin-top: 0;
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
}
|
||||
.login-page .login-actions .login-submit:hover:not(:disabled) {
|
||||
transform: none;
|
||||
filter: brightness(1.08);
|
||||
box-shadow: none;
|
||||
}
|
||||
.login-page .login-action-alt {
|
||||
min-width: 0;
|
||||
height: 54px;
|
||||
padding: 0 8px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
color: rgba(255, 255, 255, 0.88);
|
||||
background: transparent;
|
||||
font: inherit;
|
||||
font-size: 12px;
|
||||
line-height: 1.2;
|
||||
cursor: pointer;
|
||||
}
|
||||
.login-page .login-action-alt + .login-submit,
|
||||
.login-page .login-submit + .login-action-alt {
|
||||
box-shadow: inset 1px 0 0 rgba(255, 255, 255, 0.16);
|
||||
}
|
||||
.login-page .login-action-alt span {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.login-page .login-action-alt:hover { background: rgba(255, 255, 255, 0.08); }
|
||||
.login-page .login-action-alt svg { width: 14px; height: 14px; flex: 0 0 auto; }
|
||||
|
||||
.login-page .login-switch {
|
||||
margin: 18px 0 0;
|
||||
text-align: center;
|
||||
@@ -598,30 +545,6 @@
|
||||
font-size: 12px;
|
||||
transform: none;
|
||||
}
|
||||
.login-page .login-sso {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
}
|
||||
.login-page .login-sso button {
|
||||
min-width: 0;
|
||||
height: 36px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 7px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.14);
|
||||
border-radius: 10px;
|
||||
color: rgba(255, 255, 255, 0.72);
|
||||
background: rgba(255, 255, 255, 0.045);
|
||||
font: inherit;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.login-page .login-sso button:hover { background: rgba(255, 255, 255, 0.08); }
|
||||
.login-page .login-sso svg { width: 14px; height: 14px; flex: 0 0 auto; }
|
||||
|
||||
.login-page .login-note {
|
||||
margin: 0;
|
||||
color: rgba(255, 255, 255, 0.52);
|
||||
@@ -749,7 +672,6 @@
|
||||
.login-page .login-form { gap: 14px; }
|
||||
}
|
||||
|
||||
/* 窄屏:主登录在上,两侧入口两列在下 */
|
||||
@media (max-width: 560px) {
|
||||
.login-page .login-form-panel {
|
||||
padding: 24px 14px 20px;
|
||||
@@ -758,36 +680,6 @@
|
||||
width: min(100%, 480px);
|
||||
padding: 28px 16px 22px;
|
||||
}
|
||||
.login-page .login-actions {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 8px;
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
overflow: visible;
|
||||
}
|
||||
.login-page .login-actions .login-submit {
|
||||
grid-column: 1 / -1;
|
||||
order: -1;
|
||||
border-radius: 11px;
|
||||
background: linear-gradient(135deg, #002d9f, #001d70 76%);
|
||||
box-shadow: 0 14px 30px rgba(0, 25, 102, 0.32);
|
||||
}
|
||||
.login-page .login-actions .login-submit:hover:not(:disabled) {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 18px 36px rgba(0, 23, 94, 0.4);
|
||||
filter: brightness(1.06);
|
||||
}
|
||||
.login-page .login-action-alt,
|
||||
.login-page .login-action-alt + .login-submit,
|
||||
.login-page .login-submit + .login-action-alt {
|
||||
box-shadow: none;
|
||||
}
|
||||
.login-page .login-action-alt {
|
||||
height: 44px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.14);
|
||||
border-radius: 10px;
|
||||
background: rgba(255, 255, 255, 0.045);
|
||||
}
|
||||
.login-page .login-row {
|
||||
flex-wrap: wrap;
|
||||
gap: 8px 12px;
|
||||
|
||||
@@ -190,28 +190,12 @@
|
||||
|
||||
.omni-reply-guide {
|
||||
margin-top: 8px;
|
||||
padding-top: 8px;
|
||||
border-top: 1px dashed var(--border-faint);
|
||||
}
|
||||
|
||||
.omni-reply-guide p {
|
||||
margin: 0 0 8px;
|
||||
color: var(--black-alpha-56);
|
||||
font-size: 12px;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.omni-reply-guide p > span {
|
||||
margin-right: 6px;
|
||||
color: var(--accent-black);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 11px;
|
||||
letter-spacing: .04em;
|
||||
}
|
||||
|
||||
.omni-reply-guide-options {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
@@ -247,6 +231,29 @@
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.omni-reply-custom-input {
|
||||
display: flex;
|
||||
flex: 1 1 220px;
|
||||
gap: 6px;
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
.omni-reply-custom-input .input {
|
||||
min-width: 0;
|
||||
height: 30px;
|
||||
padding: 0 10px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.omni-reply-custom-input button {
|
||||
flex: 0 0 30px;
|
||||
width: 30px;
|
||||
padding: 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
|
||||
.omni-chat-row.is-user .omni-chat-bubble {
|
||||
align-items: flex-end;
|
||||
@@ -1261,10 +1268,24 @@
|
||||
|
||||
.omni-strategy-grid,
|
||||
.omni-plan-points,
|
||||
.omni-plan-timeline {
|
||||
.omni-plan-timeline,
|
||||
.omni-direction-list {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.omni-direction-card {
|
||||
width: 100%;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.omni-direction-custom {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.omni-direction-custom button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.omni-plan-summary {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
@@ -1290,6 +1311,144 @@
|
||||
padding: 14px 18px 16px;
|
||||
}
|
||||
|
||||
/* 剧情反转预设的方向选择:三个完整方向直接平铺,避免只留一句「我准备了三个方向」。 */
|
||||
.omni-direction-card {
|
||||
box-sizing: border-box;
|
||||
width: min(760px, calc(100% - 44px));
|
||||
margin: 0 0 24px 44px;
|
||||
overflow: hidden;
|
||||
border-radius: 8px;
|
||||
background: var(--surface);
|
||||
box-shadow: inset 0 0 0 1px var(--border-faint);
|
||||
}
|
||||
|
||||
.omni-direction-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-height: 62px;
|
||||
padding: 12px 16px;
|
||||
box-shadow: inset 0 -1px 0 var(--border-faint);
|
||||
}
|
||||
|
||||
.omni-direction-head > div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.omni-direction-head strong {
|
||||
color: var(--accent-black);
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.omni-direction-head span {
|
||||
color: var(--black-alpha-56);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.omni-direction-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
padding: 14px 16px;
|
||||
}
|
||||
|
||||
.omni-direction-list > button {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 7px;
|
||||
padding: 12px;
|
||||
border: 0;
|
||||
border-radius: 8px;
|
||||
background: var(--background-base);
|
||||
box-shadow: inset 0 0 0 1px var(--border-faint);
|
||||
color: var(--accent-black);
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
transition: background 200ms ease, box-shadow 200ms ease, transform 100ms ease;
|
||||
}
|
||||
|
||||
.omni-direction-list > button:hover:not(:disabled) {
|
||||
background: var(--background-lighter);
|
||||
box-shadow: inset 0 0 0 1px var(--black-alpha-24);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.omni-direction-list > button:focus-visible {
|
||||
outline: none;
|
||||
box-shadow: inset 0 0 0 1px var(--heat), 0 0 0 2px var(--heat-40);
|
||||
}
|
||||
|
||||
.omni-direction-list > button.is-selected {
|
||||
background: var(--heat-12);
|
||||
box-shadow: inset 0 0 0 1px var(--heat-40);
|
||||
}
|
||||
|
||||
.omni-direction-list > button:disabled {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.omni-direction-index,
|
||||
.omni-direction-list small {
|
||||
color: var(--black-alpha-56);
|
||||
font-size: 11px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.omni-direction-list > button > strong {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.omni-direction-list p {
|
||||
margin: 0;
|
||||
color: var(--black-alpha-64);
|
||||
font-size: 12px;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.omni-direction-list p b {
|
||||
display: block;
|
||||
margin-bottom: 2px;
|
||||
color: var(--accent-black);
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.omni-direction-custom {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
padding: 0 16px 16px;
|
||||
}
|
||||
|
||||
.omni-direction-custom .input {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.omni-direction-custom button {
|
||||
height: 36px;
|
||||
flex: 0 0 auto;
|
||||
padding: 0 14px;
|
||||
border: 0;
|
||||
border-radius: 8px;
|
||||
background: var(--accent-black);
|
||||
color: var(--surface);
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.omni-direction-custom button:disabled {
|
||||
background: var(--black-alpha-12);
|
||||
color: var(--black-alpha-48);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.omni-elicit-field > label {
|
||||
display: block;
|
||||
margin-bottom: 9px;
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
LoaderCircle,
|
||||
Lock,
|
||||
LockKeyhole,
|
||||
ScanLine,
|
||||
ShieldCheck,
|
||||
Sparkles,
|
||||
Ticket,
|
||||
@@ -559,18 +558,10 @@ export function AuthScreen({
|
||||
</label>
|
||||
<a href="#" onClick={(event) => { event.preventDefault(); showToast("重置密码", "请联系团队超管重置你的登录密码"); }}>忘记密码?</a>
|
||||
</div>
|
||||
<div className="login-actions">
|
||||
<button type="button" className="login-action-alt" onClick={() => showToast("微信扫码", "对接中 · 当前请用账号 + 密码登录")}>
|
||||
<ScanLine /> <span>微信扫码</span>
|
||||
</button>
|
||||
<button className="login-submit" type="submit" disabled={busy}>
|
||||
{busy ? (LOGIN_PROGRESS_COPY[loginProgress || "checking"] || "正在进入工作台") : "确认登录"}
|
||||
{busy ? <LoaderCircle /> : <ArrowRight />}
|
||||
</button>
|
||||
<button type="button" className="login-action-alt" onClick={() => showToast("飞书 SSO", "对接中 · 当前请用账号 + 密码登录")}>
|
||||
<ShieldCheck /> <span>飞书 SSO</span>
|
||||
</button>
|
||||
</div>
|
||||
<button className="login-submit" type="submit" disabled={busy}>
|
||||
{busy ? (LOGIN_PROGRESS_COPY[loginProgress || "checking"] || "正在进入工作台") : "确认登录"}
|
||||
{busy ? <LoaderCircle /> : <ArrowRight />}
|
||||
</button>
|
||||
</form>
|
||||
<p className="login-switch">
|
||||
还没账号? <a href="/register" onClick={(event) => { event.preventDefault(); switchMode("register"); }}>注册团队 →</a>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
||||
import { type FormEvent, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import {
|
||||
ArrowLeft,
|
||||
@@ -43,7 +43,27 @@ type PromptBlock =
|
||||
|
||||
type ReplyOption = { label: string; text: string };
|
||||
|
||||
function replyOptionsForMessage(payload: Record<string, unknown>, isVideo: boolean): ReplyOption[] {
|
||||
function stripNumericReplyInstruction(text: string): string {
|
||||
return (text || "")
|
||||
.replace(
|
||||
/(?:\n)?(?:你|请)?\s*(?:直接)?(?:回复|选择|选)\s*(?:数字|编号)?\s*1\s*(?:[/、,,]\s*2)(?:\s*(?:[/、,,]\s*3))?[^。!?!\n]*(?:[。!?!]|$)/g,
|
||||
"",
|
||||
)
|
||||
.trim();
|
||||
}
|
||||
|
||||
function numberedReplyOptions(text: string): ReplyOption[] {
|
||||
const matches = [...(text || "").matchAll(/(?:^|\n)\s*([1-3])[.、.]\s*(?:\*\*)?([^\n*]{1,80})/g)];
|
||||
if (matches.length < 2) return [];
|
||||
return matches.slice(0, 3).map((match) => ({
|
||||
label: match[1],
|
||||
text: `选择第${match[1]}个方向`,
|
||||
}));
|
||||
}
|
||||
|
||||
function replyOptionsForMessage(text: string, payload: Record<string, unknown>, isVideo: boolean): ReplyOption[] {
|
||||
const numbered = numberedReplyOptions(text);
|
||||
if (numbered.length) return numbered;
|
||||
const stored = payload.reply_options;
|
||||
if (Array.isArray(stored)) {
|
||||
const options = stored
|
||||
@@ -63,15 +83,55 @@ function replyOptionsForMessage(payload: Record<string, unknown>, isVideo: boole
|
||||
{ label: "按这个方向出图", text: "按这个方向出图" },
|
||||
{ label: "换个场景", text: "我想换个场景" },
|
||||
{ label: "改人物状态", text: "我想改人物状态" },
|
||||
];
|
||||
];
|
||||
}
|
||||
|
||||
function replyHintForMessage(payload: Record<string, unknown>, isVideo: boolean): string {
|
||||
const stored = String(payload.reply_hint || "").trim();
|
||||
if (stored) return stored;
|
||||
return isVideo
|
||||
? "可以回复「继续完善方案」,也可以选下方想调整的方向。"
|
||||
: "可以回复「按这个方向出图」,也可以选下方想调整的方向。";
|
||||
function ReplyActions({
|
||||
options,
|
||||
disabled,
|
||||
onSubmit,
|
||||
}: {
|
||||
options: ReplyOption[];
|
||||
disabled?: boolean;
|
||||
onSubmit: (text: string) => void;
|
||||
}) {
|
||||
const [customIdea, setCustomIdea] = useState("");
|
||||
const submitCustomIdea = (event: FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
const text = customIdea.trim();
|
||||
if (!text || disabled) return;
|
||||
onSubmit(text);
|
||||
setCustomIdea("");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="omni-reply-guide-options">
|
||||
{options.map((option, index) => (
|
||||
<button
|
||||
type="button"
|
||||
className={index === 0 && options.length < 3 ? "primary" : ""}
|
||||
key={option.text}
|
||||
disabled={disabled}
|
||||
onClick={() => onSubmit(option.text)}
|
||||
>
|
||||
{option.label}
|
||||
</button>
|
||||
))}
|
||||
<form className="omni-reply-custom-input" onSubmit={submitCustomIdea}>
|
||||
<input
|
||||
className="input"
|
||||
value={customIdea}
|
||||
disabled={disabled}
|
||||
onChange={(event) => setCustomIdea(event.target.value)}
|
||||
placeholder="输入自己的想法…"
|
||||
aria-label="输入自己的想法"
|
||||
/>
|
||||
<button type="submit" aria-label="发送想法" disabled={disabled || !customIdea.trim()}>
|
||||
<ArrowUp size={14} />
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function parseMarkdownTable(lines: string[]): { headers: string[]; rows: string[][] } | null {
|
||||
@@ -401,6 +461,25 @@ function collectSessionAssets(
|
||||
|
||||
}
|
||||
|
||||
// 视频确认卡保存的是最终用于出片的 Prompt。它不是聊天正文,而是本会话
|
||||
// 的一份可复查资源;右侧资源栏始终只保留最新的一版,避免多次改稿堆出一串副本。
|
||||
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
||||
const message = messages[index];
|
||||
const payload = message.payload || {};
|
||||
const body = String(
|
||||
message.kind === "prompt_file" ? payload.body || "" : payload.video_prompt || "",
|
||||
).trim();
|
||||
if (!body) continue;
|
||||
push({
|
||||
key: `prompt:${message.id || index}`,
|
||||
kind: "document",
|
||||
title: String(payload.title || "视频生成Prompt.md"),
|
||||
promptTitle: String(payload.title || "视频生成Prompt.md"),
|
||||
promptBody: body,
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -554,7 +633,15 @@ function PlanCard({ payload }: { payload: Record<string, unknown> }) {
|
||||
);
|
||||
}
|
||||
|
||||
function PromptFileCard({ payload }: { payload: Record<string, unknown> }) {
|
||||
function PromptFileCard({
|
||||
payload,
|
||||
onView,
|
||||
}: {
|
||||
payload: Record<string, unknown>;
|
||||
onView: (title: string, body: string) => void;
|
||||
}) {
|
||||
const title = String(payload.title || "视频生成Prompt.md");
|
||||
const body = String(payload.body || "").trim();
|
||||
return (
|
||||
<section className="omni-prompt-file-card">
|
||||
<span className="omni-prompt-file-icon">
|
||||
@@ -564,6 +651,11 @@ function PromptFileCard({ payload }: { payload: Record<string, unknown> }) {
|
||||
<strong>出片指令已准备好</strong>
|
||||
<small>已根据方案、参数和 {String(payload.ref_count ?? 0)} 组参考素材在后台整理完成</small>
|
||||
</div>
|
||||
{body ? (
|
||||
<button type="button" onClick={() => onView(title, body)}>
|
||||
查看 Prompt
|
||||
</button>
|
||||
) : null}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -718,6 +810,7 @@ function ElicitCard({
|
||||
const [answers, setAnswers] = useState<Record<string, string | string[]>>(saved);
|
||||
const [assetOptions, setAssetOptions] = useState<Record<string, CreationRef[]>>({});
|
||||
const [assetPicked, setAssetPicked] = useState<Record<string, CreationRef>>({});
|
||||
const [customDirection, setCustomDirection] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
if (submitted || interaction === "chat" || interaction === "step_confirm") return;
|
||||
@@ -761,6 +854,76 @@ function ElicitCard({
|
||||
);
|
||||
}
|
||||
|
||||
if (interaction === "plot_twist_directions") {
|
||||
const directions = Array.isArray(message.payload.directions)
|
||||
? message.payload.directions
|
||||
.filter((item): item is Record<string, unknown> => Boolean(item) && typeof item === "object")
|
||||
.map((item) => ({
|
||||
id: String(item.id || "").trim(),
|
||||
title: String(item.title || "剧情方向").trim(),
|
||||
conflict: String(item.conflict || "").trim(),
|
||||
productRole: String(item.product_role || "").trim(),
|
||||
reversal: String(item.reversal || "").trim(),
|
||||
tone: String(item.tone || "").trim(),
|
||||
}))
|
||||
.filter((item) => item.id)
|
||||
: [];
|
||||
const selected = String(saved.story_direction || "");
|
||||
const submitCustomDirection = (event: FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
const idea = customDirection.trim();
|
||||
if (!idea || disabled) return;
|
||||
onSubmit({ story_direction: idea }, []);
|
||||
setCustomDirection("");
|
||||
};
|
||||
return (
|
||||
<div className="omni-elicit-slot omni-direction-slot">
|
||||
<section className={`omni-direction-card${submitted ? " is-submitted" : ""}`}>
|
||||
<header className="omni-direction-head">
|
||||
<div>
|
||||
<strong>三个剧情反转方向</strong>
|
||||
<span>直接选一个,我会继续展开成完整方案</span>
|
||||
</div>
|
||||
</header>
|
||||
<div className="omni-direction-list">
|
||||
{directions.map((direction, index) => {
|
||||
const isSelected = selected === direction.id || selected === direction.title;
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
key={direction.id}
|
||||
className={isSelected ? "is-selected" : ""}
|
||||
disabled={disabled || submitted}
|
||||
onClick={() => onSubmit({ story_direction: direction.id }, [])}
|
||||
>
|
||||
<span className="omni-direction-index">方向 {index + 1}</span>
|
||||
<strong>{direction.title}</strong>
|
||||
<p><b>冲突</b>{direction.conflict}</p>
|
||||
<p><b>商品作用</b>{direction.productRole}</p>
|
||||
<p><b>反转</b>{direction.reversal}</p>
|
||||
{direction.tone ? <small>{direction.tone}</small> : null}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{!submitted ? (
|
||||
<form className="omni-direction-custom" onSubmit={submitCustomDirection}>
|
||||
<input
|
||||
className="input"
|
||||
value={customDirection}
|
||||
disabled={disabled}
|
||||
onChange={(event) => setCustomDirection(event.target.value)}
|
||||
placeholder="或者写下你自己的剧情想法…"
|
||||
aria-label="输入自己的剧情想法"
|
||||
/>
|
||||
<button type="submit" disabled={disabled || !customDirection.trim()}>使用这个想法</button>
|
||||
</form>
|
||||
) : null}
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const toggleMulti = (key: string, value: string) => {
|
||||
setAnswers((prev) => {
|
||||
const current = Array.isArray(prev[key]) ? (prev[key] as string[]) : [];
|
||||
@@ -831,8 +994,7 @@ function ElicitCard({
|
||||
<p className="omni-chat-text">{question}</p>
|
||||
</div>
|
||||
{!submitted ? (
|
||||
<div className="omni-reply-guide" aria-label="下一步引导">
|
||||
<p><span>下一步</span>直接在下方输入你的想法,回答这一项就行。</p>
|
||||
<div className="omni-reply-guide" aria-label="回复操作">
|
||||
<div className="omni-reply-guide-options">
|
||||
<button type="button" className="primary" onClick={() => document.getElementById("omniSessionPrompt")?.focus()}>
|
||||
现在填写
|
||||
@@ -991,7 +1153,7 @@ function ConfirmCard({
|
||||
const raw = String(draft.duration || "");
|
||||
const digits = raw.replace(/\D/g, "");
|
||||
// 与后端 video_duration 一致:「智能时长」/解析不出 → 15 秒
|
||||
const duration = digits ? Math.max(4, Math.min(Number(digits), 30)) : 15;
|
||||
const duration = digits ? Math.max(4, Math.min(Number(digits), 60)) : 15;
|
||||
const est = estimateCost(model, { ratio: draft.ratio || "9:16", resolution, duration, refs: [] });
|
||||
if (est.listed && est.points > 0) return est.points;
|
||||
// 挂牌缺失时仍展示后端下发的预估(若有)
|
||||
@@ -2145,6 +2307,26 @@ export function OmniSessionPage({
|
||||
|| (streaming && conversation?.agent_status !== "awaiting_user")
|
||||
}
|
||||
onSubmit={(answers, refs) => {
|
||||
const selectedDuration = String(answers.duration || "").trim();
|
||||
if (selectedDuration) {
|
||||
setConversation((prev) =>
|
||||
prev ? { ...prev, params: { ...prev.params, duration: selectedDuration } } : prev
|
||||
);
|
||||
}
|
||||
// 剧情反转预设选择故事深度后,顶部参数同步显示实际时长;服务端仍是最终事实来源。
|
||||
if (message.payload?.interaction === "plot_twist_story_depth") {
|
||||
const durationByDepth: Record<string, string> = {
|
||||
"15s": "15 秒",
|
||||
"30s": "30 秒",
|
||||
"60s": "60 秒",
|
||||
};
|
||||
const duration = durationByDepth[String(answers.story_depth || "")];
|
||||
if (duration) {
|
||||
setConversation((prev) =>
|
||||
prev ? { ...prev, params: { ...prev.params, duration } } : prev
|
||||
);
|
||||
}
|
||||
}
|
||||
// 乐观标记已提交,避免短路径返回前按钮还能连点
|
||||
setMessages((prev) =>
|
||||
prev.map((item) =>
|
||||
@@ -2173,6 +2355,7 @@ export function OmniSessionPage({
|
||||
<PromptFileCard
|
||||
key={message.clientKey || message.id}
|
||||
payload={message.payload}
|
||||
onView={(title, body) => setPromptView({ title, body })}
|
||||
/>
|
||||
);
|
||||
case "confirm": {
|
||||
@@ -2212,15 +2395,15 @@ export function OmniSessionPage({
|
||||
);
|
||||
default: {
|
||||
const refs = message.refs || [];
|
||||
const body = stripMentionText(message.text, refs);
|
||||
const rawBody = stripMentionText(message.text, refs);
|
||||
const body = message.role === "assistant" ? stripNumericReplyInstruction(rawBody) : rawBody;
|
||||
const pending = message.id === pendingUserId;
|
||||
const showReplyGuide =
|
||||
message.role === "assistant"
|
||||
&& message.kind === "text"
|
||||
&& messages[messages.length - 1]?.id === message.id
|
||||
&& !streaming;
|
||||
const replyHint = showReplyGuide ? replyHintForMessage(message.payload, isVideo) : "";
|
||||
const replyOptions = showReplyGuide ? replyOptionsForMessage(message.payload, isVideo) : [];
|
||||
const replyOptions = showReplyGuide ? replyOptionsForMessage(body, message.payload, isVideo) : [];
|
||||
return (
|
||||
<div
|
||||
className={`omni-chat-row ${message.role === "user" ? "is-user" : "agent"}${pending ? " is-pending" : ""}`}
|
||||
@@ -2239,23 +2422,12 @@ export function OmniSessionPage({
|
||||
</div>
|
||||
) : null}
|
||||
{showReplyGuide ? (
|
||||
<div className="omni-reply-guide" aria-label="下一步引导">
|
||||
<p>
|
||||
<span>下一步</span>
|
||||
{replyHint}
|
||||
</p>
|
||||
<div className="omni-reply-guide-options">
|
||||
{replyOptions.map((option, index) => (
|
||||
<button
|
||||
type="button"
|
||||
className={index === 0 ? "primary" : ""}
|
||||
key={option.text}
|
||||
onClick={() => void send({ kind: "text", text: option.text })}
|
||||
>
|
||||
{option.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="omni-reply-guide" aria-label="回复操作">
|
||||
<ReplyActions
|
||||
options={replyOptions}
|
||||
disabled={streaming}
|
||||
onSubmit={(text) => void send({ kind: "text", text })}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
{message.role === "user" && !pending ? (
|
||||
|
||||
Reference in New Issue
Block a user