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:
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; }
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user