@neo4j-ndl/react
Version:
React implementation of Neo4j Design System
160 lines (157 loc) • 10.4 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.Component = void 0;
const jsx_runtime_1 = require("react/jsx-runtime");
/**
*
* Copyright (c) "Neo4j"
* Neo4j Sweden AB [http://neo4j.com]
*
* This file is part of Neo4j.
*
* Neo4j is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
const react_1 = require("@neo4j-ndl/react");
const ai_1 = require("@neo4j-ndl/react/ai");
const icons_1 = require("@neo4j-ndl/react/icons");
const react_2 = require("react");
const FAKE_RESPONSES = [
`Here is a simple response with some **bold text** and *italics*.`,
`Here is a list of items:
- Item 1
- Item 2
- Item 3`,
`Here is a code block example:
\`\`\`typescript
const greeting = "Hello World";
console.log(greeting);
\`\`\`
`,
`# Heading 1
## Heading 2
### Heading 3
Some text under headings.`,
`You can also use tables:
| Header 1 | Header 2 |
|Data 1|Data 2|
|Data 3|Data 4|
`,
];
const Component = () => {
const [messages, setMessages] = (0, react_2.useState)([]);
const [prompt, setPrompt] = (0, react_2.useState)('');
const [isThinking, setIsThinking] = (0, react_2.useState)(false);
const [isStreaming, setIsStreaming] = (0, react_2.useState)(false);
const [responseIndex, setResponseIndex] = (0, react_2.useState)(0);
const messagesEndRef = (0, react_2.useRef)(null);
const timeoutRef = (0, react_2.useRef)(null);
const intervalRef = (0, react_2.useRef)(null);
const scrollToBottom = () => {
var _a;
(_a = messagesEndRef.current) === null || _a === void 0 ? void 0 : _a.scrollIntoView({ behavior: 'smooth' });
};
(0, react_2.useEffect)(() => {
scrollToBottom();
}, [messages, isThinking]);
const handleCancel = () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
if (intervalRef.current) {
clearInterval(intervalRef.current);
}
setIsThinking(false);
setIsStreaming(false);
// Mark the last message as done if stopped
setMessages((prev) => {
const newMessages = [...prev];
const lastMessage = newMessages[newMessages.length - 1];
if ((lastMessage === null || lastMessage === void 0 ? void 0 : lastMessage.role) === 'assistant') {
lastMessage.done = true;
}
return newMessages;
});
};
const handleSend = (overridePrompt) => {
const textToSend = overridePrompt || prompt;
if (!textToSend.trim()) {
return;
}
setMessages((prev) => [...prev, { content: textToSend, role: 'user' }]);
setPrompt('');
setIsThinking(true);
const startTime = Date.now();
// Simulate network delay (thinking time)
timeoutRef.current = setTimeout(() => {
const endTime = Date.now();
const thinkingTime = endTime - startTime;
setIsThinking(false);
setIsStreaming(true);
const responseText = FAKE_RESPONSES[responseIndex];
setResponseIndex((prev) => (prev + 1) % FAKE_RESPONSES.length);
let currentText = '';
setMessages((prev) => [
...prev,
{ content: '', done: false, role: 'assistant', thinkingTime },
]);
// Simulate streaming
intervalRef.current = setInterval(() => {
if (currentText.length < responseText.length) {
// Add a few characters at a time to simulate chunks
// Ensure we don't split newlines incorrectly if that's an issue,
// but simple slicing should be fine as long as the source has \n.
const chunk = responseText.slice(currentText.length, currentText.length + 2);
currentText += chunk;
setMessages((prev) => {
const newMessages = [...prev];
const lastMessage = newMessages[newMessages.length - 1];
if (lastMessage.role === 'assistant') {
lastMessage.content = currentText;
}
return newMessages;
});
}
else {
if (intervalRef.current) {
clearInterval(intervalRef.current);
}
setIsStreaming(false);
setMessages((prev) => {
const newMessages = [...prev];
const lastMessage = newMessages[newMessages.length - 1];
if (lastMessage.role === 'assistant') {
lastMessage.done = true;
}
return newMessages;
});
}
}, 50);
}, 2000);
};
return ((0, jsx_runtime_1.jsx)("section", { className: "n-h-screen", children: (0, jsx_runtime_1.jsxs)("div", { className: "n-w-[440px] n-h-full n-flex n-flex-col n-bg-neutral-bg-weak", children: [(0, jsx_runtime_1.jsx)("div", { className: "n-flex n-flex-row n-border-b n-border-neutral-border-weak n-p-3", children: (0, jsx_runtime_1.jsxs)("div", { className: "n-ml-auto", children: [(0, jsx_runtime_1.jsx)(react_1.CleanIconButton, { description: "settings", tooltipProps: {}, children: (0, jsx_runtime_1.jsx)(icons_1.Cog6ToothIconOutline, {}) }), (0, jsx_runtime_1.jsx)(react_1.CleanIconButton, { description: "close", children: (0, jsx_runtime_1.jsx)(icons_1.XMarkIconOutline, {}) })] }) }), (0, jsx_runtime_1.jsx)("div", { className: "n-p-4 n-flex n-flex-col n-grow n-overflow-y-auto", children: messages.length === 0 ? ((0, jsx_runtime_1.jsx)("div", { className: "n-flex n-flex-col ", children: (0, jsx_runtime_1.jsxs)("div", { className: "n-flex n-flex-col n-gap-12", children: [(0, jsx_runtime_1.jsx)(react_1.Typography, { variant: "display", children: "Hi [User], how can I help you today?" }), (0, jsx_runtime_1.jsxs)("div", { className: "n-flex n-flex-col n-gap-4", children: [(0, jsx_runtime_1.jsx)(react_1.Typography, { variant: "body-medium", children: "Suggestions" }), (0, jsx_runtime_1.jsx)(ai_1.Suggestion, { isPrimary: true, onClick: () => {
handleSend('I want to import data');
}, children: "I want to import data" }), (0, jsx_runtime_1.jsx)(ai_1.Suggestion, { onClick: () => {
handleSend('Create an AI agent');
}, children: "Create an AI agent" }), (0, jsx_runtime_1.jsx)(ai_1.Suggestion, { onClick: () => {
handleSend('Invite project members');
}, children: "Invite project members" }), (0, jsx_runtime_1.jsx)(ai_1.Suggestion, { onClick: () => {
handleSend('Generate a report');
}, children: "Generate a report" })] }), (0, jsx_runtime_1.jsxs)(react_1.Typography, { variant: "body-medium", children: ["You can also drag and drop files here, or", ' ', (0, jsx_runtime_1.jsx)(react_1.TextLink, { as: "button", type: "internal-underline", children: "browse" }), ". Supports CVG, MOV, PDF"] })] }) })) : ((0, jsx_runtime_1.jsxs)("div", { className: "n-flex n-flex-col n-gap-4 n-pb-4", children: [messages.map((msg, idx) => ((0, jsx_runtime_1.jsx)("div", { className: `n-flex ${msg.role === 'user' ? 'n-justify-end' : 'n-justify-start'}`, children: msg.role === 'user' ? ((0, jsx_runtime_1.jsx)("div", { className: "n-max-w-[85%]", children: (0, jsx_runtime_1.jsx)(ai_1.UserBubble, { avatarProps: {
name: 'NM',
type: 'letters',
}, children: msg.content }) })) : ((0, jsx_runtime_1.jsxs)("div", { className: "n-w-full n-flex n-flex-col n-gap-2", children: [msg.thinkingTime !== undefined && ((0, jsx_runtime_1.jsx)(ai_1.Thinking, { isThinking: false, thinkingMs: msg.thinkingTime })), (0, jsx_runtime_1.jsxs)("div", { className: "n-flex n-flex-col n-gap-2", children: [(0, jsx_runtime_1.jsx)(ai_1.Response, { children: msg.content }), msg.done === true && ((0, jsx_runtime_1.jsxs)("div", { className: "n-flex n-flex-row n-gap-1.5", children: [(0, jsx_runtime_1.jsx)(react_1.CleanIconButton, { size: "small", description: "Dislike", children: (0, jsx_runtime_1.jsx)(icons_1.HandThumbDownIconOutline, {}) }), (0, jsx_runtime_1.jsx)(react_1.CleanIconButton, { size: "small", description: "Re-run", children: (0, jsx_runtime_1.jsx)(icons_1.ArrowPathIconOutline, {}) }), (0, jsx_runtime_1.jsx)(react_1.CleanIconButton, { size: "small", description: "Copy", children: (0, jsx_runtime_1.jsx)(icons_1.Square2StackIconOutline, {}) })] }))] })] })) }, idx))), isThinking && (0, jsx_runtime_1.jsx)(ai_1.Thinking, { isThinking: true }), (0, jsx_runtime_1.jsx)("div", { ref: messagesEndRef })] })) }), (0, jsx_runtime_1.jsx)("div", { className: "n-px-4 n-pt-4 n-pb-1 n-mt-auto", children: (0, jsx_runtime_1.jsx)(ai_1.Prompt, { value: prompt, onChange: (e) => setPrompt(e.target.value), onSubmitPrompt: () => handleSend(), onCancelPrompt: handleCancel, isRunningPrompt: isThinking || isStreaming, isSubmitDisabled: prompt.length === 0 && !(isThinking || isStreaming), bottomContent: (0, jsx_runtime_1.jsx)(react_1.CleanIconButton, { description: "Add files", size: "small", children: (0, jsx_runtime_1.jsx)(icons_1.PlusIconOutline, {}) }) }) })] }) }));
};
exports.Component = Component;
exports.default = exports.Component;
//# sourceMappingURL=response-full-example.story.js.map