Browse Source

Improve AI and markdown handling. (#1107)

pull/1108/head
Sebastian Stehle 2 years ago
committed by GitHub
parent
commit
b1892dd64c
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 4
      backend/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj
  2. 14
      backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj
  3. 1
      backend/src/Squidex/Config/Domain/InfrastructureServices.cs
  4. 22
      backend/src/Squidex/Squidex.csproj
  5. 7
      backend/src/Squidex/appsettings.json
  6. 8
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintScriptEngineHelperTests.cs
  7. 8
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppChatToolsTests.cs
  8. 2
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemasChatToolTests.cs
  9. 2
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj
  10. 65
      frontend/.eslintrc.js
  11. 202
      frontend/package-lock.json
  12. 2
      frontend/package.json
  13. 1
      frontend/src/app/features/api/pages/graphql/graphql-page.component.ts
  14. 6
      frontend/src/app/features/content/shared/forms/field-editor.component.html
  15. 2
      frontend/src/app/features/rules/shared/actions/generic-action.component.ts
  16. 2
      frontend/src/app/features/settings/pages/clients/client-connect-form.component.ts
  17. 9
      frontend/src/app/framework/angular/markdown.directive.spec.ts
  18. 6
      frontend/src/app/framework/angular/markdown.directive.ts
  19. 6
      frontend/src/app/framework/angular/pipes/markdown.pipe.ts
  20. 1
      frontend/src/app/framework/internal.ts
  21. 2
      frontend/src/app/framework/utils/date-helper.ts
  22. 112
      frontend/src/app/framework/utils/markdown-transform.spec.ts
  23. 93
      frontend/src/app/framework/utils/markdown-transform.ts
  24. 66
      frontend/src/app/framework/utils/markdown.spec.ts
  25. 24
      frontend/src/app/framework/utils/markdown.ts
  26. 2
      frontend/src/app/shared/components/assets/asset.component.ts
  27. 1
      frontend/src/app/shared/components/chat-dialog.component.html
  28. 3
      frontend/src/app/shared/components/chat-dialog.component.ts
  29. 7
      frontend/src/app/shared/components/chat-item.component.html
  30. 54
      frontend/src/app/shared/components/chat-item.component.ts
  31. 2
      frontend/src/app/shared/components/contents/content-list-field.component.ts
  32. 6
      frontend/src/app/shared/components/forms/rich-editor.component.html
  33. 2
      frontend/src/app/shared/components/search/search-form.component.ts
  34. 2
      frontend/src/app/shared/services/query.ts
  35. 4
      frontend/src/app/shared/services/roles.service.spec.ts
  36. 2
      frontend/src/app/shared/services/translations.service.ts
  37. 4
      frontend/src/app/shell/pages/internal/chat-menu.component.ts
  38. 2
      frontend/src/app/shell/pages/internal/internal-area.component.ts

4
backend/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj

@ -28,8 +28,8 @@
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" />
<PackageReference Include="NJsonSchema" Version="11.0.1" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="Squidex.AI" Version="6.12.0" />
<PackageReference Include="Squidex.Messaging.Subscriptions" Version="6.12.0" />
<PackageReference Include="Squidex.AI" Version="6.18.0" />
<PackageReference Include="Squidex.Messaging.Subscriptions" Version="6.18.0" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
<PackageReference Include="System.Collections.Immutable" Version="8.0.0" />
<PackageReference Include="System.Linq.Async" Version="6.0.1" />

14
backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj

@ -11,7 +11,7 @@
<DebugSymbols>True</DebugSymbols>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="MailKit" Version="4.6.0" />
<PackageReference Include="MailKit" Version="4.7.0" />
<PackageReference Include="McMaster.NETCore.Plugins" Version="1.4.0" />
<PackageReference Include="Meziantou.Analyzer" Version="2.0.159">
<PrivateAssets>all</PrivateAssets>
@ -24,12 +24,12 @@
<PackageReference Include="NodaTime" Version="3.1.11" />
<PackageReference Include="OpenTelemetry.Api" Version="1.9.0" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="Squidex.Assets" Version="6.12.0" />
<PackageReference Include="Squidex.Caching" Version="6.12.0" />
<PackageReference Include="Squidex.Hosting.Abstractions" Version="6.12.0" />
<PackageReference Include="Squidex.Log" Version="6.12.0" />
<PackageReference Include="Squidex.Messaging" Version="6.12.0" />
<PackageReference Include="Squidex.Text" Version="6.12.0" />
<PackageReference Include="Squidex.Assets" Version="6.18.0" />
<PackageReference Include="Squidex.Caching" Version="6.18.0" />
<PackageReference Include="Squidex.Hosting.Abstractions" Version="6.18.0" />
<PackageReference Include="Squidex.Log" Version="6.18.0" />
<PackageReference Include="Squidex.Messaging" Version="6.18.0" />
<PackageReference Include="Squidex.Text" Version="6.18.0" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
<PackageReference Include="System.Collections.Immutable" Version="8.0.0" />
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />

1
backend/src/Squidex/Config/Domain/InfrastructureServices.cs

@ -140,6 +140,7 @@ public static class InfrastructureServices
if (!string.IsNullOrWhiteSpace(apiKey))
{
services.AddOpenAIChat(config);
services.AddAIImagePipe();
services.AddDallE(config, options =>
{
options.DownloadImage = true;

22
backend/src/Squidex/Squidex.csproj

@ -65,18 +65,18 @@
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.9.0" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="ReportGenerator" Version="5.3.7" PrivateAssets="all" />
<PackageReference Include="Squidex.Assets.Azure" Version="6.12.0" />
<PackageReference Include="Squidex.Assets.GoogleCloud" Version="6.12.0" />
<PackageReference Include="Squidex.Assets.FTP" Version="6.12.0" />
<PackageReference Include="Squidex.Assets.ImageMagick" Version="6.12.0" />
<PackageReference Include="Squidex.Assets.ImageSharp" Version="6.12.0" />
<PackageReference Include="Squidex.Assets.Mongo" Version="6.12.0" />
<PackageReference Include="Squidex.Assets.S3" Version="6.12.0" />
<PackageReference Include="Squidex.Assets.TusAdapter" Version="6.12.0" />
<PackageReference Include="Squidex.Assets.Azure" Version="6.18.0" />
<PackageReference Include="Squidex.Assets.GoogleCloud" Version="6.18.0" />
<PackageReference Include="Squidex.Assets.FTP" Version="6.18.0" />
<PackageReference Include="Squidex.Assets.ImageMagick" Version="6.18.0" />
<PackageReference Include="Squidex.Assets.ImageSharp" Version="6.18.0" />
<PackageReference Include="Squidex.Assets.Mongo" Version="6.18.0" />
<PackageReference Include="Squidex.Assets.S3" Version="6.18.0" />
<PackageReference Include="Squidex.Assets.TusAdapter" Version="6.18.0" />
<PackageReference Include="Squidex.ClientLibrary" Version="19.2.0" />
<PackageReference Include="Squidex.Hosting" Version="6.12.0" />
<PackageReference Include="Squidex.Messaging.All" Version="6.12.0" />
<PackageReference Include="Squidex.Messaging.Subscriptions" Version="6.12.0" />
<PackageReference Include="Squidex.Hosting" Version="6.18.0" />
<PackageReference Include="Squidex.Messaging.All" Version="6.18.0" />
<PackageReference Include="Squidex.Messaging.Subscriptions" Version="6.18.0" />
<PackageReference Include="Squidex.OpenIddict.MongoDb" Version="5.1.0" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
<PackageReference Include="YDotNet" Version="0.4.0" />

7
backend/src/Squidex/appsettings.json

@ -701,15 +701,16 @@
"You are a bot to generate images.",
"Say hello to the user and explain him the user about your capabilities in a single, short sentence."
],
"tools": [ "dall-e" ]
"tools": ["dall-e"]
},
"text": {
"systemMessages": [
"You are a bot to generate text content.",
"Say hello to the user and explain him about your capabilities in a single, short sentence."
"Say hello to the user and explain him about your capabilities in a single, short sentence.",
"When you are asked to generate content such as articles, add placeholders for image, describe and use the following pattern: <IMG>{description}</IMG>. {description} is the generated image description."
],
"tools": []
"tools": ["none"]
}
}
},

8
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintScriptEngineHelperTests.cs

@ -623,7 +623,13 @@ public class JintScriptEngineHelperTests : IClassFixture<TranslationsFixture>
A<ChatRequest>.That.Matches(x => x.Prompt == "prompt"),
A<ChatContext>._,
A<CancellationToken>._))
.Returns(new ChatResult { Content = "Generated", Metadata = new ChatMetadata(), Tools = [] });
.Returns(new ChatResult
{
Content = "Generated",
ToolStarts = [],
ToolEnds = [],
Metadata = new ChatMetadata(),
});
var vars = new ScriptVars
{

8
backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppChatToolsTests.cs

@ -45,7 +45,7 @@ public class AppChatToolsTests : GivenContext
var result = await tool.ExecuteAsync(Activator.CreateInstance<ToolContext>(), default);
Assert.Contains($"{App.Name}:default", result);
Assert.Contains($"{App.Name}:default", result, StringComparison.Ordinal);
A.CallTo(() => urlGenerator.ClientsUI(AppId))
.MustHaveHappened();
@ -72,7 +72,7 @@ public class AppChatToolsTests : GivenContext
var result = await tool.ExecuteAsync(Activator.CreateInstance<ToolContext>(), default);
Assert.Contains($"\"de\"", result);
Assert.Contains($"\"de\"", result, StringComparison.Ordinal);
A.CallTo(() => urlGenerator.LanguagesUI(AppId))
.MustHaveHappened();
@ -99,7 +99,7 @@ public class AppChatToolsTests : GivenContext
var result = await tool.ExecuteAsync(Activator.CreateInstance<ToolContext>(), default);
Assert.Contains($"viewers", result);
Assert.Contains($"viewers", result, StringComparison.Ordinal);
A.CallTo(() => urlGenerator.RolesUI(AppId))
.MustHaveHappened();
@ -126,7 +126,7 @@ public class AppChatToolsTests : GivenContext
var result = await tool.ExecuteAsync(Activator.CreateInstance<ToolContext>(), default);
Assert.Contains($"Business", result);
Assert.Contains($"Business", result, StringComparison.Ordinal);
A.CallTo(() => urlGenerator.PlansUI(AppId))
.MustHaveHappened();

2
backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemasChatToolTests.cs

@ -39,7 +39,7 @@ public class SchemasChatToolTests : GivenContext
var result = await tool.ExecuteAsync(Activator.CreateInstance<ToolContext>(), default);
Assert.Contains(Schema.Name, result);
Assert.Contains(Schema.Name, result, StringComparison.Ordinal);
A.CallTo(() => urlGenerator.SchemasUI(AppId))
.MustHaveHappened();

2
backend/tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj

@ -39,7 +39,7 @@
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
<PackageReference Include="System.Reactive.Linq" Version="6.0.1" />
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
<PackageReference Include="Verify.Xunit" Version="25.0.4" />
<PackageReference Include="Verify.Xunit" Version="25.3.0" />
<PackageReference Include="xunit" Version="2.8.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.1">
<PrivateAssets>all</PrivateAssets>

65
frontend/.eslintrc.js

@ -17,24 +17,23 @@ module.exports = {
"plugins": [
"deprecation",
"eslint-plugin-import",
"@typescript-eslint",
"@typescript-eslint"
],
"rules": {
"deprecation/deprecation": "warn",
"@angular-eslint/directive-selector": [
"@angular-eslint/component-selector": [
"error",
{
"type": "attribute",
"prefix": "sqx",
"style": "camelCase"
"style": "kebab-case",
"type": "element"
}
],
"@angular-eslint/component-selector": [
"@angular-eslint/directive-selector": [
"error",
{
"type": "element",
"prefix": "sqx",
"style": "kebab-case"
"style": "camelCase",
"type": "attribute"
}
],
"@angular-eslint/use-lifecycle-interface": [
@ -59,27 +58,26 @@ module.exports = {
"@typescript-eslint/naming-convention": [
"error",
{
"selector": "variable",
"format": [
"camelCase",
"PascalCase",
"UPPER_CASE",
"UPPER_CASE"
],
"leadingUnderscore": "allow",
"trailingUnderscore": "allow",
"selector": "variable",
"trailingUnderscore": "allow"
},
{
"selector": "typeLike",
"format": [
"PascalCase"
],
"selector": "typeLike"
}
],
"@typescript-eslint/no-shadow": "off",
"@typescript-eslint/no-this-alias": "error",
"@typescript-eslint/no-unnecessary-boolean-literal-compare": "error",
"@typescript-eslint/no-unused-expressions": "off",
"@typescript-eslint/no-use-before-define": "off",
"@typescript-eslint/no-shadow": "off",
"@typescript-eslint/no-unused-vars": [
"error",
{
@ -87,11 +85,12 @@ module.exports = {
"varsIgnorePattern": "^_"
}
],
"@typescript-eslint/return-await": "off",
"@typescript-eslint/no-use-before-define": "off",
"@typescript-eslint/quotes": [
"error",
"single"
],
"@typescript-eslint/return-await": "off",
"@typescript-eslint/semi": [
"error",
"always"
@ -100,22 +99,30 @@ module.exports = {
"arrow-parens": "off",
"class-methods-use-this": "off",
"default-case": "off",
"deprecation/deprecation": "warn",
"function-paren-newline": "off",
"implicit-arrow-linebreak": "off",
"import/extensions": "off",
"import/no-extraneous-dependencies": "off",
"import/no-useless-path-segments": "off",
"import/order": ["error", {
"pathGroupsExcludedImportTypes": ["builtin"],
"pathGroups": [{
"pattern": "@app/**",
"group": "external",
"position": "after"
}],
"alphabetize": {
"order": "asc"
"import/order": [
"error",
{
"alphabetize": {
"order": "asc"
},
"pathGroups": [
{
"group": "external",
"pattern": "@app/**",
"position": "after"
}
],
"pathGroupsExcludedImportTypes": [
"builtin"
]
}
}],
],
"import/prefer-default-export": "off",
"linebreak-style": "off",
"max-classes-per-file": "off",
@ -133,14 +140,14 @@ module.exports = {
"object-curly-newline": [
"error",
{
"ExportDeclaration": "never",
"ImportDeclaration": "never",
"ObjectExpression": {
"consistent": true
},
"ObjectPattern": {
"consistent": true
},
"ImportDeclaration": "never",
"ExportDeclaration": "never"
}
}
],
"operator-linebreak": "off",
@ -151,6 +158,6 @@ module.exports = {
"ignoreCase": true,
"ignoreDeclarationSort": true
}
],
]
}
};

202
frontend/package-lock.json

@ -103,7 +103,7 @@
"@typescript-eslint/eslint-plugin": "^7.11.0",
"@typescript-eslint/parser": "^7.11.0",
"@webcomponents/custom-elements": "^1.6.0",
"eslint": "^9.3.0",
"eslint": "^8.57.0",
"eslint-config-airbnb-typescript": "18.0.0",
"eslint-plugin-deprecation": "^2.0.0",
"eslint-plugin-import": "2.29.1",
@ -4432,15 +4432,15 @@
}
},
"node_modules/@eslint/eslintrc": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.1.0.tgz",
"integrity": "sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==",
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz",
"integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==",
"dev": true,
"dependencies": {
"ajv": "^6.12.4",
"debug": "^4.3.2",
"espree": "^10.0.1",
"globals": "^14.0.0",
"espree": "^9.6.0",
"globals": "^13.19.0",
"ignore": "^5.2.0",
"import-fresh": "^3.2.1",
"js-yaml": "^4.1.0",
@ -4448,7 +4448,7 @@
"strip-json-comments": "^3.1.1"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
},
"funding": {
"url": "https://opencollective.com/eslint"
@ -4477,12 +4477,15 @@
"dev": true
},
"node_modules/@eslint/eslintrc/node_modules/globals": {
"version": "14.0.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
"integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==",
"version": "13.24.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz",
"integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==",
"dev": true,
"dependencies": {
"type-fest": "^0.20.2"
},
"engines": {
"node": ">=18"
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
@ -4506,13 +4509,25 @@
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
"dev": true
},
"node_modules/@eslint/eslintrc/node_modules/type-fest": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
"integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
"dev": true,
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@eslint/js": {
"version": "9.3.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.3.0.tgz",
"integrity": "sha512-niBqk8iwv96+yuTwjM6bWg8ovzAPF9qkICsGtcoa5/dmqcEMfdwNAX7+/OHcJHc7wj7XqPxH98oAHytFYlw6Sw==",
"version": "8.57.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz",
"integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==",
"dev": true,
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
}
},
"node_modules/@fal-works/esbuild-plugin-global-externals": {
@ -4699,12 +4714,13 @@
}
},
"node_modules/@humanwhocodes/config-array": {
"version": "0.13.0",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz",
"integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==",
"version": "0.11.14",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz",
"integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==",
"deprecated": "Use @eslint/config-array instead",
"dev": true,
"dependencies": {
"@humanwhocodes/object-schema": "^2.0.3",
"@humanwhocodes/object-schema": "^2.0.2",
"debug": "^4.3.1",
"minimatch": "^3.0.5"
},
@ -4728,21 +4744,9 @@
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz",
"integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==",
"deprecated": "Use @eslint/object-schema instead",
"dev": true
},
"node_modules/@humanwhocodes/retry": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.0.tgz",
"integrity": "sha512-d2CGZR2o7fS6sWB7DG/3a95bGKQyHMACZ5aW8qGkkqQpUoZV6C0X7Pc7l4ZNMZkfNBf4VWNe9E1jRsf0G146Ew==",
"dev": true,
"engines": {
"node": ">=18.18"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/nzakas"
}
},
"node_modules/@iharbeck/ngx-virtual-scroller": {
"version": "17.0.2",
"resolved": "https://registry.npmjs.org/@iharbeck/ngx-virtual-scroller/-/ngx-virtual-scroller-17.0.2.tgz",
@ -15080,37 +15084,41 @@
}
},
"node_modules/eslint": {
"version": "9.3.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.3.0.tgz",
"integrity": "sha512-5Iv4CsZW030lpUqHBapdPo3MJetAPtejVW8B84GIcIIv8+ohFaddXsrn1Gn8uD9ijDb+kcYKFUVmC8qG8B2ORQ==",
"version": "8.57.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz",
"integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==",
"dev": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.6.1",
"@eslint/eslintrc": "^3.1.0",
"@eslint/js": "9.3.0",
"@humanwhocodes/config-array": "^0.13.0",
"@eslint/eslintrc": "^2.1.4",
"@eslint/js": "8.57.0",
"@humanwhocodes/config-array": "^0.11.14",
"@humanwhocodes/module-importer": "^1.0.1",
"@humanwhocodes/retry": "^0.3.0",
"@nodelib/fs.walk": "^1.2.8",
"@ungap/structured-clone": "^1.2.0",
"ajv": "^6.12.4",
"chalk": "^4.0.0",
"cross-spawn": "^7.0.2",
"debug": "^4.3.2",
"doctrine": "^3.0.0",
"escape-string-regexp": "^4.0.0",
"eslint-scope": "^8.0.1",
"eslint-visitor-keys": "^4.0.0",
"espree": "^10.0.1",
"eslint-scope": "^7.2.2",
"eslint-visitor-keys": "^3.4.3",
"espree": "^9.6.1",
"esquery": "^1.4.2",
"esutils": "^2.0.2",
"fast-deep-equal": "^3.1.3",
"file-entry-cache": "^8.0.0",
"file-entry-cache": "^6.0.1",
"find-up": "^5.0.0",
"glob-parent": "^6.0.2",
"globals": "^13.19.0",
"graphemer": "^1.4.0",
"ignore": "^5.2.0",
"imurmurhash": "^0.1.4",
"is-glob": "^4.0.0",
"is-path-inside": "^3.0.3",
"js-yaml": "^4.1.0",
"json-stable-stringify-without-jsonify": "^1.0.1",
"levn": "^0.4.1",
"lodash.merge": "^4.6.2",
@ -15124,7 +15132,7 @@
"eslint": "bin/eslint.js"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
},
"funding": {
"url": "https://opencollective.com/eslint"
@ -15607,6 +15615,12 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/eslint/node_modules/argparse": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
"dev": true
},
"node_modules/eslint/node_modules/chalk": {
"version": "4.1.2",
"dev": true,
@ -15650,28 +15664,16 @@
}
},
"node_modules/eslint/node_modules/eslint-scope": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.0.1.tgz",
"integrity": "sha512-pL8XjgP4ZOmmwfFE8mEhSxA7ZY4C+LWyqjQ3o4yWkkmD0qcMT9kkW3zWHOczhWcjTSgqycYAgwSlXvZltv65og==",
"version": "7.2.2",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz",
"integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==",
"dev": true,
"dependencies": {
"esrecurse": "^4.3.0",
"estraverse": "^5.2.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"url": "https://opencollective.com/eslint"
}
},
"node_modules/eslint/node_modules/eslint-visitor-keys": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz",
"integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==",
"dev": true,
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
},
"funding": {
"url": "https://opencollective.com/eslint"
@ -15712,6 +15714,21 @@
"node": ">=10.13.0"
}
},
"node_modules/eslint/node_modules/globals": {
"version": "13.24.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz",
"integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==",
"dev": true,
"dependencies": {
"type-fest": "^0.20.2"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/eslint/node_modules/has-flag": {
"version": "4.0.0",
"dev": true,
@ -15720,6 +15737,18 @@
"node": ">=8"
}
},
"node_modules/eslint/node_modules/js-yaml": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
"dev": true,
"dependencies": {
"argparse": "^2.0.1"
},
"bin": {
"js-yaml": "bin/js-yaml.js"
}
},
"node_modules/eslint/node_modules/json-schema-traverse": {
"version": "0.4.1",
"dev": true,
@ -15778,30 +15807,30 @@
"node": ">=8"
}
},
"node_modules/espree": {
"version": "10.0.1",
"resolved": "https://registry.npmjs.org/espree/-/espree-10.0.1.tgz",
"integrity": "sha512-MWkrWZbJsL2UwnjxTX3gG8FneachS/Mwg7tdGXce011sJd5b0JG54vat5KHnfSBODZ3Wvzd2WnjxyzsRoVv+ww==",
"node_modules/eslint/node_modules/type-fest": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
"integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
"dev": true,
"dependencies": {
"acorn": "^8.11.3",
"acorn-jsx": "^5.3.2",
"eslint-visitor-keys": "^4.0.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
"node": ">=10"
},
"funding": {
"url": "https://opencollective.com/eslint"
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/espree/node_modules/eslint-visitor-keys": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz",
"integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==",
"node_modules/espree": {
"version": "9.6.1",
"resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz",
"integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==",
"dev": true,
"dependencies": {
"acorn": "^8.9.0",
"acorn-jsx": "^5.3.2",
"eslint-visitor-keys": "^3.4.1"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
},
"funding": {
"url": "https://opencollective.com/eslint"
@ -16182,15 +16211,15 @@
"dev": true
},
"node_modules/file-entry-cache": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
"integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==",
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
"integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==",
"dev": true,
"dependencies": {
"flat-cache": "^4.0.0"
"flat-cache": "^3.0.4"
},
"engines": {
"node": ">=16.0.0"
"node": "^10.12.0 || >=12.0.0"
}
},
"node_modules/file-system-cache": {
@ -16339,16 +16368,17 @@
}
},
"node_modules/flat-cache": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
"integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==",
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz",
"integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==",
"dev": true,
"dependencies": {
"flatted": "^3.2.9",
"keyv": "^4.5.4"
"keyv": "^4.5.3",
"rimraf": "^3.0.2"
},
"engines": {
"node": ">=16"
"node": "^10.12.0 || >=12.0.0"
}
},
"node_modules/flatted": {

2
frontend/package.json

@ -110,7 +110,7 @@
"@typescript-eslint/eslint-plugin": "^7.11.0",
"@typescript-eslint/parser": "^7.11.0",
"@webcomponents/custom-elements": "^1.6.0",
"eslint": "^9.3.0",
"eslint": "^8.57.0",
"eslint-config-airbnb-typescript": "18.0.0",
"eslint-plugin-deprecation": "^2.0.0",
"eslint-plugin-import": "2.29.1",

1
frontend/src/app/features/api/pages/graphql/graphql-page.component.ts

@ -103,6 +103,7 @@ export class GraphQLPageComponent implements AfterViewInit, OnInit {
subscriptionUrl,
});
// eslint-disable-next-line deprecation/deprecation
ReactDOM.render(
React.createElement(GraphiQL, {
fetcher,

6
frontend/src/app/features/content/shared/forms/field-editor.component.html

@ -342,4 +342,8 @@
</div>
}
<sqx-chat-dialog (contentSelect)="setValue($event)" *sqxModal="chatDialog"></sqx-chat-dialog>
<sqx-chat-dialog
[configuration]="'text'"
(contentSelect)="setValue($event)"
[folderId]="field.rawProperties['folderId']"
*sqxModal="chatDialog"></sqx-chat-dialog>

2
frontend/src/app/features/rules/shared/actions/generic-action.component.ts

@ -29,7 +29,7 @@ import { FormattableInputComponent } from './formattable-input.component';
LowerCasePipe,
MarkdownDirective,
ReactiveFormsModule,
TranslatePipe
TranslatePipe,
],
})
export class GenericActionComponent {

2
frontend/src/app/features/settings/pages/clients/client-connect-form.component.ts

@ -26,7 +26,7 @@ import { AccessTokenDto, ApiUrlConfig, AppsState, ClientDto, ClientsService, Cli
ModalDialogComponent,
SafeHtmlPipe,
TooltipDirective,
TranslatePipe
TranslatePipe,
],
})
export class ClientConnectFormComponent implements OnInit {

9
frontend/src/app/framework/angular/markdown.directive.spec.ts

@ -52,6 +52,13 @@ describe('MarkdownDirective', () => {
verifyHtmlRender('<strong>bold</strong>');
});
it('should render with exclamation if not optional', () => {
markdownDirective.markdown = '!text';
markdownDirective.ngOnChanges();
verifyTextRender('!text');
});
it('should render as HTML if it has encoded characters', () => {
markdownDirective.inline = false;
markdownDirective.markdown = '\'Example\'';
@ -69,7 +76,7 @@ describe('MarkdownDirective', () => {
});
it('should render as inline HTML if it has tags', () => {
markdownDirective.markdown = '!**bold**';
markdownDirective.markdown = '**bold**';
markdownDirective.ngOnChanges();
verifyHtmlRender('<strong>bold</strong>');

6
frontend/src/app/framework/angular/markdown.directive.ts

@ -6,7 +6,7 @@
*/
import { booleanAttribute, Directive, ElementRef, Input, Renderer2 } from '@angular/core';
import { renderMarkdown } from '@app/framework/internal';
import { markdownRender } from '@app/framework/internal';
@Directive({
selector: '[sqxMarkdown]',
@ -38,7 +38,7 @@ export class MarkdownDirective {
const hasExclamation = markdown.indexOf('!') === 0;
if (hasExclamation) {
if (hasExclamation && this.optional) {
markdown = markdown.substring(1);
}
@ -47,7 +47,7 @@ export class MarkdownDirective {
} else if (this.optional && !hasExclamation) {
html = markdown;
} else if (this.markdown) {
html = renderMarkdown(markdown, this.inline, this.trusted);
html = markdownRender(markdown, this.inline, this.trusted);
}
const hasHtml = html.indexOf('<') >= 0 || html.indexOf('&') >= 0;

6
frontend/src/app/framework/angular/pipes/markdown.pipe.ts

@ -6,7 +6,7 @@
*/
import { Pipe, PipeTransform } from '@angular/core';
import { renderMarkdown } from '@app/framework/internal';
import { markdownRender } from '@app/framework/internal';
@Pipe({
name: 'sqxMarkdown',
@ -15,7 +15,7 @@ import { renderMarkdown } from '@app/framework/internal';
})
export class MarkdownPipe implements PipeTransform {
public transform(text: string | undefined | null, trusted = false): string {
return renderMarkdown(text, false, trusted);
return markdownRender(text, false, trusted);
}
}
@ -26,6 +26,6 @@ export class MarkdownPipe implements PipeTransform {
})
export class MarkdownInlinePipe implements PipeTransform {
public transform(text: string | undefined | null, trusted = false): string {
return renderMarkdown(text, true, trusted);
return markdownRender(text, true, trusted);
}
}

1
frontend/src/app/framework/internal.ts

@ -38,6 +38,7 @@ export * from './utils/interpolator';
export * from './utils/keys';
export * from './utils/math-helper';
export * from './utils/markdown';
export * from './utils/markdown-transform';
export * from './utils/modal-view';
export * from './utils/picasso';
export * from './utils/rxjs-extensions';

2
frontend/src/app/framework/utils/date-helper.ts

@ -5,7 +5,7 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { Locale, enUS, it, nl, zhCN } from 'date-fns/locale';
import { enUS, it, Locale, nl, zhCN } from 'date-fns/locale';
export module DateHelper {
let locale: string | null;

112
frontend/src/app/framework/utils/markdown-transform.spec.ts

@ -0,0 +1,112 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { markdownExtractImage, markdownTransformImages } from './markdown-transform';
describe('MarkdownTransform', () => {
it('should not extract image if markdown contains no image', () => {
const md = '# Header';
const result = markdownExtractImage(md);
expect(result).toBeNull();
});
it('should not extract image if URL is not valid', () => {
const md = '![](/image.png)';
const result = markdownExtractImage(md);
expect(result).toBeNull();
});
it('should extract image', () => {
const md = '![](https://squidex.io/image.png)';
const result = markdownExtractImage(md);
expect(result).toEqual({ url: 'https://squidex.io/image.png', name: 'image.webp' });
});
it('should extract image with name', () => {
const md = '![](https://squidex.io/image.png "My Picture")';
const result = markdownExtractImage(md);
expect(result).toEqual({ url: 'https://squidex.io/image.png', name: 'my-picture.webp' });
});
it('should extract image with lax name', () => {
const md = '![](https://squidex.io/image.png Picture)';
const result = markdownExtractImage(md);
expect(result).toEqual({ url: 'https://squidex.io/image.png', name: 'picture.webp' });
});
it('should extract image with alt', () => {
const md = '![Alt Text](https://squidex.io/image.png)';
const result = markdownExtractImage(md);
expect(result).toEqual({ url: 'https://squidex.io/image.png', name: 'alt-text.webp' });
});
it('should transform image url', async () => {
const md = '![](https://squidex.io/image.png)';
const result = await markdownTransformImages(md, img => Promise.resolve(`${img.url}?transformed`));
expect(result).toEqual('![](https://squidex.io/image.png?transformed)');
});
it('should transform with name', async () => {
const md = '![](https://squidex.io/image.png "My Picture")';
const result = await markdownTransformImages(md, img => Promise.resolve(`${img.url}?transformed`));
expect(result).toEqual('![](https://squidex.io/image.png?transformed "My Picture")');
});
it('should transform with lax name', async () => {
const md = '![](https://squidex.io/image.png Picture)';
const result = await markdownTransformImages(md, img => Promise.resolve(`${img.url}?transformed`));
expect(result).toEqual('![](https://squidex.io/image.png?transformed "Picture")');
});
it('should transform with alt', async () => {
const md = '![Alt](https://squidex.io/image.png)';
const result = await markdownTransformImages(md, img => Promise.resolve(`${img.url}?transformed`));
expect(result).toEqual('![Alt](https://squidex.io/image.png?transformed)');
});
it('should transform multiple images', async () => {
const md = `
# Header 1
![Alt1](https://squidex.io/image1.png "Picture1")
![Alt2](https://squidex.io/image2.png "Picture2")
## Header 2
`;
const result = await markdownTransformImages(md, img => Promise.resolve(`${img.url}?transformed`));
expect(result).toEqual(`
# Header 1
![Alt1](https://squidex.io/image1.png?transformed "Picture1")
![Alt2](https://squidex.io/image2.png?transformed "Picture2")
## Header 2
`);
});
});

93
frontend/src/app/framework/utils/markdown-transform.ts

@ -0,0 +1,93 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import slugify from 'slugify';
import { MathHelper } from './math-helper';
const IMAGE_REGEX = /!\[(?<alt>[^\]]*)\]\((?<url>.*?)([\s]["\s]*(?<name>[^")]*)["\s]*)?\)/;
const IMAGES_REGEX = /!\[(?<alt>[^\]]*)\]\((?<url>.*?)([\s]["\s]*(?<name>[^")]*)["\s]*)?\)/g;
export type MarkdownImage = { url: string; name: string };
export function markdownHasImage(markdown: string) {
return !!markdown && !!markdown.match(IMAGES_REGEX);
}
export function markdownExtractImage(markdown: string): MarkdownImage | null {
if (!markdown) {
return null;
}
const match = markdown.match(IMAGE_REGEX);
if (!match?.groups) {
return null;
}
const { url, alt, name } = match.groups as { url: string; alt?: string; name?: string };
if (!isURL(url)) {
return null;
}
return toImage({ url, alt, name });
}
export async function markdownTransformImages(markdown: string, replace: (image: MarkdownImage) => Promise<string>) {
if (!markdown) {
return markdown;
}
const jobs: { id: string; url: string; name?: string; alt?: string }[] = [];
let transformed = markdown.replace(IMAGES_REGEX, (_, alt, url, _other, name) => {
const id = MathHelper.guid();
jobs.push({ id, url, name, alt });
return id;
});
const promises = jobs.map(async job => {
const url = await replace(toImage(job));
return { job, url };
});
const results = await Promise.all(promises);
for (const result of results) {
const { job, url } = result;
const name = job.name ? ` "${job.name}"` : '';
transformed = transformed.replace(result.job.id, `![${job.alt || ''}](${url}${name})`);
}
return transformed;
}
const IMAGE_EXTENSIONS = ['.avif', '.jpeg', '.jpg', '.png', '.webp'];
function toImage(image: { url: string; name?: string; alt?: string }): MarkdownImage {
let name = image.name || image.alt || 'image';
name = slugify(name, { lower: true, trim: true });
if (!IMAGE_EXTENSIONS.find(ex => name.endsWith(ex))) {
name += '.webp';
}
return { url: image.url, name };
}
function isURL(input: string) {
try {
new URL(input);
return true;
} catch {
return false;
}
}

66
frontend/src/app/framework/utils/markdown.spec.ts

@ -0,0 +1,66 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { markdownRender } from './markdown';
describe('Markdown', () => {
it('should render text', () => {
const md = 'Text';
const result = markdownRender(md, false);
expect(result).toEqual('<p>Text</p>\n');
});
it('should render text inline', () => {
const md = 'Text';
const result = markdownRender(md, true);
expect(result).toEqual('Text');
});
it('should render escaped', () => {
const md = '<h1>Header</h1>';
const result = markdownRender(md, false);
expect(result).toEqual('<p>&lt;h1&gt;Header&lt;/h1&gt;</p>\n');
});
it('should render non escaped', () => {
const md = '<h1>Header</h1>';
const result = markdownRender(md, false, true);
expect(result).toEqual('<h1>Header</h1>');
});
it('should render mailto link', () => {
const md = '[mail](mailto:hello@squidex.io)';
const result = markdownRender(md, true);
expect(result).toEqual('<a href="mailto:hello@squidex.io">mail</a>');
});
it('should render normal link', () => {
const md = '[squidex](https://squidex.io)';
const result = markdownRender(md, true);
expect(result).toEqual('<a href="https://squidex.io" target="_blank", rel="noopener">squidex <i class="icon-external-link"></i></a>');
});
it('should render image', () => {
const md = '![{name}](https://localhost:5001/ai-images/dall-e/ea68c867-6472-4d77-a526-7c9d9c4698fe)';
const result = markdownRender(md, true);
expect(result).toEqual('<img src="https://localhost:5001/ai-images/dall-e/ea68c867-6472-4d77-a526-7c9d9c4698fe" alt="{name}">');
});
});

24
frontend/src/app/framework/utils/markdown.ts

@ -10,7 +10,7 @@ import { MathHelper } from './math-helper';
function renderLink(href: string, _: string, text: string) {
if (href && href.startsWith('mailto')) {
return text;
return `<a href="${href}">${text}</a>`;
} else {
return `<a href="${href}" target="_blank", rel="noopener">${text} <i class="icon-external-link"></i></a>`;
}
@ -27,23 +27,23 @@ function renderCode(code: string) {
<pre class="code" id="${id}">${code}</pre>
</div>
`;
`.trim();
}
function renderInlineParagraph(text: string) {
return text;
}
const RENDERER_DEFAULT = new marked.Renderer();
const RENDERER_INLINE = new marked.Renderer();
RENDERER_INLINE.paragraph = renderInlineParagraph;
RENDERER_INLINE.link = renderLink;
RENDERER_INLINE.code = renderCode;
const RENDERER_DEFAULT = new marked.Renderer();
RENDERER_DEFAULT.link = renderLink;
RENDERER_DEFAULT.code = renderCode;
export function renderMarkdown(input: string | undefined | null, inline: boolean, trusted = false) {
export function markdownRender(input: string | undefined | null, inline: boolean, trusted = false) {
if (!input) {
return '';
}
@ -59,9 +59,9 @@ export function renderMarkdown(input: string | undefined | null, inline: boolean
}
}
const escapeTestNoEncode = /[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/;
const escapeReplaceNoEncode = new RegExp(escapeTestNoEncode.source, 'g');
const escapeReplacements = {
const ESCAPE_REPLACE_NO_ENCODE = /[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/g;
const ESCAPE_REPLACEMENTS = {
'&' : '&amp;',
'<' : '&lt;',
'>' : '&gt;',
@ -69,12 +69,6 @@ const escapeReplacements = {
'\'': '&#39;',
} as Record<string, string>;
const getEscapeReplacement = (ch: string) => escapeReplacements[ch];
export function escapeHTML(html: string) {
if (escapeTestNoEncode.test(html)) {
return html.replace(escapeReplaceNoEncode, getEscapeReplacement);
}
return html;
return html.replace(ESCAPE_REPLACE_NO_ENCODE, c => ESCAPE_REPLACEMENTS[c]);
}

2
frontend/src/app/shared/components/assets/asset.component.ts

@ -37,7 +37,7 @@ interface State {
TooltipDirective,
TranslatePipe,
UserNameRefPipe,
UserPictureRefPipe
UserPictureRefPipe,
],
})
export class AssetComponent extends StatefulComponent<State> implements OnInit {

1
frontend/src/app/shared/components/chat-dialog.component.html

@ -11,6 +11,7 @@
(contentSelect)="contentSelect.emit($event)"
[copyMode]="copyMode"
(done)="setCompleted()"
[folderId]="folderId"
[isFirst]="isFirst"
[isLast]="isLast"
[type]="item.type"

3
frontend/src/app/shared/components/chat-dialog.component.ts

@ -47,6 +47,9 @@ export class ChatDialogComponent extends StatefulComponent<State> {
@Input()
public configuration?: string;
@Input()
public folderId?: string;
@Input()
public copyMode?: 'Text' | 'Image';

7
frontend/src/app/shared/components/chat-item.component.html

@ -35,7 +35,7 @@
</div>
<div class="col">
<div class="bubble bubble-right use-container">
<div class="content" #contentElement (sqxResized)="scrollIntoView()">
<div class="content" (sqxResized)="scrollIntoView()">
@if (snapshot.runningTools.length > 0) {
<div class="mb-2">
@for (tool of snapshot.runningTools; track tool) {
@ -54,8 +54,11 @@
</span>
}
@if (!snapshot.isRunning && !isFirst && type === "Bot") {
<button class="btn btn-secondary btn-sm btn-text" (click)="selectContent()" type="button">
<button class="btn btn-secondary btn-sm btn-text" (click)="selectContent()" [disabled]="snapshot.isCopying" type="button">
{{ "chat.use" | sqxTranslate }}
@if (snapshot.isCopying) {
<sqx-loader></sqx-loader>
}
</button>
}
</div>

54
frontend/src/app/shared/components/chat-item.component.ts

@ -7,9 +7,10 @@
import { ChangeDetectionStrategy, Component, ElementRef, EventEmitter, Input, Output, ViewChild } from '@angular/core';
import { Observable } from 'rxjs';
import { HTTP, MarkdownDirective, ResizedDirective, StatefulComponent, TranslatePipe, Types } from '@app/framework';
import { ChatEventDto, Profile } from '../internal';
import { lastValueFrom, Observable } from 'rxjs';
import { filter } from 'rxjs/operators';
import { ApiUrlConfig, HTTP, LoaderComponent, MarkdownDirective, markdownExtractImage, markdownHasImage, markdownTransformImages, ResizedDirective, StatefulComponent, TranslatePipe, Types } from '@app/framework';
import { AssetDto, AssetUploaderState, ChatEventDto, Profile } from '../internal';
import { UserIdPicturePipe } from './pipes';
interface State {
@ -19,6 +20,9 @@ interface State {
// True, when failed
isFailed: boolean;
// True, when a copy is in process.
isCopying: boolean;
// The content.
content: string;
@ -32,6 +36,7 @@ interface State {
styleUrls: ['./chat-item.component.scss'],
templateUrl: './chat-item.component.html',
imports: [
LoaderComponent,
MarkdownDirective,
ResizedDirective,
TranslatePipe,
@ -43,12 +48,12 @@ export class ChatItemComponent extends StatefulComponent<State> {
@ViewChild('focusElement', { static: false })
public focusElement!: ElementRef<HTMLElement>;
@ViewChild('contentElement', { static: false })
public contentElement!: ElementRef<HTMLElement>;
@Input({ required: true })
public type: 'Bot' | 'User' | 'System' = 'Bot';
@Input({ required: true })
public folderId?: string;
@Input({ required: true })
public user!: Profile;
@ -105,9 +110,13 @@ export class ChatItemComponent extends StatefulComponent<State> {
@Output()
public contentSelect = new EventEmitter<string | HTTP.UploadFile | undefined | null>();
constructor() {
constructor(
private readonly apiUrl: ApiUrlConfig,
private readonly assetUploader: AssetUploaderState,
) {
super({
content: '',
isCopying: false,
isFailed: false,
isRunning: false,
runningTools: [],
@ -122,19 +131,36 @@ export class ChatItemComponent extends StatefulComponent<State> {
this.focusElement.nativeElement?.scrollIntoView();
}
public selectContent() {
this.contentSelect.emit(this.snapshot.content);
public async selectContent() {
let markdown = this.snapshot.content;
if (!markdownHasImage(markdown)) {
this.contentSelect.emit(markdown);
}
this.next({ isCopying: true });
try {
markdown = await markdownTransformImages(markdown, async img => {
const asset = await lastValueFrom(
this.assetUploader.uploadFile(img, this.folderId)
.pipe(filter(x => Types.is(x, AssetDto)))) as AssetDto;
return asset.fullUrl(this.apiUrl);
});
this.contentSelect.emit(markdown);
} finally {
this.next({ isCopying: false });
}
}
public selectImage() {
const image = this.contentElement.nativeElement?.querySelector('img');
const image = markdownExtractImage(this.snapshot.content);
if (!image) {
return;
}
const name = image.alt || 'image.webp';
this.contentSelect.emit({ url: image.src, name });
this.contentSelect.emit(image);
}
}
}

2
frontend/src/app/shared/components/contents/content-list-field.component.ts

@ -37,7 +37,7 @@ interface State {
TranslatePipe,
TranslationStatusComponent,
UserNameRefPipe,
UserPictureRefPipe
UserPictureRefPipe,
],
})
export class ContentListFieldComponent extends StatefulComponent<State> {

6
frontend/src/app/shared/components/forms/rich-editor.component.html

@ -14,4 +14,8 @@
[schemaIdentifiers]="schemaIds"
*sqxModal="contentsDialog"></sqx-content-selector>
<sqx-chat-dialog (contentSelect)="insertText($event)" *sqxModal="chatDialog"></sqx-chat-dialog>
<sqx-chat-dialog
[configuration]="'text'"
(contentSelect)="insertText($event)"
[folderId]="folderId"
*sqxModal="chatDialog"></sqx-chat-dialog>

2
frontend/src/app/shared/components/search/search-form.component.ts

@ -38,7 +38,7 @@ import { SavedQueriesComponent } from './shared-queries.component';
TooltipDirective,
TourHintDirective,
TourStepDirective,
TranslatePipe
TranslatePipe,
],
})
export class SearchFormComponent {

2
frontend/src/app/shared/services/query.ts

@ -146,7 +146,7 @@ export function isLogical(input: FilterNode): input is FilterLogical {
}
export function isComparison(input: FilterNode): input is FilterComparison {
return !isNegation(input) &&! isComparison(input);
return !isNegation(input) && !isComparison(input);
}
export interface QuerySorting {

4
frontend/src/app/shared/services/roles.service.spec.ts

@ -5,10 +5,10 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
import { inject, TestBed } from '@angular/core/testing';
import { ApiUrlConfig, Resource, ResourceLinks, RoleDto, RolesDto, RolesPayload, RolesService, Version } from '@app/shared/internal';
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
describe('RolesService', () => {
const version = new Version('1');
@ -21,7 +21,7 @@ describe('RolesService', () => {
{ provide: ApiUrlConfig, useValue: new ApiUrlConfig('http://service/p/') },
provideHttpClient(withInterceptorsFromDi()),
provideHttpClientTesting(),
]
],
});
});

2
frontend/src/app/shared/services/translations.service.ts

@ -9,7 +9,7 @@ import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { ApiUrlConfig, StringHelper, pretifyError } from '@app/framework';
import { ApiUrlConfig, pretifyError, StringHelper } from '@app/framework';
import { AuthService } from './auth.service';
export class TranslationDto {

4
frontend/src/app/shell/pages/internal/chat-menu.component.ts

@ -18,7 +18,7 @@ import { AppsState, ChatDialogComponent, DialogModel, ModalDirective, UIOptions
imports: [
AsyncPipe,
ChatDialogComponent,
ModalDirective
ModalDirective,
],
})
export class ChatMenuComponent {
@ -27,7 +27,7 @@ export class ChatMenuComponent {
public readonly hasChatBot = inject(UIOptions).value.canUseChatBot;
constructor(
public readonly appsState: AppsState
public readonly appsState: AppsState,
) {
}
}

2
frontend/src/app/shell/pages/internal/internal-area.component.ts

@ -11,12 +11,12 @@ import { ActivatedRoute, RouterLink, RouterOutlet } from '@angular/router';
import { DialogService, LoadingService, Notification, Subscriptions, UIOptions } from '@app/shared';
import { AssetUploaderComponent } from '@app/shared/components/assets/asset-uploader.component';
import { AppsMenuComponent } from './apps-menu.component';
import { ChatMenuComponent } from './chat-menu.component';
import { FeedbackMenuComponent } from './feedback-menu.component';
import { LogoComponent } from './logo.component';
import { NotificationsMenuComponent } from './notifications-menu.component';
import { ProfileMenuComponent } from './profile-menu.component';
import { SearchMenuComponent } from './search-menu.component';
import { ChatMenuComponent } from './chat-menu.component';
@Component({
standalone: true,

Loading…
Cancel
Save