diff --git a/.env.example b/.env.example index db5973a11..ded2d82be 100644 --- a/.env.example +++ b/.env.example @@ -43,4 +43,6 @@ AWS_ACCESS_KEY_ID=test AWS_SECRET_ACCESS_KEY=test AWS_ENDPOINT=http://localhost:4566 -MONGO_MIGRATION_URL= \ No newline at end of file +MONGO_MIGRATION_URL= + +OPENAI_API_KEY= \ No newline at end of file diff --git a/.github/workflows/aws.yml b/.github/workflows/aws.yml index 27798bef4..0520bedef 100644 --- a/.github/workflows/aws.yml +++ b/.github/workflows/aws.yml @@ -36,6 +36,7 @@ env: # Needed env variables for first build on next NEXT_PUBLIC_UMAMI_SITE_ID: ${{ secrets.DEVELOPMENT_UMAMI_SITE_ID }} NEXT_PUBLIC_RECAPTCHA_SITEKEY: ${{ secrets.RECAPTCHA_SITEKEY }} + AGENTS_API_URL: ${{ secrets.DEVELOPMENT_AGENTS_API_URL }} jobs: setup-build-publish: @@ -67,6 +68,7 @@ jobs: echo "NOVU_APPLICATION_IDENTIFIER=${{ secrets.PRODUCTION_NOVU_APPLICATION_IDENTIFIER }}" >> $GITHUB_ENV echo "NEW_RELIC_APP_NAME=${{ secrets.PRODUCTION_NEW_RELIC_APP_NAME }}" >> $GITHUB_ENV echo "NEXT_PUBLIC_ORY_SDK_URL=${{ secrets.ORY_SDK_URL }}" >> $GITHUB_ENV + echo "AGENTS_API_URL=${{ secrets.PRODUCTION_AGENTS_API_URL }}" >> $GITHUB_ENV - name: Configure AWS credentials uses: aws-actions/configure-aws-credentials@v1 @@ -97,6 +99,7 @@ jobs: sed -i "s%AWS_SECRET_ACCESS_KEY%$AWS_SECRET_ACCESS_KEY%g" config.$ENVIRONMENT.yaml sed -i "s%NOVU_API_KEY%$NOVU_API_KEY%g" config.$ENVIRONMENT.yaml sed -i "s%NOVU_APPLICATION_IDENTIFIER%$NOVU_APPLICATION_IDENTIFIER%g" config.$ENVIRONMENT.yaml + sed -i "s%AGENTS_API_URL%$AGENTS_API_URL%g" config.$ENVIRONMENT.yaml - name: Set migrate-mongo-config.ts run: | diff --git a/.yarn/cache/@ai-sdk-provider-npm-0.0.3-ca72b7b7a8-cf20dbe7a1.zip b/.yarn/cache/@ai-sdk-provider-npm-0.0.3-ca72b7b7a8-cf20dbe7a1.zip new file mode 100644 index 000000000..501f2e485 Binary files /dev/null and b/.yarn/cache/@ai-sdk-provider-npm-0.0.3-ca72b7b7a8-cf20dbe7a1.zip differ diff --git a/.yarn/cache/@ai-sdk-provider-utils-npm-0.0.5-f24cb58507-90f6451bcf.zip b/.yarn/cache/@ai-sdk-provider-utils-npm-0.0.5-f24cb58507-90f6451bcf.zip new file mode 100644 index 000000000..e05ae2cfc Binary files /dev/null and b/.yarn/cache/@ai-sdk-provider-utils-npm-0.0.5-f24cb58507-90f6451bcf.zip differ diff --git a/.yarn/cache/@anthropic-ai-sdk-npm-0.9.1-ed6a7af5a8-0ec50abc0f.zip b/.yarn/cache/@anthropic-ai-sdk-npm-0.9.1-ed6a7af5a8-0ec50abc0f.zip new file mode 100644 index 000000000..79c3e1615 Binary files /dev/null and b/.yarn/cache/@anthropic-ai-sdk-npm-0.9.1-ed6a7af5a8-0ec50abc0f.zip differ diff --git a/.yarn/cache/@langchain-community-npm-0.0.54-1440fe79c8-bfc01f4dc0.zip b/.yarn/cache/@langchain-community-npm-0.0.54-1440fe79c8-bfc01f4dc0.zip new file mode 100644 index 000000000..9f58071c7 Binary files /dev/null and b/.yarn/cache/@langchain-community-npm-0.0.54-1440fe79c8-bfc01f4dc0.zip differ diff --git a/.yarn/cache/@langchain-core-npm-0.1.61-80b34a94bc-f3eadae482.zip b/.yarn/cache/@langchain-core-npm-0.1.61-80b34a94bc-f3eadae482.zip new file mode 100644 index 000000000..21a30dd83 Binary files /dev/null and b/.yarn/cache/@langchain-core-npm-0.1.61-80b34a94bc-f3eadae482.zip differ diff --git a/.yarn/cache/@langchain-openai-npm-0.0.28-9e023e2c57-ba7c8e4e57.zip b/.yarn/cache/@langchain-openai-npm-0.0.28-9e023e2c57-ba7c8e4e57.zip new file mode 100644 index 000000000..b55006f94 Binary files /dev/null and b/.yarn/cache/@langchain-openai-npm-0.0.28-9e023e2c57-ba7c8e4e57.zip differ diff --git a/.yarn/cache/@langchain-textsplitters-npm-0.0.0-c507dccfdd-13034a3099.zip b/.yarn/cache/@langchain-textsplitters-npm-0.0.0-c507dccfdd-13034a3099.zip new file mode 100644 index 000000000..e9d9960ef Binary files /dev/null and b/.yarn/cache/@langchain-textsplitters-npm-0.0.0-c507dccfdd-13034a3099.zip differ diff --git a/.yarn/cache/@types-diff-match-patch-npm-1.0.36-f65ece6691-7d7ce03422.zip b/.yarn/cache/@types-diff-match-patch-npm-1.0.36-f65ece6691-7d7ce03422.zip new file mode 100644 index 000000000..8407c9e2e Binary files /dev/null and b/.yarn/cache/@types-diff-match-patch-npm-1.0.36-f65ece6691-7d7ce03422.zip differ diff --git a/.yarn/cache/@types-node-npm-18.19.31-cace5a518d-949bddfd70.zip b/.yarn/cache/@types-node-npm-18.19.31-cace5a518d-949bddfd70.zip new file mode 100644 index 000000000..6409b75ef Binary files /dev/null and b/.yarn/cache/@types-node-npm-18.19.31-cace5a518d-949bddfd70.zip differ diff --git a/.yarn/cache/@types-react-dom-npm-17.0.2-e91edc6c98-1725928a1c.zip b/.yarn/cache/@types-react-dom-npm-17.0.2-e91edc6c98-1725928a1c.zip new file mode 100644 index 000000000..5fa5dd4da Binary files /dev/null and b/.yarn/cache/@types-react-dom-npm-17.0.2-e91edc6c98-1725928a1c.zip differ diff --git a/.yarn/cache/@types-react-dom-npm-17.0.21-7d8e92bd2a-a2e3f068c1.zip b/.yarn/cache/@types-react-dom-npm-17.0.21-7d8e92bd2a-a2e3f068c1.zip deleted file mode 100644 index 4687ee9cb..000000000 Binary files a/.yarn/cache/@types-react-dom-npm-17.0.21-7d8e92bd2a-a2e3f068c1.zip and /dev/null differ diff --git a/.yarn/cache/@types-react-npm-17.0.67-c98d561b35-eb4915fc7d.zip b/.yarn/cache/@types-react-npm-17.0.67-c98d561b35-eb4915fc7d.zip deleted file mode 100644 index 6c11e7090..000000000 Binary files a/.yarn/cache/@types-react-npm-17.0.67-c98d561b35-eb4915fc7d.zip and /dev/null differ diff --git a/.yarn/cache/@types-react-npm-17.0.80-2952871e27-1c27bfc423.zip b/.yarn/cache/@types-react-npm-17.0.80-2952871e27-1c27bfc423.zip new file mode 100644 index 000000000..5162d88b0 Binary files /dev/null and b/.yarn/cache/@types-react-npm-17.0.80-2952871e27-1c27bfc423.zip differ diff --git a/.yarn/cache/@types-react-npm-18.2.24-331ca056d1-ea5d8204e7.zip b/.yarn/cache/@types-react-npm-18.2.24-331ca056d1-ea5d8204e7.zip deleted file mode 100644 index 956cb75b2..000000000 Binary files a/.yarn/cache/@types-react-npm-18.2.24-331ca056d1-ea5d8204e7.zip and /dev/null differ diff --git a/.yarn/cache/@types-retry-npm-0.12.0-e4e6294a2c-61a072c763.zip b/.yarn/cache/@types-retry-npm-0.12.0-e4e6294a2c-61a072c763.zip new file mode 100644 index 000000000..f7c0ed21e Binary files /dev/null and b/.yarn/cache/@types-retry-npm-0.12.0-e4e6294a2c-61a072c763.zip differ diff --git a/.yarn/cache/@types-scheduler-npm-0.16.4-aba8785e94-a57b0f10da.zip b/.yarn/cache/@types-scheduler-npm-0.16.4-aba8785e94-a57b0f10da.zip deleted file mode 100644 index 84a0d9ea4..000000000 Binary files a/.yarn/cache/@types-scheduler-npm-0.16.4-aba8785e94-a57b0f10da.zip and /dev/null differ diff --git a/.yarn/cache/@types-scheduler-npm-0.16.8-303819b439-6c091b096d.zip b/.yarn/cache/@types-scheduler-npm-0.16.8-303819b439-6c091b096d.zip new file mode 100644 index 000000000..b19515df3 Binary files /dev/null and b/.yarn/cache/@types-scheduler-npm-0.16.8-303819b439-6c091b096d.zip differ diff --git a/.yarn/cache/@types-uuid-npm-9.0.8-3eeeaa5abb-b8c60b7ba8.zip b/.yarn/cache/@types-uuid-npm-9.0.8-3eeeaa5abb-b8c60b7ba8.zip new file mode 100644 index 000000000..3e5c2a380 Binary files /dev/null and b/.yarn/cache/@types-uuid-npm-9.0.8-3eeeaa5abb-b8c60b7ba8.zip differ diff --git a/.yarn/cache/ai-npm-3.1.1-ad882cd688-bb38caa714.zip b/.yarn/cache/ai-npm-3.1.1-ad882cd688-bb38caa714.zip new file mode 100644 index 000000000..2f2df7480 Binary files /dev/null and b/.yarn/cache/ai-npm-3.1.1-ad882cd688-bb38caa714.zip differ diff --git a/.yarn/cache/base-64-npm-0.1.0-41e6da6777-5a42938f82.zip b/.yarn/cache/base-64-npm-0.1.0-41e6da6777-5a42938f82.zip new file mode 100644 index 000000000..55635d228 Binary files /dev/null and b/.yarn/cache/base-64-npm-0.1.0-41e6da6777-5a42938f82.zip differ diff --git a/.yarn/cache/binary-extensions-npm-2.3.0-bd3f20d865-bcad01494e.zip b/.yarn/cache/binary-extensions-npm-2.3.0-bd3f20d865-bcad01494e.zip new file mode 100644 index 000000000..94214c4b8 Binary files /dev/null and b/.yarn/cache/binary-extensions-npm-2.3.0-bd3f20d865-bcad01494e.zip differ diff --git a/.yarn/cache/binary-search-npm-1.3.6-b150a83e72-2e6b3459a9.zip b/.yarn/cache/binary-search-npm-1.3.6-b150a83e72-2e6b3459a9.zip new file mode 100644 index 000000000..fdf4e4157 Binary files /dev/null and b/.yarn/cache/binary-search-npm-1.3.6-b150a83e72-2e6b3459a9.zip differ diff --git a/.yarn/cache/decamelize-npm-1.2.0-c5a2fdc622-ad8c51a7e7.zip b/.yarn/cache/decamelize-npm-1.2.0-c5a2fdc622-ad8c51a7e7.zip new file mode 100644 index 000000000..db4ac470f Binary files /dev/null and b/.yarn/cache/decamelize-npm-1.2.0-c5a2fdc622-ad8c51a7e7.zip differ diff --git a/.yarn/cache/diff-match-patch-npm-1.0.5-f715ad1381-841522d01b.zip b/.yarn/cache/diff-match-patch-npm-1.0.5-f715ad1381-841522d01b.zip new file mode 100644 index 000000000..ccb35d84d Binary files /dev/null and b/.yarn/cache/diff-match-patch-npm-1.0.5-f715ad1381-841522d01b.zip differ diff --git a/.yarn/cache/digest-fetch-npm-1.3.0-00876b1fae-8ebdb4b9ef.zip b/.yarn/cache/digest-fetch-npm-1.3.0-00876b1fae-8ebdb4b9ef.zip new file mode 100644 index 000000000..f3302e37f Binary files /dev/null and b/.yarn/cache/digest-fetch-npm-1.3.0-00876b1fae-8ebdb4b9ef.zip differ diff --git a/.yarn/cache/eventemitter3-npm-4.0.7-7afcdd74ae-1875311c42.zip b/.yarn/cache/eventemitter3-npm-4.0.7-7afcdd74ae-1875311c42.zip new file mode 100644 index 000000000..0cfd591e8 Binary files /dev/null and b/.yarn/cache/eventemitter3-npm-4.0.7-7afcdd74ae-1875311c42.zip differ diff --git a/.yarn/cache/eventsource-parser-npm-1.1.2-5a5b47ad45-01896eea72.zip b/.yarn/cache/eventsource-parser-npm-1.1.2-5a5b47ad45-01896eea72.zip new file mode 100644 index 000000000..675741300 Binary files /dev/null and b/.yarn/cache/eventsource-parser-npm-1.1.2-5a5b47ad45-01896eea72.zip differ diff --git a/.yarn/cache/expr-eval-npm-2.0.2-20b6d1f745-01862f09b5.zip b/.yarn/cache/expr-eval-npm-2.0.2-20b6d1f745-01862f09b5.zip new file mode 100644 index 000000000..8da3c8f90 Binary files /dev/null and b/.yarn/cache/expr-eval-npm-2.0.2-20b6d1f745-01862f09b5.zip differ diff --git a/.yarn/cache/flat-npm-5.0.2-12748102a5-12a1536ac7.zip b/.yarn/cache/flat-npm-5.0.2-12748102a5-12a1536ac7.zip new file mode 100644 index 000000000..e3295fae7 Binary files /dev/null and b/.yarn/cache/flat-npm-5.0.2-12748102a5-12a1536ac7.zip differ diff --git a/.yarn/cache/form-data-encoder-npm-1.7.2-e6028ef027-aeebd87a1c.zip b/.yarn/cache/form-data-encoder-npm-1.7.2-e6028ef027-aeebd87a1c.zip new file mode 100644 index 000000000..6c1b9e1df Binary files /dev/null and b/.yarn/cache/form-data-encoder-npm-1.7.2-e6028ef027-aeebd87a1c.zip differ diff --git a/.yarn/cache/formdata-node-npm-4.4.1-1fb15d9b89-d91d4f667c.zip b/.yarn/cache/formdata-node-npm-4.4.1-1fb15d9b89-d91d4f667c.zip new file mode 100644 index 000000000..b8a0b8263 Binary files /dev/null and b/.yarn/cache/formdata-node-npm-4.4.1-1fb15d9b89-d91d4f667c.zip differ diff --git a/.yarn/cache/is-any-array-npm-2.0.1-922fa2803c-472ed80e17.zip b/.yarn/cache/is-any-array-npm-2.0.1-922fa2803c-472ed80e17.zip new file mode 100644 index 000000000..8ea2eab84 Binary files /dev/null and b/.yarn/cache/is-any-array-npm-2.0.1-922fa2803c-472ed80e17.zip differ diff --git a/.yarn/cache/js-tiktoken-npm-1.0.11-b620fce603-0cb3e81f28.zip b/.yarn/cache/js-tiktoken-npm-1.0.11-b620fce603-0cb3e81f28.zip new file mode 100644 index 000000000..b069b6b1c Binary files /dev/null and b/.yarn/cache/js-tiktoken-npm-1.0.11-b620fce603-0cb3e81f28.zip differ diff --git a/.yarn/cache/jsondiffpatch-npm-0.6.0-8a8e017f57-27d7aa42c3.zip b/.yarn/cache/jsondiffpatch-npm-0.6.0-8a8e017f57-27d7aa42c3.zip new file mode 100644 index 000000000..28d618e36 Binary files /dev/null and b/.yarn/cache/jsondiffpatch-npm-0.6.0-8a8e017f57-27d7aa42c3.zip differ diff --git a/.yarn/cache/jsonpointer-npm-5.0.1-8e4c22e512-0b40f71290.zip b/.yarn/cache/jsonpointer-npm-5.0.1-8e4c22e512-0b40f71290.zip new file mode 100644 index 000000000..3216800d4 Binary files /dev/null and b/.yarn/cache/jsonpointer-npm-5.0.1-8e4c22e512-0b40f71290.zip differ diff --git a/.yarn/cache/langchain-npm-0.1.36-29e5d23d15-0027c3ae02.zip b/.yarn/cache/langchain-npm-0.1.36-29e5d23d15-0027c3ae02.zip new file mode 100644 index 000000000..5f6049843 Binary files /dev/null and b/.yarn/cache/langchain-npm-0.1.36-29e5d23d15-0027c3ae02.zip differ diff --git a/.yarn/cache/langchainhub-npm-0.0.10-1233c43f6a-9bce1ec7eb.zip b/.yarn/cache/langchainhub-npm-0.0.10-1233c43f6a-9bce1ec7eb.zip new file mode 100644 index 000000000..db95c61d6 Binary files /dev/null and b/.yarn/cache/langchainhub-npm-0.0.10-1233c43f6a-9bce1ec7eb.zip differ diff --git a/.yarn/cache/langsmith-npm-0.1.22-2a4495d5ac-ea507a9596.zip b/.yarn/cache/langsmith-npm-0.1.22-2a4495d5ac-ea507a9596.zip new file mode 100644 index 000000000..361fcfe69 Binary files /dev/null and b/.yarn/cache/langsmith-npm-0.1.22-2a4495d5ac-ea507a9596.zip differ diff --git a/.yarn/cache/ml-array-mean-npm-1.1.6-df75cbf3dd-81999dac8b.zip b/.yarn/cache/ml-array-mean-npm-1.1.6-df75cbf3dd-81999dac8b.zip new file mode 100644 index 000000000..dca71eddb Binary files /dev/null and b/.yarn/cache/ml-array-mean-npm-1.1.6-df75cbf3dd-81999dac8b.zip differ diff --git a/.yarn/cache/ml-array-sum-npm-1.1.6-64a901dff6-369dbb3681.zip b/.yarn/cache/ml-array-sum-npm-1.1.6-64a901dff6-369dbb3681.zip new file mode 100644 index 000000000..077fc5964 Binary files /dev/null and b/.yarn/cache/ml-array-sum-npm-1.1.6-64a901dff6-369dbb3681.zip differ diff --git a/.yarn/cache/ml-distance-euclidean-npm-2.0.0-6a442f7f40-e31f98a947.zip b/.yarn/cache/ml-distance-euclidean-npm-2.0.0-6a442f7f40-e31f98a947.zip new file mode 100644 index 000000000..28c584c92 Binary files /dev/null and b/.yarn/cache/ml-distance-euclidean-npm-2.0.0-6a442f7f40-e31f98a947.zip differ diff --git a/.yarn/cache/ml-distance-npm-4.0.1-9653973c44-21ea014064.zip b/.yarn/cache/ml-distance-npm-4.0.1-9653973c44-21ea014064.zip new file mode 100644 index 000000000..efafc09d1 Binary files /dev/null and b/.yarn/cache/ml-distance-npm-4.0.1-9653973c44-21ea014064.zip differ diff --git a/.yarn/cache/ml-tree-similarity-npm-1.0.0-a387b90b6c-f99e217dc9.zip b/.yarn/cache/ml-tree-similarity-npm-1.0.0-a387b90b6c-f99e217dc9.zip new file mode 100644 index 000000000..603343caf Binary files /dev/null and b/.yarn/cache/ml-tree-similarity-npm-1.0.0-a387b90b6c-f99e217dc9.zip differ diff --git a/.yarn/cache/mustache-npm-4.2.0-1fe3d6d77a-928fcb63e3.zip b/.yarn/cache/mustache-npm-4.2.0-1fe3d6d77a-928fcb63e3.zip new file mode 100644 index 000000000..ecb3972ef Binary files /dev/null and b/.yarn/cache/mustache-npm-4.2.0-1fe3d6d77a-928fcb63e3.zip differ diff --git a/.yarn/cache/node-domexception-npm-1.0.0-e1e813b76f-ee1d37dd2a.zip b/.yarn/cache/node-domexception-npm-1.0.0-e1e813b76f-ee1d37dd2a.zip new file mode 100644 index 000000000..d58ba924f Binary files /dev/null and b/.yarn/cache/node-domexception-npm-1.0.0-e1e813b76f-ee1d37dd2a.zip differ diff --git a/.yarn/cache/num-sort-npm-2.1.0-e0725952ee-5a80cd0456.zip b/.yarn/cache/num-sort-npm-2.1.0-e0725952ee-5a80cd0456.zip new file mode 100644 index 000000000..6d0445b94 Binary files /dev/null and b/.yarn/cache/num-sort-npm-2.1.0-e0725952ee-5a80cd0456.zip differ diff --git a/.yarn/cache/openai-npm-4.41.1-f28111a8e4-f016424c85.zip b/.yarn/cache/openai-npm-4.41.1-f28111a8e4-f016424c85.zip new file mode 100644 index 000000000..89f7ea358 Binary files /dev/null and b/.yarn/cache/openai-npm-4.41.1-f28111a8e4-f016424c85.zip differ diff --git a/.yarn/cache/openapi-types-npm-12.1.3-1b8ae4a632-7fa5547f87.zip b/.yarn/cache/openapi-types-npm-12.1.3-1b8ae4a632-7fa5547f87.zip new file mode 100644 index 000000000..bc26fcc2c Binary files /dev/null and b/.yarn/cache/openapi-types-npm-12.1.3-1b8ae4a632-7fa5547f87.zip differ diff --git a/.yarn/cache/p-finally-npm-1.0.0-35fbaa57c6-93a654c53d.zip b/.yarn/cache/p-finally-npm-1.0.0-35fbaa57c6-93a654c53d.zip new file mode 100644 index 000000000..091273a2a Binary files /dev/null and b/.yarn/cache/p-finally-npm-1.0.0-35fbaa57c6-93a654c53d.zip differ diff --git a/.yarn/cache/p-queue-npm-6.6.2-b173c5bfa8-832642fcc4.zip b/.yarn/cache/p-queue-npm-6.6.2-b173c5bfa8-832642fcc4.zip new file mode 100644 index 000000000..da69f7750 Binary files /dev/null and b/.yarn/cache/p-queue-npm-6.6.2-b173c5bfa8-832642fcc4.zip differ diff --git a/.yarn/cache/p-retry-npm-4.6.2-9f871cfc9b-45c270bfdd.zip b/.yarn/cache/p-retry-npm-4.6.2-9f871cfc9b-45c270bfdd.zip new file mode 100644 index 000000000..17581af83 Binary files /dev/null and b/.yarn/cache/p-retry-npm-4.6.2-9f871cfc9b-45c270bfdd.zip differ diff --git a/.yarn/cache/p-timeout-npm-3.2.0-7fdb33f733-3dd0eaa048.zip b/.yarn/cache/p-timeout-npm-3.2.0-7fdb33f733-3dd0eaa048.zip new file mode 100644 index 000000000..eaf8f71c7 Binary files /dev/null and b/.yarn/cache/p-timeout-npm-3.2.0-7fdb33f733-3dd0eaa048.zip differ diff --git a/.yarn/cache/retry-npm-0.13.1-89eb100ab6-47c4d5be67.zip b/.yarn/cache/retry-npm-0.13.1-89eb100ab6-47c4d5be67.zip new file mode 100644 index 000000000..9a38721ed Binary files /dev/null and b/.yarn/cache/retry-npm-0.13.1-89eb100ab6-47c4d5be67.zip differ diff --git a/.yarn/cache/secure-json-parse-npm-2.7.0-d5b89b0a3e-d9d7d5a01f.zip b/.yarn/cache/secure-json-parse-npm-2.7.0-d5b89b0a3e-d9d7d5a01f.zip new file mode 100644 index 000000000..6609a2efc Binary files /dev/null and b/.yarn/cache/secure-json-parse-npm-2.7.0-d5b89b0a3e-d9d7d5a01f.zip differ diff --git a/.yarn/cache/solid-swr-store-npm-0.10.7-9df0c0e877-c2b51b64ae.zip b/.yarn/cache/solid-swr-store-npm-0.10.7-9df0c0e877-c2b51b64ae.zip new file mode 100644 index 000000000..d9cc00304 Binary files /dev/null and b/.yarn/cache/solid-swr-store-npm-0.10.7-9df0c0e877-c2b51b64ae.zip differ diff --git a/.yarn/cache/sswr-npm-2.0.0-506572cea1-792f2e2410.zip b/.yarn/cache/sswr-npm-2.0.0-506572cea1-792f2e2410.zip new file mode 100644 index 000000000..baca6c307 Binary files /dev/null and b/.yarn/cache/sswr-npm-2.0.0-506572cea1-792f2e2410.zip differ diff --git a/.yarn/cache/swr-npm-2.2.0-290c5e8c1c-1f04795ff9.zip b/.yarn/cache/swr-npm-2.2.0-290c5e8c1c-1f04795ff9.zip new file mode 100644 index 000000000..25754c858 Binary files /dev/null and b/.yarn/cache/swr-npm-2.2.0-290c5e8c1c-1f04795ff9.zip differ diff --git a/.yarn/cache/swr-store-npm-0.10.6-526e439bbd-81a0df8ed7.zip b/.yarn/cache/swr-store-npm-0.10.6-526e439bbd-81a0df8ed7.zip new file mode 100644 index 000000000..8e4f541d2 Binary files /dev/null and b/.yarn/cache/swr-store-npm-0.10.6-526e439bbd-81a0df8ed7.zip differ diff --git a/.yarn/cache/swrev-npm-4.0.0-a93e59e6ce-454aed0e03.zip b/.yarn/cache/swrev-npm-4.0.0-a93e59e6ce-454aed0e03.zip new file mode 100644 index 000000000..210009b46 Binary files /dev/null and b/.yarn/cache/swrev-npm-4.0.0-a93e59e6ce-454aed0e03.zip differ diff --git a/.yarn/cache/swrv-npm-1.0.4-ec745e5cb9-6de43116e1.zip b/.yarn/cache/swrv-npm-1.0.4-ec745e5cb9-6de43116e1.zip new file mode 100644 index 000000000..0ac0abe7b Binary files /dev/null and b/.yarn/cache/swrv-npm-1.0.4-ec745e5cb9-6de43116e1.zip differ diff --git a/.yarn/cache/web-streams-polyfill-npm-3.3.3-f24b9f8c34-21ab5ea08a.zip b/.yarn/cache/web-streams-polyfill-npm-3.3.3-f24b9f8c34-21ab5ea08a.zip new file mode 100644 index 000000000..2b72fbddb Binary files /dev/null and b/.yarn/cache/web-streams-polyfill-npm-3.3.3-f24b9f8c34-21ab5ea08a.zip differ diff --git a/.yarn/cache/web-streams-polyfill-npm-4.0.0-beta.3-0dc6d160ed-dfec1fbf52.zip b/.yarn/cache/web-streams-polyfill-npm-4.0.0-beta.3-0dc6d160ed-dfec1fbf52.zip new file mode 100644 index 000000000..39b640afc Binary files /dev/null and b/.yarn/cache/web-streams-polyfill-npm-4.0.0-beta.3-0dc6d160ed-dfec1fbf52.zip differ diff --git a/.yarn/cache/yaml-npm-2.4.2-5c2ee7f06c-90dda4485d.zip b/.yarn/cache/yaml-npm-2.4.2-5c2ee7f06c-90dda4485d.zip new file mode 100644 index 000000000..9dc9744b9 Binary files /dev/null and b/.yarn/cache/yaml-npm-2.4.2-5c2ee7f06c-90dda4485d.zip differ diff --git a/.yarn/cache/zod-npm-3.23.6-292bd5eb23-f534119e2a.zip b/.yarn/cache/zod-npm-3.23.6-292bd5eb23-f534119e2a.zip new file mode 100644 index 000000000..3a7cc09e7 Binary files /dev/null and b/.yarn/cache/zod-npm-3.23.6-292bd5eb23-f534119e2a.zip differ diff --git a/.yarn/cache/zod-to-json-schema-npm-3.22.5-2f71669da1-3c4f87c7cf.zip b/.yarn/cache/zod-to-json-schema-npm-3.22.5-2f71669da1-3c4f87c7cf.zip new file mode 100644 index 000000000..ce10779db Binary files /dev/null and b/.yarn/cache/zod-to-json-schema-npm-3.22.5-2f71669da1-3c4f87c7cf.zip differ diff --git a/.yarn/cache/zod-to-json-schema-npm-3.23.0-dbca575535-56f220f066.zip b/.yarn/cache/zod-to-json-schema-npm-3.23.0-dbca575535-56f220f066.zip new file mode 100644 index 000000000..e99e946a5 Binary files /dev/null and b/.yarn/cache/zod-to-json-schema-npm-3.23.0-dbca575535-56f220f066.zip differ diff --git a/.yarn/install-state.gz b/.yarn/install-state.gz index 2060aa3f0..93cf1cde7 100644 Binary files a/.yarn/install-state.gz and b/.yarn/install-state.gz differ diff --git a/Dockerfile b/Dockerfile index e90d95de5..ab0567f45 100644 --- a/Dockerfile +++ b/Dockerfile @@ -33,7 +33,7 @@ COPY ./next-i18next.config.js /app/next-i18next.config.js WORKDIR /app RUN apk add --no-cache git python3 make g++ -RUN yarn install +RUN yarn install --production RUN NEXT_PUBLIC_UMAMI_SITE_ID=$NEXT_PUBLIC_UMAMI_SITE_ID \ NEXT_PUBLIC_RECAPTCHA_SITEKEY=$NEXT_PUBLIC_RECAPTCHA_SITEKEY \ NEXT_PUBLIC_ENVIRONMENT=$NEXT_PUBLIC_ENVIRONMENT \ diff --git a/config.development.yaml b/config.development.yaml index c24abd9dc..5f825a1f0 100644 --- a/config.development.yaml +++ b/config.development.yaml @@ -5,6 +5,7 @@ services: cors: '*' debug: true websocketUrl: wss://testws.aletheiafact.org + automatedFactCheckingAPIUrl: AGENTS_API_URL override_public_routes: true recaptcha_secret: RECAPTCHA_SECRET recaptcha_sitekey: 6Lc2BtYUAAAAAOUBI-9r1sDJUIfG2nt6C43noOXh diff --git a/config.example.yaml b/config.example.yaml index ceb1b539e..57950cb69 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -5,7 +5,7 @@ services: cors: '*' debug: true websocketUrl: ws://localhost:5001 - agentsUrl: http://localhost:8000 + automatedFactCheckingAPIUrl: AGENTS_API_URL #override_public_routes: true recaptcha_secret: RECAPTCHA_SECRET recaptcha_sitekey: 6Lc2BtYUAAAAAOUBI-9r1sDJUIfG2nt6C43noOXh diff --git a/config.production.yaml b/config.production.yaml index 2fe69608a..e4862b1b7 100644 --- a/config.production.yaml +++ b/config.production.yaml @@ -4,6 +4,7 @@ services: port: 3000 cors: '*' websocketUrl: wss://ws.aletheiafact.org + automatedFactCheckingAPIUrl: AGENTS_API_URL recaptcha_secret: RECAPTCHA_SECRET recaptcha_sitekey: 6Lc2BtYUAAAAAOUBI-9r1sDJUIfG2nt6C43noOXh db: diff --git a/cypress/e2e/tests/review.cy.ts b/cypress/e2e/tests/review.cy.ts index 6717e20c9..78b07a6ba 100644 --- a/cypress/e2e/tests/review.cy.ts +++ b/cypress/e2e/tests/review.cy.ts @@ -36,7 +36,6 @@ describe("Test claim review", () => { it("should be able to assign a user", () => { cy.login(); goToClaimReviewPage(); - cy.checkRecaptcha(); cy.get(locators.claimReview.BTN_START_CLAIM_REVIEW) .should("exist") .click(); diff --git a/deployment/websocket.yml b/deployment/websocket.yml index 22c047147..27407e0ed 100644 --- a/deployment/websocket.yml +++ b/deployment/websocket.yml @@ -1,77 +1,77 @@ -### INGRESS FRONTEND ### -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: ingress-aletheia-ws - namespace: ENVIRONMENT - annotations: - kubernetes.io/ingress.class: traefik -spec: - rules: - - host: testws.aletheiafact.org - http: - paths: - - path: / - pathType: Prefix - backend: - service: - name: aletheia-ws - port: - name: aletheia-ws ---- +# ### INGRESS FRONTEND ### +# apiVersion: networking.k8s.io/v1 +# kind: Ingress +# metadata: +# name: ingress-aletheia-ws +# namespace: ENVIRONMENT +# annotations: +# kubernetes.io/ingress.class: traefik +# spec: +# rules: +# - host: testws.aletheiafact.org +# http: +# paths: +# - path: / +# pathType: Prefix +# backend: +# service: +# name: aletheia-ws +# port: +# name: aletheia-ws +# --- ### SERVICE FRONTEND ### -apiVersion: v1 -kind: Service -metadata: - name: aletheia-ws - namespace: ENVIRONMENT -spec: - type: NodePort - selector: - app: aletheia-ws - ports: - - name: aletheia-ws - targetPort: 5051 - port: 80 ---- -### DEPLOYMENT FRONTEND ### -apiVersion: apps/v1 -kind: Deployment -metadata: - name: aletheia-ws - namespace: ENVIRONMENT -spec: - replicas: 1 - selector: - matchLabels: - app: aletheia-ws - template: - metadata: - labels: - app: aletheia-ws - spec: - containers: - - name: aletheia-ws - image: 134187360702.dkr.ecr.us-east-1.amazonaws.com/aletheiafact-production:TAG - args: ["-c", "config.websocket.yaml"] - imagePullPolicy: Always - readinessProbe: - httpGet: - path: /api/health - port: 5051 - initialDelaySeconds: 50 - timeoutSeconds: 5 - livenessProbe: - httpGet: - path: /api/health - port: 5051 - initialDelaySeconds: 50 - timeoutSeconds: 10 - failureThreshold: 10 - resources: - limits: - cpu: 120m - memory: 256Mi - requests: - cpu: 80m - memory: 128Mi +# apiVersion: v1 +# kind: Service +# metadata: +# name: aletheia-ws +# namespace: ENVIRONMENT +# spec: +# type: NodePort +# selector: +# app: aletheia-ws +# ports: +# - name: aletheia-ws +# targetPort: 5051 +# port: 80 +# --- +# ### DEPLOYMENT FRONTEND ### +# apiVersion: apps/v1 +# kind: Deployment +# metadata: +# name: aletheia-ws +# namespace: ENVIRONMENT +# spec: +# replicas: 1 +# selector: +# matchLabels: +# app: aletheia-ws +# template: +# metadata: +# labels: +# app: aletheia-ws +# spec: +# containers: +# - name: aletheia-ws +# image: 134187360702.dkr.ecr.us-east-1.amazonaws.com/aletheiafact-production:TAG +# args: ["-c", "config.websocket.yaml"] +# imagePullPolicy: Always +# readinessProbe: +# httpGet: +# path: /api/health +# port: 5051 +# initialDelaySeconds: 50 +# timeoutSeconds: 5 +# livenessProbe: +# httpGet: +# path: /api/health +# port: 5051 +# initialDelaySeconds: 50 +# timeoutSeconds: 10 +# failureThreshold: 10 +# resources: +# limits: +# cpu: 120m +# memory: 256Mi +# requests: +# cpu: 80m +# memory: 128Mi diff --git a/lib/editor-parser.ts b/lib/editor-parser.ts index ba655dd25..7716340d7 100644 --- a/lib/editor-parser.ts +++ b/lib/editor-parser.ts @@ -3,6 +3,8 @@ import { ReviewTaskMachineContextReviewData } from "../server/claim-review-task/ const EditorSchemaArray = ["summary", "report", "verification", "questions"]; +const MarkupCleanerRegex = /{{[^|]+\|([^}]+)}}/; + const defaultDoc: RemirrorJSON = { type: "doc", content: [ @@ -54,8 +56,10 @@ export class EditorParser { return fragmentText; } - if (type === "source" && fragmentText === targetText) { - return `${fragmentText}${sup}`; + const parsedFragmentText = this.extractTextFromMarkUp(fragmentText); + + if (type === "source" && parsedFragmentText === targetText) { + return `${parsedFragmentText}${sup}`; } return fragmentText; } @@ -187,8 +191,15 @@ export class EditorParser { schema.sources.push( ...this.getSourcesFromEditorMarks(text, type, marks) ); + const markId = marks.map( + ({ attrs }: ObjectMark) => attrs.id + ); + + // Pushing the text into content with markup based on its source id + sourceContent.push(`{{${markId}|${text}}}`); + } else { + sourceContent.push(text); } - sourceContent.push(text); } } } @@ -219,7 +230,8 @@ export class EditorParser { field, textRange: this.findTextRange( schema[field], - textRange + textRange, + id ), targetText: textRange, sup: index + 1, @@ -232,13 +244,15 @@ export class EditorParser { return newSources.sort((a, b) => a.sup - b.sup); } - findTextRange(content, textTarget) { + findTextRange(content, textTarget, sourceId) { const contentArray = Array.isArray(content) ? content : [content]; + const markUpText = `{{${sourceId}|${textTarget}}}`; + // Looks for the specific text with the right markup and returns the range of the marked-up text return contentArray.flatMap((c) => { - const start = c.indexOf(textTarget); + const start = c.indexOf(markUpText); if (start !== -1) { - const end = start + textTarget.length; + const end = start + markUpText.length; return [start, end]; } return []; @@ -305,6 +319,8 @@ export class EditorParser { const { textRange, targetText, id } = props; const fragmentText = content.slice(...textRange); + const parsedFragmentText = this.extractTextFromMarkUp(fragmentText); + switch (type) { case "text": if (fragmentText) { @@ -312,9 +328,9 @@ export class EditorParser { } break; case "source": - if (fragmentText === targetText) { + if (parsedFragmentText === targetText) { return this.getContentObjectWithMarks( - fragmentText, + parsedFragmentText, href, id ); @@ -465,4 +481,9 @@ export class EditorParser { ], })); } + + extractTextFromMarkUp(fragmentText) { + const match = fragmentText.match(MarkupCleanerRegex); + return match ? match[1] : ""; + } } diff --git a/package.json b/package.json index 6c0c023ab..d4960dd62 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "aletheia", - "version": "1.0.0", + "version": "1.0.2", "description": "Crowd-sourced fact-checking platform for the website AletheiaFact.org.", "license": "GPL-3.0", "main": "./dist/server/main.js", @@ -72,10 +72,13 @@ "@dhaiwat10/react-link-preview": "^1.9.1", "@emotion/react": "^11.10.4", "@emotion/styled": "^11.10.4", + "@langchain/community": "^0.0.54", + "@langchain/openai": "^0.0.28", "@mui/icons-material": "^5.10.9", "@mui/material": "^5.10.13", "@mui/x-data-grid": "^5.17.11", "@nestjs/axios": "^3.0.0", + "@nestjs/cli": "9.1.5", "@nestjs/common": "^9.2.0", "@nestjs/config": "^2.2.0", "@nestjs/core": "^9.2.0", @@ -90,7 +93,6 @@ "@novu/headless": "^0.19.0", "@novu/node": "^0.19.0", "@novu/notification-center": "^0.19.0", - "@ory/cli": "^0.1.24", "@ory/client": "1.6.2", "@ory/integrations": "^1.1.5", "@remirror/extension-yjs": "^3.0.14", @@ -102,16 +104,19 @@ "@typescript-eslint/eslint-plugin": "^4.29.0", "@xstate/react": "^3.0.0", "accept-language-parser": "^1.5.0", + "ai": "^3.1.1", "antd": "^4.18.5", "aws-sdk": "^2.1154.0", "axios": "^1.5.0", "babel-jest": "^29.7.0", + "babel-plugin-styled-components": "^1.13.2", "class-transformer": "^0.5.1", "class-validator": "^0.14.0", "compromise": "^13.11.4", "compromise-paragraphs": "^0.0.5", "compromise-sentences": "^0.3.0", "cookie-parser": "^1.4.5", + "copyfiles": "^2.4.1", "domino": "^2.1.6", "dompurify": "^3.0.5", "express": "^4.19.2", @@ -122,9 +127,11 @@ "jotai": "^1.9.2", "jotai-xstate": "^0.3.0", "js-cookie": "^3.0.1", + "langchain": "^0.1.36", "lottie-web": "^5.10.1", "md5": "^2.3.0", "migrate-mongo-ts": "^1.1.4", + "mongodb-memory-server": "^8.15.1", "mongoose": "^ 5.13.15", "mongoose-softdelete-typescript": "^0.0.3", "nestjs-unleash": "^2.2.3", @@ -156,6 +163,7 @@ "socket.io": "^4.7.2", "storybook": "^7.4.5", "styled-components": "^5.3.0", + "ts-node": "^10.2.0", "winston": "^3.13.0", "ws": "^8.13.0", "xstate": "^4.32.1", @@ -168,10 +176,10 @@ "@babel/core": "^7.15.0", "@babel/preset-env": "^7.22.20", "@babel/preset-react": "^7.14.5", - "@nestjs/cli": "9.1.5", "@nestjs/schematics": "9.0.3", "@nestjs/testing": "^9.3.12", "@next/eslint-plugin-next": "^12.0.10", + "@ory/cli": "^0.1.24", "@shelf/jest-mongodb": "^2.2.0", "@storybook/addon-actions": "^7.4.5", "@storybook/addon-essentials": "^7.4.5", @@ -190,9 +198,7 @@ "@typescript-eslint/parser": "^4.29.0", "babel-eslint": "^10.1.0", "babel-loader": "^8.2.5", - "babel-plugin-styled-components": "^1.13.2", "concurrently": "^8.2.1", - "copyfiles": "^2.4.1", "cypress": "^12.17.4", "env-cmd": "^10.1.0", "eslint": "7.0.0", @@ -209,13 +215,11 @@ "husky": "^8.0.1", "jest": "^29.7.0", "lint-staged": "^13.0.3", - "mongodb-memory-server": "^8.15.1", "nodemon": "^2.0.12", "prettier": "2.3.2", "react-i18next": "^11.16.7", "supertest": "^6.2.4", "ts-jest": "^29.1.1", - "ts-node": "^10.2.0", "typescript": "^4.3.5", "wait-on": "^6.0.1" }, @@ -253,7 +257,9 @@ "follow-redirects": "1.15.6", "webpack-dev-middleware": "6.1.3", "ip": "1.1.9", - "tar": "6.2.1" + "tar": "6.2.1", + "@types/react": "^17.0.38", + "@types/react-dom": "17.0.2" }, "packageManager": "yarn@3.6.3" } diff --git a/public/locales/en/claimReviewForm.json b/public/locales/en/claimReviewForm.json index b5ead686c..72db7e515 100644 --- a/public/locales/en/claimReviewForm.json +++ b/public/locales/en/claimReviewForm.json @@ -45,12 +45,5 @@ "rejectionCommentLabel": "Rejection comment", "rejectionCommentPlaceholder": "Describe what needs to be changed", "loginButton": "Login to continue", - "notReviewed": "Not reviewed", - "addAgentReview": "Add AI review", - "agentInputText": "Fact-check the following claim: {{sentence}}, provide the answer in English.", - "addAgentReviewModalTitle": "Generating fact-checking report", - "agentLoadingThoughts": "Thinking...", - "agentFinishedThoughts": "Finished.", - "agentFinishedReport": "Report generated", - "submitAgentReview": "Submit generated report" + "notReviewed": "Not reviewed" } diff --git a/public/locales/en/copilotChatBot.json b/public/locales/en/copilotChatBot.json new file mode 100644 index 000000000..2adfb9075 --- /dev/null +++ b/public/locales/en/copilotChatBot.json @@ -0,0 +1,14 @@ +{ + "chatBotGreetings": "Hello, I am the Aletheia fact-checker assistant.", + "Assistant": "Assistant", + "You": "You", + "suggestionHeader": "How can I help you ?", + "suggestion1": "I need help with my report", + "suggestion2": "List questions to be answered", + "inputPlaceholder": "Message aletheia assistant", + "addFactCheckingReportButton": "Add fact-check in my report.", + "footer": "Aletheia assistant can make mistakes. Consider checking important information.", + "agentLoadingThoughts": "Thinking", + "rateQuestion": "How would you rate this conversation ?", + "copilotWarning": "Assistant visible only in full page." +} \ No newline at end of file diff --git a/public/locales/en/personality.json b/public/locales/en/personality.json index b311d459a..f5315eeb7 100644 --- a/public/locales/en/personality.json +++ b/public/locales/en/personality.json @@ -23,5 +23,8 @@ "hideError": "Error while hidden personality", "unhideSuccess": "Personality unhide succesfully", "unhideError": "Error while unhide personality", - "hiddenPersonalityAvatarTooltip": "Hidden personality" + "hiddenPersonalityAvatarTooltip": "Hidden personality", + "personalityCardWikidataTooltip": "Information about this personality is taken from Wikidata", + "claimListEmptyFallBackPersonality": "The personality does not have any claims created", + "claimListEmptyFallBack": "Create new claim" } diff --git a/public/locales/pt/claimReviewForm.json b/public/locales/pt/claimReviewForm.json index 85a1dd094..57a09a016 100644 --- a/public/locales/pt/claimReviewForm.json +++ b/public/locales/pt/claimReviewForm.json @@ -45,12 +45,5 @@ "rejectionCommentLabel": "Comentário de rejeição", "rejectionCommentPlaceholder": "Descreva o que precisa ser alterado", "loginButton": "Faça login para continuar", - "notReviewed": "Não revisado", - "addAgentReview": "Adicionar checagem por IA", - "agentInputText": "Faça a checagem da seguinte afirmação: {{sentence}} forneça a resposta em português", - "addAgentReviewModalTitle": "Gerando relatório de checagem", - "agentLoadingThoughts": "Pensando...", - "agentFinishedThoughts": "Finalizado.", - "agentFinishedReport": "Relatório gerado", - "submitAgentReview": "Enviar relatório gerado" + "notReviewed": "Não revisado" } diff --git a/public/locales/pt/copilotChatBot.json b/public/locales/pt/copilotChatBot.json new file mode 100644 index 000000000..aa8e930ea --- /dev/null +++ b/public/locales/pt/copilotChatBot.json @@ -0,0 +1,14 @@ +{ + "chatBotGreetings": "Olá, eu sou checador de fatos assistente da Aletheia.", + "Assistant": "Assistente", + "You": "Você", + "suggestionHeader": "Como posso te ajudar ?", + "suggestion1": "Preciso de ajuda com minha checagem", + "suggestion2": "Liste perguntas a serem respondidas", + "inputPlaceholder": "Escreva para o assistente da aletheia", + "addFactCheckingReportButton": "Adicionar checagem no meu relatorio.", + "footer": "Assistente Aletheia pode cometer erros. Considere checar informações importantes.", + "agentLoadingThoughts": "Pensando", + "rateQuestion": "Como você avaliaria esta conversa ?", + "copilotWarning": "Assistente apenas disponivel na página completa." +} \ No newline at end of file diff --git a/public/locales/pt/personality.json b/public/locales/pt/personality.json index 003f1eb2e..bfb68d626 100644 --- a/public/locales/pt/personality.json +++ b/public/locales/pt/personality.json @@ -23,5 +23,8 @@ "hideError": "Erro ao esconder a personalidade", "unhideSuccess": "Personalidade pública com sucesso", "unhideError": "Erro ao mostrar a personalidade", - "hiddenPersonalityAvatarTooltip": "Personalidade oculta" + "hiddenPersonalityAvatarTooltip": "Personalidade oculta", + "personalityCardWikidataTooltip": "As informações dessa personalidade são retiradas da Wikidata", + "claimListEmptyFallBackPersonality": "A personalidade não possui nenhuma reivindicação criada", + "claimListEmptyFallBack": "Criar uma nova afirmação" } diff --git a/server/app.module.ts b/server/app.module.ts index ac7981e17..ecb2ec41e 100644 --- a/server/app.module.ts +++ b/server/app.module.ts @@ -52,6 +52,7 @@ import { CommentModule } from "./claim-review-task/comment/comment.module"; import { NameSpaceModule } from "./auth/name-space/name-space.module"; import { NameSpaceGuard } from "./auth/name-space/name-space.guard"; import { AutomatedFactCheckingModule } from "./automated-fact-checking/automated-fact-checking.module"; +import { CopilotChatModule } from "./copilot/copilot-chat.module"; @Module({}) export class AppModule implements NestModule { @@ -109,6 +110,7 @@ export class AppModule implements NestModule { CommentModule, NameSpaceModule, AutomatedFactCheckingModule, + CopilotChatModule, ]; if (options.feature_flag) { imports.push( diff --git a/server/auth/name-space/name-space.service.ts b/server/auth/name-space/name-space.service.ts index 83989d6c6..9a37ddee9 100644 --- a/server/auth/name-space/name-space.service.ts +++ b/server/auth/name-space/name-space.service.ts @@ -16,16 +16,7 @@ export class NameSpaceService { ) {} listAll() { - return this.NameSpaceModel.aggregate([ - { - $lookup: { - from: "users", - localField: "users", - foreignField: "_id", - as: "users", - }, - }, - ]); + return this.NameSpaceModel.find().populate("users"); } async create(nameSpace) { diff --git a/server/auth/session.guard.ts b/server/auth/session.guard.ts index 06322d311..227915bac 100644 --- a/server/auth/session.guard.ts +++ b/server/auth/session.guard.ts @@ -37,6 +37,8 @@ export class SessionGuard implements CanActivate { }); request.user = { _id: session?.identity?.traits?.user_id, + // Needed to enable feature flag for specific users + id: session?.identity?.traits?.user_id, role: session?.identity?.traits?.role, status: session?.identity.state, }; diff --git a/server/automated-fact-checking/automated-fact-checking.controller.ts b/server/automated-fact-checking/automated-fact-checking.controller.ts index 3edee2a3b..8e61e4e59 100644 --- a/server/automated-fact-checking/automated-fact-checking.controller.ts +++ b/server/automated-fact-checking/automated-fact-checking.controller.ts @@ -12,9 +12,10 @@ export class AutomatedFactCheckingController { @ApiTags("automated-fact-checking") @Post("api/ai-fact-checking") @Header("Cache-Control", "no-cache") - async create(@Body() { sentence }: CreateAutomatedFactCheckingDTO) { - return this.automatedFactCheckingService.getResponseFromAgents( - sentence - ); + async create(@Body() { claim, context }: CreateAutomatedFactCheckingDTO) { + return this.automatedFactCheckingService.getResponseFromAgents({ + claim, + context, + }); } } diff --git a/server/automated-fact-checking/automated-fact-checking.service.ts b/server/automated-fact-checking/automated-fact-checking.service.ts index 0c009cf5b..5490152de 100644 --- a/server/automated-fact-checking/automated-fact-checking.service.ts +++ b/server/automated-fact-checking/automated-fact-checking.service.ts @@ -3,70 +3,55 @@ import { ConfigService } from "@nestjs/config"; @Injectable({ scope: Scope.REQUEST }) export class AutomatedFactCheckingService { - constructor(private configService: ConfigService) {} + agenciaURL: string; - async getResponseFromAgents(sentence: string = ""): Promise { - try { - const url = `${this.configService.get("agentsUrl")}/stream`; - const params = { - input: { - messages: [ - { - content: sentence, - type: "human", - role: "human", - }, - ], - sender: "Supervisor", - }, - }; + constructor(private configService: ConfigService) { + this.agenciaURL = this.configService.get( + "automatedFactCheckingAPIUrl" + ); + } - const response = await fetch(url, { - method: "POST", - body: JSON.stringify(params), - headers: { - "Content-Type": "application/json", - }, + async getResponseFromAgents(data): Promise<{ stream: string; json: any }> { + console.log(this.agenciaURL); + const params = { + input: { + claim: data.claim, + context: data.context, + language: "Portuguese", + messages: [], + questions: [], + can_be_fact_checked: false, + }, + }; + const response = await fetch(`${this.agenciaURL}/stream`, { + method: "POST", + body: JSON.stringify(params), + headers: { + "Content-Type": "application/json", + }, + keepalive: true, + }); + + let reader = response.body.getReader(); + + let streamResponse = ""; + let done, value; + + while (!done) { + ({ done, value } = await reader.read()); + streamResponse += new TextDecoder().decode(value, { + stream: true, }); + } - let reader = response.body.getReader(); - - let streamResponse = ""; - let done, value; - - while (!done) { - ({ done, value } = await reader.read()); - streamResponse += new TextDecoder().decode(value, { - stream: true, - }); - } - - const jsonEvents = streamResponse - .split("\n") - .filter((line) => line.startsWith("data:")) - .map((line) => JSON.parse(line.substring(5))) - .reduce((acc, data) => ({ ...acc, ...data }), {}); + const jsonEvents = streamResponse + .split("\n") + .filter((line) => line.startsWith("data:")) + .map((line) => JSON.parse(line.substring(5))) + .reduce((acc, data) => ({ ...acc, ...data }), {}); - jsonEvents.__end__.messages = this.convertMessageContentsToJSON( - jsonEvents.__end__.messages - ); - return jsonEvents.__end__; - } catch (error) { - return { - error: "Error in data fetching process", - }; - } - } + jsonEvents.__end__.messages = JSON.parse(jsonEvents.__end__.messages); - convertMessageContentsToJSON(messages) { - return messages - .map(({ content }) => { - try { - return JSON.parse(content); - } catch (error) { - return undefined; - } - }) - .filter((message) => message !== undefined); + return { stream: streamResponse, json: jsonEvents.__end__ }; } } diff --git a/server/automated-fact-checking/dto/create-automated-fact-checking.dto.ts b/server/automated-fact-checking/dto/create-automated-fact-checking.dto.ts index 5ce07a11b..9f6508414 100644 --- a/server/automated-fact-checking/dto/create-automated-fact-checking.dto.ts +++ b/server/automated-fact-checking/dto/create-automated-fact-checking.dto.ts @@ -1,6 +1,9 @@ -import { IsString } from "class-validator"; +import { IsObject, IsString } from "class-validator"; export class CreateAutomatedFactCheckingDTO { @IsString() - sentence: string; + claim: string; + + @IsObject() + context: object; } diff --git a/server/claim-review-task/claim-review-task.controller.ts b/server/claim-review-task/claim-review-task.controller.ts index ce9efd7d5..edc2bf9fc 100644 --- a/server/claim-review-task/claim-review-task.controller.ts +++ b/server/claim-review-task/claim-review-task.controller.ts @@ -163,7 +163,7 @@ export class ClaimReviewController { public async personalityList(@Req() req: Request, @Res() res: Response) { const parsedUrl = parse(req.url, true); const enableCollaborativeEditor = this.isEnableCollaborativeEditor(); - const enableAgentReview = this.isEnableAgentReview(); + const enableCopilotChatBot = this.isEnableCopilotChatBot(); const enableEditorAnnotations = this.isEnableEditorAnnotations(); await this.viewService.getNextServer().render( @@ -174,7 +174,7 @@ export class ClaimReviewController { sitekey: this.configService.get("recaptcha_sitekey"), enableCollaborativeEditor, enableEditorAnnotations, - enableAgentReview, + enableCopilotChatBot, websocketUrl: this.configService.get("websocketUrl"), nameSpace: req.params.namespace, }) @@ -189,10 +189,10 @@ export class ClaimReviewController { : false; } - private isEnableAgentReview() { + private isEnableCopilotChatBot() { const config = this.configService.get("feature_flag"); - return config ? this.unleash.isEnabled("agent_review") : false; + return config ? this.unleash.isEnabled("copilot_chat_bot") : false; } private isEnableEditorAnnotations() { diff --git a/server/claim-review/claim-review.controller.ts b/server/claim-review/claim-review.controller.ts index ab76d281e..8bcf8b168 100644 --- a/server/claim-review/claim-review.controller.ts +++ b/server/claim-review/claim-review.controller.ts @@ -101,6 +101,7 @@ export class ClaimReviewController { const review = await this.claimReviewService.getReviewByDataHash( data_hash ); + const descriptionForHide = await this.historyService.getDescriptionForHide( review, diff --git a/server/claim-review/claim-review.service.ts b/server/claim-review/claim-review.service.ts index 1b9a7c492..b29da36df 100644 --- a/server/claim-review/claim-review.service.ts +++ b/server/claim-review/claim-review.service.ts @@ -17,7 +17,6 @@ import { ImageService } from "../claim/types/image/image.service"; import { ContentModelEnum } from "../types/enums"; import lookUpPersonalityties from "../mongo-pipelines/lookUpPersonalityties"; import lookupClaims from "../mongo-pipelines/lookupClaims"; -import lookupClaimRevisions from "../mongo-pipelines/lookupClaimRevisions"; import { NameSpaceEnum } from "../auth/name-space/schemas/name-space.schema"; import { EditorParseService } from "../editor-parse/editor-parse.service"; @@ -36,89 +35,69 @@ export class ClaimReviewService { ) {} async listAll(page, pageSize, order, query, latest = false) { - const aggregation = []; - - aggregation.push( - { - $sort: latest - ? { date: -1 } - : { _id: order === "asc" ? 1 : -1 }, - }, - { $match: query }, - lookUpPersonalityties(TargetModel.ClaimReview), - lookupClaims(TargetModel.ClaimReview), - { $unwind: "$claim" }, - lookupClaimRevisions(TargetModel.ClaimReview), - { $unwind: "$claim.latestRevision" }, - { - $match: { - "claim.isHidden": query.isHidden, - "claim.isDeleted": false, - "claim.nameSpace": + const pipeline = this.ClaimReviewModel.find(query) + .sort(latest ? { date: -1 } : { _id: order === "asc" ? 1 : -1 }) + .populate({ + path: "claim", + model: "Claim", + populate: { + path: "latestRevision", + model: "ClaimRevision", + }, + match: { + isDeleted: false, + nameSpace: this.req.params.namespace || this.req.query.nameSpace || NameSpaceEnum.Main, - "personality.isDeleted": false, }, - } - ); + }) + .skip(page * pageSize) + .limit(parseInt(pageSize)); + + const personalityPopulateOptions: any = { + path: "personality", + model: "Personality", + match: { + isDeleted: false, + }, + }; if (!query.isHidden) { - aggregation.push( - { - $unwind: { - path: "$personality", - preserveNullAndEmptyArrays: true, - includeArrayIndex: "arrayIndex", - }, - }, - { - $match: { - $or: [ - { personality: { $exists: false } }, - { "personality.isHidden": { $ne: true } }, - ], - }, - } - ); + personalityPopulateOptions.match = { + $or: [ + { personality: { $exists: false } }, + { isHidden: { $ne: true } }, + ], + }; } - aggregation.push( - { $skip: page * pageSize }, - { $limit: parseInt(pageSize) } - ); + const claimReviews = await pipeline + .populate(personalityPopulateOptions) + .exec(); - const claimReviews = await this.ClaimReviewModel.aggregate(aggregation); + const filteredClaimReviews = claimReviews.filter( + (review) => review.claim + ); return Promise.all( - claimReviews.map(async (review) => this.postProcess(review)) + filteredClaimReviews.map(async (review) => this.postProcess(review)) ); } - agreggateClassification(match: any) { + async agreggateClassification(match: any) { const nameSpace = this.req.params.namespace || this.req.query.nameSpace; - return this.ClaimReviewModel.aggregate([ - { $match: match }, - { - $lookup: { - from: "reports", - localField: "report", - foreignField: "_id", - as: "report", - }, + const claimReviews = await this.ClaimReviewModel.find(match).populate({ + path: "claim", + model: "Claim", + match: { + "claim.isHidden": false, + "claim.nameSpace": nameSpace || NameSpaceEnum.Main, }, - lookupClaims(TargetModel.ClaimReview), - { - $match: { - "claim.isHidden": false, - "claim.isDeleted": false, - "claim.nameSpace": nameSpace || NameSpaceEnum.Main, - }, - }, - { $group: { _id: "$report.classification", count: { $sum: 1 } } }, - { $sort: { count: -1 } }, - ]); + }); + + return this.sortReviewStats(claimReviews); } async count(query: any = {}) { @@ -160,27 +139,14 @@ export class ClaimReviewService { } async getReviewStatsByClaimId(claimId) { - const reviews = await this.ClaimReviewModel.aggregate([ - { - $match: { - claim: claimId, - isDeleted: false, - isPublished: true, - isHidden: false, - }, - }, - { - $lookup: { - from: "reports", - localField: "report", - foreignField: "_id", - as: "report", - }, - }, - { $group: { _id: "$report.classification", count: { $sum: 1 } } }, - { $sort: { count: -1 } }, - ]); - return this.util.formatStats(reviews); + const reviews = await this.ClaimReviewModel.find({ + claim: claimId, + isDeleted: false, + isPublished: true, + isHidden: false, + }); + const sortedReviews = this.sortReviewStats(reviews); + return this.util.formatStats(sortedReviews); } /** @@ -188,34 +154,30 @@ export class ClaimReviewService { * @param claimId claim Id * @returns */ - getReviewsByClaimId(claimId) { - return this.ClaimReviewModel.aggregate([ - { - $match: { - claim: claimId, - isDeleted: false, - isPublished: true, - isHidden: false, - }, - }, - { - $lookup: { - from: "reports", - localField: "report", - foreignField: "_id", - as: "report", - }, - }, - { - $group: { - _id: { - data_hash: "$data_hash", - classification: "$report.classification", - }, - count: { $sum: 1 }, - }, - }, - ]).option({ serializeFunctions: true }); + async getReviewsByClaimId(claimId) { + const classificationCounts = {}; + const claimReviews = await this.ClaimReviewModel.find({ + claim: claimId, + isDeleted: false, + isPublished: true, + isHidden: false, + }); + + claimReviews.forEach((review) => { + const key = JSON.stringify({ + data_hash: review.data_hash, + classification: review.report.classification, + }); + classificationCounts[key] = (classificationCounts[key] || 0) + 1; + }); + + return Object.entries(classificationCounts).map(([key, count]) => { + const { data_hash, classification } = JSON.parse(key); + return { + _id: { data_hash, classification: [classification] }, + count: count as number, + }; + }); } /** @@ -284,9 +246,7 @@ export class ClaimReviewService { } async getReport(match): Promise> { - const claimReview = await this.ClaimReviewModel.findOne(match) - .populate("report") - .lean(); + const claimReview = await this.ClaimReviewModel.findOne(match).lean(); return claimReview.report; } @@ -389,6 +349,20 @@ export class ClaimReviewService { }; } + private sortReviewStats(claimReviews) { + const classificationCounts = claimReviews.reduce((acc, review) => { + const classification = review.report.classification; + acc[classification] = (acc[classification] || 0) + 1; + return acc; + }, {}); + + return Object.entries(classificationCounts) + .sort((a, b) => (b[1] as number) - (a[1] as number)) + .map(([classification, count]) => ({ + _id: [classification], + count: count as number, + })); + } getHtmlFromSchema(schema) { const htmlContent = this.editorParseService.schema2html(schema); return { diff --git a/server/claim-review/schemas/claim-review.schema.ts b/server/claim-review/schemas/claim-review.schema.ts index 2e540a9e9..be0e3288a 100644 --- a/server/claim-review/schemas/claim-review.schema.ts +++ b/server/claim-review/schemas/claim-review.schema.ts @@ -56,6 +56,10 @@ export class ClaimReview { const ClaimReviewSchemaRaw = SchemaFactory.createForClass(ClaimReview); +ClaimReviewSchemaRaw.pre("find", function () { + this.populate("report"); +}); + ClaimReviewSchemaRaw.virtual("sources", { ref: "Source", localField: "_id", diff --git a/server/claim/claim.controller.ts b/server/claim/claim.controller.ts index ee25868e6..85c69852f 100644 --- a/server/claim/claim.controller.ts +++ b/server/claim/claim.controller.ts @@ -322,7 +322,7 @@ export class ClaimController { ); const enableCollaborativeEditor = this.isEnableCollaborativeEditor(); - const enableAgentReview = this.isEnableAgentReview(); + const enableCopilotChatBot = this.isEnableCopilotChatBot(); const enableEditorAnnotations = this.isEnableEditorAnnotations(); hideDescriptions[TargetModel.Claim] = @@ -353,7 +353,7 @@ export class ClaimController { hideDescriptions, enableCollaborativeEditor, enableEditorAnnotations, - enableAgentReview, + enableCopilotChatBot, websocketUrl: this.configService.get("websocketUrl"), nameSpace: req.params.namespace, }) @@ -534,7 +534,7 @@ export class ClaimController { namespace as NameSpaceEnum ); const enableCollaborativeEditor = this.isEnableCollaborativeEditor(); - const enableAgentReview = this.isEnableAgentReview(); + const enableCopilotChatBot = this.isEnableCopilotChatBot(); const enableEditorAnnotations = this.isEnableEditorAnnotations(); if (claim.personalities.length > 0) { @@ -557,7 +557,7 @@ export class ClaimController { sitekey: this.configService.get("recaptcha_sitekey"), enableCollaborativeEditor, enableEditorAnnotations, - enableAgentReview, + enableCopilotChatBot, websocketUrl: this.configService.get("websocketUrl"), nameSpace: namespace, }) @@ -577,7 +577,7 @@ export class ClaimController { const parsedUrl = parse(req.url, true); const enableCollaborativeEditor = this.isEnableCollaborativeEditor(); - const enableAgentReview = this.isEnableAgentReview(); + const enableCopilotChatBot = this.isEnableCopilotChatBot(); const enableEditorAnnotations = this.isEnableEditorAnnotations(); const personality = @@ -610,7 +610,7 @@ export class ClaimController { sitekey: this.configService.get("recaptcha_sitekey"), enableCollaborativeEditor, enableEditorAnnotations, - enableAgentReview, + enableCopilotChatBot, websocketUrl: this.configService.get("websocketUrl"), hideDescriptions, nameSpace: namespace, @@ -637,7 +637,7 @@ export class ClaimController { ); const enableCollaborativeEditor = this.isEnableCollaborativeEditor(); - const enableAgentReview = this.isEnableAgentReview(); + const enableCopilotChatBot = this.isEnableCopilotChatBot(); const enableEditorAnnotations = this.isEnableEditorAnnotations(); const claim = await this.claimService.getByPersonalityIdAndClaimSlug( @@ -655,7 +655,7 @@ export class ClaimController { claim, enableCollaborativeEditor, enableEditorAnnotations, - enableAgentReview, + enableCopilotChatBot: enableCopilotChatBot, websocketUrl: this.configService.get("websocketUrl"), nameSpace: namespace, }) @@ -797,10 +797,10 @@ export class ClaimController { : false; } - private isEnableAgentReview() { + private isEnableCopilotChatBot() { const config = this.configService.get("feature_flag"); - return config ? this.unleash.isEnabled("agent_review") : false; + return config ? this.unleash.isEnabled("copilot_chat_bot") : false; } private isEnableEditorAnnotations() { diff --git a/server/copilot/copilot-chat.controller.ts b/server/copilot/copilot-chat.controller.ts new file mode 100644 index 000000000..6afef72ac --- /dev/null +++ b/server/copilot/copilot-chat.controller.ts @@ -0,0 +1,49 @@ +/** + * Controller for Langchain Chat operations. + * + * Handles HTTP requests for context-aware chat interactions + * in the Langchain application. This controller is responsible for + * validating incoming request data and orchestrating chat interactions through the LangchainChatService. + * It supports endpoints for initiating context-aware chat + * and ensuring a versatile chat service experience. + * + * @class LangchainChatController + * + * @method contextAwareChat - Initiates a context-aware chat interaction. Accepts POST requests with a ContextAwareMessagesDto to manage chat context. + * Leverages LangchainChatService for processing. + * @param {ContextAwareMessagesDto} contextAwareMessagesDto - DTO for managing chat context. + * @returns Contextual chat response from the LangchainChatService. + * + * This controller uses decorators to define routes and their configurations, ensuring proper request handling and response formatting. It also integrates file upload handling for PDF documents, enabling document-context chat functionalities. + */ + +import { Body, Controller, Post, Req, UseGuards } from "@nestjs/common"; +import { CopilotChatService } from "./copilot-chat.service"; +import { ContextAwareMessagesDto } from "./dtos/context-aware-messages.dto"; +import { + CheckAbilities, + FactCheckerUserAbility, +} from "../auth/ability/ability.decorator"; +import { AbilitiesGuard } from "../auth/ability/abilities.guard"; + +@Controller() +export class CopilotChatController { + constructor(private readonly copilotChatService: CopilotChatService) {} + + @Post("api/agent-chat") + @UseGuards(AbilitiesGuard) + @CheckAbilities(new FactCheckerUserAbility()) + async agentChat( + @Body() contextAwareMessagesDto: ContextAwareMessagesDto, + @Req() req + ) { + try { + return await this.copilotChatService.agentChat( + contextAwareMessagesDto, + req.language + ); + } catch (e) { + throw new Error(e); + } + } +} diff --git a/server/copilot/copilot-chat.module.ts b/server/copilot/copilot-chat.module.ts new file mode 100644 index 000000000..61e9246a2 --- /dev/null +++ b/server/copilot/copilot-chat.module.ts @@ -0,0 +1,19 @@ +import { Module } from "@nestjs/common"; +import { CopilotChatService } from "./copilot-chat.service"; +import { CopilotChatController } from "./copilot-chat.controller"; +import { AutomatedFactCheckingModule } from "../automated-fact-checking/automated-fact-checking.module"; +import { EditorParseModule } from "../editor-parse/editor-parse.module"; +import { AbilityModule } from "../auth/ability/ability.module"; +import { ConfigModule } from "@nestjs/config"; + +@Module({ + imports: [ + AutomatedFactCheckingModule, + EditorParseModule, + AbilityModule, + ConfigModule, + ], + controllers: [CopilotChatController], + providers: [CopilotChatService], +}) +export class CopilotChatModule {} diff --git a/server/copilot/copilot-chat.service.ts b/server/copilot/copilot-chat.service.ts new file mode 100644 index 000000000..c3b33f41f --- /dev/null +++ b/server/copilot/copilot-chat.service.ts @@ -0,0 +1,215 @@ +/** + * Service for handling Langchain Chat operations. + * + * This service facilitates various types of chat interactions using OpenAI's language models. + * It supports context-aware chat. + * Basic context-aware chat utilize pre-defined templates for processing user queries, + * + * @class CopilotChatService + * + * @method contextAwareChat - Processes messages with consideration for the context of previous interactions, using a context-aware template for coherent responses. Handles errors with HttpExceptions. + * @param {ContextAwareMessagesDto} contextAwareMessagesDto - Data Transfer Object containing the user’s current message and the chat history. + * @returns Contextually relevant response from the OpenAI model. + * + * The class utilizes several internal methods for operations such as loading chat chains, formatting messages, generating success responses, and handling exceptions. + * These methods interact with external libraries and services, including the OpenAI API, file system operations, and custom utilities for message formatting and response generation. + */ + +import { HttpException, HttpStatus, Injectable, Logger } from "@nestjs/common"; +import { ChatOpenAI } from "@langchain/openai"; +import customMessage from "./customMessage.response"; +import { MESSAGES } from "./messages.constants"; +import { openAI } from "./openAI.constants"; +import { + ContextAwareMessagesDto, + SenderEnum, +} from "./dtos/context-aware-messages.dto"; +import { z } from "zod"; + +import { DynamicStructuredTool } from "@langchain/core/tools"; +import { AgentExecutor, createOpenAIFunctionsAgent } from "langchain/agents"; +import { + ChatPromptTemplate, + MessagesPlaceholder, +} from "@langchain/core/prompts"; +import { HumanMessage, AIMessage } from "langchain/schema"; +import { AutomatedFactCheckingService } from "../automated-fact-checking/automated-fact-checking.service"; +import { EditorParseService } from "../editor-parse/editor-parse.service"; + +@Injectable() +export class CopilotChatService { + private readonly logger = new Logger("CopilotChatService"); + constructor( + private automatedFactCheckingService: AutomatedFactCheckingService, + private editorParseService: EditorParseService + ) {} + editorReport = null; + + getFactCheckingReportTool = { + name: "get-fact-checking-report", + description: + "Use this tool to provide the information to the automated fact checking agents", + schema: z.object({ + claim: z.string().describe("the claim provided"), + context: z.object({ + //Bad behavior: When the user do not pass a value, the agent assumes the value from the date context + published_since: z + .string() + .describe( + "the oldest date provided specifically and just by the user" + ), + published_until: z + .string() + .describe( + "the newest date provided or if it's not provided the date that the claim was stated" + ), + city: z + .string() + .describe( + "the city location provided specifically and just by the user" + ), + sources: z + .array(z.string()) + .describe( + "the suggested sources as an array provided specifically and just by the user" + ), + }), + }), + func: async (data) => { + try { + const { stream, json } = + await this.automatedFactCheckingService.getResponseFromAgents( + data + ); + this.editorReport = await this.editorParseService.schema2editor( + { + ...json.messages, + sources: [], + } + ); + return stream; + } catch (e) { + console.log(e); + this.logger.error(e); + throw new Error(e); + } + }, + }; + + async agentChat( + contextAwareMessagesDto: ContextAwareMessagesDto, + language + ) { + try { + const date = new Date(contextAwareMessagesDto.context.claimDate); + const localizedDate = date.toLocaleDateString(); + language = language === "pt" ? "Portuguese" : "English"; + const messagesHistory = contextAwareMessagesDto.messages.map( + (message) => this.transformMessage(message) + ); + const tools = [ + new DynamicStructuredTool(this.getFactCheckingReportTool), + ]; + const currentMessageContent = + contextAwareMessagesDto.messages[ + contextAwareMessagesDto.messages.length - 1 + ]; + + const prompt = ChatPromptTemplate.fromMessages([ + //TODO: Ensure that the agent only fack-checks the claim from the context + [ + "system", + ` + You are helpful assistant, your objective is to gather relevant informations from the user about the {claim} that has to be fact-checked. + + A fact-checker is interacting with you because he needs assistance with his fact-check report. + + Follow these steps carefully: + + 1. Confirm with the user the claim to be fack-checked: + - If the user requests assistance with the fact-check, ask the user to confirm the claim that he wants to review is the claim:{claim} stated by {personality}, assure to always compose this specific question using these values {claim} and {personality}. + + 2. Analyze the {claim}: + - Based on your analyze assure if the claim is related to Brazilian municipalities or states proceed with the following questions strictly one at a time: + - Ask the user Which Brazilian city or state was the claim made in? + - Ask: Do you have any time expecific time period we should search in the public gazettes? (e.g., January 2022 to December 2022), or we should search until statement of the claim date:{date}? + + - Based on your analyze if the claim is unrelated to Brazilian municipalities or is a totally different topic proceed with the following question: + - Ask the user if he has any suggestion of sources that we should consult? + + If any information is ambiguous or missing, request clarification from the user without making assumptions. + Always ask one question at a time and in the specified order. + + You need to make all possible questions even if the user tries to rush the review. + Compose your responses using formal language and you MUST provide you answer in {language}. + Always pass the {claim} specifically to the tool without translating. + Only when you have made all questions and followed all steps to extracted information from the user then proceed to use the get-fact-checking-report tool, Do not proceed until you have asked all the questions. + `, + ], + new MessagesPlaceholder({ variableName: "chat_history" }), + ["user", "{input}"], + new MessagesPlaceholder({ variableName: "agent_scratchpad" }), + ]); + + const llm = new ChatOpenAI({ + temperature: +openAI.BASIC_CHAT_OPENAI_TEMPERATURE, + modelName: openAI.GPT_3_5_TURBO_1106.toString(), + }); + + const agent = await createOpenAIFunctionsAgent({ + llm, + tools, + prompt, + }); + + const agentExecutor = new AgentExecutor({ + agent, + tools, + }); + + const response = await agentExecutor.invoke({ + language: language, + date: localizedDate, + claim: contextAwareMessagesDto.context.sentence, + personality: contextAwareMessagesDto.context.personalityName, + input: currentMessageContent, + chat_history: messagesHistory, + }); + + return customMessage(HttpStatus.OK, MESSAGES.SUCCESS, { + sender: SenderEnum.Assistant, + content: response.output, + editorReport: this.editorReport, + }); + } catch (e: unknown) { + this.exceptionHandling(e); + } + } + + transformMessage(message) { + this.logger.log(`${message.sender}: ${message.content}`); + if (message.sender === SenderEnum.Assistant) { + return new AIMessage({ + content: message.content, + additional_kwargs: {}, + }); + } + + return new HumanMessage({ + content: message.content, + additional_kwargs: {}, + }); + } + + private exceptionHandling = (e: unknown) => { + console.log(e); + this.logger.error(e); + throw new HttpException( + customMessage( + HttpStatus.INTERNAL_SERVER_ERROR, + MESSAGES.EXTERNAL_SERVER_ERROR + ), + HttpStatus.INTERNAL_SERVER_ERROR + ); + }; +} diff --git a/server/copilot/customMessage.response.ts b/server/copilot/customMessage.response.ts new file mode 100644 index 000000000..c018fbd30 --- /dev/null +++ b/server/copilot/customMessage.response.ts @@ -0,0 +1,8 @@ +function customMessage(statusCode: number, message: string, data = {}): object { + return { + statusCode: statusCode, + message: [message], + data: data, + }; +} +export default customMessage; diff --git a/server/copilot/dtos/context-aware-messages.dto.ts b/server/copilot/dtos/context-aware-messages.dto.ts new file mode 100644 index 000000000..68c6588f8 --- /dev/null +++ b/server/copilot/dtos/context-aware-messages.dto.ts @@ -0,0 +1,44 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { IsArray, IsObject } from "class-validator"; + +/** + * Data Transfer Object for an individual message. + * + * This class represents a single message, defining its structure and applying + * validation rules. It ensures that each message has a specified sender and content, + * both of which are non-empty strings. + * + * @class MessageDto + * + * @property sender - The sender associated with the message, e.g., 'You', 'Assistant'. + * It is validated to be a non-empty string. + * @property content - The actual content of the message. + * It is validated to be a non-empty string. + */ + +export enum SenderEnum { + User = "You", + Assistant = "Assistant", +} + +export type Context = { + claimDate: Date; + claimTitle: string; + personalityName: string; + sentence: string; +}; + +export type Message = { + sender: string; + content: string; +}; + +export class ContextAwareMessagesDto { + @ApiProperty() + @IsArray() + messages: Message[]; + + @ApiProperty() + @IsObject() + context: Context; +} diff --git a/server/copilot/messages.constants.ts b/server/copilot/messages.constants.ts new file mode 100644 index 000000000..99f568b1f --- /dev/null +++ b/server/copilot/messages.constants.ts @@ -0,0 +1,12 @@ +/** + * Enum for standardized message responses. + * + * MESSAGES.BAD_REQUEST - Used for indicating a bad or invalid request. + * MESSAGES.SUCCESS - Used to indicate successful completion of an operation. + * MESSAGES.EXTERNAL_SERVER_ERROR - Used to indicate that an error has occured on server end. + */ +export enum MESSAGES { + BAD_REQUEST = "bad request", + SUCCESS = "success", + EXTERNAL_SERVER_ERROR = "Something went wrong, please try again later", +} diff --git a/server/copilot/openAI.constants.ts b/server/copilot/openAI.constants.ts new file mode 100644 index 000000000..7436bba16 --- /dev/null +++ b/server/copilot/openAI.constants.ts @@ -0,0 +1,16 @@ +/** + * Enum for OpenAI configuration parameters. + * + * This enum stores specific configuration values related to OpenAI usage in the + * application, helping in maintaining consistency and ease of updates. + * + * @enum openAI + * + * @member GPT_3_5_TURBO_1106 - Identifier for the specific OpenAI model version used. + * @member BASIC_CHAT_OPENAI_TEMPERATURE - Controls the randomness in the model's responses, + * with a higher value resulting in more random responses. + */ +export enum openAI { + GPT_3_5_TURBO_1106 = "gpt-3.5-turbo-1106", + BASIC_CHAT_OPENAI_TEMPERATURE = "0", +} diff --git a/server/editor-parse/editor-parse.service.spec.ts b/server/editor-parse/editor-parse.service.spec.ts index 84d7cc367..aea204a74 100644 --- a/server/editor-parse/editor-parse.service.spec.ts +++ b/server/editor-parse/editor-parse.service.spec.ts @@ -9,7 +9,7 @@ describe("ParserService", () => { const schemaHtml = { questions: ["teste1", "testekakaka ava"], summary: "

teste4

", - report: ``, + report: `

duplicated word duplicated1

`, verification: "

teste3

", }; @@ -19,8 +19,8 @@ describe("ParserService", () => { href: "https://google.com", props: { field: "report", - textRange: [7, 21], - targetText: "esse e um link", + textRange: [16, 39], + targetText: "duplicated", sup: 1, id: "uniqueId", }, @@ -28,7 +28,7 @@ describe("ParserService", () => { ], questions: ["teste1", "testekakaka ava"], summary: "teste4", - report: "teste2 esse e um link", + report: "duplicated word {{uniqueId|duplicated}}", verification: "teste3", }; @@ -85,11 +85,11 @@ describe("ParserService", () => { content: [ { type: "text", - text: "teste2 ", + text: "duplicated word ", }, { type: "text", - text: "esse e um link", + text: "duplicated", marks: [ { type: "link", @@ -154,5 +154,20 @@ describe("ParserService", () => { ); expect(schemaHtmlResult).toMatchObject(schemaHtml); }); + + it("Source from Schema to HTML is parsed in the right position", async () => { + const schemaHtmlResult = await editorParseService.schema2html( + schemaContent + ); + expect(schemaHtmlResult.report).toEqual(schemaHtml.report); + }); + + it("Editor content is parsed correctly with markUp source in the right position", async () => { + const schemaResult = await editorParseService.editor2schema( + editorContent + ); + + expect(schemaResult.report).toEqual(schemaContent.report); + }); }); }); diff --git a/server/tests/claim-review.e2e.spec.ts b/server/tests/claim-review.e2e.spec.ts new file mode 100644 index 000000000..6bf45b180 --- /dev/null +++ b/server/tests/claim-review.e2e.spec.ts @@ -0,0 +1,183 @@ +import { MongoMemoryServer } from "mongodb-memory-server"; +import * as request from "supertest"; +import { Test, TestingModule } from "@nestjs/testing"; +import { AppModule } from "../app.module"; +import { SessionGuard } from "../auth/session.guard"; +import { SessionGuardMock } from "./mocks/SessionGuardMock"; +import { TestConfigOptions } from "./utils/TestConfigOptions"; +import { SeedTestUser } from "./utils/SeedTestUser"; +import { SeedTestPersonality } from "./utils/SeedTestPersonality"; +import { NameSpaceEnum } from "../auth/name-space/schemas/name-space.schema"; +import { AbilitiesGuard } from "../auth/ability/abilities.guard"; +import { AbilitiesGuardMock } from "./mocks/AbilitiesGuardMock"; +import { SeedTestClaimReview } from "./utils/SeedTestClaimReview"; +import { SeedTestReport } from "./utils/SeedTestReport"; +import { SeedTestSentence } from "./utils/SeedTestSentence"; +import { SeedTestParagraph } from "./utils/SeedTestParagraph"; +import { SeedTestSpeech } from "./utils/SeedTestSpeech"; +import { SeedTestClaimRevision } from "./utils/SeedTestClaimRevision"; +import { SeedTestClaim } from "./utils/SeedTestClaim"; +import { ValidationPipe } from "@nestjs/common"; +const ObjectId = require("mongodb").ObjectID; + +jest.setTimeout(10000); + +describe("ClaimReviewController (e2e)", () => { + let app: any; + let db: any; + let userId: string; + let personalitiesId: string[]; + let reportId: string; + let sentenceId: string; + let paragraphId: string; + let speecheId: string; + let claimId: string; + let claimReviewDataHash: string; + let claimReviewId: string; + + beforeAll(async () => { + db = await MongoMemoryServer.create({ instance: { port: 35025 } }); + const user = await SeedTestUser( + TestConfigOptions.config.db.connection_uri + ); + userId = user.insertedId; + const { insertedIds } = await SeedTestPersonality( + TestConfigOptions.config.db.connection_uri + ); + personalitiesId = [insertedIds["0"], insertedIds["1"]]; + + const report = await SeedTestReport( + TestConfigOptions.config.db.connection_uri, + userId + ); + reportId = report.insertedId; + const sentence = await SeedTestSentence( + TestConfigOptions.config.db.connection_uri + ); + sentenceId = sentence.insertedId; + const paragraph = await SeedTestParagraph( + TestConfigOptions.config.db.connection_uri, + sentenceId + ); + paragraphId = paragraph.insertedId; + const speeche = await SeedTestSpeech( + TestConfigOptions.config.db.connection_uri, + paragraphId + ); + speecheId = speeche.insertedId; + const claimRevisionId = new ObjectId(); + const claim = await SeedTestClaim( + TestConfigOptions.config.db.connection_uri, + personalitiesId, + claimRevisionId + ); + claimId = claim.insertedId; + + await SeedTestClaimRevision( + TestConfigOptions.config.db.connection_uri, + claimRevisionId, + personalitiesId, + claimId, + speecheId + ); + + await SeedTestClaimReview( + TestConfigOptions.config.db.connection_uri, + claimId, + personalitiesId, + reportId, + userId + ); + + const moduleFixture: TestingModule = await Test.createTestingModule({ + imports: [AppModule.register(TestConfigOptions.config)], + }) + .overrideProvider(SessionGuard) + .useValue(SessionGuardMock) + .overrideGuard(AbilitiesGuard) + .useValue(AbilitiesGuardMock) + .compile(); + + app = moduleFixture.createNestApplication(); + await app.init(); + app.useGlobalPipes( + new ValidationPipe({ + transform: true, + transformOptions: { enableImplicitConversion: true }, + whitelist: true, + forbidNonWhitelisted: true, + }) + ); + }); + + it("/api/review (GET) - List Reviews", () => { + return request(app.getHttpServer()) + .get("/api/review") + .query({ + page: 0, + pageSize: 10, + order: "asc", + isHidden: false, + latest: false, + nameSpace: NameSpaceEnum.Main, + }) + .expect(200) + .expect(({ body }) => { + claimReviewDataHash = body.reviews[0].content.data_hash; + expect(body.totalReviews).toEqual(1); + }); + }); + + it("api/review/:data_hash (GET) - Should get claimReview by dataHash", () => { + return request(app.getHttpServer()) + .get(`/api/review/${claimReviewDataHash}`) + .expect(200) + .expect(({ body }) => { + claimReviewId = body.review._id; + expect(body.review.claim).toEqual(claimId.toString()); + expect(body.review.personality).toEqual( + personalitiesId[0].toString() + ); + }); + }); + + it("api/review/:id (PUT) - Should hide claimReview", () => { + return request(app.getHttpServer()) + .put(`/api/review/${claimReviewId}`) + .send({ + isHidden: true, + description: "Hidden claimReview description", + }) + .expect(200); + }); + + it("api/review/:id (PUT) - Should unhide claimReview", () => { + return request(app.getHttpServer()) + .put(`/api/review/${claimReviewId}`) + .send({ + isHidden: false, + description: "", + }) + .expect(200); + }); + + it("api/review/:id (DELETE) - Should delete claimReview", () => { + return request(app.getHttpServer()) + .delete(`/api/review/${claimReviewId}`) + .expect(200); + }); + + it("api/review/:data_hash (GET) - Should not be able to get claimReview", () => { + return request(app.getHttpServer()) + .get(`/api/review/${claimReviewDataHash}`) + .expect(200) + .expect(({ body }) => { + expect(body.review).toEqual(null); + }); + }); + + afterAll(async () => { + await db.stop(); + app.close(); + }); +}); diff --git a/server/tests/utils/ClaimMock.ts b/server/tests/utils/ClaimMock.ts new file mode 100644 index 000000000..5dd186ec9 --- /dev/null +++ b/server/tests/utils/ClaimMock.ts @@ -0,0 +1,11 @@ +const ObjectId = require("mongodb").ObjectID; + +export const ClaimMock = (personalitiesId, claimRevisionId) => ({ + nameSpace: "main", + isHidden: false, + personalities: [ObjectId(personalitiesId[0])], + isDeleted: false, + deletedAt: null, + latestRevision: ObjectId(claimRevisionId), + slug: "test", +}); diff --git a/server/tests/utils/ClaimReviewMock.ts b/server/tests/utils/ClaimReviewMock.ts new file mode 100644 index 000000000..abc124031 --- /dev/null +++ b/server/tests/utils/ClaimReviewMock.ts @@ -0,0 +1,15 @@ +const ObjectId = require("mongodb").ObjectID; + +export const ReviewMock = (claimId, personalitiesId, reportId, userId) => ({ + isPartialReview: false, + isHidden: false, + isDeleted: false, + deletedAt: null, + personality: ObjectId(personalitiesId[0]), + claim: ObjectId(claimId), + usersId: [ObjectId(userId)], + report: ObjectId(reportId), + data_hash: "4be75d25957a3cc0dbc6975a6939a385", + date: "2024-04-18T17:32:36.769+00:00", + isPublished: true, +}); diff --git a/server/tests/utils/ClaimRevisionMock.ts b/server/tests/utils/ClaimRevisionMock.ts new file mode 100644 index 000000000..3c28e7de9 --- /dev/null +++ b/server/tests/utils/ClaimRevisionMock.ts @@ -0,0 +1,17 @@ +const ObjectId = require("mongodb").ObjectID; + +export const ClaimRevisionMock = ( + claimRevisionId, + personalitiesId, + claimId, + speecheId +) => ({ + _id: claimRevisionId, + personalities: [ObjectId(personalitiesId[0])], + contentModel: "Speech", + tittle: "Mock Tittle", + date: "2024-04-18T16:31:16.372+00:00", + claimId: ObjectId(claimId), + slug: "test", + contentId: ObjectId(speecheId), +}); diff --git a/server/tests/utils/ParagraphMock.ts b/server/tests/utils/ParagraphMock.ts new file mode 100644 index 000000000..736797fd8 --- /dev/null +++ b/server/tests/utils/ParagraphMock.ts @@ -0,0 +1,8 @@ +const ObjectId = require("mongodb").ObjectID; + +export const ParagraphMock = (sentenceId) => ({ + content: [ObjectId(sentenceId)], + type: "paragraph", + data_hash: "cc07fdd8165c15ef17b183a69e393318", + props: { id: 1 }, +}); diff --git a/server/tests/utils/ReportMock.ts b/server/tests/utils/ReportMock.ts new file mode 100644 index 000000000..1d0377abe --- /dev/null +++ b/server/tests/utils/ReportMock.ts @@ -0,0 +1,14 @@ +const ObjectId = require("mongodb").ObjectID; + +export const ReportMock = (userId) => ({ + sources: ["https://https://www.lipsum.com/"], + questions: ["Lorem Ipsum is simply dummy"], + usersId: [ObjectId(userId)], + summary: + "Lorem Ipsum is simply dummy text of the printing and typesetting industry.", + report: "Lorem Ipsum is simply dummy text of the printing and typesetting industry.", + verification: + "Lorem Ipsum is simply dummy text of the printing and typesetting industry.", + classification: "misleading", + data_hash: "4be75d25957a3cc0dbc6975a6939a385", +}); diff --git a/server/tests/utils/SeedTestClaim.ts b/server/tests/utils/SeedTestClaim.ts new file mode 100644 index 000000000..cd7fde218 --- /dev/null +++ b/server/tests/utils/SeedTestClaim.ts @@ -0,0 +1,16 @@ +import { MongoClient } from "mongodb"; +import { ClaimMock } from "./ClaimMock"; + +export const SeedTestClaim = async (uri, personalitiesId, claimRevisionId) => { + const client = await new MongoClient(uri); + await client.connect(); + + try { + return await client + .db("Aletheia") + .collection("claims") + .insertOne(ClaimMock(personalitiesId, claimRevisionId)); + } finally { + await client.close(); + } +}; diff --git a/server/tests/utils/SeedTestClaimReview.ts b/server/tests/utils/SeedTestClaimReview.ts new file mode 100644 index 000000000..4782c21e4 --- /dev/null +++ b/server/tests/utils/SeedTestClaimReview.ts @@ -0,0 +1,22 @@ +import { MongoClient } from "mongodb"; +import { ReviewMock } from "./ClaimReviewMock"; + +export const SeedTestClaimReview = async ( + uri, + claimId, + personalitiesId, + reportId, + userId +) => { + const client = await new MongoClient(uri); + await client.connect(); + + try { + return await client + .db("Aletheia") + .collection("claimreviews") + .insertOne(ReviewMock(claimId, personalitiesId, reportId, userId)); + } finally { + await client.close(); + } +}; diff --git a/server/tests/utils/SeedTestClaimRevision.ts b/server/tests/utils/SeedTestClaimRevision.ts new file mode 100644 index 000000000..54f0a5769 --- /dev/null +++ b/server/tests/utils/SeedTestClaimRevision.ts @@ -0,0 +1,29 @@ +import { MongoClient } from "mongodb"; +import { ClaimRevisionMock } from "./ClaimRevisionMock"; + +export const SeedTestClaimRevision = async ( + uri, + claimRevisionId, + personalitiesId, + claimId, + speecheId +) => { + const client = await new MongoClient(uri); + await client.connect(); + + try { + return await client + .db("Aletheia") + .collection("claimrevisions") + .insertOne( + ClaimRevisionMock( + claimRevisionId, + personalitiesId, + claimId, + speecheId + ) + ); + } finally { + await client.close(); + } +}; diff --git a/server/tests/utils/SeedTestParagraph.ts b/server/tests/utils/SeedTestParagraph.ts new file mode 100644 index 000000000..fbfcd299e --- /dev/null +++ b/server/tests/utils/SeedTestParagraph.ts @@ -0,0 +1,16 @@ +import { MongoClient } from "mongodb"; +import { ParagraphMock } from "./ParagraphMock"; + +export const SeedTestParagraph = async (uri, sentenceId) => { + const client = await new MongoClient(uri); + await client.connect(); + + try { + return await client + .db("Aletheia") + .collection("paragraphs") + .insertOne(ParagraphMock(sentenceId)); + } finally { + await client.close(); + } +}; diff --git a/server/tests/utils/SeedTestReport.ts b/server/tests/utils/SeedTestReport.ts new file mode 100644 index 000000000..392ee874a --- /dev/null +++ b/server/tests/utils/SeedTestReport.ts @@ -0,0 +1,16 @@ +import { MongoClient } from "mongodb"; +import { ReportMock } from "./ReportMock"; + +export const SeedTestReport = async (uri, userId) => { + const client = await new MongoClient(uri); + await client.connect(); + + try { + return await client + .db("Aletheia") + .collection("reports") + .insertOne(ReportMock(userId)); + } finally { + await client.close(); + } +}; diff --git a/server/tests/utils/SeedTestSentence.ts b/server/tests/utils/SeedTestSentence.ts new file mode 100644 index 000000000..f894edaea --- /dev/null +++ b/server/tests/utils/SeedTestSentence.ts @@ -0,0 +1,16 @@ +import { MongoClient } from "mongodb"; +import { SentenceMock } from "./SentenceMock"; + +export const SeedTestSentence = async (uri) => { + const client = await new MongoClient(uri); + await client.connect(); + + try { + return await client + .db("Aletheia") + .collection("sentences") + .insertOne(SentenceMock); + } finally { + await client.close(); + } +}; diff --git a/server/tests/utils/SeedTestSpeech.ts b/server/tests/utils/SeedTestSpeech.ts new file mode 100644 index 000000000..a592f7a46 --- /dev/null +++ b/server/tests/utils/SeedTestSpeech.ts @@ -0,0 +1,16 @@ +import { MongoClient } from "mongodb"; +import { SpeechMock } from "./SpeechMock"; + +export const SeedTestSpeech = async (uri, paragraphId) => { + const client = await new MongoClient(uri); + await client.connect(); + + try { + return await client + .db("Aletheia") + .collection("speeches") + .insertOne(SpeechMock(paragraphId)); + } finally { + await client.close(); + } +}; diff --git a/server/tests/utils/SentenceMock.ts b/server/tests/utils/SentenceMock.ts new file mode 100644 index 000000000..205c4f453 --- /dev/null +++ b/server/tests/utils/SentenceMock.ts @@ -0,0 +1,9 @@ +export const SentenceMock = { + topics: [], + type: "sentence", + data_hash: "4be75d25957a3cc0dbc6975a6939a385", + deletedAt: null, + props: { id: 1 }, + content: + "Lorem Ipsum is simply dummy text of the printing and typesetting industry.", +}; diff --git a/server/tests/utils/SpeechMock.ts b/server/tests/utils/SpeechMock.ts new file mode 100644 index 000000000..c881cc163 --- /dev/null +++ b/server/tests/utils/SpeechMock.ts @@ -0,0 +1,7 @@ +const ObjectId = require("mongodb").ObjectID; + +export const SpeechMock = (paragraphId) => ({ + content: [ObjectId(paragraphId)], + type: "speech", + personality: null, +}); diff --git a/server/tsconfig.json b/server/tsconfig.json index 744175c8b..23cb17bf9 100644 --- a/server/tsconfig.json +++ b/server/tsconfig.json @@ -16,6 +16,7 @@ }, "include": ["./**/*"], "exclude": [ - "node_modules" + "node_modules", + "./**/*.spec.ts", ] } diff --git a/src/api/claimReviewApi.ts b/src/api/claimReviewApi.ts index 324e695d5..faef3a9a4 100644 --- a/src/api/claimReviewApi.ts +++ b/src/api/claimReviewApi.ts @@ -23,7 +23,7 @@ const get = (options: FetchOptions = {}) => { pageSize: options.pageSize ? options.pageSize : 5, isHidden: options?.isHidden || false, latest: options?.latest, - nameSpace: options?.nameSpace, + nameSpace: options?.nameSpace || NameSpaceEnum.Main, }; return request diff --git a/src/api/copilotApi.ts b/src/api/copilotApi.ts new file mode 100644 index 000000000..cc090f729 --- /dev/null +++ b/src/api/copilotApi.ts @@ -0,0 +1,23 @@ +import axios from "axios"; + +const request = axios.create({ + withCredentials: true, + baseURL: `/api/agent-chat`, +}); + +const agentChat = (params) => { + return request + .post("/", params) + .then((response) => { + return response.data; + }) + .catch((err) => { + throw err; + }); +}; + +const copilotApi = { + agentChat, +}; + +export default copilotApi; diff --git a/src/components/AffixButton/AffixButton.tsx b/src/components/AffixButton/AffixButton.tsx index 15b1956e0..4fa03e36b 100644 --- a/src/components/AffixButton/AffixButton.tsx +++ b/src/components/AffixButton/AffixButton.tsx @@ -17,6 +17,7 @@ import PulseAnimation from "../PulseAnimation"; import Fab from "./Fab"; import { NameSpaceEnum } from "../../types/Namespace"; import { currentNameSpace } from "../../atoms/namespace"; +import { useAppSelector } from "../../store/store"; interface AffixButtonProps { personalitySlug?: string; } @@ -25,6 +26,13 @@ interface AffixButtonProps { * @param personalitySlug if present will display the Create Claim option too */ const AffixButton = ({ personalitySlug }: AffixButtonProps) => { + const { vw, copilotDrawerCollapsed } = useAppSelector((state) => ({ + vw: state?.vw, + copilotDrawerCollapsed: + state?.copilotDrawerCollapsed !== undefined + ? state?.copilotDrawerCollapsed + : true, + })); const [isLoggedIn] = useAtom(isUserLoggedIn); const [userRole] = useAtom(currentUserRole); const [nameSpace] = useAtom(currentNameSpace); @@ -89,7 +97,10 @@ const AffixButton = ({ personalitySlug }: AffixButtonProps) => { style={{ position: "fixed", bottom: "3%", - right: "2%", + right: + copilotDrawerCollapsed || vw?.md + ? "2%" + : `calc(2% + 350px)`, display: "flex", flexDirection: "column-reverse", alignItems: "center", diff --git a/src/components/AffixButton/AffixCTAButton.tsx b/src/components/AffixButton/AffixCTAButton.tsx index f0abe6c87..c21b14722 100644 --- a/src/components/AffixButton/AffixCTAButton.tsx +++ b/src/components/AffixButton/AffixCTAButton.tsx @@ -11,6 +11,7 @@ import { Tooltip } from "antd"; import colors from "../../styles/colors"; import { t } from "i18next"; import { trackUmamiEvent } from "../../lib/umami"; +import { useAppSelector } from "../../store/store"; const CloseIcon = () => { return ( @@ -32,9 +33,15 @@ const CloseIcon = () => { ); }; -const AffixCTAButton = () => { +const AffixCTAButton = ({ copilotDrawerWidth }) => { const { t } = useTranslation(); - + const { vw, copilotDrawerCollapsed } = useAppSelector((state) => ({ + vw: state?.vw, + copilotDrawerCollapsed: + state?.copilotDrawerCollapsed !== undefined + ? state?.copilotDrawerCollapsed + : true, + })); const [modalVisible, setModalVisible] = useState(false); const [CTABannerShow, setCTABannerShow] = useState(true); @@ -60,12 +67,15 @@ const AffixCTAButton = () => { style={{ position: "fixed", top: "15%", - right: "0%", + right: + copilotDrawerCollapsed || vw?.md + ? "0%" + : copilotDrawerWidth, display: "flex", flexDirection: "column-reverse", alignItems: "center", gap: "1rem", - zIndex: 9999, + zIndex: 999, }} > diff --git a/src/components/AgentReview/AgentReviewSteps.tsx b/src/components/AgentReview/AgentReviewSteps.tsx deleted file mode 100644 index b913c1f20..000000000 --- a/src/components/AgentReview/AgentReviewSteps.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import React from "react"; -import Step from "@mui/material/Step"; -import StepLabel from "@mui/material/StepLabel"; - -const AgentReviewStep = ({ - label, - stepIconComponent = null, - ...props -}: { - label: string; - stepIconComponent?: any; - key: string; - color?: string; - last?: boolean; - completed?: boolean; -}) => { - return ( - - {label} - - ); -}; - -export default AgentReviewStep; diff --git a/src/components/AgentReview/AutomatedFactCheckingSteps.tsx b/src/components/AgentReview/AutomatedFactCheckingSteps.tsx deleted file mode 100644 index b013c85b1..000000000 --- a/src/components/AgentReview/AutomatedFactCheckingSteps.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import React from "react"; -import { LoadingOutlined } from "@ant-design/icons"; -import Stepper from "@mui/material/Stepper"; -import { StepConnector } from "@mui/material"; -import { useTranslation } from "next-i18next"; -import AgentReviewStep from "./AgentReviewSteps"; - -const AutomatedFactCheckingSteps = ({ - steps, - activeStep, - isStepFinished, -}: { - steps: any; - activeStep: number; - isStepFinished: boolean; -}) => { - const { t } = useTranslation(); - - return ( - } - > - {!isStepFinished ? ( - - ) : ( - - )} - {steps.map((stepComponent, index) => - React.cloneElement(stepComponent, { - key: `step-${index}`, - index: index - 1, - }) - )} - - ); -}; - -export default AutomatedFactCheckingSteps; diff --git a/src/components/Claim/ClaimList.tsx b/src/components/Claim/ClaimList.tsx index a3c3d36c2..7c763401c 100644 --- a/src/components/Claim/ClaimList.tsx +++ b/src/components/Claim/ClaimList.tsx @@ -7,6 +7,7 @@ import ClaimSkeleton from "../Skeleton/ClaimSkeleton"; import ClaimCard from "./ClaimCard"; import { currentNameSpace } from "../../atoms/namespace"; import { useAtom } from "jotai"; +import ClaimListEmptyFallBack from "./ClaimListEmptyFallBack"; const ClaimList = ({ personality }) => { const { i18n, t } = useTranslation(); @@ -27,6 +28,7 @@ const ClaimList = ({ personality }) => { xl: 2, xxl: 2, }} + emptyFallback={} renderItem={(claim) => claim && ( { + const [userRole] = useAtom(currentUserRole); + const [nameSpace] = useAtom(currentNameSpace); + const { t } = useTranslation(); + const router = useRouter(); + + const hrefPersonalitySlug = personality.slug + ? `?personality=${personality.slug}` + : ""; + const handleClick = () => { + const href = + nameSpace !== NameSpaceEnum.Main + ? `/${nameSpace}/claim/create${hrefPersonalitySlug}` + : `/claim/create${hrefPersonalitySlug}`; + router.push(href); + }; + + return ( +
+ + + {personality.slug + ? t("personality:claimListEmptyFallBackPersonality") + : t("personality:claimListEmptyFallBack")} + + {userRole === Roles.Regular ? null : ( +
+ + {t("personality:claimListEmptyFallBack")} + +
+ )} +
+
+ ); +}; + +export default ClaimListEmptyFallBack; diff --git a/src/components/ClaimReview/ClaimReviewDrawer.tsx b/src/components/ClaimReview/ClaimReviewDrawer.tsx index eb06b6abe..0205f404b 100644 --- a/src/components/ClaimReview/ClaimReviewDrawer.tsx +++ b/src/components/ClaimReview/ClaimReviewDrawer.tsx @@ -1,4 +1,7 @@ -import { ArrowLeftOutlined } from "@ant-design/icons"; +import { + ArrowLeftOutlined, + ExclamationCircleOutlined, +} from "@ant-design/icons"; import { Col, Row } from "antd"; import { useTranslation } from "next-i18next"; import React, { useEffect, useState } from "react"; @@ -16,6 +19,7 @@ import { CollaborativeEditorProvider } from "../Collaborative/CollaborativeEdito import { NameSpaceEnum } from "../../types/Namespace"; import { useAtom } from "jotai"; import { currentNameSpace } from "../../atoms/namespace"; +import colors from "../../styles/colors"; const ClaimReviewDrawer = () => { const [isLoading, setIsLoading] = useState(true); @@ -29,6 +33,7 @@ const ClaimReviewDrawer = () => { claim, content, data_hash, + enableCopilotChatBot, } = useAppSelector((state) => { return { reviewDrawerCollapsed: @@ -40,6 +45,7 @@ const ClaimReviewDrawer = () => { claim: state?.selectedClaim, content: state?.selectedContent, data_hash: state?.selectedDataHash, + enableCopilotChatBot: state?.enableCopilotChatBot, }; }); @@ -72,11 +78,13 @@ const ClaimReviewDrawer = () => { - + } onClick={() => @@ -87,21 +95,47 @@ const ClaimReviewDrawer = () => { > {t("common:back_button")} + + setIsLoading(true)} + type={ButtonType.gray} + style={{ + textDecoration: "underline", + fontWeight: "bold", + }} + data-cy="testSeeFullReview" + > + {t("claimReviewTask:seeFullPage")} + + - - setIsLoading(true)} - type={ButtonType.gray} + {enableCopilotChatBot && ( + - {t("claimReviewTask:seeFullPage")} - - + + + {t("copilotChatBot:copilotWarning")} + + + )} ({ - enableAgentReview: state?.enableAgentReview, - })); const { machineService } = useContext(ReviewTaskMachineContext); const reviewData = useSelector(machineService, reviewDataSelector); const isReviewing = useSelector(machineService, reviewingSelector); const isUnassigned = useSelector(machineService, reviewNotStartedSelector); const userIsAssignee = reviewData.usersId.includes(userId); const [formCollapsed, setFormCollapsed] = useState(isUnassigned); - const [isModalVisible, setIsModalVisible] = useState(false); - const [recaptchaString, setRecaptchaString] = useState(""); - const hasCaptcha: boolean = !!recaptchaString; - const recaptchaRef = useRef(null); - const userIsAdmin = role === Roles.Admin || Roles.SuperAdmin; + const userIsAdmin = role === Roles.Admin || role === Roles.SuperAdmin; + const { enableCopilotChatBot, reviewDrawerCollapsed } = useAppSelector( + (state) => ({ + enableCopilotChatBot: state?.enableCopilotChatBot, + reviewDrawerCollapsed: + state?.reviewDrawerCollapsed !== undefined + ? state?.reviewDrawerCollapsed + : true, + }) + ); const showForm = isUnassigned || @@ -82,52 +83,14 @@ const ClaimReviewForm = ({ }} > {isLoggedIn && ( - } + data-cy={"testAddReviewButton"} > - - - {enableAgentReview && ( - - )} - - - + {t("claimReviewForm:addReviewButton")} + )} )} @@ -148,19 +111,13 @@ const ClaimReviewForm = ({ )} + {showForm && enableCopilotChatBot && reviewDrawerCollapsed && ( + + )} - - setIsModalVisible(false)} - claimId={claimId} - personalityId={personalityId} - dataHash={dataHash} - /> ); }; diff --git a/src/components/ClaimReview/ClaimReviewView.tsx b/src/components/ClaimReview/ClaimReviewView.tsx index 4a13856d9..d2e3b35cc 100644 --- a/src/components/ClaimReview/ClaimReviewView.tsx +++ b/src/components/ClaimReview/ClaimReviewView.tsx @@ -120,7 +120,7 @@ const ClaimReviewView = (props: ClaimReviewViewProps) => { {!review?.isPublished && ( { const { @@ -38,6 +41,7 @@ const DynamicReviewTaskForm = ({ data_hash, personality, claim }) => { formState: { errors }, watch, } = useForm(); + const dispatch = useDispatch(); const { machineService, events, form, setFormAndEvents } = useContext( ReviewTaskMachineContext ); @@ -48,7 +52,15 @@ const DynamicReviewTaskForm = ({ data_hash, personality, claim }) => { CollaborativeEditorContext ); const reviewData = useSelector(machineService, reviewDataSelector); - + const { enableCopilotChatBot, reviewDrawerCollapsed } = useAppSelector( + (state) => ({ + enableCopilotChatBot: state?.enableCopilotChatBot, + reviewDrawerCollapsed: + state?.reviewDrawerCollapsed !== undefined + ? state?.reviewDrawerCollapsed + : true, + }) + ); const { t } = useTranslation(); const [nameSpace] = useAtom(currentNameSpace); const [role] = useAtom(currentUserRole); @@ -75,6 +87,9 @@ const DynamicReviewTaskForm = ({ data_hash, personality, claim }) => { useEffect(() => { if (isLoggedIn) { setFormAndEvents(machineService.machine.config.initial); + if (enableCopilotChatBot && reviewDrawerCollapsed) { + dispatch(actions.openCopilotDrawer()); + } } }, [isLoggedIn]); diff --git a/src/components/Collaborative/CollaborativeEditor.tsx b/src/components/Collaborative/CollaborativeEditor.tsx index 16f97286e..b6f742cae 100644 --- a/src/components/Collaborative/CollaborativeEditor.tsx +++ b/src/components/Collaborative/CollaborativeEditor.tsx @@ -91,6 +91,8 @@ const CollaborativeEditor = ({ : (editorContentObject as RemirrorContentType), }); + const editorContentNode = manager.schema.nodeFromJSON(editorContentObject); + const handleChange = useCallback( ({ state }) => { onContentChange(state); @@ -110,7 +112,11 @@ const CollaborativeEditor = ({ editable={!readonly} > - + diff --git a/src/components/Collaborative/CollaborativeEditorProvider.tsx b/src/components/Collaborative/CollaborativeEditorProvider.tsx index 3c62fe38e..d64b6b645 100644 --- a/src/components/Collaborative/CollaborativeEditorProvider.tsx +++ b/src/components/Collaborative/CollaborativeEditorProvider.tsx @@ -15,6 +15,8 @@ interface ContextType { isFetchingEditor?: boolean; comments?: any[]; setComments?: (data: any) => void; + shouldInsertAIReport?: boolean; + setShouldInsertAIReport?: (data: any) => void; } export const CollaborativeEditorContext = createContext({ @@ -38,6 +40,7 @@ export const CollaborativeEditorProvider = ( const [isFetchingEditor, setIsFetchingEditor] = useState(false); const { websocketUrl } = useAppSelector((state) => state); const [comments, setComments] = useState(null); + const [shouldInsertAIReport, setShouldInsertAIReport] = useState(false); useEffect(() => { const fetchEditorContentObject = (data_hash) => { @@ -70,6 +73,8 @@ export const CollaborativeEditorProvider = ( isFetchingEditor, comments, setComments, + shouldInsertAIReport, + setShouldInsertAIReport, }} > {props.children} diff --git a/src/components/Collaborative/Editor.tsx b/src/components/Collaborative/Editor.tsx index 64e97b604..7f234cce0 100644 --- a/src/components/Collaborative/Editor.tsx +++ b/src/components/Collaborative/Editor.tsx @@ -14,6 +14,7 @@ import { Button } from "antd"; import EditorStyle from "./Editor.style"; import { CollaborativeEditorContext } from "./CollaborativeEditorProvider"; import useCardPresence from "./hooks/useCardPresence"; +import { Node } from "@remirror/pm/model"; /** * Modifies context state to JSON using useHelpers hook @@ -22,9 +23,21 @@ import useCardPresence from "./hooks/useCardPresence"; * to add a new input. * @param state remirror state */ -const Editor = ({ editable, state }: { editable: boolean; state: any }) => { +const Editor = ({ + node, + editable, + state, +}: { + node: Node; + editable: boolean; + state: any; +}) => { const command = useCommands(); - const { setEditorContentObject } = useContext(CollaborativeEditorContext); + const { + setEditorContentObject, + shouldInsertAIReport, + setShouldInsertAIReport, + } = useContext(CollaborativeEditorContext); const { getJSON } = useHelpers(); useEffect( @@ -32,6 +45,18 @@ const Editor = ({ editable, state }: { editable: boolean; state: any }) => { [state, getJSON, setEditorContentObject] ); + /** + * Deletes current report and insert automated fact-checking generated report. + * This solution is need because when we try to update the editorContentObject react state, the remirror state overrides the changes, not applying the generated report into the document. + */ + useEffect(() => { + if (shouldInsertAIReport) { + command.delete({ from: 0, to: state.doc.content.size }); + command.insertNode(node); + } + setShouldInsertAIReport(false); + }, [shouldInsertAIReport]); + const summaryDisabled = useCardPresence(getJSON, state, "summary", false); const reportDisabled = useCardPresence(getJSON, state, "report", false); const verificationDisabled = useCardPresence( diff --git a/src/components/Collaborative/hooks/useFloatingLinkState.tsx b/src/components/Collaborative/hooks/useFloatingLinkState.tsx index 81bc5ec80..e672ab412 100644 --- a/src/components/Collaborative/hooks/useFloatingLinkState.tsx +++ b/src/components/Collaborative/hooks/useFloatingLinkState.tsx @@ -35,6 +35,7 @@ function useFloatingLinkState() { const { from, to, empty, ranges } = useCurrentSelection(); const { $to } = ranges[0]; const url = (useAttrs().link()?.href as string) ?? "https://"; + const selectedSourceId = useAttrs().link()?.id; const [href, setHref] = useState(url); const isSelected = !empty; const updateReason = useUpdateReason(); @@ -121,14 +122,8 @@ function useFloatingLinkState() { setIsEditing, ]); - const areTextRangesEqual = (arr1, arr2) => { - for (let i = 0; i < arr1.length; i++) { - if (arr1[i] !== arr2[i]) { - return false; - } - } - return true; - }; + const areSourcesIdsEqual = (sourceId, selectedSourceId) => + sourceId === selectedSourceId; const onRemoveLink = useCallback(async () => { setIsLoading(true); @@ -139,7 +134,7 @@ function useFloatingLinkState() { }, ] = editorSources.filter((source) => { return ( - areTextRangesEqual(source.props.textRange, [from, to]) && + areSourcesIdsEqual(source.props.id, selectedSourceId) && source.props.targetText === targetText ); }); diff --git a/src/components/Copilot/CopilotCollapseDrawerButton.tsx b/src/components/Copilot/CopilotCollapseDrawerButton.tsx new file mode 100644 index 000000000..a571a236a --- /dev/null +++ b/src/components/Copilot/CopilotCollapseDrawerButton.tsx @@ -0,0 +1,49 @@ +import React from "react"; +import ArrowForwardIosIcon from "@mui/icons-material/ArrowForwardIos"; +import colors from "../../styles/colors"; +import { useAppSelector } from "../../store/store"; + +const CopilotCollapseDrawerButton = ({ + handleClick, + rightPosition, + topPosition, + rotate, +}) => { + const { vw } = useAppSelector((state) => ({ vw: state?.vw })); + + return ( +
+ +
+ ); +}; + +export default CopilotCollapseDrawerButton; diff --git a/src/components/Copilot/CopilotConversation.tsx b/src/components/Copilot/CopilotConversation.tsx new file mode 100644 index 000000000..bc0f758ed --- /dev/null +++ b/src/components/Copilot/CopilotConversation.tsx @@ -0,0 +1,92 @@ +import React, { useContext, useEffect, useRef, useState } from "react"; +import CopilotConversationCard from "./CopilotConversationCard"; +import CopilotConversationLoading from "./CopilotConversationLoading"; +import { SenderEnum } from "../../types/enums"; +import CopilotConversationSuggestions from "./CopilotConversationSuggestions"; +import AletheiaButton from "../Button"; +import { CollaborativeEditorContext } from "../Collaborative/CollaborativeEditorProvider"; +import { RemirrorContentType } from "remirror"; +import { useTranslation } from "next-i18next"; +import CopilotFeedback from "./CopilotFeedback"; + +const CopilotConversation = ({ + handleSendMessage, + isLoading, + messages, + editorReport, +}) => { + const { t } = useTranslation(); + const CopilotConversationRef = useRef(null); + const { setEditorContentObject, setShouldInsertAIReport } = useContext( + CollaborativeEditorContext + ); + const [showButtons, setShowButtons] = useState({ + ADD_REPORT: true, + ADD_RATE: false, + }); + const showSuggestions = messages.length <= 1; + const loadingMessage = { + sender: SenderEnum.Assistant, + content: , + }; + + const handleClick = (e) => + handleSendMessage({ + content: e.currentTarget.textContent, + sender: SenderEnum.User, + }); + + useEffect(() => { + if (CopilotConversationRef.current) { + CopilotConversationRef.current.scrollTo({ + top: 1e9, + behavior: "smooth", + }); + } + }, [messages, showButtons]); + + const handleAddReportClick = () => { + setShouldInsertAIReport(true); + setEditorContentObject(editorReport as RemirrorContentType); + setShowButtons({ + ADD_REPORT: false, + ADD_RATE: true, + }); + }; + + return ( +
+ {messages.map((message) => ( + + ))} + {showSuggestions && ( + + )} + {showButtons.ADD_REPORT && editorReport && ( +
+ + {t("copilotChatBot:addFactCheckingReportButton")} + +
+ )} + {showButtons.ADD_RATE && ( + + )} + {isLoading && } +
+ ); +}; + +export default CopilotConversation; diff --git a/src/components/Copilot/CopilotConversationCard.style.tsx b/src/components/Copilot/CopilotConversationCard.style.tsx new file mode 100644 index 000000000..aa8734af4 --- /dev/null +++ b/src/components/Copilot/CopilotConversationCard.style.tsx @@ -0,0 +1,40 @@ +import { Row } from "antd"; +import colors from "../../styles/colors"; +import styled from "styled-components"; + +const CopilotConversationCardStyle = styled(Row)` + display: flex; + flex-direction: column; + + .conversation-card-header { + display: flex; + align-items: center; + gap: 8px; + } + + .conversation-card-content { + position: relative; + margin: 12px 0 16px 0; + padding: 16px; + border-radius: 10px; + background-color: ${colors.white}; + marginleft: 40px; + wordbreak: break-word; + &:after { + border: 1px solid red; + width: 10px; + height: 10px; + content: " "; + position: absolute; + border-top: none; + border-right: 9px solid transparent; + border-left: 9px solid transparent; + border-bottom: 9px solid ${colors.white}; + left: 8px; + top: -8px; + transform: rotate(0deg); + } + } +`; + +export default CopilotConversationCardStyle; diff --git a/src/components/Copilot/CopilotConversationCard.tsx b/src/components/Copilot/CopilotConversationCard.tsx new file mode 100644 index 000000000..612dd3c10 --- /dev/null +++ b/src/components/Copilot/CopilotConversationCard.tsx @@ -0,0 +1,34 @@ +import React from "react"; +import { Avatar } from "antd"; +import colors from "../../styles/colors"; +import CopilotConversationCardStyle from "./CopilotConversationCard.style"; +import { SenderEnum } from "../../types/enums"; +import { useTranslation } from "next-i18next"; + +const CopilotConversationCard = ({ message }) => { + const { t } = useTranslation(); + const { sender, content } = message; + return ( + +
+ {sender === SenderEnum.Assistant ? ( + Aletheia assistant bot avatar + ) : ( + + {SenderEnum.User.slice(0, 1).toUpperCase()} + + )} + {t(`copilotChatBot:${sender}`)} +
+

{content}

+
+ ); +}; + +export default CopilotConversationCard; diff --git a/src/components/Copilot/CopilotConversationLoading.style.tsx b/src/components/Copilot/CopilotConversationLoading.style.tsx new file mode 100644 index 000000000..4ecefed26 --- /dev/null +++ b/src/components/Copilot/CopilotConversationLoading.style.tsx @@ -0,0 +1,47 @@ +import colors from "../../styles/colors"; +import styled from "styled-components"; + +const CopilotConversationLoadingStyle = styled.span` + display: flex; + gap: 8px; + + .loading { + font-size: 16px; + color: ${colors.bluePrimary}; + display: flex; + } + + .loading-text { + position: relative; + width: fit-content; + margin: 0; + } + + .loading-text::after { + content: ""; + display: inline-block; + animation: dots 2s steps(4) infinite; + position: absolute; + left: 100%; + } + + @keyframes dots { + 0% { + content: ""; + } + 25% { + content: "."; + } + 50% { + content: ".."; + } + 75% { + content: "..."; + } + 100% { + content: ""; + } + } +`; + +export default CopilotConversationLoadingStyle; diff --git a/src/components/Copilot/CopilotConversationLoading.tsx b/src/components/Copilot/CopilotConversationLoading.tsx new file mode 100644 index 000000000..1bc87b759 --- /dev/null +++ b/src/components/Copilot/CopilotConversationLoading.tsx @@ -0,0 +1,18 @@ +import React from "react"; +import { LoadingOutlined } from "@ant-design/icons"; +import CopilotConversationLoadingStyle from "./CopilotConversationLoading.style"; +import { useTranslation } from "next-i18next"; + +const CopilotConversationLoading = () => { + const { t } = useTranslation(); + return ( + + + + {t("copilotChatBot:agentLoadingThoughts")} + + + ); +}; + +export default CopilotConversationLoading; diff --git a/src/components/Copilot/CopilotConversationSuggestion.style.tsx b/src/components/Copilot/CopilotConversationSuggestion.style.tsx new file mode 100644 index 000000000..8c06386d7 --- /dev/null +++ b/src/components/Copilot/CopilotConversationSuggestion.style.tsx @@ -0,0 +1,31 @@ +import colors from "../../styles/colors"; +import styled from "styled-components"; + +const CopilotConversationSuggestionStyled = styled.div` + background: ${colors.white}; + display: flex; + flex-direction: column; + border-radius: 10px; + + .suggestions-header { + padding: 16px 8px; + text-align: center; + margin: 0; + font-size: 14px; + } + + .suggestion-card { + padding: 16px 8px; + cursor: pointer; + border: none; + border-top: 1px solid #ccc; + background: ${colors.white}; + border-radius: 0 0 10px 10px; + text-align: center; + color: ${colors.lightBlueMain}; + font-weight: bold; + font-size: 14px; + } +`; + +export default CopilotConversationSuggestionStyled; diff --git a/src/components/Copilot/CopilotConversationSuggestions.tsx b/src/components/Copilot/CopilotConversationSuggestions.tsx new file mode 100644 index 000000000..6a1e3e543 --- /dev/null +++ b/src/components/Copilot/CopilotConversationSuggestions.tsx @@ -0,0 +1,29 @@ +import React from "react"; +import CopilotConversationSuggestionStyled from "./CopilotConversationSuggestion.style"; +import { useTranslation } from "next-i18next"; + +const CopilotConversationSuggestions = ({ handleClick }) => { + const { t } = useTranslation(); + const suggestions = [{ content: t("copilotChatBot:suggestion1") }]; + + return ( + +

+ {t("copilotChatBot:suggestionHeader")} +

+ {suggestions + ? suggestions.map(({ content }) => ( + + )) + : {}} +
+ ); +}; + +export default CopilotConversationSuggestions; diff --git a/src/components/Copilot/CopilotDrawer.style.tsx b/src/components/Copilot/CopilotDrawer.style.tsx new file mode 100644 index 000000000..10c6bae7d --- /dev/null +++ b/src/components/Copilot/CopilotDrawer.style.tsx @@ -0,0 +1,47 @@ +import colors from "../../styles/colors"; +import styled from "styled-components"; +import Drawer from "@mui/material/Drawer"; + +const CopilotDrawerStyled = styled(Drawer)` + width: ${(props) => props.width}; + flex-shrink: 0; + zindex: 999999; + max-height: 100vh; + overflow: hidden; + + & .MuiDrawer-paper { + width: ${(props) => props.width}; + height: ${(props) => props.height}; + padding: 32px 16px 16px 16px; + background: ${colors.lightGraySecondary}; + } + + .footer { + font-size: 10px; + text-align: center; + margin-top: 8px; + } + + .copilot-form { + width: 100%; + display: flex; + align-items: flex-end; + padding: 8px; + gap: 8px; + border-radius: 4px; + } + + .submit-message-button { + width: 24px; + height: 44px; + padding: 0; + background: none; + border: none; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + } +`; + +export default CopilotDrawerStyled; diff --git a/src/components/Copilot/CopilotDrawer.tsx b/src/components/Copilot/CopilotDrawer.tsx new file mode 100644 index 000000000..0c29e67bb --- /dev/null +++ b/src/components/Copilot/CopilotDrawer.tsx @@ -0,0 +1,133 @@ +import React, { Suspense, useEffect, useMemo, useState } from "react"; +import { useAppSelector } from "../../store/store"; +import CopilotForm from "./CopilotForm"; +import { useTranslation } from "next-i18next"; +import CopilotDrawerStyled from "./CopilotDrawer.style"; +import copilotApi from "../../api/copilotApi"; +import { SenderEnum } from "../../types/enums"; +import CopilotCollapseDrawerButton from "./CopilotCollapseDrawerButton"; +import { Claim } from "../../types/Claim"; +import { Report } from "../../types/Report"; +import { ChatMessage, ChatResponse, MessageContext } from "../../types/Copilot"; +import { calculatePosition } from "./utils/calculatePositions"; +import Loading from "../Loading"; +const CopilotConversation = React.lazy(() => import("./CopilotConversation")); + +interface Size { + width: string; + height: string; +} + +interface CopilotDrawerProps { + claim: Claim; + sentence: string; +} + +const CopilotDrawer = ({ claim, sentence }: CopilotDrawerProps) => { + const { t } = useTranslation(); + const { vw, copilotDrawerCollapsed } = useAppSelector((state) => ({ + vw: state?.vw, + copilotDrawerCollapsed: + state?.copilotDrawerCollapsed !== undefined + ? state?.copilotDrawerCollapsed + : true, + })); + + const [open, setOpen] = useState(!copilotDrawerCollapsed); + const [isLoading, setIsLoading] = useState(false); + const [editorReport, setEditorReport] = useState(null); + const [size, setSize] = useState({ width: "350px", height: "100%" }); + const { topPosition, rightPosition, rotate } = useMemo( + () => calculatePosition(open, vw), + [open, vw.sm, vw.md] + ); + const [messages, setMessages] = useState([ + { + content: t("copilotChatBot:chatBotGreetings"), + sender: SenderEnum.Assistant, + }, + ]); + + const context: MessageContext = useMemo( + () => ({ + claimDate: claim.date, + sentence: sentence, + personalityName: claim.personalities[0].name, + claimTitle: claim.title, + }), + [claim, sentence] + ); + + const handleSendMessage = async ( + newChatMessage: ChatMessage + ): Promise => { + try { + setIsLoading(true); + addNewMessage(newChatMessage); + const { + data: { sender, content, editorReport }, + } = (await copilotApi.agentChat({ + messages: [...messages, newChatMessage], + context: context, + })) as { data: ChatResponse }; + setEditorReport(editorReport); + addNewMessage({ sender, content }); + } catch (e) { + console.error({ Error: e }); + } finally { + setIsLoading(false); + } + }; + + const addNewMessage = (newMessage) => + setMessages((prevMessages) => [...prevMessages, newMessage]); + + useEffect(() => { + if (open && vw?.sm) { + setSize({ width: "100%", height: "50%" }); + } else if (open) { + setSize({ width: "350px", height: "100%" }); + } + }, [open, vw?.sm]); + + useEffect(() => { + if (!copilotDrawerCollapsed) { + setOpen(true); + } + }, [vw?.md, copilotDrawerCollapsed]); + + return ( + <> + {vw?.md && ( + setOpen(!open)} + rightPosition={rightPosition} + rotate={rotate} + topPosition={topPosition} + aria-expanded={open} + /> + )} + + }> + + + + {t("copilotChatBot:footer")} + + + ); +}; + +export default CopilotDrawer; diff --git a/src/components/Copilot/CopilotFeedback.tsx b/src/components/Copilot/CopilotFeedback.tsx new file mode 100644 index 000000000..cb79df36d --- /dev/null +++ b/src/components/Copilot/CopilotFeedback.tsx @@ -0,0 +1,43 @@ +import React from "react"; +import { Rate } from "antd"; +import colors from "../../styles/colors"; +import { trackUmamiEvent } from "../../lib/umami"; +import { useTranslation } from "react-i18next"; + +const CopilotFeedback = ({ setShowButtons }) => { + const { t } = useTranslation(); + + const handleChange = (rate) => { + trackUmamiEvent(`copilot-rate: ${rate}`, "copilot"); + setShowButtons((prev) => ({ + ...prev, + ADD_RATE: false, + })); + }; + + return ( +
+ + {t("copilotChatBot:rateQuestion")} + + +
+ ); +}; + +export default CopilotFeedback; diff --git a/src/components/Copilot/CopilotForm.tsx b/src/components/Copilot/CopilotForm.tsx new file mode 100644 index 000000000..d9bc152da --- /dev/null +++ b/src/components/Copilot/CopilotForm.tsx @@ -0,0 +1,41 @@ +import React, { useState } from "react"; +import AletheiaTextAreaAutoSize from "../TextAreaAutoSize"; +import ArrowUpwardIcon from "@mui/icons-material/ArrowUpward"; +import { SenderEnum } from "../../types/enums"; +import { useTranslation } from "next-i18next"; + +//TODO: Implement React Hook forms +const CopilotForm = ({ handleSendMessage }) => { + const { t } = useTranslation(); + const [message, setMessage] = useState(""); + + const handleSubmit = (e) => { + e.preventDefault(); + handleSendMessage({ + sender: SenderEnum.User, + content: message, + }); + setMessage(""); + }; + + return ( +
+ setMessage(target.value)} + white={"true"} + onKeyDown={(e) => { + if (!e.shiftKey && e.key === "Enter") { + handleSubmit(e); + } + }} + /> + + + ); +}; + +export default CopilotForm; diff --git a/src/components/Copilot/utils/calculatePositions.ts b/src/components/Copilot/utils/calculatePositions.ts new file mode 100644 index 000000000..da23db14b --- /dev/null +++ b/src/components/Copilot/utils/calculatePositions.ts @@ -0,0 +1,29 @@ +interface PositionStyle { + topPosition: string; + rightPosition: string; + rotate: string; +} + +interface ViewWidth { + sm: boolean; + md?: boolean; +} + +export const calculatePosition = ( + open: boolean, + vw: ViewWidth +): PositionStyle => { + if (!vw.sm) { + return { + topPosition: "50%", + rotate: open ? "rotateY(45deg)" : "rotateY(135deg)", + rightPosition: open ? "350px" : "16px", + }; + } + + return { + rightPosition: "50%", + rotate: open ? "rotate(90deg)" : "rotate(270deg)", + topPosition: open ? "50%" : "calc(100% - 64px)", + }; +}; diff --git a/src/components/MainApp.tsx b/src/components/MainApp.tsx index 7bd5c4cce..84d48a1cb 100644 --- a/src/components/MainApp.tsx +++ b/src/components/MainApp.tsx @@ -12,22 +12,37 @@ import OverlaySearchResults from "./Search/OverlaySearchResults"; import Sidebar from "./Sidebar"; import AffixCTAButton from "./AffixButton/AffixCTAButton"; +const copilotDrawerWidth = 350; + const MainApp = ({ children }) => { - const { enableOverlay } = useAppSelector((state) => { - return { + const { vw, enableOverlay, copilotDrawerCollapsed } = useAppSelector( + (state) => ({ + vw: state?.vw, enableOverlay: state?.search?.overlayVisible, - }; - }); + copilotDrawerCollapsed: + state?.copilotDrawerCollapsed !== undefined + ? state?.copilotDrawerCollapsed + : true, + }) + ); // Setup to provide breakpoints object on redux useMediaQueryBreakpoints(); return ( - +
- + {children}