Compare commits

..

10 Commits

Author SHA1 Message Date
devops 0f9809e423 chore: bump version to v0.2.25 [skip ci] 2026-06-09 21:01:16 +00:00
developer c2736d20c1 feat: Cards, Offene Aufgaben, Feed – Komplettumbau
CI - Build & Test / Backend (.NET) (push) Successful in 23s
CI - Build & Test / Frontend (Vue/TS) (push) Successful in 16s
CI - Build & Test / Security Check (push) Successful in 3s
TeamNetwork: Footer→Arrow, Current Task+Runtime inline
Missions→Offene Aufgaben (TaskCard) mit +New Task, Iris/Bao-Quelle
OperationsFeed: Text-Wrap, 5 Items, Mehr-Button→Tag-Navigation-Modal
2026-06-09 23:00:26 +02:00
devops 084cff4fe6 chore: bump version to v0.2.24 [skip ci] 2026-06-09 20:45:02 +00:00
developer ef3fc6039e fix: Modal-Layout – Dots bei Current Task + Reihenfolge + Timestamps
CI - Build & Test / Backend (.NET) (push) Successful in 26s
CI - Build & Test / Frontend (Vue/TS) (push) Successful in 16s
CI - Build & Test / Security Check (push) Successful in 3s
- Dots (blau/violett) rechts von Current Task, unterschiedlich pulsierend
- Reihenfolge: Current Task → Live Thinking → Goal → Working Feed
- Link-Button enger am Namen (margin-left:4px, opacity 0.4)
- Working Feed mit Timestamps (step.time)
- Workload-Tag aus Footer entfernt
2026-06-09 22:44:13 +02:00
devops 3599513128 chore: bump version to v0.2.23 [skip ci] 2026-06-09 20:36:05 +00:00
developer 7dd8f53f2f fix: Dots rechts vom Thinking-Text + Glow intensiver + Link-Button
CI - Build & Test / Backend (.NET) (push) Successful in 25s
CI - Build & Test / Frontend (Vue/TS) (push) Successful in 16s
CI - Build & Test / Security Check (push) Successful in 2s
- Dots (blau+lila) inline rechts vom aktuellen Thinking-Eintrag, rotierend
- Orbiter-Rahmen entfernt
- Panel-Glow verstärkt (border 0.5, shadow 24px+40px)
- ExternalLink-Button rechts vom Agent-Namen → /agents/{id}
2026-06-09 22:35:15 +02:00
devops 90bb7251e3 chore: bump version to v0.2.22 [skip ci] 2026-06-09 20:31:44 +00:00
developer e57bef95e5 fix: mehr Abstand Iris↔Grid + Linien enger gebündelt
CI - Build & Test / Backend (.NET) (push) Successful in 23s
CI - Build & Test / Frontend (Vue/TS) (push) Successful in 16s
CI - Build & Test / Security Check (push) Successful in 2s
- gap: 32px → 64px (doppelter Vertikalraum)
- startX: 0.30+0.40 → 0.38+0.24 (enger unter Iris)
- cp1y: startY+40 → startY+70 (tiefer vor Spread)
- cp2x: ±50 → ±35 (sanftere Card-Annäherung)
2026-06-09 22:30:55 +02:00
devops 71b4465595 chore: bump version to v0.2.21 [skip ci] 2026-06-09 20:28:41 +00:00
developer 9b63e5368e feat: Live Thinking Panel im AgentModal
CI - Build & Test / Backend (.NET) (push) Successful in 22s
CI - Build & Test / Frontend (Vue/TS) (push) Successful in 15s
CI - Build & Test / Security Check (push) Successful in 3s
- Scrollbarer Thinking-Stream (slide-in von links)
- Pulsierender Rahmen mit Glow-Effekt
- Blau+Violett Dots rotieren im Uhrzeigersinn
- thinkingStream in AgentNodeData + Beispieldaten für alle 5 Agenten
2026-06-09 22:27:53 +02:00
7 changed files with 692 additions and 296 deletions
+1 -1
View File
@@ -1 +1 @@
0.2.20
0.2.25
+148 -12
View File
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { X } from '@lucide/vue'
import { X, ExternalLink } from '@lucide/vue'
import type { AgentNodeData } from '../../composables/useDashboardData'
defineProps<{
@@ -25,6 +25,9 @@ defineEmits<{
<div>
<h2>{{ agent.name }}</h2>
<span class="modal-role">{{ agent.role }}</span>
<a :href="`/agents/${agent.id}`" class="agent-link-btn" title="Open agent config">
<ExternalLink :size="12" />
</a>
</div>
</div>
<button class="modal-close-btn" @click="$emit('close')" aria-label="Close">
@@ -38,7 +41,34 @@ defineEmits<{
<!-- Current Task -->
<section class="modal-section">
<h3 class="section-label">Current Task</h3>
<p class="section-value">{{ agent.currentTask }}</p>
<p class="section-value">
{{ agent.currentTask }}
<span class="thinking-dots">
<span class="thinking-dot blue"></span>
<span class="thinking-dot violet"></span>
</span>
</p>
</section>
<!-- Live Thinking -->
<section class="modal-section">
<h3 class="section-label">Live Thinking</h3>
<div class="thinking-panel">
<div class="thinking-stream" ref="thinkingStreamRef">
<div
v-for="(msg, idx) in agent.thinkingStream"
:key="idx"
class="thinking-entry"
:style="{ animationDelay: `${idx * 0.05}s` }"
>
<span class="entry-time">{{ msg.time }}</span>
<span class="entry-text">{{ msg.text }}</span>
</div>
<div v-if="!agent.thinkingStream?.length" class="thinking-placeholder">
Waiting for thought stream...
</div>
</div>
</div>
</section>
<!-- Goal + Progress -->
@@ -66,7 +96,8 @@ defineEmits<{
class="work-step"
>
<span class="step-dot"></span>
<span class="step-text">{{ step }}</span>
<span class="step-time">{{ step.time }}</span>
<span class="step-text">{{ step.text }}</span>
</div>
</div>
</section>
@@ -74,15 +105,6 @@ defineEmits<{
<!-- Footer Stats -->
<div class="modal-footer">
<span class="footer-badge">Runtime: {{ runtime }}</span>
<span
class="footer-badge"
:style="{
color: agent.workload > 65 ? '#eab308' : '#22c55e',
borderColor: agent.workload > 65 ? 'rgba(234,179,8,0.2)' : 'rgba(34,197,94,0.2)',
}"
>
Workload: {{ agent.workload }}%
</span>
</div>
</div>
</div>
@@ -168,6 +190,29 @@ defineEmits<{
color: #6b7385;
font-weight: 500;
}
.agent-link-btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
margin-left: 4px;
border: none;
border-radius: 6px;
background: transparent;
color: #6b7385;
opacity: 0.4;
cursor: pointer;
transition: opacity 0.2s;
flex-shrink: 0;
text-decoration: none;
vertical-align: middle;
}
.agent-link-btn:hover {
opacity: 1;
color: var(--agent-color);
}
.modal-close-btn {
width: 32px;
height: 32px;
@@ -211,6 +256,9 @@ defineEmits<{
font-size: 12px;
color: #e8eaf0;
line-height: 1.4;
display: flex;
align-items: center;
gap: 8px;
}
/* Progress */
@@ -241,6 +289,87 @@ defineEmits<{
transition: width 0.5s ease;
}
/* Live Thinking */
.thinking-panel {
position: relative;
border: 1px solid rgba(139, 124, 246, 0.2);
border-radius: 12px;
padding: 14px;
background: rgba(12, 16, 22, 0.6);
overflow: hidden;
animation: panel-pulse 2.5s ease-in-out infinite;
}
@keyframes panel-pulse {
0%, 100% { border-color: rgba(139, 124, 246, 0.25); box-shadow: 0 0 12px rgba(139,124,246,0.08); }
50% { border-color: rgba(139, 124, 246, 0.5); box-shadow: 0 0 24px rgba(139,124,246,0.18), 0 0 40px rgba(139,124,246,0.06); }
}
.thinking-dots {
display: inline-flex;
gap: 6px;
flex-shrink: 0;
}
.thinking-dot {
width: 7px;
height: 7px;
border-radius: 50%;
}
.thinking-dot.blue {
background: #3b82f6;
box-shadow: 0 0 8px #3b82f6;
animation: pulse-dot-blue 1.2s ease-in-out infinite;
}
.thinking-dot.violet {
background: #8b7cf6;
box-shadow: 0 0 8px #8b7cf6;
animation: pulse-dot-violet 1.8s ease-in-out infinite 0.3s;
}
@keyframes pulse-dot-blue {
0%, 100% { opacity: 0.4; transform: scale(0.7); }
50% { opacity: 1; transform: scale(1.3); }
}
@keyframes pulse-dot-violet {
0%, 100% { opacity: 0.3; transform: scale(0.6); }
50% { opacity: 1; transform: scale(1.4); }
}
.thinking-stream {
max-height: 160px;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 6px;
position: relative;
z-index: 1;
}
.thinking-entry {
display: flex;
gap: 10px;
align-items: baseline;
animation: slide-in-right 0.3s ease-out both;
font-size: 10px;
}
@keyframes slide-in-right {
from { opacity: 0; transform: translateX(-16px); }
to { opacity: 1; transform: translateX(0); }
}
.entry-time {
font-size: 8.5px;
color: #6b7385;
flex-shrink: 0;
font-variant-numeric: tabular-nums;
min-width: 42px;
}
.entry-text {
color: #9ea5b3;
line-height: 1.4;
}
.thinking-placeholder {
font-size: 10px;
color: #4a5160;
font-style: italic;
}
/* Working Feed */
.work-feed {
display: flex;
@@ -265,6 +394,13 @@ defineEmits<{
color: #7e8799;
line-height: 1.35;
}
.step-time {
font-size: 8.5px;
color: #6b7385;
flex-shrink: 0;
font-variant-numeric: tabular-nums;
min-width: 36px;
}
/* Footer */
.modal-footer {
+180 -136
View File
@@ -1,79 +1,77 @@
<script setup lang="ts">
import { computed } from 'vue'
import { Clock, ChevronRight } from '@lucide/vue'
import type { MissionData } from '../../composables/useDashboardData'
import { ref } from 'vue'
import { Plus, Circle } from '@lucide/vue'
import type { OpenTask } from '../../composables/useDashboardData'
const props = defineProps<{
mission: MissionData
defineProps<{
tasks: OpenTask[]
}>()
const statusColor: Record<string, string> = {
healthy: '#22c55e',
attention: '#eab308',
blocked: '#ef4444',
paused: '#6b7280',
}
const emit = defineEmits<{
newTask: []
}>()
const statusLabel = computed(() => {
const map: Record<string, string> = {
healthy: 'Healthy',
attention: 'Warning',
blocked: 'Blocked',
paused: 'Paused',
const expandedId = ref<string | null>(null)
function toggleExpand(id: string) {
expandedId.value = expandedId.value === id ? null : id
}
return map[props.mission.status] ?? props.mission.status
})
</script>
<template>
<article class="mission-card" tabindex="0">
<div class="mission-head">
<h3>{{ mission.name }}</h3>
<span
class="mission-status"
:style="{ color: statusColor[mission.status] }"
<div class="task-card-panel">
<div class="task-header">
<h2 class="task-title">Offene Aufgaben</h2>
<button class="new-task-btn" @click="emit('newTask')">
<Plus :size="12" />
<span>New Task</span>
</button>
</div>
<div class="task-list">
<div v-if="tasks.length === 0" class="task-empty">
Keine offenen Aufgaben. Erstelle eine mit + New Task.
</div>
<TransitionGroup name="task">
<div
v-for="task in tasks"
:key="task.id"
class="task-item"
:class="{ expanded: expandedId === task.id }"
@click="toggleExpand(task.id)"
>
{{ statusLabel }}
<div class="task-main">
<Circle
:size="8"
class="task-source-dot"
:class="task.source === 'iris' ? 'dot-iris' : 'dot-bao'"
fill="currentColor"
/>
<div class="task-content">
<div class="task-title-row">
<span class="task-name">{{ task.title }}</span>
<span class="task-time">{{ task.createdAt }}</span>
</div>
<span
class="task-source-tag"
:class="task.source === 'iris' ? 'tag-iris' : 'tag-bao'"
>
{{ task.source === 'iris' ? 'Iris' : 'Bao' }}
</span>
</div>
<div class="progress-track">
<div
class="progress-fill"
:style="{
width: `${mission.progress}%`,
background: `linear-gradient(90deg, ${statusColor[mission.status]}, color-mix(in srgb, ${statusColor[mission.status]} 65%, #fff))`,
}"
></div>
</div>
<div class="mission-body">
<div class="mission-detail">
<span class="detail-label">Current Task</span>
<span class="detail-value">{{ mission.currentTask }}</span>
</div>
<div class="mission-footer">
<div class="mission-meta">
<Clock :size="10" />
<span>{{ mission.lastActivity }}</span>
</div>
<div class="mission-tasks">
<span class="tasks-count">{{ mission.remainingTasks }}</span>
<span class="tasks-label">remaining</span>
<div v-if="expandedId === task.id" class="task-detail">
{{ task.detail }}
</div>
</div>
</TransitionGroup>
</div>
<div class="mission-arrow">
<ChevronRight :size="14" />
</div>
</article>
</template>
<style scoped>
.mission-card {
position: relative;
.task-card-panel {
display: flex;
flex-direction: column;
gap: 10px;
@@ -81,121 +79,167 @@ const statusLabel = computed(() => {
background: rgba(22, 27, 34, 0.65);
border: 1px solid rgba(139, 124, 246, 0.08);
border-radius: 14px;
cursor: pointer;
transition: all 0.25s ease;
transition: border-color 0.2s ease;
backdrop-filter: blur(6px);
-webkit-backdrop-filter: blur(6px);
}
.mission-card:hover {
border-color: rgba(139, 124, 246, 0.2);
transform: translateY(-1px);
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
}
.mission-card:focus-visible {
outline: 2px solid #a78bfa;
outline-offset: 2px;
.task-card-panel:hover {
border-color: rgba(139, 124, 246, 0.15);
}
.mission-head {
.task-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
.mission-head h3 {
.task-title {
margin: 0;
font-size: 12px;
font-size: 11px;
font-weight: 600;
color: #e8eaf0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.mission-status {
font-size: 8px;
.new-task-btn {
display: flex;
align-items: center;
gap: 4px;
padding: 4px 10px;
background: rgba(139, 124, 246, 0.12);
border: 1px solid rgba(139, 124, 246, 0.2);
border-radius: 6px;
color: #a78bfa;
font-size: 9px;
font-weight: 600;
text-transform: capitalize;
letter-spacing: 0.04em;
flex-shrink: 0;
cursor: pointer;
transition: all 0.2s;
}
.new-task-btn:hover {
background: rgba(139, 124, 246, 0.2);
border-color: rgba(139, 124, 246, 0.35);
}
.progress-track {
height: 3px;
background: rgba(255, 255, 255, 0.06);
border-radius: 3px;
overflow: hidden;
}
.progress-fill {
height: 100%;
border-radius: 3px;
transition: width 0.5s ease;
}
.mission-body {
.task-list {
display: flex;
flex-direction: column;
gap: 4px;
}
.task-item {
display: flex;
flex-direction: column;
gap: 4px;
padding: 8px 10px;
border-radius: 8px;
cursor: pointer;
transition: background 0.15s;
border: 1px solid transparent;
}
.task-item:hover {
background: rgba(255, 255, 255, 0.03);
border-color: rgba(139, 124, 246, 0.08);
}
.task-item.expanded {
background: rgba(139, 124, 246, 0.04);
border-color: rgba(139, 124, 246, 0.1);
}
.task-main {
display: flex;
align-items: flex-start;
gap: 8px;
}
.mission-detail {
.task-source-dot {
margin-top: 4px;
flex-shrink: 0;
}
.dot-iris {
color: #a78bfa;
}
.dot-bao {
color: #3b82f6;
}
.task-content {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 3px;
}
.detail-label {
font-size: 8px;
color: #6b7385;
text-transform: uppercase;
letter-spacing: 0.06em;
}
.detail-value {
font-size: 10px;
color: #7e8799;
line-height: 1.35;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.mission-footer {
.task-title-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
.mission-meta {
display: flex;
align-items: center;
gap: 4px;
font-size: 9px;
.task-name {
font-size: 10px;
font-weight: 500;
color: #d1d5db;
line-height: 1.35;
}
.task-time {
font-size: 8.5px;
color: #6b7385;
}
.mission-tasks {
display: flex;
align-items: center;
gap: 4px;
}
.tasks-count {
font-size: 12px;
font-weight: 700;
color: #a78bfa;
flex-shrink: 0;
font-variant-numeric: tabular-nums;
}
.tasks-label {
.task-source-tag {
display: inline-block;
font-size: 8px;
color: #6b7385;
text-transform: uppercase;
letter-spacing: 0.04em;
font-weight: 600;
padding: 1px 7px;
border-radius: 4px;
letter-spacing: 0.02em;
align-self: flex-start;
line-height: 1.4;
}
.tag-iris {
background: rgba(167, 139, 250, 0.15);
color: #a78bfa;
}
.tag-bao {
background: rgba(59, 130, 246, 0.15);
color: #3b82f6;
}
.mission-arrow {
position: absolute;
right: 10px;
bottom: 10px;
color: #6b7385;
opacity: 0;
transition: opacity 0.2s ease, transform 0.2s ease;
.task-detail {
padding: 6px 10px;
margin: 0 0 2px 16px;
font-size: 9.5px;
color: #7e8799;
line-height: 1.45;
background: rgba(0, 0, 0, 0.15);
border-radius: 6px;
border-left: 2px solid rgba(139, 124, 246, 0.2);
}
.mission-card:hover .mission-arrow {
opacity: 1;
transform: translateX(2px);
.task-empty {
text-align: center;
padding: 16px 8px;
font-size: 10px;
color: #6b7385;
line-height: 1.5;
}
/* TransitionGroup */
.task-enter-active {
transition: all 0.3s ease;
}
.task-leave-active {
transition: all 0.3s ease;
position: absolute;
}
.task-enter-from {
opacity: 0;
transform: translateY(-6px);
}
.task-leave-to {
opacity: 0;
transform: translateY(6px);
}
.task-move {
transition: transform 0.3s ease;
}
</style>
@@ -1,10 +1,52 @@
<script setup lang="ts">
import { Activity } from '@lucide/vue'
import { ref, computed } from 'vue'
import { Activity, ChevronLeft, ChevronRight, X } from '@lucide/vue'
import type { FeedEntry } from '../../composables/useDashboardData'
defineProps<{
const props = defineProps<{
entries: FeedEntry[]
}>()
// ── Compact feed (5 items) ──
const compactEntries = computed(() => props.entries.slice(0, 5))
// ── Feed Detail Modal ──
const showDetailModal = ref(false)
const selectedDayOffset = ref(0) // 0 = today, -1 = yesterday, etc.
function openDetailModal() {
selectedDayOffset.value = 0
showDetailModal.value = true
}
function closeDetailModal() {
showDetailModal.value = false
}
function dayLabel(offset: number): string {
if (offset === 0) return 'Heute'
if (offset === -1) return 'Gestern'
if (offset === -2) return 'Vorgestern'
const d = new Date()
d.setDate(d.getDate() + offset)
return d.toLocaleDateString('de-DE', { weekday: 'long', day: 'numeric', month: 'long' })
}
function navigateDay(dir: -1 | 1) {
const next = selectedDayOffset.value + dir
if (next >= -6 && next <= 0) {
selectedDayOffset.value = next
}
}
const filteredEntries = computed(() => {
const targetDate = new Date()
targetDate.setDate(targetDate.getDate() + selectedDayOffset.value)
const targetStr = targetDate.toISOString().slice(0, 10)
return props.entries.filter(e => e.timestamp.slice(0, 10) === targetStr)
})
</script>
<template>
@@ -17,7 +59,7 @@ defineProps<{
<div class="feed-list">
<TransitionGroup name="feed">
<div
v-for="(entry, idx) in entries.slice(0, 8)"
v-for="(entry, idx) in compactEntries"
:key="entry.timestamp + '-' + idx"
class="feed-entry"
>
@@ -33,7 +75,61 @@ defineProps<{
<div v-if="entries.length === 0" class="feed-empty">
<span>No operations recorded yet.</span>
</div>
<button v-if="entries.length > 5" class="feed-more-btn" @click="openDetailModal">
Mehr anzeigen
</button>
</div>
<!-- Feed Detail Modal -->
<Teleport to="body">
<div v-if="showDetailModal" class="modal-overlay" @click.self="closeDetailModal">
<div class="modal-content">
<div class="modal-header">
<h2 class="modal-title">Operations Log</h2>
<button class="modal-close-btn" @click="closeDetailModal">
<X :size="16" />
</button>
</div>
<div class="modal-nav">
<button
class="nav-btn"
:disabled="selectedDayOffset <= -6"
@click="navigateDay(-1)"
>
<ChevronLeft :size="14" />
</button>
<span class="nav-label">{{ dayLabel(selectedDayOffset) }}</span>
<button
class="nav-btn"
:disabled="selectedDayOffset >= 0"
@click="navigateDay(1)"
>
<ChevronRight :size="14" />
</button>
</div>
<div class="modal-entries">
<div v-if="filteredEntries.length === 0" class="modal-empty">
Keine Einträge für diesen Tag.
</div>
<div
v-for="(entry, idx) in filteredEntries"
:key="entry.timestamp + '-' + idx"
class="feed-entry"
>
<span class="feed-time">{{ entry.time }}</span>
<span class="feed-bullet">&middot;</span>
<span class="feed-agent" :class="'agent-' + entry.agent.toLowerCase()">
{{ entry.agent }}
</span>
<span class="feed-action">{{ entry.action }}</span>
</div>
</div>
</div>
</div>
</Teleport>
</div>
</template>
@@ -121,9 +217,8 @@ defineProps<{
}
.feed-action {
color: #7e8799;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
white-space: normal;
word-break: break-word;
}
.feed-empty {
@@ -133,6 +228,26 @@ defineProps<{
color: #6b7385;
}
.feed-more-btn {
display: block;
width: 100%;
padding: 8px;
margin-top: 4px;
background: rgba(139, 124, 246, 0.08);
border: 1px solid rgba(139, 124, 246, 0.12);
border-radius: 8px;
color: #a78bfa;
font-size: 9.5px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
text-align: center;
}
.feed-more-btn:hover {
background: rgba(139, 124, 246, 0.14);
border-color: rgba(139, 124, 246, 0.2);
}
/* TransitionGroup */
.feed-enter-active {
transition: all 0.3s ease;
@@ -152,4 +267,104 @@ defineProps<{
.feed-move {
transition: transform 0.3s ease;
}
/* ── Modal Overlay ── */
:global(.modal-overlay) {
position: fixed;
inset: 0;
z-index: 1000;
display: grid;
place-items: center;
background: rgba(0, 0, 0, 0.6);
backdrop-filter: blur(4px);
padding: 20px;
}
:global(.modal-content) {
background: #161b22;
border: 1px solid rgba(139, 124, 246, 0.15);
border-radius: 16px;
padding: 24px;
width: 100%;
max-width: 520px;
max-height: 80vh;
display: flex;
flex-direction: column;
gap: 16px;
box-shadow: 0 8px 40px rgba(0, 0, 0, 0.5);
}
:global(.modal-header) {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
:global(.modal-title) {
margin: 0;
font-size: 15px;
font-weight: 600;
color: #e8eaf0;
}
:global(.modal-close-btn) {
display: grid;
place-items: center;
width: 28px;
height: 28px;
border: none;
background: rgba(255, 255, 255, 0.05);
border-radius: 6px;
color: #7e8799;
cursor: pointer;
transition: all 0.15s;
}
:global(.modal-close-btn:hover) {
background: rgba(255, 255, 255, 0.1);
color: #e8eaf0;
}
:global(.modal-nav) {
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
}
:global(.nav-btn) {
display: grid;
place-items: center;
width: 30px;
height: 30px;
border: 1px solid rgba(139, 124, 246, 0.15);
background: rgba(139, 124, 246, 0.08);
border-radius: 8px;
color: #a78bfa;
cursor: pointer;
transition: all 0.15s;
}
:global(.nav-btn:hover:not(:disabled)) {
background: rgba(139, 124, 246, 0.16);
border-color: rgba(139, 124, 246, 0.3);
}
:global(.nav-btn:disabled) {
opacity: 0.3;
cursor: not-allowed;
}
:global(.nav-label) {
font-size: 12px;
font-weight: 600;
color: #d1d5db;
min-width: 100px;
text-align: center;
}
:global(.modal-entries) {
display: flex;
flex-direction: column;
gap: 4px;
overflow-y: auto;
max-height: 50vh;
padding-right: 4px;
}
:global(.modal-empty) {
text-align: center;
padding: 24px 0;
font-size: 11px;
color: #6b7385;
}
</style>
@@ -118,7 +118,7 @@ const connectionPaths = computed<Record<string, ConnectionPath | null>>(() => {
// Spread start points across Iris bottom edge (30%-70% range)
const t = total > 1 ? idx / (total - 1) : 0.5
const startX = iris.left + iris.width * (0.30 + t * 0.40)
const startX = iris.left + iris.width * (0.38 + t * 0.24)
const startY = iris.bottom - 1
// Determine column: left or right of Iris center
@@ -130,8 +130,8 @@ const connectionPaths = computed<Record<string, ConnectionPath | null>>(() => {
// Bézier control points
const cp1x = startX
const cp1y = startY + 40
const cp2x = endX + (isLeftColumn ? 50 : -50)
const cp1y = startY + 70
const cp2x = endX + (isLeftColumn ? 35 : -35)
const cp2y = endY - 10
const d = `M ${startX} ${startY} C ${cp1x} ${cp1y}, ${cp2x} ${cp2y}, ${endX} ${endY}`
@@ -356,19 +356,20 @@ onUnmounted(() => {
<span class="card-role-tag" :style="{ background: `${hero.color}18`, color: hero.color, borderColor: `${hero.color}30` }">{{ hero.role }}</span>
</div>
<p class="card-desc">{{ hero.description }}</p>
<span v-if="hero.task" class="node-task">
<div v-if="hero.task" class="task-row">
<span class="node-task">
<span class="node-task-dot"></span>
{{ hero.task }}
</span>
<span v-if="hero.runtime" class="node-runtime">{{ hero.runtime }}</span>
</div>
<div class="card-tags">
<span v-for="tag in hero.tags" :key="tag" class="card-tag" :style="{ background: `${hero.color}18`, color: hero.color }">{{ tag }}</span>
</div>
</div>
</div>
<div class="card-footer-action">
<span>ROLE CARD</span>
<span class="arrow">&rarr;</span>
<span v-if="hero.runtime" class="node-runtime">{{ hero.runtime }}</span>
<div class="card-arrow">
<span class="arrow-icon">&rarr;</span>
</div>
</article>
</div>
@@ -402,19 +403,20 @@ onUnmounted(() => {
<span class="card-role-tag" :style="{ background: `${agent.color}18`, color: agent.color, borderColor: `${agent.color}30` }">{{ agent.role }}</span>
</div>
<p class="card-desc">{{ agent.description }}</p>
<span v-if="agent.task" class="node-task">
<div v-if="agent.task" class="task-row">
<span class="node-task">
<span class="node-task-dot"></span>
{{ agent.task }}
</span>
<span v-if="agent.runtime" class="node-runtime">{{ agent.runtime }}</span>
</div>
<div class="card-tags">
<span v-for="tag in agent.tags" :key="tag" class="card-tag" :style="{ background: `${agent.color}18`, color: agent.color }">{{ tag }}</span>
</div>
</div>
</div>
<div class="card-footer-action">
<span>ROLE CARD</span>
<span class="arrow">&rarr;</span>
<span v-if="agent.runtime" class="node-runtime">{{ agent.runtime }}</span>
<div class="card-arrow">
<span class="arrow-icon">&rarr;</span>
</div>
</article>
</div>
@@ -445,7 +447,7 @@ onUnmounted(() => {
display: flex;
flex-direction: column;
align-items: center;
gap: 32px;
gap: 64px;
}
.hero-slot {
@@ -535,6 +537,38 @@ onUnmounted(() => {
line-height: 1.5;
margin: 0 0 8px;
}
/* ── Task + Runtime Row ── */
.task-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
margin-bottom: 8px;
}
.node-task {
display: inline-flex;
align-items: center;
font-size: 10px;
color: #9ea5b3;
line-height: 1.4;
flex: 1;
min-width: 0;
}
.node-task-dot {
display: inline-block;
margin-right: 4px;
font-size: 8px;
vertical-align: middle;
}
.node-runtime {
font-size: 9px;
color: #6b7385;
font-variant-numeric: tabular-nums;
flex-shrink: 0;
}
/* ── Tags ── */
.card-tags {
display: flex;
flex-wrap: wrap;
@@ -548,49 +582,25 @@ onUnmounted(() => {
border-radius: 5px;
letter-spacing: 0.02em;
}
.card-footer-action {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 6px;
margin-top: 12px;
padding-top: 10px;
border-top: 1px solid var(--line, #1f2330);
font-size: 9px;
font-weight: 600;
/* ── Hover Arrow (bottom-right) ── */
.card-arrow {
position: absolute;
right: 12px;
bottom: 12px;
color: #6b7385;
text-transform: uppercase;
letter-spacing: 0.06em;
opacity: 0;
transform: translateX(-6px);
transition: opacity 0.2s ease, transform 0.2s ease;
}
.card-footer-action .arrow {
font-size: 13px;
.agent-card:hover .card-arrow {
opacity: 1;
transform: translateX(0);
}
.arrow-icon {
font-size: 14px;
line-height: 1;
}
.agent-card:hover .card-footer-action {
color: var(--card-color, #8b7cf6);
}
/* ── Node Task ── */
.node-task {
display: block;
font-size: 10px;
color: #9ea5b3;
margin-bottom: 8px;
line-height: 1.4;
}
.node-task-dot {
display: inline-block;
margin-right: 4px;
font-size: 8px;
vertical-align: middle;
}
/* ── Node Runtime ── */
.node-runtime {
font-size: 9px;
color: #6b7385;
font-variant-numeric: tabular-nums;
margin-left: auto;
}
@media (max-width: 720px) {
+67 -75
View File
@@ -13,24 +13,23 @@ export interface AgentNodeData {
workload: number // 0-100
active: boolean
runtimeSeconds: number
workingFeed: string[]
workingFeed: Array<{ time: string; text: string }>
thinkingStream?: Array<{ time: string; text: string }>
}
export interface MissionData {
export interface OpenTask {
id: string
name: string
progress: number
currentTask: string
lastActivity: string
remainingTasks: number
status: 'healthy' | 'attention' | 'blocked' | 'paused'
title: string
detail: string
source: 'bao' | 'iris'
createdAt: string
}
export interface FeedEntry {
time: string
agent: string
action: string
timestamp: number
timestamp: string
}
export interface ChatMessage {
@@ -113,10 +112,15 @@ export function useDashboardData() {
active: true,
runtimeSeconds: 28800,
workingFeed: [
'Analyzed user feedback on Dashboard',
'Delegated card redesign to Developer',
'Verifying full-width layout deployment',
'Reviewing AgentModal integration',
{ time: '22:38', text: 'Analyzed user feedback on Dashboard' },
{ time: '22:36', text: 'Delegated card redesign to Developer' },
{ time: '22:34', text: 'Verifying full-width layout deployment' },
{ time: '22:32', text: 'Reviewing AgentModal integration' },
],
thinkingStream: [
{ time: '22:24', text: 'Analysing constraint: full-width layout' },
{ time: '22:25', text: 'Removing max-width from global CSS' },
{ time: '22:26', text: 'Verifying Dashboard grid reflow' },
],
},
{
@@ -133,10 +137,15 @@ export function useDashboardData() {
active: true,
runtimeSeconds: 3600,
workingFeed: [
'Created DungeonController',
'Defined dungeon schema',
'Implementing room generation algorithm',
'Writing unit tests for RoomFactory',
{ time: '22:30', text: 'Created DungeonController' },
{ time: '22:28', text: 'Defined dungeon schema' },
{ time: '22:26', text: 'Implementing room generation algorithm' },
{ time: '22:24', text: 'Writing unit tests for RoomFactory' },
],
thinkingStream: [
{ time: '22:22', text: 'Parsing dungeon spec from Iris' },
{ time: '22:23', text: 'Designing RoomFactory interface' },
{ time: '22:24', text: 'Implementing corridor connection logic' },
],
},
{
@@ -153,10 +162,15 @@ export function useDashboardData() {
active: false,
runtimeSeconds: 1800,
workingFeed: [
'Analyzed Docker layer cache',
'Optimized COPY order in Dockerfile',
'Added .dockerignore for node_modules',
'Testing incremental builds',
{ time: '22:20', text: 'Analyzed Docker layer cache' },
{ time: '22:18', text: 'Optimized COPY order in Dockerfile' },
{ time: '22:16', text: 'Added .dockerignore for node_modules' },
{ time: '22:14', text: 'Testing incremental builds' },
],
thinkingStream: [
{ time: '22:20', text: 'Checking build cache hit rates' },
{ time: '22:21', text: 'Benchmarking multi-stage vs single-stage' },
{ time: '22:22', text: 'Calculating potential speedup from caching' },
],
},
{
@@ -173,9 +187,14 @@ export function useDashboardData() {
active: true,
runtimeSeconds: 2700,
workingFeed: [
'Evaluated WebSocket vs SSE vs WebRTC',
'Documented SignalR limitations',
'Prototyping WebSocket fallback',
{ time: '22:18', text: 'Evaluated WebSocket vs SSE vs WebRTC' },
{ time: '22:17', text: 'Documented SignalR limitations' },
{ time: '22:16', text: 'Prototyping WebSocket fallback' },
],
thinkingStream: [
{ time: '22:18', text: 'Cross-referencing WebSocket latency benchmarks' },
{ time: '22:19', text: 'Checking SSE browser support matrix' },
{ time: '22:20', text: 'Drafting recommendation summary' },
],
},
{
@@ -192,64 +211,37 @@ export function useDashboardData() {
active: false,
runtimeSeconds: 900,
workingFeed: [
'Reviewed DungeonController.cs',
'Found 3 minor style issues',
'Approved RoomValidator',
'Running integration tests',
{ time: '22:15', text: 'Reviewed DungeonController.cs' },
{ time: '22:14', text: 'Found 3 minor style issues' },
{ time: '22:13', text: 'Approved RoomValidator' },
{ time: '22:12', text: 'Running integration tests' },
],
thinkingStream: [
{ time: '22:15', text: 'Analyzing DungeonController PR diff' },
{ time: '22:16', text: 'Checking RoomValidator edge cases' },
{ time: '22:17', text: 'Verifying integration test coverage' },
],
},
])
// Missions
const missions = ref<MissionData[]>([
{
id: 'dungeon-system',
name: 'Dungeon System',
progress: 62,
currentTask: 'Implement room generation',
lastActivity: '3 min ago',
remainingTasks: 8,
status: 'healthy',
},
{
id: 'dashboard-redesign',
name: 'Dashboard Redesign',
progress: 45,
currentTask: 'AI Team Network layout',
lastActivity: 'Just now',
remainingTasks: 6,
status: 'healthy',
},
{
id: 'infra-optimization',
name: 'Infra Optimization',
progress: 30,
currentTask: 'Optimize build caching',
lastActivity: '12 min ago',
remainingTasks: 4,
status: 'attention',
},
{
id: 'auth-system',
name: 'Auth System',
progress: 88,
currentTask: 'Finalize refresh token flow',
lastActivity: '45 min ago',
remainingTasks: 2,
status: 'healthy',
},
// Open Tasks
const openTasks = ref<OpenTask[]>([
{ id: 't1', title: 'Agent Thinking Panel visualisieren', detail: 'Live-Animation der Denkprozesse im AgentModal', source: 'iris', createdAt: '22:30' },
{ id: 't2', title: 'CI/CD Pipeline Monitoring Dashboard', detail: 'Echtzeit-Status der Gitea Actions im Dashboard', source: 'iris', createdAt: '21:15' },
{ id: 't3', title: 'Dungeon System Dokumentation', detail: 'API-Doku für Room-Generation-Endpunkte schreiben', source: 'bao', createdAt: '20:00' },
])
// Feed
const ts = (offset: number) => new Date(now + offset).toISOString()
const feedEntries = ref<FeedEntry[]>([
{ time: '20:42', agent: 'Developer', action: 'Created DungeonController endpoints', timestamp: now - 60000 },
{ time: '20:38', agent: 'DevOps', action: 'Optimized Docker COPY order', timestamp: now - 300000 },
{ time: '20:35', agent: 'Iris', action: 'Delegated room generation to Developer', timestamp: now - 540000 },
{ time: '20:28', agent: 'Researcher', action: 'Documented WebSocket vs SSE analysis', timestamp: now - 780000 },
{ time: '20:22', agent: 'Reviewer', action: 'Approved RoomValidator PR', timestamp: now - 900000 },
{ time: '20:15', agent: 'DevOps', action: 'Added .dockerignore for node_modules', timestamp: now - 1200000 },
{ time: '20:08', agent: 'Iris', action: 'Broke down Dungeon System tasks', timestamp: now - 1500000 },
{ time: '19:55', agent: 'Developer', action: 'Defined dungeon schema models', timestamp: now - 1800000 },
{ time: '22:50', agent: 'Developer', action: 'Created DungeonController endpoints', timestamp: ts(-120000) },
{ time: '22:46', agent: 'DevOps', action: 'Optimized Docker COPY order for layer caching', timestamp: ts(-360000) },
{ time: '22:42', agent: 'Iris', action: 'Delegated room generation to Developer with spec', timestamp: ts(-600000) },
{ time: '22:35', agent: 'Researcher', action: 'Documented WebSocket vs SSE analysis results', timestamp: ts(-960000) },
{ time: '22:28', agent: 'Reviewer', action: 'Approved RoomValidator PR with minor fixes', timestamp: ts(-1200000) },
{ time: '22:18', agent: 'DevOps', action: 'Added .dockerignore for node_modules and build artifacts', timestamp: ts(-1500000) },
{ time: '22:08', agent: 'Iris', action: 'Broke down Dungeon System tasks into sub-tasks', timestamp: ts(-1800000) },
{ time: '21:55', agent: 'Developer', action: 'Defined dungeon schema models with validation', timestamp: ts(-2400000) },
])
// Chat
@@ -298,7 +290,7 @@ export function useDashboardData() {
return {
agents,
missions,
openTasks,
feedEntries,
chatMessages,
irisBusy,
+3 -4
View File
@@ -1,6 +1,6 @@
<script setup lang="ts">
import { onMounted, onUnmounted, ref } from 'vue'
import MissionCard from '../components/dashboard/MissionCard.vue'
import TaskCard from '../components/dashboard/MissionCard.vue'
import OperationsFeed from '../components/dashboard/OperationsFeed.vue'
import TeamNetwork from '../components/dashboard/TeamNetwork.vue'
import ChatPanel from '../components/dashboard/ChatPanel.vue'
@@ -10,7 +10,7 @@ import { useDashboardData } from '../composables/useDashboardData'
import type { AgentNodeData } from '../../composables/useDashboardData'
const {
agents, missions, feedEntries, chatMessages,
agents, openTasks, feedEntries, chatMessages,
irisBusy, irisFocus, irisRuntime, queue,
getAgentRuntime, startRuntime, stopRuntime,
sendChat, removeQueueItem, moveQueueItem, changeQueuePriority,
@@ -48,8 +48,7 @@ function onQueueExecuteNow(id: string): void {
<div class="dashboard">
<div class="col-left">
<section class="missions-section">
<h2 class="column-title">Active Missions</h2>
<MissionCard v-for="m in missions" :key="m.id" :mission="m" />
<TaskCard :tasks="openTasks" @new-task="console.log('New task requested')" />
</section>
<OperationsFeed :entries="feedEntries" />
</div>