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:
+
+ {message.relatedQuestions.map((question) => (
+ -
+
+
+ ))}
+
+
+ )}
{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;
+}