Add modular flow step for path-specific triage questions

This commit is contained in:
Dualmind-Assistant
2026-04-21 13:34:46 +00:00
parent 0b68e4b054
commit 02c425594d
7 changed files with 212 additions and 14 deletions
+13
View File
@@ -49,3 +49,16 @@ Die Datei `mts-config/flowcharts.json` enthält aktuell folgende abgebildete Pr
- BEHAVING_STRANGELY → chief_complaint `behaving_strangely` / `psychiatric` - BEHAVING_STRANGELY → chief_complaint `behaving_strangely` / `psychiatric`
Diese Konfiguration bildet die Zuordnung von Chief-Complaint-Codes zu den entsprechenden MTS-Präsentations-Flowcharts ab. Im Backend (FastAPI) ist bereits eine erste datengetriebene Logik für alle 10 Flowcharts umgesetzt: Das API ordnet `chief_complaint` automatisch dem passenden MTS-Flowchart zu, erkennt erste Red Flags wie `breathlessness`, `severe_pain` und `moderate_pain` und schlägt darauf basierend eine grobe Prioritätsstufe vor. Zusätzlich enthalten `questions.de.json` und `questions.en.json` bereits eine Ja/Nein-Frage zu starker Luftnot als ersten expliziten Red-Flag-Discriminator im Fragenstrom. Diese Konfiguration bildet die Zuordnung von Chief-Complaint-Codes zu den entsprechenden MTS-Präsentations-Flowcharts ab. Im Backend (FastAPI) ist bereits eine erste datengetriebene Logik für alle 10 Flowcharts umgesetzt: Das API ordnet `chief_complaint` automatisch dem passenden MTS-Flowchart zu, erkennt erste Red Flags wie `breathlessness`, `severe_pain` und `moderate_pain` und schlägt darauf basierend eine grobe Prioritätsstufe vor. Zusätzlich enthalten `questions.de.json` und `questions.en.json` bereits eine Ja/Nein-Frage zu starker Luftnot als ersten expliziten Red-Flag-Discriminator im Fragenstrom.
## Aktueller App-Flow
Die Frontend-App bleibt modular als Vue-3-SPA aufgebaut und wurde um einen zusätzlichen Schritt `FlowView` erweitert. Der Ablauf ist aktuell:
- `LanguageView` Sprache wählen
- `ComplaintView` Hauptbeschwerde auswählen
- `FlowView` pfadspezifische Zusatzfragen / Red-Flag-Fragen (derzeit als modulare Boolean-Komponenten)
- `PainView` Schmerzintensität erfassen
- `SummaryView` vorgeschlagenes MTS-Flowchart, Red Flags und Prioritätsstufe anzeigen
Die Zusatzfragen werden aus `mts-config/questions.<lang>.json` geladen. Damit kann die App schrittweise in Richtung mehrerer MTS-spezifischer Entscheidungsbäume erweitert werden, ohne eine monolithische Einzeldatei zu erzeugen.
+84
View File
@@ -0,0 +1,84 @@
<script setup lang="ts">
const modelValue = defineModel<boolean | null>()
const props = defineProps<{
title: string
trueLabel?: string
falseLabel?: string
}>()
function select(value: boolean) {
modelValue.value = value
}
</script>
<template>
<section class="boolean-section" aria-labelledby="boolean-title" role="radiogroup">
<h2 id="boolean-title">{{ props.title }}</h2>
<div class="grid">
<button
type="button"
class="choice-btn"
:class="{ active: modelValue === true }"
role="radio"
:aria-checked="modelValue === true"
@click="select(true)"
>
{{ props.trueLabel ?? 'Ja' }}
</button>
<button
type="button"
class="choice-btn"
:class="{ active: modelValue === false }"
role="radio"
:aria-checked="modelValue === false"
@click="select(false)"
>
{{ props.falseLabel ?? 'Nein' }}
</button>
</div>
</section>
</template>
<style scoped>
h2 {
font-size: 1rem;
margin-bottom: 0.75rem;
}
.grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 0.75rem;
}
.choice-btn {
min-height: 56px;
padding: 0.9rem 1rem;
border-radius: 18px;
border: 1px solid #d1d5db;
background: #f9fafb;
font-size: 1rem;
font-weight: 600;
color: #111827;
cursor: pointer;
transition: background 0.12s ease-out, border-color 0.12s ease-out, box-shadow 0.12s ease-out,
transform 0.08s ease-out;
}
.choice-btn:hover {
background: #f3f4f6;
}
.choice-btn.active {
background: #e5f0ff;
border-color: #2563eb;
color: #0b1f4a;
box-shadow: 0 0 0 2px rgba(37, 99, 235, 0.25);
}
.choice-btn:focus-visible {
outline: 3px solid #2563eb;
outline-offset: 2px;
}
</style>
+46 -12
View File
@@ -29,23 +29,38 @@ const questions = reactive<Question[]>([])
const isLoadingConfig = ref(false) const isLoadingConfig = ref(false)
const loadError = ref<string | null>(null) const loadError = ref<string | null>(null)
const chiefComplaint = ref<string | null>(null) const answers = reactive<Record<string, string | number | boolean | null>>({
const pain = ref<number>(0) chief_complaint: null,
breathlessness: null,
pain_intensity: 0,
})
const isSubmitting = ref(false) const isSubmitting = ref(false)
const submitError = ref<string | null>(null) const submitError = ref<string | null>(null)
const result = ref<MtsPreparation | null>(null) const result = ref<MtsPreparation | null>(null)
const chiefComplaintQuestion = computed(() => const orderedQuestions = computed(() => questions)
questions.find((q) => q.code === 'chief_complaint') ?? null, const chiefComplaintQuestion = computed(() => questions.find((q) => q.code === 'chief_complaint') ?? null)
) const flowQuestions = computed(() => questions.filter((q) => q.code !== 'chief_complaint' && q.code !== 'pain_intensity'))
const painQuestion = computed(() => questions.find((q) => q.code === 'pain_intensity') ?? null) const painQuestion = computed(() => questions.find((q) => q.code === 'pain_intensity') ?? null)
const chiefComplaint = computed({
get: () => (typeof answers.chief_complaint === 'string' ? answers.chief_complaint : null),
set: (value: string | null) => {
answers.chief_complaint = value
},
})
const pain = computed({
get: () => (typeof answers.pain_intensity === 'number' ? answers.pain_intensity : 0),
set: (value: number) => {
answers.pain_intensity = value
},
})
const priorityClass = computed(() => { const priorityClass = computed(() => {
const level = result.value?.suggested_priority_level const level = result.value?.suggested_priority_level
if (!level) return '' if (!level) return ''
if (level === 'RED_OR_ORANGE') return 'priority-high' if (level === 'RED_OR_ORANGE') return 'priority-high'
if (level === 'YELLOW_OR_ORANGE') return 'priority-medium'
return 'priority-default' return 'priority-default'
}) })
@@ -53,15 +68,29 @@ const priorityLabel = computed(() => {
const level = result.value?.suggested_priority_level const level = result.value?.suggested_priority_level
if (!level) return '' if (!level) return ''
if (level === 'RED_OR_ORANGE') return 'Hohe Dringlichkeit' if (level === 'RED_OR_ORANGE') return 'Hohe Dringlichkeit'
if (level === 'YELLOW_OR_ORANGE') return 'Erhöhte Dringlichkeit'
return 'Priorität' return 'Priorität'
}) })
function resetAnswers() {
answers.chief_complaint = null
answers.breathlessness = null
answers.pain_intensity = 0
}
function setAnswer(questionCode: string, value: string | number | boolean | null) {
answers[questionCode] = value
}
function getAnswer(questionCode: string) {
return answers[questionCode] ?? null
}
async function loadQuestions() { async function loadQuestions() {
isLoadingConfig.value = true isLoadingConfig.value = true
loadError.value = null loadError.value = null
questions.splice(0, questions.length) questions.splice(0, questions.length)
chiefComplaint.value = null resetAnswers()
pain.value = 0
result.value = null result.value = null
try { try {
@@ -88,10 +117,9 @@ async function startSession() {
const payload = { const payload = {
language: language.value, language: language.value,
answers: [ answers: orderedQuestions.value
{ question_code: 'chief_complaint', value: chiefComplaint.value }, .filter((q) => answers[q.code] !== null && answers[q.code] !== undefined)
{ question_code: 'pain_intensity', value: pain.value }, .map((q) => ({ question_code: q.code, value: answers[q.code] })),
],
} }
try { try {
@@ -118,10 +146,13 @@ export function useTriageSession() {
return { return {
language, language,
questions, questions,
orderedQuestions,
isLoadingConfig, isLoadingConfig,
loadError, loadError,
answers,
chiefComplaint, chiefComplaint,
pain, pain,
flowQuestions,
isSubmitting, isSubmitting,
submitError, submitError,
result, result,
@@ -129,7 +160,10 @@ export function useTriageSession() {
painQuestion, painQuestion,
priorityClass, priorityClass,
priorityLabel, priorityLabel,
setAnswer,
getAnswer,
loadQuestions, loadQuestions,
startSession, startSession,
resetAnswers,
} }
} }
+2
View File
@@ -1,6 +1,7 @@
import { createRouter, createWebHistory } from 'vue-router' import { createRouter, createWebHistory } from 'vue-router'
import LanguageView from '../views/LanguageView.vue' import LanguageView from '../views/LanguageView.vue'
import ComplaintView from '../views/ComplaintView.vue' import ComplaintView from '../views/ComplaintView.vue'
import FlowView from '../views/FlowView.vue'
import PainView from '../views/PainView.vue' import PainView from '../views/PainView.vue'
import SummaryView from '../views/SummaryView.vue' import SummaryView from '../views/SummaryView.vue'
@@ -10,6 +11,7 @@ const router = createRouter({
{ path: '/', redirect: '/language' }, { path: '/', redirect: '/language' },
{ path: '/language', component: LanguageView }, { path: '/language', component: LanguageView },
{ path: '/complaint', component: ComplaintView }, { path: '/complaint', component: ComplaintView },
{ path: '/flow', component: FlowView },
{ path: '/pain', component: PainView }, { path: '/pain', component: PainView },
{ path: '/summary', component: SummaryView }, { path: '/summary', component: SummaryView },
], ],
+1 -1
View File
@@ -17,7 +17,7 @@ onMounted(() => {
function goNext() { function goNext() {
if (chiefComplaint.value) { if (chiefComplaint.value) {
router.push('/pain') router.push('/flow')
} }
} }
</script> </script>
+65
View File
@@ -0,0 +1,65 @@
<script setup lang="ts">
import { computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import BooleanChoice from '../components/BooleanChoice.vue'
import { useTriageSession } from '../composables/useTriageSession'
const router = useRouter()
const { chiefComplaint, flowQuestions, getAnswer, setAnswer } = useTriageSession()
const isReady = computed(() =>
flowQuestions.value.every((q) => getAnswer(q.code) !== null && getAnswer(q.code) !== undefined),
)
onMounted(() => {
if (!chiefComplaint.value) {
router.replace('/complaint')
}
})
function goNext() {
if (isReady.value) {
router.push('/pain')
}
}
</script>
<template>
<section>
<div class="stack" v-if="flowQuestions.length > 0">
<template v-for="question in flowQuestions" :key="question.code">
<BooleanChoice
v-if="question.type === 'boolean'"
:title="question.title"
:model-value="(getAnswer(question.code) as boolean | null)"
@update:model-value="(value) => setAnswer(question.code, value)"
/>
</template>
</div>
<p v-else class="status">Für diesen Pfad sind noch keine Zusatzfragen konfiguriert.</p>
<div class="actions">
<button class="primary" type="button" :disabled="!isReady && flowQuestions.length > 0" @click="goNext">
Weiter
</button>
</div>
</section>
</template>
<style scoped>
.stack {
display: grid;
gap: 1rem;
}
.actions {
margin-top: 1.5rem;
}
.status {
margin-top: 1rem;
font-size: 0.9rem;
color: #4b5563;
}
</style>
+1 -1
View File
@@ -11,7 +11,7 @@ const isReady = computed(() => !!chiefComplaint.value && !!painQuestion.value)
onMounted(() => { onMounted(() => {
if (!chiefComplaint.value) { if (!chiefComplaint.value) {
router.replace('/complaint') router.replace('/flow')
} }
}) })