228 lines
6.2 KiB
TypeScript
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 无效,无法新建会话')
|
|
}
|