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:
uroma
2026-01-19 16:29:44 +00:00
Unverified
commit 0dd2083556
44 changed files with 18955 additions and 0 deletions

View 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;
}