From c84861371dbfe9abb8d7505301b77709ff490080 Mon Sep 17 00:00:00 2001 From: Possiblyai Date: Tue, 28 Apr 2026 17:33:11 -0400 Subject: [PATCH 1/7] Updated jobs db function to include notes field --- backend/db/jobs.go | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/backend/db/jobs.go b/backend/db/jobs.go index 32e96e8..e26008e 100644 --- a/backend/db/jobs.go +++ b/backend/db/jobs.go @@ -19,6 +19,7 @@ type Job struct { Salary int `json:"salary"` Status string `json:"status"` DeadlineDate time.Time `json:"deadline_date"` + Notes string `json:"notes"` Description string `json:"description"` IsArchived bool `json:"is_archived"` CreatedAt time.Time `json:"created_at"` @@ -54,10 +55,10 @@ const ( func CreateJob(job Job) (int, error) { var id int - sql_query := `INSERT INTO jobs (user_id, company_name, title, location_text, salary, status, deadline_date, description, is_archived) + sql_query := `INSERT INTO jobs (user_id, company_name, title, location_text, salary, status, deadline_date, notes, description, is_archived) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) RETURNING id` - err := DbConn.QueryRow(context.Background(), sql_query, job.UserID, job.CompanyName, job.Title, job.LocationText, job.Salary, job.Status, job.DeadlineDate, job.Description, false).Scan(&id) + err := DbConn.QueryRow(context.Background(), sql_query, job.UserID, job.CompanyName, job.Title, job.LocationText, job.Salary, job.Status, job.DeadlineDate, job.Notes, job.Description, false).Scan(&id) if err != nil { return -1, fmt.Errorf("Failed to insert job for user_id=%d: %w", job.UserID, err) @@ -77,7 +78,7 @@ func CreateJob(job Job) (int, error) { func GetJobs(user_id int, searchQuery string) ([]Job, error) { sqlQuery := ` - SELECT id, user_id, company_name, title, location_text, salary, status, deadline_date, description, is_archived, created_at, updated_at FROM jobs WHERE user_id = $1 ` + SELECT id, user_id, company_name, title, location_text, salary, status, deadline_date, notes, description, is_archived, created_at, updated_at FROM jobs WHERE user_id = $1 ` var ( rows pgx.Rows @@ -106,11 +107,11 @@ func GetJobs(user_id int, searchQuery string) ([]Job, error) { for rows.Next() { var j Job - var locationText, description sql.NullString + var locationText, description, notes sql.NullString var deadlineDate sql.NullTime var salary sql.NullInt64 - err := rows.Scan(&j.ID, &j.UserID, &j.CompanyName, &j.Title, &locationText, &salary, &j.Status, &deadlineDate, &description, &j.IsArchived, &j.CreatedAt, &j.UpdatedAt) + err := rows.Scan(&j.ID, &j.UserID, &j.CompanyName, &j.Title, &locationText, &salary, &j.Status, &deadlineDate, ¬es, &description, &j.IsArchived, &j.CreatedAt, &j.UpdatedAt) if err != nil { return nil, fmt.Errorf("Failed to scan jobs for user_id=%d: %w", user_id, err) @@ -126,6 +127,9 @@ func GetJobs(user_id int, searchQuery string) ([]Job, error) { if deadlineDate.Valid { j.DeadlineDate = deadlineDate.Time } + if notes.Valid { + j.Notes = notes.String + } if description.Valid { j.Description = description.String } @@ -139,10 +143,10 @@ func GetJobs(user_id int, searchQuery string) ([]Job, error) { } func GetJob(job_id int, user_id int) (Job, error) { - sql_query := `SELECT id, user_id, company_name, title, location_text, salary, status, deadline_date, description, is_archived, created_at, updated_at FROM jobs WHERE id = $1 AND user_id = $2;` + sql_query := `SELECT id, user_id, company_name, title, location_text, salary, status, deadline_date, notes, description, is_archived, created_at, updated_at FROM jobs WHERE id = $1 AND user_id = $2;` var job Job - err := DbConn.QueryRow(context.Background(), sql_query, job_id, user_id).Scan(&job.ID, &job.UserID, &job.CompanyName, &job.Title, &job.LocationText, &job.Salary, &job.Status, &job.DeadlineDate, &job.Description, &job.IsArchived, &job.CreatedAt, &job.UpdatedAt) + err := DbConn.QueryRow(context.Background(), sql_query, job_id, user_id).Scan(&job.ID, &job.UserID, &job.CompanyName, &job.Title, &job.LocationText, &job.Salary, &job.Status, &job.DeadlineDate, &job.Notes, &job.Description, &job.IsArchived, &job.CreatedAt, &job.UpdatedAt) if err != nil { return Job{}, fmt.Errorf("Failed to get job job_id=%d user_id=%d: %w", job_id, user_id, err) @@ -157,9 +161,9 @@ func UpdateJob(job Job) error { } sql_query := `UPDATE jobs - SET company_name = $1, title = $2, location_text = $3, salary = $4, status = $5, deadline_date = $6, description = $7 - WHERE id = $8 AND user_id = $9` - result, err := DbConn.Exec(context.Background(), sql_query, job.CompanyName, job.Title, job.LocationText, job.Salary, job.Status, job.DeadlineDate, job.Description, job.ID, job.UserID) + SET company_name = $1, title = $2, location_text = $3, salary = $4, status = $5, deadline_date = $6, notes = $7, description = $8 + WHERE id = $9 AND user_id = $10` + result, err := DbConn.Exec(context.Background(), sql_query, job.CompanyName, job.Title, job.LocationText, job.Salary, job.Status, job.DeadlineDate, job.Notes, job.Description, job.ID, job.UserID) if err != nil { return fmt.Errorf("Failed to update job job_id=%d user_id=%d: %w", job.ID, job.UserID, err) } From 435d32150d0d8062388857493a6abdaef4fc9dc9 Mon Sep 17 00:00:00 2001 From: Possiblyai Date: Tue, 28 Apr 2026 17:39:31 -0400 Subject: [PATCH 2/7] Added handler for updating company notes --- backend/db/jobs.go | 19 +++++++++++++++ backend/handlers/jobs.go | 35 ++++++++++++++++++++++++++++ backend/main.go | 2 ++ frontend/src/pages/job-workspace.vue | 2 +- 4 files changed, 57 insertions(+), 1 deletion(-) diff --git a/backend/db/jobs.go b/backend/db/jobs.go index e26008e..9d10074 100644 --- a/backend/db/jobs.go +++ b/backend/db/jobs.go @@ -397,3 +397,22 @@ func IsJobOwner(jobID int, userID int) (bool, error) { return exists, nil } + +func UpdateJobCompanyNotes(jobID int, userID int, notes string) error { + query := ` + UPDATE jobs + SET notes = $1 + WHERE id = $2 AND user_id = $3 + ` + + res, err := DbConn.Exec(context.Background(), query, notes, jobID, userID) + if err != nil { + return err + } + + if res.RowsAffected() == 0 { + return fmt.Errorf("no rows updated") + } + + return nil +} diff --git a/backend/handlers/jobs.go b/backend/handlers/jobs.go index 742f19b..469ec69 100644 --- a/backend/handlers/jobs.go +++ b/backend/handlers/jobs.go @@ -336,3 +336,38 @@ func SaveAIDocumentToJob(w http.ResponseWriter, r *http.Request) { "id": docID, }) } + +// Handler for /api/jobs/{id}/company-notes {PATCH} +func UpdateCompanyNotes(w http.ResponseWriter, r *http.Request) { + err, token := GrabToken(r) + if err != nil { + http.Error(w, "Unauthorized", http.StatusUnauthorized) + return + } + + jobIDRaw := chi.URLParam(r, "id") + jobID, err := strconv.Atoi(jobIDRaw) + if err != nil { + http.Error(w, "Invalid job id", http.StatusBadRequest) + return + } + + var body struct { + CompanyNotes string `json:"company_notes"` + } + + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + http.Error(w, "Invalid body", http.StatusBadRequest) + return + } + + err = db.UpdateJobCompanyNotes(jobID, token.Uid, body.CompanyNotes) + if err != nil { + http.Error(w, "Failed to update notes", http.StatusInternalServerError) + return + } + + json.NewEncoder(w).Encode(map[string]any{ + "success": true, + }) +} diff --git a/backend/main.go b/backend/main.go index 7df7622..d1b469a 100644 --- a/backend/main.go +++ b/backend/main.go @@ -124,6 +124,8 @@ func main() { r.Post("/resume", handlers.GetResumeDraft) r.Post("/cover-letter", handlers.GetCoverLetterDraft) + r.Patch("/company-notes", handlers.UpdateCompanyNotes) + r.Get("/activities", handlers.GetJobActivities) r.Route("/interviews", func(r chi.Router) { diff --git a/frontend/src/pages/job-workspace.vue b/frontend/src/pages/job-workspace.vue index 0603ce6..4dbfbee 100644 --- a/frontend/src/pages/job-workspace.vue +++ b/frontend/src/pages/job-workspace.vue @@ -528,7 +528,7 @@ async function saveCompanyNotes() { error.value = '' const res = await fetch(`/api/jobs/${resolvedJobId.value}/company-notes`, { - method: 'PUT', + method: 'PATCH', headers: { 'Content-Type': 'application/json' }, From c2cf6525627d05554bd545a3b7439e3d09363a01 Mon Sep 17 00:00:00 2001 From: Possiblyai Date: Tue, 28 Apr 2026 17:46:03 -0400 Subject: [PATCH 3/7] Created ai function to genrerate company notes --- backend/ai/ai.go | 104 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 104 insertions(+) diff --git a/backend/ai/ai.go b/backend/ai/ai.go index 8dbba4f..f18a4c1 100644 --- a/backend/ai/ai.go +++ b/backend/ai/ai.go @@ -258,6 +258,110 @@ Description: return queryModel(query) } +func GenerateJobNotes(job db.Job, profile db.Profile) (string, error) { + fullName := strings.TrimSpace(profile.FirstName + " " + profile.LastName) + + experiences, _ := db.GetProfileExperiences(profile.UserID) + skills, _ := db.GetProfileSkills(profile.UserID) + + var expText strings.Builder + for _, e := range experiences { + expText.WriteString(fmt.Sprintf("- %s at %s\n", e.Title, e.Organization)) + } + + var skillText strings.Builder + for _, s := range skills { + skillText.WriteString(fmt.Sprintf("- %s\n", s.SkillName)) + } + + userQuery := strings.TrimSpace(job.Notes) + if userQuery == "" { + userQuery = "General job and company analysis" + } + + query := fmt.Sprintf(` +You are generating structured notes to help a candidate evaluate BOTH the job and the company. + +IMPORTANT: +- The user provided a custom query/focus — prioritize answering it +- Include BOTH job insights AND company insights +- If company info is limited, infer cautiously from job description and say "Not specified" when needed + +USER QUERY / FOCUS: +%s + +STRICT RULES: +- Be concise +- Use bullet points only +- Do NOT invent specific facts about the company +- You MAY infer general patterns (e.g., startup vs enterprise) but label them clearly +- Max 250 words + +OUTPUT FORMAT: + +1. Direct Answer to User Query +- (Focus specifically on the user's question) + +2. Job Insights +- (Key responsibilities, expectations, priorities) + +3. Company Insights +- (What kind of company this appears to be: size, culture, industry hints, stability) +- (Any signals from job description: fast-paced, growth stage, etc.) + +4. Fit Assessment +- (Candidate vs role + company alignment) + +5. Preparation Tips +- (What to study for THIS company + role) + +6. Potential Questions +- (Interview questions tailored to company + role) + +7. Red Flags (Job or Company) +- (Compensation clarity, vague role, unrealistic expectations, etc.) + +------------------------ +CANDIDATE +------------------------ +Name: %s +Headline: %s +Summary: %s + +Experience: +%s + +Skills: +%s + +------------------------ +JOB +------------------------ +Company: %s +Title: %s +Location: %s +Salary: %d +Status: %s +Description: +%s +`, + userQuery, + fullName, + profile.Headline, + profile.Summary, + expText.String(), + skillText.String(), + job.CompanyName, + job.Title, + job.LocationText, + job.Salary, + job.Status, + job.Description, + ) + + return queryModel(query) +} + func queryModel(query string) (string, error) { ctx := context.Background() From 3b31f4fbf35f5f638f4467d7d2fef9b5f3ecdb11 Mon Sep 17 00:00:00 2001 From: jnsnjit Date: Tue, 28 Apr 2026 18:02:28 -0400 Subject: [PATCH 4/7] works, made requested change to not refresh frontend form after submission of company notes --- frontend/src/pages/job-workspace.vue | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/frontend/src/pages/job-workspace.vue b/frontend/src/pages/job-workspace.vue index fecc3e2..7f11cd2 100644 --- a/frontend/src/pages/job-workspace.vue +++ b/frontend/src/pages/job-workspace.vue @@ -593,6 +593,9 @@ async function saveCompanyNotes() { savingCompanyNotes.value = true error.value = '' + // Preserve current textarea value locally + const notesToSave = company_notes.value + const res = await fetch(`/api/jobs/${resolvedJobId.value}/company-notes`, { method: 'PATCH', headers: { @@ -600,18 +603,21 @@ async function saveCompanyNotes() { }, credentials: 'include', body: JSON.stringify({ - company_notes: company_notes.value + company_notes: notesToSave }) }) - if (!res.ok) throw new Error() + if (!res.ok) { + throw new Error('Failed to save company notes') + } + + company_notes.value = notesToSave - await fetchJob() } catch (err) { error.value = 'Unable to save company notes.' console.error(err) } finally { - saving.value = false + savingCompanyNotes.value = false } } From eb64f4c5ab50601f84c52c5826b08a553ed6275e Mon Sep 17 00:00:00 2001 From: Possiblyai Date: Tue, 28 Apr 2026 18:11:20 -0400 Subject: [PATCH 5/7] Added ai note generation functionality for companies --- backend/ai/ai.go | 90 +++++----------------------- backend/handlers/jobs.go | 45 ++++++++++++++ backend/main.go | 1 + frontend/src/pages/job-workspace.vue | 60 +++++++++---------- 4 files changed, 88 insertions(+), 108 deletions(-) diff --git a/backend/ai/ai.go b/backend/ai/ai.go index f18a4c1..3a16a9f 100644 --- a/backend/ai/ai.go +++ b/backend/ai/ai.go @@ -258,80 +258,27 @@ Description: return queryModel(query) } -func GenerateJobNotes(job db.Job, profile db.Profile) (string, error) { - fullName := strings.TrimSpace(profile.FirstName + " " + profile.LastName) - - experiences, _ := db.GetProfileExperiences(profile.UserID) - skills, _ := db.GetProfileSkills(profile.UserID) - - var expText strings.Builder - for _, e := range experiences { - expText.WriteString(fmt.Sprintf("- %s at %s\n", e.Title, e.Organization)) - } - - var skillText strings.Builder - for _, s := range skills { - skillText.WriteString(fmt.Sprintf("- %s\n", s.SkillName)) - } - - userQuery := strings.TrimSpace(job.Notes) - if userQuery == "" { - userQuery = "General job and company analysis" - } - +func GenerateJobNotes(job db.Job) (string, error) { query := fmt.Sprintf(` -You are generating structured notes to help a candidate evaluate BOTH the job and the company. - -IMPORTANT: -- The user provided a custom query/focus — prioritize answering it -- Include BOTH job insights AND company insights -- If company info is limited, infer cautiously from job description and say "Not specified" when needed - -USER QUERY / FOCUS: -%s +You are generating structured notes about a company for a job application tracker. STRICT RULES: -- Be concise -- Use bullet points only -- Do NOT invent specific facts about the company -- You MAY infer general patterns (e.g., startup vs enterprise) but label them clearly -- Max 250 words +- Focus ONLY on the company (not the candidate) +- Use job description + user notes as hints +- Do NOT invent unknown facts +- If information is missing, infer carefully or leave general +- Keep it concise and useful for interview prep OUTPUT FORMAT: - -1. Direct Answer to User Query -- (Focus specifically on the user's question) - -2. Job Insights -- (Key responsibilities, expectations, priorities) - -3. Company Insights -- (What kind of company this appears to be: size, culture, industry hints, stability) -- (Any signals from job description: fast-paced, growth stage, etc.) - -4. Fit Assessment -- (Candidate vs role + company alignment) - -5. Preparation Tips -- (What to study for THIS company + role) - -6. Potential Questions -- (Interview questions tailored to company + role) - -7. Red Flags (Job or Company) -- (Compensation clarity, vague role, unrealistic expectations, etc.) +1. Company Overview +2. Role Context (how this role fits the company) +3. Key Insights (products, culture, mission, etc.) +4. Interview Talking Points +5. Questions to Ask ------------------------ -CANDIDATE +USER NOTES (IMPORTANT CONTEXT) ------------------------ -Name: %s -Headline: %s -Summary: %s - -Experience: -%s - -Skills: %s ------------------------ @@ -340,22 +287,13 @@ JOB Company: %s Title: %s Location: %s -Salary: %d -Status: %s Description: %s `, - userQuery, - fullName, - profile.Headline, - profile.Summary, - expText.String(), - skillText.String(), + job.Notes, job.CompanyName, job.Title, job.LocationText, - job.Salary, - job.Status, job.Description, ) diff --git a/backend/handlers/jobs.go b/backend/handlers/jobs.go index ea7ed15..b51d333 100644 --- a/backend/handlers/jobs.go +++ b/backend/handlers/jobs.go @@ -9,6 +9,7 @@ import ( "strconv" "strings" + "bananawafflecookies.com/m/v2/ai" "bananawafflecookies.com/m/v2/db" "bananawafflecookies.com/m/v2/settings" "github.com/go-chi/chi/v5" @@ -400,3 +401,47 @@ func UpdateCompanyNotes(w http.ResponseWriter, r *http.Request) { "success": true, }) } + +func GenerateCompanyNotes(w http.ResponseWriter, r *http.Request) { + err, token := GrabToken(r) + if err != nil { + http.Error(w, "Unauthorized", http.StatusUnauthorized) + return + } + + jobIDRaw := chi.URLParam(r, "id") + jobID, err := strconv.Atoi(jobIDRaw) + if err != nil { + http.Error(w, "Invalid job id", http.StatusBadRequest) + return + } + + // Verify ownership + isOwner, err := db.IsJobOwner(jobID, token.Uid) + if err != nil || !isOwner { + http.Error(w, "Unauthorized", http.StatusUnauthorized) + return + } + + // Get job + job, err := db.GetJob(jobID, token.Uid) + if err != nil { + http.Error(w, "Failed to fetch job", http.StatusInternalServerError) + return + } + + // Generate notes + notes, err := ai.GenerateJobNotes(job) + if err != nil { + http.Error(w, "Failed to generate notes", http.StatusInternalServerError) + return + } + + job.Notes = notes + db.UpdateJobCompanyNotes(job.ID, job.UserID, notes) + + json.NewEncoder(w).Encode(map[string]any{ + "success": true, + "notes": notes, + }) +} diff --git a/backend/main.go b/backend/main.go index d1b469a..1e06e2f 100644 --- a/backend/main.go +++ b/backend/main.go @@ -125,6 +125,7 @@ func main() { r.Post("/cover-letter", handlers.GetCoverLetterDraft) r.Patch("/company-notes", handlers.UpdateCompanyNotes) + r.Post("/company-notes", handlers.GenerateCompanyNotes) r.Get("/activities", handlers.GetJobActivities) diff --git a/frontend/src/pages/job-workspace.vue b/frontend/src/pages/job-workspace.vue index fecc3e2..ed5183c 100644 --- a/frontend/src/pages/job-workspace.vue +++ b/frontend/src/pages/job-workspace.vue @@ -615,44 +615,40 @@ async function saveCompanyNotes() { } } -async function enhanceCompanyNotes() { - if (!resolvedJobId.value || !company_notes.value.trim()) return +const enhanceCompanyNotes = async () => { + if (!resolvedJobId.value) return - try { - enhancingAI.value = true - error.value = '' + await saveCompanyNotes() - const res = await fetch(`/api/jobs/${resolvedJobId.value}/enhance-notes`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - credentials: 'include', - body: JSON.stringify({ - type: 'enhance_company_notes', //idk - content: company_notes.value - }) - }) + try { + enhancingAI.value = true + error.value = '' - if (!res.ok) { - throw new Error('AI enhancement failed, try again later') - } + const res = await fetch(`/api/jobs/${resolvedJobId.value}/company-notes`, { + method: 'POST', + credentials: 'include' + }) - const data = await res.json() + if (!res.ok) { + throw new Error('Failed to generate company notes') + } - // expecting something like: { enhanced_text: "..." } - if (data?.enhanced_text) { - company_notes.value = data.enhanced_text - } else { - throw new Error('Invalid AI response format') - } + const data = await res.json() - } catch (err) { - error.value = 'Unable to enhance notes right now.' - console.error(err) - } finally { - enhancingAI.value = false - } + if (data?.success && data?.notes) { + company_notes.value = data.notes + + await saveCompanyNotes() + } else { + throw new Error('Invalid response format') + } + + } catch (err) { + error.value = 'Unable to generate company notes right now.' + console.error(err) + } finally { + enhancingAI.value = false + } } // Get resume draft From 75282a12b2beeddd87a83b6914c9edd109b7e1b7 Mon Sep 17 00:00:00 2001 From: Possiblyai Date: Tue, 28 Apr 2026 18:12:28 -0400 Subject: [PATCH 6/7] Fixed insertion into jobs --- backend/db/jobs.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/db/jobs.go b/backend/db/jobs.go index 9d10074..16d05bc 100644 --- a/backend/db/jobs.go +++ b/backend/db/jobs.go @@ -56,7 +56,7 @@ const ( func CreateJob(job Job) (int, error) { var id int sql_query := `INSERT INTO jobs (user_id, company_name, title, location_text, salary, status, deadline_date, notes, description, is_archived) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) RETURNING id` err := DbConn.QueryRow(context.Background(), sql_query, job.UserID, job.CompanyName, job.Title, job.LocationText, job.Salary, job.Status, job.DeadlineDate, job.Notes, job.Description, false).Scan(&id) if err != nil { From f7d981ebed7392083612b23a70cdbe8aff666ae4 Mon Sep 17 00:00:00 2001 From: Possiblyai Date: Tue, 28 Apr 2026 18:22:22 -0400 Subject: [PATCH 7/7] Fixed company notes fetching --- frontend/src/pages/job-workspace.vue | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/frontend/src/pages/job-workspace.vue b/frontend/src/pages/job-workspace.vue index 482afb8..f5358e9 100644 --- a/frontend/src/pages/job-workspace.vue +++ b/frontend/src/pages/job-workspace.vue @@ -409,6 +409,8 @@ async function fetchJob() { const data = await res.json() + console.log(data) + form.id = data.id form.company_name = data.company_name || '' form.title = data.title || '' @@ -418,9 +420,9 @@ async function fetchJob() { form.deadline_date = toDateInput(data.deadline_date) form.status = data.status || '' form.description = data.description || '' - company_notes.value = data.company_notes || '' + company_notes.value = data.notes || '' createdAt.value = data.created_at || '' - + if (data.outcome) { Object.assign(outcome, data.outcome) }