Files
2026-06-09 17:00:23 +08:00

228 lines
6.2 KiB
TypeScript

import {
FASTAPI_BASE_URL,
authHeaders,
createTrainingSession,
readError,
type PatientConfigPayload,
type TrainingMode
} from './session'
export type ScenarioRecommendation = {
id: string
title: string
desc: string
tags: string[]
defaults: ScenarioForm
}
export type ScenarioOption = {
value: string
label: string
description?: string
}
export type ScenarioForm = {
environment: string
ageGroup: string
education: string
personality: string
}
export type ScenarioOptions = {
environments: ScenarioOption[]
ageGroups: ScenarioOption[]
educations: ScenarioOption[]
personalities: ScenarioOption[]
}
export type TrainingConfigRecommended = {
recommended: ScenarioForm
recommendedLabels: Record<ScenarioFormKey, string>
options: ScenarioOptions
}
export type ScenarioConfigPayload = ScenarioForm & {
caseId: string
caseNo: string
mode: TrainingMode
recommendationId?: string
}
type TrainingConfigApiKey = 'visit_environment' | 'age_group' | 'education_level' | 'personality'
const DEFAULT_CASE_ID = 1
type ApiEnvelope<T> = {
code: string
message: string
data: T
}
type ScenarioFormKey = keyof ScenarioForm
type TrainingConfigRecommendedResponse = {
case_id: number
recommended: {
visit_environment: string
age_group: string
education_level: string
personality: string
}
recommended_labels: {
visit_environment: string
age_group: string
education_level: string
personality: string
}
options?: Partial<Record<TrainingConfigApiKey, ScenarioOption[] | Record<string, string>>>
}
export const DEFAULT_SCENARIO_OPTIONS: ScenarioOptions = {
environments: [
{ value: 'outpatient', label: '门诊' },
{ value: 'emergency', label: '急诊' },
{ value: 'ward', label: '病房' }
],
ageGroups: [
{ value: 'child', label: '儿童' },
{ value: 'youth', label: '青年' },
{ value: 'middle_aged', label: '中年' },
{ value: 'elderly', label: '老年' }
],
educations: [
{ value: 'primary_or_below', label: '小学及以下' },
{ value: 'secondary', label: '中等教育' },
{ value: 'higher', label: '高等教育' }
],
personalities: [
{ value: 'calm', label: '平和' },
{ value: 'anxious', label: '焦虑' },
{ value: 'impatient', label: '急躁' },
{ value: 'cooperative', label: '配合' },
{ value: 'suspicious', label: '多疑' }
]
}
export function fetchScenarioOptions() {
return Promise.resolve({
recommendations: [
{
id: 'typical-outpatient',
title: '典型门诊病例',
desc: '门诊常规病例:针对初学者设计的标准化沟通流程。',
tags: ['中年', '配合'],
defaults: {
environment: 'outpatient',
ageGroup: 'middle_aged',
education: 'higher',
personality: 'calm'
}
},
{
id: 'emergency-crisis',
title: '急诊重症危机',
desc: '急诊危重:急性心梗紧急入院。',
tags: ['老年', '急躁'],
defaults: {
environment: 'emergency',
ageGroup: 'elderly',
education: 'secondary',
personality: 'impatient'
}
}
] as ScenarioRecommendation[],
options: DEFAULT_SCENARIO_OPTIONS
})
}
function normalizeApiOptions(
source: TrainingConfigRecommendedResponse['options'] | undefined,
key: keyof TrainingConfigRecommendedResponse['recommended'],
fallback: ScenarioOption[]
) {
const value = source?.[key]
if (Array.isArray(value)) return value
if (value && typeof value === 'object') {
return Object.entries(value).map(([optionValue, label]) => ({
value: optionValue,
label: String(label)
}))
}
return fallback
}
function normalizeTrainingConfig(payload: TrainingConfigRecommendedResponse): TrainingConfigRecommended {
return {
recommended: {
environment: payload.recommended.visit_environment,
ageGroup: payload.recommended.age_group,
education: payload.recommended.education_level,
personality: payload.recommended.personality
},
recommendedLabels: {
environment: payload.recommended_labels.visit_environment,
ageGroup: payload.recommended_labels.age_group,
education: payload.recommended_labels.education_level,
personality: payload.recommended_labels.personality
},
options: {
environments: normalizeApiOptions(payload.options, 'visit_environment', DEFAULT_SCENARIO_OPTIONS.environments),
ageGroups: normalizeApiOptions(payload.options, 'age_group', DEFAULT_SCENARIO_OPTIONS.ageGroups),
educations: normalizeApiOptions(payload.options, 'education_level', DEFAULT_SCENARIO_OPTIONS.educations),
personalities: normalizeApiOptions(payload.options, 'personality', DEFAULT_SCENARIO_OPTIONS.personalities)
}
}
}
export async function fetchTrainingConfigOptions(caseId: number) {
const response = await fetch(`${FASTAPI_BASE_URL}/training-config/options?case_id=${encodeURIComponent(caseId)}`, {
method: 'GET',
headers: authHeaders()
})
if (!response.ok) {
throw new Error(await readError(response))
}
const result = (await response.json()) as ApiEnvelope<TrainingConfigRecommendedResponse>
if (result.code !== 'OK' || !result.data?.recommended) {
throw new Error(result.message || '推荐配置加载失败')
}
return normalizeTrainingConfig(result.data)
}
export function createScenarioConfig(payload: ScenarioConfigPayload) {
return createTrainingSession({
case_id: DEFAULT_CASE_ID,
training_type: 'diagnosis_treatment',
mode: payload.mode,
score_type: 'percentage',
patient_config: {
visit_environment: payload.environment as PatientConfigPayload['visit_environment'],
age_group: payload.ageGroup as PatientConfigPayload['age_group'],
education_level: payload.education as PatientConfigPayload['education_level'],
personality: payload.personality as PatientConfigPayload['personality']
}
}).then(session => ({
id: session.session_code,
...payload,
session,
createdAt: new Date().toISOString()
}))
}
function resolveCaseId(payload: ScenarioConfigPayload) {
const explicitId = Number(payload.caseId)
if (Number.isInteger(explicitId) && explicitId > 0) return explicitId
const caseNo = Number(payload.caseNo)
if (Number.isInteger(caseNo) && caseNo > 0) return caseNo
const matched = payload.caseId.match(/\d+/)
const fallbackId = matched ? Number(matched[0]) : 0
if (Number.isInteger(fallbackId) && fallbackId > 0) return fallbackId
throw new Error('病例 ID 无效,无法新建会话')
}