Feature: Claude Assistent Chat in TaskMate

Neuer Tab "Assistent" mit interaktiver Claude Code Session:
- Chat-UI mit Session-Verwaltung (History, neue/alte Sessions)
- Claude CLI als Child-Process auf dem Host (interaktiv, mit Rueckfragen)
- Streaming-Output per Socket.io
- Nur fuer autorisierte User (Hendrik, Monami)
- 30 Min Inaktivitaets-Timeout
- Task-Uebergabe: Button im Task-Modal sendet Aufgabe an Assistenten
- Chat-Verlauf wird in DB gespeichert

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Dieser Commit ist enthalten in:
Server Deploy
2026-03-19 22:04:49 +01:00
Ursprung 71f59b276b
Commit c4304a4f88
11 geänderte Dateien mit 1574 neuen und 5 gelöschten Zeilen

434
frontend/css/assistant.css Normale Datei
Datei anzeigen

@@ -0,0 +1,434 @@
/**
* TASKMATE - Assistant View Styles
* =================================
* Claude Assistant Chat Interface
*/
/* Layout */
.assistant-layout {
display: grid;
grid-template-columns: 280px 1fr;
height: calc(100vh - 120px);
overflow: hidden;
}
/* ===================== */
/* SIDEBAR */
/* ===================== */
.assistant-sidebar {
background: var(--bg-card);
border-right: 1px solid var(--border-default);
display: flex;
flex-direction: column;
overflow: hidden;
}
.assistant-sidebar-header {
padding: 16px;
border-bottom: 1px solid var(--border-light);
}
.assistant-sidebar-header .btn-block {
width: 100%;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.assistant-sessions-list {
flex: 1;
overflow-y: auto;
padding: 8px;
}
.assistant-session-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 12px;
border-radius: 8px;
cursor: pointer;
transition: background 0.15s;
margin-bottom: 2px;
}
.assistant-session-item:hover {
background: var(--bg-main);
}
.assistant-session-item.active {
background: var(--primary-light);
border-left: 3px solid var(--primary);
}
.assistant-session-info {
flex: 1;
min-width: 0;
}
.assistant-session-title {
font-size: var(--text-sm);
font-weight: 500;
color: var(--text-primary);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.assistant-session-meta {
font-size: var(--text-xs);
color: var(--text-muted);
margin-top: 2px;
}
.assistant-session-delete {
background: none;
border: none;
cursor: pointer;
color: var(--text-muted);
padding: 4px;
border-radius: 4px;
opacity: 0;
transition: opacity 0.15s, color 0.15s;
flex-shrink: 0;
}
.assistant-session-item:hover .assistant-session-delete {
opacity: 1;
}
.assistant-session-delete:hover {
color: var(--danger);
}
/* ===================== */
/* CHAT AREA */
/* ===================== */
.assistant-chat {
display: flex;
flex-direction: column;
overflow: hidden;
position: relative;
background: var(--bg-main);
}
.assistant-chat-header {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 20px;
background: var(--bg-card);
border-bottom: 1px solid var(--border-default);
flex-shrink: 0;
}
.assistant-chat-header h3 {
margin: 0;
font-size: var(--text-base);
font-weight: 600;
color: var(--text-primary);
}
/* Status Badges */
.assistant-status-badge {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: var(--text-xs);
font-weight: 500;
padding: 2px 10px;
border-radius: 12px;
}
.assistant-status-badge:empty {
display: none;
}
.assistant-status-badge::before {
content: '';
width: 6px;
height: 6px;
border-radius: 50%;
}
.assistant-status-badge.status-running {
background: rgba(34, 197, 94, 0.1);
color: #16a34a;
}
.assistant-status-badge.status-running::before {
background: #22c55e;
}
.assistant-status-badge.status-thinking {
background: rgba(245, 158, 11, 0.1);
color: #d97706;
}
.assistant-status-badge.status-thinking::before {
background: #f59e0b;
animation: pulse-dot 1.5s infinite;
}
.assistant-status-badge.status-ended,
.assistant-status-badge.status-stopped {
background: rgba(100, 116, 139, 0.1);
color: var(--text-secondary);
}
.assistant-status-badge.status-ended::before,
.assistant-status-badge.status-stopped::before {
background: #94a3b8;
}
.assistant-status-badge.status-error {
background: rgba(239, 68, 68, 0.1);
color: #dc2626;
}
.assistant-status-badge.status-error::before {
background: #ef4444;
}
@keyframes pulse-dot {
0%, 100% { opacity: 1; }
50% { opacity: 0.3; }
}
/* Messages Area */
.assistant-messages {
flex: 1;
overflow-y: auto;
padding: 20px;
display: flex;
flex-direction: column;
gap: 12px;
}
/* Message Bubbles */
.assistant-message {
max-width: 80%;
padding: 12px 16px;
border-radius: 12px;
font-size: var(--text-sm);
line-height: 1.6;
word-wrap: break-word;
}
.assistant-message.user {
align-self: flex-end;
background: var(--primary);
color: var(--text-inverse);
border-bottom-right-radius: 4px;
}
.assistant-message.assistant {
align-self: flex-start;
background: var(--bg-card);
color: var(--text-primary);
border: 1px solid var(--border-default);
border-bottom-left-radius: 4px;
}
/* Markdown in assistant messages */
.assistant-message.assistant code {
background: rgba(0, 0, 0, 0.06);
padding: 2px 6px;
border-radius: 4px;
font-size: 0.85em;
font-family: 'Courier New', monospace;
}
.assistant-message.assistant pre {
background: #1e293b;
color: #e2e8f0;
padding: 12px 16px;
border-radius: 8px;
overflow-x: auto;
margin: 8px 0;
}
.assistant-message.assistant pre code {
background: none;
padding: 0;
color: inherit;
font-size: 0.85em;
}
.assistant-message.assistant ul,
.assistant-message.assistant ol {
margin: 4px 0;
padding-left: 20px;
}
.assistant-message.assistant li {
margin-bottom: 2px;
}
.assistant-message.assistant h1,
.assistant-message.assistant h2,
.assistant-message.assistant h3,
.assistant-message.assistant h4 {
margin: 8px 0 4px;
font-weight: 600;
}
.assistant-message.assistant h1 { font-size: 1.2em; }
.assistant-message.assistant h2 { font-size: 1.1em; }
.assistant-message.assistant h3 { font-size: 1.05em; }
.assistant-message.assistant a {
color: var(--primary);
text-decoration: underline;
}
.assistant-message.assistant blockquote {
border-left: 3px solid var(--border-default);
margin: 8px 0;
padding: 4px 12px;
color: var(--text-secondary);
}
.assistant-message .message-time {
font-size: var(--text-xs);
opacity: 0.6;
margin-top: 4px;
display: block;
}
/* Streaming cursor */
.assistant-message.streaming::after {
content: '';
display: inline-block;
width: 8px;
height: 16px;
background: var(--text-secondary);
margin-left: 2px;
vertical-align: text-bottom;
animation: blink-cursor 0.8s infinite;
}
@keyframes blink-cursor {
0%, 100% { opacity: 1; }
50% { opacity: 0; }
}
/* Input Bar */
.assistant-input-bar {
display: flex;
align-items: flex-end;
gap: 8px;
padding: 12px 20px;
background: var(--bg-card);
border-top: 1px solid var(--border-default);
flex-shrink: 0;
}
.assistant-input {
flex: 1;
border: 1px solid var(--border-default);
border-radius: 12px;
padding: 10px 16px;
font-size: var(--text-sm);
font-family: 'Poppins', sans-serif;
resize: none;
max-height: 150px;
line-height: 1.5;
background: var(--bg-main);
color: var(--text-primary);
transition: border-color 0.15s;
}
.assistant-input:focus {
outline: none;
border-color: var(--primary);
}
.assistant-input::placeholder {
color: var(--text-placeholder);
}
.assistant-send-btn {
width: 40px;
height: 40px;
min-width: 40px;
padding: 0;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
flex-shrink: 0;
}
/* Empty State */
.assistant-empty {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
text-align: center;
color: var(--text-muted);
pointer-events: none;
}
.assistant-empty-icon {
margin-bottom: 16px;
opacity: 0.3;
}
.assistant-empty h3 {
margin: 0 0 8px;
font-size: var(--text-lg);
color: var(--text-secondary);
}
.assistant-empty p {
margin: 0;
font-size: var(--text-sm);
}
/* Hide empty state when messages present */
.assistant-messages:not(:empty) ~ .assistant-empty {
display: none;
}
/* Hide input bar when no session */
.assistant-chat.no-session .assistant-input-bar {
display: none;
}
/* ===================== */
/* RESPONSIVE */
/* ===================== */
@media (max-width: 768px) {
.assistant-layout {
grid-template-columns: 1fr;
height: calc(100vh - 60px);
}
.assistant-sidebar {
display: none;
}
.assistant-sidebar.mobile-visible {
display: flex;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 10;
}
.assistant-message {
max-width: 90%;
}
.assistant-input-bar {
padding: 8px 12px;
}
}

Datei anzeigen

@@ -38,6 +38,7 @@
<link rel="stylesheet" href="css/knowledge.css">
<link rel="stylesheet" href="css/reminders.css">
<link rel="stylesheet" href="css/contacts.css">
<link rel="stylesheet" href="css/assistant.css">
<link rel="stylesheet" href="css/responsive.css">
<link rel="stylesheet" href="css/mobile.css">
<link rel="stylesheet" href="css/pwa.css">
@@ -310,6 +311,12 @@
</svg>
Kontakte
</button>
<button class="view-tab" data-view="assistant">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>
</svg>
Assistent
</button>
</nav>
<div class="filter-bar-actions">
<button id="btn-filter-toggle" class="btn btn-outline filter-toggle-btn">
@@ -687,6 +694,48 @@
</div>
<!-- Assistant View -->
<div id="view-assistant" class="view view-assistant hidden">
<div class="assistant-layout">
<aside class="assistant-sidebar">
<div class="assistant-sidebar-header">
<button id="btn-new-session" class="btn btn-primary btn-block">
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 5v14M5 12h14"/>
</svg>
Neue Session
</button>
</div>
<div id="assistant-sessions-list" class="assistant-sessions-list"></div>
</aside>
<main class="assistant-chat">
<div class="assistant-chat-header">
<h3 id="assistant-chat-title">Assistent</h3>
<span id="assistant-status-badge" class="assistant-status-badge"></span>
</div>
<div id="assistant-messages" class="assistant-messages"></div>
<div class="assistant-input-bar">
<textarea id="assistant-input" class="assistant-input" placeholder="Nachricht eingeben..." rows="1"></textarea>
<button id="btn-send-message" class="btn btn-primary assistant-send-btn" title="Senden">
<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="22" y1="2" x2="11" y2="13"/>
<polygon points="22 2 15 22 11 13 2 9 22 2"/>
</svg>
</button>
</div>
<div id="assistant-empty" class="assistant-empty">
<div class="assistant-empty-icon">
<svg viewBox="0 0 24 24" width="64" height="64" fill="none" stroke="currentColor" stroke-width="1.5">
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>
</svg>
</div>
<h3>Claude Assistent</h3>
<p>Starte eine neue Session oder waehle eine bestehende aus.</p>
</div>
</main>
</div>
</div>
<!-- Contacts View -->
<div id="view-contacts" class="view view-contacts hidden">
<div class="view-wrapper">
@@ -935,6 +984,10 @@
</div>
<div class="modal-footer">
<div class="modal-footer-left">
<button type="button" id="btn-task-to-assistant" class="btn btn-text hidden">
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
An Assistent
</button>
<button type="button" id="btn-duplicate-task" class="btn btn-text hidden">Duplizieren</button>
<button type="button" id="btn-archive-task" class="btn btn-text hidden">Archivieren</button>
<button type="button" id="btn-restore-task" class="btn btn-text hidden">Wiederherstellen</button>
@@ -1973,6 +2026,10 @@
<svg viewBox="0 0 24 24" width="20" height="20"><path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2" stroke="currentColor" stroke-width="2" fill="none"/><circle cx="8.5" cy="7" r="4" stroke="currentColor" stroke-width="2" fill="none"/><line x1="20" y1="8" x2="20" y2="14" stroke="currentColor" stroke-width="2"/><line x1="23" y1="11" x2="17" y2="11" stroke="currentColor" stroke-width="2"/></svg>
<span>Kontakte</span>
</button>
<button class="mobile-nav-item" data-view="assistant">
<svg viewBox="0 0 24 24" width="20" height="20"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" stroke="currentColor" stroke-width="2" fill="none"/></svg>
<span>Assistent</span>
</button>
</div>
</div>

Datei anzeigen

@@ -1350,6 +1350,26 @@ class ApiClient {
async getContactTags() {
return this.get('/contacts/tags/all');
}
// =============================================================================
// ASSISTANT
// =============================================================================
async getAssistantSessions() {
return this.get('/assistant/sessions');
}
async getAssistantMessages(sessionId) {
return this.get(`/assistant/sessions/${sessionId}/messages`);
}
async createAssistantSession(data) {
return this.post('/assistant/sessions', data);
}
async deleteAssistantSession(id) {
return this.delete(`/assistant/sessions/${id}`);
}
}
// Custom API Error Class

Datei anzeigen

@@ -25,6 +25,7 @@ import codingManager from './coding.js';
import mobileManager from './mobile.js';
import reminderManager from './reminders.js';
import pwaManager from './pwa.js';
import assistantManager from './assistant.js';
import { $, $$, debounce, getFromStorage, setToStorage } from './utils.js';
class App {
@@ -90,6 +91,9 @@ class App {
// Initialize knowledge manager
await knowledgeManager.init();
// Initialize assistant manager
await assistantManager.init();
// Initialize mobile features
mobileManager.init();
@@ -708,6 +712,13 @@ class App {
knowledgeManager.hide();
}
// Show/hide assistant manager
if (view === 'assistant') {
assistantManager.show();
} else {
assistantManager.hide();
}
// Initialize contacts view when switching to it
if (view === 'contacts') {
window.initContactsPromise = window.initContactsPromise || import('./contacts.js').then(module => {

455
frontend/js/assistant.js Normale Datei
Datei anzeigen

@@ -0,0 +1,455 @@
/**
* TASKMATE - Assistant Manager
* ============================
* Claude Assistant Chat Interface
*/
import api from './api.js';
import syncManager from './sync.js';
import { $ } from './utils.js';
class AssistantManager {
constructor() {
this.sessions = [];
this.currentSessionId = null;
this.sessionStatus = null; // running, ended, stopped, error
this.streamingMessageEl = null;
this.streamingContent = '';
this.initialized = false;
}
async init() {
if (this.initialized) return;
// DOM Elements
this.sessionsListEl = $('#assistant-sessions-list');
this.messagesEl = $('#assistant-messages');
this.inputEl = $('#assistant-input');
this.sendBtn = $('#btn-send-message');
this.newSessionBtn = $('#btn-new-session');
this.chatTitleEl = $('#assistant-chat-title');
this.statusBadgeEl = $('#assistant-status-badge');
this.chatEl = document.querySelector('.assistant-chat');
this.emptyEl = $('#assistant-empty');
this.bindEvents();
this.setupSocketListeners();
this.updateChatState();
this.initialized = true;
console.log('[Assistant] Initialized');
}
bindEvents() {
// New session
this.newSessionBtn?.addEventListener('click', () => this.startSession());
// Send message
this.sendBtn?.addEventListener('click', () => this.sendMessage());
// Textarea: Enter sends, Shift+Enter new line, auto-resize
this.inputEl?.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
this.sendMessage();
}
});
this.inputEl?.addEventListener('input', () => {
this.autoResizeInput();
});
}
setupSocketListeners() {
const socket = syncManager.socket;
if (!socket) {
// Socket not yet connected, wait for it
const checkSocket = setInterval(() => {
if (syncManager.socket) {
clearInterval(checkSocket);
this._attachSocketEvents(syncManager.socket);
}
}, 500);
return;
}
this._attachSocketEvents(socket);
}
_attachSocketEvents(socket) {
// Streaming output
socket.on('assistant:output', (data) => {
if (data.sessionId !== this.currentSessionId) return;
this.handleOutput(data.content);
});
// Status changes
socket.on('assistant:status', (data) => {
if (data.sessionId && data.sessionId !== this.currentSessionId) return;
if (data.status === 'error') {
this.setStatus('error');
this.showToast(data.error || 'Fehler beim Assistenten', 'error');
return;
}
this.setStatus(data.status);
// If session ended, finalize streaming
if (data.status === 'ended' || data.status === 'stopped') {
this.finalizeStreaming();
this.loadSessions();
}
});
}
// =====================
// SHOW / HIDE
// =====================
async show() {
await this.loadSessions();
}
hide() {
// Nothing to clean up
}
// =====================
// SESSIONS
// =====================
async loadSessions() {
try {
this.sessions = await api.getAssistantSessions();
this.renderSessionsList();
} catch (err) {
console.error('[Assistant] Fehler beim Laden der Sessions:', err);
}
}
renderSessionsList() {
if (!this.sessionsListEl) return;
if (this.sessions.length === 0) {
this.sessionsListEl.innerHTML = '<div style="padding: 16px; text-align: center; color: var(--text-muted); font-size: var(--text-sm);">Keine Sessions vorhanden</div>';
return;
}
this.sessionsListEl.innerHTML = this.sessions.map(s => {
const isActive = s.id === this.currentSessionId;
const date = new Date(s.created_at);
const dateStr = `${String(date.getDate()).padStart(2, '0')}.${String(date.getMonth() + 1).padStart(2, '0')}.${date.getFullYear()}`;
const statusLabel = s.status === 'active' ? 'Aktiv' : 'Beendet';
return `
<div class="assistant-session-item ${isActive ? 'active' : ''}" data-session-id="${s.id}">
<div class="assistant-session-info">
<div class="assistant-session-title">${this.escapeHtml(s.title)}</div>
<div class="assistant-session-meta">${dateStr} - ${statusLabel}</div>
</div>
<button class="assistant-session-delete" data-delete-id="${s.id}" title="Session loeschen">
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 6L6 18M6 6l12 12"/></svg>
</button>
</div>
`;
}).join('');
// Click handlers
this.sessionsListEl.querySelectorAll('.assistant-session-item').forEach(el => {
el.addEventListener('click', (e) => {
if (e.target.closest('.assistant-session-delete')) return;
const id = parseInt(el.dataset.sessionId, 10);
this.selectSession(id);
});
});
this.sessionsListEl.querySelectorAll('.assistant-session-delete').forEach(btn => {
btn.addEventListener('click', (e) => {
e.stopPropagation();
const id = parseInt(btn.dataset.deleteId, 10);
this.deleteSession(id);
});
});
}
async selectSession(id) {
this.currentSessionId = id;
const session = this.sessions.find(s => s.id === id);
if (session) {
this.chatTitleEl.textContent = session.title;
this.setStatus(session.status === 'active' ? 'ended' : session.status);
}
this.renderSessionsList();
this.updateChatState();
await this.loadMessages(id);
}
async loadMessages(sessionId) {
if (!this.messagesEl) return;
this.messagesEl.innerHTML = '';
try {
const messages = await api.getAssistantMessages(sessionId);
messages.forEach(msg => {
this.renderMessage(msg.role, msg.content);
});
this.scrollToBottom();
} catch (err) {
console.error('[Assistant] Fehler beim Laden der Nachrichten:', err);
}
}
async startSession(taskContext = null) {
try {
const session = await api.createAssistantSession({
title: 'Neue Session',
taskContext
});
this.currentSessionId = session.id;
this.chatTitleEl.textContent = session.title;
this.messagesEl.innerHTML = '';
this.setStatus('running');
this.updateChatState();
// Start Claude process via Socket
const socket = syncManager.socket;
if (socket) {
socket.emit('assistant:start', { sessionId: session.id });
}
await this.loadSessions();
this.showToast('Session gestartet', 'success');
} catch (err) {
console.error('[Assistant] Fehler beim Starten:', err);
this.showToast('Fehler beim Starten der Session', 'error');
}
}
async deleteSession(id) {
if (!confirm('Session wirklich loeschen?')) return;
try {
await api.deleteAssistantSession(id);
if (this.currentSessionId === id) {
this.currentSessionId = null;
this.messagesEl.innerHTML = '';
this.chatTitleEl.textContent = 'Assistent';
this.setStatus(null);
this.updateChatState();
}
await this.loadSessions();
this.showToast('Session geloescht', 'success');
} catch (err) {
console.error('[Assistant] Fehler beim Loeschen:', err);
this.showToast('Fehler beim Loeschen', 'error');
}
}
async endSession() {
const socket = syncManager.socket;
if (socket) {
socket.emit('assistant:stop');
}
}
// =====================
// MESSAGING
// =====================
sendMessage() {
const text = this.inputEl?.value?.trim();
if (!text || !this.currentSessionId) return;
// Render user message
this.renderMessage('user', text);
this.inputEl.value = '';
this.autoResizeInput();
this.scrollToBottom();
// Send via socket
const socket = syncManager.socket;
if (socket) {
socket.emit('assistant:message', { message: text });
}
// Prepare streaming bubble
this.streamingContent = '';
this.streamingMessageEl = this.createMessageEl('assistant', '');
this.streamingMessageEl.classList.add('streaming');
this.messagesEl.appendChild(this.streamingMessageEl);
this.setStatus('thinking');
}
handleOutput(content) {
if (!this.streamingMessageEl) {
// Create streaming bubble if not exists
this.streamingContent = '';
this.streamingMessageEl = this.createMessageEl('assistant', '');
this.streamingMessageEl.classList.add('streaming');
this.messagesEl.appendChild(this.streamingMessageEl);
}
this.streamingContent += content;
this.streamingMessageEl.innerHTML = this.renderMarkdown(this.streamingContent);
this.setStatus('running');
this.scrollToBottom();
}
finalizeStreaming() {
if (this.streamingMessageEl) {
this.streamingMessageEl.classList.remove('streaming');
this.streamingMessageEl = null;
this.streamingContent = '';
}
}
// =====================
// RENDERING
// =====================
renderMessage(role, content) {
const el = this.createMessageEl(role, content);
this.messagesEl.appendChild(el);
}
createMessageEl(role, content) {
const el = document.createElement('div');
el.className = `assistant-message ${role}`;
if (role === 'assistant') {
el.innerHTML = this.renderMarkdown(content);
} else {
el.textContent = content;
}
return el;
}
renderMarkdown(text) {
if (!text) return '';
let html = this.escapeHtml(text);
// Code blocks
html = html.replace(/```(\w*)\n?([\s\S]*?)```/g, (_, lang, code) => {
return `<pre><code>${code.trim()}</code></pre>`;
});
// Inline code
html = html.replace(/`([^`]+)`/g, '<code>$1</code>');
// Bold
html = html.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
// Italic
html = html.replace(/\*(.+?)\*/g, '<em>$1</em>');
// Headers
html = html.replace(/^#### (.+)$/gm, '<h4>$1</h4>');
html = html.replace(/^### (.+)$/gm, '<h3>$1</h3>');
html = html.replace(/^## (.+)$/gm, '<h2>$1</h2>');
html = html.replace(/^# (.+)$/gm, '<h1>$1</h1>');
// Unordered lists
html = html.replace(/^- (.+)$/gm, '<li>$1</li>');
html = html.replace(/(<li>.*<\/li>\n?)+/g, '<ul>$&</ul>');
// Blockquotes
html = html.replace(/^&gt; (.+)$/gm, '<blockquote>$1</blockquote>');
// Line breaks (but not inside pre)
html = html.replace(/\n/g, '<br>');
// Clean up extra <br> in pre blocks
html = html.replace(/<pre><code>([\s\S]*?)<\/code><\/pre>/g, (match) => {
return match.replace(/<br>/g, '\n');
});
return html;
}
// =====================
// UI HELPERS
// =====================
updateChatState() {
if (!this.chatEl) return;
if (this.currentSessionId) {
this.chatEl.classList.remove('no-session');
} else {
this.chatEl.classList.add('no-session');
}
}
setStatus(status) {
this.sessionStatus = status;
if (!this.statusBadgeEl) return;
// Remove all status classes
this.statusBadgeEl.className = 'assistant-status-badge';
if (!status) {
this.statusBadgeEl.textContent = '';
return;
}
const labels = {
running: 'Aktiv',
thinking: 'Denkt...',
ended: 'Beendet',
stopped: 'Gestoppt',
error: 'Fehler'
};
this.statusBadgeEl.classList.add(`status-${status}`);
this.statusBadgeEl.textContent = labels[status] || status;
}
autoResizeInput() {
if (!this.inputEl) return;
this.inputEl.style.height = 'auto';
this.inputEl.style.height = Math.min(this.inputEl.scrollHeight, 150) + 'px';
}
scrollToBottom() {
if (!this.messagesEl) return;
requestAnimationFrame(() => {
this.messagesEl.scrollTop = this.messagesEl.scrollHeight;
});
}
escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
async handleTaskHandover(taskContext) {
// Switch to assistant view
const assistantTab = document.querySelector('.view-tab[data-view="assistant"]');
if (assistantTab) assistantTab.click();
// Ensure initialized
await this.init();
// Start new session with task context
await this.startSession(taskContext);
}
showToast(message, type = 'info') {
window.dispatchEvent(new CustomEvent('toast:show', {
detail: { message, type }
}));
}
}
// Singleton
const assistantManager = new AssistantManager();
export default assistantManager;

Datei anzeigen

@@ -7,6 +7,7 @@
import store from './store.js';
import api from './api.js';
import syncManager from './sync.js';
import assistantManager from './assistant.js';
import {
$, $$, createElement, clearElement, formatDate, formatDateTime,
formatRelativeTime, formatFileSize, getInitials, hexToRgba,
@@ -72,6 +73,9 @@ class TaskModalManager {
// Archive button
$('#btn-archive-task')?.addEventListener('click', () => this.handleArchive());
// Task to Assistant button
$('#btn-task-to-assistant')?.addEventListener('click', () => this.handleTaskToAssistant());
// Restore button
$('#btn-restore-task')?.addEventListener('click', () => this.handleRestore());
@@ -225,6 +229,7 @@ class TaskModalManager {
const duplicateBtn = $('#btn-duplicate-task');
const archiveBtn = $('#btn-archive-task');
const restoreBtn = $('#btn-restore-task');
const assistantBtn = $('#btn-task-to-assistant');
const saveBtn = $('#btn-save-task');
const cancelBtn = $('#btn-cancel-task');
const backBtn = $('#btn-back-task');
@@ -233,6 +238,7 @@ class TaskModalManager {
if (deleteBtn) deleteBtn.classList.toggle('hidden', mode === 'create');
if (duplicateBtn) duplicateBtn.classList.toggle('hidden', mode === 'create');
if (archiveBtn) archiveBtn.classList.toggle('hidden', mode === 'create');
if (assistantBtn) assistantBtn.classList.toggle('hidden', mode === 'create');
if (restoreBtn) restoreBtn.classList.add('hidden'); // Always hide initially, shown in loadTaskData if archived
// Right side buttons (create vs edit mode)
@@ -596,6 +602,33 @@ class TaskModalManager {
}
}
handleTaskToAssistant() {
if (!this.originalTask) return;
const task = this.originalTask;
const priorityLabels = { low: 'Niedrig', medium: 'Mittel', high: 'Hoch', urgent: 'Dringend' };
// Build context text
let context = `Aufgabe: ${task.title}\n`;
if (task.description) context += `Beschreibung: ${task.description}\n`;
context += `Prioritaet: ${priorityLabels[task.priority] || task.priority}\n`;
if (task.labels && task.labels.length > 0) {
context += `Labels: ${task.labels.map(l => l.name).join(', ')}\n`;
}
if (this.subtasks && this.subtasks.length > 0) {
context += `\nSubtasks:\n`;
this.subtasks.forEach(st => {
context += `- [${st.completed ? 'x' : ' '}] ${st.title}\n`;
});
}
// Close modal and hand over to assistant
this.close();
assistantManager.handleTaskHandover(context);
}
showColumnSelectDialog() {
const columns = store.get('columns');

Datei anzeigen

@@ -4,7 +4,7 @@
* Offline support and caching
*/
const CACHE_VERSION = '394';
const CACHE_VERSION = '396';
const CACHE_NAME = 'taskmate-v' + CACHE_VERSION;
const STATIC_CACHE_NAME = 'taskmate-static-v' + CACHE_VERSION;
const DYNAMIC_CACHE_NAME = 'taskmate-dynamic-v' + CACHE_VERSION;
@@ -42,6 +42,7 @@ const STATIC_ASSETS = [
'/js/mobile.js',
'/js/reminders.js',
'/js/contacts.js',
'/js/assistant.js',
'/js/pwa.js',
'/css/list.css',
'/css/mobile.css',
@@ -53,6 +54,7 @@ const STATIC_ASSETS = [
'/css/coding.css',
'/css/reminders.css',
'/css/contacts.css',
'/css/assistant.css',
'/css/pwa.css',
'/manifest.json'
];