Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

feat: add chat ai first version #2500

Draft
wants to merge 8 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion website/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
"gleap": "^13.7.3",
"https-browserify": "^1.0.0",
"lodash": "^4.17.21",
"lucide-react": "^0.482.0",
"monaco-editor": "^0.44.0",
"near-api-js": "^2.1.4",
"near-social-vm": "github:NearSocial/VM#2.5.5",
Expand All @@ -54,8 +55,10 @@
"react-bootstrap-typeahead": "^6.3.2",
"react-dom": "^18.2.0",
"react-is": "^18.2.0",
"react-markdown": "^10.1.0",
"react-monaco-editor": "^0.54.0",
"sass": "^1.69.5",
"url": "^0.11.3"
"url": "^0.11.3",
"react-syntax-highlighter": "^15.6.1"
}
}
180 changes: 180 additions & 0 deletions website/src/components/AIChat/Chat.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
import '@generated/client-modules';
import React, { useState, useRef, useEffect } from 'react';
import { Button, Card, Form, InputGroup } from 'react-bootstrap';
import axios from 'axios';
import { useColorMode } from '@docusaurus/theme-common';
import MarkdownRenderer from './MarkdownRenderer';
import { Send, X } from 'lucide-react';


function splitTextIntoParts(text) {
if(!text) return [];
const regex = /(```[\s\S]*?```)/g;
return text.split(regex).filter(part => part !== '');
}

export const Chat = ({ toggleChat }) => {
const { colorMode } = useColorMode();
const [messages, setMessages] = useState([]);
const [inputMessage, setInputMessage] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [threadId, setThreadId] = useState(null);
const [seconds, setSeconds] = useState(1);
const messagesEndRef = useRef(null);
const chatRef = useRef(null);
const inputRef = useRef(null);

const isDarkTheme = colorMode === 'dark';

useEffect(() => {
document.documentElement.setAttribute('data-theme', colorMode);
}, [colorMode]);

useEffect(() => {
let interval;
if (isLoading) {
interval = setInterval(() => {
setSeconds((seconds) => seconds + 1);
}, 1000);
} else {
setSeconds(1);
}

return () => clearInterval(interval);
}, [isLoading])

useEffect(() => {
if (inputRef.current) {
inputRef.current.focus();
}
}, []);

useEffect(() => {
const handleKeyDown = (event) => {
if (event.key === 'Escape') {
toggleChat();
}
};

document.addEventListener('keydown', handleKeyDown);
return () => {
document.removeEventListener('keydown', handleKeyDown);
};
}, [toggleChat]);

useEffect(() => {
const handleClickOutside = (event) => {
if (chatRef.current && !chatRef.current.contains(event.target)) {
toggleChat();
}
};

document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, [toggleChat]);

const getAIResponse = async (userMessage) => {
const response = await axios.post('https://tmp-docs-ai-service.onrender.com/api/chat', {
messages: userMessage,
threadId: threadId
}, {
headers: {
'Content-Type': 'application/json'
}
});
return response.data;
};

const handleSendMessage = async (e) => {
e.preventDefault();

if (!inputMessage.trim()) return;
const userMessage = { id: Date.now(), text: inputMessage, sender: 'user' };
setMessages([...messages, userMessage]);
setInputMessage('');

setIsLoading(true);

try {
const aiResponseText = await getAIResponse(inputMessage);
setThreadId(aiResponseText.threadId);

const aiMessage = { id: Date.now() + 1, text: aiResponseText.message, sender: 'ai' };
setMessages(prevMessages => [...prevMessages, aiMessage]);
} catch (error) {
const aiMessage = { id: Date.now() + 1, text: "I was not able to process your request, please try again", sender: 'ai' };
setMessages(prevMessages => [...prevMessages, aiMessage]);
}
setIsLoading(false);
};

useEffect(() => {
if (messagesEndRef.current) {
messagesEndRef.current.scrollIntoView({ behavior: 'smooth' });
}
}, [messages]);

return <div className="floating-chat-container">
<Card className="chat-card" ref={chatRef}>
<Card.Header className="chat-header">
<div className="chat-title">
<i className="bi bi-robot me-2"></i>
Docs AI (Beta)
</div>
<X
className="close-button"
onClick={toggleChat} />
</Card.Header>

<Card.Body className="chat-body">
<div className="messages-container">
{messages.length === 0 ? (
<div className="welcome-message">
How can I help you today?
</div>
) : (
messages.map((msg) => (
<div
key={msg.id}
className={`message ${msg.sender === 'user' ? 'user-message' : 'ai-message'}`}
>
{splitTextIntoParts(msg.text).map((part, index) => {
return (<MarkdownRenderer part={part} isDarkTheme={isDarkTheme} key={index} />)
})}
</div>
))
)}
{isLoading && (
<div className="message ai-message loading">
Thinking... ({seconds}s)
</div>
)}
<div ref={messagesEndRef} />
</div>
</Card.Body>

<Card.Footer className="chat-footer">
<Form onSubmit={handleSendMessage}>
<InputGroup>
<Form.Control
className="input-message"
placeholder="Type a message..."
value={inputMessage}
onChange={(e) => setInputMessage(e.target.value)}
ref={inputRef}
/>
<Button
variant="primary"
type="submit"
disabled={!inputMessage.trim() || isLoading}
>
<Send size={16} />
</Button>
</InputGroup>
</Form>
</Card.Footer>
</Card>
</div>
}
63 changes: 63 additions & 0 deletions website/src/components/AIChat/MarkdownRenderer.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import React, { useState } from 'react';
import ReactMarkdown from 'react-markdown';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { oneDark, oneLight } from 'react-syntax-highlighter/dist/esm/styles/prism';
import { ClipboardCopy, Check } from 'lucide-react';

const CodeBlock = ({ node, inline, className, children, isDarkTheme, ...props }) => {
const match = /language-(\w+)/.exec(className || '');
const codeContent = String(children).replace(/\n$/, '');
const [isCopied, setIsCopied] = useState(false);

const copyToClipboard = () => {
navigator.clipboard.writeText(codeContent);
setIsCopied(true);
setTimeout(() => {
setIsCopied(false);
}, 2000);
};

return !inline && match ? (
<div className="code-block-container">
<button
onClick={copyToClipboard}
className={`code-copy-button ${isCopied ? 'copied' : ''}`}
>
{isCopied ? <Check size={16} /> : <ClipboardCopy size={16} />}
</button>
<SyntaxHighlighter
style={isDarkTheme ? oneDark : oneLight}
language={match[1]}
showLineNumbers={true}
// PreTag={ ({ children }) => <div>{children}</div> }
{...props}
>
{codeContent}
</SyntaxHighlighter>
</div>
) : (
<code className={className} {...props}>
{children}
</code>
);
};

const MarkdownRenderer = ({ part, isDarkTheme }) => {
return (
<ReactMarkdown
components={{
code: (props) => <CodeBlock {...props} isDarkTheme={isDarkTheme} />,
a: ({ node, ...props }) => (
<a {...props} target="_blank" rel="noopener noreferrer">
{props.children}
</a>
)
}}

>
{part}
</ReactMarkdown>
);
};

export default MarkdownRenderer;
22 changes: 22 additions & 0 deletions website/src/components/AIChat/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import '@generated/client-modules';
import React, { useState, useRef, useEffect } from 'react';
import { Button, } from 'react-bootstrap';
import { Chat } from './Chat';
import { BotMessageSquare } from 'lucide-react';
const AIChat = () => {
const [isOpen, setIsOpen] = useState(false);
const toggleChat = () => { setIsOpen(!isOpen); };

return isOpen ?
<Chat toggleChat={toggleChat} /> : (
<Button
className="chat-toggle-button animated-border-box"
variant="primary"
onClick={toggleChat}
>
<BotMessageSquare size={36} />
</Button>
)
};

export default AIChat;
Loading
Loading