From 031d332a1dc9f03f2029f5cf97fd6941cc929a81 Mon Sep 17 00:00:00 2001 From: Tracewebstudio Dev Date: Sun, 14 Jun 2026 18:04:47 +0200 Subject: [PATCH] feat: add cover letter generation and improve AI chat widget - Add AI cover letter generation button on JobSeekerJobsPage - Generate cover letters via /api/ai/generate-cover-letter - Show generated cover letter preview before applying - Update AI Chat Widget quick actions to focus on support & KB search - Update welcome message to explain Ask Ash capabilities --- src/app.css | 2 +- src/components/AiChatWidget.tsx | 22 ++-- .../dashboard/JobSeekerJobsPage.tsx | 120 +++++++++++++++++- 3 files changed, 130 insertions(+), 14 deletions(-) diff --git a/src/app.css b/src/app.css index 3c253e7..947df98 100644 --- a/src/app.css +++ b/src/app.css @@ -4423,7 +4423,7 @@ body { .back-top { position: fixed; right: 18px; - bottom: 18px; + bottom: 90px; z-index: 80; width: 46px; height: 46px; diff --git a/src/components/AiChatWidget.tsx b/src/components/AiChatWidget.tsx index 14e1ae8..f3ba42c 100644 --- a/src/components/AiChatWidget.tsx +++ b/src/components/AiChatWidget.tsx @@ -10,7 +10,7 @@ interface ChatMessage { } interface ChatResponse { - message: string; + reply: string; conversation_id: string; intent: string; confidence: number; @@ -22,7 +22,7 @@ export function AiChatWidget() { { role: "assistant", content: - "Hi! I'm your AI assistant. I can help you create support tickets, fill out forms, generate job descriptions, or write cover letters. What can I help you with?", + "Hi! I'm Ask Ash, your Nxtgauge assistant. I can help you:\n• Search help articles & KB\n• Create support tickets\n• Explain your AI plan & usage\n• Answer questions about the platform", }, ]); const [input, setInput] = createSignal(""); @@ -59,7 +59,7 @@ export function AiChatWidget() { const assistantMessage: ChatMessage = { role: "assistant", - content: data.message, + content: data.reply, intent: data.intent, }; setMessages((prev) => [...prev, assistantMessage]); @@ -148,7 +148,8 @@ export function AiChatWidget() { AI Assistant { e.currentTarget.style.display = 'none'; }} />

@@ -180,11 +181,16 @@ export function AiChatWidget() { "flex-wrap": "wrap", }} > - {["Create Ticket", "Job Description", "Cover Letter", "Fill Form"].map((label) => ( + {[ + { label: "Support Ticket", text: "I need help with " }, + { label: "Search KB", text: "How do I " }, + { label: "AI Plan", text: "Explain my AI plan" }, + { label: "Check Balance", text: "Check my AI balance" }, + ].map((action) => ( ))}

diff --git a/src/components/dashboard/JobSeekerJobsPage.tsx b/src/components/dashboard/JobSeekerJobsPage.tsx index 4dae4d0..c732859 100644 --- a/src/components/dashboard/JobSeekerJobsPage.tsx +++ b/src/components/dashboard/JobSeekerJobsPage.tsx @@ -3,9 +3,11 @@ * Endpoints: * GET /api/jobseeker/jobs - List available jobs from companies * POST /api/jobseeker/jobs/:id/apply - Apply for a job + * POST /api/ai/generate-cover-letter - AI cover letter generation * Custom data: saved_jobs - Bookmarked jobs stored in profile */ import { For, Show, createMemo, createSignal, onMount } from "solid-js"; +import { Sparkles, Loader } from "lucide-solid"; import { BTN_GHOST, BTN_PRIMARY, CARD, INPUT } from "~/components/DashboardShell"; import { readJobSeekerProfile, updateJobSeekerCustomData } from "~/lib/job-seeker-custom-data"; @@ -102,6 +104,10 @@ export default function JobSeekerJobsPage() { const [activeTag, setActiveTag] = createSignal(""); const [msg, setMsg] = createSignal(""); const [err, setErr] = createSignal(""); + const [generatingCover, setGeneratingCover] = createSignal(null); + const [coverLetter, setCoverLetter] = createSignal(null); + const [aiRemaining, setAiRemaining] = createSignal(5); + const [aiLimit, setAiLimit] = createSignal(5); const availableTags = createMemo(() => { const tags = new Set(); @@ -146,14 +152,16 @@ export default function JobSeekerJobsPage() { void loadSavedJobs(); }); - const applyJob = async (jobId: string) => { + const applyJob = async (jobId: string, generatedCoverLetter?: string | null) => { setBusyId(jobId); setMsg(""); setErr(""); try { const res = await apiFetch(`/api/jobseeker/jobs/${jobId}/apply`, { method: "POST", - body: JSON.stringify({}), + body: JSON.stringify({ + cover_letter: generatedCoverLetter || coverLetter() || undefined + }), }); const data = await res.json().catch(() => ({})); if (!res.ok) { @@ -161,6 +169,9 @@ export default function JobSeekerJobsPage() { return; } setMsg("Application submitted successfully."); + if (generatedCoverLetter) { + setCoverLetter(null); + } } catch { setErr("Network error while applying."); } finally { @@ -168,6 +179,48 @@ export default function JobSeekerJobsPage() { } }; + const generateCoverLetter = async (job: JobItem) => { + if (aiRemaining() <= 0) { + setErr("Daily AI generation limit reached. Upgrade to AI Pack for more."); + return; + } + setGeneratingCover(job.id); + setErr(""); + try { + const token = window.sessionStorage.getItem("nxtgauge_access_token") || ""; + const res = await fetch(`${API}/api/ai/generate-cover-letter`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify({ + job_title: job.title || "this position", + company_name: job.company_name || undefined, + job_description: job.description || undefined, + }), + }); + + if (res.status === 429) { + setErr("Daily AI generation limit reached. Upgrade to AI Pack for more."); + return; + } + + const data = await res.json(); + if (res.ok && data.cover_letter) { + setCoverLetter(data.cover_letter); + setAiRemaining(data.remaining_today ?? aiRemaining() - 1); + setAiLimit(data.daily_limit ?? aiLimit()); + } else { + setErr(data.error || "Failed to generate cover letter"); + } + } catch { + setErr("Network error during cover letter generation"); + } finally { + setGeneratingCover(null); + } + }; + const isSaved = (jobId: string) => savedJobs().some((row) => row.id === jobId); const toggleSave = async (job: JobItem) => { @@ -393,20 +446,40 @@ export default function JobSeekerJobsPage() { + + +
+

+ Cover Letter Ready +

+

{coverLetter()?.substring(0, 200)}...

+ +
+
)} + + ); }