diff --git a/maritime-ai-service/app/engine/multi_agent/direct_document_preview_payloads.py b/maritime-ai-service/app/engine/multi_agent/direct_document_preview_payloads.py
index ee5ae2dc..7d193a2d 100644
--- a/maritime-ai-service/app/engine/multi_agent/direct_document_preview_payloads.py
+++ b/maritime-ai-service/app/engine/multi_agent/direct_document_preview_payloads.py
@@ -9,6 +9,7 @@
DOC_PREVIEW_HOST_ACTION_TOOL as _DOC_PREVIEW_HOST_ACTION_TOOL,
find_document_host_action_tool,
looks_uploaded_document_course_request as _looks_uploaded_doc_course_request,
+ looks_uploaded_document_lesson_preview_request as _looks_uploaded_doc_lesson_preview_request,
normalize_document_contract_text as _normalize_doc_preview_text,
uploaded_document_attachments_from_state as _uploaded_document_attachments_from_state,
)
@@ -144,25 +145,7 @@ def _should_request_uploaded_doc_preview(
return False
if not _uploaded_document_attachments_from_state(state):
return False
- normalized = _normalize_doc_preview_text(query)
- return any(
- marker in normalized
- for marker in (
- "preview",
- "xem truoc",
- "ban xem truoc",
- "ban nhap",
- "draft",
- "cap nhat bai hoc",
- "tao ban xem truoc",
- "lesson patch",
- "preview_lesson_patch",
- "source_references",
- "citation",
- "trich dan",
- "nguon",
- )
- )
+ return _looks_uploaded_doc_lesson_preview_request(query)
def _resolve_doc_preview_lesson_id(state: AgentState | None) -> str:
diff --git a/maritime-ai-service/app/engine/multi_agent/direct_node_uploaded_context.py b/maritime-ai-service/app/engine/multi_agent/direct_node_uploaded_context.py
index 916e7440..274f792d 100644
--- a/maritime-ai-service/app/engine/multi_agent/direct_node_uploaded_context.py
+++ b/maritime-ai-service/app/engine/multi_agent/direct_node_uploaded_context.py
@@ -8,6 +8,8 @@
from app.engine.multi_agent.document_preview_contract import (
has_uploaded_document_context as _has_uploaded_document_context,
+ looks_uploaded_document_course_request as _looks_uploaded_document_course_request,
+ looks_uploaded_document_lesson_preview_request as _looks_uploaded_document_lesson_preview_request,
uploaded_document_attachments_from_context as _uploaded_document_attachments,
)
from app.engine.multi_agent.direct_session_memory_runtime import (
@@ -121,48 +123,11 @@ def _looks_uploaded_document_preview_request(query: str) -> bool:
folded = _fold_direct_text(query)
if not folded:
return False
- preview_markers = (
- "approval_token",
- "ban nhap",
- "ban xem truoc",
- "citation",
- "diff",
- "lesson patch",
- "preview",
- "preview_lesson_patch",
- "source references",
- "source_references",
- "lap bai giang",
- "soan bai giang",
- "soan giao an",
- "tao bai giang",
- "tao giao an",
- "tao hoc lieu",
- "tao ban xem truoc",
- "tao khoa hoc",
- "thiet ke bai giang",
- "thiet ke khoa hoc",
- "xay dung bai giang",
- "cau truc khoa hoc",
- "toan bo khoa",
- "cay khoa",
- "chia khoa",
- "course architect",
- "course outline",
- "course syllabus",
- "curriculum",
- "de cuong khoa",
- "de cuong mon",
- "giao trinh",
- "ke hoach giang day",
- "learning path",
- "lo trinh hoc",
- "syllabus",
- "generate_course_from_document",
- "trich dan",
- "xem truoc",
+ return _looks_uploaded_document_course_request(
+ folded
+ ) or _looks_uploaded_document_lesson_preview_request(
+ folded
)
- return any(marker in folded for marker in preview_markers)
def _looks_uploaded_context_fact_query(query: str, ctx: dict[str, Any]) -> bool:
diff --git a/maritime-ai-service/app/engine/multi_agent/document_preview_contract.py b/maritime-ai-service/app/engine/multi_agent/document_preview_contract.py
index bba406fb..3acfd3b7 100644
--- a/maritime-ai-service/app/engine/multi_agent/document_preview_contract.py
+++ b/maritime-ai-service/app/engine/multi_agent/document_preview_contract.py
@@ -66,6 +66,45 @@
"outline",
)
+_LESSON_AUTHORING_EXCLUSION_MARKERS = (
+ "bai tap",
+ "bai kiem tra",
+ "cau hoi",
+ "kiem tra",
+ "quiz",
+)
+
+_LESSON_AUTHORING_VERBS = (
+ "build",
+ "create",
+ "lam",
+ "lap",
+ "soan",
+ "tao",
+ "thiet ke",
+ "viet",
+ "write",
+ "xay dung",
+)
+
+_LESSON_PREVIEW_REQUEST_MARKERS = (
+ "approval_token",
+ "ban nhap",
+ "ban xem truoc",
+ "cap nhat bai hoc",
+ "citation",
+ "diff",
+ "draft",
+ "lesson patch",
+ "preview",
+ "preview_lesson_patch",
+ "source references",
+ "source_references",
+ "tao ban xem truoc",
+ "trich dan",
+ "xem truoc",
+)
+
def normalize_document_contract_text(value: Any) -> str:
text = str(value or "").replace("\\_", "_")
@@ -127,6 +166,23 @@ def looks_uploaded_document_course_request(query: str) -> bool:
return any(marker in normalized for marker in _COURSE_REQUEST_MARKERS)
+def _looks_singular_lesson_authoring_request(normalized: str) -> bool:
+ if not ("bai hoc" in normalized or "lesson" in normalized):
+ return False
+ if any(marker in normalized for marker in _LESSON_AUTHORING_EXCLUSION_MARKERS):
+ return False
+ return any(marker in normalized for marker in _LESSON_AUTHORING_VERBS)
+
+
+def looks_uploaded_document_lesson_preview_request(query: str) -> bool:
+ normalized = normalize_document_contract_text(query)
+ if not normalized:
+ return False
+ return any(marker in normalized for marker in _LESSON_PREVIEW_REQUEST_MARKERS) or (
+ _looks_singular_lesson_authoring_request(normalized)
+ )
+
+
def _runtime_tool_name(
tool: Any,
*,
diff --git a/maritime-ai-service/app/engine/multi_agent/tool_collection.py b/maritime-ai-service/app/engine/multi_agent/tool_collection.py
index 80a810fa..e7fbba75 100644
--- a/maritime-ai-service/app/engine/multi_agent/tool_collection.py
+++ b/maritime-ai-service/app/engine/multi_agent/tool_collection.py
@@ -11,7 +11,12 @@
from typing import Any, Optional
from app.core.config import settings
+from app.engine.multi_agent.document_preview_contract import (
+ looks_uploaded_document_course_request as _contract_looks_uploaded_document_course_request,
+ looks_uploaded_document_lesson_preview_request as _contract_looks_uploaded_document_lesson_preview_request,
+)
from app.engine.multi_agent.state import AgentState
+
logger = logging.getLogger(__name__)
@@ -24,6 +29,14 @@ def _normalize_for_intent(query: str) -> str:
return _load_attr("app.engine.multi_agent.direct_intent", "_normalize_for_intent")(query)
+def _looks_uploaded_document_course_request(query: str) -> bool:
+ return _contract_looks_uploaded_document_course_request(query)
+
+
+def _looks_uploaded_document_lesson_preview_request(query: str) -> bool:
+ return _contract_looks_uploaded_document_lesson_preview_request(query)
+
+
def _needs_web_search(query: str) -> bool:
return _load_attr("app.engine.multi_agent.direct_intent", "_needs_web_search")(query)
@@ -277,113 +290,17 @@ def _has_uploaded_document_context_state(state: Optional[AgentState]) -> bool:
def _looks_like_document_preview_request(query: str, state: Optional[AgentState]) -> bool:
if not _has_uploaded_document_context_state(state):
return False
- normalized = _normalize_for_intent(query)
- return any(
- marker in normalized
- for marker in (
- "preview",
- "xem truoc",
- "ban xem truoc",
- "ban nhap",
- "draft",
- "cap nhat bai hoc",
- "lap bai giang",
- "soan bai giang",
- "soan giao an",
- "tao bai giang",
- "tao giao an",
- "tao hoc lieu",
- "tao bai hoc",
- "tao khoa hoc",
- "thiet ke bai giang",
- "thiet ke khoa hoc",
- "xay dung bai giang",
- "cau truc khoa hoc",
- "toan bo khoa",
- "cay khoa",
- "chia khoa",
- "course architect",
- "course outline",
- "course syllabus",
- "curriculum",
- "de cuong khoa",
- "de cuong mon",
- "giao trinh",
- "ke hoach giang day",
- "learning path",
- "lo trinh hoc",
- "syllabus",
- "generate_course_from_document",
- "lesson patch",
- "preview_lesson_patch",
- "source_references",
- "citation",
- "trich dan",
- "nguon",
- )
+ return _looks_uploaded_document_course_request(
+ query
+ ) or _looks_uploaded_document_lesson_preview_request(
+ query
)
def _looks_like_document_course_preview_request(query: str, state: Optional[AgentState]) -> bool:
if not _has_uploaded_document_context_state(state):
return False
- normalized = _normalize_for_intent(query)
- if any(
- marker in normalized
- for marker in (
- "preview_lesson_patch",
- "lesson patch",
- "bai hoc hien tai",
- "cap nhat bai hoc",
- )
- ):
- return False
- return any(
- marker in normalized
- for marker in (
- "generate_course_from_document",
- "lap bai giang",
- "soan bai giang",
- "soan giao an",
- "tao bai giang",
- "tao giao an",
- "tao hoc lieu",
- "course architect",
- "course outline",
- "course syllabus",
- "curriculum",
- "full course",
- "toan bo khoa",
- "cay khoa",
- "chia khoa",
- "chia thanh bai",
- "chia thanh chuong",
- "chuong trinh dao tao",
- "de cuong khoa",
- "de cuong mon",
- "giao trinh",
- "ke hoach giang day",
- "khoa dao tao",
- "khoa day du",
- "khoa hoan chinh",
- "learning path",
- "lo trinh hoc",
- "lo trinh khoa",
- "nhieu bai hoc",
- "nhieu chuong",
- "phan chia bai hoc",
- "syllabus",
- "tao khoa hoc",
- "thiet ke bai giang",
- "thiet ke khoa hoc",
- "xay dung bai giang",
- "cau truc khoa hoc",
- "chuong/bai",
- "chuong bai",
- "module",
- "outline",
- )
- )
+ return _looks_uploaded_document_course_request(query)
def _document_preview_host_action_tools(tools: list[Any]) -> list[Any]:
diff --git a/maritime-ai-service/tests/unit/test_document_context_api.py b/maritime-ai-service/tests/unit/test_document_context_api.py
index 60fdf29a..8ef47f49 100644
--- a/maritime-ai-service/tests/unit/test_document_context_api.py
+++ b/maritime-ai-service/tests/unit/test_document_context_api.py
@@ -507,6 +507,33 @@ def test_uploaded_document_preview_request_bypasses_fact_fast_path():
assert not _looks_uploaded_context_fact_query(query, ctx)
+def test_uploaded_document_lesson_creation_request_bypasses_fact_fast_path():
+ from app.engine.multi_agent.direct_node_uploaded_context import (
+ _looks_uploaded_context_fact_query,
+ _looks_uploaded_document_preview_request,
+ )
+
+ ctx = {
+ "document_context": {
+ "attachments": [
+ {
+ "file_name": "lesson.docx",
+ "media_kind": "document",
+ "parser": "markitdown",
+ "markdown": (
+ "Ke hoach bai hoc thu nghiem Wiii\n"
+ "Chu de: An toan hang hai va approval_token.\n"
+ ),
+ }
+ ]
+ }
+ }
+ query = "tao cho minh bai hoc"
+
+ assert _looks_uploaded_document_preview_request(query)
+ assert not _looks_uploaded_context_fact_query(query, ctx)
+
+
def test_uploaded_document_visual_guard_does_not_describe_frames_without_vision():
from app.engine.multi_agent.direct_node_uploaded_context import (
_build_uploaded_document_visual_guard_answer,
diff --git a/maritime-ai-service/tests/unit/test_document_preview_contract.py b/maritime-ai-service/tests/unit/test_document_preview_contract.py
index e3785f77..b46498bd 100644
--- a/maritime-ai-service/tests/unit/test_document_preview_contract.py
+++ b/maritime-ai-service/tests/unit/test_document_preview_contract.py
@@ -8,6 +8,7 @@
has_document_preview_host_action_tool,
has_uploaded_document_context,
looks_uploaded_document_course_request,
+ looks_uploaded_document_lesson_preview_request,
uploaded_document_attachments_from_context,
uploaded_document_attachments_from_state,
)
@@ -44,6 +45,13 @@ def test_uploaded_document_course_intent_is_shared_by_preview_and_tool_rounds():
assert not looks_uploaded_document_course_request("cap nhat bai hoc hien tai")
+def test_uploaded_document_lesson_preview_intent_is_shared_by_preview_and_tool_rounds():
+ assert looks_uploaded_document_lesson_preview_request("tao cho minh bai hoc")
+ assert looks_uploaded_document_lesson_preview_request("cap nhat bai hoc hien tai")
+ assert looks_uploaded_document_lesson_preview_request("create a lesson from this file")
+ assert not looks_uploaded_document_lesson_preview_request("tao cau hoi cho bai hoc")
+
+
def test_document_preview_forced_tool_choice_prefers_course_when_available():
tools = [
SimpleNamespace(name=DOC_PREVIEW_HOST_ACTION_TOOL),
@@ -59,6 +67,10 @@ def test_document_preview_forced_tool_choice_prefers_course_when_available():
document_preview_forced_tool_choice("cap nhat bai hoc hien tai", tools)
== DOC_PREVIEW_HOST_ACTION_TOOL
)
+ assert (
+ document_preview_forced_tool_choice("tao cho minh bai hoc", tools)
+ == DOC_PREVIEW_HOST_ACTION_TOOL
+ )
def test_extract_document_preview_capabilities_from_state_and_context():
diff --git a/wiii-desktop/src/EmbedApp.tsx b/wiii-desktop/src/EmbedApp.tsx
index f04a9c27..f5c0a6c5 100644
--- a/wiii-desktop/src/EmbedApp.tsx
+++ b/wiii-desktop/src/EmbedApp.tsx
@@ -17,7 +17,7 @@
* 5. Render directly (no shell)
* 6. sendReadySignal() to parent
*/
-import { useEffect, useState } from "react";
+import { lazy, Suspense, useEffect, useState } from "react";
import { ChatView } from "@/components/chat/ChatView";
import { ErrorBoundary } from "@/components/common/ErrorBoundary";
import { ToastContainer } from "@/components/common/Toast";
@@ -35,6 +35,11 @@ import { sendReadySignal, sendError, setParentOrigin } from "@/lib/embed-bridge"
import { buildAuthUserFromJwt, toCompatibilitySettingsRole } from "@/lib/auth-user";
import type { EmbedConfig } from "@/lib/embed-auth";
+const PreviewPanel = lazy(async () => {
+ const mod = await import("@/components/layout/PreviewPanel");
+ return { default: mod.PreviewPanel };
+});
+
export default function EmbedApp() {
const [embedConfig, setEmbedConfig] = useState(null);
const [initError, setInitError] = useState(null);
@@ -312,6 +317,9 @@ export default function EmbedApp() {