Skip to content
Merged

Dev #106

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions backend/ai/ai.go
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,48 @@ Description:
return queryModel(query)
}

func GenerateJobNotes(job db.Job) (string, error) {
query := fmt.Sprintf(`
You are generating structured notes about a company for a job application tracker.

STRICT RULES:
- 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. 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

------------------------
USER NOTES (IMPORTANT CONTEXT)
------------------------
%s

------------------------
JOB
------------------------
Company: %s
Title: %s
Location: %s
Description:
%s
`,
job.Notes,
job.CompanyName,
job.Title,
job.LocationText,
job.Description,
)

return queryModel(query)
}

func queryModel(query string) (string, error) {
ctx := context.Background()

Expand Down
45 changes: 34 additions & 11 deletions backend/db/jobs.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down Expand Up @@ -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)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
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, $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.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)

Expand All @@ -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
Expand Down Expand Up @@ -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, &notes, &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)
Expand All @@ -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
}
Expand All @@ -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)
Expand All @@ -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)
}
Expand Down Expand Up @@ -393,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
}
80 changes: 80 additions & 0 deletions backend/handlers/jobs.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -365,3 +366,82 @@ func generatePDFBytes(content string) ([]byte, error) {

return buf.Bytes(), nil
}

// 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,
})
}

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,
})
}
3 changes: 3 additions & 0 deletions backend/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,9 @@ func main() {
r.Post("/resume", handlers.GetResumeDraft)
r.Post("/cover-letter", handlers.GetCoverLetterDraft)

r.Patch("/company-notes", handlers.UpdateCompanyNotes)
r.Post("/company-notes", handlers.GenerateCompanyNotes)

r.Get("/activities", handlers.GetJobActivities)

r.Route("/interviews", func(r chi.Router) {
Expand Down
82 changes: 43 additions & 39 deletions frontend/src/pages/job-workspace.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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 || ''
Expand All @@ -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)
}
Expand Down Expand Up @@ -593,66 +595,68 @@ 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: 'PUT',
method: 'PATCH',
headers: {
'Content-Type': 'application/json'
},
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
}
}

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
Expand Down
Loading