Initial commit: Obsidian Web Interface for Claude Code
- Full IDE with terminal integration using xterm.js - Session management with local and web sessions - HTML preview functionality - Multi-terminal support with session picker Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
475
public/claude-ide/chat-enhanced.css
Normal file
475
public/claude-ide/chat-enhanced.css
Normal file
@@ -0,0 +1,475 @@
|
||||
/**
|
||||
* Enhanced Chat Styles - Similar to chat.z.ai
|
||||
* Smooth animations, better input, modern design
|
||||
*/
|
||||
|
||||
/* ============================================
|
||||
Chat Input Enhancements
|
||||
============================================ */
|
||||
|
||||
.chat-input-wrapper {
|
||||
transition: all 0.3s ease;
|
||||
border: 2px solid #333;
|
||||
}
|
||||
|
||||
.chat-input-wrapper.input-focused {
|
||||
border-color: #4a9eff;
|
||||
box-shadow: 0 0 0 3px rgba(74, 158, 255, 0.1);
|
||||
}
|
||||
|
||||
#chat-input {
|
||||
transition: height 0.2s ease;
|
||||
font-size: 15px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
#chat-input:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
Chat History Sidebar
|
||||
============================================ */
|
||||
|
||||
.chat-history-list {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.chat-history-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
margin-bottom: 8px;
|
||||
background: #252525;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.chat-history-item:hover {
|
||||
background: #2a2a2a;
|
||||
border-color: #444;
|
||||
transform: translateX(2px);
|
||||
}
|
||||
|
||||
.chat-history-item.active {
|
||||
background: #1a3a5a;
|
||||
border-color: #4a9eff;
|
||||
}
|
||||
|
||||
.chat-history-item.historical {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.chat-history-icon {
|
||||
font-size: 20px;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #1a1a1a;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.chat-history-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.chat-history-title {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #e0e0e0;
|
||||
margin-bottom: 4px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.chat-history-meta {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.chat-history-status {
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
background: #1a1a1a;
|
||||
}
|
||||
|
||||
.chat-history-status.active {
|
||||
color: #51cf66;
|
||||
background: rgba(81, 207, 102, 0.1);
|
||||
}
|
||||
|
||||
.chat-history-status.historical {
|
||||
color: #ffa94d;
|
||||
background: rgba(255, 169, 77, 0.1);
|
||||
}
|
||||
|
||||
.resume-badge {
|
||||
font-size: 11px;
|
||||
padding: 4px 8px;
|
||||
background: #4a9eff;
|
||||
color: white;
|
||||
border-radius: 4px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.chat-history-empty {
|
||||
text-align: center;
|
||||
padding: 40px 20px;
|
||||
color: #888;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
Enhanced Message Animations
|
||||
============================================ */
|
||||
|
||||
.chat-message {
|
||||
animation: messageSlideIn 0.3s ease;
|
||||
}
|
||||
|
||||
@keyframes messageSlideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.message-appear {
|
||||
animation: messageAppear 0.4s cubic-bezier(0.16, 1, 0.3, 1);
|
||||
}
|
||||
|
||||
@keyframes messageAppear {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.95) translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1) translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.message-avatar {
|
||||
animation: avatarPop 0.3s cubic-bezier(0.68, -0.55, 0.265, 1.55);
|
||||
}
|
||||
|
||||
@keyframes avatarPop {
|
||||
0% {
|
||||
transform: scale(0);
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
Code Operation Styling
|
||||
============================================ */
|
||||
|
||||
.code-operation {
|
||||
background: #1a1a1a;
|
||||
border: 1px solid #333;
|
||||
border-radius: 8px;
|
||||
margin: 12px 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.operation-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 14px;
|
||||
background: #252525;
|
||||
border-bottom: 1px solid #333;
|
||||
}
|
||||
|
||||
.operation-icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.operation-label {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
.operation-code {
|
||||
margin: 0;
|
||||
padding: 14px;
|
||||
background: #0d0d0d;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.operation-code code {
|
||||
font-family: 'Fira Code', 'Courier New', monospace;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
Quick Actions
|
||||
============================================ */
|
||||
|
||||
.quick-actions {
|
||||
padding: 20px;
|
||||
background: #1a1a1a;
|
||||
border-radius: 12px;
|
||||
margin: 20px 0;
|
||||
animation: quickActionsSlide 0.4s ease;
|
||||
}
|
||||
|
||||
@keyframes quickActionsSlide {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.quick-actions-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #888;
|
||||
margin-bottom: 16px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.quick-actions-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.quick-action-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 14px 16px;
|
||||
background: #252525;
|
||||
border: 1px solid #333;
|
||||
border-radius: 8px;
|
||||
color: #e0e0e0;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.quick-action-btn:hover {
|
||||
background: #2a2a2a;
|
||||
border-color: #4a9eff;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(74, 158, 255, 0.2);
|
||||
}
|
||||
|
||||
.quick-action-btn:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.action-icon {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.action-label {
|
||||
flex: 1;
|
||||
text-align: left;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
Tag Placeholders
|
||||
============================================ */
|
||||
|
||||
.tag-placeholder {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 4px 10px;
|
||||
background: #1a3a5a;
|
||||
border: 1px solid #4a9eff;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
color: #4a9eff;
|
||||
margin: 4px;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
Enhanced Chat Layout
|
||||
============================================ */
|
||||
|
||||
.chat-layout {
|
||||
height: calc(100vh - 60px);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.chat-main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.chat-messages {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 20px;
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
.chat-messages::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.chat-messages::-webkit-scrollbar-track {
|
||||
background: #1a1a1a;
|
||||
}
|
||||
|
||||
.chat-messages::-webkit-scrollbar-thumb {
|
||||
background: #333;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.chat-messages::-webkit-scrollbar-thumb:hover {
|
||||
background: #444;
|
||||
}
|
||||
|
||||
.chat-input-container {
|
||||
border-top: 1px solid #333;
|
||||
background: #1a1a1a;
|
||||
padding: 16px 20px;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
Message Header Styling
|
||||
============================================ */
|
||||
|
||||
.message-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.message-label {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
.message-time {
|
||||
font-size: 11px;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
Responsive Design
|
||||
============================================ */
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.quick-actions-grid {
|
||||
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.chat-history-item {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.quick-action-btn {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.action-icon {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.action-label {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
Loading States
|
||||
============================================ */
|
||||
|
||||
.chat-history-loading {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.chat-history-loading .spinner {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border: 3px solid #333;
|
||||
border-top-color: #4a9eff;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
Typing Indicator
|
||||
============================================ */
|
||||
|
||||
.typing-indicator {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
padding: 12px 16px;
|
||||
background: #1a1a1a;
|
||||
border-radius: 12px;
|
||||
width: fit-content;
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
.typing-indicator .dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: #888;
|
||||
border-radius: 50%;
|
||||
animation: typingBounce 1.4s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.typing-indicator .dot:nth-child(1) {
|
||||
animation-delay: 0s;
|
||||
}
|
||||
|
||||
.typing-indicator .dot:nth-child(2) {
|
||||
animation-delay: 0.2s;
|
||||
}
|
||||
|
||||
.typing-indicator .dot:nth-child(3) {
|
||||
animation-delay: 0.4s;
|
||||
}
|
||||
|
||||
@keyframes typingBounce {
|
||||
0%, 60%, 100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
30% {
|
||||
transform: translateY(-8px);
|
||||
}
|
||||
}
|
||||
422
public/claude-ide/chat-enhanced.js
Normal file
422
public/claude-ide/chat-enhanced.js
Normal file
@@ -0,0 +1,422 @@
|
||||
/**
|
||||
* Enhanced Chat Interface - Similar to chat.z.ai
|
||||
* Features: Better input, chat history, session resumption, smooth animations
|
||||
*/
|
||||
|
||||
// ============================================
|
||||
// Enhanced Chat Input Experience
|
||||
// ============================================
|
||||
|
||||
// Auto-focus chat input when switching to chat view
|
||||
function focusChatInput() {
|
||||
setTimeout(() => {
|
||||
const input = document.getElementById('chat-input');
|
||||
if (input) {
|
||||
input.focus();
|
||||
// Move cursor to end
|
||||
input.setSelectionRange(input.value.length, input.value.length);
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
// Smooth textarea resize with animation
|
||||
function enhanceChatInput() {
|
||||
const input = document.getElementById('chat-input');
|
||||
if (!input) return;
|
||||
|
||||
// Auto-resize with smooth transition
|
||||
input.style.transition = 'height 0.2s ease';
|
||||
input.addEventListener('input', function() {
|
||||
this.style.height = 'auto';
|
||||
const newHeight = Math.min(this.scrollHeight, 200);
|
||||
this.style.height = newHeight + 'px';
|
||||
});
|
||||
|
||||
// Focus animation
|
||||
input.addEventListener('focus', function() {
|
||||
this.parentElement.classList.add('input-focused');
|
||||
});
|
||||
|
||||
input.addEventListener('blur', function() {
|
||||
this.parentElement.classList.remove('input-focused');
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Chat History & Session Management
|
||||
// ============================================
|
||||
|
||||
// Load chat history with sessions
|
||||
// loadChatHistory is now in chat-functions.js to avoid conflicts
|
||||
// This file only provides the enhanced features (animations, quick actions, etc.)
|
||||
try {
|
||||
const res = await fetch('/claude/api/claude/sessions');
|
||||
const data = await res.json();
|
||||
|
||||
const historyList = document.getElementById('chat-history-list');
|
||||
if (!historyList) return;
|
||||
|
||||
// Combine active and historical sessions
|
||||
const allSessions = [
|
||||
...(data.active || []).map(s => ({...s, status: 'active'})),
|
||||
...(data.historical || []).map(s => ({...s, status: 'historical'}))
|
||||
];
|
||||
|
||||
// Sort by creation date (newest first)
|
||||
allSessions.sort((a, b) => new Date(b.createdAt || b.created_at) - new Date(a.createdAt || b.created_at));
|
||||
|
||||
if (allSessions.length === 0) {
|
||||
historyList.innerHTML = '<div class="chat-history-empty">No chat history yet</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
historyList.innerHTML = allSessions.map(session => {
|
||||
const title = session.metadata?.project ||
|
||||
session.project ||
|
||||
session.id.substring(0, 12) + '...';
|
||||
const date = new Date(session.createdAt || session.created_at).toLocaleDateString();
|
||||
const isActive = session.id === attachedSessionId;
|
||||
|
||||
return `
|
||||
<div class="chat-history-item ${isActive ? 'active' : ''} ${session.status === 'historical' ? 'historical' : ''}"
|
||||
onclick="${session.status === 'historical' ? `resumeSession('${session.id}')` : `attachToSession('${session.id}')`}">
|
||||
<div class="chat-history-icon">
|
||||
${session.status === 'historical' ? '📁' : '💬'}
|
||||
</div>
|
||||
<div class="chat-history-content">
|
||||
<div class="chat-history-title">${title}</div>
|
||||
<div class="chat-history-meta">
|
||||
<span class="chat-history-date">${date}</span>
|
||||
<span class="chat-history-status ${session.status}">
|
||||
${session.status === 'historical' ? 'Historical' : 'Active'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
${session.status === 'historical' ? '<span class="resume-badge">Resume</span>' : ''}
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error loading chat history:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Resume historical session
|
||||
async function resumeSession(sessionId) {
|
||||
console.log('Resuming historical session:', sessionId);
|
||||
|
||||
// Show loading message
|
||||
appendSystemMessage('📂 Loading historical session...');
|
||||
|
||||
try {
|
||||
// Load the historical session
|
||||
const res = await fetch('/claude/api/claude/sessions/' + sessionId);
|
||||
|
||||
// Check if response is OK
|
||||
if (!res.ok) {
|
||||
const errorText = await res.text();
|
||||
console.error('Session fetch error:', res.status, errorText);
|
||||
|
||||
// Handle 404 - session not found
|
||||
if (res.status === 404) {
|
||||
appendSystemMessage('❌ Session not found. It may have been deleted or the ID is incorrect.');
|
||||
return;
|
||||
}
|
||||
|
||||
throw new Error(`HTTP ${res.status}: ${errorText}`);
|
||||
}
|
||||
|
||||
// Parse JSON with error handling
|
||||
let data;
|
||||
try {
|
||||
data = await res.json();
|
||||
} catch (jsonError) {
|
||||
const responseText = await res.text();
|
||||
console.error('JSON parse error:', jsonError);
|
||||
console.error('Response text:', responseText);
|
||||
throw new Error('Invalid JSON response from server');
|
||||
}
|
||||
|
||||
if (data.session) {
|
||||
attachedSessionId = sessionId;
|
||||
chatSessionId = sessionId;
|
||||
|
||||
// Update UI
|
||||
document.getElementById('current-session-id').textContent = sessionId;
|
||||
|
||||
// Load session messages
|
||||
clearChatDisplay();
|
||||
|
||||
// Add historical messages
|
||||
if (data.session.outputBuffer && data.session.outputBuffer.length > 0) {
|
||||
data.session.outputBuffer.forEach(entry => {
|
||||
appendMessage('assistant', entry.content, false);
|
||||
});
|
||||
}
|
||||
|
||||
// Show resume message
|
||||
const sessionDate = new Date(data.session.createdAt || data.session.created_at);
|
||||
appendSystemMessage('✅ Resumed historical session from ' + sessionDate.toLocaleString());
|
||||
|
||||
appendSystemMessage('ℹ️ This is a read-only historical session. Start a new chat to continue working.');
|
||||
|
||||
// Update active state in sidebar
|
||||
loadChatHistory();
|
||||
|
||||
// Subscribe to session (for any future updates)
|
||||
subscribeToSession(sessionId);
|
||||
} else {
|
||||
throw new Error('No session data in response');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error resuming session:', error);
|
||||
appendSystemMessage('❌ Failed to resume session: ' + error.message);
|
||||
|
||||
// Remove the loading message
|
||||
const messagesContainer = document.getElementById('chat-messages');
|
||||
const loadingMessages = messagesContainer.querySelectorAll('.chat-system');
|
||||
loadingMessages.forEach(msg => {
|
||||
if (msg.textContent.includes('Loading historical session')) {
|
||||
msg.remove();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Enhanced Message Rendering
|
||||
// ============================================
|
||||
|
||||
// Enhanced append with animations
|
||||
function appendMessageWithAnimation(role, content, animate = true) {
|
||||
const messagesContainer = document.getElementById('chat-messages');
|
||||
if (!messagesContainer) return;
|
||||
|
||||
const messageDiv = document.createElement('div');
|
||||
messageDiv.className = `chat-message chat-message-${role} ${animate ? 'message-appear' : ''}`;
|
||||
|
||||
const avatar = role === 'user' ? '👤' : '🤖';
|
||||
const label = role === 'user' ? 'You' : 'Claude';
|
||||
|
||||
// Strip dyad tags for display
|
||||
const displayContent = stripDyadTags(content);
|
||||
|
||||
messageDiv.innerHTML = `
|
||||
<div class="message-avatar">${avatar}</div>
|
||||
<div class="message-content">
|
||||
<div class="message-header">
|
||||
<span class="message-label">${label}</span>
|
||||
<span class="message-time">${new Date().toLocaleTimeString()}</span>
|
||||
</div>
|
||||
<div class="message-text">${formatMessageText(displayContent)}</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
messagesContainer.appendChild(messageDiv);
|
||||
|
||||
// Scroll to bottom
|
||||
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
||||
|
||||
// Update token usage
|
||||
updateTokenUsage(content.length);
|
||||
}
|
||||
|
||||
// Strip dyad tags from message for display
|
||||
function stripDyadTags(content) {
|
||||
let stripped = content;
|
||||
|
||||
// Remove dyad-write tags and replace with placeholder
|
||||
stripped = stripped.replace(/<dyad-write\s+path="[^"]+">([\s\S]*?)<\/dyad-write>/g, (match, content) => {
|
||||
return `
|
||||
<div class="code-operation">
|
||||
<div class="operation-header">
|
||||
<span class="operation-icon">📄</span>
|
||||
<span class="operation-label">Code generated</span>
|
||||
</div>
|
||||
<pre class="operation-code"><code>${escapeHtml(content.trim())}</code></pre>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
// Remove other dyad tags
|
||||
stripped = stripped.replace(/<dyad-[^>]+>/g, (match) => {
|
||||
const tagType = match.match(/dyad-(\w+)/)?.[1] || 'operation';
|
||||
const icons = {
|
||||
'rename': '✏️',
|
||||
'delete': '🗑️',
|
||||
'add-dependency': '📦',
|
||||
'command': '⚡'
|
||||
};
|
||||
return `<span class="tag-placeholder">${icons[tagType] || '⚙️'} ${tagType}</span>`;
|
||||
});
|
||||
|
||||
return stripped;
|
||||
}
|
||||
|
||||
// Format message text with markdown-like rendering
|
||||
function formatMessageText(text) {
|
||||
// Basic markdown-like formatting
|
||||
let formatted = escapeHtml(text);
|
||||
|
||||
// Code blocks
|
||||
formatted = formatted.replace(/```(\w+)?\n([\s\S]*?)```/g, (match, lang, code) => {
|
||||
return `<pre><code class="language-${lang || 'text'}">${escapeHtml(code.trim())}</code></pre>`;
|
||||
});
|
||||
|
||||
// Inline code
|
||||
formatted = formatted.replace(/`([^`]+)`/g, '<code>$1</code>');
|
||||
|
||||
// Bold
|
||||
formatted = formatted.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
|
||||
|
||||
// Links
|
||||
formatted = formatted.replace(/https?:\/\/[^\s]+/g, '<a href="$&" target="_blank">$&</a>');
|
||||
|
||||
// Line breaks
|
||||
formatted = formatted.replace(/\n/g, '<br>');
|
||||
|
||||
return formatted;
|
||||
}
|
||||
|
||||
// Escape HTML
|
||||
function escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Quick Actions & Suggestions
|
||||
// ============================================
|
||||
|
||||
// Show quick action suggestions
|
||||
function showQuickActions() {
|
||||
const messagesContainer = document.getElementById('chat-messages');
|
||||
if (!messagesContainer) return;
|
||||
|
||||
const quickActions = document.createElement('div');
|
||||
quickActions.className = 'quick-actions';
|
||||
quickActions.innerHTML = `
|
||||
<div class="quick-actions-title">💡 Quick Actions</div>
|
||||
<div class="quick-actions-grid">
|
||||
<button class="quick-action-btn" onclick="executeQuickAction('create-react')">
|
||||
<span class="action-icon">⚛️</span>
|
||||
<span class="action-label">Create React App</span>
|
||||
</button>
|
||||
<button class="quick-action-btn" onclick="executeQuickAction('create-nextjs')">
|
||||
<span class="action-icon">▲</span>
|
||||
<span class="action-label">Create Next.js App</span>
|
||||
</button>
|
||||
<button class="quick-action-btn" onclick="executeQuickAction('create-vue')">
|
||||
<span class="action-icon">💚</span>
|
||||
<span class="action-label">Create Vue App</span>
|
||||
</button>
|
||||
<button class="quick-action-btn" onclick="executeQuickAction('create-html')">
|
||||
<span class="action-icon">📄</span>
|
||||
<span class="action-label">Create HTML Page</span>
|
||||
</button>
|
||||
<button class="quick-action-btn" onclick="executeQuickAction('explain-code')">
|
||||
<span class="action-icon">📖</span>
|
||||
<span class="action-label">Explain Codebase</span>
|
||||
</button>
|
||||
<button class="quick-action-btn" onclick="executeQuickAction('fix-bug')">
|
||||
<span class="action-icon">🐛</span>
|
||||
<span class="action-label">Fix Bug</span>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
messagesContainer.appendChild(quickActions);
|
||||
}
|
||||
|
||||
// Execute quick action
|
||||
function executeQuickAction(action) {
|
||||
const actions = {
|
||||
'create-react': 'Create a React app with components and routing',
|
||||
'create-nextjs': 'Create a Next.js app with server-side rendering',
|
||||
'create-vue': 'Create a Vue 3 app with composition API',
|
||||
'create-html': 'Create a responsive HTML5 page with modern styling',
|
||||
'explain-code': 'Explain the codebase structure and main files',
|
||||
'fix-bug': 'Help me fix a bug in my code'
|
||||
};
|
||||
|
||||
const prompt = actions[action];
|
||||
if (prompt) {
|
||||
const input = document.getElementById('chat-input');
|
||||
if (input) {
|
||||
input.value = prompt;
|
||||
input.focus();
|
||||
// Auto-send after short delay
|
||||
setTimeout(() => sendChatMessage(), 300);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Enhanced Chat View Loading
|
||||
// ============================================
|
||||
|
||||
// Hook into loadChatView to add enhancements
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Wait for chat-functions.js to load
|
||||
setTimeout(() => {
|
||||
// Override loadChatView to add enhancements
|
||||
if (typeof window.loadChatView === 'function') {
|
||||
const originalLoadChatView = window.loadChatView;
|
||||
window.loadChatView = async function() {
|
||||
// Call original function first
|
||||
await originalLoadChatView.call(this);
|
||||
|
||||
// Add our enhancements
|
||||
setTimeout(() => {
|
||||
enhanceChatInput();
|
||||
loadChatHistory();
|
||||
focusChatInput();
|
||||
|
||||
// Show quick actions on first load
|
||||
const messagesContainer = document.getElementById('chat-messages');
|
||||
if (messagesContainer && messagesContainer.querySelector('.chat-welcome')) {
|
||||
showQuickActions();
|
||||
}
|
||||
}, 100);
|
||||
};
|
||||
}
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
// Auto-start enhancements when chat view is active
|
||||
const observer = new MutationObserver((mutations) => {
|
||||
mutations.forEach((mutation) => {
|
||||
if (mutation.target.id === 'chat-view' && mutation.target.classList.contains('active')) {
|
||||
enhanceChatInput();
|
||||
loadChatHistory();
|
||||
focusChatInput();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Start observing after DOM loads
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
setTimeout(() => {
|
||||
const chatView = document.getElementById('chat-view');
|
||||
if (chatView) observer.observe(chatView, { attributes: true });
|
||||
}, 1500);
|
||||
});
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
const chatView = document.getElementById('chat-view');
|
||||
if (chatView) observer.observe(chatView, { attributes: true });
|
||||
}, 1500);
|
||||
}
|
||||
|
||||
// Export functions
|
||||
if (typeof window !== 'undefined') {
|
||||
window.resumeSession = resumeSession;
|
||||
window.loadChatHistory = loadChatHistory;
|
||||
window.executeQuickAction = executeQuickAction;
|
||||
window.showQuickActions = showQuickActions;
|
||||
window.enhanceChatInput = enhanceChatInput;
|
||||
window.focusChatInput = focusChatInput;
|
||||
}
|
||||
499
public/claude-ide/chat-functions.js
Normal file
499
public/claude-ide/chat-functions.js
Normal file
@@ -0,0 +1,499 @@
|
||||
// ============================================
|
||||
// Chat Interface Functions
|
||||
// ============================================
|
||||
|
||||
let chatSessionId = null;
|
||||
let chatMessages = [];
|
||||
let attachedSessionId = null;
|
||||
|
||||
// Reset all chat state
|
||||
function resetChatState() {
|
||||
console.log('Resetting chat state...');
|
||||
chatSessionId = null;
|
||||
chatMessages = [];
|
||||
attachedSessionId = null;
|
||||
console.log('Chat state reset complete');
|
||||
}
|
||||
|
||||
// Load Chat View
|
||||
async function loadChatView() {
|
||||
console.log('[loadChatView] Loading chat view...');
|
||||
|
||||
// Reset state on view load to prevent stale session references
|
||||
resetChatState();
|
||||
|
||||
// Load chat sessions
|
||||
try {
|
||||
console.log('[loadChatView] Fetching sessions...');
|
||||
const res = await fetch('/claude/api/claude/sessions');
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(`HTTP ${res.status}: ${await res.text()}`);
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
console.log('[loadChatView] Sessions data received:', data);
|
||||
|
||||
const sessionsListEl = document.getElementById('chat-history-list');
|
||||
|
||||
if (!sessionsListEl) {
|
||||
console.error('[loadChatView] chat-history-list element not found!');
|
||||
return;
|
||||
}
|
||||
|
||||
// ONLY show active sessions - no historical sessions in chat view
|
||||
// Historical sessions are read-only and can't receive new messages
|
||||
const activeSessions = (data.active || []).filter(s => s.status === 'running');
|
||||
|
||||
console.log('Active sessions (can receive messages):', activeSessions.length);
|
||||
|
||||
if (activeSessions.length > 0) {
|
||||
sessionsListEl.innerHTML = activeSessions.map(session => {
|
||||
const projectName = session.metadata && session.metadata.project ?
|
||||
session.metadata.project :
|
||||
session.id.substring(0, 20);
|
||||
return `
|
||||
<div class="chat-history-item ${session.id === attachedSessionId ? 'active' : ''}"
|
||||
onclick="attachToSession('${session.id}')">
|
||||
<div class="chat-history-icon">💬</div>
|
||||
<div class="chat-history-content">
|
||||
<div class="chat-history-title">${projectName}</div>
|
||||
<div class="chat-history-meta">
|
||||
<span class="chat-history-date">${new Date(session.createdAt).toLocaleDateString()}</span>
|
||||
<span class="chat-history-status active">Running</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
} else {
|
||||
sessionsListEl.innerHTML = `
|
||||
<div class="chat-history-empty">
|
||||
<p>No active sessions</p>
|
||||
<button class="btn-primary" onclick="startNewChat()" style="margin-top: 12px;">+ Start New Chat</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
console.log('[loadChatView] Chat view loaded successfully');
|
||||
} catch (error) {
|
||||
console.error('[loadChatView] Error loading chat sessions:', error);
|
||||
const sessionsListEl = document.getElementById('chat-history-list');
|
||||
if (sessionsListEl) {
|
||||
sessionsListEl.innerHTML = `
|
||||
<div class="chat-history-empty">
|
||||
<p>Error: ${error.message}</p>
|
||||
<button class="btn-secondary" onclick="loadChatView()" style="margin-top: 12px;">Retry</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Start New Chat
|
||||
async function startNewChat() {
|
||||
// Reset all state first
|
||||
resetChatState();
|
||||
|
||||
// Clear current chat
|
||||
clearChatDisplay();
|
||||
|
||||
appendSystemMessage('Creating new chat session...');
|
||||
|
||||
// Create new session
|
||||
try {
|
||||
console.log('Creating new Claude Code session...');
|
||||
const res = await fetch('/claude/api/claude/sessions', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
workingDir: '/home/uroma/obsidian-vault',
|
||||
metadata: { type: 'chat', source: 'web-ide' }
|
||||
})
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
console.log('Session creation response:', data);
|
||||
|
||||
if (data.success) {
|
||||
attachedSessionId = data.session.id;
|
||||
chatSessionId = data.session.id;
|
||||
|
||||
console.log('New session created:', data.session.id);
|
||||
|
||||
// Update UI
|
||||
document.getElementById('current-session-id').textContent = data.session.id;
|
||||
document.getElementById('chat-title').textContent = 'New Chat';
|
||||
|
||||
// Subscribe to session via WebSocket
|
||||
subscribeToSession(data.session.id);
|
||||
|
||||
// Reload sessions list
|
||||
loadChatView();
|
||||
|
||||
// Show success message
|
||||
appendSystemMessage('✅ New chat session started! You can now chat with Claude Code.');
|
||||
} else {
|
||||
console.error('Session creation failed:', data);
|
||||
appendSystemMessage('❌ Failed to create session: ' + (data.error || 'Unknown error'));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error starting new chat:', error);
|
||||
appendSystemMessage('❌ Failed to start new chat session: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Attach to Existing Session
|
||||
function attachToSession(sessionId) {
|
||||
attachedSessionId = sessionId;
|
||||
chatSessionId = sessionId;
|
||||
|
||||
// Update UI
|
||||
document.getElementById('current-session-id').textContent = sessionId;
|
||||
|
||||
// Load session messages
|
||||
loadSessionMessages(sessionId);
|
||||
|
||||
// Subscribe to session via WebSocket
|
||||
subscribeToSession(sessionId);
|
||||
|
||||
// Update active state in sidebar
|
||||
document.querySelectorAll('.chat-session-item').forEach(item => {
|
||||
item.classList.remove('active');
|
||||
if (item.getAttribute('onclick') && item.getAttribute('onclick').includes(sessionId)) {
|
||||
item.classList.add('active');
|
||||
}
|
||||
});
|
||||
|
||||
appendSystemMessage('Attached to session: ' + sessionId);
|
||||
}
|
||||
|
||||
// Subscribe to session via WebSocket
|
||||
function subscribeToSession(sessionId) {
|
||||
if (window.ws && window.ws.readyState === WebSocket.OPEN) {
|
||||
window.ws.send(JSON.stringify({
|
||||
type: 'subscribe',
|
||||
sessionId: sessionId
|
||||
}));
|
||||
console.log('Subscribed to session:', sessionId);
|
||||
}
|
||||
}
|
||||
|
||||
// Load Session Messages
|
||||
async function loadSessionMessages(sessionId) {
|
||||
try {
|
||||
const res = await fetch('/claude/api/claude/sessions/' + sessionId);
|
||||
const data = await res.json();
|
||||
|
||||
if (data.session) {
|
||||
clearChatDisplay();
|
||||
|
||||
// Add existing messages from output buffer
|
||||
data.session.outputBuffer.forEach(entry => {
|
||||
appendMessage('assistant', entry.content, false);
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading session messages:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle Chat Key Press
|
||||
function handleChatKeypress(event) {
|
||||
const input = document.getElementById('chat-input');
|
||||
|
||||
// Update character count
|
||||
const charCount = input.value.length;
|
||||
document.getElementById('char-count').textContent = charCount + ' characters';
|
||||
|
||||
// Send on Enter (but allow Shift+Enter for new line)
|
||||
if (event.key === 'Enter' && !event.shiftKey) {
|
||||
event.preventDefault();
|
||||
sendChatMessage();
|
||||
}
|
||||
|
||||
// Auto-resize textarea
|
||||
input.style.height = 'auto';
|
||||
input.style.height = Math.min(input.scrollHeight, 150) + 'px';
|
||||
}
|
||||
|
||||
// Send Chat Message
|
||||
async function sendChatMessage() {
|
||||
const input = document.getElementById('chat-input');
|
||||
const message = input.value.trim();
|
||||
|
||||
if (!message) return;
|
||||
|
||||
if (!attachedSessionId) {
|
||||
appendSystemMessage('Please start or attach to a session first.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Add user message to chat
|
||||
appendMessage('user', message);
|
||||
clearInput();
|
||||
|
||||
// Show streaming indicator
|
||||
showStreamingIndicator();
|
||||
|
||||
try {
|
||||
// Check WebSocket state
|
||||
if (!window.ws) {
|
||||
console.error('WebSocket is null/undefined');
|
||||
appendSystemMessage('WebSocket not initialized. Please refresh the page.');
|
||||
hideStreamingIndicator();
|
||||
return;
|
||||
}
|
||||
|
||||
const state = window.ws.readyState;
|
||||
const stateName = ['CONNECTING', 'OPEN', 'CLOSING', 'CLOSED'][state] || 'UNKNOWN';
|
||||
|
||||
console.log('WebSocket state:', state, stateName);
|
||||
|
||||
if (state !== WebSocket.OPEN) {
|
||||
console.error('WebSocket not in OPEN state:', stateName);
|
||||
appendSystemMessage(`WebSocket not ready (state: ${stateName}). Retrying...`);
|
||||
hideStreamingIndicator();
|
||||
|
||||
// Trigger reconnection if closed
|
||||
if (state === WebSocket.CLOSED) {
|
||||
console.log('WebSocket closed, triggering reconnection...');
|
||||
if (typeof connectWebSocket === 'function') {
|
||||
connectWebSocket();
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Send command via WebSocket
|
||||
window.ws.send(JSON.stringify({
|
||||
type: 'command',
|
||||
sessionId: attachedSessionId,
|
||||
command: message
|
||||
}));
|
||||
console.log('Sent command via WebSocket:', message.substring(0, 50));
|
||||
} catch (error) {
|
||||
console.error('Error sending message:', error);
|
||||
hideStreamingIndicator();
|
||||
appendSystemMessage('Failed to send message: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Append Message to Chat
|
||||
function appendMessage(role, content, scroll) {
|
||||
const messagesContainer = document.getElementById('chat-messages');
|
||||
|
||||
// Remove welcome message if present
|
||||
const welcome = messagesContainer.querySelector('.chat-welcome');
|
||||
if (welcome) {
|
||||
welcome.remove();
|
||||
}
|
||||
|
||||
// Remove streaming indicator if present
|
||||
const streaming = messagesContainer.querySelector('.streaming-indicator');
|
||||
if (streaming) {
|
||||
streaming.remove();
|
||||
}
|
||||
|
||||
const messageDiv = document.createElement('div');
|
||||
messageDiv.className = 'chat-message ' + role;
|
||||
|
||||
const avatar = document.createElement('div');
|
||||
avatar.className = 'chat-message-avatar';
|
||||
avatar.textContent = role === 'user' ? '👤' : '🤖';
|
||||
|
||||
const contentDiv = document.createElement('div');
|
||||
contentDiv.className = 'chat-message-content';
|
||||
|
||||
const bubble = document.createElement('div');
|
||||
bubble.className = 'chat-message-bubble';
|
||||
|
||||
// Format content (handle code blocks, etc.)
|
||||
bubble.innerHTML = formatMessage(content);
|
||||
|
||||
const timestamp = document.createElement('div');
|
||||
timestamp.className = 'chat-message-timestamp';
|
||||
timestamp.textContent = new Date().toLocaleTimeString();
|
||||
|
||||
contentDiv.appendChild(bubble);
|
||||
contentDiv.appendChild(timestamp);
|
||||
|
||||
messageDiv.appendChild(avatar);
|
||||
messageDiv.appendChild(contentDiv);
|
||||
|
||||
messagesContainer.appendChild(messageDiv);
|
||||
|
||||
// Scroll to bottom
|
||||
if (scroll) {
|
||||
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
||||
}
|
||||
|
||||
// Update token usage (estimated)
|
||||
updateTokenUsage(content.length);
|
||||
}
|
||||
|
||||
// Append System Message
|
||||
function appendSystemMessage(text) {
|
||||
const messagesContainer = document.getElementById('chat-messages');
|
||||
|
||||
// Remove welcome message if present
|
||||
const welcome = messagesContainer.querySelector('.chat-welcome');
|
||||
if (welcome) {
|
||||
welcome.remove();
|
||||
}
|
||||
|
||||
const systemDiv = document.createElement('div');
|
||||
systemDiv.className = 'chat-message assistant';
|
||||
systemDiv.style.opacity = '0.8';
|
||||
|
||||
const avatar = document.createElement('div');
|
||||
avatar.className = 'chat-message-avatar';
|
||||
avatar.textContent = 'ℹ️';
|
||||
|
||||
const contentDiv = document.createElement('div');
|
||||
contentDiv.className = 'chat-message-content';
|
||||
|
||||
const bubble = document.createElement('div');
|
||||
bubble.className = 'chat-message-bubble';
|
||||
bubble.innerHTML = '<em>' + escapeHtml(text) + '</em>';
|
||||
|
||||
contentDiv.appendChild(bubble);
|
||||
systemDiv.appendChild(avatar);
|
||||
systemDiv.appendChild(contentDiv);
|
||||
|
||||
messagesContainer.appendChild(systemDiv);
|
||||
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
||||
}
|
||||
|
||||
// Format Message (handle code blocks, markdown, etc.)
|
||||
function formatMessage(content) {
|
||||
// Escape HTML first
|
||||
let formatted = escapeHtml(content);
|
||||
|
||||
// Handle code blocks
|
||||
formatted = formatted.replace(/```(\w+)?\n([\s\S]*?)```/g, function(match, lang, code) {
|
||||
return '<pre><code class="language-' + (lang || 'text') + '">' + code.trim() + '</code></pre>';
|
||||
});
|
||||
|
||||
// Handle inline code
|
||||
formatted = formatted.replace(/`([^`]+)`/g, '<code>$1</code>');
|
||||
|
||||
// Handle line breaks
|
||||
formatted = formatted.replace(/\n/g, '<br>');
|
||||
|
||||
return formatted;
|
||||
}
|
||||
|
||||
// Show Streaming Indicator
|
||||
function showStreamingIndicator() {
|
||||
const messagesContainer = document.getElementById('chat-messages');
|
||||
|
||||
// Remove existing streaming indicator
|
||||
const existing = messagesContainer.querySelector('.streaming-indicator');
|
||||
if (existing) {
|
||||
existing.remove();
|
||||
}
|
||||
|
||||
const streamingDiv = document.createElement('div');
|
||||
streamingDiv.className = 'streaming-indicator';
|
||||
streamingDiv.innerHTML = '<div class="streaming-dot"></div><div class="streaming-dot"></div><div class="streaming-dot"></div>';
|
||||
|
||||
messagesContainer.appendChild(streamingDiv);
|
||||
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
||||
}
|
||||
|
||||
// Hide Streaming Indicator
|
||||
function hideStreamingIndicator() {
|
||||
const streaming = document.querySelector('.streaming-indicator');
|
||||
if (streaming) {
|
||||
streaming.remove();
|
||||
}
|
||||
}
|
||||
|
||||
// Clear Chat Display
|
||||
function clearChatDisplay() {
|
||||
const messagesContainer = document.getElementById('chat-messages');
|
||||
messagesContainer.innerHTML = '';
|
||||
|
||||
// Reset token usage
|
||||
document.getElementById('token-usage').textContent = '0 tokens used';
|
||||
}
|
||||
|
||||
// Clear Chat
|
||||
function clearChat() {
|
||||
if (confirm('Clear all messages in this chat?')) {
|
||||
clearChatDisplay();
|
||||
|
||||
// Show welcome message again
|
||||
const messagesContainer = document.getElementById('chat-messages');
|
||||
messagesContainer.innerHTML = `
|
||||
<div class="chat-welcome">
|
||||
<h2>👋 Chat Cleared</h2>
|
||||
<p>Start a new conversation with Claude Code.</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
// Clear Input
|
||||
function clearInput() {
|
||||
const input = document.getElementById('chat-input');
|
||||
input.value = '';
|
||||
input.style.height = 'auto';
|
||||
document.getElementById('char-count').textContent = '0 characters';
|
||||
}
|
||||
|
||||
// Update Token Usage
|
||||
function updateTokenUsage(charCount) {
|
||||
// Rough estimation: ~4 characters per token
|
||||
const estimatedTokens = Math.ceil(charCount / 4);
|
||||
const currentUsage = parseInt(document.getElementById('token-usage').textContent) || 0;
|
||||
document.getElementById('token-usage').textContent = (currentUsage + estimatedTokens) + ' tokens used';
|
||||
}
|
||||
|
||||
// Show Attach CLI Modal
|
||||
function showAttachCliModal() {
|
||||
document.getElementById('modal-overlay').classList.remove('hidden');
|
||||
document.getElementById('attach-cli-modal').classList.remove('hidden');
|
||||
}
|
||||
|
||||
// Submit Attach CLI Session
|
||||
async function submitAttachCliSession() {
|
||||
const sessionId = document.getElementById('cli-session-id').value.trim();
|
||||
|
||||
if (!sessionId) {
|
||||
alert('Please enter a session ID');
|
||||
return;
|
||||
}
|
||||
|
||||
attachToSession(sessionId);
|
||||
closeModal();
|
||||
}
|
||||
|
||||
// Attach File (placeholder for now)
|
||||
function attachFile() {
|
||||
appendSystemMessage('File attachment feature coming soon! For now, use @filename to reference files.');
|
||||
}
|
||||
|
||||
// Show Chat Settings (placeholder)
|
||||
function showChatSettings() {
|
||||
appendSystemMessage('Chat settings coming soon!');
|
||||
}
|
||||
|
||||
// Export variables to window for global access
|
||||
if (typeof window !== 'undefined') {
|
||||
window.attachedSessionId = attachedSessionId;
|
||||
window.chatSessionId = chatSessionId;
|
||||
window.chatMessages = chatMessages;
|
||||
|
||||
// Create a proxy to keep window vars in sync
|
||||
Object.defineProperty(window, 'attachedSessionId', {
|
||||
get: function() { return attachedSessionId; },
|
||||
set: function(value) { attachedSessionId = value; }
|
||||
});
|
||||
|
||||
Object.defineProperty(window, 'chatSessionId', {
|
||||
get: function() { return chatSessionId; },
|
||||
set: function(value) { chatSessionId = value; }
|
||||
});
|
||||
}
|
||||
1098
public/claude-ide/ide.css
Normal file
1098
public/claude-ide/ide.css
Normal file
File diff suppressed because it is too large
Load Diff
834
public/claude-ide/ide.js
Normal file
834
public/claude-ide/ide.js
Normal file
@@ -0,0 +1,834 @@
|
||||
// Claude Code IDE JavaScript
|
||||
let currentSession = null;
|
||||
let ws = null;
|
||||
|
||||
// Make ws globally accessible for other scripts
|
||||
Object.defineProperty(window, 'ws', {
|
||||
get: function() { return ws; },
|
||||
set: function(value) { ws = value; },
|
||||
enumerable: true,
|
||||
configurable: true
|
||||
});
|
||||
|
||||
// Initialize
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
initNavigation();
|
||||
connectWebSocket();
|
||||
|
||||
// Check URL params for session and prompt
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const sessionId = urlParams.get('session');
|
||||
const prompt = urlParams.get('prompt');
|
||||
|
||||
if (sessionId || prompt) {
|
||||
// Switch to chat view first
|
||||
switchView('chat');
|
||||
|
||||
// Wait for chat to load, then handle session/prompt
|
||||
setTimeout(() => {
|
||||
if (sessionId) {
|
||||
attachToSession(sessionId);
|
||||
}
|
||||
|
||||
if (prompt) {
|
||||
setTimeout(() => {
|
||||
const input = document.getElementById('chat-input');
|
||||
if (input) {
|
||||
input.value = decodeURIComponent(prompt);
|
||||
sendChatMessage();
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
}, 500);
|
||||
} else {
|
||||
// Default to chat view
|
||||
switchView('chat');
|
||||
}
|
||||
});
|
||||
|
||||
// Navigation
|
||||
function initNavigation() {
|
||||
const navItems = document.querySelectorAll('.nav-item');
|
||||
navItems.forEach(item => {
|
||||
item.addEventListener('click', () => {
|
||||
const view = item.dataset.view;
|
||||
switchView(view);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function switchView(viewName) {
|
||||
// Update nav items
|
||||
document.querySelectorAll('.nav-item').forEach(item => {
|
||||
item.classList.remove('active');
|
||||
if (item.dataset.view === viewName) {
|
||||
item.classList.add('active');
|
||||
}
|
||||
});
|
||||
|
||||
// Update views
|
||||
document.querySelectorAll('.view').forEach(view => {
|
||||
view.classList.remove('active');
|
||||
});
|
||||
document.getElementById(`${viewName}-view`).classList.add('active');
|
||||
|
||||
// Load content for the view
|
||||
switch(viewName) {
|
||||
case 'dashboard':
|
||||
loadDashboard();
|
||||
break;
|
||||
case 'chat':
|
||||
loadChatView();
|
||||
break;
|
||||
case 'sessions':
|
||||
loadSessions();
|
||||
break;
|
||||
case 'projects':
|
||||
loadProjects();
|
||||
break;
|
||||
case 'files':
|
||||
loadFiles();
|
||||
break;
|
||||
case 'terminal':
|
||||
loadTerminal();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// WebSocket Connection
|
||||
function connectWebSocket() {
|
||||
const wsUrl = `wss://${window.location.host}/claude/api/claude/chat`;
|
||||
console.log('Connecting to WebSocket:', wsUrl);
|
||||
|
||||
window.ws = new WebSocket(wsUrl);
|
||||
|
||||
window.ws.onopen = () => {
|
||||
console.log('WebSocket connected, readyState:', window.ws.readyState);
|
||||
// Send a test message to verify connection
|
||||
try {
|
||||
window.ws.send(JSON.stringify({ type: 'ping' }));
|
||||
} catch (error) {
|
||||
console.error('Error sending ping:', error);
|
||||
}
|
||||
};
|
||||
|
||||
window.ws.onmessage = (event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
console.log('WebSocket message received:', data.type);
|
||||
handleWebSocketMessage(data);
|
||||
};
|
||||
|
||||
window.ws.onerror = (error) => {
|
||||
console.error('WebSocket error:', error);
|
||||
console.log('WebSocket error details:', {
|
||||
type: error.type,
|
||||
target: error.target,
|
||||
readyState: window.ws?.readyState
|
||||
});
|
||||
};
|
||||
|
||||
window.ws.onclose = (event) => {
|
||||
console.log('WebSocket disconnected:', {
|
||||
code: event.code,
|
||||
reason: event.reason,
|
||||
wasClean: event.wasClean
|
||||
});
|
||||
// Clear the ws reference
|
||||
window.ws = null;
|
||||
// Attempt to reconnect after 5 seconds
|
||||
setTimeout(() => {
|
||||
console.log('Attempting to reconnect...');
|
||||
connectWebSocket();
|
||||
}, 5000);
|
||||
};
|
||||
}
|
||||
|
||||
function handleWebSocketMessage(data) {
|
||||
switch(data.type) {
|
||||
case 'connected':
|
||||
console.log(data.message);
|
||||
break;
|
||||
case 'output':
|
||||
handleSessionOutput(data);
|
||||
break;
|
||||
case 'operations-detected':
|
||||
handleOperationsDetected(data);
|
||||
break;
|
||||
case 'operations-executed':
|
||||
handleOperationsExecuted(data);
|
||||
break;
|
||||
case 'operations-error':
|
||||
handleOperationsError(data);
|
||||
break;
|
||||
case 'operation-progress':
|
||||
handleOperationProgress(data);
|
||||
break;
|
||||
case 'error':
|
||||
console.error('WebSocket error:', data.error);
|
||||
// Show error in chat if attached
|
||||
if (typeof appendSystemMessage === 'function') {
|
||||
appendSystemMessage('Error: ' + data.error);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle operations detected event
|
||||
*/
|
||||
function handleOperationsDetected(data) {
|
||||
console.log('Operations detected:', data.operations.length);
|
||||
|
||||
// Only show if we're attached to this session
|
||||
if (data.sessionId !== attachedSessionId) return;
|
||||
|
||||
// Store response for execution
|
||||
window.currentOperationsResponse = data.response;
|
||||
|
||||
// Use tag renderer to show operations panel
|
||||
if (typeof tagRenderer !== 'undefined') {
|
||||
tagRenderer.showOperationsPanel(data.operations, data.response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle operations executed event
|
||||
*/
|
||||
function handleOperationsExecuted(data) {
|
||||
console.log('Operations executed:', data.results);
|
||||
|
||||
// Only handle if we're attached to this session
|
||||
if (data.sessionId !== attachedSessionId) return;
|
||||
|
||||
// Hide progress and show completion
|
||||
if (typeof tagRenderer !== 'undefined') {
|
||||
tagRenderer.hideProgress();
|
||||
tagRenderer.hideOperationsPanel();
|
||||
tagRenderer.showCompletion(data.results);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle operations error event
|
||||
*/
|
||||
function handleOperationsError(data) {
|
||||
console.error('Operations error:', data.error);
|
||||
|
||||
// Only handle if we're attached to this session
|
||||
if (data.sessionId !== attachedSessionId) return;
|
||||
|
||||
// Show error
|
||||
if (typeof tagRenderer !== 'undefined') {
|
||||
tagRenderer.hideProgress();
|
||||
tagRenderer.showError(data.error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle operation progress event
|
||||
*/
|
||||
function handleOperationProgress(data) {
|
||||
console.log('Operation progress:', data.progress);
|
||||
|
||||
// Only handle if we're attached to this session
|
||||
if (data.sessionId !== attachedSessionId) return;
|
||||
|
||||
// Update progress
|
||||
if (typeof tagRenderer !== 'undefined') {
|
||||
const progress = data.progress;
|
||||
let message = '';
|
||||
|
||||
switch(progress.type) {
|
||||
case 'write':
|
||||
message = `Creating ${progress.path}...`;
|
||||
break;
|
||||
case 'rename':
|
||||
message = `Renaming ${progress.from} to ${progress.to}...`;
|
||||
break;
|
||||
case 'delete':
|
||||
message = `Deleting ${progress.path}...`;
|
||||
break;
|
||||
case 'install':
|
||||
message = `Installing packages: ${progress.packages.join(', ')}...`;
|
||||
break;
|
||||
case 'command':
|
||||
message = `Executing command: ${progress.command}...`;
|
||||
break;
|
||||
default:
|
||||
message = 'Processing...';
|
||||
}
|
||||
|
||||
tagRenderer.updateProgress(message);
|
||||
}
|
||||
}
|
||||
|
||||
function handleSessionOutput(data) {
|
||||
// Handle output for sessions view
|
||||
if (currentSession && data.sessionId === currentSession.id) {
|
||||
appendOutput(data.data);
|
||||
}
|
||||
|
||||
// Handle output for chat view
|
||||
if (typeof attachedSessionId !== 'undefined' && data.sessionId === attachedSessionId) {
|
||||
// Hide streaming indicator
|
||||
if (typeof hideStreamingIndicator === 'function') {
|
||||
hideStreamingIndicator();
|
||||
}
|
||||
|
||||
// Append output as assistant message
|
||||
if (typeof appendMessage === 'function') {
|
||||
appendMessage('assistant', data.data.content, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Dashboard
|
||||
async function loadDashboard() {
|
||||
try {
|
||||
// Load stats
|
||||
const [sessionsRes, projectsRes] = await Promise.all([
|
||||
fetch('/claude/api/claude/sessions'),
|
||||
fetch('/claude/api/claude/projects')
|
||||
]);
|
||||
|
||||
const sessionsData = await sessionsRes.json();
|
||||
const projectsData = await projectsRes.json();
|
||||
|
||||
// Update stats
|
||||
document.getElementById('active-sessions-count').textContent =
|
||||
sessionsData.active?.length || 0;
|
||||
document.getElementById('historical-sessions-count').textContent =
|
||||
sessionsData.historical?.length || 0;
|
||||
document.getElementById('total-projects-count').textContent =
|
||||
projectsData.projects?.length || 0;
|
||||
|
||||
// Update active sessions list
|
||||
const activeSessionsEl = document.getElementById('active-sessions-list');
|
||||
if (sessionsData.active && sessionsData.active.length > 0) {
|
||||
activeSessionsEl.innerHTML = sessionsData.active.map(session => `
|
||||
<div class="session-item" onclick="viewSession('${session.id}')">
|
||||
<div class="session-header">
|
||||
<span class="session-id">${session.id.substring(0, 20)}...</span>
|
||||
<span class="session-status ${session.status}">${session.status}</span>
|
||||
</div>
|
||||
<div class="session-meta">
|
||||
Working: ${session.workingDir}<br>
|
||||
Created: ${new Date(session.createdAt).toLocaleString()}
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
} else {
|
||||
activeSessionsEl.innerHTML = '<p class="placeholder">No active sessions</p>';
|
||||
}
|
||||
|
||||
// Update projects list
|
||||
const projectsEl = document.getElementById('recent-projects-list');
|
||||
if (projectsData.projects && projectsData.projects.length > 0) {
|
||||
projectsEl.innerHTML = projectsData.projects.slice(0, 5).map(project => `
|
||||
<div class="project-card" onclick="viewProject('${project.name}')">
|
||||
<h3>${project.name}</h3>
|
||||
<p class="project-meta">
|
||||
Modified: ${new Date(project.modified).toLocaleDateString()}
|
||||
</p>
|
||||
</div>
|
||||
`).join('');
|
||||
} else {
|
||||
projectsEl.innerHTML = '<p class="placeholder">No projects yet</p>';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading dashboard:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function refreshSessions() {
|
||||
loadDashboard();
|
||||
}
|
||||
|
||||
// Sessions
|
||||
async function loadSessions() {
|
||||
try {
|
||||
const res = await fetch('/claude/api/claude/sessions');
|
||||
const data = await res.json();
|
||||
|
||||
const sessionsListEl = document.getElementById('sessions-list');
|
||||
const allSessions = [
|
||||
...(data.active || []),
|
||||
...(data.historical || [])
|
||||
];
|
||||
|
||||
if (allSessions.length > 0) {
|
||||
sessionsListEl.innerHTML = allSessions.map(session => `
|
||||
<div class="session-item" onclick="viewSession('${session.id}')">
|
||||
<div class="session-header">
|
||||
<span class="session-id">${session.id.substring(0, 20)}...</span>
|
||||
<span class="session-status ${session.status}">${session.status}</span>
|
||||
</div>
|
||||
<div class="session-meta">
|
||||
${session.workingDir}<br>
|
||||
${new Date(session.createdAt).toLocaleString()}
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
} else {
|
||||
sessionsListEl.innerHTML = '<p class="placeholder">No sessions</p>';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading sessions:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async function viewSession(sessionId) {
|
||||
try {
|
||||
const res = await fetch(`/claude/api/claude/sessions/${sessionId}`);
|
||||
const data = await res.json();
|
||||
|
||||
currentSession = data.session;
|
||||
|
||||
const detailEl = document.getElementById('session-detail');
|
||||
detailEl.innerHTML = `
|
||||
<div class="session-header-info">
|
||||
<h2>${data.session.id}</h2>
|
||||
<p>Status: <span class="session-status ${data.session.status}">${data.session.status}</span></p>
|
||||
<p>PID: ${data.session.pid || 'N/A'}</p>
|
||||
<p>Working Directory: ${data.session.workingDir}</p>
|
||||
<p>Created: ${new Date(data.session.createdAt).toLocaleString()}</p>
|
||||
</div>
|
||||
|
||||
<h3>Context Usage</h3>
|
||||
<div class="context-bar">
|
||||
<div class="context-fill" style="width: ${data.session.context.totalTokens / data.session.context.maxTokens * 100}%"></div>
|
||||
</div>
|
||||
<div class="context-stats">
|
||||
<span>${data.session.context.totalTokens.toLocaleString()} tokens</span>
|
||||
<span>${Math.round(data.session.context.totalTokens / data.session.context.maxTokens * 100)}% used</span>
|
||||
</div>
|
||||
|
||||
<h3>Session Output</h3>
|
||||
<div class="session-output" id="session-output">
|
||||
${data.session.outputBuffer.map(entry => `
|
||||
<div class="output-line ${entry.type}">${escapeHtml(entry.content)}</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
|
||||
${data.session.status === 'running' ? `
|
||||
<div class="command-input-container">
|
||||
<input type="text" id="command-input" class="command-input" placeholder="Enter command..." onkeypress="handleCommandKeypress(event)">
|
||||
<button class="btn-primary" onclick="sendCommand()">Send</button>
|
||||
</div>
|
||||
` : ''}
|
||||
`;
|
||||
|
||||
// Switch to sessions view
|
||||
switchView('sessions');
|
||||
} catch (error) {
|
||||
console.error('Error viewing session:', error);
|
||||
alert('Failed to load session');
|
||||
}
|
||||
}
|
||||
|
||||
function handleCommandKeypress(event) {
|
||||
if (event.key === 'Enter') {
|
||||
sendCommand();
|
||||
}
|
||||
}
|
||||
|
||||
async function sendCommand() {
|
||||
const input = document.getElementById('command-input');
|
||||
const command = input.value.trim();
|
||||
|
||||
if (!command || !currentSession) return;
|
||||
|
||||
try {
|
||||
await fetch(`/claude/api/claude/sessions/${currentSession.id}/command`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ command })
|
||||
});
|
||||
|
||||
input.value = '';
|
||||
|
||||
// Append command to output
|
||||
appendOutput({
|
||||
type: 'command',
|
||||
content: `$ ${command}\n`
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error sending command:', error);
|
||||
alert('Failed to send command');
|
||||
}
|
||||
}
|
||||
|
||||
function appendOutput(data) {
|
||||
const outputEl = document.getElementById('session-output');
|
||||
if (outputEl) {
|
||||
const line = document.createElement('div');
|
||||
line.className = `output-line ${data.type}`;
|
||||
line.textContent = data.content;
|
||||
outputEl.appendChild(line);
|
||||
outputEl.scrollTop = outputEl.scrollHeight;
|
||||
}
|
||||
}
|
||||
|
||||
// Projects
|
||||
async function loadProjects() {
|
||||
try {
|
||||
const res = await fetch('/claude/api/claude/projects');
|
||||
const data = await res.json();
|
||||
|
||||
const gridEl = document.getElementById('projects-grid');
|
||||
if (data.projects && data.projects.length > 0) {
|
||||
gridEl.innerHTML = data.projects.map(project => `
|
||||
<div class="project-card" onclick="viewProject('${project.name}')">
|
||||
<h3>${project.name}</h3>
|
||||
<p class="project-meta">
|
||||
Modified: ${new Date(project.modified).toLocaleString()}
|
||||
</p>
|
||||
<p class="project-description">Click to view project details</p>
|
||||
</div>
|
||||
`).join('');
|
||||
} else {
|
||||
gridEl.innerHTML = '<p class="placeholder">No projects yet. Create your first project!</p>';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading projects:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async function viewProject(projectName) {
|
||||
// Open the project file in the files view
|
||||
const path = `Claude Projects/${projectName}.md`;
|
||||
loadFileContent(path);
|
||||
switchView('files');
|
||||
}
|
||||
|
||||
// Files
|
||||
async function loadFiles() {
|
||||
try {
|
||||
const res = await fetch('/claude/api/files');
|
||||
const data = await res.json();
|
||||
|
||||
const treeEl = document.getElementById('file-tree');
|
||||
treeEl.innerHTML = renderFileTree(data.tree);
|
||||
} catch (error) {
|
||||
console.error('Error loading files:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadTerminal() {
|
||||
// Initialize terminal manager if not already done
|
||||
if (!window.terminalManager) {
|
||||
window.terminalManager = new TerminalManager();
|
||||
await window.terminalManager.initialize();
|
||||
}
|
||||
|
||||
// Set up new terminal button
|
||||
const btnNewTerminal = document.getElementById('btn-new-terminal');
|
||||
if (btnNewTerminal) {
|
||||
btnNewTerminal.onclick = () => {
|
||||
window.terminalManager.createTerminal();
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function renderFileTree(tree, level = 0) {
|
||||
return tree.map(item => {
|
||||
const padding = level * 1 + 0.5;
|
||||
const icon = item.type === 'folder' ? '📁' : '📄';
|
||||
|
||||
if (item.type === 'folder' && item.children) {
|
||||
return `
|
||||
<div style="padding-left: ${padding}rem">
|
||||
<div class="tree-item folder" onclick="toggleFolder(this)">
|
||||
<span>${icon}</span>
|
||||
<span>${item.name}</span>
|
||||
</div>
|
||||
<div class="tree-children" style="display: none;">
|
||||
${renderFileTree(item.children, level + 1)}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
return `
|
||||
<div style="padding-left: ${padding}rem">
|
||||
<div class="tree-item file" onclick="loadFile('${item.path}')">
|
||||
<span>${icon}</span>
|
||||
<span>${item.name}</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function toggleFolder(element) {
|
||||
const children = element.parentElement.querySelector('.tree-children');
|
||||
const icon = element.querySelector('span:first-child');
|
||||
|
||||
if (children.style.display === 'none') {
|
||||
children.style.display = 'block';
|
||||
icon.textContent = '📂';
|
||||
} else {
|
||||
children.style.display = 'none';
|
||||
icon.textContent = '📁';
|
||||
}
|
||||
}
|
||||
|
||||
async function loadFile(filePath) {
|
||||
try {
|
||||
const res = await fetch(`/claude/api/file/${encodeURIComponent(filePath)}`);
|
||||
const data = await res.json();
|
||||
|
||||
const isHtmlFile = filePath.toLowerCase().endsWith('.html') || filePath.toLowerCase().endsWith('.htm');
|
||||
|
||||
const editorEl = document.getElementById('file-editor');
|
||||
|
||||
if (isHtmlFile) {
|
||||
// HTML file - show with preview option
|
||||
editorEl.innerHTML = `
|
||||
<div class="file-header">
|
||||
<h2>${filePath}</h2>
|
||||
<div class="file-actions">
|
||||
<button class="btn-secondary btn-sm" onclick="editFile('${filePath}')">Edit</button>
|
||||
<button class="btn-primary btn-sm" onclick="showHtmlPreview('${filePath}')">👁️ Preview</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="file-content" id="file-content-view">
|
||||
<div class="view-toggle">
|
||||
<button class="toggle-btn active" data-view="code" onclick="switchFileView('code')">Code</button>
|
||||
<button class="toggle-btn" data-view="preview" onclick="switchFileView('preview')">Preview</button>
|
||||
</div>
|
||||
<div class="code-view">
|
||||
<pre><code class="language-html">${escapeHtml(data.content)}</code></pre>
|
||||
</div>
|
||||
<div class="preview-view" style="display: none;">
|
||||
<iframe id="html-preview-frame" sandbox="allow-scripts allow-same-origin allow-forms"></iframe>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Store file content for preview
|
||||
window.currentFileContent = data.content;
|
||||
window.currentFilePath = filePath;
|
||||
|
||||
// Highlight code
|
||||
if (window.hljs) {
|
||||
document.querySelectorAll('#file-content-view pre code').forEach((block) => {
|
||||
hljs.highlightElement(block);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Non-HTML file - show as before
|
||||
editorEl.innerHTML = `
|
||||
<div class="file-header">
|
||||
<h2>${filePath}</h2>
|
||||
<div class="file-actions">
|
||||
<button class="btn-secondary btn-sm" onclick="editFile('${filePath}')">Edit</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="file-content">
|
||||
<div class="markdown-body">${data.html}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading file:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadFileContent(filePath) {
|
||||
await loadFile(filePath);
|
||||
switchView('files');
|
||||
}
|
||||
|
||||
// HTML Preview Functions
|
||||
function showHtmlPreview(filePath) {
|
||||
switchFileView('preview');
|
||||
}
|
||||
|
||||
function switchFileView(view) {
|
||||
const codeView = document.querySelector('.code-view');
|
||||
const previewView = document.querySelector('.preview-view');
|
||||
const toggleBtns = document.querySelectorAll('.toggle-btn');
|
||||
|
||||
// Update buttons
|
||||
toggleBtns.forEach(btn => {
|
||||
btn.classList.remove('active');
|
||||
if (btn.dataset.view === view) {
|
||||
btn.classList.add('active');
|
||||
}
|
||||
});
|
||||
|
||||
// Show/hide views
|
||||
if (view === 'code') {
|
||||
codeView.style.display = 'block';
|
||||
previewView.style.display = 'none';
|
||||
} else if (view === 'preview') {
|
||||
codeView.style.display = 'none';
|
||||
previewView.style.display = 'block';
|
||||
|
||||
// Load HTML into iframe using blob URL
|
||||
const iframe = document.getElementById('html-preview-frame');
|
||||
if (iframe && window.currentFileContent) {
|
||||
// Create blob URL from HTML content
|
||||
const blob = new Blob([window.currentFileContent], { type: 'text/html' });
|
||||
const blobUrl = URL.createObjectURL(blob);
|
||||
|
||||
// Load blob URL in iframe
|
||||
iframe.src = blobUrl;
|
||||
|
||||
// Clean up blob URL when iframe is unloaded
|
||||
iframe.onload = () => {
|
||||
// Keep the blob URL active while preview is shown
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
// Modals
|
||||
function createNewSession() {
|
||||
document.getElementById('modal-overlay').classList.remove('hidden');
|
||||
document.getElementById('new-session-modal').classList.remove('hidden');
|
||||
}
|
||||
|
||||
function createNewProject() {
|
||||
document.getElementById('modal-overlay').classList.remove('hidden');
|
||||
document.getElementById('new-project-modal').classList.remove('hidden');
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
document.getElementById('modal-overlay').classList.add('hidden');
|
||||
document.querySelectorAll('.modal').forEach(modal => {
|
||||
modal.classList.add('hidden');
|
||||
});
|
||||
}
|
||||
|
||||
async function submitNewSession() {
|
||||
const workingDir = document.getElementById('session-working-dir').value;
|
||||
const project = document.getElementById('session-project').value;
|
||||
|
||||
try {
|
||||
const res = await fetch('/claude/api/claude/sessions', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
workingDir,
|
||||
metadata: { project }
|
||||
})
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
if (data.success) {
|
||||
closeModal();
|
||||
viewSession(data.session.id);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error creating session:', error);
|
||||
alert('Failed to create session');
|
||||
}
|
||||
}
|
||||
|
||||
async function submitNewProject() {
|
||||
const name = document.getElementById('project-name').value;
|
||||
const description = document.getElementById('project-description').value;
|
||||
const type = document.getElementById('project-type').value;
|
||||
|
||||
if (!name) {
|
||||
alert('Please enter a project name');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch('/claude/api/claude/projects', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name, description, type })
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
if (data.success) {
|
||||
closeModal();
|
||||
loadProjects();
|
||||
viewProject(name);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error creating project:', error);
|
||||
alert('Failed to create project');
|
||||
}
|
||||
}
|
||||
|
||||
// Utility functions
|
||||
function escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
/**
|
||||
* Show toast notification
|
||||
* @param {string} message - The message to display
|
||||
* @param {string} type - The type of toast: 'success', 'error', 'info'
|
||||
* @param {number} duration - Duration in milliseconds (default: 3000)
|
||||
*/
|
||||
function showToast(message, type = 'info', duration = 3000) {
|
||||
// Remove existing toasts
|
||||
const existingToasts = document.querySelectorAll('.toast-notification');
|
||||
existingToasts.forEach(toast => toast.remove());
|
||||
|
||||
// Create toast element
|
||||
const toast = document.createElement('div');
|
||||
toast.className = `toast-notification toast-${type}`;
|
||||
toast.innerHTML = `
|
||||
<span class="toast-icon">${getToastIcon(type)}</span>
|
||||
<span class="toast-message">${escapeHtml(message)}</span>
|
||||
`;
|
||||
|
||||
document.body.appendChild(toast);
|
||||
|
||||
// Trigger animation
|
||||
setTimeout(() => {
|
||||
toast.classList.add('visible');
|
||||
}, 10);
|
||||
|
||||
// Auto remove after duration
|
||||
setTimeout(() => {
|
||||
toast.classList.remove('visible');
|
||||
setTimeout(() => {
|
||||
toast.remove();
|
||||
}, 300);
|
||||
}, duration);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get toast icon based on type
|
||||
*/
|
||||
function getToastIcon(type) {
|
||||
const icons = {
|
||||
success: '✓',
|
||||
error: '✕',
|
||||
info: 'ℹ',
|
||||
warning: '⚠'
|
||||
};
|
||||
return icons[type] || icons.info;
|
||||
}
|
||||
|
||||
function showProjects() {
|
||||
switchView('projects');
|
||||
}
|
||||
|
||||
// Logout
|
||||
document.getElementById('logout-btn')?.addEventListener('click', async () => {
|
||||
try {
|
||||
await fetch('/claude/api/logout', { method: 'POST' });
|
||||
window.location.reload();
|
||||
} catch (error) {
|
||||
console.error('Error logging out:', error);
|
||||
}
|
||||
});
|
||||
316
public/claude-ide/index.html
Normal file
316
public/claude-ide/index.html
Normal file
@@ -0,0 +1,316 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Claude Code IDE</title>
|
||||
<link rel="stylesheet" href="/claude/css/style.css">
|
||||
<link rel="stylesheet" href="/claude/claude-ide/ide.css">
|
||||
<link rel="stylesheet" href="/claude/claude-ide/tag-renderer.css">
|
||||
<link rel="stylesheet" href="/claude/claude-ide/preview-manager.css">
|
||||
<link rel="stylesheet" href="/claude/claude-ide/chat-enhanced.css">
|
||||
<link rel="stylesheet" href="/claude/claude-ide/terminal.css">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<!-- Navigation -->
|
||||
<nav class="navbar">
|
||||
<div class="nav-brand">
|
||||
<h1>Claude Code IDE</h1>
|
||||
</div>
|
||||
<div class="nav-menu">
|
||||
<button class="nav-item active" data-view="dashboard">Dashboard</button>
|
||||
<button class="nav-item" data-view="chat">💬 Chat</button>
|
||||
<button class="nav-item" data-view="sessions">Sessions</button>
|
||||
<button class="nav-item" data-view="projects">Projects</button>
|
||||
<button class="nav-item" data-view="files">Files</button>
|
||||
<button class="nav-item" data-view="terminal">🖥️ Terminal</button>
|
||||
</div>
|
||||
<div class="nav-user">
|
||||
<button id="logout-btn" class="btn-secondary">Logout</button>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Dashboard View -->
|
||||
<div id="dashboard-view" class="view active">
|
||||
<div class="dashboard-grid">
|
||||
<!-- Stats Cards -->
|
||||
<div class="stat-card">
|
||||
<h3>Active Sessions</h3>
|
||||
<div class="stat-value" id="active-sessions-count">0</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<h3>Total Projects</h3>
|
||||
<div class="stat-value" id="total-projects-count">0</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<h3>Historical Sessions</h3>
|
||||
<div class="stat-value" id="historical-sessions-count">0</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<h3>Quick Actions</h3>
|
||||
<div class="stat-actions">
|
||||
<button class="btn-primary" onclick="createNewSession()">New Session</button>
|
||||
<button class="btn-secondary" onclick="createNewProject()">New Project</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Active Sessions Panel -->
|
||||
<div class="panel">
|
||||
<div class="panel-header">
|
||||
<h2>Active Sessions</h2>
|
||||
<button class="btn-secondary btn-sm" onclick="refreshSessions()">Refresh</button>
|
||||
</div>
|
||||
<div class="panel-content" id="active-sessions-list">
|
||||
<div class="loading">Loading...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Recent Projects Panel -->
|
||||
<div class="panel">
|
||||
<div class="panel-header">
|
||||
<h2>Recent Projects</h2>
|
||||
<button class="btn-secondary btn-sm" onclick="showProjects()">View All</button>
|
||||
</div>
|
||||
<div class="panel-content" id="recent-projects-list">
|
||||
<div class="loading">Loading...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sessions View -->
|
||||
<div id="sessions-view" class="view">
|
||||
<div class="sessions-layout">
|
||||
<div class="sessions-sidebar">
|
||||
<div class="sidebar-header">
|
||||
<h2>Sessions</h2>
|
||||
<button class="btn-primary" onclick="createNewSession()">+ New</button>
|
||||
</div>
|
||||
<div class="sessions-list" id="sessions-list">
|
||||
<div class="loading">Loading...</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sessions-main">
|
||||
<div id="session-detail" class="session-detail">
|
||||
<div class="placeholder">
|
||||
<h2>Select a session</h2>
|
||||
<p>Choose a session from the sidebar to view details</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Projects View -->
|
||||
<div id="projects-view" class="view">
|
||||
<div class="projects-header">
|
||||
<h2>Projects</h2>
|
||||
<button class="btn-primary" onclick="createNewProject()">+ New Project</button>
|
||||
</div>
|
||||
<div class="projects-grid" id="projects-grid">
|
||||
<div class="loading">Loading...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Chat View -->
|
||||
<div id="chat-view" class="view">
|
||||
<div class="chat-layout">
|
||||
<div class="chat-sidebar">
|
||||
<div class="sidebar-header">
|
||||
<h2>Chat</h2>
|
||||
<button class="btn-primary" onclick="startNewChat()">+ New</button>
|
||||
</div>
|
||||
<div class="chat-history-list" id="chat-history-list">
|
||||
<div class="loading">Loading...</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chat-main">
|
||||
<div class="chat-header" id="chat-header">
|
||||
<div class="chat-session-info">
|
||||
<h2 id="chat-title">New Chat</h2>
|
||||
<span class="chat-session-id" id="current-session-id"></span>
|
||||
</div>
|
||||
<div class="chat-actions">
|
||||
<button class="btn-secondary btn-sm" onclick="clearChat()" title="Clear chat">Clear</button>
|
||||
<button class="btn-secondary btn-sm" onclick="showChatSettings()" title="Settings">⚙️</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chat-messages" id="chat-messages">
|
||||
<div class="chat-welcome">
|
||||
<h2>👋 Welcome to Claude Code Chat!</h2>
|
||||
<p>Start a conversation with Claude Code. Your session will be saved automatically.</p>
|
||||
<div class="chat-tips">
|
||||
<h3>Quick Tips:</h3>
|
||||
<ul>
|
||||
<li>Type your message and press Enter to send</li>
|
||||
<li>Shift+Enter for a new line</li>
|
||||
<li>Use <code>/help</code> to see available commands</li>
|
||||
<li>Attach files from your vault using <code>@filename</code></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="chat-connection-info">
|
||||
<h3>💡 Pro Tip: Continue from CLI</h3>
|
||||
<p>To continue a CLI session in the web interface:</p>
|
||||
<ol>
|
||||
<li>In your terminal, note the session ID shown by Claude Code</li>
|
||||
<li>Click "Attach CLI Session" below</li>
|
||||
<li>Enter the session ID to connect</li>
|
||||
</ol>
|
||||
<button class="btn-secondary" onclick="showAttachCliModal()" style="margin-top: 1rem;">Attach CLI Session</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chat-input-container">
|
||||
<div class="chat-input-wrapper">
|
||||
<textarea id="chat-input"
|
||||
placeholder="Type your message to Claude Code... (Enter to send, Shift+Enter for new line)"
|
||||
rows="1"
|
||||
onkeydown="handleChatKeypress(event)"></textarea>
|
||||
<div class="chat-input-actions">
|
||||
<button class="btn-icon" onclick="attachFile()" title="Attach file">📎</button>
|
||||
<button class="btn-primary btn-send" onclick="sendChatMessage()">Send</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chat-input-info">
|
||||
<span class="token-usage" id="token-usage">0 tokens used</span>
|
||||
<span class="char-count" id="char-count">0 characters</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Files View -->
|
||||
<div id="files-view" class="view">
|
||||
<div class="files-layout">
|
||||
<div class="files-sidebar">
|
||||
<div class="sidebar-header">
|
||||
<h2>Files</h2>
|
||||
</div>
|
||||
<div class="search-box">
|
||||
<input type="text" id="file-search" placeholder="Search files...">
|
||||
</div>
|
||||
<div class="file-tree" id="file-tree">
|
||||
<div class="loading">Loading...</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="files-main">
|
||||
<div id="file-editor" class="file-editor">
|
||||
<div class="placeholder">
|
||||
<h2>Select a file</h2>
|
||||
<p>Choose a file from the sidebar to view and edit</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Terminal View -->
|
||||
<div id="terminal-view" class="view">
|
||||
<div class="terminal-layout">
|
||||
<div class="terminal-header">
|
||||
<h2>🖥️ Terminals</h2>
|
||||
<button class="btn-primary" id="btn-new-terminal">+ New Terminal</button>
|
||||
</div>
|
||||
<div class="terminal-tabs" id="terminal-tabs">
|
||||
<!-- Terminal tabs will be added here -->
|
||||
</div>
|
||||
<div class="terminals-container" id="terminals-container">
|
||||
<!-- Terminal instances will be added here -->
|
||||
<div class="terminal-placeholder">
|
||||
<h3>No terminals open</h3>
|
||||
<p>Click "+ New Terminal" to get started</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modals -->
|
||||
<div id="modal-overlay" class="modal-overlay hidden">
|
||||
<div id="new-session-modal" class="modal hidden">
|
||||
<div class="modal-header">
|
||||
<h2>New Session</h2>
|
||||
<button class="modal-close" onclick="closeModal()">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
<label>Working Directory</label>
|
||||
<input type="text" id="session-working-dir" value="/home/uroma/obsidian-vault">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Project (optional)</label>
|
||||
<input type="text" id="session-project" placeholder="e.g., DedicatedNodes">
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn-secondary" onclick="closeModal()">Cancel</button>
|
||||
<button class="btn-primary" onclick="submitNewSession()">Create</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="new-project-modal" class="modal hidden">
|
||||
<div class="modal-header">
|
||||
<h2>New Project</h2>
|
||||
<button class="modal-close" onclick="closeModal()">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
<label>Project Name</label>
|
||||
<input type="text" id="project-name" placeholder="My Project">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Description</label>
|
||||
<textarea id="project-description" rows="3" placeholder="Project description..."></textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Type</label>
|
||||
<select id="project-type">
|
||||
<option value="general">General</option>
|
||||
<option value="web">Web Development</option>
|
||||
<option value="mobile">Mobile App</option>
|
||||
<option value="infrastructure">Infrastructure</option>
|
||||
<option value="research">Research</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn-secondary" onclick="closeModal()">Cancel</button>
|
||||
<button class="btn-primary" onclick="submitNewProject()">Create</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="attach-cli-modal" class="modal hidden">
|
||||
<div class="modal-header">
|
||||
<h2>Attach CLI Session</h2>
|
||||
<button class="modal-close" onclick="closeModal()">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p class="modal-info">
|
||||
Enter the session ID from your Claude Code CLI session to continue it in the web interface.
|
||||
</p>
|
||||
<div class="form-group">
|
||||
<label>Session ID</label>
|
||||
<input type="text" id="cli-session-id" placeholder="e.g., session-1234567890-abc123">
|
||||
<small style="display: block; margin-top: 0.5rem; color: var(--text-secondary);">
|
||||
Tip: When you start Claude Code in the terminal, it shows the session ID at the top.
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn-secondary" onclick="closeModal()">Cancel</button>
|
||||
<button class="btn-primary" onclick="submitAttachCliSession()">Attach</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/claude/claude-ide/ide.js"></script>
|
||||
<script src="/claude/claude-ide/chat-functions.js"></script>
|
||||
<script src="/claude/claude-ide/tag-renderer.js"></script>
|
||||
<script src="/claude/claude-ide/preview-manager.js"></script>
|
||||
<script src="/claude/claude-ide/chat-enhanced.js"></script>
|
||||
<script src="/claude/claude-ide/terminal.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
218
public/claude-ide/preview-manager.css
Normal file
218
public/claude-ide/preview-manager.css
Normal file
@@ -0,0 +1,218 @@
|
||||
/**
|
||||
* Preview Manager Styles
|
||||
*/
|
||||
|
||||
/* Preview Panel */
|
||||
.preview-panel {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
height: 60vh;
|
||||
background: #1e1e1e;
|
||||
border-top: 2px solid #333;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
z-index: 1000;
|
||||
box-shadow: 0 -4px 12px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.preview-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px 20px;
|
||||
background: #252525;
|
||||
border-bottom: 1px solid #333;
|
||||
}
|
||||
|
||||
.preview-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.preview-title h3 {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
.preview-url {
|
||||
font-size: 12px;
|
||||
color: #888;
|
||||
background: #1a1a1a;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.preview-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.preview-content {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
#preview-iframe {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.preview-status {
|
||||
padding: 8px 20px;
|
||||
background: #1a1a1a;
|
||||
border-top: 1px solid #333;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 13px;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
animation: pulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.status-running {
|
||||
background: #51cf66;
|
||||
}
|
||||
|
||||
.status-stopped {
|
||||
background: #ff6b6b;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
/* Preview Error */
|
||||
.preview-error {
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
background: #3a1e1e;
|
||||
border: 1px solid #a83d3d;
|
||||
border-radius: 8px;
|
||||
padding: 16px 20px;
|
||||
max-width: 400px;
|
||||
z-index: 2000;
|
||||
animation: slideUp 0.3s ease;
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.preview-error .error-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.preview-error .error-icon {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.preview-error .error-header h4 {
|
||||
margin: 0;
|
||||
flex: 1;
|
||||
color: #ff6b6b;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.preview-error .btn-close {
|
||||
background: #3a3a3a;
|
||||
border: 1px solid #555;
|
||||
color: #e0e0e0;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.preview-error .error-message {
|
||||
color: #e0e0e0;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* Button variants */
|
||||
.btn {
|
||||
padding: 8px 16px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
padding: 4px 12px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #3a3a3a;
|
||||
color: #e0e0e0;
|
||||
border: 1px solid #555;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: #4a4a4a;
|
||||
border-color: #666;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: #a83d3d;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background: #c94e4e;
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 768px) {
|
||||
.preview-panel {
|
||||
height: 50vh;
|
||||
}
|
||||
|
||||
.preview-header {
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.preview-actions {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.preview-title {
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
230
public/claude-ide/preview-manager.js
Normal file
230
public/claude-ide/preview-manager.js
Normal file
@@ -0,0 +1,230 @@
|
||||
/**
|
||||
* Live Preview Manager
|
||||
* Manages application preview in an iframe
|
||||
*/
|
||||
|
||||
class PreviewManager {
|
||||
constructor() {
|
||||
this.previewUrl = null;
|
||||
this.previewServer = null;
|
||||
this.previewPort = null;
|
||||
this.isPreviewRunning = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start preview server
|
||||
*/
|
||||
async startPreview(sessionId, workingDir) {
|
||||
console.log('Starting preview for session:', sessionId);
|
||||
|
||||
try {
|
||||
const res = await fetch(`/claude/api/claude/sessions/${sessionId}/preview/start`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ workingDir })
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(data.error || 'Failed to start preview');
|
||||
}
|
||||
|
||||
this.previewUrl = data.url;
|
||||
this.previewPort = data.port;
|
||||
this.previewServer = data.processId;
|
||||
this.isPreviewRunning = true;
|
||||
|
||||
console.log('Preview started:', this.previewUrl);
|
||||
|
||||
// Show preview panel
|
||||
this.showPreviewPanel(this.previewUrl);
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('Error starting preview:', error);
|
||||
this.showError(error.message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop preview server
|
||||
*/
|
||||
async stopPreview(sessionId) {
|
||||
console.log('Stopping preview for session:', sessionId);
|
||||
|
||||
try {
|
||||
const res = await fetch(`/claude/api/claude/sessions/${sessionId}/preview/stop`, {
|
||||
method: 'POST'
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(data.error || 'Failed to stop preview');
|
||||
}
|
||||
|
||||
this.isPreviewRunning = false;
|
||||
this.previewUrl = null;
|
||||
this.previewServer = null;
|
||||
|
||||
// Hide preview panel
|
||||
this.hidePreviewPanel();
|
||||
|
||||
console.log('Preview stopped');
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('Error stopping preview:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh preview
|
||||
*/
|
||||
refreshPreview() {
|
||||
if (!this.previewUrl || !this.isPreviewRunning) {
|
||||
console.warn('No preview to refresh');
|
||||
return;
|
||||
}
|
||||
|
||||
const iframe = document.getElementById('preview-iframe');
|
||||
if (iframe) {
|
||||
iframe.src = iframe.src; // Reload iframe
|
||||
console.log('Preview refreshed');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show preview panel
|
||||
*/
|
||||
showPreviewPanel(url) {
|
||||
// Remove existing panel
|
||||
const existing = document.querySelector('.preview-panel');
|
||||
if (existing) {
|
||||
existing.remove();
|
||||
}
|
||||
|
||||
// Create preview panel
|
||||
const previewPanel = document.createElement('div');
|
||||
previewPanel.className = 'preview-panel';
|
||||
previewPanel.innerHTML = `
|
||||
<div class="preview-header">
|
||||
<div class="preview-title">
|
||||
<h3>Live Preview</h3>
|
||||
<span class="preview-url">${url}</span>
|
||||
</div>
|
||||
<div class="preview-actions">
|
||||
<button class="btn btn-secondary btn-sm" onclick="previewManager.refreshPreview()" title="Refresh">
|
||||
🔄 Refresh
|
||||
</button>
|
||||
<button class="btn btn-secondary btn-sm" onclick="previewManager.openInNewTab()" title="Open in new tab">
|
||||
🔗 Open
|
||||
</button>
|
||||
<button class="btn btn-danger btn-sm" onclick="stopCurrentPreview()" title="Stop preview">
|
||||
✕ Stop
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="preview-content">
|
||||
<iframe id="preview-iframe" src="${url}" sandbox="allow-scripts allow-same-origin allow-forms allow-popups"></iframe>
|
||||
</div>
|
||||
<div class="preview-status">
|
||||
<span class="status-dot status-running"></span>
|
||||
<span>Preview running on ${this.previewPort || 'local port'}</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Add to page - find best place to insert
|
||||
const chatView = document.getElementById('chat-view');
|
||||
if (chatView) {
|
||||
chatView.appendChild(previewPanel);
|
||||
}
|
||||
|
||||
previewPanel.style.display = 'block';
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide preview panel
|
||||
*/
|
||||
hidePreviewPanel() {
|
||||
const panel = document.querySelector('.preview-panel');
|
||||
if (panel) {
|
||||
panel.remove();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Open preview in new tab
|
||||
*/
|
||||
openInNewTab() {
|
||||
if (this.previewUrl) {
|
||||
window.open(this.previewUrl, '_blank');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show error message
|
||||
*/
|
||||
showError(message) {
|
||||
// Remove existing error
|
||||
const existing = document.querySelector('.preview-error');
|
||||
if (existing) {
|
||||
existing.remove();
|
||||
}
|
||||
|
||||
const error = document.createElement('div');
|
||||
error.className = 'preview-error';
|
||||
error.innerHTML = `
|
||||
<div class="error-header">
|
||||
<span class="error-icon">⚠️</span>
|
||||
<h4>Preview Error</h4>
|
||||
<button class="btn-close" onclick="this.closest('.preview-error').remove()">✕</button>
|
||||
</div>
|
||||
<div class="error-message">${message}</div>
|
||||
`;
|
||||
|
||||
const chatView = document.getElementById('chat-view');
|
||||
if (chatView) {
|
||||
chatView.appendChild(error);
|
||||
}
|
||||
|
||||
error.style.display = 'block';
|
||||
|
||||
// Auto-hide after 5 seconds
|
||||
setTimeout(() => {
|
||||
error.remove();
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update preview status
|
||||
*/
|
||||
updateStatus(status, message) {
|
||||
const statusElement = document.querySelector('.preview-status');
|
||||
if (statusElement) {
|
||||
const dotClass = status === 'running' ? 'status-running' : 'status-stopped';
|
||||
statusElement.innerHTML = `
|
||||
<span class="status-dot ${dotClass}"></span>
|
||||
<span>${message}</span>
|
||||
`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Global instance
|
||||
const previewManager = new PreviewManager();
|
||||
|
||||
// Global function to stop preview
|
||||
window.stopCurrentPreview = async function() {
|
||||
if (window.chatSessionId) {
|
||||
await previewManager.stopPreview(window.chatSessionId);
|
||||
}
|
||||
};
|
||||
|
||||
// Export for use in other files
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = PreviewManager;
|
||||
}
|
||||
475
public/claude-ide/sessions-landing.css
Normal file
475
public/claude-ide/sessions-landing.css
Normal file
@@ -0,0 +1,475 @@
|
||||
/**
|
||||
* Sessions Landing Page Styles
|
||||
*/
|
||||
|
||||
body.sessions-page {
|
||||
background: #0d0d0d;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* === Hero Section === */
|
||||
.hero-section {
|
||||
min-height: 50vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 80px 20px 60px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-size: 56px;
|
||||
font-weight: 700;
|
||||
margin: 0 0 16px 0;
|
||||
background: linear-gradient(135deg, #4a9eff 0%, #a78bfa 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.hero-subtitle {
|
||||
font-size: 20px;
|
||||
color: #888;
|
||||
margin: 0 0 48px 0;
|
||||
}
|
||||
|
||||
.project-input {
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
padding: 18px 24px;
|
||||
font-size: 18px;
|
||||
background: #1a1a1a;
|
||||
border: 2px solid #333;
|
||||
border-radius: 12px;
|
||||
color: #e0e0e0;
|
||||
outline: none;
|
||||
transition: all 0.3s ease;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.project-input:focus {
|
||||
border-color: #4a9eff;
|
||||
box-shadow: 0 0 0 4px rgba(74, 158, 255, 0.15);
|
||||
}
|
||||
|
||||
.input-status {
|
||||
height: 20px;
|
||||
margin-top: 12px;
|
||||
font-size: 14px;
|
||||
color: #ff6b6b;
|
||||
}
|
||||
|
||||
/* === Projects Section === */
|
||||
.projects-section {
|
||||
padding: 40px 20px 80px;
|
||||
border-top: 1px solid #222;
|
||||
}
|
||||
|
||||
.projects-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 24px;
|
||||
max-width: 1200px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.projects-title {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #e0e0e0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.btn-refresh {
|
||||
padding: 8px 16px;
|
||||
background: #1a1a1a;
|
||||
border: 1px solid #333;
|
||||
border-radius: 8px;
|
||||
color: #e0e0e0;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-refresh:hover {
|
||||
background: #252525;
|
||||
border-color: #4a9eff;
|
||||
}
|
||||
|
||||
.projects-empty {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
text-align: center;
|
||||
padding: 80px 40px;
|
||||
color: #888;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
/* === Projects Table === */
|
||||
.projects-table-wrapper {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
background: #1a1a1a;
|
||||
border: 1px solid #333;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.projects-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.projects-table thead {
|
||||
background: #0d0d0d;
|
||||
border-bottom: 1px solid #333;
|
||||
}
|
||||
|
||||
.projects-table th {
|
||||
padding: 16px 20px;
|
||||
text-align: left;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #888;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.projects-table tbody tr {
|
||||
border-bottom: 1px solid #252525;
|
||||
transition: background 0.2s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.projects-table tbody tr:hover {
|
||||
background: #252525;
|
||||
}
|
||||
|
||||
.projects-table tbody tr:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.projects-table td {
|
||||
padding: 16px 20px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
/* Column Widths */
|
||||
.col-name { width: 40%; }
|
||||
.col-activity { width: 25%; }
|
||||
.col-status { width: 15%; }
|
||||
.col-actions { width: 20%; text-align: right; }
|
||||
|
||||
/* Project Name */
|
||||
.project-name {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #e0e0e0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.project-name-input {
|
||||
background: #0d0d0d;
|
||||
border: 2px solid #4a9eff;
|
||||
border-radius: 6px;
|
||||
padding: 8px 12px;
|
||||
color: #e0e0e0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
font-family: inherit;
|
||||
width: 100%;
|
||||
max-width: 300px;
|
||||
outline: none;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.project-name-input:focus {
|
||||
box-shadow: 0 0 0 3px rgba(74, 158, 255, 0.15);
|
||||
}
|
||||
|
||||
/* Last Activity */
|
||||
.last-activity {
|
||||
font-size: 14px;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
/* Status Badges */
|
||||
.status-badge {
|
||||
display: inline-block;
|
||||
padding: 6px 12px;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.status-badge.active { color: #51cf66; background: rgba(81, 207, 102, 0.15); }
|
||||
.status-badge.done { color: #888; background: rgba(136, 136, 136, 0.15); }
|
||||
.status-badge.archived { color: #ffa94d; background: rgba(255, 169, 77, 0.15); }
|
||||
|
||||
/* Buttons */
|
||||
.btn-continue {
|
||||
padding: 8px 20px;
|
||||
background: linear-gradient(135deg, #4a9eff 0%, #a78bfa 100%);
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
color: white;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-continue:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(74, 158, 255, 0.4);
|
||||
}
|
||||
|
||||
.btn-menu {
|
||||
background: #2a2a2a;
|
||||
border: 1px solid #444;
|
||||
color: #e0e0e0;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 8px;
|
||||
font-size: 20px;
|
||||
cursor: pointer;
|
||||
margin-left: 8px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-menu:hover {
|
||||
background: #3a3a3a;
|
||||
border-color: #666;
|
||||
}
|
||||
|
||||
/* Dropdown Menu */
|
||||
.dropdown-menu {
|
||||
position: absolute;
|
||||
background: #2a2a2a;
|
||||
border: 1px solid #444;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5);
|
||||
z-index: 1000;
|
||||
min-width: 180px;
|
||||
}
|
||||
|
||||
.dropdown-menu-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
padding: 12px 16px;
|
||||
background: none;
|
||||
border: none;
|
||||
color: #e0e0e0;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
|
||||
.dropdown-menu-item:hover { background: #3a3a3a; }
|
||||
.dropdown-menu-item:first-child { border-radius: 8px 8px 0 0; }
|
||||
.dropdown-menu-item:last-child { border-radius: 0 0 8px 8px; }
|
||||
.dropdown-menu-item.danger { color: #ff6b6b; }
|
||||
|
||||
/* Loading State */
|
||||
.loading {
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
color: #888;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 768px) {
|
||||
.hero-title { font-size: 40px; }
|
||||
.hero-subtitle { font-size: 16px; }
|
||||
.project-input { font-size: 16px; padding: 14px 20px; }
|
||||
|
||||
.projects-table th,
|
||||
.projects-table td { padding: 12px 16px; }
|
||||
.col-actions { display: none; }
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.hero-section { padding: 60px 16px 40px; }
|
||||
.hero-title { font-size: 32px; }
|
||||
.hero-subtitle { font-size: 14px; }
|
||||
.project-input { font-size: 14px; padding: 12px 16px; }
|
||||
|
||||
.projects-section { padding: 24px 16px 60px; }
|
||||
.projects-header { flex-direction: column; align-items: flex-start; gap: 12px; }
|
||||
|
||||
.projects-table thead { display: none; }
|
||||
.projects-table tbody { display: flex; flex-direction: column; gap: 12px; }
|
||||
.projects-table tr {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border: 1px solid #333;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
gap: 8px;
|
||||
}
|
||||
.projects-table td {
|
||||
padding: 4px 0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border: none;
|
||||
}
|
||||
.projects-table td::before {
|
||||
content: attr(data-label);
|
||||
font-size: 12px;
|
||||
color: #888;
|
||||
font-weight: 600;
|
||||
}
|
||||
.col-actions { display: flex; justify-content: flex-end; gap: 8px; }
|
||||
}
|
||||
|
||||
/**
|
||||
* Loading Overlay
|
||||
*/
|
||||
.loading-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 10000;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.loading-overlay.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.loading-overlay.visible {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border: 4px solid #333;
|
||||
border-top-color: #4a9eff;
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
margin-top: 20px;
|
||||
color: #e0e0e0;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/**
|
||||
* Toast Notifications
|
||||
*/
|
||||
.toast-notification {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
min-width: 300px;
|
||||
max-width: 500px;
|
||||
padding: 16px 20px;
|
||||
background: #2a2a2a;
|
||||
border: 1px solid #444;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
z-index: 10001;
|
||||
opacity: 0;
|
||||
transform: translateX(400px);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.toast-notification.visible {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.toast-icon {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
flex-shrink: 0;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.toast-message {
|
||||
flex: 1;
|
||||
color: #e0e0e0;
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* Toast Types */
|
||||
.toast-success {
|
||||
border-color: #51cf66;
|
||||
}
|
||||
|
||||
.toast-success .toast-icon {
|
||||
color: #51cf66;
|
||||
background: rgba(81, 207, 102, 0.1);
|
||||
}
|
||||
|
||||
.toast-error {
|
||||
border-color: #ff6b6b;
|
||||
}
|
||||
|
||||
.toast-error .toast-icon {
|
||||
color: #ff6b6b;
|
||||
background: rgba(255, 107, 107, 0.1);
|
||||
}
|
||||
|
||||
.toast-info {
|
||||
border-color: #4a9eff;
|
||||
}
|
||||
|
||||
.toast-info .toast-icon {
|
||||
color: #4a9eff;
|
||||
background: rgba(74, 158, 255, 0.1);
|
||||
}
|
||||
|
||||
.toast-warning {
|
||||
border-color: #ffa94d;
|
||||
}
|
||||
|
||||
.toast-warning .toast-icon {
|
||||
color: #ffa94d;
|
||||
background: rgba(255, 169, 77, 0.1);
|
||||
}
|
||||
|
||||
/* Responsive Toast */
|
||||
@media (max-width: 768px) {
|
||||
.toast-notification {
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
left: 10px;
|
||||
min-width: auto;
|
||||
max-width: none;
|
||||
}
|
||||
}
|
||||
608
public/claude-ide/sessions-landing.js
Normal file
608
public/claude-ide/sessions-landing.js
Normal file
@@ -0,0 +1,608 @@
|
||||
/**
|
||||
* Sessions Landing Page JavaScript
|
||||
*/
|
||||
|
||||
// Load sessions on page load
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
checkAuth();
|
||||
initializeProjectInput();
|
||||
loadSessions();
|
||||
});
|
||||
|
||||
// Check authentication
|
||||
async function checkAuth() {
|
||||
try {
|
||||
const res = await fetch('/claude/api/auth/status');
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error('Request failed');
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
if (!data.authenticated) {
|
||||
// Redirect to login if not authenticated
|
||||
window.location.href = '/claude/login.html';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Auth check failed:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize project input field in hero section
|
||||
*/
|
||||
function initializeProjectInput() {
|
||||
const input = document.getElementById('project-input');
|
||||
const status = document.getElementById('input-status');
|
||||
|
||||
if (!input) return;
|
||||
|
||||
// Auto-focus on page load
|
||||
input.focus();
|
||||
|
||||
input.addEventListener('input', () => {
|
||||
const projectName = input.value.trim();
|
||||
const hasInvalidChars = !validateProjectName(projectName);
|
||||
|
||||
if (hasInvalidChars && projectName.length > 0) {
|
||||
status.textContent = 'Invalid characters';
|
||||
status.classList.add('error');
|
||||
} else {
|
||||
status.textContent = '';
|
||||
status.classList.remove('error');
|
||||
}
|
||||
});
|
||||
|
||||
input.addEventListener('keypress', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
const projectName = input.value.trim();
|
||||
if (projectName && validateProjectName(projectName)) {
|
||||
createProject(projectName);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new project and navigate to IDE
|
||||
*/
|
||||
async function createProject(projectName) {
|
||||
try {
|
||||
showLoadingOverlay('Creating project...');
|
||||
|
||||
const res = await fetch('/claude/api/claude/sessions', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
metadata: {
|
||||
type: 'chat',
|
||||
source: 'web-ide',
|
||||
project: projectName
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
if (!res.ok) throw new Error('Request failed');
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
if (data.success) {
|
||||
// Minimum display time for smooth UX
|
||||
await new Promise(resolve => setTimeout(resolve, 300));
|
||||
window.location.href = `/claude/ide?session=${data.session.id}`;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error creating project:', error);
|
||||
hideLoadingOverlay();
|
||||
showToast('Failed to create project', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load all sessions and render in table
|
||||
*/
|
||||
async function loadSessions() {
|
||||
const tbody = document.getElementById('projects-tbody');
|
||||
const emptyState = document.getElementById('projects-empty');
|
||||
const table = document.getElementById('projects-table');
|
||||
|
||||
if (!tbody) return;
|
||||
|
||||
tbody.innerHTML = '<tr><td colspan="4" style="text-align: center; padding: 40px;">Loading...</td></tr>';
|
||||
|
||||
try {
|
||||
const res = await fetch('/claude/api/claude/sessions');
|
||||
|
||||
if (!res.ok) throw new Error('Request failed');
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
const allSessions = [
|
||||
...(data.active || []).map(s => ({...s, type: 'active'})),
|
||||
...(data.historical || []).map(s => ({...s, type: 'historical'}))
|
||||
];
|
||||
|
||||
renderProjectsTable(allSessions);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error loading sessions:', error);
|
||||
tbody.innerHTML = '<tr><td colspan="4" style="text-align: center; color: #ff6b6b;">Failed to load</td></tr>';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render projects table with session data
|
||||
*/
|
||||
function renderProjectsTable(sessions) {
|
||||
const tbody = document.getElementById('projects-tbody');
|
||||
const emptyState = document.getElementById('projects-empty');
|
||||
const table = document.getElementById('projects-table');
|
||||
|
||||
if (!tbody) return;
|
||||
|
||||
// Sort by last activity (newest first)
|
||||
sessions.sort((a, b) => {
|
||||
const dateA = new Date(a.lastActivity || a.createdAt || a.created_at);
|
||||
const dateB = new Date(b.lastActivity || b.createdAt || b.created_at);
|
||||
return dateB - dateA;
|
||||
});
|
||||
|
||||
tbody.innerHTML = '';
|
||||
|
||||
if (sessions.length === 0) {
|
||||
if (table) table.style.display = 'none';
|
||||
if (emptyState) emptyState.style.display = 'block';
|
||||
return;
|
||||
}
|
||||
|
||||
if (table) table.style.display = 'table';
|
||||
if (emptyState) emptyState.style.display = 'none';
|
||||
|
||||
sessions.forEach(session => {
|
||||
const row = createProjectRow(session);
|
||||
tbody.appendChild(row);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a single project row for the table
|
||||
*/
|
||||
function createProjectRow(session) {
|
||||
const tr = document.createElement('tr');
|
||||
tr.dataset.sessionId = session.id;
|
||||
|
||||
const projectName = getProjectName(session);
|
||||
const relativeTime = getRelativeTime(session);
|
||||
const status = getStatus(session);
|
||||
const statusClass = getStatusClass(session);
|
||||
|
||||
tr.innerHTML = `
|
||||
<td data-label="Project">
|
||||
<h3 class="project-name">${escapeHtml(projectName)}</h3>
|
||||
</td>
|
||||
<td data-label="Last Activity">
|
||||
<span class="last-activity">${relativeTime}</span>
|
||||
</td>
|
||||
<td data-label="Status">
|
||||
<span class="status-badge ${statusClass}">${status}</span>
|
||||
</td>
|
||||
<td data-label="Actions">
|
||||
<button class="btn-continue">Continue</button>
|
||||
<button class="btn-menu" aria-label="Menu">⋮</button>
|
||||
</td>
|
||||
`;
|
||||
|
||||
tr.addEventListener('click', (e) => {
|
||||
if (!e.target.closest('.btn-menu')) {
|
||||
continueToSession(session.id);
|
||||
}
|
||||
});
|
||||
|
||||
tr.querySelector('.btn-continue').addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
continueToSession(session.id);
|
||||
});
|
||||
|
||||
tr.querySelector('.btn-menu').addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
showProjectMenu(e, session);
|
||||
});
|
||||
|
||||
return tr;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract project name from session metadata
|
||||
*/
|
||||
function getProjectName(session) {
|
||||
return session.metadata?.project ||
|
||||
session.metadata?.projectName ||
|
||||
session.workingDir?.split('/').pop() ||
|
||||
'Session ' + session.id.substring(0, 8);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get relative time string for session
|
||||
*/
|
||||
function getRelativeTime(session) {
|
||||
const date = new Date(session.lastActivity || session.createdAt || session.created_at);
|
||||
const now = new Date();
|
||||
const diffMins = Math.floor((now - date) / 60000);
|
||||
const diffHours = Math.floor((now - date) / 3600000);
|
||||
const diffDays = Math.floor((now - date) / 86400000);
|
||||
|
||||
if (diffMins < 1) return 'Just now';
|
||||
if (diffMins < 60) return `${diffMins}m ago`;
|
||||
if (diffHours < 24) return `${diffHours}h ago`;
|
||||
if (diffDays < 7) return `${diffDays}d ago`;
|
||||
return date.toLocaleDateString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get status text for session
|
||||
*/
|
||||
function getStatus(session) {
|
||||
if (session.status === 'running') return 'Active';
|
||||
return 'Done';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get status CSS class for session
|
||||
*/
|
||||
function getStatusClass(session) {
|
||||
if (session.status === 'running') return 'active';
|
||||
return 'done';
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to IDE with session
|
||||
*/
|
||||
async function continueToSession(sessionId) {
|
||||
showLoadingOverlay('Opening workspace...');
|
||||
await new Promise(resolve => setTimeout(resolve, 300));
|
||||
window.location.href = `/claude/ide?session=${sessionId}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Show project menu dropdown
|
||||
*/
|
||||
function showProjectMenu(event, session) {
|
||||
closeProjectMenu();
|
||||
|
||||
const menu = document.createElement('div');
|
||||
menu.className = 'dropdown-menu';
|
||||
menu.id = 'project-menu';
|
||||
|
||||
menu.innerHTML = `
|
||||
<button class="dropdown-menu-item" data-action="rename">✏️ Rename</button>
|
||||
<button class="dropdown-menu-item" data-action="duplicate">📋 Duplicate</button>
|
||||
<button class="dropdown-menu-item danger" data-action="delete">🗑️ Delete</button>
|
||||
`;
|
||||
|
||||
const rect = event.target.getBoundingClientRect();
|
||||
menu.style.position = 'fixed';
|
||||
menu.style.top = `${rect.bottom + 8}px`;
|
||||
menu.style.right = `${window.innerWidth - rect.right}px`;
|
||||
|
||||
document.body.appendChild(menu);
|
||||
|
||||
menu.querySelector('[data-action="rename"]').addEventListener('click', () => {
|
||||
closeProjectMenu();
|
||||
renameProject(session);
|
||||
});
|
||||
|
||||
menu.querySelector('[data-action="duplicate"]').addEventListener('click', async () => {
|
||||
closeProjectMenu();
|
||||
await duplicateProject(session);
|
||||
loadSessions();
|
||||
});
|
||||
|
||||
menu.querySelector('[data-action="delete"]').addEventListener('click', async () => {
|
||||
closeProjectMenu();
|
||||
await deleteProject(session);
|
||||
loadSessions();
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
document.addEventListener('click', closeProjectMenu, { once: true });
|
||||
}, 10);
|
||||
}
|
||||
|
||||
/**
|
||||
* Close project menu dropdown
|
||||
*/
|
||||
function closeProjectMenu() {
|
||||
document.getElementById('project_menu')?.remove();
|
||||
}
|
||||
|
||||
/**
|
||||
* Rename project (inline edit)
|
||||
*/
|
||||
function renameProject(session) {
|
||||
const row = document.querySelector(`tr[data-session-id="${session.id}"]`);
|
||||
if (!row) return;
|
||||
|
||||
const nameCell = row.querySelector('.project-name');
|
||||
if (!nameCell) return;
|
||||
|
||||
const currentName = nameCell.textContent;
|
||||
|
||||
// Only block truly historical sessions (loaded from disk, not in memory)
|
||||
// Active and terminated sessions in memory can be renamed
|
||||
if (session.type === 'historical') {
|
||||
showToast('Cannot rename historical sessions', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
// Create input element
|
||||
const input = document.createElement('input');
|
||||
input.type = 'text';
|
||||
input.className = 'project-name-input';
|
||||
input.value = currentName;
|
||||
input.maxLength = 50;
|
||||
|
||||
// Replace text with input
|
||||
nameCell.innerHTML = '';
|
||||
nameCell.appendChild(input);
|
||||
input.focus();
|
||||
input.select();
|
||||
|
||||
// Handle save
|
||||
const saveRename = async () => {
|
||||
const newName = input.value.trim();
|
||||
|
||||
// Validation
|
||||
if (!newName) {
|
||||
nameCell.textContent = currentName;
|
||||
showToast('Project name cannot be empty', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!validateProjectName(newName)) {
|
||||
nameCell.textContent = currentName;
|
||||
showToast('Project name contains invalid characters', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (newName === currentName) {
|
||||
nameCell.textContent = currentName;
|
||||
return;
|
||||
}
|
||||
|
||||
// Show saving state
|
||||
input.disabled = true;
|
||||
input.style.opacity = '0.6';
|
||||
|
||||
try {
|
||||
const res = await fetch(`/claude/api/claude/sessions/${session.id}`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
metadata: {
|
||||
...session.metadata,
|
||||
project: newName
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const data = await res.json();
|
||||
throw new Error(data.error || 'Failed to rename');
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
if (data.success) {
|
||||
nameCell.textContent = newName;
|
||||
showToast('Project renamed successfully', 'success');
|
||||
} else {
|
||||
throw new Error(data.error || 'Failed to rename');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error renaming project:', error);
|
||||
nameCell.textContent = currentName;
|
||||
showToast(error.message || 'Failed to rename project', 'error');
|
||||
}
|
||||
};
|
||||
|
||||
// Handle cancel
|
||||
const cancelRename = () => {
|
||||
nameCell.textContent = currentName;
|
||||
};
|
||||
|
||||
// Event listeners
|
||||
input.addEventListener('keypress', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
input.blur(); // Trigger save on blur
|
||||
} else if (e.key === 'Escape') {
|
||||
cancelRename();
|
||||
}
|
||||
});
|
||||
|
||||
input.addEventListener('blur', () => {
|
||||
// Small delay to allow Enter key to process first
|
||||
setTimeout(() => {
|
||||
if (document.activeElement !== input) {
|
||||
saveRename();
|
||||
}
|
||||
}, 100);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Duplicate project/session
|
||||
*/
|
||||
async function duplicateProject(session) {
|
||||
try {
|
||||
showLoadingOverlay('Duplicating project...');
|
||||
|
||||
const res = await fetch(`/claude/api/claude/sessions/${session.id}/duplicate`, {
|
||||
method: 'POST'
|
||||
});
|
||||
|
||||
if (!res.ok) throw new Error('Request failed');
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
if (data.success) {
|
||||
hideLoadingOverlay();
|
||||
showToast('Project duplicated successfully', 'success');
|
||||
} else {
|
||||
throw new Error(data.error || 'Failed to duplicate');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error duplicating project:', error);
|
||||
hideLoadingOverlay();
|
||||
showToast('Failed to duplicate project', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete project/session with confirmation
|
||||
*/
|
||||
async function deleteProject(session) {
|
||||
const projectName = getProjectName(session);
|
||||
|
||||
if (!confirm(`Are you sure you want to delete "${projectName}"? This action cannot be undone.`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
showLoadingOverlay('Deleting project...');
|
||||
|
||||
const res = await fetch(`/claude/api/claude/sessions/${session.id}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
|
||||
if (!res.ok) throw new Error('Request failed');
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
if (data.success) {
|
||||
hideLoadingOverlay();
|
||||
showToast('Project deleted successfully', 'success');
|
||||
} else {
|
||||
throw new Error(data.error || 'Failed to delete');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error deleting project:', error);
|
||||
hideLoadingOverlay();
|
||||
showToast('Failed to delete project', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Refresh sessions
|
||||
function refreshSessions() {
|
||||
loadSessions();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate project name for invalid characters
|
||||
* @param {string} name - The project name to validate
|
||||
* @returns {boolean} - True if valid, false if contains invalid characters
|
||||
*/
|
||||
function validateProjectName(name) {
|
||||
const invalidChars = /[\/\\<>:"|?*]/;
|
||||
return !invalidChars.test(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show loading overlay
|
||||
* @param {string} message - Optional custom message (default: "Loading...")
|
||||
*/
|
||||
function showLoadingOverlay(message = 'Loading...') {
|
||||
let overlay = document.getElementById('loading-overlay');
|
||||
|
||||
if (!overlay) {
|
||||
overlay = document.createElement('div');
|
||||
overlay.id = 'loading-overlay';
|
||||
overlay.className = 'loading-overlay';
|
||||
overlay.innerHTML = `
|
||||
<div class="loading-spinner"></div>
|
||||
<p class="loading-text">${escapeHtml(message)}</p>
|
||||
`;
|
||||
document.body.appendChild(overlay);
|
||||
} else {
|
||||
// Update message if provided
|
||||
const textElement = overlay.querySelector('.loading-text');
|
||||
if (textElement) {
|
||||
textElement.textContent = message;
|
||||
}
|
||||
}
|
||||
|
||||
overlay.classList.remove('hidden');
|
||||
setTimeout(() => {
|
||||
overlay.classList.add('visible');
|
||||
}, 10);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide loading overlay
|
||||
*/
|
||||
function hideLoadingOverlay() {
|
||||
const overlay = document.getElementById('loading-overlay');
|
||||
if (overlay) {
|
||||
overlay.classList.remove('visible');
|
||||
setTimeout(() => {
|
||||
overlay.classList.add('hidden');
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show toast notification
|
||||
* @param {string} message - The message to display
|
||||
* @param {string} type - The type of toast: 'success', 'error', 'info'
|
||||
* @param {number} duration - Duration in milliseconds (default: 3000)
|
||||
*/
|
||||
function showToast(message, type = 'info', duration = 3000) {
|
||||
// Remove existing toasts
|
||||
const existingToasts = document.querySelectorAll('.toast-notification');
|
||||
existingToasts.forEach(toast => toast.remove());
|
||||
|
||||
// Create toast element
|
||||
const toast = document.createElement('div');
|
||||
toast.className = `toast-notification toast-${type}`;
|
||||
toast.innerHTML = `
|
||||
<span class="toast-icon">${getToastIcon(type)}</span>
|
||||
<span class="toast-message">${escapeHtml(message)}</span>
|
||||
`;
|
||||
|
||||
document.body.appendChild(toast);
|
||||
|
||||
// Trigger animation
|
||||
setTimeout(() => {
|
||||
toast.classList.add('visible');
|
||||
}, 10);
|
||||
|
||||
// Auto remove after duration
|
||||
setTimeout(() => {
|
||||
toast.classList.remove('visible');
|
||||
setTimeout(() => {
|
||||
toast.remove();
|
||||
}, 300);
|
||||
}, duration);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get toast icon based on type
|
||||
*/
|
||||
function getToastIcon(type) {
|
||||
const icons = {
|
||||
success: '✓',
|
||||
error: '✕',
|
||||
info: 'ℹ',
|
||||
warning: '⚠'
|
||||
};
|
||||
return icons[type] || icons.info;
|
||||
}
|
||||
|
||||
/**
|
||||
* Escape HTML to prevent XSS
|
||||
*/
|
||||
function escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
395
public/claude-ide/tag-renderer.css
Normal file
395
public/claude-ide/tag-renderer.css
Normal file
@@ -0,0 +1,395 @@
|
||||
/**
|
||||
* Tag Renderer Styles
|
||||
*/
|
||||
|
||||
/* Operations Panel */
|
||||
.operations-panel {
|
||||
background: #1e1e1e;
|
||||
border: 1px solid #333;
|
||||
border-radius: 8px;
|
||||
margin: 20px 0;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.operations-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 20px;
|
||||
background: #252525;
|
||||
border-bottom: 1px solid #333;
|
||||
}
|
||||
|
||||
.operations-header h3 {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
.operations-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.operations-list {
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* Operation Item */
|
||||
.operation-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12px 20px;
|
||||
border-bottom: 1px solid #333;
|
||||
gap: 12px;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.operation-item:hover {
|
||||
background: #2a2a2a;
|
||||
}
|
||||
|
||||
.operation-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.operation-icon {
|
||||
font-size: 24px;
|
||||
width: 40px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.operation-details {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.operation-description {
|
||||
flex: 1;
|
||||
color: #e0e0e0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.btn-view-code {
|
||||
background: #3a3a3a;
|
||||
border: 1px solid #555;
|
||||
color: #e0e0e0;
|
||||
padding: 4px 12px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.btn-view-code:hover {
|
||||
background: #4a4a4a;
|
||||
border-color: #666;
|
||||
}
|
||||
|
||||
.operation-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.operation-status {
|
||||
min-width: 80px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.operation-status span {
|
||||
font-size: 12px;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Operation Type Colors */
|
||||
.op-write {
|
||||
border-left: 3px solid #4a9eff;
|
||||
}
|
||||
|
||||
.op-rename {
|
||||
border-left: 3px solid #ffa94d;
|
||||
}
|
||||
|
||||
.op-delete {
|
||||
border-left: 3px solid #ff6b6b;
|
||||
}
|
||||
|
||||
.op-install {
|
||||
border-left: 3px solid #51cf66;
|
||||
}
|
||||
|
||||
.op-command {
|
||||
border-left: 3px solid #cc5de8;
|
||||
}
|
||||
|
||||
/* Status Colors */
|
||||
.status-pending {
|
||||
color: #ffd43b;
|
||||
background: rgba(255, 212, 59, 0.1);
|
||||
}
|
||||
|
||||
.status-approved {
|
||||
color: #51cf66;
|
||||
background: rgba(81, 207, 102, 0.1);
|
||||
}
|
||||
|
||||
.status-rejected {
|
||||
color: #ff6b6b;
|
||||
background: rgba(255, 107, 107, 0.1);
|
||||
}
|
||||
|
||||
.status-executing {
|
||||
color: #4a9eff;
|
||||
background: rgba(74, 158, 255, 0.1);
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
.btn {
|
||||
padding: 8px 16px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.btn-approve {
|
||||
background: #2b7a4b;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-approve:hover:not(:disabled) {
|
||||
background: #349858;
|
||||
}
|
||||
|
||||
.btn-reject {
|
||||
background: #a83d3d;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-reject:hover:not(:disabled) {
|
||||
background: #c94e4e;
|
||||
}
|
||||
|
||||
.btn-approve-all {
|
||||
background: #2b7a4b;
|
||||
color: white;
|
||||
padding: 6px 12px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.btn-approve-all:hover {
|
||||
background: #349858;
|
||||
}
|
||||
|
||||
.btn-reject-all {
|
||||
background: #a83d3d;
|
||||
color: white;
|
||||
padding: 6px 12px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.btn-reject-all:hover {
|
||||
background: #c94e4e;
|
||||
}
|
||||
|
||||
/* Progress Indicator */
|
||||
.operation-progress {
|
||||
background: #1e1e1e;
|
||||
border: 1px solid #333;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin: 20px 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.progress-spinner {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border: 3px solid #333;
|
||||
border-top-color: #4a9eff;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.progress-message {
|
||||
color: #e0e0e0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* Completion Message */
|
||||
.operation-completion {
|
||||
background: #1e3a2e;
|
||||
border: 1px solid #2b7a4b;
|
||||
border-radius: 8px;
|
||||
padding: 16px 20px;
|
||||
margin: 20px 0;
|
||||
animation: slideIn 0.3s ease;
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.completion-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.completion-icon {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.completion-header h4 {
|
||||
margin: 0;
|
||||
color: #51cf66;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.completion-summary {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.completion-summary .success {
|
||||
color: #51cf66;
|
||||
}
|
||||
|
||||
.completion-summary .error {
|
||||
color: #ff6b6b;
|
||||
}
|
||||
|
||||
/* Error Message */
|
||||
.operation-error {
|
||||
background: #3a1e1e;
|
||||
border: 1px solid #a83d3d;
|
||||
border-radius: 8px;
|
||||
padding: 16px 20px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.error-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.error-icon {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.error-header h4 {
|
||||
margin: 0;
|
||||
color: #ff6b6b;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
color: #e0e0e0;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* Code Modal */
|
||||
.code-modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 10000;
|
||||
animation: fadeIn 0.2s ease;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: #1e1e1e;
|
||||
border: 1px solid #333;
|
||||
border-radius: 8px;
|
||||
max-width: 80vw;
|
||||
max-height: 80vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid #333;
|
||||
}
|
||||
|
||||
.modal-header h3 {
|
||||
margin: 0;
|
||||
color: #e0e0e0;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.btn-close {
|
||||
background: #3a3a3a;
|
||||
border: 1px solid #555;
|
||||
color: #e0e0e0;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 18px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.btn-close:hover {
|
||||
background: #4a4a4a;
|
||||
}
|
||||
|
||||
.modal-content pre {
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
overflow: auto;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.modal-content code {
|
||||
font-family: 'Fira Code', 'Courier New', monospace;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
543
public/claude-ide/tag-renderer.js
Normal file
543
public/claude-ide/tag-renderer.js
Normal file
@@ -0,0 +1,543 @@
|
||||
/**
|
||||
* Tag Renderer - Display and manage dyad-style operations in UI
|
||||
*/
|
||||
|
||||
class TagRenderer {
|
||||
constructor() {
|
||||
this.pendingOperations = null;
|
||||
this.isExecuting = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse tags from text response
|
||||
*/
|
||||
parseTags(response) {
|
||||
const tags = {
|
||||
writes: this.extractWriteTags(response),
|
||||
renames: this.extractRenameTags(response),
|
||||
deletes: this.extractDeleteTags(response),
|
||||
dependencies: this.extractDependencyTags(response),
|
||||
commands: this.extractCommandTags(response)
|
||||
};
|
||||
|
||||
return tags;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract dyad-write tags
|
||||
*/
|
||||
extractWriteTags(response) {
|
||||
const regex = /<dyad-write\s+path="([^"]+)">([\s\S]*?)<\/dyad-write>/g;
|
||||
const tags = [];
|
||||
let match;
|
||||
|
||||
while ((match = regex.exec(response)) !== null) {
|
||||
tags.push({
|
||||
type: 'write',
|
||||
path: match[1],
|
||||
content: match[2].trim()
|
||||
});
|
||||
}
|
||||
|
||||
return tags;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract dyad-rename tags
|
||||
*/
|
||||
extractRenameTags(response) {
|
||||
const regex = /<dyad-rename\s+from="([^"]+)"\s+to="([^"]+)">/g;
|
||||
const tags = [];
|
||||
let match;
|
||||
|
||||
while ((match = regex.exec(response)) !== null) {
|
||||
tags.push({
|
||||
type: 'rename',
|
||||
from: match[1],
|
||||
to: match[2]
|
||||
});
|
||||
}
|
||||
|
||||
return tags;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract dyad-delete tags
|
||||
*/
|
||||
extractDeleteTags(response) {
|
||||
const regex = /<dyad-delete\s+path="([^"]+)">/g;
|
||||
const tags = [];
|
||||
let match;
|
||||
|
||||
while ((match = regex.exec(response)) !== null) {
|
||||
tags.push({
|
||||
type: 'delete',
|
||||
path: match[1]
|
||||
});
|
||||
}
|
||||
|
||||
return tags;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract dyad-add-dependency tags
|
||||
*/
|
||||
extractDependencyTags(response) {
|
||||
const regex = /<dyad-add-dependency\s+packages="([^"]+)">/g;
|
||||
const tags = [];
|
||||
let match;
|
||||
|
||||
while ((match = regex.exec(response)) !== null) {
|
||||
tags.push({
|
||||
type: 'install',
|
||||
packages: match[1].split(' ').filter(p => p.trim())
|
||||
});
|
||||
}
|
||||
|
||||
return tags;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract dyad-command tags
|
||||
*/
|
||||
extractCommandTags(response) {
|
||||
const regex = /<dyad-command\s+type="([^"]+)">/g;
|
||||
const tags = [];
|
||||
let match;
|
||||
|
||||
while ((match = regex.exec(response)) !== null) {
|
||||
tags.push({
|
||||
type: 'command',
|
||||
command: match[1]
|
||||
});
|
||||
}
|
||||
|
||||
return tags;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate operation summary for display
|
||||
*/
|
||||
generateOperationsSummary(tags) {
|
||||
const operations = [];
|
||||
|
||||
tags.deletes.forEach(tag => {
|
||||
operations.push({
|
||||
type: 'delete',
|
||||
description: `Delete ${tag.path}`,
|
||||
tag,
|
||||
status: 'pending'
|
||||
});
|
||||
});
|
||||
|
||||
tags.renames.forEach(tag => {
|
||||
operations.push({
|
||||
type: 'rename',
|
||||
description: `Rename ${tag.from} → ${tag.to}`,
|
||||
tag,
|
||||
status: 'pending'
|
||||
});
|
||||
});
|
||||
|
||||
tags.writes.forEach(tag => {
|
||||
operations.push({
|
||||
type: 'write',
|
||||
description: `Create/update ${tag.path}`,
|
||||
tag,
|
||||
status: 'pending'
|
||||
});
|
||||
});
|
||||
|
||||
tags.dependencies.forEach(tag => {
|
||||
operations.push({
|
||||
type: 'install',
|
||||
description: `Install packages: ${tag.packages.join(', ')}`,
|
||||
tag,
|
||||
status: 'pending'
|
||||
});
|
||||
});
|
||||
|
||||
tags.commands.forEach(tag => {
|
||||
operations.push({
|
||||
type: 'command',
|
||||
description: `Execute command: ${tag.command}`,
|
||||
tag,
|
||||
status: 'pending'
|
||||
});
|
||||
});
|
||||
|
||||
return operations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Strip tags from response for clean display
|
||||
*/
|
||||
stripTags(response) {
|
||||
let stripped = response;
|
||||
|
||||
stripped = stripped.replace(/<dyad-write\s+path="[^"]+">[\s\S]*?<\/dyad-write>/g, '[File: code]');
|
||||
stripped = stripped.replace(/<dyad-rename\s+from="[^"]+"\s+to="[^"]+">/g, '[Rename]');
|
||||
stripped = stripped.replace(/<dyad-delete\s+path="[^"]+">/g, '[Delete]');
|
||||
stripped = stripped.replace(/<dyad-add-dependency\s+packages="[^"]+">/g, '[Install]');
|
||||
stripped = stripped.replace(/<dyad-command\s+type="[^"]+">/g, '[Command]');
|
||||
|
||||
return stripped;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render operations panel HTML
|
||||
*/
|
||||
renderOperationsPanel(operations) {
|
||||
const panel = document.createElement('div');
|
||||
panel.className = 'operations-panel';
|
||||
panel.innerHTML = `
|
||||
<div class="operations-header">
|
||||
<h3>Pending Operations (${operations.length})</h3>
|
||||
<div class="operations-actions">
|
||||
<button class="btn btn-approve-all" onclick="approveAllOperations()">
|
||||
✓ Approve All
|
||||
</button>
|
||||
<button class="btn btn-reject-all" onclick="rejectAllOperations()">
|
||||
✗ Reject All
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="operations-list">
|
||||
${operations.map((op, index) => this.renderOperationItem(op, index)).join('')}
|
||||
</div>
|
||||
`;
|
||||
|
||||
return panel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render single operation item
|
||||
*/
|
||||
renderOperationItem(operation, index) {
|
||||
const icon = this.getOperationIcon(operation.type);
|
||||
const colorClass = this.getOperationColorClass(operation.type);
|
||||
|
||||
return `
|
||||
<div class="operation-item ${colorClass}" data-index="${index}">
|
||||
<div class="operation-icon">${icon}</div>
|
||||
<div class="operation-details">
|
||||
<div class="operation-description">${operation.description}</div>
|
||||
${operation.tag.content ? `<button class="btn-view-code" onclick="viewCode(${index})">View Code</button>` : ''}
|
||||
</div>
|
||||
<div class="operation-actions">
|
||||
<button class="btn btn-approve" onclick="approveOperation(${index})" title="Approve">✓</button>
|
||||
<button class="btn btn-reject" onclick="rejectOperation(${index})" title="Reject">✗</button>
|
||||
</div>
|
||||
<div class="operation-status">
|
||||
<span class="status-pending">Pending</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get icon for operation type
|
||||
*/
|
||||
getOperationIcon(type) {
|
||||
const icons = {
|
||||
write: '📄',
|
||||
rename: '✏️',
|
||||
delete: '🗑️',
|
||||
install: '📦',
|
||||
command: '⚡'
|
||||
};
|
||||
return icons[type] || '⚙️';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get color class for operation type
|
||||
*/
|
||||
getOperationColorClass(type) {
|
||||
const classes = {
|
||||
write: 'op-write',
|
||||
rename: 'op-rename',
|
||||
delete: 'op-delete',
|
||||
install: 'op-install',
|
||||
command: 'op-command'
|
||||
};
|
||||
return classes[type] || '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Show operations panel
|
||||
*/
|
||||
showOperationsPanel(operations, response) {
|
||||
this.pendingOperations = operations;
|
||||
window.currentOperationsResponse = response;
|
||||
|
||||
// Remove existing panel
|
||||
const existing = document.querySelector('.operations-panel');
|
||||
if (existing) {
|
||||
existing.remove();
|
||||
}
|
||||
|
||||
// Add new panel
|
||||
const chatContainer = document.getElementById('chat-messages');
|
||||
const panel = this.renderOperationsPanel(operations);
|
||||
chatContainer.appendChild(panel);
|
||||
|
||||
// Make it visible
|
||||
panel.style.display = 'block';
|
||||
}
|
||||
|
||||
/**
|
||||
* Update operation status
|
||||
*/
|
||||
updateOperationStatus(index, status) {
|
||||
const operationItem = document.querySelector(`.operation-item[data-index="${index}"]`);
|
||||
if (!operationItem) return;
|
||||
|
||||
const statusElement = operationItem.querySelector('.operation-status');
|
||||
statusElement.innerHTML = `<span class="status-${status}">${status.charAt(0).toUpperCase() + status.slice(1)}</span>`;
|
||||
|
||||
if (status === 'approved' || status === 'rejected') {
|
||||
// Disable buttons
|
||||
const approveBtn = operationItem.querySelector('.btn-approve');
|
||||
const rejectBtn = operationItem.querySelector('.btn-reject');
|
||||
approveBtn.disabled = true;
|
||||
rejectBtn.disabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update all operations status
|
||||
*/
|
||||
updateAllOperationsStatus(status) {
|
||||
if (!this.pendingOperations) return;
|
||||
|
||||
this.pendingOperations.forEach((_, index) => {
|
||||
this.updateOperationStatus(index, status);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide operations panel
|
||||
*/
|
||||
hideOperationsPanel() {
|
||||
const panel = document.querySelector('.operations-panel');
|
||||
if (panel) {
|
||||
panel.remove();
|
||||
}
|
||||
this.pendingOperations = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Show progress indicator
|
||||
*/
|
||||
showProgress(message) {
|
||||
const chatContainer = document.getElementById('chat-messages');
|
||||
|
||||
const progress = document.createElement('div');
|
||||
progress.className = 'operation-progress';
|
||||
progress.innerHTML = `
|
||||
<div class="progress-spinner"></div>
|
||||
<div class="progress-message">${message}</div>
|
||||
`;
|
||||
|
||||
chatContainer.appendChild(progress);
|
||||
progress.style.display = 'block';
|
||||
}
|
||||
|
||||
/**
|
||||
* Update progress message
|
||||
*/
|
||||
updateProgress(message) {
|
||||
const progressElement = document.querySelector('.operation-progress');
|
||||
if (progressElement) {
|
||||
const messageElement = progressElement.querySelector('.progress-message');
|
||||
if (messageElement) {
|
||||
messageElement.textContent = message;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide progress indicator
|
||||
*/
|
||||
hideProgress() {
|
||||
const progressElement = document.querySelector('.operation-progress');
|
||||
if (progressElement) {
|
||||
progressElement.remove();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show completion message
|
||||
*/
|
||||
showCompletion(results) {
|
||||
const chatContainer = document.querySelector('.chat-container');
|
||||
|
||||
const completion = document.createElement('div');
|
||||
completion.className = 'operation-completion';
|
||||
|
||||
const successCount = results.operations.filter(op => op.success).length;
|
||||
const errorCount = results.errors.length;
|
||||
|
||||
completion.innerHTML = `
|
||||
<div class="completion-header">
|
||||
<span class="completion-icon">✓</span>
|
||||
<h4>Operations Complete</h4>
|
||||
</div>
|
||||
<div class="completion-summary">
|
||||
<span class="success">${successCount} succeeded</span>
|
||||
${errorCount > 0 ? `<span class="error">${errorCount} failed</span>` : ''}
|
||||
</div>
|
||||
`;
|
||||
|
||||
chatContainer.appendChild(completion);
|
||||
completion.style.display = 'block';
|
||||
|
||||
// Auto-hide after 3 seconds
|
||||
setTimeout(() => {
|
||||
completion.remove();
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show error message
|
||||
*/
|
||||
showError(message) {
|
||||
const chatContainer = document.querySelector('.chat-container');
|
||||
|
||||
const error = document.createElement('div');
|
||||
error.className = 'operation-error';
|
||||
error.innerHTML = `
|
||||
<div class="error-header">
|
||||
<span class="error-icon">⚠️</span>
|
||||
<h4>Operation Failed</h4>
|
||||
</div>
|
||||
<div class="error-message">${message}</div>
|
||||
`;
|
||||
|
||||
chatContainer.appendChild(error);
|
||||
error.style.display = 'block';
|
||||
}
|
||||
}
|
||||
|
||||
// Global instance
|
||||
const tagRenderer = new TagRenderer();
|
||||
|
||||
// Store current response for execution
|
||||
window.currentOperationsResponse = null;
|
||||
|
||||
// Global functions for onclick handlers
|
||||
window.approveOperation = async function(index) {
|
||||
if (!tagRenderer.pendingOperations) return;
|
||||
|
||||
tagRenderer.updateOperationStatus(index, 'approved');
|
||||
};
|
||||
|
||||
window.rejectOperation = function(index) {
|
||||
if (!tagRenderer.pendingOperations) return;
|
||||
|
||||
tagRenderer.updateOperationStatus(index, 'rejected');
|
||||
};
|
||||
|
||||
window.approveAllOperations = async function() {
|
||||
if (!tagRenderer.pendingOperations) return;
|
||||
|
||||
tagRenderer.updateAllOperationsStatus('approved');
|
||||
|
||||
// Execute operations
|
||||
await executeApprovedOperations();
|
||||
};
|
||||
|
||||
window.rejectAllOperations = function() {
|
||||
if (!tagRenderer.pendingOperations) return;
|
||||
|
||||
tagRenderer.hideOperationsPanel();
|
||||
window.currentOperationsResponse = null;
|
||||
};
|
||||
|
||||
window.viewCode = function(index) {
|
||||
if (!tagRenderer.pendingOperations) return;
|
||||
|
||||
const operation = tagRenderer.pendingOperations[index];
|
||||
if (operation.tag && operation.tag.content) {
|
||||
// Show code in modal
|
||||
const modal = document.createElement('div');
|
||||
modal.className = 'code-modal';
|
||||
modal.innerHTML = `
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3>${operation.tag.path}</h3>
|
||||
<button class="btn-close" onclick="this.closest('.code-modal').remove()">✕</button>
|
||||
</div>
|
||||
<pre><code>${escapeHtml(operation.tag.content)}</code></pre>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(modal);
|
||||
modal.style.display = 'block';
|
||||
}
|
||||
};
|
||||
|
||||
function escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute approved operations
|
||||
*/
|
||||
async function executeApprovedOperations() {
|
||||
if (!window.currentOperationsResponse) {
|
||||
console.error('No response to execute');
|
||||
tagRenderer.showError('No operations to execute');
|
||||
return;
|
||||
}
|
||||
|
||||
// Use attachedSessionId (from chat-functions.js) instead of chatSessionId
|
||||
const sessionId = window.attachedSessionId || window.chatSessionId;
|
||||
|
||||
if (!sessionId) {
|
||||
console.error('No session ID - not attached to a session');
|
||||
tagRenderer.showError('Not attached to a session. Please start or attach to a session first.');
|
||||
return;
|
||||
}
|
||||
|
||||
const response = window.currentOperationsResponse;
|
||||
|
||||
console.log('Executing operations for session:', sessionId);
|
||||
|
||||
try {
|
||||
// Show progress
|
||||
tagRenderer.showProgress('Executing operations...');
|
||||
|
||||
// Call execute API with credentials
|
||||
const res = await fetch(`/claude/api/claude/sessions/${sessionId}/operations/execute`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'same-origin',
|
||||
body: JSON.stringify({ response })
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
if (!res.ok) {
|
||||
console.error('Execute API error:', data);
|
||||
throw new Error(data.error || 'Failed to execute operations');
|
||||
}
|
||||
|
||||
console.log('Operations executed successfully:', data.results);
|
||||
|
||||
// The WebSocket will handle showing completion
|
||||
// when it receives the operations-executed event
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error executing operations:', error);
|
||||
tagRenderer.hideProgress();
|
||||
tagRenderer.showError(error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Export for use in other files
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = TagRenderer;
|
||||
}
|
||||
658
public/claude-ide/terminal.css
Normal file
658
public/claude-ide/terminal.css
Normal file
@@ -0,0 +1,658 @@
|
||||
/**
|
||||
* Terminal Panel Styles
|
||||
*/
|
||||
|
||||
/* Terminal Layout */
|
||||
.terminal-layout {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: calc(100vh - 60px);
|
||||
background: #0d0d0d;
|
||||
}
|
||||
|
||||
/* Terminal Header */
|
||||
.terminal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px 20px;
|
||||
background: #1a1a1a;
|
||||
border-bottom: 1px solid #333;
|
||||
}
|
||||
|
||||
.terminal-header h2 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
/* Terminal Tabs */
|
||||
.terminal-tabs {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
padding: 8px 8px 0;
|
||||
background: #0a0a0a;
|
||||
border-bottom: 1px solid #333;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
min-height: 40px;
|
||||
}
|
||||
|
||||
.terminal-tabs::-webkit-scrollbar {
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
.terminal-tabs::-webkit-scrollbar-track {
|
||||
background: #1a1a1a;
|
||||
}
|
||||
|
||||
.terminal-tabs::-webkit-scrollbar-thumb {
|
||||
background: #444;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.terminal-tabs::-webkit-scrollbar-thumb:hover {
|
||||
background: #555;
|
||||
}
|
||||
|
||||
.terminal-tab {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 12px;
|
||||
background: #1a1a1a;
|
||||
border: 1px solid #333;
|
||||
border-bottom: none;
|
||||
border-radius: 6px 6px 0 0;
|
||||
color: #888;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
white-space: nowrap;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.terminal-tab:hover {
|
||||
background: #252525;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
.terminal-tab.active {
|
||||
background: #2a2a2a;
|
||||
color: #e0e0e0;
|
||||
border-color: #4a9eff;
|
||||
border-bottom-color: #2a2a2a;
|
||||
}
|
||||
|
||||
.tab-id {
|
||||
font-family: 'Monaco', 'Menlo', monospace;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.tab-mode {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.tab-close {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #888;
|
||||
font-size: 18px;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 3px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.tab-close:hover {
|
||||
background: #ff6b6b;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Terminals Container */
|
||||
.terminals-container {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.terminal-placeholder {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
color: #666;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.terminal-placeholder h3 {
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.terminal-placeholder p {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* Terminal Container */
|
||||
.terminal-container {
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
background: #1a1a1a;
|
||||
}
|
||||
|
||||
.terminal-container.active {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
/* Terminal Toolbar */
|
||||
.terminal-toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 16px;
|
||||
background: #252525;
|
||||
border-bottom: 1px solid #333;
|
||||
}
|
||||
|
||||
.terminal-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.terminal-id {
|
||||
font-family: 'Monaco', 'Menlo', monospace;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.terminal-path {
|
||||
color: #4a9eff;
|
||||
max-width: 300px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.terminal-mode {
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.terminal-mode[data-mode="session"] {
|
||||
background: rgba(74, 158, 255, 0.15);
|
||||
color: #4a9eff;
|
||||
}
|
||||
|
||||
.terminal-mode[data-mode="shell"] {
|
||||
background: rgba(81, 207, 102, 0.15);
|
||||
color: #51cf66;
|
||||
}
|
||||
|
||||
.terminal-mode[data-mode="mixed"] {
|
||||
background: rgba(255, 193, 7, 0.15);
|
||||
color: #ffc107;
|
||||
}
|
||||
|
||||
.terminal-session {
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: #4a9eff;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.terminal-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.btn-terminal-attach,
|
||||
.btn-terminal-mode,
|
||||
.btn-terminal-clear,
|
||||
.btn-terminal-close {
|
||||
padding: 6px 12px;
|
||||
background: #2a2a2a;
|
||||
border: 1px solid #444;
|
||||
border-radius: 4px;
|
||||
color: #e0e0e0;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-terminal-attach:hover,
|
||||
.btn-terminal-mode:hover,
|
||||
.btn-terminal-clear:hover,
|
||||
.btn-terminal-close:hover {
|
||||
background: #3a3a3a;
|
||||
border-color: #555;
|
||||
}
|
||||
|
||||
.btn-terminal-close:hover {
|
||||
background: #ff6b6b;
|
||||
border-color: #ff6b6b;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Terminal xterm.js container */
|
||||
.terminal-xterm {
|
||||
flex: 1;
|
||||
padding: 8px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.terminal-xterm .xterm {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.terminal-xterm .xterm:focus {
|
||||
outline: 2px solid #4a9eff;
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
.terminal-xterm .xterm .xterm-viewport {
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* Directory Picker Modal */
|
||||
.directory-picker-modal {
|
||||
max-width: 600px;
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
.directory-picker-modal .modal-body {
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.recent-directories,
|
||||
.custom-directory {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.recent-directories label,
|
||||
.custom-directory label {
|
||||
display: block;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #888;
|
||||
margin-bottom: 8px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.directory-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.directory-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px 12px;
|
||||
background: #1a1a1a;
|
||||
border: 1px solid #333;
|
||||
border-radius: 6px;
|
||||
color: #e0e0e0;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.directory-item:hover {
|
||||
background: #252525;
|
||||
border-color: #4a9eff;
|
||||
}
|
||||
|
||||
.directory-item.selected {
|
||||
background: #2a2a2a;
|
||||
border-color: #4a9eff;
|
||||
}
|
||||
|
||||
.dir-icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.dir-path {
|
||||
font-family: 'Monaco', 'Menlo', monospace;
|
||||
font-size: 12px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.directory-input {
|
||||
width: 100%;
|
||||
padding: 10px 12px;
|
||||
background: #1a1a1a;
|
||||
border: 1px solid #333;
|
||||
border-radius: 6px;
|
||||
color: #e0e0e0;
|
||||
font-family: 'Monaco', 'Menlo', monospace;
|
||||
font-size: 13px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.directory-input:focus {
|
||||
border-color: #4a9eff;
|
||||
}
|
||||
|
||||
/* Session Picker Modal */
|
||||
.session-picker-modal {
|
||||
max-width: 650px;
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
.session-picker-modal .modal-body {
|
||||
max-height: 450px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.session-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.session-list-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px 14px;
|
||||
background: #1a1a1a;
|
||||
border: 1px solid #333;
|
||||
border-radius: 8px;
|
||||
color: #e0e0e0;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.session-list-item:hover {
|
||||
background: #252525;
|
||||
border-color: #4a9eff;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 8px rgba(74, 158, 255, 0.15);
|
||||
}
|
||||
|
||||
.session-list-item.selected {
|
||||
background: #2a2a2a;
|
||||
border-color: #4a9eff;
|
||||
box-shadow: 0 0 0 2px rgba(74, 158, 255, 0.3);
|
||||
}
|
||||
|
||||
.session-list-item.create-new {
|
||||
background: linear-gradient(135deg, rgba(74, 158, 255, 0.1) 0%, rgba(74, 158, 255, 0.05) 100%);
|
||||
border-color: #4a9eff;
|
||||
}
|
||||
|
||||
.session-list-item.create-new:hover {
|
||||
background: linear-gradient(135deg, rgba(74, 158, 255, 0.15) 0%, rgba(74, 158, 255, 0.1) 100%);
|
||||
border-color: #5aaeff;
|
||||
}
|
||||
|
||||
.session-list-item.create-new.selected {
|
||||
background: linear-gradient(135deg, rgba(74, 158, 255, 0.2) 0%, rgba(74, 158, 255, 0.15) 100%);
|
||||
border-color: #4a9eff;
|
||||
box-shadow: 0 0 0 2px rgba(74, 158, 255, 0.4);
|
||||
}
|
||||
|
||||
.session-icon {
|
||||
font-size: 20px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.session-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.session-name {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
color: #e0e0e0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.session-detail {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
font-size: 12px;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.session-time {
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.session-id {
|
||||
font-family: 'Monaco', 'Menlo', monospace;
|
||||
color: #666;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
/* Status Badges */
|
||||
.status-badge {
|
||||
padding: 4px 10px;
|
||||
border-radius: 12px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.status-badge.active {
|
||||
background: rgba(81, 207, 102, 0.15);
|
||||
color: #51cf66;
|
||||
border: 1px solid rgba(81, 207, 102, 0.3);
|
||||
}
|
||||
|
||||
.status-badge.done {
|
||||
background: rgba(136, 136, 136, 0.15);
|
||||
color: #888;
|
||||
border: 1px solid rgba(136, 136, 136, 0.3);
|
||||
}
|
||||
|
||||
.status-badge.local {
|
||||
background: rgba(255, 136, 77, 0.15);
|
||||
color: #ff874d;
|
||||
border: 1px solid rgba(255, 136, 77, 0.3);
|
||||
}
|
||||
|
||||
/* Session Sections */
|
||||
.session-section {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.session-section:first-of-type {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
color: #888;
|
||||
margin-bottom: 8px;
|
||||
padding: 0 4px;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.no-sessions {
|
||||
text-align: center;
|
||||
padding: 40px 20px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.no-sessions p {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* Restore Toast */
|
||||
.restore-toast {
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.restore-content {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.restore-message {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.restore-message strong {
|
||||
display: block;
|
||||
margin-bottom: 4px;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
.restore-message br + span {
|
||||
font-size: 13px;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.restore-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.btn-restore-all,
|
||||
.btn-dismiss {
|
||||
padding: 6px 12px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-restore-all {
|
||||
background: #4a9eff;
|
||||
border: none;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-restore-all:hover {
|
||||
background: #3a8eef;
|
||||
}
|
||||
|
||||
.btn-dismiss {
|
||||
background: #2a2a2a;
|
||||
border: 1px solid #444;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
.btn-dismiss:hover {
|
||||
background: #3a3a3a;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.terminal-header {
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.terminal-header h2 {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.terminal-toolbar {
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.terminal-actions {
|
||||
width: 100%;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.terminal-path {
|
||||
max-width: 150px;
|
||||
}
|
||||
|
||||
.session-picker-modal {
|
||||
max-width: 95%;
|
||||
}
|
||||
|
||||
.session-list-item {
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.session-name {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.session-detail {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.terminal-tab {
|
||||
padding: 6px 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.tab-id {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.terminal-toolbar {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.terminal-path {
|
||||
max-width: 100px;
|
||||
}
|
||||
|
||||
.session-list-item {
|
||||
gap: 8px;
|
||||
padding: 8px 10px;
|
||||
}
|
||||
|
||||
.session-icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
font-size: 10px;
|
||||
padding: 3px 8px;
|
||||
}
|
||||
}
|
||||
1196
public/claude-ide/terminal.js
Normal file
1196
public/claude-ide/terminal.js
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user