Added pages for frontend

This commit is contained in:
Viswamedha Nalabotu 2025-12-17 14:47:51 +00:00
parent a10632e4bf
commit a5f039d021
21 changed files with 2268 additions and 916 deletions

View file

@ -4,7 +4,8 @@
"license": "MIT",
"scripts": {
"dev": "vite dev",
"build": "vite build"
"build": "vite build",
"format": "prettier --write --tab-width 4 --use-tabs false \"src/**/*.{ts,vue,js,css}\""
},
"private": true,
"dependencies": {

67
src/lib/api.ts Normal file
View file

@ -0,0 +1,67 @@
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';
class ApiClient {
private client: AxiosInstance;
constructor() {
this.client = axios.create({ withCredentials: true });
}
private getCsrfToken(): string {
const match = document.cookie.match(/(?:^|; )csrftoken=([^;]+)/);
return match ? decodeURIComponent(match[1]) : '';
}
private withCsrf(config?: AxiosRequestConfig): AxiosRequestConfig {
const token = this.getCsrfToken();
const csrfHeader = token ? { 'X-CSRFToken': token } : {};
return {
...config,
headers: {
...csrfHeader,
...(config?.headers || {}),
},
};
}
get<T = unknown>(
url: string,
config?: AxiosRequestConfig
): Promise<AxiosResponse<T>> {
return this.client.get<T>(url, this.withCsrf(config));
}
post<T = unknown>(
url: string,
data?: unknown,
config?: AxiosRequestConfig
): Promise<AxiosResponse<T>> {
return this.client.post<T>(url, data, this.withCsrf(config));
}
put<T = unknown>(
url: string,
data?: unknown,
config?: AxiosRequestConfig
): Promise<AxiosResponse<T>> {
return this.client.put<T>(url, data, this.withCsrf(config));
}
patch<T = unknown>(
url: string,
data?: unknown,
config?: AxiosRequestConfig
): Promise<AxiosResponse<T>> {
return this.client.patch<T>(url, data, this.withCsrf(config));
}
delete<T = unknown>(
url: string,
config?: AxiosRequestConfig
): Promise<AxiosResponse<T>> {
return this.client.delete<T>(url, this.withCsrf(config));
}
}
export const apiClient = new ApiClient();
export { isAxiosError } from 'axios';

View file

@ -1,8 +1,11 @@
import './styles.css';
import 'ant-design-vue/dist/reset.css';
import router from './router';
import { createApp } from 'vue';
import { createPinia } from 'pinia';
import App from './app/App.vue';
const app = createApp(App);
app.use(createPinia());
app.use(router);
app.mount('#root');

View file

@ -1,4 +1,6 @@
import { createRouter, createWebHistory } from 'vue-router';
import { useAuthStore } from '../stores/authStore';
import { message } from 'ant-design-vue';
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
@ -17,53 +19,92 @@ const router = createRouter({
path: '/login',
name: 'login',
component: () => import('../views/LoginView.vue'),
meta: { guestOnly: true },
},
{
path: '/register',
name: 'register',
component: () => import('../views/RegisterView.vue'),
meta: { guestOnly: true },
},
{
path: '/onboarding',
name: 'onboarding',
component: () => import('../views/OnboardingFlow.vue'),
meta: { requiresAuth: true },
},
{
path: '/training/:moduleId?',
name: 'training',
component: () => import('../views/TrainingModule.vue'),
meta: { requiresAuth: true },
},
{
path: '/agents',
name: 'agents',
component: () => import('../views/Agents.vue'),
meta: { requiresAuth: true, roles: ['manager', 'admin'] },
},
{
path: '/agents/:id',
name: 'agent-detail',
component: () => import('../views/AgentDetail.vue'),
meta: { requiresAuth: true, roles: ['manager', 'admin'] },
},
{
path: '/roles',
name: 'roles',
component: () => import('../views/RoleProfiles.vue'),
meta: { requiresAuth: true, roles: ['manager', 'admin'] },
},
{
path: '/progress',
name: 'progress',
component: () => import('../views/ProgressDashboard.vue'),
meta: { requiresAuth: true },
},
{
path: '/assessments',
name: 'assessments',
component: () => import('../views/Assessments.vue'),
meta: { requiresAuth: true },
},
{
path: '/resources',
name: 'resources',
component: () => import('../views/Resources.vue'),
meta: { requiresAuth: true },
},
],
});
export default router;
router.beforeEach(async (to, _from, next) => {
const authStore = useAuthStore();
try {
await authStore.fetchSession();
} catch (err) {
console.error('Failed to fetch session during navigation:', err);
}
const isAuthed = authStore.isAuthenticated;
const role = authStore.user?.role;
if (to.meta?.guestOnly && isAuthed) {
return next({ path: '/onboarding' });
}
if (to.meta?.requiresAuth && !isAuthed) {
return next({ path: '/login', query: { redirect: to.fullPath } });
}
const allowedRoles = (to.meta?.roles as string[] | undefined) || null;
if (allowedRoles && (!role || !allowedRoles.includes(role))) {
message.error('You do not have access to that page');
return next({ path: '/' });
}
return next();
});

271
src/stores/agentStore.ts Normal file
View file

@ -0,0 +1,271 @@
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
interface AgentEvent {
type: string;
content?: string | Record<string, unknown>;
message?: string;
timestamp: Date;
event_type?: string;
error_message?: string;
execution_id?: string;
output_data?: Record<string, unknown>;
}
export const useAgentStore = defineStore('agent', () => {
const socket = ref<WebSocket | null>(null);
const isConnected = ref(false);
const currentExecutionId = ref<string | null>(null);
const events = ref<AgentEvent[]>([]);
const executionStatus = ref<string>('idle');
const agentId = ref<string | null>(null);
const reconnectAttempts = ref(0);
const maxReconnectAttempts = 5;
const connect = (agentIdParam: string) => {
console.log(
'[agentStore] connect() called with agent ID:',
agentIdParam
);
if (socket.value && isConnected.value) {
console.log(
'[agentStore] Already connected to agent:',
agentIdParam
);
return;
}
agentId.value = agentIdParam;
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsUrl = `${protocol}//${window.location.host}/ws/agents/${agentIdParam}/`;
console.log('[agentStore] WebSocket URL:', wsUrl);
socket.value = new WebSocket(wsUrl);
console.log('[agentStore] WebSocket object created');
socket.value.onopen = () => {
isConnected.value = true;
reconnectAttempts.value = 0;
console.log(
'[agentStore] SUCCESS - WebSocket connected to agent:',
agentIdParam
);
console.log('[agentStore] isConnected is now:', isConnected.value);
};
socket.value.onmessage = (event) => {
console.log('[agentStore] Message received from WebSocket');
try {
const data = JSON.parse(event.data);
console.log('[agentStore] Parsed message:', data);
handleMessage(data);
} catch (error) {
console.error(
'[agentStore] ERROR - Failed to parse WebSocket message:',
error
);
console.error('[agentStore] Raw message:', event.data);
}
};
socket.value.onerror = (error) => {
console.error(
'[agentStore] ERROR - WebSocket error occurred:',
error
);
isConnected.value = false;
};
socket.value.onclose = () => {
isConnected.value = false;
console.log(
'[agentStore] WebSocket closed for agent:',
agentIdParam
);
attemptReconnect(agentIdParam);
};
};
const attemptReconnect = (agentIdParam: string) => {
console.log('[agentStore] attemptReconnect() called');
if (reconnectAttempts.value < maxReconnectAttempts) {
reconnectAttempts.value++;
const delay = Math.min(
1000 * Math.pow(2, reconnectAttempts.value),
10000
);
console.log(
`[agentStore] Attempting to reconnect... (attempt ${reconnectAttempts.value}/${maxReconnectAttempts}, delay: ${delay}ms)`
);
setTimeout(() => connect(agentIdParam), delay);
} else {
console.error(
'[agentStore] ERROR - Max reconnection attempts reached (${maxReconnectAttempts})'
);
}
};
const handleMessage = (data: Record<string, unknown>) => {
console.log(
'[agentStore] handleMessage() called with type:',
data.type
);
console.log('[agentStore] Full message data:', data);
if (data.type === 'connection') {
console.log('[agentStore] Connection message:', data.message);
} else if (data.type === 'execution_started') {
console.log('[agentStore] Execution started');
currentExecutionId.value = data.execution_id as string;
executionStatus.value = 'running';
events.value = [];
console.log(
'[agentStore] Status changed to: running, execution ID:',
currentExecutionId.value
);
events.value.push({
type: 'started',
message: data.message as string,
timestamp: new Date(),
});
} else if (data.type === 'agent_event') {
console.log('[agentStore] Agent event received:', data.event_type);
events.value.push({
type: data.event_type as string,
content: data.content as string | Record<string, unknown>,
timestamp: new Date(data.timestamp as string),
});
} else if (data.type === 'execution_completed') {
console.log('[agentStore] Execution completed');
executionStatus.value = 'completed';
events.value.push({
type: 'completed',
content: data.output_data as Record<string, unknown>,
message: data.message as string,
timestamp: new Date(),
});
} else if (data.type === 'execution_error') {
console.log('[agentStore] Execution error:', data.error_message);
executionStatus.value = 'failed';
events.value.push({
type: 'error',
message: data.error_message as string,
timestamp: new Date(),
});
} else if (data.type === 'execution_stopped') {
console.log('[agentStore] Execution stopped');
executionStatus.value = 'stopped';
events.value.push({
type: 'stopped',
message: data.message as string,
timestamp: new Date(),
});
} else if (data.type === 'error') {
console.log('[agentStore] Generic error:', data.message);
events.value.push({
type: 'error',
message: data.message as string,
timestamp: new Date(),
});
} else {
console.warn(
'[agentStore] WARNING - Unknown message type:',
data.type
);
}
};
const startAgent = (inputData: Record<string, unknown> = {}) => {
console.log('[agentStore] startAgent() called with data:', inputData);
if (!socket.value) {
console.error('[agentStore] ERROR - WebSocket not initialized');
return;
}
if (!isConnected.value) {
console.error(
'[agentStore] ERROR - WebSocket not connected (isConnected:',
isConnected.value,
')'
);
return;
}
try {
const message = {
action: 'start_agent',
input_data: inputData,
};
console.log('[agentStore] Sending message:', message);
socket.value.send(JSON.stringify(message));
console.log('[agentStore] SUCCESS - Message sent to WebSocket');
} catch (error) {
console.error(
'[agentStore] ERROR - Failed to send WebSocket message:',
error
);
}
};
const stopAgent = () => {
console.log('[agentStore] stopAgent() called');
if (!socket.value) {
console.error('[agentStore] ERROR - WebSocket not initialized');
return;
}
if (!isConnected.value) {
console.error('[agentStore] ERROR - WebSocket not connected');
return;
}
try {
const message = {
action: 'stop_agent',
execution_id: currentExecutionId.value,
};
console.log('[agentStore] Sending message:', message);
socket.value.send(JSON.stringify(message));
console.log(
'[agentStore] SUCCESS - Stop message sent to WebSocket'
);
} catch (error) {
console.error(
'[agentStore] ERROR - Failed to send stop message:',
error
);
}
};
const disconnect = () => {
console.log('[agentStore] disconnect() called');
reconnectAttempts.value = maxReconnectAttempts;
if (socket.value) {
console.log('[agentStore] Closing WebSocket connection');
socket.value.close();
console.log('[agentStore] WebSocket close initiated');
} else {
console.warn('[agentStore] WARNING - No WebSocket to disconnect');
}
};
const eventLog = computed(() => events.value);
return {
socket,
isConnected,
currentExecutionId,
executionStatus,
agentId,
eventLog,
connect,
startAgent,
stopAgent,
disconnect,
};
});

159
src/stores/authStore.ts Normal file
View file

@ -0,0 +1,159 @@
import { computed, ref } from 'vue';
import { defineStore } from 'pinia';
import { apiClient, isAxiosError } from '../lib/api';
export interface AuthUser {
id: number;
uuid: string;
email_address: string;
first_name: string;
last_name: string;
bio?: string;
timezone?: string;
avatar_url?: string;
role?: string;
date_of_birth?: string;
created_at?: string;
updated_at?: string;
}
interface SessionResponse {
isAuthenticated: boolean;
isStaff: boolean;
}
export const useAuthStore = defineStore('auth', () => {
const user = ref<AuthUser | null>(null);
const loading = ref(false);
const initialized = ref(false);
const error = ref<string | null>(null);
const isAuthenticated = computed(() => Boolean(user.value));
const hasRole = (roles: string[] = []) => {
if (!roles.length) return true;
return roles.includes(user.value?.role || '');
};
const displayName = computed(() => {
if (!user.value) return '';
if (user.value.first_name || user.value.last_name) {
return `${user.value.first_name || ''} ${
user.value.last_name || ''
}`.trim();
}
return user.value.email_address;
});
const setUser = (value: AuthUser | null) => {
user.value = value;
initialized.value = true;
};
const fetchSession = async (force = false) => {
if (initialized.value && !force) return user.value;
loading.value = true;
error.value = null;
try {
const sessionRes = await apiClient.get<SessionResponse>(
'/api/user/session/'
);
if (sessionRes.data?.isAuthenticated) {
const meRes = await apiClient.get<AuthUser>('/api/user/me/');
setUser(meRes.data);
} else {
setUser(null);
}
return user.value;
} catch (err) {
error.value = isAxiosError(err)
? err.response?.data?.detail || err.message
: 'Unable to fetch session';
setUser(null);
throw err;
} finally {
loading.value = false;
}
};
const login = async (emailAddress: string, password: string) => {
loading.value = true;
error.value = null;
try {
const res = await apiClient.post<{
user: AuthUser;
message?: string;
}>('/api/user/login/', { email_address: emailAddress, password });
setUser(res.data?.user ?? null);
return res.data;
} catch (err) {
error.value = isAxiosError(err)
? err.response?.data?.error ||
err.response?.data?.detail ||
err.message
: 'Login failed';
throw err;
} finally {
loading.value = false;
}
};
const register = async (payload: {
email_address: string;
password: string;
confirm_password?: string;
first_name: string;
last_name: string;
date_of_birth?: string;
role?: string;
}) => {
loading.value = true;
error.value = null;
try {
await apiClient.post('/api/user/signup/', {
...payload,
confirm_password: payload.confirm_password || payload.password,
});
await login(payload.email_address, payload.password);
} catch (err) {
error.value = isAxiosError(err)
? err.response?.data?.detail ||
err.response?.data?.error ||
err.message
: 'Registration failed';
throw err;
} finally {
loading.value = false;
}
};
const logout = async () => {
loading.value = true;
error.value = null;
try {
await apiClient.post('/api/user/logout/');
} catch (err) {
error.value = isAxiosError(err)
? err.response?.data?.detail ||
err.response?.data?.error ||
err.message
: 'Logout failed';
throw err;
} finally {
setUser(null);
loading.value = false;
}
};
return {
user,
loading,
initialized,
error,
isAuthenticated,
hasRole,
displayName,
fetchSession,
login,
register,
logout,
};
});

View file

@ -2,7 +2,8 @@ html {
-webkit-text-size-adjust: 100%;
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,
'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif,
'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
'Noto Color Emoji';
line-height: 1.5;
tab-size: 4;
scroll-behavior: smooth;
@ -11,6 +12,8 @@ body {
font-family: inherit;
line-height: inherit;
margin: 0;
background: #0b1220;
color: #e5e7eb;
}
h1,
h2,

View file

@ -1,63 +1,134 @@
<script setup lang="ts">
import {
Card,
Typography,
Divider,
List,
Timeline,
Space,
} from 'ant-design-vue';
const pathways = [
'Admin: system settings, user management, reporting, invitations.',
'Manager: create onboarding flows, assign roles, monitor team progress.',
'Employee: complete training modules, assessments, and track personal progress.',
];
const highlights = [
'Ready for agent-driven workflows that guide people through onboarding tasks.',
'Flexible role-based gating across pages (managers/admins vs employees).',
'Django REST API + Vue 3 frontend with a shared Pinia auth/session store.',
'Docker-friendly dev setup (frontend on 5173, API on 8000).',
];
const roadmap = [
{
title: 'Short term',
items: [
'Add richer assessments with adaptive scoring.',
'Improve content versioning for training modules.',
'Expose activity feed for audits.',
],
},
{
title: 'Next',
items: [
'Integrate external IDP (SSO) and SCIM user sync.',
'Launch webhooks for downstream HRIS updates.',
'Add multilingual content support.',
],
},
];
const steps = [
'Register or login (demo credentials only).',
'Complete Onboarding and Training to simulate a role journey.',
'Managers assign employees to roles and review progress reports.',
];
</script>
<template>
<div class="about">
<h1>About Agentic Trainers</h1>
<p>
Agentic Trainers is a lightweight platform for onboarding, training,
and assessing employees using modular training content and
agent-driven workflows. This repo contains a demo front-end and
example pipelines for role-based experiences.
</p>
<div class="page">
<Card class="panel" :bordered="false">
<Typography.Title :level="2"
>About Agentic Trainers</Typography.Title
>
<Typography.Paragraph type="secondary">
Agentic Trainers is a lightweight platform for onboarding,
training, and assessing employees with modular content and
agent-driven workflows. It is designed for teams that want to
ship tangible learning experiences quickly without complex LMS
setup.
</Typography.Paragraph>
<h2>Role pathways</h2>
<ul>
<li>
<strong>Admin</strong>: full access to system settings, user
management, and reporting. Admins can invite or deactivate
managers and view overall progress.
</li>
<li>
<strong>Manager</strong>: responsible for company-level tasks:
create onboarding flows, assign employees to roles, and monitor
team progress.
</li>
<li>
<strong>Employee</strong>: follows onboarding and training
modules, completes assessments, and views personal progress on
the dashboard.
</li>
</ul>
<Divider />
<Typography.Title :level="4">Role pathways</Typography.Title>
<List :data-source="pathways" :bordered="false">
<template #renderItem="{ item }">
<List.Item class="row">{{ item }}</List.Item>
</template>
</List>
<h2>Getting started</h2>
<ol>
<li>
Register or login from the top-right to choose your role (demo
only).
</li>
<li>
Use the <em>Onboarding</em> and <em>Training</em> links to
begin.
</li>
<li>
Managers can create roles and assign employees via the
<em>Roles</em> page.
</li>
</ol>
<Divider />
<Typography.Title :level="4">Highlights</Typography.Title>
<List :data-source="highlights" :bordered="false">
<template #renderItem="{ item }">
<List.Item class="row">{{ item }}</List.Item>
</template>
</List>
<p>
This is a demo implementation authentication and permissions are
local-storage based for example purposes. For production-grade apps
integrate a backend auth provider and persistent user store.
</p>
<Divider />
<Typography.Title :level="4">Getting started</Typography.Title>
<List :data-source="steps" :bordered="false">
<template #renderItem="{ item, index }">
<List.Item class="row"
><strong>{{ index + 1 }}.</strong>&nbsp;{{
item
}}</List.Item
>
</template>
</List>
<Divider />
<Typography.Title :level="4">Roadmap</Typography.Title>
<Space :size="24" direction="vertical" style="width: 100%">
<Timeline>
<Timeline.Item
v-for="bucket in roadmap"
:key="bucket.title"
>
<Typography.Text strong>{{
bucket.title
}}</Typography.Text>
<List :data-source="bucket.items" :bordered="false">
<template #renderItem="{ item }">
<List.Item class="row">{{ item }}</List.Item>
</template>
</List>
</Timeline.Item>
</Timeline>
</Space>
<Typography.Paragraph type="secondary" style="margin-top: 1rem">
Demo-only auth; integrate a real identity provider and
persistence for production.
</Typography.Paragraph>
</Card>
</div>
</template>
<style>
@media (min-width: 768px) {
.about {
max-width: 768px;
margin-left: auto;
margin-right: auto;
padding: 0 1rem;
}
<style scoped>
.page {
max-width: 900px;
margin: 0 auto;
padding: 1rem;
}
.panel {
background: #0f172a;
border: 1px solid #1f2937;
color: #e5e7eb;
}
.row {
color: #e5e7eb;
}
</style>

View file

@ -1,68 +1,421 @@
<script setup lang="ts">
import { ref } from 'vue';
import { ref, onMounted, onUnmounted, computed } from 'vue';
import { useRoute } from 'vue-router';
import {
Card,
Typography,
Button,
List,
Space,
Spin,
Input,
message,
Tag,
} from 'ant-design-vue';
import { useAgentStore } from '../stores/agentStore';
import { apiClient, isAxiosError } from '../lib/api';
const route = useRoute();
const id = route.params.id || 'unknown';
console.log('[AgentDetail] Route params:', route.params);
const agent = ref({
id,
name: `Agent ${id}`,
description:
'This agent helps new hires by providing step-by-step assistance and quick answers to common questions.',
const agentStore = useAgentStore();
console.log('[AgentDetail] Store instance:', agentStore);
const agentId = route.params.id as string;
console.log('[AgentDetail] Agent ID:', agentId);
if (!agentId) {
console.error('[AgentDetail] ERROR: No agent ID in route params');
}
const agent = ref<Record<string, unknown>>({
id: agentId,
name: 'Loading...',
description: '',
status: 'idle',
});
console.log('[AgentDetail] Initial agent state:', agent.value);
const queryInput = ref('');
const isRunning = computed(() => {
console.log(
'[AgentDetail] isRunning computed - executionStatus:',
agentStore.executionStatus
);
return agentStore.executionStatus === 'running';
});
const isConnected = computed(() => {
console.log(
'[AgentDetail] isConnected computed - isConnected:',
agentStore.isConnected
);
return 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 Record<string, unknown>;
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 () => {
console.log('[AgentDetail] Fetching agent details for ID:', agentId);
try {
const response = await apiClient.get(`/api/agent/${agentId}/`);
agent.value = response.data;
console.log('[AgentDetail] Agent fetched successfully:', agent.value);
} catch (error) {
console.error('[AgentDetail] ERROR - Failed to fetch agent:', error);
if (isAxiosError(error)) {
console.error('[AgentDetail] Axios error details:', {
status: error.response?.status,
data: error.response?.data,
message: error.message,
});
}
message.error('Failed to load agent details');
}
};
const startAgent = () => {
console.log('[AgentDetail] Starting agent execution');
if (!agentStore.isConnected) {
console.warn('[AgentDetail] WARNING: WebSocket not connected');
console.log('[AgentDetail] Connection state:', {
isConnected: agentStore.isConnected,
});
message.error('WebSocket not connected');
return;
}
if (!queryInput.value.trim()) {
message.error('Please enter a query');
return;
}
try {
const data = {
query: queryInput.value.trim(),
};
console.log('[AgentDetail] Sending data:', data);
console.log('[AgentDetail] Calling startAgent on store');
agentStore.startAgent(data);
console.log('[AgentDetail] Agent execution initiated');
message.success('Agent execution started');
} catch (error) {
console.error('[AgentDetail] ERROR - Failed to start agent:', error);
message.error('Failed to start agent');
}
};
const stopAgent = () => {
console.log('[AgentDetail] Stopping agent execution');
try {
console.log('[AgentDetail] Calling stopAgent on store');
agentStore.stopAgent();
console.log('[AgentDetail] Agent stop signal sent');
message.success('Agent stop requested');
} catch (error) {
console.error('[AgentDetail] ERROR - Failed to stop agent:', error);
}
};
onMounted(() => {
console.log('[AgentDetail] Component mounted');
console.log('[AgentDetail] Lifecycle: onMounted - starting initialization');
fetchAgent();
console.log(
'[AgentDetail] Attempting WebSocket connection for agent:',
agentId
);
try {
agentStore.connect(agentId);
console.log('[AgentDetail] WebSocket connection initiated');
} catch (error) {
console.error(
'[AgentDetail] ERROR - Failed to connect WebSocket:',
error
);
}
});
onUnmounted(() => {
console.log('[AgentDetail] Component unmounted');
console.log('[AgentDetail] Lifecycle: onUnmounted - cleaning up');
try {
console.log('[AgentDetail] Disconnecting WebSocket');
agentStore.disconnect();
console.log('[AgentDetail] WebSocket disconnected successfully');
} catch (error) {
console.error(
'[AgentDetail] ERROR - Failed to disconnect WebSocket:',
error
);
}
});
</script>
<template>
<div class="page-wrap">
<header class="page-header">
<h1>{{ agent.name }}</h1>
<p class="lead">{{ agent.description }}</p>
</header>
<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>
<section class="controls">
<button class="cta-button">Run Simulation</button>
<button class="link">Edit Configuration</button>
</section>
<Typography.Paragraph type="secondary">{{
agent.description || 'No description available'
}}</Typography.Paragraph>
<section class="logs">
<h3>Recent Interactions</h3>
<ul>
<li>2025-11-01: Simulated onboarding session (score: 92%)</li>
<li>2025-11-03: Updated knowledge sources</li>
<li>2025-11-07: Minor behavior tweak applied</li>
</ul>
</section>
<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>
<Space>
<Button
type="primary"
:disabled="isRunning || !isConnected"
@click="startAgent"
>
Run Agent
</Button>
<Button
danger
:disabled="!isRunning"
@click="stopAgent"
>
Stop Agent
</Button>
</Space>
</Space>
</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>
<div v-if="agentResponse" class="response-section">
<Typography.Title :level="4" class="section-title"
>Response</Typography.Title
>
<Card class="response-card" :bordered="false">
<div class="response-content">
{{ agentResponse }}
</div>
</Card>
</div>
</Card>
</div>
</template>
<style scoped>
.page-wrap {
max-width: 960px;
.page {
max-width: 1200px;
margin: 0 auto;
padding: 1rem;
}
.controls {
.panel {
background: #0f172a;
border: 1px solid #1f2937;
color: #e5e7eb;
}
.header {
display: flex;
gap: 1rem;
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;
}
.cta-button {
background-color: #4f46e5;
color: white;
padding: 0.6rem 1rem;
border-radius: 0.5rem;
border: none;
.log-container {
background: #1f2937;
border-radius: 4px;
max-height: 500px;
overflow-y: auto;
}
.link {
background: transparent;
color: #4f46e5;
border: none;
padding: 0.6rem 1rem;
.log-item {
border-bottom: 1px solid #374151 !important;
padding: 0.75rem !important;
}
.logs ul {
.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;
padding-left: 1.25rem;
color: #374151;
font-size: 0.8rem;
color: #d1d5db;
}
.response-section {
margin-top: 2rem;
}
.response-card {
background: #1f2937;
border: 1px solid #374151;
}
.response-content {
color: #e5e7eb;
font-size: 1rem;
line-height: 1.6;
white-space: pre-wrap;
word-wrap: break-word;
padding: 0.5rem;
}
</style>

View file

@ -1,61 +1,106 @@
<script setup lang="ts">
import { ref } from 'vue';
import { ref, onMounted } from 'vue';
import { List, Typography, Button, Card, Spin, message } from 'ant-design-vue';
import { apiClient } from '../lib/api';
const agents = ref([
{ id: 'a1', name: 'Onboarding Bot', role: 'Guided tours' },
{ id: 'a2', name: 'Docs Helper', role: 'Knowledge base' },
{ id: 'a3', name: 'QA Coach', role: 'Assessment' },
]);
interface Agent {
uuid: string;
id: string;
name: string;
description: string;
status: string;
}
const agents = ref<Agent[]>([]);
const loading = ref(false);
const fetchAgents = async () => {
loading.value = true;
try {
const response = await apiClient.get('/api/agent/');
agents.value = response.data;
} catch (error) {
console.error('Failed to fetch agents:', error);
message.error('Failed to load agents');
agents.value = [
{
uuid: 'a1',
id: 'a1',
name: 'Onboarding Bot',
description: 'Guided tours',
status: 'idle',
},
{
uuid: 'a2',
id: 'a2',
name: 'Docs Helper',
description: 'Knowledge base',
status: 'idle',
},
{
uuid: 'a3',
id: 'a3',
name: 'QA Coach',
description: 'Assessment',
status: 'idle',
},
];
} finally {
loading.value = false;
}
};
onMounted(() => {
fetchAgents();
});
</script>
<template>
<div class="page-wrap">
<header class="page-header">
<h1>Agents</h1>
<p class="lead">Manage and inspect the available AI agents.</p>
</header>
<section class="agent-list">
<div v-for="agent in agents" :key="agent.id" class="agent-card">
<div>
<h3>{{ agent.name }}</h3>
<p class="muted">{{ agent.role }}</p>
</div>
<router-link :to="`/agents/${agent.id}`" class="cta-small"
>Open</router-link
<div class="page">
<Typography.Title :level="2">Agents</Typography.Title>
<Typography.Paragraph type="secondary"
>Manage and inspect the available AI agents.</Typography.Paragraph
>
</div>
</section>
<Card class="panel" :bordered="false">
<Spin :spinning="loading" tip="Loading agents...">
<List
:data-source="agents"
item-layout="horizontal"
:bordered="false"
>
<template #renderItem="{ item }">
<List.Item class="item">
<List.Item.Meta
:title="item.name"
:description="`${item.description} • Status: ${item.status}`"
/>
<RouterLink :to="`/agents/${item.uuid || item.id}`">
<Button type="primary" size="small"
>Open</Button
>
</RouterLink>
</List.Item>
</template>
</List>
</Spin>
</Card>
</div>
</template>
<style scoped>
.page-wrap {
max-width: 960px;
.page {
max-width: 900px;
margin: 0 auto;
padding: 1rem;
}
.agent-list {
display: grid;
gap: 0.75rem;
.panel {
background: #0f172a;
border: 1px solid #1f2937;
color: #e5e7eb;
}
.agent-card {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem;
background: #fff;
border-radius: 8px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
}
.muted {
color: #6b7280;
}
.cta-small {
background: #4f46e5;
color: #fff;
padding: 0.4rem 0.6rem;
border-radius: 6px;
text-decoration: none;
.item :deep(.ant-list-item-meta-title),
.item :deep(.ant-list-item-meta-description) {
color: #e5e7eb;
}
</style>

View file

@ -1,5 +1,6 @@
<script setup lang="ts">
import { ref } from 'vue';
import { Card, Typography, Row, Col, Tag, Button, Space } from 'ant-design-vue';
const assessments = ref([
{ id: 't1', title: 'Knowledge Check - Basics', type: 'Quiz', passing: 70 },
@ -13,59 +14,47 @@ const assessments = ref([
</script>
<template>
<div class="page-wrap">
<header class="page-header">
<h1>Assessments</h1>
<p class="lead">
Create and run assessments to validate readiness.
</p>
</header>
<div class="page">
<Typography.Title :level="2">Assessments</Typography.Title>
<Typography.Paragraph type="secondary"
>Create and run assessments to validate
readiness.</Typography.Paragraph
>
<section class="assess-list">
<div v-for="a in assessments" :key="a.id" class="assess-card">
<h3>{{ a.title }}</h3>
<p class="meta">
Type: {{ a.type }} · Passing: {{ a.passing }}%
</p>
<div class="actions">
<button class="cta-small">Preview</button>
<button class="link">Run</button>
</div>
</div>
</section>
<Row :gutter="16">
<Col v-for="a in assessments" :key="a.id" :xs="24" :md="12">
<Card class="card" hoverable :bordered="false">
<Typography.Title :level="4">{{
a.title
}}</Typography.Title>
<Typography.Text type="secondary"
>Type: {{ a.type }} ÷ Passing:
{{ a.passing }}%</Typography.Text
>
<Space class="actions">
<Tag color="blue">{{ a.type }}</Tag>
<Button size="small">Preview</Button>
<Button size="small" type="primary">Run</Button>
</Space>
</Card>
</Col>
</Row>
</div>
</template>
<style scoped>
.assess-list {
display: grid;
gap: 0.75rem;
.page {
max-width: 1000px;
margin: 0 auto;
padding: 1rem;
}
.assess-card {
background: #fff;
padding: 0.75rem;
border-radius: 8px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
}
.meta {
color: #6b7280;
font-size: 0.95rem;
.card {
background: #0f172a;
border: 1px solid #1f2937;
color: #e5e7eb;
}
.actions {
margin-top: 0.5rem;
display: flex;
margin-top: 0.75rem;
gap: 0.5rem;
}
.cta-small {
background: #4f46e5;
color: #fff;
padding: 0.4rem 0.6rem;
border-radius: 6px;
border: none;
}
.link {
background: transparent;
color: #4f46e5;
border: none;
}
</style>

View file

@ -1,87 +1,329 @@
<script setup lang="ts" />
<script setup lang="ts">
import {
Row,
Col,
Card,
Button,
Typography,
Tag,
Statistic,
Carousel,
Avatar,
Space,
Divider,
} from 'ant-design-vue';
import {
CheckCircleTwoTone,
ThunderboltTwoTone,
CloudTwoTone,
} from '@ant-design/icons-vue';
const heroImage =
'https://images.unsplash.com/photo-1521737604893-d14cc237f11d?auto=format&fit=crop&w=1400&q=80';
const stats = [
{ title: 'Teams Onboarded', value: '240+' },
{ title: 'Avg. Time Saved', value: '38%' },
{ title: 'Playbooks Ready', value: '120' },
];
const features = [
{
title: 'Adaptive AI Guides',
description:
'Role-specific checklists, interactive tours, and contextual help tuned to your stack.',
icon: CheckCircleTwoTone,
},
{
title: 'Skills & Assessments',
description:
'Scenario-based quizzes and code tasks with instant insights and coach-like feedback.',
icon: ThunderboltTwoTone,
},
{
title: 'Knowledge Mesh',
description:
'Ingest docs, wikis, and reposââ¬â€keep assistants current with zero manual updates.',
icon: CloudTwoTone,
},
];
const journeys = [
{
name: 'Engineer Launch',
steps: 'Access, environments, codebase tour, first PR, observability basics.',
image: 'https://images.unsplash.com/photo-1522075469751-3a6694fb2f61?auto=format&fit=crop&w=800&q=80',
},
{
name: 'Customer Success Ramp',
steps: 'Playbooks, product scenarios, objection handling, success plans, CRM hygiene.',
image: 'https://images.unsplash.com/photo-1521737604893-d14cc237f11d?auto=format&fit=crop&w=900&q=80',
},
{
name: 'Product Discovery',
steps: 'Interview templates, JTBD mapping, experiment cards, roadmap debates.',
image: 'https://images.unsplash.com/photo-1483478550801-ceba5fe50e8e?auto=format&fit=crop&w=900&q=80',
},
];
const testimonials = [
{
name: 'Amira Chen',
role: 'VP Engineering, Nimbus',
quote: 'We cut onboarding from weeks to days. The guided flows and assessments keep everyone aligned.',
avatar: 'https://images.unsplash.com/photo-1524504388940-b1c1722653e1?auto=format&fit=crop&w=200&q=80',
},
{
name: 'Luis Ortega',
role: 'Head of Success, Calypso',
quote: 'Playbooks stay fresh automatically. New CSMs ship value on day one.',
avatar: 'https://images.unsplash.com/photo-1500648767791-00dcc994a43e?auto=format&fit=crop&w=200&q=80',
},
];
const logos = [
'https://dummyimage.com/120x40/111827/ffffff&text=Nova',
'https://dummyimage.com/120x40/1f2937/ffffff&text=Helio',
'https://dummyimage.com/120x40/111827/ffffff&text=Arcus',
'https://dummyimage.com/120x40/1f2937/ffffff&text=Vertex',
];
</script>
<template>
<main class="home">
<main class="page">
<section class="hero">
<h1>Welcome to Agentic Trainers</h1>
<p>
Automate onboarding and support new team members with AI agents.
Our platform creates domain-specific training workflows tailored
to each role.
</p>
<router-link to="/about" class="cta-button">
Learn More
</router-link>
<Row :gutter="32" :align="'middle'">
<Col :xs="24" :md="14">
<Typography.Title :level="1" class="hero-title">
Build agentic onboarding that feels bespoke to every
role
</Typography.Title>
<Typography.Paragraph class="hero-sub">
AI-led workflows, assessments, and knowledge delivery
that adapt to your stack, your rituals, and your teams -
so every new hire ships confidently, faster.
</Typography.Paragraph>
<Space>
<RouterLink to="/about">
<Button type="primary" size="large"
>Learn More</Button
>
</RouterLink>
<RouterLink to="/onboarding">
<Button size="large">See Onboarding Flows</Button>
</RouterLink>
</Space>
<Divider />
<Row :gutter="16">
<Col
v-for="stat in stats"
:key="stat.title"
:xs="24"
:sm="8"
>
<Card :bordered="false" class="stat-card" hoverable>
<Statistic
:title="stat.title"
:value="stat.value"
/>
</Card>
</Col>
</Row>
</Col>
<Col :xs="24" :md="10">
<Card class="hero-card" hoverable :cover="null">
<img
:src="heroImage"
alt="Team collaborating"
class="hero-img"
/>
<div class="hero-overlay">Adaptive AI playbooks</div>
</Card>
</Col>
</Row>
</section>
<section class="trusted">
<Typography.Text type="secondary"
>Trusted by modern teams</Typography.Text
>
<div class="logo-row">
<img v-for="logo in logos" :key="logo" :src="logo" alt="logo" />
</div>
</section>
<section class="features">
<h2>Key Features</h2>
<ul>
<li>Reusable AI-powered workflows for role induction.</li>
<li>Adaptive guidance tailored to each team member.</li>
<li>Track progress and generate actionable insights.</li>
<li>Extensible to any domain or industry.</li>
</ul>
<Typography.Title :level="2"
>Everything you need to ramp faster</Typography.Title
>
<Row :gutter="16">
<Col
v-for="feature in features"
:key="feature.title"
:xs="24"
:md="8"
>
<Card hoverable class="feature-card">
<feature.icon
two-tone-color="#8b5cf6"
style="font-size: 28px"
/>
<Typography.Title :level="4">{{
feature.title
}}</Typography.Title>
<Typography.Paragraph>{{
feature.description
}}</Typography.Paragraph>
<Tag color="purple">Live</Tag>
</Card>
</Col>
</Row>
</section>
<section class="get-started">
<h2>Get Started</h2>
<p>
Begin your AI-driven onboarding journey today. Explore how our
agentic approach can help your team succeed faster.
</p>
<router-link to="/about" class="cta-button">
Explore Now
</router-link>
<section class="journeys">
<Typography.Title :level="2"
>Prebuilt journeys, tailored in minutes</Typography.Title
>
<Row :gutter="16">
<Col
v-for="journey in journeys"
:key="journey.name"
:xs="24"
:md="8"
>
<Card hoverable class="journey-card">
<template #cover>
<img :alt="journey.name" :src="journey.image" />
</template>
<Typography.Title :level="4">{{
journey.name
}}</Typography.Title>
<Typography.Text>{{ journey.steps }}</Typography.Text>
</Card>
</Col>
</Row>
</section>
<section class="testimonials">
<Typography.Title :level="2"
>What teams are saying</Typography.Title
>
<Typography.Text
type="secondary"
style="display: block; text-align: center; margin-bottom: 1rem"
>
(Demo testimonials ââ¬âœ real feedback coming soon)
</Typography.Text>
<Carousel autoplay dot-position="bottom">
<div
v-for="t in testimonials"
:key="t.name"
class="testimonial-slide"
>
<Card :bordered="false" class="testimonial-card">
<Typography.Paragraph
>ââ¬Å{{ t.quote }}ââ¬Â</Typography.Paragraph
>
<Space>
<Avatar :src="t.avatar" size="large" />
<div>
<div class="t-name">{{ t.name }}</div>
<Typography.Text type="secondary">{{
t.role
}}</Typography.Text>
</div>
</Space>
</Card>
</div>
</Carousel>
</section>
</main>
</template>
<style scoped>
.home {
max-width: 960px;
.page {
padding: 2rem 1.5rem 3rem;
max-width: 1200px;
margin: 0 auto;
padding: 1rem;
}
.hero {
text-align: center;
margin-bottom: 3rem;
margin-bottom: 2.5rem;
}
.hero h1 {
font-size: 2.5rem;
.hero-title {
margin-bottom: 1rem;
}
.hero p {
font-size: 1.25rem;
margin-bottom: 2rem;
.hero-sub {
font-size: 1.05rem;
color: #cbd5e1;
}
.cta-button {
background-color: #4f46e5;
color: white;
padding: 0.75rem 1.5rem;
border-radius: 0.5rem;
text-decoration: none;
.hero-card {
border: none;
}
.hero-img {
width: 100%;
height: 280px;
object-fit: cover;
border-radius: 8px;
}
.hero-overlay {
margin-top: 0.75rem;
color: #8b5cf6;
font-weight: 600;
}
.cta-button:hover {
background-color: #4338ca;
.stat-card {
background: #0f172a;
border: 1px solid #1f2937;
}
.trusted {
text-align: center;
margin: 2rem 0;
}
.logo-row {
display: flex;
gap: 1.5rem;
justify-content: center;
align-items: center;
flex-wrap: wrap;
margin-top: 0.75rem;
}
.logo-row img {
opacity: 0.8;
height: 32px;
}
.features {
margin-bottom: 3rem;
margin: 2.5rem 0;
}
.features h2 {
font-size: 2rem;
margin-bottom: 1rem;
.feature-card {
height: 100%;
background: #0f172a;
border: 1px solid #1f2937;
color: #e5e7eb;
}
.features ul {
list-style-type: disc;
padding-left: 1.5rem;
.journeys {
margin: 2.5rem 0;
}
.get-started h2 {
font-size: 2rem;
margin-bottom: 1rem;
.journey-card {
background: #0f172a;
border: 1px solid #1f2937;
color: #e5e7eb;
}
.get-started p {
margin-bottom: 1.5rem;
.testimonials {
margin: 2.5rem 0;
}
.testimonial-slide {
padding: 0 6px;
}
.testimonial-card {
background: #0f172a;
border: 1px solid #1f2937;
color: #e5e7eb;
}
@media (max-width: 768px) {
.page {
padding: 1.25rem 1rem 2.5rem;
}
.hero-img {
height: 220px;
}
}
</style>

View file

@ -1,53 +1,106 @@
<script setup lang="ts">
import { ref } from 'vue';
import { useRouter } from 'vue-router';
import { reactive, computed, onMounted } from 'vue';
import { useRouter, useRoute } from 'vue-router';
import { Card, Typography, Form, Input, Button, message } from 'ant-design-vue';
import { useAuthStore } from '../stores/authStore';
const router = useRouter();
const name = ref('');
const role = ref<'admin' | 'manager' | 'employee'>('employee');
const route = useRoute();
const authStore = useAuthStore();
const loading = computed(() => authStore.loading);
function submit() {
router.push('/');
}
const formState = reactive({
email: '',
password: '',
});
const submit = async () => {
try {
await authStore.login(formState.email, formState.password);
message.success('Login successful');
const redirect = (route.query.redirect as string) || '/onboarding';
router.push(redirect);
} catch (error: any) {
const errorMsg =
authStore.error ||
error?.response?.data?.detail ||
error?.response?.data?.message ||
'Login failed';
message.error(errorMsg);
}
};
onMounted(async () => {
await authStore.fetchSession();
if (authStore.isAuthenticated) {
const redirect = (route.query.redirect as string) || '/onboarding';
router.replace(redirect);
}
});
</script>
<template>
<div class="auth">
<h1>Login (demo)</h1>
<div>
<label>Name</label>
<input v-model="name" placeholder="Your name" />
</div>
<div>
<label>Role</label>
<select v-model="role">
<option value="employee">Employee</option>
<option value="manager">Manager</option>
<option value="admin">Admin</option>
</select>
</div>
<button class="cta-button" @click="submit">Login</button>
<div class="auth-page">
<Card class="panel" :bordered="false">
<Typography.Title :level="3">Login</Typography.Title>
<Form
ref="form"
layout="vertical"
:model="formState"
@finish="submit"
>
<Form.Item
label="Email"
name="email"
:rules="[
{ required: true, message: 'Enter your email' },
{
type: 'email',
message: 'Please enter a valid email',
},
]"
><Input
v-model:value="formState.email"
type="email"
placeholder="Email address"
:disabled="loading"
/></Form.Item>
<Form.Item
label="Password"
name="password"
:rules="[
{ required: true, message: 'Enter your password' },
]"
><Input.Password
v-model:value="formState.password"
placeholder="Password"
:disabled="loading"
/></Form.Item>
<Button
type="primary"
html-type="submit"
block
:loading="loading"
>Login</Button
>
</Form>
</Card>
</div>
</template>
<style scoped>
.auth {
max-width: 480px;
margin: 0 auto;
.auth-page {
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
padding: 1rem;
}
label {
display: block;
margin-top: 0.5rem;
}
input,
select {
.panel {
max-width: 400px;
width: 100%;
padding: 0.5rem;
margin-top: 0.25rem;
}
.cta-button {
margin-top: 1rem;
background: #0f172a;
border: 1px solid #1f2937;
color: #e5e7eb;
}
</style>

View file

@ -1,5 +1,6 @@
<script setup lang="ts">
import { ref } from 'vue';
import { Card, Typography, Timeline, Button, Space } from 'ant-design-vue';
const steps = ref([
{
@ -24,95 +25,63 @@ const steps = ref([
</script>
<template>
<div class="page-wrap">
<header class="page-header">
<h1>Onboarding Flow</h1>
<p class="lead">
<div class="page">
<Card class="panel" :bordered="false">
<Typography.Title :level="2">Onboarding Flow</Typography.Title>
<Typography.Paragraph type="secondary">
Step-by-step AI-guided onboarding for new team members.
</p>
</header>
</Typography.Paragraph>
<section class="steps">
<div v-for="step in steps" :key="step.id" class="card">
<div class="card-left">
<div class="step-index">{{ step.id }}</div>
</div>
<div class="card-body">
<h3>{{ step.title }}</h3>
<p>{{ step.description }}</p>
<div class="meta">Est. time: {{ step.eta }} mins</div>
</div>
</div>
</section>
<footer class="actions">
<router-link to="/training" class="cta-button"
>Start Training</router-link
<Timeline mode="left" class="timeline">
<Timeline.Item
v-for="step in steps"
:key="step.id"
color="purple"
>
<router-link to="/agents" class="link">View Agents</router-link>
</footer>
<Space direction="vertical" size="small">
<Typography.Title :level="4">{{
step.title
}}</Typography.Title>
<Typography.Text>{{
step.description
}}</Typography.Text>
<Typography.Text type="secondary"
>Est. time: {{ step.eta }} mins</Typography.Text
>
</Space>
</Timeline.Item>
</Timeline>
<Space class="actions" wrap>
<RouterLink to="/training"
><Button type="primary">Start Training</Button></RouterLink
>
<RouterLink to="/agents"
><Button ghost>View Agents</Button></RouterLink
>
</Space>
</Card>
</div>
</template>
<style scoped>
.page-wrap {
max-width: 960px;
.page {
max-width: 1100px;
margin: 0 auto;
padding: 1rem;
}
.page-header h1 {
font-size: 2rem;
margin-bottom: 0.25rem;
.panel {
background: #0f172a;
border: 1px solid #1f2937;
color: #e5e7eb;
}
.lead {
color: #6b7280;
margin-bottom: 1rem;
.timeline :deep(.ant-timeline-item-head) {
background: #8b5cf6;
}
.steps {
display: grid;
gap: 1rem;
}
.card {
display: flex;
align-items: center;
background: #fff;
border-radius: 8px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
padding: 0.75rem;
}
.card-left {
margin-right: 1rem;
}
.step-index {
background: #eef2ff;
color: #4f46e5;
font-weight: 700;
padding: 0.5rem 0.75rem;
border-radius: 6px;
}
.card-body h3 {
margin: 0 0 0.25rem 0;
}
.meta {
color: #9ca3af;
font-size: 0.9rem;
.timeline :deep(.ant-typography) {
color: #e5e7eb;
}
.actions {
display: flex;
gap: 1rem;
margin-top: 1.25rem;
}
.cta-button {
background-color: #4f46e5;
color: white;
padding: 0.6rem 1rem;
border-radius: 0.5rem;
text-decoration: none;
font-weight: 600;
}
.link {
color: #4f46e5;
align-self: center;
text-decoration: none;
margin-top: 1rem;
}
</style>

View file

@ -1,5 +1,6 @@
<script setup lang="ts">
import { ref, computed } from 'vue';
import { Card, Typography, Progress, List } from 'ant-design-vue';
const users = ref([
{ id: 'u1', name: 'Alex', progress: 78 },
@ -15,76 +16,64 @@ const avg = computed(() =>
</script>
<template>
<div class="page-wrap">
<header class="page-header">
<h1>Progress Dashboard</h1>
<p class="lead">Track cohort progress and module completion.</p>
</header>
<div class="page">
<Typography.Title :level="2">Progress Dashboard</Typography.Title>
<Typography.Paragraph type="secondary"
>Track cohort progress and module completion.</Typography.Paragraph
>
<section class="overview">
<div class="stat">
<div class="stat-value">{{ avg }}%</div>
<div class="stat-label">Average Progress</div>
<div class="overview">
<Card class="stat" :bordered="false">
<Typography.Title :level="3">{{ avg }}%</Typography.Title>
<Typography.Text type="secondary"
>Average Progress</Typography.Text
>
</Card>
<Card class="panel" :bordered="false">
<List :data-source="users" :bordered="false">
<template #renderItem="{ item }">
<List.Item class="user-row">
<div class="user-name">{{ item.name }}</div>
<Progress
:percent="item.progress"
stroke-color="#8b5cf6"
:show-info="true"
/>
</List.Item>
</template>
</List>
</Card>
</div>
<div class="user-list">
<div v-for="u in users" :key="u.id" class="user-row">
<div>{{ u.name }}</div>
<div class="progress">
<div
class="bar"
:style="{ width: u.progress + '%' }"
></div>
</div>
<div class="pct">{{ u.progress }}%</div>
</div>
</div>
</section>
</div>
</template>
<style scoped>
.page {
max-width: 900px;
margin: 0 auto;
padding: 1rem;
}
.overview {
display: grid;
gap: 1rem;
}
.stat {
background: #fff;
padding: 1rem;
border-radius: 8px;
text-align: center;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
}
.stat-value {
font-size: 2rem;
color: #111827;
font-weight: 700;
}
.user-list {
background: #fff;
padding: 0.75rem;
border-radius: 8px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
.stat,
.panel {
background: #0f172a;
border: 1px solid #1f2937;
color: #e5e7eb;
}
.user-row {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.5rem 0;
gap: 1rem;
}
.progress {
flex: 1;
background: #f3f4f6;
height: 10px;
border-radius: 999px;
overflow: hidden;
.user-name {
width: 140px;
color: #e5e7eb;
}
.bar {
height: 100%;
background: #4f46e5;
}
.pct {
width: 48px;
text-align: right;
color: #374151;
.panel :deep(.ant-progress-text) {
color: #e5e7eb;
}
</style>

View file

@ -1,52 +1,172 @@
<script setup lang="ts">
import { ref } from 'vue';
import { useRouter } from 'vue-router';
import { reactive, computed, onMounted } from 'vue';
import { useRouter, useRoute } from 'vue-router';
import {
Card,
Typography,
Form,
Input,
Select,
Button,
message,
} from 'ant-design-vue';
import { useAuthStore } from '../stores/authStore';
const router = useRouter();
const name = ref('');
const role = ref<'admin' | 'manager' | 'employee'>('employee');
const route = useRoute();
const authStore = useAuthStore();
const loading = computed(() => authStore.loading);
function submit() {
router.push('/');
}
const formState = reactive({
email: '',
firstName: '',
lastName: '',
password: '',
confirmPassword: '',
role: 'employee' as 'admin' | 'manager' | 'employee',
});
const submit = async () => {
try {
await authStore.register({
email_address: formState.email,
password: formState.password,
confirm_password: formState.confirmPassword,
first_name: formState.firstName,
last_name: formState.lastName,
role: formState.role,
});
message.success('Account created');
const redirect = (route.query.redirect as string) || '/onboarding';
router.push(redirect);
} catch (error: any) {
const errorMsg =
authStore.error ||
error?.response?.data?.detail ||
error?.response?.data?.message ||
'Registration failed';
message.error(errorMsg);
}
};
onMounted(async () => {
await authStore.fetchSession();
if (authStore.isAuthenticated) {
const redirect = (route.query.redirect as string) || '/onboarding';
router.replace(redirect);
}
});
</script>
<template>
<div class="auth">
<h1>Register (demo)</h1>
<div>
<label>Name</label>
<input v-model="name" placeholder="Your name" />
</div>
<div>
<label>Role</label>
<select v-model="role">
<option value="employee">Employee</option>
<option value="manager">Manager</option>
</select>
</div>
<button class="cta-button" @click="submit">Register</button>
<div class="auth-page">
<Card class="panel" :bordered="false">
<Typography.Title :level="3">Register</Typography.Title>
<Form layout="vertical" :model="formState" @finish="submit">
<Form.Item
label="Email"
name="email"
:rules="[
{ required: true, message: 'Enter your email' },
{
type: 'email',
message: 'Please enter a valid email',
},
]"
>
<Input
v-model:value="formState.email"
type="email"
placeholder="Email address"
:disabled="loading"
/>
</Form.Item>
<Form.Item
label="First name"
name="firstName"
:rules="[
{ required: true, message: 'Enter your first name' },
]"
>
<Input
v-model:value="formState.firstName"
placeholder="First name"
:disabled="loading"
/>
</Form.Item>
<Form.Item
label="Last name"
name="lastName"
:rules="[
{ required: true, message: 'Enter your last name' },
]"
>
<Input
v-model:value="formState.lastName"
placeholder="Last name"
:disabled="loading"
/>
</Form.Item>
<Form.Item
label="Password"
name="password"
:rules="[{ required: true, message: 'Create a password' }]"
>
<Input.Password
v-model:value="formState.password"
placeholder="Password"
:disabled="loading"
/>
</Form.Item>
<Form.Item
label="Confirm password"
name="confirmPassword"
:rules="[
{ required: true, message: 'Confirm your password' },
{
validator: (_, value) =>
value === formState.password
? Promise.resolve()
: Promise.reject(
new Error('Passwords do not match')
),
},
]"
>
<Input.Password
v-model:value="formState.confirmPassword"
placeholder="Confirm password"
:disabled="loading"
/>
</Form.Item>
<Form.Item label="Role" name="role">
<Select v-model:value="formState.role" :disabled="loading">
<Select.Option value="employee">Employee</Select.Option>
<Select.Option value="manager">Manager</Select.Option>
<Select.Option value="admin">Admin</Select.Option>
</Select>
</Form.Item>
<Button
type="primary"
html-type="submit"
block
:loading="loading"
>Register</Button
>
</Form>
</Card>
</div>
</template>
<style scoped>
.auth {
max-width: 480px;
.auth-page {
max-width: 520px;
margin: 0 auto;
padding: 1rem;
}
label {
display: block;
margin-top: 0.5rem;
}
input,
select {
width: 100%;
padding: 0.5rem;
margin-top: 0.25rem;
}
.cta-button {
margin-top: 1rem;
.panel {
background: #0f172a;
border: 1px solid #1f2937;
color: #e5e7eb;
}
</style>

View file

@ -1,4 +1,6 @@
<script setup lang="ts">
import { Card, Typography, Row, Col, Button, Tag } from 'ant-design-vue';
const resources = [
{ id: 'r1', title: 'Engineering Handbook', type: 'PDF' },
{ id: 'r2', title: 'Team Slack Guide', type: 'Article' },
@ -7,49 +9,41 @@ const resources = [
</script>
<template>
<div class="page-wrap">
<header class="page-header">
<h1>Resources</h1>
<p class="lead">
Curated links and assets to help new hires get productive.
</p>
</header>
<div class="page">
<Typography.Title :level="2">Resources</Typography.Title>
<Typography.Paragraph type="secondary"
>Curated links and assets to help new hires get
productive.</Typography.Paragraph
>
<section class="resource-grid">
<div v-for="r in resources" :key="r.id" class="resource-card">
<h3>{{ r.title }}</h3>
<p class="muted">{{ r.type }}</p>
<Row :gutter="16">
<Col v-for="r in resources" :key="r.id" :xs="24" :md="8">
<Card class="card" hoverable :bordered="false">
<Typography.Title :level="4">{{
r.title
}}</Typography.Title>
<Tag color="geekblue">{{ r.type }}</Tag>
<div class="actions">
<button class="cta-small">Open</button>
<Button size="small" type="primary">Open</Button>
</div>
</div>
</section>
</Card>
</Col>
</Row>
</div>
</template>
<style scoped>
.resource-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 1rem;
.page {
max-width: 1000px;
margin: 0 auto;
padding: 1rem;
}
.resource-card {
background: #fff;
padding: 0.75rem;
border-radius: 8px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
}
.muted {
color: #6b7280;
.card {
background: #0f172a;
border: 1px solid #1f2937;
color: #e5e7eb;
}
.actions {
margin-top: 0.5rem;
}
.cta-small {
background: #4f46e5;
color: #fff;
padding: 0.4rem 0.6rem;
border-radius: 6px;
border: none;
}
</style>

View file

@ -1,4 +1,6 @@
<script setup lang="ts">
import { Card, Typography, Row, Col, Button, Tag } from 'ant-design-vue';
const roles = [
{
id: 'r1',
@ -19,48 +21,51 @@ const roles = [
</script>
<template>
<div class="page-wrap">
<header class="page-header">
<h1>Role Profiles</h1>
<p class="lead">
Pre-built role templates and suggested onboarding paths.
</p>
</header>
<div class="page">
<Typography.Title :level="2">Role Profiles</Typography.Title>
<Typography.Paragraph type="secondary"
>Pre-built role templates and suggested onboarding
paths.</Typography.Paragraph
>
<section class="profiles">
<div v-for="role in roles" :key="role.id" class="profile-card">
<h3>{{ role.title }}</h3>
<p>{{ role.summary }}</p>
<Row :gutter="16">
<Col v-for="role in roles" :key="role.id" :xs="24" :md="8">
<Card class="card" hoverable :bordered="false">
<Typography.Title :level="4">{{
role.title
}}</Typography.Title>
<Typography.Paragraph>{{
role.summary
}}</Typography.Paragraph>
<div class="actions">
<router-link :to="`/onboarding`" class="cta-small"
>Use Template</router-link
<Tag color="purple">Template</Tag>
<RouterLink to="/onboarding"
><Button type="primary" size="small"
>Use Template</Button
></RouterLink
>
</div>
</div>
</section>
</Card>
</Col>
</Row>
</div>
</template>
<style scoped>
.profiles {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 1rem;
.page {
max-width: 1100px;
margin: 0 auto;
padding: 1rem;
}
.profile-card {
background: #fff;
padding: 0.75rem;
border-radius: 8px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
.card {
background: #0f172a;
border: 1px solid #1f2937;
color: #e5e7eb;
}
.actions {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 0.75rem;
}
.cta-small {
background: #4f46e5;
color: #fff;
padding: 0.4rem 0.6rem;
border-radius: 6px;
text-decoration: none;
}
</style>

View file

@ -1,5 +1,6 @@
<script setup lang="ts">
import { ref } from 'vue';
import { Card, Typography, Row, Col, Tag, Button } from 'ant-design-vue';
const lessons = ref([
{
@ -24,52 +25,42 @@ const lessons = ref([
</script>
<template>
<div class="page-wrap">
<header class="page-header">
<h1>Training Module</h1>
<p class="lead">Interactive module with lessons and checkpoints.</p>
</header>
<section class="lessons">
<article
v-for="lesson in lessons"
:key="lesson.id"
class="lesson-card"
<div class="page">
<Typography.Title :level="2">Training Module</Typography.Title>
<Typography.Paragraph type="secondary"
>Interactive module with lessons and
checkpoints.</Typography.Paragraph
>
<h3>{{ lesson.title }}</h3>
<p>{{ lesson.summary }}</p>
<Row :gutter="16">
<Col v-for="lesson in lessons" :key="lesson.id" :xs="24" :md="8">
<Card class="card" hoverable :bordered="false">
<Typography.Title :level="4">{{
lesson.title
}}</Typography.Title>
<Typography.Paragraph>{{
lesson.summary
}}</Typography.Paragraph>
<div class="lesson-footer">
<span class="badge">{{ lesson.type }}</span>
<button class="cta-small">Open</button>
<Tag color="purple">{{ lesson.type }}</Tag>
<Button type="primary" size="small">Open</Button>
</div>
</article>
</section>
</Card>
</Col>
</Row>
</div>
</template>
<style scoped>
.page-wrap {
max-width: 960px;
.page {
max-width: 1100px;
margin: 0 auto;
padding: 1rem;
}
.page-header h1 {
font-size: 2rem;
}
.lead {
color: #6b7280;
margin-bottom: 1rem;
}
.lessons {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 1rem;
}
.lesson-card {
background: #fff;
padding: 0.75rem;
border-radius: 8px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
.card {
background: #0f172a;
border: 1px solid #1f2937;
color: #e5e7eb;
}
.lesson-footer {
display: flex;
@ -77,18 +68,4 @@ const lessons = ref([
align-items: center;
margin-top: 0.75rem;
}
.badge {
background: #eef2ff;
color: #4f46e5;
padding: 0.25rem 0.5rem;
border-radius: 6px;
font-weight: 600;
}
.cta-small {
background: #4f46e5;
color: #fff;
padding: 0.4rem 0.6rem;
border-radius: 6px;
border: none;
}
</style>