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