diff --git a/website/package.json b/website/package.json index 150fbf7ce0b..073ef901370 100644 --- a/website/package.json +++ b/website/package.json @@ -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", @@ -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" } } diff --git a/website/src/components/AIChat/Chat.jsx b/website/src/components/AIChat/Chat.jsx new file mode 100644 index 00000000000..c291ace6f88 --- /dev/null +++ b/website/src/components/AIChat/Chat.jsx @@ -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
+ + +
+ + Docs AI (Beta) +
+ +
+ + +
+ {messages.length === 0 ? ( +
+ How can I help you today? +
+ ) : ( + messages.map((msg) => ( +
+ {splitTextIntoParts(msg.text).map((part, index) => { + return () + })} +
+ )) + )} + {isLoading && ( +
+ Thinking... ({seconds}s) +
+ )} +
+
+ + + +
+ + setInputMessage(e.target.value)} + ref={inputRef} + /> + + +
+
+ +
+} diff --git a/website/src/components/AIChat/MarkdownRenderer.jsx b/website/src/components/AIChat/MarkdownRenderer.jsx new file mode 100644 index 00000000000..bd9498feb3c --- /dev/null +++ b/website/src/components/AIChat/MarkdownRenderer.jsx @@ -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 ? ( +
+ +
{children}
} + {...props} + > + {codeContent} +
+
+ ) : ( + + {children} + + ); +}; + +const MarkdownRenderer = ({ part, isDarkTheme }) => { + return ( + , + a: ({ node, ...props }) => ( + + {props.children} + + ) + }} + + > + {part} + + ); +}; + +export default MarkdownRenderer; \ No newline at end of file diff --git a/website/src/components/AIChat/index.jsx b/website/src/components/AIChat/index.jsx new file mode 100644 index 00000000000..c25d3af3635 --- /dev/null +++ b/website/src/components/AIChat/index.jsx @@ -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 ? + : ( + + ) +}; + +export default AIChat; \ No newline at end of file diff --git a/website/src/css/custom.scss b/website/src/css/custom.scss index 6e3f6cb5415..17c1535ee21 100644 --- a/website/src/css/custom.scss +++ b/website/src/css/custom.scss @@ -8,6 +8,19 @@ --near-font-headlines: Inter, sans-serif; --near-font-body: Inter, sans-serif; + + --chat-bg: #f8f9fa; + --chat-header-bg: #e9ecef; + --chat-input-bg: #f3f4f6; + --chat-user-message-bg: #007bff; + --chat-ai-message-bg: #f1f3f5; + --chat-ai-message-border: #dee2e6; + --chat-text-color: #212529; + --chat-ai-text-color: #212529; + --chat-welcome-text: #6c757d; + --chat-input-text: #212529; + --chat-input-bg-focus: #fff; + --chat-toggle-button-bg: #dee2e6; } [data-theme="dark"] { @@ -17,6 +30,19 @@ hr { background-color: #2d2d2d; } + + --chat-bg: #212529; + --chat-header-bg: #343a40; + --chat-input-bg: #495057; + --chat-user-message-bg: #007bff; + --chat-ai-message-bg: #495057; + --chat-ai-message-border: #6c757d; + --chat-text-color: #ffffff; + --chat-ai-text-color: #ffffff; + --chat-welcome-text: #adb5bd; + --chat-input-text: #ffffff; + --chat-input-bg-focus:#5a6268; + --chat-toggle-button-bg: #343a40; } [data-theme='light'] li hr { @@ -249,7 +275,7 @@ li hr { border-width: 0; } -li > ul { +li>ul { margin-top: 0.4rem; } @@ -460,7 +486,7 @@ div[class^="announcementBar_"] { h2 { margin-top: 4rem; } -} +} #__blog-post-container { padding-top: 1rem; @@ -473,4 +499,334 @@ div[class^="announcementBar_"] { .moving-forward__support-title { margin-top: 10px !important; +} + + +.chat-toggle-button { + width: 60px; + height: 60px; + position: fixed; + bottom: 2rem; + right: 2.5rem; + display: flex; + justify-content: center; + align-items: center; + background-color: var(--chat-ai-message-bg); + color: var(--chat-text-color); + cursor: pointer; + border: none; + + i { + font-size: 1.5rem; + } + + &:hover { + transform: scale(1.05); + transition: transform 0.2s ease; + } +} + +.animated-border-box { + max-height: 60px; + max-width: 60px; + overflow: hidden; + border-radius: 10px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); +} + +.animated-border-box:before { + content: ''; + z-index: -2; + text-align: center; + top: 50%; + left: 50%; + transform: translate(-50%, -50%) rotate(0deg); + position: absolute; + width: 100px; + height: 100px; + background-repeat: no-repeat; + background-position: 0 0; + /*border color, change middle color*/ + background-image: conic-gradient(rgba(0, 0, 0, 0), #1976ed, rgba(0, 0, 0, 0) 25%); + /* change speed here */ + animation: rotate 4s linear infinite; +} + +.animated-border-box:after { + content: ''; + position: absolute; + z-index: -1; + /* border width */ + left: 5px; + top: 5px; + /* double the px from the border width left */ + width: calc(100% - 10px); + height: calc(100% - 10px); + /*bg color*/ + background: var(--chat-ai-message-bg); + /*box border radius*/ + border-radius: 7px; +} + +@keyframes rotate { + 100% { + transform: translate(-50%, -50%) rotate(1turn); + } +} + +/*// Border Animation END//*/ + + + +/*// Ignore This //*/ +body { + margin: 0px; +} + +.center-box { + height: 100vh; + display: flex; + justify-content: center; + align-items: center; + background-color: #1D1E22; +} + +.floating-chat-container { + position: fixed; + top: 0; + left: 0; + height: 100vh; + width: 100vw; + background-color: #00000066; + align-content: center; + z-index: 1000; + + .card:hover { + transform: translateY(0); + } + + .chat-card { + margin: auto; + width: 70vw; + height: 80vh; + border-radius: 15px; + overflow: hidden; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15); + display: flex; + flex-direction: column; + background-color: var(--chat-bg); + + .chat-header { + background-color: var(--chat-header-bg); + color: var(--chat-text-color); + padding: 0.75rem 1rem; + display: flex; + justify-content: space-between; + align-items: center; + + .chat-title { + font-weight: 600; + display: flex; + align-items: center; + } + + .close-button { + color: var(--chat-text-color); + padding: 0; + + &:hover { + opacity: 0.8; + cursor: pointer; + } + } + + &:hover { + transform: translateY(0); + } + } + + .chat-body { + flex: 1; + overflow-y: auto; + padding: 1rem; + background-color: var(--chat-bg); + + .messages-container { + display: flex; + flex-direction: column; + gap: 12px; + + .welcome-message { + text-align: center; + color: var(--chat-welcome-text); + margin: 2rem 0; + } + + .message { + max-width: 90%; + padding: 10px 15px; + border-radius: 15px; + word-break: break-word; + + &.user-message { + align-self: flex-end; + background-color: var(--chat-user-message-bg); + color: white; + border-bottom-right-radius: 3px; + } + + &.ai-message { + align-self: flex-start; + background-color: var(--chat-ai-message-bg); + color: var(--chat-ai-text-color); + border: 1px solid var(--chat-ai-message-border); + border-bottom-left-radius: 3px; + } + + &.loading { + display: flex; + justify-content: center; + align-items: center; + padding: 15px; + } + } + } + } + + .chat-footer { + background-color: var(--chat-header-bg); + padding: 1rem; + border-top: 1px solid var(--chat-ai-message-border); + border: none; + + .input-group { + display: flex; + border-radius: 8px; + overflow: hidden; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); + + .form-control { + flex: 1; + padding: 14px 16px; + background-color: var(--chat-input-bg); + color: var(--chat-input-text); + border-radius: 8px 0 0 8px; + border: none; + font-size: 1rem; + transition: background-color 0.2s ease; + + &:focus { + outline: none; + box-shadow: none; + background-color: var(--chat-input-bg-focus); + } + } + + .btn { + display: flex; + align-items: center; + justify-content: center; + padding: 12px 16px; + background-color: var(--chat-user-message-bg); + color: white; + border-radius: 0 8px 8px 0; + border: none; + transition: background-color 0.2s ease; + + &:hover { + background-color: #0069d9; + cursor: pointer; + } + + svg { + width: 20px; + height: 20px; + } + } + } + } + } +} + +.dot-typing { + display: flex; + justify-content: center; + align-items: center; + height: 20px; + + span { + display: inline-block; + width: 6px; + height: 6px; + margin: 0 2px; + background-color: #fff; + border-radius: 50%; + animation: dotTyping 1.4s infinite ease-in-out both; + } + + span:nth-child(1) { + animation-delay: -0.32s; + } + + span:nth-child(2) { + animation-delay: -0.16s; + } +} + +@keyframes dotTyping { + + 0%, + 80%, + 100% { + transform: scale(0); + } + + 40% { + transform: scale(1); + } +} + +.code-block-container { + position: relative; +} + +.code-copy-button { + position: absolute; + right: 10px; + top: 10px; + background: none; + border: none; + cursor: pointer; + color: var(--chat-text-color); + z-index: 2; + padding: 4px; + border-radius: 4px; + transition: all 0.2s ease; + display: flex; + align-items: center; + justify-content: center; +} + +.code-copy-button:hover { + background-color: rgba(255, 255, 255, 0.1); + transform: scale(1.1); +} + +.code-copy-button.copied { + color: #10b981; + animation: pulse 0.5s ease-in-out; +} + +@keyframes pulse { + 0% { + transform: scale(1); + } + + 50% { + transform: scale(1.2); + } + + 100% { + transform: scale(1); + } } \ No newline at end of file diff --git a/website/src/theme/DocItem/Layout/index.js b/website/src/theme/DocItem/Layout/index.js index 816835508a7..d5fc34e401a 100644 --- a/website/src/theme/DocItem/Layout/index.js +++ b/website/src/theme/DocItem/Layout/index.js @@ -15,6 +15,7 @@ import styles from './styles.module.css'; import { HelpComponent } from '../../../components/helpcomponent'; import { FeedbackComponent } from '../../../components/FeedbackComponent'; +import AIChat from '../../../components/AIChat'; /** * Decide if the toc should be rendered, on mobile or desktop viewports @@ -52,6 +53,7 @@ export default function DocItemLayout({ children }) { +
diff --git a/website/yarn.lock b/website/yarn.lock index ff0c52c2704..57947ab371d 100644 --- a/website/yarn.lock +++ b/website/yarn.lock @@ -7941,6 +7941,11 @@ highlight.js@^10.4.1, highlight.js@~10.7.0: resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-10.7.3.tgz#697272e3991356e40c3cac566a74eef681756531" integrity sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A== +highlightjs-vue@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/highlightjs-vue/-/highlightjs-vue-1.0.0.tgz#fdfe97fbea6354e70ee44e3a955875e114db086d" + integrity sha512-PDEfEF102G23vHmPhLyPboFCD+BkMGu+GuJe2d9/eH4FsCwvgBpnc9n0pGE+ffKdph38s6foEZiEjdgHdzp+IA== + history@^4.9.0: version "4.10.1" resolved "https://registry.yarnpkg.com/history/-/history-4.10.1.tgz#33371a65e3a83b267434e2b3f3b1b4c58aad4cf3" @@ -8025,6 +8030,11 @@ html-tags@^3.3.1: resolved "https://registry.yarnpkg.com/html-tags/-/html-tags-3.3.1.tgz#a04026a18c882e4bba8a01a3d39cfe465d40b5ce" integrity sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ== +html-url-attributes@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/html-url-attributes/-/html-url-attributes-3.0.1.tgz#83b052cd5e437071b756cd74ae70f708870c2d87" + integrity sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ== + html-void-elements@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/html-void-elements/-/html-void-elements-3.0.0.tgz#fc9dbd84af9e747249034d4d62602def6517f1d7" @@ -9021,6 +9031,11 @@ lru-cache@^6.0.0: resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.1.0.tgz#2098d41c2dc56500e6c88584aa656c84de7d0484" integrity sha512-/1clY/ui8CzjKFyjdvwPWJUYKiFVXG2I2cY0ssG7h4+hwk+XOIX7ZSG9Q7TW8TW3Kp3BUSqgFWBLgL4PJ+Blag== +lucide-react@^0.482.0: + version "0.482.0" + resolved "https://registry.yarnpkg.com/lucide-react/-/lucide-react-0.482.0.tgz#0277ec1c728bfcacab0ea8c4fb4aa5cc2f155623" + integrity sha512-XM8PzHzSrg8ATmmO+fzf+JyYlVVdQnJjuyLDj2p4V2zEtcKeBNAqAoJIGFv1x2HSBa7kT8gpYUxwdQ0g7nypfw== + markdown-extensions@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/markdown-extensions/-/markdown-extensions-2.0.0.tgz#34bebc83e9938cae16e0e017e4a9814a8330d3c4" @@ -11660,6 +11675,23 @@ react-loadable-ssr-addon-v5-slorber@^1.0.1: dependencies: "@types/react" "*" +react-markdown@^10.1.0: + version "10.1.0" + resolved "https://registry.yarnpkg.com/react-markdown/-/react-markdown-10.1.0.tgz#e22bc20faddbc07605c15284255653c0f3bad5ca" + integrity sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ== + dependencies: + "@types/hast" "^3.0.0" + "@types/mdast" "^4.0.0" + devlop "^1.0.0" + hast-util-to-jsx-runtime "^2.0.0" + html-url-attributes "^3.0.0" + mdast-util-to-hast "^13.0.0" + remark-parse "^11.0.0" + remark-rehype "^11.0.0" + unified "^11.0.0" + unist-util-visit "^5.0.0" + vfile "^6.0.0" + react-markdown@^7.1.0: version "7.1.2" resolved "https://registry.yarnpkg.com/react-markdown/-/react-markdown-7.1.2.tgz#c9fa9d1c87e24529f028e1cdf731e81ccdd8e547" @@ -11788,6 +11820,18 @@ react-syntax-highlighter@^15.5.0: prismjs "^1.27.0" refractor "^3.6.0" +react-syntax-highlighter@^15.6.1: + version "15.6.1" + resolved "https://registry.yarnpkg.com/react-syntax-highlighter/-/react-syntax-highlighter-15.6.1.tgz#fa567cb0a9f96be7bbccf2c13a3c4b5657d9543e" + integrity sha512-OqJ2/vL7lEeV5zTJyG7kmARppUjiB9h9udl4qHQjjgEos66z00Ia0OckwYfRxCSFrW8RJIBnsBwQsHZbVPspqg== + dependencies: + "@babel/runtime" "^7.3.1" + highlight.js "^10.4.1" + highlightjs-vue "^1.0.0" + lowlight "^1.17.0" + prismjs "^1.27.0" + refractor "^3.6.0" + react-transition-group@^4.4.5: version "4.4.5" resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.5.tgz#e53d4e3f3344da8521489fbef8f2581d42becdd1"