diff --git a/core/backend/apps/ai/creation_agent.py b/core/backend/apps/ai/creation_agent.py index 51bcfa0..f532462 100644 --- a/core/backend/apps/ai/creation_agent.py +++ b/core/backend/apps/ai/creation_agent.py @@ -671,8 +671,9 @@ def default_reply_options( *, has_context: bool = False, is_video: bool = True, + assistant_text: str = "", ) -> list[dict[str, str]]: - """纯文字收束时给前端的可点下一步,避免用户面对一段说明不知道怎么继续。""" + """只给与上一句相关的快捷回复,绝不塞与当前进度无关的固定话术。""" stage = "" if conversation is not None: try: @@ -686,17 +687,39 @@ def default_reply_options( {"label": "重新来", "text": "重新来"}, ] if has_context: - if is_video: + text = str(assistant_text or "") + if re.search(r"颜色|色号|色彩|SKU|款式|几种", text, re.IGNORECASE): return [ - {"label": "继续完善方案", "text": "继续完善方案"}, - {"label": "换个场景", "text": "我想换个场景"}, - {"label": "改卖点", "text": "我想改卖点"}, + {"label": "补充颜色和顺序", "text": "我来补充每个颜色和展示顺序"}, + {"label": "上传各款实物图", "text": "我补充各颜色/款式的实物图"}, + {"label": "先按当前主款做", "text": "先按当前主款做,其他颜色后面再补"}, ] - return [ - {"label": "按这个方向出图", "text": "按这个方向出图"}, - {"label": "换个场景", "text": "我想换个场景"}, - {"label": "改人物状态", "text": "我想改人物状态"}, - ] + if re.search(r"商品|产品|主推|哪款", text, re.IGNORECASE): + return [ + {"label": "发商品列表", "text": "把商品列表发给我选"}, + {"label": "我直接说商品名", "text": "我直接告诉你商品名"}, + {"label": "你来推荐", "text": "你根据当前需求推荐一款"}, + ] + if re.search(r"人物|角色|模特|出镜", text, re.IGNORECASE): + return [ + {"label": "上传人物图", "text": "我上传人物参考图"}, + {"label": "由你设定角色", "text": "你先帮我设定一个合适的角色"}, + {"label": "不需要人物", "text": "这条先不需要人物出镜"}, + ] + if re.search(r"场景|地点|背景|在哪", text, re.IGNORECASE): + return [ + {"label": "上传场景图", "text": "我上传场景参考图"}, + {"label": "你来推荐场景", "text": "你按商品和预设推荐场景"}, + {"label": "用干净日常场景", "text": "先用干净自然的日常场景"}, + ] + if re.search(r"卖点|功能|效果|优惠|价格", text, re.IGNORECASE): + return [ + {"label": "补充真实卖点", "text": "我来补充商品真实卖点"}, + {"label": "从素材里判断", "text": "先根据我上传的素材判断可表达的卖点"}, + {"label": "先只突出一个点", "text": "先围绕一个最核心的卖点创作"}, + ] + # 没有足够语境时只留自由输入,不伪造“继续/改卖点”这种人机按钮。 + return [] kind = "短视频" if is_video else "商品图" return [ {"label": f"帮我做一条{kind}" if is_video else "帮我做一张商品图", "text": f"帮我做一条{kind}" if is_video else "帮我做一张商品图"}, @@ -745,7 +768,12 @@ def ensure_turn_guides( return [] hint = default_reply_hint(conversation, has_context=has_context, is_video=is_video) - options = default_reply_options(conversation, has_context=has_context, is_video=is_video) + options = default_reply_options( + conversation, + has_context=has_context, + is_video=is_video, + assistant_text=last_text_bubble.text if last_text_bubble is not None else "", + ) events: list[dict] = [] if last_text_bubble is not None: if text_already_guides(last_text_bubble.text): diff --git a/core/backend/apps/ai/test_creation_agent.py b/core/backend/apps/ai/test_creation_agent.py index a2fd150..cb9fef0 100644 --- a/core/backend/apps/ai/test_creation_agent.py +++ b/core/backend/apps/ai/test_creation_agent.py @@ -29,6 +29,7 @@ from .creation_agent import ( build_system_prompt, build_messages, default_reply_hint, + default_reply_options, ensure_turn_guides, get_creation_chat_model, get_video_gate_stage, @@ -1842,6 +1843,26 @@ class ReplyGuidanceTests(CreationAgentBaseTests): self.assertIn("按这个方向出图", str((bubble.payload or {}).get("reply_hint") or "")) self.assertTrue((bubble.payload or {}).get("reply_options")) + def test_reply_options_follow_the_current_question(self): + options = default_reply_options( + self.conversation, + has_context=True, + is_video=True, + assistant_text="这款耳机一共有六个配色,分别是什么颜色?", + ) + labels = [option["label"] for option in options] + self.assertEqual(labels, ["补充颜色和顺序", "上传各款实物图", "先按当前主款做"]) + self.assertNotIn("改卖点", labels) + + def test_unknown_context_question_does_not_get_fixed_actions(self): + options = default_reply_options( + self.conversation, + has_context=True, + is_video=True, + assistant_text="我先把你刚刚提到的细节整理一下。", + ) + self.assertEqual(options, []) + def test_text_already_guides_detects_reply_instruction(self): self.assertTrue(text_already_guides("这个方向能做。请回复:按这个方向继续。")) self.assertFalse(text_already_guides("这个方向能做。有想调整可以直接说。")) diff --git a/core/backend/apps/projects/test_quick_create.py b/core/backend/apps/projects/test_quick_create.py index 7a995cc..27e1b62 100644 --- a/core/backend/apps/projects/test_quick_create.py +++ b/core/backend/apps/projects/test_quick_create.py @@ -1370,6 +1370,9 @@ class QuickCreateCoordinatorTests(TestCase): + + + diff --git a/core/frontend/src/omni-session-page.css b/core/frontend/src/omni-session-page.css index a71e6c4..d983d3c 100644 --- a/core/frontend/src/omni-session-page.css +++ b/core/frontend/src/omni-session-page.css @@ -163,6 +163,54 @@ white-space: pre-wrap; } +.omni-chat-markdown { + width: 100%; +} + +.omni-chat-markdown > :first-child { + margin-top: 0; +} + +.omni-chat-markdown > :last-child { + margin-bottom: 0; +} + +.omni-chat-markdown h3 { + margin: 0 0 8px; + color: var(--accent-black); + font-size: 14px; + font-weight: 600; + line-height: 1.5; +} + +.omni-chat-markdown p { + margin: 0 0 8px; +} + +.omni-chat-markdown ol, +.omni-chat-markdown ul { + margin: 0 0 8px; + padding-left: 20px; +} + +.omni-chat-markdown li + li { + margin-top: 4px; +} + +.omni-chat-markdown strong { + color: var(--accent-black); + font-weight: 600; +} + +.omni-chat-markdown code { + padding: 1px 4px; + border-radius: var(--r-sm); + color: var(--accent-black); + background: var(--black-alpha-4); + font-family: var(--font-mono); + font-size: .92em; +} + .omni-reply-hint { margin: 8px 0 0; diff --git a/core/frontend/src/routes/omni-create.tsx b/core/frontend/src/routes/omni-create.tsx index b7a677b..96db6ea 100644 --- a/core/frontend/src/routes/omni-create.tsx +++ b/core/frontend/src/routes/omni-create.tsx @@ -556,7 +556,7 @@ export function OmniHistoryPage({ navigate: NavigateFn; onNotify?: (type: "success" | "error" | "info", text: string) => void; }) { - const [filter, setFilter] = useState<"all" | "running" | "completed">("all"); + const [modeFilter, setModeFilter] = useState<"all" | "video" | "image">("all"); const [items, setItems] = useState(null); const [deleteTarget, setDeleteTarget] = useState(null); // 同会话页:onNotify 是内联箭头,进依赖会让列表每次 App 重渲染都重拉一遍 @@ -567,7 +567,7 @@ export function OmniHistoryPage({ let cancelled = false; setItems(null); api - .listCreations(filter === "all" ? {} : { status: filter }) + .listCreations(modeFilter === "all" ? {} : { mode: modeFilter }) .then((page) => { if (!cancelled) setItems(page.results || []); }) @@ -579,7 +579,7 @@ export function OmniHistoryPage({ return () => { cancelled = true; }; - }, [filter]); + }, [modeFilter]); const remove = async (id: string) => { // 软删:会话没了,已生成的图和视频仍留在资产库里 @@ -602,20 +602,20 @@ export function OmniHistoryPage({

创作历史

-

查看和继续全能创作中的每一次独立会话。

+

按视频创作或图片创作查看,并继续每一次独立会话。

{([ ["all", "全部"], - ["running", "进行中"], - ["completed", "已完成"], + ["video", "视频创作"], + ["image", "图片创作"], ] as const).map(([key, label]) => ( @@ -634,7 +634,13 @@ export function OmniHistoryPage({
) : items.length === 0 ? (
-

还没有创作记录,从全能创作开始。

+

+ {modeFilter === "video" + ? "还没有视频创作记录,从全能创作开始。" + : modeFilter === "image" + ? "还没有图片创作记录,从全能创作开始。" + : "还没有创作记录,从全能创作开始。"} +