From c5c84ff2977c86ed61f47ae5006d5d24576b07f4 Mon Sep 17 00:00:00 2001 From: vikingowl Date: Sat, 25 Apr 2026 11:15:16 +0200 Subject: [PATCH] fix(research): apply description via reactive state, add name correction Description wasn't being applied because querySelector-then-assign runs before Svelte's reactive flush of researchResult=null, which resets the textarea to its initial market.description value. Fix: reactive state + exported setter (same pattern as setHours/setAdmission). Also add markt_name to felder in both schemas and the prompt so the LLM can suggest a name correction. Name suggestions are gated to extraktion=direkt (high confidence only) and guarded on the frontend with setName(). --- backend/internal/domain/market/research.go | 6 ++++++ .../research/assets/researcher_prompt.de.md | 4 ++++ .../research/assets/researcher_schema.json | 3 ++- .../assets/researcher_schema_simple.json | 3 ++- web/src/lib/components/admin/MarketForm.svelte | 16 +++++++++++++--- .../lib/components/admin/ResearchPanel.svelte | 1 + .../admin/maerkte/[id]/bearbeiten/+page.svelte | 8 ++++++++ 7 files changed, 36 insertions(+), 5 deletions(-) diff --git a/backend/internal/domain/market/research.go b/backend/internal/domain/market/research.go index b269b42..0ab0d47 100644 --- a/backend/internal/domain/market/research.go +++ b/backend/internal/domain/market/research.go @@ -100,6 +100,7 @@ type llmOutput struct { } type llmFelder struct { + MarktName llmFieldStr `json:"markt_name"` Beschreibung llmFieldStr `json:"beschreibung"` Website llmFieldStr `json:"website"` Strasse llmFieldStr `json:"strasse"` @@ -171,6 +172,7 @@ func toLLMResearchResult(raw llmOutput, m Market) ResearchResult { } entries := []entry{ + {"name", strPtr(raw.Felder.MarktName.Wert), str(m.Name), raw.Felder.MarktName.Extraktion, raw.Felder.MarktName.Hinweis}, {"description", strPtr(raw.Felder.Beschreibung.Wert), str(m.Description), raw.Felder.Beschreibung.Extraktion, raw.Felder.Beschreibung.Hinweis}, {"website", strPtr(raw.Felder.Website.Wert), str(m.Website), raw.Felder.Website.Extraktion, raw.Felder.Website.Hinweis}, {"street", strPtr(raw.Felder.Strasse.Wert), str(m.Street), raw.Felder.Strasse.Extraktion, raw.Felder.Strasse.Hinweis}, @@ -195,6 +197,10 @@ func toLLMResearchResult(raw llmOutput, m Market) ResearchResult { if fmt.Sprintf("%v", e.suggested) == fmt.Sprintf("%v", e.current) { continue } + // Name corrections only shown when the LLM is certain (direkt). + if e.field == "name" && e.extraktion != "direkt" { + continue + } confidence := "medium" if e.extraktion == "direkt" { confidence = "high" diff --git a/backend/internal/domain/market/research/assets/researcher_prompt.de.md b/backend/internal/domain/market/research/assets/researcher_prompt.de.md index d96e36c..d4de310 100644 --- a/backend/internal/domain/market/research/assets/researcher_prompt.de.md +++ b/backend/internal/domain/market/research/assets/researcher_prompt.de.md @@ -24,6 +24,10 @@ Extraktion aus Quellen. ## Felder +- **markt_name**: Offizieller Name des Markts laut Veranstalter-Website. Nur + setzen wenn der Name klar und mit sehr hoher Sicherheit vom Input-Namen + abweicht (z.B. Jahrgang/Edition fehlt, offensichtlicher Tippfehler im Input). + Wenn der Input-Name korrekt oder nur minimal abweichend ist: `wert: null`. - **beschreibung**: Kurzbeschreibung des Markts direkt aus der Quelle (z.B. "Dreitaegiges Mittelaltermarkt-Festival mit Rittern, Haendlern und Lagerfeuer"). Nur Text der auf der Quelle steht - kein selbst verfasster Text. Typischerweise diff --git a/backend/internal/domain/market/research/assets/researcher_schema.json b/backend/internal/domain/market/research/assets/researcher_schema.json index e7489ce..79b9877 100644 --- a/backend/internal/domain/market/research/assets/researcher_schema.json +++ b/backend/internal/domain/market/research/assets/researcher_schema.json @@ -28,8 +28,9 @@ "properties": { "felder": { "type": "object", - "required": ["beschreibung", "website", "strasse", "plz", "stadt", "bundesland", "land", "veranstalter", "start_datum", "end_datum", "oeffnungszeiten", "eintrittspreise", "bild_url"], + "required": ["markt_name", "beschreibung", "website", "strasse", "plz", "stadt", "bundesland", "land", "veranstalter", "start_datum", "end_datum", "oeffnungszeiten", "eintrittspreise", "bild_url"], "properties": { + "markt_name": {"$ref": "#/$defs/stringFeld"}, "beschreibung": {"$ref": "#/$defs/stringFeld"}, "plz": {"$ref": "#/$defs/stringFeld"}, "land": {"$ref": "#/$defs/stringFeld"}, diff --git a/backend/internal/domain/market/research/assets/researcher_schema_simple.json b/backend/internal/domain/market/research/assets/researcher_schema_simple.json index e11e2bf..ba23682 100644 --- a/backend/internal/domain/market/research/assets/researcher_schema_simple.json +++ b/backend/internal/domain/market/research/assets/researcher_schema_simple.json @@ -8,8 +8,9 @@ "quellen_gesamt": {"type": "array", "items": {"type": "string"}}, "felder": { "type": "object", - "required": ["beschreibung", "website", "strasse", "plz", "stadt", "bundesland", "land", "veranstalter", "start_datum", "end_datum", "oeffnungszeiten", "eintrittspreise", "bild_url"], + "required": ["markt_name", "beschreibung", "website", "strasse", "plz", "stadt", "bundesland", "land", "veranstalter", "start_datum", "end_datum", "oeffnungszeiten", "eintrittspreise", "bild_url"], "properties": { + "markt_name": {"type": "object", "required": ["wert", "quellen", "extraktion", "hinweis"], "properties": {"wert": {"anyOf": [{"type": "string"}, {"type": "null"}]}, "hinweis": {"anyOf": [{"type": "string"}, {"type": "null"}]}, "quellen": {"type": "array", "items": {"type": "string"}}, "extraktion": {"type": "string", "enum": ["direkt", "kombiniert"]}}}, "beschreibung": {"type": "object", "required": ["wert", "quellen", "extraktion", "hinweis"], "properties": {"wert": {"anyOf": [{"type": "string"}, {"type": "null"}]}, "hinweis": {"anyOf": [{"type": "string"}, {"type": "null"}]}, "quellen": {"type": "array", "items": {"type": "string"}}, "extraktion": {"type": "string", "enum": ["direkt", "kombiniert"]}}}, "website": {"type": "object", "required": ["wert", "quellen", "extraktion", "hinweis"], "properties": {"wert": {"anyOf": [{"type": "string"}, {"type": "null"}]}, "hinweis": {"anyOf": [{"type": "string"}, {"type": "null"}]}, "quellen": {"type": "array", "items": {"type": "string"}}, "extraktion": {"type": "string", "enum": ["direkt", "kombiniert"]}}}, "strasse": {"type": "object", "required": ["wert", "quellen", "extraktion", "hinweis"], "properties": {"wert": {"anyOf": [{"type": "string"}, {"type": "null"}]}, "hinweis": {"anyOf": [{"type": "string"}, {"type": "null"}]}, "quellen": {"type": "array", "items": {"type": "string"}}, "extraktion": {"type": "string", "enum": ["direkt", "kombiniert"]}}}, diff --git a/web/src/lib/components/admin/MarketForm.svelte b/web/src/lib/components/admin/MarketForm.svelte index 1cacc8d..a56ba9a 100644 --- a/web/src/lib/components/admin/MarketForm.svelte +++ b/web/src/lib/components/admin/MarketForm.svelte @@ -40,6 +40,8 @@ UA: 'UAH' }; + let marktName = $state(untrack(() => market?.name ?? '')); + let description = $state(untrack(() => market?.description ?? '')); let selectedCountry = $state(untrack(() => market?.country ?? 'DE')); const currency = $derived(currencyByCountry[selectedCountry] ?? 'EUR'); @@ -190,6 +192,14 @@ export function setAdmission(newAdmission: AdmissionInfo) { admission = newAdmission; } + + export function setDescription(val: string) { + description = val; + } + + export function setName(val: string) { + marktName = val; + } {#if error} @@ -211,7 +221,7 @@ name="name" type="text" required - value={market?.name ?? ''} + value={marktName} placeholder={mode === 'public' ? 'z.B. Ritterturnier zu München' : ''} /> @@ -227,8 +237,8 @@ text-sm shadow-sm focus:ring-2 focus:outline-none dark:border-stone-600 dark:bg-stone-800" placeholder={mode === 'public' ? 'Beschreibe den Markt kurz...' : ''} - >{market?.description ?? ''} + bind:value={description} + > diff --git a/web/src/lib/components/admin/ResearchPanel.svelte b/web/src/lib/components/admin/ResearchPanel.svelte index 2ebbdd8..cad645c 100644 --- a/web/src/lib/components/admin/ResearchPanel.svelte +++ b/web/src/lib/components/admin/ResearchPanel.svelte @@ -14,6 +14,7 @@ let selected: boolean[] = $state(untrack(() => result.suggestions.map(() => true))); const fieldLabels: Record = { + name: 'Name', description: 'Beschreibung', street: 'Straße', city: 'Stadt', diff --git a/web/src/routes/admin/maerkte/[id]/bearbeiten/+page.svelte b/web/src/routes/admin/maerkte/[id]/bearbeiten/+page.svelte index a0cb774..342f5b1 100644 --- a/web/src/routes/admin/maerkte/[id]/bearbeiten/+page.svelte +++ b/web/src/routes/admin/maerkte/[id]/bearbeiten/+page.svelte @@ -84,6 +84,14 @@ } continue; } + if (s.field === 'description' && typeof s.suggested_value === 'string') { + marketForm.setDescription(s.suggested_value); + continue; + } + if (s.field === 'name' && typeof s.suggested_value === 'string') { + marketForm.setName(s.suggested_value); + continue; + } const el = document.querySelector( `[name="${s.field}"]` );