diff --git a/application/frontend/src/pages/chatbot/chatbot.scss b/application/frontend/src/pages/chatbot/chatbot.scss index 3eb0f12e9..7e745c4ed 100644 --- a/application/frontend/src/pages/chatbot/chatbot.scss +++ b/application/frontend/src/pages/chatbot/chatbot.scss @@ -1,78 +1,193 @@ -// #SearchBar { -// margin: auto; -// padding-bottom: 7px; -// } - -// #SearchButton{ -// margin: auto; -// padding-bottom: 7px; -// } - -// .search-page { -// background-color: #242e4c; -// display: flex; -// align-items: center; -// flex-direction: column; -// justify-content: center; -// padding-top: 40px; -// padding-bottom: 40px; -// min-height: 100%; -// } - -// // mobile -// @media (min-width: 0px) and (max-width: 599px) { -// .search-page { -// padding-top: 30px; -// padding-bottom: 30px; -// } -// } - - -// .chat-container { -// width: 1000px; -// display: flex; -// flex-direction: column; -// margin-top: 5rem; -// } - -// .chat-messages { -// background-color: #e3f2fd; -// border: .5px solid #4f4f4f; /* Added for better visibility */ -// border-radius: 10px; -// box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); -// padding: 1rem; -// overflow-y: auto; -// max-height: 1000px; -// margin-bottom: 1rem; -// } +.chat-container { + margin-top: 1.25rem; + max-width: 960px; + margin: 3rem auto; +} -.chat-input { - background-color: #c8e6c9; - border-radius: 10px; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); - padding: 1rem; - // display: flex; - // justify-content: space-between; +.chat-messages { + display: flex; + flex-direction: column; + gap: 1.25rem; +} +h1.ui.header { + margin-bottom: 1rem !important; +} +.chat-message { + display: flex; +} + +.chat-message.user { + justify-content: flex-end; } +.chat-message.assistant { + justify-content: flex-start; +} .message-card { - background-color: white; - border-radius: 10px; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); - padding: 1rem; - margin-bottom: 1rem; - width: 100%; - height: 100%; + max-width: 65%; + background: #ffffff; + border-radius: 16px; + padding: 1rem 1.25rem; + box-shadow: 0 6px 18px rgba(0, 0, 0, 0.08); + line-height: 1.6; + animation: fadeInUp 0.25s ease-out; } -.user .message-card { - margin-left: auto; - background-color: #e3f2fd; - height: 100%; +.chat-message.user .message-card { + background: #e3f2fd; +} + +.chat-message.assistant .message-card { + background: #f1f8e9; +} + +.message-header { + display: flex; + justify-content: space-between; + font-size: 0.7rem; + margin-bottom: 0.4rem; + color: #666; +} + +.message-role { + font-weight: 600; + text-transform: capitalize; +} + +.message-timestamp { + opacity: 0.7; +} + +.message-body { + font-size: 0.95rem; + + p { + margin: 0.5rem 0; + } + + ul { + padding-left: 1.25rem; + } + + code { + background: #f4f4f4; + padding: 0.2rem 0.4rem; + border-radius: 4px; + font-size: 0.85rem; + } +} + +.references { + margin-top: 0.75rem; + border-top: 1px solid #e0e0e0; + padding-top: 0.5rem; +} + +.references-title { + font-size: 0.75rem; + font-weight: 600; + margin-bottom: 0.25rem; + color: #444; } -.assistant .message-card { +.reference-card { + font-size: 0.8rem; + margin-bottom: 0.25rem; + + a { + color: #1976d2; + text-decoration: none; + } + + a:hover { + text-decoration: underline; + } +} + +.reference-link { + font-size: 0.7rem; + opacity: 0.8; +} + +.accuracy-warning { + margin-top: 0.75rem; + font-size: 0.75rem; + color: #b71c1c; +} + +.chat-input { + margin-top: 1.5rem; + background-color: #d3ead4; + padding: 1rem; + border-radius: 12px; + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05); +} +.chat-input .ui.input input { + border-radius: 10px !important; +} +.chatbot-disclaimer { + margin-top: 2.5rem; + font-size: 1.01rem; + color: #333; + max-width: 900px; + margin-left: auto; margin-right: auto; - background-color: #c8e6c9; + line-height: 1.6; +} + +/* AI typing indicator bubble */ +.typing-indicator { + display: flex; + gap: 0.4rem; + align-items: center; + min-height: 32px; + padding: 0.75rem 1rem; +} +.typing-indicator .dot { + width: 8px; + height: 8px; + background-color: #21ba45; + border-radius: 50%; + animation: typingBounce 1.4s infinite ease-in-out both; +} + +.typing-indicator .dot:nth-child(2) { + animation-delay: 0.2s; +} + +.typing-indicator .dot:nth-child(3) { + animation-delay: 0.4s; +} +.chat-message.user .message-card { + background: #eaf4ff; + border-left: 4px solid #2185d0; +} + +.chat-message.assistant .message-card { + background: #f9fafb; + border-left: 4px solid #21ba45; +} +@keyframes typingBounce { + 0%, + 80%, + 100% { + transform: scale(0); + opacity: 0.3; + } + 40% { + transform: scale(1); + opacity: 1; + } +} + +@keyframes fadeInUp { + from { + opacity: 0; + transform: translateY(6px); + } + to { + opacity: 1; + transform: translateY(0); + } } diff --git a/application/frontend/src/pages/chatbot/chatbot.tsx b/application/frontend/src/pages/chatbot/chatbot.tsx index a0ee25992..1f305b1e8 100644 --- a/application/frontend/src/pages/chatbot/chatbot.tsx +++ b/application/frontend/src/pages/chatbot/chatbot.tsx @@ -5,7 +5,7 @@ import { marked } from 'marked'; import React, { useState } from 'react'; import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; import { oneLight } from 'react-syntax-highlighter/dist/esm/styles/prism'; -import { Button, Comment, Container, Form, GridRow, Header, Icon } from 'semantic-ui-react'; +import { Button, Container, Form, GridRow, Header, Icon } from 'semantic-ui-react'; import { Grid } from 'semantic-ui-react'; import { LoadingAndErrorIndicator } from '../../components/LoadingAndErrorIndicator'; @@ -20,28 +20,23 @@ export const Chatbot = () => { data: Document[] | null; accurate: boolean; }; + interface ChatState { term: string; error: string; } - interface ResponseMessagePart { - iscode: boolean; - message: string; - } + const DEFAULT_CHAT_STATE: ChatState = { term: '', error: '' }; const { apiUrl } = useEnvironment(); const [loading, setLoading] = useState(false); - const [chatMessages, setChatMessages] = useState([]); const [error, setError] = useState(''); const [chat, setChat] = useState(DEFAULT_CHAT_STATE); const [user, setUser] = useState(''); function login() { - fetch(`${apiUrl}/user`, { - method: 'GET', - }) + fetch(`${apiUrl}/user`, { method: 'GET' }) .then((response) => { if (response.status === 200) { response.text().then((user) => setUser(user)); @@ -56,35 +51,45 @@ export const Chatbot = () => { }); } - function processResponse(response) { + function processResponse(response: string) { const responses = response.split('```'); - let i = 0; - const res = [<>]; - for (const txt of responses) { - if (i % 2 == 0) { + const res: JSX.Element[] = []; + + responses.forEach((txt, i) => { + if (i % 2 === 0) { res.push(

); } else { - res.push({txt}); + res.push( + + {txt} + + ); } - i++; - } + }); + return res; } function onSubmit() { + if (!chat.term.trim()) return; + + const currentTerm = chat.term; + setChat({ ...chat, term: '' }); setLoading(true); - setChatMessages((chatMessages) => [ - ...chatMessages, + + setChatMessages((prev) => [ + ...prev, { timestamp: new Date().toLocaleTimeString(), role: 'user', - message: chat.term, + message: currentTerm, data: [], accurate: true, }, @@ -92,17 +97,15 @@ export const Chatbot = () => { fetch(`${apiUrl}/completion`, { method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ prompt: chat.term }), + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ prompt: currentTerm }), // ✅ use captured term }) .then((response) => response.json()) .then((data) => { setLoading(false); setError(''); - setChatMessages((chatMessages) => [ - ...chatMessages, + setChatMessages((prev) => [ + ...prev, { timestamp: new Date().toLocaleTimeString(), role: 'assistant', @@ -120,126 +123,112 @@ export const Chatbot = () => { } function displayDocument(d: Document) { - if (d === null || d.doctype === null) { - return

{d}

; - } - var link = '/node/' + d.doctype.toLowerCase() + '/' + d.name; - if (d.section) { - link = link + '/section/' + d.section; - } else { - link = link + '/sectionid/' + d.sectionID; - } + if (!d || !d.doctype) return null; + + let link = `/node/${d.doctype.toLowerCase()}/${d.name}`; + link += d.section ? `/section/${d.section}` : `/sectionid/${d.sectionID}`; + return ( -

-

- *Reference: The above answer used as preferred input: - - {' '} - {d.name} section: {d.section ? d.section : d.sectionID}; - -

-

- You can find more information about this section of {d.name} on its OpenCRE page -

-

+
+ + {d.name} — section {d.section ?? d.sectionID} + + +
); } return ( <> - {user != '' ? '' : login()} - + {user !== '' ? null : login()} + + {/* */} + - +
OWASP OpenCRE Chat
+ - - - -
- {chatMessages.map((m) => ( -
- - - - {m.role} - - {m.timestamp} - - {processResponse(m.message)} - {m.data - ? m.data?.map((m2) => { - return displayDocument(m2); - }) - : ''} - {m.accurate ? ( - '' - ) : ( - - Note: The content of OpenCRE could not be used to answer your question, as - no matching standard was found. The answer therefore has no reference and - needs to be regarded as less reliable. Try rephrasing your question, use - similar topics, or OpenCRE search. - - )} - - - +
+ {error && ( +
+
Document could not be loaded
+
+ )} +
+ {chatMessages.map((m, idx) => ( +
+
+
+ {m.role} + {m.timestamp}
- ))} + +
{processResponse(m.message)}
+ + {m.data && m.data.length > 0 && ( +
+
References
+ {m.data.map((d, i) => ( + {displayDocument(d)} + ))} +
+ )} + + {!m.accurate && ( +
+ This answer could not be fully verified against OpenCRE sources. Please validate + independently. +
+ )} +
- - - - -
- { - setChat({ - ...chat, - term: e.target.value, - }); - }} - placeholder="Type your infosec question here..." - /> - - -
-
-
-
-
-
- - Answers are generated by a Google PALM2 Large Language Model, which uses the internet as - training data, plus collected key cybersecurity standards from{' '} - OpenCRE as the preferred source. This leads to more - reliable answers and adds references, but note: it is still generative AI which is never - guaranteed correct. -
-
- Model operation is generously sponsored by{' '} - Software Improvement Group. -
-
- Privacy & Security: Your question is sent to Heroku, the hosting provider for OpenCRE, and - then to GCP, all via protected connections. Your data isn't stored on OpenCRE servers. The - OpenCRE team employed extensive measures to ensure privacy and security. To review the code: - https://github.com/owasp/OpenCRE -
+ ))} + {loading && ( +
+
+ + + +
+
+ )}
- + +
+ setChat({ ...chat, term: e.target.value })} + placeholder="Type your infosec question here…" + /> + + +
+ +
+ + Answers are generated by a Google PALM2 Large Language Model, which uses the internet as + training data, plus collected key cybersecurity standards from{' '} + OpenCRE as the preferred source. This leads to more reliable + answers and adds references, but note: it is still generative AI which is never guaranteed + correct. +
+
+ Model operation is generously sponsored by{' '} + Software Improvement Group. +
+
+ Privacy & Security: Your question is sent to Heroku, the hosting provider for OpenCRE, and + then to GCP, all via protected connections. Your data isn't stored on OpenCRE servers. The + OpenCRE team employed extensive measures to ensure privacy and security. To review the code: + https://github.com/owasp/OpenCRE +
+