@neo4j-ndl/react
Version:
React implementation of Neo4j Design System
156 lines (153 loc) • 9.48 kB
JavaScript
import { jsx as _jsx, jsxs as _jsxs } from "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/>.
*/
import { CleanIconButton, TextLink, Typography } from '@neo4j-ndl/react';
import { Prompt, Response, Suggestion, Thinking, UserBubble, } from '@neo4j-ndl/react/ai';
import { ArrowPathIconOutline, Cog6ToothIconOutline, HandThumbDownIconOutline, PlusIconOutline, Square2StackIconOutline, XMarkIconOutline, } from '@neo4j-ndl/react/icons';
import { useEffect, useRef, useState } from '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|
`,
];
export const Component = () => {
const [messages, setMessages] = useState([]);
const [prompt, setPrompt] = useState('');
const [isThinking, setIsThinking] = useState(false);
const [isStreaming, setIsStreaming] = useState(false);
const [responseIndex, setResponseIndex] = useState(0);
const messagesEndRef = useRef(null);
const timeoutRef = useRef(null);
const intervalRef = useRef(null);
const scrollToBottom = () => {
var _a;
(_a = messagesEndRef.current) === null || _a === void 0 ? void 0 : _a.scrollIntoView({ behavior: 'smooth' });
};
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 (_jsx("section", { className: "n-h-screen", children: _jsxs("div", { className: "n-w-[440px] n-h-full n-flex n-flex-col n-bg-neutral-bg-weak", children: [_jsx("div", { className: "n-flex n-flex-row n-border-b n-border-neutral-border-weak n-p-3", children: _jsxs("div", { className: "n-ml-auto", children: [_jsx(CleanIconButton, { description: "settings", tooltipProps: {}, children: _jsx(Cog6ToothIconOutline, {}) }), _jsx(CleanIconButton, { description: "close", children: _jsx(XMarkIconOutline, {}) })] }) }), _jsx("div", { className: "n-p-4 n-flex n-flex-col n-grow n-overflow-y-auto", children: messages.length === 0 ? (_jsx("div", { className: "n-flex n-flex-col ", children: _jsxs("div", { className: "n-flex n-flex-col n-gap-12", children: [_jsx(Typography, { variant: "display", children: "Hi [User], how can I help you today?" }), _jsxs("div", { className: "n-flex n-flex-col n-gap-4", children: [_jsx(Typography, { variant: "body-medium", children: "Suggestions" }), _jsx(Suggestion, { isPrimary: true, onClick: () => {
handleSend('I want to import data');
}, children: "I want to import data" }), _jsx(Suggestion, { onClick: () => {
handleSend('Create an AI agent');
}, children: "Create an AI agent" }), _jsx(Suggestion, { onClick: () => {
handleSend('Invite project members');
}, children: "Invite project members" }), _jsx(Suggestion, { onClick: () => {
handleSend('Generate a report');
}, children: "Generate a report" })] }), _jsxs(Typography, { variant: "body-medium", children: ["You can also drag and drop files here, or", ' ', _jsx(TextLink, { as: "button", type: "internal-underline", children: "browse" }), ". Supports CVG, MOV, PDF"] })] }) })) : (_jsxs("div", { className: "n-flex n-flex-col n-gap-4 n-pb-4", children: [messages.map((msg, idx) => (_jsx("div", { className: `n-flex ${msg.role === 'user' ? 'n-justify-end' : 'n-justify-start'}`, children: msg.role === 'user' ? (_jsx("div", { className: "n-max-w-[85%]", children: _jsx(UserBubble, { avatarProps: {
name: 'NM',
type: 'letters',
}, children: msg.content }) })) : (_jsxs("div", { className: "n-w-full n-flex n-flex-col n-gap-2", children: [msg.thinkingTime !== undefined && (_jsx(Thinking, { isThinking: false, thinkingMs: msg.thinkingTime })), _jsxs("div", { className: "n-flex n-flex-col n-gap-2", children: [_jsx(Response, { children: msg.content }), msg.done === true && (_jsxs("div", { className: "n-flex n-flex-row n-gap-1.5", children: [_jsx(CleanIconButton, { size: "small", description: "Dislike", children: _jsx(HandThumbDownIconOutline, {}) }), _jsx(CleanIconButton, { size: "small", description: "Re-run", children: _jsx(ArrowPathIconOutline, {}) }), _jsx(CleanIconButton, { size: "small", description: "Copy", children: _jsx(Square2StackIconOutline, {}) })] }))] })] })) }, idx))), isThinking && _jsx(Thinking, { isThinking: true }), _jsx("div", { ref: messagesEndRef })] })) }), _jsx("div", { className: "n-px-4 n-pt-4 n-pb-1 n-mt-auto", children: _jsx(Prompt, { value: prompt, onChange: (e) => setPrompt(e.target.value), onSubmitPrompt: () => handleSend(), onCancelPrompt: handleCancel, isRunningPrompt: isThinking || isStreaming, isSubmitDisabled: prompt.length === 0 && !(isThinking || isStreaming), bottomContent: _jsx(CleanIconButton, { description: "Add files", size: "small", children: _jsx(PlusIconOutline, {}) }) }) })] }) }));
};
export default Component;
//# sourceMappingURL=response-full-example.story.js.map