feat(dashboard): AgentModal live working feed & thinking stream
This commit is contained in:
@@ -24,11 +24,19 @@ interface ModelOption {
|
||||
provider: string
|
||||
}
|
||||
|
||||
interface ActivityEntry {
|
||||
time: string
|
||||
text: string
|
||||
}
|
||||
|
||||
const availableModels = ref<ModelOption[]>([])
|
||||
const selectedModel = ref('')
|
||||
const currentModel = ref('')
|
||||
const saving = ref(false)
|
||||
|
||||
const activityEntries = ref<ActivityEntry[]>([])
|
||||
const activityLoaded = ref(false)
|
||||
|
||||
async function loadModels() {
|
||||
try {
|
||||
const res = await fetch('/api/dashboard/models', { credentials: 'include' })
|
||||
@@ -75,9 +83,23 @@ async function saveModel() {
|
||||
}
|
||||
}
|
||||
|
||||
async function loadActivity() {
|
||||
try {
|
||||
const res = await fetch(`/api/dashboard/agents/${props.agent.id}/activity?limit=5`, { credentials: 'include' })
|
||||
if (res.ok) {
|
||||
activityEntries.value = await res.json()
|
||||
}
|
||||
} catch {
|
||||
// silent — fallback to empty
|
||||
} finally {
|
||||
activityLoaded.value = true
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await loadModels()
|
||||
await loadCurrentModel()
|
||||
await loadActivity()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -139,16 +161,35 @@ onMounted(async () => {
|
||||
|
||||
<!-- Current Task -->
|
||||
<section class="modal-section">
|
||||
<h3 class="section-label">Current Task</h3>
|
||||
<h3 class="section-label">
|
||||
Current Task
|
||||
<span v-if="agent.active" class="thinking-indicator">
|
||||
<span class="thinking-dots-inline">
|
||||
<span class="tdot"></span>
|
||||
<span class="tdot"></span>
|
||||
<span class="tdot"></span>
|
||||
</span>
|
||||
Thinking…
|
||||
</span>
|
||||
</h3>
|
||||
<p class="section-value">
|
||||
{{ agent.currentTask }}
|
||||
<span v-if="agent.active" class="thinking-dots">
|
||||
<span class="thinking-dot blue"></span>
|
||||
<span class="thinking-dot violet"></span>
|
||||
</span>
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<!-- Recent Activity -->
|
||||
<section class="modal-section">
|
||||
<h3 class="section-label">Recent Activity</h3>
|
||||
<div v-if="!activityLoaded" class="activity-placeholder">Loading…</div>
|
||||
<div v-else-if="activityEntries.length === 0" class="activity-placeholder">No recent activity</div>
|
||||
<div v-else class="activity-list">
|
||||
<div v-for="(entry, idx) in activityEntries" :key="idx" class="activity-item">
|
||||
<span class="activity-time">{{ entry.time }}</span>
|
||||
<span class="activity-text">{{ entry.text.length > 100 ? entry.text.slice(0, 100) + '…' : entry.text }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Goal + Progress -->
|
||||
<section class="modal-section">
|
||||
<h3 class="section-label">Goal</h3>
|
||||
@@ -384,34 +425,88 @@ onMounted(async () => {
|
||||
transition: width 0.5s ease;
|
||||
}
|
||||
|
||||
/* Thinking Dots */
|
||||
.thinking-dots {
|
||||
/* Thinking Indicator */
|
||||
.thinking-indicator {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
flex-shrink: 0;
|
||||
font-size: 9px;
|
||||
font-weight: 500;
|
||||
text-transform: none;
|
||||
letter-spacing: normal;
|
||||
color: #a78bfa;
|
||||
margin-left: 8px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.thinking-dot {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
.thinking-dots-inline {
|
||||
display: inline-flex;
|
||||
gap: 4px;
|
||||
}
|
||||
.thinking-dots-inline .tdot {
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
border-radius: 50%;
|
||||
background: #a78bfa;
|
||||
animation: thinking-bounce 1.2s ease-in-out infinite;
|
||||
}
|
||||
.thinking-dot.blue {
|
||||
background: #3b82f6;
|
||||
box-shadow: 0 0 8px #3b82f6;
|
||||
animation: pulse-dot-blue 1.2s ease-in-out infinite;
|
||||
.thinking-dots-inline .tdot:nth-child(2) {
|
||||
animation-delay: 0.2s;
|
||||
}
|
||||
.thinking-dot.violet {
|
||||
background: #8b7cf6;
|
||||
box-shadow: 0 0 8px #8b7cf6;
|
||||
animation: pulse-dot-violet 1.8s ease-in-out infinite 0.3s;
|
||||
.thinking-dots-inline .tdot:nth-child(3) {
|
||||
animation-delay: 0.4s;
|
||||
}
|
||||
@keyframes pulse-dot-blue {
|
||||
0%, 100% { opacity: 0.4; transform: scale(0.7); }
|
||||
50% { opacity: 1; transform: scale(1.3); }
|
||||
@keyframes thinking-bounce {
|
||||
0%, 80%, 100% { transform: scale(0.6); opacity: 0.4; }
|
||||
40% { transform: scale(1); opacity: 1; }
|
||||
}
|
||||
@keyframes pulse-dot-violet {
|
||||
0%, 100% { opacity: 0.3; transform: scale(0.6); }
|
||||
50% { opacity: 1; transform: scale(1.4); }
|
||||
|
||||
/* Recent Activity */
|
||||
.activity-placeholder {
|
||||
font-size: 10px;
|
||||
color: #6b7385;
|
||||
font-style: italic;
|
||||
padding: 4px 0;
|
||||
}
|
||||
.activity-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
max-height: 160px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.activity-list::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
.activity-list::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
.activity-list::-webkit-scrollbar-thumb {
|
||||
background: rgba(139, 124, 246, 0.15);
|
||||
border-radius: 3px;
|
||||
}
|
||||
.activity-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
padding: 6px 10px;
|
||||
border-radius: 8px;
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
border: 1px solid rgba(255, 255, 255, 0.04);
|
||||
}
|
||||
.activity-time {
|
||||
font-size: 9px;
|
||||
font-weight: 600;
|
||||
color: #6b7385;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.activity-text {
|
||||
font-size: 11px;
|
||||
line-height: 1.45;
|
||||
color: #c4c8d4;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Footer */
|
||||
|
||||
Reference in New Issue
Block a user