Assistent
- -Claude Assistent
-Starte eine neue Session oder waehle eine bestehende aus.
-diff --git a/backend/routes/assistant.js b/backend/routes/assistant.js deleted file mode 100644 index 228c9ef..0000000 --- a/backend/routes/assistant.js +++ /dev/null @@ -1,571 +0,0 @@ -/** - * TASKMATE - Claude Assistant Routes - * ==================================== - * REST-Endpunkte und Session-Manager fuer den Claude-Assistenten - * - * Kommuniziert ueber HTTP mit dem Claude-Proxy (SSE-Streaming) - * Proxy-URL: http://172.20.0.1:3100/api/chat - */ - -const express = require('express'); -const router = express.Router(); -const http = require('http'); -const { getDb } = require('../database'); -const logger = require('../utils/logger'); - -// ============================================================================ -// SESSION MANAGER -// ============================================================================ - -// Aktive Sessions: userId -> { claudeSessionId, busy, sessionId, socket, currentRequest, timeout } -const activeSessions = new Map(); - -const SESSION_TIMEOUT_MS = 30 * 60 * 1000; // 30 Minuten - -/** - * Erlaubte Benutzer fuer den Assistenten (lowercase) - */ -const ALLOWED_ASSISTANT_USERS = new Set([ - 'hendrik', 'monami', - 'hendrik_gebhardt@gmx.de', 'momohomma@googlemail.com' -]); - -function hasAssistantAccess(user) { - const username = (user.username || '').toLowerCase(); - const displayName = (user.displayName || '').toLowerCase(); - return ALLOWED_ASSISTANT_USERS.has(username) || ALLOWED_ASSISTANT_USERS.has(displayName); -} - -/** - * Berechtigungs-Middleware: Nur Hendrik und Monami duerfen den Assistenten nutzen - */ -function requireAssistantAccess(req, res, next) { - if (!hasAssistantAccess(req.user)) { - return res.status(403).json({ error: 'Kein Zugriff auf den Assistenten' }); - } - next(); -} - -/** - * Socket-Berechtigung pruefen - */ -function checkSocketAccess(socket) { - return hasAssistantAccess(socket.user); -} - -/** - * Timeout zuruecksetzen fuer eine Session - */ -function resetTimeout(userId) { - const session = activeSessions.get(userId); - if (!session) return; - - if (session.timeout) { - clearTimeout(session.timeout); - } - - session.timeout = setTimeout(() => { - logger.info(`[Assistant] Session-Timeout fuer User ${userId} - wird beendet`); - stopSession(userId); - }, SESSION_TIMEOUT_MS); -} - -/** - * Proxy-URL und Token - */ -const PROXY_URL = 'http://172.20.0.1:3100/api/chat'; -const PROXY_TOKEN = process.env.PROXY_TOKEN || ''; - -/** - * Claude-Session aktivieren (kein Prozess - der wird erst bei sendMessage gestartet) - */ -function startSession(userId, sessionId, socket) { - const existing = activeSessions.get(userId); - - // Gleiche Session: Nur Socket aktualisieren - if (existing && existing.sessionId === sessionId) { - existing.socket = socket; - resetTimeout(userId); - socket.emit('assistant:status', { - sessionId, - status: existing.busy ? 'thinking' : 'active' - }); - return; - } - - // Andere Session: bestehende aufraemen - if (existing) { - if (existing.currentRequest) { - try { existing.currentRequest.destroy(); } catch (e) {} - } - if (existing.timeout) clearTimeout(existing.timeout); - } - - const sessionData = { - claudeSessionId: null, - busy: false, - sessionId, - socket, - currentRequest: null, - timeout: null - }; - - activeSessions.set(userId, sessionData); - resetTimeout(userId); - - logger.info(`[Assistant] Session ${sessionId} aktiviert fuer User ${userId}`); - socket.emit('assistant:status', { sessionId, status: 'active' }); - - // TaskContext als erste Nachricht senden - try { - const db = getDb(); - const dbSession = db.prepare('SELECT task_context FROM assistant_sessions WHERE id = ?').get(sessionId); - if (dbSession && dbSession.task_context) { - sendMessage(userId, dbSession.task_context, socket); - } - } catch (err) { - logger.error(`[Assistant] TaskContext-Fehler: ${err.message}`); - } -} - -/** - * Nachricht an Claude senden (HTTP-Request an Proxy mit SSE-Streaming) - */ -function sendMessage(userId, message, socket) { - const session = activeSessions.get(userId); - if (!session) throw new Error('Keine aktive Session'); - if (session.busy) throw new Error('Assistent verarbeitet noch eine Nachricht'); - - session.busy = true; - session.socket = socket; - - // User-Nachricht in DB speichern - try { - const db = getDb(); - db.prepare(` - INSERT INTO assistant_messages (session_id, role, content) - VALUES (?, 'user', ?) - `).run(session.sessionId, message); - } catch (err) { - logger.error(`[Assistant] DB-Fehler (user msg): ${err.message}`); - } - - socket.emit('assistant:status', { sessionId: session.sessionId, status: 'thinking' }); - - logger.info(`[Assistant] Proxy-Aufruf fuer Session ${session.sessionId} (resume: ${session.claudeSessionId || 'nein'})`); - - // HTTP-Request an Proxy - const postData = JSON.stringify({ - message, - resumeSessionId: session.claudeSessionId || null - }); - - const url = new URL(PROXY_URL); - const options = { - hostname: url.hostname, - port: url.port, - path: url.pathname, - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'X-Proxy-Token': PROXY_TOKEN, - 'Content-Length': Buffer.byteLength(postData) - } - }; - - const req = http.request(options, (res) => { - let fullOutput = ''; - let buffer = ''; - let currentEvent = null; - - res.setEncoding('utf8'); - - res.on('data', (chunk) => { - buffer += chunk; - const lines = buffer.split('\n'); - buffer = lines.pop(); // Unvollstaendige Zeile behalten - - for (const line of lines) { - // SSE event-Zeile - if (line.startsWith('event: ')) { - currentEvent = line.substring(7).trim(); - continue; - } - - // SSE data-Zeile - if (line.startsWith('data: ')) { - const dataStr = line.substring(6); - - if (currentEvent === 'done') { - // Done-Event: Enthaelt claudeSessionId - try { - const json = JSON.parse(dataStr); - if (json.sessionId) { - session.claudeSessionId = json.sessionId; - logger.info(`[Assistant] Claude-SessionId gesetzt: ${json.sessionId}`); - } - } catch (e) { - logger.warn(`[Assistant] Done-Event Parse-Fehler: ${e.message}`); - } - currentEvent = null; - continue; - } - - if (currentEvent === 'error') { - // Error-Event - try { - const json = JSON.parse(dataStr); - logger.error(`[Assistant] Proxy-Fehler: ${json.error || dataStr}`); - if (socket.connected) { - socket.emit('assistant:status', { - sessionId: session.sessionId, - status: 'error', - error: json.error || 'Proxy-Fehler' - }); - } - } catch (e) { - logger.error(`[Assistant] Proxy-Fehler (raw): ${dataStr}`); - } - currentEvent = null; - continue; - } - - // Normales Text-Event - try { - const json = JSON.parse(dataStr); - const text = json.text || json.content || ''; - if (text) { - fullOutput += text; - if (socket.connected) { - socket.emit('assistant:output', { sessionId: session.sessionId, content: text }); - } - } - } catch (e) { - // Kein valides JSON - Rohtext verwenden - if (dataStr.trim()) { - fullOutput += dataStr; - if (socket.connected) { - socket.emit('assistant:output', { sessionId: session.sessionId, content: dataStr }); - } - } - } - currentEvent = null; - } - } - }); - - res.on('end', () => { - // Restlichen Buffer verarbeiten - if (buffer.trim()) { - const lines = buffer.split('\n'); - for (const line of lines) { - if (line.startsWith('data: ')) { - const dataStr = line.substring(6); - try { - const json = JSON.parse(dataStr); - const text = json.text || json.content || ''; - if (text) { - fullOutput += text; - if (socket.connected) { - socket.emit('assistant:output', { sessionId: session.sessionId, content: text }); - } - } - } catch (e) {} - } - } - } - - // Komplette Antwort in DB speichern - if (fullOutput) { - try { - const db = getDb(); - db.prepare(` - INSERT INTO assistant_messages (session_id, role, content) - VALUES (?, 'assistant', ?) - `).run(session.sessionId, fullOutput); - } catch (err) { - logger.error(`[Assistant] DB-Fehler (assistant msg): ${err.message}`); - } - } - - logger.info(`[Assistant] Proxy-Antwort abgeschlossen fuer Session ${session.sessionId}`); - - session.busy = false; - session.currentRequest = null; - resetTimeout(userId); - - if (socket.connected) { - if (res.statusCode !== 200 && !fullOutput) { - socket.emit('assistant:status', { - sessionId: session.sessionId, - status: 'error', - error: `Proxy-Fehler (HTTP ${res.statusCode})` - }); - } else { - socket.emit('assistant:status', { sessionId: session.sessionId, status: 'active' }); - } - } - }); - }); - - req.on('error', (err) => { - logger.error(`[Assistant] HTTP-Fehler: ${err.message}`); - session.busy = false; - session.currentRequest = null; - - if (socket.connected) { - socket.emit('assistant:status', { - sessionId: session.sessionId, - status: 'error', - error: `Verbindung zum Assistenten fehlgeschlagen: ${err.message}` - }); - } - }); - - session.currentRequest = req; - req.write(postData); - req.end(); -} - -/** - * Session beenden - */ -function stopSession(userId) { - const session = activeSessions.get(userId); - if (!session) return; - - logger.info(`[Assistant] Session ${session.sessionId} wird beendet fuer User ${userId}`); - - if (session.currentRequest) { - try { session.currentRequest.destroy(); } catch (e) {} - } - if (session.timeout) clearTimeout(session.timeout); - - activeSessions.delete(userId); - - // DB aktualisieren - try { - const db = getDb(); - db.prepare(` - UPDATE assistant_sessions SET status = 'ended', ended_at = CURRENT_TIMESTAMP - WHERE id = ? - `).run(session.sessionId); - } catch (err) { - logger.error(`[Assistant] DB-Fehler beim Beenden: ${err.message}`); - } -} - -// ============================================================================ -// REST ENDPOINTS -// ============================================================================ - -// Alle Routen brauchen Assistant-Zugriff -router.use(requireAssistantAccess); - -/** - * GET /sessions - Alle Sessions des Users - */ -router.get('/sessions', (req, res) => { - try { - const db = getDb(); - const sessions = db.prepare(` - SELECT id, user_id, title, status, task_context, created_at, ended_at - FROM assistant_sessions - WHERE user_id = ? - ORDER BY created_at DESC - `).all(req.user.id); - - res.json(sessions); - } catch (err) { - logger.error(`[Assistant] Fehler beim Laden der Sessions: ${err.message}`); - res.status(500).json({ error: 'Fehler beim Laden der Sessions' }); - } -}); - -/** - * GET /sessions/:id/messages - Alle Nachrichten einer Session - */ -router.get('/sessions/:id/messages', (req, res) => { - try { - const db = getDb(); - const sessionId = parseInt(req.params.id, 10); - - // Pruefen ob Session dem User gehoert - const session = db.prepare(` - SELECT id FROM assistant_sessions WHERE id = ? AND user_id = ? - `).get(sessionId, req.user.id); - - if (!session) { - return res.status(404).json({ error: 'Session nicht gefunden' }); - } - - const messages = db.prepare(` - SELECT id, session_id, role, content, created_at - FROM assistant_messages - WHERE session_id = ? - ORDER BY created_at ASC - `).all(sessionId); - - res.json(messages); - } catch (err) { - logger.error(`[Assistant] Fehler beim Laden der Nachrichten: ${err.message}`); - res.status(500).json({ error: 'Fehler beim Laden der Nachrichten' }); - } -}); - -/** - * POST /sessions - Neue Session erstellen - */ -router.post('/sessions', (req, res) => { - try { - const db = getDb(); - const { title, taskContext } = req.body; - - const result = db.prepare(` - INSERT INTO assistant_sessions (user_id, title, task_context) - VALUES (?, ?, ?) - `).run(req.user.id, title || 'Neue Session', taskContext || null); - - const session = db.prepare(` - SELECT id, user_id, title, status, task_context, created_at, ended_at - FROM assistant_sessions WHERE id = ? - `).get(result.lastInsertRowid); - - logger.info(`[Assistant] Neue Session ${session.id} erstellt von ${req.user.username}`); - res.status(201).json(session); - } catch (err) { - logger.error(`[Assistant] Fehler beim Erstellen der Session: ${err.message}`); - res.status(500).json({ error: 'Fehler beim Erstellen der Session' }); - } -}); - -/** - * DELETE /sessions/:id - Session loeschen - */ -router.delete('/sessions/:id', (req, res) => { - try { - const db = getDb(); - const sessionId = parseInt(req.params.id, 10); - - // Pruefen ob Session dem User gehoert - const session = db.prepare(` - SELECT id, user_id FROM assistant_sessions WHERE id = ? AND user_id = ? - `).get(sessionId, req.user.id); - - if (!session) { - return res.status(404).json({ error: 'Session nicht gefunden' }); - } - - // Wenn aktiver Prozess laeuft, zuerst beenden - const activeSession = activeSessions.get(req.user.id); - if (activeSession && activeSession.sessionId === sessionId) { - stopSession(req.user.id); - } - - // Session und zugehoerige Nachrichten loeschen (CASCADE) - db.prepare('DELETE FROM assistant_sessions WHERE id = ?').run(sessionId); - - logger.info(`[Assistant] Session ${sessionId} geloescht von ${req.user.username}`); - res.json({ success: true }); - } catch (err) { - logger.error(`[Assistant] Fehler beim Loeschen der Session: ${err.message}`); - res.status(500).json({ error: 'Fehler beim Loeschen der Session' }); - } -}); - -// ============================================================================ -// SOCKET EVENT HANDLER (exportiert fuer server.js) -// ============================================================================ - -/** - * Socket-Events registrieren - */ -function registerSocketEvents(socket) { - const userId = socket.user.id; - - // assistant:start - Session aktivieren - socket.on('assistant:start', (data) => { - try { - if (!checkSocketAccess(socket)) { - socket.emit('assistant:status', { status: 'error', error: 'Kein Zugriff' }); - return; - } - - const sessionId = data && data.sessionId; - if (!sessionId) { - socket.emit('assistant:status', { status: 'error', error: 'Keine Session-ID angegeben' }); - return; - } - - // Pruefen ob Session existiert und dem User gehoert - const db = getDb(); - const session = db.prepare(` - SELECT id, user_id, status FROM assistant_sessions WHERE id = ? AND user_id = ? - `).get(sessionId, userId); - - if (!session) { - socket.emit('assistant:status', { status: 'error', error: 'Session nicht gefunden' }); - return; - } - - // Beendete Session reaktivieren - if (session.status === 'ended') { - db.prepare(`UPDATE assistant_sessions SET status = 'active', ended_at = NULL WHERE id = ?`).run(sessionId); - } - - startSession(userId, sessionId, socket); - } catch (err) { - logger.error(`[Assistant] Fehler beim Starten: ${err.message}`); - socket.emit('assistant:status', { status: 'error', error: err.message }); - } - }); - - // assistant:message - Nachricht senden - socket.on('assistant:message', (data) => { - try { - if (!checkSocketAccess(socket)) { - socket.emit('assistant:status', { status: 'error', error: 'Kein Zugriff' }); - return; - } - - const message = data && data.message; - if (!message || typeof message !== 'string') { - socket.emit('assistant:status', { status: 'error', error: 'Keine Nachricht angegeben' }); - return; - } - - sendMessage(userId, message, socket); - } catch (err) { - logger.error(`[Assistant] Fehler beim Senden: ${err.message}`); - socket.emit('assistant:status', { status: 'error', error: err.message }); - } - }); - - // assistant:stop - Session beenden - socket.on('assistant:stop', () => { - try { - if (!checkSocketAccess(socket)) { - socket.emit('assistant:status', { status: 'error', error: 'Kein Zugriff' }); - return; - } - - stopSession(userId); - socket.emit('assistant:status', { status: 'stopped' }); - } catch (err) { - logger.error(`[Assistant] Fehler beim Stoppen: ${err.message}`); - socket.emit('assistant:status', { status: 'error', error: err.message }); - } - }); - - // Bei Disconnect: aktive Session beenden - socket.on('disconnect', () => { - if (activeSessions.has(userId)) { - logger.info(`[Assistant] Socket disconnect - Session wird beendet fuer User ${userId}`); - stopSession(userId); - } - }); -} - -module.exports = router; -module.exports.registerSocketEvents = registerSocketEvents; -module.exports.stopSession = stopSession; diff --git a/backend/server.js b/backend/server.js index f50912b..7dd53d6 100644 --- a/backend/server.js +++ b/backend/server.js @@ -48,7 +48,6 @@ const giteaRoutes = require('./routes/gitea'); const knowledgeRoutes = require('./routes/knowledge'); const codingRoutes = require('./routes/coding'); const reminderRoutes = require('./routes/reminders'); -const assistantRoutes = require('./routes/assistant'); // Express App erstellen const app = express(); @@ -179,9 +178,6 @@ app.use('/api/reminders', authenticateToken, csrfProtection, reminderRoutes); // Contacts-Routes (Kontakte) app.use('/api/contacts', authenticateToken, csrfProtection, require('./routes/contacts')); -// Assistant-Routes (Claude-Assistent) -app.use('/api/assistant', authenticateToken, csrfProtection, assistantRoutes); - // ============================================================================= // SOCKET.IO // ============================================================================= @@ -245,9 +241,6 @@ io.on('connection', (socket) => { })) }); }); - - // Assistant Socket-Events registrieren - assistantRoutes.registerSocketEvents(socket); }); // Socket.io Instance global verfügbar machen für Routes @@ -340,11 +333,6 @@ process.on('SIGTERM', () => { const reminderServiceInstance = reminderService.getInstance(); reminderServiceInstance.stop(); - // Aktive Assistant-Sessions beenden - for (const [userId] of connectedClients) { - assistantRoutes.stopSession(userId); - } - server.close(() => { database.close(); logger.info('Server beendet'); @@ -359,11 +347,6 @@ process.on('SIGINT', () => { const reminderServiceInstance = reminderService.getInstance(); reminderServiceInstance.stop(); - // Aktive Assistant-Sessions beenden - for (const [userId] of connectedClients) { - assistantRoutes.stopSession(userId); - } - server.close(() => { database.close(); logger.info('Server beendet'); diff --git a/docker-compose.yml b/docker-compose.yml index 8e2e685..cba6265 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -32,7 +32,6 @@ services: - USER2_DISPLAYNAME=${USER2_DISPLAYNAME:-Benutzer 2} - USER2_COLOR=${USER2_COLOR:-#FF9500} - ENCRYPTION_KEY=${ENCRYPTION_KEY} - - PROXY_TOKEN=${PROXY_TOKEN} healthcheck: test: ["CMD", "curl", "-f", "http://localhost:3000/api/health"] interval: 30s diff --git a/frontend/css/assistant.css b/frontend/css/assistant.css deleted file mode 100644 index 7cf8a8c..0000000 --- a/frontend/css/assistant.css +++ /dev/null @@ -1,441 +0,0 @@ -/** - * TASKMATE - Assistant View Styles - * ================================= - * Claude Assistant Chat Interface - */ - -/* Layout */ -.view.view-assistant { - height: calc(100vh - var(--header-height) - 52px); - overflow: hidden; - display: flex; - flex-direction: column; -} - -.assistant-layout { - display: grid; - grid-template-columns: 280px 1fr; - flex: 1; - min-height: 0; - 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; - height: 100%; - 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; - min-height: 0; - 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 { - flex: 1; - min-height: 0; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - 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; -} - -/* ===================== */ -/* 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; - } -} diff --git a/frontend/index.html b/frontend/index.html index 0885bbd..fc360b2 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -38,7 +38,6 @@ - @@ -311,12 +310,6 @@ Kontakte -
- - -