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", "license": "MIT",
"scripts": { "scripts": {
"dev": "vite dev", "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, "private": true,
"dependencies": { "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 './styles.css';
import 'ant-design-vue/dist/reset.css';
import router from './router'; import router from './router';
import { createApp } from 'vue'; import { createApp } from 'vue';
import { createPinia } from 'pinia';
import App from './app/App.vue'; import App from './app/App.vue';
const app = createApp(App); const app = createApp(App);
app.use(createPinia());
app.use(router); app.use(router);
app.mount('#root'); app.mount('#root');

View file

@ -1,4 +1,6 @@
import { createRouter, createWebHistory } from 'vue-router'; import { createRouter, createWebHistory } from 'vue-router';
import { useAuthStore } from '../stores/authStore';
import { message } from 'ant-design-vue';
const router = createRouter({ const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL), history: createWebHistory(import.meta.env.BASE_URL),
@ -17,53 +19,92 @@ const router = createRouter({
path: '/login', path: '/login',
name: 'login', name: 'login',
component: () => import('../views/LoginView.vue'), component: () => import('../views/LoginView.vue'),
meta: { guestOnly: true },
}, },
{ {
path: '/register', path: '/register',
name: 'register', name: 'register',
component: () => import('../views/RegisterView.vue'), component: () => import('../views/RegisterView.vue'),
meta: { guestOnly: true },
}, },
{ {
path: '/onboarding', path: '/onboarding',
name: 'onboarding', name: 'onboarding',
component: () => import('../views/OnboardingFlow.vue'), component: () => import('../views/OnboardingFlow.vue'),
meta: { requiresAuth: true },
}, },
{ {
path: '/training/:moduleId?', path: '/training/:moduleId?',
name: 'training', name: 'training',
component: () => import('../views/TrainingModule.vue'), component: () => import('../views/TrainingModule.vue'),
meta: { requiresAuth: true },
}, },
{ {
path: '/agents', path: '/agents',
name: 'agents', name: 'agents',
component: () => import('../views/Agents.vue'), component: () => import('../views/Agents.vue'),
meta: { requiresAuth: true, roles: ['manager', 'admin'] },
}, },
{ {
path: '/agents/:id', path: '/agents/:id',
name: 'agent-detail', name: 'agent-detail',
component: () => import('../views/AgentDetail.vue'), component: () => import('../views/AgentDetail.vue'),
meta: { requiresAuth: true, roles: ['manager', 'admin'] },
}, },
{ {
path: '/roles', path: '/roles',
name: 'roles', name: 'roles',
component: () => import('../views/RoleProfiles.vue'), component: () => import('../views/RoleProfiles.vue'),
meta: { requiresAuth: true, roles: ['manager', 'admin'] },
}, },
{ {
path: '/progress', path: '/progress',
name: 'progress', name: 'progress',
component: () => import('../views/ProgressDashboard.vue'), component: () => import('../views/ProgressDashboard.vue'),
meta: { requiresAuth: true },
}, },
{ {
path: '/assessments', path: '/assessments',
name: 'assessments', name: 'assessments',
component: () => import('../views/Assessments.vue'), component: () => import('../views/Assessments.vue'),
meta: { requiresAuth: true },
}, },
{ {
path: '/resources', path: '/resources',
name: 'resources', name: 'resources',
component: () => import('../views/Resources.vue'), component: () => import('../views/Resources.vue'),
meta: { requiresAuth: true },
}, },
], ],
}); });
export default router; 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%; -webkit-text-size-adjust: 100%;
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,
'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, '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; line-height: 1.5;
tab-size: 4; tab-size: 4;
scroll-behavior: smooth; scroll-behavior: smooth;
@ -11,6 +12,8 @@ body {
font-family: inherit; font-family: inherit;
line-height: inherit; line-height: inherit;
margin: 0; margin: 0;
background: #0b1220;
color: #e5e7eb;
} }
h1, h1,
h2, 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> <template>
<div class="about"> <div class="page">
<h1>About Agentic Trainers</h1> <Card class="panel" :bordered="false">
<p> <Typography.Title :level="2"
Agentic Trainers is a lightweight platform for onboarding, training, >About Agentic Trainers</Typography.Title
and assessing employees using modular training content and >
agent-driven workflows. This repo contains a demo front-end and <Typography.Paragraph type="secondary">
example pipelines for role-based experiences. Agentic Trainers is a lightweight platform for onboarding,
</p> 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> <Divider />
<ul> <Typography.Title :level="4">Role pathways</Typography.Title>
<li> <List :data-source="pathways" :bordered="false">
<strong>Admin</strong>: full access to system settings, user <template #renderItem="{ item }">
management, and reporting. Admins can invite or deactivate <List.Item class="row">{{ item }}</List.Item>
managers and view overall progress. </template>
</li> </List>
<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>
<h2>Getting started</h2> <Divider />
<ol> <Typography.Title :level="4">Highlights</Typography.Title>
<li> <List :data-source="highlights" :bordered="false">
Register or login from the top-right to choose your role (demo <template #renderItem="{ item }">
only). <List.Item class="row">{{ item }}</List.Item>
</li> </template>
<li> </List>
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>
<p> <Divider />
This is a demo implementation authentication and permissions are <Typography.Title :level="4">Getting started</Typography.Title>
local-storage based for example purposes. For production-grade apps <List :data-source="steps" :bordered="false">
integrate a backend auth provider and persistent user store. <template #renderItem="{ item, index }">
</p> <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> </div>
</template> </template>
<style> <style scoped>
@media (min-width: 768px) { .page {
.about { max-width: 900px;
max-width: 768px; margin: 0 auto;
margin-left: auto; padding: 1rem;
margin-right: auto; }
padding: 0 1rem; .panel {
} background: #0f172a;
border: 1px solid #1f2937;
color: #e5e7eb;
}
.row {
color: #e5e7eb;
} }
</style> </style>

View file

@ -1,68 +1,421 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue'; import { ref, onMounted, onUnmounted, computed } from 'vue';
import { useRoute } from 'vue-router'; 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 route = useRoute();
const id = route.params.id || 'unknown'; console.log('[AgentDetail] Route params:', route.params);
const agent = ref({ const agentStore = useAgentStore();
id, console.log('[AgentDetail] Store instance:', agentStore);
name: `Agent ${id}`,
description: const agentId = route.params.id as string;
'This agent helps new hires by providing step-by-step assistance and quick answers to common questions.', 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> </script>
<template> <template>
<div class="page-wrap"> <div class="page">
<header class="page-header"> <Card class="panel" :bordered="false">
<h1>{{ agent.name }}</h1> <div class="header">
<p class="lead">{{ agent.description }}</p> <Typography.Title :level="2">{{ agent.name }}</Typography.Title>
</header> <Tag
:color="
statusColor(
String(agentStore.executionStatus || 'idle')
)
"
>
{{
(agentStore.executionStatus || 'idle')
.toString()
.toUpperCase()
}}
</Tag>
</div>
<section class="controls"> <Typography.Paragraph type="secondary">{{
<button class="cta-button">Run Simulation</button> agent.description || 'No description available'
<button class="link">Edit Configuration</button> }}</Typography.Paragraph>
</section>
<section class="logs"> <div class="connection-status">
<h3>Recent Interactions</h3> <span>WebSocket Status:</span>
<ul> <Tag :color="agentStore.isConnected ? 'green' : 'red'">
<li>2025-11-01: Simulated onboarding session (score: 92%)</li> {{ agentStore.isConnected ? 'CONNECTED' : 'DISCONNECTED' }}
<li>2025-11-03: Updated knowledge sources</li> </Tag>
<li>2025-11-07: Minor behavior tweak applied</li> </div>
</ul>
</section> <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> </div>
</template> </template>
<style scoped> <style scoped>
.page-wrap { .page {
max-width: 960px; max-width: 1200px;
margin: 0 auto; margin: 0 auto;
padding: 1rem; padding: 1rem;
} }
.controls {
.panel {
background: #0f172a;
border: 1px solid #1f2937;
color: #e5e7eb;
}
.header {
display: flex; 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; margin: 1rem 0;
} }
.cta-button {
background-color: #4f46e5; .log-container {
color: white; background: #1f2937;
padding: 0.6rem 1rem; border-radius: 4px;
border-radius: 0.5rem; max-height: 500px;
border: none; overflow-y: auto;
} }
.link {
background: transparent; .log-item {
color: #4f46e5; border-bottom: 1px solid #374151 !important;
border: none; padding: 0.75rem !important;
padding: 0.6rem 1rem;
} }
.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; margin: 0;
padding-left: 1.25rem; font-size: 0.8rem;
color: #374151; 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> </style>

View file

@ -1,61 +1,106 @@
<script setup lang="ts"> <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([ interface Agent {
{ id: 'a1', name: 'Onboarding Bot', role: 'Guided tours' }, uuid: string;
{ id: 'a2', name: 'Docs Helper', role: 'Knowledge base' }, id: string;
{ id: 'a3', name: 'QA Coach', role: 'Assessment' }, 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> </script>
<template> <template>
<div class="page-wrap"> <div class="page">
<header class="page-header"> <Typography.Title :level="2">Agents</Typography.Title>
<h1>Agents</h1> <Typography.Paragraph type="secondary"
<p class="lead">Manage and inspect the available AI agents.</p> >Manage and inspect the available AI agents.</Typography.Paragraph
</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>
</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> </div>
</template> </template>
<style scoped> <style scoped>
.page-wrap { .page {
max-width: 960px; max-width: 900px;
margin: 0 auto; margin: 0 auto;
padding: 1rem; padding: 1rem;
} }
.agent-list { .panel {
display: grid; background: #0f172a;
gap: 0.75rem; border: 1px solid #1f2937;
color: #e5e7eb;
} }
.agent-card { .item :deep(.ant-list-item-meta-title),
display: flex; .item :deep(.ant-list-item-meta-description) {
justify-content: space-between; color: #e5e7eb;
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;
} }
</style> </style>

View file

@ -1,5 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue'; import { ref } from 'vue';
import { Card, Typography, Row, Col, Tag, Button, Space } from 'ant-design-vue';
const assessments = ref([ const assessments = ref([
{ id: 't1', title: 'Knowledge Check - Basics', type: 'Quiz', passing: 70 }, { id: 't1', title: 'Knowledge Check - Basics', type: 'Quiz', passing: 70 },
@ -13,59 +14,47 @@ const assessments = ref([
</script> </script>
<template> <template>
<div class="page-wrap"> <div class="page">
<header class="page-header"> <Typography.Title :level="2">Assessments</Typography.Title>
<h1>Assessments</h1> <Typography.Paragraph type="secondary"
<p class="lead"> >Create and run assessments to validate
Create and run assessments to validate readiness. readiness.</Typography.Paragraph
</p> >
</header>
<section class="assess-list"> <Row :gutter="16">
<div v-for="a in assessments" :key="a.id" class="assess-card"> <Col v-for="a in assessments" :key="a.id" :xs="24" :md="12">
<h3>{{ a.title }}</h3> <Card class="card" hoverable :bordered="false">
<p class="meta"> <Typography.Title :level="4">{{
Type: {{ a.type }} · Passing: {{ a.passing }}% a.title
</p> }}</Typography.Title>
<div class="actions"> <Typography.Text type="secondary"
<button class="cta-small">Preview</button> >Type: {{ a.type }} ÷ Passing:
<button class="link">Run</button> {{ a.passing }}%</Typography.Text
</div> >
</div> <Space class="actions">
</section> <Tag color="blue">{{ a.type }}</Tag>
<Button size="small">Preview</Button>
<Button size="small" type="primary">Run</Button>
</Space>
</Card>
</Col>
</Row>
</div> </div>
</template> </template>
<style scoped> <style scoped>
.assess-list { .page {
display: grid; max-width: 1000px;
gap: 0.75rem; margin: 0 auto;
padding: 1rem;
} }
.assess-card { .card {
background: #fff; background: #0f172a;
padding: 0.75rem; border: 1px solid #1f2937;
border-radius: 8px; color: #e5e7eb;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
}
.meta {
color: #6b7280;
font-size: 0.95rem;
} }
.actions { .actions {
margin-top: 0.5rem; margin-top: 0.75rem;
display: flex;
gap: 0.5rem; 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> </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> <template>
<main class="home"> <main class="page">
<section class="hero"> <section class="hero">
<h1>Welcome to Agentic Trainers</h1> <Row :gutter="32" :align="'middle'">
<p> <Col :xs="24" :md="14">
Automate onboarding and support new team members with AI agents. <Typography.Title :level="1" class="hero-title">
Our platform creates domain-specific training workflows tailored Build agentic onboarding that feels bespoke to every
to each role. role
</p> </Typography.Title>
<router-link to="/about" class="cta-button"> <Typography.Paragraph class="hero-sub">
Learn More AI-led workflows, assessments, and knowledge delivery
</router-link> 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>
<section class="features"> <section class="features">
<h2>Key Features</h2> <Typography.Title :level="2"
<ul> >Everything you need to ramp faster</Typography.Title
<li>Reusable AI-powered workflows for role induction.</li> >
<li>Adaptive guidance tailored to each team member.</li> <Row :gutter="16">
<li>Track progress and generate actionable insights.</li> <Col
<li>Extensible to any domain or industry.</li> v-for="feature in features"
</ul> :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>
<section class="get-started"> <section class="journeys">
<h2>Get Started</h2> <Typography.Title :level="2"
<p> >Prebuilt journeys, tailored in minutes</Typography.Title
Begin your AI-driven onboarding journey today. Explore how our >
agentic approach can help your team succeed faster. <Row :gutter="16">
</p> <Col
<router-link to="/about" class="cta-button"> v-for="journey in journeys"
Explore Now :key="journey.name"
</router-link> :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> </section>
</main> </main>
</template> </template>
<style scoped> <style scoped>
.home { .page {
max-width: 960px; padding: 2rem 1.5rem 3rem;
max-width: 1200px;
margin: 0 auto; margin: 0 auto;
padding: 1rem;
} }
.hero { .hero {
text-align: center; margin-bottom: 2.5rem;
margin-bottom: 3rem;
} }
.hero h1 { .hero-title {
font-size: 2.5rem;
margin-bottom: 1rem; margin-bottom: 1rem;
} }
.hero p { .hero-sub {
font-size: 1.25rem; font-size: 1.05rem;
margin-bottom: 2rem; color: #cbd5e1;
} }
.cta-button { .hero-card {
background-color: #4f46e5; border: none;
color: white; }
padding: 0.75rem 1.5rem; .hero-img {
border-radius: 0.5rem; width: 100%;
text-decoration: none; height: 280px;
object-fit: cover;
border-radius: 8px;
}
.hero-overlay {
margin-top: 0.75rem;
color: #8b5cf6;
font-weight: 600; font-weight: 600;
} }
.cta-button:hover { .stat-card {
background-color: #4338ca; 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 { .features {
margin-bottom: 3rem; margin: 2.5rem 0;
} }
.features h2 { .feature-card {
font-size: 2rem; height: 100%;
margin-bottom: 1rem; background: #0f172a;
border: 1px solid #1f2937;
color: #e5e7eb;
} }
.features ul { .journeys {
list-style-type: disc; margin: 2.5rem 0;
padding-left: 1.5rem;
} }
.get-started h2 { .journey-card {
font-size: 2rem; background: #0f172a;
margin-bottom: 1rem; border: 1px solid #1f2937;
color: #e5e7eb;
} }
.get-started p { .testimonials {
margin-bottom: 1.5rem; 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> </style>

View file

@ -1,53 +1,106 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue'; import { reactive, computed, onMounted } from 'vue';
import { useRouter } from 'vue-router'; 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 router = useRouter();
const name = ref(''); const route = useRoute();
const role = ref<'admin' | 'manager' | 'employee'>('employee'); const authStore = useAuthStore();
const loading = computed(() => authStore.loading);
function submit() { const formState = reactive({
router.push('/'); 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> </script>
<template> <template>
<div class="auth"> <div class="auth-page">
<h1>Login (demo)</h1> <Card class="panel" :bordered="false">
<div> <Typography.Title :level="3">Login</Typography.Title>
<label>Name</label> <Form
<input v-model="name" placeholder="Your name" /> ref="form"
</div> layout="vertical"
:model="formState"
<div> @finish="submit"
<label>Role</label> >
<select v-model="role"> <Form.Item
<option value="employee">Employee</option> label="Email"
<option value="manager">Manager</option> name="email"
<option value="admin">Admin</option> :rules="[
</select> { required: true, message: 'Enter your email' },
</div> {
type: 'email',
<button class="cta-button" @click="submit">Login</button> 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> </div>
</template> </template>
<style scoped> <style scoped>
.auth { .auth-page {
max-width: 480px; display: flex;
margin: 0 auto; align-items: center;
justify-content: center;
min-height: 100vh;
padding: 1rem;
} }
label { .panel {
display: block; max-width: 400px;
margin-top: 0.5rem;
}
input,
select {
width: 100%; width: 100%;
padding: 0.5rem; background: #0f172a;
margin-top: 0.25rem; border: 1px solid #1f2937;
} color: #e5e7eb;
.cta-button {
margin-top: 1rem;
} }
</style> </style>

View file

@ -1,5 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue'; import { ref } from 'vue';
import { Card, Typography, Timeline, Button, Space } from 'ant-design-vue';
const steps = ref([ const steps = ref([
{ {
@ -24,95 +25,63 @@ const steps = ref([
</script> </script>
<template> <template>
<div class="page-wrap"> <div class="page">
<header class="page-header"> <Card class="panel" :bordered="false">
<h1>Onboarding Flow</h1> <Typography.Title :level="2">Onboarding Flow</Typography.Title>
<p class="lead"> <Typography.Paragraph type="secondary">
Step-by-step AI-guided onboarding for new team members. Step-by-step AI-guided onboarding for new team members.
</p> </Typography.Paragraph>
</header>
<section class="steps"> <Timeline mode="left" class="timeline">
<div v-for="step in steps" :key="step.id" class="card"> <Timeline.Item
<div class="card-left"> v-for="step in steps"
<div class="step-index">{{ step.id }}</div> :key="step.id"
</div> color="purple"
<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
> >
<router-link to="/agents" class="link">View Agents</router-link> <Space direction="vertical" size="small">
</footer> <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> </div>
</template> </template>
<style scoped> <style scoped>
.page-wrap { .page {
max-width: 960px; max-width: 1100px;
margin: 0 auto; margin: 0 auto;
padding: 1rem; padding: 1rem;
} }
.page-header h1 { .panel {
font-size: 2rem; background: #0f172a;
margin-bottom: 0.25rem; border: 1px solid #1f2937;
color: #e5e7eb;
} }
.lead { .timeline :deep(.ant-timeline-item-head) {
color: #6b7280; background: #8b5cf6;
margin-bottom: 1rem;
} }
.steps { .timeline :deep(.ant-typography) {
display: grid; color: #e5e7eb;
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;
} }
.actions { .actions {
display: flex; margin-top: 1rem;
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;
} }
</style> </style>

View file

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

View file

@ -1,52 +1,172 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue'; import { reactive, computed, onMounted } from 'vue';
import { useRouter } from 'vue-router'; 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 router = useRouter();
const name = ref(''); const route = useRoute();
const role = ref<'admin' | 'manager' | 'employee'>('employee'); const authStore = useAuthStore();
const loading = computed(() => authStore.loading);
function submit() { const formState = reactive({
router.push('/'); 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> </script>
<template> <template>
<div class="auth"> <div class="auth-page">
<h1>Register (demo)</h1> <Card class="panel" :bordered="false">
<div> <Typography.Title :level="3">Register</Typography.Title>
<label>Name</label> <Form layout="vertical" :model="formState" @finish="submit">
<input v-model="name" placeholder="Your name" /> <Form.Item
</div> label="Email"
name="email"
<div> :rules="[
<label>Role</label> { required: true, message: 'Enter your email' },
<select v-model="role"> {
<option value="employee">Employee</option> type: 'email',
<option value="manager">Manager</option> message: 'Please enter a valid email',
</select> },
</div> ]"
>
<button class="cta-button" @click="submit">Register</button> <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> </div>
</template> </template>
<style scoped> <style scoped>
.auth { .auth-page {
max-width: 480px; max-width: 520px;
margin: 0 auto; margin: 0 auto;
padding: 1rem;
} }
label { .panel {
display: block; background: #0f172a;
margin-top: 0.5rem; border: 1px solid #1f2937;
} color: #e5e7eb;
input,
select {
width: 100%;
padding: 0.5rem;
margin-top: 0.25rem;
}
.cta-button {
margin-top: 1rem;
} }
</style> </style>

View file

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

View file

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

View file

@ -1,5 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue'; import { ref } from 'vue';
import { Card, Typography, Row, Col, Tag, Button } from 'ant-design-vue';
const lessons = ref([ const lessons = ref([
{ {
@ -24,52 +25,42 @@ const lessons = ref([
</script> </script>
<template> <template>
<div class="page-wrap"> <div class="page">
<header class="page-header"> <Typography.Title :level="2">Training Module</Typography.Title>
<h1>Training Module</h1> <Typography.Paragraph type="secondary"
<p class="lead">Interactive module with lessons and checkpoints.</p> >Interactive module with lessons and
</header> checkpoints.</Typography.Paragraph
<section class="lessons">
<article
v-for="lesson in lessons"
:key="lesson.id"
class="lesson-card"
> >
<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"> <div class="lesson-footer">
<span class="badge">{{ lesson.type }}</span> <Tag color="purple">{{ lesson.type }}</Tag>
<button class="cta-small">Open</button> <Button type="primary" size="small">Open</Button>
</div> </div>
</article> </Card>
</section> </Col>
</Row>
</div> </div>
</template> </template>
<style scoped> <style scoped>
.page-wrap { .page {
max-width: 960px; max-width: 1100px;
margin: 0 auto; margin: 0 auto;
padding: 1rem; padding: 1rem;
} }
.page-header h1 { .card {
font-size: 2rem; background: #0f172a;
} border: 1px solid #1f2937;
.lead { color: #e5e7eb;
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);
} }
.lesson-footer { .lesson-footer {
display: flex; display: flex;
@ -77,18 +68,4 @@ const lessons = ref([
align-items: center; align-items: center;
margin-top: 0.75rem; 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> </style>