Commit d90ea308 by luoqi

fix(web): 潜在治疗的中文一律 code 查表 —— 修详情页「潜在补牙」vs 卡片「充填治疗」

同一个患者两处口径不一致(测试服实证):
  召回池卡片   充填治疗   ← 拿 types=['filling'] 查 POTENTIAL_TREATMENT_CARD_LABEL
  详情页 chip   潜在补牙   ← 读 data.labels,那是**算画像那一刻烤进 JSON 的**旧措辞

画像 JSON 长这样:{"types":["filling"],"labels":["潜在补牙"]} —— 措辞这几天改过三轮
(潜在种植→种植治疗→种植),烤死的那份自然全是旧词,要改回来得全量重算画像(百万级、几小时)。
labels.ts 里当初就写了这条预警,卡片按它改了,详情页那两处漏改。

改:首屏 chip + 画像标签云 + 画像详情抽屉,potential_treatment 的中文统统从 code 查表。
compactPersonaValue / personaValueLabels 多收一个 featureKey 参数,只对 potential_treatment
生效 —— 其余多值特征(治疗史/权益/禁忌/时间偏好…)没有 code 表、措辞也没改过,继续读 labels。

纪律写进注释:**凡是中文措辞可能改的标签,展示一律 code → 查表,别读 data.labels。**

本地实测(赵欣冉,types=[endo,filling]):首屏 chip 从「潜在根管 +1」变成「根管治疗 +1」,
与卡片一致。717 tests / 47 suites + web typecheck 通过。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
parent 0ad1ab4a
Pipeline #3495 failed in 0 seconds
......@@ -12,6 +12,7 @@ import { cn } from '@/lib/utils';
import { tone } from './shared';
import { PersonaFeatureHover } from './persona-feature-hover';
import { factLabel } from './fact-label';
import { personaValueLabels } from './persona-display';
import type { PersonaFeature } from './mock-data';
import type { AdaptedFact } from './adapt-data';
......@@ -148,9 +149,8 @@ function FeatureCard({
// 多值特征(治疗史 / 潜在治疗 / 时间偏好…)后端给结构化 data.labels —— 详情里全部展开,
// 不像卡片那样截成 "+N"。单值特征直接显示后端那句完整 description。
const labels = (f.data as { labels?: unknown } | null | undefined)?.labels;
const valueLabels =
Array.isArray(labels) && labels.every((x) => typeof x === 'string') ? (labels as string[]) : null;
// ⭐ potential_treatment 走 code 查表(措辞可改),不读烤死的 labels —— 见 persona-display 注释。
const valueLabels = personaValueLabels(f.data, f.key);
// 证据:只渲染能在本次响应的 facts 里找到的(跨版本的旧 fact 可能已被 supersede,查不到就不显)
const resolved = f.evidence
......
import { potentialTreatmentCardLabel } from '@pac/types';
/**
* persona feature.description 的展示辅助 —— 把后端那句自包含描述裁成不同密度的展示形态。
*
......@@ -23,20 +25,54 @@ export function shortPersonaValueLabel(raw: string | null | undefined): string {
}
/**
* 潜在治疗的展示中文 —— **从 code 查表,不读 data.labels**。
*
* ⚠️ 2026-07-30 线上口径不一致就是这里踩的:同一个患者,召回池卡片显示「充填治疗」、
* 详情页 chip 显示「潜在补牙」。因为 `data.labels` 是**算画像那一刻烤进 JSON 的**
* (`{"types":["filling"],"labels":["潜在补牙"]}`),而卡片早就改成"只拿 code、中文查表"了。
* 措辞这几天改过三轮,烤死的那份自然全是旧词 —— 要改回来得全量重算画像(百万级、几小时)。
*
* 纪律:**凡是中文措辞可能改的标签,展示一律 code → 查表**,别读 labels。
* 目前只有 potential_treatment 有 code 表(POTENTIAL_TREATMENT_CARD_LABEL);
* 其余多值特征(治疗史/权益/禁忌/时间偏好…)暂时只能读 labels,它们的措辞也没改过。
*/
function potentialTreatmentLabels(data: unknown): string[] | null {
const types = (data as { types?: unknown } | null | undefined)?.types;
if (!Array.isArray(types)) return null;
const codes = types.filter((t): t is string => typeof t === 'string');
return codes.length > 0 ? codes.map(potentialTreatmentCardLabel) : null;
}
/**
* 多值标签的紧凑显示(首值 + `+N` 溢出)。
*
* 治疗史 / 权益 / 禁忌 / 潜在治疗 / 时间偏好 / 治疗敏感 / 特别关注 这 7 个特征是「`/` 并列多值」,
* 直接交给 shortPersonaValueLabel 会在第一个 `/` 处截断、只剩首值且不提示还有更多。
* 这里改读结构化 `data.labels`(这 7 个后端都带),显示首值 + `+N`,完整列表走 hover 卡片。
* 单值特征 → `{ text, more: 0 }`。
*
* ⭐ 传 featureKey:potential_treatment 走 code 查表(见上方注释),不读烤死的 labels。
*/
export function compactPersonaValue(
raw: string | null | undefined,
data?: unknown,
featureKey?: string,
): { text: string; more: number } {
const labels = (data as { labels?: unknown } | null | undefined)?.labels;
const fromCode = featureKey === 'potential_treatment' ? potentialTreatmentLabels(data) : null;
const labels =
fromCode ?? (data as { labels?: unknown } | null | undefined)?.labels;
if (Array.isArray(labels) && labels.length > 0 && typeof labels[0] === 'string') {
return { text: labels[0] as string, more: labels.length - 1 };
}
return { text: shortPersonaValueLabel(raw), more: 0 };
}
/** 抽屉里"全部展开"用的多值中文 —— 同样对 potential_treatment 走 code 查表 */
export function personaValueLabels(data: unknown, featureKey?: string): string[] | null {
const fromCode = featureKey === 'potential_treatment' ? potentialTreatmentLabels(data) : null;
if (fromCode) return fromCode;
const labels = (data as { labels?: unknown } | null | undefined)?.labels;
return Array.isArray(labels) && labels.every((x) => typeof x === 'string')
? (labels as string[])
: null;
}
......@@ -2483,7 +2483,8 @@ function PersonaTagCloud({
<div className={cn('flex flex-wrap', plain ? 'gap-1' : 'gap-1.5')}>
{ordered.map((f) => {
const T = tone(f.tone);
const { text: short, more } = compactPersonaValue(f.value, f.data);
// 传 f.key:potential_treatment 的中文走 code 查表,不读烤死的 data.labels(见 persona-display)
const { text: short, more } = compactPersonaValue(f.value, f.data, f.key);
// plain 下取值就是全部内容 —— 取不到值(理论上不该有)兜底显示标签名,不出空 chip
const text = plain ? short || f.label : short;
const inner = (
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment