feat: implement YAML-based configuration, .env support, and customizable prompt templates for AI analysis

This commit is contained in:
2026-08-22 10:17:38 -07:00
parent 7ec91f384f
commit 0de312edbd
30 changed files with 895 additions and 211 deletions
+69 -18
View File
@@ -43,7 +43,7 @@ const jobListHTML = `{{ define "jobs" }}
<tr>
<th class="px-4 py-3" data-sort="title">Job Title &amp; Company</th>
<th class="px-4 py-3 text-center">Link</th>
<th class="px-4 py-3" data-sort="status">Analysis</th>
<th class="px-4 py-3" data-sort="status">Status</th>
<th class="px-4 py-3" data-sort="score">Score &amp; Qualification</th>
<th class="px-4 py-3">Reason / Tags</th>
<th class="px-4 py-3" data-sort="date">Last Updated</th>
@@ -86,9 +86,13 @@ const jobListHTML = `{{ define "jobs" }}
<td class="px-4 py-3" data-sort-val="{{ if .Analysis }}{{ .Analysis.Status }}{{ else }}UNANALYZED{{ end }}">
{{ if .Analysis }}
{{ if eq .Analysis.Status "PARSED" }}
<span class="inline-flex items-center px-2 py-0.5 rounded text-[10px] font-bold bg-indigo-500/20 text-indigo-300 border border-indigo-500/30">PARSED</span>
<span class="inline-flex items-center px-2 py-0.5 rounded text-[10px] font-bold bg-indigo-500/20 text-indigo-300 border border-indigo-500/30">AI PARSED</span>
{{ else if eq .Analysis.Status "PENDING" }}
<span class="inline-flex items-center px-2 py-0.5 rounded text-[10px] font-bold bg-amber-500/20 text-amber-400 border border-amber-500/30">PENDING</span>
<span class="inline-flex items-center px-2 py-0.5 rounded text-[10px] font-bold bg-amber-500/20 text-amber-400 border border-amber-500/30">QUEUED</span>
{{ else if eq .Analysis.Status "FILTERED_OUT" }}
<span class="inline-flex items-center px-2 py-0.5 rounded text-[10px] font-bold bg-slate-700/40 text-slate-400 border border-slate-600/30">LOW FIT</span>
{{ else if eq .Analysis.Status "REJECTED" }}
<span class="inline-flex items-center px-2 py-0.5 rounded text-[10px] font-bold bg-rose-500/20 text-rose-400 border border-rose-500/30">REJECTED</span>
{{ else if eq .Analysis.Status "DONE" }}
<span class="inline-flex items-center px-2 py-0.5 rounded text-[10px] font-bold bg-emerald-500/20 text-emerald-400 border border-emerald-500/30">DONE</span>
{{ else }}
@@ -102,14 +106,32 @@ const jobListHTML = `{{ define "jobs" }}
<!-- Score & Qualification -->
<td class="px-4 py-3" data-sort-val="{{ if .Analysis }}{{ printf "%.0f" .Analysis.Score }}{{ else }}-1{{ end }}">
{{ if .Analysis }}
{{ if .Analysis.IsQualified }}
<span class="inline-flex items-center px-2.5 py-1 rounded-full text-xs font-black border {{ scoreBadgeClass .Analysis.Score }}">
★ {{ printf "%.0f" .Analysis.Score }}
</span>
{{ if eq .Analysis.Status "PARSED" }}
{{ if .Analysis.IsQualified }}
<span class="inline-flex items-center px-2.5 py-1 rounded-full text-xs font-black border {{ scoreBadgeClass .Analysis.Score }}" title="AI Match Score (DeepSeek)">
🤖 {{ printf "%.0f" .Analysis.Score }}
</span>
{{ else }}
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-[10px] font-semibold bg-rose-500/10 text-rose-400 border border-rose-500/20" title="AI Score: {{ printf "%.0f" .Analysis.Score }}">
✗ {{ printf "%.0f" .Analysis.Score }} Not Qual
</span>
{{ end }}
{{ else if eq .Analysis.Status "PENDING" }}
<span class="inline-flex items-center px-2.5 py-1 rounded-full text-xs font-semibold bg-amber-500/10 text-amber-300 border border-amber-500/30" title="Keyword Fit: {{ printf "%.0f" .Analysis.FilterScore }} - Waiting for AI">
⚡ {{ printf "%.0f" .Analysis.FilterScore }}
</span>
{{ else if eq .Analysis.Status "FILTERED_OUT" }}
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-[10px] font-medium bg-slate-800 text-slate-400 border border-slate-700" title="Keyword Fit: {{ printf "%.0f" .Analysis.FilterScore }}">
⚡ {{ printf "%.0f" .Analysis.FilterScore }}
</span>
{{ else if eq .Analysis.Status "REJECTED" }}
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-[10px] font-semibold bg-rose-500/10 text-rose-400 border border-rose-500/20">
🚫 Disqualified
</span>
{{ else }}
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-[10px] font-semibold bg-rose-500/10 text-rose-400 border border-rose-500/20">
✗ Not Qualified
</span>
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-[10px] font-semibold bg-slate-700/50 text-slate-400 border border-slate-600/30">
{{ printf "%.0f" .Analysis.Score }}
</span>
{{ end }}
{{ else }}
<span class="text-slate-500 text-[11px]">–</span>
@@ -138,6 +160,8 @@ const jobListHTML = `{{ define "jobs" }}
{{ $missing := parseJSON .Analysis.MissingHighlights }}
{{ if $missing }}
<span class="text-[10px] text-rose-400/90 leading-snug line-clamp-2" title="{{ index $missing 0 }}">{{ index $missing 0 }}</span>
{{ else if .Analysis.Summary }}
<span class="text-[10px] text-slate-400/90 leading-snug line-clamp-2" title="{{ .Analysis.Summary }}">{{ .Analysis.Summary }}</span>
{{ end }}
{{ end }}
{{ end }}
@@ -232,23 +256,50 @@ const jobDetailHTML = `{{ define "jobDetail" }}
</div>
{{ if .Analysis }}
<!-- Score + Status Grid -->
<!-- Dual Score Breakdown Grid -->
<div class="grid grid-cols-2 gap-3">
<!-- AI Match Score Card -->
<div class="p-4 rounded-xl bg-slate-800/60 border border-slate-700/60">
<span class="text-[10px] text-slate-400 uppercase tracking-wider font-semibold block">Match Score</span>
{{ if .Analysis.IsQualified }}
<span class="text-2xl font-black text-emerald-400 mt-1 block">{{ printf "%.1f" .Analysis.Score }} <span class="text-sm font-medium text-slate-400">/ 100</span></span>
<div class="flex items-center justify-between">
<span class="text-[10px] text-indigo-300 uppercase tracking-wider font-bold">🤖 AI Match Score</span>
<span class="text-[10px] font-semibold px-2 py-0.5 rounded {{ if eq .Analysis.Status "PARSED" }}bg-indigo-500/20 text-indigo-300 border border-indigo-500/30{{ else }}bg-amber-500/20 text-amber-300 border border-amber-500/30{{ end }}">
{{ .Analysis.Status }}
</span>
</div>
{{ if eq .Analysis.Status "PARSED" }}
{{ if .Analysis.IsQualified }}
<span class="text-2xl font-black text-emerald-400 mt-1.5 block">{{ printf "%.1f" .Analysis.Score }} <span class="text-xs font-normal text-slate-400">/ 100</span></span>
<span class="text-[10px] text-emerald-400/90 font-medium">✓ Qualified for interview profile</span>
{{ else }}
<span class="text-2xl font-black text-rose-400 mt-1.5 block">{{ printf "%.1f" .Analysis.Score }} <span class="text-xs font-normal text-slate-400">/ 100</span></span>
<span class="text-[10px] text-rose-400 font-medium">✗ Does not meet core fit threshold</span>
{{ end }}
{{ else }}
<span class="text-base font-bold text-rose-400 mt-1 block">Not Qualified</span>
<span class="text-lg font-bold text-amber-300 mt-1.5 block">Pending Evaluation</span>
<span class="text-[10px] text-slate-400">DeepSeek LLM analysis queued</span>
{{ end }}
</div>
<!-- Keyword Relevance Card -->
<div class="p-4 rounded-xl bg-slate-800/60 border border-slate-700/60">
<span class="text-[10px] text-slate-400 uppercase tracking-wider font-semibold block">Analysis Status</span>
<span class="text-sm font-bold text-indigo-300 mt-1 block uppercase">{{ .Analysis.Status }}</span>
<div class="flex items-center justify-between">
<span class="text-[10px] text-amber-400 uppercase tracking-wider font-bold">⚡ Keyword Fit</span>
<span class="text-[10px] text-slate-400">Pre-filter rule</span>
</div>
<span class="text-2xl font-black text-white mt-1.5 block">{{ printf "%.1f" .Analysis.FilterScore }} <span class="text-xs font-normal text-slate-400">/ 100</span></span>
<span class="text-[10px] text-slate-400">Lexical match against filters.yaml</span>
</div>
</div>
<!-- Highlights -->
<!-- AI Executive Summary -->
{{ if .Analysis.Summary }}
<div class="p-3.5 rounded-xl bg-indigo-950/40 border border-indigo-500/30">
<span class="text-[10px] font-bold text-indigo-300 uppercase tracking-wider block mb-1">💬 AI Executive Assessment</span>
<p class="text-xs text-indigo-100/90 leading-relaxed">{{ .Analysis.Summary }}</p>
</div>
{{ end }}
<!-- Highlights & Gaps -->
<div class="space-y-4">
{{ if .Analysis.IsQualified }}
<div>