172 lines
4.5 KiB
Vue
172 lines
4.5 KiB
Vue
<script setup lang="ts">
|
||
/**
|
||
* FlowBoard – Das neue V2 Dashboard
|
||
*
|
||
* Layout:
|
||
* Stage (AlertBar + FlowCanvas) + Rail (IrisChat) + TaskStrip (unten)
|
||
*
|
||
* Datenquellen:
|
||
* - AgentStore: agents, models, AlertBar-Metriken, Modal-Status
|
||
* - ChatStore: messages, isThinking, sendMessage()
|
||
* - TaskStore: tasks
|
||
*
|
||
* Polling startet bei Mount, stoppt bei Unmount.
|
||
*/
|
||
import { onMounted, onUnmounted } from 'vue'
|
||
import { useAgentStore } from '../../stores/agents'
|
||
import { useChatStore } from '../../stores/chat'
|
||
import { useDashboardStore } from '../../stores/dashboard'
|
||
import { useTaskStore } from '../../stores/tasks'
|
||
import AlertBar from '../../components/dashboard/v2/AlertBar.vue'
|
||
import FlowCanvas from '../../components/dashboard/v2/FlowCanvas.vue'
|
||
import IrisChat from '../../components/dashboard/v2/IrisChat.vue'
|
||
import TaskStrip from '../../components/dashboard/v2/TaskStrip.vue'
|
||
import AgentDetailModal from '../../components/dashboard/v2/AgentDetailModal.vue'
|
||
import { useFlowBoardState } from '../../composables/useFlowBoardState'
|
||
|
||
/* ── Stores ──────────────────────────────────────── */
|
||
const agentStore = useAgentStore()
|
||
const chatStore = useChatStore()
|
||
const dashboardStore = useDashboardStore()
|
||
const taskStore = useTaskStore()
|
||
|
||
const {
|
||
addAgent,
|
||
agentPositions,
|
||
changeModel,
|
||
closeAgent,
|
||
enteringIds,
|
||
resetLayout,
|
||
selectAgent,
|
||
sendChatMessage,
|
||
updatePositions,
|
||
} = useFlowBoardState(agentStore, chatStore)
|
||
|
||
function handleBlockerClick() {
|
||
console.log('[FlowBoard] blocker clicked')
|
||
}
|
||
|
||
function blockerLabel() {
|
||
const blockedTask = taskStore.taskList.find(task => task.status === 'blocked')
|
||
if (!blockedTask) return undefined
|
||
return `${taskStore.taskList.filter(task => task.status === 'blocked').length} Blocker — ${blockedTask.title}`
|
||
}
|
||
|
||
function blockerCount() {
|
||
return taskStore.taskList.filter(task => task.status === 'blocked').length
|
||
}
|
||
|
||
/* ── Lifecycle ────────────────────────────────────── */
|
||
onMounted(() => {
|
||
agentStore.startPolling()
|
||
chatStore.startPolling()
|
||
dashboardStore.startPolling()
|
||
taskStore.startPolling()
|
||
})
|
||
|
||
onUnmounted(() => {
|
||
agentStore.stopPolling()
|
||
chatStore.stopPolling()
|
||
dashboardStore.stopPolling()
|
||
taskStore.stopPolling()
|
||
})
|
||
</script>
|
||
|
||
<template>
|
||
<div class="flow-board">
|
||
<!-- Stage + Rail row -->
|
||
<div class="board-body">
|
||
<!-- Stage: AlertBar + FlowCanvas + TaskStrip -->
|
||
<div class="stage">
|
||
<AlertBar
|
||
:active-count="agentStore.activeCount"
|
||
:think-count="agentStore.thinkCount"
|
||
:idle-count="agentStore.idleCount"
|
||
:blocker-count="blockerCount()"
|
||
:today-cost="agentStore.todayCost"
|
||
:today-tokens="agentStore.todayTokens"
|
||
:blocker-label="blockerLabel()"
|
||
@blocker-click="handleBlockerClick"
|
||
/>
|
||
|
||
<FlowCanvas
|
||
:agents="agentStore.agentList"
|
||
:positions="agentPositions"
|
||
:entering-ids="enteringIds"
|
||
@select="selectAgent"
|
||
@add="addAgent"
|
||
@reset-layout="resetLayout"
|
||
@update-positions="updatePositions"
|
||
/>
|
||
|
||
<TaskStrip :tasks="taskStore.taskList" :loading="taskStore.loading" :error="taskStore.error" />
|
||
</div>
|
||
|
||
<!-- Rail: IrisChat -->
|
||
<IrisChat
|
||
:messages="chatStore.messageList"
|
||
:is-thinking="chatStore.isThinking"
|
||
:error="chatStore.error"
|
||
@send="sendChatMessage"
|
||
/>
|
||
</div>
|
||
|
||
<!-- Agent Detail Modal -->
|
||
<AgentDetailModal
|
||
v-if="agentStore.modalOpen && agentStore.selectedAgent"
|
||
:agent="agentStore.selectedAgent"
|
||
:agent-order="agentStore.agentOrder"
|
||
@close="closeAgent"
|
||
@select="selectAgent"
|
||
@change-model="changeModel"
|
||
/>
|
||
</div>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.flow-board {
|
||
height: 100%;
|
||
display: flex;
|
||
flex-direction: column;
|
||
min-height: 0;
|
||
animation: fade-in 0.35s ease-out;
|
||
}
|
||
|
||
@keyframes fade-in {
|
||
from { opacity: 0; transform: translateY(8px); }
|
||
to { opacity: 1; transform: translateY(0); }
|
||
}
|
||
|
||
.board-body {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: row;
|
||
gap: 18px;
|
||
padding: 18px 20px;
|
||
overflow: hidden;
|
||
min-height: 0;
|
||
}
|
||
|
||
.stage {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 14px;
|
||
min-height: 0;
|
||
min-width: 0;
|
||
overflow: hidden;
|
||
}
|
||
|
||
@media (max-width: 767px) {
|
||
.board-body {
|
||
flex-direction: column;
|
||
padding: 8px;
|
||
gap: 10px;
|
||
}
|
||
|
||
.stage {
|
||
flex: 1;
|
||
}
|
||
}
|
||
</style>
|