diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 00000000..091c7ead --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,24 @@ +FROM node:lts + +# Install basic development tools +RUN apt-get update && apt-get install -y \ + git \ + procps \ + && rm -rf /var/lib/apt/lists/* + +# Ensure default `node` user has access to `sudo` +ARG USERNAME=node +RUN apt-get update \ + && apt-get install -y sudo \ + && echo $USERNAME ALL=\(root\) NOPASSWD:ALL > /etc/sudoers.d/$USERNAME \ + && chmod 0440 /etc/sudoers.d/$USERNAME + +ENV NODE_ENV=development + +# Set the default user +USER node + +EXPOSE 3000 + +# Set the working directory +WORKDIR /workspace \ No newline at end of file diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 00000000..c4f107ea --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,34 @@ +{ + "name": "React Development", + "build": { + "dockerfile": "Dockerfile", + "context": ".." + }, + "customizations": { + "vscode": { + "extensions": [ + "dbaeumer.vscode-eslint", + "esbenp.prettier-vscode", + "formulahendry.auto-rename-tag", + "dsznajder.es7-react-js-snippets", + "bradlc.vscode-tailwindcss" + ], + "settings": { + "editor.formatOnSave": true, + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.codeActionsOnSave": { + "source.fixAll.eslint": true + } + } + } + }, + "forwardPorts": [3000], + "postCreateCommand": "npm install", + "remoteUser": "node", + "features": { + "ghcr.io/devcontainers/features/node:1": {}, + "ghcr.io/devcontainers/features/terraform:1": {}, + "ghcr.io/devcontainers/features/aws-cli:1": {}, + "ghcr.io/devcontainers-extra/features/terragrunt:1": {} + } +} \ No newline at end of file diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml new file mode 100644 index 00000000..af512052 --- /dev/null +++ b/.devcontainer/docker-compose.yml @@ -0,0 +1,13 @@ +version: '3.8' +services: + app: + build: + context: . + dockerfile: .devcontainer/Dockerfile + volumes: + - .:/workspace:cached + ports: + - "3000:3000" + environment: + - NODE_ENV=development + command: sleep infinity \ No newline at end of file diff --git a/.eslintrc.json b/.eslintrc.json index 35cf744e..2e087f6a 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -2,7 +2,7 @@ "parser": "@babel/eslint-parser", "parserOptions": { "requireConfigFile": false, - "ecmaVersion": 2021, + "ecmaVersion": "latest", "sourceType": "module", "ecmaFeatures": { "jsx": true @@ -14,12 +14,17 @@ "node": true }, "extends": [ - "react-app", "eslint:recommended", - "plugin:react/recommended" + "plugin:react/recommended", + "plugin:react-hooks/recommended", + "plugin:import/recommended", + "plugin:prettier/recommended" ], "plugins": [ - "react" + "react", + "react-hooks", + "prettier", + "import" ], "rules": { "react/prop-types": "off", @@ -28,11 +33,19 @@ "semi": ["error", "always"], "quotes": "off", "no-irregular-whitespace": "off", - "react/no-unescaped-entities": "off" + "react/no-unescaped-entities": "off", + "react/react-in-jsx-scope": "off", + "prettier/prettier": "error", + "import/no-unused-modules": "off" }, "settings": { "react": { "version": "detect" + }, + "import/resolver": { + "node": { + "extensions": [".js", ".jsx"] + } } } } \ No newline at end of file diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..f33a02cd --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,12 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for more information: +# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates +# https://containers.dev/guide/dependabot + +version: 2 +updates: + - package-ecosystem: "devcontainers" + directory: "/" + schedule: + interval: weekly diff --git a/.github/workflows/export_github_data.yml b/.github/workflows/export_github_data.yml new file mode 100644 index 00000000..8991cf85 --- /dev/null +++ b/.github/workflows/export_github_data.yml @@ -0,0 +1,25 @@ +name: GitHub repository metadata exporter +on: + workflow_dispatch: + schedule: + - cron: '20 7 * * *' + +jobs: + export-data: + runs-on: ubuntu-latest + steps: + - name: Audit DNS requests + uses: cds-snc/dns-proxy-action@main + env: + DNS_PROXY_FORWARDTOSENTINEL: 'true' + DNS_PROXY_LOGANALYTICSWORKSPACEID: ${{ secrets.LOG_ANALYTICS_WORKSPACE_ID }} + DNS_PROXY_LOGANALYTICSSHAREDKEY: ${{ secrets.LOG_ANALYTICS_WORKSPACE_KEY }} + - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 + - name: Export Data + uses: cds-snc/github-repository-metadata-exporter@main + with: + github-app-id: ${{ secrets.SRE_BOT_RO_APP_ID }} + github-app-installation-id: ${{ secrets.SRE_BOT_RO_INSTALLATION_ID }} + github-app-private-key: ${{ secrets.SRE_BOT_RO_PRIVATE_KEY }} + log-analytics-workspace-id: ${{ secrets.LOG_ANALYTICS_WORKSPACE_ID }} + log-analytics-workspace-key: ${{ secrets.LOG_ANALYTICS_WORKSPACE_KEY }} diff --git a/.github/workflows/labels.yml b/.github/workflows/labels.yml new file mode 100644 index 00000000..c0e3ddcf --- /dev/null +++ b/.github/workflows/labels.yml @@ -0,0 +1,10 @@ +on: [issues, pull_request, workflow_dispatch] + +jobs: + sync-labels: + runs-on: ubuntu-latest + name: Sync repository labels + steps: + - uses: cds-snc/labels@v1 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/ossf-scorecard.yml b/.github/workflows/ossf-scorecard.yml new file mode 100644 index 00000000..2607798a --- /dev/null +++ b/.github/workflows/ossf-scorecard.yml @@ -0,0 +1,47 @@ +name: Scorecards supply-chain security +on: + workflow_dispatch: + schedule: + # Weekly on Saturdays. + - cron: '30 1 * * 6' + push: + branches: + - main + +permissions: read-all + +jobs: + analysis: + name: Scorecards analysis + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + + steps: + - name: 'Checkout code' + uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 + with: + persist-credentials: false + + - name: 'Run analysis' + uses: ossf/scorecard-action@bfa3f0d2c52a31cf9f6bc003e1f15e8b99640aec + with: + results_file: ossf-results.json + results_format: json + publish_results: false + + - name: 'Add metadata' + run: | + full_repo="${{ github.repository }}" + OWNER=${full_repo%/*} + REPO=${full_repo#*/} + jq -c '. + {"metadata_owner": "'$OWNER'", "metadata_repo": "'$REPO'", "metadata_query": "ossf"}' ossf-results.json > ossf-results-modified.json + + - name: 'Post results to Sentinel' + uses: cds-snc/sentinel-forward-data-action@main + with: + file_name: ossf-results-modified.json + log_type: GitHubMetadata_OSSF_Scorecard + log_analytics_workspace_id: ${{ secrets.LOG_ANALYTICS_WORKSPACE_ID }} + log_analytics_workspace_key: ${{ secrets.LOG_ANALYTICS_WORKSPACE_KEY }} diff --git a/.github/workflows/s3-backup.yml b/.github/workflows/s3-backup.yml new file mode 100644 index 00000000..565c7047 --- /dev/null +++ b/.github/workflows/s3-backup.yml @@ -0,0 +1,38 @@ +name: S3 backup +on: + workflow_dispatch: + schedule: + - cron: '0 6 * * *' + +jobs: + s3-backup: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 + with: + fetch-depth: 0 # retrieve all history + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@04b98b3f9e85f563fb061be8751a0352327246b0 # v3.0.1 + with: + aws-access-key-id: ${{ secrets.AWS_S3_BACKUP_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_S3_BACKUP_SECRET_ACCESS_KEY }} + aws-region: ca-central-1 + + - name: Create ZIP bundle + run: | + ZIP_FILE=`basename ${{ github.repository }}`-`date '+%Y-%m-%d'`.zip + zip -rq "${ZIP_FILE}" . + mkdir -p ${{ github.repository }} + mv "${ZIP_FILE}" ${{ github.repository }} + + - name: Upload to S3 bucket + run: | + aws s3 sync . s3://${{ secrets.AWS_S3_BACKUP_BUCKET }} --exclude='*' --include='${{ github.repository }}/*' + + - name: Notify Slack channel if this job failed + if: ${{ failure() }} + run: | + json='{"text":"S3 backup failed in !"}' + curl -X POST -H 'Content-type: application/json' --data "$json" ${{ secrets.SLACK_NOTIFY_WEBHOOK }} diff --git a/.gitignore b/.gitignore index e23041a3..bf16719e 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,18 @@ server/node_modules/ .vercel temp.jsonl + +# Terragrunt and Terraform +.terragrunt-cache +*.tfstate +*.tfstate.backup +*.tfstate.lock.info + +# Ignored Terraform files +*gitignore*.tf + +# Backup files +*.bak + +# Ignore local stack data file +.devcontainer/data \ No newline at end of file diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 00000000..aa83ed58 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,9 @@ +{ + "semi": true, + "tabWidth": 2, + "printWidth": 100, + "singleQuote": true, + "trailingComma": "es5", + "bracketSpacing": true, + "jsxBracketSameLine": false +} \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..21a999e0 --- /dev/null +++ b/Makefile @@ -0,0 +1,22 @@ +.PHONY: fmt checkov install lint test fmt-ci lint-ci build install-dev + +terraform-fmt: + terraform fmt -recursive terragrunt/aws &&\ + terragrunt hclfmt + +checkov: + checkov --directory=aws + +run-dev: + npm run dev + +install: + npm install + +lint: + npm run lint + +fmt: + npm run format + +test: ; diff --git a/package-lock.json b/package-lock.json index 21309419..5165e106 100644 --- a/package-lock.json +++ b/package-lock.json @@ -60,7 +60,11 @@ "concurrently": "^9.0.1", "dotenv-cli": "^7.4.4", "eslint": "^8.57.1", - "eslint-plugin-react": "^7.37.2" + "eslint-config-prettier": "^10.0.1", + "eslint-plugin-import": "^2.31.0", + "eslint-plugin-prettier": "^5.2.3", + "eslint-plugin-react": "^7.37.2", + "prettier": "^3.4.2" } }, "node_modules/@adobe/css-tools": { @@ -5191,6 +5195,18 @@ "node": ">=14" } }, + "node_modules/@pkgr/core": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.1.tgz", + "integrity": "sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + } + }, "node_modules/@pmmmwh/react-refresh-webpack-plugin": { "version": "0.5.15", "resolved": "https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.15.tgz", @@ -10811,6 +10827,18 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint-config-prettier": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.0.1.tgz", + "integrity": "sha512-lZBts941cyJyeaooiKxAtzoPHTN+GbQTJFAIdQbRhA4/8whaAraEh47Whw/ZFfrjNSnlAxqfm9i0XVAEkULjCw==", + "dev": true, + "bin": { + "eslint-config-prettier": "build/bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, "node_modules/eslint-config-react-app": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/eslint-config-react-app/-/eslint-config-react-app-7.0.1.tgz", @@ -11007,6 +11035,36 @@ "node": ">= 0.4" } }, + "node_modules/eslint-plugin-prettier": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.3.tgz", + "integrity": "sha512-qJ+y0FfCp/mQYQ/vWQ3s7eUlFEL4PyKfAJxsnYTJ4YT73nsJBWqmEpFryxV9OeUiqmsTsYJ5Y+KDNaeP31wrRw==", + "dev": true, + "dependencies": { + "prettier-linter-helpers": "^1.0.0", + "synckit": "^0.9.1" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-plugin-prettier" + }, + "peerDependencies": { + "@types/eslint": ">=8.0.0", + "eslint": ">=8.0.0", + "eslint-config-prettier": "*", + "prettier": ">=3.0.0" + }, + "peerDependenciesMeta": { + "@types/eslint": { + "optional": true + }, + "eslint-config-prettier": { + "optional": true + } + } + }, "node_modules/eslint-plugin-react": { "version": "7.37.3", "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.3.tgz", @@ -11531,6 +11589,12 @@ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "dev": true + }, "node_modules/fast-glob": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", @@ -21815,6 +21879,33 @@ "node": ">= 0.8.0" } }, + "node_modules/prettier": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.2.tgz", + "integrity": "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==", + "devOptional": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-linter-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", + "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "dev": true, + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/pretty-bytes": { "version": "5.6.0", "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", @@ -24457,6 +24548,22 @@ "resolved": "https://registry.npmjs.org/synchronous-promise/-/synchronous-promise-2.0.17.tgz", "integrity": "sha512-AsS729u2RHUfEra9xJrE39peJcc2stq2+poBXX8bcM08Y6g9j/i/PUzwNQqkaJde7Ntg1TO7bSREbR5sdosQ+g==" }, + "node_modules/synckit": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.9.2.tgz", + "integrity": "sha512-vrozgXDQwYO72vHjUb/HnFbQx1exDjoKzqx23aXEg2a9VIg2TSFZ8FmeZpTjUCFMYw7mpX4BE2SFu8wI7asYsw==", + "dev": true, + "dependencies": { + "@pkgr/core": "^0.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + } + }, "node_modules/tailwindcss": { "version": "3.4.17", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz", diff --git a/package.json b/package.json index 968fa26d..57584500 100644 --- a/package.json +++ b/package.json @@ -65,7 +65,9 @@ "eject": "react-scripts eject", "start-server": "node server/server.js", "dev": "concurrently \"npm run start-server\" \"npm start\"", - "lint": "dotenv -e .env eslint src/**/*.{js,jsx}" + "lint": "eslint src --ext .js,.jsx,.ts,.tsx", + "lint:fix": "eslint src --ext .js,.jsx,.ts,.tsx --fix", + "format": "prettier --write \"src/**/*.{js,jsx,ts,tsx,css,md}\"" }, "eslintConfig": { "extends": [ @@ -93,6 +95,10 @@ "concurrently": "^9.0.1", "dotenv-cli": "^7.4.4", "eslint": "^8.57.1", - "eslint-plugin-react": "^7.37.2" + "eslint-config-prettier": "^10.0.1", + "eslint-plugin-import": "^2.31.0", + "eslint-plugin-prettier": "^5.2.3", + "eslint-plugin-react": "^7.37.2", + "prettier": "^3.4.2" } } diff --git a/src/App.js b/src/App.js index be8e6d47..e0096926 100644 --- a/src/App.js +++ b/src/App.js @@ -5,7 +5,12 @@ import { BrowserRouter as Router, Routes, Route, Link, useLocation } from 'react import HomePage from './pages/HomePage.js'; import AdminPage from './pages/AdminPage.js'; import EvaluationPage from './pages/EvaluationPage.js'; -import { GcdsHeader, GcdsBreadcrumbs, GcdsBreadcrumbsItem, GcdsFooter } from '@cdssnc/gcds-components-react'; +import { + GcdsHeader, + GcdsBreadcrumbs, + GcdsBreadcrumbsItem, + GcdsFooter, +} from '@cdssnc/gcds-components-react'; import './styles/App.css'; // Helper function to get alternate language path @@ -29,20 +34,16 @@ const AppContent = () => {
- Alpha   - {currentLang === 'en' ? 'Experimental page - not public.' : 'Page expérimentale - non publique.'} + Alpha   + {currentLang === 'en' + ? 'Experimental page - not public.' + : 'Page expérimentale - non publique.'}
- - - - {/* Add breadcrumb items as needed */} - + + + {/* Add breadcrumb items as needed */}
@@ -52,7 +53,7 @@ const AppContent = () => { } /> } /> } /> - + {/* French routes */} } /> } /> @@ -60,10 +61,7 @@ const AppContent = () => {
- + ); }; @@ -76,4 +74,4 @@ function App() { ); } -export default App; \ No newline at end of file +export default App; diff --git a/src/components/admin/AdminCodeInput.js b/src/components/admin/AdminCodeInput.js index 16b051ae..a56a3c29 100644 --- a/src/components/admin/AdminCodeInput.js +++ b/src/components/admin/AdminCodeInput.js @@ -6,15 +6,9 @@ const AdminCodeInput = ({ code, onChange, correctCode, label }) => { - + ); }; -export default AdminCodeInput; \ No newline at end of file +export default AdminCodeInput; diff --git a/src/components/admin/ChatLogsDashboard.js b/src/components/admin/ChatLogsDashboard.js index d5524b18..7fd1f64b 100644 --- a/src/components/admin/ChatLogsDashboard.js +++ b/src/components/admin/ChatLogsDashboard.js @@ -8,20 +8,22 @@ const extractSentences = (text) => { const sentenceRegex = /(.*?)<\/s-\d+>/g; const sentences = []; let match; - + while ((match = sentenceRegex.exec(text)) !== null) { const index = parseInt(match[1]) - 1; if (index >= 0 && index < 4) { sentences[index] = match[2].trim(); } } - + // If no sentence tags found, treat entire text as first sentence if (sentences.length === 0 && text) { sentences[0] = text.trim(); } - - return Array(4).fill('').map((_, i) => sentences[i] || ''); + + return Array(4) + .fill('') + .map((_, i) => sentences[i] || ''); }; const ChatLogsDashboard = () => { @@ -38,10 +40,10 @@ const ChatLogsDashboard = () => { setLoading(true); try { - const response = await fetch(getApiUrl("db-chat-logs?days=") + timeRange); + const response = await fetch(getApiUrl('db-chat-logs?days=') + timeRange); const data = await response.json(); console.log('API Response:', data); - + if (data.success) { setLogs(data.logs || []); } else { @@ -56,8 +58,8 @@ const ChatLogsDashboard = () => { }; const downloadJSON = () => { - const blob = new Blob([JSON.stringify(logs, null, 2)], { - type: 'application/json' + const blob = new Blob([JSON.stringify(logs, null, 2)], { + type: 'application/json', }); const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); @@ -94,25 +96,29 @@ const ChatLogsDashboard = () => { 'expertFeedback.citationScore', 'expertFeedback.answerImprovement', 'expertFeedback.expertCitationUrl', - 'context' + 'context', ]; // Create CSV header - const header = columns.map(column => { - return column.includes('.') ? column.split('.')[1] : column; - }).join(','); + const header = columns + .map((column) => { + return column.includes('.') ? column.split('.')[1] : column; + }) + .join(','); const extractLanguages = (preliminaryChecks) => { const result = { pageLanguage: '', - questionLanguage: '' + questionLanguage: '', }; if (!preliminaryChecks) return result; try { const pageMatch = /- (.*?)<\/page-language>/s.exec(preliminaryChecks); - const questionMatch = /- (.*?)<\/question-language>/s.exec(preliminaryChecks); + const questionMatch = /- (.*?)<\/question-language>/s.exec( + preliminaryChecks + ); if (pageMatch) result.pageLanguage = pageMatch[1].trim(); if (questionMatch) result.questionLanguage = questionMatch[1].trim(); @@ -125,7 +131,7 @@ const ChatLogsDashboard = () => { const extractContext = (preliminaryChecks) => { if (!preliminaryChecks) return ''; - + try { const contextMatch = /(.*?)<\/context>/s.exec(preliminaryChecks); return contextMatch ? contextMatch[1].trim() : ''; @@ -136,39 +142,41 @@ const ChatLogsDashboard = () => { }; // Create CSV rows - const rows = logs.map(log => { + const rows = logs.map((log) => { // Extract sentences from answer const sentences = extractSentences(log.answer || ''); - + // Extract languages from preliminary checks const languages = extractLanguages(log.preliminaryChecks); - return columns.map(column => { - let value = ''; - try { - if (column === 'pageLanguage') { - value = languages.pageLanguage; - } else if (column === 'questionLanguage') { - value = languages.questionLanguage; - } else if (column === 'context') { - value = extractContext(log.preliminaryChecks); - } else if (column.includes('.')) { - const [parent, child] = column.split('.'); - value = log[parent]?.[child]; - } else if (column.startsWith('sentence')) { - const index = parseInt(column.charAt(column.length - 1)) - 1; - value = sentences[index]; - } else { - value = log[column]; + return columns + .map((column) => { + let value = ''; + try { + if (column === 'pageLanguage') { + value = languages.pageLanguage; + } else if (column === 'questionLanguage') { + value = languages.questionLanguage; + } else if (column === 'context') { + value = extractContext(log.preliminaryChecks); + } else if (column.includes('.')) { + const [parent, child] = column.split('.'); + value = log[parent]?.[child]; + } else if (column.startsWith('sentence')) { + const index = parseInt(column.charAt(column.length - 1)) - 1; + value = sentences[index]; + } else { + value = log[column]; + } + } catch (error) { + console.error(`Error processing column ${column}:`, error); } - } catch (error) { - console.error(`Error processing column ${column}:`, error); - } - - // Handle null/undefined and escape quotes - const escapedValue = (value ?? '').toString().replace(/"/g, '""'); - return `"${escapedValue}"`; - }).join(','); + + // Handle null/undefined and escape quotes + const escapedValue = (value ?? '').toString().replace(/"/g, '""'); + return `"${escapedValue}"`; + }) + .join(','); }); // Combine header and rows @@ -177,7 +185,7 @@ const ChatLogsDashboard = () => { // Add UTF-8 BOM and create blob const BOM = '\uFEFF'; const blob = new Blob([BOM + csv], { type: 'text/csv;charset=utf-8;' }); - + // Create and download file const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); @@ -204,10 +212,7 @@ const ChatLogsDashboard = () => {
-
- {loading ? 'Loading...' : 'Get logs'} - + {logs.length > 0 && adminCode === correctAdminCode && ( <> - { Download JSON - {
) : logs.length > 0 ? (
-

Found {logs.length} chat interactions. Download the logs to see the full set and details.

+

+ Found {logs.length} chat interactions. Download the logs to see the full set and + details. +

- - - @@ -295,7 +312,9 @@ const ChatLogsDashboard = () => { ) : (
-

Select a time range and click 'Get logs' to view chat history

+

+ Select a time range and click 'Get logs' to view chat history +

)} @@ -303,4 +322,4 @@ const ChatLogsDashboard = () => { ); }; -export default ChatLogsDashboard; \ No newline at end of file +export default ChatLogsDashboard; diff --git a/src/components/chat/ChatAppContainer.js b/src/components/chat/ChatAppContainer.js index e620399a..5b595e04 100644 --- a/src/components/chat/ChatAppContainer.js +++ b/src/components/chat/ChatAppContainer.js @@ -43,7 +43,7 @@ const parseMessageContent = (text) => { // Extract citation information before processing answers const citationHeadMatch = /(.*?)<\/citation-head>/s.exec(content); const citationUrlMatch = /(.*?)<\/citation-url>/s.exec(content); - + if (citationHeadMatch) { citationHead = citationHeadMatch[1].trim(); } @@ -55,7 +55,7 @@ const parseMessageContent = (text) => { const englishMatch = /(.*?)<\/english-answer>/s.exec(content); if (englishMatch) { englishAnswer = englishMatch[1].trim(); - content = englishAnswer; // Use English answer as content for English questions + content = englishAnswer; // Use English answer as content for English questions } // Extract main answer if it exists @@ -129,7 +129,7 @@ const ChatAppContainer = ({ lang = 'en' }) => { const clearInput = useCallback(() => { setInputText(''); - setTextareaKey(prevKey => prevKey + 1); + setTextareaKey((prevKey) => prevKey + 1); }, []); const parseAIResponse = useCallback((text, aiService) => { @@ -150,10 +150,11 @@ const ChatAppContainer = ({ lang = 'en' }) => { // Split content into paragraphs, but exclude any remaining citation tags const paragraphs = mainContent .split(/\n+/) - .filter(para => - !para.includes('') && - !para.includes('') && - !para.includes('') + .filter( + (para) => + !para.includes('') && + !para.includes('') && + !para.includes('') ); const result = { @@ -161,106 +162,119 @@ const ChatAppContainer = ({ lang = 'en' }) => { citationHead: headMatch ? headMatch[1].trim() : null, citationUrl: urlMatch ? urlMatch[1].trim() : null, confidenceRating: confidenceMatch ? confidenceMatch[1] : null, - aiService + aiService, }; return result; }, []); // TODO: Refactor logging to update existing logs with feedback instead of creating duplicates -// Current behavior creates a new log entry when feedback is provided, resulting in duplicate entries -// Should implement: -// 1. LoggingService.updateInteraction method -// 2. Backend API support for updating existing logs -// 3. Modify handleFeedback to update instead of create - const logInteraction = useCallback(( - aiService, - redactedQuestion, - referringUrl, - aiResponse, - citationUrl, - originalCitationUrl, - confidenceRating, - feedback, - expertFeedback - ) => { - // Parse all components from the AI response - const { preliminaryChecks, englishAnswer, content } = parseMessageContent(aiResponse); - - // Standardize expert feedback format - only accept new format - let formattedExpertFeedback = null; - if (expertFeedback) { - formattedExpertFeedback = { - totalScore: expertFeedback.totalScore || null, - sentence1Score: expertFeedback.sentence1Score || null, - sentence2Score: expertFeedback.sentence2Score || null, - sentence3Score: expertFeedback.sentence3Score || null, - sentence4Score: expertFeedback.sentence4Score || null, - citationScore: expertFeedback.citationScore || null, - answerImprovement: expertFeedback.answerImprovement || '', - expertCitationUrl: expertFeedback.expertCitationUrl || '' - }; - } - - const logEntry = { - timestamp: new Date(), - aiService: aiService || '', + // Current behavior creates a new log entry when feedback is provided, resulting in duplicate entries + // Should implement: + // 1. LoggingService.updateInteraction method + // 2. Backend API support for updating existing logs + // 3. Modify handleFeedback to update instead of create + const logInteraction = useCallback( + ( + aiService, redactedQuestion, - referringUrl: referringUrl || '', - preliminaryChecks: preliminaryChecks || '', - aiResponse: aiResponse || '', - englishAnswer: englishAnswer || '', - answer: content || '', - originalCitationUrl: originalCitationUrl || '', - citationUrl: citationUrl || '', - confidenceRating: confidenceRating || '', - ...(feedback && { feedback }), - ...(formattedExpertFeedback && { expertFeedback: formattedExpertFeedback }) - }; + referringUrl, + aiResponse, + citationUrl, + originalCitationUrl, + confidenceRating, + feedback, + expertFeedback + ) => { + // Parse all components from the AI response + const { preliminaryChecks, englishAnswer, content } = parseMessageContent(aiResponse); + + // Standardize expert feedback format - only accept new format + let formattedExpertFeedback = null; + if (expertFeedback) { + formattedExpertFeedback = { + totalScore: expertFeedback.totalScore || null, + sentence1Score: expertFeedback.sentence1Score || null, + sentence2Score: expertFeedback.sentence2Score || null, + sentence3Score: expertFeedback.sentence3Score || null, + sentence4Score: expertFeedback.sentence4Score || null, + citationScore: expertFeedback.citationScore || null, + answerImprovement: expertFeedback.answerImprovement || '', + expertCitationUrl: expertFeedback.expertCitationUrl || '', + }; + } + + const logEntry = { + timestamp: new Date(), + aiService: aiService || '', + redactedQuestion, + referringUrl: referringUrl || '', + preliminaryChecks: preliminaryChecks || '', + aiResponse: aiResponse || '', + englishAnswer: englishAnswer || '', + answer: content || '', + originalCitationUrl: originalCitationUrl || '', + citationUrl: citationUrl || '', + confidenceRating: confidenceRating || '', + ...(feedback && { feedback }), + ...(formattedExpertFeedback && { expertFeedback: formattedExpertFeedback }), + }; - console.log('Final log entry:', logEntry); + console.log('Final log entry:', logEntry); - if (process.env.REACT_APP_ENV === 'production') { - LoggingService.logInteraction(logEntry, false); - } - }, []); + if (process.env.REACT_APP_ENV === 'production') { + LoggingService.logInteraction(logEntry, false); + } + }, + [] + ); - const handleFeedback = useCallback((isPositive, expertFeedback = null) => { - const feedback = isPositive ? 'positive' : 'negative'; - console.log(`User feedback: ${feedback}`, expertFeedback); - - // Get the last message (which should be the AI response) - const lastMessage = messages[messages.length - 1]; - if (lastMessage && lastMessage.sender === 'ai') { - const { text: aiResponse, aiService: selectedAIService } = lastMessage; - // Get original URL from AI response - const { citationUrl: originalCitationUrl, confidenceRating } = parseAIResponse(aiResponse, selectedAIService); - // Extract preliminaryChecks, englishAnswer, and the displayed answer - const { preliminaryChecks, englishAnswer, content: answer } = parseMessageContent(aiResponse); - - // Get validated URL from checkedCitations - const lastIndex = messages.length - 1; - const validationResult = checkedCitations[lastIndex]; - const finalCitationUrl = validationResult?.url || validationResult?.fallbackUrl; - - // Get the user's message (which should be the second-to-last message) - const userMessage = messages[messages.length - 2]; - if (userMessage && userMessage.sender === 'user') { - // Only log if there's feedback - logInteraction( - selectedAIService, - userMessage.redactedText, - referringUrl, + const handleFeedback = useCallback( + (isPositive, expertFeedback = null) => { + const feedback = isPositive ? 'positive' : 'negative'; + console.log(`User feedback: ${feedback}`, expertFeedback); + + // Get the last message (which should be the AI response) + const lastMessage = messages[messages.length - 1]; + if (lastMessage && lastMessage.sender === 'ai') { + const { text: aiResponse, aiService: selectedAIService } = lastMessage; + // Get original URL from AI response + const { citationUrl: originalCitationUrl, confidenceRating } = parseAIResponse( aiResponse, - finalCitationUrl, - originalCitationUrl, - confidenceRating, - feedback, - expertFeedback + selectedAIService ); + // Extract preliminaryChecks, englishAnswer, and the displayed answer + const { + preliminaryChecks, + englishAnswer, + content: answer, + } = parseMessageContent(aiResponse); + + // Get validated URL from checkedCitations + const lastIndex = messages.length - 1; + const validationResult = checkedCitations[lastIndex]; + const finalCitationUrl = validationResult?.url || validationResult?.fallbackUrl; + + // Get the user's message (which should be the second-to-last message) + const userMessage = messages[messages.length - 2]; + if (userMessage && userMessage.sender === 'user') { + // Only log if there's feedback + logInteraction( + selectedAIService, + userMessage.redactedText, + referringUrl, + aiResponse, + finalCitationUrl, + originalCitationUrl, + confidenceRating, + feedback, + expertFeedback + ); + } } - } - }, [messages, checkedCitations, logInteraction, parseAIResponse, referringUrl]); + }, + [messages, checkedCitations, logInteraction, parseAIResponse, referringUrl] + ); const handleReferringUrlChange = (e) => { const url = e.target.value.trim(); @@ -302,15 +316,11 @@ const ChatAppContainer = ({ lang = 'en' }) => { const getNextAIService = (currentAI) => { const failoverOrder = { // always openai until we add another provider - 'openai': 'openai', - + openai: 'openai', }; return failoverOrder[currentAI]; }; - - - const handleSendMessage = useCallback(async () => { if (inputText.trim() !== '' && !isLoading) { try { @@ -320,14 +330,14 @@ const ChatAppContainer = ({ lang = 'en' }) => { // Initial validation checks if (inputText.length > MAX_CHAR_LIMIT) { const errorMessageId = messageIdCounter.current++; - setMessages(prevMessages => [ + setMessages((prevMessages) => [ ...prevMessages, { id: errorMessageId, text: t('homepage.chat.messages.characterLimit'), sender: 'system', - error: true - } + error: true, + }, ]); return; } @@ -341,24 +351,31 @@ const ChatAppContainer = ({ lang = 'en' }) => { if (hasBlockedContent) { const userMessageId = messageIdCounter.current++; const blockedMessageId = messageIdCounter.current++; - setMessages(prevMessages => [ + setMessages((prevMessages) => [ ...prevMessages, { id: userMessageId, text: redactedText, redactedText: redactedText, redactedItems: redactedItems, - sender: 'user' + sender: 'user', }, { id: blockedMessageId, - text:
' + - (redactedText.includes('XXX') ? t('homepage.chat.messages.privateContent') : t('homepage.chat.messages.blockedContent')) - }} />, + text: ( +
' + + (redactedText.includes('XXX') + ? t('homepage.chat.messages.privateContent') + : t('homepage.chat.messages.blockedContent')), + }} + /> + ), sender: 'system', - error: true - } + error: true, + }, ]); clearInput(); return; @@ -367,7 +384,7 @@ const ChatAppContainer = ({ lang = 'en' }) => { setDisplayStatus('startingToThink'); // Now that message is validated and redacted, show formatted message with "Starting to think..." const userMessageId = messageIdCounter.current++; - setMessages(prevMessages => [ + setMessages((prevMessages) => [ ...prevMessages, { id: userMessageId, @@ -375,8 +392,8 @@ const ChatAppContainer = ({ lang = 'en' }) => { redactedText: redactedText, redactedItems: redactedItems, sender: 'user', - ...(referringUrl.trim() && { referringUrl: referringUrl.trim() }) - } + ...(referringUrl.trim() && { referringUrl: referringUrl.trim() }), + }, ]); clearInput(); @@ -391,7 +408,12 @@ const ChatAppContainer = ({ lang = 'en' }) => { if (turnCount === 0) { try { const contextMessage = `${redactedText}${referringUrl ? `\n${referringUrl}` : ''}`; - const derivedContext = await ContextService.deriveContext(selectedAI, contextMessage, lang, department); + const derivedContext = await ContextService.deriveContext( + selectedAI, + contextMessage, + lang, + department + ); department = derivedContext.department; topic = derivedContext.topic; topicUrl = derivedContext.topicUrl; @@ -402,7 +424,13 @@ const ChatAppContainer = ({ lang = 'en' }) => { setCurrentSearchResults(derivedContext.searchResults); setCurrentDepartmentUrl(derivedContext.departmentUrl); setCurrentTopicUrl(derivedContext.topicUrl); - console.log('Derived context:', { department, topic, topicUrl, departmentUrl, searchResults }); + console.log('Derived context:', { + department, + topic, + topicUrl, + departmentUrl, + searchResults, + }); } catch (error) { console.error('Error deriving context:', error); department = ''; @@ -430,20 +458,26 @@ const ChatAppContainer = ({ lang = 'en' }) => { setDisplayStatus('thinking'); } - // Get conversation history for context const conversationHistory = messages - .filter(m => !m.temporary) - .map(m => ({ + .filter((m) => !m.temporary) + .map((m) => ({ role: m.sender === 'user' ? 'user' : 'assistant', - content: m.redactedText || m.text + content: m.redactedText || m.text, })); // Create formatted message with referring URL (add this before the first try block) - const messageWithReferrer = `${redactedText}${referringUrl.trim() ? `\n${referringUrl.trim()}` : '' - }}`; + const messageWithReferrer = `${redactedText}${ + referringUrl.trim() ? `\n${referringUrl.trim()}` : '' + }}`; // Try primary AI service first, yes first try { - const response = await MessageService.sendMessage(selectedAI, messageWithReferrer, conversationHistory, lang, context); + const response = await MessageService.sendMessage( + selectedAI, + messageWithReferrer, + conversationHistory, + lang, + context + ); console.log(`✅ ${selectedAI} response:`, response); // Parse the response for citations @@ -467,16 +501,15 @@ const ChatAppContainer = ({ lang = 'en' }) => { console.log(`✅ Validated URL:`, validationResult); - // Store validation result in checkedCitations - setCheckedCitations(prev => ({ + setCheckedCitations((prev) => ({ ...prev, [newMessageId]: { url: validationResult?.url, fallbackUrl: validationResult?.fallbackUrl, confidenceRating: validationResult?.confidenceRating || '0.1', - finalCitationUrl: validationResult?.url || validationResult?.fallbackUrl - } + finalCitationUrl: validationResult?.url || validationResult?.fallbackUrl, + }, })); finalCitationUrl = validationResult?.url || validationResult?.fallbackUrl; @@ -485,15 +518,18 @@ const ChatAppContainer = ({ lang = 'en' }) => { // Add the AI response to messages using addMessage // Add message with the new ID - setMessages(prev => [...prev, { - id: newMessageId, - text: response, - sender: 'ai', - aiService: usedAI, - department: department - }]); - - setTurnCount(prev => prev + 1); + setMessages((prev) => [ + ...prev, + { + id: newMessageId, + text: response, + sender: 'ai', + aiService: usedAI, + department: department, + }, + ]); + + setTurnCount((prev) => prev + 1); setShowFeedback(true); // Log the interaction with the validated URL @@ -505,10 +541,9 @@ const ChatAppContainer = ({ lang = 'en' }) => { finalCitationUrl, originalCitationUrl, confidenceRating, - null, // feedback - null // expertFeedback + null, // feedback + null // expertFeedback ); - } catch (error) { console.error(`Error with ${selectedAI}:`, error); @@ -518,8 +553,13 @@ const ChatAppContainer = ({ lang = 'en' }) => { try { // Try fallback AI service - const fallbackResponse = await MessageService.sendMessage(fallbackAI, messageWithReferrer, conversationHistory, lang, context); - + const fallbackResponse = await MessageService.sendMessage( + fallbackAI, + messageWithReferrer, + conversationHistory, + lang, + context + ); // Add the fallback AI response const fallbackMessageId = messageIdCounter.current++; @@ -537,34 +577,36 @@ const ChatAppContainer = ({ lang = 'en' }) => { ); // Store validation result in checkedCitations - setCheckedCitations(prev => ({ + setCheckedCitations((prev) => ({ ...prev, [fallbackMessageId]: { url: validationResult?.url, fallbackUrl: validationResult?.fallbackUrl, confidenceRating: validationResult?.confidenceRating || '0.1', - finalCitationUrl: validationResult?.url || validationResult?.fallbackUrl - } + finalCitationUrl: validationResult?.url || validationResult?.fallbackUrl, + }, })); - } - setMessages(prevMessages => [ + setMessages((prevMessages) => [ ...prevMessages, { id: fallbackMessageId, text: fallbackResponse, sender: 'ai', - aiService: fallbackAI - } + aiService: fallbackAI, + }, ]); - setTurnCount(prev => prev + 1); + setTurnCount((prev) => prev + 1); setShowFeedback(true); setDisplayStatus('thinkingMore'); // Log the fallback interaction - const { citationUrl: originalCitationUrl, confidenceRating } = parseAIResponse(fallbackResponse, fallbackAI); + const { citationUrl: originalCitationUrl, confidenceRating } = parseAIResponse( + fallbackResponse, + fallbackAI + ); logInteraction( fallbackAI, redactedText, @@ -574,33 +616,31 @@ const ChatAppContainer = ({ lang = 'en' }) => { originalCitationUrl, confidenceRating ); - } catch (fallbackError) { console.error(`Error with fallback ${fallbackAI}:`, fallbackError); const errorMessageId = messageIdCounter.current++; - setMessages(prevMessages => [ + setMessages((prevMessages) => [ ...prevMessages, { id: errorMessageId, text: t('homepage.chat.messages.error'), sender: 'system', - error: true - } + error: true, + }, ]); } } - } catch (error) { console.error('Error in handleSendMessage:', error); const errorMessageId = messageIdCounter.current++; - setMessages(prevMessages => [ + setMessages((prevMessages) => [ ...prevMessages, { id: errorMessageId, text: t('homepage.chat.messages.error'), sender: 'system', - error: true - } + error: true, + }, ]); } finally { setIsLoading(false); @@ -623,7 +663,7 @@ const ChatAppContainer = ({ lang = 'en' }) => { currentDepartmentUrl, currentSearchResults, currentTopic, - currentTopicUrl + currentTopicUrl, ]); useEffect(() => { @@ -655,67 +695,77 @@ const ChatAppContainer = ({ lang = 'en' }) => { responseType, paragraphs, citationHead, - aiService: message.aiService + aiService: message.aiService, }; } }); return responses; }, [messages, parseAIResponse]); - const formatAIResponse = useCallback((text, aiService, messageId) => { - if (!isTyping.current && messageId !== undefined) { - // console.log('Formatting message:', messageId); - } + const formatAIResponse = useCallback( + (text, aiService, messageId) => { + if (!isTyping.current && messageId !== undefined) { + // console.log('Formatting message:', messageId); + } - const parsedResponse = parsedResponses[messageId]; - if (!parsedResponse) return null; + const parsedResponse = parsedResponses[messageId]; + if (!parsedResponse) return null; - // Clean up any instruction tags from the paragraphs - if (parsedResponse.paragraphs) { - parsedResponse.paragraphs = parsedResponse.paragraphs.map(paragraph => - paragraph.replace(/.*?<\/translated-question>/g, '') - ); - } + // Clean up any instruction tags from the paragraphs + if (parsedResponse.paragraphs) { + parsedResponse.paragraphs = parsedResponse.paragraphs.map((paragraph) => + paragraph.replace(/.*?<\/translated-question>/g, '') + ); + } - const citationResult = checkedCitations[messageId]; - const displayUrl = citationResult?.finalCitationUrl || citationResult?.url || citationResult?.fallbackUrl; - const finalConfidenceRating = citationResult ? citationResult.confidenceRating : '0.1'; - - // Find the message to get its department - const message = messages.find(m => m.id === messageId); - const messageDepartment = message?.department || selectedDepartment; - - return ( -
- {parsedResponse.paragraphs.map((paragraph, index) => { - const sentences = extractSentences(paragraph); - return sentences.map((sentence, sentenceIndex) => ( -

- {sentence} -

- )); - })} - {parsedResponse.responseType === 'normal' && (parsedResponse.citationHead || displayUrl) && ( -
- {parsedResponse.citationHead &&

{parsedResponse.citationHead}

} - {displayUrl && ( -

- - {displayUrl} - + const citationResult = checkedCitations[messageId]; + const displayUrl = + citationResult?.finalCitationUrl || citationResult?.url || citationResult?.fallbackUrl; + const finalConfidenceRating = citationResult ? citationResult.confidenceRating : '0.1'; + + // Find the message to get its department + const message = messages.find((m) => m.id === messageId); + const messageDepartment = message?.department || selectedDepartment; + + return ( +

+ {parsedResponse.paragraphs.map((paragraph, index) => { + const sentences = extractSentences(paragraph); + return sentences.map((sentence, sentenceIndex) => ( +

+ {sentence}

+ )); + })} + {parsedResponse.responseType === 'normal' && + (parsedResponse.citationHead || displayUrl) && ( +
+ {parsedResponse.citationHead && ( +

+ {parsedResponse.citationHead} +

+ )} + {displayUrl && ( +

+ + {displayUrl} + +

+ )} +

+ {finalConfidenceRating !== undefined && + `${t('homepage.chat.citation.confidence')} ${finalConfidenceRating}`} + {finalConfidenceRating !== undefined && (aiService || messageDepartment) && ' | '} + {aiService && `${t('homepage.chat.citation.ai')} ${aiService}`} + {messageDepartment && ` | ${messageDepartment}`} +

+
)} -

- {finalConfidenceRating !== undefined && `${t('homepage.chat.citation.confidence')} ${finalConfidenceRating}`} - {finalConfidenceRating !== undefined && (aiService || messageDepartment) && ' | '} - {aiService && `${t('homepage.chat.citation.ai')} ${aiService}`} - {messageDepartment && ` | ${messageDepartment}`} -

-
- )} -
- ); - }, [parsedResponses, checkedCitations, t, selectedDepartment, messages]); +
+ ); + }, + [parsedResponses, checkedCitations, t, selectedDepartment, messages] + ); // Add handler for department changes const handleDepartmentChange = (department) => { @@ -749,7 +799,9 @@ const ChatAppContainer = ({ lang = 'en' }) => { t={t} lang={lang} privacyMessage={t('homepage.chat.messages.privacy')} - getLabelForInput={() => turnCount >= 1 ? t('homepage.chat.input.followUp') : t('homepage.chat.input.initial')} + getLabelForInput={() => + turnCount >= 1 ? t('homepage.chat.input.followUp') : t('homepage.chat.input.initial') + } extractSentences={extractSentences} parsedResponses={parsedResponses} checkedCitations={checkedCitations} diff --git a/src/components/chat/ChatInterface.js b/src/components/chat/ChatInterface.js index cc4b6c13..9a1b1729 100644 --- a/src/components/chat/ChatInterface.js +++ b/src/components/chat/ChatInterface.js @@ -46,7 +46,7 @@ const ChatInterface = ({ return () => clearTimeout(timeoutId); }, []); - + useEffect(() => { const handleCitationAppearance = () => { if (textareaRef.current && !userHasClickedTextarea) { @@ -69,21 +69,21 @@ const ChatInterface = ({ observer.observe(document.body, { childList: true, - subtree: true + subtree: true, }); return () => observer.disconnect(); }, [userHasClickedTextarea]); - + useEffect(() => { const textarea = document.querySelector('#message'); const button = document.querySelector('.btn-primary-send'); - + // Create loading hint const placeholderHint = document.createElement('div'); placeholderHint.id = 'temp-hint'; placeholderHint.innerHTML = `

${t('homepage.chat.input.loadingHint')}

`; - + if (isLoading) { if (textarea) { textarea.style.display = 'none'; @@ -110,10 +110,12 @@ const ChatInterface = ({ }; const getLastMessageSentenceCount = () => { - const lastAiMessage = messages.filter(m => m.sender === 'ai').pop(); + const lastAiMessage = messages.filter((m) => m.sender === 'ai').pop(); if (lastAiMessage && parsedResponses[lastAiMessage.id]) { - return parsedResponses[lastAiMessage.id].paragraphs.reduce((count, paragraph) => - count + extractSentences(paragraph).length, 0); + return parsedResponses[lastAiMessage.id].paragraphs.reduce( + (count, paragraph) => count + extractSentences(paragraph).length, + 0 + ); } return 1; }; @@ -122,7 +124,7 @@ const ChatInterface = ({ const textarea = event.target; setCharCount(textarea.value.length); handleInputChange(event); - + // Auto-resize textarea.style.height = 'auto'; textarea.style.height = `${textarea.scrollHeight}px`; @@ -131,12 +133,12 @@ const ChatInterface = ({ const handleKeyPress = (event) => { if (event.key === 'Enter') { if (event.shiftKey) return; - + if (inputText.trim().length === 0 || charCount > MAX_CHARS) { event.preventDefault(); return; } - + event.preventDefault(); handleSendMessage(event); } @@ -162,25 +164,43 @@ const ChatInterface = ({ {messages.map((message) => (
{message.sender === 'user' ? ( -
-

+

+

{message.redactedText}

{message.redactedItems?.length > 0 && message.redactedText && ( -

+

{message.redactedText.includes('XXX') && ( - <> {t('homepage.chat.messages.privacyMessage')} + <> + {' '} + {t('homepage.chat.messages.privacyMessage')} + )} - {message.redactedText.includes('###') && + {message.redactedText.includes('###') && t('homepage.chat.messages.blockedMessage')}

)} @@ -192,16 +212,16 @@ const ChatInterface = ({ ) : ( formatAIResponse(message.text, message.aiService, message.id) )} - {message.id === messages.length - 1 && - showFeedback && - !message.error && - !message.text.includes('') && ( - - )} + {message.id === messages.length - 1 && + showFeedback && + !message.error && + !message.text.includes('') && ( + + )} )}
@@ -212,26 +232,24 @@ const ChatInterface = ({
- {displayStatus === 'thinkingWithContext' ? - `${t('homepage.chat.messages.thinkingWithContext')}: ${currentDepartment} - ${currentTopic}` : - t(`homepage.chat.messages.${displayStatus}`) - } + {displayStatus === 'thinkingWithContext' + ? `${t('homepage.chat.messages.thinkingWithContext')}: ${currentDepartment} - ${currentTopic}` + : t(`homepage.chat.messages.${displayStatus}`)}
-   + +   {t('homepage.chat.input.loadingHint')}
)} - + {turnCount >= MAX_CONVERSATION_TURNS && (

{t('homepage.chat.messages.limitReached', { count: MAX_CONVERSATION_TURNS })}

-
@@ -239,66 +257,75 @@ const ChatInterface = ({ )}
- {turnCount < MAX_CONVERSATION_TURNS && ( -
- {!isLoading && ( -
-
- - -   - {t('homepage.chat.input.hint')} - -
-
+ Date + User Query + Response Length