UNPKG

@neo4j-ndl/react

Version:

React implementation of Neo4j Design System

1,110 lines (866 loc) 23.9 kB
# Response Import: `import { Response } from '@neo4j-ndl/react/ai'` ## Props ### Response | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `children` | `string` | ✅ | | The response from the AI | | `isAnimating` | `boolean` | | | Whether the response is animating | | `ref` | `Ref<HTMLDivElement>` | | | A ref to apply to the root element. | ## Examples ### Default ```tsx import { Response } from '@neo4j-ndl/react/ai'; const markdownContent = ` # Hello World This is a **bold** text and this is an *italic* text. Here is a list: - Item 1 - Item 2 - Item 3 And a code block: \`\`\`typescript const greeting = "Hello World"; console.log(greeting); \`\`\` [Link to Neo4j](https://neo4j.com) `; export const Component = () => { return <Response>{markdownContent}</Response>; }; export default Component; ``` ### All Components ```tsx import { Response } from '@neo4j-ndl/react/ai'; const comprehensiveMarkdown = ` # All Markdown Components Demo This story demonstrates all the markdown components that the Response component can render. ## Headings # Heading 1 ## Heading 2 ### Heading 3 #### Heading 4 ##### Heading 5 ###### Heading 6 ## Text Formatting This is **bold text** and this is *italic text*. You can also use __bold__ and _italic_ with underscores. Combine them: **bold and *italic* together**. ## Lists ### Unordered Lists - Item 1 - Item 2 - Item 3 - Nested item 1 - Nested item 2 - Deeply nested item - <h1>Nested nested heading</h1> ### Ordered Lists 1. First item 2. Second item 3. Third item 1. Nested numbered item 2. Another nested item 3. <h2>Nested nested heading</h2> ## Code ### Inline Code Use \`const x = 5\` for inline code. ### Code Blocks #### JavaScript \`\`\`javascript const greeting = "Hello World"; console.log(greeting); function sayHello(name) { return \`Hello, \${name}!\`; } \`\`\` #### Python \`\`\`python def greet(name): return f"Hello, {name}!" print(greet("World")) \`\`\` #### Cypher \`\`\`cypher MATCH (n:Person)-[:KNOWS]->(m:Person) WHERE n.name = "Alice" RETURN m.name \`\`\` #### TypeScript \`\`\`typescript interface User { name: string; age: number; } const user: User = { name: "John", age: 30 }; \`\`\` #### Without Language Specification \`\`\` Plain code block without syntax highlighting \`\`\` ## Links Visit [Neo4j](https://neo4j.com) for more information. Multiple links: [Documentation](https://neo4j.com/docs) and [Community](https://community.neo4j.com) ## Blockquotes > This is a blockquote. > It can span multiple lines. > ### Blockquote with heading > > You can also include other markdown inside blockquotes. > > - Like lists > - And other elements ### Nested Blockquotes > Outer quote >> Nested quote >>> Deeply nested quote ## Horizontal Rules Content above the rule --- Content below the rule ## Images ![A smiling man](https://media.istockphoto.com/id/1334716681/photo/a-smiling-man.jpg?s=612x612&w=0&k=20&c=U6rkSDpQMzkcJEqx2hAa63fNLIhqnZb31Xuc_QSi648=) ## Paragraphs This is a paragraph with multiple sentences. It demonstrates how regular text is rendered. You can write long-form content that spans multiple lines. Another paragraph here. Paragraphs are separated by blank lines in markdown. ## Mixed Content Example Here's a practical example combining multiple elements: ### Database Query Tutorial To query your Neo4j database, follow these steps: 1. **Connect to your database** Use the following code: \`\`\`javascript const driver = neo4j.driver( 'bolt://localhost:7687', neo4j.auth.basic('neo4j', 'password') ); \`\`\` 2. **Write your Cypher query** \`\`\`cypher MATCH (n:Movie) WHERE n.released > 2000 RETURN n.title, n.released ORDER BY n.released DESC LIMIT 10 \`\`\` 3. **Execute and process results** > **Note**: Always close your session after use! For more information, visit the [Neo4j Documentation](https://neo4j.com/docs). --- ## Special Characters and Escaping You can escape special characters: \\* \\_ \\[ \\] ## Preformatted Text Use code blocks for preformatted text with preserved spacing: \`\`\` This text has preserved indentation \`\`\` | Item | In Stock | Price | | :---------------- | :------: | ----: | | Python Hat | True | 222222223.99 | | SQL Hat | True | 23.99 | | Codecademy Tee | False | 19.99 | | Codecademy Hoodie | False | 42.99 | `; export const Component = () => { return ( <div className="max-w-4xl"> <Response>{comprehensiveMarkdown}</Response> </div> ); }; export default Component; ``` ### Code Examples ```tsx import { Response } from '@neo4j-ndl/react/ai'; const codeExamplesMarkdown = ` # Code Examples This story focuses on different code rendering capabilities. ## Multiple Programming Languages ### JavaScript \`\`\`javascript // Array methods const numbers = [1, 2, 3, 4, 5]; const doubled = numbers.map(n => n * 2); const sum = numbers.reduce((acc, n) => acc + n, 0); console.log('Doubled:', doubled); console.log('Sum:', sum); \`\`\` ### TypeScript \`\`\`typescript // Generics function identity<T>(arg: T): T { return arg; } const result = identity<string>("Hello"); const num = identity(42); \`\`\` ### Python \`\`\`python # List comprehension squares = [x**2 for x in range(10)] evens = [x for x in range(20) if x % 2 == 0] # Dictionary comprehension square_dict = {x: x**2 for x in range(5)} print(f"Squares: {squares}") print(f"Evens: {evens}") \`\`\` ### Cypher \`\`\`cypher // Create nodes and relationships CREATE (alice:Person {name: 'Alice', age: 30}) CREATE (bob:Person {name: 'Bob', age: 25}) CREATE (alice)-[:KNOWS {since: 2020}]->(bob) RETURN alice, bob \`\`\` ### SQL \`\`\`sql -- Complex query SELECT u.name, COUNT(o.id) as order_count, SUM(o.total) as total_spent FROM users u LEFT JOIN orders o ON u.id = o.user_id WHERE u.active = true GROUP BY u.id, u.name HAVING COUNT(o.id) > 5 ORDER BY total_spent DESC LIMIT 10; \`\`\` ### Rust \`\`\`rust // Pattern matching fn describe_number(n: i32) -> String { match n { 0 => "zero".to_string(), 1..=10 => "small".to_string(), 11..=100 => "medium".to_string(), _ => "large".to_string(), } } \`\`\` ### Go \`\`\`go // Goroutines and channels func worker(id int, jobs <-chan int, results chan<- int) { for j := range jobs { fmt.Printf("worker %d processing job %d\\n", id, j) results <- j * 2 } } \`\`\` ### JSON \`\`\`json { "name": "Response Component", "version": "1.0.0", "features": [ "markdown rendering", "syntax highlighting", "streaming support" ], "config": { "theme": "dark", "lineNumbers": true } } \`\`\` ### YAML \`\`\`yaml # Configuration file app: name: MyApp version: 1.0.0 port: 3000 database: host: localhost port: 5432 credentials: username: admin password: secret features: - authentication - logging - caching \`\`\` ### Shell/Bash \`\`\`bash #!/bin/bash # Deploy script echo "Starting deployment..." npm install npm run build npm test if [ $? -eq 0 ]; then echo "Deployment successful!" else echo "Deployment failed!" exit 1 fi \`\`\` ## Inline Code You can use inline code like \`const x = 5\`, \`npm install\`, or \`SELECT * FROM users\` within sentences. Mixed with text: The \`Array.map()\` method creates a new array and the \`filter()\` method filters elements. ## Code Without Language \`\`\` Plain text code block No syntax highlighting Just monospace font \`\`\` ## Long Code Blocks \`\`\`javascript // Complex React component import React, { useState, useEffect, useCallback } from 'react'; const UserDashboard = ({ userId }) => { const [user, setUser] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const fetchUser = useCallback(async () => { try { setLoading(true); const response = await fetch(\`/api/users/\${userId}\`); if (!response.ok) throw new Error('Failed to fetch user'); const data = await response.json(); setUser(data); } catch (err) { setError(err.message); } finally { setLoading(false); } }, [userId]); useEffect(() => { fetchUser(); }, [fetchUser]); if (loading) return <div>Loading...</div>; if (error) return <div>Error: {error}</div>; if (!user) return <div>No user found</div>; return ( <div className="dashboard"> <h1>{user.name}</h1> <p>Email: {user.email}</p> <button onClick={fetchUser}>Refresh</button> </div> ); }; export default UserDashboard; \`\`\` `; export const Component = () => { return ( <div className="max-w-4xl"> <Response>{codeExamplesMarkdown}</Response> </div> ); }; export default Component; ``` ### Full Example ```tsx 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< { role: 'user' | 'assistant'; content: string; thinkingTime?: number; done?: boolean; }[] >([]); const [prompt, setPrompt] = useState(''); const [isThinking, setIsThinking] = useState(false); const [isStreaming, setIsStreaming] = useState(false); const [responseIndex, setResponseIndex] = useState(0); const messagesEndRef = useRef<HTMLDivElement>(null); const timeoutRef = useRef<NodeJS.Timeout | null>(null); const intervalRef = useRef<NodeJS.Timeout | null>(null); const scrollToBottom = () => { messagesEndRef.current?.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?.role === 'assistant') { lastMessage.done = true; } return newMessages; }); }; const handleSend = (overridePrompt?: string) => { 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 ( <section className="n-h-screen"> <div className="n-w-[440px] n-h-full n-flex n-flex-col n-bg-neutral-bg-weak"> <div className="n-flex n-flex-row n-border-b n-border-neutral-border-weak n-p-3"> <div className="n-ml-auto"> <CleanIconButton description="settings" tooltipProps={{}}> <Cog6ToothIconOutline /> </CleanIconButton> <CleanIconButton description="close"> <XMarkIconOutline /> </CleanIconButton> </div> </div> <div className="n-p-4 n-flex n-flex-col n-grow n-overflow-y-auto"> {messages.length === 0 ? ( <div className="n-flex n-flex-col "> <div className="n-flex n-flex-col n-gap-12"> <Typography variant="display"> Hi [User], how can I help you today? </Typography> <div className="n-flex n-flex-col n-gap-4"> <Typography variant="body-medium">Suggestions</Typography> <Suggestion isPrimary onClick={() => { handleSend('I want to import data'); }} > I want to import data </Suggestion> <Suggestion onClick={() => { handleSend('Create an AI agent'); }} > Create an AI agent </Suggestion> <Suggestion onClick={() => { handleSend('Invite project members'); }} > Invite project members </Suggestion> <Suggestion onClick={() => { handleSend('Generate a report'); }} > Generate a report </Suggestion> </div> <Typography variant="body-medium"> You can also drag and drop files here, or{' '} <TextLink as="button" type="internal-underline"> browse </TextLink> . Supports CVG, MOV, PDF </Typography> </div> </div> ) : ( <div className="n-flex n-flex-col n-gap-4 n-pb-4"> {messages.map((msg, idx) => ( <div key={idx} className={`n-flex ${ msg.role === 'user' ? 'n-justify-end' : 'n-justify-start' }`} > {msg.role === 'user' ? ( <div className="n-max-w-[85%]"> <UserBubble avatarProps={{ name: 'NM', type: 'letters', }} > {msg.content} </UserBubble> </div> ) : ( <div className="n-w-full n-flex n-flex-col n-gap-2"> {msg.thinkingTime !== undefined && ( <Thinking isThinking={false} thinkingMs={msg.thinkingTime} /> )} <div className="n-flex n-flex-col n-gap-2"> <Response>{msg.content}</Response> {msg.done === true && ( <div className="n-flex n-flex-row n-gap-1.5"> <CleanIconButton size="small" description="Dislike"> <HandThumbDownIconOutline /> </CleanIconButton> <CleanIconButton size="small" description="Re-run"> <ArrowPathIconOutline /> </CleanIconButton> <CleanIconButton size="small" description="Copy"> <Square2StackIconOutline /> </CleanIconButton> </div> )} </div> </div> )} </div> ))} {isThinking && <Thinking isThinking={true} />} <div ref={messagesEndRef} /> </div> )} </div> <div className="n-px-4 n-pt-4 n-pb-1 n-mt-auto"> <Prompt value={prompt} onChange={(e) => setPrompt(e.target.value)} onSubmitPrompt={() => handleSend()} onCancelPrompt={handleCancel} isRunningPrompt={isThinking || isStreaming} isSubmitDisabled={ prompt.length === 0 && !(isThinking || isStreaming) } bottomContent={ <CleanIconButton description="Add files" size="small"> <PlusIconOutline /> </CleanIconButton> } /> </div> </div> </section> ); }; export default Component; ``` ### Headings ```tsx import { Response } from '@neo4j-ndl/react/ai'; const headingsMarkdown = ` # Heading Level 1 This is content under heading 1. ## Heading Level 2 This is content under heading 2. ### Heading Level 3 This is content under heading 3. #### Heading Level 4 This is content under heading 4. ##### Heading Level 5 This is content under heading 5. ###### Heading Level 6 This is content under heading 6. --- ## Document Structure Example # Main Document Title Brief introduction to the document. ## Introduction This section provides an overview of what we'll cover. ### Background Some background information about the topic. #### Historical Context Details about the history. ##### Key Events - Event 1 - Event 2 - Event 3 ###### Notes Additional notes and references. ## Content Section Main content goes here. ### Subsection A Details for subsection A. ### Subsection B Details for subsection B. ## Conclusion Final thoughts and summary. --- ## Headings with Other Elements ### Code Example \`\`\`javascript console.log("Hello from heading section"); \`\`\` ### List Example - Item 1 - Item 2 - Item 3 ### Quote Example > "This is a quote under a heading." ### Mixed Content Regular paragraph with **bold** and *italic* text. `; export const Component = () => { return ( <div className="max-w-4xl"> <Response>{headingsMarkdown}</Response> </div> ); }; export default Component; ``` ### Lists ```tsx import { Response } from '@neo4j-ndl/react/ai'; const listsMarkdown = ` # Lists Examples ## Simple Unordered List - Apple - Banana - Cherry - Date - Elderberry ## Simple Ordered List 1. First step 2. Second step 3. Third step 4. Fourth step 5. Fifth step ## Nested Unordered Lists - Fruits - Apples - Granny Smith - Honeycrisp - Fuji - Oranges - Navel - Blood Orange - Berries - Strawberry - Blueberry - Raspberry - Vegetables - Leafy Greens - Spinach - Kale - Root Vegetables - Carrot - Potato ## Nested Ordered Lists 1. Introduction 1. Overview 2. Purpose 3. Scope 2. Main Content 1. Chapter 1 1. Section 1.1 2. Section 1.2 3. Section 1.3 2. Chapter 2 1. Section 2.1 2. Section 2.2 3. Conclusion 1. Summary 2. Recommendations ## Mixed Lists 1. Install dependencies - Node.js - npm or yarn - Git 2. Clone repository - \`git clone <url>\` - \`cd project-name\` 3. Configure settings 1. Copy \`.env.example\` to \`.env\` 2. Update configuration values: - Database URL - API keys - Port number 4. Start development server - Run \`npm install\` - Run \`npm start\` ## Lists with Paragraphs - **First Item** This is a detailed explanation of the first item. It can contain multiple sentences and provide context. - **Second Item** The second item also has a detailed explanation. This helps provide more information to the reader. - **Third Item** Each item can have as much detail as needed. ## Lists with Code - JavaScript example: \`\`\`javascript const items = ['a', 'b', 'c']; items.forEach(item => console.log(item)); \`\`\` - Python example: \`\`\`python items = ['a', 'b', 'c'] for item in items: print(item) \`\`\` ## Task Lists Representation - [x] Complete feature A - [x] Write tests - [ ] Review code - [ ] Deploy to production - [ ] Monitor metrics ## Lists with Links 1. Official Resources - [Neo4j Documentation](https://neo4j.com/docs) - [Neo4j Community](https://community.neo4j.com) - [Neo4j GitHub](https://github.com/neo4j) 2. Tutorials - [Getting Started Guide](https://neo4j.com/start) - [Cypher Query Language](https://neo4j.com/cypher) 3. Tools - Neo4j Desktop - Neo4j Browser - Neo4j Bloom ## Lists with Inline Formatting - **Bold item** with regular text - *Italic item* with regular text - \`Code item\` with regular text - Regular text with **bold**, *italic*, and \`code\` - Combination: **bold and *italic* text** together `; export const Component = () => { return ( <div className="max-w-4xl"> <Response>{listsMarkdown}</Response> </div> ); }; export default Component; ``` ### Streaming ```tsx import { Response } from '@neo4j-ndl/react/ai'; import { useEffect, useState } from 'react'; const StreamingComponent = () => { const [content, setContent] = useState(''); useEffect(() => { const fullContent = ` # Streaming Response This content is being **streamed (and here's a long long long long bold text)** to simulate an AI response. It handles: 1. Partial updates 2. Incomplete markdown 3. Code blocks \`\`\`javascript function stream() { return "streaming..."; } \`\`\` And here comes more text after the code block.`; let currentIndex = 0; const interval = setInterval(() => { if (currentIndex < fullContent.length) { setContent(fullContent.slice(0, currentIndex + 1)); currentIndex++; } else { clearInterval(interval); } }, 30); return () => clearInterval(interval); }, []); return <Response>{content}</Response>; }; export const Component = () => { return <StreamingComponent />; }; export default Component; ```