diff --git a/LehrerApp.Core/AiPlanning/AiPlanningDtos.cs b/LehrerApp.Core/AiPlanning/AiPlanningDtos.cs index fade0f8..aa1227d 100644 --- a/LehrerApp.Core/AiPlanning/AiPlanningDtos.cs +++ b/LehrerApp.Core/AiPlanning/AiPlanningDtos.cs @@ -153,3 +153,24 @@ public class AiHazardSubstance public string HStatements { get; set; } = ""; public string PStatements { get; set; } = ""; } + +/// +/// Wire-Vertrag für die Chemikalien-Recherche (ai-backend/substance.php, Nutzerwunsch neben 4.2): +/// ein reiner Datenbank-Lookup gegen die lokale RiSU-Stoffliste, kein KI-Aufruf (siehe +/// ai-backend/stoffliste.php) — der Name "Ai..." bleibt trotzdem bestehen, da der Aufruf über +/// dieselbe Anmeldung/denselben Host wie die echten KI-Endpunkte läuft. Eigener, sehr schmaler +/// Wire-Vertrag statt Wiederverwendung von AiHazardSubstance — die Recherche liefert +/// zusätzliche, nur hier relevante Felder (CAS, Signalwort, Tätigkeitsbeschränkung, Quelle). +/// Enthält bewusst KEIN Entsorgungsfeld — die RiSU-Stoffliste führt keine Entsorgungshinweise. +/// +public class AiSubstanceResearchResponse +{ + public string Name { get; set; } = ""; + public string Cas { get; set; } = ""; + public List GhsPictograms { get; set; } = []; + public string SignalWord { get; set; } = ""; + public string HStatements { get; set; } = ""; + public string PStatements { get; set; } = ""; + public string ActivityRestriction { get; set; } = ""; + public string Source { get; set; } = ""; +} diff --git a/LehrerApp.Core/Models/HazardAssessment.cs b/LehrerApp.Core/Models/HazardAssessment.cs index 985d67a..6a81662 100644 --- a/LehrerApp.Core/Models/HazardAssessment.cs +++ b/LehrerApp.Core/Models/HazardAssessment.cs @@ -31,15 +31,25 @@ public class HazardAssessment public class HazardSubstance { public string Name { get; set; } = ""; + /// CAS-Nummer, falls über die Chemikalien-Recherche (TODO.md 4.2-Nachtrag) ermittelt oder von + /// Hand eingetragen — optional, nur zur eindeutigeren Wiedererkennung/Recherche. + public string Cas { get; set; } = ""; /// Typische Einsatzmenge, z.B. "ca. 2 g" — Freitext statt strukturierter Einheit, da der /// Rahmen (fest/Lösung/Gas, Konzentration ...) zu unterschiedlich für ein starres Feld ist. public string Amount { get; set; } = ""; public List GhsPictograms { get; set; } = []; + /// "Gefahr" oder "Achtung" (GHS-Signalwort) — Freitext statt Enum, da GESTIS-Recherchen auch + /// mal ohne eindeutiges Signalwort zurückkommen können. + public string SignalWord { get; set; } = ""; /// H-Sätze (Gefahrenhinweise) als Freitext, z.B. "H314, H290" oder mit Kurztext — bewusst kein /// eingebauter Katalog amtlicher H-/P-Satz-Texte in der App (Fehlerrisiko bei /// sicherheitsrelevanten Angaben), die Lehrkraft prüft/ergänzt gegen das Sicherheitsdatenblatt. public string HStatements { get; set; } = ""; public string PStatements { get; set; } = ""; + /// Tätigkeitsbeschränkung laut RiSU-Stoffliste (Freigabe für alle/Schulversuche/nur + /// Lehrkraft, Altersgrenzen, Ersatzstoffprüfung nötig, verboten, ...) — Freitext, da die + /// tatsächlichen Formulierungen zu unterschiedlich für ein starres Feld/Enum sind. + public string ActivityRestriction { get; set; } = ""; } public enum ExperimentKind { Lehrerversuch, Schuelerversuch, Demonstrationsversuch } diff --git a/LehrerApp.Desktop.Tests/HazardAssessmentWizardViewModelTests.cs b/LehrerApp.Desktop.Tests/HazardAssessmentWizardViewModelTests.cs index afb1515..72b7ae6 100644 --- a/LehrerApp.Desktop.Tests/HazardAssessmentWizardViewModelTests.cs +++ b/LehrerApp.Desktop.Tests/HazardAssessmentWizardViewModelTests.cs @@ -1,4 +1,6 @@ +using LehrerApp.Core.AiPlanning; using LehrerApp.Core.Models; +using LehrerApp.Desktop.Services; using LehrerApp.Desktop.ViewModels.Groups; using Xunit; @@ -8,6 +10,13 @@ public sealed class HazardAssessmentWizardViewModelTests { private static HazardAssessmentWizardViewModel Build(HazardAssessment? editing = null) => new(editing, "10c"); + private static string BuildTempPath() + { + var path = Path.Combine(Path.GetTempPath(), $"lehrerapp-gbuwizard-tests-{Guid.NewGuid():N}"); + Directory.CreateDirectory(path); + return path; + } + [Fact] public void Neu_StartetAufSchritt0OhneKiOhneEditingKennzeichnung() { @@ -227,6 +236,111 @@ public sealed class HazardAssessmentWizardViewModelTests Assert.Equal("Notiz", vm.Notes); Assert.True(vm.IsAiAssisted); } + + [Fact] + public void CanUseAi_OhneKiAbhaengigkeiten_IstFalseUndSubstanzenKoennenNichtRecherchieren() + { + var vm = Build(); + vm.AddSubstanceCommand.Execute(null); + + Assert.False(vm.CanUseAi); + Assert.False(vm.Substances[0].CanResearch); + } + + [Fact] + public void CanUseAi_MitAngemeldeterKiUndUnitUndLesson_IstTrueUndSubstanzenKoennenRecherchieren() + { + var aiSettings = new AiSettingsService(BuildTempPath()); + aiSettings.SetEnabled(true); + aiSettings.SetCredentialsAndToken("sebastian", "token-abc"); + var ai = new AiPlanningService(new HttpClient(), new FakeLessons(), new FakeGroups([]), + new FakeSubjects([]), new FakeCompetencyDomains(), new FakeAlternativeLessonPaths([])); + var unit = new Unit { Title = "Redoxreaktionen" }; + var lesson = new Lesson { Topic = "Natrium in Wasser" }; + + var vm = new HazardAssessmentWizardViewModel(null, "10c", ai, aiSettings, unit, lesson); + vm.AddSubstanceCommand.Execute(null); + + Assert.True(vm.CanUseAi); + Assert.True(vm.Substances[0].CanResearch); + } +} + +public sealed class HazardSubstanceEditItemTests +{ + [Fact] + public async Task Research_OhneOnResearch_TutNichtsUndBleibtOhneFehler() + { + var item = new HazardSubstanceEditItem { Name = "Natrium" }; + + await item.ResearchCommand.ExecuteAsync(null); + + Assert.Equal("", item.ResearchError); + Assert.False(item.IsResearching); + } + + [Fact] + public async Task Research_OhneNamen_SetztFehlerUndRuftDelegateNicht() + { + var called = false; + var item = new HazardSubstanceEditItem + { + Name = " ", + OnResearch = _ => { called = true; return Task.FromResult(new AiSubstanceResearchResponse()); }, + }; + + await item.ResearchCommand.ExecuteAsync(null); + + Assert.False(called); + Assert.Equal("Stoffname eingeben.", item.ResearchError); + } + + [Fact] + public async Task Research_UebernimmtAlleFelderAusDerAntwort() + { + var item = new HazardSubstanceEditItem { Name = "Natrium" }; + item.OnResearch = name => + { + Assert.Equal("Natrium", name); + return Task.FromResult(new AiSubstanceResearchResponse + { + Name = "Natrium", + Cas = "7440-23-5", + SignalWord = "Gefahr", + HStatements = "H260", + PStatements = "P223, P231+P232", + ActivityRestriction = "Für Schüler ab der 5. Klasse freigegeben", + GhsPictograms = ["GHS02", "GHS05"], + }); + }; + + await item.ResearchCommand.ExecuteAsync(null); + + Assert.Equal("Natrium", item.Name); + Assert.Equal("7440-23-5", item.Cas); + Assert.Equal("Gefahr", item.SignalWord); + Assert.Equal("H260", item.HStatements); + Assert.Equal("P223, P231+P232", item.PStatements); + Assert.Equal("Für Schüler ab der 5. Klasse freigegeben", item.ActivityRestriction); + Assert.True(item.PictogramOptions.Single(o => o.Value == GhsPictogram.Flammable).IsSelected); + Assert.True(item.PictogramOptions.Single(o => o.Value == GhsPictogram.Corrosive).IsSelected); + Assert.False(item.PictogramOptions.Single(o => o.Value == GhsPictogram.Toxic).IsSelected); + Assert.False(item.IsResearching); + Assert.Equal("", item.ResearchError); + } + + [Fact] + public async Task Research_BeiFehler_SetztResearchErrorUndBehaeltAlteFelderUnveraendert() + { + var item = new HazardSubstanceEditItem { Name = "Unbekannt", HStatements = "H999" }; + item.OnResearch = _ => throw new AiBackendException("Nicht genügend KI-Guthaben. Bitte Guthaben aufladen."); + + await item.ResearchCommand.ExecuteAsync(null); + + Assert.Equal("Nicht genügend KI-Guthaben. Bitte Guthaben aufladen.", item.ResearchError); + Assert.Equal("H999", item.HStatements); + Assert.False(item.IsResearching); + } } public sealed class ExperimentKindDisplayTests diff --git a/LehrerApp.Desktop.Tests/PdfExportServiceTests.cs b/LehrerApp.Desktop.Tests/PdfExportServiceTests.cs index e860fb5..fc7bdca 100644 --- a/LehrerApp.Desktop.Tests/PdfExportServiceTests.cs +++ b/LehrerApp.Desktop.Tests/PdfExportServiceTests.cs @@ -162,8 +162,9 @@ public sealed class PdfExportServiceTests "Natrium in Wasser", "10c", "12.09.2026", "Lehrerversuch", "Ein kleines Stück Natrium wird in eine Schale mit Wasser gegeben.", [ - new HazardSubstancePrintRow("Natrium", "ca. 0,5 g", "GHS02, GHS05", - "H260 - Reagiert mit Wasser unter Bildung entzündbarer Gase", "P223, P231+P232, P370+P378"), + new HazardSubstancePrintRow("Natrium", "7440-23-5", "ca. 0,5 g", "GHS02, GHS05", "Gefahr", + "H260 - Reagiert mit Wasser unter Bildung entzündbarer Gase", "P223, P231+P232, P370+P378", + "Abgabe an unter 18-Jährige verboten"), ], ["Wasserstoffbildung, Verpuffungsgefahr", "Verätzungsgefahr durch Natronlauge"], ["Schutzbrille", "Sicherheitsabstand einhalten", "Abzug/Freiluftversuch"], diff --git a/LehrerApp.Desktop/Services/AiPlanningService.cs b/LehrerApp.Desktop/Services/AiPlanningService.cs index abb331b..1b58ce3 100644 --- a/LehrerApp.Desktop/Services/AiPlanningService.cs +++ b/LehrerApp.Desktop/Services/AiPlanningService.cs @@ -76,6 +76,29 @@ public class AiPlanningService(HttpClient http, ILessonRepository lessons, Converters = { new GermanDateOnlyJsonConverter(), new GermanTimeOnlyJsonConverter() }, }; + /// + /// Baut eine nutzergerichtete Fehlermeldung für eine fehlgeschlagene Backend-Antwort, die + /// weder 401 noch 402 war (die haben eigene, feste Meldungen). Liest den vom Backend + /// gesendeten Klartext-Grund (ai_backend_fail liefert immer {"error": "..."}) + /// mit, statt nur eine pauschale Meldung zu zeigen — ohne das war z.B. beim Fehlschlagen der + /// Websuche in substance.php (falscher Tool-Bezeichner, fehlender Beta-Header o.ä.) der + /// eigentliche Grund nirgends sichtbar, nur "Die Anfrage ist fehlgeschlagen". + /// + private static async Task BuildRequestFailedExceptionAsync(HttpResponseMessage resp) + { + string? backendReason = null; + try + { + var body = await resp.Content.ReadFromJsonAsync(JsonOptions); + backendReason = string.IsNullOrWhiteSpace(body?.Error) ? null : body!.Error; + } + catch { /* Antwortkörper war kein valides {"error": "..."}-JSON - Fallback unten greift. */ } + + return new AiBackendException(backendReason is null + ? "Die Anfrage an den KI-Dienst ist fehlgeschlagen." + : $"Die Anfrage an den KI-Dienst ist fehlgeschlagen: {backendReason}"); + } + public async Task LoginAsync(string username, string password) { HttpResponseMessage resp; @@ -327,7 +350,7 @@ public class AiPlanningService(HttpClient http, ILessonRepository lessons, if (resp.StatusCode == (HttpStatusCode)402) throw new AiBackendException("Nicht genügend KI-Guthaben. Bitte Guthaben aufladen."); if (!resp.IsSuccessStatusCode) - throw new AiBackendException("Die Anfrage an den KI-Dienst ist fehlgeschlagen."); + throw await BuildRequestFailedExceptionAsync(resp); try { @@ -368,7 +391,7 @@ public class AiPlanningService(HttpClient http, ILessonRepository lessons, if (resp.StatusCode == (HttpStatusCode)402) throw new AiBackendException("Nicht genügend KI-Guthaben. Bitte Guthaben aufladen."); if (!resp.IsSuccessStatusCode) - throw new AiBackendException("Die Anfrage an den KI-Dienst ist fehlgeschlagen."); + throw await BuildRequestFailedExceptionAsync(resp); try { @@ -409,7 +432,7 @@ public class AiPlanningService(HttpClient http, ILessonRepository lessons, if (resp.StatusCode == (HttpStatusCode)402) throw new AiBackendException("Nicht genügend KI-Guthaben. Bitte Guthaben aufladen."); if (!resp.IsSuccessStatusCode) - throw new AiBackendException("Die Anfrage an den KI-Dienst ist fehlgeschlagen."); + throw await BuildRequestFailedExceptionAsync(resp); try { @@ -422,6 +445,46 @@ public class AiPlanningService(HttpClient http, ILessonRepository lessons, } } + /// + /// Fragt sicherheitsrelevante Daten zu einer Chemikalie ab (Nutzerwunsch neben 4.2): eigener + /// Endpunkt ai-backend/substance.php, serverseitig dateibasiert gecacht (Name/CAS, + /// Ablauffrist je nach Einstufung) — wiederholte Anfragen für denselben Stoff verursachen + /// nach dem ersten Mal keine weiteren Kosten mehr. Kein Unit-/Guppen-Kontext nötig, nur der + /// eingetippte Stoffname. + /// + public async Task RequestSubstanceResearchAsync(string substanceName, string token) + { + using var req = new HttpRequestMessage(HttpMethod.Post, "substance.php") + { + Content = JsonContent.Create(new { name = substanceName }, options: JsonOptions), + }; + req.Headers.Authorization = new("Bearer", token); + + HttpResponseMessage resp; + try { resp = await http.SendAsync(req); } + catch (HttpRequestException) + { + throw new AiBackendException("Der KI-Dienst ist nicht erreichbar. Bitte Internetverbindung prüfen."); + } + + if (resp.StatusCode == HttpStatusCode.Unauthorized) + throw new AiBackendException("Anmeldung abgelaufen. Bitte in den Einstellungen erneut anmelden."); + if (resp.StatusCode == (HttpStatusCode)402) + throw new AiBackendException("Nicht genügend KI-Guthaben. Bitte Guthaben aufladen."); + if (!resp.IsSuccessStatusCode) + throw await BuildRequestFailedExceptionAsync(resp); + + try + { + var result = await resp.Content.ReadFromJsonAsync(JsonOptions); + return result ?? throw new AiBackendException("Die Antwort der KI konnte nicht verarbeitet werden."); + } + catch (Exception ex) when (ex is not AiBackendException) + { + throw new AiBackendException("Die Antwort der KI konnte nicht verarbeitet werden. Bitte erneut versuchen."); + } + } + /// /// Rein (nur Repository-Lesezugriff für den Alternativpfad-Katalog, kein Schreiben) — testbar /// mit Fakes. Gibt die zu speichernden Lesson-Objekte zurück; der Aufrufer ruft @@ -478,4 +541,5 @@ public class AiPlanningService(HttpClient http, ILessonRepository lessons, private class LoginResult { public string Token { get; set; } = ""; } private class BalanceResult { public decimal BalanceUsd { get; set; } } + private class BackendErrorResult { public string? Error { get; set; } } } diff --git a/LehrerApp.Desktop/Services/PdfExportService.cs b/LehrerApp.Desktop/Services/PdfExportService.cs index 81e6ce0..e271b33 100644 --- a/LehrerApp.Desktop/Services/PdfExportService.cs +++ b/LehrerApp.Desktop/Services/PdfExportService.cs @@ -88,7 +88,8 @@ public sealed record HazardAssessmentPrintData( bool IsAiAssisted); public sealed record HazardSubstancePrintRow( - string Name, string Amount, string Pictograms, string HStatements, string PStatements); + string Name, string Cas, string Amount, string Pictograms, string SignalWord, + string HStatements, string PStatements, string ActivityRestriction); /// /// PDF-Erzeugung über QuestPDF (11.3). Liefert fertige Bytes - der Speicherdialog läuft wie bei @@ -380,7 +381,7 @@ public sealed class PdfExportService if (!string.IsNullOrWhiteSpace(data.Procedure)) column.Item().Element(c => TextSection(c, "Durchführung", data.Procedure)); if (data.Substances.Count > 0) - column.Item().Element(c => SubstanceTable(c, data.Substances)); + column.Item().Element(c => SubstanceBlocks(c, data.Substances)); if (data.Hazards.Count > 0) column.Item().Element(c => BulletSection(c, "Gefährdungen", data.Hazards)); if (data.ProtectiveMeasures.Count > 0) @@ -419,37 +420,43 @@ public sealed class PdfExportService column.Item().Text($"• {item}"); }); - private static void SubstanceTable(IContainer container, IReadOnlyList rows) => + /// Eigener Block je Gefahrstoff statt einer engen Tabelle — bei sieben Datenpunkten pro Stoff + /// (inkl. CAS, Signalwort, Altersbeschränkung) wäre eine Tabelle auf A4-Hochformat zu schmal + /// für lesbare H-/P-Satz-Texte. + private static void SubstanceBlocks(IContainer container, IReadOnlyList rows) => container.Column(column => { + column.Spacing(6); column.Item().Text("Gefahrstoffe").FontSize(11).SemiBold(); - column.Item().Table(table => + foreach (var row in rows) + column.Item().Element(c => SubstanceBlock(c, row)); + }); + + private static void SubstanceBlock(IContainer container, HazardSubstancePrintRow row) => + container.Border(0.8f).BorderColor(Colors.Grey.Lighten2).Padding(6).Column(block => + { + block.Spacing(2); + block.Item().Row(header => { - table.ColumnsDefinition(columns => + header.RelativeItem().Text(text => { - columns.RelativeColumn(2f); - columns.RelativeColumn(1f); - columns.RelativeColumn(1.3f); - columns.RelativeColumn(2f); - columns.RelativeColumn(2f); + text.Span(row.Name).SemiBold(); + if (!string.IsNullOrWhiteSpace(row.Cas)) text.Span($" · CAS {row.Cas}").FontColor(Colors.Grey.Darken1); + if (!string.IsNullOrWhiteSpace(row.Amount)) text.Span($" · {row.Amount}").FontColor(Colors.Grey.Darken1); }); - table.Header(header => - { - header.Cell().Element(HeaderCell).Text("Stoff").SemiBold(); - header.Cell().Element(HeaderCell).Text("Menge").SemiBold(); - header.Cell().Element(HeaderCell).Text("GHS").SemiBold(); - header.Cell().Element(HeaderCell).Text("H-Sätze").SemiBold(); - header.Cell().Element(HeaderCell).Text("P-Sätze").SemiBold(); - }); - foreach (var row in rows) - { - table.Cell().Element(BodyCell).Text(row.Name); - table.Cell().Element(BodyCell).Text(row.Amount); - table.Cell().Element(BodyCell).Text(row.Pictograms); - table.Cell().Element(BodyCell).Text(row.HStatements); - table.Cell().Element(BodyCell).Text(row.PStatements); - } + if (!string.IsNullOrWhiteSpace(row.SignalWord)) + header.ConstantItem(70).AlignRight().Text(row.SignalWord).SemiBold() + .FontColor(Colors.Red.Darken2); }); + if (!string.IsNullOrWhiteSpace(row.Pictograms)) + block.Item().Text($"GHS: {row.Pictograms}").FontSize(8).FontColor(Colors.Grey.Darken1); + if (!string.IsNullOrWhiteSpace(row.HStatements)) + block.Item().Text($"H-Sätze: {row.HStatements}"); + if (!string.IsNullOrWhiteSpace(row.PStatements)) + block.Item().Text($"P-Sätze: {row.PStatements}"); + if (!string.IsNullOrWhiteSpace(row.ActivityRestriction)) + block.Item().Text($"Tätigkeitsbeschränkung: {row.ActivityRestriction}").SemiBold() + .FontColor(Colors.Orange.Darken3); }); /// Horizontaler Balken, 0-100 % der verfügbaren Zellenbreite (max. 150pt). diff --git a/LehrerApp.Desktop/ViewModels/Groups/HazardAssessmentWizardViewModel.cs b/LehrerApp.Desktop/ViewModels/Groups/HazardAssessmentWizardViewModel.cs index b00c11b..6b83c59 100644 --- a/LehrerApp.Desktop/ViewModels/Groups/HazardAssessmentWizardViewModel.cs +++ b/LehrerApp.Desktop/ViewModels/Groups/HazardAssessmentWizardViewModel.cs @@ -80,32 +80,81 @@ public partial class GhsPictogramOption(GhsPictogram value, string label) : Obse public partial class HazardSubstanceEditItem : ObservableObject { [ObservableProperty] private string _name = ""; + [ObservableProperty] private string _cas = ""; [ObservableProperty] private string _amount = ""; + [ObservableProperty] private string _signalWord = ""; [ObservableProperty] private string _hStatements = ""; [ObservableProperty] private string _pStatements = ""; + [ObservableProperty] private string _activityRestriction = ""; + [ObservableProperty] private bool _isResearching; + [ObservableProperty] private string _researchError = ""; public ObservableCollection PictogramOptions { get; } public Action? OnRemove { get; set; } + /// Vom Wizard gesetzt (nur wenn eine KI-Backend-Anmeldung vorliegt, siehe CanUseAi — der + /// Lookup selbst ruft aber keine KI auf, sondern nur die lokale RiSU-Stoffliste auf dem + /// Server, siehe ai-backend/substance.php): führt die eigentliche Chemikalien-Recherche aus. + /// Wird vor der Anzeige in der UI (Objektinitialisierer in NewSubstanceItem) endgültig + /// gesetzt, daher genügt eine einmalig berechnete Eigenschaft ohne Änderungsbenachrichtigung. + public Func>? OnResearch { get; set; } + public bool CanResearch => OnResearch is not null; + public HazardSubstanceEditItem(HazardSubstance? source = null) { PictogramOptions = new ObservableCollection( GhsPictogramDisplay.BuildOptions(source?.GhsPictograms)); if (source is null) return; Name = source.Name; + Cas = source.Cas; Amount = source.Amount; + SignalWord = source.SignalWord; HStatements = source.HStatements; PStatements = source.PStatements; + ActivityRestriction = source.ActivityRestriction; } [RelayCommand] private void Remove() => OnRemove?.Invoke(this); + [RelayCommand] + private async Task Research() + { + if (OnResearch is null) return; + if (string.IsNullOrWhiteSpace(Name)) { ResearchError = "Stoffname eingeben."; return; } + + ResearchError = ""; IsResearching = true; + try + { + var result = await OnResearch(Name.Trim()); + Name = result.Name; + Cas = result.Cas; + SignalWord = result.SignalWord; + HStatements = result.HStatements; + PStatements = result.PStatements; + ActivityRestriction = result.ActivityRestriction; + var pictograms = result.GhsPictograms + .Select(GhsPictogramDisplay.FromCode).Where(p => p.HasValue).Select(p => p!.Value).ToHashSet(); + foreach (var option in PictogramOptions) option.IsSelected = pictograms.Contains(option.Value); + } + catch (AiBackendException ex) + { + ResearchError = ex.Message; + } + finally + { + IsResearching = false; + } + } + public HazardSubstance ToModel() => new() { Name = Name.Trim(), + Cas = Cas.Trim(), Amount = Amount.Trim(), + SignalWord = SignalWord.Trim(), HStatements = HStatements.Trim(), PStatements = PStatements.Trim(), + ActivityRestriction = ActivityRestriction.Trim(), GhsPictograms = PictogramOptions.Where(o => o.IsSelected).Select(o => o.Value).ToList(), }; } @@ -217,10 +266,25 @@ public partial class HazardAssessmentWizardViewModel : ObservableObject private HazardSubstanceEditItem NewSubstanceItem(HazardSubstance? source = null) { - var item = new HazardSubstanceEditItem(source) { OnRemove = RemoveSubstance }; + var item = new HazardSubstanceEditItem(source) + { + OnRemove = RemoveSubstance, + OnResearch = CanUseAi ? ResearchSubstanceAsync : null, + }; return item; } + /// Führt die eigentliche Chemikalien-Recherche aus — ein reiner Datenbank-Lookup gegen die + /// lokale RiSU-Stoffliste auf dem Server (ai-backend/stoffliste.php), keine KI beteiligt und + /// damit auch kein IsAiAssisted-Hinweis nötig. Die Anmeldung läuft trotzdem über dieselbe + /// KI-Backend-Sitzung wie die echten KI-Funktionen (gemeinsamer Host/Token). + private async Task ResearchSubstanceAsync(string substanceName) + { + var token = _aiSettings!.GetToken() + ?? throw new AiBackendException("Nicht angemeldet. Bitte in den Einstellungen bei der KI-Unterstützung anmelden."); + return await _ai!.RequestSubstanceResearchAsync(substanceName, token); + } + [RelayCommand] private void AddSubstance() => Substances.Add(NewSubstanceItem()); diff --git a/LehrerApp.Desktop/Views/Groups/HazardAssessmentWizardDialog.axaml b/LehrerApp.Desktop/Views/Groups/HazardAssessmentWizardDialog.axaml index d36742f..e5cb1d8 100644 --- a/LehrerApp.Desktop/Views/Groups/HazardAssessmentWizardDialog.axaml +++ b/LehrerApp.Desktop/Views/Groups/HazardAssessmentWizardDialog.axaml @@ -81,14 +81,32 @@ - + - +