Added onboarding session progress view logic
This commit is contained in:
parent
a4f0fb3ea6
commit
83d6f38a24
6 changed files with 241 additions and 160 deletions
|
|
@ -54,13 +54,22 @@ class OnboardingConsumer(AsyncWebsocketConsumer):
|
|||
await self.run_full_onboarding_generation(role_uuid)
|
||||
elif action == "progress_monitor":
|
||||
role_uuid = data.get("role_uuid") or self.context_uuid
|
||||
target_user_uuid = data.get("user_uuid")
|
||||
flow_uuid = data.get("flow_uuid")
|
||||
if not role_uuid:
|
||||
await self.send_log("error", "Missing role_uuid for progress monitoring")
|
||||
return
|
||||
if not await self.can_access_role(role_uuid, self.user.id):
|
||||
await self.send_log("error", "Forbidden")
|
||||
return
|
||||
await self.run_progress_monitor(role_uuid)
|
||||
target_user_id = self.user.id
|
||||
if target_user_uuid and str(target_user_uuid) != str(self.user.uuid):
|
||||
target_user_id = await self.resolve_target_user_id(role_uuid, self.user.id, target_user_uuid)
|
||||
if not target_user_id:
|
||||
await self.send_log("error", "Forbidden")
|
||||
return
|
||||
|
||||
await self.run_progress_monitor(role_uuid, target_user_id=target_user_id, flow_uuid=flow_uuid)
|
||||
else:
|
||||
|
||||
user_message = data.get("query") or data.get("message")
|
||||
|
|
@ -232,7 +241,7 @@ class OnboardingConsumer(AsyncWebsocketConsumer):
|
|||
"message": "Onboarding pipeline complete and structure saved."
|
||||
}))
|
||||
|
||||
async def run_progress_monitor(self, role_uuid):
|
||||
async def run_progress_monitor(self, role_uuid, target_user_id=None, flow_uuid=None):
|
||||
await self.send_log("status", "Progress Monitor is analyzing your onboarding progress...", "monitor")
|
||||
|
||||
monitor_config = await self.get_config_by_type(role_uuid, 'monitor')
|
||||
|
|
@ -240,7 +249,11 @@ class OnboardingConsumer(AsyncWebsocketConsumer):
|
|||
await self.send_log("error", "Missing Progress Monitor AgentConfig for this role")
|
||||
return
|
||||
|
||||
progress_context = await self.get_role_progress_context(role_uuid, self.user.id)
|
||||
progress_context = await self.get_role_progress_context(
|
||||
role_uuid,
|
||||
target_user_id or self.user.id,
|
||||
flow_uuid=flow_uuid,
|
||||
)
|
||||
|
||||
monitor_prompt = (
|
||||
"You are a progress monitoring agent for onboarding. "
|
||||
|
|
@ -250,12 +263,21 @@ class OnboardingConsumer(AsyncWebsocketConsumer):
|
|||
f"Progress context JSON:\n{json.dumps(progress_context)}"
|
||||
)
|
||||
|
||||
feedback = await self.orchestrate_ai(
|
||||
monitor_prompt,
|
||||
monitor_config,
|
||||
min_internal_turns=1,
|
||||
max_tokens=640,
|
||||
)
|
||||
try:
|
||||
feedback = await self.orchestrate_ai(
|
||||
monitor_prompt,
|
||||
monitor_config,
|
||||
min_internal_turns=1,
|
||||
max_tokens=640,
|
||||
raise_on_error=True,
|
||||
)
|
||||
except Exception as exc:
|
||||
await self.send_log("error", f"Inference failed: {str(exc)}")
|
||||
return
|
||||
|
||||
if str(feedback).startswith("Error:"):
|
||||
await self.send_log("error", str(feedback))
|
||||
return
|
||||
|
||||
await self.send(json.dumps({
|
||||
"type": "completed",
|
||||
|
|
@ -265,6 +287,9 @@ class OnboardingConsumer(AsyncWebsocketConsumer):
|
|||
"role_uuid": role_uuid,
|
||||
"feedback": feedback,
|
||||
"status": progress_context.get("latest_status", "unknown"),
|
||||
"user_id": target_user_id or self.user.id,
|
||||
"flow_uuid": flow_uuid,
|
||||
"is_completed": progress_context.get("is_completed", False),
|
||||
}
|
||||
}))
|
||||
|
||||
|
|
@ -275,6 +300,7 @@ class OnboardingConsumer(AsyncWebsocketConsumer):
|
|||
min_internal_turns=2,
|
||||
max_turns=6,
|
||||
max_tokens=None,
|
||||
raise_on_error=False,
|
||||
):
|
||||
"""
|
||||
Handles the multi-turn ReAct loop (Reasoning + Tool Use).
|
||||
|
|
@ -360,6 +386,8 @@ class OnboardingConsumer(AsyncWebsocketConsumer):
|
|||
|
||||
except Exception as e:
|
||||
await self.send_log("error", f"Inference failed: {str(e)}")
|
||||
if raise_on_error:
|
||||
raise
|
||||
return f"Error: {str(e)}"
|
||||
|
||||
return last_content
|
||||
|
|
@ -663,13 +691,20 @@ class OnboardingConsumer(AsyncWebsocketConsumer):
|
|||
).order_by('-updated_at').first()
|
||||
|
||||
@database_sync_to_async
|
||||
def get_role_progress_context(self, role_uuid, user_id):
|
||||
def get_role_progress_context(self, role_uuid, user_id, flow_uuid=None):
|
||||
from apps.accounts.models import Role
|
||||
|
||||
role = Role.objects.get(uuid=role_uuid)
|
||||
sessions = OnboardingSession.objects.filter(user_id=user_id, role=role).order_by('-updated_at')
|
||||
latest_session = sessions.first()
|
||||
active_flow = OnboardingFlow.objects.filter(role=role, is_active=True).order_by('-updated_at').first()
|
||||
scoped_flow = None
|
||||
if flow_uuid:
|
||||
scoped_flow = OnboardingFlow.objects.filter(role=role, uuid=flow_uuid).first()
|
||||
|
||||
sessions = OnboardingSession.objects.filter(user_id=user_id, role=role).order_by('-updated_at')
|
||||
if flow_uuid:
|
||||
sessions = sessions.filter(state__flow_uuid=str(flow_uuid))
|
||||
|
||||
latest_session = sessions.first()
|
||||
|
||||
if not latest_session:
|
||||
return {
|
||||
|
|
@ -677,10 +712,12 @@ class OnboardingConsumer(AsyncWebsocketConsumer):
|
|||
"role_name": role.name,
|
||||
"latest_status": "not_started",
|
||||
"session_count": 0,
|
||||
"flow_exists": bool(active_flow),
|
||||
"flow_exists": bool(scoped_flow or active_flow),
|
||||
"flow_uuid": str((scoped_flow or active_flow).uuid) if (scoped_flow or active_flow) else None,
|
||||
"progress": 0,
|
||||
"responses_count": 0,
|
||||
"completed_modules": [],
|
||||
"is_completed": False,
|
||||
}
|
||||
|
||||
state = latest_session.state or {}
|
||||
|
|
@ -693,9 +730,31 @@ class OnboardingConsumer(AsyncWebsocketConsumer):
|
|||
"role_name": role.name,
|
||||
"latest_status": latest_session.status,
|
||||
"session_count": sessions.count(),
|
||||
"flow_exists": bool(active_flow),
|
||||
"flow_exists": bool(scoped_flow or active_flow),
|
||||
"flow_uuid": str((scoped_flow or active_flow).uuid) if (scoped_flow or active_flow) else None,
|
||||
"progress": progress,
|
||||
"responses_count": len(responses) if isinstance(responses, dict) else 0,
|
||||
"completed_modules": completed_modules if isinstance(completed_modules, list) else [],
|
||||
"updated_at": latest_session.updated_at.isoformat() if latest_session.updated_at else None,
|
||||
}
|
||||
"is_completed": latest_session.status == 'completed',
|
||||
}
|
||||
|
||||
@database_sync_to_async
|
||||
def resolve_target_user_id(self, role_uuid, requester_id, target_user_uuid):
|
||||
from apps.accounts.models import Role, User
|
||||
|
||||
role = Role.objects.filter(uuid=role_uuid).first()
|
||||
requester = User.objects.filter(id=requester_id).first()
|
||||
target = User.objects.filter(uuid=target_user_uuid).first()
|
||||
if role is None or requester is None or target is None:
|
||||
return None
|
||||
|
||||
is_owner = role.organization.owner.id == requester_id
|
||||
is_manager_member = bool(requester.is_manager) and role.organization.members.filter(id=requester_id).exists()
|
||||
if not (is_owner or is_manager_member):
|
||||
return None
|
||||
|
||||
if not role.members.filter(id=target.id).exists():
|
||||
return None
|
||||
|
||||
return target.id
|
||||
|
|
@ -144,6 +144,7 @@ export const API = {
|
|||
},
|
||||
sessions: {
|
||||
list: () => 'onboarding-session/',
|
||||
progressOverview: () => 'onboarding-session/progress-overview/',
|
||||
byId: (uuid: string) => `onboarding-session/${uuid}/`,
|
||||
interact: (uuid: string) => `onboarding-session/${uuid}/interact/`,
|
||||
askKa: (uuid: string) => `onboarding-session/${uuid}/ask-ka/`,
|
||||
|
|
|
|||
|
|
@ -29,7 +29,6 @@ export type OnboardingFlow = {
|
|||
agent?: string | null
|
||||
title: string
|
||||
description?: string
|
||||
status: 'draft' | 'published' | 'archived'
|
||||
pages?: OnboardingPage[]
|
||||
}
|
||||
|
||||
|
|
@ -63,6 +62,12 @@ export type ProgressSessionApi = {
|
|||
uuid: string
|
||||
status: OnboardingSessionStatus
|
||||
role: UuidNameRef
|
||||
user?: {
|
||||
uuid: string
|
||||
email_address?: string
|
||||
first_name?: string
|
||||
last_name?: string
|
||||
}
|
||||
updated_at?: string
|
||||
state?: Record<string, unknown>
|
||||
}
|
||||
|
|
@ -81,3 +86,26 @@ export type RoleProgressItem = {
|
|||
feedback?: string
|
||||
loadingFeedback: boolean
|
||||
}
|
||||
|
||||
export type ProgressOverviewItem = {
|
||||
role: UuidNameRef
|
||||
user: {
|
||||
uuid: string
|
||||
name: string
|
||||
email: string
|
||||
}
|
||||
flow: {
|
||||
uuid: string
|
||||
title: string
|
||||
is_active: boolean
|
||||
}
|
||||
latest_status: string
|
||||
progress: number
|
||||
is_completed: boolean
|
||||
latest_session_uuid?: string | null
|
||||
updated_at?: string | null
|
||||
}
|
||||
|
||||
export type FlowLookup = {
|
||||
title?: string
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ export interface Role {
|
|||
name: string
|
||||
description?: string
|
||||
organization: Organization
|
||||
members?: User[]
|
||||
member_count?: number
|
||||
created_at: string
|
||||
updated_at: string
|
||||
|
|
|
|||
|
|
@ -1,21 +1,26 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ref, computed, onMounted, onBeforeUnmount } from 'vue'
|
||||
import { useRoute, useRouter, onBeforeRouteLeave } from 'vue-router'
|
||||
import { Card, Typography, Button, Spin, Tag, List, message } from 'ant-design-vue'
|
||||
import { apiClient, API } from '../router/api'
|
||||
import type { Role } from '../types/organization'
|
||||
import type { ProgressSessionApi } from '../types/onboarding'
|
||||
import type { ProgressSessionApi, FlowLookup } from '../types/onboarding'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const roleId = computed(() => route.params.roleId as string)
|
||||
const selectedUserUuid = computed(() => String(route.query.user_uuid || ''))
|
||||
const selectedFlowUuid = computed(() => String(route.query.flow_uuid || ''))
|
||||
|
||||
const loading = ref(false)
|
||||
const monitoring = ref(false)
|
||||
const role = ref<Role | null>(null)
|
||||
const roleName = ref('Unknown Role')
|
||||
const learnerName = ref('Learner')
|
||||
const flowTitle = ref('')
|
||||
const sessions = ref<ProgressSessionApi[]>([])
|
||||
const feedback = ref<string>('')
|
||||
const monitorLogs = ref<string[]>([])
|
||||
const monitorSocket = ref<WebSocket | null>(null)
|
||||
const monitorTimeout = ref<number | null>(null)
|
||||
|
||||
const latestSession = computed(() => sessions.value[0] || null)
|
||||
|
||||
|
|
@ -24,21 +29,44 @@ const websocketUrl = (id: string) => {
|
|||
return `${protocol}://${window.location.host}/ws/onboarding/${id}/`
|
||||
}
|
||||
|
||||
const closeMonitorSocket = () => {
|
||||
if (monitorTimeout.value !== null) {
|
||||
window.clearTimeout(monitorTimeout.value)
|
||||
monitorTimeout.value = null
|
||||
}
|
||||
if (monitorSocket.value) {
|
||||
monitorSocket.value.close()
|
||||
monitorSocket.value = null
|
||||
}
|
||||
}
|
||||
|
||||
const runProgressMonitor = async () => {
|
||||
monitoring.value = true
|
||||
monitorLogs.value = []
|
||||
|
||||
try {
|
||||
closeMonitorSocket()
|
||||
const ws = new WebSocket(websocketUrl(roleId.value))
|
||||
monitorSocket.value = ws
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const timeout = window.setTimeout(() => {
|
||||
let settled = false
|
||||
|
||||
monitorTimeout.value = window.setTimeout(() => {
|
||||
settled = true
|
||||
ws.close()
|
||||
reject(new Error('Progress monitor timed out'))
|
||||
}, 30000)
|
||||
|
||||
ws.onopen = () => {
|
||||
ws.send(JSON.stringify({ action: 'progress_monitor', role_uuid: roleId.value }))
|
||||
ws.send(
|
||||
JSON.stringify({
|
||||
action: 'progress_monitor',
|
||||
role_uuid: roleId.value,
|
||||
user_uuid: selectedUserUuid.value || undefined,
|
||||
flow_uuid: selectedFlowUuid.value || undefined,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
|
|
@ -52,28 +80,55 @@ const runProgressMonitor = async () => {
|
|||
feedback.value = String(
|
||||
payload.content?.feedback || payload.message || 'No feedback returned.',
|
||||
)
|
||||
window.clearTimeout(timeout)
|
||||
if (monitorTimeout.value !== null) {
|
||||
window.clearTimeout(monitorTimeout.value)
|
||||
monitorTimeout.value = null
|
||||
}
|
||||
settled = true
|
||||
ws.close()
|
||||
resolve()
|
||||
}
|
||||
|
||||
if (payload.type === 'error') {
|
||||
window.clearTimeout(timeout)
|
||||
if (monitorTimeout.value !== null) {
|
||||
window.clearTimeout(monitorTimeout.value)
|
||||
monitorTimeout.value = null
|
||||
}
|
||||
settled = true
|
||||
ws.close()
|
||||
reject(new Error(payload.message || 'Progress monitor failed'))
|
||||
}
|
||||
} catch {
|
||||
window.clearTimeout(timeout)
|
||||
if (monitorTimeout.value !== null) {
|
||||
window.clearTimeout(monitorTimeout.value)
|
||||
monitorTimeout.value = null
|
||||
}
|
||||
settled = true
|
||||
ws.close()
|
||||
reject(new Error('Invalid monitor response'))
|
||||
}
|
||||
}
|
||||
|
||||
ws.onerror = () => {
|
||||
window.clearTimeout(timeout)
|
||||
if (monitorTimeout.value !== null) {
|
||||
window.clearTimeout(monitorTimeout.value)
|
||||
monitorTimeout.value = null
|
||||
}
|
||||
settled = true
|
||||
ws.close()
|
||||
reject(new Error('Progress monitor websocket error'))
|
||||
}
|
||||
|
||||
ws.onclose = () => {
|
||||
if (monitorTimeout.value !== null) {
|
||||
window.clearTimeout(monitorTimeout.value)
|
||||
monitorTimeout.value = null
|
||||
}
|
||||
monitorSocket.value = null
|
||||
if (!settled) {
|
||||
reject(new Error('Progress monitor connection closed before completion'))
|
||||
}
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? error.message : 'Failed to run progress monitor')
|
||||
|
|
@ -85,21 +140,40 @@ const runProgressMonitor = async () => {
|
|||
const loadData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const [rolesRes, sessionsRes] = await Promise.all([
|
||||
apiClient.get<Role[]>(API.roles.mine()),
|
||||
apiClient.get<ProgressSessionApi[]>(API.onboarding.sessions.list()),
|
||||
])
|
||||
|
||||
const roles = Array.isArray(rolesRes.data) ? rolesRes.data : []
|
||||
role.value = roles.find((r) => r.uuid === roleId.value) || null
|
||||
const sessionsRes = await apiClient.get<ProgressSessionApi[]>(API.onboarding.sessions.list(), {
|
||||
params: {
|
||||
role_uuid: roleId.value,
|
||||
user_uuid: selectedUserUuid.value || undefined,
|
||||
flow_uuid: selectedFlowUuid.value || undefined,
|
||||
},
|
||||
})
|
||||
|
||||
const allSessions = Array.isArray(sessionsRes.data) ? sessionsRes.data : []
|
||||
sessions.value = allSessions
|
||||
.filter((session) => session.role?.uuid === roleId.value)
|
||||
.sort(
|
||||
(a, b) =>
|
||||
new Date(b.updated_at || 0).getTime() - new Date(a.updated_at || 0).getTime(),
|
||||
sessions.value = allSessions.sort(
|
||||
(a, b) => new Date(b.updated_at || 0).getTime() - new Date(a.updated_at || 0).getTime(),
|
||||
)
|
||||
|
||||
if (sessions.value[0]?.role?.name) {
|
||||
roleName.value = String(sessions.value[0].role.name)
|
||||
}
|
||||
if (sessions.value[0]?.user?.first_name || sessions.value[0]?.user?.last_name) {
|
||||
const first = String(sessions.value[0].user.first_name || '')
|
||||
const last = String(sessions.value[0].user.last_name || '')
|
||||
learnerName.value = `${first} ${last}`.trim()
|
||||
}
|
||||
|
||||
if (!sessions.value.length) {
|
||||
feedback.value = 'No onboarding session found for this learner and flow yet.'
|
||||
monitorLogs.value = []
|
||||
return
|
||||
}
|
||||
|
||||
if (selectedFlowUuid.value) {
|
||||
const flowRes = await apiClient.get<FlowLookup>(
|
||||
API.onboarding.flows.byId(selectedFlowUuid.value),
|
||||
)
|
||||
flowTitle.value = String(flowRes.data?.title || '')
|
||||
}
|
||||
|
||||
await runProgressMonitor()
|
||||
} catch {
|
||||
|
|
@ -112,6 +186,14 @@ const loadData = async () => {
|
|||
onMounted(() => {
|
||||
loadData()
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
closeMonitorSocket()
|
||||
})
|
||||
|
||||
onBeforeRouteLeave(() => {
|
||||
closeMonitorSocket()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -123,7 +205,11 @@ onMounted(() => {
|
|||
|
||||
<Card class="panel" :bordered="false">
|
||||
<Spin :spinning="loading" tip="Loading role progress...">
|
||||
<Typography.Title :level="4">{{ role?.name || 'Unknown Role' }}</Typography.Title>
|
||||
<Typography.Title :level="4">{{ roleName }}</Typography.Title>
|
||||
<Typography.Paragraph type="secondary">
|
||||
Learner: {{ learnerName }}
|
||||
<span v-if="flowTitle"> | Flow: {{ flowTitle }}</span>
|
||||
</Typography.Paragraph>
|
||||
|
||||
<div class="status-row">
|
||||
<Typography.Text strong>Latest Status:</Typography.Text>
|
||||
|
|
|
|||
|
|
@ -3,107 +3,19 @@ import { ref, onMounted } from 'vue'
|
|||
import { useRouter } from 'vue-router'
|
||||
import { Card, Typography, List, Tag, Button, Spin, message } from 'ant-design-vue'
|
||||
import { apiClient, API } from '../router/api'
|
||||
import type { Role } from '../types/organization'
|
||||
import type { ProgressSessionApi, ProgressFlowApi, RoleProgressItem } from '../types/onboarding'
|
||||
import type { ProgressOverviewItem } from '../types/onboarding'
|
||||
|
||||
const router = useRouter()
|
||||
const loading = ref(false)
|
||||
const rows = ref<RoleProgressItem[]>([])
|
||||
|
||||
const websocketUrl = (roleId: string) => {
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss' : 'ws'
|
||||
return `${protocol}://${window.location.host}/ws/onboarding/${roleId}/`
|
||||
}
|
||||
|
||||
const runProgressMonitor = (roleId: string): Promise<string> =>
|
||||
new Promise((resolve, reject) => {
|
||||
const ws = new WebSocket(websocketUrl(roleId))
|
||||
const timeout = window.setTimeout(() => {
|
||||
ws.close()
|
||||
reject(new Error('Progress monitor timed out'))
|
||||
}, 30000)
|
||||
|
||||
ws.onopen = () => {
|
||||
ws.send(
|
||||
JSON.stringify({
|
||||
action: 'progress_monitor',
|
||||
role_uuid: roleId,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
try {
|
||||
const payload = JSON.parse(event.data)
|
||||
if (payload.type === 'completed') {
|
||||
window.clearTimeout(timeout)
|
||||
const feedback =
|
||||
payload.content?.feedback || payload.message || 'No feedback returned.'
|
||||
ws.close()
|
||||
resolve(String(feedback))
|
||||
} else if (payload.type === 'error') {
|
||||
window.clearTimeout(timeout)
|
||||
ws.close()
|
||||
reject(new Error(payload.message || 'Progress monitor failed'))
|
||||
}
|
||||
} catch {
|
||||
window.clearTimeout(timeout)
|
||||
ws.close()
|
||||
reject(new Error('Invalid monitor response'))
|
||||
}
|
||||
}
|
||||
|
||||
ws.onerror = () => {
|
||||
window.clearTimeout(timeout)
|
||||
ws.close()
|
||||
reject(new Error('Progress monitor websocket error'))
|
||||
}
|
||||
})
|
||||
const rows = ref<ProgressOverviewItem[]>([])
|
||||
|
||||
const loadProgress = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const [rolesRes, sessionsRes, flowsRes] = await Promise.all([
|
||||
apiClient.get<Role[]>(API.roles.mine()),
|
||||
apiClient.get<ProgressSessionApi[]>(API.onboarding.sessions.list()),
|
||||
apiClient.get<ProgressFlowApi[]>(API.onboarding.flows.list()),
|
||||
])
|
||||
|
||||
const roles = Array.isArray(rolesRes.data) ? rolesRes.data : []
|
||||
const sessions = Array.isArray(sessionsRes.data) ? sessionsRes.data : []
|
||||
const flows = Array.isArray(flowsRes.data) ? flowsRes.data : []
|
||||
|
||||
rows.value = roles.map((role) => {
|
||||
const roleSessions = sessions
|
||||
.filter((session) => session.role?.uuid === role.uuid)
|
||||
.sort(
|
||||
(a, b) =>
|
||||
new Date(b.updated_at || 0).getTime() -
|
||||
new Date(a.updated_at || 0).getTime(),
|
||||
)
|
||||
const latestSession = roleSessions[0]
|
||||
const flow = flows.find((f) => f.role?.uuid === role.uuid)
|
||||
|
||||
return {
|
||||
role,
|
||||
latestStatus: latestSession?.status || 'not_started',
|
||||
latestSessionUuid: latestSession?.uuid,
|
||||
flowTitle: flow?.title,
|
||||
feedback: undefined,
|
||||
loadingFeedback: true,
|
||||
}
|
||||
})
|
||||
|
||||
for (const row of rows.value) {
|
||||
try {
|
||||
row.feedback = await runProgressMonitor(row.role.uuid)
|
||||
} catch (error) {
|
||||
row.feedback =
|
||||
error instanceof Error ? error.message : 'Unable to fetch monitor feedback.'
|
||||
} finally {
|
||||
row.loadingFeedback = false
|
||||
}
|
||||
}
|
||||
const overviewRes = await apiClient.get<ProgressOverviewItem[]>(
|
||||
API.onboarding.sessions.progressOverview(),
|
||||
)
|
||||
rows.value = Array.isArray(overviewRes.data) ? overviewRes.data : []
|
||||
} catch {
|
||||
message.error('Failed to load progress overview')
|
||||
} finally {
|
||||
|
|
@ -129,39 +41,40 @@ onMounted(() => {
|
|||
<template #renderItem="{ item }">
|
||||
<List.Item>
|
||||
<List.Item.Meta
|
||||
:title="item.role.name"
|
||||
:description="item.flowTitle || 'No active flow yet'"
|
||||
:title="`${item.role.name} · ${item.user.name}`"
|
||||
:description="item.flow.title"
|
||||
/>
|
||||
<div class="row-meta">
|
||||
<Tag
|
||||
:color="
|
||||
item.latestStatus === 'completed'
|
||||
item.latest_status === 'completed'
|
||||
? 'green'
|
||||
: item.latestStatus === 'active'
|
||||
: item.latest_status === 'active'
|
||||
? 'blue'
|
||||
: 'default'
|
||||
"
|
||||
>
|
||||
{{ item.latestStatus }}
|
||||
{{ item.latest_status }}
|
||||
</Tag>
|
||||
<Tag color="gold">{{ item.progress }}%</Tag>
|
||||
<Tag :color="item.flow.is_active ? 'cyan' : 'default'">
|
||||
{{ item.flow.is_active ? 'active flow' : 'inactive flow' }}
|
||||
</Tag>
|
||||
<Button
|
||||
type="primary"
|
||||
@click="router.push(`/progress/${item.role.uuid}`)"
|
||||
@click="
|
||||
router.push({
|
||||
path: `/progress/${item.role.uuid}`,
|
||||
query: {
|
||||
user_uuid: item.user.uuid,
|
||||
flow_uuid: item.flow.uuid,
|
||||
},
|
||||
})
|
||||
"
|
||||
>
|
||||
View Details
|
||||
</Button>
|
||||
</div>
|
||||
<div class="feedback-block">
|
||||
<Typography.Text strong>Progress Monitor:</Typography.Text>
|
||||
<Spin
|
||||
v-if="item.loadingFeedback"
|
||||
size="small"
|
||||
style="margin-left: 0.5rem"
|
||||
/>
|
||||
<Typography.Paragraph v-else class="feedback-text">
|
||||
{{ item.feedback || 'No feedback yet.' }}
|
||||
</Typography.Paragraph>
|
||||
</div>
|
||||
</List.Item>
|
||||
</template>
|
||||
</List>
|
||||
|
|
@ -183,15 +96,8 @@ onMounted(() => {
|
|||
.row-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
.feedback-block {
|
||||
margin-top: 0.8rem;
|
||||
}
|
||||
.feedback-text {
|
||||
margin-top: 0.4rem;
|
||||
color: #6b7280;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
Loading…
Reference in a new issue