Dynavera/site/src/views/AgentDetailView.vue

367 lines
9.9 KiB
Vue

<script setup lang="ts">
import { ref, onMounted, onUnmounted, computed } from 'vue'
import { useRoute } from 'vue-router'
import {
Card,
Typography,
Button,
List,
Space,
Spin,
Input,
message,
Tag,
InputNumber,
} from 'ant-design-vue'
import { marked } from 'marked'
import DOMPurify from 'dompurify'
import { useAgentStore } from '../stores/agentStore'
import { apiClient, isAxiosError, API } from '../router/api'
import type { AgentConfig, AgentRunResult } from '../types/agent'
const route = useRoute()
const agentStore = useAgentStore()
const agentId = route.params.id as string
const agent = ref<AgentConfig>({
id: agentId,
name: 'Loading...',
description: '',
status: 'idle',
uuid: agentId,
agent_type: 'knowledge',
llm_config: {},
organization: '',
})
const maxTokens = ref<number>(256)
const queryInput = ref('')
const isRunning = computed(() => agentStore.executionStatus === 'running')
const isConnected = computed(() => agentStore.isConnected ?? false)
const agentResponse = computed(() => {
const completedEvent = agentStore.eventLog?.find((event) => event.type === 'completed')
if (completedEvent?.content && typeof completedEvent.content === 'object') {
const output = completedEvent.content as AgentRunResult
return (output.response as string) || null
}
return null
})
const statusColor = (status: string) => {
const colors: Record<string, string> = {
idle: 'default',
running: 'processing',
completed: 'success',
failed: 'error',
stopped: 'warning',
}
return colors[status] || 'default'
}
const fetchAgent = async () => {
try {
const response = await apiClient.get<AgentConfig>(API.agents.configs.byId(agentId))
agent.value = response.data
} catch (error) {
console.error('Failed to fetch agent:', error)
if (isAxiosError(error)) {
console.error('Axios error details:', {
status: error.response?.status,
data: error.response?.data,
message: error.message,
})
}
message.error('Failed to load agent details')
}
}
const renderedAgentResponse = computed(() => {
const rawMarkdown = agentResponse.value
if (!rawMarkdown) return ''
const html = marked.parse(rawMarkdown) as string
return DOMPurify.sanitize(html)
})
const startAgent = () => {
if (!agentStore.isConnected) {
message.error('WebSocket not connected')
return
}
if (!queryInput.value.trim()) {
message.error('Please enter a query')
return
}
agentStore.startAgent({
query: queryInput.value.trim(),
max_tokens: maxTokens.value,
})
}
const stopAgent = () => {
agentStore.stopAgent(agentStore.lastExecutionId || undefined)
message.success('Agent stop requested')
}
onMounted(() => {
fetchAgent()
agentStore.connect(agentId)
})
onUnmounted(() => {
agentStore.disconnect()
})
</script>
<template>
<div class="page">
<Card class="panel" :bordered="false">
<div class="header">
<Typography.Title :level="2">{{ agent.name }}</Typography.Title>
<Tag :color="statusColor(String(agentStore.executionStatus || 'idle'))">
{{ (agentStore.executionStatus || 'idle').toString().toUpperCase() }}
</Tag>
</div>
<Typography.Paragraph type="secondary">
{{ agent.description || 'No description available' }}
</Typography.Paragraph>
<div class="connection-status">
<span>WebSocket Status:</span>
<Tag :color="agentStore.isConnected ? 'green' : 'red'">
{{ agentStore.isConnected ? 'CONNECTED' : 'DISCONNECTED' }}
</Tag>
</div>
<Typography.Title :level="4" class="section-title">Execution</Typography.Title>
<div class="execution-controls">
<Space direction="vertical" style="width: 100%">
<div>
<Typography.Text>Query:</Typography.Text>
<Input.TextArea
v-model:value="queryInput"
:disabled="isRunning"
placeholder="Enter your query here..."
:rows="4"
/>
</div>
<div>
<Typography.Text>Max Tokens:</Typography.Text>
<InputNumber
v-model:value="maxTokens"
:min="1"
:max="4096"
:disabled="isRunning"
style="width: 100%"
/>
</div>
<Space>
<Button
type="primary"
:disabled="isRunning || !isConnected"
@click="startAgent"
>
Run Agent
</Button>
<Button danger :disabled="!isRunning" @click="stopAgent">Stop Agent</Button>
</Space>
</Space>
</div>
<div v-if="agentResponse" class="response-section">
<Typography.Title :level="4" class="section-title">Final Response</Typography.Title>
<Card class="response-card response-final" :bordered="false">
<div
class="response-content markdown-body"
v-html="renderedAgentResponse"
></div>
</Card>
</div>
<Typography.Title :level="4" class="section-title">Execution Log</Typography.Title>
<Spin :spinning="isRunning" tip="Agent running...">
<div class="log-container">
<List
v-if="(agentStore.eventLog?.length ?? 0) > 0"
:data-source="agentStore.eventLog || []"
:bordered="false"
>
<template #renderItem="{ item }">
<List.Item class="log-item">
<div class="log-entry">
<Tag class="log-type">{{ item.type }}</Tag>
<span class="log-time">
{{ item.timestamp.toLocaleTimeString() }}
</span>
<div v-if="item.message" class="log-message">
{{ item.message }}
</div>
<div
v-if="item.content && typeof item.content === 'object'"
class="log-content"
>
<pre>{{ JSON.stringify(item.content, null, 2) }}</pre>
</div>
<div v-else-if="item.content" class="log-content">
{{ item.content }}
</div>
</div>
</List.Item>
</template>
</List>
<Typography.Paragraph v-else type="secondary">
No events yet. Start the agent to see execution logs.
</Typography.Paragraph>
</div>
</Spin>
</Card>
</div>
</template>
<style scoped>
.page {
max-width: 1200px;
margin: 0 auto;
padding: 1rem;
}
.panel {
background: #0f172a;
border: 1px solid #1f2937;
color: #e5e7eb;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
}
.section-title {
margin-top: 2rem !important;
margin-bottom: 1rem !important;
}
.connection-status {
display: flex;
align-items: center;
gap: 0.5rem;
margin: 1rem 0;
padding: 0.5rem;
background: #1f2937;
border-radius: 4px;
}
.execution-controls {
background: #1f2937;
padding: 1rem;
border-radius: 4px;
margin: 1rem 0;
}
.log-container {
background: #1f2937;
border-radius: 4px;
max-height: 500px;
overflow-y: auto;
}
.log-item {
border-bottom: 1px solid #374151 !important;
padding: 0.75rem !important;
}
.log-entry {
display: flex;
flex-direction: column;
gap: 0.5rem;
width: 100%;
}
.log-type {
width: fit-content;
}
.log-time {
font-size: 0.75rem;
color: #9ca3af;
}
.log-message {
color: #e5e7eb;
font-size: 0.9rem;
}
.log-content {
background: #111827;
padding: 0.5rem;
border-radius: 3px;
overflow-x: auto;
}
.log-content pre {
margin: 0;
font-size: 0.8rem;
color: #d1d5db;
}
.response-section {
margin-top: 2rem;
}
.response-card {
background: #1f2937;
border: 1px solid #374151;
}
.response-final {
border-color: #6366f1;
box-shadow: 0 0 0 1px rgba(99, 102, 241, 0.35);
}
.response-content {
color: #e5e7eb;
font-size: 1rem;
line-height: 1.6;
white-space: pre-wrap;
word-wrap: break-word;
padding: 0.5rem;
}
.markdown-body :deep(h1),
.markdown-body :deep(h2),
.markdown-body :deep(h3) {
color: #f8fafc;
margin-top: 1rem;
margin-bottom: 0.5rem;
}
.markdown-body :deep(ul),
.markdown-body :deep(ol) {
padding-left: 1.5rem;
margin-bottom: 1rem;
color: #e5e7eb;
}
.markdown-body :deep(code) {
background: #020617;
padding: 0.2rem 0.4rem;
border-radius: 4px;
font-family: monospace;
color: #10b981;
}
.markdown-body :deep(p) {
margin-bottom: 1rem;
}
</style>