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 7b25fa3ce..678825fa5 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 @@ -28,8 +28,8 @@ - - + + diff --git a/backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj b/backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj index 384d75a72..978c5d302 100644 --- a/backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj +++ b/backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj @@ -11,7 +11,7 @@ True - + all @@ -24,12 +24,12 @@ - - - - - - + + + + + + diff --git a/backend/src/Squidex/Config/Domain/InfrastructureServices.cs b/backend/src/Squidex/Config/Domain/InfrastructureServices.cs index e7e0be95e..749f18e5f 100644 --- a/backend/src/Squidex/Config/Domain/InfrastructureServices.cs +++ b/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; diff --git a/backend/src/Squidex/Squidex.csproj b/backend/src/Squidex/Squidex.csproj index d6c85ad96..1c251975c 100644 --- a/backend/src/Squidex/Squidex.csproj +++ b/backend/src/Squidex/Squidex.csproj @@ -65,18 +65,18 @@ - - - - - - - - + + + + + + + + - - - + + + diff --git a/backend/src/Squidex/appsettings.json b/backend/src/Squidex/appsettings.json index f538fa3d1..918492f0c 100644 --- a/backend/src/Squidex/appsettings.json +++ b/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: {description}. {description} is the generated image description." ], - "tools": [] + "tools": ["none"] } } }, 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 79cf0a4ec..c6fa018f9 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 @@ -623,7 +623,13 @@ public class JintScriptEngineHelperTests : IClassFixture A.That.Matches(x => x.Prompt == "prompt"), A._, A._)) - .Returns(new ChatResult { Content = "Generated", Metadata = new ChatMetadata(), Tools = [] }); + .Returns(new ChatResult + { + Content = "Generated", + ToolStarts = [], + ToolEnds = [], + Metadata = new ChatMetadata(), + }); var vars = new ScriptVars { diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppChatToolsTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppChatToolsTests.cs index 8898998b8..52010af75 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppChatToolsTests.cs +++ b/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(), 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(), 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(), 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(), default); - Assert.Contains($"Business", result); + Assert.Contains($"Business", result, StringComparison.Ordinal); A.CallTo(() => urlGenerator.PlansUI(AppId)) .MustHaveHappened(); diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemasChatToolTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemasChatToolTests.cs index 58abf7b9c..061d17086 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemasChatToolTests.cs +++ b/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(), default); - Assert.Contains(Schema.Name, result); + Assert.Contains(Schema.Name, result, StringComparison.Ordinal); A.CallTo(() => urlGenerator.SchemasUI(AppId)) .MustHaveHappened(); 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 75e2770dc..716e85dcb 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 @@ -39,7 +39,7 @@ - + all diff --git a/frontend/.eslintrc.js b/frontend/.eslintrc.js index 44e04d120..6f9cd1abb 100644 --- a/frontend/.eslintrc.js +++ b/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 } - ], + ] } }; diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 3c30ec1ee..6cf5c03fd 100644 --- a/frontend/package-lock.json +++ b/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": { diff --git a/frontend/package.json b/frontend/package.json index 9db266cbf..ad1c8a8c4 100644 --- a/frontend/package.json +++ b/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", diff --git a/frontend/src/app/features/api/pages/graphql/graphql-page.component.ts b/frontend/src/app/features/api/pages/graphql/graphql-page.component.ts index 1c1e81f6c..c36c46ace 100644 --- a/frontend/src/app/features/api/pages/graphql/graphql-page.component.ts +++ b/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, 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 8052d6af6..fee1a08ed 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 @@ -342,4 +342,8 @@ } - + diff --git a/frontend/src/app/features/rules/shared/actions/generic-action.component.ts b/frontend/src/app/features/rules/shared/actions/generic-action.component.ts index a88f61010..581498616 100644 --- a/frontend/src/app/features/rules/shared/actions/generic-action.component.ts +++ b/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 { diff --git a/frontend/src/app/features/settings/pages/clients/client-connect-form.component.ts b/frontend/src/app/features/settings/pages/clients/client-connect-form.component.ts index 62c737fb9..a4c511564 100644 --- a/frontend/src/app/features/settings/pages/clients/client-connect-form.component.ts +++ b/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 { diff --git a/frontend/src/app/framework/angular/markdown.directive.spec.ts b/frontend/src/app/framework/angular/markdown.directive.spec.ts index ad1b9ae5f..5deeca4a0 100644 --- a/frontend/src/app/framework/angular/markdown.directive.spec.ts +++ b/frontend/src/app/framework/angular/markdown.directive.spec.ts @@ -52,6 +52,13 @@ describe('MarkdownDirective', () => { verifyHtmlRender('bold'); }); + 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('bold'); diff --git a/frontend/src/app/framework/angular/markdown.directive.ts b/frontend/src/app/framework/angular/markdown.directive.ts index 73f4ea113..46f4a8699 100644 --- a/frontend/src/app/framework/angular/markdown.directive.ts +++ b/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; diff --git a/frontend/src/app/framework/angular/pipes/markdown.pipe.ts b/frontend/src/app/framework/angular/pipes/markdown.pipe.ts index cedb9ed2c..d8f18113c 100644 --- a/frontend/src/app/framework/angular/pipes/markdown.pipe.ts +++ b/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); } } \ No newline at end of file diff --git a/frontend/src/app/framework/internal.ts b/frontend/src/app/framework/internal.ts index c51bdfd57..252326d81 100644 --- a/frontend/src/app/framework/internal.ts +++ b/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'; diff --git a/frontend/src/app/framework/utils/date-helper.ts b/frontend/src/app/framework/utils/date-helper.ts index e40678c36..73b2254f9 100644 --- a/frontend/src/app/framework/utils/date-helper.ts +++ b/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; diff --git a/frontend/src/app/framework/utils/markdown-transform.spec.ts b/frontend/src/app/framework/utils/markdown-transform.spec.ts new file mode 100644 index 000000000..edf2b89a8 --- /dev/null +++ b/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 +`); + }); +}); \ No newline at end of file diff --git a/frontend/src/app/framework/utils/markdown-transform.ts b/frontend/src/app/framework/utils/markdown-transform.ts new file mode 100644 index 000000000..04446e5d4 --- /dev/null +++ b/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 = /!\[(?[^\]]*)\]\((?.*?)([\s]["\s]*(?[^")]*)["\s]*)?\)/; +const IMAGES_REGEX = /!\[(?[^\]]*)\]\((?.*?)([\s]["\s]*(?[^")]*)["\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) { + 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; + } +} \ No newline at end of file diff --git a/frontend/src/app/framework/utils/markdown.spec.ts b/frontend/src/app/framework/utils/markdown.spec.ts new file mode 100644 index 000000000..9f716bda3 --- /dev/null +++ b/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('

Text

\n'); + }); + + it('should render text inline', () => { + const md = 'Text'; + + const result = markdownRender(md, true); + + expect(result).toEqual('Text'); + }); + + it('should render escaped', () => { + const md = '

Header

'; + + const result = markdownRender(md, false); + + expect(result).toEqual('

<h1>Header</h1>

\n'); + }); + + it('should render non escaped', () => { + const md = '

Header

'; + + const result = markdownRender(md, false, true); + + expect(result).toEqual('

Header

'); + }); + + it('should render mailto link', () => { + const md = '[mail](mailto:hello@squidex.io)'; + + const result = markdownRender(md, true); + + expect(result).toEqual('mail'); + }); + + it('should render normal link', () => { + const md = '[squidex](https://squidex.io)'; + + const result = markdownRender(md, true); + + expect(result).toEqual('squidex '); + }); + + 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('{name}'); + }); +}); \ No newline at end of file diff --git a/frontend/src/app/framework/utils/markdown.ts b/frontend/src/app/framework/utils/markdown.ts index dcc394d67..d51bd64b2 100644 --- a/frontend/src/app/framework/utils/markdown.ts +++ b/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 `${text}`; } else { return `${text} `; } @@ -27,23 +27,23 @@ function renderCode(code: string) {
${code}
- `; + `.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 = { '&' : '&', '<' : '<', '>' : '>', @@ -69,12 +69,6 @@ const escapeReplacements = { '\'': ''', } as Record; -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]); } \ No newline at end of file diff --git a/frontend/src/app/shared/components/assets/asset.component.ts b/frontend/src/app/shared/components/assets/asset.component.ts index a1dd65b6b..56316c991 100644 --- a/frontend/src/app/shared/components/assets/asset.component.ts +++ b/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 implements OnInit { diff --git a/frontend/src/app/shared/components/chat-dialog.component.html b/frontend/src/app/shared/components/chat-dialog.component.html index 0074a123d..ee73a0a59 100644 --- a/frontend/src/app/shared/components/chat-dialog.component.html +++ b/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" diff --git a/frontend/src/app/shared/components/chat-dialog.component.ts b/frontend/src/app/shared/components/chat-dialog.component.ts index 0678562c8..70dbe53bc 100644 --- a/frontend/src/app/shared/components/chat-dialog.component.ts +++ b/frontend/src/app/shared/components/chat-dialog.component.ts @@ -47,6 +47,9 @@ export class ChatDialogComponent extends StatefulComponent { @Input() public configuration?: string; + @Input() + public folderId?: string; + @Input() public copyMode?: 'Text' | 'Image'; diff --git a/frontend/src/app/shared/components/chat-item.component.html b/frontend/src/app/shared/components/chat-item.component.html index 6293ff4b9..ecbfc9041 100644 --- a/frontend/src/app/shared/components/chat-item.component.html +++ b/frontend/src/app/shared/components/chat-item.component.html @@ -35,7 +35,7 @@
-
+
@if (snapshot.runningTools.length > 0) {
@for (tool of snapshot.runningTools; track tool) { @@ -54,8 +54,11 @@ } @if (!snapshot.isRunning && !isFirst && type === "Bot") { - }
diff --git a/frontend/src/app/shared/components/chat-item.component.ts b/frontend/src/app/shared/components/chat-item.component.ts index cf5163bdf..a380a7924 100644 --- a/frontend/src/app/shared/components/chat-item.component.ts +++ b/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 { @ViewChild('focusElement', { static: false }) public focusElement!: ElementRef; - @ViewChild('contentElement', { static: false }) - public contentElement!: ElementRef; - @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 { @Output() public contentSelect = new EventEmitter(); - 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 { 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); } -} +} \ No newline at end of file diff --git a/frontend/src/app/shared/components/contents/content-list-field.component.ts b/frontend/src/app/shared/components/contents/content-list-field.component.ts index 3119a86f7..dc421e2c3 100644 --- a/frontend/src/app/shared/components/contents/content-list-field.component.ts +++ b/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 { diff --git a/frontend/src/app/shared/components/forms/rich-editor.component.html b/frontend/src/app/shared/components/forms/rich-editor.component.html index 2639770e3..3ec7fdbc3 100644 --- a/frontend/src/app/shared/components/forms/rich-editor.component.html +++ b/frontend/src/app/shared/components/forms/rich-editor.component.html @@ -14,4 +14,8 @@ [schemaIdentifiers]="schemaIds" *sqxModal="contentsDialog"> - + diff --git a/frontend/src/app/shared/components/search/search-form.component.ts b/frontend/src/app/shared/components/search/search-form.component.ts index e9c6c5963..f884c5938 100644 --- a/frontend/src/app/shared/components/search/search-form.component.ts +++ b/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 { diff --git a/frontend/src/app/shared/services/query.ts b/frontend/src/app/shared/services/query.ts index 3e863754f..87621680c 100644 --- a/frontend/src/app/shared/services/query.ts +++ b/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 { diff --git a/frontend/src/app/shared/services/roles.service.spec.ts b/frontend/src/app/shared/services/roles.service.spec.ts index 3d8068100..46e140fd7 100644 --- a/frontend/src/app/shared/services/roles.service.spec.ts +++ b/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(), - ] + ], }); }); diff --git a/frontend/src/app/shared/services/translations.service.ts b/frontend/src/app/shared/services/translations.service.ts index 6b2aa68cb..c23141bc4 100644 --- a/frontend/src/app/shared/services/translations.service.ts +++ b/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 { diff --git a/frontend/src/app/shell/pages/internal/chat-menu.component.ts b/frontend/src/app/shell/pages/internal/chat-menu.component.ts index 562144d84..c944b3ef9 100644 --- a/frontend/src/app/shell/pages/internal/chat-menu.component.ts +++ b/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, ) { } } diff --git a/frontend/src/app/shell/pages/internal/internal-area.component.ts b/frontend/src/app/shell/pages/internal/internal-area.component.ts index 9e803e8c8..facd55ea8 100644 --- a/frontend/src/app/shell/pages/internal/internal-area.component.ts +++ b/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,