Spaces:
Running
Running
Commit ·
c3a0082
1
Parent(s): cb9ad57
commit 0001
Browse files- package-lock.json +0 -0
- package.json +10 -1
- public/index.html +1 -1
- src/App.css +174 -26
- src/App.js +4 -19
- src/components/ChatApp.jsx +145 -0
- src/components/CodeBlock.jsx +90 -0
- src/components/MessageBubble.jsx +40 -0
- src/components/ThemeProviderWrapper.jsx +33 -0
- src/index.js +2 -0
- src/styles/prism-lines.css +36 -0
- src/utils/languageDetect.js +48 -0
package-lock.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
package.json
CHANGED
|
@@ -1,12 +1,17 @@
|
|
| 1 |
{
|
| 2 |
-
"name": "
|
| 3 |
"version": "0.1.0",
|
| 4 |
"private": true,
|
| 5 |
"dependencies": {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
"@testing-library/dom": "^10.4.0",
|
| 7 |
"@testing-library/jest-dom": "^6.6.3",
|
| 8 |
"@testing-library/react": "^16.3.0",
|
| 9 |
"@testing-library/user-event": "^13.5.0",
|
|
|
|
| 10 |
"react": "^19.1.0",
|
| 11 |
"react-dom": "^19.1.0",
|
| 12 |
"react-scripts": "5.0.1",
|
|
@@ -35,5 +40,9 @@
|
|
| 35 |
"last 1 firefox version",
|
| 36 |
"last 1 safari version"
|
| 37 |
]
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
}
|
| 39 |
}
|
|
|
|
| 1 |
{
|
| 2 |
+
"name": "chat-mate",
|
| 3 |
"version": "0.1.0",
|
| 4 |
"private": true,
|
| 5 |
"dependencies": {
|
| 6 |
+
"@emotion/react": "^11.14.0",
|
| 7 |
+
"@emotion/styled": "^11.14.1",
|
| 8 |
+
"@mui/icons-material": "^7.2.0",
|
| 9 |
+
"@mui/material": "^7.2.0",
|
| 10 |
"@testing-library/dom": "^10.4.0",
|
| 11 |
"@testing-library/jest-dom": "^6.6.3",
|
| 12 |
"@testing-library/react": "^16.3.0",
|
| 13 |
"@testing-library/user-event": "^13.5.0",
|
| 14 |
+
"prismjs": "^1.30.0",
|
| 15 |
"react": "^19.1.0",
|
| 16 |
"react-dom": "^19.1.0",
|
| 17 |
"react-scripts": "5.0.1",
|
|
|
|
| 40 |
"last 1 firefox version",
|
| 41 |
"last 1 safari version"
|
| 42 |
]
|
| 43 |
+
},
|
| 44 |
+
"devDependencies": {
|
| 45 |
+
"@types/react": "^19.1.8",
|
| 46 |
+
"@types/react-dom": "^19.1.6"
|
| 47 |
}
|
| 48 |
}
|
public/index.html
CHANGED
|
@@ -24,7 +24,7 @@
|
|
| 24 |
work correctly both with client-side routing and a non-root public URL.
|
| 25 |
Learn how to configure a non-root public URL by running `npm run build`.
|
| 26 |
-->
|
| 27 |
-
<title>
|
| 28 |
</head>
|
| 29 |
<body>
|
| 30 |
<noscript>You need to enable JavaScript to run this app.</noscript>
|
|
|
|
| 24 |
work correctly both with client-side routing and a non-root public URL.
|
| 25 |
Learn how to configure a non-root public URL by running `npm run build`.
|
| 26 |
-->
|
| 27 |
+
<title>ChatMate App</title>
|
| 28 |
</head>
|
| 29 |
<body>
|
| 30 |
<noscript>You need to enable JavaScript to run this app.</noscript>
|
src/App.css
CHANGED
|
@@ -1,38 +1,186 @@
|
|
| 1 |
-
|
| 2 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
}
|
| 4 |
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
|
|
|
| 8 |
}
|
| 9 |
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
}
|
| 15 |
|
| 16 |
-
.
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
flex-direction: column;
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
}
|
| 26 |
|
| 27 |
-
.
|
| 28 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
}
|
| 30 |
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
transform: rotate(360deg);
|
| 37 |
-
}
|
| 38 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* App.css */
|
| 2 |
+
.chat-container {
|
| 3 |
+
max-width: 800px;
|
| 4 |
+
margin: auto;
|
| 5 |
+
display: flex;
|
| 6 |
+
flex-direction: column;
|
| 7 |
+
height: 100vh;
|
| 8 |
+
color: #ffffff;
|
| 9 |
+
background: #2a2a2a;
|
| 10 |
+
border-radius: 12px;
|
| 11 |
+
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
|
| 12 |
}
|
| 13 |
|
| 14 |
+
body {
|
| 15 |
+
font-family: 'Roboto', sans-serif;
|
| 16 |
+
background: #121212;
|
| 17 |
+
color: #ffffff;
|
| 18 |
}
|
| 19 |
|
| 20 |
+
h2 {
|
| 21 |
+
|
| 22 |
+
color: #bb86fc;
|
| 23 |
+
margin-bottom: 20px;
|
| 24 |
+
font-weight: 500;
|
| 25 |
+
}
|
| 26 |
+
.header-area {
|
| 27 |
+
display: flex;
|
| 28 |
+
justify-content: center;
|
| 29 |
+
text-align: center;
|
| 30 |
+
background: #383838;
|
| 31 |
}
|
| 32 |
|
| 33 |
+
.chat-box {
|
| 34 |
+
flex-grow: 1;
|
| 35 |
+
max-height: 420px;
|
| 36 |
+
overflow-y: auto;
|
| 37 |
+
padding: 12px;
|
| 38 |
+
border: 1px solid #2d2d2d;
|
| 39 |
+
border-radius: 8px;
|
| 40 |
+
background: #2a2a2a;
|
| 41 |
+
margin-bottom: 16px;
|
| 42 |
+
display: flex;
|
| 43 |
flex-direction: column;
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
.message {
|
| 47 |
+
display: block; /* ensures full-width layout */
|
| 48 |
+
width: fit-content;
|
| 49 |
+
max-width: 85%;
|
| 50 |
+
margin-bottom: 12px;
|
| 51 |
+
padding: 10px 14px;
|
| 52 |
+
border-radius: 16px;
|
| 53 |
+
line-height: 1.5;
|
| 54 |
+
word-wrap: break-word;
|
| 55 |
}
|
| 56 |
|
| 57 |
+
.message.user {
|
| 58 |
+
align-self: flex-end;
|
| 59 |
+
margin-left: auto;
|
| 60 |
+
background: #333c4d;
|
| 61 |
+
color: #e3f2fd;
|
| 62 |
+
border-bottom-right-radius: 0;
|
| 63 |
}
|
| 64 |
|
| 65 |
+
.message.assistant {
|
| 66 |
+
align-self: flex-start;
|
| 67 |
+
margin-right: auto;
|
| 68 |
+
background: #383838;
|
| 69 |
+
border-bottom-left-radius: 0;
|
|
|
|
|
|
|
| 70 |
}
|
| 71 |
+
|
| 72 |
+
.input-area {
|
| 73 |
+
display: flex;
|
| 74 |
+
padding: 10px;
|
| 75 |
+
background: #383838;
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
textarea {
|
| 79 |
+
flex-grow: 1;
|
| 80 |
+
padding: 10px 12px;
|
| 81 |
+
resize: none;
|
| 82 |
+
border-radius: 6px;
|
| 83 |
+
background: #2a2a2a;
|
| 84 |
+
color: #fff;
|
| 85 |
+
border: 1px solid #444;
|
| 86 |
+
font-size: 14px;
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
button {
|
| 90 |
+
margin-left: 10px;
|
| 91 |
+
padding: 8px 16px;
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
.timestamp {
|
| 95 |
+
font-size: 0.75em;
|
| 96 |
+
color: #888;
|
| 97 |
+
margin-top: 4px;
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
.formatted-text {
|
| 101 |
+
white-space: pre-wrap;
|
| 102 |
+
line-height: 1.5;
|
| 103 |
+
margin-bottom: 8px;
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
.language-label {
|
| 107 |
+
font-weight: bold;
|
| 108 |
+
font-size: 12px;
|
| 109 |
+
padding: 4px 0;
|
| 110 |
+
}
|
| 111 |
+
pre.line-numbers {
|
| 112 |
+
padding-left: 3.8em; /* ensure room for line numbers */
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
.code-block-wrapper pre {
|
| 116 |
+
background: #2d2d2d; /* override if needed */
|
| 117 |
+
padding: 1em;
|
| 118 |
+
font-size: 0.9em;
|
| 119 |
+
line-height: 1.5;
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
.code-block-wrapper {
|
| 123 |
+
margin: 12px 0;
|
| 124 |
+
background-color: #2d2d2d;
|
| 125 |
+
border-radius: 8px;
|
| 126 |
+
overflow: auto;
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
|
| 130 |
+
.typing-indicator {
|
| 131 |
+
display: flex;
|
| 132 |
+
gap: 6px;
|
| 133 |
+
margin: 6px 0 12px;
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
.dot {
|
| 137 |
+
width: 8px;
|
| 138 |
+
height: 8px;
|
| 139 |
+
background-color: #bb86fc;
|
| 140 |
+
border-radius: 50%;
|
| 141 |
+
animation: blink 1.4s infinite both;
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
.dot:nth-child(2) {
|
| 145 |
+
animation-delay: 0.2s;
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
.dot:nth-child(3) {
|
| 149 |
+
animation-delay: 0.4s;
|
| 150 |
+
}
|
| 151 |
+
|
| 152 |
+
@keyframes blink {
|
| 153 |
+
0%, 80%, 100% { opacity: 0.2; }
|
| 154 |
+
40% { opacity: 1; }
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
|
| 158 |
+
/* Base style — minimal/invisible scrollbar */
|
| 159 |
+
* {
|
| 160 |
+
scrollbar-width: thin; /* Firefox */
|
| 161 |
+
scrollbar-color: transparent transparent;
|
| 162 |
+
}
|
| 163 |
+
|
| 164 |
+
/* Webkit browsers (Chrome, Edge, Safari) */
|
| 165 |
+
*::-webkit-scrollbar {
|
| 166 |
+
width: 4px;
|
| 167 |
+
}
|
| 168 |
+
|
| 169 |
+
*::-webkit-scrollbar-track {
|
| 170 |
+
background: transparent;
|
| 171 |
+
}
|
| 172 |
+
|
| 173 |
+
*::-webkit-scrollbar-thumb {
|
| 174 |
+
background-color: transparent;
|
| 175 |
+
border-radius: 8px;
|
| 176 |
+
transition: background-color 0.3s ease;
|
| 177 |
+
}
|
| 178 |
+
|
| 179 |
+
/* On hover — visible scrollbar */
|
| 180 |
+
*:hover::-webkit-scrollbar-thumb {
|
| 181 |
+
background-color: rgba(100, 100, 100, 0.5);
|
| 182 |
+
}
|
| 183 |
+
|
| 184 |
+
*:hover {
|
| 185 |
+
scrollbar-color: rgba(100, 100, 100, 0.5) transparent;
|
| 186 |
+
}
|
src/App.js
CHANGED
|
@@ -1,25 +1,10 @@
|
|
| 1 |
-
|
|
|
|
|
|
|
| 2 |
import './App.css';
|
| 3 |
|
| 4 |
function App() {
|
| 5 |
-
return
|
| 6 |
-
<div className="App">
|
| 7 |
-
<header className="App-header">
|
| 8 |
-
<img src={logo} className="App-logo" alt="logo" />
|
| 9 |
-
<p>
|
| 10 |
-
Edit <code>src/App.js</code> and save to reload.
|
| 11 |
-
</p>
|
| 12 |
-
<a
|
| 13 |
-
className="App-link"
|
| 14 |
-
href="https://reactjs.org"
|
| 15 |
-
target="_blank"
|
| 16 |
-
rel="noopener noreferrer"
|
| 17 |
-
>
|
| 18 |
-
Learn React
|
| 19 |
-
</a>
|
| 20 |
-
</header>
|
| 21 |
-
</div>
|
| 22 |
-
);
|
| 23 |
}
|
| 24 |
|
| 25 |
export default App;
|
|
|
|
| 1 |
+
// App.js
|
| 2 |
+
import React from 'react';
|
| 3 |
+
import ChatApp from './components/ChatApp';
|
| 4 |
import './App.css';
|
| 5 |
|
| 6 |
function App() {
|
| 7 |
+
return <ChatApp />;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
}
|
| 9 |
|
| 10 |
export default App;
|
src/components/ChatApp.jsx
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// components/ChatApp.jsx
|
| 2 |
+
import React, { useState, useRef, useEffect } from 'react';
|
| 3 |
+
import MessageBubble from './MessageBubble';
|
| 4 |
+
import { Button , Typography, Box} from '@mui/material';
|
| 5 |
+
import { ChatBubbleOutline } from '@mui/icons-material';
|
| 6 |
+
export default function ChatApp() {
|
| 7 |
+
const [message, setMessage] = useState('');
|
| 8 |
+
const [history, setHistory] = useState([]);
|
| 9 |
+
const [loading, setLoading] = useState(false);
|
| 10 |
+
const chatEndRef = useRef(null);
|
| 11 |
+
|
| 12 |
+
const examples = [
|
| 13 |
+
"What is Python?",
|
| 14 |
+
"Write a JavaScript function to reverse a string.",
|
| 15 |
+
"Explain how transformers work.",
|
| 16 |
+
];
|
| 17 |
+
|
| 18 |
+
const scrollToBottom = () => {
|
| 19 |
+
chatEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
| 20 |
+
};
|
| 21 |
+
|
| 22 |
+
useEffect(scrollToBottom, [history]);
|
| 23 |
+
|
| 24 |
+
const sendMessage = async () => {
|
| 25 |
+
if (!message.trim()) return;
|
| 26 |
+
|
| 27 |
+
const time = new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
| 28 |
+
|
| 29 |
+
const newHistory = [...history, { role: 'user', content: message, time }];
|
| 30 |
+
setHistory(newHistory);
|
| 31 |
+
setMessage('');
|
| 32 |
+
setLoading(true);
|
| 33 |
+
|
| 34 |
+
//const assistant = { role: 'assistant', content: '', time };
|
| 35 |
+
//setHistory(h => [...h, assistant]);
|
| 36 |
+
|
| 37 |
+
const response = await fetch('https://fredericksundeep-aichatmate.hf.space/chat-stream', {
|
| 38 |
+
method: 'POST',
|
| 39 |
+
headers: { 'Content-Type': 'application/json' },
|
| 40 |
+
body: JSON.stringify({ message, history: newHistory }),
|
| 41 |
+
});
|
| 42 |
+
|
| 43 |
+
const reader = response.body.getReader();
|
| 44 |
+
const decoder = new TextDecoder();
|
| 45 |
+
let done = false;
|
| 46 |
+
let content = '';
|
| 47 |
+
|
| 48 |
+
while (!done) {
|
| 49 |
+
const { value, done: isDone } = await reader.read();
|
| 50 |
+
if (value) {
|
| 51 |
+
const chunk = decoder.decode(value);
|
| 52 |
+
content += chunk;
|
| 53 |
+
|
| 54 |
+
const currentContent = content; // capture safe reference
|
| 55 |
+
setHistory(h =>
|
| 56 |
+
h.map((msg, i) =>
|
| 57 |
+
i === newHistory.length
|
| 58 |
+
? { ...msg, content: currentContent }
|
| 59 |
+
: msg
|
| 60 |
+
)
|
| 61 |
+
);
|
| 62 |
+
}
|
| 63 |
+
done = isDone;
|
| 64 |
+
}
|
| 65 |
+
setHistory(h => [...h, { role: 'assistant', content, time }]);
|
| 66 |
+
setLoading(false);
|
| 67 |
+
};
|
| 68 |
+
|
| 69 |
+
return (
|
| 70 |
+
<div className="chat-container">
|
| 71 |
+
<div className="header-area">
|
| 72 |
+
<Box display="flex" alignItems="center" gap={1} mb={2}>
|
| 73 |
+
<ChatBubbleOutline color="primary" />
|
| 74 |
+
<Typography variant="h5" component="h2">
|
| 75 |
+
Chat Mate
|
| 76 |
+
</Typography>
|
| 77 |
+
</Box>
|
| 78 |
+
</div>
|
| 79 |
+
<div className="chat-box">
|
| 80 |
+
{history.length === 0 && !loading && (
|
| 81 |
+
<Box
|
| 82 |
+
display="flex"
|
| 83 |
+
flexDirection="column"
|
| 84 |
+
alignItems="center"
|
| 85 |
+
gap={2}
|
| 86 |
+
pt={8}
|
| 87 |
+
>
|
| 88 |
+
{examples.map((example, i) => (
|
| 89 |
+
<Button
|
| 90 |
+
key={i}
|
| 91 |
+
variant="outlined"
|
| 92 |
+
onClick={() => {
|
| 93 |
+
const selected = example;
|
| 94 |
+
setMessage(selected);
|
| 95 |
+
setTimeout(() => {
|
| 96 |
+
sendMessage(selected);
|
| 97 |
+
}, 50);
|
| 98 |
+
}}
|
| 99 |
+
sx={{
|
| 100 |
+
color: '#fff',
|
| 101 |
+
borderColor: '#444',
|
| 102 |
+
backgroundColor: '#1e1e2f',
|
| 103 |
+
borderRadius: '12px',
|
| 104 |
+
width: '70%',
|
| 105 |
+
fontSize: '1rem',
|
| 106 |
+
fontWeight: 400,
|
| 107 |
+
textTransform: 'none',
|
| 108 |
+
'&:hover': {
|
| 109 |
+
backgroundColor: '#2c2c3e',
|
| 110 |
+
borderColor: '#666',
|
| 111 |
+
},
|
| 112 |
+
}}
|
| 113 |
+
>
|
| 114 |
+
{example}
|
| 115 |
+
</Button>
|
| 116 |
+
))}
|
| 117 |
+
</Box>
|
| 118 |
+
)}
|
| 119 |
+
|
| 120 |
+
{history.map((msg, i) => (
|
| 121 |
+
<MessageBubble key={i} {...msg} />
|
| 122 |
+
))}
|
| 123 |
+
|
| 124 |
+
{loading && (
|
| 125 |
+
<div className="typing-indicator">
|
| 126 |
+
<span className="dot"></span><span className="dot"></span><span className="dot"></span>
|
| 127 |
+
</div>
|
| 128 |
+
)}
|
| 129 |
+
|
| 130 |
+
<div ref={chatEndRef} />
|
| 131 |
+
</div>
|
| 132 |
+
<div className="input-area">
|
| 133 |
+
<textarea
|
| 134 |
+
value={message}
|
| 135 |
+
rows={2}
|
| 136 |
+
onChange={(e) => setMessage(e.target.value)}
|
| 137 |
+
placeholder="Ask something..."
|
| 138 |
+
/>
|
| 139 |
+
<Button variant="contained"
|
| 140 |
+
color="primary"
|
| 141 |
+
sx={{ ml: 1, px: 2, py: 1 }} disabled={!message.trim() || loading} onClick={sendMessage}>Send</Button>
|
| 142 |
+
</div>
|
| 143 |
+
</div>
|
| 144 |
+
);
|
| 145 |
+
}
|
src/components/CodeBlock.jsx
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useEffect, useRef } from 'react';
|
| 2 |
+
import Prism from 'prismjs';
|
| 3 |
+
import { detectLanguage } from '../utils/languageDetect';
|
| 4 |
+
|
| 5 |
+
// Prism core styles and plugins
|
| 6 |
+
import 'prismjs/themes/prism-tomorrow.css';
|
| 7 |
+
import 'prismjs/plugins/line-numbers/prism-line-numbers.css';
|
| 8 |
+
import 'prismjs/plugins/line-numbers/prism-line-numbers';
|
| 9 |
+
|
| 10 |
+
// Required languages
|
| 11 |
+
import 'prismjs/components/prism-clike';
|
| 12 |
+
import 'prismjs/components/prism-markup-templating';
|
| 13 |
+
import 'prismjs/components/prism-python';
|
| 14 |
+
import 'prismjs/components/prism-javascript';
|
| 15 |
+
import 'prismjs/components/prism-typescript';
|
| 16 |
+
import 'prismjs/components/prism-java';
|
| 17 |
+
import 'prismjs/components/prism-c';
|
| 18 |
+
import 'prismjs/components/prism-cpp';
|
| 19 |
+
import 'prismjs/components/prism-bash';
|
| 20 |
+
import 'prismjs/components/prism-shell-session';
|
| 21 |
+
import 'prismjs/components/prism-sql';
|
| 22 |
+
import 'prismjs/components/prism-markup'; // HTML
|
| 23 |
+
import 'prismjs/components/prism-css';
|
| 24 |
+
import 'prismjs/components/prism-go';
|
| 25 |
+
import 'prismjs/components/prism-php';
|
| 26 |
+
import 'prismjs/components/prism-ruby';
|
| 27 |
+
import 'prismjs/components/prism-kotlin';
|
| 28 |
+
import 'prismjs/components/prism-swift';
|
| 29 |
+
import 'prismjs/components/prism-rust';
|
| 30 |
+
import 'prismjs/components/prism-scala';
|
| 31 |
+
import 'prismjs/components/prism-dart';
|
| 32 |
+
|
| 33 |
+
export default function CodeBlock({ content }) {
|
| 34 |
+
const codeRef = useRef(null);
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
const lines = content.split('\n');
|
| 39 |
+
const firstLine = lines[0].trim();
|
| 40 |
+
const hasLang = /^[a-zA-Z]+$/.test(firstLine);
|
| 41 |
+
const lang = hasLang ? firstLine.toLowerCase() : detectLanguage(content) || 'markdown';
|
| 42 |
+
const code = hasLang ? lines.slice(1).join('\n') : content;
|
| 43 |
+
//const escapedCode = escapeHTML(code);
|
| 44 |
+
|
| 45 |
+
useEffect(() => {
|
| 46 |
+
if (codeRef.current) {
|
| 47 |
+
Prism.highlightElement(codeRef.current);
|
| 48 |
+
}
|
| 49 |
+
}, [code, lang]);
|
| 50 |
+
|
| 51 |
+
const handleCopy = () => {
|
| 52 |
+
navigator.clipboard.writeText(code).then(() => {
|
| 53 |
+
alert('Copied to clipboard!');
|
| 54 |
+
});
|
| 55 |
+
};
|
| 56 |
+
|
| 57 |
+
return (
|
| 58 |
+
<div className="code-block-wrapper" style={{ position: 'relative', marginBottom: '1rem' }}>
|
| 59 |
+
<button
|
| 60 |
+
onClick={handleCopy}
|
| 61 |
+
style={{
|
| 62 |
+
position: 'absolute',
|
| 63 |
+
right: '10px',
|
| 64 |
+
top: '10px',
|
| 65 |
+
zIndex: 5,
|
| 66 |
+
fontSize: '12px',
|
| 67 |
+
padding: '4px 8px',
|
| 68 |
+
cursor: 'pointer',
|
| 69 |
+
}}
|
| 70 |
+
>
|
| 71 |
+
Copy
|
| 72 |
+
</button>
|
| 73 |
+
<div style={{ fontSize: '13px', color: '#ccc', paddingBottom: '5px' }}>
|
| 74 |
+
{lang.toUpperCase()}
|
| 75 |
+
</div>
|
| 76 |
+
<pre
|
| 77 |
+
className={`line-numbers language-${lang}`}
|
| 78 |
+
style={{ borderRadius: '8px', overflowX: 'auto' }}
|
| 79 |
+
>
|
| 80 |
+
<code
|
| 81 |
+
ref={codeRef}
|
| 82 |
+
className={`language-${lang}`}
|
| 83 |
+
>
|
| 84 |
+
{code}
|
| 85 |
+
</code>
|
| 86 |
+
</pre>
|
| 87 |
+
</div>
|
| 88 |
+
);
|
| 89 |
+
|
| 90 |
+
}
|
src/components/MessageBubble.jsx
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from 'react';
|
| 2 |
+
import CodeBlock from './CodeBlock';
|
| 3 |
+
|
| 4 |
+
const formatText = (text) => {
|
| 5 |
+
return text
|
| 6 |
+
.replace(/\n/g, "<br>")
|
| 7 |
+
.replace(/\*\*(.*?)\*\*/g, "<strong>$1</strong>")
|
| 8 |
+
.replace(/\*(.*?)\*/g, "<em>$1</em>");
|
| 9 |
+
};
|
| 10 |
+
|
| 11 |
+
export default function MessageBubble({ role, content, time }) {
|
| 12 |
+
return (
|
| 13 |
+
<div className={`message ${role}`}>
|
| 14 |
+
<div className="bubble">
|
| 15 |
+
<FormattedContent content={content} />
|
| 16 |
+
<div className="timestamp">{time}</div>
|
| 17 |
+
</div>
|
| 18 |
+
</div>
|
| 19 |
+
);
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
function FormattedContent({ content }) {
|
| 23 |
+
const blocks = content.split('```');
|
| 24 |
+
|
| 25 |
+
return (
|
| 26 |
+
<>
|
| 27 |
+
{blocks.map((block, i) =>
|
| 28 |
+
i % 2 === 1 ? (
|
| 29 |
+
<CodeBlock key={i} content={block} />
|
| 30 |
+
) : (
|
| 31 |
+
<div
|
| 32 |
+
key={i}
|
| 33 |
+
className="formatted-text"
|
| 34 |
+
dangerouslySetInnerHTML={{ __html: formatText(block) }}
|
| 35 |
+
/>
|
| 36 |
+
)
|
| 37 |
+
)}
|
| 38 |
+
</>
|
| 39 |
+
);
|
| 40 |
+
}
|
src/components/ThemeProviderWrapper.jsx
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// components/ThemeProviderWrapper.jsx
|
| 2 |
+
import React, { useMemo, useState, createContext } from 'react';
|
| 3 |
+
import { ThemeProvider, createTheme, CssBaseline } from '@mui/material';
|
| 4 |
+
|
| 5 |
+
export const ColorModeContext = createContext({ toggleColorMode: () => {} });
|
| 6 |
+
|
| 7 |
+
export default function ThemeProviderWrapper({ children }) {
|
| 8 |
+
const [mode, setMode] = useState('light');
|
| 9 |
+
|
| 10 |
+
const colorMode = useMemo(() => ({
|
| 11 |
+
toggleColorMode: () => {
|
| 12 |
+
setMode(prev => (prev === 'light' ? 'dark' : 'light'));
|
| 13 |
+
}
|
| 14 |
+
}), []);
|
| 15 |
+
|
| 16 |
+
const theme = useMemo(() =>
|
| 17 |
+
createTheme({
|
| 18 |
+
palette: {
|
| 19 |
+
mode,
|
| 20 |
+
primary: { main: '#1976d2' },
|
| 21 |
+
secondary: { main: '#9c27b0' }
|
| 22 |
+
}
|
| 23 |
+
}), [mode]);
|
| 24 |
+
|
| 25 |
+
return (
|
| 26 |
+
<ColorModeContext.Provider value={colorMode}>
|
| 27 |
+
<ThemeProvider theme={theme}>
|
| 28 |
+
<CssBaseline />
|
| 29 |
+
{children}
|
| 30 |
+
</ThemeProvider>
|
| 31 |
+
</ColorModeContext.Provider>
|
| 32 |
+
);
|
| 33 |
+
}
|
src/index.js
CHANGED
|
@@ -3,6 +3,8 @@ import ReactDOM from 'react-dom/client';
|
|
| 3 |
import './index.css';
|
| 4 |
import App from './App';
|
| 5 |
import reportWebVitals from './reportWebVitals';
|
|
|
|
|
|
|
| 6 |
|
| 7 |
const root = ReactDOM.createRoot(document.getElementById('root'));
|
| 8 |
root.render(
|
|
|
|
| 3 |
import './index.css';
|
| 4 |
import App from './App';
|
| 5 |
import reportWebVitals from './reportWebVitals';
|
| 6 |
+
import 'prismjs/themes/prism-tomorrow.css';
|
| 7 |
+
import 'prismjs/plugins/line-numbers/prism-line-numbers.css';
|
| 8 |
|
| 9 |
const root = ReactDOM.createRoot(document.getElementById('root'));
|
| 10 |
root.render(
|
src/styles/prism-lines.css
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
prism-lines.css
|
| 2 |
+
pre.line-numbers {
|
| 3 |
+
padding-left: 3.8em;
|
| 4 |
+
position: relative;
|
| 5 |
+
counter-reset: linenumber;
|
| 6 |
+
}
|
| 7 |
+
|
| 8 |
+
pre.line-numbers > code {
|
| 9 |
+
position: relative;
|
| 10 |
+
white-space: pre-wrap;
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
.line-numbers .line-numbers-rows {
|
| 14 |
+
position: absolute;
|
| 15 |
+
pointer-events: none;
|
| 16 |
+
top: 0;
|
| 17 |
+
font-size: 100%;
|
| 18 |
+
left: 0;
|
| 19 |
+
width: 3em;
|
| 20 |
+
letter-spacing: -1px;
|
| 21 |
+
border-right: 1px solid #999;
|
| 22 |
+
user-select: none;
|
| 23 |
+
padding: 0;
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
.line-numbers-rows > span {
|
| 27 |
+
display: block;
|
| 28 |
+
counter-increment: linenumber;
|
| 29 |
+
padding-left: 0.5em;
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
.line-numbers-rows > span:before {
|
| 33 |
+
content: counter(linenumber);
|
| 34 |
+
display: block;
|
| 35 |
+
color: #999;
|
| 36 |
+
}
|
src/utils/languageDetect.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// utils/languageDetect.js
|
| 2 |
+
|
| 3 |
+
const escapeRegExp = (string) =>
|
| 4 |
+
string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
| 5 |
+
|
| 6 |
+
export const detectLanguage = (code = '') => {
|
| 7 |
+
const language_keywords = {
|
| 8 |
+
python: ['def ', 'print(', 'import ', 'class '],
|
| 9 |
+
javascript: ['function ', 'console.log(', 'let ', 'const ', 'document.getElementById'],
|
| 10 |
+
typescript: ['interface ', 'type ', 'let ', 'const ', ': string', ': number'],
|
| 11 |
+
java: ['import java.', 'ArrayList<', 'System.out', 'void main(', 'public class', 'new '],
|
| 12 |
+
c: ['#include <stdio.h>', 'printf(', 'scanf(', 'int main('],
|
| 13 |
+
cpp: ['#include', 'std::', 'cout <<', 'cin >>'],
|
| 14 |
+
bash: ['#!/bin/bash', 'echo ', 'cd ', 'ls', 'pwd'],
|
| 15 |
+
shell: ['#!/bin/sh', 'echo ', 'export ', 'fi'],
|
| 16 |
+
sql: ['SELECT ', 'INSERT ', 'UPDATE ', 'FROM ', 'WHERE ', 'JOIN ', 'DELETE '],
|
| 17 |
+
html: ['<!DOCTYPE html>', '<html>', '<div>', '<script>'],
|
| 18 |
+
css: ['color:', 'font-size:', 'margin:', 'padding:'],
|
| 19 |
+
go: ['package main', 'fmt.Println', 'func main()'],
|
| 20 |
+
php: ['<?php', 'echo ', '$_', '->'],
|
| 21 |
+
ruby: ['def ', 'puts ', 'end', 'class '],
|
| 22 |
+
kotlin: ['fun main(', 'val ', 'var ', 'println('],
|
| 23 |
+
swift: ['import SwiftUI', 'struct ', 'var body:', 'Text('],
|
| 24 |
+
rust: ['fn main()', 'println!', 'let mut'],
|
| 25 |
+
scala: ['object ', 'def ', 'val ', 'println('],
|
| 26 |
+
dart: ['void main()', 'print(', 'var ', 'class '],
|
| 27 |
+
};
|
| 28 |
+
|
| 29 |
+
let bestMatch = 'plaintext';
|
| 30 |
+
let maxScore = 0;
|
| 31 |
+
|
| 32 |
+
for (const [lang, keywords] of Object.entries(language_keywords)) {
|
| 33 |
+
let score = 0;
|
| 34 |
+
|
| 35 |
+
for (const keyword of keywords) {
|
| 36 |
+
const regex = new RegExp(escapeRegExp(keyword), 'g');
|
| 37 |
+
const matches = (code.match(regex) || []).length;
|
| 38 |
+
score += matches * Math.max(1, keyword.length / 4);
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
if (score > maxScore) {
|
| 42 |
+
maxScore = score;
|
| 43 |
+
bestMatch = lang;
|
| 44 |
+
}
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
return bestMatch;
|
| 48 |
+
};
|