diff --git a/backend/extensions/Squidex.Extensions/Squidex.Extensions.csproj b/backend/extensions/Squidex.Extensions/Squidex.Extensions.csproj index ee5279d21..d93fa49c3 100644 --- a/backend/extensions/Squidex.Extensions/Squidex.Extensions.csproj +++ b/backend/extensions/Squidex.Extensions/Squidex.Extensions.csproj @@ -14,27 +14,27 @@ - + - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + - - + + - - - - + + + + diff --git a/backend/i18n/frontend_en.json b/backend/i18n/frontend_en.json index 25afa050f..404013842 100644 --- a/backend/i18n/frontend_en.json +++ b/backend/i18n/frontend_en.json @@ -166,6 +166,11 @@ "backups.started": "Backup started, it can take several minutes to complete.", "backups.startedLabel": "Started", "backups.startFailed": "Failed to start backup.", + "chat.ask": "Ask", + "chat.description": "Use the ChatBot (usually OpenAI) to generate content. Just write a prompt and describe, what you need.\n\n\nAlso add the desired format (for example Markdown or HTML) to your prompt, dependending on the editor that you use.", + "chat.prompt": "Describe the content you want to generate", + "chat.title": "Chat Bot", + "chat.use": "Use", "clients.add": "Add Client", "clients.add.description": "Add a client to give other applications access to your content.", "clients.add.title": "Add a new Client", diff --git a/backend/i18n/frontend_fr.json b/backend/i18n/frontend_fr.json index 0cabaa972..ff2151e6b 100644 --- a/backend/i18n/frontend_fr.json +++ b/backend/i18n/frontend_fr.json @@ -166,6 +166,11 @@ "backups.started": "La sauvegarde a commencé, cela peut prendre plusieurs minutes.", "backups.startedLabel": "Commencé", "backups.startFailed": "Échec du démarrage de la sauvegarde.", + "chat.ask": "Ask", + "chat.description": "Use the ChatBot (usually OpenAI) to generate content. Just write a prompt and describe, what you need.\n\n\nAlso add the desired format (for example Markdown or HTML) to your prompt, dependending on the editor that you use.", + "chat.prompt": "Describe the content you want to generate", + "chat.title": "Chat Bot", + "chat.use": "Use", "clients.add": "Ajouter un client", "clients.add.description": "Ajoutez un client pour permettre à d'autres applications d'accéder à votre contenu.", "clients.add.title": "Ajouter un nouveau client", diff --git a/backend/i18n/frontend_it.json b/backend/i18n/frontend_it.json index 726549dc2..e86663160 100644 --- a/backend/i18n/frontend_it.json +++ b/backend/i18n/frontend_it.json @@ -166,6 +166,11 @@ "backups.started": "Backup avviato, il suo completamento potrebbe richiedere alcuni minuti.", "backups.startedLabel": "Avviato", "backups.startFailed": "Non è stato possibile avviare il backup.", + "chat.ask": "Ask", + "chat.description": "Use the ChatBot (usually OpenAI) to generate content. Just write a prompt and describe, what you need.\n\n\nAlso add the desired format (for example Markdown or HTML) to your prompt, dependending on the editor that you use.", + "chat.prompt": "Describe the content you want to generate", + "chat.title": "Chat Bot", + "chat.use": "Use", "clients.add": "Aggiungi un Client", "clients.add.description": "Add a client to give other applications access to your content.", "clients.add.title": "Add a new Client", diff --git a/backend/i18n/frontend_nl.json b/backend/i18n/frontend_nl.json index 2ac382621..08154b5ad 100644 --- a/backend/i18n/frontend_nl.json +++ b/backend/i18n/frontend_nl.json @@ -166,6 +166,11 @@ "backups.started": "Back-up gestart, het kan enkele minuten duren om te voltooien.", "backups.startedLabel": "Gestart", "backups.startFailed": "Starten van back-up is mislukt.", + "chat.ask": "Ask", + "chat.description": "Use the ChatBot (usually OpenAI) to generate content. Just write a prompt and describe, what you need.\n\n\nAlso add the desired format (for example Markdown or HTML) to your prompt, dependending on the editor that you use.", + "chat.prompt": "Describe the content you want to generate", + "chat.title": "Chat Bot", + "chat.use": "Use", "clients.add": "Client toevoegen", "clients.add.description": "Voeg een client toe om andere applicaties toegang te geven tot uw inhoud.", "clients.add.title": "Client toevoegen", diff --git a/backend/i18n/frontend_pt.json b/backend/i18n/frontend_pt.json index 0f3f7a5a7..a7283bc0c 100644 --- a/backend/i18n/frontend_pt.json +++ b/backend/i18n/frontend_pt.json @@ -166,6 +166,11 @@ "backups.started": "O reforço começou, pode levar vários minutos para ser concluído.", "backups.startedLabel": "Começou", "backups.startFailed": "Falhou em começar o backup.", + "chat.ask": "Ask", + "chat.description": "Use the ChatBot (usually OpenAI) to generate content. Just write a prompt and describe, what you need.\n\n\nAlso add the desired format (for example Markdown or HTML) to your prompt, dependending on the editor that you use.", + "chat.prompt": "Describe the content you want to generate", + "chat.title": "Chat Bot", + "chat.use": "Use", "clients.add": "Adicionar Cliente", "clients.add.description": "Adicione um cliente para dar a outras aplicações acesso ao seu conteúdo.", "clients.add.title": "Adicionar um novo Cliente", diff --git a/backend/i18n/frontend_zh.json b/backend/i18n/frontend_zh.json index e704c1548..b9f457023 100644 --- a/backend/i18n/frontend_zh.json +++ b/backend/i18n/frontend_zh.json @@ -166,6 +166,11 @@ "backups.started": "备份已开始,可能需要几分钟才能完成。", "backups.startedLabel": "开始", "backups.startFailed": "启动备份失败。", + "chat.ask": "Ask", + "chat.description": "Use the ChatBot (usually OpenAI) to generate content. Just write a prompt and describe, what you need.\n\n\nAlso add the desired format (for example Markdown or HTML) to your prompt, dependending on the editor that you use.", + "chat.prompt": "Describe the content you want to generate", + "chat.title": "Chat Bot", + "chat.use": "Use", "clients.add": "添加客户端", "clients.add.description": "Add a client to give other applications access to your content.", "clients.add.title": "Add a new Client", diff --git a/backend/i18n/source/frontend_en.json b/backend/i18n/source/frontend_en.json index 25afa050f..404013842 100644 --- a/backend/i18n/source/frontend_en.json +++ b/backend/i18n/source/frontend_en.json @@ -166,6 +166,11 @@ "backups.started": "Backup started, it can take several minutes to complete.", "backups.startedLabel": "Started", "backups.startFailed": "Failed to start backup.", + "chat.ask": "Ask", + "chat.description": "Use the ChatBot (usually OpenAI) to generate content. Just write a prompt and describe, what you need.\n\n\nAlso add the desired format (for example Markdown or HTML) to your prompt, dependending on the editor that you use.", + "chat.prompt": "Describe the content you want to generate", + "chat.title": "Chat Bot", + "chat.use": "Use", "clients.add": "Add Client", "clients.add.description": "Add a client to give other applications access to your content.", "clients.add.title": "Add a new Client", diff --git a/backend/src/Migrations/Migrations.csproj b/backend/src/Migrations/Migrations.csproj index d44374a82..639e53508 100644 --- a/backend/src/Migrations/Migrations.csproj +++ b/backend/src/Migrations/Migrations.csproj @@ -6,7 +6,7 @@ enable - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/Component.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/Component.cs index 4803ec4b3..abf875f7f 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/Component.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/Component.cs @@ -18,6 +18,8 @@ public sealed record Component(string Type, JsonObject Data, Schema Schema) { public const string Discriminator = "schemaId"; + public const string Descriptor = "schemaName"; + public string Type { get; } = Guard.NotNullOrEmpty(Type); public Schema Schema { get; } = Guard.NotNull(Schema); diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj b/backend/src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj index 9c897a285..cd3df41a9 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj @@ -12,7 +12,7 @@ True - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/AddSchemaNames.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/AddSchemaNames.cs index 6f3ab0d2e..21b5e8325 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/AddSchemaNames.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/AddSchemaNames.cs @@ -28,7 +28,7 @@ public sealed class AddSchemaNames : IContentItemConverter return source; } - if (source.ContainsKey("schemaName")) + if (source.ContainsKey(Component.Descriptor)) { return source; } @@ -42,7 +42,7 @@ public sealed class AddSchemaNames : IContentItemConverter if (components.TryGetValue(id, out var component)) { - source["schemaName"] = component.Name; + source[Component.Descriptor] = component.Name; } return source; diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Properties/Resources.Designer.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Properties/Resources.Designer.cs index 1c507d92a..b51169662 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Properties/Resources.Designer.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Properties/Resources.Designer.cs @@ -8,298 +8,317 @@ // //------------------------------------------------------------------------------ -namespace Squidex.Domain.Apps.Core.Properties; -using System; - - -/// -/// A strongly-typed resource class, for looking up localized strings, etc. -/// -// This class was auto-generated by the StronglyTypedResourceBuilder -// class via a tool like ResGen or Visual Studio. -// To add or remove a member, edit your .ResX file then rerun ResGen -// with the /str option, or rebuild your VS project. -[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] -[global::System.Diagnostics.DebuggerNonUserCodeAttribute()] -[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] -internal class Resources { - - private static global::System.Resources.ResourceManager resourceMan; +namespace Squidex.Domain.Apps.Core.Properties { + using System; - private static global::System.Globalization.CultureInfo resourceCulture; - - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal Resources() { - } /// - /// Returns the cached ResourceManager instance used by this class. + /// A strongly-typed resource class, for looking up localized strings, etc. /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { - get { - if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Squidex.Domain.Apps.Core.Properties.Resources", typeof(Resources).Assembly); - resourceMan = temp; + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Squidex.Domain.Apps.Core.Properties.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; } - return resourceMan; } - } - - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { - get { - return resourceCulture; + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } } - set { - resourceCulture = value; + + /// + /// Looks up a localized string similar to The download URL to the asset.. + /// + internal static string ScriptingAssetContentAppUrl { + get { + return ResourceManager.GetString("ScriptingAssetContentAppUrl", resourceCulture); + } } - } - - /// - /// Looks up a localized string similar to The download URL to the asset.. - /// - internal static string ScriptingAssetContentAppUrl { - get { - return ResourceManager.GetString("ScriptingAssetContentAppUrl", resourceCulture); + + /// + /// Looks up a localized string similar to The download URL to the asset using the file slug instead of the ID.. + /// + internal static string ScriptingAssetContentSlugUrl { + get { + return ResourceManager.GetString("ScriptingAssetContentSlugUrl", resourceCulture); + } } - } - - /// - /// Looks up a localized string similar to The download URL to the asset using the file slug instead of the ID.. - /// - internal static string ScriptingAssetContentSlugUrl { - get { - return ResourceManager.GetString("ScriptingAssetContentSlugUrl", resourceCulture); + + /// + /// Looks up a localized string similar to The download URL to the asset without the app name (deprecated).. + /// + internal static string ScriptingAssetContentUrl { + get { + return ResourceManager.GetString("ScriptingAssetContentUrl", resourceCulture); + } } - } - - /// - /// Looks up a localized string similar to The download URL to the asset without the app name (deprecated).. - /// - internal static string ScriptingAssetContentUrl { - get { - return ResourceManager.GetString("ScriptingAssetContentUrl", resourceCulture); + + /// + /// Looks up a localized string similar to Counts the number of characters in a text. Useful in combination with html2Text or markdown2Text.. + /// + internal static string ScriptingCharacterCount { + get { + return ResourceManager.GetString("ScriptingCharacterCount", resourceCulture); + } } - } - - /// - /// Looks up a localized string similar to Counts the number of characters in a text. Useful in combination with html2Text or markdown2Text.. - /// - internal static string ScriptingCharacterCount { - get { - return ResourceManager.GetString("ScriptingCharacterCount", resourceCulture); + + /// + /// Looks up a localized string similar to Completes the script when an async method is used.. + /// + internal static string ScriptingComplete { + get { + return ResourceManager.GetString("ScriptingComplete", resourceCulture); + } } - } - - /// - /// Looks up a localized string similar to Completes the script when an async method is used.. - /// - internal static string ScriptingComplete { - get { - return ResourceManager.GetString("ScriptingComplete", resourceCulture); + + /// + /// Looks up a localized string similar to The status of the content.. + /// + internal static string ScriptingContentAction { + get { + return ResourceManager.GetString("ScriptingContentAction", resourceCulture); + } } - } - - /// - /// Looks up a localized string similar to The status of the content.. - /// - internal static string ScriptingContentAction { - get { - return ResourceManager.GetString("ScriptingContentAction", resourceCulture); + + /// + /// Looks up a localized string similar to The URL to the content in the UI.. + /// + internal static string ScriptingContentUrl { + get { + return ResourceManager.GetString("ScriptingContentUrl", resourceCulture); + } } - } - - /// - /// Looks up a localized string similar to The URL to the content in the UI.. - /// - internal static string ScriptingContentUrl { - get { - return ResourceManager.GetString("ScriptingContentUrl", resourceCulture); + + /// + /// Looks up a localized string similar to Makes a DELETE request to the defined URL and parses the result as JSON. Headers are optional.. + /// + internal static string ScriptingDeleteJson { + get { + return ResourceManager.GetString("ScriptingDeleteJson", resourceCulture); + } } - } - - /// - /// Looks up a localized string similar to Makes a DELETE request to the defined URL and parses the result as JSON. Headers are optional.. - /// - internal static string ScriptingDeleteJson { - get { - return ResourceManager.GetString("ScriptingDeleteJson", resourceCulture); + + /// + /// Looks up a localized string similar to Tell Squidex to not allow the current operation and to return a 400 (BadRequest).. + /// + internal static string ScriptingDisallow { + get { + return ResourceManager.GetString("ScriptingDisallow", resourceCulture); + } } - } - - /// - /// Looks up a localized string similar to Tell Squidex to not allow the current operation and to return a 400 (BadRequest).. - /// - internal static string ScriptingDisallow { - get { - return ResourceManager.GetString("ScriptingDisallow", resourceCulture); + + /// + /// Looks up a localized string similar to Formats a JavaScript date object using the specified pattern.. + /// + internal static string ScriptingFormatDate { + get { + return ResourceManager.GetString("ScriptingFormatDate", resourceCulture); + } } - } - - /// - /// Looks up a localized string similar to Formats a JavaScript date object using the specified pattern.. - /// - internal static string ScriptingFormatDate { - get { - return ResourceManager.GetString("ScriptingFormatDate", resourceCulture); + + /// + /// Looks up a localized string similar to Formats a JavaScript date object using the specified pattern.. + /// + internal static string ScriptingFormatTime { + get { + return ResourceManager.GetString("ScriptingFormatTime", resourceCulture); + } } - } - - /// - /// Looks up a localized string similar to Formats a JavaScript date object using the specified pattern.. - /// - internal static string ScriptingFormatTime { - get { - return ResourceManager.GetString("ScriptingFormatTime", resourceCulture); + + /// + /// Looks up a localized string similar to Uses OpenAI or other machine learning platforms to generate content from a prompt.. + /// + internal static string ScriptingGenerate { + get { + return ResourceManager.GetString("ScriptingGenerate", resourceCulture); + } } - } - - /// - /// Looks up a localized string similar to Makes a GET request to the defined URL and parses the result as JSON. Headers are optional.. - /// - internal static string ScriptingGetJSON { - get { - return ResourceManager.GetString("ScriptingGetJSON", resourceCulture); + + /// + /// Looks up a localized string similar to Makes a GET request to the defined URL and parses the result as JSON. Headers are optional.. + /// + internal static string ScriptingGetJSON { + get { + return ResourceManager.GetString("ScriptingGetJSON", resourceCulture); + } } - } - - /// - /// Looks up a localized string similar to Generates a guid.. - /// - internal static string ScriptingGuid { - get { - return ResourceManager.GetString("ScriptingGuid", resourceCulture); + + /// + /// Looks up a localized string similar to Generates a guid.. + /// + internal static string ScriptingGuid { + get { + return ResourceManager.GetString("ScriptingGuid", resourceCulture); + } } - } - - /// - /// Looks up a localized string similar to Converts a HTML string to plain text.. - /// - internal static string ScriptingHtml2Text { - get { - return ResourceManager.GetString("ScriptingHtml2Text", resourceCulture); + + /// + /// Looks up a localized string similar to Converts a HTML string to plain text.. + /// + internal static string ScriptingHtml2Text { + get { + return ResourceManager.GetString("ScriptingHtml2Text", resourceCulture); + } } - } - - /// - /// Looks up a localized string similar to Converts a markdown string to plain text.. - /// - internal static string ScriptingMarkdown2Text { - get { - return ResourceManager.GetString("ScriptingMarkdown2Text", resourceCulture); + + /// + /// Looks up a localized string similar to Converts a markdown string to plain text.. + /// + internal static string ScriptingMarkdown2Text { + get { + return ResourceManager.GetString("ScriptingMarkdown2Text", resourceCulture); + } } - } - - /// - /// Looks up a localized string similar to Calculate the MD5 hash from a given string. Use this method for hashing passwords, when backwards compatibility is important.. - /// - internal static string ScriptingMD5 { - get { - return ResourceManager.GetString("ScriptingMD5", resourceCulture); + + /// + /// Looks up a localized string similar to Calculate the MD5 hash from a given string. Use this method for hashing passwords, when backwards compatibility is important.. + /// + internal static string ScriptingMD5 { + get { + return ResourceManager.GetString("ScriptingMD5", resourceCulture); + } } - } - - /// - /// Looks up a localized string similar to Makes a PATCH request to the defined URL and parses the result as JSON. Headers are optional.. - /// - internal static string ScriptingPatchJson { - get { - return ResourceManager.GetString("ScriptingPatchJson", resourceCulture); + + /// + /// Looks up a localized string similar to Makes a PATCH request to the defined URL and parses the result as JSON. Headers are optional.. + /// + internal static string ScriptingPatchJson { + get { + return ResourceManager.GetString("ScriptingPatchJson", resourceCulture); + } } - } - - /// - /// Looks up a localized string similar to Makes a POST request to the defined URL and parses the result as JSON. Headers are optional.. - /// - internal static string ScriptingPostJSON { - get { - return ResourceManager.GetString("ScriptingPostJSON", resourceCulture); + + /// + /// Looks up a localized string similar to Makes a POST request to the defined URL and parses the result as JSON. Headers are optional.. + /// + internal static string ScriptingPostJSON { + get { + return ResourceManager.GetString("ScriptingPostJSON", resourceCulture); + } } - } - - /// - /// Looks up a localized string similar to Makes a PUT request to the defined URL and parses the result as JSON. Headers are optional.. - /// - internal static string ScriptingPutJson { - get { - return ResourceManager.GetString("ScriptingPutJson", resourceCulture); + + /// + /// Looks up a localized string similar to Makes a PUT request to the defined URL and parses the result as JSON. Headers are optional.. + /// + internal static string ScriptingPutJson { + get { + return ResourceManager.GetString("ScriptingPutJson", resourceCulture); + } } - } - - /// - /// Looks up a localized string similar to Tell Squidex to reject the current operation and to return a 403 (Forbidden).. - /// - internal static string ScriptingReject { - get { - return ResourceManager.GetString("ScriptingReject", resourceCulture); + + /// + /// Looks up a localized string similar to Tell Squidex to reject the current operation and to return a 403 (Forbidden).. + /// + internal static string ScriptingReject { + get { + return ResourceManager.GetString("ScriptingReject", resourceCulture); + } } - } - - /// - /// Looks up a localized string similar to Tell Squidex that you have modified the data and that the change should be applied.. - /// - internal static string ScriptingReplace { - get { - return ResourceManager.GetString("ScriptingReplace", resourceCulture); + + /// + /// Looks up a localized string similar to Tell Squidex that you have modified the data and that the change should be applied.. + /// + internal static string ScriptingReplace { + get { + return ResourceManager.GetString("ScriptingReplace", resourceCulture); + } } - } - - /// - /// Looks up a localized string similar to Calculate the SHA256 hash from a given string. Use this method for hashing passwords.. - /// - internal static string ScriptingSHA256 { - get { - return ResourceManager.GetString("ScriptingSHA256", resourceCulture); + + /// + /// Looks up a localized string similar to Calculate the SHA256 hash from a given string. Use this method for hashing passwords.. + /// + internal static string ScriptingSHA256 { + get { + return ResourceManager.GetString("ScriptingSHA256", resourceCulture); + } } - } - - /// - /// Looks up a localized string similar to Calculate the SHA256 hash from a given string. Use this method for hashing passwords.. - /// - internal static string ScriptingSHA512 { - get { - return ResourceManager.GetString("ScriptingSHA512", resourceCulture); + + /// + /// Looks up a localized string similar to Calculate the SHA256 hash from a given string. Use this method for hashing passwords.. + /// + internal static string ScriptingSHA512 { + get { + return ResourceManager.GetString("ScriptingSHA512", resourceCulture); + } } - } - - /// - /// Looks up a localized string similar to Calculates the slug of a text by removing all special characters and whitespaces to create a friendly term that can be used for SEO-friendly URLs.. - /// - internal static string ScriptingSlugify { - get { - return ResourceManager.GetString("ScriptingSlugify", resourceCulture); + + /// + /// Looks up a localized string similar to Calculates the slug of a text by removing all special characters and whitespaces to create a friendly term that can be used for SEO-friendly URLs.. + /// + internal static string ScriptingSlugify { + get { + return ResourceManager.GetString("ScriptingSlugify", resourceCulture); + } } - } - - /// - /// Looks up a localized string similar to Converts a text to camelCase.. - /// - internal static string ScriptingToCamelCase { - get { - return ResourceManager.GetString("ScriptingToCamelCase", resourceCulture); + + /// + /// Looks up a localized string similar to Converts a text to camelCase.. + /// + internal static string ScriptingToCamelCase { + get { + return ResourceManager.GetString("ScriptingToCamelCase", resourceCulture); + } } - } - - /// - /// Looks up a localized string similar to Converts a text to PascalCase. - /// - internal static string ScriptingToPascalCase { - get { - return ResourceManager.GetString("ScriptingToPascalCase", resourceCulture); + + /// + /// Looks up a localized string similar to Converts a text to PascalCase. + /// + internal static string ScriptingToPascalCase { + get { + return ResourceManager.GetString("ScriptingToPascalCase", resourceCulture); + } } - } - - /// - /// Looks up a localized string similar to Counts the number of words in a text. Useful in combination with html2Text or markdown2Text.. - /// - internal static string ScriptingWordCount { - get { - return ResourceManager.GetString("ScriptingWordCount", resourceCulture); + + /// + /// Looks up a localized string similar to Translate from the source text to the specified language.. + /// + internal static string ScriptingTranslate { + get { + return ResourceManager.GetString("ScriptingTranslate", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Counts the number of words in a text. Useful in combination with html2Text or markdown2Text.. + /// + internal static string ScriptingWordCount { + get { + return ResourceManager.GetString("ScriptingWordCount", resourceCulture); + } } } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Properties/Resources.resx b/backend/src/Squidex.Domain.Apps.Core.Operations/Properties/Resources.resx index f81ab5834..786ba4da3 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Properties/Resources.resx +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Properties/Resources.resx @@ -150,6 +150,9 @@ Formats a JavaScript date object using the specified pattern. + + Uses OpenAI or other machine learning platforms to generate content from a prompt. + Makes a GET request to the defined URL and parses the result as JSON. Headers are optional. @@ -195,6 +198,9 @@ Converts a text to PascalCase + + Translate from the source text to the specified language. + Counts the number of words in a text. Useful in combination with html2Text or markdown2Text. diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Extensions/HttpJintExtension.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Extensions/HttpJintExtension.cs index ecf46a481..727a0006f 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Extensions/HttpJintExtension.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Extensions/HttpJintExtension.cs @@ -17,8 +17,8 @@ namespace Squidex.Domain.Apps.Core.Scripting.Extensions; public sealed class HttpJintExtension : IJintExtension, IScriptDescriptor { - private delegate void HttpJson(string url, Action callback, JsValue? headers = null, bool ignoreError = false); - private delegate void HttpJsonWithBody(string url, JsValue post, Action callback, JsValue? headers = null, bool ignoreError = false); + private delegate void HttpJsonDelegate(string url, Action callback, JsValue? headers = null, bool ignoreError = false); + private delegate void HttpJsonWithBodyDelegate(string url, JsValue post, Action callback, JsValue? headers = null, bool ignoreError = false); private readonly IHttpClientFactory httpClientFactory; public HttpJintExtension(IHttpClientFactory httpClientFactory) @@ -35,32 +35,9 @@ public sealed class HttpJintExtension : IJintExtension, IScriptDescriptor AddMethod(context, HttpMethod.Get, "getJSON"); } - public void Describe(AddDescription describe, ScriptScope scope) - { - if (!scope.HasFlag(ScriptScope.Async)) - { - return; - } - - describe(JsonType.Function, "getJSON(url, callback, headers?, ignoreError?)", - Resources.ScriptingGetJSON); - - describe(JsonType.Function, "postJSON(url, body, callback, headers?, ignoreError?)", - Resources.ScriptingPostJSON); - - describe(JsonType.Function, "putJSON(url, body, callback, headers?, ignoreError?)", - Resources.ScriptingPutJson); - - describe(JsonType.Function, "patchJSON(url, body, callback, headers?, ignoreError?)", - Resources.ScriptingPatchJson); - - describe(JsonType.Function, "deleteJSON(url, callback, headers?, ignoreError?)", - Resources.ScriptingDeleteJson); - } - private void AddMethod(ScriptExecutionContext context, HttpMethod method, string name) { - var action = new HttpJson((url, callback, headers, ignoreError) => + var action = new HttpJsonDelegate((url, callback, headers, ignoreError) => { Request(context, method, url, null, callback, headers, ignoreError); }); @@ -70,7 +47,7 @@ public sealed class HttpJintExtension : IJintExtension, IScriptDescriptor private void AddBodyMethod(ScriptExecutionContext context, HttpMethod method, string name) { - var action = new HttpJsonWithBody((url, body, callback, headers, ignoreError) => + var action = new HttpJsonWithBodyDelegate((url, body, callback, headers, ignoreError) => { Request(context, method, url, body, callback, headers, ignoreError); }); @@ -80,21 +57,20 @@ public sealed class HttpJintExtension : IJintExtension, IScriptDescriptor private void Request(ScriptExecutionContext context, HttpMethod method, string url, JsValue? body, Action callback, JsValue? headers, bool ignoreError) { - context.Schedule(async (scheduler, ct) => + if (!Uri.TryCreate(url, UriKind.Absolute, out var uri)) { - if (!Uri.TryCreate(url, UriKind.Absolute, out var uri)) - { - throw new JavaScriptException("URL is not valid."); - } + throw new JavaScriptException("URL is not valid."); + } - if (callback == null) - { - throw new JavaScriptException("Callback is not defined."); - } + if (callback == null) + { + throw new JavaScriptException("Callback is not defined."); + } + context.Schedule(async (scheduler, ct) => + { try { - var httpClient = httpClientFactory.CreateClient("Jint"); var request = CreateRequest(context, method, uri, body, headers); @@ -186,4 +162,27 @@ public sealed class HttpJintExtension : IJintExtension, IScriptDescriptor return jsonValue; } + + public void Describe(AddDescription describe, ScriptScope scope) + { + if (!scope.HasFlag(ScriptScope.Async)) + { + return; + } + + describe(JsonType.Function, "getJSON(url, callback, headers?, ignoreError?)", + Resources.ScriptingGetJSON); + + describe(JsonType.Function, "postJSON(url, body, callback, headers?, ignoreError?)", + Resources.ScriptingPostJSON); + + describe(JsonType.Function, "putJSON(url, body, callback, headers?, ignoreError?)", + Resources.ScriptingPutJson); + + describe(JsonType.Function, "patchJSON(url, body, callback, headers?, ignoreError?)", + Resources.ScriptingPatchJson); + + describe(JsonType.Function, "deleteJSON(url, callback, headers?, ignoreError?)", + Resources.ScriptingDeleteJson); + } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Extensions/StringAsyncJintExtension.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Extensions/StringAsyncJintExtension.cs new file mode 100644 index 000000000..a136efdc9 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Extensions/StringAsyncJintExtension.cs @@ -0,0 +1,114 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Jint.Native; +using Jint.Runtime; +using Squidex.Domain.Apps.Core.Properties; +using Squidex.Text.ChatBots; +using Squidex.Text.Translations; + +#pragma warning disable CA1826 // Do not use Enumerable methods on indexable collections + +namespace Squidex.Domain.Apps.Core.Scripting.Extensions; + +public sealed class StringAsyncJintExtension : IJintExtension, IScriptDescriptor +{ + private delegate void TextGenerateDelegate(string prompt, Action callback); + private delegate void TextTranslateDelegate(string text, string language, Action callback, string sourceLanguage); + private readonly ITranslator translator; + private readonly IChatBot chatBot; + + public StringAsyncJintExtension(ITranslator translator, IChatBot chatBot) + { + this.translator = translator; + this.chatBot = chatBot; + } + + public void ExtendAsync(ScriptExecutionContext context) + { + var generate = new TextGenerateDelegate((prompt, callback) => + { + Generate(context, prompt, callback); + }); + + var translate = new TextTranslateDelegate((text, language, callback, sourceLanguage) => + { + Translate(context, text, language, callback, sourceLanguage); + }); + + context.Engine.SetValue("generate", generate); + context.Engine.SetValue("translate", translate); + } + + private void Generate(ScriptExecutionContext context, string prompt, Action callback) + { + if (callback == null) + { + throw new JavaScriptException("Callback is not defined."); + } + + context.Schedule(async (scheduler, ct) => + { + try + { + if (string.IsNullOrWhiteSpace(prompt)) + { + scheduler.Run(callback, JsValue.Null); + return; + } + + var choices = await chatBot.AskQuestionAsync(prompt, ct); + + scheduler.Run(callback, JsValue.FromObject(context.Engine, choices.FirstOrDefault())); + } + catch (Exception ex) + { + throw new JavaScriptException(ex.Message); + } + }); + } + + private void Translate(ScriptExecutionContext context, string text, string language, Action callback, string sourceLanguage) + { + if (callback == null) + { + throw new JavaScriptException("Callback is not defined."); + } + + context.Schedule(async (scheduler, ct) => + { + try + { + if (string.IsNullOrWhiteSpace(text) || string.IsNullOrWhiteSpace(language)) + { + scheduler.Run(callback, JsValue.Null); + return; + } + + var translation = await translator.TranslateAsync(text, language, sourceLanguage, ct); + + scheduler.Run(callback, JsValue.FromObject(context.Engine, translation.Text)); + } + catch (Exception ex) + { + throw new JavaScriptException(ex.Message); + } + }); + } + + public void Describe(AddDescription describe, ScriptScope scope) + { + if (scope.HasFlag(ScriptScope.Async)) + { + describe(JsonType.Function, "generate(prompt, callback?", + Resources.ScriptingGenerate); + + describe(JsonType.Function, "translate(text, language, callback, sourceLanguage?", + Resources.ScriptingTranslate); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj b/backend/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj index 950fe151f..9f780a395 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj @@ -21,14 +21,14 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/Adapt.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/Adapt.cs index c03912bb8..ce4c3a940 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/Adapt.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/Adapt.cs @@ -20,33 +20,17 @@ public static class Adapt public static IReadOnlyDictionary PropertyMap { - get - { - if (propertyMap == null) - { - propertyMap = - BsonClassMap.LookupClassMap(typeof(MongoContentEntity)).AllMemberMaps - .ToDictionary( - x => x.MemberName, - x => x.ElementName, - StringComparer.OrdinalIgnoreCase); - } - - return propertyMap; - } + get => propertyMap ??= + BsonClassMap.LookupClassMap(typeof(MongoContentEntity)).AllMemberMaps + .ToDictionary( + x => x.MemberName, + x => x.ElementName, + StringComparer.OrdinalIgnoreCase); } public static IReadOnlyDictionary PathMap { - get - { - if (pathMap == null) - { - pathMap = PropertyMap.ToDictionary(x => x.Key, x => (PropertyPath)x.Value); - } - - return pathMap; - } + get => pathMap ??= PropertyMap.ToDictionary(x => x.Key, x => (PropertyPath)x.Value); } public static PropertyPath MapPath(PropertyPath path) diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Squidex.Domain.Apps.Entities.MongoDb.csproj b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Squidex.Domain.Apps.Entities.MongoDb.csproj index 8f195a77b..93bf05b07 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Squidex.Domain.Apps.Entities.MongoDb.csproj +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Squidex.Domain.Apps.Entities.MongoDb.csproj @@ -19,11 +19,11 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Text/Tokenizer.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Text/Tokenizer.cs index 4a23b9666..8ae968d34 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Text/Tokenizer.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Text/Tokenizer.cs @@ -68,7 +68,7 @@ public static class Tokenizer if (query.Length > 2 && query[2] == ':') { - textLanguage = query.Substring(0, 2); + textLanguage = query[..2]; textReader.Read(); textReader.Read(); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetsJintExtension.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetsJintExtension.cs index 89888dcba..1a9f21ad1 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetsJintExtension.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetsJintExtension.cs @@ -38,36 +38,12 @@ public sealed class AssetsJintExtension : IJintExtension, IScriptDescriptor public void ExtendAsync(ScriptExecutionContext context) { - AddAssetText(context); - AddAssetBlurHash(context); - AddAsset(context); + AddGetAssetText(context); + AddGetAssetBlurHash(context); + AddGetAssetObject(context); } - public void Describe(AddDescription describe, ScriptScope scope) - { - if (!scope.HasFlag(ScriptScope.Async)) - { - return; - } - - describe(JsonType.Function, "getAsset(ids, callback)", - Resources.ScriptingGetAsset, - deprecationReason: Resources.ScriptingGetAssetDeprecated); - - describe(JsonType.Function, "getAssetV2(ids, callback)", - Resources.ScriptingGetAssetV2); - - describe(JsonType.Function, "getAssets(ids, callback)", - Resources.ScriptingGetAssets); - - describe(JsonType.Function, "getAssetText(asset, callback, encoding?)", - Resources.ScriptingGetAssetText); - - describe(JsonType.Function, "getAssetBlurHash(asset, callback, x?, y?)", - Resources.ScriptingGetBlurHash); - } - - private void AddAsset(ScriptExecutionContext context) + private void AddGetAssetObject(ScriptExecutionContext context) { if (!context.TryGetValue("appId", out var appId)) { @@ -94,7 +70,7 @@ public sealed class AssetsJintExtension : IJintExtension, IScriptDescriptor context.Engine.SetValue("getAssets", getAssets); } - private void AddAssetText(ScriptExecutionContext context) + private void AddGetAssetText(ScriptExecutionContext context) { var action = new GetAssetTextDelegate((references, callback, encoding) => { @@ -104,7 +80,7 @@ public sealed class AssetsJintExtension : IJintExtension, IScriptDescriptor context.Engine.SetValue("getAssetText", action); } - private void AddAssetBlurHash(ScriptExecutionContext context) + private void AddGetAssetBlurHash(ScriptExecutionContext context) { var getBlurHash = new GetBlurHashDelegate((input, callback, componentX, componentY) => { @@ -116,7 +92,10 @@ public sealed class AssetsJintExtension : IJintExtension, IScriptDescriptor private void GetText(ScriptExecutionContext context, JsValue input, Action callback, JsValue? encoding) { - Guard.NotNull(callback); + if (callback == null) + { + throw new JavaScriptException("Callback is not defined."); + } context.Schedule(async (scheduler, ct) => { @@ -166,7 +145,10 @@ public sealed class AssetsJintExtension : IJintExtension, IScriptDescriptor private void GetBlurHash(ScriptExecutionContext context, JsValue input, Action callback, JsValue? componentX, JsValue? componentY) { - Guard.NotNull(callback); + if (callback == null) + { + throw new JavaScriptException("Callback is not defined."); + } context.Schedule(async (scheduler, ct) => { @@ -229,7 +211,10 @@ public sealed class AssetsJintExtension : IJintExtension, IScriptDescriptor private void GetAssets(ScriptExecutionContext context, DomainId appId, ClaimsPrincipal user, JsValue references, Action callback) { - Guard.NotNull(callback); + if (callback == null) + { + throw new JavaScriptException("Callback is not defined."); + } context.Schedule(async (scheduler, ct) => { @@ -311,4 +296,28 @@ public sealed class AssetsJintExtension : IJintExtension, IScriptDescriptor return app; } + + public void Describe(AddDescription describe, ScriptScope scope) + { + if (!scope.HasFlag(ScriptScope.Async)) + { + return; + } + + describe(JsonType.Function, "getAsset(ids, callback)", + Resources.ScriptingGetAsset, + deprecationReason: Resources.ScriptingGetAssetDeprecated); + + describe(JsonType.Function, "getAssetV2(ids, callback)", + Resources.ScriptingGetAssetV2); + + describe(JsonType.Function, "getAssets(ids, callback)", + Resources.ScriptingGetAssets); + + describe(JsonType.Function, "getAssetText(asset, callback, encoding?)", + Resources.ScriptingGetAssetText); + + describe(JsonType.Function, "getAssetBlurHash(asset, callback, x?, y?)", + Resources.ScriptingGetBlurHash); + } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Counter/CounterJintExtension.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Counter/CounterJintExtension.cs index dbf6e802c..94886db15 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Counter/CounterJintExtension.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Counter/CounterJintExtension.cs @@ -15,8 +15,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.Counter; public sealed class CounterJintExtension : IJintExtension, IScriptDescriptor { - private delegate long CounterReset(string name, long value = 0); - private delegate void CounterResetV2(string name, Action? callback = null, long value = 0); + private delegate long CounterResetDelegate(string name, long value = 0); + private delegate void CounterResetV2Delegate(string name, Action? callback = null, long value = 0); private readonly ICounterService counterService; public CounterJintExtension(ICounterService counterService) @@ -38,7 +38,7 @@ public sealed class CounterJintExtension : IJintExtension, IScriptDescriptor context.Engine.SetValue("incrementCounter", increment); - var reset = new CounterReset((name, value) => + var reset = new CounterResetDelegate((name, value) => { return Reset(appId, name, value); }); @@ -60,7 +60,7 @@ public sealed class CounterJintExtension : IJintExtension, IScriptDescriptor context.Engine.SetValue("incrementCounterV2", increment); - var reset = new CounterResetV2((name, callback, value) => + var reset = new CounterResetV2Delegate((name, callback, value) => { ResetV2(context, appId, name, callback, value); }); @@ -68,21 +68,6 @@ public sealed class CounterJintExtension : IJintExtension, IScriptDescriptor context.Engine.SetValue("resetCounterV2", reset); } - public void Describe(AddDescription describe, ScriptScope scope) - { - describe(JsonType.Function, "incrementCounter(name)", - Resources.ScriptingIncrementCounter); - - describe(JsonType.Function, "incrementCounterV2(name, callback?)", - Resources.ScriptingIncrementCounterV2); - - describe(JsonType.Function, "resetCounter(name, value?)", - Resources.ScriptingResetCounter); - - describe(JsonType.Function, "resetCounter(name, callback?, value?)", - Resources.ScriptingResetCounterV2); - } - private long Increment(DomainId appId, string name) { return AsyncHelper.Sync(() => counterService.IncrementAsync(appId, name)); @@ -118,4 +103,19 @@ public sealed class CounterJintExtension : IJintExtension, IScriptDescriptor } }); } + + public void Describe(AddDescription describe, ScriptScope scope) + { + describe(JsonType.Function, "incrementCounter(name)", + Resources.ScriptingIncrementCounter); + + describe(JsonType.Function, "incrementCounterV2(name, callback?)", + Resources.ScriptingIncrementCounterV2); + + describe(JsonType.Function, "resetCounter(name, value?)", + Resources.ScriptingResetCounter); + + describe(JsonType.Function, "resetCounter(name, callback?, value?)", + Resources.ScriptingResetCounterV2); + } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ComponentGraphType.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ComponentGraphType.cs index e8c063c59..98fd028aa 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ComponentGraphType.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ComponentGraphType.cs @@ -27,6 +27,7 @@ internal sealed class ComponentGraphType : ObjectGraphType Description = $"The structure of the {schemaInfo.DisplayName} component schema."; AddField(ContentFields.SchemaId); + AddField(ContentFields.SchemaName); foreach (var fieldInfo in schemaInfo.Fields) { diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ComponentInterfaceGraphType.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ComponentInterfaceGraphType.cs index cdffd92d3..31824b6f1 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ComponentInterfaceGraphType.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ComponentInterfaceGraphType.cs @@ -17,7 +17,8 @@ internal sealed class ComponentInterfaceGraphType : InterfaceGraphType GetSchemaName(x)), + Description = FieldDescriptions.ContentSchemaName + }; + + public static readonly FieldType SchemaNameNoResolver = SchemaName.WithouthResolver(); + public static readonly FieldType Url = new FieldType { Name = "url", @@ -147,6 +184,8 @@ internal static class ContentFields Description = FieldDescriptions.ContentUrl }; + public static readonly FieldType UrlNoResolver = Url.WithouthResolver(); + public static readonly FieldType EditToken = new FieldType { Name = "editToken", @@ -155,6 +194,8 @@ internal static class ContentFields Description = FieldDescriptions.EditToken }; + public static readonly FieldType EditTokenNoResolver = EditToken.WithouthResolver(); + public static readonly FieldType DataDynamic = new FieldType { Name = "data__dynamic", @@ -163,6 +204,8 @@ internal static class ContentFields Description = FieldDescriptions.ContentData }; + public static readonly FieldType DataDynamicNoResolver = DataDynamic.WithouthResolver(); + public static readonly FieldType StringFieldText = new FieldType { Name = "text", @@ -188,4 +231,14 @@ internal static class ContentFields { return Resolvers.Sync(resolver); } + + private static string? GetSchemaName(JsonObject component) + { + if (component.TryGetValue("schemaName", out var name) && name.Type == JsonValueType.String) + { + return name.ToString(); + } + + return null; + } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentInterfaceGraphType.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentInterfaceGraphType.cs index 21df91da5..cef2b131a 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentInterfaceGraphType.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentInterfaceGraphType.cs @@ -16,18 +16,19 @@ internal sealed class ContentInterfaceGraphType : InterfaceGraphType callback) { - if (!scope.HasFlag(ScriptScope.Async)) + if (callback == null) { - return; + throw new JavaScriptException("Callback is not defined."); } - describe(JsonType.Function, "getReference(id, callback)", - Resources.ScriptingGetReference, - deprecationReason: Resources.ScriptingGetReferenceDeprecated); - - describe(JsonType.Function, "getReferenceV2(id, callback)", - Resources.ScriptingGetReferenceV2); - - describe(JsonType.Function, "getReferences(ids, callback)", - Resources.ScriptingGetReferences); - } - - private void GetReferences(ScriptExecutionContext context, DomainId appId, ClaimsPrincipal user, JsValue references, Action callback) - { - Guard.NotNull(callback); - context.Schedule(async (scheduler, ct) => { var ids = references.ToIds(); @@ -110,7 +95,10 @@ public sealed class ReferencesJintExtension : IJintExtension, IScriptDescriptor private void GetReference(ScriptExecutionContext context, DomainId appId, ClaimsPrincipal user, JsValue references, Action callback) { - Guard.NotNull(callback); + if (callback == null) + { + throw new JavaScriptException("Callback is not defined."); + } context.Schedule(async (scheduler, ct) => { @@ -157,4 +145,22 @@ public sealed class ReferencesJintExtension : IJintExtension, IScriptDescriptor return app; } + + public void Describe(AddDescription describe, ScriptScope scope) + { + if (!scope.HasFlag(ScriptScope.Async)) + { + return; + } + + describe(JsonType.Function, "getReference(id, callback)", + Resources.ScriptingGetReference, + deprecationReason: Resources.ScriptingGetReferenceDeprecated); + + describe(JsonType.Function, "getReferenceV2(id, callback)", + Resources.ScriptingGetReferenceV2); + + describe(JsonType.Function, "getReferences(ids, callback)", + Resources.ScriptingGetReferences); + } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Notifications/EmailUserNotifications.cs b/backend/src/Squidex.Domain.Apps.Entities/Notifications/EmailUserNotifications.cs index 35ca3456a..2b1780eca 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Notifications/EmailUserNotifications.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Notifications/EmailUserNotifications.cs @@ -14,6 +14,7 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Email; using Squidex.Shared.Identity; using Squidex.Shared.Users; +using System.Globalization; namespace Squidex.Domain.Apps.Entities.Notifications; @@ -187,12 +188,12 @@ public sealed class EmailUserNotifications : IUserNotifications if (vars.ApiCallsLimit != null) { - text = text.Replace("$API_CALLS_LIMIT", vars.ApiCallsLimit.ToString(), StringComparison.Ordinal); + text = text.Replace("$API_CALLS_LIMIT", vars.ApiCallsLimit.Value.ToString(CultureInfo.InvariantCulture), StringComparison.Ordinal); } if (vars.ApiCalls != null) { - text = text.Replace("$API_CALLS", vars.ApiCalls.ToString(), StringComparison.Ordinal); + text = text.Replace("$API_CALLS", vars.ApiCalls.Value.ToString(CultureInfo.InvariantCulture), StringComparison.Ordinal); } text = text.Replace("$UI_URL", vars.URL, StringComparison.Ordinal); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj b/backend/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj index 14a3446a1..72127c150 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj +++ b/backend/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj @@ -20,16 +20,16 @@ - - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/backend/src/Squidex.Domain.Apps.Events/Squidex.Domain.Apps.Events.csproj b/backend/src/Squidex.Domain.Apps.Events/Squidex.Domain.Apps.Events.csproj index 5d24b781c..4cb2d5a7b 100644 --- a/backend/src/Squidex.Domain.Apps.Events/Squidex.Domain.Apps.Events.csproj +++ b/backend/src/Squidex.Domain.Apps.Events/Squidex.Domain.Apps.Events.csproj @@ -14,7 +14,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/backend/src/Squidex.Domain.Users.MongoDb/Squidex.Domain.Users.MongoDb.csproj b/backend/src/Squidex.Domain.Users.MongoDb/Squidex.Domain.Users.MongoDb.csproj index 57ee1af23..68da5334a 100644 --- a/backend/src/Squidex.Domain.Users.MongoDb/Squidex.Domain.Users.MongoDb.csproj +++ b/backend/src/Squidex.Domain.Users.MongoDb/Squidex.Domain.Users.MongoDb.csproj @@ -19,12 +19,12 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/backend/src/Squidex.Domain.Users/DefaultXmlRepository.cs b/backend/src/Squidex.Domain.Users/DefaultXmlRepository.cs index 607e938c0..238579dc3 100644 --- a/backend/src/Squidex.Domain.Users/DefaultXmlRepository.cs +++ b/backend/src/Squidex.Domain.Users/DefaultXmlRepository.cs @@ -50,7 +50,9 @@ public sealed class DefaultXmlRepository : IXmlRepository { var state = new State(element); +#pragma warning disable MA0134 // Observe result of async calls store.WriteAsync(new SnapshotWriteJob(DomainId.Create(friendlyName), state, 0)); +#pragma warning restore MA0134 // Observe result of async calls } private async Task> GetAllElementsAsync() diff --git a/backend/src/Squidex.Domain.Users/Squidex.Domain.Users.csproj b/backend/src/Squidex.Domain.Users/Squidex.Domain.Users.csproj index 58ee45697..677f6f044 100644 --- a/backend/src/Squidex.Domain.Users/Squidex.Domain.Users.csproj +++ b/backend/src/Squidex.Domain.Users/Squidex.Domain.Users.csproj @@ -17,14 +17,14 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + - + diff --git a/backend/src/Squidex.Infrastructure.GetEventStore/EventSourcing/GetEventStoreSubscription.cs b/backend/src/Squidex.Infrastructure.GetEventStore/EventSourcing/GetEventStoreSubscription.cs index d387aceb3..e5d511f58 100644 --- a/backend/src/Squidex.Infrastructure.GetEventStore/EventSourcing/GetEventStoreSubscription.cs +++ b/backend/src/Squidex.Infrastructure.GetEventStore/EventSourcing/GetEventStoreSubscription.cs @@ -25,6 +25,7 @@ internal sealed class GetEventStoreSubscription : IEventSubscription string? prefix, string? streamFilter) { +#pragma warning disable MA0134 // Observe result of async calls Task.Run(async () => { var ct = cts.Token; @@ -69,6 +70,7 @@ internal sealed class GetEventStoreSubscription : IEventSubscription cancellationToken: ct); } }, cts.Token); +#pragma warning restore MA0134 // Observe result of async calls } public void Dispose() diff --git a/backend/src/Squidex.Infrastructure.GetEventStore/Squidex.Infrastructure.GetEventStore.csproj b/backend/src/Squidex.Infrastructure.GetEventStore/Squidex.Infrastructure.GetEventStore.csproj index 7a01a1097..8c545d629 100644 --- a/backend/src/Squidex.Infrastructure.GetEventStore/Squidex.Infrastructure.GetEventStore.csproj +++ b/backend/src/Squidex.Infrastructure.GetEventStore/Squidex.Infrastructure.GetEventStore.csproj @@ -14,8 +14,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/backend/src/Squidex.Infrastructure.MongoDb/Squidex.Infrastructure.MongoDb.csproj b/backend/src/Squidex.Infrastructure.MongoDb/Squidex.Infrastructure.MongoDb.csproj index 583b29647..1de5a4f9a 100644 --- a/backend/src/Squidex.Infrastructure.MongoDb/Squidex.Infrastructure.MongoDb.csproj +++ b/backend/src/Squidex.Infrastructure.MongoDb/Squidex.Infrastructure.MongoDb.csproj @@ -14,12 +14,12 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + diff --git a/backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj b/backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj index 293ce5856..db21de289 100644 --- a/backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj +++ b/backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj @@ -11,30 +11,30 @@ True - + - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + - - - + + + - - - - - - + + + + + + - + diff --git a/backend/src/Squidex.Infrastructure/Tasks/AsyncHelper.cs b/backend/src/Squidex.Infrastructure/Tasks/AsyncHelper.cs index ba8040fb0..6b986f362 100644 --- a/backend/src/Squidex.Infrastructure/Tasks/AsyncHelper.cs +++ b/backend/src/Squidex.Infrastructure/Tasks/AsyncHelper.cs @@ -54,6 +54,7 @@ public static class AsyncHelper public static void Batch(this Channel source, Channel target, int batchSize, int timeout, CancellationToken ct = default) { +#pragma warning disable MA0134 // Observe result of async calls Task.Run(async () => { var batch = new List(batchSize); @@ -94,5 +95,6 @@ public static class AsyncHelper await TrySendAsync(); }, ct).ContinueWith(x => target.Writer.TryComplete(x.Exception)); +#pragma warning restore MA0134 // Observe result of async calls } } diff --git a/backend/src/Squidex.Infrastructure/Tasks/TaskExtensions.cs b/backend/src/Squidex.Infrastructure/Tasks/TaskExtensions.cs index 87043ffdf..32e9c4603 100644 --- a/backend/src/Squidex.Infrastructure/Tasks/TaskExtensions.cs +++ b/backend/src/Squidex.Infrastructure/Tasks/TaskExtensions.cs @@ -23,12 +23,14 @@ public static class TaskExtensions } else { +#pragma warning disable MA0134 // Observe result of async calls task.ContinueWith( IgnoreTaskContinuation, CancellationToken.None, TaskContinuationOptions.OnlyOnFaulted | TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default); +#pragma warning restore MA0134 // Observe result of async calls } } diff --git a/backend/src/Squidex.Shared/Squidex.Shared.csproj b/backend/src/Squidex.Shared/Squidex.Shared.csproj index 088dc4ff2..cc282e17a 100644 --- a/backend/src/Squidex.Shared/Squidex.Shared.csproj +++ b/backend/src/Squidex.Shared/Squidex.Shared.csproj @@ -9,7 +9,7 @@ True - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/backend/src/Squidex.Web/Pipeline/CachingManager.cs b/backend/src/Squidex.Web/Pipeline/CachingManager.cs index 141a5a14c..cc7db242d 100644 --- a/backend/src/Squidex.Web/Pipeline/CachingManager.cs +++ b/backend/src/Squidex.Web/Pipeline/CachingManager.cs @@ -16,6 +16,8 @@ using Microsoft.Net.Http.Headers; using Squidex.Infrastructure; using Squidex.Infrastructure.Caching; +#pragma warning disable CA1822 // Mark members as static + namespace Squidex.Web.Pipeline; public sealed class CachingManager : IRequestCache @@ -197,7 +199,6 @@ public sealed class CachingManager : IRequestCache public CachingManager(IHttpContextAccessor httpContextAccessor, IOptions cachingOptions) { this.httpContextAccessor = httpContextAccessor; - this.cachingOptions = cachingOptions.Value; stringBuilderPool = new DefaultObjectPool(new StringBuilderPooledObjectPolicy diff --git a/backend/src/Squidex.Web/Squidex.Web.csproj b/backend/src/Squidex.Web/Squidex.Web.csproj index 405ae094e..b2e344aa3 100644 --- a/backend/src/Squidex.Web/Squidex.Web.csproj +++ b/backend/src/Squidex.Web/Squidex.Web.csproj @@ -13,10 +13,10 @@ - - - - + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/backend/src/Squidex/Areas/Api/Controllers/Translations/Models/AskDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Translations/Models/AskDto.cs new file mode 100644 index 000000000..dc3a510d7 --- /dev/null +++ b/backend/src/Squidex/Areas/Api/Controllers/Translations/Models/AskDto.cs @@ -0,0 +1,21 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.Infrastructure.Validation; +using Squidex.Web; + +namespace Squidex.Areas.Api.Controllers.Translations.Models; + +[OpenApiRequest] +public sealed class AskDto +{ + /// + /// The text to ask. + /// + [LocalizedRequired] + public string Prompt { get; set; } +} diff --git a/backend/src/Squidex/Areas/Api/Controllers/Translations/Models/TranslationDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Translations/Models/TranslationDto.cs index 3fc1e31ed..4e436eebf 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Translations/Models/TranslationDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Translations/Models/TranslationDto.cs @@ -15,7 +15,13 @@ public sealed class TranslationDto /// /// The result of the translation. /// - public TranslationResultCode Result { get; set; } + public TranslationStatus Status { get; set; } + + /// + /// The result of the translation. + /// + [Obsolete("Use Status property now.")] + public TranslationStatus Result => Status; /// /// The translated text. @@ -24,6 +30,6 @@ public sealed class TranslationDto public static TranslationDto FromDomain(TranslationResult translation) { - return SimpleMapper.Map(translation, new TranslationDto { Result = translation.Code }); + return SimpleMapper.Map(translation, new TranslationDto()); } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Translations/TranslationsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Translations/TranslationsController.cs index 7540ba2cc..75d1bf474 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Translations/TranslationsController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Translations/TranslationsController.cs @@ -9,6 +9,7 @@ using Microsoft.AspNetCore.Mvc; using Squidex.Areas.Api.Controllers.Translations.Models; using Squidex.Infrastructure.Commands; using Squidex.Shared; +using Squidex.Text.ChatBots; using Squidex.Text.Translations; using Squidex.Web; @@ -17,15 +18,18 @@ namespace Squidex.Areas.Api.Controllers.Translations; /// /// Manage translations. /// +[ApiModelValidation(true)] [ApiExplorerSettings(GroupName = nameof(Translations))] public sealed class TranslationsController : ApiController { private readonly ITranslator translator; + private readonly IChatBot chatBot; - public TranslationsController(ICommandBus commandBus, ITranslator translator) + public TranslationsController(ICommandBus commandBus, ITranslator translator, IChatBot chatBot) : base(commandBus) { this.translator = translator; + this.chatBot = chatBot; } /// @@ -38,7 +42,7 @@ public sealed class TranslationsController : ApiController [Route("apps/{app}/translations/")] [ProducesResponseType(typeof(TranslationDto), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous(PermissionIds.AppTranslate)] - [ApiCosts(0)] + [ApiCosts(10)] public async Task PostTranslation(string app, [FromBody] TranslateDto request) { var result = await translator.TranslateAsync(request.Text, request.TargetLanguage, request.SourceLanguage, HttpContext.RequestAborted); @@ -46,4 +50,23 @@ public sealed class TranslationsController : ApiController return Ok(response); } + + /// + /// Asks the chatbot a question a text. + /// + /// The name of the app. + /// The question request. + /// Question asked.. + [HttpPost] + [Route("apps/{app}/ask/")] + [ProducesResponseType(typeof(string[]), StatusCodes.Status200OK)] + [ApiPermissionOrAnonymous(PermissionIds.AppTranslate)] + [ApiCosts(10)] + public async Task PostQuestion(string app, [FromBody] AskDto request) + { + var result = await chatBot.AskQuestionAsync(request.Prompt, HttpContext.RequestAborted); + var response = result; + + return Ok(response); + } } diff --git a/backend/src/Squidex/Config/Domain/AssetServices.cs b/backend/src/Squidex/Config/Domain/AssetServices.cs index 04940d6f5..aa831a4fb 100644 --- a/backend/src/Squidex/Config/Domain/AssetServices.cs +++ b/backend/src/Squidex/Config/Domain/AssetServices.cs @@ -49,15 +49,6 @@ public static class AssetServices services.AddTransientAs() .As(); - services.AddSingletonAs() - .AsSelf(); - - services.AddSingletonAs() - .As().As(); - - services.AddSingletonAs(c => InMemoryFileLockProvider.Instance) - .As(); - services.AddSingletonAs() .AsSelf(); @@ -108,6 +99,8 @@ public static class AssetServices services.AddSingletonAs() .As(); + + services.AddAssetTus(); } public static void AddSquidexAssetInfrastructure(this IServiceCollection services, IConfiguration config) @@ -116,43 +109,27 @@ public static class AssetServices { ["Default"] = () => { - services.AddSingletonAs() - .AsOptional(); + services.AddFolderAssetStore(config); }, ["Folder"] = () => { - var path = config.GetRequiredValue("assetStore:folder:path"); - - services.AddSingletonAs(c => new FolderAssetStore(path, c.GetRequiredService>())) - .As(); + services.AddFolderAssetStore(config); }, ["GoogleCloud"] = () => { - var options = new GoogleCloudAssetOptions - { - BucketName = config.GetRequiredValue("assetStore:googleCloud:bucket") - }; - - services.AddSingletonAs(c => new GoogleCloudAssetStore(options)) - .As(); + services.AddGoogleCloudAssetStore(config); }, ["AzureBlob"] = () => { - var options = new AzureBlobAssetOptions - { - ConnectionString = config.GetRequiredValue("assetStore:azureBlob:connectionString"), - ContainerName = config.GetRequiredValue("assetStore:azureBlob:containerName") - }; - - services.AddSingletonAs(c => new AzureBlobAssetStore(options)) - .As(); + services.AddAzureBlobAssetStore(config); }, ["AmazonS3"] = () => { - var amazonS3Options = config.GetSection("assetStore:amazonS3").Get() ?? new (); - - services.AddSingletonAs(c => new AmazonS3AssetStore(amazonS3Options)) - .As(); + services.AddAmazonS3AssetStore(config); + }, + ["FTP"] = () => + { + services.AddFTPAssetStore(config); }, ["MongoDb"] = () => { @@ -160,48 +137,17 @@ public static class AssetServices var mongoDatabaseName = config.GetRequiredValue("assetStore:mongoDb:database"); var mongoGridFsBucketName = config.GetRequiredValue("assetStore:mongoDb:bucket"); - services.AddSingletonAs(c => - { - var mongoClient = StoreServices.GetMongoClient(mongoConfiguration); - var mongoDatabase = mongoClient.GetDatabase(mongoDatabaseName); - - var gridFsbucket = new GridFSBucket(mongoDatabase, new GridFSBucketOptions - { - BucketName = mongoGridFsBucketName - }); - - return new MongoGridFsAssetStore(gridFsbucket); - }) - .As(); - }, - ["Ftp"] = () => - { - var serverHost = config.GetRequiredValue("assetStore:ftp:serverHost"); - var serverPort = config.GetOptionalValue("assetStore:ftp:serverPort", 21); - - var username = config.GetRequiredValue("assetStore:ftp:username"); - var password = config.GetRequiredValue("assetStore:ftp:password"); - - var options = new FTPAssetOptions + services.AddMongoAssetStore(c => { - Path = config.GetOptionalValue("assetStore:ftp:path", "/")! - }; + var mongoClient = StoreServices.GetMongoClient(mongoConfiguration); + var mongoDatabase = mongoClient.GetDatabase(mongoDatabaseName); - services.AddSingletonAs(c => + return new GridFSBucket(mongoDatabase, new GridFSBucketOptions { - var factory = new Func(() => new AsyncFtpClient(serverHost, username, password, serverPort)); - - return new FTPAssetStore(factory, options, c.GetRequiredService>()); - }) - .As(); + BucketName = mongoGridFsBucketName + }); + }); } }); - - services.AddSingletonAs(c => - { - var service = c.GetRequiredService(); - - return new DelegateInitializer(service.GetType().Name, service.InitializeAsync); - }); } } diff --git a/backend/src/Squidex/Config/Domain/InfrastructureServices.cs b/backend/src/Squidex/Config/Domain/InfrastructureServices.cs index 934a58cae..205341cac 100644 --- a/backend/src/Squidex/Config/Domain/InfrastructureServices.cs +++ b/backend/src/Squidex/Config/Domain/InfrastructureServices.cs @@ -86,6 +86,9 @@ public static class InfrastructureServices services.AddSingletonAs() .As().As(); + services.AddSingletonAs() + .As().As(); + services.AddSingletonAs() .As().As(); @@ -127,26 +130,15 @@ public static class InfrastructureServices public static void AddSquidexTranslation(this IServiceCollection services, IConfiguration config) { - services.Configure(config, - "translations:googleCloud"); - - services.Configure(config, - "translations:deepL"); - services.Configure(config, "languages"); services.AddSingletonAs() .AsSelf(); - services.AddSingletonAs(c => new DeepLTranslationService(c.GetRequiredService>().Value)) - .As(); - - services.AddSingletonAs(c => new GoogleCloudTranslationService(c.GetRequiredService>().Value)) - .As(); - - services.AddSingletonAs() - .As(); + services.AddDeepLTranslations(config); + services.AddGoogleCloudTranslations(config); + services.AddOpenAIChatBot(config); } public static void AddSquidexLocalization(this IServiceCollection services) diff --git a/backend/src/Squidex/Config/Domain/StoreServices.cs b/backend/src/Squidex/Config/Domain/StoreServices.cs index 4d1795b37..1035ca714 100644 --- a/backend/src/Squidex/Config/Domain/StoreServices.cs +++ b/backend/src/Squidex/Config/Domain/StoreServices.cs @@ -67,6 +67,7 @@ public static class StoreServices var mongoDatabaseName = config.GetRequiredValue("store:mongoDb:database")!; var mongoContentDatabaseName = config.GetOptionalValue("store:mongoDb:contentDatabase", mongoDatabaseName)!; + services.AddMongoAssetKeyValueStore(); services.AddSingleton(typeof(ISnapshotStore<>), typeof(MongoSnapshotStore<>)); services.AddSingletonAs(c => GetMongoClient(mongoConfiguration)) @@ -111,9 +112,6 @@ public static class StoreServices services.AddHealthChecks() .AddCheck("MongoDB", tags: new[] { "node" }); - services.AddSingletonAs>() - .As>(); - services.AddSingletonAs() .As(); @@ -208,13 +206,6 @@ public static class StoreServices services.AddSingleton(typeof(IPersistenceFactory<>), typeof(Store<>)); - - services.AddSingletonAs(c => - { - var service = c.GetRequiredService>(); - - return new DelegateInitializer(service.GetType().Name, service.InitializeAsync); - }); } public static IMongoClient GetMongoClient(string configuration) diff --git a/backend/src/Squidex/Squidex.csproj b/backend/src/Squidex/Squidex.csproj index 4a8393ed7..034305341 100644 --- a/backend/src/Squidex/Squidex.csproj +++ b/backend/src/Squidex/Squidex.csproj @@ -34,24 +34,24 @@ - - - - - + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - + + + + + - - + + @@ -61,19 +61,19 @@ - - - - - - - - - + + + + + + + + + - - - + + + diff --git a/backend/src/Squidex/appsettings.json b/backend/src/Squidex/appsettings.json index caa4b5fef..fb7164098 100644 --- a/backend/src/Squidex/appsettings.json +++ b/backend/src/Squidex/appsettings.json @@ -550,11 +550,11 @@ "suppressXFrameOptionsHeader": false, // Initial admin user. - "adminEmail": "", - "adminPassword": "", + "adminEmail": "hello@squidex.io", + "adminPassword": "1q2w3e$R", // Recreate the admin if it does not exist or the password does not match. - "adminRecreate": false, + "adminRecreate": true, // Client with all admin permissions. "adminClientId": "", @@ -624,6 +624,13 @@ } }, + "chatbot": { + "openai": { + // The OpenAI API Key. + "apiKey": "" + } + }, + "rebuild": { // Set to true to rebuild apps. "apps": false, diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/ContentConversionTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/ContentConversionTests.cs index 554547edc..7fd57b6cf 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/ContentConversionTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/ContentConversionTests.cs @@ -17,8 +17,6 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent; public class ContentConversionTests { private readonly Schema schema; - private readonly LanguagesConfig languages = LanguagesConfig.English.Set(Language.DE); - private readonly Language language = Language.DE; private readonly ResolvedComponents components; public ContentConversionTests() diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintScriptEngineHelperTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintScriptEngineHelperTests.cs index d4b2ea49e..1bcc517e1 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintScriptEngineHelperTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintScriptEngineHelperTests.cs @@ -7,6 +7,7 @@ using System.Net; using System.Text; +using Jint.Runtime; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Options; using Squidex.Domain.Apps.Core.Scripting; @@ -15,12 +16,16 @@ using Squidex.Domain.Apps.Core.TestHelpers; using Squidex.Infrastructure; using Squidex.Infrastructure.Json.Objects; using Squidex.Infrastructure.Validation; +using Squidex.Text.ChatBots; +using Squidex.Text.Translations; namespace Squidex.Domain.Apps.Core.Operations.Scripting; public class JintScriptEngineHelperTests : IClassFixture { private readonly IHttpClientFactory httpClientFactory = A.Fake(); + private readonly ITranslator translator = A.Fake(); + private readonly IChatBot chatBot = A.Fake(); private readonly JintScriptEngine sut; public JintScriptEngineHelperTests() @@ -30,7 +35,8 @@ public class JintScriptEngineHelperTests : IClassFixture new DateTimeJintExtension(), new HttpJintExtension(httpClientFactory), new StringJintExtension(), - new StringWordsJintExtension() + new StringWordsJintExtension(), + new StringAsyncJintExtension(translator, chatBot) }; sut = new JintScriptEngine(new MemoryCache(Options.Create(new MemoryCacheOptions())), @@ -611,6 +617,148 @@ public class JintScriptEngineHelperTests : IClassFixture Assert.Equal(expectedResult, actual); } + [Fact] + public async Task Should_generate_content() + { + A.CallTo(() => chatBot.AskQuestionAsync("prompt", A._)) + .Returns(new List { "Generated" }); + + var vars = new ScriptVars + { + }; + + const string script = @" + generate('prompt', function(actual) { + complete(actual); + }); + "; + + var actual = await sut.ExecuteAsync(vars, script); + + Assert.Equal("Generated", actual.ToString()); + } + + [Theory] + [InlineData("null")] + [InlineData("''")] + [InlineData("' '")] + public async Task Should_return_null_string_on_generate_if_prompt_is_invalid(string input) + { + var vars = new ScriptVars + { + }; + + var script = $@" + generate({input}, function(actual) {{ + complete(actual); + }}); + "; + + var actual = await sut.ExecuteAsync(vars, script); + + Assert.Equal(JsonValue.Null, actual); + + A.CallTo(() => chatBot.AskQuestionAsync(A._, A._)) + .MustNotHaveHappened(); + } + + [Fact] + public async Task Should_throw_exception_on_generate_if_callback_is_null() + { + var vars = new ScriptVars + { + }; + + const string script = @" + generate('prompt', null); + "; + + await Assert.ThrowsAsync(() => sut.ExecuteAsync(vars, script)); + } + + [Fact] + public async Task Should_translate_content() + { + A.CallTo(() => translator.TranslateAsync("text", "en", "it", A._)) + .Returns(TranslationResult.Success("Translated", "it")); + + var vars = new ScriptVars + { + }; + + const string script = @" + translate('text', 'en', function(actual) { + complete(actual); + }, 'it'); + "; + + var actual = await sut.ExecuteAsync(vars, script); + + Assert.Equal("Translated", actual.ToString()); + } + + [Theory] + [InlineData("null")] + [InlineData("''")] + [InlineData("' '")] + public async Task Should_return_null_string_on_translate_if_input_is_invalid(string input) + { + var vars = new ScriptVars + { + }; + + var script = $@" + translate({input}, 'en', function(actual) {{ + complete(actual); + }}); + "; + + var actual = await sut.ExecuteAsync(vars, script); + + Assert.Equal(JsonValue.Null, actual); + + A.CallTo(() => translator.TranslateAsync(A._, A._, A._, A._)) + .MustNotHaveHappened(); + } + + [Theory] + [InlineData("null")] + [InlineData("''")] + [InlineData("' '")] + public async Task Should_return_null_string_on_input_if_target_language_is_invalid(string input) + { + var vars = new ScriptVars + { + }; + + var script = $@" + translate('text', {input}, function(actual) {{ + complete(actual); + }}); + "; + + var actual = await sut.ExecuteAsync(vars, script); + + Assert.Equal(JsonValue.Null, actual); + + A.CallTo(() => translator.TranslateAsync(A._, A._, A._, A._)) + .MustNotHaveHappened(); + } + + [Fact] + public async Task Should_throw_exception_on_translate_if_callback_is_null() + { + var vars = new ScriptVars + { + }; + + const string script = @" + translate('text', 'en', null); + "; + + await Assert.ThrowsAsync(() => sut.ExecuteAsync(vars, script)); + } + private MockupHttpHandler SetupRequest(HttpStatusCode statusCode = HttpStatusCode.OK) { var httpResponse = new HttpResponseMessage(statusCode) diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Squidex.Domain.Apps.Core.Tests.csproj b/backend/tests/Squidex.Domain.Apps.Core.Tests/Squidex.Domain.Apps.Core.Tests.csproj index 8f8186993..db08fd3fc 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Squidex.Domain.Apps.Core.Tests.csproj +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Squidex.Domain.Apps.Core.Tests.csproj @@ -14,15 +14,15 @@ - - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/TestContent.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/TestContent.cs index c924b5503..2899b9101 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/TestContent.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/TestContent.cs @@ -7,6 +7,7 @@ using NodaTime; using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Infrastructure; using Squidex.Infrastructure.Json.Objects; @@ -87,6 +88,7 @@ public static class TestContent myComponent { iv { schemaId + schemaName schemaRef1Field } } @@ -98,10 +100,12 @@ public static class TestContent __typename ... on MyRefSchema1Component { schemaId + schemaName schemaRef1Field } ... on MyRefSchema2Component { schemaId + schemaName schemaRef2Field } } @@ -172,6 +176,7 @@ public static class TestContent myComponent__Dynamic myComponent { schemaId + schemaName schemaRef1Field } myComponents__Dynamic @@ -179,10 +184,12 @@ public static class TestContent __typename ... on MyRefSchema1Component { schemaId + schemaName schemaRef1Field } ... on MyRefSchema2Component { schemaId + schemaName schemaRef2Field } } @@ -244,6 +251,7 @@ public static class TestContent .AddInvariant( JsonValue.Object() .Add(Component.Discriminator, TestSchemas.Ref1.Id) + .Add(Component.Descriptor, TestSchemas.Ref1.SchemaDef.Name) .Add("schemaRef1Field", "Component1"))) .AddField("my-components", new ContentFieldData() @@ -251,9 +259,11 @@ public static class TestContent JsonValue.Array( JsonValue.Object() .Add(Component.Discriminator, TestSchemas.Ref1.Id) + .Add(Component.Descriptor, TestSchemas.Ref1.SchemaDef.Name) .Add("schemaRef1Field", "Component1"), JsonValue.Object() .Add(Component.Discriminator, TestSchemas.Ref2.Id) + .Add(Component.Descriptor, TestSchemas.Ref2.SchemaDef.Name) .Add("schemaRef2Field", "Component2")))) .AddField("my-json", new ContentFieldData() @@ -484,6 +494,7 @@ public static class TestContent iv = new Dictionary { ["schemaId"] = TestSchemas.Ref1.Id.ToString(), + ["schemaName"] = TestSchemas.Ref1.SchemaDef.Name, ["schemaRef1Field"] = "Component1" } }, @@ -494,11 +505,13 @@ public static class TestContent new Dictionary { ["schemaId"] = TestSchemas.Ref1.Id.ToString(), + ["schemaName"] = TestSchemas.Ref1.SchemaDef.Name, ["schemaRef1Field"] = "Component1" }, new Dictionary { ["schemaId"] = TestSchemas.Ref2.Id.ToString(), + ["schemaName"] = TestSchemas.Ref2.SchemaDef.Name, ["schemaRef2Field"] = "Component2" } } @@ -648,6 +661,7 @@ public static class TestContent iv = new Dictionary { ["schemaId"] = TestSchemas.Ref1.Id.ToString(), + ["schemaName"] = TestSchemas.Ref1.SchemaDef.Name, ["schemaRef1Field"] = "Component1" } }, @@ -656,6 +670,7 @@ public static class TestContent iv = new Dictionary { ["schemaId"] = TestSchemas.Ref1.Id.ToString(), + ["schemaName"] = TestSchemas.Ref1.SchemaDef.Name, ["schemaRef1Field"] = "Component1" } }, @@ -666,11 +681,13 @@ public static class TestContent new Dictionary { ["schemaId"] = TestSchemas.Ref1.Id.ToString(), + ["schemaName"] = TestSchemas.Ref1.SchemaDef.Name, ["schemaRef1Field"] = "Component1" }, new Dictionary { ["schemaId"] = TestSchemas.Ref2.Id.ToString(), + ["schemaName"] = TestSchemas.Ref2.SchemaDef.Name, ["schemaRef2Field"] = "Component2" } } @@ -683,12 +700,14 @@ public static class TestContent { ["__typename"] = "MyRefSchema1Component", ["schemaId"] = TestSchemas.Ref1.Id.ToString(), + ["schemaName"] = TestSchemas.Ref1.SchemaDef.Name, ["schemaRef1Field"] = "Component1" }, new Dictionary { ["__typename"] = "MyRefSchema2Component", ["schemaId"] = TestSchemas.Ref2.Id.ToString(), + ["schemaName"] = TestSchemas.Ref2.SchemaDef.Name, ["schemaRef2Field"] = "Component2" } } @@ -771,11 +790,13 @@ public static class TestContent ["myComponent__Dynamic"] = new Dictionary { ["schemaId"] = TestSchemas.Ref1.Id.ToString(), + ["schemaName"] = TestSchemas.Ref1.SchemaDef.Name, ["schemaRef1Field"] = "Component1" }, ["myComponent"] = new Dictionary { ["schemaId"] = TestSchemas.Ref1.Id.ToString(), + ["schemaName"] = TestSchemas.Ref1.SchemaDef.Name, ["schemaRef1Field"] = "Component1" }, ["myComponents__Dynamic"] = new[] @@ -783,11 +804,13 @@ public static class TestContent new Dictionary { ["schemaId"] = TestSchemas.Ref1.Id.ToString(), + ["schemaName"] = TestSchemas.Ref1.SchemaDef.Name, ["schemaRef1Field"] = "Component1" }, new Dictionary { ["schemaId"] = TestSchemas.Ref2.Id.ToString(), + ["schemaName"] = TestSchemas.Ref2.SchemaDef.Name, ["schemaRef2Field"] = "Component2" } }, @@ -797,12 +820,14 @@ public static class TestContent { ["__typename"] = "MyRefSchema1Component", ["schemaId"] = TestSchemas.Ref1.Id.ToString(), + ["schemaName"] = TestSchemas.Ref1.SchemaDef.Name, ["schemaRef1Field"] = "Component1" }, new Dictionary { ["__typename"] = "MyRefSchema2Component", ["schemaId"] = TestSchemas.Ref2.Id.ToString(), + ["schemaName"] = TestSchemas.Ref2.SchemaDef.Name, ["schemaRef2Field"] = "Component2" } }, diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj index a76810610..dc496705a 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj @@ -21,21 +21,21 @@ - - - - + + + + - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + - + diff --git a/backend/tests/Squidex.Domain.Users.Tests/Squidex.Domain.Users.Tests.csproj b/backend/tests/Squidex.Domain.Users.Tests/Squidex.Domain.Users.Tests.csproj index ceb2c2546..0242c0923 100644 --- a/backend/tests/Squidex.Domain.Users.Tests/Squidex.Domain.Users.Tests.csproj +++ b/backend/tests/Squidex.Domain.Users.Tests/Squidex.Domain.Users.Tests.csproj @@ -14,13 +14,13 @@ - - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/backend/tests/Squidex.Infrastructure.Tests/MongoDb/BsonJsonSerializerTests.cs b/backend/tests/Squidex.Infrastructure.Tests/MongoDb/BsonJsonSerializerTests.cs index 5e7d5c5fe..392e77887 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/MongoDb/BsonJsonSerializerTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/MongoDb/BsonJsonSerializerTests.cs @@ -89,8 +89,8 @@ public class BsonJsonSerializerTests Bool = true, Byte = 0x2, Bytes = new byte[] { 0x10, 0x12, 0x13 }, - DateTimeOffset = DateTime.Today, - DateTime = DateTime.UtcNow.Date, + DateTimeOffset = new DateTimeOffset(2022, 12, 11, 10, 9, 8, TimeSpan.Zero), + DateTime = new DateTime(2022, 12, 11), Float32 = 32.5f, Float64 = 32.5d, Guid = Guid.NewGuid(), diff --git a/backend/tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj b/backend/tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj index aeab747de..bf287b5f1 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj +++ b/backend/tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj @@ -14,14 +14,17 @@ - - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + + + + diff --git a/backend/tests/Squidex.Web.Tests/Squidex.Web.Tests.csproj b/backend/tests/Squidex.Web.Tests/Squidex.Web.Tests.csproj index 2d5cefcea..7f6eb70b8 100644 --- a/backend/tests/Squidex.Web.Tests/Squidex.Web.Tests.csproj +++ b/backend/tests/Squidex.Web.Tests/Squidex.Web.Tests.csproj @@ -12,13 +12,13 @@ - - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/frontend/src/app/features/content/declarations.ts b/frontend/src/app/features/content/declarations.ts index 0862ab669..7e6bb098e 100644 --- a/frontend/src/app/features/content/declarations.ts +++ b/frontend/src/app/features/content/declarations.ts @@ -28,6 +28,7 @@ export * from './shared/due-time-selector.component'; export * from './shared/forms/array-editor.component'; export * from './shared/forms/array-item.component'; export * from './shared/forms/assets-editor.component'; +export * from './shared/forms/chat-dialog.component'; export * from './shared/forms/component-section.component'; export * from './shared/forms/component.component'; export * from './shared/forms/field-editor.component'; diff --git a/frontend/src/app/features/content/module.ts b/frontend/src/app/features/content/module.ts index 012e9fe40..52a8abdb0 100644 --- a/frontend/src/app/features/content/module.ts +++ b/frontend/src/app/features/content/module.ts @@ -9,7 +9,7 @@ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { VirtualScrollerModule } from 'ngx-virtual-scroller'; import { CanDeactivateGuard, ContentMustExistGuard, LoadLanguagesGuard, LoadSchemasGuard, SchemaMustExistPublishedGuard, SchemaMustNotBeSingletonGuard, SqxFrameworkModule, SqxSharedModule } from '@app/shared'; -import { ArrayEditorComponent, ArrayItemComponent, AssetsEditorComponent, CalendarPageComponent, CommentsPageComponent, ComponentComponent, ComponentSectionComponent, ContentComponent, ContentCreatorComponent, ContentEditorComponent, ContentEventComponent, ContentExtensionComponent, ContentFieldComponent, ContentHistoryPageComponent, ContentInspectionComponent, ContentPageComponent, ContentReferencesComponent, ContentSectionComponent, ContentsFiltersPageComponent, ContentsPageComponent, CustomViewEditorComponent, DueTimeSelectorComponent, FieldCopyButtonComponent, FieldEditorComponent, FieldLanguagesComponent, IFrameEditorComponent, PreviewButtonComponent, ReferenceDropdownComponent, ReferenceItemComponent, ReferencesCheckboxesComponent, ReferencesEditorComponent, ReferencesPageComponent, ReferencesTagsComponent, SchemasPageComponent, SidebarPageComponent, StockPhotoEditorComponent } from './declarations'; +import { ArrayEditorComponent, ArrayItemComponent, AssetsEditorComponent, CalendarPageComponent, ChatDialogComponent, CommentsPageComponent, ComponentComponent, ComponentSectionComponent, ContentComponent, ContentCreatorComponent, ContentEditorComponent, ContentEventComponent, ContentExtensionComponent, ContentFieldComponent, ContentHistoryPageComponent, ContentInspectionComponent, ContentPageComponent, ContentReferencesComponent, ContentSectionComponent, ContentsFiltersPageComponent, ContentsPageComponent, CustomViewEditorComponent, DueTimeSelectorComponent, FieldCopyButtonComponent, FieldEditorComponent, FieldLanguagesComponent, IFrameEditorComponent, PreviewButtonComponent, ReferenceDropdownComponent, ReferenceItemComponent, ReferencesCheckboxesComponent, ReferencesEditorComponent, ReferencesPageComponent, ReferencesTagsComponent, SchemasPageComponent, SidebarPageComponent, StockPhotoEditorComponent } from './declarations'; const routes: Routes = [ { @@ -97,6 +97,7 @@ const routes: Routes = [ ArrayItemComponent, AssetsEditorComponent, CalendarPageComponent, + ChatDialogComponent, CommentsPageComponent, ComponentComponent, ComponentSectionComponent, diff --git a/frontend/src/app/features/content/pages/content/editor/content-field.component.html b/frontend/src/app/features/content/pages/content/editor/content-field.component.html index 7c9e52f2f..15d826b50 100644 --- a/frontend/src/app/features/content/pages/content/editor/content-field.component.html +++ b/frontend/src/app/features/content/pages/content/editor/content-field.component.html @@ -4,10 +4,6 @@
- - + +
diff --git a/frontend/src/app/features/content/pages/content/editor/content-field.component.scss b/frontend/src/app/features/content/pages/content/editor/content-field.component.scss index 6c54189df..2d1052149 100644 --- a/frontend/src/app/features/content/pages/content/editor/content-field.component.scss +++ b/frontend/src/app/features/content/pages/content/editor/content-field.component.scss @@ -31,7 +31,7 @@ } &-buttons { - @include absolute(-.75rem, 4.25rem, null, 0); + @include absolute(-.75rem, 6.5rem, null, 0); overflow: hidden; } diff --git a/frontend/src/app/features/content/pages/content/editor/field-languages.component.html b/frontend/src/app/features/content/pages/content/editor/field-languages.component.html index 7a00d1b2e..5b14f54cf 100644 --- a/frontend/src/app/features/content/pages/content/editor/field-languages.component.html +++ b/frontend/src/app/features/content/pages/content/editor/field-languages.component.html @@ -1,5 +1,5 @@ - -
+
+ + {{ 'chat.title' | sqxTranslate }} + + + + + + + +
+
+
+ +
+
+ +
+
+
+ + + +

{{ 'chat.answers' | sqxTranslate }}

+ +
+ {{ 'chat.noAnswers' | sqxTranslate }} +
+ +
+
+ +
+
+ +
+
+
+
+ + +
+ +
+
+
+ diff --git a/frontend/src/app/features/content/shared/forms/chat-dialog.component.scss b/frontend/src/app/features/content/shared/forms/chat-dialog.component.scss new file mode 100644 index 000000000..2ad9ae3f0 --- /dev/null +++ b/frontend/src/app/features/content/shared/forms/chat-dialog.component.scss @@ -0,0 +1,6 @@ +@import 'mixins'; +@import 'vars'; + +textarea { + height: 100px; +} \ No newline at end of file diff --git a/frontend/src/app/features/content/shared/forms/chat-dialog.component.ts b/frontend/src/app/features/content/shared/forms/chat-dialog.component.ts new file mode 100644 index 000000000..bbae2db7e --- /dev/null +++ b/frontend/src/app/features/content/shared/forms/chat-dialog.component.ts @@ -0,0 +1,65 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. + */ + +import { ChangeDetectorRef, Component, EventEmitter, Output } from '@angular/core'; +import { AppsState, StatefulComponent, TranslationsService } from '@app/shared'; + +interface State { + // True, when running + isRunning: boolean; + + // The questions. + chatQuestion: string; + + // The answers. + chatAnswers?: ReadonlyArray; +} + +@Component({ + selector: 'sqx-chat-dialog', + styleUrls: ['./chat-dialog.component.scss'], + templateUrl: './chat-dialog.component.html', +}) +export class ChatDialogComponent extends StatefulComponent { + @Output() + public close = new EventEmitter(); + + @Output() + public complete = new EventEmitter(); + + constructor(changeDetector: ChangeDetectorRef, + private readonly appsState: AppsState, + private readonly translator: TranslationsService, + ) { + super(changeDetector, { + isRunning: false, + chatQuestion: '', + chatAnswers: undefined, + }); + } + + public setQuestion(chatQuestion: string) { + this.next({ chatQuestion }); + } + + public ask() { + this.next({ isRunning: true }); + + this.translator.ask(this.appsState.appName, { prompt: this.snapshot.chatQuestion }) + .subscribe({ + next: chatAnswers => { + this.next({ chatAnswers }); + }, + error: () => { + this.next({ chatAnswers: [] }); + }, + complete: () => { + this.next({ isRunning: false }); + }, + }); + } +} \ No newline at end of file diff --git a/frontend/src/app/features/content/shared/forms/field-editor.component.html b/frontend/src/app/features/content/shared/forms/field-editor.component.html index 0998e86e1..f533630c2 100644 --- a/frontend/src/app/features/content/shared/forms/field-editor.component.html +++ b/frontend/src/app/features/content/shared/forms/field-editor.component.html @@ -1,11 +1,15 @@
- + + -
+ + + + + \ No newline at end of file diff --git a/frontend/src/app/features/content/shared/forms/field-editor.component.ts b/frontend/src/app/features/content/shared/forms/field-editor.component.ts index 2ac0f9ae8..a6eb03010 100644 --- a/frontend/src/app/features/content/shared/forms/field-editor.component.ts +++ b/frontend/src/app/features/content/shared/forms/field-editor.component.ts @@ -8,7 +8,7 @@ import { Component, ElementRef, EventEmitter, Input, Output, ViewChild } from '@angular/core'; import { AbstractControl } from '@angular/forms'; import { Observable } from 'rxjs'; -import { AbstractContentForm, AppLanguageDto, EditContentForm, FieldDto, hasNoValue$, MathHelper, TypedSimpleChanges, Types } from '@app/shared'; +import { AbstractContentForm, AppLanguageDto, DialogModel, EditContentForm, FieldDto, hasNoValue$, MathHelper, TypedSimpleChanges, Types } from '@app/shared'; @Component({ selector: 'sqx-field-editor[form][formContext][formLevel][formModel][isComparing][language][languages]', @@ -57,6 +57,8 @@ export class FieldEditorComponent { public isEmpty?: Observable; public isExpanded = false; + public chatDialog = new DialogModel(); + public get field() { return this.formModel.field; } @@ -92,4 +94,10 @@ export class FieldEditorComponent { public unset() { this.formModel.unset(); } + + public setValue(value: any) { + this.formModel.setValue(value); + + this.chatDialog.hide(); + } } diff --git a/frontend/src/app/features/settings/pages/templates/template.component.html b/frontend/src/app/features/settings/pages/templates/template.component.html index 8e2403e67..8dfdfb355 100644 --- a/frontend/src/app/features/settings/pages/templates/template.component.html +++ b/frontend/src/app/features/settings/pages/templates/template.component.html @@ -20,7 +20,13 @@
-
+ +
+
+ + + +
\ No newline at end of file diff --git a/frontend/src/app/shared/services/translations.service.spec.ts b/frontend/src/app/shared/services/translations.service.spec.ts index 6736b22fc..5956830a4 100644 --- a/frontend/src/app/shared/services/translations.service.spec.ts +++ b/frontend/src/app/shared/services/translations.service.spec.ts @@ -47,4 +47,30 @@ describe('TranslationsService', () => { expect(translation!).toEqual(new TranslationDto('Translated', 'Hallo')); })); + + it('should make post request to ask question', + inject([TranslationsService, HttpTestingController], (translationsService: TranslationsService, httpMock: HttpTestingController) => { + const dto = { prompt: 'My Question' }; + + let answers: ReadonlyArray; + + translationsService.ask('my-app', dto).subscribe(result => { + answers = result; + }); + + const req = httpMock.expectOne('http://service/p/api/apps/my-app/ask'); + + expect(req.request.method).toEqual('POST'); + expect(req.request.headers.get('If-Match')).toBeNull(); + + req.flush([ + 'Answer1', + 'Answer2', + ]); + + expect(answers!).toEqual([ + 'Answer1', + 'Answer2', + ]); + })); }); diff --git a/frontend/src/app/shared/services/translations.service.ts b/frontend/src/app/shared/services/translations.service.ts index 30b3471f7..3cfc0d911 100644 --- a/frontend/src/app/shared/services/translations.service.ts +++ b/frontend/src/app/shared/services/translations.service.ts @@ -30,6 +30,11 @@ export type TranslateDto = Readonly<{ targetLanguage: string; }>; + export type AskDto = Readonly<{ + // The question to ask. + prompt: string; + }>; + @Injectable() export class TranslationsService { constructor( @@ -47,6 +52,13 @@ export class TranslationsService { }), pretifyError('i18n:translate.translateFailed')); } + + public ask(appName: string, request: AskDto): Observable> { + const url = this.apiUrl.buildUrl(`api/apps/${appName}/ask`); + + return this.http.post(url, request).pipe( + pretifyError('i18n:chatBot.questionFailed')); + } } function parseTranslation(body: any): TranslationDto { diff --git a/frontend/src/app/shell/pages/home/home-page.component.ts b/frontend/src/app/shell/pages/home/home-page.component.ts index b2e9d5002..bcf97fd81 100644 --- a/frontend/src/app/shell/pages/home/home-page.component.ts +++ b/frontend/src/app/shell/pages/home/home-page.component.ts @@ -38,9 +38,17 @@ export class HomePageComponent { } try { - const path = await this.authService.loginPopup(redirectPath) || '/app'; + let path = await this.authService.loginPopup(redirectPath); - this.router.navigateByUrl(path, { replaceUrl: true }); + if (!path) { + path = '/app'; + } + + const success = await this.router.navigateByUrl(path, { replaceUrl: true }); + + if (!success) { + this.router.navigate(['/app'], { replaceUrl: true }); + } } catch { this.router.navigate(['/'], { replaceUrl: true }); }