Folder-Browser für Workspace-Auswahl, Layout/Style Anpassungen
This commit is contained in:
1 parent
f4ad949db4
commit
aa2d9f1200
6 files changed
+179
-23
No files matched your search
@@ -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();
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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
@@ -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>
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
Reference in new issue
Block a user