Files
nexus/frontend/src/components/dashboard/v2/AgentDetailModal.vue
T
devops 2ee1fe973f
CI - Build & Test / Backend (.NET) (push) Successful in 33s
CI - Build & Test / Frontend (Vue/TS) (push) Successful in 17s
CI - Build & Test / Security Check (push) Successful in 3s
CI - Build & Test / Deploy Nexus (push) Has been skipped
feat(board): rework quick-peek modal (Linear-style) + board toolbar
Modal redesign — compact summary instead of two long cards:
- Topbar: breadcrumb + state pill + copyable task-id/link chip; review
  actions (approve / request changes) live in the modal header too
- Main column: title, meta chips (ball, created, last update), compact
  description, then square summary tiles (done/total with progress,
  active, open, blocked) + per-agent pills — details stay in Vollansicht
- Right sidebar: properties as slim Linear-style rows (status, priority,
  assignee, due, source) with hover-reveal controls; activity moved next
  to properties as a compact timeline with a new comment composer
  (POST tasks/{id}/activity via new store action postTaskActivity)
- Unified button system (32px: primary/ghost/approve/changes/icon)
- flattenBoard includes nested children again so quick-peek works for
  child rows inside master cards

Board (issue-tracker basics):
- Toolbar with search (title/detail/id/child titles) and ball filters:
  Alle / Du bist dran / Bei Iris / Haengt

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 12:10:51 +02:00

615 lines
16 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { ref, onMounted, onUnmounted, watch } from 'vue'
import type { AgentDetailData } from './types'
import { icons } from '../../../composables/icons'
const props = defineProps<{
agent: AgentDetailData
agentOrder: string[]
}>()
const emit = defineEmits<{
close: []
select: [id: string]
changeModel: [agentId: string, modelId: string]
}>()
/* ── Progress animation ────────────────────────── */
const displayProgress = ref(0)
function animateProgress() {
displayProgress.value = 0
setTimeout(() => { displayProgress.value = props.agent.progress }, 60)
}
/* ── Typewriter ────────────────────────────────── */
const thinkDisplay = ref('')
let thinkTimer: ReturnType<typeof setInterval> | null = null
function startTypewriter() {
if (thinkTimer) clearInterval(thinkTimer)
thinkDisplay.value = ''
if (!props.agent.think) return
const text = props.agent.think
let i = 0
thinkTimer = setInterval(() => {
thinkDisplay.value = text.slice(0, i)
i = i >= text.length ? 0 : i + 1
}, 38)
}
/* ── Selected model ────────────────────────────── */
const selectedModel = ref(props.agent.model)
watch(() => props.agent.id, () => {
selectedModel.value = props.agent.model
animateProgress()
startTypewriter()
})
function selectModel(alias: string) {
selectedModel.value = alias
emit('changeModel', props.agent.id, alias)
}
/* ── Keyboard / Backdrop ───────────────────────── */
function handleKeydown(e: KeyboardEvent) {
if (e.key === 'Escape') { emit('close'); return }
if (e.key === 'ArrowLeft' || e.key === 'ArrowRight') {
e.preventDefault()
const idx = props.agentOrder.indexOf(props.agent.id)
if (idx === -1) return
const next = e.key === 'ArrowRight'
? (idx + 1) % props.agentOrder.length
: (idx - 1 + props.agentOrder.length) % props.agentOrder.length
emit('select', props.agentOrder[next])
}
}
function onBackdrop(e: MouseEvent) {
if ((e.target as HTMLElement).classList.contains('modal-ov')) emit('close')
}
onMounted(() => {
document.addEventListener('keydown', handleKeydown)
animateProgress()
startTypewriter()
})
onUnmounted(() => {
document.removeEventListener('keydown', handleKeydown)
if (thinkTimer) clearInterval(thinkTimer)
})
const statusColors: Record<string, string> = {
work: 'var(--st-work)',
think: 'var(--st-think)',
idle: 'var(--st-idle)',
block: 'var(--st-block)',
}
function avatarLabel() {
if (props.agent.id === 'iris') return 'IR'
if (props.agent.name === 'Full-Stack Developer') return '</>'
return props.agent.name.slice(0, 2).toUpperCase()
}
</script>
<template>
<div class="modal-ov" @click="onBackdrop">
<div class="modal-card">
<!-- Close -->
<button class="m-close" @click="emit('close')">×</button>
<!-- Header -->
<div class="m-head">
<div :class="['m-av', { iris: agent.id === 'iris' }]">
{{ avatarLabel() }}
</div>
<div style="flex:1; min-width:0">
<div class="m-name">{{ agent.name }}</div>
<div class="m-sub">
<span :class="['badge', agent.roleBadge]">{{ agent.role }}</span>
<span class="m-pill">{{ selectedModel }}</span>
<span class="m-status" :style="{ color: statusColors[agent.status] }">
<span class="dot" :class="agent.status"></span>
{{ agent.statusLabel }}
</span>
</div>
</div>
</div>
<!-- Aktuelle Aufgabe -->
<div v-if="agent.task" class="m-sec">
<h4>Aktuelle Aufgabe</h4>
<div class="m-task">{{ agent.task }}</div>
<div class="m-goal">
<span v-html="icons.target || ''"></span>
Ziel: {{ agent.goal }}
</div>
<div class="m-bar" :class="{ work: agent.status === 'work' }">
<i :style="{ width: displayProgress + '%' }"></i>
</div>
<div class="m-pct-row">
<div class="m-pct grad-tx">{{ displayProgress }}%</div>
<div class="m-next"> {{ agent.next }}</div>
</div>
</div>
<!-- Metriken -->
<div class="m-sec">
<h4>Metriken</h4>
<div class="m-metrics">
<div class="m-metric">
<div class="mk">Elapsed</div>
<div class="mv">{{ agent.elapsed }}</div>
</div>
<div class="m-metric">
<div class="mk">Token</div>
<div class="mv">{{ agent.tokens }}</div>
</div>
<div class="m-metric">
<div class="mk">Kosten</div>
<div class="mv grad-tx">${{ agent.cost }}</div>
</div>
<div class="m-metric">
<div class="mk">Fortschritt</div>
<div class="mv">{{ agent.progress }}%</div>
</div>
</div>
</div>
<!-- Live Thinking -->
<div v-if="agent.think" class="m-sec">
<h4>Live Thinking</h4>
<div class="m-think">{{ thinkDisplay }}<span class="caret"></span></div>
</div>
<div v-if="agent.activity.length" class="m-sec">
<h4>Agent Activity</h4>
<div class="m-activity">
<div v-for="(entry, index) in agent.activity" :key="index" class="m-activity-row">
<span class="m-activity-time">{{ entry.time }}</span>
<span class="m-activity-text">{{ entry.text }}</span>
</div>
</div>
</div>
<!-- Modell wählen -->
<div class="m-sec">
<h4>Modell wählen</h4>
<div class="m-models">
<button
v-for="m in agent.availableModels"
:key="m.id"
:class="['m-model-btn', { active: m.alias === selectedModel }]"
@click="selectModel(m.alias)"
>{{ m.alias }}</button>
</div>
</div>
<!-- MD Footer -->
<div v-if="agent.md" class="m-md">
<span class="dot work"></span>
Synced: <span class="m-md-path">{{ agent.md }}</span>
</div>
</div>
</div>
</template>
<style scoped>
/* ── Overlay ─────────────────────────────────── */
.modal-ov {
position: fixed;
inset: 0;
z-index: 100;
display: flex;
align-items: center;
justify-content: center;
background: rgba(5,4,16,.78);
backdrop-filter: blur(16px);
animation: ov-in .2s ease-out;
}
@keyframes ov-in {
from { opacity: 0; }
to { opacity: 1; }
}
/* ── Card ────────────────────────────────────── */
.modal-card {
width: 640px;
max-height: 88vh;
overflow-y: auto;
border-radius: 20px;
position: relative;
background: linear-gradient(160deg, rgba(22,18,50,.97), rgba(10,8,28,.97));
border: 1px solid rgba(150,140,255,.28);
box-shadow:
0 0 0 1px rgba(124,108,255,.15),
0 32px 80px -16px rgba(0,0,0,.8),
0 0 60px -10px rgba(124,108,255,.35);
animation: card-in .24s cubic-bezier(.2,.8,.3,1);
}
@keyframes card-in {
from { opacity: 0; transform: scale(.94) translateY(12px); }
to { opacity: 1; transform: scale(1) translateY(0); }
}
.modal-card::-webkit-scrollbar { width: 7px; }
.modal-card::-webkit-scrollbar-thumb {
background: rgba(124,108,255,.22);
border-radius: 7px;
border: 2px solid transparent;
background-clip: padding-box;
}
.modal-card::-webkit-scrollbar-track { background: transparent; }
/* ── Close ───────────────────────────────────── */
.m-close {
position: absolute;
top: 16px;
right: 16px;
width: 32px;
height: 32px;
border-radius: 9px;
border: 1px solid var(--line-2);
background: rgba(124,108,255,.07);
color: var(--tx-2);
font-size: 20px;
line-height: 1;
cursor: pointer;
display: grid;
place-items: center;
transition: .15s;
z-index: 2;
}
.m-close:hover {
background: rgba(124,108,255,.18);
color: var(--tx);
}
/* ── Head ────────────────────────────────────── */
.m-head {
display: flex;
align-items: flex-start;
gap: 16px;
padding: 26px 26px 20px;
}
.m-av {
width: 56px;
height: 56px;
border-radius: 15px;
flex: 0 0 auto;
display: grid;
place-items: center;
font-family: 'JetBrains Mono', monospace;
font-weight: 700;
font-size: 16px;
background: var(--grad-soft);
border: 1px solid var(--line-2);
color: var(--tx);
}
.m-av.iris {
background: var(--grad);
color: #fff;
box-shadow: var(--glow-purple);
}
.m-name {
font-family: 'Space Grotesk', sans-serif;
font-size: 21px;
font-weight: 700;
line-height: 1.1;
color: var(--tx);
}
.m-sub {
display: flex;
align-items: center;
gap: 9px;
margin-top: 7px;
flex-wrap: wrap;
}
/* ── Badges ──────────────────────────────────── */
.badge {
display: inline-flex;
align-items: center;
padding: 3px 9px;
border-radius: 7px;
font-size: 11px;
font-weight: 600;
border: 1px solid transparent;
}
.badge-blue { background:rgba(79,124,255,.14); color:#9db6ff; border-color:rgba(79,124,255,.3); }
.badge-purple { background:rgba(181,87,246,.14); color:#d7a8ff; border-color:rgba(181,87,246,.3); }
.badge-amber { background:rgba(251,191,36,.13); color:#fcd34d; border-color:rgba(251,191,36,.3); }
.badge-green { background:rgba(61,220,151,.13); color:#7ef0bd; border-color:rgba(61,220,151,.3); }
.badge-cyan { background:rgba(52,214,245,.13); color:#8ee9fb; border-color:rgba(52,214,245,.3); }
.badge-rose { background:rgba(251,113,133,.13); color:#fda4b0; border-color:rgba(251,113,133,.3); }
.badge-slate { background:rgba(150,140,255,.08); color:var(--tx-2); border-color:var(--line-2); }
.m-pill {
display: inline-flex;
align-items: center;
height: 26px;
padding: 0 11px;
border-radius: 20px;
font-size: 11px;
font-weight: 600;
border: 1px solid var(--line-2);
background: rgba(124,108,255,.07);
color: var(--tx-2);
}
.m-status {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 12.5px;
font-weight: 600;
}
.dot {
width: 7px;
height: 7px;
border-radius: 50%;
flex: 0 0 auto;
}
.dot.work { background: var(--st-work); box-shadow: 0 0 0 0 rgba(61,220,151,.5); animation: pulse-work 1.8s infinite; }
.dot.think { background: var(--st-think); box-shadow: 0 0 0 0 rgba(52,214,245,.5); animation: pulse-think 1.6s infinite; }
.dot.idle { background: var(--st-idle); }
.dot.block { background: var(--st-block); }
/* ── Section ─────────────────────────────────── */
.m-sec {
padding: 16px 26px;
border-top: 1px solid var(--line);
}
.m-sec h4 {
font-size: 10px;
letter-spacing: .18em;
text-transform: uppercase;
color: var(--tx-3);
font-weight: 700;
margin: 0 0 13px;
}
/* ── Task ────────────────────────────────────── */
.m-task {
font-size: 15px;
font-weight: 700;
margin-bottom: 8px;
color: var(--tx);
}
.m-goal {
display: flex;
align-items: center;
gap: 7px;
font-size: 13px;
color: var(--tx-2);
margin-bottom: 14px;
}
.m-goal :deep(svg) {
width: 14px;
height: 14px;
color: var(--a-mid);
flex: 0 0 auto;
}
/* ── Progress Bar ────────────────────────────── */
.m-bar {
height: 10px;
border-radius: 10px;
background: rgba(124,108,255,.12);
overflow: hidden;
position: relative;
}
.m-bar i {
display: block;
height: 100%;
border-radius: 10px;
background: var(--grad);
box-shadow: 0 0 14px -2px rgba(124,108,255,.8);
transition: width .6s ease;
position: relative;
}
.m-bar.work i {
background: linear-gradient(90deg, #2bb87f, #3ddc97);
box-shadow: var(--glow-work);
}
.m-bar i::after {
content: '';
position: absolute;
inset: 0;
background: linear-gradient(90deg, transparent, rgba(255,255,255,.4), transparent);
animation: shimmer 2s linear infinite;
}
@keyframes shimmer {
0% { transform: translateX(-100%); }
100% { transform: translateX(180%); }
}
.m-pct-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 8px;
}
.m-pct {
font-family: 'JetBrains Mono', monospace;
font-size: 22px;
font-weight: 700;
}
.grad-tx {
background: var(--grad);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
.m-next {
font-size: 12px;
color: var(--tx-3);
font-family: 'JetBrains Mono', monospace;
}
/* ── Metrics ─────────────────────────────────── */
.m-metrics {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 10px;
}
.m-metric {
padding: 11px 13px;
border-radius: 12px;
background: rgba(124,108,255,.06);
border: 1px solid var(--line);
}
.mk {
font-size: 10px;
color: var(--tx-3);
font-weight: 700;
letter-spacing: .06em;
text-transform: uppercase;
}
.mv {
font-family: 'JetBrains Mono', monospace;
font-size: 14px;
font-weight: 600;
margin-top: 5px;
color: var(--tx);
}
/* ── Live Thinking ───────────────────────────── */
.m-think {
background: rgba(5,20,36,.7);
border: 1px solid rgba(52,214,245,.2);
border-radius: 13px;
padding: 14px 16px;
font-family: 'JetBrains Mono', monospace;
font-size: 12px;
line-height: 1.7;
color: #9fe8fb;
min-height: 72px;
position: relative;
overflow: hidden;
}
.m-think::before {
content: '▶ thinking';
position: absolute;
top: 10px;
right: 14px;
font-size: 9px;
color: var(--st-think);
letter-spacing: .12em;
opacity: .7;
}
.caret::after {
content: '▍';
animation: blink 1s steps(1) infinite;
color: var(--st-think);
}
@keyframes blink { 50% { opacity: 0; } }
.m-activity {
display: flex;
flex-direction: column;
gap: 10px;
}
.m-activity-row {
display: grid;
grid-template-columns: 72px 1fr;
gap: 10px;
align-items: start;
padding: 10px 12px;
border-radius: 12px;
background: rgba(124,108,255,.06);
border: 1px solid var(--line);
}
.m-activity-time {
font-family: 'JetBrains Mono', monospace;
font-size: 10px;
color: var(--tx-3);
}
.m-activity-text {
font-size: 12px;
line-height: 1.45;
color: var(--tx-2);
}
/* ── Models ──────────────────────────────────── */
.m-models {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.m-model-btn {
height: 34px;
padding: 0 14px;
border-radius: 9px;
font-family: 'Manrope', sans-serif;
font-size: 12.5px;
font-weight: 600;
border: 1px solid var(--line-2);
background: rgba(124,108,255,.06);
color: var(--tx-2);
cursor: pointer;
transition: .15s;
}
.m-model-btn:hover {
border-color: var(--line-3);
color: var(--tx);
}
.m-model-btn.active {
background: var(--grad);
border: none;
color: #fff;
box-shadow: var(--glow-purple);
}
/* ── MD Footer ───────────────────────────────── */
.m-md {
display: flex;
align-items: center;
gap: 9px;
font-family: 'JetBrains Mono', monospace;
font-size: 11.5px;
color: var(--tx-2);
padding: 12px 26px;
border-top: 1px solid var(--line);
}
.m-md .dot { width: 7px; height: 7px; }
.m-md-path { color: var(--tx-3); }
</style>