- 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>
500 lines
16 KiB
JavaScript
500 lines
16 KiB
JavaScript
// ============================================
|
||
// 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; }
|
||
});
|
||
}
|