wire llms, ui tweaks

This commit is contained in:
2026-04-12 11:32:36 -03:00
parent 4de44baf98
commit 0f122fa8f7
22 changed files with 960 additions and 203 deletions

View File

@@ -1,8 +1,9 @@
<script setup lang="ts">
import { ref, onMounted, watch, inject } from 'vue'
import { Panel } from 'soleprint-ui'
import { Panel, SplitPane, LogRenderer } from 'soleprint-ui'
import NotificationCard from '../components/NotificationCard.vue'
import HandoverBrief from '../components/HandoverBrief.vue'
import { useAgentEvents } from '../composables/useAgentEvents'
const flights = ref<any[]>([])
const selectedFlight = ref('')
@@ -12,6 +13,13 @@ const notification = ref<any>(null)
const handoverBrief = ref<any>(null)
const scenarioVersion = inject<any>('scenarioVersion')
const showOps = inject<any>('showOps')
const showInternals = inject<any>('showInternals')
const { agentStatus, entries, graphNodes, currentRun, connect } = useAgentEvents()
// Connect WebSocket immediately so we don't miss events
onMounted(connect)
watch(scenarioVersion, () => {
loadFlights()
notification.value = null
@@ -34,6 +42,7 @@ async function runFce() {
if (!selectedFlight.value) return
fceStatus.value = 'processing'
notification.value = null
if (!showInternals.value) showInternals.value = true
const res = await fetch('/agents/fce', {
method: 'POST',
@@ -42,7 +51,6 @@ async function runFce() {
})
const { run_id } = await res.json()
// Poll for result
const poll = setInterval(async () => {
const r = await fetch(`/agents/runs/${run_id}`)
const data = await r.json()
@@ -60,6 +68,7 @@ async function runFce() {
async function runHandover() {
handoverStatus.value = 'processing'
handoverBrief.value = null
if (!showInternals.value) showInternals.value = true
const res = await fetch('/agents/handover', {
method: 'POST',
@@ -86,55 +95,106 @@ onMounted(loadFlights)
</script>
<template>
<div class="ops-page">
<Panel title="FCE — Behind Every Departure" :status="fceStatus">
<template #actions>
<select v-model="selectedFlight" class="flight-select">
<option v-for="f in flights" :key="f.id" :value="f.id">{{ f.label }}</option>
</select>
<button class="run-btn" @click="runFce" :disabled="fceStatus === 'processing'">
{{ fceStatus === 'processing' ? 'Running...' : 'Run FCE' }}
</button>
</template>
<div :class="['ops-layout', { split: showOps && showInternals }]">
<!-- Ops pane (left) -->
<div v-show="showOps" class="ops-pane">
<Panel title="FCE — Behind Every Departure" :status="fceStatus">
<template #actions>
<select v-model="selectedFlight" class="flight-select">
<option v-for="f in flights" :key="f.id" :value="f.id">{{ f.label }}</option>
</select>
<button class="run-btn" @click="runFce" :disabled="fceStatus === 'processing'">
{{ fceStatus === 'processing' ? 'Running...' : 'Run FCE' }}
</button>
</template>
<div v-if="notification" class="result-area"><NotificationCard :data="notification" /></div>
<div v-else-if="fceStatus === 'processing'" class="loading">Running agent...</div>
<div v-else class="empty">Select a flight and click Run FCE.</div>
</Panel>
<div v-if="notification" class="result-area">
<NotificationCard :data="notification" />
</div>
<div v-else-if="fceStatus === 'processing'" class="loading">
Running agent... gathering flight data, weather, crew notes...
</div>
<div v-else class="empty">
Select a flight and click Run FCE to generate a notification.
</div>
</Panel>
<Panel title="Shift Handover Brief" :status="handoverStatus">
<template #actions>
<button class="run-btn" @click="runHandover" :disabled="handoverStatus === 'processing'">
{{ handoverStatus === 'processing' ? 'Running...' : 'Run Handover' }}
</button>
</template>
<div v-if="handoverBrief" class="result-area"><HandoverBrief :data="handoverBrief" /></div>
<div v-else-if="handoverStatus === 'processing'" class="loading">Running agent...</div>
<div v-else class="empty">Click Run Handover.</div>
</Panel>
</div>
<Panel title="Shift Handover Brief" :status="handoverStatus">
<template #actions>
<button class="run-btn" @click="runHandover" :disabled="handoverStatus === 'processing'">
{{ handoverStatus === 'processing' ? 'Running...' : 'Run Handover' }}
</button>
</template>
<div v-if="handoverBrief" class="result-area">
<HandoverBrief :data="handoverBrief" />
<!-- Internals pane (right) -->
<div v-show="showInternals" class="internals-pane">
<Panel title="Agent Graph" :status="agentStatus">
<div class="graph-container">
<div v-if="graphNodes.length === 0" class="empty">Waiting for agent run...</div>
<div v-else class="graph-nodes">
<div v-for="node in graphNodes" :key="node.id" :class="['graph-node', node.status]">
<div class="node-dot"></div>
<span class="node-label">{{ node.id }}</span>
</div>
<div class="graph-edge-line"></div>
</div>
</div>
</Panel>
<Panel title="Tool Call Stream" :status="agentStatus" class="stream-panel">
<LogRenderer :entries="entries" :auto-scroll="true" />
</Panel>
<div v-if="currentRun" class="run-summary">
{{ currentRun.agent }} / {{ currentRun.run_id }} / {{ entries.length }} events
</div>
<div v-else-if="handoverStatus === 'processing'" class="loading">
Running agent... scanning all hubs for active issues...
</div>
<div v-else class="empty">
Click Run Handover to generate a shift handover brief.
</div>
</Panel>
</div>
</div>
</template>
<style scoped>
.ops-page {
.ops-layout {
display: flex;
gap: 16px;
height: calc(100vh - 80px);
position: relative;
}
/* Both visible: 50/50 with divider */
.ops-layout.split > .ops-pane { flex: 1; }
.ops-layout.split > .internals-pane { flex: 1; border-left: var(--panel-border); padding-left: 16px; }
/* Single pane: full width */
.ops-layout > .internals-pane { flex: 1; }
.ops-layout > .ops-pane { flex: 1; }
.ops-pane {
display: flex;
flex-direction: column;
gap: 24px;
overflow: auto;
height: 100%;
min-width: 0;
}
.internals-pane {
display: flex;
flex-direction: column;
gap: 8px;
height: 100%;
overflow: auto;
min-width: 0;
}
.internals-pane > :first-child { flex-shrink: 0; }
.internals-pane > .stream-panel { flex: 1; min-height: 0; }
.run-summary {
padding: 4px 12px;
font-family: var(--font-mono);
font-size: 11px;
color: var(--text-dim);
flex-shrink: 0;
}
.flight-select {
background: var(--surface-2);
color: var(--text-primary);
@@ -152,15 +212,12 @@ onMounted(loadFlights)
font-family: var(--font-mono);
font-size: 12px;
cursor: pointer;
transition: background 0.15s;
}
.run-btn:hover { background: var(--accent-dim); }
.run-btn:disabled { opacity: 0.5; cursor: not-allowed; }
.result-area {
padding: 16px;
}
.result-area { padding: 16px; }
.loading, .empty {
padding: 32px;
@@ -170,7 +227,58 @@ onMounted(loadFlights)
font-size: 13px;
}
.loading {
color: var(--accent);
.loading { color: var(--accent); }
.graph-container { padding: 12px; }
.graph-nodes {
display: flex;
flex-direction: column;
gap: 6px;
position: relative;
padding-left: 12px;
}
.graph-edge-line {
position: absolute;
left: 17px;
top: 10px;
bottom: 10px;
width: 2px;
background: var(--surface-3);
}
.graph-node {
display: flex;
align-items: center;
gap: 10px;
padding: 6px 10px;
background: var(--surface-2);
border: var(--panel-border);
position: relative;
z-index: 1;
}
.node-dot {
width: 7px;
height: 7px;
border-radius: 50%;
background: var(--status-idle);
flex-shrink: 0;
}
.graph-node.processing .node-dot {
background: var(--status-processing);
box-shadow: 0 0 8px var(--status-processing);
}
.graph-node.done .node-dot {
background: var(--status-live);
box-shadow: 0 0 8px var(--status-live);
}
.node-label {
font-family: var(--font-mono);
font-size: 12px;
}
</style>