Initial commit: AMS Frontend - Vue 3 + PrimeVue

This commit is contained in:
FluxKit
2026-02-19 14:03:01 +00:00
commit 263d52141d
50 changed files with 20254 additions and 0 deletions

970
src/views/AgentTaskView.vue Normal file
View File

@@ -0,0 +1,970 @@
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
import { api } from '../api'
import { useToast } from 'primevue/usetoast'
import { useWebSocketStore } from '../stores/websocket'
import Dialog from 'primevue/dialog'
import Dropdown from 'primevue/dropdown'
import TaskEditDialog from '../components/TaskEditDialog.vue'
const toast = useToast()
const wsStore = useWebSocketStore()
// --- Daten ---
interface Project {
_id: string
name: string
}
interface Task {
_id: string
number?: number
title: string
status: string
priority: string
}
interface QuickText {
_id: string
title: string
text: string
sortOrder: number
}
interface Agent {
_id: string
name: string
emoji: string
}
const messageText = ref('')
const projects = ref<Project[]>([])
const tasks = ref<Task[]>([])
const agents = ref<Agent[]>([])
const quickTexts = ref<QuickText[]>([])
const selectedProject = ref<string | null>(localStorage.getItem('ams_agent_task_project') || null)
const selectedTasks = ref<string[]>([])
const selectedAgent = ref<string | null>(localStorage.getItem('ams_agent_task_agent') || null)
const sending = ref(false)
const recording = ref(false)
const transcribing = ref(false)
// Task-Detail Dialog
const taskDialogVisible = ref(false)
const taskDialogId = ref<string | null>(null)
function openTaskDialog(taskId: string) {
taskDialogId.value = taskId
taskDialogVisible.value = true
}
function onTaskDialogSaved() {
// Reload tasks to reflect changes
if (selectedProject.value) {
loadProjectTasks(selectedProject.value)
} else {
loadTasks()
}
}
// Quick-Text Dialog
const quickTextDialog = ref(false)
const editingQuickText = ref<QuickText | null>(null)
const qtTitle = ref('')
const qtText = ref('')
// MediaRecorder
let mediaRecorder: MediaRecorder | null = null
let audioChunks: Blob[] = []
// --- LocalStorage persist ---
watch(selectedProject, (val) => {
if (val) localStorage.setItem('ams_agent_task_project', val)
else localStorage.removeItem('ams_agent_task_project')
})
watch(selectedAgent, (val) => {
if (val) localStorage.setItem('ams_agent_task_agent', val)
else localStorage.removeItem('ams_agent_task_agent')
})
// --- Computed ---
const filteredTasks = computed(() => {
if (!selectedProject.value) return tasks.value
return tasks.value
})
const projectOptions = computed(() =>
projects.value.map(p => ({ label: p.name, value: p._id }))
)
const agentOptions = computed(() =>
agents.value.map(a => ({ label: `${a.emoji} ${a.name}`, value: a._id }))
)
// --- API Calls ---
async function loadProjects() {
try {
const res = await api.get<{ projects: Project[] }>('/tasks/projects/list')
projects.value = res.projects || []
} catch {
// silent
}
}
const statusLabels: Record<string, string> = {
in_progress: 'In Progress',
pending: 'Pending',
review: 'Review',
done: 'Done',
rejected: 'Rejected',
backlog: 'Backlog',
}
function formatStatus(status: string): string {
return statusLabels[status] || status
}
function sortTasks() {
tasks.value.sort((a, b) => {
const p: Record<string, number> = { urgent: 0, high: 1, medium: 2, low: 3 }
return (p[String(a.priority || 'medium')] ?? 4) - (p[String(b.priority || 'medium')] ?? 4)
})
}
async function loadTasks() {
try {
const res = await api.get<{ tasks: Task[] }>('/tasks')
const openStatuses = ['todo', 'pending']
tasks.value = (res.tasks || []).filter(t => openStatuses.includes(String(t.status)))
sortTasks()
} catch {
toast.add({ severity: 'error', summary: 'Fehler', detail: 'Tasks konnten nicht geladen werden', life: 3000 })
}
}
async function loadAgents() {
try {
const res = await api.get<{ agents: Agent[] }>('/agents')
agents.value = res.agents || []
} catch {
// silent
}
}
async function loadQuickTexts() {
try {
const res = await api.get<{ quickTexts: QuickText[] }>('/quicktexts')
quickTexts.value = res.quickTexts || []
} catch {
// silent
}
}
async function loadProjectTasks(projectId: string) {
try {
const res = await api.get<{ tasks: Task[] }>(`/tasks?project=${projectId}`)
const openStatuses = ['todo', 'pending']
tasks.value = (res.tasks || []).filter(t => openStatuses.includes(String(t.status)))
sortTasks()
} catch {
toast.add({ severity: 'error', summary: 'Fehler', detail: 'Tasks konnten nicht geladen werden', life: 3000 })
}
}
// --- Quick-Texts CRUD ---
function openQuickTextDialog(qt?: QuickText) {
if (qt) {
editingQuickText.value = qt
qtTitle.value = qt.title
qtText.value = qt.text
} else {
editingQuickText.value = null
qtTitle.value = ''
qtText.value = ''
}
quickTextDialog.value = true
}
async function saveQuickText() {
if (!qtTitle.value || !qtText.value) return
try {
if (editingQuickText.value) {
await api.put(`/quicktexts/${editingQuickText.value._id}`, { title: qtTitle.value, text: qtText.value })
toast.add({ severity: 'success', summary: 'Gespeichert', detail: 'Quick-Text aktualisiert', life: 2000 })
} else {
await api.post('/quicktexts', { title: qtTitle.value, text: qtText.value })
toast.add({ severity: 'success', summary: 'Erstellt', detail: 'Quick-Text hinzugefügt', life: 2000 })
}
quickTextDialog.value = false
await loadQuickTexts()
} catch {
toast.add({ severity: 'error', summary: 'Fehler', detail: 'Speichern fehlgeschlagen', life: 3000 })
}
}
async function deleteQuickText(id: string) {
try {
await api.delete(`/quicktexts/${id}`)
toast.add({ severity: 'success', summary: 'Gelöscht', detail: 'Quick-Text entfernt', life: 2000 })
await loadQuickTexts()
} catch {
toast.add({ severity: 'error', summary: 'Fehler', detail: 'Löschen fehlgeschlagen', life: 3000 })
}
}
function appendQuickText(text: string) {
if (messageText.value && !messageText.value.endsWith('\n')) {
messageText.value += '\n'
}
messageText.value += text
}
function insertTaskId(taskId: string) {
const task = tasks.value.find(t => t._id === taskId)
if (task) {
const insertText = `[${task.title}] (${taskId})`
if (messageText.value && !messageText.value.endsWith('\n')) {
messageText.value += '\n'
}
messageText.value += insertText
// Checkbox ebenfalls anhaken
if (!selectedTasks.value.includes(taskId)) {
selectedTasks.value.push(taskId)
}
}
}
function toggleTask(taskId: string) {
const idx = selectedTasks.value.indexOf(taskId)
if (idx >= 0) {
selectedTasks.value.splice(idx, 1)
} else {
selectedTasks.value.push(taskId)
}
}
// --- Audio Recording ---
async function toggleRecording() {
if (recording.value) {
stopRecording()
} else {
await startRecording()
}
}
async function startRecording() {
try {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true })
mediaRecorder = new MediaRecorder(stream, { mimeType: 'audio/webm' })
audioChunks = []
mediaRecorder.ondataavailable = (event) => {
if (event.data.size > 0) audioChunks.push(event.data)
}
mediaRecorder.onstop = async () => {
stream.getTracks().forEach(track => track.stop())
const audioBlob = new Blob(audioChunks, { type: 'audio/webm' })
await transcribeAudio(audioBlob)
}
mediaRecorder.start()
recording.value = true
} catch {
toast.add({ severity: 'error', summary: 'Fehler', detail: 'Mikrofon-Zugriff verweigert', life: 3000 })
}
}
function stopRecording() {
if (mediaRecorder && mediaRecorder.state !== 'inactive') mediaRecorder.stop()
recording.value = false
}
async function transcribeAudio(blob: Blob) {
transcribing.value = true
try {
const formData = new FormData()
formData.append('audio', blob, 'recording.webm')
const apiBase = import.meta.env.VITE_API_URL || 'http://localhost:8000/api'
const token = localStorage.getItem('ams_token')
const response = await fetch(`${apiBase}/transcribe`, {
method: 'POST',
headers: { 'Authorization': `Bearer ${token}` },
body: formData,
})
const data = await response.json()
if (data.error) {
toast.add({ severity: 'error', summary: 'Transkription', detail: data.error, life: 5000 })
return
}
if (data.text) {
if (messageText.value && !messageText.value.endsWith('\n')) messageText.value += ' '
messageText.value += data.text
}
} catch {
toast.add({ severity: 'error', summary: 'Fehler', detail: 'Transkription fehlgeschlagen', life: 3000 })
} finally {
transcribing.value = false
}
}
async function sendMessage() {
if (!messageText.value.trim()) {
toast.add({ severity: 'warn', summary: 'Hinweis', detail: 'Nachricht darf nicht leer sein', life: 2000 })
return
}
sending.value = true
try {
// Kontext sammeln
const selectedProjectObj = projects.value.find(p => p._id === selectedProject.value)
const selectedAgentObj = agents.value.find(a => a._id === selectedAgent.value)
const linkedTitles = selectedTasks.value.map(id => tasks.value.find(t => t._id === id)?.title || id)
// 1. Auftrag in DB speichern
await api.post('/agent-tasks', {
message: messageText.value.trim(),
projectId: selectedProject.value || undefined,
projectName: selectedProjectObj?.name || undefined,
agentId: selectedAgent.value || undefined,
agentName: selectedAgentObj ? `${selectedAgentObj.emoji} ${selectedAgentObj.name}` : undefined,
linkedTaskIds: selectedTasks.value,
linkedTaskTitles: linkedTitles,
})
// 2. Optional: Telegram-Benachrichtigung
try {
await api.post('/messaging/telegram', { text: messageText.value.trim() })
} catch {
// Telegram ist optional — kein Fehler wenn nicht konfiguriert
}
// 3. OpenClaw Wake — Agent sofort wecken (via Backend-Proxy)
try {
await api.post('/agent-tasks/wake', {})
} catch {
// Wake ist optional
}
toast.add({ severity: 'success', summary: 'Auftrag erstellt', detail: 'Auftrag gespeichert und Agent benachrichtigt', life: 3000 })
messageText.value = ''
selectedTasks.value = []
} catch (err: unknown) {
const message = err instanceof Error ? err.message : 'Senden fehlgeschlagen'
toast.add({ severity: 'error', summary: 'Fehler', detail: message, life: 5000 })
} finally {
sending.value = false
}
}
function onProjectChange() {
selectedTasks.value = []
if (selectedProject.value) {
loadProjectTasks(selectedProject.value)
} else {
loadTasks()
}
}
// --- WebSocket Live-Updates ---
function reloadTasks() {
if (selectedProject.value) {
loadProjectTasks(selectedProject.value)
} else {
loadTasks()
}
}
function onTaskStatusChanged(data: unknown) {
const { taskId, newStatus } = data as { taskId: string; newStatus: string }
if (newStatus === 'in_progress') {
// Task wurde auf in_progress gesetzt — neu laden damit er erscheint
reloadTasks()
} else {
// Task ist nicht mehr in_progress — entfernen
tasks.value = tasks.value.filter(t => t._id !== taskId)
selectedTasks.value = selectedTasks.value.filter(id => id !== taskId)
}
}
function onTaskUpdated(data: unknown) {
const { taskId, changes } = data as { taskId: string; changes: Record<string, unknown> }
const task = tasks.value.find(t => t._id === taskId)
if (task) {
// Status-Änderung über update-Route
if (changes.status && String(changes.status) !== 'in_progress') {
tasks.value = tasks.value.filter(t => t._id !== taskId)
selectedTasks.value = selectedTasks.value.filter(id => id !== taskId)
return
}
// Andere Felder aktualisieren
if (changes.title) task.title = String(changes.title)
if (changes.priority) task.priority = String(changes.priority)
if (changes.status) task.status = String(changes.status)
} else if (changes.status === 'in_progress') {
reloadTasks()
}
}
function onTaskCreated() {
reloadTasks()
}
function onTaskDeleted(data: unknown) {
const { taskId } = data as { taskId: string }
tasks.value = tasks.value.filter(t => t._id !== taskId)
selectedTasks.value = selectedTasks.value.filter(id => id !== taskId)
}
onMounted(() => {
loadProjects()
loadAgents()
loadQuickTexts()
reloadTasks()
// WebSocket-Events registrieren
wsStore.on('task:status_changed', onTaskStatusChanged)
wsStore.on('task:updated', onTaskUpdated)
wsStore.on('task:created', onTaskCreated)
wsStore.on('task:deleted', onTaskDeleted)
})
onUnmounted(() => {
wsStore.off('task:status_changed', onTaskStatusChanged)
wsStore.off('task:updated', onTaskUpdated)
wsStore.off('task:created', onTaskCreated)
wsStore.off('task:deleted', onTaskDeleted)
})
</script>
<template>
<div class="agent-task-page">
<div class="page-header">
<h1><i class="pi pi-send" style="margin-right: 0.5rem;"></i>Aufgabe stellen</h1>
</div>
<!-- Kontext-Zeile: Projekt + Agent -->
<div class="context-bar">
<div class="context-item">
<label>Projekt</label>
<Dropdown
v-model="selectedProject"
:options="projectOptions"
optionLabel="label"
optionValue="value"
placeholder="Alle Projekte"
showClear
class="dropdown-dark"
@change="onProjectChange"
/>
</div>
<div class="context-item">
<label>Agent</label>
<Dropdown
v-model="selectedAgent"
:options="agentOptions"
optionLabel="label"
optionValue="value"
placeholder="Agent wählen..."
showClear
class="dropdown-dark"
/>
</div>
</div>
<!-- 3-Spalten Layout -->
<div class="three-col">
<!-- Links: Tasks -->
<div class="col-tasks">
<div class="col-header">
<h3><i class="pi pi-list-check"></i> Tasks ({{ filteredTasks.length }})</h3>
</div>
<div class="task-list-scroll">
<div v-if="filteredTasks.length > 0" class="task-list">
<div
v-for="task in filteredTasks"
:key="task._id"
class="task-list-item"
:class="{ selected: selectedTasks.includes(task._id) }"
@click="toggleTask(task._id)"
>
<i :class="selectedTasks.includes(task._id) ? 'pi pi-check-square' : 'pi pi-stop'"
:style="{ color: selectedTasks.includes(task._id) ? '#818cf8' : 'rgba(255,255,255,0.25)', fontSize: '0.95rem' }"></i>
<span class="task-prio" :class="'prio-' + String(task.priority || 'medium')">{{ String(task.priority || 'medium').substring(0, 3).toUpperCase() }}</span>
<span class="task-status-badge">{{ formatStatus(task.status) }}</span>
<span class="task-title clickable" @click.stop="openTaskDialog(task._id)">{{ task.title }}</span>
<button class="icon-btn-sm" @click.stop="insertTaskId(task._id)" title="In Nachricht einfügen">
<i class="pi pi-arrow-right"></i>
</button>
</div>
</div>
<div v-else class="empty-hint">Keine offenen Tasks.</div>
</div>
</div>
<!-- Mitte: Nachricht -->
<div class="col-message">
<div class="col-header">
<h3><i class="pi pi-comment"></i> Nachricht</h3>
</div>
<div class="message-area">
<textarea
v-model="messageText"
placeholder="Nachricht eingeben oder einsprechen..."
class="message-input"
></textarea>
<div class="message-toolbar">
<button
class="toolbar-btn"
:class="{ active: recording, pulse: recording }"
@click="toggleRecording"
:disabled="transcribing"
>
<i :class="recording ? 'pi pi-stop-circle' : 'pi pi-microphone'"></i>
{{ recording ? 'Stop' : transcribing ? 'Transkribiert...' : 'Aufnehmen' }}
</button>
<button
class="toolbar-btn send"
@click="sendMessage"
:disabled="sending || !messageText.trim()"
>
<i :class="sending ? 'pi pi-spin pi-spinner' : 'pi pi-send'"></i>
{{ sending ? 'Sende...' : 'Senden' }}
</button>
</div>
</div>
</div>
<!-- Rechts: Quick-Texte -->
<div class="col-quicktexts">
<div class="col-header">
<h3><i class="pi pi-bolt"></i> Quick-Texte</h3>
<button class="icon-btn" @click="openQuickTextDialog()" title="Neuer Quick-Text">
<i class="pi pi-plus"></i>
</button>
</div>
<div class="quicktext-scroll">
<div v-if="quickTexts.length > 0" class="qt-list">
<div v-for="qt in quickTexts" :key="qt._id" class="quick-text-card">
<div class="qt-content" @click="appendQuickText(qt.text)">
<div class="qt-title">{{ qt.title }}</div>
<div class="qt-preview">{{ qt.text.substring(0, 60) }}{{ qt.text.length > 60 ? '...' : '' }}</div>
</div>
<div class="qt-actions">
<button class="icon-btn-sm" @click.stop="openQuickTextDialog(qt)" title="Bearbeiten">
<i class="pi pi-pencil"></i>
</button>
<button class="icon-btn-sm danger" @click.stop="deleteQuickText(qt._id)" title="Löschen">
<i class="pi pi-trash"></i>
</button>
</div>
</div>
</div>
<div v-else class="empty-hint">Keine Quick-Texte. Klicke + zum Erstellen.</div>
</div>
</div>
</div>
<!-- Task-Detail Dialog -->
<TaskEditDialog
:taskId="taskDialogId"
v-model:visible="taskDialogVisible"
@saved="onTaskDialogSaved"
/>
<!-- Quick-Text Dialog -->
<Dialog
v-model:visible="quickTextDialog"
:header="editingQuickText ? 'Quick-Text bearbeiten' : 'Neuer Quick-Text'"
:modal="true"
:style="{ width: '450px' }"
>
<div class="dialog-form">
<div class="form-group">
<label>Titel</label>
<input v-model="qtTitle" class="form-input" placeholder="z.B. Bug melden" />
</div>
<div class="form-group">
<label>Text</label>
<textarea v-model="qtText" class="form-input" rows="4" placeholder="Der Text der eingefügt wird..."></textarea>
</div>
</div>
<template #footer>
<button class="toolbar-btn" @click="quickTextDialog = false">Abbrechen</button>
<button class="toolbar-btn send" @click="saveQuickText" :disabled="!qtTitle || !qtText">Speichern</button>
</template>
</Dialog>
</div>
</template>
<style scoped>
.agent-task-page {
display: flex;
flex-direction: column;
height: calc(100vh - 2rem);
overflow: hidden;
}
.page-header {
flex-shrink: 0;
}
/* Kontext-Zeile */
.context-bar {
display: flex;
gap: 1rem;
margin-bottom: 1rem;
flex-shrink: 0;
}
.context-item {
display: flex;
align-items: center;
gap: 0.5rem;
}
.context-item label {
color: rgba(255, 255, 255, 0.6);
font-size: 0.85rem;
font-weight: 500;
white-space: nowrap;
}
.dropdown-dark {
min-width: 200px;
}
/* 3-Spalten Layout */
.three-col {
display: grid;
grid-template-columns: 1fr 1.2fr 0.8fr;
gap: 1rem;
flex: 1;
min-height: 0;
overflow: hidden;
}
.col-tasks, .col-message, .col-quicktexts {
display: flex;
flex-direction: column;
min-height: 0;
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 12px;
overflow: hidden;
}
.col-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.6rem 0.75rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
flex-shrink: 0;
}
.col-header h3 {
font-size: 0.85rem;
color: rgba(255, 255, 255, 0.6);
margin: 0;
display: flex;
align-items: center;
gap: 0.4rem;
}
/* Task Liste */
.task-list-scroll {
flex: 1;
overflow-y: auto;
min-height: 0;
}
.task-list {
display: flex;
flex-direction: column;
}
.task-list-item {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 0.75rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.04);
cursor: pointer;
transition: background 0.15s;
}
.task-list-item:hover {
background: rgba(255, 255, 255, 0.05);
}
.task-list-item.selected {
background: rgba(99, 102, 241, 0.1);
}
.task-prio {
font-size: 0.6rem;
font-weight: 700;
letter-spacing: 0.03em;
padding: 0.1rem 0.3rem;
border-radius: 3px;
flex-shrink: 0;
min-width: 30px;
text-align: center;
}
.prio-urgent { background: rgba(239, 68, 68, 0.2); color: #fca5a5; }
.prio-high { background: rgba(249, 115, 22, 0.2); color: #fdba74; }
.prio-medium { background: rgba(234, 179, 8, 0.2); color: #fde047; }
.prio-low { background: rgba(34, 197, 94, 0.2); color: #86efac; }
.task-status-badge {
font-size: 0.6rem;
font-weight: 600;
letter-spacing: 0.03em;
padding: 0.1rem 0.35rem;
border-radius: 3px;
flex-shrink: 0;
background: rgba(99, 102, 241, 0.2);
color: #a5b4fc;
white-space: nowrap;
}
.task-title {
flex: 1;
color: rgba(255, 255, 255, 0.8);
font-size: 0.82rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.task-title.clickable {
cursor: pointer;
transition: color 0.15s;
}
.task-title.clickable:hover {
color: #818cf8;
text-decoration: underline;
}
/* Nachricht */
.col-message {
display: flex;
flex-direction: column;
}
.message-area {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
}
.message-input {
flex: 1;
width: 100%;
padding: 0.75rem;
background: transparent;
border: none;
color: #fff;
font-size: 0.9rem;
font-family: inherit;
resize: none;
outline: none;
min-height: 0;
}
.message-input::placeholder {
color: rgba(255, 255, 255, 0.25);
}
.message-toolbar {
display: flex;
justify-content: space-between;
padding: 0.5rem 0.75rem;
border-top: 1px solid rgba(255, 255, 255, 0.06);
flex-shrink: 0;
gap: 0.5rem;
}
.toolbar-btn {
display: inline-flex;
align-items: center;
gap: 0.35rem;
padding: 0.4rem 0.75rem;
background: rgba(255, 255, 255, 0.08);
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 6px;
color: rgba(255, 255, 255, 0.65);
font-size: 0.8rem;
cursor: pointer;
transition: all 0.2s;
}
.toolbar-btn:hover { background: rgba(255, 255, 255, 0.12); color: #fff; }
.toolbar-btn:disabled { opacity: 0.35; cursor: not-allowed; }
.toolbar-btn.active { background: rgba(239, 68, 68, 0.2); border-color: rgba(239, 68, 68, 0.4); color: #fca5a5; }
.toolbar-btn.send { background: rgba(99, 102, 241, 0.2); border-color: rgba(99, 102, 241, 0.3); color: #a5b4fc; }
.toolbar-btn.send:hover:not(:disabled) { background: rgba(99, 102, 241, 0.35); color: #fff; }
@keyframes pulse-glow {
0%, 100% { box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.4); }
50% { box-shadow: 0 0 0 6px rgba(239, 68, 68, 0); }
}
.pulse { animation: pulse-glow 1.5s infinite; }
/* Quick-Texte */
.quicktext-scroll {
flex: 1;
overflow-y: auto;
min-height: 0;
padding: 0.5rem;
}
.qt-list {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.quick-text-card {
background: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 8px;
transition: all 0.15s;
}
.quick-text-card:hover {
border-color: rgba(99, 102, 241, 0.3);
background: rgba(255, 255, 255, 0.06);
}
.qt-content {
padding: 0.5rem 0.65rem;
cursor: pointer;
}
.qt-title {
font-weight: 600;
color: #fff;
font-size: 0.82rem;
margin-bottom: 0.15rem;
}
.qt-preview {
color: rgba(255, 255, 255, 0.4);
font-size: 0.72rem;
line-height: 1.3;
}
.qt-actions {
display: flex;
gap: 0.2rem;
padding: 0.15rem 0.5rem 0.4rem;
}
.icon-btn {
background: rgba(99, 102, 241, 0.15);
border: 1px solid rgba(99, 102, 241, 0.25);
border-radius: 6px;
color: #a5b4fc;
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
font-size: 0.75rem;
transition: all 0.2s;
}
.icon-btn:hover { background: rgba(99, 102, 241, 0.3); color: #fff; }
.icon-btn-sm {
background: none;
border: none;
color: rgba(255, 255, 255, 0.3);
cursor: pointer;
padding: 0.2rem;
font-size: 0.75rem;
transition: color 0.15s;
}
.icon-btn-sm:hover { color: #fff; }
.icon-btn-sm.danger:hover { color: #ef4444; }
.empty-hint {
color: rgba(255, 255, 255, 0.3);
font-size: 0.8rem;
padding: 1rem;
text-align: center;
}
/* Dialog Dark Theme - moved to global style block below */
/* Dialog Form */
.dialog-form { display: flex; flex-direction: column; gap: 1rem; }
.form-group { display: flex; flex-direction: column; gap: 0.3rem; }
.form-group label { color: rgba(255, 255, 255, 0.7); font-size: 0.85rem; font-weight: 500; }
.form-input {
padding: 0.5rem 0.65rem;
background: rgba(255, 255, 255, 0.08);
border: 1px solid rgba(255, 255, 255, 0.15);
border-radius: 6px;
color: #fff;
font-size: 0.9rem;
font-family: inherit;
outline: none;
}
.form-input:focus { border-color: #6366f1; }
.form-input::placeholder { color: rgba(255, 255, 255, 0.3); }
/* Scrollbar styling */
.task-list-scroll::-webkit-scrollbar,
.quicktext-scroll::-webkit-scrollbar {
width: 4px;
}
.task-list-scroll::-webkit-scrollbar-thumb,
.quicktext-scroll::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.1);
border-radius: 2px;
}
.task-list-scroll::-webkit-scrollbar-thumb:hover,
.quicktext-scroll::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.2);
}
/* Responsive — unter 900px: Single-Column */
@media (max-width: 900px) {
.agent-task-page {
height: auto;
overflow: auto;
}
.three-col {
grid-template-columns: 1fr;
overflow: visible;
}
.task-list-scroll {
max-height: 300px;
}
.quicktext-scroll {
max-height: 250px;
}
.col-message {
min-height: 250px;
}
.context-bar {
flex-direction: column;
}
.dropdown-dark {
min-width: 100%;
}
}
</style>
<!-- Dialog Dark Theme now in global style.css -->