package api import ( "encoding/json" "fmt" "html/template" "io" "log" "net/http" "regexp" "strconv" "strings" "gopkg.in/yaml.v3" "resume-builder/internal/db" "resume-builder/internal/llm" "resume-builder/internal/rag" ) type Handler struct { db *db.Database rag *rag.Service } func New(h *db.Database, r *rag.Service) *Handler { return &Handler{db: h, rag: r} } // JSON helpers func writeJSON(w http.ResponseWriter, status int, v interface{}) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) if err := json.NewEncoder(w).Encode(v); err != nil { log.Printf("failed to write JSON response: %v", err) } } func writeError(w http.ResponseWriter, status int, msg string) { writeJSON(w, status, map[string]string{"error": msg}) } func readJSON(r *http.Request, v interface{}) error { body, err := io.ReadAll(r.Body) if err != nil { return err } return json.Unmarshal(body, v) } func parseID(r *http.Request) (int64, bool) { if qid, err := strconv.ParseInt(r.URL.Query().Get("id"), 10, 64); err == nil && qid > 0 { return qid, true } for _, part := range strings.Split(r.URL.Path, "/") { if part == "applications" || part == "experiences" || part == "educations" || part == "skills" || part == "samples" || part == "templates" || part == "qa" || part == "active" || part == "detail" || part == "preview" || part == "render" { continue } if id, err := strconv.ParseInt(part, 10, 64); err == nil && id > 0 { return id, true } } return 0, false } // getLLMConfig extracts LLM configuration from request headers. // Returns nil if no headers are set, meaning the client's defaults will be used. func getLLMConfig(r *http.Request) *llm.Config { baseURL := r.Header.Get("X-LLM-Base") model := r.Header.Get("X-LLM-Model") apiKey := r.Header.Get("X-LLM-Key") if baseURL == "" && model == "" && apiKey == "" { return nil } return &llm.Config{ BaseURL: baseURL, Model: model, APIKey: apiKey, } } // Profile endpoints func (h *Handler) GetProfile(w http.ResponseWriter, r *http.Request) { p, err := h.db.GetOrCreateProfile() if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } writeJSON(w, http.StatusOK, p) } func (h *Handler) UpdateProfile(w http.ResponseWriter, r *http.Request) { var p db.Profile if err := readJSON(r, &p); err != nil { writeError(w, http.StatusBadRequest, "invalid JSON") return } if p.ID == 0 { existing, _ := h.db.GetOrCreateProfile() p.ID = existing.ID } if err := h.db.UpdateProfile(&p); err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } writeJSON(w, http.StatusOK, p) } // Experience endpoints func (h *Handler) GetExperiences(w http.ResponseWriter, r *http.Request) { ids, err := h.db.GetExperiences() if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } writeJSON(w, http.StatusOK, ids) } func (h *Handler) CreateExperience(w http.ResponseWriter, r *http.Request) { var e db.Experience if err := readJSON(r, &e); err != nil { writeError(w, http.StatusBadRequest, "invalid JSON") return } id, err := h.db.CreateExperience(&e) if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } e.ID = id writeJSON(w, http.StatusCreated, e) } func (h *Handler) UpdateExperience(w http.ResponseWriter, r *http.Request) { var e db.Experience if err := readJSON(r, &e); err != nil { writeError(w, http.StatusBadRequest, "invalid JSON") return } if err := h.db.UpdateExperience(&e); err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } writeJSON(w, http.StatusOK, e) } func (h *Handler) DeleteExperience(w http.ResponseWriter, r *http.Request) { id, ok := parseID(r) if !ok { writeError(w, http.StatusBadRequest, "ID required") return } if err := h.db.DeleteExperience(id); err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } writeJSON(w, http.StatusOK, map[string]bool{"deleted": true}) } // Education endpoints func (h *Handler) GetEducations(w http.ResponseWriter, r *http.Request) { eds, err := h.db.GetEducations() if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } writeJSON(w, http.StatusOK, eds) } func (h *Handler) CreateEducation(w http.ResponseWriter, r *http.Request) { var e db.Education if err := readJSON(r, &e); err != nil { writeError(w, http.StatusBadRequest, "invalid JSON") return } id, err := h.db.CreateEducation(&e) if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } e.ID = id writeJSON(w, http.StatusCreated, e) } func (h *Handler) UpdateEducation(w http.ResponseWriter, r *http.Request) { var e db.Education if err := readJSON(r, &e); err != nil { writeError(w, http.StatusBadRequest, "invalid JSON") return } if err := h.db.UpdateEducation(&e); err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } writeJSON(w, http.StatusOK, e) } func (h *Handler) DeleteEducation(w http.ResponseWriter, r *http.Request) { id, ok := parseID(r) if !ok { writeError(w, http.StatusBadRequest, "ID required") return } if err := h.db.DeleteEducation(id); err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } writeJSON(w, http.StatusOK, map[string]bool{"deleted": true}) } // Skill endpoints func (h *Handler) GetSkills(w http.ResponseWriter, r *http.Request) { skills, err := h.db.GetSkills() if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } writeJSON(w, http.StatusOK, skills) } func (h *Handler) CreateSkill(w http.ResponseWriter, r *http.Request) { var s db.Skill if err := readJSON(r, &s); err != nil { writeError(w, http.StatusBadRequest, "invalid JSON") return } id, err := h.db.CreateSkill(&s) if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } s.ID = id writeJSON(w, http.StatusCreated, s) } func (h *Handler) UpdateSkill(w http.ResponseWriter, r *http.Request) { var s db.Skill if err := readJSON(r, &s); err != nil { writeError(w, http.StatusBadRequest, "invalid JSON") return } if err := h.db.UpdateSkill(&s); err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } writeJSON(w, http.StatusOK, s) } func (h *Handler) DeleteSkill(w http.ResponseWriter, r *http.Request) { id, ok := parseID(r) if !ok { writeError(w, http.StatusBadRequest, "ID required") return } if err := h.db.DeleteSkill(id); err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } writeJSON(w, http.StatusOK, map[string]bool{"deleted": true}) } // Work sample endpoints func (h *Handler) GetWorkSamples(w http.ResponseWriter, r *http.Request) { samples, err := h.db.GetWorkSamples() if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } writeJSON(w, http.StatusOK, samples) } func (h *Handler) CreateWorkSample(w http.ResponseWriter, r *http.Request) { var wdb db.WorkSample if err := readJSON(r, &wdb); err != nil { writeError(w, http.StatusBadRequest, "invalid JSON") return } id, err := h.db.CreateWorkSample(&wdb) if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } wdb.ID = id writeJSON(w, http.StatusCreated, wdb) } func (h *Handler) UpdateWorkSample(w http.ResponseWriter, r *http.Request) { var wdb db.WorkSample if err := readJSON(r, &wdb); err != nil { writeError(w, http.StatusBadRequest, "invalid JSON") return } if err := h.db.UpdateWorkSample(&wdb); err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } writeJSON(w, http.StatusOK, wdb) } func (h *Handler) DeleteWorkSample(w http.ResponseWriter, r *http.Request) { id, ok := parseID(r) if !ok { writeError(w, http.StatusBadRequest, "ID required") return } if err := h.db.DeleteWorkSample(id); err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } writeJSON(w, http.StatusOK, map[string]bool{"deleted": true}) } // Template endpoints func (h *Handler) GetTemplates(w http.ResponseWriter, r *http.Request) { templates, err := h.db.GetCsTemplates() if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } writeJSON(w, http.StatusOK, templates) } func (h *Handler) CreateTemplate(w http.ResponseWriter, r *http.Request) { var t db.CsTemplate if err := readJSON(r, &t); err != nil { writeError(w, http.StatusBadRequest, "invalid JSON") return } id, err := h.db.CreateCsTemplate(&t) if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } t.ID = id writeJSON(w, http.StatusCreated, t) } func (h *Handler) UpdateTemplate(w http.ResponseWriter, r *http.Request) { var t db.CsTemplate if err := readJSON(r, &t); err != nil { writeError(w, http.StatusBadRequest, "invalid JSON") return } if err := h.db.UpdateCsTemplate(&t); err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } writeJSON(w, http.StatusOK, t) } func (h *Handler) DeleteTemplate(w http.ResponseWriter, r *http.Request) { id, ok := parseID(r) if !ok { writeError(w, http.StatusBadRequest, "ID required") return } if err := h.db.DeleteCsTemplate(id); err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } writeJSON(w, http.StatusOK, map[string]bool{"deleted": true}) } func (h *Handler) SetActiveTemplate(w http.ResponseWriter, r *http.Request) { id, ok := parseID(r) if !ok { writeError(w, http.StatusBadRequest, "ID required") return } if err := h.db.SetActiveCsTemplate(id); err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } writeJSON(w, http.StatusOK, map[string]bool{"success": true}) } // Mock resume YAML for template preview func mockResumeYAML() *db.ResumeYAML { return &db.ResumeYAML{ Name: "Jane Smith", Email: "jane@example.com", Phone: "555-1234", Location: "San Francisco, CA", Website: "linkedin.com/jane", Summary: "Motivated software engineer with experience in building scalable web applications and microservices.", Experience: []db.ExpEntry{ { Company: "Acme Technologies", Title: "Senior Software Engineer", StartDate: "Mar 2021", EndDate: "Present", Location: "San Francisco, CA", Bullets: []string{ "Led development of a microservices platform serving 2M+ daily active users", "Reduced deployment time by 80% by implementing CI/CD pipelines with GitHub Actions", "Mentored 4 junior developers and conducted code reviews across the engineering team", }, }, { Company: "StartupXYZ", Title: "Software Engineer", StartDate: "Jun 2018", EndDate: "Feb 2021", Location: "San Francisco, CA", Bullets: []string{ "Built real-time data processing pipeline handling 500K events/day", "Designed RESTful APIs consumed by web and mobile applications", }, }, }, Education: []db.EduEntry{ { Institution: "University of Technology", Degree: "BS", FieldOfStudy: "Computer Science", StartDate: "2014", EndDate: "2018", Location: "San Jose, CA", }, }, Skills: []db.SkillGroup{ { Category: "Languages", Items: []string{"Go", "Python", "TypeScript", "Java"}, }, { Category: "Infrastructure", Items: []string{"Docker", "Kubernetes", "AWS", "Terraform"}, }, { Category: "Databases", Items: []string{"PostgreSQL", "Redis", "MongoDB"}, }, }, WorkSamples: []db.WorkSampleEntry{ { Title: "Open Source CLI Tool", Description: "Built a Go CLI tool for automating CI/CD pipeline configuration with 500+ GitHub stars", URL: "https://github.com/jane/cli-tool", }, { Title: "Real-time Analytics Dashboard", Description: "Designed and implemented a real-time analytics dashboard processing 10K+ events/sec", URL: "https://analytics-demo.example.com", }, }, } } // Template preview: render HTML template with mock YAML data in iframe type PreviewRequest struct { HTMLTemplate string `json:"html_template"` } func (h *Handler) PreviewTemplate(w http.ResponseWriter, r *http.Request) { var req PreviewRequest if err := readJSON(r, &req); err != nil { writeError(w, http.StatusBadRequest, "invalid JSON") return } if req.HTMLTemplate == "" { req.HTMLTemplate = "

Empty template

" } mockData := mockResumeYAML() html, err := renderTemplate(req.HTMLTemplate, mockData) if err != nil { writeJSON(w, http.StatusOK, map[string]interface{}{ "html": "", "error": err.Error(), }) return } writeJSON(w, http.StatusOK, map[string]string{"html": html}) } // RenderResume: combine YAML data with template to produce HTML type RenderRequest struct { YAMLContent string `json:"yaml"` TemplateID int64 `json:"template_id"` } func (h *Handler) RenderResume(w http.ResponseWriter, r *http.Request) { var req RenderRequest if err := readJSON(r, &req); err != nil { writeError(w, http.StatusBadRequest, "invalid JSON") return } // Parse YAML var resumeData db.ResumeYAML if err := yaml.Unmarshal([]byte(req.YAMLContent), &resumeData); err != nil { writeError(w, http.StatusBadRequest, "invalid YAML: "+err.Error()) return } // Get template var tpl *db.CsTemplate if req.TemplateID > 0 { templates, err := h.db.GetCsTemplates() if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } for _, t := range templates { if t.ID == req.TemplateID { tmpl := t tpl = &tmpl break } } if tpl == nil { writeError(w, http.StatusNotFound, "template not found") return } } else { active, err := h.db.GetActiveCsTemplate() if err != nil { writeError(w, http.StatusInternalServerError, "no active template") return } tpl = active } html, err := renderTemplate(tpl.HTMLTemplate, &resumeData) if err != nil { writeError(w, http.StatusBadRequest, "template render error: "+err.Error()) return } writeJSON(w, http.StatusOK, map[string]string{"html": html}) } // renderTemplate executes a Go html/template with the resume YAML data func renderTemplate(tplStr string, data *db.ResumeYAML) (string, error) { tpl, err := template.New("resume").Funcs(template.FuncMap{ "join": func(sep string, s []string) string { return strings.Join(s, sep) }, "hasPrefix": strings.HasPrefix, "safeURL": func(s string) template.URL { return template.URL(s) }, }).Parse(tplStr) if err != nil { return "", fmt.Errorf("template parse error: %w", err) } var buf strings.Builder if err := tpl.Execute(&buf, data); err != nil { return "", fmt.Errorf("template execution error: %w", err) } return buf.String(), nil } // Template variable documentation func (h *Handler) GetPlaceholders(w http.ResponseWriter, r *http.Request) { placeholders := map[string]string{ "{{.Name}}": "Full name", "{{.Email}}": "Email address", "{{.Phone}}": "Phone number", "{{.Location}}": "City, State", "{{.Website}}": "Website / LinkedIn URL", "{{.Summary}}": "Professional summary", "{{range .Experience}}...{{end}}": "Loop over experience entries", "{{range .Education}}...{{end}}": "Loop over education entries", "{{range .Skills}}...{{end}}": "Loop over skill groups", "{{range .WorkSamples}}...{{end}}": "Loop over work samples / projects", } writeJSON(w, http.StatusOK, placeholders) } // Application endpoints func (h *Handler) GetApplications(w http.ResponseWriter, r *http.Request) { apps, err := h.db.GetApplications() if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } writeJSON(w, http.StatusOK, apps) } func (h *Handler) GetApplication(w http.ResponseWriter, r *http.Request) { id, ok := parseID(r) if !ok { writeError(w, http.StatusBadRequest, "ID required") return } app, err := h.db.GetApplication(id) if err != nil { writeError(w, http.StatusNotFound, "application not found") return } writeJSON(w, http.StatusOK, app) } func (h *Handler) CreateApplication(w http.ResponseWriter, r *http.Request) { var app db.Application if err := readJSON(r, &app); err != nil { writeError(w, http.StatusBadRequest, "invalid JSON") return } id, err := h.db.CreateApplication(&app) if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } app.ID = id writeJSON(w, http.StatusCreated, app) } func (h *Handler) UpdateApplication(w http.ResponseWriter, r *http.Request) { var app db.Application if err := readJSON(r, &app); err != nil { writeError(w, http.StatusBadRequest, "invalid JSON") return } if err := h.db.UpdateApplication(&app); err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } writeJSON(w, http.StatusOK, app) } func (h *Handler) DeleteApplication(w http.ResponseWriter, r *http.Request) { id, ok := parseID(r) if !ok { writeError(w, http.StatusBadRequest, "ID required") return } if err := h.db.DeleteApplication(id); err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } writeJSON(w, http.StatusOK, map[string]bool{"deleted": true}) } // Application detail (includes Q&As) func (h *Handler) GetApplicationDetail(w http.ResponseWriter, r *http.Request) { id, ok := parseID(r) if !ok { writeError(w, http.StatusBadRequest, "ID required") return } app, err := h.db.GetApplication(id) if err != nil { writeError(w, http.StatusNotFound, "application not found") return } qas, errQas := h.db.GetQuestionAnswers(id) if errQas != nil { log.Printf("Failed to get question answers: %v", errQas) } writeJSON(w, http.StatusOK, map[string]interface{}{ "application": app, "questions": qas, }) } // Q&A endpoints func (h *Handler) CreateQA(w http.ResponseWriter, r *http.Request) { var qa db.QuestionAnswer if err := readJSON(r, &qa); err != nil { writeError(w, http.StatusBadRequest, "invalid JSON") return } if qa.ApplicationID == 0 { writeError(w, http.StatusBadRequest, "application_id required") return } id, err := h.db.CreateQuestionAnswer(&qa) if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } qa.ID = id writeJSON(w, http.StatusCreated, qa) } func (h *Handler) UpdateQA(w http.ResponseWriter, r *http.Request) { var qa db.QuestionAnswer if err := readJSON(r, &qa); err != nil { writeError(w, http.StatusBadRequest, "invalid JSON") return } if err := h.db.UpdateQuestionAnswer(&qa); err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } writeJSON(w, http.StatusOK, qa) } func (h *Handler) DeleteQA(w http.ResponseWriter, r *http.Request) { id, ok := parseID(r) if !ok { writeError(w, http.StatusBadRequest, "ID required") return } if err := h.db.DeleteQuestionAnswer(id); err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } writeJSON(w, http.StatusOK, map[string]bool{"deleted": true}) } type SaveResumeRequest struct { ApplicationID int64 `json:"application_id"` YAMLContent string `json:"yaml_content"` } // Saved resume endpoints func (h *Handler) GetSavedResume(w http.ResponseWriter, r *http.Request) { id, ok := parseID(r) if !ok { writeError(w, http.StatusBadRequest, "ID required") return } sr, err := h.db.GetSavedResume(id) if err != nil { writeJSON(w, http.StatusOK, map[string]interface{}{ "yaml_content": "", }) return } writeJSON(w, http.StatusOK, sr) } func (h *Handler) SaveResume(w http.ResponseWriter, r *http.Request) { var req SaveResumeRequest if err := readJSON(r, &req); err != nil { writeError(w, http.StatusBadRequest, "invalid JSON") return } if req.ApplicationID == 0 || req.YAMLContent == "" { writeError(w, http.StatusBadRequest, "application_id and yaml_content required") return } sr := &db.SavedResume{ ApplicationID: req.ApplicationID, YAMLContent: req.YAMLContent, } if err := h.db.SaveResume(sr); err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } writeJSON(w, http.StatusOK, sr) } func (h *Handler) UpdateSavedResume(w http.ResponseWriter, r *http.Request) { var req SaveResumeRequest if err := readJSON(r, &req); err != nil { writeError(w, http.StatusBadRequest, "invalid JSON") return } sr := &db.SavedResume{ ID: req.ApplicationID, ApplicationID: req.ApplicationID, YAMLContent: req.YAMLContent, } if err := h.db.UpdateSavedResume(sr); err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } writeJSON(w, http.StatusOK, sr) } func (h *Handler) DeleteSavedResume(w http.ResponseWriter, r *http.Request) { id, ok := parseID(r) if !ok { writeError(w, http.StatusBadRequest, "ID required") return } if err := h.db.DeleteSavedResume(id); err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } writeJSON(w, http.StatusOK, map[string]bool{"deleted": true}) } // Resume data endpoint func (h *Handler) GetResumeData(w http.ResponseWriter, r *http.Request) { data, err := h.db.GetResumeData() if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } writeJSON(w, http.StatusOK, data) } // Export endpoint func (h *Handler) ExportData(w http.ResponseWriter, r *http.Request) { data, err := h.db.GetResumeData() if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } w.Header().Set("Content-Disposition", "attachment; filename=resume-data.json") writeJSON(w, http.StatusOK, data) } // Import endpoint func (h *Handler) ImportData(w http.ResponseWriter, r *http.Request) { var data db.ResumeData if err := readJSON(r, &data); err != nil { writeError(w, http.StatusBadRequest, "invalid JSON") return } tx, err := h.db.DB().Begin() if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } defer tx.Rollback() if _, err := tx.Exec(`DELETE FROM profiles`); err != nil { writeError(w, http.StatusInternalServerError, "failed to clear profiles: "+err.Error()) return } if _, err := tx.Exec(`INSERT INTO profiles (first_name, last_name, email, phone, location, website, summary) VALUES (?,?,?,?,?,?,?)`, data.Profile.FirstName, data.Profile.LastName, data.Profile.Email, data.Profile.Phone, data.Profile.Location, data.Profile.Website, data.Profile.Summary); err != nil { writeError(w, http.StatusInternalServerError, "failed to insert profile: "+err.Error()) return } if _, err := tx.Exec(`DELETE FROM experiences`); err != nil { writeError(w, http.StatusInternalServerError, "failed to clear experiences: "+err.Error()) return } for _, e := range data.Experiences { if _, err := tx.Exec(`INSERT INTO experiences (company, title, start_date, end_date, location, description, sort_order) VALUES (?,?,?,?,?,?,?)`, e.Company, e.Title, e.StartDate, e.EndDate, e.Location, e.Description, e.SortOrder); err != nil { writeError(w, http.StatusInternalServerError, "failed to insert experience: "+err.Error()) return } } if _, err := tx.Exec(`DELETE FROM educations`); err != nil { writeError(w, http.StatusInternalServerError, "failed to clear educations: "+err.Error()) return } for _, e := range data.Educations { if _, err := tx.Exec(`INSERT INTO educations (institution, degree, field_of_study, start_date, end_date, location, gpa, description, sort_order) VALUES (?,?,?,?,?,?,?,?,?)`, e.Institution, e.Degree, e.FieldOfStudy, e.StartDate, e.EndDate, e.Location, e.GPA, e.Description, e.SortOrder); err != nil { writeError(w, http.StatusInternalServerError, "failed to insert education: "+err.Error()) return } } if _, err := tx.Exec(`DELETE FROM skills`); err != nil { writeError(w, http.StatusInternalServerError, "failed to clear skills: "+err.Error()) return } for _, s := range data.Skills { if _, err := tx.Exec(`INSERT INTO skills (category, name, level, description, sort_order) VALUES (?,?,?,?,?)`, s.Category, s.Name, s.Level, s.Description, s.SortOrder); err != nil { writeError(w, http.StatusInternalServerError, "failed to insert skill: "+err.Error()) return } } if _, err := tx.Exec(`DELETE FROM work_samples`); err != nil { writeError(w, http.StatusInternalServerError, "failed to clear work samples: "+err.Error()) return } for _, ws := range data.WorkSamples { if _, err := tx.Exec(`INSERT INTO work_samples (title, description, url, content) VALUES (?,?,?,?)`, ws.Title, ws.Description, ws.URL, ws.Content); err != nil { writeError(w, http.StatusInternalServerError, "failed to insert work sample: "+err.Error()) return } } if err := tx.Commit(); err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } writeJSON(w, http.StatusOK, map[string]string{"message": "data imported successfully"}) } // Generate endpoints type GenerateRequest struct { JobPosting string `json:"job_posting"` IncludeSections string `json:"include_sections"` Company string `json:"company"` Position string `json:"position"` Question string `json:"question"` ApplicationID int64 `json:"application_id"` } func (h *Handler) GenerateResume(w http.ResponseWriter, r *http.Request) { var req GenerateRequest if err := readJSON(r, &req); err != nil { writeError(w, http.StatusBadRequest, "invalid JSON") return } if req.ApplicationID > 0 && req.JobPosting == "" { if app, err := h.db.GetApplication(req.ApplicationID); err != nil { log.Printf("Failed to get application %d: %v", req.ApplicationID, err) } else if app != nil { req.JobPosting = app.JobPosting } } cfg := getLLMConfig(r) result, err := h.rag.GenerateResume(req.JobPosting, req.IncludeSections, cfg) if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } result = h.mergeProfileIntoYAML(cleanMarkdownFences(result)) writeJSON(w, http.StatusOK, map[string]string{"yaml": result}) } func (h *Handler) GenerateCoverLetter(w http.ResponseWriter, r *http.Request) { var req GenerateRequest if err := readJSON(r, &req); err != nil { writeError(w, http.StatusBadRequest, "invalid JSON") return } if req.ApplicationID > 0 && req.JobPosting == "" { if app, err := h.db.GetApplication(req.ApplicationID); err != nil { log.Printf("Failed to get application %d: %v", req.ApplicationID, err) } else if app != nil { req.JobPosting = app.JobPosting } } cfg := getLLMConfig(r) result, err := h.rag.GenerateCoverLetter(req.Company, req.Position, req.JobPosting, cfg) if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } writeJSON(w, http.StatusOK, map[string]string{"text": result}) } func (h *Handler) GenerateAnswer(w http.ResponseWriter, r *http.Request) { var req GenerateRequest if err := readJSON(r, &req); err != nil { writeError(w, http.StatusBadRequest, "invalid JSON") return } jobPosting := req.JobPosting if req.ApplicationID > 0 && jobPosting == "" { if app, err := h.db.GetApplication(req.ApplicationID); err != nil { log.Printf("Failed to get application %d: %v", req.ApplicationID, err) } else if app != nil { jobPosting = app.JobPosting } } cfg := getLLMConfig(r) result, err := h.rag.GenerateAnswer(req.Question, jobPosting, cfg) if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } writeJSON(w, http.StatusOK, map[string]string{"answer": result}) } // SSE streaming endpoints func (h *Handler) StreamResume(w http.ResponseWriter, r *http.Request) { var req GenerateRequest if err := readJSON(r, &req); err != nil { writeError(w, http.StatusBadRequest, "invalid JSON") return } if req.ApplicationID > 0 && req.JobPosting == "" { if app, err := h.db.GetApplication(req.ApplicationID); err != nil { log.Printf("Failed to get application %d: %v", req.ApplicationID, err) } else if app != nil { req.JobPosting = app.JobPosting } } flusher, ok := w.(http.Flusher) if !ok { writeError(w, http.StatusInternalServerError, "streaming not supported") return } w.Header().Set("Content-Type", "text/event-stream") w.Header().Set("Cache-Control", "no-cache") w.Header().Set("Connection", "keep-alive") w.Header().Set("X-Accel-Buffering", "no") var buf strings.Builder done := make(chan struct{}) go func() { <-r.Context().Done() close(done) }() cfg := getLLMConfig(r) streamErr := h.rag.StreamResume(req.JobPosting, req.IncludeSections, func(chunk string) { select { case <-done: return default: } buf.WriteString(chunk) fmt.Fprintf(w, "data: %s\n\n", chunk) flusher.Flush() }, cfg) if streamErr != nil { log.Printf("Stream error: %v", streamErr) fmt.Fprintf(w, "event: error\ndata: %s\n\n", streamErr.Error()) flusher.Flush() return } cleaned := h.mergeProfileIntoYAML(cleanMarkdownFences(buf.String())) fmt.Fprintf(w, "event: done\ndata: %s\n\n", cleaned) flusher.Flush() } func (h *Handler) StreamCoverLetter(w http.ResponseWriter, r *http.Request) { var req GenerateRequest if err := readJSON(r, &req); err != nil { writeError(w, http.StatusBadRequest, "invalid JSON") return } if req.ApplicationID > 0 && req.JobPosting == "" { if app, err := h.db.GetApplication(req.ApplicationID); err != nil { log.Printf("Failed to get application %d: %v", req.ApplicationID, err) } else if app != nil { req.JobPosting = app.JobPosting } } flusher, ok := w.(http.Flusher) if !ok { writeError(w, http.StatusInternalServerError, "streaming not supported") return } w.Header().Set("Content-Type", "text/event-stream") w.Header().Set("Cache-Control", "no-cache") w.Header().Set("Connection", "keep-alive") w.Header().Set("X-Accel-Buffering", "no") var buf strings.Builder done := make(chan struct{}) go func() { <-r.Context().Done() close(done) }() cfg := getLLMConfig(r) streamErr := h.rag.StreamCoverLetter(req.Company, req.Position, req.JobPosting, func(chunk string) { select { case <-done: return default: } buf.WriteString(chunk) fmt.Fprintf(w, "data: %s\n\n", chunk) flusher.Flush() }, cfg) if streamErr != nil { log.Printf("Stream error: %v", streamErr) fmt.Fprintf(w, "event: error\ndata: %s\n\n", streamErr.Error()) flusher.Flush() return } fmt.Fprintf(w, "event: done\ndata: %s\n\n", buf.String()) flusher.Flush() } func (h *Handler) StreamAnswer(w http.ResponseWriter, r *http.Request) { var req GenerateRequest if err := readJSON(r, &req); err != nil { writeError(w, http.StatusBadRequest, "invalid JSON") return } jobPosting := req.JobPosting if req.ApplicationID > 0 && jobPosting == "" { if app, err := h.db.GetApplication(req.ApplicationID); err != nil { log.Printf("Failed to get application %d: %v", req.ApplicationID, err) } else if app != nil { jobPosting = app.JobPosting } } flusher, ok := w.(http.Flusher) if !ok { writeError(w, http.StatusInternalServerError, "streaming not supported") return } w.Header().Set("Content-Type", "text/event-stream") w.Header().Set("Cache-Control", "no-cache") w.Header().Set("Connection", "keep-alive") w.Header().Set("X-Accel-Buffering", "no") var buf strings.Builder done := make(chan struct{}) go func() { <-r.Context().Done() close(done) }() cfg := getLLMConfig(r) streamErr := h.rag.StreamAnswer(req.Question, jobPosting, func(chunk string) { select { case <-done: return default: } buf.WriteString(chunk) fmt.Fprintf(w, "data: %s\n\n", chunk) flusher.Flush() }, cfg) if streamErr != nil { log.Printf("Stream error: %v", streamErr) fmt.Fprintf(w, "event: error\ndata: %s\n\n", streamErr.Error()) flusher.Flush() return } fmt.Fprintf(w, "event: done\ndata: %s\n\n", buf.String()) flusher.Flush() } func cleanMarkdownFences(s string) string { s = strings.TrimSpace(s) // Match ```lang ... ``` or ``` ... ``` (possibly with text before/after) fenceRe := regexp.MustCompile("(?s)^.*?```[a-zA-Z]*\\n?(.+?)```\\s*$") matches := fenceRe.FindStringSubmatch(s) if len(matches) >= 2 { return strings.TrimSpace(matches[1]) } return s } // mergeProfileIntoYAML injects the user's profile info into the LLM-generated YAML. func (h *Handler) mergeProfileIntoYAML(yamlContent string) string { profile, err := h.db.GetOrCreateProfile() if err != nil { return yamlContent } // Build the profile header lines var header strings.Builder name := profile.FirstName + " " + profile.LastName name = strings.TrimSpace(name) if name != "" { header.WriteString(fmt.Sprintf("name: %q\n", name)) } if profile.Email != "" { header.WriteString(fmt.Sprintf("email: %q\n", profile.Email)) } if profile.Phone != "" { header.WriteString(fmt.Sprintf("phone: %q\n", profile.Phone)) } if profile.Location != "" { header.WriteString(fmt.Sprintf("location: %q\n", profile.Location)) } if profile.Website != "" { header.WriteString(fmt.Sprintf("website: %q\n", profile.Website)) } if header.Len() > 0 { return header.String() + "\n" + strings.TrimSpace(yamlContent) } return yamlContent } // TestLLMConnection tests the LLM connection using the provided config from headers. func (h *Handler) TestLLMConnection(w http.ResponseWriter, r *http.Request) { cfg := getLLMConfig(r) if cfg == nil { writeError(w, http.StatusBadRequest, "LLM configuration required (base URL and model)") return } if cfg.BaseURL == "" { writeError(w, http.StatusBadRequest, "base URL required") return } if cfg.Model == "" { writeError(w, http.StatusBadRequest, "model name required") return } client := llm.New(cfg.BaseURL, cfg.APIKey, cfg.Model) resp, err := client.Chat([]llm.ChatMessage{ {Role: "user", Content: "Say OK"}, }, 16, 0, cfg) if err != nil { writeError(w, http.StatusBadGateway, "Connection failed: "+err.Error()) return } if len(resp.Choices) == 0 { writeError(w, http.StatusBadGateway, "Empty response from LLM") return } writeJSON(w, http.StatusOK, map[string]interface{}{ "ok": true, "model": resp.Model, "response": strings.TrimSpace(resp.Choices[0].Message.Content), }) }