diff --git a/TODO.md b/TODO.md index 24ce4a0..85ccf2f 100644 --- a/TODO.md +++ b/TODO.md @@ -13,7 +13,7 @@ - [x] Refactoring: Agent/Session-Einstellungen, die den System-Prompt bzw. das Tool Use bearbeiten - [ ] Cronjobs: Planung, ob cron von Debian/Linux verwendet werden soll, oder ein eigener Worker/Service in der App selbst, der das übernimmt - [x] Weiteres Modell hinzufügen: "Haiku 4.5" -- [ ] Weitere Tools: Glob? Search? Todo List? +- [x] Weitere Tools: Glob? Search? Todo List? - [ ] Settings in Datenbank speichern (users table) anstelle von localStorage - [ ] Einzelne Nachicht als Link teilen @@ -27,7 +27,7 @@ ## Frontend - [x] Chat: Es scrollt beim Laden nicht ganz nach unten, ich denke weil im DOM noch etwas nachgeladen wird (ich denke es ist die Schriftart), und die Höhe sich beim Laden nochmal verändert -- [x] Chat: User-Nachrichten mit dem Tag [Datei: pfad/zur/datei.xyz:1-10] wird nicht als HTML geparsed +- [x] Chat: User-Nachrichten mit dem Tag `[Datei: pfad/zur/datei.xyz:1-10]` wird nicht als HTML geparsed - [x] Chat: Wird zwar erkannt und ist klickbar, aber es öffnet sich einfach nur der File Explorer, ohne dass sich die Datei öffnet und die Darstellung in Monospace ist auch nicht schön - [x] Chat: Es werden momentan alle Tool-Calls angezeigt - die Anzeige soll individuell in den Einstellungen gemacht werden können. Auswahl aus drei Stufen: "Alle anzeigen" / "Nur Modifikationen" (file änderungen, memory änderungen, etc.) / "Verstecken" - [x] Browser: "Zurück"-Taste soll auf den vorherigen location.hash wechseln @@ -35,6 +35,7 @@ - [ ] Chat: Datum und Uhrzeit bei den Nachrichten anzeigen (Format: dd.MM.yyyy HH:mm:ss) - [x] Dashboard: Menu-Dropdown bei Cards sind abgeschnitten wenn die Card-Höhe nicht ausreicht (logisch, weil das Menu-Dropdown ein Child von der Card ist) - [ ] Auto-scrolling deaktivieren, wenn man weiterhoch gescrollt hat, und er wieder fortsetzen wenn man ganz nach unten gescrollt hat +- [ ] Anstelle von `[Bild: ...]`-Platzhalter direkt das gesendete Bild anzeigen ## Ideas / Backlog - [ ] Memory: Embedding-Modell für semantische Suche über Memory-Dateien (Vektor-basiert + BM25 Hybrid Search) diff --git a/wwwroot/css/chat.css b/wwwroot/css/chat.css index d66a812..33a402e 100644 --- a/wwwroot/css/chat.css +++ b/wwwroot/css/chat.css @@ -141,7 +141,7 @@ padding: 0; } -.tool-box:has([hidden]) { +.tool-box:has([hidden]):not(:has(.todo-list)) { opacity: var(--semi-transparent); } @@ -206,6 +206,77 @@ overflow: auto; } +.tool-result-markdown { + white-space: normal; +} + +.tool-result-markdown p, +.tool-result-markdown ul, +.tool-result-markdown ol { + margin: 0 0 .4em 0; +} + +.tool-result-markdown p:last-child, +.tool-result-markdown ul:last-child, +.tool-result-markdown ol:last-child { + margin-bottom: 0; +} + +.tool-result-markdown li { + margin-left: 1.2em; +} + +/* Todo-Liste */ + +.todo-list { + padding: var(--box-padding); + padding-top: 0; + display: flex; + flex-direction: column; + font-size: .85rem; + gap: var(--badge-spacing); +} + +.todo-item { + display: flex; + align-items: center; + gap: var(--badge-spacing); +} + +.todo-icon { + flex-shrink: 0; +} + +.todo-id { + flex-shrink: 0; +} + +.todo-prio { + flex-shrink: 0; + border-radius: var(--border-radius); + border: var(--border-width) solid currentcolor; + font-size: var(--font-size); + padding: var(--badge-padding); +} + +.todo-prio-high { color: var(--dangerous); } +.todo-prio-medium { color: var(--warning); } +.todo-prio-low { opacity: var(--semi-transparent); } + +.todo-text { + flex: 1; + word-break: break-word; +} + +.todo-done .todo-text { + text-decoration: line-through; + opacity: var(--semi-transparent); +} + +.todo-progress .todo-text { + font-style: italic; +} + .tool-write-meta { opacity: var(--semi-transparent); margin-left: auto; diff --git a/wwwroot/js/ui.js b/wwwroot/js/ui.js index 3ead21a..4b69380 100644 --- a/wwwroot/js/ui.js +++ b/wwwroot/js/ui.js @@ -422,6 +422,7 @@ function isChainMetaVisible(chainData) { function addToolBoxLoading(name) { const div = document.createElement('div'); div.className = 'tool-box streamed'; + div.dataset.toolName = name; const displayName = TOOL_DISPLAY_NAMES[name] || name; div.innerHTML = '' + esc(displayName) + ' '; chatEl().appendChild(div); @@ -485,6 +486,14 @@ function renderToolBoxContent(div, name, inputJson, result, isError) { div.appendChild(preview); foldTarget = preview; + } else if (name === 'todo_write') { + // Todo-Liste: kein tool-input, standardmäßig sichtbar, custom parser + if (result) { + const todoList = buildTodoList(result, isError); + div.appendChild(todoList); + foldTarget = todoList; + } + } else { // Standard: path/command als input anzeigen let inputLabel = ''; @@ -508,7 +517,7 @@ function renderToolBoxContent(div, name, inputJson, result, isError) { foldTarget = resultDiv; } - if (foldTarget) { + if (foldTarget && name !== 'todo_write') { header.style.cursor = 'pointer'; header.addEventListener('click', () => { foldTarget.hidden = !foldTarget.hidden; @@ -579,6 +588,63 @@ function buildWritePreview(content) { return container; } +function buildTodoList(result, isError) { + const container = document.createElement('div'); + container.className = 'todo-list' + (isError ? ' tool-error' : ''); + + const lines = result.split('\n').filter(l => l.trim()); + for (const line of lines) { + // Format: ICON [id] [priority] content + const m = line.match(/^(\S+)\s+\[([^\]]+)\]\s+(?:\[([^\]]+)\]\s+)?(.+)$/); + const row = document.createElement('div'); + row.className = 'todo-item'; + + if (m) { + const icon = m[1]; // ✅ / 🔄 / ⬜ + const id = m[2]; + const prio = m[3] || null; + const text = m[4]; + + const statusClass = + icon === '✅' ? 'todo-done' : + icon === '🔄' ? 'todo-progress' : + 'todo-pending'; + row.classList.add(statusClass); + + const iconSpan = document.createElement('span'); + iconSpan.className = 'todo-icon'; + iconSpan.textContent = icon; + + const idSpan = document.createElement('span'); + idSpan.className = 'todo-id'; + idSpan.textContent = id + '.'; + + const textSpan = document.createElement('span'); + textSpan.className = 'todo-text'; + textSpan.textContent = text; + + row.appendChild(iconSpan); + row.appendChild(idSpan); + row.appendChild(textSpan); + + if (prio) { + const prioSpan = document.createElement('span'); + prioSpan.className = 'todo-prio todo-prio-' + prio.toLowerCase(); + prioSpan.textContent = prio; + row.appendChild(prioSpan); + } + } else { + row.textContent = line; + } + + container.appendChild(row); + } + + parseFluentEmojis(container); + + return container; +} + function finishToolBox(div, data) { if (!div) return; renderToolBoxContent(div, data.name, data.input, data.result, data.isError); @@ -588,6 +654,7 @@ function finishToolBox(div, data) { function addToolBoxStatic(name, input, result, isError) { const div = document.createElement('div'); div.className = 'tool-box'; + div.dataset.toolName = name; renderToolBoxContent(div, name, input, result, isError); chatEl().appendChild(div); scrollDown(); @@ -602,6 +669,20 @@ function appendToolResult(result, isError) { // Wenn Diff vorhanden (edit_file/write_file) → kein Result anhängen if (lastBox.querySelector('.tool-diff')) return; + const toolName = lastBox.dataset.toolName || ''; + const isTodoWrite = toolName === 'todo_write'; + + if (isTodoWrite) { + // Vorhandene todo-list ersetzen oder neu erstellen + const existing = lastBox.querySelector('.todo-list'); + if (existing) existing.remove(); + if (result && !isError) { + const todoList = buildTodoList(result, isError); + lastBox.appendChild(todoList); + } + return; + } + const existing = lastBox.querySelector('.tool-result'); if (existing) { existing.textContent = result || '';