Size
18.6 KB
Version
1.0.1
Created
Mar 4, 2026
Updated
about 1 month ago
1// ==UserScript==
2// @name Claude AI Sidepanel Chat
3// @description Chat with Claude AI in a convenient sidepanel on any webpage
4// @version 1.0.1
5// @match *://*/*
6// @icon https://www.gstatic.com/images/branding/searchlogo/ico/favicon.ico
7// ==/UserScript==
8(function() {
9 'use strict';
10
11 // State management
12 let chatHistory = [];
13 let isPanelOpen = false;
14
15 // Initialize the extension
16 async function init() {
17 console.log('Claude AI Sidepanel initializing...');
18
19 // Load chat history from storage
20 const savedHistory = await GM.getValue('claude_chat_history', '[]');
21 chatHistory = JSON.parse(savedHistory);
22
23 // Load panel state
24 isPanelOpen = await GM.getValue('claude_panel_open', false);
25
26 createToggleButton();
27 createSidePanel();
28
29 if (isPanelOpen) {
30 openPanel();
31 }
32
33 console.log('Claude AI Sidepanel initialized');
34 }
35
36 // Create the toggle button
37 function createToggleButton() {
38 const button = document.createElement('button');
39 button.id = 'claude-toggle-btn';
40 button.innerHTML = `
41 <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
42 <path d="M12 2L2 7L12 12L22 7L12 2Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
43 <path d="M2 17L12 22L22 17" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
44 <path d="M2 12L12 17L22 12" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
45 </svg>
46 `;
47 button.title = 'Toggle Claude AI Chat';
48
49 button.addEventListener('click', togglePanel);
50 document.body.appendChild(button);
51 }
52
53 // Create the side panel
54 function createSidePanel() {
55 const panel = document.createElement('div');
56 panel.id = 'claude-sidepanel';
57 panel.innerHTML = `
58 <div class="claude-panel-header">
59 <h3>Claude AI Chat</h3>
60 <div class="claude-header-actions">
61 <button id="claude-clear-btn" title="Clear chat">
62 <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
63 <polyline points="3 6 5 6 21 6"></polyline>
64 <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
65 </svg>
66 </button>
67 <button id="claude-close-btn" title="Close panel">
68 <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
69 <line x1="18" y1="6" x2="6" y2="18"></line>
70 <line x1="6" y1="6" x2="18" y2="18"></line>
71 </svg>
72 </button>
73 </div>
74 </div>
75 <div class="claude-chat-container" id="claude-chat-container">
76 <div class="claude-welcome-message">
77 <h4>Welcome to Claude AI Chat</h4>
78 <p>Ask me anything! I'm here to help you with information, analysis, or conversation.</p>
79 </div>
80 </div>
81 <div class="claude-input-container">
82 <textarea id="claude-input" placeholder="Type your message here..." rows="1"></textarea>
83 <button id="claude-send-btn" title="Send message">
84 <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
85 <line x1="22" y1="2" x2="11" y2="13"></line>
86 <polygon points="22 2 15 22 11 13 2 9 22 2"></polygon>
87 </svg>
88 </button>
89 </div>
90 `;
91
92 document.body.appendChild(panel);
93
94 // Add event listeners
95 document.getElementById('claude-close-btn').addEventListener('click', closePanel);
96 document.getElementById('claude-clear-btn').addEventListener('click', clearChat);
97 document.getElementById('claude-send-btn').addEventListener('click', sendMessage);
98
99 const input = document.getElementById('claude-input');
100 input.addEventListener('keydown', (e) => {
101 if (e.key === 'Enter' && !e.shiftKey) {
102 e.preventDefault();
103 sendMessage();
104 }
105 });
106
107 // Auto-resize textarea
108 input.addEventListener('input', () => {
109 input.style.height = 'auto';
110 input.style.height = Math.min(input.scrollHeight, 150) + 'px';
111 });
112
113 // Restore chat history
114 restoreChatHistory();
115 }
116
117 // Toggle panel open/close
118 async function togglePanel() {
119 if (isPanelOpen) {
120 closePanel();
121 } else {
122 openPanel();
123 }
124 }
125
126 // Open panel
127 async function openPanel() {
128 const panel = document.getElementById('claude-sidepanel');
129 panel.classList.add('open');
130 isPanelOpen = true;
131 await GM.setValue('claude_panel_open', true);
132
133 // Focus input
134 setTimeout(() => {
135 document.getElementById('claude-input').focus();
136 }, 300);
137 }
138
139 // Close panel
140 async function closePanel() {
141 const panel = document.getElementById('claude-sidepanel');
142 panel.classList.remove('open');
143 isPanelOpen = false;
144 await GM.setValue('claude_panel_open', false);
145 }
146
147 // Send message to Claude
148 async function sendMessage() {
149 const input = document.getElementById('claude-input');
150 const message = input.value.trim();
151
152 if (!message) return;
153
154 // Clear input
155 input.value = '';
156 input.style.height = 'auto';
157
158 // Add user message to chat
159 addMessageToChat('user', message);
160
161 // Show loading indicator
162 const loadingId = addLoadingMessage();
163
164 try {
165 // Call Claude AI
166 const response = await RM.aiCall(message);
167
168 // Remove loading indicator
169 removeLoadingMessage(loadingId);
170
171 // Add Claude's response to chat
172 addMessageToChat('assistant', response);
173
174 } catch (error) {
175 console.error('Error calling Claude AI:', error);
176 removeLoadingMessage(loadingId);
177 addMessageToChat('error', 'Sorry, I encountered an error. Please try again.');
178 }
179 }
180
181 // Add message to chat
182 function addMessageToChat(role, content) {
183 const container = document.getElementById('claude-chat-container');
184
185 // Remove welcome message if it exists
186 const welcomeMsg = container.querySelector('.claude-welcome-message');
187 if (welcomeMsg) {
188 welcomeMsg.remove();
189 }
190
191 const messageDiv = document.createElement('div');
192 messageDiv.className = `claude-message claude-message-${role}`;
193
194 const avatar = document.createElement('div');
195 avatar.className = 'claude-message-avatar';
196 avatar.textContent = role === 'user' ? 'You' : role === 'error' ? '⚠️' : 'AI';
197
198 const contentDiv = document.createElement('div');
199 contentDiv.className = 'claude-message-content';
200 contentDiv.textContent = content;
201
202 messageDiv.appendChild(avatar);
203 messageDiv.appendChild(contentDiv);
204 container.appendChild(messageDiv);
205
206 // Scroll to bottom
207 container.scrollTop = container.scrollHeight;
208
209 // Save to history (except error messages)
210 if (role !== 'error') {
211 chatHistory.push({ role, content, timestamp: Date.now() });
212 saveChatHistory();
213 }
214 }
215
216 // Add loading message
217 function addLoadingMessage() {
218 const container = document.getElementById('claude-chat-container');
219 const loadingId = 'loading-' + Date.now();
220
221 const messageDiv = document.createElement('div');
222 messageDiv.className = 'claude-message claude-message-assistant claude-message-loading';
223 messageDiv.id = loadingId;
224
225 const avatar = document.createElement('div');
226 avatar.className = 'claude-message-avatar';
227 avatar.textContent = 'AI';
228
229 const contentDiv = document.createElement('div');
230 contentDiv.className = 'claude-message-content';
231 contentDiv.innerHTML = '<div class="claude-loading-dots"><span></span><span></span><span></span></div>';
232
233 messageDiv.appendChild(avatar);
234 messageDiv.appendChild(contentDiv);
235 container.appendChild(messageDiv);
236
237 container.scrollTop = container.scrollHeight;
238
239 return loadingId;
240 }
241
242 // Remove loading message
243 function removeLoadingMessage(loadingId) {
244 const loadingMsg = document.getElementById(loadingId);
245 if (loadingMsg) {
246 loadingMsg.remove();
247 }
248 }
249
250 // Clear chat
251 async function clearChat() {
252 if (!confirm('Are you sure you want to clear the chat history?')) {
253 return;
254 }
255
256 chatHistory = [];
257 await saveChatHistory();
258
259 const container = document.getElementById('claude-chat-container');
260 container.innerHTML = `
261 <div class="claude-welcome-message">
262 <h4>Welcome to Claude AI Chat</h4>
263 <p>Ask me anything! I'm here to help you with information, analysis, or conversation.</p>
264 </div>
265 `;
266 }
267
268 // Save chat history
269 async function saveChatHistory() {
270 await GM.setValue('claude_chat_history', JSON.stringify(chatHistory));
271 }
272
273 // Restore chat history
274 function restoreChatHistory() {
275 if (chatHistory.length === 0) return;
276
277 const container = document.getElementById('claude-chat-container');
278 const welcomeMsg = container.querySelector('.claude-welcome-message');
279 if (welcomeMsg) {
280 welcomeMsg.remove();
281 }
282
283 chatHistory.forEach(msg => {
284 const messageDiv = document.createElement('div');
285 messageDiv.className = `claude-message claude-message-${msg.role}`;
286
287 const avatar = document.createElement('div');
288 avatar.className = 'claude-message-avatar';
289 avatar.textContent = msg.role === 'user' ? 'You' : 'AI';
290
291 const contentDiv = document.createElement('div');
292 contentDiv.className = 'claude-message-content';
293 contentDiv.textContent = msg.content;
294
295 messageDiv.appendChild(avatar);
296 messageDiv.appendChild(contentDiv);
297 container.appendChild(messageDiv);
298 });
299
300 container.scrollTop = container.scrollHeight;
301 }
302
303 // Add styles
304 TM_addStyle(`
305 #claude-toggle-btn {
306 position: fixed;
307 bottom: 20px;
308 right: 20px;
309 width: 56px;
310 height: 56px;
311 border-radius: 50%;
312 background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
313 border: none;
314 color: white;
315 cursor: pointer;
316 box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
317 z-index: 999999;
318 display: flex;
319 align-items: center;
320 justify-content: center;
321 transition: all 0.3s ease;
322 }
323
324 #claude-toggle-btn:hover {
325 transform: scale(1.1);
326 box-shadow: 0 6px 20px rgba(102, 126, 234, 0.6);
327 }
328
329 #claude-sidepanel {
330 position: fixed;
331 top: 0;
332 right: -400px;
333 width: 400px;
334 height: 100vh;
335 background: #ffffff;
336 box-shadow: -2px 0 20px rgba(0, 0, 0, 0.1);
337 z-index: 1000000;
338 display: flex;
339 flex-direction: column;
340 transition: right 0.3s ease;
341 font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
342 }
343
344 #claude-sidepanel.open {
345 right: 0;
346 }
347
348 .claude-panel-header {
349 padding: 20px;
350 background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
351 color: white;
352 display: flex;
353 justify-content: space-between;
354 align-items: center;
355 box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
356 }
357
358 .claude-panel-header h3 {
359 margin: 0;
360 font-size: 18px;
361 font-weight: 600;
362 }
363
364 .claude-header-actions {
365 display: flex;
366 gap: 8px;
367 }
368
369 .claude-header-actions button {
370 background: rgba(255, 255, 255, 0.2);
371 border: none;
372 color: white;
373 width: 32px;
374 height: 32px;
375 border-radius: 6px;
376 cursor: pointer;
377 display: flex;
378 align-items: center;
379 justify-content: center;
380 transition: background 0.2s ease;
381 }
382
383 .claude-header-actions button:hover {
384 background: rgba(255, 255, 255, 0.3);
385 }
386
387 .claude-chat-container {
388 flex: 1;
389 overflow-y: auto;
390 padding: 20px;
391 background: #f8f9fa;
392 }
393
394 .claude-welcome-message {
395 text-align: center;
396 padding: 40px 20px;
397 color: #6c757d;
398 }
399
400 .claude-welcome-message h4 {
401 margin: 0 0 10px 0;
402 color: #495057;
403 font-size: 20px;
404 }
405
406 .claude-welcome-message p {
407 margin: 0;
408 font-size: 14px;
409 line-height: 1.6;
410 }
411
412 .claude-message {
413 display: flex;
414 gap: 12px;
415 margin-bottom: 16px;
416 animation: fadeIn 0.3s ease;
417 }
418
419 @keyframes fadeIn {
420 from {
421 opacity: 0;
422 transform: translateY(10px);
423 }
424 to {
425 opacity: 1;
426 transform: translateY(0);
427 }
428 }
429
430 .claude-message-avatar {
431 width: 36px;
432 height: 36px;
433 border-radius: 50%;
434 display: flex;
435 align-items: center;
436 justify-content: center;
437 font-size: 11px;
438 font-weight: 600;
439 flex-shrink: 0;
440 }
441
442 .claude-message-user .claude-message-avatar {
443 background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
444 color: white;
445 }
446
447 .claude-message-assistant .claude-message-avatar {
448 background: #e9ecef;
449 color: #495057;
450 }
451
452 .claude-message-error .claude-message-avatar {
453 background: #fee;
454 color: #c00;
455 }
456
457 .claude-message-content {
458 flex: 1;
459 padding: 12px 16px;
460 border-radius: 12px;
461 line-height: 1.6;
462 font-size: 14px;
463 word-wrap: break-word;
464 }
465
466 .claude-message-user .claude-message-content {
467 background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
468 color: white;
469 }
470
471 .claude-message-assistant .claude-message-content {
472 background: white;
473 color: #212529;
474 box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
475 }
476
477 .claude-message-error .claude-message-content {
478 background: #fee;
479 color: #c00;
480 }
481
482 .claude-loading-dots {
483 display: flex;
484 gap: 4px;
485 padding: 4px 0;
486 }
487
488 .claude-loading-dots span {
489 width: 8px;
490 height: 8px;
491 border-radius: 50%;
492 background: #6c757d;
493 animation: bounce 1.4s infinite ease-in-out both;
494 }
495
496 .claude-loading-dots span:nth-child(1) {
497 animation-delay: -0.32s;
498 }
499
500 .claude-loading-dots span:nth-child(2) {
501 animation-delay: -0.16s;
502 }
503
504 @keyframes bounce {
505 0%, 80%, 100% {
506 transform: scale(0);
507 }
508 40% {
509 transform: scale(1);
510 }
511 }
512
513 .claude-input-container {
514 padding: 16px;
515 background: white;
516 border-top: 1px solid #e9ecef;
517 display: flex;
518 gap: 12px;
519 align-items: flex-end;
520 }
521
522 #claude-input {
523 flex: 1;
524 padding: 12px;
525 border: 2px solid #e9ecef;
526 border-radius: 12px;
527 font-size: 14px;
528 font-family: inherit;
529 resize: none;
530 outline: none;
531 transition: border-color 0.2s ease;
532 max-height: 150px;
533 }
534
535 #claude-input:focus {
536 border-color: #667eea;
537 }
538
539 #claude-send-btn {
540 width: 44px;
541 height: 44px;
542 border-radius: 12px;
543 background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
544 border: none;
545 color: white;
546 cursor: pointer;
547 display: flex;
548 align-items: center;
549 justify-content: center;
550 transition: all 0.2s ease;
551 flex-shrink: 0;
552 }
553
554 #claude-send-btn:hover {
555 transform: scale(1.05);
556 box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
557 }
558
559 #claude-send-btn:active {
560 transform: scale(0.95);
561 }
562
563 /* Scrollbar styling */
564 .claude-chat-container::-webkit-scrollbar {
565 width: 6px;
566 }
567
568 .claude-chat-container::-webkit-scrollbar-track {
569 background: transparent;
570 }
571
572 .claude-chat-container::-webkit-scrollbar-thumb {
573 background: #cbd5e0;
574 border-radius: 3px;
575 }
576
577 .claude-chat-container::-webkit-scrollbar-thumb:hover {
578 background: #a0aec0;
579 }
580 `);
581
582 // Initialize when DOM is ready
583 if (document.readyState === 'loading') {
584 document.addEventListener('DOMContentLoaded', init);
585 } else {
586 init();
587 }
588})();