UNPKG

@neo4j-ndl/react

Version:

React implementation of Neo4j Design System

156 lines (153 loc) 9.48 kB
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