2026-02-26 01:32:04 +00:00
|
|
|
<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,
|
2026-02-27 14:56:20 +00:00
|
|
|
Select,
|
2026-02-26 01:32:04 +00:00
|
|
|
} from 'ant-design-vue'
|
|
|
|
|
import { marked } from 'marked'
|
|
|
|
|
import DOMPurify from 'dompurify'
|
|
|
|
|
import { useAgentStore } from '../stores/agentStore'
|
|
|
|
|
import { apiClient, isAxiosError, API } from '../router/api'
|
2026-02-27 12:26:51 +00:00
|
|
|
import type { AgentConfig, AgentRunResult } from '../types/agent'
|
2026-02-26 01:32:04 +00:00
|
|
|
|
|
|
|
|
const route = useRoute()
|
|
|
|
|
const agentStore = useAgentStore()
|
|
|
|
|
|
2026-02-27 14:56:20 +00:00
|
|
|
const agentUuid = route.params.agentUuid as string
|
2026-02-26 01:32:04 +00:00
|
|
|
|
2026-02-27 12:26:51 +00:00
|
|
|
const agent = ref<AgentConfig>({
|
2026-02-26 01:32:04 +00:00
|
|
|
name: 'Loading...',
|
|
|
|
|
description: '',
|
|
|
|
|
status: 'idle',
|
2026-02-27 14:56:20 +00:00
|
|
|
uuid: agentUuid,
|
2026-02-27 12:26:51 +00:00
|
|
|
agent_type: 'knowledge',
|
|
|
|
|
llm_config: {},
|
|
|
|
|
organization: '',
|
2026-02-26 01:32:04 +00:00
|
|
|
})
|
2026-02-27 14:56:20 +00:00
|
|
|
const saveLoading = ref(false)
|
|
|
|
|
const editingConfig = ref(false)
|
|
|
|
|
const agentForm = ref({
|
|
|
|
|
name: '',
|
|
|
|
|
agent_type: 'knowledge',
|
|
|
|
|
model_id: '',
|
|
|
|
|
system_prompt: '',
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const agentTypeOptions = [
|
|
|
|
|
{ label: 'Curriculum Agent', value: 'curriculum' },
|
|
|
|
|
{ label: 'Knowledge Agent', value: 'knowledge' },
|
|
|
|
|
{ label: 'Assessment Agent', value: 'assessment' },
|
|
|
|
|
{ label: 'Progress Monitor', value: 'monitor' },
|
|
|
|
|
]
|
2026-02-26 01:32:04 +00:00
|
|
|
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') {
|
2026-02-27 12:26:51 +00:00
|
|
|
const output = completedEvent.content as AgentRunResult
|
2026-02-26 01:32:04 +00:00
|
|
|
|
|
|
|
|
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 {
|
2026-02-27 14:56:20 +00:00
|
|
|
const response = await apiClient.get<AgentConfig>(API.agents.configs.byId(agentUuid))
|
2026-02-26 01:32:04 +00:00
|
|
|
agent.value = response.data
|
2026-02-27 14:56:20 +00:00
|
|
|
agentForm.value = {
|
|
|
|
|
name: response.data.name || '',
|
|
|
|
|
agent_type: response.data.agent_type || 'knowledge',
|
|
|
|
|
model_id: String(response.data.llm_config?.model_id || ''),
|
|
|
|
|
system_prompt: String(response.data.system_prompt || ''),
|
|
|
|
|
}
|
2026-02-26 01:32:04 +00:00
|
|
|
} 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')
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-27 14:56:20 +00:00
|
|
|
const resetForm = () => {
|
|
|
|
|
agentForm.value = {
|
|
|
|
|
name: agent.value.name || '',
|
|
|
|
|
agent_type: agent.value.agent_type || 'knowledge',
|
|
|
|
|
model_id: String(agent.value.llm_config?.model_id || ''),
|
|
|
|
|
system_prompt: String(agent.value.system_prompt || ''),
|
|
|
|
|
}
|
|
|
|
|
editingConfig.value = false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const saveConfig = async () => {
|
|
|
|
|
const payload = {
|
|
|
|
|
name: agentForm.value.name.trim(),
|
|
|
|
|
system_prompt: agentForm.value.system_prompt.trim(),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!payload.name) {
|
|
|
|
|
message.error('Agent name is required')
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!payload.system_prompt) {
|
|
|
|
|
message.error('System prompt is required')
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
saveLoading.value = true
|
|
|
|
|
try {
|
|
|
|
|
const response = await apiClient.patch<AgentConfig>(API.agents.configs.byId(agentUuid), payload)
|
|
|
|
|
agent.value = response.data
|
|
|
|
|
editingConfig.value = false
|
|
|
|
|
message.success('Agent configuration updated')
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Failed to update agent config:', error)
|
|
|
|
|
if (isAxiosError(error)) {
|
|
|
|
|
message.error(error.response?.data?.detail || 'Failed to update configuration')
|
|
|
|
|
} else {
|
|
|
|
|
message.error('Failed to update configuration')
|
|
|
|
|
}
|
|
|
|
|
} finally {
|
|
|
|
|
saveLoading.value = false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-26 01:32:04 +00:00
|
|
|
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()
|
2026-02-27 14:56:20 +00:00
|
|
|
agentStore.connect(agentUuid)
|
2026-02-26 01:32:04 +00:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
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>
|
|
|
|
|
|
2026-02-27 14:56:20 +00:00
|
|
|
<Typography.Title :level="4" class="section-title">Configuration</Typography.Title>
|
|
|
|
|
<div class="execution-controls">
|
|
|
|
|
<Space direction="vertical" style="width: 100%" :size="12">
|
|
|
|
|
<div>
|
|
|
|
|
<Typography.Text>Agent Name:</Typography.Text>
|
|
|
|
|
<Input v-model:value="agentForm.name" :disabled="!editingConfig" />
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div>
|
|
|
|
|
<Typography.Text>Agent Type:</Typography.Text>
|
|
|
|
|
<Select
|
|
|
|
|
v-model:value="agentForm.agent_type"
|
|
|
|
|
:options="agentTypeOptions"
|
|
|
|
|
disabled
|
|
|
|
|
style="width: 100%"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div>
|
|
|
|
|
<Typography.Text>Model ID:</Typography.Text>
|
|
|
|
|
<Input v-model:value="agentForm.model_id" disabled />
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div>
|
|
|
|
|
<Typography.Text>System Prompt:</Typography.Text>
|
|
|
|
|
<Input.TextArea
|
|
|
|
|
v-model:value="agentForm.system_prompt"
|
|
|
|
|
:rows="6"
|
|
|
|
|
:disabled="!editingConfig"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<Space>
|
|
|
|
|
<Button v-if="!editingConfig" type="primary" @click="editingConfig = true">
|
|
|
|
|
Edit Configuration
|
|
|
|
|
</Button>
|
|
|
|
|
<template v-else>
|
|
|
|
|
<Button type="primary" :loading="saveLoading" @click="saveConfig">
|
|
|
|
|
Save Changes
|
|
|
|
|
</Button>
|
|
|
|
|
<Button @click="resetForm">Cancel</Button>
|
|
|
|
|
</template>
|
|
|
|
|
</Space>
|
|
|
|
|
</Space>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-02-26 01:32:04 +00:00
|
|
|
<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>
|