971 lines
27 KiB
Vue
971 lines
27 KiB
Vue
<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 -->
|