feat: real-time activity indicator in log panel

- New WsMessage::ActivityUpdate for live status broadcasting
- Shows current activity at bottom of log: LLM calls, tool execution, user approval
- Activity bar with spinner, auto-clears on workflow completion
- Status badge with pulse animation in log header
This commit is contained in:
Fam Zheng
2026-03-09 10:26:42 +00:00
parent cc75f8deac
commit 29f026e383
4 changed files with 87 additions and 1 deletions

View File

@@ -12,6 +12,7 @@ const props = defineProps<{
createdAt: string
workflowStatus: string
workflowId: string
currentActivity: string
}>()
const emit = defineEmits<{
@@ -278,6 +279,10 @@ watch(logItems, () => {
</div>
</div>
</template>
<div v-if="currentActivity" class="activity-bar">
<span class="activity-spinner" />
<span class="activity-text">{{ currentActivity }}</span>
</div>
<div v-if="!entries.length && !requirement" class="empty-state">
提交需求后日志将显示在这里
</div>
@@ -529,6 +534,36 @@ watch(logItems, () => {
margin: 0;
}
.activity-bar {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 12px;
border-radius: 6px;
background: rgba(79, 195, 247, 0.08);
border: 1px dashed var(--accent);
font-size: 13px;
color: var(--accent);
}
.activity-spinner {
width: 14px;
height: 14px;
border: 2px solid var(--accent);
border-top-color: transparent;
border-radius: 50%;
animation: spin 0.8s linear infinite;
flex-shrink: 0;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.activity-text {
font-weight: 500;
}
.empty-state {
color: var(--text-secondary);
font-size: 13px;

View File

@@ -24,6 +24,7 @@ const planSteps = ref<PlanStepInfo[]>([])
const comments = ref<Comment[]>([])
const llmCalls = ref<LlmCallLogEntry[]>([])
const quotes = ref<string[]>([])
const currentActivity = ref('')
const error = ref('')
const rightTab = ref<'log' | 'timers'>('log')
const commentRef = ref<InstanceType<typeof CommentSection> | null>(null)
@@ -92,6 +93,14 @@ function handleWsMessage(msg: WsMessage) {
case 'WorkflowStatusUpdate':
if (workflow.value && msg.workflow_id === workflow.value.id) {
workflow.value = { ...workflow.value, status: msg.status as any }
if (msg.status === 'done' || msg.status === 'failed') {
currentActivity.value = ''
}
}
break
case 'ActivityUpdate':
if (workflow.value && msg.workflow_id === workflow.value.id) {
currentActivity.value = msg.activity
}
break
case 'RequirementUpdate':
@@ -186,6 +195,7 @@ async function onSubmitComment(text: string) {
:createdAt="workflow?.created_at || ''"
:workflowStatus="workflow?.status || 'pending'"
:workflowId="workflow?.id || ''"
:currentActivity="currentActivity"
@quote="addQuote"
/>
<TimerSection

View File

@@ -40,12 +40,18 @@ export interface WsLlmCallLog {
entry: import('./types').LlmCallLogEntry
}
export interface WsActivityUpdate {
type: 'ActivityUpdate'
workflow_id: string
activity: string
}
export interface WsError {
type: 'Error'
message: string
}
export type WsMessage = WsPlanUpdate | WsStepStatusUpdate | WsWorkflowStatusUpdate | WsRequirementUpdate | WsReportReady | WsProjectUpdate | WsLlmCallLog | WsError
export type WsMessage = WsPlanUpdate | WsStepStatusUpdate | WsWorkflowStatusUpdate | WsRequirementUpdate | WsReportReady | WsProjectUpdate | WsLlmCallLog | WsActivityUpdate | WsError
export type WsHandler = (msg: WsMessage) => void