Folder-Browser für Workspace-Auswahl, Layout/Style Anpassungen

This commit is contained in:
Meleki committed 2026-02-27 09:43:58 +01:00
1 parent f4ad949db4
commit aa2d9f1200
6 files changed
+179 -23

No files matched your search

+36
View File
@@ -38,6 +38,7 @@ public static class WorkspaceEndpoint
{ {
app.MapGet("/api/workspace/tree", HandleTree); app.MapGet("/api/workspace/tree", HandleTree);
app.MapGet("/api/workspace/file", HandleFile); app.MapGet("/api/workspace/file", HandleFile);
app.MapGet("/api/workspace/browse", HandleBrowse);
} }
private static async Task HandleTree(HttpContext ctx) private static async Task HandleTree(HttpContext ctx)
@@ -157,6 +158,41 @@ public static class WorkspaceEndpoint
}); });
} }
private static async Task HandleBrowse(HttpContext ctx)
{
var path = ctx.Request.Query["path"].FirstOrDefault() ?? "/";
var targetPath = Path.GetFullPath(path);
if (!Directory.Exists(targetPath))
{
ctx.Response.StatusCode = 404;
await ctx.Response.WriteAsJsonAsync(new { error = "Verzeichnis nicht gefunden" });
return;
}
var entries = new List<object>();
// Parent
var parent = Directory.GetParent(targetPath);
if (parent != null)
{
entries.Add(new { name = "..", path = parent.FullName.Replace('\\', '/'), type = "dir" });
}
try
{
foreach (var dir in Directory.GetDirectories(targetPath).OrderBy(d => Path.GetFileName(d), StringComparer.OrdinalIgnoreCase))
{
var name = Path.GetFileName(dir);
if (IgnoredDirs.Contains(name)) continue;
entries.Add(new { name, path = dir.Replace('\\', '/'), type = "dir" });
}
}
catch (UnauthorizedAccessException) { }
await ctx.Response.WriteAsJsonAsync(new { current = targetPath.Replace('\\', '/'), entries });
}
private static bool IsLikelyTextFile(string name) private static bool IsLikelyTextFile(string name)
{ {
var lower = name.ToLowerInvariant(); var lower = name.ToLowerInvariant();
+4 -22
View File
@@ -46,16 +46,7 @@
} }
.fe-btn { .fe-btn {
background: var(--f3-bg);
border: var(--border-width) solid var(--f3-border);
padding: var(--s1) var(--s2); padding: var(--s1) var(--s2);
cursor: pointer;
color: var(--text);
font-family: monospace;
}
.fe-btn:hover:not(:disabled) {
border-color: var(--primary);
} }
.fe-btn:disabled { .fe-btn:disabled {
@@ -78,6 +69,10 @@
flex-shrink: 0; flex-shrink: 0;
} }
.fe-sidebar-full {
width: 100%;
}
.fe-main { .fe-main {
flex: 1; flex: 1;
overflow: hidden; overflow: hidden;
@@ -118,10 +113,6 @@
margin: auto; margin: auto;
} }
.fe-error {
color: #ff6b6b;
opacity: 1;
}
/* Preview */ /* Preview */
@@ -200,12 +191,3 @@
flex: 1; flex: 1;
} }
.fe-select-btn {
background: var(--primary);
border-color: var(--primary);
white-space: nowrap;
}
.fe-select-btn:hover {
opacity: 0.85;
}
+14
View File
@@ -32,6 +32,20 @@
object-fit: cover; object-fit: cover;
} }
.input-with-btn {
display: flex;
gap: var(--s2);
}
.input-with-btn input {
flex: 1;
}
.input-addon-btn {
flex-shrink: 0;
font-size: 1.1em;
}
.form-actions { .form-actions {
display: flex; display: flex;
gap: var(--s4); gap: var(--s4);
+4 -1
View File
@@ -69,7 +69,10 @@
<label>Kontext</label> <label>Kontext</label>
<textarea id="sessionContext" rows="4" placeholder="Zusätzlicher Kontext für den System-Prompt (optional)"></textarea> <textarea id="sessionContext" rows="4" placeholder="Zusätzlicher Kontext für den System-Prompt (optional)"></textarea>
<label>Workspace</label> <label>Workspace</label>
<input type="text" id="sessionWorkspace" placeholder="Absoluter Pfad zum Arbeitsverzeichnis (optional)"> <div class="input-with-btn">
<input type="text" id="sessionWorkspace" placeholder="Absoluter Pfad zum Arbeitsverzeichnis (optional)">
<button type="button" id="sessionWorkspaceBrowse" class="input-addon-btn">📂</button>
</div>
<div class="form-actions"> <div class="form-actions">
<button id="sessionSaveBtn">Speichern</button> <button id="sessionSaveBtn">Speichern</button>
<button id="sessionCancelBtn">Abbrechen</button> <button id="sessionCancelBtn">Abbrechen</button>
+108
View File
@@ -258,6 +258,114 @@ function feDoSelect() {
closeFileExplorer(); closeFileExplorer();
} }
// ── Folder Browser Mode ──
// Nutzt /api/workspace/browse mit absolutem Pfad, zum Ordner-Auswählen.
function openFolderBrowser(opts = {}) {
if (_feState) closeFileExplorer();
_feState = {
mode: 'folder',
onSelect: opts.onSelect || null,
currentAbsPath: opts.startPath || '/',
history: []
};
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 fe-sidebar-full" id="feSidebar">
<div class="fe-loading">Laden...</div>
</div>
</div>
<div class="fe-footer" id="feFooter">
<span class="fe-info" id="feInfo"></span>
<button class="fe-btn fe-select-btn" id="feSelectBtn">Diesen Ordner wählen</button>
</div>
`;
overlay.appendChild(modal);
document.body.appendChild(overlay);
document.getElementById('feBack').addEventListener('click', fbGoBack);
document.getElementById('feClose').addEventListener('click', closeFileExplorer);
document.getElementById('feSelectBtn').addEventListener('click', () => {
if (_feState && _feState.onSelect) _feState.onSelect(_feState.currentAbsPath);
closeFileExplorer();
});
_feState._keyHandler = (e) => { if (e.key === 'Escape') closeFileExplorer(); };
document.addEventListener('keydown', _feState._keyHandler);
fbLoadDir(opts.startPath || '/');
}
async function fbLoadDir(absPath) {
if (!_feState) return;
const pathEl = document.getElementById('fePath');
const sidebar = document.getElementById('feSidebar');
const info = document.getElementById('feInfo');
pathEl.textContent = absPath;
info.textContent = absPath;
sidebar.innerHTML = '<div class="fe-loading">Laden...</div>';
try {
const resp = await fetch(`/api/workspace/browse?path=${encodeURIComponent(absPath)}`);
if (!resp.ok) {
const err = await resp.json().catch(() => ({}));
sidebar.innerHTML = `<div class="fe-error">${err.error || 'Fehler'}</div>`;
return;
}
const data = await resp.json();
_feState.currentAbsPath = data.current;
pathEl.textContent = data.current;
info.textContent = data.current;
sidebar.innerHTML = '';
for (const entry of data.entries) {
const item = document.createElement('div');
item.className = 'fe-item fe-dir';
item.textContent = '📁 ' + entry.name;
item.addEventListener('click', () => {
_feState.history.push(_feState.currentAbsPath);
fbLoadDir(entry.path);
});
sidebar.appendChild(item);
}
if (data.entries.length === 0) {
sidebar.innerHTML = '<div class="fe-empty">Leer</div>';
}
document.getElementById('feBack').disabled = _feState.history.length === 0;
} catch (e) {
sidebar.innerHTML = '<div class="fe-error">Netzwerk-Fehler</div>';
}
}
function fbGoBack() {
if (!_feState || _feState.history.length === 0) return;
const prev = _feState.history.pop();
fbLoadDir(prev);
}
// ── Helpers ── // ── Helpers ──
function getFileIcon(ext) { function getFileIcon(ext) {
+13
View File
@@ -79,6 +79,19 @@ registerPage('session', {
imageInput.value = ''; imageInput.value = '';
}); });
// Workspace Browse
const workspaceBrowse = document.getElementById('sessionWorkspaceBrowse');
if (workspaceBrowse) {
workspaceBrowse.addEventListener('click', () => {
openFolderBrowser({
startPath: workspaceEl.value.trim() || '/',
onSelect(path) {
workspaceEl.value = path;
}
});
});
}
// Speichern // Speichern
saveBtn.addEventListener('click', async () => { saveBtn.addEventListener('click', async () => {
const data = { const data = {