- @foreach (var segment in renderPlan.Segments)
- {
- var segmentContent = segment.GetContent(renderPlan.Source);
- if (segment.Type is MarkdownRenderSegmentType.MARKDOWN)
+
+ @{ var renderPlan = this.GetMarkdownRenderPlan(textContent.Text); }
+ @foreach (var segment in renderPlan.Segments)
{
-
-
-
-
-
-
-
+ var segmentContent = segment.GetContent(renderPlan.Source);
+ if (segment.Type is MarkdownRenderSegmentType.MARKDOWN)
+ {
+
+
+
+
+
+
+
+ }
+ else
+ {
+
+ }
}
- else
- {
-
- }
- }
+
@if (textContent.Sources.Count > 0)
{
diff --git a/app/MindWork AI Studio/Chat/ContentBlockComponent.razor.cs b/app/MindWork AI Studio/Chat/ContentBlockComponent.razor.cs
index f0d9195de..27dca169b 100644
--- a/app/MindWork AI Studio/Chat/ContentBlockComponent.razor.cs
+++ b/app/MindWork AI Studio/Chat/ContentBlockComponent.razor.cs
@@ -430,6 +430,8 @@ public partial class ContentBlockComponent : MSGComponentBase
hash.Add(text.Sources.Count);
hash.Add(text.SourcePassages.Count);
hash.Add(text.ToolInvocations.Count);
+ hash.Add(text.Presentation?.Kind);
+ hash.Add(text.Presentation?.GetDataHashCode() ?? 0);
hash.Add(text.ToolRuntimeStatus.IsRunning);
hash.Add(text.ToolRuntimeStatus.Message);
hash.Add(this.showToolTrace);
diff --git a/app/MindWork AI Studio/Chat/ContentPresentation.cs b/app/MindWork AI Studio/Chat/ContentPresentation.cs
new file mode 100644
index 000000000..eca78320b
--- /dev/null
+++ b/app/MindWork AI Studio/Chat/ContentPresentation.cs
@@ -0,0 +1,58 @@
+using System.Text.Json;
+
+namespace AIStudio.Chat;
+
+///
+/// Structured data which a renderer may use instead of the canonical text of a message.
+///
+///
+/// The text stays canonical: providers, copying, exporting, and every renderer which does not know
+/// the presentation use it. ContentPresentationView shows the presentation instead, and falls back to
+/// the text when its data cannot be read, e.g., from a chat file which was changed by hand.
+///
+public sealed class ContentPresentation
+{
+ private JsonElement? data;
+ private int? dataHashCode;
+
+ ///
+ /// The renderer which understands the data.
+ ///
+ public ContentPresentationKind Kind { get; set; } = ContentPresentationKind.NONE;
+
+ ///
+ /// The renderer-specific data.
+ ///
+ public JsonElement? Data
+ {
+ get => this.data;
+ set
+ {
+ this.data = value;
+ this.dataHashCode = null;
+ }
+ }
+
+ internal static ContentPresentation Create
(ContentPresentationKind kind, T data) => new()
+ {
+ Kind = kind,
+ Data = JsonSerializer.SerializeToElement(data),
+ };
+
+ internal ContentPresentation DeepClone() => new()
+ {
+ Kind = this.Kind,
+ Data = this.Data?.Clone(),
+ };
+
+ ///
+ /// A hash code of the data, for telling whether it changed.
+ ///
+ ///
+ /// Computed once per data, since every render of a message asks for it and computing means
+ /// writing out the whole data.
+ ///
+ internal int GetDataHashCode() => this.dataHashCode ??= this.Data is not { ValueKind: not JsonValueKind.Undefined } value
+ ? 0
+ : value.GetRawText().GetHashCode(StringComparison.Ordinal);
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Chat/ContentPresentationKind.cs b/app/MindWork AI Studio/Chat/ContentPresentationKind.cs
new file mode 100644
index 000000000..787e18210
--- /dev/null
+++ b/app/MindWork AI Studio/Chat/ContentPresentationKind.cs
@@ -0,0 +1,10 @@
+namespace AIStudio.Chat;
+
+///
+/// Identifies an optional specialized presentation of text content.
+///
+public enum ContentPresentationKind
+{
+ NONE,
+ SWOT_ANALYSIS,
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Chat/ContentPresentationView.razor b/app/MindWork AI Studio/Chat/ContentPresentationView.razor
new file mode 100644
index 000000000..15b4d56c8
--- /dev/null
+++ b/app/MindWork AI Studio/Chat/ContentPresentationView.razor
@@ -0,0 +1,13 @@
+@namespace AIStudio.Chat
+@using AIStudio.Assistants.SWOTAnalysis
+
+@switch (this.data)
+{
+ case SwotAnalysisResult swotAnalysisResult:
+
+ break;
+
+ default:
+ @this.ChildContent
+ break;
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Chat/ContentPresentationView.razor.cs b/app/MindWork AI Studio/Chat/ContentPresentationView.razor.cs
new file mode 100644
index 000000000..fdc557e8e
--- /dev/null
+++ b/app/MindWork AI Studio/Chat/ContentPresentationView.razor.cs
@@ -0,0 +1,63 @@
+using AIStudio.Assistants.SWOTAnalysis;
+
+using Microsoft.AspNetCore.Components;
+
+namespace AIStudio.Chat;
+
+///
+/// Shows the structured presentation of a text, or the text itself when it has none.
+///
+///
+/// The one place which knows every kind of presentation and the view which shows it. Chats and
+/// assistants render each text through this component, so a new kind needs a case here and in
+/// Read, and nothing else in the chat.
+///
+public partial class ContentPresentationView
+{
+ ///
+ /// The presentation of the text, if it has one.
+ ///
+ [Parameter]
+ public ContentPresentation? Presentation { get; set; }
+
+ ///
+ /// How the text looks without a presentation, and when its data cannot be read.
+ ///
+ [Parameter]
+ public RenderFragment? ChildContent { get; set; }
+
+ private ContentPresentation? readPresentation;
+ private int readDataHashCode;
+ private object? data;
+
+ #region Overrides of ComponentBase
+
+ protected override void OnParametersSet()
+ {
+ //
+ // Reading the data means deserializing it, so only a new presentation or changed data are
+ // read again; a parent renders far more often than either happens:
+ //
+ var dataHashCode = this.Presentation?.GetDataHashCode() ?? 0;
+ if (ReferenceEquals(this.Presentation, this.readPresentation) && dataHashCode == this.readDataHashCode)
+ return;
+
+ this.readPresentation = this.Presentation;
+ this.readDataHashCode = dataHashCode;
+ this.data = Read(this.Presentation);
+ }
+
+ #endregion
+
+ ///
+ /// Reads the data of a presentation into the type its view shows.
+ ///
+ /// The presentation to read.
+ /// The data, or null when there is no presentation or its data cannot be read.
+ private static object? Read(ContentPresentation? presentation) => presentation?.Kind switch
+ {
+ ContentPresentationKind.SWOT_ANALYSIS => SwotAnalysisContentPresentation.TryRead(presentation, out var result) ? result : null,
+
+ _ => null,
+ };
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Chat/ContentText.cs b/app/MindWork AI Studio/Chat/ContentText.cs
index 5d4fe834e..297c7ae5f 100644
--- a/app/MindWork AI Studio/Chat/ContentText.cs
+++ b/app/MindWork AI Studio/Chat/ContentText.cs
@@ -57,6 +57,16 @@ public sealed class ContentText : IContent
public List ToolInvocations { get; set; } = [];
+ ///
+ /// Optional structured data for a specialized visual presentation of this text.
+ ///
+ ///
+ /// The text remains the canonical content sent to providers, copied, and exported. A renderer
+ /// which understands the presentation may use its data instead, while every other consumer
+ /// safely continues to use the text.
+ ///
+ public ContentPresentation? Presentation { get; set; }
+
///
/// The passages the data sources handed to the AI for this answer, as the AI received them.
///
@@ -540,6 +550,7 @@ public sealed class ContentText : IContent
Sources = [..this.Sources],
SourcePassages = [..this.SourcePassages],
FileAttachments = [..this.FileAttachments],
+ Presentation = this.Presentation?.DeepClone(),
ToolInvocations = [..this.ToolInvocations.Select(x => new ToolInvocationTrace
{
Order = x.Order,
diff --git a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogSWOTAnalysis.razor b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogSWOTAnalysis.razor
new file mode 100644
index 000000000..2a528d03d
--- /dev/null
+++ b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogSWOTAnalysis.razor
@@ -0,0 +1,33 @@
+@using AIStudio.Settings
+@inherits SettingsDialogBase
+
+
+
+
+
+ @T("Assistant: SWOT Analysis Options")
+
+
+
+
+
+
+
+
+
+ @if (this.SettingsManager.ConfigurationData.SWOTAnalysis.PreselectedTargetLanguage is CommonLanguages.OTHER)
+ {
+
+ }
+
+
+
+
+
+
+
+
+ @T("Close")
+
+
+
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogSWOTAnalysis.razor.cs b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogSWOTAnalysis.razor.cs
new file mode 100644
index 000000000..4d3a8702d
--- /dev/null
+++ b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogSWOTAnalysis.razor.cs
@@ -0,0 +1,3 @@
+namespace AIStudio.Dialogs.Settings;
+
+public partial class SettingsDialogSWOTAnalysis : SettingsDialogBase;
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Pages/Assistants.razor b/app/MindWork AI Studio/Pages/Assistants.razor
index 5a34b8c8f..bb8209109 100644
--- a/app/MindWork AI Studio/Pages/Assistants.razor
+++ b/app/MindWork AI Studio/Pages/Assistants.razor
@@ -56,6 +56,7 @@
+
diff --git a/app/MindWork AI Studio/Pages/Supporters.razor b/app/MindWork AI Studio/Pages/Supporters.razor
index 7a50d02a2..6c67c036f 100644
--- a/app/MindWork AI Studio/Pages/Supporters.razor
+++ b/app/MindWork AI Studio/Pages/Supporters.razor
@@ -90,7 +90,7 @@
-
+
diff --git a/app/MindWork AI Studio/Plugins/configuration/plugin.lua b/app/MindWork AI Studio/Plugins/configuration/plugin.lua
index 2ea1032a6..cd95932cf 100644
--- a/app/MindWork AI Studio/Plugins/configuration/plugin.lua
+++ b/app/MindWork AI Studio/Plugins/configuration/plugin.lua
@@ -904,7 +904,8 @@ CONFIG["SETTINGS"] = {}
-- Allowed values are:
-- GRAMMAR_SPELLING_ASSISTANT, ICON_FINDER_ASSISTANT, REWRITE_ASSISTANT,
-- PROMPT_OPTIMIZER_ASSISTANT, TRANSLATION_ASSISTANT, AGENDA_ASSISTANT,
--- CODING_ASSISTANT, TEXT_SUMMARIZER_ASSISTANT, EMAIL_ASSISTANT,
+-- CODING_ASSISTANT, TEXT_SUMMARIZER_ASSISTANT, SWOT_ANALYSIS_ASSISTANT,
+-- EMAIL_ASSISTANT,
-- LEGAL_CHECK_ASSISTANT, SYNONYMS_ASSISTANT, MY_TASKS_ASSISTANT,
-- JOB_POSTING_ASSISTANT, BIAS_DAY_ASSISTANT, ERI_ASSISTANT,
-- DOCUMENT_ANALYSIS_ASSISTANT, BATCH_PROCESSING_ASSISTANT, SLIDE_BUILDER_ASSISTANT,
diff --git a/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua
index 884c362f1..93834e162 100644
--- a/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua
+++ b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua
@@ -2856,6 +2856,126 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::TEMPLATESLIDEUSEEXTENSIONS:
-- Keep unchanged
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::TEMPLATESLIDEUSEEXTENSIONS::T2931681185"] = "Unverändert übernehmen"
+-- Create a SWOT analysis of my content
+UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SWOTANALYSIS::ASSISTANTSWOTANALYSIS::T1113304215"] = "Erstelle eine SWOT-Analyse meiner Inhalte."
+
+-- The answer for this category did not have the expected form, even after asking the model once more.
+UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SWOTANALYSIS::ASSISTANTSWOTANALYSIS::T1151444865"] = "Die Antwort für diese Kategorie entsprach auch nach einer erneuten Anfrage an das Modell nicht dem erwarteten Format."
+
+-- This assistant serves as a strategic planning tool that systematically captures strengths, weaknesses, opportunities, and threats. It can assist in positioning and strategy development for companies, organizations, or individuals. The AI analyzes the four categories at the same time, then reconciles them and writes an overall assessment with prioritized actions.
+UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SWOTANALYSIS::ASSISTANTSWOTANALYSIS::T1281456376"] = "Dieser Assistent unterstützt die strategische Planung, indem er systematisch Stärken, Schwächen, Chancen und Risiken erfasst. Er hilft Unternehmen, Organisationen oder Einzelpersonen bei der Positionierung und Strategieentwicklung. Die KI analysiert die vier Kategorien gleichzeitig, gleicht sie anschließend miteinander ab und schreibt eine Gesamtbewertung mit priorisierten Maßnahmen."
+
+-- The overall assessment did not have the expected form. Asking the model once more...
+UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SWOTANALYSIS::ASSISTANTSWOTANALYSIS::T1374582329"] = "Die Gesamtbewertung entsprach nicht dem erwarteten Format. Das Modell wird erneut gefragt …"
+
+-- Reconciling the four SWOT categories and writing the overall assessment...
+UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SWOTANALYSIS::ASSISTANTSWOTANALYSIS::T1491038257"] = "Die vier SWOT-Kategorien werden abgeglichen und die Gesamtbewertung wird geschrieben …"
+
+-- Researching with the selected tools...
+UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SWOTANALYSIS::ASSISTANTSWOTANALYSIS::T1849317035"] = "Recherche mit den ausgewählten Werkzeugen läuft …"
+
+-- SWOT Analysis
+UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SWOTANALYSIS::ASSISTANTSWOTANALYSIS::T1864542518"] = "SWOT-Analyse"
+
+-- Please describe the goal the SWOT analysis should support.
+UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SWOTANALYSIS::ASSISTANTSWOTANALYSIS::T2136978542"] = "Bitte beschreiben Sie das Ziel, das die SWOT-Analyse unterstützen soll."
+
+-- Target language
+UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SWOTANALYSIS::ASSISTANTSWOTANALYSIS::T237828418"] = "Zielsprache"
+
+-- Input
+UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SWOTANALYSIS::ASSISTANTSWOTANALYSIS::T2677268763"] = "Eingabe"
+
+-- Analysis goal
+UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SWOTANALYSIS::ASSISTANTSWOTANALYSIS::T2718020690"] = "Analyseziel"
+
+-- This category could not be created.
+UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SWOTANALYSIS::ASSISTANTSWOTANALYSIS::T2855208826"] = "Diese Kategorie konnte nicht erstellt werden."
+
+-- At least one SWOT category could not be created. The completed categories remain visible below.
+UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SWOTANALYSIS::ASSISTANTSWOTANALYSIS::T3160246784"] = "Mindestens eine SWOT-Kategorie konnte nicht erstellt werden. Die abgeschlossenen Kategorien bleiben unten sichtbar."
+
+-- The selected model cannot read images. The SWOT analysis continues without the attached images.
+UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SWOTANALYSIS::ASSISTANTSWOTANALYSIS::T3248926642"] = "Das ausgewählte Modell kann keine Bilder verarbeiten. Die SWOT-Analyse wird ohne die angehängten Bilder fortgesetzt."
+
+-- To enhance the validity of the SWOT analysis, please upload supplementary documents that provide empirical or contextual data for a comprehensive internal and external assessment.
+UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SWOTANALYSIS::ASSISTANTSWOTANALYSIS::T3485401617"] = "Um die Aussagekraft der SWOT-Analyse zu erhöhen, hängen Sie bitte ergänzende Dokumente an, die empirische oder kontextbezogene Daten für eine umfassende interne und externe Bewertung liefern."
+
+-- Create SWOT analysis
+UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SWOTANALYSIS::ASSISTANTSWOTANALYSIS::T3549988380"] = "SWOT-Analyse erstellen"
+
+-- Please provide source material for the SWOT analysis. You can enter text, load a document, or import content from a website.
+UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SWOTANALYSIS::ASSISTANTSWOTANALYSIS::T381075478"] = "Bitte stellen Sie Quellmaterial für die SWOT-Analyse bereit. Sie können Text eingeben, ein Dokument laden oder Inhalte von einer Website importieren."
+
+-- Custom target language
+UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SWOTANALYSIS::ASSISTANTSWOTANALYSIS::T3848935911"] = "Benutzerdefinierte Zielsprache"
+
+-- Specify topics the SWOT analysis should pay particular attention to.
+UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SWOTANALYSIS::ASSISTANTSWOTANALYSIS::T3857591121"] = "Geben Sie die Themen an, auf die die SWOT-Analyse besonders achten soll."
+
+-- Analysis Options
+UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SWOTANALYSIS::ASSISTANTSWOTANALYSIS::T3882472641"] = "Analyseoptionen"
+
+-- The image '{0}' could not be read and is left out of the SWOT analysis.
+UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SWOTANALYSIS::ASSISTANTSWOTANALYSIS::T4112888115"] = "Das Bild „{0}“ konnte nicht gelesen werden und wird bei der SWOT-Analyse nicht berücksichtigt."
+
+-- The research with the selected tools failed. The SWOT analysis continues without it.
+UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SWOTANALYSIS::ASSISTANTSWOTANALYSIS::T445799638"] = "Die Recherche mit den ausgewählten Werkzeugen ist fehlgeschlagen. Die SWOT-Analyse wird ohne diese Recherche fortgesetzt."
+
+-- The category analysis was canceled.
+UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SWOTANALYSIS::ASSISTANTSWOTANALYSIS::T476307744"] = "Die Analyse dieser Kategorie wurde abgebrochen."
+
+-- The quality of this SWOT analysis depends on the material you provide. The AI uses only your input, the attached documents, and the results of the tools you selected. Treat the result as a first draft and check it before you base decisions on it.
+UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SWOTANALYSIS::ASSISTANTSWOTANALYSIS::T503683591"] = "Die Qualität dieser SWOT-Analyse hängt von dem Material ab, das Sie bereitstellen. Die KI verwendet ausschließlich Ihre Eingaben, die angehängten Dokumente und die Ergebnisse der ausgewählten Werkzeuge. Betrachten Sie das Ergebnis als ersten Entwurf und prüfen Sie es, bevor Sie Entscheidungen darauf stützen."
+
+-- (Optional) Analysis focus
+UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SWOTANALYSIS::ASSISTANTSWOTANALYSIS::T578386116"] = "(Optional) Analysefokus"
+
+-- Please provide a custom language.
+UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SWOTANALYSIS::ASSISTANTSWOTANALYSIS::T656744944"] = "Bitte geben Sie eine benutzerdefinierte Sprache an."
+
+-- Contextual Knowledge
+UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SWOTANALYSIS::ASSISTANTSWOTANALYSIS::T701558710"] = "Kontextwissen"
+
+-- Subject of analysis
+UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SWOTANALYSIS::ASSISTANTSWOTANALYSIS::T871926658"] = "Gegenstand der Analyse"
+
+-- Analyzing the four SWOT categories...
+UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SWOTANALYSIS::ASSISTANTSWOTANALYSIS::T981239180"] = "Die vier SWOT-Kategorien werden analysiert …"
+
+-- The four SWOT categories could not be reconciled because the answer did not have the expected form, even after asking the model once more. The preliminary results of the categories remain visible below.
+UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SWOTANALYSIS::ASSISTANTSWOTANALYSIS::T982373207"] = "Die vier SWOT-Kategorien konnten nicht miteinander abgeglichen werden, da die Antwort auch nach einer erneuten Anfrage an das Modell nicht das erwartete Format hatte. Die vorläufigen Ergebnisse der Kategorien sind unten weiterhin sichtbar."
+
+-- The answer did not have the expected form. Asking the model once more...
+UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SWOTANALYSIS::SWOTANALYSISPROGRESSVIEW::T1020899874"] = "Die Antwort hatte nicht das erwartete Format. Das Modell wird erneut gefragt …"
+
+-- Strengths
+UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SWOTANALYSIS::SWOTANALYSISPROGRESSVIEW::T1245493657"] = "Stärken"
+
+-- Negative
+UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SWOTANALYSIS::SWOTANALYSISPROGRESSVIEW::T190369114"] = "Negativ"
+
+-- Weaknesses
+UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SWOTANALYSIS::SWOTANALYSISPROGRESSVIEW::T2279747526"] = "Schwächen"
+
+-- Positive
+UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SWOTANALYSIS::SWOTANALYSISPROGRESSVIEW::T230444442"] = "Positiv"
+
+-- Opportunities
+UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SWOTANALYSIS::SWOTANALYSISPROGRESSVIEW::T2409493450"] = "Chancen"
+
+-- Internal
+UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SWOTANALYSIS::SWOTANALYSISPROGRESSVIEW::T2805904416"] = "Intern"
+
+-- Analyzing this category...
+UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SWOTANALYSIS::SWOTANALYSISPROGRESSVIEW::T3114892934"] = "Diese Kategorie wird analysiert …"
+
+-- Threats
+UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SWOTANALYSIS::SWOTANALYSISPROGRESSVIEW::T592141878"] = "Risiken"
+
+-- External
+UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SWOTANALYSIS::SWOTANALYSISPROGRESSVIEW::T697638314"] = "Extern"
+
-- Find synonyms for the following word or phrase:
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SYNONYM::ASSISTANTSYNONYMS::T1793532807"] = "Finde Synonyme für das folgende Wort oder die folgende Phrase:"
@@ -10086,6 +10206,75 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T97357
-- A slide holds at most this many bullet points, in all of its text slots together. Match it to the room for text in the layouts of your template.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T995644230"] = "Eine Folie enthält höchstens so viele Aufzählungspunkte, in allen ihren Textfeldern zusammen. Stimmen Sie den Wert auf den Platz für Text in den Layouts Ihrer Vorlage ab."
+-- Content cleaner agent is preselected
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSWOTANALYSIS::T1013787967"] = "Agent zur Inhaltsbereinigung ist vorausgewählt"
+
+-- Web content reader is shown
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSWOTANALYSIS::T1030372436"] = "Der Web-Content-Reader zum Lesen von Webinhalten wird angezeigt"
+
+-- Preselect the target language
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSWOTANALYSIS::T1417990312"] = "Zielsprache vorauswählen"
+
+-- Preselect another target language
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSWOTANALYSIS::T1462295644"] = "Eine andere Zielsprache vorauswählen"
+
+-- When activated, the web content reader is hidden and cannot be used. As a result, the user interface becomes a bit easier to use.
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSWOTANALYSIS::T1633101895"] = "Wenn aktiviert, wird der Web-Content-Reader ausgeblendet und kann nicht verwendet werden. Dadurch wird die Benutzeroberfläche etwas einfacher zu bedienen."
+
+-- Web content reader is not preselected
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSWOTANALYSIS::T1701127912"] = "Der Web-Content-Reader zum Lesen von Webinhalten ist nicht vorausgewählt"
+
+-- Content cleaner agent is not preselected
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSWOTANALYSIS::T1969816694"] = "Agent zur Inhaltsbereinigung ist nicht vorausgewählt"
+
+-- Hide the web content reader?
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSWOTANALYSIS::T2090693677"] = "Web-Content-Reader ausblenden?"
+
+-- SWOT analysis options are preselected
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSWOTANALYSIS::T2171839562"] = "Optionen für die SWOT-Analyse sind vorausgewählt"
+
+-- No SWOT analysis options are preselected
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSWOTANALYSIS::T2361666775"] = "Keine Optionen für die SWOT-Analyse sind vorausgewählt"
+
+-- Preselect SWOT analysis options?
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSWOTANALYSIS::T2593950994"] = "Optionen für die SWOT-Analyse vorauswählen?"
+
+-- Web content reader is hidden
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSWOTANALYSIS::T2799795311"] = "Der Web-Content-Reader zum Lesen von Webinhalten ist ausgeblendet"
+
+-- When enabled, the web content reader is preselected. This is useful when you often analyze content from websites.
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSWOTANALYSIS::T3088856907"] = "Wenn aktiviert, ist der Web-Content-Reader vorausgewählt. Das kann hilfreich sein, wenn Sie häufig Inhalte von Websites analysieren."
+
+-- When enabled, the content cleaner agent is preselected for cleaning website content before it is analyzed.
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSWOTANALYSIS::T332064979"] = "Wenn aktiviert, ist der Agent zur Inhaltsbereinigung vorausgewählt, um Webinhalte vor der Analyse zu bereinigen."
+
+-- Close
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSWOTANALYSIS::T3448155331"] = "Schließen"
+
+-- Which target language should be preselected?
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSWOTANALYSIS::T3547337928"] = "Welche Zielsprache soll vorausgewählt werden?"
+
+-- Web content reader is preselected
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSWOTANALYSIS::T3641773985"] = "Der Web-Content-Reader zum Lesen von Webinhalten ist vorausgewählt"
+
+-- Preselect the content cleaner agent?
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSWOTANALYSIS::T3649428096"] = "Agent zur Inhaltsbereinigung vorauswählen?"
+
+-- Specify topics the SWOT analysis should pay particular attention to.
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSWOTANALYSIS::T3857591121"] = "Geben Sie die Themen an, auf die die SWOT-Analyse besonders achten soll."
+
+-- Assistant: SWOT Analysis Options
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSWOTANALYSIS::T4267340780"] = "Assistent: Optionen für die SWOT-Analyse"
+
+-- Preselect the web content reader?
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSWOTANALYSIS::T629158142"] = "Den Web-Content-Reader zum Lesen von Webinhalten vorauswählen?"
+
+-- Preselect an analysis focus
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSWOTANALYSIS::T656094797"] = "Analysefokus vorauswählen"
+
+-- When enabled, you can preselect the SWOT analysis options. This is useful when you prefer a specific language, analysis focus, or LLM.
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSWOTANALYSIS::T975761680"] = "Wenn aktiviert, können Sie die Optionen für die SWOT-Analyse vorauswählen. Das kann nützlich sein, wenn Sie eine bestimmte Sprache, einen bestimmten Analysefokus oder ein bestimmtes LLM bevorzugen."
+
-- When enabled, you can preselect synonym options. This is might be useful when you prefer a specific language or LLM model.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSYNONYMS::T183953912"] = "Wenn diese Option aktiviert ist, können Sie Synonymoptionen im Voraus auswählen. Dies kann nützlich sein, wenn Sie eine bestimmte Sprache oder ein bestimmtes LLM-Modell bevorzugen."
@@ -10752,6 +10941,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T131837803"] = "Business"
-- Legal Check
UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T1348190638"] = "Rechtliche Prüfung"
+-- Identify strengths, weaknesses, opportunities, threats, and practical next steps from provided information.
+UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T1356376259"] = "Stärken, Schwächen, Chancen, Risiken und praktische nächste Schritte aus den bereitgestellten Informationen identifizieren."
+
-- General
UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T1432485131"] = "Allgemein"
@@ -10773,6 +10965,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T1728590051"] = "Analysieren Sie e
-- Prompt Optimizer
UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T1777666968"] = "Prompt-Optimierer"
+-- SWOT Analysis
+UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T1864542518"] = "SWOT-Analyse"
+
-- Slide Assistant
UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T1883918574"] = "Folien-Assistent"
@@ -11946,9 +12141,6 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::SUPPORTERS::T2640760894"] = "Werden Sie einer
-- Supporters
UI_TEXT_CONTENT["AISTUDIO::PAGES::SUPPORTERS::T2929332068"] = "Unterstützer"
--- Thank you, Sabrina, for bringing file attachments to the chat, the Slide Assistant, and the protection against prompt injection.
-UI_TEXT_CONTENT["AISTUDIO::PAGES::SUPPORTERS::T2978040576"] = "Vielen Dank, Sabrina, dass du den Chat um Dateianhänge erweitert und den Folien-Assistenten sowie den Schutz vor Prompt-Injection entwickelt hast."
-
-- Content Contributors
UI_TEXT_CONTENT["AISTUDIO::PAGES::SUPPORTERS::T3060804484"] = "Inhaltliche Beiträge"
@@ -11958,6 +12150,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::SUPPORTERS::T3061261435"] = "Finanzielle Unter
-- Thank you, Prodman, for bringing LiteLLM to AI Studio as a new provider.
UI_TEXT_CONTENT["AISTUDIO::PAGES::SUPPORTERS::T3226413110"] = "Vielen Dank, Prodman, dass du LiteLLM als neuen Anbieter in AI Studio integriert hast."
+-- Thank you, Sabrina, for bringing file attachments to the chat, for the Slide Assistant and the SWOT Analysis Assistant, and for the protection against prompt injection.
+UI_TEXT_CONTENT["AISTUDIO::PAGES::SUPPORTERS::T32630309"] = "Vielen Dank, Sabrina, dass du den Chat um Dateianhänge erweitert, den Folien-Assistenten und den SWOT-Analyse-Assistenten entwickelt und den Schutz vor Prompt-Injection eingebaut hast."
+
-- German Aerospace Center (DLR)
UI_TEXT_CONTENT["AISTUDIO::PAGES::SUPPORTERS::T3264998714"] = "Deutsches Zentrum für Luft- und Raumfahrt (DLR)"
@@ -12762,6 +12957,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::COMPONENTSEXTENSIONS::T1993795352"] = "Prompt-
-- Job Posting Assistant
UI_TEXT_CONTENT["AISTUDIO::TOOLS::COMPONENTSEXTENSIONS::T2212811874"] = "Stellenanzeigen-Assistent"
+-- SWOT Analysis Assistant
+UI_TEXT_CONTENT["AISTUDIO::TOOLS::COMPONENTSEXTENSIONS::T2431303678"] = "SWOT-Analyse-Assistent"
+
-- Icon Finder Assistant
UI_TEXT_CONTENT["AISTUDIO::TOOLS::COMPONENTSEXTENSIONS::T2457005512"] = "Icon Finder-Assistent"
diff --git a/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua b/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua
index d8216bb7a..adc7118d1 100644
--- a/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua
+++ b/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua
@@ -2856,6 +2856,126 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::TEMPLATESLIDEUSEEXTENSIONS:
-- Keep unchanged
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::TEMPLATESLIDEUSEEXTENSIONS::T2931681185"] = "Keep unchanged"
+-- Create a SWOT analysis of my content
+UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SWOTANALYSIS::ASSISTANTSWOTANALYSIS::T1113304215"] = "Create a SWOT analysis of my content"
+
+-- The answer for this category did not have the expected form, even after asking the model once more.
+UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SWOTANALYSIS::ASSISTANTSWOTANALYSIS::T1151444865"] = "The answer for this category did not have the expected form, even after asking the model once more."
+
+-- This assistant serves as a strategic planning tool that systematically captures strengths, weaknesses, opportunities, and threats. It can assist in positioning and strategy development for companies, organizations, or individuals. The AI analyzes the four categories at the same time, then reconciles them and writes an overall assessment with prioritized actions.
+UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SWOTANALYSIS::ASSISTANTSWOTANALYSIS::T1281456376"] = "This assistant serves as a strategic planning tool that systematically captures strengths, weaknesses, opportunities, and threats. It can assist in positioning and strategy development for companies, organizations, or individuals. The AI analyzes the four categories at the same time, then reconciles them and writes an overall assessment with prioritized actions."
+
+-- The overall assessment did not have the expected form. Asking the model once more...
+UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SWOTANALYSIS::ASSISTANTSWOTANALYSIS::T1374582329"] = "The overall assessment did not have the expected form. Asking the model once more..."
+
+-- Reconciling the four SWOT categories and writing the overall assessment...
+UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SWOTANALYSIS::ASSISTANTSWOTANALYSIS::T1491038257"] = "Reconciling the four SWOT categories and writing the overall assessment..."
+
+-- Researching with the selected tools...
+UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SWOTANALYSIS::ASSISTANTSWOTANALYSIS::T1849317035"] = "Researching with the selected tools..."
+
+-- SWOT Analysis
+UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SWOTANALYSIS::ASSISTANTSWOTANALYSIS::T1864542518"] = "SWOT Analysis"
+
+-- Please describe the goal the SWOT analysis should support.
+UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SWOTANALYSIS::ASSISTANTSWOTANALYSIS::T2136978542"] = "Please describe the goal the SWOT analysis should support."
+
+-- Target language
+UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SWOTANALYSIS::ASSISTANTSWOTANALYSIS::T237828418"] = "Target language"
+
+-- Input
+UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SWOTANALYSIS::ASSISTANTSWOTANALYSIS::T2677268763"] = "Input"
+
+-- Analysis goal
+UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SWOTANALYSIS::ASSISTANTSWOTANALYSIS::T2718020690"] = "Analysis goal"
+
+-- This category could not be created.
+UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SWOTANALYSIS::ASSISTANTSWOTANALYSIS::T2855208826"] = "This category could not be created."
+
+-- At least one SWOT category could not be created. The completed categories remain visible below.
+UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SWOTANALYSIS::ASSISTANTSWOTANALYSIS::T3160246784"] = "At least one SWOT category could not be created. The completed categories remain visible below."
+
+-- The selected model cannot read images. The SWOT analysis continues without the attached images.
+UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SWOTANALYSIS::ASSISTANTSWOTANALYSIS::T3248926642"] = "The selected model cannot read images. The SWOT analysis continues without the attached images."
+
+-- To enhance the validity of the SWOT analysis, please upload supplementary documents that provide empirical or contextual data for a comprehensive internal and external assessment.
+UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SWOTANALYSIS::ASSISTANTSWOTANALYSIS::T3485401617"] = "To enhance the validity of the SWOT analysis, please upload supplementary documents that provide empirical or contextual data for a comprehensive internal and external assessment."
+
+-- Create SWOT analysis
+UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SWOTANALYSIS::ASSISTANTSWOTANALYSIS::T3549988380"] = "Create SWOT analysis"
+
+-- Please provide source material for the SWOT analysis. You can enter text, load a document, or import content from a website.
+UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SWOTANALYSIS::ASSISTANTSWOTANALYSIS::T381075478"] = "Please provide source material for the SWOT analysis. You can enter text, load a document, or import content from a website."
+
+-- Custom target language
+UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SWOTANALYSIS::ASSISTANTSWOTANALYSIS::T3848935911"] = "Custom target language"
+
+-- Specify topics the SWOT analysis should pay particular attention to.
+UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SWOTANALYSIS::ASSISTANTSWOTANALYSIS::T3857591121"] = "Specify topics the SWOT analysis should pay particular attention to."
+
+-- Analysis Options
+UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SWOTANALYSIS::ASSISTANTSWOTANALYSIS::T3882472641"] = "Analysis Options"
+
+-- The image '{0}' could not be read and is left out of the SWOT analysis.
+UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SWOTANALYSIS::ASSISTANTSWOTANALYSIS::T4112888115"] = "The image '{0}' could not be read and is left out of the SWOT analysis."
+
+-- The research with the selected tools failed. The SWOT analysis continues without it.
+UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SWOTANALYSIS::ASSISTANTSWOTANALYSIS::T445799638"] = "The research with the selected tools failed. The SWOT analysis continues without it."
+
+-- The category analysis was canceled.
+UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SWOTANALYSIS::ASSISTANTSWOTANALYSIS::T476307744"] = "The category analysis was canceled."
+
+-- The quality of this SWOT analysis depends on the material you provide. The AI uses only your input, the attached documents, and the results of the tools you selected. Treat the result as a first draft and check it before you base decisions on it.
+UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SWOTANALYSIS::ASSISTANTSWOTANALYSIS::T503683591"] = "The quality of this SWOT analysis depends on the material you provide. The AI uses only your input, the attached documents, and the results of the tools you selected. Treat the result as a first draft and check it before you base decisions on it."
+
+-- (Optional) Analysis focus
+UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SWOTANALYSIS::ASSISTANTSWOTANALYSIS::T578386116"] = "(Optional) Analysis focus"
+
+-- Please provide a custom language.
+UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SWOTANALYSIS::ASSISTANTSWOTANALYSIS::T656744944"] = "Please provide a custom language."
+
+-- Contextual Knowledge
+UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SWOTANALYSIS::ASSISTANTSWOTANALYSIS::T701558710"] = "Contextual Knowledge"
+
+-- Subject of analysis
+UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SWOTANALYSIS::ASSISTANTSWOTANALYSIS::T871926658"] = "Subject of analysis"
+
+-- Analyzing the four SWOT categories...
+UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SWOTANALYSIS::ASSISTANTSWOTANALYSIS::T981239180"] = "Analyzing the four SWOT categories..."
+
+-- The four SWOT categories could not be reconciled because the answer did not have the expected form, even after asking the model once more. The preliminary results of the categories remain visible below.
+UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SWOTANALYSIS::ASSISTANTSWOTANALYSIS::T982373207"] = "The four SWOT categories could not be reconciled because the answer did not have the expected form, even after asking the model once more. The preliminary results of the categories remain visible below."
+
+-- The answer did not have the expected form. Asking the model once more...
+UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SWOTANALYSIS::SWOTANALYSISPROGRESSVIEW::T1020899874"] = "The answer did not have the expected form. Asking the model once more..."
+
+-- Strengths
+UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SWOTANALYSIS::SWOTANALYSISPROGRESSVIEW::T1245493657"] = "Strengths"
+
+-- Negative
+UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SWOTANALYSIS::SWOTANALYSISPROGRESSVIEW::T190369114"] = "Negative"
+
+-- Weaknesses
+UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SWOTANALYSIS::SWOTANALYSISPROGRESSVIEW::T2279747526"] = "Weaknesses"
+
+-- Positive
+UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SWOTANALYSIS::SWOTANALYSISPROGRESSVIEW::T230444442"] = "Positive"
+
+-- Opportunities
+UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SWOTANALYSIS::SWOTANALYSISPROGRESSVIEW::T2409493450"] = "Opportunities"
+
+-- Internal
+UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SWOTANALYSIS::SWOTANALYSISPROGRESSVIEW::T2805904416"] = "Internal"
+
+-- Analyzing this category...
+UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SWOTANALYSIS::SWOTANALYSISPROGRESSVIEW::T3114892934"] = "Analyzing this category..."
+
+-- Threats
+UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SWOTANALYSIS::SWOTANALYSISPROGRESSVIEW::T592141878"] = "Threats"
+
+-- External
+UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SWOTANALYSIS::SWOTANALYSISPROGRESSVIEW::T697638314"] = "External"
+
-- Find synonyms for the following word or phrase:
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SYNONYM::ASSISTANTSYNONYMS::T1793532807"] = "Find synonyms for the following word or phrase:"
@@ -10086,6 +10206,75 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T97357
-- A slide holds at most this many bullet points, in all of its text slots together. Match it to the room for text in the layouts of your template.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T995644230"] = "A slide holds at most this many bullet points, in all of its text slots together. Match it to the room for text in the layouts of your template."
+-- Content cleaner agent is preselected
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSWOTANALYSIS::T1013787967"] = "Content cleaner agent is preselected"
+
+-- Web content reader is shown
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSWOTANALYSIS::T1030372436"] = "Web content reader is shown"
+
+-- Preselect the target language
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSWOTANALYSIS::T1417990312"] = "Preselect the target language"
+
+-- Preselect another target language
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSWOTANALYSIS::T1462295644"] = "Preselect another target language"
+
+-- When activated, the web content reader is hidden and cannot be used. As a result, the user interface becomes a bit easier to use.
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSWOTANALYSIS::T1633101895"] = "When activated, the web content reader is hidden and cannot be used. As a result, the user interface becomes a bit easier to use."
+
+-- Web content reader is not preselected
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSWOTANALYSIS::T1701127912"] = "Web content reader is not preselected"
+
+-- Content cleaner agent is not preselected
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSWOTANALYSIS::T1969816694"] = "Content cleaner agent is not preselected"
+
+-- Hide the web content reader?
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSWOTANALYSIS::T2090693677"] = "Hide the web content reader?"
+
+-- SWOT analysis options are preselected
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSWOTANALYSIS::T2171839562"] = "SWOT analysis options are preselected"
+
+-- No SWOT analysis options are preselected
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSWOTANALYSIS::T2361666775"] = "No SWOT analysis options are preselected"
+
+-- Preselect SWOT analysis options?
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSWOTANALYSIS::T2593950994"] = "Preselect SWOT analysis options?"
+
+-- Web content reader is hidden
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSWOTANALYSIS::T2799795311"] = "Web content reader is hidden"
+
+-- When enabled, the web content reader is preselected. This is useful when you often analyze content from websites.
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSWOTANALYSIS::T3088856907"] = "When enabled, the web content reader is preselected. This is useful when you often analyze content from websites."
+
+-- When enabled, the content cleaner agent is preselected for cleaning website content before it is analyzed.
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSWOTANALYSIS::T332064979"] = "When enabled, the content cleaner agent is preselected for cleaning website content before it is analyzed."
+
+-- Close
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSWOTANALYSIS::T3448155331"] = "Close"
+
+-- Which target language should be preselected?
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSWOTANALYSIS::T3547337928"] = "Which target language should be preselected?"
+
+-- Web content reader is preselected
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSWOTANALYSIS::T3641773985"] = "Web content reader is preselected"
+
+-- Preselect the content cleaner agent?
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSWOTANALYSIS::T3649428096"] = "Preselect the content cleaner agent?"
+
+-- Specify topics the SWOT analysis should pay particular attention to.
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSWOTANALYSIS::T3857591121"] = "Specify topics the SWOT analysis should pay particular attention to."
+
+-- Assistant: SWOT Analysis Options
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSWOTANALYSIS::T4267340780"] = "Assistant: SWOT Analysis Options"
+
+-- Preselect the web content reader?
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSWOTANALYSIS::T629158142"] = "Preselect the web content reader?"
+
+-- Preselect an analysis focus
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSWOTANALYSIS::T656094797"] = "Preselect an analysis focus"
+
+-- When enabled, you can preselect the SWOT analysis options. This is useful when you prefer a specific language, analysis focus, or LLM.
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSWOTANALYSIS::T975761680"] = "When enabled, you can preselect the SWOT analysis options. This is useful when you prefer a specific language, analysis focus, or LLM."
+
-- When enabled, you can preselect synonym options. This is might be useful when you prefer a specific language or LLM model.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSYNONYMS::T183953912"] = "When enabled, you can preselect synonym options. This is might be useful when you prefer a specific language or LLM model."
@@ -10752,6 +10941,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T131837803"] = "Business"
-- Legal Check
UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T1348190638"] = "Legal Check"
+-- Identify strengths, weaknesses, opportunities, threats, and practical next steps from provided information.
+UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T1356376259"] = "Identify strengths, weaknesses, opportunities, threats, and practical next steps from provided information."
+
-- General
UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T1432485131"] = "General"
@@ -10773,6 +10965,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T1728590051"] = "Analyze a text or
-- Prompt Optimizer
UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T1777666968"] = "Prompt Optimizer"
+-- SWOT Analysis
+UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T1864542518"] = "SWOT Analysis"
+
-- Slide Assistant
UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T1883918574"] = "Slide Assistant"
@@ -11946,9 +12141,6 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::SUPPORTERS::T2640760894"] = "Become one of our
-- Supporters
UI_TEXT_CONTENT["AISTUDIO::PAGES::SUPPORTERS::T2929332068"] = "Supporters"
--- Thank you, Sabrina, for bringing file attachments to the chat, the Slide Assistant, and the protection against prompt injection.
-UI_TEXT_CONTENT["AISTUDIO::PAGES::SUPPORTERS::T2978040576"] = "Thank you, Sabrina, for bringing file attachments to the chat, the Slide Assistant, and the protection against prompt injection."
-
-- Content Contributors
UI_TEXT_CONTENT["AISTUDIO::PAGES::SUPPORTERS::T3060804484"] = "Content Contributors"
@@ -11958,6 +12150,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::SUPPORTERS::T3061261435"] = "Financial Support
-- Thank you, Prodman, for bringing LiteLLM to AI Studio as a new provider.
UI_TEXT_CONTENT["AISTUDIO::PAGES::SUPPORTERS::T3226413110"] = "Thank you, Prodman, for bringing LiteLLM to AI Studio as a new provider."
+-- Thank you, Sabrina, for bringing file attachments to the chat, for the Slide Assistant and the SWOT Analysis Assistant, and for the protection against prompt injection.
+UI_TEXT_CONTENT["AISTUDIO::PAGES::SUPPORTERS::T32630309"] = "Thank you, Sabrina, for bringing file attachments to the chat, for the Slide Assistant and the SWOT Analysis Assistant, and for the protection against prompt injection."
+
-- German Aerospace Center (DLR)
UI_TEXT_CONTENT["AISTUDIO::PAGES::SUPPORTERS::T3264998714"] = "German Aerospace Center (DLR)"
@@ -12762,6 +12957,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::COMPONENTSEXTENSIONS::T1993795352"] = "Prompt
-- Job Posting Assistant
UI_TEXT_CONTENT["AISTUDIO::TOOLS::COMPONENTSEXTENSIONS::T2212811874"] = "Job Posting Assistant"
+-- SWOT Analysis Assistant
+UI_TEXT_CONTENT["AISTUDIO::TOOLS::COMPONENTSEXTENSIONS::T2431303678"] = "SWOT Analysis Assistant"
+
-- Icon Finder Assistant
UI_TEXT_CONTENT["AISTUDIO::TOOLS::COMPONENTSEXTENSIONS::T2457005512"] = "Icon Finder Assistant"
diff --git a/app/MindWork AI Studio/Routes.razor.cs b/app/MindWork AI Studio/Routes.razor.cs
index a5466a6dd..49a343b3b 100644
--- a/app/MindWork AI Studio/Routes.razor.cs
+++ b/app/MindWork AI Studio/Routes.razor.cs
@@ -19,6 +19,7 @@ public sealed partial class Routes
public const string ASSISTANT_ICON_FINDER = "/assistant/icons";
public const string ASSISTANT_GRAMMAR_SPELLING = "/assistant/grammar-spelling";
public const string ASSISTANT_SUMMARIZER = "/assistant/summarizer";
+ public const string ASSISTANT_SWOT_ANALYSIS = "/assistant/swot-analysis";
public const string ASSISTANT_CODING = "/assistant/coding";
public const string ASSISTANT_AGENDA = "/assistant/agenda";
public const string ASSISTANT_EMAIL = "/assistant/email";
diff --git a/app/MindWork AI Studio/Settings/ConfigurableAssistant.cs b/app/MindWork AI Studio/Settings/ConfigurableAssistant.cs
index 1505b0b88..250fafe4c 100644
--- a/app/MindWork AI Studio/Settings/ConfigurableAssistant.cs
+++ b/app/MindWork AI Studio/Settings/ConfigurableAssistant.cs
@@ -16,6 +16,7 @@ public enum ConfigurableAssistant
AGENDA_ASSISTANT,
CODING_ASSISTANT,
TEXT_SUMMARIZER_ASSISTANT,
+ SWOT_ANALYSIS_ASSISTANT,
EMAIL_ASSISTANT,
LEGAL_CHECK_ASSISTANT,
SYNONYMS_ASSISTANT,
diff --git a/app/MindWork AI Studio/Settings/DataModel/Data.cs b/app/MindWork AI Studio/Settings/DataModel/Data.cs
index 5d2013f6a..82f5c3b68 100644
--- a/app/MindWork AI Studio/Settings/DataModel/Data.cs
+++ b/app/MindWork AI Studio/Settings/DataModel/Data.cs
@@ -186,6 +186,8 @@ public sealed class Data
public DataTextSummarizer TextSummarizer { get; init; } = new();
+ public DataSWOTAnalysis SWOTAnalysis { get; init; } = new();
+
public DataTextContentCleaner TextContentCleaner { get; init; } = new();
public DataAgentDataSourceSelection AgentDataSourceSelection { get; init; } = new(x => x.AgentDataSourceSelection);
diff --git a/app/MindWork AI Studio/Settings/DataModel/DataSWOTAnalysis.cs b/app/MindWork AI Studio/Settings/DataModel/DataSWOTAnalysis.cs
new file mode 100644
index 000000000..e6e722f65
--- /dev/null
+++ b/app/MindWork AI Studio/Settings/DataModel/DataSWOTAnalysis.cs
@@ -0,0 +1,51 @@
+using AIStudio.Provider;
+
+namespace AIStudio.Settings.DataModel;
+
+public sealed class DataSWOTAnalysis
+{
+ ///
+ /// Preselect any SWOT analysis options?
+ ///
+ public bool PreselectOptions { get; set; }
+
+ ///
+ /// Hide the web content reader?
+ ///
+ public bool HideWebContentReader { get; set; }
+
+ ///
+ /// Preselect the web content reader?
+ ///
+ public bool PreselectWebContentReader { get; set; }
+
+ ///
+ /// Preselect the content cleaner agent?
+ ///
+ public bool PreselectContentCleanerAgent { get; set; }
+
+ ///
+ /// Preselect the target language?
+ ///
+ public CommonLanguages PreselectedTargetLanguage { get; set; }
+
+ ///
+ /// Preselect another target language?
+ ///
+ public string PreselectedOtherLanguage { get; set; } = string.Empty;
+
+ ///
+ /// Preselect any aspects that the SWOT analysis should focus on?
+ ///
+ public string PreselectedImportantAspects { get; set; } = string.Empty;
+
+ ///
+ /// The minimum confidence level required for a provider to be considered.
+ ///
+ public ConfidenceLevel MinimumProviderConfidence { get; set; } = ConfidenceLevel.NONE;
+
+ ///
+ /// The preselected provider.
+ ///
+ public string PreselectedProvider { get; set; } = string.Empty;
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Tools/AssistantVisibilityExtensions.cs b/app/MindWork AI Studio/Tools/AssistantVisibilityExtensions.cs
index 1583e5335..25f5d0a84 100644
--- a/app/MindWork AI Studio/Tools/AssistantVisibilityExtensions.cs
+++ b/app/MindWork AI Studio/Tools/AssistantVisibilityExtensions.cs
@@ -58,6 +58,7 @@ public static class AssistantVisibilityExtensions
Components.AGENDA_ASSISTANT => ConfigurableAssistant.AGENDA_ASSISTANT,
Components.CODING_ASSISTANT => ConfigurableAssistant.CODING_ASSISTANT,
Components.TEXT_SUMMARIZER_ASSISTANT => ConfigurableAssistant.TEXT_SUMMARIZER_ASSISTANT,
+ Components.SWOT_ANALYSIS_ASSISTANT => ConfigurableAssistant.SWOT_ANALYSIS_ASSISTANT,
Components.EMAIL_ASSISTANT => ConfigurableAssistant.EMAIL_ASSISTANT,
Components.LEGAL_CHECK_ASSISTANT => ConfigurableAssistant.LEGAL_CHECK_ASSISTANT,
Components.SYNONYMS_ASSISTANT => ConfigurableAssistant.SYNONYMS_ASSISTANT,
diff --git a/app/MindWork AI Studio/Tools/Components.cs b/app/MindWork AI Studio/Tools/Components.cs
index 20c99d269..b15c670c7 100644
--- a/app/MindWork AI Studio/Tools/Components.cs
+++ b/app/MindWork AI Studio/Tools/Components.cs
@@ -12,6 +12,7 @@ public enum Components
AGENDA_ASSISTANT,
CODING_ASSISTANT,
TEXT_SUMMARIZER_ASSISTANT,
+ SWOT_ANALYSIS_ASSISTANT,
EMAIL_ASSISTANT,
LEGAL_CHECK_ASSISTANT,
SYNONYMS_ASSISTANT,
diff --git a/app/MindWork AI Studio/Tools/ComponentsExtensions.cs b/app/MindWork AI Studio/Tools/ComponentsExtensions.cs
index 3b9d96b89..b2f43de64 100644
--- a/app/MindWork AI Studio/Tools/ComponentsExtensions.cs
+++ b/app/MindWork AI Studio/Tools/ComponentsExtensions.cs
@@ -85,6 +85,7 @@ public static class ComponentsExtensions
{
Components.GRAMMAR_SPELLING_ASSISTANT => TB("Grammar & Spelling Assistant"),
Components.TEXT_SUMMARIZER_ASSISTANT => TB("Text Summarizer Assistant"),
+ Components.SWOT_ANALYSIS_ASSISTANT => TB("SWOT Analysis Assistant"),
Components.ICON_FINDER_ASSISTANT => TB("Icon Finder Assistant"),
Components.TRANSLATION_ASSISTANT => TB("Translation Assistant"),
Components.REWRITE_ASSISTANT => TB("Rewrite Assistant"),
@@ -121,6 +122,7 @@ public static class ComponentsExtensions
Components.ICON_FINDER_ASSISTANT => new(Event.SEND_TO_ICON_FINDER_ASSISTANT, Routes.ASSISTANT_ICON_FINDER),
Components.GRAMMAR_SPELLING_ASSISTANT => new(Event.SEND_TO_GRAMMAR_SPELLING_ASSISTANT, Routes.ASSISTANT_GRAMMAR_SPELLING),
Components.TEXT_SUMMARIZER_ASSISTANT => new(Event.SEND_TO_TEXT_SUMMARIZER_ASSISTANT, Routes.ASSISTANT_SUMMARIZER),
+ Components.SWOT_ANALYSIS_ASSISTANT => new(Event.SEND_TO_SWOT_ANALYSIS_ASSISTANT, Routes.ASSISTANT_SWOT_ANALYSIS),
Components.LEGAL_CHECK_ASSISTANT => new(Event.SEND_TO_LEGAL_CHECK_ASSISTANT, Routes.ASSISTANT_LEGAL_CHECK),
Components.SYNONYMS_ASSISTANT => new(Event.SEND_TO_SYNONYMS_ASSISTANT, Routes.ASSISTANT_SYNONYMS),
Components.MY_TASKS_ASSISTANT => new(Event.SEND_TO_MY_TASKS_ASSISTANT, Routes.ASSISTANT_MY_TASKS),
@@ -144,6 +146,7 @@ public static class ComponentsExtensions
Components.AGENDA_ASSISTANT => settingsManager.ConfigurationData.Agenda.PreselectOptions ? settingsManager.ConfigurationData.Agenda.MinimumProviderConfidence : default,
Components.CODING_ASSISTANT => settingsManager.ConfigurationData.Coding.PreselectOptions ? settingsManager.ConfigurationData.Coding.MinimumProviderConfidence : default,
Components.TEXT_SUMMARIZER_ASSISTANT => settingsManager.ConfigurationData.TextSummarizer.PreselectOptions ? settingsManager.ConfigurationData.TextSummarizer.MinimumProviderConfidence : default,
+ Components.SWOT_ANALYSIS_ASSISTANT => settingsManager.ConfigurationData.SWOTAnalysis.PreselectOptions ? settingsManager.ConfigurationData.SWOTAnalysis.MinimumProviderConfidence : default,
Components.EMAIL_ASSISTANT => settingsManager.ConfigurationData.EMail.PreselectOptions ? settingsManager.ConfigurationData.EMail.MinimumProviderConfidence : default,
Components.LEGAL_CHECK_ASSISTANT => settingsManager.ConfigurationData.LegalCheck.PreselectOptions ? settingsManager.ConfigurationData.LegalCheck.MinimumProviderConfidence : default,
Components.SYNONYMS_ASSISTANT => settingsManager.ConfigurationData.Synonyms.PreselectOptions ? settingsManager.ConfigurationData.Synonyms.MinimumProviderConfidence : default,
@@ -176,6 +179,7 @@ public static class ComponentsExtensions
Components.AGENDA_ASSISTANT => settingsManager.ConfigurationData.Agenda.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.Agenda.PreselectedProvider) : Settings.Provider.NONE,
Components.CODING_ASSISTANT => settingsManager.ConfigurationData.Coding.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.Coding.PreselectedProvider) : Settings.Provider.NONE,
Components.TEXT_SUMMARIZER_ASSISTANT => settingsManager.ConfigurationData.TextSummarizer.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.TextSummarizer.PreselectedProvider) : Settings.Provider.NONE,
+ Components.SWOT_ANALYSIS_ASSISTANT => settingsManager.ConfigurationData.SWOTAnalysis.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.SWOTAnalysis.PreselectedProvider) : Settings.Provider.NONE,
Components.EMAIL_ASSISTANT => settingsManager.ConfigurationData.EMail.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.EMail.PreselectedProvider) : Settings.Provider.NONE,
Components.LEGAL_CHECK_ASSISTANT => settingsManager.ConfigurationData.LegalCheck.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.LegalCheck.PreselectedProvider) : Settings.Provider.NONE,
Components.SYNONYMS_ASSISTANT => settingsManager.ConfigurationData.Synonyms.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.Synonyms.PreselectedProvider) : Settings.Provider.NONE,
diff --git a/app/MindWork AI Studio/Tools/Event.cs b/app/MindWork AI Studio/Tools/Event.cs
index 7a1ae1675..940417330 100644
--- a/app/MindWork AI Studio/Tools/Event.cs
+++ b/app/MindWork AI Studio/Tools/Event.cs
@@ -286,6 +286,11 @@ public enum Event
///
SEND_TO_TEXT_SUMMARIZER_ASSISTANT,
+ ///
+ /// Sends content to the SWOT analysis assistant.
+ ///
+ SEND_TO_SWOT_ANALYSIS_ASSISTANT,
+
///
/// Sends the result of the current assistant to the chat component.
///
diff --git a/app/MindWork AI Studio/wwwroot/changelog/v26.10.1.md b/app/MindWork AI Studio/wwwroot/changelog/v26.10.1.md
index 21b15560a..f27a3e3f4 100644
--- a/app/MindWork AI Studio/wwwroot/changelog/v26.10.1.md
+++ b/app/MindWork AI Studio/wwwroot/changelog/v26.10.1.md
@@ -31,6 +31,7 @@
- Added organization-wide management for the defaults of the Assistant Builder. IT departments can preset its provider, the language in which the assistants built with it answer, and the minimum confidence level, either fixed or as a default which users may change.
- Added support for PowerPoint templates to the Slide Assistant. When you choose a presentation or template (.pptx or .potx), for example the one of your organization, the assistant plans every slide for one of its layouts, and you can save the slides as a PowerPoint file in its design. You can also choose which of its layouts the assistant uses for kinds of slides, such as chapters or comparisons, add framing slides such as a table of contents or an imprint, and keep slides of your file. Without a template, you get an outline, as before. In both cases, you can now name the occasion of your presentation, such as a lecture or a report to management, and the number of bullet points you set is a maximum per slide. You can also attach pictures which the slides must show, let the assistant choose pictures and icons by their names from a folder, such as the picture library of your organization, and keep empty picture placeholders to insert pictures of your own later. Thanks, Sabrina (`Sabrina-devops`), for this contribution.
- Added a way for IT departments to provide the PowerPoint template of the Slide Assistant, its layouts for kinds of slides, which slides of the template a presentation keeps, the maximum number of bullet points per slide, whether slides keep empty picture placeholders, and a picture library, either fixed or as a default which users may change. The template and the picture library may come with the configuration of your organization.
+- Added a SWOT Analysis Assistant. It finds the strengths, weaknesses, opportunities, and threats in your text, documents, or a web page. The AI analyzes the four categories at the same time, and you see each one as soon as it is ready. A final review then resolves contradictions between the categories and writes an overall assessment with prioritized actions. You can export the analysis as a document or continue with it in a chat. Thanks, Sabrina (`Sabrina-devops`), for this contribution.
- Added a way to read what the AI received for an answer. In the list of sources below an answer, the eye button next to a source shows the passages the AI got from it, for example, from your data sources, a web page, a mail, or your Confluence wiki, as they were after AI Studio removed suspicious instructions. They stay readable when you open the chat again later. Thanks, Paul Koudelka (`PaulKoudelka`), for this contribution.
- Added organization-wide management for whether AI Studio reads a file attached to a chat again after it has changed. IT departments can set this for everybody, either fixed or as a default which users may change.
- Added a way for IT departments to slow down the indexing of local data sources, so that a few people with large folders or mailboxes no longer keep a shared embedding server busy for hours. IT departments set the shortest and the longest pause between the indexing steps, either fixed or as a default which users may change.
diff --git a/app/Tests/Assistants/SWOTAnalysis/SwotAnalysisContentPresentationTests.cs b/app/Tests/Assistants/SWOTAnalysis/SwotAnalysisContentPresentationTests.cs
new file mode 100644
index 000000000..221433a95
--- /dev/null
+++ b/app/Tests/Assistants/SWOTAnalysis/SwotAnalysisContentPresentationTests.cs
@@ -0,0 +1,176 @@
+using System.Text.Json;
+
+using AIStudio.Assistants.SWOTAnalysis;
+using AIStudio.Chat;
+using AIStudio.Tools;
+
+namespace AIStudio.Tests.Assistants.SWOTAnalysis;
+
+[TestFixture]
+public sealed class SwotAnalysisContentPresentationTests
+{
+ [Test]
+ public void PresentationSurvivesCloningAndWorkspaceSerialization()
+ {
+ var content = new ContentText
+ {
+ Text = "Complete SWOT analysis in Markdown",
+ Presentation = SwotAnalysisContentPresentation.Create(CreateResult()),
+ };
+ var clone = (ContentText)content.DeepClone();
+ var thread = new ChatThread
+ {
+ Blocks =
+ [
+ new ContentBlock
+ {
+ ContentType = ContentType.TEXT,
+ Role = ChatRole.AI,
+ Content = clone,
+ },
+ ],
+ };
+
+ var json = JsonSerializer.Serialize(thread, WorkspaceBehaviour.JSON_OPTIONS);
+ var restoredThread = JsonSerializer.Deserialize(json, WorkspaceBehaviour.JSON_OPTIONS);
+ var restoredContent = (ContentText)restoredThread!.Blocks.Single().Content!;
+ var presentationWasRead = SwotAnalysisContentPresentation.TryRead(restoredContent.Presentation, out var restoredResult);
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(clone.Presentation, Is.Not.SameAs(content.Presentation));
+ Assert.That(clone.Text, Is.EqualTo(content.Text));
+ Assert.That(restoredContent.Presentation?.Kind, Is.EqualTo(ContentPresentationKind.SWOT_ANALYSIS));
+ Assert.That(presentationWasRead, Is.True);
+ Assert.That(restoredResult.MatrixHeading, Is.EqualTo("SWOT Matrix"));
+ Assert.That(restoredResult.Strengths.Findings.Single().Summary, Is.EqualTo("Experienced team"));
+ Assert.That(restoredResult.PrioritizedActions.Items.Single().Action, Is.EqualTo("Prepare expansion"));
+ });
+ }
+
+ [Test]
+ public void InvalidPresentationDataFallsBackToMarkdown()
+ {
+ var content = new ContentText
+ {
+ Text = "Fallback Markdown",
+ Presentation = ContentPresentation.Create(ContentPresentationKind.SWOT_ANALYSIS, "not a SWOT result"),
+ };
+
+ var presentationWasRead = SwotAnalysisContentPresentation.TryRead(content.Presentation, out _);
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(presentationWasRead, Is.False);
+ Assert.That(content.Text, Is.EqualTo("Fallback Markdown"));
+ });
+ }
+
+ [Test]
+ public void TextWithoutPresentationUsesMarkdown()
+ {
+ var content = new ContentText { Text = "Ordinary Markdown" };
+
+ Assert.That(SwotAnalysisContentPresentation.TryRead(content.Presentation, out _), Is.False);
+ }
+
+ [Test]
+ public void AnAnalysisWithoutFindingsAndActionsIsStillComplete()
+ {
+ var result = CreateResult();
+ foreach (var category in result.Categories)
+ {
+ category.Findings.Clear();
+ category.EmptyMessage = $"No evidence for {category.Label}.";
+ }
+
+ result.PrioritizedActions.Items.Clear();
+ result.PrioritizedActions.EmptyMessage = "No supported actions.";
+ var content = new ContentText { Presentation = SwotAnalysisContentPresentation.Create(result) };
+
+ var presentationWasRead = SwotAnalysisContentPresentation.TryRead(content.Presentation, out var readResult);
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(presentationWasRead, Is.True);
+ Assert.That(readResult.Categories.All(category => category.Findings.Count == 0), Is.True);
+ Assert.That(readResult.PrioritizedActions.Items, Is.Empty);
+ });
+ }
+
+ [Test]
+ public void AStoredFindingWithoutAnExplanationFallsBackToMarkdown()
+ {
+ var result = CreateResult();
+ result.Strengths.Findings[0].Explanation = string.Empty;
+ var content = new ContentText { Presentation = SwotAnalysisContentPresentation.Create(result) };
+
+ Assert.That(SwotAnalysisContentPresentation.TryRead(content.Presentation, out _), Is.False);
+ }
+
+ [Test]
+ public void StoredDataWithANullFallsBackToMarkdown()
+ {
+ var data = JsonSerializer.SerializeToNode(CreateResult())!;
+ data["strengths"]!["findings"] = null;
+ var content = new ContentText
+ {
+ Presentation = new ContentPresentation
+ {
+ Kind = ContentPresentationKind.SWOT_ANALYSIS,
+ Data = JsonSerializer.SerializeToElement(data),
+ },
+ };
+
+ Assert.That(SwotAnalysisContentPresentation.TryRead(content.Presentation, out _), Is.False);
+ }
+
+ private static SwotAnalysisResult CreateResult() => new()
+ {
+ MatrixHeading = "SWOT Matrix",
+ InternalLabel = "Internal",
+ ExternalLabel = "External",
+ PositiveLabel = "Positive",
+ NegativeLabel = "Negative",
+ Strengths = Category("Strengths", "Experienced team", "The documents describe long employee tenure."),
+ Weaknesses = Category("Weaknesses", "Legacy system", "The documents name an unsupported application."),
+ Opportunities = Category("Opportunities", "Growing market", "The supplied forecast shows increasing demand."),
+ Threats = Category("Threats", "New competitor", "The material announces a funded market entrant."),
+ OverallAssessment = new()
+ {
+ Label = "Overall Assessment",
+ Text = "The position is solid.",
+ },
+ PrioritizedActions = new()
+ {
+ Label = "Prioritized Actions",
+ Items =
+ [
+ new()
+ {
+ Action = "Prepare expansion",
+ Rationale = "Use the team's experience to address demand.",
+ AddressedFactors = ["Experienced team", "Growing market"],
+ },
+ ],
+ },
+ ModerationNotes = new()
+ {
+ Label = "Reconciliation of the Quadrants",
+ EmptyMessage = "The four categories needed no changes.",
+ },
+ };
+
+ private static SwotCategory Category(string label, string summary, string explanation) => new()
+ {
+ Label = label,
+ Findings =
+ [
+ new()
+ {
+ Summary = summary,
+ Explanation = explanation,
+ },
+ ],
+ };
+}
\ No newline at end of file
diff --git a/app/Tests/Assistants/SWOTAnalysis/SwotAnalysisMarkdownFormatterTests.cs b/app/Tests/Assistants/SWOTAnalysis/SwotAnalysisMarkdownFormatterTests.cs
new file mode 100644
index 000000000..0d89ecf44
--- /dev/null
+++ b/app/Tests/Assistants/SWOTAnalysis/SwotAnalysisMarkdownFormatterTests.cs
@@ -0,0 +1,113 @@
+using System.Text.RegularExpressions;
+
+using AIStudio.Assistants.SWOTAnalysis;
+
+namespace AIStudio.Tests.Assistants.SWOTAnalysis;
+
+[TestFixture]
+public sealed class SwotAnalysisMarkdownFormatterTests
+{
+ [Test]
+ public void EveryFindingAppearsInTheDetailAndInTheFinalMatrix()
+ {
+ var result = CreateResult();
+ for (var index = 2; index <= 6; index++)
+ result.Strengths.Findings.Add(new() { Summary = $"Strength {index}", Explanation = $"Evidence {index}" });
+
+ var markdown = SwotAnalysisMarkdownFormatter.Format(result);
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(markdown, Does.Contain("| | Positive | Negative |"));
+ Assert.That(Count(markdown, "Experienced team"), Is.GreaterThanOrEqualTo(2));
+ Assert.That(Count(markdown, "Strength 6"), Is.EqualTo(2), "The matrix must not cap the number of findings.");
+ Assert.That(Count(markdown, "Evidence 6"), Is.EqualTo(1), "Explanations belong to the detailed section, not the matrix.");
+ Assert.That(markdown, Does.StartWith("## SWOT Matrix"));
+ Assert.That(markdown.IndexOf("## SWOT Matrix", StringComparison.Ordinal), Is.LessThan(markdown.IndexOf("## Strengths", StringComparison.Ordinal)));
+ Assert.That(markdown.IndexOf("## SWOT Matrix", StringComparison.Ordinal), Is.LessThan(markdown.IndexOf("## Prioritized Actions", StringComparison.Ordinal)));
+ Assert.That(markdown, Does.Contain("| **External** | **Opportunities:** Growing market | **Threats:** New competitor |"));
+ });
+ }
+
+ [Test]
+ public void TheSectionsFollowTheOrderOfTheResultView()
+ {
+ var result = CreateResult();
+ result.OverallAssessment.Text = "The position is solid.\n\nThe competitor is the main risk.";
+ result.ModerationNotes = new()
+ {
+ Label = "Reconciliation of the Quadrants",
+ Items = [new() { Change = "Moved the new competitor to the threats.", Reason = "It is external and negative." }],
+ };
+
+ var markdown = SwotAnalysisMarkdownFormatter.Format(result).ReplaceLineEndings("\n");
+ var positions = new[] { "## SWOT Matrix", "## Overall Assessment", "## Prioritized Actions", "## Strengths", "## Threats", "## Reconciliation of the Quadrants" }
+ .Select(heading => markdown.IndexOf(heading, StringComparison.Ordinal))
+ .ToList();
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(positions, Is.All.GreaterThanOrEqualTo(0));
+ Assert.That(positions, Is.Ordered);
+ Assert.That(markdown, Does.Contain("The position is solid\\.\n\nThe competitor is the main risk\\."));
+ Assert.That(markdown, Does.EndWith("- **Moved the new competitor to the threats\\.** — It is external and negative\\."));
+ });
+ }
+
+ [Test]
+ public void MatrixCellsEscapePipesFromModelText()
+ {
+ var result = CreateResult();
+ result.Strengths.Findings[0].Summary = "Fast | reliable";
+
+ var markdown = SwotAnalysisMarkdownFormatter.Format(result);
+
+ Assert.That(markdown, Does.Contain("Fast \\| reliable"));
+ }
+
+ private static int Count(string value, string search) => Regex.Matches(value, Regex.Escape(search)).Count;
+
+ private static SwotAnalysisResult CreateResult() => new()
+ {
+ MatrixHeading = "SWOT Matrix",
+ InternalLabel = "Internal",
+ ExternalLabel = "External",
+ PositiveLabel = "Positive",
+ NegativeLabel = "Negative",
+ Strengths = Category("Strengths", "Experienced team", "The documents describe long employee tenure."),
+ Weaknesses = Category("Weaknesses", "Legacy system", "The documents name an unsupported application."),
+ Opportunities = Category("Opportunities", "Growing market", "The supplied forecast shows increasing demand."),
+ Threats = Category("Threats", "New competitor", "The material announces a funded market entrant."),
+ OverallAssessment = new()
+ {
+ Label = "Overall Assessment",
+ Text = "The position is solid.",
+ },
+ PrioritizedActions = new()
+ {
+ Label = "Prioritized Actions",
+ EmptyMessage = string.Empty,
+ Items =
+ [
+ new()
+ {
+ Action = "Prepare expansion",
+ Rationale = "Use the team's experience to address demand.",
+ AddressedFactors = ["Experienced team", "Growing market"],
+ },
+ ],
+ },
+ ModerationNotes = new()
+ {
+ Label = "Reconciliation of the Quadrants",
+ EmptyMessage = "The four categories needed no changes.",
+ },
+ };
+
+ private static SwotCategory Category(string label, string summary, string explanation) => new()
+ {
+ Label = label,
+ EmptyMessage = string.Empty,
+ Findings = [new() { Summary = summary, Explanation = explanation }],
+ };
+}
\ No newline at end of file
diff --git a/app/Tests/Assistants/SWOTAnalysis/SwotAnalysisModerationTests.cs b/app/Tests/Assistants/SWOTAnalysis/SwotAnalysisModerationTests.cs
new file mode 100644
index 000000000..4484e2b49
--- /dev/null
+++ b/app/Tests/Assistants/SWOTAnalysis/SwotAnalysisModerationTests.cs
@@ -0,0 +1,209 @@
+using System.Text.Json;
+
+using AIStudio.Assistants.SWOTAnalysis;
+
+namespace AIStudio.Tests.Assistants.SWOTAnalysis;
+
+[TestFixture]
+public sealed class SwotAnalysisModerationTests
+{
+ [Test]
+ public void AValidAnswerIsAssembledIntoTheResult()
+ {
+ var parsed = SwotAnalysisModerationResponseParser.TryParse(JsonSerializer.Serialize(CreateModeration()), out var moderation, out var issue);
+ var result = SwotAnalysisModeration.Assemble(moderation);
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(parsed, Is.True);
+ Assert.That(issue, Is.Empty);
+ Assert.That(result.Strengths.Label, Is.EqualTo("Stärken"));
+ Assert.That(result.Strengths.Findings.Single().Summary, Is.EqualTo("Experienced team"));
+ Assert.That(result.Threats.Findings.Single().Explanation, Is.EqualTo("The material announces a funded market entrant."));
+ Assert.That(result.OverallAssessment.Paragraphs, Is.EqualTo(new[] { "The position is solid.", "The competitor is the main risk." }));
+ Assert.That(result.ModerationNotes.Items.Single().Change, Is.EqualTo("Merged two strengths about the team."));
+ Assert.That(result.PrioritizedActions.Items.Single().AddressedFactors, Is.EqualTo(new[] { "Experienced team", "Growing market" }));
+ });
+ }
+
+ [Test]
+ public void FactorIdsAreMatchedWithoutCaseSensitivity()
+ {
+ var moderation = CreateModeration();
+ moderation.PrioritizedActions.Items.Single().AddressedFactorIds = ["s1", "o1"];
+
+ var parsed = SwotAnalysisModerationResponseParser.TryParse(JsonSerializer.Serialize(moderation), out var parsedModeration, out _);
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(parsed, Is.True);
+ Assert.That(SwotAnalysisModeration.Assemble(parsedModeration).PrioritizedActions.Items.Single().AddressedFactors, Is.EqualTo(new[] { "Experienced team", "Growing market" }));
+ });
+ }
+
+ [Test]
+ public void AFactorIdOfAnotherCategoryIsRejected()
+ {
+ var moderation = CreateModeration();
+ moderation.Strengths.Findings.Single().FactorId = "W2";
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(SwotAnalysisModerationResponseParser.TryParse(JsonSerializer.Serialize(moderation), out _, out var issue), Is.False);
+ Assert.That(issue, Is.EqualTo("Finding 1 of strengths has the factor_id \"W2\", which does not start with \"S\"."));
+ });
+ }
+
+ [Test]
+ public void AFactorIdUsedTwiceIsRejected()
+ {
+ var moderation = CreateModeration();
+ moderation.Strengths.Findings.Add(Finding("s1", "Loyal customers", "The survey shows long customer relationships."));
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(SwotAnalysisModerationResponseParser.TryParse(JsonSerializer.Serialize(moderation), out _, out var issue), Is.False);
+ Assert.That(issue, Does.Contain("\"s1\" is used more than once"));
+ });
+ }
+
+ [Test]
+ public void AnActionForAnUnknownFactorIsRejected()
+ {
+ var moderation = CreateModeration();
+ moderation.PrioritizedActions.Items.Single().AddressedFactorIds = ["S1", "X9"];
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(SwotAnalysisModerationResponseParser.TryParse(JsonSerializer.Serialize(moderation), out _, out var issue), Is.False);
+ Assert.That(issue, Does.Contain("\"X9\""));
+ });
+ }
+
+ [Test]
+ public void AnEmptyOverallAssessmentIsRejected()
+ {
+ var moderation = CreateModeration();
+ moderation.OverallAssessment.Text = " ";
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(SwotAnalysisModerationResponseParser.TryParse(JsonSerializer.Serialize(moderation), out _, out var issue), Is.False);
+ Assert.That(issue, Is.EqualTo("The field overall_assessment.text is empty."));
+ });
+ }
+
+ [Test]
+ public void NotesWithoutChangesExplainWhy()
+ {
+ var withoutReason = CreateModeration();
+ withoutReason.ModerationNotes.Items.Clear();
+ var withReason = CreateModeration();
+ withReason.ModerationNotes.Items.Clear();
+ withReason.ModerationNotes.EmptyMessage = "The four categories needed no changes.";
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(SwotAnalysisModerationResponseParser.TryParse(JsonSerializer.Serialize(withoutReason), out _, out var issue), Is.False);
+ Assert.That(issue, Does.StartWith("moderation_notes has no items"));
+ Assert.That(SwotAnalysisModerationResponseParser.TryParse(JsonSerializer.Serialize(withReason), out _, out _), Is.True);
+ });
+ }
+
+ [Test]
+ public void AnAnswerWithoutJsonNamesTheProblemForTheModel()
+ {
+ Assert.Multiple(() =>
+ {
+ Assert.That(SwotAnalysisModerationResponseParser.TryParse("Here is my assessment.", out _, out var issue), Is.False);
+ Assert.That(issue, Does.Contain("no complete JSON object"));
+ });
+ }
+
+ [Test]
+ public void TheRequestHoldsTheFourCategoriesTheGoalAndTheResearch()
+ {
+ var request = SwotAnalysisModeration.BuildUserRequest(CreateCategories(), "Support the investment decision", string.Empty, "A market report documents increasing demand.");
+
+ Assert.Multiple(() =>
+ {
+ foreach (var category in new[] { "strengths", "weaknesses", "opportunities", "threats" })
+ Assert.That(request, Does.Contain($"\"{category}\""));
+
+ Assert.That(request, Does.Contain("\"factor_id\": \"S1\""));
+ Assert.That(request, Does.Contain("\"factor_id\": \"O1\""));
+ Assert.That(request, Does.Contain("The material names no threats."));
+ Assert.That(request, Does.Match("(?s)\\s*Support the investment decision\\s*"));
+ Assert.That(request, Does.Match("(?s)\\s*No additional analysis focus was provided\\.\\s*"));
+ Assert.That(request, Does.Match("(?s)\\s*A market report documents increasing demand\\.\\s*"));
+ Assert.That(request, Does.Not.Contain(""), "The moderator works on the results, not on the material.");
+ });
+ }
+
+ [Test]
+ public void TheRequestOmitsEmptyResearch() =>
+ Assert.That(SwotAnalysisModeration.BuildUserRequest(CreateCategories(), "Support the investment decision", string.Empty), Does.Not.Contain(""));
+
+ [Test]
+ public void ThePromptAsksForTheWholeModeratorSchema()
+ {
+ var prompt = SwotAnalysisModeration.BuildSystemPrompt("Answer in German.");
+
+ Assert.Multiple(() =>
+ {
+ foreach (var key in new[] { "\"moderation_notes\"", "\"overall_assessment\"", "\"prioritized_actions\"", "\"addressed_factor_ids\"", "\"factor_id\"" })
+ Assert.That(prompt, Does.Contain(key));
+
+ Assert.That(prompt, Does.EndWith("Answer in German."));
+ });
+ }
+
+ private static Dictionary CreateCategories() => new()
+ {
+ [SwotAnalysisCategory.STRENGTHS] = new() { Findings = [new() { Summary = "Experienced team", Explanation = "The documents describe long employee tenure." }] },
+ [SwotAnalysisCategory.WEAKNESSES] = new() { Findings = [new() { Summary = "Legacy system", Explanation = "The documents name an unsupported application." }] },
+ [SwotAnalysisCategory.OPPORTUNITIES] = new() { Findings = [new() { Summary = "Growing market", Explanation = "The supplied forecast shows increasing demand." }] },
+ [SwotAnalysisCategory.THREATS] = new() { EmptyMessage = "The material names no threats." },
+ };
+
+ private static SwotModerationResult CreateModeration() => new()
+ {
+ MatrixHeading = "SWOT-Matrix",
+ InternalLabel = "Intern",
+ ExternalLabel = "Extern",
+ PositiveLabel = "Positiv",
+ NegativeLabel = "Negativ",
+ Strengths = Category("Stärken", Finding("S1", "Experienced team", "The documents describe long employee tenure.")),
+ Weaknesses = Category("Schwächen", Finding("W1", "Legacy system", "The documents name an unsupported application.")),
+ Opportunities = Category("Chancen", Finding("O1", "Growing market", "The supplied forecast shows increasing demand.")),
+ Threats = Category("Risiken", Finding("T1", "New competitor", "The material announces a funded market entrant.")),
+ ModerationNotes = new()
+ {
+ Label = "Abgleich der Quadranten",
+ Items = [new() { Change = "Merged two strengths about the team.", Reason = "Both described the same tenure." }],
+ },
+ OverallAssessment = new()
+ {
+ Label = "Gesamtbewertung",
+ Text = "The position is solid.\n\nThe competitor is the main risk.",
+ },
+ PrioritizedActions = new()
+ {
+ Label = "Maßnahmen",
+ Items = [new() { Action = "Prepare expansion", Rationale = "Use the team's experience to address demand.", AddressedFactorIds = ["S1", "O1"] }],
+ },
+ };
+
+ private static SwotModeratedCategory Category(string label, SwotModeratedFinding finding) => new()
+ {
+ Label = label,
+ Findings = [finding],
+ };
+
+ private static SwotModeratedFinding Finding(string factorId, string summary, string explanation) => new()
+ {
+ FactorId = factorId,
+ Summary = summary,
+ Explanation = explanation,
+ };
+}
\ No newline at end of file
diff --git a/app/Tests/Assistants/SWOTAnalysis/SwotAnalysisSendToChatTests.cs b/app/Tests/Assistants/SWOTAnalysis/SwotAnalysisSendToChatTests.cs
new file mode 100644
index 000000000..3dd26833d
--- /dev/null
+++ b/app/Tests/Assistants/SWOTAnalysis/SwotAnalysisSendToChatTests.cs
@@ -0,0 +1,107 @@
+using AIStudio.Assistants.SWOTAnalysis;
+using AIStudio.Chat;
+using AIStudio.Provider;
+
+namespace AIStudio.Tests.Assistants.SWOTAnalysis;
+
+[TestFixture]
+public sealed class SwotAnalysisSendToChatTests
+{
+ private const string ADDRESS_A_MODEL_CHOSE = "https://research.example.org/report";
+
+ [Test]
+ public void TheChatGetsTheVisibleRequestAndTheVisibleResultOnly()
+ {
+ var assistantThread = CreateAssistantThread();
+ var result = assistantThread.Blocks.Last();
+ var visiblePrompt = VisiblePrompt(result.Time.AddTicks(-1));
+
+ var chatThread = AssistantSWOTAnalysis.CreateChatThreadForResult(assistantThread, visiblePrompt, result);
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(chatThread.Blocks, Has.Count.EqualTo(2));
+ Assert.That(chatThread.Blocks[0], Is.SameAs(visiblePrompt));
+ Assert.That(chatThread.Blocks[1].Role, Is.EqualTo(ChatRole.AI));
+ Assert.That(chatThread.Blocks[1].HideFromUser, Is.False);
+ Assert.That(((ContentText)chatThread.Blocks[1].Content!).Text, Is.EqualTo("## SWOT Matrix"));
+ Assert.That(chatThread.Blocks[1].Content, Is.Not.SameAs(result.Content));
+ Assert.That(chatThread.SystemPrompt, Is.EqualTo(SystemPrompts.DEFAULT));
+ });
+ }
+
+ [Test]
+ public void AnAddressAModelWroteIsNotGivenInTheChat()
+ {
+ var assistantThread = CreateAssistantThread();
+ var result = assistantThread.Blocks.Last();
+
+ var chatThread = AssistantSWOTAnalysis.CreateChatThreadForResult(assistantThread, VisiblePrompt(result.Time.AddTicks(-1)), result);
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(assistantThread.IsWebAddressGivenToTheModel(new Uri(ADDRESS_A_MODEL_CHOSE)), Is.True, "The hidden request counts as a message of the user.");
+ Assert.That(chatThread.IsWebAddressGivenToTheModel(new Uri(ADDRESS_A_MODEL_CHOSE)), Is.False);
+ });
+ }
+
+ [Test]
+ public void TheChatKeepsTheIdAndTheRestrictionsOfTheAssistant()
+ {
+ var assistantThread = CreateAssistantThread();
+ assistantThread.RequireProviderConfidence(ConfidenceLevel.HIGH);
+ var result = assistantThread.Blocks.Last();
+
+ var chatThread = AssistantSWOTAnalysis.CreateChatThreadForResult(assistantThread, VisiblePrompt(result.Time.AddTicks(-1)), result);
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(chatThread.ChatId, Is.EqualTo(assistantThread.ChatId));
+ Assert.That(chatThread.RequiredProviderConfidence, Is.EqualTo(ConfidenceLevel.HIGH));
+ });
+ }
+
+ [Test]
+ public void WithoutAResultTheChatStartsEmpty()
+ {
+ var chatThread = AssistantSWOTAnalysis.CreateChatThreadForResult(CreateAssistantThread(), null, null);
+
+ Assert.That(chatThread.Blocks, Is.Empty);
+ }
+
+ private static ChatThread CreateAssistantThread()
+ {
+ var time = DateTimeOffset.Now;
+ return new ChatThread
+ {
+ ChatId = Guid.NewGuid(),
+ Blocks =
+ [
+ new ContentBlock
+ {
+ Time = time,
+ Role = ChatRole.USER,
+ ContentType = ContentType.TEXT,
+ HideFromUser = true,
+ Content = new ContentText { Text = $"The opportunities rest on {ADDRESS_A_MODEL_CHOSE}." },
+ },
+ new ContentBlock
+ {
+ Time = time.AddSeconds(1),
+ Role = ChatRole.AI,
+ ContentType = ContentType.TEXT,
+ HideFromUser = true,
+ Content = new ContentText { Text = "## SWOT Matrix" },
+ },
+ ],
+ };
+ }
+
+ private static ContentBlock VisiblePrompt(DateTimeOffset time) => new()
+ {
+ Time = time,
+ Role = ChatRole.USER,
+ ContentType = ContentType.TEXT,
+ Content = new ContentText { Text = "Create a SWOT analysis of my content" },
+ };
+}
\ No newline at end of file
diff --git a/app/Tests/Assistants/SWOTAnalysis/SwotAnalysisSessionStateTests.cs b/app/Tests/Assistants/SWOTAnalysis/SwotAnalysisSessionStateTests.cs
new file mode 100644
index 000000000..7ed055e7a
--- /dev/null
+++ b/app/Tests/Assistants/SWOTAnalysis/SwotAnalysisSessionStateTests.cs
@@ -0,0 +1,60 @@
+using AIStudio.Assistants.SWOTAnalysis;
+using AIStudio.Tools.AssistantSessions;
+
+namespace AIStudio.Tests.Assistants.SWOTAnalysis;
+
+[TestFixture]
+public sealed class SwotAnalysisSessionStateTests
+{
+ [Test]
+ public void TheStructuredResultSurvivesAnAssistantSessionSnapshot()
+ {
+ var key = new AssistantSessionStateKey("analysisResult");
+ var writer = new AssistantSessionStateWriter();
+ writer.Set(key, new SwotAnalysisResult
+ {
+ MatrixHeading = "SWOT Matrix",
+ Strengths = new() { Findings = [Finding()] },
+ });
+ SwotAnalysisResult? restored = null;
+
+ var reader = new AssistantSessionStateReader(writer.ToDictionary(), "SWOT Analysis");
+ reader.Restore(key, value => restored = value);
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(restored, Is.Not.Null);
+ Assert.That(restored!.MatrixHeading, Is.EqualTo("SWOT Matrix"));
+ Assert.That(restored.Strengths.Findings.Single().Summary, Is.EqualTo("Experienced team"));
+ });
+ }
+
+ [Test]
+ public void CategoryProgressSurvivesAnAssistantSessionSnapshot()
+ {
+ var key = new AssistantSessionStateKey>("categoryAnalysisProgress");
+ var writer = new AssistantSessionStateWriter();
+ writer.SetDictionary(key, new Dictionary
+ {
+ [SwotAnalysisCategory.STRENGTHS] = new(SwotCategoryAnalysisStatus.COMPLETED, new SwotCategoryAnalysisResult { Findings = [Finding()] }),
+ [SwotAnalysisCategory.WEAKNESSES] = new(SwotCategoryAnalysisStatus.RUNNING),
+ });
+ Dictionary restored = [];
+
+ var reader = new AssistantSessionStateReader(writer.ToDictionary(), "SWOT Analysis");
+ reader.RestoreDictionary(key, restored);
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(restored[SwotAnalysisCategory.STRENGTHS].Status, Is.EqualTo(SwotCategoryAnalysisStatus.COMPLETED));
+ Assert.That(restored[SwotAnalysisCategory.STRENGTHS].Result!.Findings.Single().Summary, Is.EqualTo("Experienced team"));
+ Assert.That(restored[SwotAnalysisCategory.WEAKNESSES].Status, Is.EqualTo(SwotCategoryAnalysisStatus.RUNNING));
+ });
+ }
+
+ private static SwotFinding Finding() => new()
+ {
+ Summary = "Experienced team",
+ Explanation = "The documents describe long employee tenure.",
+ };
+}
\ No newline at end of file
diff --git a/app/Tests/Assistants/SWOTAnalysis/SwotCategoryAnalysisResponseTests.cs b/app/Tests/Assistants/SWOTAnalysis/SwotCategoryAnalysisResponseTests.cs
new file mode 100644
index 000000000..b88aef59a
--- /dev/null
+++ b/app/Tests/Assistants/SWOTAnalysis/SwotCategoryAnalysisResponseTests.cs
@@ -0,0 +1,181 @@
+using System.Text.Json;
+
+using AIStudio.Assistants.SWOTAnalysis;
+
+namespace AIStudio.Tests.Assistants.SWOTAnalysis;
+
+[TestFixture]
+public sealed class SwotCategoryAnalysisResponseTests
+{
+ [Test]
+ public void TheFindingsOfACategoryAreReadAndTrimmed()
+ {
+ var result = CreateResult();
+ result.Findings[0].Summary = " Experienced team ";
+
+ var parsed = SwotCategoryAnalysisResponseParser.TryParse(JsonSerializer.Serialize(result), out var parsedResult, out var issue);
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(parsed, Is.True);
+ Assert.That(issue, Is.Empty);
+ Assert.That(parsedResult.EmptyMessage, Is.Empty);
+ Assert.That(parsedResult.Findings.Single().Summary, Is.EqualTo("Experienced team"));
+ });
+ }
+
+ [Test]
+ public void AnAnswerWithoutJsonNamesTheProblemForTheModel()
+ {
+ var parsed = SwotCategoryAnalysisResponseParser.TryParse("I cannot analyze this category.", out _, out var issue);
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(parsed, Is.False);
+ Assert.That(issue, Does.Contain("no complete JSON object"));
+ });
+ }
+
+ [Test]
+ public void BrokenJsonNamesTheProblemForTheModel()
+ {
+ var parsed = SwotCategoryAnalysisResponseParser.TryParse("""{"findings": [{"summary": "Experienced team",}""", out _, out var issue);
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(parsed, Is.False);
+ Assert.That(issue, Is.Not.Empty);
+ });
+ }
+
+ [Test]
+ public void ANullFieldIsRejectedRatherThanReadAsEmpty()
+ {
+ var parsed = SwotCategoryAnalysisResponseParser.TryParse("""{"empty_message": null, "findings": []}""", out _, out var issue);
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(parsed, Is.False);
+ Assert.That(issue, Does.StartWith("The JSON object cannot be read:"));
+ });
+ }
+
+ [Test]
+ public void ANullFindingIsLeftOut()
+ {
+ var parsed = SwotCategoryAnalysisResponseParser.TryParse("""{"empty_message": "", "findings": [null, {"summary": "Experienced team", "explanation": "Long tenure."}]}""", out var result, out _);
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(parsed, Is.True);
+ Assert.That(result.Findings.Single().Summary, Is.EqualTo("Experienced team"));
+ });
+ }
+
+ [Test]
+ public void AnUnsupportedCategoryRequiresAnExplanation()
+ {
+ var validEmptyResponse = JsonSerializer.Serialize(new SwotCategoryAnalysisResult
+ {
+ EmptyMessage = "The supplied material contains no evidence for this category.",
+ });
+ var invalidEmptyResponse = JsonSerializer.Serialize(new SwotCategoryAnalysisResult());
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(SwotCategoryAnalysisResponseParser.TryParse(validEmptyResponse, out var result, out _), Is.True);
+ Assert.That(result.Findings, Is.Empty);
+ Assert.That(SwotCategoryAnalysisResponseParser.TryParse(invalidEmptyResponse, out _, out var issue), Is.False);
+ Assert.That(issue, Does.Contain("empty_message"));
+ });
+ }
+
+ [Test]
+ public void FindingsRejectAnEmptyMessageOrMissingEvidenceExplanation()
+ {
+ var result = CreateResult();
+ result.EmptyMessage = "No evidence.";
+ var responseWithContradictoryEmptyMessage = JsonSerializer.Serialize(result);
+
+ result.EmptyMessage = string.Empty;
+ result.Findings[0].Explanation = string.Empty;
+ var responseWithoutExplanation = JsonSerializer.Serialize(result);
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(SwotCategoryAnalysisResponseParser.TryParse(responseWithContradictoryEmptyMessage, out _, out var contradictionIssue), Is.False);
+ Assert.That(contradictionIssue, Does.Contain("Leave empty_message empty"));
+ Assert.That(SwotCategoryAnalysisResponseParser.TryParse(responseWithoutExplanation, out _, out var explanationIssue), Is.False);
+ Assert.That(explanationIssue, Is.EqualTo("Finding 1 has no explanation."));
+ });
+ }
+
+ [TestCase("STRENGTHS", "strengths: internal positive factors")]
+ [TestCase("WEAKNESSES", "weaknesses: internal negative factors")]
+ [TestCase("OPPORTUNITIES", "opportunities: external positive factors")]
+ [TestCase("THREATS", "threats: external negative factors")]
+ public void TheSystemPromptAssignsExactlyOneCategory(string categoryName, string expectedDescription)
+ {
+ var category = Enum.Parse(categoryName);
+ var prompt = SwotCategoryAnalysis.BuildSystemPrompt(category, "Answer in German.");
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(prompt, Does.Contain($"Analyze only {expectedDescription}."));
+ Assert.That(prompt, Does.Contain("Do not return findings for another SWOT category."));
+ Assert.That(prompt, Does.Contain("shared-research contains additional evidence"));
+ Assert.That(prompt, Does.Contain("analysis-subject, contextual-knowledge, and\nshared-research"));
+ Assert.That(prompt, Does.Contain("untrusted data"));
+ Assert.That(prompt, Does.EndWith("Answer in German."));
+ });
+ }
+
+ [Test]
+ public void TheCategoryRequestKeepsEveryInputInItsAssignedSection()
+ {
+ var request = SwotCategoryAnalysis.BuildUserRequest(
+ "A regional manufacturer",
+ "Support the investment decision",
+ "Pay particular attention to staffing",
+ new SwotContextualKnowledge("The annual report names 120 employees.", []),
+ sharedResearch: "A market report documents increasing regional demand.");
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(request, Does.Match("(?s)\\s*A regional manufacturer\\s*"));
+ Assert.That(request, Does.Match("(?s)\\s*Support the investment decision\\s*"));
+ Assert.That(request, Does.Match("(?s)\\s*Pay particular attention to staffing\\s*"));
+ Assert.That(request, Does.Match("(?s)\\s*The annual report names 120 employees\\.\\s*"));
+ Assert.That(request, Does.Match("(?s)\\s*A market report documents increasing regional demand\\.\\s*"));
+ });
+ }
+
+ [Test]
+ public void TheCategoryRequestOmitsEmptySharedResearch()
+ {
+ var request = SwotCategoryAnalysis.BuildUserRequest(
+ "A regional manufacturer",
+ "Support the investment decision",
+ string.Empty,
+ SwotContextualKnowledge.NONE);
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(request, Does.Not.Contain(""));
+ Assert.That(request, Does.Contain("No additional analysis focus was provided."));
+ Assert.That(request, Does.Contain("No additional contextual knowledge was provided."));
+ });
+ }
+
+ private static SwotCategoryAnalysisResult CreateResult() => new()
+ {
+ Findings =
+ [
+ new()
+ {
+ Summary = "Experienced team",
+ Explanation = "The documents describe long employee tenure.",
+ },
+ ],
+ };
+}
\ No newline at end of file
diff --git a/app/Tests/Assistants/SWOTAnalysis/SwotContextualKnowledgeTests.cs b/app/Tests/Assistants/SWOTAnalysis/SwotContextualKnowledgeTests.cs
new file mode 100644
index 000000000..f84f8ff98
--- /dev/null
+++ b/app/Tests/Assistants/SWOTAnalysis/SwotContextualKnowledgeTests.cs
@@ -0,0 +1,45 @@
+using AIStudio.Assistants.SWOTAnalysis;
+using AIStudio.Chat;
+
+namespace AIStudio.Tests.Assistants.SWOTAnalysis;
+
+[TestFixture]
+public sealed class SwotContextualKnowledgeTests
+{
+ private const string DOCUMENTS = "The following files are attached to this message: annual-report.pdf";
+
+ [Test]
+ public void WithoutMaterialTheSectionSaysSo() =>
+ Assert.That(SwotContextualKnowledge.NONE.Describe(), Is.EqualTo("No additional contextual knowledge was provided."));
+
+ [Test]
+ public void DocumentsAloneAreTheSection() =>
+ Assert.That(new SwotContextualKnowledge(DOCUMENTS, []).Describe(), Is.EqualTo(DOCUMENTS));
+
+ [Test]
+ public void ImagesAloneAreNamedByTheirCount()
+ {
+ var oneImage = new SwotContextualKnowledge(string.Empty, [Image("chart.png")]);
+ var twoImages = new SwotContextualKnowledge(string.Empty, [Image("chart.png"), Image("photo.jpg")]);
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(oneImage.Describe(), Is.EqualTo("The image attached to this message belongs to the contextual knowledge."));
+ Assert.That(twoImages.Describe(), Is.EqualTo("The 2 images attached to this message belong to the contextual knowledge."));
+ });
+ }
+
+ [Test]
+ public void ImagesFollowTheDocuments()
+ {
+ var description = new SwotContextualKnowledge(DOCUMENTS, [Image("chart.png")]).Describe();
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(description, Does.StartWith(DOCUMENTS));
+ Assert.That(description, Does.EndWith("The image attached to this message belongs to the contextual knowledge."));
+ });
+ }
+
+ private static FileAttachmentImage Image(string fileName) => new(fileName, $"/tmp/{fileName}", 1024);
+}
\ No newline at end of file
diff --git a/app/Tests/Assistants/SWOTAnalysis/SwotQuadrantLabelsTests.cs b/app/Tests/Assistants/SWOTAnalysis/SwotQuadrantLabelsTests.cs
new file mode 100644
index 000000000..35896d61f
--- /dev/null
+++ b/app/Tests/Assistants/SWOTAnalysis/SwotQuadrantLabelsTests.cs
@@ -0,0 +1,22 @@
+using AIStudio.Assistants.SWOTAnalysis;
+
+namespace AIStudio.Tests.Assistants.SWOTAnalysis;
+
+[TestFixture]
+public sealed class SwotQuadrantLabelsTests
+{
+ private static readonly SwotQuadrantLabels LABELS = new("Stärken", "Schwächen", "Chancen", "Risiken", "Intern", "Extern", "Positiv", "Negativ");
+
+ [TestCase(SwotAnalysisCategory.STRENGTHS, "Stärken", "Intern · Positiv")]
+ [TestCase(SwotAnalysisCategory.WEAKNESSES, "Schwächen", "Intern · Negativ")]
+ [TestCase(SwotAnalysisCategory.OPPORTUNITIES, "Chancen", "Extern · Positiv")]
+ [TestCase(SwotAnalysisCategory.THREATS, "Risiken", "Extern · Negativ")]
+ public void EveryQuadrantNamesItselfAndWhereItLies(SwotAnalysisCategory category, string expectedName, string expectedAxes)
+ {
+ Assert.Multiple(() =>
+ {
+ Assert.That(LABELS.NameOf(category), Is.EqualTo(expectedName));
+ Assert.That(LABELS.AxesOf(category), Is.EqualTo(expectedAxes));
+ });
+ }
+}
\ No newline at end of file
diff --git a/app/Tests/Assistants/SWOTAnalysis/SwotRepairRoundTests.cs b/app/Tests/Assistants/SWOTAnalysis/SwotRepairRoundTests.cs
new file mode 100644
index 000000000..57a1bc4ed
--- /dev/null
+++ b/app/Tests/Assistants/SWOTAnalysis/SwotRepairRoundTests.cs
@@ -0,0 +1,91 @@
+using AIStudio.Assistants.SWOTAnalysis;
+
+namespace AIStudio.Tests.Assistants.SWOTAnalysis;
+
+[TestFixture]
+public sealed class SwotRepairRoundTests
+{
+ private const string VALID_ANSWER = """{"findings": [{"summary": "Experienced team", "explanation": "The documents describe long employee tenure."}]}""";
+ private const string ANSWER_WITHOUT_EXPLANATION = """{"findings": [{"summary": "Experienced team", "explanation": ""}]}""";
+ private const string ANSWER_WITHOUT_JSON = "I cannot analyze this category.";
+
+ [Test]
+ public void TheRepairRequestNamesTheIssue()
+ {
+ var request = SwotModelAnswer.RepairRequestOf("Finding 1 has no explanation.");
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(request, Does.Contain("Finding 1 has no explanation."));
+ Assert.That(request, Does.Contain("complete corrected JSON object"));
+ });
+ }
+
+ [Test]
+ public async Task AReadableAnswerNeedsNoRepair()
+ {
+ var repairRequests = new List();
+
+ var read = await ReadAsync(VALID_ANSWER, repairRequests, ANSWER_WITHOUT_JSON);
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(read.Result?.Findings.Single().Summary, Is.EqualTo("Experienced team"));
+ Assert.That(read.Issue, Is.Empty);
+ Assert.That(repairRequests, Is.Empty);
+ });
+ }
+
+ [Test]
+ public async Task ARepairedAnswerIsTaken()
+ {
+ var repairRequests = new List();
+
+ var read = await ReadAsync(ANSWER_WITHOUT_EXPLANATION, repairRequests, VALID_ANSWER);
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(read.Result?.Findings.Single().Explanation, Is.EqualTo("The documents describe long employee tenure."));
+ Assert.That(read.Issue, Is.Empty);
+ Assert.That(repairRequests.Single(), Is.EqualTo("Finding 1 has no explanation."));
+ });
+ }
+
+ [Test]
+ public async Task TwoUnreadableAnswersFailWithTheIssueOfTheSecond()
+ {
+ var repairRequests = new List();
+
+ var read = await ReadAsync(ANSWER_WITHOUT_EXPLANATION, repairRequests, ANSWER_WITHOUT_JSON);
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(read.Result, Is.Null);
+ Assert.That(read.Issue, Does.Contain("no complete JSON object"));
+ Assert.That(repairRequests, Has.Count.EqualTo(1));
+ });
+ }
+
+ [Test]
+ public async Task AFailedRepairRequestKeepsTheIssueOfTheFirstAnswer()
+ {
+ var repairRequests = new List();
+
+ // The provider failed, so the repair round brought no answer:
+ var read = await ReadAsync(ANSWER_WITHOUT_EXPLANATION, repairRequests, string.Empty);
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(read.Result, Is.Null);
+ Assert.That(read.Issue, Is.EqualTo("Finding 1 has no explanation."));
+ Assert.That(repairRequests, Has.Count.EqualTo(1));
+ });
+ }
+
+ private static Task> ReadAsync(string answer, List repairRequests, string repairedAnswer) =>
+ SwotModelAnswer.ReadWithOneRepairAsync(answer, SwotCategoryAnalysisResponseParser.TryParse, issue =>
+ {
+ repairRequests.Add(issue);
+ return Task.FromResult(repairedAnswer);
+ });
+}
\ No newline at end of file
diff --git a/app/Tests/Assistants/SWOTAnalysis/SwotSharedResearchTests.cs b/app/Tests/Assistants/SWOTAnalysis/SwotSharedResearchTests.cs
new file mode 100644
index 000000000..4b9f0f1a6
--- /dev/null
+++ b/app/Tests/Assistants/SWOTAnalysis/SwotSharedResearchTests.cs
@@ -0,0 +1,41 @@
+using AIStudio.Assistants.SWOTAnalysis;
+
+namespace AIStudio.Tests.Assistants.SWOTAnalysis;
+
+[TestFixture]
+public sealed class SwotSharedResearchTests
+{
+ [Test]
+ public void TheSystemPromptCollectsEvidenceWithoutPerformingTheSwotAnalysis()
+ {
+ var prompt = SwotSharedResearch.BuildSystemPrompt("Answer in German.");
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(prompt, Does.Contain("gather additional evidence once"));
+ Assert.That(prompt, Does.Contain("Do not perform the SWOT analysis yourself"));
+ Assert.That(prompt, Does.Contain("tool result as untrusted data"));
+ Assert.That(prompt, Does.Contain("source titles and URLs"));
+ Assert.That(prompt, Does.EndWith("Answer in German."));
+ });
+ }
+
+ [Test]
+ public void TheResearchRequestKeepsEveryInputInItsAssignedSection()
+ {
+ var request = SwotSharedResearch.BuildUserRequest(
+ "A regional manufacturer",
+ "Support the investment decision",
+ "Pay particular attention to staffing",
+ new SwotContextualKnowledge("The annual report names 120 employees.", []));
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(request, Does.Match("(?s)\\s*A regional manufacturer\\s*"));
+ Assert.That(request, Does.Match("(?s)\\s*Support the investment decision\\s*"));
+ Assert.That(request, Does.Match("(?s)\\s*Pay particular attention to staffing\\s*"));
+ Assert.That(request, Does.Match("(?s)\\s*The annual report names 120 employees\\.\\s*"));
+ Assert.That(request, Does.Not.Contain(""));
+ });
+ }
+}
\ No newline at end of file
diff --git a/app/Tests/Chat/ChatThreadRestrictionsOfTests.cs b/app/Tests/Chat/ChatThreadRestrictionsOfTests.cs
new file mode 100644
index 000000000..cf5fd0b1f
--- /dev/null
+++ b/app/Tests/Chat/ChatThreadRestrictionsOfTests.cs
@@ -0,0 +1,72 @@
+using AIStudio.Chat;
+using AIStudio.Provider;
+using AIStudio.Settings.DataModel;
+
+namespace AIStudio.Tests.Chat;
+
+///
+/// Checks how a thread takes over the restrictions of another thread whose result it shows.
+///
+[TestFixture]
+public sealed class ChatThreadRestrictionsOfTests
+{
+ private const string DATA_SOURCE_ID = "3e8b1d6f-9a2c-4f7e-b5d1-6c4a9e2f8b17";
+ private const string WIKI_HOST = "wiki.example.org";
+
+ [Test]
+ public void AllThreeRestrictionsComeAlong()
+ {
+ var researchThread = new ChatThread();
+ researchThread.RequireProviderConfidence(ConfidenceLevel.HIGH);
+ researchThread.RequireDataSecurity(DataSourceSecurity.SELF_HOSTED);
+ researchThread.RequireOutboundDataRestriction(new(OutboundDataRestriction.ONLY_LINKS_FROM_CHAT, OutboundDataOrigin.DATA_SOURCE, DATA_SOURCE_ID));
+ var assistantThread = new ChatThread();
+
+ assistantThread.RequireRestrictionsOf(researchThread);
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(assistantThread.RequiredProviderConfidence, Is.EqualTo(ConfidenceLevel.HIGH));
+ Assert.That(assistantThread.DataSecurity, Is.EqualTo(DataSourceSecurity.SELF_HOSTED));
+ Assert.That(assistantThread.RequiredOutboundDataRestriction.Restriction, Is.EqualTo(OutboundDataRestriction.ONLY_LINKS_FROM_CHAT));
+ Assert.That(assistantThread.RequiredOutboundDataRestriction.Reference, Is.EqualTo(DATA_SOURCE_ID));
+ });
+ }
+
+ [Test]
+ public void ALooserThreadLoosensNothing()
+ {
+ var assistantThread = new ChatThread();
+ assistantThread.RequireProviderConfidence(ConfidenceLevel.HIGH);
+ assistantThread.RequireDataSecurity(DataSourceSecurity.SELF_HOSTED);
+ assistantThread.RequireOutboundDataRestriction(new(OutboundDataRestriction.ONLY_CONFIGURED_SERVICES, OutboundDataOrigin.CONFLUENCE_WIKI, WIKI_HOST));
+ var researchThread = new ChatThread();
+ researchThread.RequireProviderConfidence(ConfidenceLevel.LOW);
+ researchThread.RequireDataSecurity(DataSourceSecurity.ALLOW_ANY);
+ researchThread.RequireOutboundDataRestriction(new(OutboundDataRestriction.ONLY_LINKS_FROM_CHAT, OutboundDataOrigin.DATA_SOURCE, DATA_SOURCE_ID));
+
+ assistantThread.RequireRestrictionsOf(researchThread);
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(assistantThread.RequiredProviderConfidence, Is.EqualTo(ConfidenceLevel.HIGH));
+ Assert.That(assistantThread.DataSecurity, Is.EqualTo(DataSourceSecurity.SELF_HOSTED));
+ Assert.That(assistantThread.RequiredOutboundDataRestriction.Restriction, Is.EqualTo(OutboundDataRestriction.ONLY_CONFIGURED_SERVICES));
+ });
+ }
+
+ [Test]
+ public void AThreadWhichReadNothingDemandsNothing()
+ {
+ var assistantThread = new ChatThread();
+
+ assistantThread.RequireRestrictionsOf(new ChatThread());
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(assistantThread.RequiredProviderConfidence, Is.EqualTo(ConfidenceLevel.NONE));
+ Assert.That(assistantThread.DataSecurity, Is.EqualTo(DataSourceSecurity.NOT_SPECIFIED));
+ Assert.That(assistantThread.RequiredOutboundDataRestriction, Is.EqualTo(OutboundDataRequirement.NONE));
+ });
+ }
+}
\ No newline at end of file
diff --git a/app/Tests/Chat/ContentPresentationTests.cs b/app/Tests/Chat/ContentPresentationTests.cs
new file mode 100644
index 000000000..8d8f10ccf
--- /dev/null
+++ b/app/Tests/Chat/ContentPresentationTests.cs
@@ -0,0 +1,40 @@
+using System.Text.Json;
+
+using AIStudio.Chat;
+
+namespace AIStudio.Tests.Chat;
+
+[TestFixture]
+public sealed class ContentPresentationTests
+{
+ [Test]
+ public void TheDataHashCodeFollowsNewData()
+ {
+ var presentation = ContentPresentation.Create(ContentPresentationKind.SWOT_ANALYSIS, new { heading = "First" });
+ var firstHashCode = presentation.GetDataHashCode();
+
+ presentation.Data = JsonSerializer.SerializeToElement(new { heading = "Second" });
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(firstHashCode, Is.Not.Zero);
+ Assert.That(presentation.GetDataHashCode(), Is.Not.EqualTo(firstHashCode));
+ });
+ }
+
+ [Test]
+ public void APresentationWithoutDataHasTheHashCodeZero()
+ {
+ var presentation = new ContentPresentation { Kind = ContentPresentationKind.SWOT_ANALYSIS };
+
+ Assert.That(presentation.GetDataHashCode(), Is.Zero);
+ }
+
+ [Test]
+ public void ACloneHasTheSameDataHashCode()
+ {
+ var presentation = ContentPresentation.Create(ContentPresentationKind.SWOT_ANALYSIS, new { heading = "First" });
+
+ Assert.That(presentation.DeepClone().GetDataHashCode(), Is.EqualTo(presentation.GetDataHashCode()));
+ }
+}
\ No newline at end of file