feat: wire dashboard v2 to backend data
This commit is contained in:
@@ -160,6 +160,16 @@ const statusColors: Record<string, string> = {
|
|||||||
<div class="m-think">{{ thinkDisplay }}<span class="caret"></span></div>
|
<div class="m-think">{{ thinkDisplay }}<span class="caret"></span></div>
|
||||||
</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 -->
|
<!-- Modell wählen -->
|
||||||
<div class="m-sec">
|
<div class="m-sec">
|
||||||
<h4>Modell wählen</h4>
|
<h4>Modell wählen</h4>
|
||||||
@@ -519,6 +529,35 @@ const statusColors: Record<string, string> = {
|
|||||||
|
|
||||||
@keyframes blink { 50% { opacity: 0; } }
|
@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 ──────────────────────────────────── */
|
/* ── Models ──────────────────────────────────── */
|
||||||
.m-models {
|
.m-models {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ defineProps<{
|
|||||||
blockerCount: number
|
blockerCount: number
|
||||||
todayCost: string
|
todayCost: string
|
||||||
todayTokens: string
|
todayTokens: string
|
||||||
|
blockerLabel?: string
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
defineEmits<{
|
defineEmits<{
|
||||||
@@ -62,7 +63,7 @@ defineEmits<{
|
|||||||
@click="$emit('blockerClick')"
|
@click="$emit('blockerClick')"
|
||||||
>
|
>
|
||||||
<span class="dot block"></span>
|
<span class="dot block"></span>
|
||||||
{{ blockerCount }} Blocker
|
{{ blockerLabel || `${blockerCount} Blocker` }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ export interface TaskItem {
|
|||||||
priority: 'high' | 'medium' | 'low'
|
priority: 'high' | 'medium' | 'low'
|
||||||
status: 'active' | 'pending' | 'blocked'
|
status: 'active' | 'pending' | 'blocked'
|
||||||
progress: number // 0–100
|
progress: number // 0–100
|
||||||
|
detail?: string | null
|
||||||
|
source?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Agent Detail Modal Types ─────────────────── */
|
/* ── Agent Detail Modal Types ─────────────────── */
|
||||||
@@ -26,6 +28,11 @@ export interface ThinkingItem {
|
|||||||
ts: string
|
ts: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AgentActivityItem {
|
||||||
|
time: string
|
||||||
|
text: string
|
||||||
|
}
|
||||||
|
|
||||||
/** Dashboard view-model for an agent detail modal */
|
/** Dashboard view-model for an agent detail modal */
|
||||||
export interface AgentDetailData {
|
export interface AgentDetailData {
|
||||||
id: string
|
id: string
|
||||||
@@ -51,5 +58,6 @@ export interface AgentDetailData {
|
|||||||
lastActive: string
|
lastActive: string
|
||||||
activeTaskCount: number
|
activeTaskCount: number
|
||||||
thinking: ThinkingItem[]
|
thinking: ThinkingItem[]
|
||||||
|
activity: AgentActivityItem[]
|
||||||
availableModels: { id: string; alias: string }[]
|
availableModels: { id: string; alias: string }[]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { icons } from '../../composables/icons'
|
|||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
connected?: boolean
|
connected?: boolean
|
||||||
|
statusLabel?: string
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
defineEmits<{
|
defineEmits<{
|
||||||
@@ -27,7 +28,7 @@ defineEmits<{
|
|||||||
<!-- Status Pill -->
|
<!-- Status Pill -->
|
||||||
<span :class="['pill', connected ? 'live' : 'preview']">
|
<span :class="['pill', connected ? 'live' : 'preview']">
|
||||||
<span class="status-dot" :class="connected ? 'on' : 'off'"></span>
|
<span class="status-dot" :class="connected ? 'on' : 'off'"></span>
|
||||||
{{ connected ? 'OpenClaw verbunden' : 'Preview' }}
|
{{ connected ? (statusLabel || 'OpenClaw verbunden') : 'Preview' }}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<!-- Ask Iris Button -->
|
<!-- Ask Iris Button -->
|
||||||
|
|||||||
@@ -13,8 +13,20 @@ interface FlowBoardChatStore {
|
|||||||
sendMessage: (text: string) => void
|
sendMessage: (text: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const STORAGE_KEY = 'nexus-flow-positions'
|
||||||
|
|
||||||
|
function readStoredPositions() {
|
||||||
|
if (typeof window === 'undefined') return {}
|
||||||
|
try {
|
||||||
|
const raw = window.localStorage.getItem(STORAGE_KEY)
|
||||||
|
return raw ? JSON.parse(raw) as Record<string, { x: number; y: number }> : {}
|
||||||
|
} catch {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function useFlowBoardState(agentStore: FlowBoardAgentStore, chatStore: FlowBoardChatStore) {
|
export function useFlowBoardState(agentStore: FlowBoardAgentStore, chatStore: FlowBoardChatStore) {
|
||||||
const agentPositions = ref<Record<string, { x: number; y: number }>>({})
|
const agentPositions = ref<Record<string, { x: number; y: number }>>(readStoredPositions())
|
||||||
const enteringIds = ref<string[]>([])
|
const enteringIds = ref<string[]>([])
|
||||||
const localAgentPool = ref<AgentNodeData[]>([...extraAgentPool])
|
const localAgentPool = ref<AgentNodeData[]>([...extraAgentPool])
|
||||||
|
|
||||||
@@ -46,10 +58,14 @@ export function useFlowBoardState(agentStore: FlowBoardAgentStore, chatStore: Fl
|
|||||||
|
|
||||||
function resetLayout() {
|
function resetLayout() {
|
||||||
agentPositions.value = {}
|
agentPositions.value = {}
|
||||||
|
if (typeof window !== 'undefined') window.localStorage.removeItem(STORAGE_KEY)
|
||||||
}
|
}
|
||||||
|
|
||||||
function updatePositions(positions: Record<string, { x: number; y: number }>) {
|
function updatePositions(positions: Record<string, { x: number; y: number }>) {
|
||||||
agentPositions.value = { ...positions }
|
agentPositions.value = { ...positions }
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(agentPositions.value))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function sendChatMessage(text: string) {
|
function sendChatMessage(text: string) {
|
||||||
|
|||||||
@@ -7,12 +7,12 @@
|
|||||||
*/
|
*/
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { RouterView } from 'vue-router'
|
import { RouterView } from 'vue-router'
|
||||||
import { useAgentStore } from '../stores/agents'
|
import { useDashboardStore } from '../stores/dashboard'
|
||||||
import GalaxyBackground from '../components/background/GalaxyBackground.vue'
|
import GalaxyBackground from '../components/background/GalaxyBackground.vue'
|
||||||
import Sidebar from '../components/layout/Sidebar.vue'
|
import Sidebar from '../components/layout/Sidebar.vue'
|
||||||
import Topbar from '../components/layout/Topbar.vue'
|
import Topbar from '../components/layout/Topbar.vue'
|
||||||
|
|
||||||
const agentStore = useAgentStore()
|
const dashboardStore = useDashboardStore()
|
||||||
|
|
||||||
/* ── Mobile Sidebar State ───────────────────────── */
|
/* ── Mobile Sidebar State ───────────────────────── */
|
||||||
const mobileMenuOpen = ref(false)
|
const mobileMenuOpen = ref(false)
|
||||||
@@ -39,7 +39,8 @@ function closeMobileMenu() {
|
|||||||
|
|
||||||
<main class="nexus-main">
|
<main class="nexus-main">
|
||||||
<Topbar
|
<Topbar
|
||||||
:connected="agentStore.isConnected"
|
:connected="dashboardStore.isGatewayConnected"
|
||||||
|
:status-label="dashboardStore.irisStatusLabel"
|
||||||
@toggle-sidebar="mobileMenuOpen = !mobileMenuOpen"
|
@toggle-sidebar="mobileMenuOpen = !mobileMenuOpen"
|
||||||
/>
|
/>
|
||||||
<div class="nexus-content">
|
<div class="nexus-content">
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { apiFetch } from '../services/api'
|
import { apiFetch } from '../services/api'
|
||||||
import type { AgentNodeData } from '../composables/useFlowLayout'
|
import type { AgentNodeData } from '../composables/useFlowLayout'
|
||||||
import type { AgentDetailData, ThinkingItem } from '../components/dashboard/v2/types'
|
import type { AgentActivityItem, AgentDetailData, ThinkingItem } from '../components/dashboard/v2/types'
|
||||||
|
|
||||||
/* ── API Response Shapes ──────────────────────────── */
|
/* ── API Response Shapes ──────────────────────────── */
|
||||||
|
|
||||||
@@ -40,6 +40,11 @@ interface ModelOption {
|
|||||||
provider: string
|
provider: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface AgentActivityEntry {
|
||||||
|
time: string
|
||||||
|
text: string
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Status Mapping ───────────────────────────────── */
|
/* ── Status Mapping ───────────────────────────────── */
|
||||||
|
|
||||||
function mapStatus(isActive: boolean, currentTask: string | null): AgentNodeData['status'] {
|
function mapStatus(isActive: boolean, currentTask: string | null): AgentNodeData['status'] {
|
||||||
@@ -148,6 +153,7 @@ export function buildAgentDetail(data: AgentNodeData, models: { id: string; alia
|
|||||||
lastActive: data.elapsed !== '—' ? 'Vor ' + data.elapsed : 'Nicht aktiv',
|
lastActive: data.elapsed !== '—' ? 'Vor ' + data.elapsed : 'Nicht aktiv',
|
||||||
activeTaskCount: data.task ? 1 : 0,
|
activeTaskCount: data.task ? 1 : 0,
|
||||||
thinking: buildThinkingItems(data),
|
thinking: buildThinkingItems(data),
|
||||||
|
activity: [],
|
||||||
availableModels: models,
|
availableModels: models,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -159,6 +165,7 @@ export const useAgentStore = defineStore('agents', {
|
|||||||
loading: false,
|
loading: false,
|
||||||
error: null as string | null,
|
error: null as string | null,
|
||||||
selectedAgentId: null as string | null,
|
selectedAgentId: null as string | null,
|
||||||
|
activityByAgentId: {} as Record<string, AgentActivityItem[]>,
|
||||||
refreshInterval: null as ReturnType<typeof setInterval> | null,
|
refreshInterval: null as ReturnType<typeof setInterval> | null,
|
||||||
isConnected: false,
|
isConnected: false,
|
||||||
}),
|
}),
|
||||||
@@ -179,7 +186,10 @@ export const useAgentStore = defineStore('agents', {
|
|||||||
if (!state.selectedAgentId) return null
|
if (!state.selectedAgentId) return null
|
||||||
const data = state.agents.find(a => a.id === state.selectedAgentId)
|
const data = state.agents.find(a => a.id === state.selectedAgentId)
|
||||||
if (!data) return null
|
if (!data) return null
|
||||||
return buildAgentDetail(data, state.models)
|
return {
|
||||||
|
...buildAgentDetail(data, state.models),
|
||||||
|
activity: state.activityByAgentId[data.id] ?? [],
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
/** Is the modal open? */
|
/** Is the modal open? */
|
||||||
@@ -249,9 +259,24 @@ export const useAgentStore = defineStore('agents', {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async fetchAgentActivity(agentId: string) {
|
||||||
|
try {
|
||||||
|
const res = await apiFetch(`/api/dashboard/agents/${encodeURIComponent(agentId)}/activity?limit=5`)
|
||||||
|
if (!res.ok) return
|
||||||
|
const data: AgentActivityEntry[] = await res.json()
|
||||||
|
this.activityByAgentId[agentId] = data.map(entry => ({
|
||||||
|
time: entry.time,
|
||||||
|
text: entry.text,
|
||||||
|
}))
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('[AgentStore] fetchAgentActivity failed', err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
/* ── Selection ───────────────────────────────── */
|
/* ── Selection ───────────────────────────────── */
|
||||||
selectAgent(id: string | null) {
|
selectAgent(id: string | null) {
|
||||||
this.selectedAgentId = id
|
this.selectedAgentId = id
|
||||||
|
if (id) void this.fetchAgentActivity(id)
|
||||||
},
|
},
|
||||||
|
|
||||||
/* ── Polling ─────────────────────────────────── */
|
/* ── Polling ─────────────────────────────────── */
|
||||||
|
|||||||
@@ -0,0 +1,110 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { apiFetch } from '../services/api'
|
||||||
|
|
||||||
|
interface DashboardStatusDto {
|
||||||
|
gatewayOk: boolean
|
||||||
|
irisStatus: string
|
||||||
|
activeAgents: number
|
||||||
|
pendingTasks: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FeedEntryDto {
|
||||||
|
agent: string
|
||||||
|
action: string
|
||||||
|
timestamp: string
|
||||||
|
time: string
|
||||||
|
agentId?: string | null
|
||||||
|
type?: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
interface QueueItemDto {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
status: string
|
||||||
|
priority: string
|
||||||
|
source: string
|
||||||
|
waitTime: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useDashboardStore = defineStore('dashboard', {
|
||||||
|
state: () => ({
|
||||||
|
status: null as DashboardStatusDto | null,
|
||||||
|
operations: [] as FeedEntryDto[],
|
||||||
|
queue: [] as QueueItemDto[],
|
||||||
|
loading: false,
|
||||||
|
error: null as string | null,
|
||||||
|
refreshInterval: null as ReturnType<typeof setInterval> | null,
|
||||||
|
}),
|
||||||
|
|
||||||
|
getters: {
|
||||||
|
isGatewayConnected: state => state.status?.gatewayOk ?? false,
|
||||||
|
irisStatusLabel: state => state.status?.irisStatus ?? 'Offline',
|
||||||
|
},
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
async fetchStatus() {
|
||||||
|
try {
|
||||||
|
const res = await apiFetch('/api/dashboard/status')
|
||||||
|
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
||||||
|
this.status = await res.json()
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('[DashboardStore] fetchStatus failed', err)
|
||||||
|
this.status = null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async fetchOperations() {
|
||||||
|
try {
|
||||||
|
const res = await apiFetch('/api/dashboard/operations?limit=20')
|
||||||
|
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
||||||
|
this.operations = await res.json()
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('[DashboardStore] fetchOperations failed', err)
|
||||||
|
this.operations = []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async fetchQueue() {
|
||||||
|
try {
|
||||||
|
const res = await apiFetch('/api/dashboard/queue')
|
||||||
|
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
||||||
|
this.queue = await res.json()
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('[DashboardStore] fetchQueue failed', err)
|
||||||
|
this.queue = []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async refresh() {
|
||||||
|
this.loading = true
|
||||||
|
try {
|
||||||
|
await Promise.all([
|
||||||
|
this.fetchStatus(),
|
||||||
|
this.fetchOperations(),
|
||||||
|
this.fetchQueue(),
|
||||||
|
])
|
||||||
|
this.error = null
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('[DashboardStore] refresh failed', err)
|
||||||
|
this.error = 'Dashboard metadata could not be loaded'
|
||||||
|
} finally {
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
startPolling() {
|
||||||
|
if (this.refreshInterval) return
|
||||||
|
this.refresh()
|
||||||
|
this.refreshInterval = setInterval(() => {
|
||||||
|
this.refresh()
|
||||||
|
}, 30000)
|
||||||
|
},
|
||||||
|
|
||||||
|
stopPolling() {
|
||||||
|
if (this.refreshInterval) {
|
||||||
|
clearInterval(this.refreshInterval)
|
||||||
|
this.refreshInterval = null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -56,6 +56,8 @@ function mapTask(t: DashboardTaskDto): TaskItem {
|
|||||||
priority: mapPriority(t.priority),
|
priority: mapPriority(t.priority),
|
||||||
status: mapState(t.state),
|
status: mapState(t.state),
|
||||||
progress: mapProgress(t.state),
|
progress: mapProgress(t.state),
|
||||||
|
detail: t.detail,
|
||||||
|
source: t.source,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
import { onMounted, onUnmounted } from 'vue'
|
import { onMounted, onUnmounted } from 'vue'
|
||||||
import { useAgentStore } from '../../stores/agents'
|
import { useAgentStore } from '../../stores/agents'
|
||||||
import { useChatStore } from '../../stores/chat'
|
import { useChatStore } from '../../stores/chat'
|
||||||
|
import { useDashboardStore } from '../../stores/dashboard'
|
||||||
import { useTaskStore } from '../../stores/tasks'
|
import { useTaskStore } from '../../stores/tasks'
|
||||||
import AlertBar from '../../components/dashboard/v2/AlertBar.vue'
|
import AlertBar from '../../components/dashboard/v2/AlertBar.vue'
|
||||||
import FlowCanvas from '../../components/dashboard/v2/FlowCanvas.vue'
|
import FlowCanvas from '../../components/dashboard/v2/FlowCanvas.vue'
|
||||||
@@ -26,6 +27,7 @@ import { useFlowBoardState } from '../../composables/useFlowBoardState'
|
|||||||
/* ── Stores ──────────────────────────────────────── */
|
/* ── Stores ──────────────────────────────────────── */
|
||||||
const agentStore = useAgentStore()
|
const agentStore = useAgentStore()
|
||||||
const chatStore = useChatStore()
|
const chatStore = useChatStore()
|
||||||
|
const dashboardStore = useDashboardStore()
|
||||||
const taskStore = useTaskStore()
|
const taskStore = useTaskStore()
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -44,16 +46,28 @@ function handleBlockerClick() {
|
|||||||
console.log('[FlowBoard] blocker clicked')
|
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 ────────────────────────────────────── */
|
/* ── Lifecycle ────────────────────────────────────── */
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
agentStore.startPolling()
|
agentStore.startPolling()
|
||||||
chatStore.startPolling()
|
chatStore.startPolling()
|
||||||
|
dashboardStore.startPolling()
|
||||||
taskStore.startPolling()
|
taskStore.startPolling()
|
||||||
})
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
agentStore.stopPolling()
|
agentStore.stopPolling()
|
||||||
chatStore.stopPolling()
|
chatStore.stopPolling()
|
||||||
|
dashboardStore.stopPolling()
|
||||||
taskStore.stopPolling()
|
taskStore.stopPolling()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
@@ -68,9 +82,10 @@ onUnmounted(() => {
|
|||||||
:active-count="agentStore.activeCount"
|
:active-count="agentStore.activeCount"
|
||||||
:think-count="agentStore.thinkCount"
|
:think-count="agentStore.thinkCount"
|
||||||
:idle-count="agentStore.idleCount"
|
:idle-count="agentStore.idleCount"
|
||||||
:blocker-count="agentStore.blockerCount"
|
:blocker-count="blockerCount()"
|
||||||
:today-cost="agentStore.todayCost"
|
:today-cost="agentStore.todayCost"
|
||||||
:today-tokens="agentStore.todayTokens"
|
:today-tokens="agentStore.todayTokens"
|
||||||
|
:blocker-label="blockerLabel()"
|
||||||
@blocker-click="handleBlockerClick"
|
@blocker-click="handleBlockerClick"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user