diff --git a/package-lock.json b/package-lock.json index bb573035..969a09fe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -65,6 +65,7 @@ "@storybook/svelte": "^6.2.9", "@types/jest": "^24.9.1", "@types/koa-router": "^7.4.4", + "@types/string-similarity": "^4.0.0", "@types/universal-analytics": "^0.4.5", "@typescript-eslint/eslint-plugin": "^5.56.0", "@typescript-eslint/parser": "^5.56.0", @@ -15363,6 +15364,12 @@ "integrity": "sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw==", "dev": true }, + "node_modules/@types/string-similarity": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@types/string-similarity/-/string-similarity-4.0.0.tgz", + "integrity": "sha512-dMS4S07fbtY1AILG/RhuwmptmzK1Ql8scmAebOTJ/8iBtK/KI17NwGwKzu1uipjj8Kk+3mfPxum56kKZE93mzQ==", + "dev": true + }, "node_modules/@types/tapable": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/@types/tapable/-/tapable-1.0.5.tgz", @@ -24625,26 +24632,34 @@ }, "node_modules/fsevents/node_modules/abbrev": { "version": "1.1.1", + "dev": true, "inBundle": true, - "license": "ISC" + "license": "ISC", + "optional": true }, "node_modules/fsevents/node_modules/ansi-regex": { "version": "2.1.1", + "dev": true, "inBundle": true, "license": "MIT", + "optional": true, "engines": { "node": ">=0.10.0" } }, "node_modules/fsevents/node_modules/aproba": { "version": "1.2.0", + "dev": true, "inBundle": true, - "license": "ISC" + "license": "ISC", + "optional": true }, "node_modules/fsevents/node_modules/are-we-there-yet": { "version": "1.1.5", + "dev": true, "inBundle": true, "license": "ISC", + "optional": true, "dependencies": { "delegates": "^1.0.0", "readable-stream": "^2.0.6" @@ -24652,13 +24667,17 @@ }, "node_modules/fsevents/node_modules/balanced-match": { "version": "1.0.0", + "dev": true, "inBundle": true, - "license": "MIT" + "license": "MIT", + "optional": true }, "node_modules/fsevents/node_modules/brace-expansion": { "version": "1.1.11", + "dev": true, "inBundle": true, "license": "MIT", + "optional": true, "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -24666,57 +24685,75 @@ }, "node_modules/fsevents/node_modules/chownr": { "version": "1.1.4", + "dev": true, "inBundle": true, - "license": "ISC" + "license": "ISC", + "optional": true }, "node_modules/fsevents/node_modules/code-point-at": { "version": "1.1.0", + "dev": true, "inBundle": true, "license": "MIT", + "optional": true, "engines": { "node": ">=0.10.0" } }, "node_modules/fsevents/node_modules/concat-map": { "version": "0.0.1", + "dev": true, "inBundle": true, - "license": "MIT" + "license": "MIT", + "optional": true }, "node_modules/fsevents/node_modules/console-control-strings": { "version": "1.1.0", + "dev": true, "inBundle": true, - "license": "ISC" + "license": "ISC", + "optional": true }, "node_modules/fsevents/node_modules/core-util-is": { "version": "1.0.2", + "dev": true, "inBundle": true, - "license": "MIT" + "license": "MIT", + "optional": true }, "node_modules/fsevents/node_modules/debug": { "version": "3.2.6", + "dev": true, "inBundle": true, "license": "MIT", + "optional": true, "dependencies": { "ms": "^2.1.1" } }, "node_modules/fsevents/node_modules/deep-extend": { "version": "0.6.0", + "dev": true, "inBundle": true, "license": "MIT", + "optional": true, "engines": { "node": ">=4.0.0" } }, "node_modules/fsevents/node_modules/delegates": { "version": "1.0.0", + "dev": true, "inBundle": true, - "license": "MIT" + "license": "MIT", + "optional": true }, "node_modules/fsevents/node_modules/detect-libc": { "version": "1.0.3", + "dev": true, "inBundle": true, "license": "Apache-2.0", + "optional": true, "bin": { "detect-libc": "bin/detect-libc.js" }, @@ -24726,21 +24763,27 @@ }, "node_modules/fsevents/node_modules/fs-minipass": { "version": "1.2.7", + "dev": true, "inBundle": true, "license": "ISC", + "optional": true, "dependencies": { "minipass": "^2.6.0" } }, "node_modules/fsevents/node_modules/fs.realpath": { "version": "1.0.0", + "dev": true, "inBundle": true, - "license": "ISC" + "license": "ISC", + "optional": true }, "node_modules/fsevents/node_modules/gauge": { "version": "2.7.4", + "dev": true, "inBundle": true, "license": "ISC", + "optional": true, "dependencies": { "aproba": "^1.0.3", "console-control-strings": "^1.0.0", @@ -24754,8 +24797,10 @@ }, "node_modules/fsevents/node_modules/glob": { "version": "7.1.6", + "dev": true, "inBundle": true, "license": "ISC", + "optional": true, "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -24773,13 +24818,17 @@ }, "node_modules/fsevents/node_modules/has-unicode": { "version": "2.0.1", + "dev": true, "inBundle": true, - "license": "ISC" + "license": "ISC", + "optional": true }, "node_modules/fsevents/node_modules/iconv-lite": { "version": "0.4.24", + "dev": true, "inBundle": true, "license": "MIT", + "optional": true, "dependencies": { "safer-buffer": ">= 2.1.2 < 3" }, @@ -24789,16 +24838,20 @@ }, "node_modules/fsevents/node_modules/ignore-walk": { "version": "3.0.3", + "dev": true, "inBundle": true, "license": "ISC", + "optional": true, "dependencies": { "minimatch": "^3.0.4" } }, "node_modules/fsevents/node_modules/inflight": { "version": "1.0.6", + "dev": true, "inBundle": true, "license": "ISC", + "optional": true, "dependencies": { "once": "^1.3.0", "wrappy": "1" @@ -24806,21 +24859,27 @@ }, "node_modules/fsevents/node_modules/inherits": { "version": "2.0.4", + "dev": true, "inBundle": true, - "license": "ISC" + "license": "ISC", + "optional": true }, "node_modules/fsevents/node_modules/ini": { "version": "1.3.5", + "dev": true, "inBundle": true, "license": "ISC", + "optional": true, "engines": { "node": "*" } }, "node_modules/fsevents/node_modules/is-fullwidth-code-point": { "version": "1.0.0", + "dev": true, "inBundle": true, "license": "MIT", + "optional": true, "dependencies": { "number-is-nan": "^1.0.0" }, @@ -24830,13 +24889,17 @@ }, "node_modules/fsevents/node_modules/isarray": { "version": "1.0.0", + "dev": true, "inBundle": true, - "license": "MIT" + "license": "MIT", + "optional": true }, "node_modules/fsevents/node_modules/minimatch": { "version": "3.0.4", + "dev": true, "inBundle": true, "license": "ISC", + "optional": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -24846,13 +24909,17 @@ }, "node_modules/fsevents/node_modules/minimist": { "version": "1.2.5", + "dev": true, "inBundle": true, - "license": "MIT" + "license": "MIT", + "optional": true }, "node_modules/fsevents/node_modules/minipass": { "version": "2.9.0", + "dev": true, "inBundle": true, "license": "ISC", + "optional": true, "dependencies": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -24860,8 +24927,10 @@ }, "node_modules/fsevents/node_modules/minizlib": { "version": "1.3.3", + "dev": true, "inBundle": true, "license": "MIT", + "optional": true, "dependencies": { "minipass": "^2.9.0" } @@ -24869,8 +24938,10 @@ "node_modules/fsevents/node_modules/mkdirp": { "version": "0.5.3", "deprecated": "Legacy versions of mkdirp are no longer supported. Please update to mkdirp 1.x. (Note that the API surface has changed to use Promises in 1.x.)", + "dev": true, "inBundle": true, "license": "MIT", + "optional": true, "dependencies": { "minimist": "^1.2.5" }, @@ -24880,13 +24951,17 @@ }, "node_modules/fsevents/node_modules/ms": { "version": "2.1.2", + "dev": true, "inBundle": true, - "license": "MIT" + "license": "MIT", + "optional": true }, "node_modules/fsevents/node_modules/needle": { "version": "2.3.3", + "dev": true, "inBundle": true, "license": "MIT", + "optional": true, "dependencies": { "debug": "^3.2.6", "iconv-lite": "^0.4.4", @@ -24901,8 +24976,10 @@ }, "node_modules/fsevents/node_modules/node-pre-gyp": { "version": "0.14.0", + "dev": true, "inBundle": true, "license": "BSD-3-Clause", + "optional": true, "dependencies": { "detect-libc": "^1.0.2", "mkdirp": "^0.5.1", @@ -24921,8 +24998,10 @@ }, "node_modules/fsevents/node_modules/nopt": { "version": "4.0.3", + "dev": true, "inBundle": true, "license": "ISC", + "optional": true, "dependencies": { "abbrev": "1", "osenv": "^0.1.4" @@ -24933,21 +25012,27 @@ }, "node_modules/fsevents/node_modules/npm-bundled": { "version": "1.1.1", + "dev": true, "inBundle": true, "license": "ISC", + "optional": true, "dependencies": { "npm-normalize-package-bin": "^1.0.1" } }, "node_modules/fsevents/node_modules/npm-normalize-package-bin": { "version": "1.0.1", + "dev": true, "inBundle": true, - "license": "ISC" + "license": "ISC", + "optional": true }, "node_modules/fsevents/node_modules/npm-packlist": { "version": "1.4.8", + "dev": true, "inBundle": true, "license": "ISC", + "optional": true, "dependencies": { "ignore-walk": "^3.0.1", "npm-bundled": "^1.0.1", @@ -24956,8 +25041,10 @@ }, "node_modules/fsevents/node_modules/npmlog": { "version": "4.1.2", + "dev": true, "inBundle": true, "license": "ISC", + "optional": true, "dependencies": { "are-we-there-yet": "~1.1.2", "console-control-strings": "~1.1.0", @@ -24967,48 +25054,60 @@ }, "node_modules/fsevents/node_modules/number-is-nan": { "version": "1.0.1", + "dev": true, "inBundle": true, "license": "MIT", + "optional": true, "engines": { "node": ">=0.10.0" } }, "node_modules/fsevents/node_modules/object-assign": { "version": "4.1.1", + "dev": true, "inBundle": true, "license": "MIT", + "optional": true, "engines": { "node": ">=0.10.0" } }, "node_modules/fsevents/node_modules/once": { "version": "1.4.0", + "dev": true, "inBundle": true, "license": "ISC", + "optional": true, "dependencies": { "wrappy": "1" } }, "node_modules/fsevents/node_modules/os-homedir": { "version": "1.0.2", + "dev": true, "inBundle": true, "license": "MIT", + "optional": true, "engines": { "node": ">=0.10.0" } }, "node_modules/fsevents/node_modules/os-tmpdir": { "version": "1.0.2", + "dev": true, "inBundle": true, "license": "MIT", + "optional": true, "engines": { "node": ">=0.10.0" } }, "node_modules/fsevents/node_modules/osenv": { "version": "0.1.5", + "dev": true, "inBundle": true, "license": "ISC", + "optional": true, "dependencies": { "os-homedir": "^1.0.0", "os-tmpdir": "^1.0.0" @@ -25016,21 +25115,27 @@ }, "node_modules/fsevents/node_modules/path-is-absolute": { "version": "1.0.1", + "dev": true, "inBundle": true, "license": "MIT", + "optional": true, "engines": { "node": ">=0.10.0" } }, "node_modules/fsevents/node_modules/process-nextick-args": { "version": "2.0.1", + "dev": true, "inBundle": true, - "license": "MIT" + "license": "MIT", + "optional": true }, "node_modules/fsevents/node_modules/rc": { "version": "1.2.8", + "dev": true, "inBundle": true, "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "optional": true, "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", @@ -25043,8 +25148,10 @@ }, "node_modules/fsevents/node_modules/readable-stream": { "version": "2.3.7", + "dev": true, "inBundle": true, "license": "MIT", + "optional": true, "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -25057,8 +25164,10 @@ }, "node_modules/fsevents/node_modules/rimraf": { "version": "2.7.1", + "dev": true, "inBundle": true, "license": "ISC", + "optional": true, "dependencies": { "glob": "^7.1.3" }, @@ -25068,49 +25177,65 @@ }, "node_modules/fsevents/node_modules/safe-buffer": { "version": "5.1.2", + "dev": true, "inBundle": true, - "license": "MIT" + "license": "MIT", + "optional": true }, "node_modules/fsevents/node_modules/safer-buffer": { "version": "2.1.2", + "dev": true, "inBundle": true, - "license": "MIT" + "license": "MIT", + "optional": true }, "node_modules/fsevents/node_modules/sax": { "version": "1.2.4", + "dev": true, "inBundle": true, - "license": "ISC" + "license": "ISC", + "optional": true }, "node_modules/fsevents/node_modules/semver": { "version": "5.7.1", + "dev": true, "inBundle": true, "license": "ISC", + "optional": true, "bin": { "semver": "bin/semver" } }, "node_modules/fsevents/node_modules/set-blocking": { "version": "2.0.0", + "dev": true, "inBundle": true, - "license": "ISC" + "license": "ISC", + "optional": true }, "node_modules/fsevents/node_modules/signal-exit": { "version": "3.0.2", + "dev": true, "inBundle": true, - "license": "ISC" + "license": "ISC", + "optional": true }, "node_modules/fsevents/node_modules/string_decoder": { "version": "1.1.1", + "dev": true, "inBundle": true, "license": "MIT", + "optional": true, "dependencies": { "safe-buffer": "~5.1.0" } }, "node_modules/fsevents/node_modules/string-width": { "version": "1.0.2", + "dev": true, "inBundle": true, "license": "MIT", + "optional": true, "dependencies": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -25122,8 +25247,10 @@ }, "node_modules/fsevents/node_modules/strip-ansi": { "version": "3.0.1", + "dev": true, "inBundle": true, "license": "MIT", + "optional": true, "dependencies": { "ansi-regex": "^2.0.0" }, @@ -25133,16 +25260,20 @@ }, "node_modules/fsevents/node_modules/strip-json-comments": { "version": "2.0.1", + "dev": true, "inBundle": true, "license": "MIT", + "optional": true, "engines": { "node": ">=0.10.0" } }, "node_modules/fsevents/node_modules/tar": { "version": "4.4.13", + "dev": true, "inBundle": true, "license": "ISC", + "optional": true, "dependencies": { "chownr": "^1.1.1", "fs-minipass": "^1.2.5", @@ -25158,26 +25289,34 @@ }, "node_modules/fsevents/node_modules/util-deprecate": { "version": "1.0.2", + "dev": true, "inBundle": true, - "license": "MIT" + "license": "MIT", + "optional": true }, "node_modules/fsevents/node_modules/wide-align": { "version": "1.1.3", + "dev": true, "inBundle": true, "license": "ISC", + "optional": true, "dependencies": { "string-width": "^1.0.2 || 2" } }, "node_modules/fsevents/node_modules/wrappy": { "version": "1.0.2", + "dev": true, "inBundle": true, - "license": "ISC" + "license": "ISC", + "optional": true }, "node_modules/fsevents/node_modules/yallist": { "version": "3.1.1", + "dev": true, "inBundle": true, - "license": "ISC" + "license": "ISC", + "optional": true }, "node_modules/ftp": { "version": "0.3.10", @@ -52829,6 +52968,12 @@ "integrity": "sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw==", "dev": true }, + "@types/string-similarity": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@types/string-similarity/-/string-similarity-4.0.0.tgz", + "integrity": "sha512-dMS4S07fbtY1AILG/RhuwmptmzK1Ql8scmAebOTJ/8iBtK/KI17NwGwKzu1uipjj8Kk+3mfPxum56kKZE93mzQ==", + "dev": true + }, "@types/tapable": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/@types/tapable/-/tapable-1.0.5.tgz", @@ -60152,19 +60297,27 @@ "dependencies": { "abbrev": { "version": "1.1.1", - "bundled": true + "bundled": true, + "dev": true, + "optional": true }, "ansi-regex": { "version": "2.1.1", - "bundled": true + "bundled": true, + "dev": true, + "optional": true }, "aproba": { "version": "1.2.0", - "bundled": true + "bundled": true, + "dev": true, + "optional": true }, "are-we-there-yet": { "version": "1.1.5", "bundled": true, + "dev": true, + "optional": true, "requires": { "delegates": "^1.0.0", "readable-stream": "^2.0.6" @@ -60172,11 +60325,15 @@ }, "balanced-match": { "version": "1.0.0", - "bundled": true + "bundled": true, + "dev": true, + "optional": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, + "dev": true, + "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -60184,57 +60341,81 @@ }, "chownr": { "version": "1.1.4", - "bundled": true + "bundled": true, + "dev": true, + "optional": true }, "code-point-at": { "version": "1.1.0", - "bundled": true + "bundled": true, + "dev": true, + "optional": true }, "concat-map": { "version": "0.0.1", - "bundled": true + "bundled": true, + "dev": true, + "optional": true }, "console-control-strings": { "version": "1.1.0", - "bundled": true + "bundled": true, + "dev": true, + "optional": true }, "core-util-is": { "version": "1.0.2", - "bundled": true + "bundled": true, + "dev": true, + "optional": true }, "debug": { "version": "3.2.6", "bundled": true, + "dev": true, + "optional": true, "requires": { "ms": "^2.1.1" } }, "deep-extend": { "version": "0.6.0", - "bundled": true + "bundled": true, + "dev": true, + "optional": true }, "delegates": { "version": "1.0.0", - "bundled": true + "bundled": true, + "dev": true, + "optional": true }, "detect-libc": { "version": "1.0.3", - "bundled": true + "bundled": true, + "dev": true, + "optional": true }, "fs-minipass": { "version": "1.2.7", "bundled": true, + "dev": true, + "optional": true, "requires": { "minipass": "^2.6.0" } }, "fs.realpath": { "version": "1.0.0", - "bundled": true + "bundled": true, + "dev": true, + "optional": true }, "gauge": { "version": "2.7.4", "bundled": true, + "dev": true, + "optional": true, "requires": { "aproba": "^1.0.3", "console-control-strings": "^1.0.0", @@ -60249,6 +60430,8 @@ "glob": { "version": "7.1.6", "bundled": true, + "dev": true, + "optional": true, "requires": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -60260,11 +60443,15 @@ }, "has-unicode": { "version": "2.0.1", - "bundled": true + "bundled": true, + "dev": true, + "optional": true }, "iconv-lite": { "version": "0.4.24", "bundled": true, + "dev": true, + "optional": true, "requires": { "safer-buffer": ">= 2.1.2 < 3" } @@ -60272,6 +60459,8 @@ "ignore-walk": { "version": "3.0.3", "bundled": true, + "dev": true, + "optional": true, "requires": { "minimatch": "^3.0.4" } @@ -60279,6 +60468,8 @@ "inflight": { "version": "1.0.6", "bundled": true, + "dev": true, + "optional": true, "requires": { "once": "^1.3.0", "wrappy": "1" @@ -60286,37 +60477,51 @@ }, "inherits": { "version": "2.0.4", - "bundled": true + "bundled": true, + "dev": true, + "optional": true }, "ini": { "version": "1.3.5", - "bundled": true + "bundled": true, + "dev": true, + "optional": true }, "is-fullwidth-code-point": { "version": "1.0.0", "bundled": true, + "dev": true, + "optional": true, "requires": { "number-is-nan": "^1.0.0" } }, "isarray": { "version": "1.0.0", - "bundled": true + "bundled": true, + "dev": true, + "optional": true }, "minimatch": { "version": "3.0.4", "bundled": true, + "dev": true, + "optional": true, "requires": { "brace-expansion": "^1.1.7" } }, "minimist": { "version": "1.2.5", - "bundled": true + "bundled": true, + "dev": true, + "optional": true }, "minipass": { "version": "2.9.0", "bundled": true, + "dev": true, + "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -60325,6 +60530,8 @@ "minizlib": { "version": "1.3.3", "bundled": true, + "dev": true, + "optional": true, "requires": { "minipass": "^2.9.0" } @@ -60332,17 +60539,23 @@ "mkdirp": { "version": "0.5.3", "bundled": true, + "dev": true, + "optional": true, "requires": { "minimist": "^1.2.5" } }, "ms": { "version": "2.1.2", - "bundled": true + "bundled": true, + "dev": true, + "optional": true }, "needle": { "version": "2.3.3", "bundled": true, + "dev": true, + "optional": true, "requires": { "debug": "^3.2.6", "iconv-lite": "^0.4.4", @@ -60352,6 +60565,8 @@ "node-pre-gyp": { "version": "0.14.0", "bundled": true, + "dev": true, + "optional": true, "requires": { "detect-libc": "^1.0.2", "mkdirp": "^0.5.1", @@ -60368,6 +60583,8 @@ "nopt": { "version": "4.0.3", "bundled": true, + "dev": true, + "optional": true, "requires": { "abbrev": "1", "osenv": "^0.1.4" @@ -60376,17 +60593,23 @@ "npm-bundled": { "version": "1.1.1", "bundled": true, + "dev": true, + "optional": true, "requires": { "npm-normalize-package-bin": "^1.0.1" } }, "npm-normalize-package-bin": { "version": "1.0.1", - "bundled": true + "bundled": true, + "dev": true, + "optional": true }, "npm-packlist": { "version": "1.4.8", "bundled": true, + "dev": true, + "optional": true, "requires": { "ignore-walk": "^3.0.1", "npm-bundled": "^1.0.1", @@ -60396,6 +60619,8 @@ "npmlog": { "version": "4.1.2", "bundled": true, + "dev": true, + "optional": true, "requires": { "are-we-there-yet": "~1.1.2", "console-control-strings": "~1.1.0", @@ -60405,30 +60630,42 @@ }, "number-is-nan": { "version": "1.0.1", - "bundled": true + "bundled": true, + "dev": true, + "optional": true }, "object-assign": { "version": "4.1.1", - "bundled": true + "bundled": true, + "dev": true, + "optional": true }, "once": { "version": "1.4.0", "bundled": true, + "dev": true, + "optional": true, "requires": { "wrappy": "1" } }, "os-homedir": { "version": "1.0.2", - "bundled": true + "bundled": true, + "dev": true, + "optional": true }, "os-tmpdir": { "version": "1.0.2", - "bundled": true + "bundled": true, + "dev": true, + "optional": true }, "osenv": { "version": "0.1.5", "bundled": true, + "dev": true, + "optional": true, "requires": { "os-homedir": "^1.0.0", "os-tmpdir": "^1.0.0" @@ -60436,15 +60673,21 @@ }, "path-is-absolute": { "version": "1.0.1", - "bundled": true + "bundled": true, + "dev": true, + "optional": true }, "process-nextick-args": { "version": "2.0.1", - "bundled": true + "bundled": true, + "dev": true, + "optional": true }, "rc": { "version": "1.2.8", "bundled": true, + "dev": true, + "optional": true, "requires": { "deep-extend": "^0.6.0", "ini": "~1.3.0", @@ -60455,6 +60698,8 @@ "readable-stream": { "version": "2.3.7", "bundled": true, + "dev": true, + "optional": true, "requires": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -60468,37 +60713,53 @@ "rimraf": { "version": "2.7.1", "bundled": true, + "dev": true, + "optional": true, "requires": { "glob": "^7.1.3" } }, "safe-buffer": { "version": "5.1.2", - "bundled": true + "bundled": true, + "dev": true, + "optional": true }, "safer-buffer": { "version": "2.1.2", - "bundled": true + "bundled": true, + "dev": true, + "optional": true }, "sax": { "version": "1.2.4", - "bundled": true + "bundled": true, + "dev": true, + "optional": true }, "semver": { "version": "5.7.1", - "bundled": true + "bundled": true, + "dev": true, + "optional": true }, "set-blocking": { "version": "2.0.0", - "bundled": true + "bundled": true, + "dev": true, + "optional": true }, "signal-exit": { "version": "3.0.2", - "bundled": true + "bundled": true, + "dev": true, + "optional": true }, "string_decoder": { "version": "1.1.1", "bundled": true, + "dev": true, + "optional": true, "requires": { "safe-buffer": "~5.1.0" } @@ -60506,6 +60767,8 @@ "string-width": { "version": "1.0.2", "bundled": true, + "dev": true, + "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -60515,17 +60778,23 @@ "strip-ansi": { "version": "3.0.1", "bundled": true, + "dev": true, + "optional": true, "requires": { "ansi-regex": "^2.0.0" } }, "strip-json-comments": { "version": "2.0.1", - "bundled": true + "bundled": true, + "dev": true, + "optional": true }, "tar": { "version": "4.4.13", "bundled": true, + "dev": true, + "optional": true, "requires": { "chownr": "^1.1.1", "fs-minipass": "^1.2.5", @@ -60538,22 +60807,30 @@ }, "util-deprecate": { "version": "1.0.2", - "bundled": true + "bundled": true, + "dev": true, + "optional": true }, "wide-align": { "version": "1.1.3", "bundled": true, + "dev": true, + "optional": true, "requires": { "string-width": "^1.0.2 || 2" } }, "wrappy": { "version": "1.0.2", - "bundled": true + "bundled": true, + "dev": true, + "optional": true }, "yallist": { "version": "3.1.1", - "bundled": true + "bundled": true, + "dev": true, + "optional": true } } }, diff --git a/package.json b/package.json index b488b8b1..193c055e 100644 --- a/package.json +++ b/package.json @@ -21,8 +21,8 @@ "clean": "run-s clean:*", "clean:server": "rm -rf build", "clean:liff": "rm -rf liff", - "notify": "babel-node -r dotenv/config src/scripts/scanRepliesAndNotify.js", - "cofactsapi": "babel-node -r dotenv/config src/scripts/introspectCofactsApi.js", + "notify": "babel-node --extensions .ts,.js -r dotenv/config src/scripts/scanRepliesAndNotify.js", + "cofactsapi": "babel-node --extensions .ts,.js -r dotenv/config src/scripts/introspectCofactsApi.js", "postcofactsapi": "npm run typegen", "storybook": "start-storybook -p 6006", "build-storybook": "build-storybook", @@ -102,6 +102,7 @@ "@storybook/svelte": "^6.2.9", "@types/jest": "^24.9.1", "@types/koa-router": "^7.4.4", + "@types/string-similarity": "^4.0.0", "@types/universal-analytics": "^0.4.5", "@typescript-eslint/eslint-plugin": "^5.56.0", "@typescript-eslint/parser": "^5.56.0", diff --git a/src/graphql/cofacts-api.graphql b/src/graphql/cofacts-api.graphql index c7ea3118..8c0f56f5 100644 --- a/src/graphql/cofacts-api.graphql +++ b/src/graphql/cofacts-api.graphql @@ -136,6 +136,40 @@ type Query { """ before: String ): AnalyticsConnection! + ListAIResponses( + filter: ListAIResponsesFilter + orderBy: [ListAIResponsesOrderBy] + + """Returns only first results""" + first: Int = 10 + + """ + Specify a cursor, returns results after this cursor. cannot be used with "before". + """ + after: String + + """ + Specify a cursor, returns results before this cursor. cannot be used with "after". + """ + before: String + ): AIResponseConnection! + ListCooccurrences( + filter: ListCooccurrenceFilter + orderBy: [ListCooccurrenceOrderBy] + + """Returns only first results""" + first: Int = 10 + + """ + Specify a cursor, returns results after this cursor. cannot be used with "before". + """ + after: String + + """ + Specify a cursor, returns results before this cursor. cannot be used with "after". + """ + before: String + ): ListCooccurrenceConnection ValidateSlug(slug: String!): ValidationResult } @@ -144,6 +178,7 @@ type Article implements Node { text: String createdAt: String updatedAt: String + status: ReplyRequestStatusEnum! references: [ArticleReference] """Number of normal article replies""" @@ -167,9 +202,17 @@ type Article implements Node { """Show only articleReplies created by the specific user.""" userId: String + """Show only articleReplies created by the specified users.""" + userIds: [String!] + """Only list the articleReplies created by the currently logged in user""" selfOnly: Boolean ): [ArticleReply] + + """ + Automated reply from AI before human fact checkers compose an fact check + """ + aiReplies: [AIReply!]! articleCategories( """ Deprecated. Please use statuses instead. When specified, returns only article categories with the specified status @@ -231,6 +274,7 @@ type Article implements Node { """Attachment hash to search or identify files""" attachmentHash: String + cooccurrences: [Cooccurrence!] } """Basic entity. Modeled after Relay's GraphQL Server Specification.""" @@ -238,6 +282,13 @@ interface Node { id: ID! } +enum ReplyRequestStatusEnum { + NORMAL + + """Created by a blocked user violating terms of use.""" + BLOCKED +} + type ArticleReference { createdAt: String type: ArticleReferenceTypeEnum @@ -581,6 +632,72 @@ enum FeedbackVote { DOWNVOTE } +"""A ChatGPT reply for an article with no human fact-checks yet""" +type AIReply implements Node & AIResponse { + id: ID! + + """The id for the document that this AI response is for.""" + docId: ID + + """AI response type""" + type: AIResponseTypeEnum! + + """The user triggered this AI response""" + user: User + + """Processing status of AI""" + status: AIResponseStatusEnum! + + """AI response text. Populated after status becomes SUCCESS.""" + text: String + createdAt: String! + updatedAt: String + + """ + The usage returned from OpenAI. Populated after status becomes SUCCESS. + """ + usage: OpenAICompletionUsage +} + +"""Denotes an AI processed response and its processing status.""" +interface AIResponse implements Node { + id: ID! + + """The id for the document that this AI response is for.""" + docId: ID + + """AI response type""" + type: AIResponseTypeEnum! + + """The user triggered this AI response""" + user: User + + """Processing status of AI""" + status: AIResponseStatusEnum! + + """AI response text. Populated after status becomes SUCCESS.""" + text: String + createdAt: String! + updatedAt: String +} + +enum AIResponseTypeEnum { + """The AI Response is an automated analysis / reply of an article.""" + AI_REPLY +} + +enum AIResponseStatusEnum { + LOADING + SUCCESS + ERROR +} + +type OpenAICompletionUsage { + promptTokens: Int! + completionTokens: Int! + totalTokens: Int! +} + """The linkage between an Article and a Category""" type ArticleCategory implements Node { id: ID! @@ -710,13 +827,6 @@ type ReplyRequest implements Node { status: ReplyRequestStatusEnum! } -enum ReplyRequestStatusEnum { - NORMAL - - """Created by a blocked user violating terms of use.""" - BLOCKED -} - type ArticleConnection implements Connection { """ The total count of the entire collection, regardless of "before", "after". @@ -823,6 +933,16 @@ enum AttachmentVariantEnum { THUMBNAIL } +type Cooccurrence implements Node { + id: ID! + userId: String! + appId: String! + articles: [Article!]! + articleIds: [String!]! + createdAt: String! + updatedAt: String! +} + input ListArticleFilter { """Show only articles created by a specific app.""" appId: String @@ -830,6 +950,9 @@ input ListArticleFilter { """Show only articles created by the specific user.""" userId: String + """Show only articles created by the specified users.""" + userIds: [String!] + """ List only the articles that were created between the specific time range. """ @@ -891,8 +1014,16 @@ input ListArticleFilter { """Show the media article similar to the input url""" mediaUrl: String + """ + Specifies how the transcript of `mediaUrl` can be used to search. Can only specify `transcript` when `mediaUrl` is specified. + """ + transcript: TranscriptFilter + """Show articles with article replies matching this criteria""" articleReply: ArticleReplyFilterInput + + """Returns only articles with the specified statuses""" + statuses: [ArticleStatusEnum!] = [NORMAL] } """ @@ -922,6 +1053,19 @@ input UserAndExistInput { exists: Boolean = true } +input TranscriptFilter { + """ + more_like_this query's "minimum_should_match" query param for the transcript of `mediaUrl` + See https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-minimum-should-match.html for possible values. + """ + minimumShouldMatch: String + + """ + Only used when `filter.mediaUrl` is provided. Generates transcript if provided `filter.mediaUrl` is not transcribed previously. + """ + shouldCreate: Boolean = false +} + input ArticleReplyFilterInput { """Show only articleReplies created by a specific app.""" appId: String @@ -929,6 +1073,9 @@ input ArticleReplyFilterInput { """Show only articleReplies created by the specific user.""" userId: String + """Show only articleReplies created by the specified users.""" + userIds: [String!] + """ List only the articleReplies that were created between the specific time range. """ @@ -940,6 +1087,13 @@ input ArticleReplyFilterInput { replyTypes: [ReplyTypeEnum] } +enum ArticleStatusEnum { + NORMAL + + """Created by a blocked user violating terms of use.""" + BLOCKED +} + """ An entry of orderBy argument. Specifies field name and the sort order. Only one field name is allowd per entry. """ @@ -961,6 +1115,9 @@ input ListReplyFilter { """Show only replies created by the specific user.""" userId: String + """Show only replies created by the specified users.""" + userIds: [String!] + """ List only the replies that were created between the specific time range. """ @@ -1044,6 +1201,9 @@ input ListArticleReplyFeedbackFilter { """Show only article reply feedbacks created by the specific user.""" userId: String + """Show only article reply feedbacks created by the specified users.""" + userIds: [String!] + """ List only the article reply feedbacks that were created between the specific time range. """ @@ -1125,6 +1285,9 @@ input ListReplyRequestFilter { """Show only reply requests created by the specific user.""" userId: String + """Show only reply requests created by the specified users.""" + userIds: [String!] + """ List only the reply requests that were created between the specific time range. """ @@ -1221,6 +1384,105 @@ input ListAnalyticsOrderBy { date: SortOrderEnum } +type AIResponseConnection implements Connection { + """ + The total count of the entire collection, regardless of "before", "after". + """ + totalCount: Int! + edges: [AIResponseConnectionEdge!]! + pageInfo: AIResponseConnectionPageInfo! +} + +type AIResponseConnectionEdge implements Edge { + node: AIResponse! + cursor: String! + score: Float + highlight: Highlights +} + +type AIResponseConnectionPageInfo implements PageInfo { + lastCursor: String + firstCursor: String +} + +input ListAIResponsesFilter { + """Show only AI responses created by a specific app.""" + appId: String + + """Show only AI responses created by the specific user.""" + userId: String + + """Show only AI responses created by the specified users.""" + userIds: [String!] + + """ + List only the AI responses that were created between the specific time range. + """ + createdAt: TimeRangeInput + + """If given, only list out AI responses with specific IDs""" + ids: [ID!] + + """Only list the AI responses created by the currently logged in user""" + selfOnly: Boolean + + """If specified, only return AI repsonses with the specified types.""" + types: [AIResponseTypeEnum!] + + """If specified, only return AI repsonses under the specified doc IDs.""" + docIds: [ID!] + + """If specified, only return AI repsonses under the specified statuses.""" + statuses: [AIResponseStatusEnum!] + + """List only the AI responses updated within the specific time range.""" + updatedAt: TimeRangeInput +} + +""" +An entry of orderBy argument. Specifies field name and the sort order. Only one field name is allowd per entry. +""" +input ListAIResponsesOrderBy { + createdAt: SortOrderEnum + updatedAt: SortOrderEnum +} + +type ListCooccurrenceConnection implements Connection { + """ + The total count of the entire collection, regardless of "before", "after". + """ + totalCount: Int! + edges: [ListCooccurrenceConnectionEdge!]! + pageInfo: ListCooccurrenceConnectionPageInfo! +} + +type ListCooccurrenceConnectionEdge implements Edge { + node: Cooccurrence! + cursor: String! + score: Float + highlight: Highlights +} + +type ListCooccurrenceConnectionPageInfo implements PageInfo { + lastCursor: String + firstCursor: String +} + +input ListCooccurrenceFilter { + """ + List only the cooccurrence that were last updated within the specific time range. + """ + updatedAt: TimeRangeInput +} + +""" +An entry of orderBy argument. Specifies field name and the sort order. Only one field name is allowd per entry. +""" +input ListCooccurrenceOrderBy { + createdAt: SortOrderEnum + updatedAt: SortOrderEnum +} + type ValidationResult { success: Boolean! error: SlugErrorEnum @@ -1276,6 +1538,11 @@ type Mutation { waitForHyperlinks: Boolean = false ): MutationResult + """ + Create an AI reply for a specific article. If existed, returns an existing one. If information in the article is not sufficient for AI, return null. + """ + CreateAIReply(articleId: String!): AIReply + """Connects specified reply and specified article.""" CreateArticleReply(articleId: String!, replyId: String!): [ArticleReply] @@ -1310,6 +1577,9 @@ type Mutation { """Create or update a feedback on a reply request reason""" CreateOrUpdateReplyRequestFeedback(replyRequestId: String!, vote: FeedbackVote!): ReplyRequest + """Create or update a cooccurrence for the given articles""" + CreateOrUpdateCooccurrence(articleIds: [String!]): Cooccurrence + """Change status of specified articleReplies""" UpdateArticleReplyStatus(articleId: String!, replyId: String!, status: ArticleReplyStatusEnum!): [ArticleReply] diff --git a/src/lib/__mocks__/gql.js b/src/lib/__mocks__/gql.ts similarity index 90% rename from src/lib/__mocks__/gql.js rename to src/lib/__mocks__/gql.ts index a14c705b..fabedc39 100644 --- a/src/lib/__mocks__/gql.js +++ b/src/lib/__mocks__/gql.ts @@ -1,4 +1,4 @@ -const mockResultQueue = []; +const mockResultQueue: object[] = []; function gqlMock() { return () => { @@ -15,7 +15,7 @@ function gqlMock() { * @param {*} returnValue the mock value that gql()()'s returned promise will resolve to. * @returns nothing */ -gqlMock.__push = function (returnValue) { +gqlMock.__push = function (returnValue: object) { mockResultQueue.push(returnValue); }; diff --git a/src/lib/__tests__/gql.js b/src/lib/__tests__/gql.js index 9d7e5864..0bc769d8 100644 --- a/src/lib/__tests__/gql.js +++ b/src/lib/__tests__/gql.js @@ -22,7 +22,6 @@ it('invokes fetch and returns result', async () => { "https://dev-api.cofacts.tw/graphql", Object { "body": "[{\\"query\\":\\"(bar: String){foo}\\",\\"variables\\":{\\"bar\\":\\"bar\\"}}]", - "credentials": "include", "headers": Object { "Content-Type": "application/json", "x-app-secret": "CHANGE_ME", @@ -151,7 +150,6 @@ it('batches consecutive requests by URL', async () => { "https://dev-api.cofacts.tw/graphql", Object { "body": "[{\\"query\\":\\"\\\\n {\\\\n foo\\\\n }\\\\n \\"},{\\"query\\":\\"\\\\n {\\\\n bar\\\\n }\\\\n \\"}]", - "credentials": "include", "headers": Object { "Content-Type": "application/json", "x-app-secret": "CHANGE_ME", @@ -163,7 +161,6 @@ it('batches consecutive requests by URL', async () => { "https://dev-api.cofacts.tw/graphql?userId=another-user", Object { "body": "[{\\"query\\":\\"\\\\n {\\\\n foobar\\\\n }\\\\n \\",\\"variables\\":{}}]", - "credentials": "include", "headers": Object { "Content-Type": "application/json", "x-app-secret": "CHANGE_ME", diff --git a/src/lib/gql.js b/src/lib/gql.ts similarity index 50% rename from src/lib/gql.js rename to src/lib/gql.ts index 62486e89..28d8b0c1 100644 --- a/src/lib/gql.js +++ b/src/lib/gql.ts @@ -3,12 +3,16 @@ import rollbar from './rollbar'; import { format } from 'url'; import Dataloader from 'dataloader'; -const API_URL = - process.env.API_URL || 'https://cofacts-api.hacktabl.org/graphql'; +const API_URL = process.env.API_URL || 'https://dev-api.cofacts.tw/graphql'; + +type QV = { + query: string; + variables?: object; +}; // Maps URL to dataloader. Cleared after batched request is fired. // Exported just for unit test. -export const loaders = {}; +export const loaders: Record> = {}; /** * Returns a dataloader instance that can send query & variable to the GraphQL endpoint specified by `url`. @@ -16,31 +20,32 @@ export const loaders = {}; * The dataloader instance is automatically created when not exist for the specified `url`, and is * cleared automatically when the batch request fires. * - * @param {string} url - GraphQL endpoint URL - * @returns {Dataloader} A dataloader instance that loads response of the given {query, variable} + * @param url - GraphQL endpoint URL + * @returns A dataloader instance that loads response of the given {query, variable} */ -function getGraphQLRespLoader(url) { +function getGraphQLRespLoader(url: string) { if (loaders[url]) return loaders[url]; - return (loaders[url] = new Dataloader(async (queryAndVariables) => { - // Clear dataloader so that next batch will get a fresh dataloader - delete loaders[url]; + return (loaders[url] = new Dataloader( + async (queryAndVariables) => { + // Clear dataloader so that next batch will get a fresh dataloader + delete loaders[url]; - // Implements Apollo's transport layer batching - // https://www.apollographql.com/blog/apollo-client/performance/query-batching/#1bce - // - return ( - await fetch(url, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'x-app-secret': process.env.APP_SECRET, - }, - credentials: 'include', - body: JSON.stringify(queryAndVariables), - }) - ).json(); - })); + // Implements Apollo's transport layer batching + // https://www.apollographql.com/blog/apollo-client/performance/query-batching/#1bce + // + return ( + await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'x-app-secret': process.env.APP_SECRET ?? '', + }, + body: JSON.stringify(queryAndVariables), + }) + ).json(); + } + )); } // Usage: @@ -53,9 +58,12 @@ function getGraphQLRespLoader(url) { // We use template string here so that Atom's language-babel does syntax highlight // for us. // -export default (query, ...substitutions) => - (variables, search) => { - const queryAndVariable = { +export default (query: TemplateStringsArray, ...substitutions: string[]) => + ( + variables: Variable, + search?: Record + ) => { + const queryAndVariable: QV = { query: String.raw(query, ...substitutions), }; @@ -70,15 +78,16 @@ export default (query, ...substitutions) => // but we can guess that it's not 2xx if `data` is null or does not exist. // Ref: https://github.com/graphql/graphql-over-http/blob/main/spec/GraphQLOverHTTP.md#status-codes // - if (!resp.data) { - throw new Error( - `GraphQL Error: ${resp.errors - .map(({ message }) => message) - .join('\n')}` - ); + if (!('data' in resp) || !resp.data || typeof resp.data !== 'object') { + const errorStr = + 'errors' in resp && Array.isArray(resp.errors) + ? resp.errors.map(({ message }) => message).join('\n') + : 'Unknown error'; + + throw new Error(`GraphQL Error: ${errorStr}`); } - if (resp.errors) { + if ('errors' in resp && resp.errors) { console.error('GraphQL operation contains error:', resp.errors); rollbar.error( 'GraphQL error', @@ -89,6 +98,7 @@ export default (query, ...substitutions) => { resp } ); } - return resp; + + return resp as { data: QueryResp; errors?: object[] }; }); }; diff --git a/src/types/chatbotState.ts b/src/types/chatbotState.ts new file mode 100644 index 00000000..fe4854b3 --- /dev/null +++ b/src/types/chatbotState.ts @@ -0,0 +1,62 @@ +import type { Message, MessageEvent, WebhookEvent } from '@line/bot-sdk'; + +export type ChatbotState = + | '__INIT__' + | 'TUTORIAL' + | 'CHOOSING_ARTICLE' + | 'CHOOSING_REPLY' + | 'ASKING_ARTICLE_SOURCE' + | 'ASKING_ARTICLE_SUBMISSION_CONSENT'; + +/** + * Dummy event, used exclusively when calling handler from another handler + */ +type ServerChooseEvent = { + type: 'server_choose'; +}; + +/** + * Parameters that are added by handleInput. + * + * @todo: We should consider using value from authentic event instead of manually adding fields. + */ +type ArgumentedEventParams = { + /** + * The text in text message, or value from payload in actions. + */ + input: string; +}; + +export type ChatbotStateHandlerParams = { + data: { + /** Used to differientiate different search sessions (searched text or media) */ + sessionId: number; + + /** Searched text that started this search session */ + searchedText?: string; + + /** Searched multi-media message that started this search session */ + messageId: MessageEvent['message']['id']; + messageType: MessageEvent['message']['type']; + + /** User selected article in DB */ + selectedArticleId?: string; + selectedArticleText?: string; + }; + state: ChatbotState; + event: (WebhookEvent | ServerChooseEvent) & ArgumentedEventParams; + userId: string; + replies: Message[]; +}; + +type ChatbotStateHandlerReturnType = Omit< + ChatbotStateHandlerParams, + /** The state is determined by payloads in actions. No need to return state. */ 'state' +>; + +/** + * Generic handler type for function under src/webhook/handlers + */ +export type ChatbotStateHandler = ( + params: ChatbotStateHandlerParams +) => Promise; diff --git a/src/webhook/handlers/initState.js b/src/webhook/handlers/initState.ts similarity index 83% rename from src/webhook/handlers/initState.js rename to src/webhook/handlers/initState.ts index 9de9f5de..a8205ca5 100644 --- a/src/webhook/handlers/initState.js +++ b/src/webhook/handlers/initState.ts @@ -1,5 +1,12 @@ import stringSimilarity from 'string-similarity'; import { t } from 'ttag'; +import { + FlexBubble, + FlexComponent, + FlexMessage, + TextMessage, +} from '@line/bot-sdk'; +import type { ChatbotStateHandler } from 'src/types/chatbotState'; import gql from 'src/lib/gql'; import { createPostbackAction, @@ -11,17 +18,26 @@ import { } from './utils'; import ga from 'src/lib/ga'; import detectDialogflowIntent from 'src/lib/detectDialogflowIntent'; -import choosingArticle from '../handlers/choosingArticle'; +import choosingArticle from './choosingArticle'; +import { + ListArticlesInInitStateQuery, + ListArticlesInInitStateQueryVariables, +} from 'typegen/graphql'; const SIMILARITY_THRESHOLD = 0.95; -export default async function initState(params) { - let { data, event, userId, replies } = params; +const initState: ChatbotStateHandler = async (params) => { + const { data, userId } = params; + let { event, replies } = params; const state = '__INIT__'; // Track text message type send by user const visitor = ga(userId, state, event.input); - visitor.event({ ec: 'UserInput', ea: 'MessageType', el: event.message.type }); + visitor.event({ + ec: 'UserInput', + ea: 'MessageType', + el: 'message' in event ? event.message.type : '', + }); // Store user input into context data.searchedText = event.input; @@ -40,13 +56,13 @@ export default async function initState(params) { replies = [ { type: 'text', - text: dialogflowResponse.queryResult.fulfillmentText, + text: dialogflowResponse.queryResult.fulfillmentText ?? '', }, ]; visitor.event({ ec: 'UserInput', ea: 'ChatWithBot', - el: dialogflowResponse.queryResult.intent.displayName, + el: dialogflowResponse.queryResult.intent.displayName ?? undefined, }); visitor.send(); return { data, event, userId, replies }; @@ -56,7 +72,7 @@ export default async function initState(params) { const { data: { ListArticles }, } = await gql` - query ($text: String!) { + query ListArticlesInInitState($text: String!) { ListArticles( filter: { moreLikeThis: { like: $text } } orderBy: [{ _score: DESC }] @@ -77,13 +93,13 @@ export default async function initState(params) { } } } - `({ + `({ text: event.input, }); const inputSummary = ellipsis(event.input, 12); - if (ListArticles.edges.length) { + if (ListArticles?.edges.length) { // Track if find similar Articles in DB. visitor.event({ ec: 'UserInput', ea: 'ArticleSearch', el: 'ArticleFound' }); @@ -98,15 +114,15 @@ export default async function initState(params) { }); const edgesSortedWithSimilarity = ListArticles.edges - .map((edge) => { - edge.similarity = stringSimilarity.compareTwoStrings( + .map((edge) => ({ + ...edge, + similarity: stringSimilarity.compareTwoStrings( // Remove spaces so that we count word's similarities only // - edge.node.text.replace(/\s/g, ''), + (edge.node.text ?? '').replace(/\s/g, ''), event.input.replace(/\s/g, '') - ); - return edge; - }) + ), + })) .sort((edge1, edge2) => edge2.similarity - edge1.similarity) .slice(0, 9); /* flex carousel has at most 10 bubbles */ @@ -131,15 +147,15 @@ export default async function initState(params) { }); } - const articleOptions = edgesSortedWithSimilarity.map( + const articleOptions: FlexBubble[] = edgesSortedWithSimilarity.map( ({ node: { text, id }, highlight, similarity }) => { const similarityPercentage = Math.round(similarity * 100); const similarityEmoji = ['😐', '🙂', '😀', '😃', '😄'][ Math.floor(similarity * 4.999) ]; - const displayTextWhenChosen = ellipsis(text, 25, '...'); + const displayTextWhenChosen = ellipsis(text ?? '', 25, '...'); - const bodyContents = []; + const bodyContents: FlexComponent[] = []; if (highlight && !highlight.text) { bodyContents.push({ type: 'text', @@ -151,7 +167,7 @@ export default async function initState(params) { } bodyContents.push({ type: 'text', - contents: createHighlightContents(highlight, text), // 50KB for entire Flex carousel + contents: createHighlightContents(highlight, text ?? undefined), // 50KB for entire Flex carousel maxLines: 6, flex: 0, gravity: 'top', @@ -271,7 +287,7 @@ export default async function initState(params) { }); } - const templateMessage = { + const templateMessage: FlexMessage = { type: 'flex', altText: t`Please choose the most similar message from the list.`, contents: { @@ -284,7 +300,7 @@ export default async function initState(params) { { type: 'text', text: `🔍 ${t`There are some messages that looks similar to "${inputSummary}" you have sent to me.`}`, - }, + } satisfies TextMessage, ]; const textArticleFound = [ { @@ -292,11 +308,11 @@ export default async function initState(params) { text: t`Internet rumors are often mutated and shared. Please choose the version that looks the most similar` + '👇', - }, + } satisfies TextMessage, templateMessage, ]; - replies = prefixTextArticleFound.concat(textArticleFound); + replies = [...prefixTextArticleFound, ...textArticleFound]; } else { // Track if find similar Articles in DB. visitor.event({ @@ -317,4 +333,6 @@ export default async function initState(params) { } visitor.send(); return { data, event, userId, replies }; -} +}; + +export default initState; diff --git a/src/webhook/handlers/utils.ts b/src/webhook/handlers/utils.ts index 7377d2ad..b709cda0 100644 --- a/src/webhook/handlers/utils.ts +++ b/src/webhook/handlers/utils.ts @@ -16,6 +16,8 @@ import type { CreateReplyMessagesReplyFragment, CreateReplyMessagesArticleFragment, CreateReferenceWordsReplyFragment, + CreateAiReplyMutation, + CreateAiReplyMutationVariables, } from 'typegen/graphql'; import { getArticleURL, createTypeWords } from 'src/lib/sharedUtils'; import { sign } from 'src/lib/jwt'; @@ -33,7 +35,7 @@ export function createPostbackAction( label: string, input: string, displayText: string, - sessionId: string, + sessionId: number, state: string ): Action { return { @@ -105,7 +107,7 @@ export function createReferenceWords({ * @returns reply message object */ export function createAskArticleSubmissionConsentReply( - sessionId: string + sessionId: number ): Message { const btnText = `🆕 ${t`Report to database`}`; const spans: FlexSpan[] = [ @@ -387,11 +389,11 @@ export function createSuggestOtherFactCheckerReply(): Message { * @returns Flex text contents */ export function createHighlightContents( - highlight: CreateHighlightContentsHighlightFragment, + highlight: CreateHighlightContentsHighlightFragment | null | undefined, oriText = '', lettersLimit = 200, contentsLimit = 4000 -) { +): FlexSpan[] { const result: FlexSpan[] = []; let totalLength = 4; // 4 comes from JSON.stringify([]).length; let totalLetters = 0; @@ -524,12 +526,15 @@ const AI_REPLY_IMAGE_VERSION = '20230405'; export async function createAIReply(articleId: string, userId: string) { const text = ( await gql` - mutation ($articleId: String!) { + mutation CreateAIReply($articleId: String!) { CreateAIReply(articleId: $articleId) { text } } - `({ articleId }, { userId }) + `( + { articleId }, + { userId } + ) ).data.CreateAIReply?.text; return !text @@ -703,7 +708,7 @@ export const POSTBACK_NO = '__POSTBACK_NO__'; * @param sessionId - Chatbot session ID * @returns {object} Messaging API message object */ -export function createArticleSourceReply(sessionId: string) { +export function createArticleSourceReply(sessionId: number): FlexMessage { const question = t`Did you forward this message as a whole to me from the LINE app?`; return { diff --git a/typegen/gql.ts b/typegen/gql.ts index 6b912a4d..d53bfce0 100644 --- a/typegen/gql.ts +++ b/typegen/gql.ts @@ -13,7 +13,9 @@ import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/ * Therefore it is highly recommended to use the babel or swc plugin for production. */ const documents = { + "\n query ListArticlesInInitState($text: String!) {\n ListArticles(\n filter: { moreLikeThis: { like: $text } }\n orderBy: [{ _score: DESC }]\n first: 4\n ) {\n edges {\n node {\n text\n id\n }\n highlight {\n text\n hyperlinks {\n title\n summary\n }\n }\n }\n }\n }\n ": types.ListArticlesInInitStateDocument, "fragment CreateReferenceWordsReply on Reply {\n reference\n type\n}\n\nfragment CreateReplyMessagesReply on Reply {\n text\n ...CreateReferenceWordsReply\n}\n\nfragment CreateReplyMessagesArticle on Article {\n replyCount\n}\n\nfragment CreateHighlightContentsHighlight on Highlights {\n text\n hyperlinks {\n title\n summary\n }\n}": types.CreateReferenceWordsReplyFragmentDoc, + "\n mutation CreateAIReply($articleId: String!) {\n CreateAIReply(articleId: $articleId) {\n text\n }\n }\n ": types.CreateAiReplyDocument, }; /** @@ -30,10 +32,18 @@ const documents = { */ export function graphql(source: string): unknown; +/** + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function graphql(source: "\n query ListArticlesInInitState($text: String!) {\n ListArticles(\n filter: { moreLikeThis: { like: $text } }\n orderBy: [{ _score: DESC }]\n first: 4\n ) {\n edges {\n node {\n text\n id\n }\n highlight {\n text\n hyperlinks {\n title\n summary\n }\n }\n }\n }\n }\n "): (typeof documents)["\n query ListArticlesInInitState($text: String!) {\n ListArticles(\n filter: { moreLikeThis: { like: $text } }\n orderBy: [{ _score: DESC }]\n first: 4\n ) {\n edges {\n node {\n text\n id\n }\n highlight {\n text\n hyperlinks {\n title\n summary\n }\n }\n }\n }\n }\n "]; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ export function graphql(source: "fragment CreateReferenceWordsReply on Reply {\n reference\n type\n}\n\nfragment CreateReplyMessagesReply on Reply {\n text\n ...CreateReferenceWordsReply\n}\n\nfragment CreateReplyMessagesArticle on Article {\n replyCount\n}\n\nfragment CreateHighlightContentsHighlight on Highlights {\n text\n hyperlinks {\n title\n summary\n }\n}"): (typeof documents)["fragment CreateReferenceWordsReply on Reply {\n reference\n type\n}\n\nfragment CreateReplyMessagesReply on Reply {\n text\n ...CreateReferenceWordsReply\n}\n\nfragment CreateReplyMessagesArticle on Article {\n replyCount\n}\n\nfragment CreateHighlightContentsHighlight on Highlights {\n text\n hyperlinks {\n title\n summary\n }\n}"]; +/** + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function graphql(source: "\n mutation CreateAIReply($articleId: String!) {\n CreateAIReply(articleId: $articleId) {\n text\n }\n }\n "): (typeof documents)["\n mutation CreateAIReply($articleId: String!) {\n CreateAIReply(articleId: $articleId) {\n text\n }\n }\n "]; export function graphql(source: string) { return (documents as any)[source] ?? {}; diff --git a/typegen/graphql.ts b/typegen/graphql.ts index 028056f5..4cde920d 100644 --- a/typegen/graphql.ts +++ b/typegen/graphql.ts @@ -14,6 +14,70 @@ export type Scalars = { Float: number; }; +/** A ChatGPT reply for an article with no human fact-checks yet */ +export type AiReply = AiResponse & Node & { + createdAt: Scalars['String']; + /** The id for the document that this AI response is for. */ + docId: Maybe; + id: Scalars['ID']; + /** Processing status of AI */ + status: AiResponseStatusEnum; + /** AI response text. Populated after status becomes SUCCESS. */ + text: Maybe; + /** AI response type */ + type: AiResponseTypeEnum; + updatedAt: Maybe; + /** The usage returned from OpenAI. Populated after status becomes SUCCESS. */ + usage: Maybe; + /** The user triggered this AI response */ + user: Maybe; +}; + +/** Denotes an AI processed response and its processing status. */ +export type AiResponse = { + createdAt: Scalars['String']; + /** The id for the document that this AI response is for. */ + docId: Maybe; + id: Scalars['ID']; + /** Processing status of AI */ + status: AiResponseStatusEnum; + /** AI response text. Populated after status becomes SUCCESS. */ + text: Maybe; + /** AI response type */ + type: AiResponseTypeEnum; + updatedAt: Maybe; + /** The user triggered this AI response */ + user: Maybe; +}; + +export type AiResponseConnection = Connection & { + edges: Array; + pageInfo: AiResponseConnectionPageInfo; + /** The total count of the entire collection, regardless of "before", "after". */ + totalCount: Scalars['Int']; +}; + +export type AiResponseConnectionEdge = Edge & { + cursor: Scalars['String']; + highlight: Maybe; + node: AiResponse; + score: Maybe; +}; + +export type AiResponseConnectionPageInfo = PageInfo & { + firstCursor: Maybe; + lastCursor: Maybe; +}; + +export type AiResponseStatusEnum = + | 'ERROR' + | 'LOADING' + | 'SUCCESS'; + +export type AiResponseTypeEnum = + /** The AI Response is an automated analysis / reply of an article. */ + | 'AI_REPLY'; + export type Analytics = Node & { /** The day this analytic datapoint is represented, in YYYY-MM-DD format */ date: Scalars['String']; @@ -68,6 +132,8 @@ export type AnalyticsLiffEntry = { }; export type Article = Node & { + /** Automated reply from AI before human fact checkers compose an fact check */ + aiReplies: Array; articleCategories: Maybe>>; /** Connections between this article and replies. Sorted by the logic described in https://github.com/cofacts/rumors-line-bot/issues/78. */ articleReplies: Maybe>>; @@ -79,6 +145,7 @@ export type Article = Node & { attachmentUrl: Maybe; /** Number of normal article categories */ categoryCount: Maybe; + cooccurrences: Maybe>; createdAt: Maybe; /** Hyperlinks in article text */ hyperlinks: Maybe>>; @@ -94,6 +161,7 @@ export type Article = Node & { requestedForReply: Maybe; /** Activities analytics for the given article */ stats: Maybe>>; + status: ReplyRequestStatusEnum; text: Maybe; updatedAt: Maybe; /** The user submitted this article */ @@ -113,6 +181,7 @@ export type ArticleArticleRepliesArgs = { status: InputMaybe; statuses?: InputMaybe>; userId: InputMaybe; + userIds: InputMaybe>; }; @@ -312,6 +381,8 @@ export type ArticleReplyFilterInput = { statuses: InputMaybe>; /** Show only articleReplies created by the specific user. */ userId: InputMaybe; + /** Show only articleReplies created by the specified users. */ + userIds: InputMaybe>; }; export type ArticleReplyStatusEnum = @@ -320,6 +391,11 @@ export type ArticleReplyStatusEnum = | 'DELETED' | 'NORMAL'; +export type ArticleStatusEnum = + /** Created by a blocked user violating terms of use. */ + | 'BLOCKED' + | 'NORMAL'; + export type ArticleTypeEnum = | 'AUDIO' | 'IMAGE' @@ -377,6 +453,16 @@ export type Contribution = { date: Maybe; }; +export type Cooccurrence = Node & { + appId: Scalars['String']; + articleIds: Array; + articles: Array
; + createdAt: Scalars['String']; + id: Scalars['ID']; + updatedAt: Scalars['String']; + userId: Scalars['String']; +}; + /** Edge in Connection. Modeled after GraphQL connection model. */ export type Edge = { cursor: Scalars['String']; @@ -411,6 +497,35 @@ export type Hyperlink = { url: Maybe; }; +export type ListAiResponsesFilter = { + /** Show only AI responses created by a specific app. */ + appId: InputMaybe; + /** List only the AI responses that were created between the specific time range. */ + createdAt: InputMaybe; + /** If specified, only return AI repsonses under the specified doc IDs. */ + docIds: InputMaybe>; + /** If given, only list out AI responses with specific IDs */ + ids: InputMaybe>; + /** Only list the AI responses created by the currently logged in user */ + selfOnly: InputMaybe; + /** If specified, only return AI repsonses under the specified statuses. */ + statuses: InputMaybe>; + /** If specified, only return AI repsonses with the specified types. */ + types: InputMaybe>; + /** List only the AI responses updated within the specific time range. */ + updatedAt: InputMaybe; + /** Show only AI responses created by the specific user. */ + userId: InputMaybe; + /** Show only AI responses created by the specified users. */ + userIds: InputMaybe>; +}; + +/** An entry of orderBy argument. Specifies field name and the sort order. Only one field name is allowd per entry. */ +export type ListAiResponsesOrderBy = { + createdAt: InputMaybe; + updatedAt: InputMaybe; +}; + export type ListAnalyticsFilter = { /** List only the activities between the specific time range. */ date: InputMaybe; @@ -464,8 +579,14 @@ export type ListArticleFilter = { replyTypes: InputMaybe>>; /** Only list the articles created by the currently logged in user */ selfOnly: InputMaybe; + /** Returns only articles with the specified statuses */ + statuses: InputMaybe>; + /** Specifies how the transcript of `mediaUrl` can be used to search. Can only specify `transcript` when `mediaUrl` is specified. */ + transcript: InputMaybe; /** Show only articles created by the specific user. */ userId: InputMaybe; + /** Show only articles created by the specified users. */ + userIds: InputMaybe>; }; /** An entry of orderBy argument. Specifies field name and the sort order. Only one field name is allowd per entry. */ @@ -524,6 +645,8 @@ export type ListArticleReplyFeedbackFilter = { updatedAt: InputMaybe; /** Show only article reply feedbacks created by the specific user. */ userId: InputMaybe; + /** Show only article reply feedbacks created by the specified users. */ + userIds: InputMaybe>; /** When specified, list only article reply feedbacks with specified vote */ vote: InputMaybe>>; }; @@ -571,6 +694,36 @@ export type ListCategoryOrderBy = { createdAt: InputMaybe; }; +export type ListCooccurrenceConnection = Connection & { + edges: Array; + pageInfo: ListCooccurrenceConnectionPageInfo; + /** The total count of the entire collection, regardless of "before", "after". */ + totalCount: Scalars['Int']; +}; + +export type ListCooccurrenceConnectionEdge = Edge & { + cursor: Scalars['String']; + highlight: Maybe; + node: Cooccurrence; + score: Maybe; +}; + +export type ListCooccurrenceConnectionPageInfo = PageInfo & { + firstCursor: Maybe; + lastCursor: Maybe; +}; + +export type ListCooccurrenceFilter = { + /** List only the cooccurrence that were last updated within the specific time range. */ + updatedAt: InputMaybe; +}; + +/** An entry of orderBy argument. Specifies field name and the sort order. Only one field name is allowd per entry. */ +export type ListCooccurrenceOrderBy = { + createdAt: InputMaybe; + updatedAt: InputMaybe; +}; + export type ListReplyFilter = { /** Show only replies created by a specific app. */ appId: InputMaybe; @@ -587,6 +740,8 @@ export type ListReplyFilter = { types: InputMaybe>>; /** Show only replies created by the specific user. */ userId: InputMaybe; + /** Show only replies created by the specified users. */ + userIds: InputMaybe>; }; /** An entry of orderBy argument. Specifies field name and the sort order. Only one field name is allowd per entry. */ @@ -628,6 +783,8 @@ export type ListReplyRequestFilter = { statuses: InputMaybe>; /** Show only reply requests created by the specific user. */ userId: InputMaybe; + /** Show only reply requests created by the specified users. */ + userIds: InputMaybe>; }; /** An entry of orderBy argument. Specifies field name and the sort order. Only one field name is allowd per entry. */ @@ -651,6 +808,8 @@ export type MoreLikeThisInput = { }; export type Mutation = { + /** Create an AI reply for a specific article. If existed, returns an existing one. If information in the article is not sufficient for AI, return null. */ + CreateAIReply: Maybe; /** Create an article and/or a replyRequest */ CreateArticle: Maybe; /** Adds specified category to specified article. */ @@ -665,6 +824,8 @@ export type Mutation = { CreateOrUpdateArticleCategoryFeedback: Maybe; /** Create or update a feedback on an article-reply connection */ CreateOrUpdateArticleReplyFeedback: Maybe; + /** Create or update a cooccurrence for the given articles */ + CreateOrUpdateCooccurrence: Maybe; /** Create or update a reply request for the given article */ CreateOrUpdateReplyRequest: Maybe
; /** Create or update a feedback on a reply request reason */ @@ -685,6 +846,11 @@ export type Mutation = { }; +export type MutationCreateAiReplyArgs = { + articleId: Scalars['String']; +}; + + export type MutationCreateArticleArgs = { reason: InputMaybe; reference: ArticleReferenceInput; @@ -736,6 +902,11 @@ export type MutationCreateOrUpdateArticleReplyFeedbackArgs = { }; +export type MutationCreateOrUpdateCooccurrenceArgs = { + articleIds: InputMaybe>; +}; + + export type MutationCreateOrUpdateReplyRequestArgs = { articleId: Scalars['String']; reason: InputMaybe; @@ -794,6 +965,12 @@ export type Node = { id: Scalars['ID']; }; +export type OpenAiCompletionUsage = { + completionTokens: Scalars['Int']; + promptTokens: Scalars['Int']; + totalTokens: Scalars['Int']; +}; + /** PageInfo in Connection. Modeled after GraphQL connection model. */ export type PageInfo = { /** The cursor pointing to the first node of the entire collection, regardless of "before" and "after". Can be used to determine if is in the last page. Null when the collection is empty. */ @@ -821,11 +998,13 @@ export type Query = { * Note that some fields like email is not visible to other users. */ GetUser: Maybe; + ListAIResponses: AiResponseConnection; ListAnalytics: AnalyticsConnection; ListArticleReplyFeedbacks: Maybe; ListArticles: Maybe; ListBlockedUsers: UserConnection; ListCategories: Maybe; + ListCooccurrences: Maybe; ListReplies: Maybe; ListReplyRequests: Maybe; ValidateSlug: Maybe; @@ -853,6 +1032,15 @@ export type QueryGetUserArgs = { }; +export type QueryListAiResponsesArgs = { + after: InputMaybe; + before: InputMaybe; + filter: InputMaybe; + first?: InputMaybe; + orderBy: InputMaybe>>; +}; + + export type QueryListAnalyticsArgs = { after: InputMaybe; before: InputMaybe; @@ -897,6 +1085,15 @@ export type QueryListCategoriesArgs = { }; +export type QueryListCooccurrencesArgs = { + after: InputMaybe; + before: InputMaybe; + filter: InputMaybe; + first?: InputMaybe; + orderBy: InputMaybe>>; +}; + + export type QueryListRepliesArgs = { after: InputMaybe; before: InputMaybe; @@ -1051,6 +1248,16 @@ export type TimeRangeInput = { LTE: InputMaybe; }; +export type TranscriptFilter = { + /** + * more_like_this query's "minimum_should_match" query param for the transcript of `mediaUrl` + * See https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-minimum-should-match.html for possible values. + */ + minimumShouldMatch: InputMaybe; + /** Only used when `filter.mediaUrl` is provided. Generates transcript if provided `filter.mediaUrl` is not transcribed previously. */ + shouldCreate: InputMaybe; +}; + export type User = Node & { appId: Maybe; /** Returns only for current user. Returns `null` otherwise. */ @@ -1127,6 +1334,13 @@ export type ValidationResult = { success: Scalars['Boolean']; }; +export type ListArticlesInInitStateQueryVariables = Exact<{ + text: Scalars['String']; +}>; + + +export type ListArticlesInInitStateQuery = { ListArticles: { edges: Array<{ node: { text: string | null, id: string }, highlight: { text: string | null, hyperlinks: Array<{ title: string | null, summary: string | null } | null> | null } | null }> } | null }; + export type CreateReferenceWordsReplyFragment = { reference: string | null, type: ReplyTypeEnum | null }; export type CreateReplyMessagesReplyFragment = { text: string | null, reference: string | null, type: ReplyTypeEnum | null }; @@ -1135,7 +1349,16 @@ export type CreateReplyMessagesArticleFragment = { replyCount: number | null }; export type CreateHighlightContentsHighlightFragment = { text: string | null, hyperlinks: Array<{ title: string | null, summary: string | null } | null> | null }; +export type CreateAiReplyMutationVariables = Exact<{ + articleId: Scalars['String']; +}>; + + +export type CreateAiReplyMutation = { CreateAIReply: { text: string | null } | null }; + export const CreateReferenceWordsReplyFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"CreateReferenceWordsReply"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Reply"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"reference"}},{"kind":"Field","name":{"kind":"Name","value":"type"}}]}}]} as unknown as DocumentNode; export const CreateReplyMessagesReplyFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"CreateReplyMessagesReply"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Reply"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"text"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"CreateReferenceWordsReply"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"CreateReferenceWordsReply"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Reply"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"reference"}},{"kind":"Field","name":{"kind":"Name","value":"type"}}]}}]} as unknown as DocumentNode; export const CreateReplyMessagesArticleFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"CreateReplyMessagesArticle"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Article"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"replyCount"}}]}}]} as unknown as DocumentNode; -export const CreateHighlightContentsHighlightFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"CreateHighlightContentsHighlight"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Highlights"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"text"}},{"kind":"Field","name":{"kind":"Name","value":"hyperlinks"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"summary"}}]}}]}}]} as unknown as DocumentNode; \ No newline at end of file +export const CreateHighlightContentsHighlightFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"CreateHighlightContentsHighlight"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Highlights"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"text"}},{"kind":"Field","name":{"kind":"Name","value":"hyperlinks"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"summary"}}]}}]}}]} as unknown as DocumentNode; +export const ListArticlesInInitStateDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ListArticlesInInitState"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"text"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"ListArticles"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"moreLikeThis"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"like"},"value":{"kind":"Variable","name":{"kind":"Name","value":"text"}}}]}}]}},{"kind":"Argument","name":{"kind":"Name","value":"orderBy"},"value":{"kind":"ListValue","values":[{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"_score"},"value":{"kind":"EnumValue","value":"DESC"}}]}]}},{"kind":"Argument","name":{"kind":"Name","value":"first"},"value":{"kind":"IntValue","value":"4"}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"edges"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"node"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"text"}},{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"Field","name":{"kind":"Name","value":"highlight"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"text"}},{"kind":"Field","name":{"kind":"Name","value":"hyperlinks"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"summary"}}]}}]}}]}}]}}]}}]} as unknown as DocumentNode; +export const CreateAiReplyDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateAIReply"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"articleId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"CreateAIReply"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"articleId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"articleId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"text"}}]}}]}}]} as unknown as DocumentNode; \ No newline at end of file