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)
+
+ )}
+
+
+
+
+
+
+
+
+
+}
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"