File Explorer Modal: Workspace-Browser mit Textvorschau, Zeilennummern, Selektion als File-Picker im Chat

This commit is contained in:
Meleki committed 2026-02-27 08:57:35 +01:00
1 parent 663f2c2a9a
commit 491d3b916e
6 files changed
+682

No files matched your search

+282
View File
@@ -0,0 +1,282 @@
// ── File Explorer Modal ──
// Wiederverwendbares Modal zum Durchsuchen des Workspaces.
// Nutzung: openFileExplorer({ sessionId, onSelect(path, from, to) })
let _feState = null;
function openFileExplorer(opts = {}) {
if (_feState) closeFileExplorer();
_feState = {
sessionId: opts.sessionId || getCurrentSessionId(),
onSelect: opts.onSelect || null,
currentPath: '',
history: [],
selectedFile: null
};
const overlay = document.createElement('div');
overlay.id = 'feOverlay';
overlay.className = 'fe-overlay';
overlay.addEventListener('click', (e) => { if (e.target === overlay) closeFileExplorer(); });
const modal = document.createElement('div');
modal.className = 'fe-modal';
modal.innerHTML = `
<div class="fe-header">
<div class="fe-nav">
<button class="fe-btn" id="feBack" title="Zurück" disabled>←</button>
<span class="fe-path" id="fePath">/</span>
</div>
<button class="fe-btn" id="feClose">✕</button>
</div>
<div class="fe-body">
<div class="fe-sidebar" id="feSidebar">
<div class="fe-loading">Laden...</div>
</div>
<div class="fe-main" id="feMain">
<div class="fe-placeholder">Datei auswählen</div>
</div>
</div>
<div class="fe-footer" id="feFooter" hidden>
<span class="fe-info" id="feInfo"></span>
<button class="fe-btn fe-select-btn" id="feSelectBtn">Einfügen</button>
</div>
`;
overlay.appendChild(modal);
document.body.appendChild(overlay);
document.getElementById('feBack').addEventListener('click', feGoBack);
document.getElementById('feClose').addEventListener('click', closeFileExplorer);
document.getElementById('feSelectBtn').addEventListener('click', feDoSelect);
// ESC
_feState._keyHandler = (e) => { if (e.key === 'Escape') closeFileExplorer(); };
document.addEventListener('keydown', _feState._keyHandler);
feLoadDir('');
}
function closeFileExplorer() {
const overlay = document.getElementById('feOverlay');
if (overlay) overlay.remove();
if (_feState && _feState._keyHandler) {
document.removeEventListener('keydown', _feState._keyHandler);
}
_feState = null;
}
async function feLoadDir(path) {
if (!_feState) return;
_feState.currentPath = path;
_feState.selectedFile = null;
const pathEl = document.getElementById('fePath');
pathEl.textContent = '/' + (path || '');
const backBtn = document.getElementById('feBack');
backBtn.disabled = _feState.history.length === 0 && path === '';
const sidebar = document.getElementById('feSidebar');
sidebar.innerHTML = '<div class="fe-loading">Laden...</div>';
// Preview zurücksetzen
const main = document.getElementById('feMain');
main.innerHTML = '<div class="fe-placeholder">Datei auswählen</div>';
document.getElementById('feFooter').hidden = true;
try {
const resp = await fetch(`/api/workspace/tree?sessionId=${_feState.sessionId}&path=${encodeURIComponent(path)}`);
if (!resp.ok) {
const err = await resp.json().catch(() => ({}));
sidebar.innerHTML = `<div class="fe-error">${err.error || 'Fehler'}</div>`;
return;
}
const entries = await resp.json();
sidebar.innerHTML = '';
if (path) {
const upItem = document.createElement('div');
upItem.className = 'fe-item fe-dir';
upItem.textContent = '📁 ..';
upItem.addEventListener('click', () => {
_feState.history.push(path);
const parent = path.includes('/') ? path.substring(0, path.lastIndexOf('/')) : '';
feLoadDir(parent);
});
sidebar.appendChild(upItem);
}
for (const entry of entries) {
const item = document.createElement('div');
item.className = 'fe-item';
if (entry.type === 'dir') {
item.classList.add('fe-dir');
item.textContent = '📁 ' + entry.name;
item.addEventListener('click', () => {
_feState.history.push(path);
feLoadDir(entry.path);
});
} else {
item.classList.add('fe-file');
const icon = entry.isText ? '📄' : getFileIcon(entry.ext);
item.textContent = icon + ' ' + entry.name;
item.dataset.path = entry.path;
item.dataset.isText = entry.isText;
item.addEventListener('click', () => feSelectFile(entry, item));
}
sidebar.appendChild(item);
}
if (entries.length === 0) {
sidebar.innerHTML = '<div class="fe-empty">Leer</div>';
}
backBtn.disabled = path === '';
} catch (e) {
sidebar.innerHTML = '<div class="fe-error">Netzwerk-Fehler</div>';
}
}
function feGoBack() {
if (!_feState || _feState.history.length === 0) {
if (_feState && _feState.currentPath) feLoadDir('');
return;
}
const prev = _feState.history.pop();
feLoadDir(prev);
}
async function feSelectFile(entry, itemEl) {
if (!_feState) return;
// Active-State setzen
document.querySelectorAll('.fe-item.active').forEach(el => el.classList.remove('active'));
itemEl.classList.add('active');
const main = document.getElementById('feMain');
const footer = document.getElementById('feFooter');
const info = document.getElementById('feInfo');
if (!entry.isText) {
main.innerHTML = `<div class="fe-placeholder">${getFileIcon(entry.ext)} ${entry.name}<br><small>${formatSize(entry.size)} - Vorschau nicht verfügbar</small></div>`;
footer.hidden = true;
_feState.selectedFile = null;
return;
}
main.innerHTML = '<div class="fe-loading">Laden...</div>';
try {
const resp = await fetch(`/api/workspace/file?sessionId=${_feState.sessionId}&path=${encodeURIComponent(entry.path)}`);
if (!resp.ok) {
main.innerHTML = '<div class="fe-error">Fehler beim Laden</div>';
return;
}
const data = await resp.json();
_feState.selectedFile = data;
// Textarea mit Zeilennummern
main.innerHTML = `
<div class="fe-preview">
<div class="fe-line-numbers" id="feLineNums"></div>
<textarea class="fe-textarea" id="feTextarea" readonly spellcheck="false">${escHtml(data.content)}</textarea>
</div>
`;
const textarea = document.getElementById('feTextarea');
const lineNums = document.getElementById('feLineNums');
// Zeilennummern generieren
renderLineNumbers(data.content, lineNums);
// Sync scroll
textarea.addEventListener('scroll', () => {
lineNums.scrollTop = textarea.scrollTop;
});
// Selection-Tracking für Zeilen-Info
textarea.addEventListener('mouseup', () => updateSelectionInfo());
textarea.addEventListener('keyup', () => updateSelectionInfo());
info.textContent = `${data.path} - ${data.lineCount} Zeilen, ${formatSize(data.size)}`;
footer.hidden = false;
} catch (e) {
main.innerHTML = '<div class="fe-error">Netzwerk-Fehler</div>';
}
}
function renderLineNumbers(content, container) {
const lines = content.split('\n');
container.innerHTML = lines.map((_, i) => `<div>${i + 1}</div>`).join('');
}
function updateSelectionInfo() {
if (!_feState || !_feState.selectedFile) return;
const textarea = document.getElementById('feTextarea');
const info = document.getElementById('feInfo');
const data = _feState.selectedFile;
if (textarea.selectionStart !== textarea.selectionEnd) {
const from = getLineNumber(textarea.value, textarea.selectionStart);
const to = getLineNumber(textarea.value, textarea.selectionEnd);
info.textContent = `${data.path}:${from}-${to} (${to - from + 1} Zeilen ausgewählt)`;
} else {
info.textContent = `${data.path} - ${data.lineCount} Zeilen, ${formatSize(data.size)}`;
}
}
function getLineNumber(text, pos) {
return text.substring(0, pos).split('\n').length;
}
function feDoSelect() {
if (!_feState || !_feState.selectedFile) return;
const data = _feState.selectedFile;
const textarea = document.getElementById('feTextarea');
let ref = data.path;
if (textarea && textarea.selectionStart !== textarea.selectionEnd) {
const from = getLineNumber(textarea.value, textarea.selectionStart);
const to = getLineNumber(textarea.value, textarea.selectionEnd);
ref = `${data.path}:${from}-${to}`;
}
if (_feState.onSelect) {
_feState.onSelect(ref);
}
closeFileExplorer();
}
// ── Helpers ──
function getFileIcon(ext) {
const icons = {
'.png': '🖼️', '.jpg': '🖼️', '.jpeg': '🖼️', '.gif': '🖼️', '.webp': '🖼️', '.svg': '🖼️',
'.pdf': '📑', '.zip': '📦', '.gz': '📦', '.tar': '📦',
'.mp3': '🎵', '.wav': '🎵', '.mp4': '🎬', '.avi': '🎬',
'.exe': '⚙️', '.dll': '⚙️', '.so': '⚙️',
'.db': '🗃️', '.sqlite': '🗃️'
};
return icons[ext?.toLowerCase()] || '📄';
}
function formatSize(bytes) {
if (bytes < 1024) return bytes + ' B';
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
}
function escHtml(s) {
return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
+18
View File
@@ -43,6 +43,24 @@ registerPage('chat', {
});
}
// Workspace Explorer
const explorerBtn = document.getElementById('explorerBtn');
if (explorerBtn) {
explorerBtn.addEventListener('click', () => {
openFileExplorer({
sessionId,
onSelect(ref) {
const input = document.getElementById('input');
if (!input) return;
const tag = `[Datei: ${ref}]`;
const pos = input.selectionStart || input.value.length;
input.value = input.value.substring(0, pos) + tag + input.value.substring(pos);
input.focus();
}
});
});
}
// File Upload
const fileBtn = document.getElementById('fileBtn');
const fileInput = document.getElementById('fileInput');