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:
313
public/js/app.js
Normal file
313
public/js/app.js
Normal file
@@ -0,0 +1,313 @@
|
||||
// API Base URL
|
||||
const API_BASE = '/claude/api';
|
||||
|
||||
// State
|
||||
let currentFile = null;
|
||||
let isEditing = false;
|
||||
let fileTree = [];
|
||||
|
||||
// DOM Elements
|
||||
const loginScreen = document.getElementById('login-screen');
|
||||
const mainApp = document.getElementById('main-app');
|
||||
const loginForm = document.getElementById('login-form');
|
||||
const loginError = document.getElementById('login-error');
|
||||
const logoutBtn = document.getElementById('logout-btn');
|
||||
const searchInput = document.getElementById('search-input');
|
||||
const fileTreeEl = document.getElementById('file-tree');
|
||||
const recentFilesEl = document.getElementById('recent-files');
|
||||
const contentView = document.getElementById('content-view');
|
||||
const contentEditor = document.getElementById('content-editor');
|
||||
const breadcrumb = document.getElementById('breadcrumb');
|
||||
const editBtn = document.getElementById('edit-btn');
|
||||
const saveBtn = document.getElementById('save-btn');
|
||||
const cancelBtn = document.getElementById('cancel-btn');
|
||||
|
||||
// Initialize
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
checkAuth();
|
||||
setupEventListeners();
|
||||
});
|
||||
|
||||
function setupEventListeners() {
|
||||
// Login form
|
||||
loginForm.addEventListener('submit', handleLogin);
|
||||
|
||||
// Logout
|
||||
logoutBtn.addEventListener('click', handleLogout);
|
||||
|
||||
// Search
|
||||
let searchTimeout;
|
||||
searchInput.addEventListener('input', (e) => {
|
||||
clearTimeout(searchTimeout);
|
||||
searchTimeout = setTimeout(() => handleSearch(e.target.value), 300);
|
||||
});
|
||||
|
||||
// Edit/Save/Cancel buttons
|
||||
editBtn.addEventListener('click', startEditing);
|
||||
saveBtn.addEventListener('click', saveFile);
|
||||
cancelBtn.addEventListener('click', stopEditing);
|
||||
}
|
||||
|
||||
// Auth functions
|
||||
async function checkAuth() {
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/auth/status`);
|
||||
const data = await res.json();
|
||||
|
||||
if (data.authenticated) {
|
||||
// Redirect to landing page if authenticated
|
||||
window.location.href = '/claude/';
|
||||
} else {
|
||||
showLogin();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Auth check failed:', error);
|
||||
showLogin();
|
||||
}
|
||||
}
|
||||
|
||||
async function handleLogin(e) {
|
||||
e.preventDefault();
|
||||
const username = document.getElementById('username').value;
|
||||
const password = document.getElementById('password').value;
|
||||
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/login`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username, password })
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
if (data.success) {
|
||||
// Redirect to landing page after successful login
|
||||
window.location.href = '/claude/';
|
||||
} else {
|
||||
loginError.textContent = data.error || 'Login failed';
|
||||
}
|
||||
} catch (error) {
|
||||
loginError.textContent = 'Network error. Please try again.';
|
||||
}
|
||||
}
|
||||
|
||||
async function handleLogout() {
|
||||
try {
|
||||
await fetch(`${API_BASE}/logout`, { method: 'POST' });
|
||||
showLogin();
|
||||
} catch (error) {
|
||||
console.error('Logout failed:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function showLogin() {
|
||||
loginScreen.style.display = 'flex';
|
||||
mainApp.style.display = 'none';
|
||||
}
|
||||
|
||||
function showApp() {
|
||||
loginScreen.style.display = 'none';
|
||||
mainApp.style.display = 'flex';
|
||||
loadFileTree();
|
||||
loadRecentFiles();
|
||||
}
|
||||
|
||||
// File functions
|
||||
async function loadFileTree() {
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/files`);
|
||||
const data = await res.json();
|
||||
fileTree = data.tree;
|
||||
renderFileTree(fileTree, fileTreeEl);
|
||||
} catch (error) {
|
||||
console.error('Failed to load file tree:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function renderFileTree(tree, container, level = 0) {
|
||||
container.innerHTML = '';
|
||||
|
||||
tree.forEach(item => {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'tree-item ' + item.type;
|
||||
div.style.paddingLeft = (level * 1 + 0.75) + 'rem';
|
||||
|
||||
const icon = document.createElement('span');
|
||||
icon.className = 'tree-icon';
|
||||
icon.textContent = item.type === 'folder' ? '📁' : '📄';
|
||||
div.appendChild(icon);
|
||||
|
||||
const name = document.createElement('span');
|
||||
name.textContent = item.name;
|
||||
div.appendChild(name);
|
||||
|
||||
if (item.type === 'folder' && item.children) {
|
||||
const children = document.createElement('div');
|
||||
children.className = 'tree-children';
|
||||
children.style.display = 'none';
|
||||
|
||||
div.addEventListener('click', () => {
|
||||
children.style.display = children.style.display === 'none' ? 'block' : 'none';
|
||||
icon.textContent = children.style.display === 'none' ? '📁' : '📂';
|
||||
});
|
||||
|
||||
renderFileTree(item.children, children, level + 1);
|
||||
div.appendChild(children);
|
||||
} else if (item.type === 'file') {
|
||||
div.addEventListener('click', () => loadFile(item.path));
|
||||
}
|
||||
|
||||
container.appendChild(div);
|
||||
});
|
||||
}
|
||||
|
||||
async function loadRecentFiles() {
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/recent?limit=10`);
|
||||
const data = await res.json();
|
||||
|
||||
recentFilesEl.innerHTML = '';
|
||||
data.files.forEach(file => {
|
||||
const li = document.createElement('li');
|
||||
li.textContent = file.name;
|
||||
li.title = file.path;
|
||||
li.addEventListener('click', () => loadFile(file.path));
|
||||
recentFilesEl.appendChild(li);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to load recent files:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadFile(filePath) {
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/file/${encodeURIComponent(filePath)}`);
|
||||
const data = await res.json();
|
||||
|
||||
if (data.error) {
|
||||
console.error('Error loading file:', data.error);
|
||||
return;
|
||||
}
|
||||
|
||||
currentFile = data;
|
||||
breadcrumb.textContent = data.path;
|
||||
contentView.innerHTML = data.html;
|
||||
contentEditor.value = data.content;
|
||||
|
||||
editBtn.style.display = 'inline-block';
|
||||
stopEditing();
|
||||
} catch (error) {
|
||||
console.error('Failed to load file:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSearch(query) {
|
||||
if (!query.trim()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/search?q=${encodeURIComponent(query)}`);
|
||||
const data = await res.json();
|
||||
|
||||
// Show search results in file tree
|
||||
fileTreeEl.innerHTML = '';
|
||||
|
||||
if (data.results.length === 0) {
|
||||
fileTreeEl.innerHTML = '<div style="padding: 1rem; color: var(--text-secondary);">No results found</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
data.results.forEach(result => {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'tree-item file';
|
||||
|
||||
const name = document.createElement('div');
|
||||
name.textContent = result.name;
|
||||
name.style.fontWeight = '500';
|
||||
div.appendChild(name);
|
||||
|
||||
const preview = document.createElement('div');
|
||||
preview.textContent = result.preview;
|
||||
preview.style.fontSize = '0.8rem';
|
||||
preview.style.color = 'var(--text-secondary)';
|
||||
preview.style.marginTop = '0.25rem';
|
||||
div.appendChild(preview);
|
||||
|
||||
div.addEventListener('click', () => loadFile(result.path));
|
||||
fileTreeEl.appendChild(div);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Search failed:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Edit functions
|
||||
function startEditing() {
|
||||
if (!currentFile) return;
|
||||
|
||||
isEditing = true;
|
||||
contentView.style.display = 'none';
|
||||
contentEditor.style.display = 'block';
|
||||
editBtn.style.display = 'none';
|
||||
saveBtn.style.display = 'inline-block';
|
||||
cancelBtn.style.display = 'inline-block';
|
||||
contentEditor.focus();
|
||||
}
|
||||
|
||||
function stopEditing() {
|
||||
isEditing = false;
|
||||
contentView.style.display = 'block';
|
||||
contentEditor.style.display = 'none';
|
||||
editBtn.style.display = 'inline-block';
|
||||
saveBtn.style.display = 'none';
|
||||
cancelBtn.style.display = 'none';
|
||||
}
|
||||
|
||||
async function saveFile() {
|
||||
if (!currentFile || !isEditing) return;
|
||||
|
||||
const content = contentEditor.value;
|
||||
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/file/${encodeURIComponent(currentFile.path)}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ content })
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
if (data.success) {
|
||||
// Reload the file to show updated content
|
||||
await loadFile(currentFile.path);
|
||||
} else {
|
||||
alert('Failed to save: ' + (data.error || 'Unknown error'));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to save file:', error);
|
||||
alert('Failed to save file. Please try again.');
|
||||
}
|
||||
}
|
||||
|
||||
// Keyboard shortcuts
|
||||
document.addEventListener('keydown', (e) => {
|
||||
// Ctrl/Cmd + S to save
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === 's') {
|
||||
e.preventDefault();
|
||||
if (isEditing) {
|
||||
saveFile();
|
||||
}
|
||||
}
|
||||
|
||||
// Escape to cancel editing
|
||||
if (e.key === 'Escape' && isEditing) {
|
||||
stopEditing();
|
||||
}
|
||||
|
||||
// Ctrl/Cmd + E to edit
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === 'e' && !isEditing && currentFile) {
|
||||
e.preventDefault();
|
||||
startEditing();
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user