f564ecfbc7
Shell: - One shared NexusLayout for ALL routes (dashboard + pages): compact 68px icon rail with hover-expand overlay, replaces both old sidebars + topbars - Single flat nav source (railNav) — same menu everywhere incl. Settings - Removed dead shell components (AppSidebar, AppHeader, Topbar, NavGroup, NavItem, ModuleView) and dead nav routes; App.vue is now just RouterView Live updates: - Fix SSE loop in DashboardController: PeriodicTimer.WaitForNextTickAsync and ChannelReader.ReadAsync were re-invoked while pending — every published update threw InvalidOperationException and killed ALL live streams - liveSync store: treat graceful stream close as disconnect (was stuck connected=true with polling stopped -> page frozen until manual reload), fast first retry, heartbeat watchdog (65s), reconnect on online/visibility - One app-wide SSE connection owned by the layout instead of per-view connect/disconnect churn; removed duplicate live-sync.ts store Performance: - Board endpoint: drop nested childTasks duplication (counts stay) — payload 104KB -> 65KB; SSE snapshots shrink equally - nginx: gzip for JSON/JS/CSS (board 18KB, bundle 95KB over the wire); text/event-stream excluded to keep SSE unbuffered Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
179 lines
4.6 KiB
Vue
179 lines
4.6 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 { computed, onMounted, onUnmounted } from 'vue'
|
||
import { useRouter } from 'vue-router'
|
||
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 router = useRouter()
|
||
|
||
const {
|
||
addAgent,
|
||
agentPositions,
|
||
changeModel,
|
||
closeAgent,
|
||
enteringIds,
|
||
resetLayout,
|
||
selectAgent,
|
||
sendChatMessage,
|
||
updatePositions,
|
||
} = useFlowBoardState(agentStore, chatStore)
|
||
|
||
const blockedTasks = computed(() => taskStore.taskList.filter(task => task.status === 'blocked'))
|
||
|
||
function handleBlockerClick() {
|
||
if (!blockedTasks.value.length) return
|
||
router.push('/tasks')
|
||
}
|
||
|
||
function blockerLabel() {
|
||
const blockedTask = blockedTasks.value[0]
|
||
if (!blockedTask) return undefined
|
||
return `${blockedTasks.value.length} Blocker — ${blockedTask.title}`
|
||
}
|
||
|
||
function blockerCount() {
|
||
return blockedTasks.value.length
|
||
}
|
||
|
||
/* ── Lifecycle ────────────────────────────────────── */
|
||
onMounted(() => {
|
||
agentStore.startPolling()
|
||
chatStore.startPolling()
|
||
dashboardStore.startPolling()
|
||
taskStore.startPolling()
|
||
taskStore.startBoardPolling()
|
||
})
|
||
|
||
onUnmounted(() => {
|
||
agentStore.stopPolling()
|
||
chatStore.stopPolling()
|
||
dashboardStore.stopPolling()
|
||
taskStore.stopPolling()
|
||
taskStore.stopBoardPolling()
|
||
})
|
||
</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>
|