Compare commits
7 Commits
6c1f6af523
...
feature/ai
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
937857a842 | ||
|
|
949423b1ae | ||
|
|
e6df75c1ff | ||
|
|
de0392bbe7 | ||
|
|
4459cbde69 | ||
|
|
26d723f239 | ||
|
|
33fcdcac3d |
@@ -23,6 +23,12 @@ class AIAnalysisController extends Controller
|
||||
|
||||
try {
|
||||
$analysis = $this->aiService->analyze($candidate);
|
||||
|
||||
// Persist the analysis on the candidate profile
|
||||
$candidate->update([
|
||||
'ai_analysis' => $analysis
|
||||
]);
|
||||
|
||||
return response()->json($analysis);
|
||||
} catch (\Exception $e) {
|
||||
return response()->json(['error' => $e->getMessage()], 500);
|
||||
|
||||
@@ -25,12 +25,14 @@ class JobPositionController extends Controller
|
||||
'title' => 'required|string|max:255',
|
||||
'description' => 'required|string',
|
||||
'requirements' => 'nullable|array',
|
||||
'ai_prompt' => 'nullable|string',
|
||||
]);
|
||||
|
||||
JobPosition::create([
|
||||
'title' => $request->title,
|
||||
'description' => $request->description,
|
||||
'requirements' => $request->requirements,
|
||||
'ai_prompt' => $request->ai_prompt,
|
||||
]);
|
||||
|
||||
return back()->with('success', 'Fiche de poste créée avec succès.');
|
||||
@@ -44,12 +46,14 @@ class JobPositionController extends Controller
|
||||
'title' => 'required|string|max:255',
|
||||
'description' => 'required|string',
|
||||
'requirements' => 'nullable|array',
|
||||
'ai_prompt' => 'nullable|string',
|
||||
]);
|
||||
|
||||
$jobPosition->update([
|
||||
'title' => $request->title,
|
||||
'description' => $request->description,
|
||||
'requirements' => $request->requirements,
|
||||
'ai_prompt' => $request->ai_prompt,
|
||||
]);
|
||||
|
||||
return back()->with('success', 'Fiche de poste mise à jour.');
|
||||
|
||||
@@ -9,11 +9,15 @@ use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
|
||||
#[Fillable(['user_id', 'job_position_id', 'phone', 'linkedin_url', 'status', 'notes', 'cv_score', 'motivation_score', 'interview_score'])]
|
||||
#[Fillable(['user_id', 'job_position_id', 'phone', 'linkedin_url', 'status', 'notes', 'cv_score', 'motivation_score', 'interview_score', 'ai_analysis'])]
|
||||
class Candidate extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $casts = [
|
||||
'ai_analysis' => 'array',
|
||||
];
|
||||
|
||||
public function jobPosition(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(JobPosition::class);
|
||||
|
||||
@@ -7,7 +7,7 @@ use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
|
||||
#[Fillable(['title', 'description', 'requirements'])]
|
||||
#[Fillable(['title', 'description', 'requirements', 'ai_prompt'])]
|
||||
class JobPosition extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
@@ -50,13 +50,28 @@ class AIAnalysisService
|
||||
|
||||
try {
|
||||
$pdf = $this->parser->parseFile(Storage::disk('local')->path($document->file_path));
|
||||
return $pdf->getText();
|
||||
$text = $pdf->getText();
|
||||
return $this->cleanText($text);
|
||||
} catch (\Exception $e) {
|
||||
Log::error("PDF Extraction Error: " . $e->getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean text to ensure it's valid UTF-8 and fits well in JSON.
|
||||
*/
|
||||
protected function cleanText(string $text): string
|
||||
{
|
||||
// Remove non-UTF8 characters
|
||||
$text = mb_convert_encoding($text, 'UTF-8', 'UTF-8');
|
||||
|
||||
// Remove control characters (except newlines and tabs)
|
||||
$text = preg_replace('/[^\x20-\x7E\xA0-\xFF\x0A\x0D\x09]/u', '', $text);
|
||||
|
||||
return trim($text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Call the AI API (using a placeholder for now, or direct Http call).
|
||||
*/
|
||||
@@ -80,6 +95,9 @@ class AIAnalysisService
|
||||
CONTENU DE LA LETTRE DE MOTIVATION:
|
||||
" . ($letterText ?? "Non fournie") . "
|
||||
|
||||
CONTEXTE ADDITIONNEL & INSTRUCTIONS PARTICULIÈRES:
|
||||
" . ($candidate->jobPosition->ai_prompt ?? "Aucune instruction spécifique.") . "
|
||||
|
||||
Fournis une analyse structurée en JSON avec les clés suivantes:
|
||||
- match_score: note de 0 à 100
|
||||
- summary: résumé de 3-4 phrases sur le profil
|
||||
@@ -92,11 +110,12 @@ class AIAnalysisService
|
||||
// For now, I'll use a mocked response or try to use a generic endpoint if configured.
|
||||
// I'll check if the user has an Ollama endpoint.
|
||||
|
||||
$ollamaUrl = config('services.ollama.url', 'http://localhost:11434/api/generate');
|
||||
$ollamaUrl = env('OLLAMA_URL', 'http://localhost:11434/api/generate');
|
||||
$ollamaModel = env('OLLAMA_MODEL', 'mistral');
|
||||
|
||||
try {
|
||||
$response = Http::timeout(60)->post($ollamaUrl, [
|
||||
'model' => 'mistral', // or llama3
|
||||
$response = Http::timeout(120)->post($ollamaUrl, [
|
||||
'model' => $ollamaModel,
|
||||
'prompt' => $prompt,
|
||||
'stream' => false,
|
||||
'format' => 'json'
|
||||
@@ -104,9 +123,11 @@ class AIAnalysisService
|
||||
|
||||
if ($response->successful()) {
|
||||
return json_decode($response->json('response'), true);
|
||||
} else {
|
||||
Log::warning("AI Provider Error: HTTP " . $response->status() . " - " . $response->body());
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
Log::error("AI Analysis Call Failed: " . $e->getMessage());
|
||||
Log::error("AI Connection Failed (Ollama): " . $e->getMessage());
|
||||
}
|
||||
|
||||
// Fallback for demo if Ollama is not running
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('candidates', function (Blueprint $table) {
|
||||
$table->json('ai_analysis')->nullable()->after('interview_score');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('candidates', function (Blueprint $table) {
|
||||
$table->dropColumn('ai_analysis');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('job_positions', function (Blueprint $table) {
|
||||
$table->text('ai_prompt')->nullable()->after('description');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('job_positions', function (Blueprint $table) {
|
||||
$table->dropColumn('ai_prompt');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -144,6 +144,7 @@ const sortedCandidates = computed(() => {
|
||||
</svg>
|
||||
</div>
|
||||
</th>
|
||||
<th class="px-6 py-4 font-semibold text-slate-700 dark:text-slate-300">Adéquation IA</th>
|
||||
<th class="px-6 py-4 font-semibold text-slate-700 dark:text-slate-300">Documents</th>
|
||||
<th class="px-6 py-4 font-semibold text-slate-700 dark:text-slate-300 text-right">Actions</th>
|
||||
</tr>
|
||||
@@ -191,6 +192,22 @@ const sortedCandidates = computed(() => {
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<div v-if="candidate.ai_analysis" class="flex items-center gap-2">
|
||||
<div
|
||||
class="px-2 py-0.5 rounded text-[10px] font-black"
|
||||
:class="[
|
||||
candidate.ai_analysis.match_score >= 80 ? 'bg-emerald-100 text-emerald-700' :
|
||||
candidate.ai_analysis.match_score >= 60 ? 'bg-amber-100 text-amber-700' :
|
||||
'bg-red-100 text-red-700'
|
||||
]"
|
||||
>
|
||||
{{ candidate.ai_analysis.match_score }}%
|
||||
</div>
|
||||
<span class="text-[10px] font-bold text-slate-400 uppercase truncate max-w-[60px]">{{ candidate.ai_analysis.verdict }}</span>
|
||||
</div>
|
||||
<span v-else class="text-[10px] text-slate-300 italic">Non analysé</span>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
|
||||
@@ -120,7 +120,7 @@ const updateAnswerScore = (answerId, score) => {
|
||||
});
|
||||
};
|
||||
|
||||
const aiAnalysis = ref(null);
|
||||
const aiAnalysis = ref(props.candidate.ai_analysis || null);
|
||||
const isAnalyzing = ref(false);
|
||||
|
||||
const runAI = async () => {
|
||||
@@ -361,85 +361,6 @@ const runAI = async () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Notes Section -->
|
||||
<div class="bg-white dark:bg-slate-800 rounded-2xl shadow-sm border border-slate-200 dark:border-slate-700 p-8">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h4 class="text-xl font-bold flex items-center gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-amber-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
|
||||
</svg>
|
||||
Notes d'entretien & Préparation
|
||||
</h4>
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="flex items-center bg-slate-100 dark:bg-slate-900 p-1 rounded-xl">
|
||||
<button
|
||||
@click="isPreview = false"
|
||||
class="px-4 py-1.5 text-[10px] font-black uppercase tracking-widest rounded-lg transition-all"
|
||||
:class="!isPreview ? 'bg-white dark:bg-slate-800 shadow-sm text-indigo-600' : 'text-slate-500'"
|
||||
>
|
||||
Édition
|
||||
</button>
|
||||
<button
|
||||
@click="isPreview = true"
|
||||
class="px-4 py-1.5 text-[10px] font-black uppercase tracking-widest rounded-lg transition-all"
|
||||
:class="isPreview ? 'bg-white dark:bg-slate-800 shadow-sm text-indigo-600' : 'text-slate-500'"
|
||||
>
|
||||
Aperçu
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="notesForm.isDirty" class="flex items-center gap-2 animate-pulse">
|
||||
<PrimaryButton @click="saveNotes" class="!px-4 !py-1 text-[10px]" :disabled="notesForm.processing">
|
||||
Enregistrer
|
||||
</PrimaryButton>
|
||||
</div>
|
||||
<div v-else class="flex items-center gap-2 text-emerald-500">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
<span class="text-[10px] font-black uppercase tracking-widest">Enregistré</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Markdown Help Guide -->
|
||||
<div v-if="!isPreview" class="mb-6 grid grid-cols-2 md:grid-cols-4 gap-2">
|
||||
<div class="p-2 border border-slate-100 dark:border-slate-700/50 rounded-lg text-[10px] text-slate-500 dark:text-slate-400">
|
||||
<code class="text-indigo-500 font-bold"># Titre</code>, <code class="text-indigo-500 font-bold">## Sous-titre</code>
|
||||
</div>
|
||||
<div class="p-2 border border-slate-100 dark:border-slate-700/50 rounded-lg text-[10px] text-slate-500 dark:text-slate-400">
|
||||
<code class="text-indigo-500 font-bold">**Gras**</code>, <code class="text-indigo-500 font-bold">*Italique*</code>
|
||||
</div>
|
||||
<div class="p-2 border border-slate-100 dark:border-slate-700/50 rounded-lg text-[10px] text-slate-500 dark:text-slate-400">
|
||||
<code class="text-indigo-500 font-bold">* Liste</code>, <code class="text-indigo-500 font-bold">1. Liste num.</code>
|
||||
</div>
|
||||
<div class="p-2 border border-slate-100 dark:border-slate-700/50 rounded-lg text-[10px] text-slate-500 dark:text-slate-400">
|
||||
<code class="text-indigo-500 font-bold">> Citation</code>, <code class="text-indigo-500 font-bold">--- (Ligne)</code>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="relative group">
|
||||
<div v-if="isPreview"
|
||||
class="prose dark:prose-invert prose-slate max-w-none w-full bg-slate-50 dark:bg-slate-900 rounded-2xl p-8 min-h-[300px] text-sm leading-relaxed"
|
||||
v-html="renderedNotes">
|
||||
</div>
|
||||
<textarea
|
||||
v-else
|
||||
v-model="notesForm.notes"
|
||||
rows="12"
|
||||
class="w-full bg-slate-50 dark:bg-slate-900 border-none rounded-2xl p-6 text-sm selection:bg-indigo-100 dark:selection:bg-indigo-900 focus:ring-2 focus:ring-indigo-500/20 transition-all placeholder:text-slate-300 dark:placeholder:text-slate-600 leading-relaxed font-mono"
|
||||
placeholder="Rédigez ici vos questions (utilisez # pour les titres, * pour les listes...)"
|
||||
></textarea>
|
||||
|
||||
<div v-if="!isPreview" class="absolute bottom-4 right-4 text-[9px] font-bold text-slate-400 opacity-60 flex items-center gap-1">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M2.166 4.999A11.954 11.954 0 0010 1.944 11.954 11.954 0 0017.834 5c.11.65.166 1.32.166 2.001 0 5.225-3.34 9.67-8 11.317C5.34 16.67 2 12.225 2 7c0-.682.057-1.35.166-2.001zm11.541 3.708a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
Markdown Supporté
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- AI Analysis Section -->
|
||||
<div class="bg-white dark:bg-slate-800 rounded-2xl shadow-sm border border-slate-200 dark:border-slate-700 p-8 overflow-hidden relative">
|
||||
<div class="flex items-center justify-between mb-8">
|
||||
@@ -540,6 +461,85 @@ const runAI = async () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Notes Section -->
|
||||
<div class="bg-white dark:bg-slate-800 rounded-2xl shadow-sm border border-slate-200 dark:border-slate-700 p-8">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h4 class="text-xl font-bold flex items-center gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-amber-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
|
||||
</svg>
|
||||
Notes d'entretien & Préparation
|
||||
</h4>
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="flex items-center bg-slate-100 dark:bg-slate-900 p-1 rounded-xl">
|
||||
<button
|
||||
@click="isPreview = false"
|
||||
class="px-4 py-1.5 text-[10px] font-black uppercase tracking-widest rounded-lg transition-all"
|
||||
:class="!isPreview ? 'bg-white dark:bg-slate-800 shadow-sm text-indigo-600' : 'text-slate-500'"
|
||||
>
|
||||
Édition
|
||||
</button>
|
||||
<button
|
||||
@click="isPreview = true"
|
||||
class="px-4 py-1.5 text-[10px] font-black uppercase tracking-widest rounded-lg transition-all"
|
||||
:class="isPreview ? 'bg-white dark:bg-slate-800 shadow-sm text-indigo-600' : 'text-slate-500'"
|
||||
>
|
||||
Aperçu
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="notesForm.isDirty" class="flex items-center gap-2 animate-pulse">
|
||||
<PrimaryButton @click="saveNotes" class="!px-4 !py-1 text-[10px]" :disabled="notesForm.processing">
|
||||
Enregistrer
|
||||
</PrimaryButton>
|
||||
</div>
|
||||
<div v-else class="flex items-center gap-2 text-emerald-500">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
<span class="text-[10px] font-black uppercase tracking-widest">Enregistré</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Markdown Help Guide -->
|
||||
<div v-if="!isPreview" class="mb-6 grid grid-cols-2 md:grid-cols-4 gap-2">
|
||||
<div class="p-2 border border-slate-100 dark:border-slate-700/50 rounded-lg text-[10px] text-slate-500 dark:text-slate-400">
|
||||
<code class="text-indigo-500 font-bold"># Titre</code>, <code class="text-indigo-500 font-bold">## Sous-titre</code>
|
||||
</div>
|
||||
<div class="p-2 border border-slate-100 dark:border-slate-700/50 rounded-lg text-[10px] text-slate-500 dark:text-slate-400">
|
||||
<code class="text-indigo-500 font-bold">**Gras**</code>, <code class="text-indigo-500 font-bold">*Italique*</code>
|
||||
</div>
|
||||
<div class="p-2 border border-slate-100 dark:border-slate-700/50 rounded-lg text-[10px] text-slate-500 dark:text-slate-400">
|
||||
<code class="text-indigo-500 font-bold">* Liste</code>, <code class="text-indigo-500 font-bold">1. Liste num.</code>
|
||||
</div>
|
||||
<div class="p-2 border border-slate-100 dark:border-slate-700/50 rounded-lg text-[10px] text-slate-500 dark:text-slate-400">
|
||||
<code class="text-indigo-500 font-bold">> Citation</code>, <code class="text-indigo-500 font-bold">--- (Ligne)</code>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="relative group">
|
||||
<div v-if="isPreview"
|
||||
class="prose dark:prose-invert prose-slate max-w-none w-full bg-slate-50 dark:bg-slate-900 rounded-2xl p-8 min-h-[300px] text-sm leading-relaxed"
|
||||
v-html="renderedNotes">
|
||||
</div>
|
||||
<textarea
|
||||
v-else
|
||||
v-model="notesForm.notes"
|
||||
rows="12"
|
||||
class="w-full bg-slate-50 dark:bg-slate-900 border-none rounded-2xl p-6 text-sm selection:bg-indigo-100 dark:selection:bg-indigo-900 focus:ring-2 focus:ring-indigo-500/20 transition-all placeholder:text-slate-300 dark:placeholder:text-slate-600 leading-relaxed font-mono"
|
||||
placeholder="Rédigez ici vos questions (utilisez # pour les titres, * pour les listes...)"
|
||||
></textarea>
|
||||
|
||||
<div v-if="!isPreview" class="absolute bottom-4 right-4 text-[9px] font-bold text-slate-400 opacity-60 flex items-center gap-1">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M2.166 4.999A11.954 11.954 0 0010 1.944 11.954 11.954 0 0017.834 5c.11.65.166 1.32.166 2.001 0 5.225-3.34 9.67-8 11.317C5.34 16.67 2 12.225 2 7c0-.682.057-1.35.166-2.001zm11.541 3.708a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
Markdown Supporté
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white dark:bg-slate-800 rounded-2xl shadow-sm border border-slate-200 dark:border-slate-700 p-8">
|
||||
<h3 class="text-xl font-bold mb-8 flex items-center justify-between">
|
||||
Historique des Tests
|
||||
|
||||
@@ -18,7 +18,8 @@ const editingPosition = ref(null);
|
||||
const form = useForm({
|
||||
title: '',
|
||||
description: '',
|
||||
requirements: []
|
||||
requirements: [],
|
||||
ai_prompt: ''
|
||||
});
|
||||
|
||||
const openModal = (position = null) => {
|
||||
@@ -27,6 +28,7 @@ const openModal = (position = null) => {
|
||||
form.title = position.title;
|
||||
form.description = position.description;
|
||||
form.requirements = position.requirements || [];
|
||||
form.ai_prompt = position.ai_prompt || '';
|
||||
} else {
|
||||
form.reset();
|
||||
}
|
||||
@@ -165,6 +167,18 @@ const removeRequirement = (index) => {
|
||||
<InputError :message="form.errors.description" />
|
||||
</div>
|
||||
|
||||
<div class="bg-indigo-50/50 dark:bg-indigo-900/10 p-6 rounded-3xl border border-indigo-100 dark:border-indigo-800/50">
|
||||
<label class="block text-xs font-black uppercase tracking-widest text-indigo-600 dark:text-indigo-400 mb-2">IA Context & Prompt Personnalisé</label>
|
||||
<p class="text-[10px] text-indigo-400 mb-4 font-bold uppercase tracking-tight">Utilisez cette zone pour donner des instructions spécifiques à l'IA (priorités, contexte entreprise, ton de l'analyse...)</p>
|
||||
<textarea
|
||||
v-model="form.ai_prompt"
|
||||
rows="5"
|
||||
class="w-full bg-white dark:bg-slate-900 border-none rounded-2xl p-4 focus:ring-2 focus:ring-indigo-500/20 transition-all text-sm leading-relaxed"
|
||||
placeholder="Ex: Sois particulièrement attentif à l'expérience sur des projets SaaS à forte charge. Favorise les candidats ayant travaillé en environnement Agile."
|
||||
></textarea>
|
||||
<InputError :message="form.errors.ai_prompt" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<label class="text-xs font-black uppercase tracking-widest text-slate-400">Compétences clés / Pré-requis</label>
|
||||
|
||||
@@ -70,6 +70,7 @@ const getStatusColor = (status) => {
|
||||
<tr class="bg-slate-50/50 dark:bg-slate-900/30">
|
||||
<th class="px-8 py-4 text-[10px] font-black uppercase tracking-[0.2em] text-slate-400">Candidat</th>
|
||||
<th class="px-8 py-4 text-[10px] font-black uppercase tracking-[0.2em] text-slate-400">Score Pondéré</th>
|
||||
<th class="px-8 py-4 text-[10px] font-black uppercase tracking-[0.2em] text-slate-400">Adéquation IA</th>
|
||||
<th class="px-8 py-4 text-[10px] font-black uppercase tracking-[0.2em] text-slate-400">Statut</th>
|
||||
<th class="px-8 py-4 text-[10px] font-black uppercase tracking-[0.2em] text-slate-400 text-right">Actions</th>
|
||||
</tr>
|
||||
@@ -85,6 +86,21 @@ const getStatusColor = (status) => {
|
||||
{{ candidate.weighted_score }} / 20
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-8 py-5">
|
||||
<div v-if="candidate.ai_analysis" class="flex items-center gap-2">
|
||||
<div
|
||||
class="px-2 py-0.5 rounded text-[10px] font-black"
|
||||
:class="[
|
||||
candidate.ai_analysis.match_score >= 80 ? 'bg-emerald-100 text-emerald-700' :
|
||||
candidate.ai_analysis.match_score >= 60 ? 'bg-amber-100 text-amber-700' :
|
||||
'bg-red-100 text-red-700'
|
||||
]"
|
||||
>
|
||||
{{ candidate.ai_analysis.match_score }}%
|
||||
</div>
|
||||
</div>
|
||||
<span v-else class="text-[10px] text-slate-300 italic font-medium">Non analysé</span>
|
||||
</td>
|
||||
<td class="px-8 py-5">
|
||||
<span
|
||||
class="px-3 py-1 text-[10px] font-black uppercase tracking-widest rounded-full"
|
||||
|
||||
@@ -41,7 +41,8 @@ Route::get('/dashboard', function () {
|
||||
'name' => $candidate->user->name,
|
||||
'email' => $candidate->user->email,
|
||||
'status' => $candidate->status,
|
||||
'weighted_score' => $candidate->weighted_score
|
||||
'weighted_score' => $candidate->weighted_score,
|
||||
'ai_analysis' => $candidate->ai_analysis
|
||||
];
|
||||
})
|
||||
->values()
|
||||
|
||||
Reference in New Issue
Block a user