From b5f40343280d4d9c03b64abfb5a2265f8aadf2ff Mon Sep 17 00:00:00 2001 From: yuqei Date: Wed, 27 May 2026 11:24:20 +0800 Subject: [PATCH] Refine web governance and chat surfaces --- backend/app/api/chat.py | 34 ++++++++- backend/tests/test_chat_api.py | 31 ++++++++ frontend/src/app/contracts/page.tsx | 17 ++--- .../src/app/documents/new/[type]/page.tsx | 15 +++- frontend/src/app/documents/new/page.tsx | 8 +- frontend/src/app/globals.css | 29 ++++++++ .../src/app/governance/workbench/page.tsx | 2 +- frontend/src/app/profile/page.tsx | 73 ++++++++----------- frontend/src/components/layout/app-topbar.tsx | 7 +- 9 files changed, 150 insertions(+), 66 deletions(-) diff --git a/backend/app/api/chat.py b/backend/app/api/chat.py index ea4551c7..9f1be52f 100644 --- a/backend/app/api/chat.py +++ b/backend/app/api/chat.py @@ -98,6 +98,7 @@ def stream_chat( template_key=template_key, ), local_legal_sources, + payload.message, ), media_type="text/event-stream", ) @@ -347,7 +348,7 @@ def _build_legal_article_source_title(ref: Mapping[str, Any]) -> str: return f"{source_title} {article_no}".strip() -def _merge_local_sources_into_stream(upstream: Iterable[str], local_sources: List[Dict[str, Any]]) -> Iterable[str]: +def _merge_local_sources_into_stream(upstream: Iterable[str], local_sources: List[Dict[str, Any]], message: str = "") -> Iterable[str]: if not local_sources: yield from upstream return @@ -355,6 +356,10 @@ def _merge_local_sources_into_stream(upstream: Iterable[str], local_sources: Lis emitted_sources = False for chunk in upstream: event = _parse_sse_event(chunk) + if event and event.get("type") == "text": + content = str(event.get("content") or "") + yield _format_sse_event({"type": "text", "content": _polish_chat_answer(content, message, local_sources)}) + continue if event and event.get("type") == "sources": merged = _dedupe_sources([*(event.get("sources") or []), *local_sources]) emitted_sources = True @@ -366,6 +371,33 @@ def _merge_local_sources_into_stream(upstream: Iterable[str], local_sources: Lis yield chunk +def _polish_chat_answer(content: str, message: str, sources: List[Dict[str, Any]]) -> str: + if not _looks_like_raw_retrieval_answer(content): + return content + source_titles = [str(source.get("title") or "依据来源").strip() for source in sources[:3] if source.get("title")] + source_line = "、".join(source_titles) if source_titles else "下方引用来源" + return ( + "我已检索到相关依据,但当前平台返回的是依据片段而不是完整法律分析。\n\n" + f"针对你的问题“{message.strip()}”,建议先按以下方向人工核查:\n" + "1. 明确付款节点、付款前置条件、发票要求和验收标准是否写清。\n" + "2. 检查逾期付款、拒付、扣款和违约金规则是否具体、对等、可执行。\n" + "3. 核对付款条款是否和交付、验收、解除、争议解决条款互相冲突。\n\n" + f"可优先复核这些依据:{source_line}。" + ) + + +def _looks_like_raw_retrieval_answer(content: str) -> bool: + normalized = content.strip().lower() + if not normalized: + return False + return ( + normalized.startswith("i found relevant knowledge-base content") + or "please treat this as ai-assisted guidance" in normalized + or "chapter_title:" in normalized + or "official_url:" in normalized + ) + + def _parse_sse_event(chunk: str) -> Optional[Dict[str, Any]]: for line in chunk.splitlines(): if not line.startswith("data: "): diff --git a/backend/tests/test_chat_api.py b/backend/tests/test_chat_api.py index 033c43d4..6b7eb8f0 100644 --- a/backend/tests/test_chat_api.py +++ b/backend/tests/test_chat_api.py @@ -371,6 +371,37 @@ class ChatApiTestCase(unittest.TestCase): self.assertEqual(review_context["legalArticleSearchSources"][0]["source_id"], "lart-cyber-001") self.assertEqual(review_context["metadata"]["legalArticleSearchSourceCount"], 1) + def test_stream_chat_polishes_raw_retrieval_answer_when_local_sources_exist(self): + class RawRetrievalChatService(StubChatService): + def stream_answer(self, current_user, conversation_id, message, review_context=None, template_key=None): + yield 'data: {"type":"text","content":"I found relevant knowledge-base content for this question:\\n1. official_url: https://example.test chapter_title: 第一章\\n\\nPlease treat this as AI-assisted guidance and review the cited source text."}\n\n' + yield 'data: {"type":"end","conversation_id":"conv-raw"}\n\n' + + app.dependency_overrides[require_current_user] = lambda: {"id": "user-1", "tenant_id": "tenant-1"} + app.dependency_overrides[get_shared_ai_platform_client] = lambda: RawRetrievalChatService() + app.dependency_overrides[get_chat_legal_article_service] = lambda: StubLegalArticleService( + [ + { + "title": "中华人民共和国民法典 第五百零九条", + "content": "当事人应当按照约定全面履行自己的义务。", + "source_type": "legal_article", + "source_id": "lart-civil-509", + "detail_path": "/legal-articles?article_id=lart-civil-509", + } + ] + ) + + response = self.client.post("/api/v1/chat/stream", json={"message": "这份合同的付款条款有哪些风险?请用中文直接回答。"}) + + self.assertEqual(response.status_code, 200) + events = [line.removeprefix("data: ") for line in response.text.split("\n\n") if line.startswith("data: ")] + payloads = [json.loads(item) for item in events] + text_event = next(item for item in payloads if item["type"] == "text") + + self.assertIn("我已检索到相关依据", text_event["content"]) + self.assertIn("付款节点", text_event["content"]) + self.assertNotIn("I found relevant knowledge-base content", text_event["content"]) + def test_list_conversations_returns_paginated_payload(self): app.dependency_overrides[require_current_user] = lambda: {"id": "user-1", "tenant_id": "tenant-1"} diff --git a/frontend/src/app/contracts/page.tsx b/frontend/src/app/contracts/page.tsx index 23f97827..a7b40bf2 100644 --- a/frontend/src/app/contracts/page.tsx +++ b/frontend/src/app/contracts/page.tsx @@ -882,21 +882,16 @@ export default function ContractsPage() {
- Contract Generation + 合同生成 从模板到下载,一屏完成合同生成。 先选模板,再填写变量;预览确认后生成文件,历史结果保留在下方。
- Smart Workflow + 合同生成中心 - 当前链路 + 继续处理 -
- 选模板 - 填变量 - 生成文件 -
- 支持 DOCX、PDF、HTML、TXT 导出;从审查结果进入时会自动带入上下文。 + 选模板、填变量、预览和生成都在这一页完成,历史记录放在下方。
@@ -943,12 +938,13 @@ export default function ContractsPage() { ) : ( }} - pagination={{ pageSize: 10 }} + pagination={{ pageSize: 6, responsive: true }} scroll={{ x: 900 }} /> )} @@ -1186,6 +1182,7 @@ export default function ContractsPage() { } >
{riskLabelMap[documentType?.risk_level ?? ""] ?? documentType?.risk_level ?? "风险等级"} - {schema?.version_label ?? "schema"} + 要素版本 {schema?.version_label ?? "当前"}
@@ -747,7 +753,7 @@ export default function LegalDocumentBuilderPage({ params }: { params: Promise<{ type="warning" style={{ marginBottom: 16 }} message={`当前还有 ${validationErrorCount} 个要素需要补充或修正`} - description="可以先生成初稿,缺失必填字段会以“请补充”标识;建议正式交付前补齐。" + description="请先补齐必填字段,再生成正式合同。你仍可保存草稿,稍后继续完善。" /> ) : validationResult ? ( @@ -1077,8 +1084,8 @@ export default function LegalDocumentBuilderPage({ params }: { params: Promise<{ 生成来源
- 文书类型:{typeCode ?? "-"} - schema:{schema?.version_label ?? "-"} + 文书类型:{readableDocumentName(documentType, typeCode ?? undefined)} + 要素版本:{schema?.version_label ?? "当前"} 启用模板:{templates.length} 个 当前校验:{validationResult ? (validationResult.valid ? "通过" : "待补充") : "未校验"} diff --git a/frontend/src/app/documents/new/page.tsx b/frontend/src/app/documents/new/page.tsx index 3e4e2e77..cd0fb8d8 100644 --- a/frontend/src/app/documents/new/page.tsx +++ b/frontend/src/app/documents/new/page.tsx @@ -204,14 +204,14 @@ export default function NewDocumentPage() {
- Legal Documents + 法律文书 按业务场景起草法律文书 从文书类型、行业和使用场景进入,再填写动态要素。系统会保存草稿,并把生成来源、待补字段、AI 提醒和相关法条带到结果页。
- Phase15-A + 文书入口 文书类型入口
@@ -307,8 +307,8 @@ export default function NewDocumentPage() {
- 下一阶段 - Phase15-A 前端闭环完成后,继续进入审查建议采纳闭环。 + 继续处理 + 生成文书后,可以进入审查记录继续查看风险、下载报告或追问条款。
diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css index c42b88ff..869230f7 100644 --- a/frontend/src/app/globals.css +++ b/frontend/src/app/globals.css @@ -4340,6 +4340,35 @@ body::selection { max-height: 360px; font-size: 12px; } + + .contracts-page .review-side-card, + .contracts-page .ant-steps { + display: none; + } + + .contracts-page .ant-card-head, + .contracts-page .ant-card-body { + padding: 14px !important; + } + + .contracts-page .ant-table-cell { + padding: 10px 8px !important; + font-size: 12px; + } + + .contracts-template-table .ant-table-cell:nth-child(2), + .contracts-template-table .ant-table-cell:nth-child(4), + .contracts-history-table .ant-table-cell:nth-child(2), + .contracts-history-table .ant-table-cell:nth-child(3), + .contracts-history-table .ant-table-cell:nth-child(5), + .contracts-history-table .ant-table-cell:nth-child(6) { + display: none; + } + + .contracts-page .review-surface-card .ant-card-extra .ant-input-search, + .contracts-page .review-surface-card .ant-card-extra .ant-select { + width: 100% !important; + } } /* 2026 full-interface polish: shared product workbench system */ diff --git a/frontend/src/app/governance/workbench/page.tsx b/frontend/src/app/governance/workbench/page.tsx index 1ae0bc5f..d5515b8e 100644 --- a/frontend/src/app/governance/workbench/page.tsx +++ b/frontend/src/app/governance/workbench/page.tsx @@ -48,7 +48,7 @@ type EvidenceFormValues = { export default function GovernanceWorkbenchPage() { const [messageApi, contextHolder] = message.useMessage(); const currentUser = useAuthStore((state) => state.currentUser); - const canGovernanceOperate = currentUser?.role === "admin" || currentUser?.role === "legal" || currentUser?.role === "business"; + const canGovernanceOperate = currentUser?.role === "admin" || currentUser?.role === "legal"; const canManageAssignments = currentUser?.role === "admin"; const [assignmentForm] = Form.useForm(); const [decisionForm] = Form.useForm(); diff --git a/frontend/src/app/profile/page.tsx b/frontend/src/app/profile/page.tsx index 8d074de5..fe40c6fb 100644 --- a/frontend/src/app/profile/page.tsx +++ b/frontend/src/app/profile/page.tsx @@ -19,16 +19,16 @@ type ProfileFormValues = { function formatSharedAiMode(mode: SharedAiStatusResponse["mode"]) { if (mode === "remote-preferred") { return { - color: "processing", - label: "Remote preferred", - description: "chat, knowledge, and legal review are currently expected to use shared-ai-platform remote capabilities.", + color: "success", + label: "AI 服务可用", + description: "合同问答、知识检索和合同审查服务当前可正常使用。", }; } return { color: "warning", - label: "Configuration required", - description: "shared-ai-platform is not fully configured for current AI features.", + label: "AI 服务待配置", + description: "部分 AI 功能暂时不可用,请联系管理员检查服务配置。", }; } @@ -123,19 +123,19 @@ export default function ProfilePage() {
- Profile Settings - 管理当前账号信息,并查看 Shared AI 接入状态。 - 这里保留个人资料与密码设置,同时把当前站点接入 shared-ai-platform 的运行模式直接展示出来。 + 个人设置 + 管理当前账号信息。 + 你可以在这里更新显示名称和登录密码。
- My Workspace + 账号信息 - 日常操作边界 + 日常设置
  • 显示名称会同步到顶部导航。
  • 密码留空时不会改动现有登录凭证。
  • -
  • Shared AI 状态可以帮助联调 chat、knowledge、legal review 的实际后端链路。
  • +
  • 如需查看系统配置,请联系管理员。
@@ -185,16 +185,17 @@ export default function ProfilePage() { - void loadSharedAiStatus()} loading={sharedAiLoading}> - 刷新状态 - - } - loading={sharedAiLoading && !sharedAiStatus} - > + {currentUser?.role === "admin" ? ( + void loadSharedAiStatus()} loading={sharedAiLoading}> + 刷新状态 + + } + loading={sharedAiLoading && !sharedAiStatus} + > {sharedAiError ? ( - {sharedAiStatus.app_id} - {sharedAiStatus.api_prefix} - - {sharedAiStatus.transport.sdk} / {sharedAiStatus.transport.fallback} - - - {sharedAiStatus.knowledge_defaults.retrieval_mode} - - - {sharedAiStatus.base_url ?? "not configured"} - + 合同问答、知识检索、合同审查 + {sharedAiStatus.knowledge_defaults.retrieval_mode === "hybrid" ? "综合检索" : "标准检索"} - chat stream - conversation history - knowledge base - document ingest - document retry - legal-review domain pack - {sharedAiStatus.capabilities.local_legal_review_fallback ? ( - legacy fallback configured - ) : null} + 问答服务 + 会话历史 + 知识库 + 文档处理 + 合同审查
) : null} - + + ) : null} ); diff --git a/frontend/src/components/layout/app-topbar.tsx b/frontend/src/components/layout/app-topbar.tsx index 48ef81b9..5b41a8f5 100644 --- a/frontend/src/components/layout/app-topbar.tsx +++ b/frontend/src/components/layout/app-topbar.tsx @@ -62,7 +62,6 @@ function getPrivateNavItems(role?: string | null) { ...sharedItems, { href: "/documents/new", label: "文书起草", icon: }, { href: "/contracts", label: "合同生成", icon: }, - ...(normalizedRole === "business" ? [{ href: "/governance/workbench", label: "复核工作台", icon: }] : []), { href: "/reviews", label: "审查记录", icon: }, { href: "/chat", label: "法律问答", icon: }, { href: "/profile", label: "个人设置", icon: }, @@ -147,10 +146,10 @@ export function AppTopbar() {
- AI +
- AI 合同平台 - Contract Review Workbench + 合同平台 + 合同审查工作台