Compare commits
4
Commits
ec8342b5e2
...
release
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7a868d7f14 | ||
|
|
824bf93dfb | ||
|
|
7f9cf79a21 | ||
|
|
b83c448573 |
@@ -0,0 +1,46 @@
|
|||||||
|
# Multi-stage build for ASP.NET Core application
|
||||||
|
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy project files
|
||||||
|
COPY AISApp.csproj .
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Restore dependencies
|
||||||
|
RUN dotnet restore AISApp.csproj
|
||||||
|
|
||||||
|
# Build the application
|
||||||
|
RUN dotnet build AISApp.csproj -c Release -o /app/build
|
||||||
|
|
||||||
|
# Publish the application
|
||||||
|
RUN dotnet publish AISApp.csproj -c Release -o /app/publish
|
||||||
|
|
||||||
|
# Runtime stage
|
||||||
|
FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS runtime
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install curl for health checks
|
||||||
|
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Copy published application
|
||||||
|
COPY --from=build /app/publish .
|
||||||
|
|
||||||
|
# Copy prompt.txt file
|
||||||
|
COPY prompt.txt .
|
||||||
|
|
||||||
|
# Create directory for static files
|
||||||
|
RUN mkdir -p static
|
||||||
|
|
||||||
|
# Expose port
|
||||||
|
EXPOSE 80
|
||||||
|
|
||||||
|
# Set environment variables
|
||||||
|
ENV ASPNETCORE_URLS=http://+:80
|
||||||
|
ENV ASPNETCORE_ENVIRONMENT=Production
|
||||||
|
|
||||||
|
# Health check
|
||||||
|
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
|
||||||
|
CMD curl -f http://localhost/api/chat || exit 1
|
||||||
|
|
||||||
|
# Run the application
|
||||||
|
ENTRYPOINT ["dotnet", "AISApp.dll"]
|
||||||
@@ -20,3 +20,5 @@ This will start the application with all necessary services.
|
|||||||
---
|
---
|
||||||
|
|
||||||
This README provides the basic steps to configure and run the AISApp project using Docker Compose and environment variables.
|
This README provides the basic steps to configure and run the AISApp project using Docker Compose and environment variables.
|
||||||
|
|
||||||
|
portainer= tunaadmin/tunatainer8!
|
||||||
@@ -20,7 +20,7 @@ Integrate the ASP.NET chatbot service into the existing Node.js backend system t
|
|||||||
### 1.3 Create Chatbot Service Dockerfile
|
### 1.3 Create Chatbot Service Dockerfile
|
||||||
- **Status**: âś… Completed
|
- **Status**: âś… Completed
|
||||||
- **Description**: Create proper Dockerfile for the ASP.NET chatbot service
|
- **Description**: Create proper Dockerfile for the ASP.NET chatbot service
|
||||||
- **Files**: `tuna/tuna/Dockerfile`
|
- **Files**: `AISApp/Dockerfile`
|
||||||
- **Details**: Multi-stage build with proper .NET 9.0 runtime and configuration
|
- **Details**: Multi-stage build with proper .NET 9.0 runtime and configuration
|
||||||
|
|
||||||
## Phase 2: Backend Service Integration
|
## Phase 2: Backend Service Integration
|
||||||
@@ -54,25 +54,25 @@ Integrate the ASP.NET chatbot service into the existing Node.js backend system t
|
|||||||
### 3.1 Add MySQL Database Support
|
### 3.1 Add MySQL Database Support
|
||||||
- **Status**: âś… Completed
|
- **Status**: âś… Completed
|
||||||
- **Description**: Replace SQLite with MySQL database connection in ASP.NET service
|
- **Description**: Replace SQLite with MySQL database connection in ASP.NET service
|
||||||
- **Files**: `tuna/tuna/AISApp/AIS.cs`, `tuna/tuna/AISApp/Program.cs`
|
- **Files**: `AISApp/AISApp/AIS.cs`, `AISApp/AISApp/Program.cs`
|
||||||
- **Details**: Add MySQL connection string and update database operations
|
- **Details**: Add MySQL connection string and update database operations
|
||||||
|
|
||||||
### 3.2 Add Interview Context Endpoints
|
### 3.2 Add Interview Context Endpoints
|
||||||
- **Status**: âś… Completed
|
- **Status**: âś… Completed
|
||||||
- **Description**: Create endpoints for interview initialization and context management
|
- **Description**: Create endpoints for interview initialization and context management
|
||||||
- **Files**: `tuna/tuna/AISApp/Program.cs`
|
- **Files**: `AISApp/AISApp/Program.cs`
|
||||||
- **Details**: Add endpoints for interview start, status, and completion
|
- **Details**: Add endpoints for interview start, status, and completion
|
||||||
|
|
||||||
### 3.3 Implement Conversation Sync
|
### 3.3 Implement Conversation Sync
|
||||||
- **Status**: âś… Completed
|
- **Status**: âś… Completed
|
||||||
- **Description**: Sync conversation data between ASP.NET service and MySQL database
|
- **Description**: Sync conversation data between ASP.NET service and MySQL database
|
||||||
- **Files**: `tuna/tuna/AISApp/AIS.cs`
|
- **Files**: `AISApp/AISApp/AIS.cs`
|
||||||
- **Details**: Update conversation persistence to use MySQL instead of SQLite
|
- **Details**: Update conversation persistence to use MySQL instead of SQLite
|
||||||
|
|
||||||
### 3.4 Add Interview-Specific Prompts
|
### 3.4 Add Interview-Specific Prompts
|
||||||
- **Status**: âś… Completed
|
- **Status**: âś… Completed
|
||||||
- **Description**: Modify system prompts based on job requirements and interview context
|
- **Description**: Modify system prompts based on job requirements and interview context
|
||||||
- **Files**: `tuna/tuna/AISApp/prompt.txt`, `tuna/tuna/AISApp/AIS.cs`
|
- **Files**: `AISApp/AISApp/prompt.txt`, `AISApp/AISApp/AIS.cs`
|
||||||
- **Details**: Dynamic prompt generation based on job details and interview stage
|
- **Details**: Dynamic prompt generation based on job details and interview stage
|
||||||
|
|
||||||
## Phase 4: Database Integration
|
## Phase 4: Database Integration
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ services:
|
|||||||
|
|
||||||
chatbot:
|
chatbot:
|
||||||
build:
|
build:
|
||||||
context: ../../tuna/tuna
|
context: ../../AISApp
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
image: candidat/chatbot:latest
|
image: candidat/chatbot:latest
|
||||||
container_name: backend-chatbot
|
container_name: backend-chatbot
|
||||||
|
|||||||
@@ -35,7 +35,12 @@ import {$log} from "@tsed/logger";
|
|||||||
version: process.env.APP_VERSION || "1.0.0",
|
version: process.env.APP_VERSION || "1.0.0",
|
||||||
description:
|
description:
|
||||||
"REST API for Candivista. Authentication via JWT Bearer tokens.\n\n" +
|
"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: {
|
contact: {
|
||||||
name: "Candivista Team",
|
name: "Candivista Team",
|
||||||
url: "https://candivista.com",
|
url: "https://candivista.com",
|
||||||
@@ -51,7 +56,7 @@ import {$log} from "@tsed/logger";
|
|||||||
{ name: "Users", description: "User profile and token summary" },
|
{ name: "Users", description: "User profile and token summary" },
|
||||||
{ name: "Jobs", description: "Job posting and interview token operations" },
|
{ name: "Jobs", description: "Job posting and interview token operations" },
|
||||||
{ name: "Admin", description: "Administrative statistics and management" },
|
{ 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: {
|
components: {
|
||||||
securitySchemes: {
|
securitySchemes: {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Controller } from "@tsed/di";
|
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 { BodyParams, PathParams, QueryParams } from "@tsed/platform-params";
|
||||||
import { Req } from "@tsed/platform-http";
|
import { Req } from "@tsed/platform-http";
|
||||||
import { BadRequest, NotFound } from "@tsed/exceptions";
|
import { BadRequest, NotFound } from "@tsed/exceptions";
|
||||||
@@ -8,6 +8,7 @@ import { AIService } from "../../services/AIService.js";
|
|||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
|
|
||||||
@Controller("/ai")
|
@Controller("/ai")
|
||||||
|
@Tags("AI")
|
||||||
export class AIController {
|
export class AIController {
|
||||||
private jobService = new JobService();
|
private jobService = new JobService();
|
||||||
private aiService = new AIService();
|
private aiService = new AIService();
|
||||||
@@ -17,6 +18,9 @@ export class AIController {
|
|||||||
|
|
||||||
// Test AI connection
|
// Test AI connection
|
||||||
@Get("/test-ai")
|
@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() {
|
async testAI() {
|
||||||
try {
|
try {
|
||||||
if (this.aiProvider === 'openrouter') {
|
if (this.aiProvider === 'openrouter') {
|
||||||
@@ -60,6 +64,10 @@ export class AIController {
|
|||||||
|
|
||||||
// Get mandatory questions for the job
|
// Get mandatory questions for the job
|
||||||
@Get("/mandatory-questions/:linkId")
|
@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) {
|
async getMandatoryQuestions(@PathParams("linkId") linkId: string) {
|
||||||
try {
|
try {
|
||||||
// Verify the job exists and link is valid
|
// Verify the job exists and link is valid
|
||||||
@@ -83,6 +91,11 @@ export class AIController {
|
|||||||
|
|
||||||
// Submit mandatory question answers
|
// Submit mandatory question answers
|
||||||
@Post("/submit-mandatory-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) {
|
async submitMandatoryAnswers(@BodyParams() body: any, @QueryParams() query: any) {
|
||||||
try {
|
try {
|
||||||
const { candidateName, job, linkId, answers } = body;
|
const { candidateName, job, linkId, answers } = body;
|
||||||
@@ -135,6 +148,12 @@ export class AIController {
|
|||||||
|
|
||||||
// Start interview with AI agent (only after mandatory questions)
|
// Start interview with AI agent (only after mandatory questions)
|
||||||
@Post("/start-interview")
|
@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) {
|
async startInterview(@BodyParams() body: any, @QueryParams() query: any) {
|
||||||
try {
|
try {
|
||||||
const { candidateName, job, linkId } = body;
|
const { candidateName, job, linkId } = body;
|
||||||
@@ -197,6 +216,12 @@ export class AIController {
|
|||||||
|
|
||||||
// Handle chat messages
|
// Handle chat messages
|
||||||
@Post("/chat")
|
@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) {
|
async handleChat(@BodyParams() body: any, @QueryParams() query: any) {
|
||||||
try {
|
try {
|
||||||
const { message, candidateName, job, linkId, conversationHistory } = body;
|
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)}`);
|
console.log(`[DEBUG] Using frontend conversation history (test mode): ${JSON.stringify(conversationHistoryToUse, null, 2)}`);
|
||||||
|
|
||||||
// Filter out any messages with undefined content
|
// Filter out any messages with undefined content
|
||||||
conversationHistoryToUse = conversationHistoryToUse.filter(msg =>
|
conversationHistoryToUse = conversationHistoryToUse.filter((msg: any) =>
|
||||||
msg && msg.message && msg.message !== 'undefined' && msg.sender
|
msg && msg.message && msg.message !== 'undefined' && msg.sender
|
||||||
);
|
);
|
||||||
console.log(`[DEBUG] Filtered conversation history: ${JSON.stringify(conversationHistoryToUse, null, 2)}`);
|
console.log(`[DEBUG] Filtered conversation history: ${JSON.stringify(conversationHistoryToUse, null, 2)}`);
|
||||||
@@ -275,6 +300,10 @@ export class AIController {
|
|||||||
|
|
||||||
// Get conversation history
|
// Get conversation history
|
||||||
@Get("/conversation/:linkId")
|
@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) {
|
async getConversation(@PathParams("linkId") linkId: string) {
|
||||||
try {
|
try {
|
||||||
const jobData = await this.jobService.getJobByLinkId(linkId);
|
const jobData = await this.jobService.getJobByLinkId(linkId);
|
||||||
@@ -304,6 +333,10 @@ export class AIController {
|
|||||||
|
|
||||||
// End interview
|
// End interview
|
||||||
@Post("/end-interview/:linkId")
|
@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) {
|
async endInterview(@PathParams("linkId") linkId: string) {
|
||||||
try {
|
try {
|
||||||
const jobData = await this.jobService.getJobByLinkId(linkId);
|
const jobData = await this.jobService.getJobByLinkId(linkId);
|
||||||
|
|||||||
@@ -246,4 +246,21 @@ export class AdminController {
|
|||||||
await this.checkAdmin(req);
|
await this.checkAdmin(req);
|
||||||
return await this.adminService.getPaymentById(id);
|
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();
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
|
# Visual Studio Version 17
|
||||||
|
VisualStudioVersion = 17.5.2.0
|
||||||
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AISApp", "AISApp\AISApp.csproj", "{1671248C-43AD-2D25-4F0F-918991BEF94A}"
|
||||||
|
EndProject
|
||||||
|
Global
|
||||||
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
Release|Any CPU = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||||
|
{1671248C-43AD-2D25-4F0F-918991BEF94A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{1671248C-43AD-2D25-4F0F-918991BEF94A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{1671248C-43AD-2D25-4F0F-918991BEF94A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{1671248C-43AD-2D25-4F0F-918991BEF94A}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
|
HideSolutionNode = FALSE
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
|
SolutionGuid = {14FC4F28-B89B-49C6-BD21-D4B1F80AE49C}
|
||||||
|
EndGlobalSection
|
||||||
|
EndGlobal
|
||||||
+3
-3
@@ -82,7 +82,7 @@ services:
|
|||||||
# Frontend Service
|
# Frontend Service
|
||||||
frontend:
|
frontend:
|
||||||
build:
|
build:
|
||||||
context: ./frontend/candidat-frontend
|
context: ./frontend
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
image: candidat/frontend:${APP_VERSION:-latest}
|
image: candidat/frontend:${APP_VERSION:-latest}
|
||||||
container_name: candidat-frontend
|
container_name: candidat-frontend
|
||||||
@@ -93,7 +93,7 @@ services:
|
|||||||
- "${FRONTEND_PORT:-3000}:3000"
|
- "${FRONTEND_PORT:-3000}:3000"
|
||||||
volumes:
|
volumes:
|
||||||
# Development hot reloading (only if NODE_ENV=development)
|
# Development hot reloading (only if NODE_ENV=development)
|
||||||
- ./frontend/candidat-frontend/src:/app/src:ro
|
- ./frontend/src:/app/src:ro
|
||||||
depends_on:
|
depends_on:
|
||||||
backend:
|
backend:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
@@ -116,7 +116,7 @@ services:
|
|||||||
# Chatbot Service
|
# Chatbot Service
|
||||||
chatbot:
|
chatbot:
|
||||||
build:
|
build:
|
||||||
context: ./tuna/tuna
|
context: ./AISApp
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
image: candidat/chatbot:${APP_VERSION:-latest}
|
image: candidat/chatbot:${APP_VERSION:-latest}
|
||||||
container_name: candidat-chatbot
|
container_name: candidat-chatbot
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ A stunning, responsive frontend built with Next.js 15, TypeScript, and Tailwind
|
|||||||
## 🏗️ **Project Structure**
|
## 🏗️ **Project Structure**
|
||||||
|
|
||||||
```
|
```
|
||||||
frontend/candidat-frontend/
|
frontend/
|
||||||
├── src/
|
├── src/
|
||||||
│ ├── app/
|
│ ├── app/
|
||||||
│ │ ├── page.tsx # Main landing page
|
│ │ ├── page.tsx # Main landing page
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
|
import axios from "axios";
|
||||||
import AnimatedCounter from "@/components/AnimatedCounter";
|
import AnimatedCounter from "@/components/AnimatedCounter";
|
||||||
import FeatureCard from "@/components/FeatureCard";
|
import FeatureCard from "@/components/FeatureCard";
|
||||||
import PricingCard from "@/components/PricingCard";
|
import PricingCard from "@/components/PricingCard";
|
||||||
@@ -17,10 +18,30 @@ export default function Home() {
|
|||||||
// Check if user is already logged in
|
// Check if user is already logged in
|
||||||
const token = localStorage.getItem("token");
|
const token = localStorage.getItem("token");
|
||||||
if (token) {
|
if (token) {
|
||||||
|
// 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");
|
router.push("/dashboard");
|
||||||
} else {
|
} else {
|
||||||
setIsLoading(false);
|
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);
|
||||||
|
}
|
||||||
}, [router]);
|
}, [router]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -137,6 +137,42 @@ export default function JobManagement() {
|
|||||||
setIsAddTokensModalOpen(true);
|
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) => {
|
const handleToggleJobStatus = async (job: Job) => {
|
||||||
try {
|
try {
|
||||||
const token = localStorage.getItem("token");
|
const token = localStorage.getItem("token");
|
||||||
@@ -392,6 +428,12 @@ export default function JobManagement() {
|
|||||||
>
|
>
|
||||||
Add Tokens
|
Add Tokens
|
||||||
</button>
|
</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>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={() => handleToggleJobStatus(job)}
|
onClick={() => handleToggleJobStatus(job)}
|
||||||
|
|||||||
Submodule
+1
Submodule tuna added at bcd25503c5
Reference in New Issue
Block a user