Add initial config-driven triage webapp (backend + frontend)

This commit is contained in:
Dualmind-Assistant
2026-04-21 12:27:53 +00:00
parent 2da5d183aa
commit a1ccd568b0
824 changed files with 996177 additions and 35 deletions
+179 -14
View File
@@ -1,16 +1,116 @@
<script setup lang="ts">
import { ref } from 'vue'
import { computed, ref, watch } from 'vue'
import LanguageSelect from './components/LanguageSelect.vue'
import SymptomSelector from './components/SymptomSelector.vue'
import PainSlider from './components/PainSlider.vue'
const language = ref('de')
const chiefComplaint = ref<string | null>(null)
const pain = ref(0)
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL ?? 'http://localhost:8000'
function startSession() {
console.log('Start session', { language: language.value, chiefComplaint: chiefComplaint.value, pain: pain.value })
// TODO: POST an Backend /sessions
interface QuestionOption {
value: string
label: string
}
interface Question {
code: string
type: string
title: string
options?: QuestionOption[]
min?: number
max?: number
}
interface MtsPreparation {
session_id: string
proposed_presenting_flowchart?: string | null
red_flag_indicators: string[]
suggested_priority_level?: string | null
}
const language = ref<'de' | 'en' | 'ar'>('de')
const questions = ref<Question[]>([])
const isLoadingConfig = ref(false)
const loadError = ref<string | null>(null)
const chiefComplaint = ref<string | null>(null)
const pain = ref<number>(0)
const isSubmitting = ref(false)
const submitError = ref<string | null>(null)
const result = ref<MtsPreparation | null>(null)
const chiefComplaintQuestion = computed(() =>
questions.value.find((q) => q.code === 'chief_complaint') ?? null,
)
const painQuestion = computed(() =>
questions.value.find((q) => q.code === 'pain_intensity') ?? null,
)
async function loadQuestions() {
isLoadingConfig.value = true
loadError.value = null
questions.value = []
chiefComplaint.value = null
pain.value = 0
result.value = null
try {
const res = await fetch(`${API_BASE_URL}/questions/${language.value}`)
if (!res.ok) {
throw new Error(`HTTP ${res.status}`)
}
const data = (await res.json()) as { language: string; questions: Question[] }
questions.value = data.questions
} catch (err) {
console.error('Failed to load questions', err)
loadError.value = 'Konfiguration konnte nicht geladen werden.'
} finally {
isLoadingConfig.value = false
}
}
watch(
() => language.value,
() => {
void loadQuestions()
},
{ immediate: true },
)
async function startSession() {
if (!chiefComplaint.value) return
isSubmitting.value = true
submitError.value = null
result.value = null
const payload = {
language: language.value,
answers: [
{ question_code: 'chief_complaint', value: chiefComplaint.value },
{ question_code: 'pain_intensity', value: pain.value },
],
}
try {
const res = await fetch(`${API_BASE_URL}/sessions`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
})
if (!res.ok) {
throw new Error(`HTTP ${res.status}`)
}
result.value = (await res.json()) as MtsPreparation
} catch (err) {
console.error('Failed to start session', err)
submitError.value = 'Sitzung konnte nicht angelegt werden.'
} finally {
isSubmitting.value = false
}
}
</script>
@@ -22,17 +122,63 @@ function startSession() {
<LanguageSelect v-model="language" />
</section>
<section v-if="language">
<SymptomSelector v-model="chiefComplaint" />
<section v-if="isLoadingConfig" class="status">
Konfiguration wird geladen ...
</section>
<section v-if="chiefComplaint">
<PainSlider v-model="pain" />
<section v-if="loadError" class="status error">
{{ loadError }}
</section>
<section v-if="chiefComplaint">
<button class="primary" @click="startSession">Weiter</button>
</section>
<template v-if="!isLoadingConfig && !loadError">
<section v-if="chiefComplaintQuestion">
<SymptomSelector
v-model="chiefComplaint"
:title="chiefComplaintQuestion.title"
:options="chiefComplaintQuestion.options ?? []"
/>
</section>
<section v-if="chiefComplaint && painQuestion">
<PainSlider
v-model="pain"
:title="painQuestion.title"
:min="painQuestion.min ?? 0"
:max="painQuestion.max ?? 10"
/>
</section>
<section v-if="chiefComplaint" class="actions">
<button class="primary" type="button" :disabled="isSubmitting" @click="startSession">
<span v-if="!isSubmitting">Weiter</span>
<span v-else>Wird gesendet ...</span>
</button>
</section>
<section v-if="submitError" class="status error">
{{ submitError }}
</section>
<section v-if="result" class="result">
<h2>Vorbereitung für MTS</h2>
<p>
Sitzungs-ID:
<strong>{{ result.session_id }}</strong>
</p>
<p v-if="result.proposed_presenting_flowchart">
Vorgeschlagenes Flussdiagramm:
<strong>{{ result.proposed_presenting_flowchart }}</strong>
</p>
<p v-if="result.red_flag_indicators.length">
Red Flags:
<strong>{{ result.red_flag_indicators.join(', ') }}</strong>
</p>
<p v-if="result.suggested_priority_level">
Vorgeschlagene Priorität:
<strong>{{ result.suggested_priority_level }}</strong>
</p>
</section>
</template>
</main>
</template>
@@ -52,9 +198,28 @@ section {
margin-top: 1.5rem;
}
.actions {
margin-top: 2rem;
}
button.primary {
width: 100%;
padding: 1rem;
font-size: 1.1rem;
}
.status {
margin-top: 1rem;
}
.status.error {
color: #b91c1c;
}
.result {
margin-top: 2rem;
padding: 1rem;
border-radius: 0.5rem;
background: #f9fafb;
}
</style>
+9 -3
View File
@@ -1,6 +1,12 @@
<script setup lang="ts">
const modelValue = defineModel<number>({ default: 0 })
const props = defineProps<{
title: string
min: number
max: number
}>()
function onInput(ev: Event) {
const target = ev.target as HTMLInputElement
modelValue.value = Number(target.value)
@@ -9,12 +15,12 @@ function onInput(ev: Event) {
<template>
<div>
<h2>Wie stark sind die Schmerzen?</h2>
<h2>{{ props.title }}</h2>
<div class="value">{{ modelValue }}</div>
<input
type="range"
min="0"
max="10"
:min="props.min"
:max="props.max"
:value="modelValue"
@input="onInput"
/>
+16 -14
View File
@@ -1,30 +1,32 @@
<script setup lang="ts">
const modelValue = defineModel<string | null>()
const options = [
{ code: 'chest_pain', label: 'Brust' },
{ code: 'abdominal_pain', label: 'Bauch' },
{ code: 'headache', label: 'Kopf' },
{ code: 'trauma', label: 'Unfall/Verletzung' },
{ code: 'unwell', label: 'Allgemein schlecht' }
]
interface QuestionOption {
value: string
label: string
}
function select(code: string) {
modelValue.value = code
const props = defineProps<{
title: string
options: QuestionOption[]
}>()
function select(value: string) {
modelValue.value = value
}
</script>
<template>
<div>
<h2>Wo ist das Hauptproblem?</h2>
<h2>{{ props.title }}</h2>
<div class="grid">
<button
v-for="opt in options"
:key="opt.code"
v-for="opt in props.options"
:key="opt.value"
type="button"
class="symptom-btn"
:class="{ active: opt.code === modelValue }"
@click="select(opt.code)"
:class="{ active: opt.value === modelValue }"
@click="select(opt.value)"
>
{{ opt.label }}
</button>