added a test link so the admin can test the interviews too, admin can now go to the landing page too, fixes for the swagger links
This commit is contained in:
parent
824bf93dfb
commit
7a868d7f14
@ -35,7 +35,12 @@ import {$log} from "@tsed/logger";
|
||||
version: process.env.APP_VERSION || "1.0.0",
|
||||
description:
|
||||
"REST API for Candivista. Authentication via JWT Bearer tokens.\n\n" +
|
||||
"Includes endpoints for auth, users, jobs, tokens, AI, and admin reporting.",
|
||||
"Includes endpoints for auth, users, jobs, tokens, AI-powered interviews (OpenRouter/Ollama), and admin reporting.\n\n" +
|
||||
"AI Features:\n" +
|
||||
"- OpenRouter integration for cloud-based AI interviews\n" +
|
||||
"- Ollama support for local AI processing\n" +
|
||||
"- Test mode for admin interview testing\n" +
|
||||
"- Mandatory question support before AI interviews",
|
||||
contact: {
|
||||
name: "Candivista Team",
|
||||
url: "https://candivista.com",
|
||||
@ -51,7 +56,7 @@ import {$log} from "@tsed/logger";
|
||||
{ name: "Users", description: "User profile and token summary" },
|
||||
{ name: "Jobs", description: "Job posting and interview token operations" },
|
||||
{ name: "Admin", description: "Administrative statistics and management" },
|
||||
{ name: "AI", description: "AI provider tests and operations" }
|
||||
{ name: "AI", description: "AI-powered interview operations with OpenRouter and Ollama support" }
|
||||
],
|
||||
components: {
|
||||
securitySchemes: {
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { Controller } from "@tsed/di";
|
||||
import { Post, Get } from "@tsed/schema";
|
||||
import { Post, Get, Tags, Summary, Description, Returns, Security } from "@tsed/schema";
|
||||
import { BodyParams, PathParams, QueryParams } from "@tsed/platform-params";
|
||||
import { Req } from "@tsed/platform-http";
|
||||
import { BadRequest, NotFound } from "@tsed/exceptions";
|
||||
@ -8,6 +8,7 @@ import { AIService } from "../../services/AIService.js";
|
||||
import axios from "axios";
|
||||
|
||||
@Controller("/ai")
|
||||
@Tags("AI")
|
||||
export class AIController {
|
||||
private jobService = new JobService();
|
||||
private aiService = new AIService();
|
||||
@ -17,6 +18,9 @@ export class AIController {
|
||||
|
||||
// Test AI connection
|
||||
@Get("/test-ai")
|
||||
@Summary("Test AI connection")
|
||||
@Description("Test the AI service connection and configuration. Works with both Ollama and OpenRouter providers.")
|
||||
@(Returns(200, Object).Description("AI test result with success status and response"))
|
||||
async testAI() {
|
||||
try {
|
||||
if (this.aiProvider === 'openrouter') {
|
||||
@ -60,6 +64,10 @@ export class AIController {
|
||||
|
||||
// Get mandatory questions for the job
|
||||
@Get("/mandatory-questions/:linkId")
|
||||
@Summary("Get mandatory interview questions")
|
||||
@Description("Retrieve mandatory questions for a specific job interview link")
|
||||
@(Returns(200, Object).Description("List of mandatory questions for the job"))
|
||||
@(Returns(404, Object).Description("Interview link not found or expired"))
|
||||
async getMandatoryQuestions(@PathParams("linkId") linkId: string) {
|
||||
try {
|
||||
// Verify the job exists and link is valid
|
||||
@ -83,6 +91,11 @@ export class AIController {
|
||||
|
||||
// Submit mandatory question answers
|
||||
@Post("/submit-mandatory-answers")
|
||||
@Summary("Submit mandatory question answers")
|
||||
@Description("Submit answers to mandatory interview questions before starting the AI interview")
|
||||
@(Returns(200, Object).Description("Success response with interview data"))
|
||||
@(Returns(400, Object).Description("Missing required fields"))
|
||||
@(Returns(404, Object).Description("Interview link not found or expired"))
|
||||
async submitMandatoryAnswers(@BodyParams() body: any, @QueryParams() query: any) {
|
||||
try {
|
||||
const { candidateName, job, linkId, answers } = body;
|
||||
@ -135,6 +148,12 @@ export class AIController {
|
||||
|
||||
// Start interview with AI agent (only after mandatory questions)
|
||||
@Post("/start-interview")
|
||||
@Summary("Start AI interview")
|
||||
@Description("Initialize an AI-powered interview session. Can be used in test mode for admins.")
|
||||
@(Returns(200, Object).Description("Interview started successfully with initial AI message"))
|
||||
@(Returns(400, Object).Description("Missing required fields"))
|
||||
@(Returns(404, Object).Description("Interview link not found or expired"))
|
||||
@(Returns(500, Object).Description("AI service unavailable"))
|
||||
async startInterview(@BodyParams() body: any, @QueryParams() query: any) {
|
||||
try {
|
||||
const { candidateName, job, linkId } = body;
|
||||
@ -197,6 +216,12 @@ export class AIController {
|
||||
|
||||
// Handle chat messages
|
||||
@Post("/chat")
|
||||
@Summary("Send chat message to AI")
|
||||
@Description("Send a message to the AI interviewer and receive a response. Supports both test and production modes.")
|
||||
@(Returns(200, Object).Description("AI response message"))
|
||||
@(Returns(400, Object).Description("Missing required fields"))
|
||||
@(Returns(404, Object).Description("Interview link not found or expired"))
|
||||
@(Returns(500, Object).Description("AI service unavailable"))
|
||||
async handleChat(@BodyParams() body: any, @QueryParams() query: any) {
|
||||
try {
|
||||
const { message, candidateName, job, linkId, conversationHistory } = body;
|
||||
@ -226,7 +251,7 @@ export class AIController {
|
||||
console.log(`[DEBUG] Using frontend conversation history (test mode): ${JSON.stringify(conversationHistoryToUse, null, 2)}`);
|
||||
|
||||
// Filter out any messages with undefined content
|
||||
conversationHistoryToUse = conversationHistoryToUse.filter(msg =>
|
||||
conversationHistoryToUse = conversationHistoryToUse.filter((msg: any) =>
|
||||
msg && msg.message && msg.message !== 'undefined' && msg.sender
|
||||
);
|
||||
console.log(`[DEBUG] Filtered conversation history: ${JSON.stringify(conversationHistoryToUse, null, 2)}`);
|
||||
@ -275,6 +300,10 @@ export class AIController {
|
||||
|
||||
// Get conversation history
|
||||
@Get("/conversation/:linkId")
|
||||
@Summary("Get conversation history")
|
||||
@Description("Retrieve the conversation history for a specific interview")
|
||||
@(Returns(200, Object).Description("Conversation history messages"))
|
||||
@(Returns(404, Object).Description("Interview link not found or expired"))
|
||||
async getConversation(@PathParams("linkId") linkId: string) {
|
||||
try {
|
||||
const jobData = await this.jobService.getJobByLinkId(linkId);
|
||||
@ -304,6 +333,10 @@ export class AIController {
|
||||
|
||||
// End interview
|
||||
@Post("/end-interview/:linkId")
|
||||
@Summary("End interview session")
|
||||
@Description("End an active interview session and mark it as completed")
|
||||
@(Returns(200, Object).Description("Interview ended successfully"))
|
||||
@(Returns(404, Object).Description("Interview link or interview not found"))
|
||||
async endInterview(@PathParams("linkId") linkId: string) {
|
||||
try {
|
||||
const jobData = await this.jobService.getJobByLinkId(linkId);
|
||||
|
||||
@ -246,4 +246,21 @@ export class AdminController {
|
||||
await this.checkAdmin(req);
|
||||
return await this.adminService.getPaymentById(id);
|
||||
}
|
||||
|
||||
// Job Links Management
|
||||
@Get("/jobs/:id/links")
|
||||
@Summary("Get job links")
|
||||
@(Returns(200).Description("Job links returned"))
|
||||
async getJobLinks(@Req() req: any, @PathParams("id") id: string) {
|
||||
await this.checkAdmin(req);
|
||||
return await this.adminService.getJobLinks(id);
|
||||
}
|
||||
|
||||
@Post("/jobs/:id/create-link")
|
||||
@Summary("Create job link for testing")
|
||||
@(Returns(200).Description("Job link created"))
|
||||
async createJobLink(@Req() req: any, @PathParams("id") id: string, @BodyParams() linkData: any) {
|
||||
await this.checkAdmin(req);
|
||||
return await this.adminService.createJobLink(id, linkData.tokensAvailable || 1);
|
||||
}
|
||||
}
|
||||
|
||||
@ -773,4 +773,58 @@ export class AdminService {
|
||||
connection.release();
|
||||
}
|
||||
}
|
||||
|
||||
// Job Links Management
|
||||
async getJobLinks(jobId: string) {
|
||||
const connection = await pool.getConnection();
|
||||
|
||||
try {
|
||||
const [rows] = await connection.execute(
|
||||
'SELECT * FROM job_links WHERE job_id = ? ORDER BY created_at DESC',
|
||||
[jobId]
|
||||
);
|
||||
|
||||
if (Array.isArray(rows)) {
|
||||
return rows;
|
||||
}
|
||||
return [];
|
||||
} catch (error) {
|
||||
$log.error('Error getting job links:', error);
|
||||
return [];
|
||||
} finally {
|
||||
connection.release();
|
||||
}
|
||||
}
|
||||
|
||||
async createJobLink(jobId: string, tokensAvailable: number = 1) {
|
||||
const connection = await pool.getConnection();
|
||||
|
||||
try {
|
||||
// Generate a random URL slug and UUID
|
||||
const linkId = randomUUID();
|
||||
const urlSlug = randomUUID().replace(/-/g, '').substring(0, 8);
|
||||
|
||||
await connection.execute(
|
||||
'INSERT INTO job_links (id, job_id, url_slug, tokens_available, created_at, updated_at) VALUES (?, ?, ?, ?, NOW(), NOW())',
|
||||
[linkId, jobId, urlSlug, tokensAvailable]
|
||||
);
|
||||
|
||||
// Get the created link
|
||||
const [rows] = await connection.execute(
|
||||
'SELECT * FROM job_links WHERE id = ?',
|
||||
[linkId]
|
||||
);
|
||||
|
||||
if (Array.isArray(rows) && rows.length > 0) {
|
||||
return rows[0];
|
||||
}
|
||||
|
||||
throw new Error('Failed to create job link');
|
||||
} catch (error) {
|
||||
$log.error('Error creating job link:', error);
|
||||
throw error;
|
||||
} finally {
|
||||
connection.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import Image from "next/image";
|
||||
import axios from "axios";
|
||||
import AnimatedCounter from "@/components/AnimatedCounter";
|
||||
import FeatureCard from "@/components/FeatureCard";
|
||||
import PricingCard from "@/components/PricingCard";
|
||||
@ -17,7 +18,27 @@ export default function Home() {
|
||||
// Check if user is already logged in
|
||||
const token = localStorage.getItem("token");
|
||||
if (token) {
|
||||
router.push("/dashboard");
|
||||
// Check if user is admin - if so, allow them to stay on landing page
|
||||
// Non-admin users will be redirected to dashboard
|
||||
axios.get(`${process.env.NEXT_PUBLIC_API_URL}/rest/auth/me`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
})
|
||||
.then(response => {
|
||||
const userData = response.data;
|
||||
if (userData.role !== 'admin') {
|
||||
router.push("/dashboard");
|
||||
} else {
|
||||
setIsLoading(false);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
// If token is invalid, remove it and stay on landing page
|
||||
localStorage.removeItem("token");
|
||||
localStorage.removeItem("user");
|
||||
setIsLoading(false);
|
||||
});
|
||||
} else {
|
||||
setIsLoading(false);
|
||||
}
|
||||
|
||||
@ -137,6 +137,42 @@ export default function JobManagement() {
|
||||
setIsAddTokensModalOpen(true);
|
||||
};
|
||||
|
||||
const handleTestInterview = async (job: Job) => {
|
||||
try {
|
||||
const token = localStorage.getItem("token");
|
||||
|
||||
// Get job links for this job
|
||||
const response = await axios.get(`${process.env.NEXT_PUBLIC_API_URL}/rest/admin/jobs/${job.id}/links`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
let linkId;
|
||||
if (response.data && response.data.length > 0) {
|
||||
// Use the first available link
|
||||
linkId = response.data[0].url_slug;
|
||||
} else {
|
||||
// Create a test link if none exists
|
||||
const createLinkResponse = await axios.post(`${process.env.NEXT_PUBLIC_API_URL}/rest/admin/jobs/${job.id}/create-link`, {
|
||||
tokensAvailable: 1
|
||||
}, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
linkId = createLinkResponse.data.url_slug;
|
||||
}
|
||||
|
||||
// Open interview page in test mode with the correct link ID
|
||||
const testUrl = `/interview?id=${linkId}&test=true`;
|
||||
window.open(testUrl, '_blank');
|
||||
} catch (error) {
|
||||
console.error("Failed to get job link for testing:", error);
|
||||
alert("Failed to create test interview link. Please try again.");
|
||||
}
|
||||
};
|
||||
|
||||
const handleToggleJobStatus = async (job: Job) => {
|
||||
try {
|
||||
const token = localStorage.getItem("token");
|
||||
@ -392,6 +428,12 @@ export default function JobManagement() {
|
||||
>
|
||||
Add Tokens
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleTestInterview(job)}
|
||||
className="text-purple-600 hover:text-purple-800 dark:text-purple-400 dark:hover:text-purple-300 text-sm font-medium"
|
||||
>
|
||||
Test Interview
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => handleToggleJobStatus(job)}
|
||||
|
||||
1
tuna
Submodule
1
tuna
Submodule
@ -0,0 +1 @@
|
||||
Subproject commit bcd25503c5fa7562e1fb6cef8b719dbf39b5bab1
|
||||
Loading…
Reference in New Issue
Block a user