From 1267f9ff6358e45081c5b928037fad951391820d Mon Sep 17 00:00:00 2001 From: Kollipara Naga Harshitha Date: Sat, 6 Jun 2026 16:50:38 +0530 Subject: [PATCH] feat: add AI follow-up questions --- client/package-lock.json | 52 +-- client/src/main.jsx | 45 ++- client/src/styles.css | 56 +++ package-lock.json | 329 ++++++++++++++++++ server/src/routes/chatRoutes.js | 34 +- .../src/services/followUpQuestionService.js | 125 +++++++ 6 files changed, 585 insertions(+), 56 deletions(-) create mode 100644 package-lock.json create mode 100644 server/src/services/followUpQuestionService.js diff --git a/client/package-lock.json b/client/package-lock.json index ff909c1..3b56511 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -14,8 +14,7 @@ "react-dom": "^19.0.0", "recharts": "^3.8.1", "vite": "^6.0.7" - }, - "devDependencies": {} + } }, "node_modules/@babel/code-frame": { "version": "7.29.7", @@ -45,6 +44,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", @@ -868,9 +868,6 @@ "cpu": [ "arm" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -884,9 +881,6 @@ "cpu": [ "arm" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -900,9 +894,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -916,9 +907,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -932,9 +920,6 @@ "cpu": [ "loong64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -948,9 +933,6 @@ "cpu": [ "loong64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -964,9 +946,6 @@ "cpu": [ "ppc64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -980,9 +959,6 @@ "cpu": [ "ppc64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -996,9 +972,6 @@ "cpu": [ "riscv64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1012,9 +985,6 @@ "cpu": [ "riscv64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1028,9 +998,6 @@ "cpu": [ "s390x" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1044,9 +1011,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1060,9 +1024,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1326,6 +1287,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", @@ -1742,6 +1704,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -1782,6 +1745,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.6.tgz", "integrity": "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -1791,6 +1755,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.6.tgz", "integrity": "sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -1810,6 +1775,7 @@ "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.3.0.tgz", "integrity": "sha512-KQopgqFo/p/fgmAs5qz6p5RWaNAzq40WAu7fJIXnQpYxFPbJYtsJPWvGeF2rOBaY/kEuV77AVsX8TsQzKm+A/g==", "license": "MIT", + "peer": true, "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" @@ -1871,7 +1837,8 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/redux-thunk": { "version": "3.1.0", @@ -2044,6 +2011,7 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.2.tgz", "integrity": "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==", "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", diff --git a/client/src/main.jsx b/client/src/main.jsx index 5657785..44f2fa4 100644 --- a/client/src/main.jsx +++ b/client/src/main.jsx @@ -41,7 +41,7 @@ function ConfidenceBadge({ found, confidence }) { ); } -function Message({ message, isLatestBotMessage, onRegenerate, onEditPrompt }) { +function Message({ message, isLatestBotMessage, onRegenerate, onEditPrompt, onRelatedQuestion }) { const isUser = message.role === 'user'; const [vote, setVote] = useState(null); const [copied, setCopied] = useState(false); @@ -111,6 +111,21 @@ function Message({ message, isLatestBotMessage, onRegenerate, onEditPrompt }) { ) : (

{message.text}

)} + + {!isUser && message.relatedQuestions?.length > 0 && ( +
+ Related Questions: + +
+ )} {isUser ? (
@@ -366,6 +381,7 @@ function DefaultChat({ onCreateOrg }) { answerFound: data.answerFound, confidence: data.confidence, sources: data.sources, + relatedQuestions: data.relatedQuestions || [], timestamp: getTime() } ]); @@ -382,6 +398,7 @@ function DefaultChat({ onCreateOrg }) { answerFound: false, confidence: 0, sources: [], + relatedQuestions: [], timestamp: getTime() } ]); @@ -423,6 +440,7 @@ function DefaultChat({ onCreateOrg }) { answerFound: data.answerFound, confidence: data.confidence, sources: data.sources, + relatedQuestions: data.relatedQuestions || [], timestamp: getTime() } ]); @@ -438,6 +456,7 @@ function DefaultChat({ onCreateOrg }) { answerFound: false, confidence: 0, sources: [], + relatedQuestions: [], timestamp: getTime() } ]); @@ -446,10 +465,9 @@ function DefaultChat({ onCreateOrg }) { } } - async function sendMessage(event) { - if (event) event.preventDefault(); - const text = input.trim(); - if (!text || isLoading || input.length > 500) return; + async function sendQuestion(question) { + const text = String(question || '').trim(); + if (!text || isLoading || text.length > 500) return; setMessages((current) => [...current, { id: crypto.randomUUID(), role: 'user', text, timestamp: getTime() }]); setInput(''); @@ -477,6 +495,7 @@ function DefaultChat({ onCreateOrg }) { answerFound: data.answerFound, confidence: data.confidence, sources: data.sources, + relatedQuestions: data.relatedQuestions || [], timestamp: getTime() } ]); @@ -492,6 +511,7 @@ function DefaultChat({ onCreateOrg }) { answerFound: false, confidence: 0, sources: [], + relatedQuestions: [], timestamp: getTime() } ]); @@ -500,6 +520,16 @@ function DefaultChat({ onCreateOrg }) { } } + async function sendMessage(event) { + if (event) event.preventDefault(); + await sendQuestion(input); + } + + function sendRelatedQuestion(question) { + setInput(question); + sendQuestion(question); + } + function useQuickPrompt(prompt) { setInput(prompt); } @@ -583,7 +613,8 @@ function DefaultChat({ onCreateOrg }) { message={message} isLatestBotMessage={isLatestBotMessage} onRegenerate={handleRegenerate} - onEditPrompt={handleEditPrompt} + onEditPrompt={handleEditPrompt} + onRelatedQuestion={sendRelatedQuestion} /> ); })} @@ -1050,4 +1081,4 @@ function App() { if (view === 'orgChat') return setView('home')} />; } -createRoot(document.getElementById('root')).render(); \ No newline at end of file +createRoot(document.getElementById('root')).render(); diff --git a/client/src/styles.css b/client/src/styles.css index 46a5e10..558c601 100644 --- a/client/src/styles.css +++ b/client/src/styles.css @@ -301,6 +301,46 @@ button { font-weight: 700; } +.relatedQuestions { + margin-top: 16px; +} + +.relatedQuestions strong { + display: block; + margin-bottom: 8px; + color: #334155; + font-size: 0.88rem; + font-weight: 850; +} + +.relatedQuestions ul { + display: grid; + gap: 6px; + margin: 0; + padding-left: 18px; +} + +.relatedQuestions li { + color: #7c8aa1; +} + +.relatedQuestions button { + padding: 0; + border: 0; + color: #345df7; + background: transparent; + cursor: pointer; + font-size: 0.95rem; + font-weight: 750; + line-height: 1.45; + text-align: left; +} + +.relatedQuestions button:hover { + color: #655af5; + text-decoration: underline; +} + .loadingBubble { display: inline-flex; align-items: center; @@ -639,6 +679,22 @@ button { color: #94a3b8; } +[data-theme="dark"] .relatedQuestions strong { + color: #cbd5e1; +} + +[data-theme="dark"] .relatedQuestions li { + color: #64748b; +} + +[data-theme="dark"] .relatedQuestions button { + color: #93c5fd; +} + +[data-theme="dark"] .relatedQuestions button:hover { + color: #bfdbfe; +} + [data-theme="dark"] .quickPrompts button { background: #1f293d; border-color: #334155; diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..83df68e --- /dev/null +++ b/package-lock.json @@ -0,0 +1,329 @@ +{ + "name": "faq-vled-rag-chatbot", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "faq-vled-rag-chatbot", + "version": "1.0.0", + "devDependencies": { + "concurrently": "^9.1.2" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concurrently": { + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.1.tgz", + "integrity": "sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "4.1.2", + "rxjs": "7.8.2", + "shell-quote": "1.8.3", + "supports-color": "8.1.1", + "tree-kill": "1.2.2", + "yargs": "17.7.2" + }, + "bin": { + "conc": "dist/bin/concurrently.js", + "concurrently": "dist/bin/concurrently.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/shell-quote": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", + "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + } + } +} diff --git a/server/src/routes/chatRoutes.js b/server/src/routes/chatRoutes.js index 09745b3..41cb97b 100644 --- a/server/src/routes/chatRoutes.js +++ b/server/src/routes/chatRoutes.js @@ -1,5 +1,6 @@ import express from 'express'; -import { answerQuestion } from '../services/ragService.js'; +import { answerQuestion, retrieveContext } from '../services/ragService.js'; +import { generateFollowUpQuestions } from '../services/followUpQuestionService.js'; import SearchLog from '../models/SearchLog.js'; export const chatRouter = express.Router(); @@ -7,13 +8,32 @@ chatRouter.post('/', async (req, res, next) => { try { const message = req.body?.message; -// Save search -await SearchLog.create({ - query: message -}); + await SearchLog.create({ + query: message + }); + + const result = await answerQuestion(message); + let relatedQuestions = []; + const normalizedMessage = String(message || '').trim(); + + try { + if (!normalizedMessage) { + return res.json({ + ...result, + relatedQuestions + }); + } + + const retrievedDocs = await retrieveContext(message); + relatedQuestions = await generateFollowUpQuestions(retrievedDocs, message); + } catch (followUpError) { + console.error('Failed to generate follow-up questions:', followUpError); + } -const result = await answerQuestion(message); - res.json(result); + res.json({ + ...result, + relatedQuestions + }); } catch (error) { next(error); } diff --git a/server/src/services/followUpQuestionService.js b/server/src/services/followUpQuestionService.js new file mode 100644 index 0000000..efd3ea4 --- /dev/null +++ b/server/src/services/followUpQuestionService.js @@ -0,0 +1,125 @@ +import { Faq } from '../models/Faq.js'; +import normalizeQuestion from '../utils/normalizeQuestion.js'; + +const MIN_FOLLOW_UP_QUESTIONS = 3; +const MAX_FOLLOW_UP_QUESTIONS = 5; + +function getFaqFromRetrievedDoc(doc) { + return doc?.faq || doc || null; +} + +function normalizeKey(question) { + return normalizeQuestion(String(question || '')); +} + +function getQuestionCandidate(faq) { + const question = String(faq?.question || '').trim(); + if (!question) return null; + + return { + question, + category: String(faq?.category || 'General').trim() || 'General', + }; +} + +function addUniqueQuestion(candidates, seen, candidate, currentQuestionKey) { + if (!candidate) return; + + const questionKey = normalizeKey(candidate.question); + if (!questionKey || questionKey === currentQuestionKey || seen.has(questionKey)) { + return; + } + + seen.add(questionKey); + candidates.push(candidate.question); +} + +function getPreferredCategories(retrievedDocs) { + const categories = []; + const seen = new Set(); + + for (const doc of retrievedDocs) { + const faq = getFaqFromRetrievedDoc(doc); + const category = String(faq?.category || '').trim(); + const key = category.toLowerCase(); + + if (category && !seen.has(key)) { + seen.add(key); + categories.push(category); + } + } + + return categories; +} + +async function findSameCategoryQuestions(categories, excludedQuestionKeys) { + if (categories.length === 0) { + return []; + } + + const faqs = await Faq.find({ + isActive: true, + category: { $in: categories }, + }) + .select('question category') + .lean(); + + return faqs.filter((faq) => !excludedQuestionKeys.has(normalizeKey(faq.question))); +} + +export async function generateFollowUpQuestions(retrievedDocs = [], currentQuestion = '') { + const docs = Array.isArray(retrievedDocs) ? retrievedDocs : []; + const currentQuestionKey = normalizeKey(currentQuestion); + const seen = new Set(); + const relatedQuestions = []; + + if (currentQuestionKey) { + seen.add(currentQuestionKey); + } + + const retrievedFaqs = docs.map(getFaqFromRetrievedDoc).filter(Boolean); + const preferredCategories = getPreferredCategories(docs); + const preferredCategoryKeys = new Set(preferredCategories.map((category) => category.toLowerCase())); + + for (const faq of retrievedFaqs) { + addUniqueQuestion(relatedQuestions, seen, getQuestionCandidate(faq), currentQuestionKey); + + if (relatedQuestions.length >= MAX_FOLLOW_UP_QUESTIONS) { + return relatedQuestions; + } + } + + const sameCategoryFaqs = await findSameCategoryQuestions(preferredCategories, seen); + sameCategoryFaqs.sort((left, right) => { + const leftPreferred = preferredCategoryKeys.has(String(left.category || '').toLowerCase()) ? 0 : 1; + const rightPreferred = preferredCategoryKeys.has(String(right.category || '').toLowerCase()) ? 0 : 1; + return leftPreferred - rightPreferred; + }); + + for (const faq of sameCategoryFaqs) { + addUniqueQuestion(relatedQuestions, seen, getQuestionCandidate(faq), currentQuestionKey); + + if (relatedQuestions.length >= MAX_FOLLOW_UP_QUESTIONS) { + return relatedQuestions; + } + } + + if (relatedQuestions.length >= MIN_FOLLOW_UP_QUESTIONS) { + return relatedQuestions; + } + + const fallbackFaqs = await Faq.find({ isActive: true }) + .select('question category') + .limit(MAX_FOLLOW_UP_QUESTIONS * 3) + .lean(); + + for (const faq of fallbackFaqs) { + addUniqueQuestion(relatedQuestions, seen, getQuestionCandidate(faq), currentQuestionKey); + + if (relatedQuestions.length >= MAX_FOLLOW_UP_QUESTIONS) { + break; + } + } + + return relatedQuestions; +}