Эх сурвалжийг харах

feat(prototype): refine P0 conversational agent flows

wangkangyjy 22 цаг өмнө
parent
commit
cb75bca763
23 өөрчлөгдсөн 1688 нэмэгдсэн , 125 устгасан
  1. BIN
      artifacts/p0-conversation-first/01-portal-home.png
  2. BIN
      artifacts/p0-conversation-first/02-registration-slot-fill.png
  3. BIN
      artifacts/p0-conversation-first/03-registration-result.png
  4. BIN
      artifacts/p0-conversation-first/04-report-markdown-result.png
  5. BIN
      artifacts/p0-conversation-first/05-tongue-combined-result.png
  6. BIN
      artifacts/p0-conversation-first/tile-01.png
  7. BIN
      artifacts/p0-conversation-first/tile-02.png
  8. BIN
      artifacts/p0-conversation-first/tile-03.png
  9. BIN
      artifacts/p0-conversation-first/tile-04.png
  10. BIN
      artifacts/p0-conversation-first/tile-05.png
  11. BIN
      artifacts/p0-conversation-first/医梦P0_对话式三智能体原型总览.png
  12. 61 69
      prototype/adjutant-mobile/design-qa.md
  13. BIN
      prototype/adjutant-mobile/qa/comparison-composer.png
  14. BIN
      prototype/adjutant-mobile/qa/comparison-services.png
  15. BIN
      prototype/adjutant-mobile/qa/implementation-composer.png
  16. BIN
      prototype/adjutant-mobile/qa/implementation-home.png
  17. BIN
      prototype/adjutant-mobile/qa/implementation-registration-full.png
  18. BIN
      prototype/adjutant-mobile/qa/implementation-services-full.png
  19. BIN
      prototype/adjutant-mobile/qa/implementation-services.png
  20. BIN
      prototype/adjutant-mobile/qa/reference-composer-normalized.png
  21. BIN
      prototype/adjutant-mobile/qa/reference-services-normalized.png
  22. 715 45
      prototype/adjutant-mobile/src/Prototype.tsx
  23. 912 11
      prototype/adjutant-mobile/src/prototype.css

BIN
artifacts/p0-conversation-first/01-portal-home.png


BIN
artifacts/p0-conversation-first/02-registration-slot-fill.png


BIN
artifacts/p0-conversation-first/03-registration-result.png


BIN
artifacts/p0-conversation-first/04-report-markdown-result.png


BIN
artifacts/p0-conversation-first/05-tongue-combined-result.png


BIN
artifacts/p0-conversation-first/tile-01.png


BIN
artifacts/p0-conversation-first/tile-02.png


BIN
artifacts/p0-conversation-first/tile-03.png


BIN
artifacts/p0-conversation-first/tile-04.png


BIN
artifacts/p0-conversation-first/tile-05.png


BIN
artifacts/p0-conversation-first/医梦P0_对话式三智能体原型总览.png


+ 61 - 69
prototype/adjutant-mobile/design-qa.md

@@ -1,88 +1,80 @@
-# Product Design QA — Adjutant P0 complete flow
+# Design QA — 医梦患者智能服务门户 P0
 
 ## Comparison target
 
-- Source visual truth: `final-visual-target.png`
-- Browser-rendered implementation: `implementation-home-p0-complete.png`
-- Normalized phone-screen crop: `implementation-home-p0-screen.png`
-- Side-by-side evidence: `design-comparison-p0-complete.png`
-- Source pixels: 852 × 1846
-- Browser screenshot pixels: 679 × 818
-- Normalized comparison: 393 × 852 per side, deviceScaleFactor 1
-- State: single-hospital P0 portal home, fixed Mock patient
-
-The source is a full scroll-length mobile board without device chrome. The implementation uses the protected iPhone runtime and a visible above-the-fold viewport. The comparison therefore evaluates app-owned hierarchy, spacing, colors, cards, copy and entry prominence; the device bezel and scroll crop are expected runtime differences.
+- Source visual truth:
+  - `qa/reference-services-normalized.png`
+  - `qa/reference-composer-normalized.png`
+- Implementation:
+  - `qa/implementation-services.png`
+  - `qa/implementation-composer.png`
+- Combined comparison evidence:
+  - `qa/comparison-services.png`
+  - `qa/comparison-composer.png`
+- Browser viewport: `1280 × 720`
+- Mobile Web content viewport: `428 × 720 CSS px`
+- Density: browser screenshot at 1×
+- Source pixels:
+  - Service reference: `849 × 1888`, normalized to `324 × 702`
+  - Composer reference: `1178 × 289`, normalized to `428 × 105`
+- Implementation pixels:
+  - Service catalog: `428 × 720`
+  - Composer region: `428 × 190`
+- State:
+  - Service catalog: 门诊服务 / 诊前
+  - Conversation: 智能分诊挂号 / 医生补槽
+
+The references describe component structure and interaction density rather than the same hospital content. Native status bars and provider logos were therefore excluded from fidelity requirements; the P0 target is explicitly a mobile Web portal.
 
 ## Full-view comparison
 
-No actionable P0, P1 or P2 mismatch remains.
+- The service page keeps the reference hierarchy: search, first-level horizontal directory, second-level stage chips, then vertically stacked agent cards.
+- The implementation uses hospital-facing categories from the supplied Excel and preserves the current医梦 color system rather than copying the reference app's branding.
+- Enabled and disabled services are visually distinct without changing the page skeleton.
+- No device bezel, native status bar, native home indicator, or simulated native keyboard is present in the final mobile Web view.
 
-- Fonts and typography: heading hierarchy, weights, muted supporting copy and compact card labels match the selected direction.
-- Spacing and layout rhythm: hospital header, assistant entry, status/todo strip, registration hero, two secondary services and bottom navigation preserve the source order and density.
-- Colors and visual tokens: medical purple, cyan accent, pale blue surface and white-card system are consistent with the source.
-- Image quality and asset fidelity: the supplied registration illustration and brand mark render sharply; Radix icons are used for standard interface actions.
-- Copy and content: the only intentional product change is “语音挂号”, which reflects the frozen voice-first interaction model more accurately than “开始分诊”.
+## Focused-region comparison
 
-Focused region comparison was not required because the implementation reuses the already approved home visual system and the new work is primarily additional route/state behavior. The report-confirmation and tongue-processing screens use the same established card, type and spacing tokens.
+- The composer comparison verifies the required three-part control: voice on the left, text in the center, and camera/send on the right.
+- A single clickable “推荐回答” row is placed directly above the composer, matching the requested patient guidance pattern.
+- The focused comparison was required because the input controls and suggestion affordance are too small to judge reliably in the full service-page view.
 
-## Interaction verification
+## Required fidelity surfaces
 
-Verified in the Codex in-app browser at `http://127.0.0.1:4173/`:
+- Typography: clear title/body/helper hierarchy; compact Chinese mobile-web sizes remain readable; no problematic wrapping in the compared states.
+- Spacing and layout: stable 18 px page margins, pill-shaped search and composer, consistent card rhythm, and persistent bottom controls without overlap.
+- Colors and tokens: existing医梦 purple/cyan semantic tokens retained; disabled services use neutral opacity and enabled P0 services use distinct semantic colors.
+- Image and icon quality: standard icon-library vectors are used for portal capabilities. External provider logos from the reference were intentionally not copied because the cards represent医梦 platform capabilities, not third-party organizations.
+- Copy and content: service names follow the supplied Excel; only the three P0 services are labeled “P0 已开通”; default answers use realistic patient language.
 
-1. Home “语音挂号” → voice transcript → persistent 5/6 slot summary → modify department → missing doctor follow-up → complete 6/6 slot summary → query schedule.
-2. Registration emergency demo → ordinary registration stops → emergency guidance and 120 action appear.
-3. New-report todo → report voice context + known-information panel → select hospital report → owner/source/final-status confirmation.
-4. Tongue service → voice context + known-information panel → consent → symptom question → qualified demo image → tongue MCP processing state.
-5. Services, health records and root navigation remain accessible.
-6. The expandable “本次对话与操作记录” shows the voice transcription, parameter extraction, safety screening and completion events.
+## Comparison history
 
-Browser console: no errors; only Vite connection and React development information messages.
+### Pass 1
 
-## Comparison history
+- [P2] Native runtime residue remained visible as a home indicator and mobile cursor.
+  - Fix: hid runtime-only status, home-indicator, cursor, device frame, device picker, and keyboard elements in the app-owned Web presentation stylesheet.
+- [P2] The first conversation composer did not yet mirror the reference's voice/text/photo hierarchy.
+  - Fix: rebuilt it as a rounded Web composer and added a separate default-answer row above it.
+
+### Pass 2
+
+- Revised captures show no remaining native chrome.
+- Service navigation, disabled state, P0 entry state, and the conversation recommendation control are visually clear.
+- No actionable P0, P1, or P2 findings remain.
+
+## Interaction checks
 
-### Iteration 1
-
-- Finding: the report todo previously opened a completed result directly.
-- Fix: it now enters the report agent, confirms source and ownership, then proceeds to processing.
-- Finding: the tongue flow skipped the visible MCP-processing stage.
-- Fix: added image-quality completion, existing tongue MCP call and evidence-fusion progress.
-- Finding: the emergency branch was not directly reachable from the voice-first registration entry.
-- Fix: added an explicit P0 emergency-door demo and urgent text routing.
-- Post-fix evidence: all three paths and the emergency branch were exercised in the in-app browser.
-
-### Iteration 2 — conversational task interface
-
-- Finding: the first version minimized dialogue too aggressively, so patients could not easily review what they said or how the agent understood it.
-- Fix: every agent task start now preserves the latest patient utterance and the assistant's interpretation.
-- Finding: registration slots were only shown after all information was complete.
-- Fix: the slot panel is persistent from the first follow-up, highlights the missing doctor, reports 5/6 or 6/6 completion and supports field modification.
-- Finding: report and tongue entry states looked like one-off navigation cards rather than the same agent interaction system.
-- Fix: both now use the shared pattern of recent dialogue, known context, current task and expandable operation history.
-- Finding: the previous dark transcript surface had weak text contrast in the rendered device frame.
-- Fix: the patient utterance uses a light brand-tint surface with explicit dark text.
-- Evidence:
-  - `artifacts/product-design/conversational-task-ui/01-registration-followup.png`
-  - `artifacts/product-design/conversational-task-ui/02-registration-complete.png`
-  - `artifacts/product-design/conversational-task-ui/03-report-conversational-task.png`
-  - `artifacts/product-design/conversational-task-ui/04-tongue-conversational-task.png`
-
-### Iteration 3 — complete P0 interaction prototype
-
-- Unified assistant: voice input now stops at an explicit route-confirmation step, shows the target service and inherited patient context, and only then enters the selected task.
-- Registration: completed doctor slot filling, 6/6 parameter confirmation, schedule query, schedule selection, Mock lock/submit, success result, no-slot/lock-failure recovery and emergency safety-gate branches.
-- Report interpretation: completed report selection/upload, OCR failure recovery, report ownership confirmation, processing, structured interpretation and high-risk priority guidance.
-- TCM tongue assessment: completed consent, questionnaire, tongue-image capture, existing MCP call, MCP failure recovery and combined result.
-- Portal linkage: the home page now exposes appointment, new-report and unfinished-tongue states; health records include completed, waiting and failed tasks with direct resume actions.
-- Browser verification: all normal, failure-recovery and medical-safety branches were clicked through in the in-app browser. Console errors: none.
-- Review boards:
-  - `artifacts/p0-complete-prototype/boards/医梦P0_门户总览长图.png`
-  - `artifacts/p0-complete-prototype/boards/医梦P0_挂号智能体流程长图.png`
-  - `artifacts/p0-complete-prototype/boards/医梦P0_报告解读流程长图.png`
-  - `artifacts/p0-complete-prototype/boards/医梦P0_中医舌诊流程长图.png`
+- Opened 服务 and switched 门诊服务 from 诊前 to 诊中.
+- Confirmed only 智能分诊挂号、中医舌诊、检验检查报告解读 are enabled.
+- Confirmed disabled agents cannot be clicked.
+- Used the recommended answer to move registration from initial request to doctor follow-up and Mock slot selection.
+- Used the recommended answers to complete report upload/analysis and reach the structured report result.
+- Used the recommended answers to complete tongue questions, image upload, MCP processing, and the综合结果.
+- Browser console warnings/errors checked: none.
+- Runtime integrity, production build, and Sites packaging tests all passed.
 
 ## Follow-up polish
 
-- P3: add a real speech-to-text adapter when the P0 demo moves beyond scripted voice transcripts.
-- P3: connect task state to a shared mock store so newly completed tasks change the record list during the same session.
+- [P3] After real hospital branding is confirmed, replace the generic hospital mark with the hospital-provided logo asset.
 
 final result: passed

BIN
prototype/adjutant-mobile/qa/comparison-composer.png


BIN
prototype/adjutant-mobile/qa/comparison-services.png


BIN
prototype/adjutant-mobile/qa/implementation-composer.png


BIN
prototype/adjutant-mobile/qa/implementation-home.png


BIN
prototype/adjutant-mobile/qa/implementation-registration-full.png


BIN
prototype/adjutant-mobile/qa/implementation-services-full.png


BIN
prototype/adjutant-mobile/qa/implementation-services.png


BIN
prototype/adjutant-mobile/qa/reference-composer-normalized.png


BIN
prototype/adjutant-mobile/qa/reference-services-normalized.png


+ 715 - 45
prototype/adjutant-mobile/src/Prototype.tsx

@@ -1,4 +1,4 @@
-import { useState } from "react";
+import { useEffect, useState } from "react";
 import {
   ArrowLeftIcon,
   CalendarIcon,
@@ -22,10 +22,13 @@ import {
   UploadIcon,
 } from "@radix-ui/react-icons";
 import {
+  Carousel,
   FlowStack,
   KeyboardInput,
   KeyboardTextarea,
   MobileScroll,
+  useKeyboard,
+  useKeyboardInsets,
   type FlowControls,
   type FlowScreen,
 } from "./mobile";
@@ -152,7 +155,11 @@ function HomeContent({ flow }: { flow: FlowControls }) {
             </span>
             <em>待解读</em>
           </button>
-          <button className="patient-state-row tongue" type="button" onClick={() => flow.push(tongueCaptureScreen)}>
+          <button
+            className="patient-state-row tongue"
+            type="button"
+            onClick={() => flow.push(makeAssistantScreen("tongue", "继续上次已保存的体质问答"))}
+          >
             <span className="state-icon"><HeartIcon /></span>
             <span>
               <strong>中医舌诊待完成</strong>
@@ -230,57 +237,229 @@ function HomeContent({ flow }: { flow: FlowControls }) {
 }
 
 function ServicesContent({ flow }: { flow: FlowControls }) {
-  const services = [
+  const catalog = [
     {
-      title: "智能分诊挂号",
-      desc: "症状分析、科室推荐、模拟预约",
-      icon: CalendarIcon,
-      screen: makeAssistantScreen("registration"),
+      id: "outpatient",
+      label: "门诊服务",
+      stages: [
+        {
+          id: "before",
+          label: "诊前",
+          agents: [
+            { title: "智能导诊", desc: "院内服务与就医流程引导" },
+            { title: "智能分诊", desc: "根据症状推荐合适科室" },
+            { title: "智能分诊挂号", desc: "语音补槽、Mock 号源与预约确认", mode: "registration" as const },
+            { title: "门诊签到叫号", desc: "到院签到与候诊进度提醒" },
+          ],
+        },
+        {
+          id: "during",
+          label: "诊中",
+          agents: [
+            { title: "中医舌诊", desc: "体质问答、舌象采集与综合建议", mode: "tongue" as const },
+            { title: "临床决策支持", desc: "为诊疗过程提供知识辅助" },
+            { title: "检验检查预约", desc: "检查项目预约与注意事项" },
+            { title: "检验检查报告解读", desc: "上传报告、结构化解读与风险提醒", mode: "report" as const },
+            { title: "门诊缴费", desc: "门诊费用查询与缴费引导" },
+            { title: "病历书写", desc: "病历信息整理与结构化生成" },
+            { title: "病历质控", desc: "门诊病历完整性与规范检查" },
+            { title: "自助门诊场景", desc: "常见门诊服务自助办理" },
+          ],
+        },
+        {
+          id: "after",
+          label: "诊后",
+          agents: [
+            { title: "智能随访", desc: "诊后情况采集与随访提醒" },
+            { title: "康复计划生成", desc: "个体化康复任务与计划" },
+            { title: "智能用药提醒", desc: "按医嘱提供用药时间提醒" },
+          ],
+        },
+      ],
     },
     {
-      title: "报告智能解读",
-      desc: "检验检查报告、异常指标、就医建议",
-      icon: FileTextIcon,
-      screen: makeAssistantScreen("report"),
+      id: "inpatient",
+      label: "住院服务",
+      stages: [
+        {
+          id: "admission",
+          label: "入院前",
+          agents: [
+            { title: "智能预住院评估", desc: "入院前资料与风险信息采集" },
+            { title: "智能预约与入院办理", desc: "预约床位与入院流程引导" },
+            { title: "住院费预交", desc: "预交金查询与缴纳引导" },
+          ],
+        },
+        {
+          id: "ward",
+          label: "住院中",
+          agents: [
+            { title: "入院宣教 / 入科接待", desc: "入科须知和病区服务介绍" },
+            { title: "智慧病历管理", desc: "住院病历信息整理与管理" },
+            { title: "临床决策支持", desc: "住院诊疗知识辅助" },
+            { title: "检验检查报告解读", desc: "住院报告结构化解读" },
+            { title: "AI 病区物联网", desc: "病区设备与状态智能协同" },
+            { title: "智慧护理", desc: "护理任务与患者服务协同" },
+            { title: "智能出院准备与规划", desc: "出院条件与后续事项准备" },
+          ],
+        },
+        {
+          id: "discharge",
+          label: "出院后",
+          agents: [
+            { title: "床旁结算", desc: "出院费用与床旁结算引导" },
+            { title: "远程管理", desc: "出院后健康状态远程管理" },
+            { title: "智能随访", desc: "出院后恢复情况随访" },
+            { title: "康复计划生成", desc: "院外康复任务规划" },
+            { title: "智能用药提醒", desc: "出院用药提醒与反馈" },
+          ],
+        },
+      ],
     },
     {
-      title: "中医舌诊",
-      desc: "体质问答、舌象采集、综合建议",
-      icon: HeartIcon,
-      screen: makeAssistantScreen("tongue"),
+      id: "checkup",
+      label: "体检服务",
+      stages: [
+        {
+          id: "checkup-before",
+          label: "检前",
+          agents: [
+            { title: "调查问卷", desc: "体检前健康信息采集" },
+            { title: "方案设计", desc: "个体化体检方案建议" },
+            { title: "预约体检", desc: "体检时间与套餐预约" },
+            { title: "检前宣教", desc: "体检准备事项提醒" },
+          ],
+        },
+        {
+          id: "checkup-during",
+          label: "检中",
+          agents: [
+            { title: "智能签到", desc: "到院签到与身份确认" },
+            { title: "智能导检", desc: "检查顺序与位置引导" },
+            { title: "空腹项目优先", desc: "智能安排空腹检查项目" },
+            { title: "科室检查", desc: "检查科室流程协同" },
+            { title: "标本采集", desc: "采集项目与注意事项提示" },
+            { title: "营养早餐", desc: "空腹项目结束后的营养服务" },
+          ],
+        },
+        {
+          id: "checkup-after",
+          label: "检后",
+          agents: [
+            { title: "智能辅助总检审核", desc: "体检结论辅助审核" },
+            { title: "报告生成", desc: "体检结果汇总与报告生成" },
+            { title: "报告解读", desc: "体检报告健康解读" },
+            { title: "绿色通道", desc: "异常结果进一步就医协助" },
+            { title: "档案建立", desc: "院内体检健康记录归档" },
+            { title: "定期随访", desc: "体检后定期健康随访" },
+          ],
+        },
+      ],
+    },
+    {
+      id: "bi",
+      label: "运营管理",
+      stages: [
+        {
+          id: "bi-all",
+          label: "全部",
+          agents: [
+            { title: "全局运营数据问答", desc: "医院运营指标查询与解释" },
+            { title: "运营预测", desc: "运营趋势与资源需求预测" },
+            { title: "财务管控", desc: "财务指标分析与风险提示" },
+            { title: "物资管理", desc: "库存、消耗和补给管理" },
+            { title: "质量管理", desc: "医疗质量指标监测" },
+            { title: "人力绩效", desc: "人员与绩效数据分析" },
+            { title: "设备运维", desc: "设备状态与运维任务管理" },
+          ],
+        },
+      ],
     },
   ];
+  const [clusterId, setClusterId] = useState("outpatient");
+  const cluster = catalog.find((item) => item.id === clusterId) ?? catalog[0];
+  const [stageId, setStageId] = useState(cluster.stages[0].id);
+  const activeStage = cluster.stages.find((stage) => stage.id === stageId) ?? cluster.stages[0];
+
+  const selectCluster = (id: string) => {
+    const next = catalog.find((item) => item.id === id) ?? catalog[0];
+    setClusterId(id);
+    setStageId(next.stages[0].id);
+  };
+
   return (
     <MobileScroll className="portal-scroll">
-      <main className="portal-page root-list-page">
-        <header>
-          <h1>服务</h1>
-          <p>空海医院已开通的智能服务</p>
+      <main className="portal-page root-list-page service-catalog-page">
+        <header className="catalog-header">
+          <span>医梦患者智能服务门户</span>
+          <h1>智能服务</h1>
+          <p>浏览医梦可为医院提供的智能体能力</p>
         </header>
-        <section className="grouped-list">
-          {services.map((service) => {
-            const Icon = service.icon;
+
+        <label className="service-search">
+          <ReaderIcon />
+          <KeyboardInput placeholder="搜索智能体服务" aria-label="搜索智能体服务" />
+        </label>
+
+        <Carousel ariaLabel="智能体业务目录" className="catalog-cluster-carousel">
+          {catalog.map((item) => (
+            <button
+              key={item.id}
+              type="button"
+              className={item.id === cluster.id ? "selected" : ""}
+              onClick={() => selectCluster(item.id)}
+            >
+              {item.label}
+            </button>
+          ))}
+        </Carousel>
+
+        <Carousel ariaLabel={`${cluster.label}阶段`} className="catalog-stage-carousel">
+          {cluster.stages.map((stage) => (
+            <button
+              key={stage.id}
+              type="button"
+              className={stage.id === activeStage.id ? "selected" : ""}
+              onClick={() => setStageId(stage.id)}
+            >
+              {stage.label}
+            </button>
+          ))}
+        </Carousel>
+
+        <div className="catalog-summary">
+          <strong>{cluster.label} · {activeStage.label}</strong>
+          <span>{activeStage.agents.length} 项能力</span>
+        </div>
+
+        <section className="agent-catalog-list">
+          {activeStage.agents.map((agent) => {
+            const Icon = agent.mode === "registration" ? CalendarIcon : agent.mode === "report" ? FileTextIcon : agent.mode === "tongue" ? HeartIcon : DashboardIcon;
+            const enabled = Boolean(agent.mode);
             return (
-              <button key={service.title} type="button" onClick={() => flow.push(service.screen)}>
-                <span className="list-icon">
+              <button
+                key={agent.title}
+                type="button"
+                disabled={!enabled}
+                className={enabled ? "enabled" : "disabled"}
+                onClick={() => agent.mode && flow.push(makeAssistantScreen(agent.mode))}
+              >
+                <span className={`catalog-agent-icon ${agent.mode ?? "locked"}`}>
                   <Icon />
                 </span>
-                <span>
-                  <strong>{service.title}</strong>
-                  <small>{service.desc}</small>
+                <span className="catalog-agent-copy">
+                  <span className="catalog-agent-title">
+                    <strong>{agent.title}</strong>
+                    <em>{enabled ? "P0 已开通" : "本院未开通"}</em>
+                  </span>
+                  <small>{agent.desc}</small>
                 </span>
-                <ChevronRightIcon />
+                {enabled ? <ChevronRightIcon /> : null}
               </button>
             );
           })}
         </section>
-        <section className="plain-info">
-          <InfoCircledIcon />
-          <div>
-            <strong>服务范围</strong>
-            <p>当前为单医院 P0 演示。所有挂号、报告和支付数据均为 Mock。</p>
-          </div>
-        </section>
+        <p className="catalog-footnote">P0 仅开放智能分诊挂号、检验检查报告解读和中医舌诊,其余能力仅用于展示平台规划。</p>
       </main>
     </MobileScroll>
   );
@@ -306,7 +485,7 @@ function RecordsContent({ flow }: { flow: FlowControls }) {
             <span><strong>血常规智能解读</strong><small>今天 14:32 · 已完成</small><p>发现 3 项异常指标</p></span>
             <em>查看</em>
           </button>
-          <button type="button" onClick={() => flow.push(tongueCaptureScreen)}>
+          <button type="button" onClick={() => flow.push(makeAssistantScreen("tongue", "继续上次已保存的体质问答"))}>
             <span className="record-icon tongue"><HeartIcon /></span>
             <span><strong>中医舌诊评估</strong><small>今天 13:10 · 待上传</small><p>问答已完成,待拍摄舌象</p></span>
             <em>继续</em>
@@ -316,12 +495,15 @@ function RecordsContent({ flow }: { flow: FlowControls }) {
             <span><strong>神经内科预约</strong><small>明天 09:30 · 已完成</small><p>李明 主任医师</p></span>
             <em>查看</em>
           </button>
-          <button type="button" onClick={() => flow.push(registrationFailureScreen)}>
+          <button
+            type="button"
+            onClick={() => flow.push(makeAssistantScreen("registration", "继续查询神经内科明天上午的专家号", "complete"))}
+          >
             <span className="record-icon warning"><ReloadIcon /></span>
             <span><strong>智能挂号任务</strong><small>今天 12:20 · 未完成</small><p>号源发生变化,挂号条件已保留</p></span>
             <em>重试</em>
           </button>
-          <button type="button" onClick={() => flow.push(reportFileFailureScreen)}>
+          <button type="button" onClick={() => flow.push(makeAssistantScreen("report"))}>
             <span className="record-icon warning"><FileTextIcon /></span>
             <span><strong>报告解读任务</strong><small>今天 11:08 · 待重新上传</small><p>原图片模糊,患者信息已保留</p></span>
             <em>继续</em>
@@ -766,24 +948,512 @@ function AssistantExperience({
   );
 }
 
+type AgentConversationMode = Exclude<AssistantMode, "general">;
+type RegistrationPhase = "start" | "doctor" | "slots" | "confirm" | "done" | "emergency";
+type ReportPhase = "start" | "processing" | "result";
+type TonguePhase = "question-one" | "question-two" | "upload" | "processing" | "result";
+
+function AgentConversationExperience({
+  flow,
+  mode,
+  initialText,
+  initialStage,
+}: {
+  flow: FlowControls;
+  mode: AgentConversationMode;
+  initialText: string;
+  initialStage: AssistantStage;
+}) {
+  const keyboard = useKeyboard();
+  const { bottomInset } = useKeyboardInsets();
+  const [draft, setDraft] = useState("");
+  const [registrationPhase, setRegistrationPhase] = useState<RegistrationPhase>(
+    initialStage === "complete" ? "slots" : initialText ? "doctor" : "start",
+  );
+  const [reportPhase, setReportPhase] = useState<ReportPhase>(
+    initialStage === "complete" || initialText ? "start" : "start",
+  );
+  const [tonguePhase, setTonguePhase] = useState<TonguePhase>(
+    initialText ? "upload" : "question-one",
+  );
+  const [followupQuestion, setFollowupQuestion] = useState("");
+
+  useEffect(() => {
+    if (registrationPhase === "done" || registrationPhase === "emergency" || reportPhase === "result" || tonguePhase === "result") {
+      keyboard.hide();
+    }
+  }, [registrationPhase, reportPhase, tonguePhase]);
+
+  const sendRegistration = (value?: string) => {
+    const content = (value ?? draft).trim();
+    keyboard.hide();
+    setDraft("");
+    if (content.includes("胸痛") || content.includes("呼吸困难")) {
+      setRegistrationPhase("emergency");
+      return;
+    }
+    if (registrationPhase === "start") setRegistrationPhase("doctor");
+    else if (registrationPhase === "doctor") setRegistrationPhase("slots");
+  };
+
+  const sendTongue = (value?: string) => {
+    keyboard.hide();
+    setDraft("");
+    if (tonguePhase === "question-one") setTonguePhase("question-two");
+    else if (tonguePhase === "question-two") setTonguePhase("upload");
+    else if (value === "upload") setTonguePhase("processing");
+  };
+
+  const uploadReport = () => {
+    keyboard.hide();
+    setReportPhase("processing");
+  };
+
+  const composerPlaceholder =
+    mode === "registration"
+      ? registrationPhase === "doctor"
+        ? "说出医生姓名,或说“不限医生”"
+        : "继续描述挂号需求"
+      : mode === "report"
+        ? "可继续追问这份报告"
+        : tonguePhase === "question-one"
+          ? "说出近期最明显的不适"
+          : "继续回答问题";
+
+  const sendDraft = () => {
+    if (mode === "registration") sendRegistration();
+    else if (mode === "tongue") sendTongue();
+    else if (mode === "report" && draft.trim()) {
+      setFollowupQuestion(draft.trim());
+      setDraft("");
+      keyboard.hide();
+    }
+  };
+
+  const defaultAnswer = (() => {
+    if (mode === "registration") {
+      if (registrationPhase === "start") return "我头疼,帮我挂神经内科明天上午九点的专家号";
+      if (registrationPhase === "doctor") return "我想挂李明医生";
+      if (registrationPhase === "slots") return "我选择明天上午 09:00 的号源";
+      if (registrationPhase === "confirm") return "确认以上挂号信息";
+      if (registrationPhase === "done") return "请告诉我就诊前需要注意什么";
+      return "请告诉我最近的急诊入口";
+    }
+    if (mode === "report") {
+      if (reportPhase === "start") return "我想上传一份血常规报告";
+      if (reportPhase === "processing") return "请继续分析这份报告";
+      return "请解释血小板偏低是什么意思";
+    }
+    if (tonguePhase === "question-one") return "我最近容易疲倦,饭后有些腹胀";
+    if (tonguePhase === "question-two") return "我平时更容易怕冷";
+    if (tonguePhase === "upload") return "我已准备好拍摄舌象";
+    if (tonguePhase === "processing") return "请继续生成综合结果";
+    return "我在饮食上需要注意什么";
+  })();
+
+  const useDefaultAnswer = () => {
+    setFollowupQuestion("");
+    if (mode === "registration") {
+      if (registrationPhase === "start") sendRegistration(defaultAnswer);
+      else if (registrationPhase === "doctor") sendRegistration("李明医生");
+      else if (registrationPhase === "slots") setRegistrationPhase("confirm");
+      else if (registrationPhase === "confirm") setRegistrationPhase("done");
+      else setFollowupQuestion(defaultAnswer);
+      return;
+    }
+    if (mode === "report") {
+      if (reportPhase === "start") uploadReport();
+      else if (reportPhase === "processing") setReportPhase("result");
+      else setFollowupQuestion(defaultAnswer);
+      return;
+    }
+    if (tonguePhase === "question-one" || tonguePhase === "question-two") sendTongue(defaultAnswer);
+    else if (tonguePhase === "upload") sendTongue("upload");
+    else if (tonguePhase === "processing") setTonguePhase("result");
+    else setFollowupQuestion(defaultAnswer);
+  };
+
+  return (
+    <div className="agent-conversation-shell">
+      <MobileScroll className="agent-conversation-scroll">
+        <main className={`agent-conversation ${mode}`}>
+          <section className="agent-session-intro">
+            <span className={`service-icon ${mode}`}>
+              {mode === "registration" ? <CalendarIcon /> : mode === "report" ? <FileTextIcon /> : <HeartIcon />}
+            </span>
+            <div>
+              <strong>
+                {mode === "registration" ? "智能分诊挂号" : mode === "report" ? "报告智能解读" : "中医舌诊"}
+              </strong>
+              <small>本次服务独立保存 · 演示数据为 Mock</small>
+            </div>
+          </section>
+
+          {mode === "registration" ? (
+            <>
+              <AssistantMessage>
+                <p>您好,我会通过连续追问收集挂号所需信息。您可以直接用一句话说出症状、科室、日期、时间和号别。</p>
+              </AssistantMessage>
+
+              {registrationPhase === "start" ? (
+                <section className="conversation-demo-prompt">
+                  <strong>推荐演示语句</strong>
+                  <p>“我头疼,帮我挂神经内科明天早上九点的专家号。”</p>
+                  <button type="button" onClick={() => sendRegistration("我头疼,帮我挂神经内科明天早上九点的专家号")}>
+                    <SpeakerLoudIcon /> 点击模拟语音输入
+                  </button>
+                  <button className="danger-plain-action" type="button" onClick={() => sendRegistration("我胸痛,还呼吸困难")}>
+                    演示急诊风险识别
+                  </button>
+                </section>
+              ) : null}
+
+              {registrationPhase !== "start" && registrationPhase !== "emergency" ? (
+                <UserMessage label="语音已转文字">
+                  {initialText || "我头疼,帮我挂神经内科明天早上九点的专家号"}
+                </UserMessage>
+              ) : null}
+
+              {registrationPhase === "doctor" ? (
+                <>
+                  <AssistantMessage>
+                    <p>我已记录:头疼、神经内科、明天上午 09:00、专家号。当前未发现明确急诊红旗。</p>
+                    <p><strong>还差最后一项:您想挂哪位医生?</strong></p>
+                  </AssistantMessage>
+                  <section className="doctor-return-note">
+                    <span>Mock MCP 返回</span>
+                    <strong>李明主任、王佳副主任、周海主任</strong>
+                  </section>
+                </>
+              ) : null}
+
+              {["slots", "confirm", "done"].includes(registrationPhase) ? (
+                <>
+                  <UserMessage>李明主任</UserMessage>
+                  <AssistantMessage>
+                    <p>信息已经收集完整,正在查询符合条件的 Mock 号源。</p>
+                  </AssistantMessage>
+                </>
+              ) : null}
+
+              {registrationPhase === "slots" ? (
+                <section className="conversation-markdown">
+                  <div className="markdown-heading">
+                    <strong>可预约号源</strong>
+                    <span>Mock HIS</span>
+                  </div>
+                  <p>神经内科 · 李明 主任医师 · 明天上午</p>
+                  <button type="button" onClick={() => setRegistrationPhase("confirm")}>
+                    <span><strong>09:00</strong><small>专家号 · ¥25 · 剩余 2</small></span>
+                    <em>选择</em>
+                  </button>
+                  <button type="button">
+                    <span><strong>09:30</strong><small>专家号 · ¥25 · 剩余 1</small></span>
+                    <em>选择</em>
+                  </button>
+                </section>
+              ) : null}
+
+              {registrationPhase === "confirm" ? (
+                <>
+                  <UserMessage>选择明天 09:00</UserMessage>
+                  <AssistantMessage>
+                    <div className="markdown-result compact-result">
+                      <h3>请确认挂号信息</h3>
+                      <ul>
+                        <li>就诊人:王*明</li>
+                        <li>科室:神经内科</li>
+                        <li>医生:李明 主任医师</li>
+                        <li>时间:明天 09:00</li>
+                        <li>费用:¥25(Mock)</li>
+                      </ul>
+                    </div>
+                  </AssistantMessage>
+                  <button className="conversation-confirm" type="button" onClick={() => setRegistrationPhase("done")}>
+                    确认 Mock 挂号
+                  </button>
+                </>
+              ) : null}
+
+              {registrationPhase === "done" ? (
+                <>
+                  <UserMessage>确认挂号</UserMessage>
+                  <AssistantMessage tone="success">
+                    <div className="markdown-result compact-result">
+                      <h3>挂号成功(Mock)</h3>
+                      <p>明天 09:00 · 神经内科 · 李明 主任医师</p>
+                      <p>请提前 15 分钟到院,并携带有效就诊凭证。</p>
+                    </div>
+                  </AssistantMessage>
+                </>
+              ) : null}
+
+              {registrationPhase === "emergency" ? (
+                <>
+                  <UserMessage label="语音已转文字">我胸痛,还呼吸困难,想挂明天心内科。</UserMessage>
+                  <AssistantMessage tone="danger">
+                    <div className="markdown-result compact-result">
+                      <h3>请立即寻求紧急医疗帮助</h3>
+                      <p>“胸痛 + 呼吸困难”可能存在紧急风险,已停止普通挂号流程。</p>
+                      <p>请立即拨打 120 或前往最近的急诊。如身边有人,请让其陪同。</p>
+                    </div>
+                  </AssistantMessage>
+                </>
+              ) : null}
+              {followupQuestion && registrationPhase === "done" ? (
+                <>
+                  <UserMessage>{followupQuestion}</UserMessage>
+                  <AssistantMessage><p>建议提前 15 分钟到院,携带有效就诊凭证和既往检查资料;如症状明显加重,请及时转急诊。</p></AssistantMessage>
+                </>
+              ) : null}
+            </>
+          ) : null}
+
+          {mode === "report" ? (
+            <>
+              <AssistantMessage>
+                <p>请在本次对话中上传清晰的检验或检查报告图片。识别后,我会按“风险提醒、异常指标、联合解读和行动建议”整理结果。</p>
+              </AssistantMessage>
+
+              {reportPhase === "start" ? (
+                <button className="conversation-upload" type="button" onClick={uploadReport}>
+                  <UploadIcon />
+                  <span><strong>上传演示血常规</strong><small>支持图片或 PDF · 点击体验</small></span>
+                </button>
+              ) : (
+                <UserMessage label="已上传文件">
+                  <span className="message-file"><FileTextIcon /> 血常规_2026-07-27.jpg</span>
+                </UserMessage>
+              )}
+
+              {reportPhase === "processing" ? (
+                <AssistantMessage>
+                  <div className="conversation-processing">
+                    <span className="processing-spinner" />
+                    <div><strong>正在识别和解读报告</strong><small>正在校验患者信息、指标名称、参考范围和风险项</small></div>
+                  </div>
+                  <button
+                    className="inline-demo-action"
+                    type="button"
+                    onClick={() => {
+                      keyboard.hide();
+                      setReportPhase("result");
+                    }}
+                  >
+                    完成演示分析
+                  </button>
+                </AssistantMessage>
+              ) : null}
+
+              {reportPhase === "result" ? (
+                <AssistantMessage tone="result">
+                  <div className="markdown-result">
+                    <h2>血常规解读</h2>
+                    <blockquote><strong>风险提醒:</strong>本演示未触发危急值,但血小板偏低。如伴随明显出血、黑便或持续头晕,请及时就医。</blockquote>
+                    <h3>报告概览</h3>
+                    <p>共识别 22 项指标,其中 <strong>3 项异常</strong>,主要集中在血小板相关指标。</p>
+                    <h3>异常指标</h3>
+                    <div className="markdown-table">
+                      <div><strong>指标</strong><strong>结果</strong><strong>状态</strong></div>
+                      <div><span>血小板 PLT</span><span>78 ×10⁹/L</span><em>偏低</em></div>
+                      <div><span>平均血小板体积</span><span>12.8 fL</span><em>偏高</em></div>
+                      <div><span>血小板分布宽度</span><span>17.2%</span><em>偏高</em></div>
+                    </div>
+                    <h3>联合解读</h3>
+                    <p>血小板计数降低,同时平均血小板体积升高。需要结合近期感染、用药、出血表现和既往趋势进一步判断。</p>
+                    <h3>建议下一步</h3>
+                    <ol>
+                      <li>携带本次及既往报告咨询原就诊科室或血液科。</li>
+                      <li>如近期服用影响凝血的药物,请主动告知医生。</li>
+                      <li>正式判断以医院医生意见为准。</li>
+                    </ol>
+                  </div>
+                </AssistantMessage>
+              ) : null}
+              {followupQuestion && reportPhase === "result" ? (
+                <>
+                  <UserMessage>{followupQuestion}</UserMessage>
+                  <AssistantMessage><p>血小板偏低表示参与止血的血小板数量少于本报告参考范围。单次结果不能直接判断病因,需要结合出血表现、近期感染、用药和复查趋势由医生综合评估。</p></AssistantMessage>
+                </>
+              ) : null}
+            </>
+          ) : null}
+
+          {mode === "tongue" ? (
+            <>
+              <AssistantMessage>
+                <p>我会先了解您近期的身体感受,再引导拍摄舌象。分析结果用于健康参考,不能替代中医师完整辨证。</p>
+              </AssistantMessage>
+
+              {tonguePhase === "question-one" ? (
+                <>
+                  <AssistantMessage><p><strong>第 1 题:</strong>最近最明显的不适是什么?</p></AssistantMessage>
+                </>
+              ) : null}
+
+              {tonguePhase !== "question-one" ? <UserMessage>容易疲倦,饭后有些腹胀</UserMessage> : null}
+
+              {tonguePhase === "question-two" ? (
+                <>
+                  <AssistantMessage><p><strong>第 2 题:</strong>平时更容易怕冷还是怕热?</p></AssistantMessage>
+                </>
+              ) : null}
+
+              {["upload", "processing", "result"].includes(tonguePhase) ? (
+                <>
+                  <UserMessage>更怕冷</UserMessage>
+                  <AssistantMessage>
+                    <p>问答已完成。请在自然光下伸出舌体,避免滤镜,确保舌尖和舌根完整入镜。</p>
+                  </AssistantMessage>
+                </>
+              ) : null}
+
+              {tonguePhase === "upload" ? (
+                <button className="conversation-upload tongue-upload" type="button" onClick={() => sendTongue("upload")}>
+                  <CameraIcon />
+                  <span><strong>拍摄并上传演示舌象</strong><small>正面 · 清晰 · 自然光</small></span>
+                </button>
+              ) : null}
+
+              {["processing", "result"].includes(tonguePhase) ? (
+                <UserMessage label="已上传图片">
+                  <span className="message-file"><CameraIcon /> 舌象照片.jpg</span>
+                </UserMessage>
+              ) : null}
+
+              {tonguePhase === "processing" ? (
+                <AssistantMessage>
+                  <div className="conversation-processing">
+                    <span className="processing-spinner" />
+                    <div><strong>正在分析舌象</strong><small>正在调用医院已接入的舌象分析服务并融合问答信息</small></div>
+                  </div>
+                  <button
+                    className="inline-demo-action"
+                    type="button"
+                    onClick={() => {
+                      keyboard.hide();
+                      setTonguePhase("result");
+                    }}
+                  >
+                    返回演示结果
+                  </button>
+                </AssistantMessage>
+              ) : null}
+
+              {tonguePhase === "result" ? (
+                <AssistantMessage tone="result">
+                  <div className="markdown-result">
+                    <h2>中医舌诊综合结果</h2>
+                    <blockquote className="positive"><strong>综合倾向:</strong>气虚体质,兼有脾气虚表现(仅供健康参考)</blockquote>
+                    <h3>问答信息</h3>
+                    <ul><li>容易疲倦、饭后腹胀</li><li>平时较怕冷</li></ul>
+                    <h3>舌象特征</h3>
+                    <ul><li>舌色:淡红</li><li>舌体:偏胖,可见齿痕</li><li>舌苔:薄白,偏润</li></ul>
+                    <h3>综合建议</h3>
+                    <ol><li>饮食规律,减少生冷油腻。</li><li>保持适量运动和充足睡眠。</li><li>如疲倦或腹胀持续加重,建议由中医师结合脉诊进一步辨证。</li></ol>
+                  </div>
+                </AssistantMessage>
+              ) : null}
+              {followupQuestion && tonguePhase === "result" ? (
+                <>
+                  <UserMessage>{followupQuestion}</UserMessage>
+                  <AssistantMessage><p>饮食以温热、清淡、规律为主,减少生冷和油腻食物。建议把持续疲倦、腹胀等情况记录下来,必要时由中医师结合脉诊进一步辨证。</p></AssistantMessage>
+                </>
+              ) : null}
+            </>
+          ) : null}
+
+          <div className="conversation-end-space" />
+        </main>
+      </MobileScroll>
+
+      <div className="conversation-input-dock" style={{ bottom: bottomInset }}>
+        <button className="default-answer" type="button" onClick={useDefaultAnswer}>
+          <span>推荐回答</span>
+          <strong>{defaultAnswer}</strong>
+          <ChevronRightIcon />
+        </button>
+        <div className="conversation-composer">
+          <button className="composer-voice" type="button" onClick={useDefaultAnswer} aria-label="按住说话">
+            <SpeakerLoudIcon />
+          </button>
+          <KeyboardInput
+            value={draft}
+            onChange={(event) => setDraft(event.target.value)}
+            onKeyDown={(event) => {
+              if (event.key === "Enter" && draft.trim()) sendDraft();
+            }}
+            placeholder={composerPlaceholder}
+            aria-label="输入消息"
+          />
+          <button
+            className="composer-camera"
+            type="button"
+            aria-label={draft ? "发送文字" : "拍照或上传图片"}
+            onClick={() => {
+              if (draft) {
+                sendDraft();
+                return;
+              }
+              if (mode === "report") uploadReport();
+              if (mode === "tongue" && tonguePhase === "upload") sendTongue("upload");
+            }}
+          >
+            {draft ? <ChevronRightIcon /> : <CameraIcon />}
+          </button>
+        </div>
+        <small>语音内容会先转为文字,重要信息需确认后执行</small>
+      </div>
+    </div>
+  );
+}
+
+function AssistantMessage({
+  children,
+  tone = "default",
+}: {
+  children: React.ReactNode;
+  tone?: "default" | "success" | "danger" | "result";
+}) {
+  return <article className={`conversation-message assistant ${tone}`}>{children}</article>;
+}
+
+function UserMessage({ children, label }: { children: React.ReactNode; label?: string }) {
+  return (
+    <article className="conversation-message user">
+      {label ? <small>{label}</small> : null}
+      <p>{children}</p>
+    </article>
+  );
+}
+
 function makeAssistantScreen(
   mode: AssistantMode,
   initialText = "",
   initialStage: AssistantStage = "ready",
 ): FlowScreen {
-  const title = mode === "registration" ? "语音挂号" : mode === "report" ? "报告解读" : mode === "tongue" ? "中医舌诊" : "智能助手";
+  const title = mode === "registration" ? "智能分诊挂号" : mode === "report" ? "报告智能解读" : mode === "tongue" ? "中医舌诊" : "智能助手";
   return {
     id: `assistant-${mode}-${initialStage}`,
     title,
     headerHeight: 52,
     header: (flow) => <PageHeader flow={flow} title={title} />,
     render: (flow) => (
-      <AssistantExperience
-        flow={flow}
-        mode={mode}
-        initialText={initialText}
-        initialStage={initialStage}
-      />
+      mode === "general" ? (
+        <AssistantExperience
+          flow={flow}
+          mode={mode}
+          initialText={initialText}
+          initialStage={initialStage}
+        />
+      ) : (
+        <AgentConversationExperience
+          flow={flow}
+          mode={mode}
+          initialText={initialText}
+          initialStage={initialStage}
+        />
+      )
     ),
   };
 }

+ 912 - 11
prototype/adjutant-mobile/src/prototype.css

@@ -25,6 +25,10 @@ button {
   color: inherit;
 }
 
+.keyboard-dock[data-visible="false"] {
+  visibility: hidden;
+}
+
 .flow-stack,
 .flow-scenes,
 .flow-screen {
@@ -2473,16 +2477,913 @@ button {
   color: #c0780d;
 }
 
-@media (max-width: 390px) {
-  .portal-page {
-    padding-inline: 15px;
-  }
-  .welcome-block h2 {
-    font-size: 26px;
-  }
-  .registration-copy img {
-    width: 82px;
-    height: 82px;
-    flex-basis: 82px;
+.agent-conversation-shell {
+  position: relative;
+  height: 100%;
+  overflow: hidden;
+  background:
+    radial-gradient(circle at 100% 0, rgba(47, 201, 210, 0.08), transparent 30%),
+    var(--bg);
+}
+
+.agent-conversation-scroll {
+  height: 100%;
+  background: transparent;
+}
+
+.agent-conversation {
+  display: grid;
+  align-content: start;
+  gap: 12px;
+  min-height: 100%;
+  padding: 14px 16px 108px;
+  color: var(--text);
+}
+
+.agent-session-intro {
+  display: flex;
+  align-items: center;
+  gap: 10px;
+  padding: 10px 12px;
+  border: 1px solid rgba(223, 231, 247, 0.82);
+  border-radius: 16px;
+  background: rgba(255, 255, 255, 0.78);
+}
+
+.agent-session-intro .service-icon {
+  width: 38px;
+  height: 38px;
+  flex: 0 0 38px;
+  border-radius: 12px;
+}
+
+.agent-session-intro .service-icon.registration {
+  background: var(--brand-soft);
+  color: var(--brand);
+}
+
+.agent-session-intro > div {
+  min-width: 0;
+}
+
+.agent-session-intro strong,
+.agent-session-intro small {
+  display: block;
+}
+
+.agent-session-intro strong {
+  font-size: 14px;
+}
+
+.agent-session-intro small {
+  margin-top: 3px;
+  color: var(--muted);
+  font-size: 10px;
+}
+
+.conversation-message {
+  max-width: 88%;
+  padding: 13px 14px;
+  border-radius: 18px;
+  font-size: 13px;
+  line-height: 1.65;
+  box-shadow: 0 4px 14px rgba(27, 31, 74, 0.035);
+}
+
+.conversation-message p,
+.conversation-message h2,
+.conversation-message h3,
+.conversation-message ul,
+.conversation-message ol,
+.conversation-message blockquote {
+  margin: 0;
+}
+
+.conversation-message p + p,
+.conversation-message h3,
+.conversation-message ul,
+.conversation-message ol,
+.conversation-message blockquote,
+.conversation-message .markdown-table {
+  margin-top: 10px;
+}
+
+.conversation-message.assistant {
+  justify-self: start;
+  border: 1px solid var(--border);
+  border-top-left-radius: 7px;
+  background: var(--surface);
+}
+
+.conversation-message.user {
+  justify-self: end;
+  border: 1px solid #d9d7ff;
+  border-top-right-radius: 7px;
+  background: #efefff;
+  color: #272064;
+}
+
+.conversation-message.user small {
+  display: block;
+  margin-bottom: 4px;
+  color: var(--brand);
+  font-size: 10px;
+  font-weight: 750;
+}
+
+.conversation-message.success {
+  border-color: #b9ead7;
+  background: #effbf6;
+}
+
+.conversation-message.danger {
+  border-color: #efc0c0;
+  background: var(--danger-soft);
+  color: #7e2525;
+}
+
+.conversation-message.result {
+  width: 100%;
+  max-width: 100%;
+}
+
+.conversation-demo-prompt {
+  display: grid;
+  gap: 8px;
+  margin-left: 24px;
+  padding: 13px;
+  border: 1px dashed #c7c5ef;
+  border-radius: 16px;
+  background: rgba(255, 255, 255, 0.64);
+}
+
+.conversation-demo-prompt > strong {
+  color: var(--muted);
+  font-size: 10px;
+  letter-spacing: 0.05em;
+}
+
+.conversation-demo-prompt > p {
+  margin: 0;
+  font-size: 12px;
+  line-height: 1.55;
+}
+
+.conversation-demo-prompt button {
+  display: flex;
+  min-height: 42px;
+  align-items: center;
+  justify-content: center;
+  gap: 7px;
+  border: 0;
+  border-radius: 12px;
+  background: var(--brand);
+  color: white;
+  font-size: 12px;
+  font-weight: 750;
+}
+
+.conversation-demo-prompt button.danger-plain-action {
+  min-height: 34px;
+  background: transparent;
+  color: #a23b3b;
+  font-size: 11px;
+}
+
+.conversation-quick-replies {
+  display: flex;
+  flex-wrap: wrap;
+  justify-content: flex-end;
+  gap: 7px;
+}
+
+.conversation-quick-replies button {
+  min-height: 36px;
+  padding: 0 12px;
+  border: 1px solid #cbc9ef;
+  border-radius: 999px;
+  background: var(--surface);
+  color: var(--brand);
+  font-size: 11px;
+  font-weight: 700;
+}
+
+.conversation-markdown,
+.conversation-upload {
+  width: calc(100% - 22px);
+  justify-self: start;
+  border: 1px solid var(--border);
+  border-radius: 18px;
+  background: var(--surface);
+}
+
+.conversation-markdown {
+  overflow: hidden;
+  padding: 14px;
+}
+
+.markdown-heading {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+}
+
+.markdown-heading > span {
+  padding: 4px 7px;
+  border-radius: 7px;
+  background: var(--brand-soft);
+  color: var(--brand);
+  font-size: 9px;
+  font-weight: 800;
+}
+
+.conversation-markdown > p {
+  margin: 5px 0 10px;
+  color: var(--muted);
+  font-size: 11px;
+}
+
+.conversation-markdown > button {
+  display: flex;
+  width: 100%;
+  min-height: 54px;
+  padding: 8px 10px;
+  align-items: center;
+  justify-content: space-between;
+  border: 0;
+  border-top: 1px solid var(--border);
+  background: transparent;
+  text-align: left;
+}
+
+.conversation-markdown > button span,
+.conversation-markdown > button strong,
+.conversation-markdown > button small {
+  display: block;
+}
+
+.conversation-markdown > button small {
+  margin-top: 2px;
+  color: var(--muted);
+  font-size: 10px;
+}
+
+.conversation-markdown > button em {
+  color: var(--brand);
+  font-size: 11px;
+  font-style: normal;
+  font-weight: 750;
+}
+
+.conversation-confirm,
+.conversation-upload {
+  border: 0;
+  font-weight: 750;
+}
+
+.conversation-confirm {
+  min-height: 44px;
+  border-radius: 13px;
+  background: var(--brand);
+  color: white;
+}
+
+.conversation-upload {
+  display: flex;
+  min-height: 82px;
+  padding: 14px;
+  align-items: center;
+  gap: 12px;
+  border: 1px dashed #b9b6e8;
+  color: var(--brand);
+  text-align: left;
+}
+
+.conversation-upload > svg {
+  width: 26px;
+  height: 26px;
+}
+
+.conversation-upload span,
+.conversation-upload strong,
+.conversation-upload small {
+  display: block;
+}
+
+.conversation-upload small {
+  margin-top: 3px;
+  color: var(--muted);
+  font-size: 10px;
+  font-weight: 500;
+}
+
+.conversation-upload.tongue-upload {
+  border-color: #f1c7a5;
+  background: #fff9f3;
+  color: #b7672e;
+}
+
+.message-file {
+  display: inline-flex;
+  align-items: center;
+  gap: 7px;
+}
+
+.conversation-processing {
+  display: flex;
+  align-items: center;
+  gap: 11px;
+}
+
+.conversation-processing strong,
+.conversation-processing small {
+  display: block;
+}
+
+.conversation-processing small {
+  margin-top: 3px;
+  color: var(--muted);
+  font-size: 10px;
+  line-height: 1.5;
+}
+
+.processing-spinner {
+  width: 25px;
+  height: 25px;
+  flex: 0 0 25px;
+  border: 3px solid #dedcff;
+  border-top-color: var(--brand);
+  border-radius: 50%;
+  animation: conversation-spin 1s linear infinite;
+}
+
+@keyframes conversation-spin {
+  to { transform: rotate(360deg); }
+}
+
+.inline-demo-action {
+  width: 100%;
+  min-height: 36px;
+  margin-top: 12px;
+  border: 0;
+  border-radius: 10px;
+  background: var(--brand-soft);
+  color: var(--brand);
+  font-size: 11px;
+  font-weight: 750;
+}
+
+.markdown-result h2 {
+  font-size: 18px;
+}
+
+.markdown-result h3 {
+  font-size: 13px;
+}
+
+.markdown-result ul,
+.markdown-result ol {
+  padding-left: 18px;
+  color: #434963;
+}
+
+.markdown-result blockquote {
+  padding: 10px 11px;
+  border-left: 3px solid var(--warning);
+  border-radius: 5px 10px 10px 5px;
+  background: #fff8e9;
+  color: #725112;
+}
+
+.markdown-result blockquote.positive {
+  border-left-color: var(--success);
+  background: #eefaf5;
+  color: #23634e;
+}
+
+.markdown-result.compact-result h3 {
+  margin-top: 0;
+  font-size: 15px;
+}
+
+.markdown-table {
+  overflow: hidden;
+  border: 1px solid var(--border);
+  border-radius: 10px;
+}
+
+.markdown-table > div {
+  display: grid;
+  grid-template-columns: 1.2fr 1fr 0.55fr;
+  gap: 6px;
+  padding: 7px 8px;
+  border-bottom: 1px solid var(--border);
+  font-size: 9px;
+}
+
+.markdown-table > div:last-child {
+  border-bottom: 0;
+}
+
+.markdown-table > div:first-child {
+  background: #f5f7fd;
+}
+
+.markdown-table em {
+  color: var(--danger);
+  font-style: normal;
+  font-weight: 750;
+}
+
+.conversation-end-space {
+  min-height: 154px;
+}
+
+.conversation-input-dock {
+  position: absolute;
+  z-index: 4;
+  right: 0;
+  left: 0;
+  padding: 8px 12px 10px;
+  border-top: 1px solid var(--border);
+  background: rgba(244, 245, 253, 0.97);
+  backdrop-filter: blur(18px);
+}
+
+.conversation-input-dock > small {
+  display: block;
+  margin-top: 6px;
+  color: #858aa0;
+  font-size: 9px;
+  line-height: 1.25;
+  text-align: center;
+}
+
+.default-answer {
+  display: grid;
+  grid-template-columns: auto minmax(0, 1fr) auto;
+  gap: 7px;
+  width: 100%;
+  margin-bottom: 7px;
+  padding: 8px 10px;
+  align-items: center;
+  border: 1px solid #d9dcf2;
+  border-radius: 12px;
+  background: white;
+  color: var(--text);
+  text-align: left;
+  box-shadow: 0 5px 14px rgba(47, 40, 112, 0.06);
+}
+
+.default-answer span {
+  padding: 3px 6px;
+  border-radius: 999px;
+  background: var(--brand-soft);
+  color: var(--brand);
+  font-size: 9px;
+  font-weight: 800;
+  white-space: nowrap;
+}
+
+.default-answer strong {
+  overflow: hidden;
+  font-size: 11px;
+  font-weight: 700;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.default-answer svg {
+  width: 14px;
+  height: 14px;
+  color: var(--brand);
+}
+
+.conversation-composer {
+  display: grid;
+  grid-template-columns: 42px 1fr 42px;
+  gap: 7px;
+  min-height: 56px;
+  padding: 6px 7px;
+  align-items: center;
+  border: 1px solid #dfe3f2;
+  border-radius: 28px;
+  background: white;
+  box-shadow: 0 8px 24px rgba(33, 20, 125, 0.09);
+}
+
+.conversation-composer input {
+  width: 100%;
+  height: 42px;
+  padding: 0 5px;
+  border: 0;
+  border-radius: 0;
+  outline: 0;
+  background: white;
+  color: var(--text);
+  font-size: 12px;
+}
+
+.conversation-composer input:focus {
+  background: white;
+}
+
+.composer-voice,
+.composer-camera {
+  display: grid;
+  width: 42px;
+  height: 42px;
+  place-items: center;
+  border: 0;
+  border-radius: 50%;
+}
+
+.composer-voice {
+  background: #f1f0ff;
+  color: var(--brand);
+}
+
+.composer-camera {
+  background: transparent;
+  color: #272347;
+}
+
+.composer-voice svg,
+.composer-camera svg {
+  width: 19px;
+  height: 19px;
+}
+
+.doctor-return-note {
+  display: flex;
+  gap: 6px;
+  margin: -5px 7px 10px 46px;
+  align-items: center;
+  color: var(--muted);
+  font-size: 9px;
+}
+
+.doctor-return-note span {
+  padding: 3px 5px;
+  border-radius: 999px;
+  background: #edf8f4;
+  color: #197759;
+  font-weight: 800;
+  white-space: nowrap;
+}
+
+.doctor-return-note strong {
+  font-size: 9px;
+  font-weight: 650;
+}
+
+@media (max-width: 390px) {
+  .portal-page {
+    padding-inline: 15px;
+  }
+  .welcome-block h2 {
+    font-size: 26px;
+  }
+  .registration-copy img {
+    width: 82px;
+    height: 82px;
+    flex-basis: 82px;
+  }
+}
+
+/* P0 以移动 Web 交付:保留响应式视口,不展示原生设备外框、状态栏和虚拟键盘。 */
+.phone-stage {
+  display: block !important;
+  height: 100dvh !important;
+  padding: 0 !important;
+  overflow: hidden;
+  background: #edf0f7 !important;
+}
+
+.device-menu-bar,
+.phone-bezel,
+.device-camera,
+.status-bar,
+.status-indicators,
+.home-indicator,
+.home-indicator-svg,
+.mobile-cursor,
+.keyboard-dock {
+  display: none !important;
+}
+
+.phone-scale-box {
+  width: min(100vw, 430px) !important;
+  height: 100dvh !important;
+  margin: 0 auto;
+}
+
+.phone-device {
+  width: 100% !important;
+  height: 100% !important;
+  transform: none !important;
+}
+
+.device-screen {
+  top: 0 !important;
+  left: 0 !important;
+  width: 100% !important;
+  height: 100% !important;
+  border-radius: 0 !important;
+  box-shadow: 0 0 36px rgba(25, 29, 68, 0.14) !important;
+  cursor: auto !important;
+  user-select: auto !important;
+}
+
+.device-screen,
+.device-screen * {
+  cursor: auto !important;
+}
+
+.mobile-page {
+  --mobile-safe-area-height: 0px !important;
+}
+
+.flow-stack {
+  --flow-header-height: 52px !important;
+  --flow-header-safe-area: 0px !important;
+  --keyboard-height: 0px !important;
+}
+
+.mobile-scroll {
+  bottom: 0 !important;
+}
+
+.portal-page {
+  padding-top: 26px;
+}
+
+.task-header {
+  padding-top: 0 !important;
+}
+
+.conversation-input-dock {
+  bottom: 0 !important;
+}
+
+.service-catalog-page {
+  padding-inline: 0;
+  background:
+    radial-gradient(circle at 92% 4%, rgba(255, 228, 221, 0.72), transparent 27%),
+    radial-gradient(circle at 5% 2%, rgba(225, 224, 255, 0.86), transparent 30%),
+    #f8f8fd;
+}
+
+.catalog-header,
+.service-search,
+.catalog-summary,
+.agent-catalog-list,
+.catalog-footnote {
+  margin-inline: 18px;
+}
+
+.catalog-header span {
+  display: block;
+  margin-bottom: 4px;
+  color: var(--brand);
+  font-size: 10px;
+  font-weight: 800;
+  letter-spacing: 0.06em;
+}
+
+.catalog-header h1 {
+  font-size: 23px;
+}
+
+.catalog-header p {
+  margin-top: 6px !important;
+  font-size: 12px;
+}
+
+.service-search {
+  display: flex;
+  gap: 8px;
+  height: 46px;
+  margin-top: 18px;
+  padding: 0 14px;
+  align-items: center;
+  border: 1px solid rgba(218, 220, 239, 0.9);
+  border-radius: 23px;
+  background: rgba(255, 255, 255, 0.92);
+  box-shadow: 0 8px 22px rgba(50, 45, 98, 0.05);
+}
+
+.service-search svg {
+  width: 18px;
+  height: 18px;
+  color: #878ba3;
+}
+
+.service-search input {
+  width: 100%;
+  border: 0;
+  outline: 0;
+  background: transparent;
+  color: var(--text);
+  font-size: 12px;
+}
+
+.catalog-cluster-carousel,
+.catalog-stage-carousel {
+  padding-inline: 18px !important;
+}
+
+.catalog-cluster-carousel {
+  margin-top: 18px;
+}
+
+.catalog-cluster-carousel button,
+.catalog-stage-carousel button {
+  flex: 0 0 auto;
+  border: 0;
+  white-space: nowrap;
+}
+
+.catalog-cluster-carousel button {
+  position: relative;
+  padding: 10px 12px 12px;
+  background: transparent;
+  color: #7b8098;
+  font-size: 14px;
+  font-weight: 700;
+}
+
+.catalog-cluster-carousel button.selected {
+  color: var(--text);
+}
+
+.catalog-cluster-carousel button.selected::after {
+  position: absolute;
+  right: 28%;
+  bottom: 3px;
+  left: 28%;
+  height: 3px;
+  border-radius: 999px;
+  background: var(--brand);
+  content: "";
+}
+
+.catalog-stage-carousel {
+  margin-top: 6px;
+}
+
+.catalog-stage-carousel button {
+  min-width: 64px;
+  padding: 8px 16px;
+  border: 1px solid #e4e5f1;
+  border-radius: 999px;
+  background: rgba(255, 255, 255, 0.84);
+  color: #6f748b;
+  font-size: 11px;
+  font-weight: 700;
+}
+
+.catalog-stage-carousel button.selected {
+  border-color: var(--brand);
+  background: var(--brand);
+  color: white;
+}
+
+.catalog-summary {
+  display: flex;
+  margin-top: 18px;
+  margin-bottom: 9px;
+  align-items: center;
+  justify-content: space-between;
+}
+
+.catalog-summary strong {
+  font-size: 13px;
+}
+
+.catalog-summary span {
+  color: var(--muted);
+  font-size: 10px;
+}
+
+.agent-catalog-list {
+  display: grid;
+  gap: 10px;
+}
+
+.agent-catalog-list > button {
+  display: grid;
+  grid-template-columns: 48px minmax(0, 1fr) auto;
+  gap: 11px;
+  min-height: 86px;
+  padding: 13px;
+  align-items: center;
+  border: 1px solid #e8e9f3;
+  border-radius: 18px;
+  background: rgba(255, 255, 255, 0.94);
+  text-align: left;
+  box-shadow: 0 8px 24px rgba(46, 43, 88, 0.05);
+}
+
+.agent-catalog-list > button.enabled {
+  border-color: #d9d5fb;
+}
+
+.agent-catalog-list > button.disabled {
+  cursor: not-allowed !important;
+  opacity: 0.58;
+}
+
+.catalog-agent-icon {
+  display: grid;
+  width: 48px;
+  height: 48px;
+  place-items: center;
+  border-radius: 14px;
+  background: #f1f2f7;
+  color: #8a8fa5;
+}
+
+.catalog-agent-icon.registration {
+  background: var(--brand-soft);
+  color: var(--brand);
+}
+
+.catalog-agent-icon.report {
+  background: #eaf8fb;
+  color: #168699;
+}
+
+.catalog-agent-icon.tongue {
+  background: #fff0f1;
+  color: #c94e5c;
+}
+
+.catalog-agent-icon svg {
+  width: 22px;
+  height: 22px;
+}
+
+.catalog-agent-copy {
+  min-width: 0;
+}
+
+.catalog-agent-title {
+  display: flex;
+  gap: 6px;
+  align-items: center;
+}
+
+.catalog-agent-title strong {
+  overflow: hidden;
+  font-size: 13px;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.catalog-agent-title em {
+  padding: 2px 5px;
+  border-radius: 4px;
+  background: #f0efff;
+  color: var(--brand);
+  font-size: 8px;
+  font-style: normal;
+  font-weight: 800;
+  white-space: nowrap;
+}
+
+.agent-catalog-list > button.disabled .catalog-agent-title em {
+  background: #f0f1f5;
+  color: #85899a;
+}
+
+.catalog-agent-copy > small {
+  display: -webkit-box;
+  margin-top: 7px;
+  overflow: hidden;
+  color: var(--muted);
+  font-size: 10px;
+  line-height: 1.45;
+  -webkit-box-orient: vertical;
+  -webkit-line-clamp: 2;
+}
+
+.agent-catalog-list > button > svg {
+  width: 17px;
+  height: 17px;
+  color: var(--brand);
+}
+
+.catalog-footnote {
+  margin-top: 16px;
+  margin-bottom: 6px;
+  color: #85899d;
+  font-size: 9px;
+  line-height: 1.55;
+}
+
+@media (min-width: 431px) {
+  .phone-scale-box {
+    border-inline: 1px solid rgba(210, 214, 228, 0.85);
   }
 }